一、 实验简介
实验目标:在Linux下通过串口屏显示并控制功能模块的状态和参数
操作系统:Ubuntu 20.04.6 LTS
串口屏:迪文串口屏 DMG48270C043_03W
二、实现代码-- C语言
代码功能就是在Linux下使用串口和TCP,重点在于如何处理好串口和网口接收的数据。 通过本实验可以基本掌握如何在Linux下使用串口和TCP。网上很多教程都会介绍Liunx下如何实现串口和TCP这两种通信方式,很少有结合实际应用进行介绍的文章,本文就将结合一个实际应用例子进行分析记录,也是对自己学习这些东西的一个简单总结记录。
2.1 通信协议
这里的通信协议不是说解释串口或是TCP这样的通信协议,本文指的是在传输数据过程自己定义的数据帧格式,不同的数据需要设置功能模块的不同功能通信协议的有助于我们管理不同的控制信号。发送数据时按以下格式设置好数据后再通过到其他模块。
主控模块<----->功能模块 网口通信TCP
字段 | 指令头 | 指令码 | 数据长度 | 数据 |
---|---|---|---|---|
长度(字节) | 1 | 1 | 4 | N |
值 | 0xAA | 不同指令 | N | 具体数据 |
主控模块<----->串口屏 串口通信
2.2 串口通信
Linux的串口表现为设备文件。实验使用的串口屏是USB扩展的,实验前需要安装CH341驱动串口设备文件命名为dev/ttyCH341USB*,不同的硬件平台对串口设备文件的命名有所区别。
2.2.1 初始化串口
在Linux下,不管是设备、文档、可执行程序,对于内核来说都是读写文件,会涉及到open、read、write的操作,对于文件操作就有了“阻塞”和“非阻塞”的概念。不同模式对文件的处理方式会有不同,使用的场景也不一样。
“阻塞”的定义
对于 read,当串口的接收缓冲区没有数据的时候,read函数会阻塞在那里,不返回,程序也无法下一步执行,一直到串口的接收缓冲区中有数据可读时,read读到了想要长度的字节数后才会返回,返回值为读到的字节数。
对于write,当串口发送缓冲区满时,或者剩下的空间小鱼将要写入的字节数时,则write阻塞,一直到串口的发送缓冲区中剩下的空间大于等于将要写入的字节数,再执行写操作,返回写入的字节数。
“非阻塞”的定义
对于read,当串口的接收缓冲区中没有数据时,read操作立即返回,返回值为0.
对于write,当串口发送缓冲区满,或者剩下的空间小鱼将要写入的字节数时,write仍然会被执行,写入当前串口发送缓冲区剩下的空间字节数,然后返回写入的字节数。
serial init
serial = serial_new();
if (serial_open(serial,"/dev/ttyCH341USB1",115200) <0) //打开并设置设备文件
{
serial_free(serial);
printf("serial open failed!\n");
}
else
{
printf("serial opened! \n");
}
serial_open 函数
网上很多都是直接用的open函数,实验使用的函数同样基于open函数通过该函数设置好串口,默认使用的阻塞模式。
int serial_open(serial_t *serial, const char *path, uint32_t baudrate) {
return serial_open_advanced(serial, path, baudrate, 8, PARITY_NONE, 1, false, false);//参数设置
}
int serial_open_advanced(serial_t *serial, const char *path, uint32_t baudrate, unsigned int databits, serial_parity_t parity, unsigned int stopbits, bool xonxoff, bool rtscts) {
struct termios termios_settings;
/* Validate args */
if (databits != 5 && databits != 6 && databits != 7 && databits != 8)
return _serial_error(serial, SERIAL_ERROR_ARG, 0, "Invalid data bits (can be 5,6,7,8)");
if (parity != PARITY_NONE && parity != PARITY_ODD && parity != PARITY_EVEN)
return _serial_error(serial, SERIAL_ERROR_ARG, 0, "Invalid parity (can be PARITY_NONE,PARITY_ODD,PARITY_EVEN)");
if (stopbits != 1 && stopbits != 2)
return _serial_error(serial, SERIAL_ERROR_ARG, 0, "Invalid stop bits (can be 1,2)");
memset(serial, 0, sizeof(serial_t));
/* Open serial port */
if ((serial->fd = open(path, O_RDWR | O_NOCTTY)) < 0)
return _serial_error(serial, SERIAL_ERROR_OPEN, errno, "Opening serial port \"%s\"", path);
fcntl(serial->fd,F_SETFL,0);
memset(&termios_settings, 0, sizeof(termios_settings));
/* c_iflag */
/* Ignore break characters */
termios_settings.c_iflag = IGNBRK;
if (parity != PARITY_NONE)
termios_settings.c_iflag |= INPCK;
/* Only use ISTRIP when less than 8 bits as it strips the 8th bit */
if (parity != PARITY_NONE && databits != 8)
termios_settings.c_iflag |= ISTRIP;
if (xonxoff)
termios_settings.c_iflag |= (IXON | IXOFF);
/* c_oflag */
termios_settings.c_oflag = 0;
/* c_lflag */
termios_settings.c_lflag = 0;
/* c_cflag */
/* Enable receiver, ignore modem control lines */
termios_settings.c_cflag = CREAD | CLOCAL;
/* Databits */
if (databits == 5)
termios_settings.c_cflag |= CS5;
else if (databits == 6)
termios_settings.c_cflag |= CS6;
else if (databits == 7)
termios_settings.c_cflag |= CS7;
else if (databits == 8)
termios_settings.c_cflag |= CS8;
/* Parity */
if (parity == PARITY_EVEN)
termios_settings.c_cflag |= PARENB;
else if (parity == PARITY_ODD)
termios_settings.c_cflag |= (PARENB | PARODD);
/* Stopbits */
if (stopbits == 2)
termios_settings.c_cflag |= CSTOPB;
/* RTS/CTS */
if (rtscts)
termios_settings.c_cflag |= CRTSCTS;
/* Baudrate */
cfsetispeed(&termios_settings, _serial_baudrate_to_bits(baudrate));
cfsetospeed(&termios_settings, _serial_baudrate_to_bits(baudrate));
/* Set termios attributes */
if (tcsetattr(serial->fd, TCSANOW, &termios_settings) < 0) {
int errsv = errno;
close(serial->fd);
serial->fd = -1;
return _serial_error(serial, SERIAL_ERROR_CONFIGURE, errsv, "Setting serial port attributes");
}
serial->use_termios_timeout = false;
return 0;
}
2.2.2 读写串口
fd为文件识别号,buf为收发数组
接收数据
int len;unsigned char buf[11];
len = read(fd, buf, 11);
if (len < 0){
printf("reading data faile \n");
}
发送数据
int len;
char buf[] = "hello world!";
len = write(fd, buf, sizeof(buf));
if (len< 0) {
printf("write data to serial failed! \n");
}
2.3 TCP通信
Socket编程是一种网络编程的方式,用于实现不同计算机之间的数据通信。在网络应用中,Socket是端到端通信的一个抽象概念,它建立在网络模型的传输层之上,提供了一种方式来允许程序跨网络发送和接收数据。最常见的是TCP(传输控制协议)和UDP(用户数据报协议)。
2.4.1 TCP初始化
Socket编程通常涉及以下几个基本步骤:
- 创建Socket:首先,需要在客户端和服务器端分别创建Socket。在服务器端,Socket用于监听来自客户端的连接请求;在客户端,Socket用于与服务器建立连接。
- 绑定(Bind):服务器端的Socket需要绑定到一个网络地址(IP地址)和端口上,以便客户端能够找到并连接到它。
- 监听(Listen):服务器端的Socket开始监听绑定的地址和端口,等待客户端的连接请求。
- 接受连接(Accept):当服务器Socket监听到客户端的连接请求时,它会接受这个连接,从而在服务器和客户端之间建立一个新的通信链路。
- 数据传输:一旦连接建立,客户端和服务器端就可以通过读写操作在这个连接上发送和接收数据。数据的发送和接收可以是阻塞的也可以是非阻塞的,这取决于Socket的配置。
- 关闭连接:数据传输完成后,双方可以关闭连接。关闭连接的一方会向对方发送一个连接释放信号,以结束会话。
#include"TCPserver.h"
int TCPserverinit(void)
{
//服务器监听套接字和连接套接字
listen_fd=-1;
connect_fd=-1;
struct sockaddr_in servaddr;//定义服务器对应的套接字地址
//服务器接收和发送缓冲区
uint8_t sendbuf[MAXLINE], recbuf[MAXLINE];
//初始化套接字地址结构体
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;//IPv4
servaddr.sin_port = htons(PORT);//设置监听端口
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示接收任意IP的连接请求
//创建套接字
if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
//如果创建套接字失败,返回错误信息
//strerror(int errnum)获取错误的描述字符串
printf("create socket error: %s(error: %d)\n", strerror(errno), errno);
exit(0);
}
//绑定套接字和本地IP地址和端口
if(bind(listen_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
//绑定出现错误
printf("bind socket error: %s(error: %d)\n", strerror(errno), errno);
exit(0);
}
//使得listen_fd变成监听描述符
if(listen(listen_fd, 10) == -1){
printf("listen socket error: %s(error: %d)\n", strerror(errno), errno);
exit(0);
}
//accept阻塞等待客户端请求
printf("等待客户端发起连接\n");
if((connect_fd = accept(listen_fd, (struct sockaddr*)NULL, NULL)) == -1){
printf("accept socket error: %s(error: %d)\n", strerror(errno), errno);
}
}
2.4.1 TCP收发数据
在Linux下,都是按读写文件的方式去管理设备。
接收数据
int len;unsigned char buf[11];
len = read(fd, buf, 11);
if (len < 0){
printf("reading data faile \n");
}
发送数据
int len;
char buf[] = "hello world!";
len = write(fd, buf, sizeof(buf));
if (len< 0) {
printf("write data to serial failed! \n");
}
2.4 数据收发处理
2.4.1 串口屏
void* ScreenTranslateMessage(void* arg)
{
uint8_t i,j=0;
uint8_t hedebyte[3];
uint8_t onebyte;
while (1)
{
serial_read(serial,&onebyte, 1);
usleep(5000);
if(onebyte==0X5A) //识别帧头标识
{
hedebyte[0]=onebyte;
onebyte=0;
serial_read(serial, hedebyte+1, 2);
usleep(5000);
}
if (hedebyte[0]==0X5A && hedebyte[1]==0XA5)
{
sdatalen=hedebyte[2];
serial_read(serial,sdata,sdatalen);//接受串口屏反馈数据
usleep(2500);
for (i = 0; i<3; i++)
{
printf("%02x",hedebyte[i]);
}
for (j = 0; j<sdatalen; j++)//数据
{
printf("%02x",sdata[j]);
}
printf("\n");
serialPolling(sdata,sdatalen); //按照通信协议依次处理数据,通过TCP反馈到功能模块
memset(sdata,0,sizeof(sdata));
sdatalen=0;
}
usleep(5000);
memset(hedebyte,0,sizeof(hedebyte));
}
return NULL;
}
uint8_t serialPolling(uint8_t *data,uint8_t datalen)
{
int i=0,j=0;
double power=2;
uint8_t chartonum=0;
if(datalen==0)
{ return 0;}
else
{
LCDaddr=((uint16_t)data[1]<<8)|data[2];//识别串口屏部件
switch (LCDaddr)
{
case State_check:
sendbuf[0]=0xAA;
sendbuf[1]=0x01;
sendbuf[2]=0x00;
sendbuf[3]=0x00;
sendbuf[4]=0x00;
sendbuf[5]=0x00; //设置数据帧
write(connect_fd, sendbuf, 6);//通过TCP反馈到功能模块
break;
case IP_address : //获得网络网址,将bits数据转化为网址
for(i=4;i < 19;i ++)
{
if(data[i]!= 0xFF)
{
IPaddress[i] = data[i];
if(data[i]!=0x2e)
{
chartonum=(IPaddress[i]-0x30)*pow(10,power);
IP_buffer[j]=IP_buffer[j]+chartonum;
power--;
}
else
{
j++;
power=2;
}
}
}
memset(IPaddress,0,15);
break;
default:
break;
}
}
}
2.4.2 功能模块
void* TCPtranslateMessage(void* arg)
{
while (1)
{
//读取客户端发来的信息
ssize_t tdatalen = read(connect_fd,tdata, sizeof(tdata));
if(tdatalen <= 0){
return 0;
}
for (uint8_t i = 0; i < tdatalen; i++)
{
printf("%02x",tdata[i]);
}
printf("\n");
TCPPolling(tdata,tdatalen);//处理TCP接收数据,和串口类似先找帧头然后针对不同数据进行处理
memset(tdata,0,sizeof(tdata));
}
return NULL;
}
根据自己的需求去设计处理数据的函数
uint8_t TCPPolling(uint8_t *data,uint8_t datalen)
{
uint8_t position=6;
if(datalen==0)
{ return 0;}
else
{
command=data[1];
switch (command)
{
case 01:
/* code */
//状态显示,设置屏幕显示数据
DW_SetValue(Running_state,data[position]);
DW_SetValue(Input_datastate,data[position+1]);
break;
case 02:
DW_SetValue(Output_power,data[position]);
DW_SetValue(PAR_coefficient,data[position+1]);
break;
case 03:
/* code */
break;
default:
break;
}
}
三、参考资料
《T5L DGUSII应用开发指南》
串口通信协议和Linux下的串口编程
TCP的socket详解
迪文串口屏教程
迪文串口屏(T5L2 & DGUS II)开发 – 入门