串口通信优化:调整波特率、使用DMA传输、减少串口读写次数、配置流控

串口通信,说白了就是地面站和飞控之间的「对话通道」。通道宽不宽、吵不吵、会不会丢话,直接决定了你看到的数传数据是实时流畅还是卡成PPT。这一章我把自己这些年踩过的坑、试过的方案都摊开来讲。

核心思路:串口优化的本质是「让硬件多干活,让CPU少操心」。调整波特率是拓宽车道,DMA是自动送货,减少读写次数是合并包裹,流控是红绿灯——四招齐下,数据自然跑得顺。

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 极短距离调试 慎用,容易丢包
避坑:我曾经在项目里把波特率从115200直接跳到921600,结果飞控日志里全是CRC校验错误。后来用示波器一看,信号上升沿都变成正弦波了——线缆寄生电容太大。记住:波特率不是越高越好,是「刚好够用」最好。

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模拟。

小技巧:开启DMA后,记得把串口FIFO触发阈值调高。默认一般是1字节触发,改成14字节(16550A UART的FIFO深度)再触发DMA请求,中断次数能减少90%以上。

3. 减少串口读写次数:攒够了再处理

很多新手写串口程序,喜欢来一个字节读一个字节。这在115200bps下就是每秒11520次read()系统调用——每次都要陷入内核态,开销巨大。

正确的做法是:批量读写。QGC里MAVLink消息最短8字节,最长263字节。你完全可以攒够一帧再处理。

我常用的模式:

  1. 接收端:开一个4KB的环形缓冲区,DMA或中断把数据往里塞。主循环每10ms检查一次,把缓冲区里完整的MAVLink帧解析出来。
  2. 发送端:把多条待发消息拼成一个包,一次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) {
    // 处理写失败
}
效果对比:我在一个Pixhawk + 树莓派的项目里做过测试。优化前每秒约12000次read(),CPU占用率35%;优化后(4KB缓冲区+10ms轮询)每秒降到约100次read(),CPU占用率直接掉到8%。多出来的CPU时间,刚好用来跑更复杂的航点规划算法。

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里可能是正常数据,一旦被解释成流控字符,整个数据流就乱了。
重要:如果你用USB转串口(比如CP2102、CH340),很多廉价模块根本没接RTS/CTS引脚。这时候开硬件流控,程序会一直等CTS信号变低,然后永远等不到——串口直接「假死」。用之前先查清楚你的硬件到底支不支持。

5. 整体优化流程

下面这张图是我自己总结的串口优化决策流程,每次调新项目都照着走一遍:

串口通信优化决策流程 开始优化 步骤1:确定波特率 短距离→115200+ | 长距离→57600- 步骤2:启用DMA传输 Linux→ioctl配置 | Win→重叠I/O 步骤3:减少读写次数 4KB环形缓冲区 + 10ms轮询 步骤4:配置流控 有线→硬件流控 | 无线→关闭流控 压力测试验证 优化目标 CPU占用率 < 15% 丢包率 < 0.1% 延迟 < 20ms 常见坑点 USB模块无RTS/CTS 无线链路流控死锁 高波特率信号畸变

按这个流程走一遍,大部分串口性能问题都能解决。如果还卡,那问题可能不在串口本身——比如飞控端MAVLink消息发送频率太高,或者地面站UI渲染线程阻塞了。那些是后面章节要聊的话题。

最后说一句:优化完记得做压力测试。我习惯用QGC自带的MAVLink Inspector,同时开三个虚拟串口对发数据,跑半小时看有没有丢包、延迟抖动。没跑过压力测试的优化,都是自我安慰。