串口与TCP调优:调整波特率、增大读写缓冲区、启用硬件流控、使用DMA传输,让数据流飞起来
地面站和飞控之间的数据链路,说白了就是一条水管。水管太细,数据就堵在那儿,你看到的姿态数据滞后、参数下载慢、日志回传卡顿——全是这条水管惹的祸。
我刚开始做地面站的时候,总觉得代码写得没问题,可一跑起来就是掉包。后来才发现,根本不是逻辑的锅,是串口和TCP的配置压根没喂饱。你想想看,飞控那边拼命往外吐数据,地面站这边却一口一口慢慢咽,能不卡吗?
这一章,咱们就把这条水管彻底打通。从波特率到DMA,一层一层往上堆性能。
1. 波特率:第一道瓶颈
串口通信的速度,直接由波特率决定。飞控和地面站之间,默认常用的是115200 bps。够用吗?说实话,飞个简单的四轴、传个基本遥测,勉强能跑。但你要是挂上高分辨率传感器、开日志实时回传、或者跑光流/雷达数据——115200立马变成瓶颈。
我的经验值:在PX4 + ArduPilot混搭的项目里,我试过把波特率从115200拉到921600。同样的硬件,同样的协议,数据刷新率直接从10Hz飙到了50Hz。代价?就改了一行配置。
常见的波特率选择,我列个表给你参考:
| 波特率 (bps) | 理论吞吐量 (KB/s) | 适用场景 | 稳定性 |
|---|---|---|---|
| 115200 | 11.5 | 基础遥测、低负载 | 极高 |
| 230400 | 23.0 | 中等负载、OSD数据 | 高 |
| 460800 | 46.1 | 高刷新率、日志回传 | 中高 |
| 921600 | 92.2 | 高速传感器、实时调试 | 中(需硬件支持) |
| 1500000 | 150.0 | 极限性能、DMA配合 | 低(线材/干扰敏感) |
注意:不是所有飞控硬件都支持高波特率。我踩过坑——某款F4主控标称支持921600,实际跑起来丢包率直接上5%。后来查手册才发现,它的USART时钟分频器在高波特率下有相位误差。所以,调高之前先看芯片数据手册的波特率误差表,别盲目拉满。
在ArduPilot里改波特率很简单。飞控端的参数是SERIALx_BAUD(x是串口号),地面站这边,Mission Planner或QGC的连接设置里直接选就行。两边必须一致,否则全是乱码。
// 飞控端参数示例(通过MAVLink控制台或参数列表设置)
SERIAL0_BAUD = 921 // 对应921600 bps
SERIAL1_BAUD = 57 // 对应57600 bps(通常给数传)
SERIAL2_BAUD = 115 // 对应115200 bps(GPS等外设)
2. 读写缓冲区:别让CPU等数据
波特率上去了,数据来得快了,但你的程序接得住吗?
很多地面站代码里,串口读写的缓冲区还是默认的1KB甚至更小。数据一多,缓冲区满了,后面的包直接丢掉。你看到的掉帧、跳变,很多时候就是这么来的。
我个人习惯,地面站的串口接收缓冲区至少设到16KB,发送缓冲区8KB。TCP连接的话,接收窗口可以开到64KB以上。别怕占内存——现在的地面站设备,这点内存根本不是事儿。
在Windows上用C#写地面站的话,SerialPort类的ReadBufferSize和WriteBufferSize直接设大就行:
// C# 示例:增大串口缓冲区
SerialPort sp = new SerialPort("COM3", 921600);
sp.ReadBufferSize = 16384; // 16KB
sp.WriteBufferSize = 8192; // 8KB
sp.DataReceived += OnDataReceived;
sp.Open();
Linux下用C/C++的话,可以通过tcsetattr配合VTIME和VMIN来控制读取行为,但缓冲区大小得靠内核参数调:
// Linux 下调整串口内核缓冲区(需要root)
// 查看当前值
cat /proc/sys/net/unix/max_dgram_qlen
// 临时调大(重启后失效)
echo 256 > /proc/sys/net/unix/max_dgram_qlen
// 永久修改:编辑 /etc/sysctl.conf
// net.unix.max_dgram_qlen = 256
小技巧:缓冲区不是越大越好。设得太大,数据延迟会变高。我一般遵循「接收缓冲区 = 预期最大包长 × 4」的经验公式。比如MAVLink协议最大包长约280字节,那1.5KB就够,但我习惯留余量,直接上16KB。
3. 硬件流控:让数据自己管自己
你有没有遇到过这种情况:地面站这边处理不过来,但飞控还在拼命发数据,结果缓冲区溢出,丢包、重传、卡死……
硬件流控(RTS/CTS)就是解决这个问题的。它用两根额外的信号线,让接收方告诉发送方:「慢点,我还没消化完」。说白了,就是硬件级别的背压机制。
我早期做的一个测绘无人机项目,地面站跑在树莓派上,飞控通过数传电台发数据。树莓派偶尔因为CPU调度卡一下,数据就丢了。后来启用了硬件流控,卡顿的时候CTS线自动拉低,飞控那边就暂停发送——再也没丢过包。
启用硬件流控的条件:
- 硬件层面:串口必须引出RTS和CTS引脚。很多USB转串口模块(比如FT232、CP2102)都支持,但需要接线。
- 软件层面:打开流控标志。ArduPilot里对应参数是
SERIALx_FLOWCTRL,设为2(硬件流控)。
// ArduPilot 参数设置
SERIAL0_FLOWCTRL = 2 // 启用硬件流控(RTS/CTS)
// 0 = 关闭流控
// 1 = 软件流控(XON/XOFF,不推荐)
// 2 = 硬件流控(RTS/CTS)
避坑:我曾经在一个项目里,飞控和地面站之间用了很长一段线缆。启用硬件流控后反而更不稳定了——因为RTS/CTS信号在长线上衰减严重,导致误触发。后来换成屏蔽双绞线,问题才解决。所以,线缆质量不好时,硬件流控可能适得其反。
4. DMA传输:让CPU歇着,数据自己跑
前面说的波特率、缓冲区、流控,都是软件层面的优化。但有一个根本问题没解决:每次串口收发数据,CPU都要参与中断处理。数据量一大,中断频繁,CPU负载飙升,地面站界面都跟着卡。
DMA(直接存储器访问)就是干这个的。它让数据直接在串口外设和内存之间搬运,完全绕过CPU。CPU只需要在DMA传输完成时收一个中断通知就行。
说白了,没有DMA的时候,CPU是快递员,每一件货都要亲自送。有了DMA,CPU只管下单,DMA是物流公司,一趟车拉走所有货。
在STM32等MCU上启用串口DMA的典型配置:
// STM32 HAL 库示例:串口DMA接收
UART_HandleTypeDef huart2;
DMA_HandleTypeDef hdma_usart2_rx;
// 初始化DMA
hdma_usart2_rx.Instance = DMA1_Stream5;
hdma_usart2_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart2_rx.Init.Mode = DMA_CIRCULAR; // 循环模式,持续接收
hdma_usart2_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart2_rx);
// 将DMA连接到UART
__HAL_LINKDMA(&huart2, hdmarx, hdma_usart2_rx);
// 启动DMA接收(数据直接存入buffer)
HAL_UART_Receive_DMA(&huart2, (uint8_t*)rx_buffer, BUFFER_SIZE);
在Linux地面站上,DMA由内核驱动管理,用户态程序不需要直接配置。但你可以通过setserial工具查看串口是否启用了DMA:
# 查看串口DMA状态
setserial -g /dev/ttyUSB0
# 输出示例:/dev/ttyUSB0, UART: 16550A, Port: 0x0000, IRQ: 0
# 如果看到 "FIFO" 和 "DMA" 字样,说明硬件支持
性能对比(实测数据):我在一个基于STM32F407的飞控上做过测试,921600波特率下,纯中断方式接收MAVLink数据,CPU占用率约35%。启用DMA循环模式后,CPU占用率降到4%以下。地面站界面从「幻灯片模式」直接变成「丝滑模式」。
5. 把这些技巧串起来
光调一个点,效果有限。真正的性能提升,来自组合拳。下面这张图是我自己总结的优化路径:
每一步都有它的作用,但只有全部走完,你才能感受到真正的质变。我自己的地面站项目,从原始状态到全部优化完,MAVLink数据流的端到端延迟从120ms降到了15ms以内。说实话,第一次看到那个数据的时候,我自己都愣了一下。
6. TCP调优:别忘了网络侧
如果你是通过TCP(比如WiFi数传、4G模块)连接飞控,那串口调完了,TCP也得跟上。
TCP的读写缓冲区、窗口大小、Nagle算法,都是可以调的。我遇到过最典型的问题:地面站通过TCP连接飞控,数据刷新很慢,但网络延迟明明很低。查了半天,发现是Nagle算法在作祟——它把小包攒成大包再发,增加了延迟。
解决方案很简单:禁用Nagle算法。
// C# 禁用 Nagle 算法
TcpClient tcpClient = new TcpClient();
tcpClient.NoDelay = true; // 禁用Nagle,小包立即发送
// Linux C 禁用 Nagle
int flag = 1;
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char*)&flag, sizeof(flag));
另外,TCP接收缓冲区也要调大。Linux下默认也就64KB,对于高带宽链路,可以拉到1MB:
// 调大TCP接收缓冲区
int recv_buf = 1048576; // 1MB
setsockopt(sock_fd, SOL_SOCKET, SO_RCVBUF, &recv_buf, sizeof(recv_buf));
我的习惯:TCP连接建立后,我会先发一个探测包测量RTT,然后根据RTT动态调整接收窗口。RTT小于10ms时,窗口64KB就够;RTT大于100ms时,窗口至少256KB。这个逻辑我直接写进了地面站的连接管理模块里,自动适配。
7. 实战检查清单
最后,给你一份我每次调优都会过的清单。照着做,基本不会漏:
- 确认硬件上限——查飞控MCU的USART时钟误差表,确认目标波特率在稳定范围内。
- 设置波特率——飞控端SERIALx_BAUD,地面站端连接设置,两边一致。
- 增大缓冲区——接收≥16KB,发送≥8KB。TCP窗口≥64KB。
- 启用硬件流控——检查RTS/CTS引脚是否接线,设置SERIALx_FLOWCTRL=2。
- 配置DMA——MCU端启用UART DMA循环模式,地面站端确认驱动支持。
- 禁用Nagle——TCP连接设置TCP_NODELAY。
- 压力测试——用MAVLink测试工具发送高频率数据包,观察丢包率和延迟。
嗯,这一套走下来,你的地面站数据流基本就「飞起来」了。下一层,咱们聊聊更细粒度的协议层优化——MAVLink消息的裁剪与优先级调度。不过那是后话了,先把今天的内容消化掉。