Rice我叫加饭? · 2021年06月17日

教你动手写TCP上位机与小熊派通信

首发:Rice 嵌入式开发技术分享
作者:RiceDIY

背景

  • 上位机开发不限于语言,找我之前开发中,初衷就是那种方便就使用那种语言开发,如:C#, QT, python, VB等。
  • 本篇文章分享是采用QT开发的TCP上位机,功能:通过TCP上位机控制小熊派板载外设。
  • 上位机采用QT开发,小熊派跑RT-Thread,如下图为总体框图。

image.png

‍TCP上位机


  • 本上危机支持作为服务器也支持作为客户端,可以通过按键进行切换到不同的模式。该上位机主要功能:①控制板子LED,②调节扩展板E53\_IA1上LED的亮度。
  • 不过属于是作为客户端还是服务器都可以实现上述两个功能。
  • 上位机功能实现主要有两个文件:bearpi.cpp和bearpi.h

image.png

TCP上位机开发说明:

  1. 在项目文件中添加如下内容:
QT       += network  
  1. TCP网络编程需要用到的头文件:
#include <QTcpSocket>  
#include <QTcpServer>  
  1. bearpi.h头文件内容说明:申明了界面的事件槽函数,并且定义了Tcp\_Server和TcpSocket的句柄。
#ifndef BEARPI_H  
#define BEARPI_H  
  
#include <QMainWindow>  
#include <QTcpSocket>  
#include <QTcpServer>  
  
QT_BEGIN_NAMESPACE  
namespace Ui { class bearpi; }  
QT_END_NAMESPACE  
  
class bearpi : public QMainWindow  
{  
    Q_OBJECT  
  
public:  
    bearpi(QWidget *parent = nullptr);  
    ~bearpi();  
  
  
private slots:  
    void on_mode_pushButton_clicked();  
  
    void on_start_pushButton_clicked();  
  
    void on_open_pushButton_clicked();  
  
    void on_pwm_horizontalSlider_valueChanged(int value);  
  
private:  
    void switch_mode();  
    void new_client_connect();  
  
private:  
    Ui::bearpi *ui;  
    QTcpServer *server;  
    QTcpSocket *socket;  
};  
#endif // BEARPI_H  
  1. bearpi.cpp源文件构造函数内容说明:①实例化Tcp\_Server和TcpSocket的句柄,②定义IP地址的lineEdit控件格式,③根据模式使能对应的控件。
bearpi::bearpi(QWidget *parent)  
    : QMainWindow(parent)  
    , ui(new Ui::bearpi)  
{  
    ui->setupUi(this);  
  
    server = new QTcpServer();  
    socket = new QTcpSocket();  
  
    QRegExp ip_RegExp("^((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)$");  
    QRegExpValidator *ip_format = new QRegExpValidator(ip_RegExp, this);  
    ui->ip_lineEdit->setValidator(ip_format);  
  
    switch_mode();  
}  
  1. bearpi.cpp源文件switch\_mode()函数内容说明:①根据模式使能对应的控件。
void bearpi::switch_mode()  
{  
    if(ui->mode_pushButton->text() == tr("SERVER"))  
    {  
        ui->ip_lineEdit->setEnabled(false);  
    }  
    else if(ui->mode_pushButton->text() == tr("CLIENT"))  
    {  
        ui->ip_lineEdit->setEnabled(true);  
    }  
}  
  1. bearpi.cpp源文件new\_client\_connect()函数内容说明:①当模式作为server时,有客户端请求建立连接时的信号槽函数,主要与客户端建立socket句柄。
void bearpi::new_client_connect()  
{  
    socket = server->nextPendingConnection();  
    qDebug() << "A Client connect!";  
}  
  1. bearpi.on\_start\_pushButton\_clicked()函数内容说明:①当作为服务器,则进行建立服务器,并联建立客户端连接槽函数。①当作为客户端,根据IP地址和端口号与服务器建立连接。
void bearpi::on_start_pushButton_clicked()  
{  
    if(ui->start_pushButton->text() == tr("START"))  
    {  
        if(ui->mode_pushButton->text() == tr("SERVER"))  
        {  
            int port = 0;  
  
            connect(server,&QTcpServer::newConnection,this,&bearpi::new_client_connect);  
  
            port = ui->port_lineEdit->text().toInt();  
  
            if(!server->listen(QHostAddress::Any, port))  
            {  
                qDebug() << server->errorString();  
                return;  
            }  
            qDebug() << "Listen successfully";  
        }  
        else if(ui->mode_pushButton->text() == tr("CLIENT"))  
        {  
            QString ip;  
            int port = 8080;  
  
            ip = ui->ip_lineEdit->text();  
            port = ui->port_lineEdit->text().toInt();  
  
            socket->abort();  
            socket->connectToHost(ip, port);  
  
            if(!socket->waitForConnected(3000))  
            {  
                qDebug() << "Connect failed";  
                return;  
            }  
            qDebug() << "Connect successfully";  
        }  
        ui->start_pushButton->setText(tr("STOP"));  
        ui->mode_pushButton->setEnabled(false);  
    }  
    else if(ui->start_pushButton->text() == tr("STOP"))  
    {  
        if(ui->mode_pushButton->text() == tr("SERVER"))  
        {  
            socket->abort();  
            server->close();  
  
        }  
        else if(ui->mode_pushButton->text() == tr("CLIENT"))  
        {  
            socket->disconnectFromHost();  
        }  
        ui->start_pushButton->setText(tr("START"));  
        ui->mode_pushButton->setEnabled(true);  
    }  
}  
  1. bearpi.cpp源文件on\_open\_pushButton\_clicked()函数内容说明:①控制小熊派板载LED的开关的槽函数,②通过发送led\_open和led\_close字符串来控制板载LED。
void bearpi::on_open_pushButton_clicked()  
{  
    qDebug() << ui->open_pushButton->text();  
    if(ui->open_pushButton->text() == tr("OPEN LED"))  
    {  
        socket->write("led_open");  
        socket->flush();  
        ui->open_pushButton->setText(tr("CLOSE LED"));  
    }  
    else if(ui->open_pushButton->text() == tr("CLOSE LED"))  
    {  
        socket->write("led_close");  
        socket->flush();  
        ui->open_pushButton->setText(tr("OPEN LED"));  
    }  
}  
  1. bearpi.cpp源文件on\_pwm\_horizontalSlider\_valueChanged()函数内容说明:①调节扩展板E53\_IA1上LED的亮度的槽函数,②根据滑动条的值调节扩展板E53\_IA1上LED的PWM。
void bearpi::on_pwm_horizontalSlider_valueChanged(int value)  
{  
    char pwm_str[1] = {0};  
  
    pwm_str[0] = (char)value;  
  
    socket->write(pwm_str);  
    socket->flush();  
  
    qDebug() << pwm_str[0];  
}  

TCP上位机演示:

1. 作为server:

image.png

2. 作为client:

image.png

小熊派开发

  • 为了快速的开发,我直接采用rt-thread,它提供的丰富的组件,不用自己造轮子。小熊派上的功能主要是启动一个client,然后通过接收到上位机的命令之后通过PWM或GPIO控制LED。
  • 网络:采用小熊派板载模组ESP8266。使用RT-Thread的AT组件,SAL组件,netdev组件。
  • PWM:使用了PWM设备驱动框架
  • demo中小熊派作为客户端,TCP上位机作为服务器。上位机通过TCP控制小熊派。

小熊派代码说明:

  1. 通过RT-THREAD强大的组件,使我们编程更加统一简单。
  • 创建一个socket,然后连接到对应上位机服务器。
  • 根据设备名获取PWM的句柄,然后初始化pwm的初始值并使能。
  • 创建一个线程,用于处理服务器下发的指令及数据。
void bearpi_client(int argc, char **argv)  
{  
  int ret;  
  struct hostent *host;  
  struct sockaddr_in server_addr;  
  
  host = gethostbyname(IP_ADDR);  
  
  if ((sock_client_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)  
  {  
    rt_kprintf("Socket error\n");  
    return;  
  }  
  
  server_addr.sin_family = AF_INET;  
  server_addr.sin_port = htons(PORT);  
  server_addr.sin_addr = *((struct in_addr *)host->h_addr);  
  rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));  
  
  if (connect(sock_client_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)  
  {  
    rt_kprintf("Connect fail!\n");  
    closesocket(sock_client_fd);  
    return;  
  }  
  
  pwm_dev = (struct rt_device_pwm *)rt_device_find(PWM_DEV_NAME);  
  
  rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse);  
  
  rt_pwm_enable(pwm_dev, PWM_DEV_CHANNEL);  
  
  rt_thread_init(&bearpi_thread,  
                "bearpi",  
                bearpi_entry,  
                RT_NULL,  
                &bearpi_stack[0],  
                sizeof(bearpi_stack),  
                10, 5);  
  rt_thread_startup(&bearpi_thread);  
  
  return;  
}  
MSH_CMD_EXPORT(bearpi_client, a tcp client sample);  
  1. 接受数据线程:根据命令的类型进行调节板载的LED。
void bearpi_entry(void* paramenter)  
{  
  char recv_data[BUFSZ] = {0};  
  uint32_t recv_len = 0;  
  
  uint32_t is_open = 0;  
  while (1)  
  {  
    recv_len = recv(sock_client_fd, recv_data, BUFSZ - 1, 0);  
  
    if(recv_len > 0)  
    {  
      if(strncmp(recv_data, "led_open", 8) == 0)  
      {  
        rt_pin_write(LED0_PIN, PIN_HIGH);  
        is_open = RT_TRUE;  
      }  
      else if(strncmp(recv_data, "led_close", 1) == 0)  
      {  
        rt_pin_write(LED0_PIN, PIN_LOW);  
        is_open = RT_FALSE;  
        rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, 0);  
      }  
      else  
      {  
        if(is_open == RT_TRUE)  
        {  
          pulse = period / 100 * recv_data[0];  
          rt_pwm_set(pwm_dev, PWM_DEV_CHANNEL, period, pulse);  
        }  
      }  
    }  
  }  
}  

吐槽说明:

  • 在RT-THREAD的驱动中,有一个做的不是很友好的地方如下图:
  • drv\_pwm.c中了为了划分不同的型号进行的归类,RT-THREAD值进行大类归类,而没有进行细化不同的型号。如L4系列,但实际STM32L431并没有那么多TIM,所以当我们使能PWM这个功能时,会编译不过。。
  • 所以这里需要细分不同型号,但在作者工程中,把报错的地方直接注释了。

image.png

整体功能演示

image.png

源代码厂库

推荐阅读

更多嵌入式技术干货请关注Rice 嵌入式开发技术分享
推荐阅读
关注数
1761
内容数
51
一个周末很无聊的嵌入式软件工程师,写写经验,写写总结。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息