串口通信优化:调整波特率、使用DMA传输、减少串口读写次数、配置流控
串口通信,说白了就是地面站和飞控之间的「对话通道」。通道宽不宽、吵不吵、会不会丢话,直接决定了你看到的数传数据是实时流畅还是卡成PPT。这一章我把自己这些年踩过的坑、试过的方案都摊开来讲。
1. 调整波特率:别盲目拉高
波特率就是串口每秒能传多少比特。很多人一上来就设个921600,觉得越快越好。我在项目中遇到过这种情况:飞控端确实支持高波特率,但地面站这边用的是USB转串口模块,线缆长了点,结果数据全是乱码。
为什么会这样?因为波特率越高,对时钟精度和信号质量的要求就越苛刻。你想想看,同样的1%误差,在9600bps下只偏96Hz,到了921600就偏9.2kHz——信号直接歪到隔壁码元上去了。
我的建议:
- 短距离(<1米):可以用115200或460800,前提是两端晶振精度都在±1%以内
- 长距离或无线数传:老老实实57600或38400,稳定比速度重要
- 调试阶段:先用115200跑通,再逐步往上试,每次升一档就跑半小时压力测试
| 波特率 | 理论速率(KB/s) | 适用场景 | 注意事项 |
|---|---|---|---|
| 9600 | ~0.96 | 长距离无线数传 | 稳定但慢,适合低速率遥测 |
| 57600 | ~5.76 | 常规数传链路 | 平衡之选,大多数场景够用 |
| 115200 | ~11.52 | USB直连或短距离 | QGC默认值,兼容性好 |
| 460800 | ~46.08 | 高速数据记录 | 需要高质量线缆和屏蔽 |
| 921600 | ~92.16 | 极短距离调试 | 慎用,容易丢包 |
2. 使用DMA传输:让CPU喘口气
传统串口收发是这样的:每来一个字节,CPU就中断一次,把数据从寄存器搬到内存。115200bps下每秒约11520个字节,也就是11520次中断。CPU大部分时间都在「搬砖」,哪有空处理MAVLink解析、UI刷新?
DMA(直接存储器访问)就是给CPU配了个「搬运工」。你告诉DMA控制器:从串口接收寄存器搬200个字节到内存缓冲区,搬完了通知我一声。中间CPU完全不用管。
在QGC里怎么配? 以Linux平台为例,打开串口时加上DMA标志:
// 打开串口并启用DMA
int fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("open");
return -1;
}
// 配置DMA接收(Linux内核4.0+)
struct serial_rs485 rs485conf;
rs485conf.flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND;
ioctl(fd, TIOCSRS485, &rs485conf);
// 设置DMA缓冲区大小(建议4KB以上)
struct serial_struct serinfo;
ioctl(fd, TIOCGSERIAL, &serinfo);
serinfo.xmit_fifo_size = 4096;
serinfo.rx_fifo_size = 4096;
ioctl(fd, TIOCSSERIAL, &serinfo);
嗯,这里要注意:DMA不是所有平台都支持。Windows下需要驱动支持,macOS限制更多。我个人的习惯是:Linux下必开DMA,Windows下用重叠I/O模拟。
3. 减少串口读写次数:攒够了再处理
很多新手写串口程序,喜欢来一个字节读一个字节。这在115200bps下就是每秒11520次read()系统调用——每次都要陷入内核态,开销巨大。
正确的做法是:批量读写。QGC里MAVLink消息最短8字节,最长263字节。你完全可以攒够一帧再处理。
我常用的模式:
- 接收端:开一个4KB的环形缓冲区,DMA或中断把数据往里塞。主循环每10ms检查一次,把缓冲区里完整的MAVLink帧解析出来。
- 发送端:把多条待发消息拼成一个包,一次write()发出去。比如心跳+姿态+GPS三合一,比三条单独发省两次系统调用。
// 批量发送示例
uint8_t tx_buffer[512];
int offset = 0;
// 打包三条消息
offset += mavlink_msg_heartbeat_pack(1, 200, &msg);
offset += mavlink_msg_attitude_pack(1, 200, &msg);
offset += mavlink_msg_gps_raw_int_pack(1, 200, &msg);
// 一次写入
int ret = write(fd, tx_buffer, offset);
if (ret != offset) {
// 处理写失败
}
4. 配置流控:别让数据撑爆缓冲区
流控就是「你慢点发,我快吃不消了」的信号机制。分两种:
- 硬件流控(RTS/CTS):用两根额外的信号线,接收方拉低RTS表示「暂停」,发送方看到CTS变低就停。实时性好,但需要硬件支持。
- 软件流控(XON/XOFF):在数据流里插特殊字符,0x13表示暂停,0x11表示继续。不需要额外线,但特殊字符可能和二进制数据冲突。
QGC里怎么配?以串口打开代码为例:
struct termios tty;
tcgetattr(fd, &tty);
// 启用硬件流控
tty.c_cflag |= CRTSCTS;
// 或者启用软件流控(二选一)
// tty.c_iflag |= IXON | IXOFF | IXANY;
tcsetattr(fd, TCSANOW, &tty);
我的建议:
- 有线连接:优先用硬件流控,稳定可靠
- 无线数传:别用流控。无线链路本身就有重传机制,再加一层流控反而容易死锁。我曾经在3DR数传上开硬件流控,结果RTS/CTS信号在无线链路里延迟太大,两边互相等对方先发,直接卡死——那次排查了我整整两天。
- 二进制协议(MAVLink就是):别用软件流控。0x13和0x11在MAVLink里可能是正常数据,一旦被解释成流控字符,整个数据流就乱了。
5. 整体优化流程
下面这张图是我自己总结的串口优化决策流程,每次调新项目都照着走一遍:
按这个流程走一遍,大部分串口性能问题都能解决。如果还卡,那问题可能不在串口本身——比如飞控端MAVLink消息发送频率太高,或者地面站UI渲染线程阻塞了。那些是后面章节要聊的话题。