3. 串口通信基础
在无人车系统中,模型推理得到的控制指令(如转向角、油门开度)最终必须通过物理链路传递给底层执行机构(电机驱动器、舵机控制器)。在众多通信接口中,串口(UART)因其协议简单、硬件成本低、实时性好、驱动成熟,成为无人车底盘控制最常用的接口之一。理解串口通信的底层机制,是正确封装控制协议、排查通信故障、保障指令实时性的前提。本节将系统讲解串口协议的核心参数、数据帧结构、校验方法,并给出Linux环境下串口编程的完整操作流程。
上图展示了无人车控制链路中串口通信的完整角色:ROS控制节点将模型输出的控制指令封装为特定数据帧,通过UART物理链路发送至底盘执行控制器,执行控制器解析并驱动电机。整个过程涉及串口协议参数配置、数据帧格式设计、校验机制以及Linux系统下的设备操作。下面逐一深入剖析。
3.1 串口协议核心参数与物理特性
3.1.1 串口通信的本质
串口通信(UART,Universal Asynchronous Receiver/Transmitter)是一种异步串行通信协议,数据按位(bit)依次在一条数据线上传输。与SPI、I2C等同步通信不同,UART不需要时钟线,收发双方依靠预先约定的波特率和数据帧格式来同步采样时刻。在无人车场景中,串口通常用于连接ROS主控(如Jetson Nano、树莓派)与底盘控制板(如STM32、Arduino Mega),传输距离一般在1米以内,采用TTL电平(3.3V或5V)。
串口通信的关键物理参数包括:
- 波特率(Baud Rate):每秒传输的码元数,常见值有9600、57600、115200、460800等。无人车底盘控制通常选用115200或更高,以降低传输延迟。波特率必须收发一致,否则数据必然乱码。
- 电平标准:TTL电平(0~3.3V/5V)用于板级通信;RS-232电平(±12V)用于远距离工控设备;RS-485差分信号用于抗干扰场景。无人车内部一般使用TTL,若连接外部设备需转换芯片。
- 数据位(Data Bits):每个字节的有效数据位数,通常为8位(8N1格式),也可配置为7位或9位。
- 停止位(Stop Bits):标志一帧数据结束的位,常见为1位或2位,用于接收方校准时钟。
- 流控(Flow Control):硬件流控(RTS/CTS)或软件流控(XON/XOFF),无人车控制中一般禁用,以简化协议。
| 参数项 | 典型值 | 无人车推荐配置 | 说明 |
|---|---|---|---|
| 波特率 | 9600 / 115200 / 460800 | 115200 | 115200下每字节约87μs,满足10Hz控制周期 |
| 数据位 | 7 / 8 / 9 | 8 | 8位数据位兼容标准ASCII和二进制协议 |
| 校验位 | None / Even / Odd | None(或CRC应用层校验) | 硬件校验仅检错,应用层CRC更可靠 |
| 停止位 | 1 / 2 | 1 | 1位停止位效率最高 |
| 流控 | 无 / 硬件 / 软件 | 无 | 避免流控引脚占用和协议复杂度 |
3.2 数据帧格式与协议设计
3.2.1 标准UART数据帧结构
每一帧UART数据由以下部分组成:
- 起始位(Start Bit):1位低电平(逻辑0),标志一帧开始,接收方以此同步采样时钟。
- 数据位(Data Bits):5~9位,通常为8位,低位先传(LSB first)。
- 校验位(Parity Bit):可选,用于简单的错误检测。
- 停止位(Stop Bit):1或2位高电平(逻辑1),标志帧结束。
例如,发送十六进制字节 0x55(二进制01010101),在8N1格式下,线上波形为:起始位0,接着8个数据位(从LSB开始:1,0,1,0,1,0,1,0),最后停止位1。接收方在波特率时钟的每个位周期中点采样,恢复出原始数据。
3.2.2 应用层控制协议设计
裸的UART帧只能传输字节,无法表达“转向角90度”这样的语义。因此,无人车系统必须在应用层定义一套控制协议。一个典型的底盘控制帧格式如下:
帧头(2字节) | 帧ID(1字节) | 数据长度(1字节) | 数据区(N字节) | 校验(2字节) | 帧尾(1字节)
0xAA 0x55 | 0x01 | 0x04 | 0x00 0x5A 0x00 0x64 | CRC16 | 0x0D 0x0A
- 帧头:固定为
0xAA 0x55,用于接收方识别帧起始,防止数据流错位。 - 帧ID:区分指令类型,如0x01表示速度控制,0x02表示转向控制,0x03表示刹车。
- 数据长度:数据区的字节数,便于接收方解析。
- 数据区:具体控制参数,如转向角(有符号16位整数,单位0.1度)、速度(有符号16位整数,单位mm/s)。
- 校验:推荐使用CRC16-CCITT,对帧头到数据区所有字节计算,接收方校验失败则丢弃整帧。
- 帧尾:
0x0D 0x0A(回车换行),便于调试时用串口工具直接观察。
0xAA 0x55是因为其二进制为10101010 01010101,具有丰富的电平跳变,便于接收方用示波器或逻辑分析仪快速定位帧边界。数据区建议使用小端字节序(低字节在前),与ARM Cortex-M系列MCU默认内存序一致,减少转换开销。
3.3 校验方式:从奇偶校验到CRC
3.3.1 硬件校验的局限
UART硬件支持奇校验(Odd)、偶校验(Even)或无校验(None)。奇偶校验只能检测数据位中“1”的个数奇偶性是否变化,无法检测偶数位错误,更无法纠正错误。在无人车高速运动场景下,电磁干扰可能导致连续多位翻转,奇偶校验形同虚设。
3.3.2 CRC校验原理与实现
CRC(循环冗余校验)通过多项式除法生成校验码,能检测出突发错误(连续多位错误)。无人车控制协议中常用CRC16-CCITT(多项式0x1021)或CRC16-MODBUS(多项式0x8005)。CRC16的检错能力为:所有单比特错误、所有双比特错误、所有奇数位错误、所有长度≤16的突发错误,以及99.998%的更长突发错误。
Linux下可用C语言实现CRC16计算,核心代码如下:
uint16_t crc16_ccitt(uint8_t *data, size_t len) {
uint16_t crc = 0xFFFF; // 初始值
for (size_t i = 0; i < len; i++) {
crc ^= (data[i] << 8);
for (int j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x1021;
else
crc <<= 1;
}
}
return crc;
}
3.4 Linux下串口操作完整指南
3.4.1 设备节点识别
在Linux系统中,串口设备以文件形式存在于/dev目录下。常见节点包括:
/dev/ttyUSB*:USB转串口适配器(如CH340、CP2102、FT232),无人车调试中最常用。/dev/ttyACM*:USB CDC类设备(如Arduino Uno、STM32虚拟串口)。/dev/ttyS*:主板原生串口(如RS-232),工控机常见。/dev/ttyAMA*:树莓派板载UART。
插入设备后,可用dmesg | grep tty查看内核日志确认设备节点。若权限不足,需将用户加入dialout组:sudo usermod -aG dialout $USER,然后重新登录。
3.4.2 使用stty配置串口参数
命令行下可用stty快速配置串口。例如配置/dev/ttyUSB0为115200、8N1、无流控:
stty -F /dev/ttyUSB0 115200 cs8 -cstopb -parenb -crtscts
# 解释: cs8=8位数据位, -cstopb=1位停止位, -parenb=无校验, -crtscts=禁用硬件流控
查看当前配置:stty -F /dev/ttyUSB0 -a。发送测试数据:echo "hello" > /dev/ttyUSB0,接收数据:cat /dev/ttyUSB0(需另开终端)。
3.4.3 C语言串口编程流程
在ROS节点或独立控制程序中,串口操作遵循“打开→配置→读写→关闭”四步。以下是一个标准配置代码片段:
int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0) { perror("open"); return -1; }
struct termios options;
tcgetattr(fd, &options);
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
options.c_cflag |= (CLOCAL | CREAD); // 启用接收,忽略调制解调器控制
options.c_cflag &= ~PARENB; // 无校验
options.c_cflag &= ~CSTOPB; // 1位停止位
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8; // 8位数据位
options.c_cflag &= ~CRTSCTS; // 禁用硬件流控
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 原始模式
options.c_iflag &= ~(IXON | IXOFF | IXANY); // 禁用软件流控
options.c_oflag &= ~OPOST; // 原始输出
tcsetattr(fd, TCSANOW, &options);
tcflush(fd, TCIOFLUSH); // 清空缓冲区
读写操作使用标准的read()和write()系统调用。注意:串口读写可能部分完成,需循环处理。推荐使用select()或poll()实现超时控制,避免阻塞。
3.4.4 Python串口操作(pyserial)
对于快速原型验证,Python的pyserial库更为便捷:
import serial
ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=0.1)
ser.write(b'\xAA\x55\x01\x04\x00\x5A\x00\x64\xXX\xXX\x0D\x0A')
resp = ser.read(32) # 读取响应
ser.close()
在ROS中,可使用serial包或rosserial,将串口封装为ROS话题和服务,实现控制指令的透明传输。
3.5 实战案例:无人车底盘通信故障排查
3.5.1 问题背景与现象
某无人车项目采用Jetson Nano作为主控,通过USB转TTL模块连接STM32底盘控制板。上电后,ROS节点发送速度指令,但底盘无任何响应。使用串口调试助手监听STM32 TX引脚,发现能收到数据但均为乱码;监听Jetson TX引脚,波形正常。
3.5.2 分析过程与定位
按照以下步骤排查:
- 检查波特率一致性:使用示波器测量Jetson TX引脚的波特率,发现实际波特率为115200,但STM32固件配置为9600。这是最直接的原因。
- 检查电平标准:USB转TTL模块输出3.3V,但STM32板载UART引脚为5V容忍,虽然能工作但存在风险。更换为5V电平模块后信号更稳定。
- 检查帧格式:即使波特率修正后,仍偶发乱码。进一步检查发现,Jetson侧代码中
termios配置未设置CLOCAL标志,导致串口在无载波检测时自动断开。 - 检查接地:USB转TTL模块与STM32板卡未共地,导致电平参考点不一致,产生漂移。重新连接GND后问题彻底解决。
3.5.3 解决措施与验证
修正后的配置为:波特率115200、8N1、无流控、共地连接。在Jetson上使用stty验证配置,并用echo发送0x55测试帧,STM32返回正确应答。随后在ROS节点中增加CRC校验和帧超时重传机制,连续运行2小时,丢帧率从5%降至0.01%以下。
3.6 常见误区与总结
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 依赖硬件奇偶校验 | 无法检测偶数位错误,帧同步丢失 | 应用层使用CRC16,硬件层设为None |
| 忽略共地 | 电平漂移导致随机乱码 | 所有串口设备必须共地 |
| read()未处理部分读取 | 数据截断、协议解析错位 | 循环读取直到满足帧长度 |
| 未设置超时 | 程序阻塞,控制周期超时 | 使用select()或设置VTIME |
| 帧头过短(如单字节) | 数据区内容可能误判为帧头 | 使用2字节以上帧头,并加入帧尾 |