STM32 TCPIP + Qt GUI

kenGwon·2024년 1월 29일

[STM32] Firmware/RTOS

목록 보기
9/11

텍스트### payload(페이로드)
사용자가 보낸 원래의 데이터
페이로드(영어: payload)는 전송되는 '순수한 데이터'를 뜻한다. 페이로드는 전송의 근본적인 목적이 되는 데이터의 일부분으로 그 데이터와 함께 전송되는 헤더, 메타데이터와 같은 부분은 제외

main.c

//-------- UDP start ------------
extern struct udp_pcb *upcb1;    // Add	UDP Control Block
extern ip_addr_t addr1;   // Add
extern uint8_t udp_data[100];    // Add
extern uint8_t udp_flag;         // Add
extern uint8_t led_flag[4];		//Add

struct pbuf *p1;    // Add  Send buffer
char temp_str[40] = "";
uint8_t servo[5];
uint8_t led[4];
//--------- UDP end ------------

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART3_UART_Init();
  MX_USB_OTG_FS_PCD_Init();
  MX_USART6_UART_Init();
  MX_TIM10_Init();
  MX_TIM11_Init();
  MX_TIM3_Init();
  MX_TIM4_Init();
  MX_TIM2_Init();
  MX_RTC_Init();
  MX_TIM5_Init();
  MX_I2C1_Init();
  MX_ADC1_Init();
  MX_LWIP_Init();
  /* USER CODE BEGIN 2 */
  HAL_UART_Receive_IT(&huart3, &rx_data, 1);   // assing to RX INT
  HAL_UART_Receive_IT(&huart6, &bt_rx_data, 1);   // for BT assing to RX INT
  HAL_TIM_Base_Start_IT(&htim10);   // ADD_SIKWON_1011
  HAL_TIM_Base_Start_IT(&htim11);   // ADD_SIKWON_1011
//  HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);   // for count pulse(rising edge & falling edge)
//  HAL_TIM_PWM_Start_IT(&htim4, TIM_CHANNEL_1);  // for DC motor PWM control
//  HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);   // for SERVO motor PWM control
//  HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_4);   // PIEZO Buzzer
  HAL_ADC_Start_IT(&hadc1);

//  DHT11_Init();
//  i2c_lcd_init();

  TIM10_10ms_counter=0;

// stepmotor_main_test();

//  dotmatrix_main();

//  dotmatrix_main_test();
//  led_main();
//  DHT11_main();
//  i2c_lcd_main();
//  servo_motor_test_main();
//    buzzer_main();
//------ TCP/IP start -------------
    udp_echoserver_init(); // Add TCP
	upcb1 = udp_new();                //Add
	IP4_ADDR(&addr1,10,10,15,70);    //Add
	udp_bind(upcb1,IP_ADDR_ANY,9999); //Add
//------- TCP/IP end -----------------
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  
  /*
  * 덩어리 1
  */
	ethernetif_input(&gnetif);
	sys_check_timeouts();

	if (udp_flag == 1)      // Add
	{
		udp_flag = 0;
		strncpy(servo, udp_data + 6, 4);
		HAL_UART_Transmit(&huart3, servo, strlen(servo), 1000);
		//__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, atoi(servo));
	}
	if(led_flag == 1){
		led_flag = 0;
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);   // LED1
		HAL_UART_Transmit(&huart3, led, strlen(led), 1000);
	}
    
  /*
  * 덩어리 2
  */
	udp_connect(upcb1, &addr1, 9999);  // Add
	sprintf(temp_str, "stm32->server send");
	p1 = pbuf_alloc(PBUF_TRANSPORT, strlen((char *) temp_str), PBUF_POOL);
	if (p1 != NULL)
	{
		// copy data to pbuf
		pbuf_take(p1, (char *)temp_str, strlen((char *)temp_str));
		// send udp data
		udp_send(upcb1, p1);
		/* free the UDP connection, so we can accept new clients */
		udp_disconnect(upcb1);

		// free pbuf
		pbuf_free(p1);
	}
	else
	{
		HAL_UART_Transmit(&huart3,  "pbuf not allocated\n", strlen("pbuf not allocated\n"), 10);
	}
	HAL_Delay(50);
#if 0
	  printf("cds sensor: %d\n", adcValue[0]);
	  if (adcValue[0] < 2500)
	  {
		  nucleo_stm32f429zi_led_on();
	  }
	  else
	  {
		  nucleo_stm32f429zi_led_off();
	  }
	  HAL_Delay(10);
#endif
//	DHT11_processing();
// 	pc_command_processing();
// 	bt_command_processing();
// 	ultrasonic_processing();
// 	dcmotor_pwm_control();
// 	get_rtc();
// 	lcd_display_mode_select();
// 	set_time_button_ui();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

udp_echoserver.c

main.c에서 ethernetif_input(&gnetif);를 만나면 콜백으로 들어감

void udp_echoserver_receive_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port)
{

  /* Connect to the remote client */
  udp_connect(upcb, addr, UDP_CLIENT_PORT);
    
  MEMCPY(udp_data, p->payload, sizeof(udp_data));    // UDP data를 1 byte 읽어온다
  if (strncmp(udp_data, "SERVO:", 6) == 0)
  {
	  /* Tell the client that we have accepted it */

	  udp_flag = 1;
  }

  if (strncmp(udp_data, "LED001", 6) == 0){
	  led_flag = 1;
  }
  /* Tell the client that we have accepted it */
  udp_send(upcb, p);

  /* free the UDP connection, so we can accept new clients */
  udp_disconnect(upcb);
	
  /* Free the p buffer */
  pbuf_free(p);
   
}

콜백 인터럽트 함수는 길게 짜면 안된다. ISR은 짧게 상태 변화만 indicate하고, 그로 인해 발생해야 하는 작업은 main.c에서 돌고 있는 loop monitor에서 처리되어야 한다.

Qt Creater

Qt - File - Qt Widgets Application으로 만들었다.

Qt의 ".pro" 파일이 바로 makefile에 해당하는 것이다.

make는 컴파일을 할 때 특정 파일만 골라서 컴파일 함으로써, 디버깅 할 때마다 모든 파일을 전부 다 컴파일 하는 쓸데없는 오버헤드를 방지해주는 것이다.

교수님이 제공해주신 chart library

우리 카페에 있음

  1. 차트 라이브러리를 다운 받아서 프로젝트 폴더에 넣고 에디터에 경로를 잡아줬다.
  2. 그랬더니 .pro 파일에 자동 등록이 되었다.
  3. 그리고 .pro 파일 위에다가 몇줄의 코드라인을 추가했다.
#-------------------------------------------------
#
# Project created by QtCreator 2024-01-29T13:46:57
#
#-------------------------------------------------

QT       += core gui
QT       += network #add kenGwon TCP/IP
QT       += printsupport #add kenGwon plot chart

...

CONFIG += c++11

SOURCES += \
        main.cpp \
        mainwindow.cpp \
        qcustomplot.cpp

HEADERS += \
        ../../../Downloads/qcustomplot.h \
        mainwindow.h \
        qcustomplot.h

FORMS += \
        mainwindow.ui
        
...
  1. 이하는 각 파일의 최종 수정결과이다.

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include <QPen>
#include <QUdpSocket> // add kenGwon
#include <QTextStream> // 상위 레이어에서 메세지를 텍스트 단위로 핸들링하기 위한 것 :: add kenGwon
#include <QDebug> // stdio.h와 비슷 :: add kenGwon
#include <QString> // c++의 string을 override한 클래스 :: add kenGwon


namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

    void add_point(double time_x, double tmp_y, double wet_y); // plot그리기용 함수 :: add kenGwon
    void clear_data(); // plot 지우기 :: add kenGwon
    void plot(); // plot 출력 :: add kenGwon

    QVector <double> qv_tmp_x, qv_tmp_y, qv_wet_x, qv_wet_y; // plot그리기용 변수
    QString temperature, wet; // plot그리기용 변수

public slots: // signal에 대한 callback function
    void readyRead();

private slots:
    void on_pushButtonSend_clicked();

    void on_checkBox_LED1_stateChanged(int arg1);

    void on_checkBox_LED2_stateChanged(int arg1);

    void on_checkBox_LED3_stateChanged(int arg1);

    void on_dial_servo_valueChanged(int value);

    void on_dial_led_valueChanged(int value);

    void on_horizontalSlider_DHT11_interval_valueChanged(int value);

    void on_pushButton_clear_clicked();

private:
    Ui::MainWindow *ui;

    QUdpSocket *socket = nullptr;


};

#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include "qcustomplot.h" // add kenGwon


MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    /* kenGwon START */

    // 1. 소켓 생성
    socket = new QUdpSocket(this);

    // 2. IP, SW PORT번호 등록
    bool result = socket->bind(QHostAddress::AnyIPv4, 9999);
    qDebug() << result; // c++의 cout과 동일한 역할을 하는게 qDebug()이다.

    if (result) {qDebug() << "pass!!!";}
    else {qDebug() << "fail...";}

    // 3. Qt signal(event발생) / slot(event에 대한 interrupt service routine) / connect(signal과 slot을 mapping)
    connect(socket, SIGNAL(readyRead()), this, SLOT(readyRead())); // 소켓으로부터 읽을 데이터가 존재하면 this의 readyRead()함수를 호출한다는 것


    ui->plot->setInteraction(QCP::iRangeDrag, true); // 여기서 plot이라는 멤버는 .ui GUI에서 설정한 Object Name이다.
    ui->plot->setInteraction(QCP::iRangeZoom, true);
    ui->plot->addGraph();
    ui->plot->addGraph();
    //ui->plot->xAxis->setLabel("time(s)");
    ui->plot->yAxis->setLabel("temp");
    ui->plot->yAxis->setRange(0.0, 40.0);

    QPen red_pen, blue_pen;
    red_pen.setColor(QColor(255, 0, 0));
    blue_pen.setColor(QColor(0, 0, 255));

    ui->plot->graph(0)->setScatterStyle(QCPScatterStyle::ssCircle);     //점찍는 스타일 결정.
    ui->plot->graph(0)->setLineStyle(QCPGraph::lsLine);                           //라인 스타일 결정.
    ui->plot->graph(0)->setPen(red_pen);
    ui->plot->graph(1)->setScatterStyle(QCPScatterStyle::ssSquare);     //점찍는 스타일 결정.
    ui->plot->graph(1)->setLineStyle(QCPGraph::lsLine);                           //라인 스타일 결정.
    ui->plot->graph(1)->setPen(blue_pen);

    connect(ui->plot, SIGNAL(mouseDoubleClickEvent(QMouseEvent*)), SLOT(QMouseEvent*));


    /* kenGwon END */
}

MainWindow::~MainWindow()
{
    delete ui;
}


int time_gv = 0;

void MainWindow::readyRead()
{
    QByteArray buffer;
    double temp_digit; // 온도값 저장용 변수
//    static int time = 0;

    QHostAddress sender;
    quint16 senderPort; // 송신자의 port주소

    buffer.resize(socket->pendingDatagramSize());

    // 소켓으로부터 데이터를 읽음
    socket->readDatagram(buffer.data(), buffer.size(), &sender, &senderPort);
    buffer.chop(1); // STM32로부터 수신한 버퍼에서 문자열 맨 끝에 있는 "\n"를 제거하기 위한 함수이다. (chop: 맨 오른쪽에서 1바이트 제거하는 함수)

    // 읽어온 데이터를 ui의 textEdit요소에 출력(ui는 생성자에서 만들어진 멤버변수)
    ui->textEditRxData->append(buffer);
    ui->textEditRxData->show();

    // 읽어온 데이터를 ui의 LCD number요소에 출력
//    buffer = buffer.right(2); // 버퍼의 맨 오른쪽부터 두자리만 buffer의 값으로 취하겠다는 말... [Tmperature]:25
//                              // 버퍼에는 25가 들어있다. 25는 utf-8모드로 저장되어있다. 그래서 그걸 ascii로 바꾸고, 다시 integer로 바꿔줘야 한다.
//                              // LCD number요소에는 int 혹은 double 값을 줘야 올바르게 출력된다.
//    temp_digit = buffer.toDouble();
//    ui->lcdNumber_tmp->display(temp_digit);
//    ui->lcdNumber_tmp->show();


    QList<QByteArray> parts = buffer.split(' ');

    // 각 부분을 확인하면서 "Tmp:"이 포함된 부분 찾기
    QByteArray tmp;
    QByteArray wet;
    for (const QByteArray& part : parts) {
        if (part.startsWith("Tmp:")) {
            // "Tmp:" 다음의 부분을 추출
            tmp = part.mid(4); // "Tmp:" 이후의 문자열만 추출
        }
        else if (part.startsWith("Wet:")) {
            // "Wet:" 다음의 부분을 추출
            wet = part.mid(4); // "Wet:" 이후의 문자열만 추출
        }
    }


    if (!(tmp.toInt() == 0 && wet.toDouble() == 0))
    {
        ui->lcdNumber_tmp->display(tmp.toDouble());
        ui->lcdNumber_tmp->show();
        ui->lcdNumber_wet->display(wet.toDouble());
        ui->lcdNumber_wet->show();

        // 그림 그리는 부분
        add_point(time_gv, tmp.toDouble(), wet.toDouble());
        time_gv+=3;
        ui->plot->xAxis->setRange(0, time_gv+3);
        plot();
    }
}

/*
 * 생성경로: .ui Design GUI에서 "send버튼" 우클릭 - "go to slot" 클릭
*/
void MainWindow::on_pushButtonSend_clicked()
{
    QByteArray Data;
    Data = ui->lineEditSendData->text().toUtf8();

    socket->writeDatagram(Data, QHostAddress("10.10.15.82"), 9999);
}

void MainWindow::on_checkBox_LED1_stateChanged(int arg1)
{
    QString buffer;
    QByteArray send_data;

    buffer.sprintf("LED001");
    send_data = buffer.toUtf8(); // ascii는 utf-8과 값이 동일하여 변환을 굳이 안해도 된다. 하지만 한글이라면 반드시 변환해줘야 한다.

    socket->writeDatagram(send_data, QHostAddress("10.10.15.82"), 9999);
}

void MainWindow::on_checkBox_LED2_stateChanged(int arg1)
{
    socket->writeDatagram("LED002", QHostAddress("10.10.15.82"), 9999);
}

void MainWindow::on_checkBox_LED3_stateChanged(int arg1)
{
    socket->writeDatagram("LED003", QHostAddress("10.10.15.82"), 9999);
}


void MainWindow::add_point(double time_x, double tmp_y, double wet_y)
{
    qv_tmp_x.append(time_x);
    qv_wet_x.append(time_x);

    qv_tmp_y.append(tmp_y);
    qv_wet_y.append(wet_y);
}

void MainWindow::clear_data()
{
    qv_tmp_x.clear();
    qv_wet_x.clear();

    qv_tmp_y.clear();
    qv_wet_y.clear();
}

void MainWindow::plot()
{
    ui->plot->graph(0)->setData(qv_tmp_x, qv_tmp_y);
    ui->plot->graph(1)->setData(qv_wet_x, qv_wet_y);
    ui->plot->replot();
    ui->plot->update();
}

// SERVO: ~99
void MainWindow::on_dial_servo_valueChanged(int value)
{
    QByteArray servo_data = "SERVO:";

    servo_data.append(QString::number(ui->dial_servo->value()));
    ui->lcdNumber_servo->display(ui->dial_servo->value());

    socket->writeDatagram(servo_data, QHostAddress("10.10.15.82"), 9999);

    qDebug() << "servo data: " << servo_data << endl;
}


void MainWindow::on_dial_led_valueChanged(int value)
{
    QByteArray LED_data = "LED:";

    LED_data.append(QString::number(ui->dial_led->value()));
    ui->lcdNumber_led->display(ui->dial_led->value());

    socket->writeDatagram(LED_data, QHostAddress("10.10.15.82"), 9999);

    qDebug() << "led data: " << LED_data << endl;
}

void MainWindow::on_horizontalSlider_DHT11_interval_valueChanged(int value)
{
    QByteArray DHT11_interval_data = "DHT11_interval:";

    DHT11_interval_data.append(QString::number(ui->horizontalSlider_DHT11_interval->value()));
    ui->lcdNumber_DHT11_interval->display(ui->horizontalSlider_DHT11_interval->value());

    socket->writeDatagram(DHT11_interval_data, QHostAddress("10.10.15.82"), 9999);

    qDebug() << "DHT11 interval data: " << DHT11_interval_data << endl;
}

void MainWindow::on_pushButton_clear_clicked()
{
    clear_data();
    time_gv = 0;
}

차트 출력용 widget 컨테이너 생성

  1. 위젯(container)를 만들고 우클릭 해서 "promote to ..."를 클릭
  2. Promoted class name에 "QCustomPlot"을 명명하고 add 후 promote 눌러서 위젯을 생성
  3. 이름을 "QCustomPlot"이라고 지은 이유는 교수님이 준 파일명이 "qcustomplot.h"였기 때문이다. 위처럼 만들면 오버라이드 되서 덮어써진다.

공식 doc가 되게 잘되있네

Qt 5.15 docs

profile
스펀지맨

0개의 댓글