这两天在研究用C语言从串口接收数据。在Windows下接收数据网上已经有例子直接能用,但是在Linux下接收数据总是有问题,费了几天时间才解决,这里简单记录一下踩过的坑。
Linux下使用termios库来进行串口通信,主要是VTIME和VMIN两个参数的配置,具体的参数含义和组合说明有文章已经写得很清楚了,具体看Linux 串口编程学习记录(termios.h)
但是按照这样配置完成后发现读取的数据老是掉帧。把录制下来的数据打开后一个个字节地寻找帧头帧尾查看问题原因,发现是循环read的过程中,每次read经常是漏了一两个字节的数据,造成数据丢失,解析不出完整帧导致掉帧。最后发现是其中一些特定数据被用作特殊控制了。具体解决方法在解决方法:Linux串口接收字节0x11,0x0d,0x13丢失。简单来说就是网上的很多Linux下读写串口的示例程序都没对c_iflag进行有效的设置,这样传送ASCII码时没什么问题,但传送二进制数据时遇到0x0d,0x11和0x13却会被用作特殊控制了,关掉 ICRNL 和 IXON 选项即可解决。
实际数据 | 特殊控制字符 |
---|---|
0x0d | 回车符CR |
0x11 | ^Q VSTART字符 |
0x13 | ^S VSTOP字符 |
在我的程序中具体表现是360000字节的数据中没有一个0x11或者0x13,0x0d倒还是正常的。
插入一行代码就能解决
// 把终端I/O设置为原始模式(串口通讯就是终端I/O的原始模式)时输入属性设置为
options.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
具体在Linux下读取串口数据并保存在二进制文件中的完整代码附在下面
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
int main(int argc, char *argv[])
{
int fd;
uint8_t buf[60];
int msg_length;
// 用来存储原始串口消息
FILE *fp_msg = fopen("msg_result.txt", "wb+");
// O_RDWR表示以读写方式打开,O_NOCTTY表示不将串口设备作为控制终端
fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY );
if (fd < 0)
{
perror("open serial port failed!!!");
return -1;
}
// 使用tcgetattr()函数获取当前的串口配置参数,然后修改需要的参数,最后使用tcsetattr()函数重新设置串口参数
struct termios options;
tcgetattr(fd, &options);
cfsetispeed(&options, B921600); // 串口波特率为115200
cfsetospeed(&options, B921600);
options.c_cflag |= CLOCAL | CREAD; // 忽略调制解调器状态线 | 启用接收器
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8; // 使用8个数据位
options.c_cflag &= ~PARENB; // 禁用奇偶校验
options.c_cflag &= ~CSTOPB; // 使用1个停止位
// options.c_cflag |= CRTSCTS; // 使用硬件流控制。在高速(19200bps或更高)传输时,使用软件流控制会使效率降低,这个时候必须使用硬件流控制。
// options.c_iflag |= IXON | IXOFF; //使用软件流控制
// options.c_cflag &= ~OPOST; //原始数据(RAW)输出
options.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_oflag &= ~OPOST;
options.c_cc[VMIN] = 60;
options.c_cc[VTIME] = 0; // 串口读取的超时等待时间
tcsetattr(fd, TCSANOW, &options);
// 读取前先清空缓存,防止数据污染
tcflush(fd, TCIOFLUSH);
// while (1)
for(int32_t j = 0; j < 6000; j++)
{
msg_length = read(fd, buf, sizeof(buf));
printf("%d attempt get %d byte\n", j, msg_length);
if (msg_length > 0)
{
fwrite(buf, sizeof(uint8_t), msg_length, fp_msg);
}
}
fclose(fp_msg);
close(fd);
return 1;
}
顺便把Windows下读取串口数据并保存在二进制文件中的完整代码也附上吧
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdint.h>
#include <Windows.h>
#include <time.h>
#include <winnt.h>
int main()
{
HANDLE hCom;
TCHAR serial_port[100];
uint8_t buf[58] = {0};
DWORD RLen = 0;
BOOL status;
wsprintf(serial_port, TEXT("\\\\.\\COM%d"), 5);
// COM口
// 允许读和写
// 指定共享属性,由于串口不能共享,所以该参数必须为0
// 打开已经存在的串口
// 属性描述,该值为FILE_FLAG_OVERLAPPED,表示使用异步I/O,该参数为0,表示同步I/O操作
hCom = CreateFile(serial_port, GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hCom == INVALID_HANDLE_VALUE){
// printf("打开COM口失败! %s\n", serial_port);
return 0;
}
SetupComm(hCom, 5800, 5800); //设置输入缓冲区和输出缓冲区的大小
// 超时设置
COMMTIMEOUTS TimeOuts;
// 设定读超时
TimeOuts.ReadIntervalTimeout = 0; //读间隔超时
TimeOuts.ReadTotalTimeoutMultiplier = 0; //读时间系数
TimeOuts.ReadTotalTimeoutConstant = 5000; //读时间常量
// 设定写超时
TimeOuts.WriteTotalTimeoutMultiplier = 1; //写时间系数
TimeOuts.WriteTotalTimeoutConstant = 1; //写时间常量
SetCommTimeouts(hCom, &TimeOuts); //设置超时
// 配置串口
DCB dcb;
GetCommState(hCom, &dcb);
dcb.BaudRate = 921600; // 波特率
dcb.ByteSize = 8; // 每个字节有8位
dcb.Parity = NOPARITY; // 无奇偶校验位
dcb.StopBits = ONESTOPBIT; // 一个停止位
SetCommState(hCom, &dcb);
// 用来存储原始串口消息
FILE *msg_result_txt = fopen("msg_result.txt", "wb+");
assert(msg_result_txt != NULL);
// while (1)
for(int32_t j = 0; j < 60000; j++)
{
// PurgeComm(hCom, PURGE_TXCLEAR | PURGE_RXCLEAR); // 清空缓冲区
status = ReadFile(hCom, buf, sizeof(buf), &RLen, NULL);
if (status)
{
fwrite(buf, sizeof(uint8_t), RLen, msg_result_txt);
printf("%d attempt get %d byte\n", j, RLen);
}
}
fclose(msg_result_txt);
CloseHandle(hCom);
return 1;
}