2、常见通信链路分析:串口通信、UDP通信、TCP通信,以及各自的故障特征
做MAVLink开发,说白了就是在跟三种通信方式打交道:串口、UDP、TCP。我见过不少新手,一上来就扎进协议细节,结果链路出了问题反而无从下手。其实你想想看,链路都不通,解析协议有什么用?
这一章,我们就来拆解这三种最常见的通信链路。我会结合自己踩过的坑,把它们的原理、适用场景、以及那些让人头疼的故障特征,一次性说清楚。
2.1 串口通信:最基础,也最容易出问题
串口,也就是UART,是飞控和地面站之间最传统的连接方式。PX4和ArduPilot的TELEM口,本质上就是串口。它的原理很简单:一根TX发,一根RX收,再加上一根GND共地。
但简单不代表可靠。我在项目中遇到过最诡异的一次:飞控和机载电脑之间用串口通信,数据总是丢字节。查了两天,最后发现是TX和RX两根线靠得太近,产生了串扰。嗯,这里要注意,串口线最好用双绞线或者屏蔽线,尤其是长距离传输时。
串口通信的故障特征
| 故障现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全收不到数据 | 波特率不匹配、TX/RX接反、GND未共地 | 检查接线,用示波器看波形 |
| 数据乱码 | 波特率偏差、电平不匹配(3.3V vs 5V) | 校准波特率,检查电平转换 |
| 间歇性丢包 | 缓冲区溢出、中断优先级冲突 | 增大缓冲区,检查中断嵌套 |
| 一帧数据被拆成多段 | 接收端处理不及时、DMA配置不当 | 使用DMA+空闲中断,或增加超时重拼机制 |
串口通信的典型代码框架
下面是一个基于Linux的串口初始化示例,我习惯用这种方式打开串口:
// Linux 串口初始化示例
int uart_fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
if (uart_fd < 0) {
perror("打开串口失败");
return -1;
}
struct termios options;
tcgetattr(uart_fd, &options);
// 设置波特率 115200
cfsetispeed(&options, B115200);
cfsetospeed(&options, B115200);
// 8N1 模式
options.c_cflag &= ~PARENB; // 无校验
options.c_cflag &= ~CSTOPB; // 1位停止位
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8; // 8位数据位
// 启用接收,忽略调制解调器控制线
options.c_cflag |= CREAD | CLOCAL;
// 原始输入输出
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
options.c_oflag &= ~OPOST;
tcsetattr(uart_fd, TCSANOW, &options);
2.2 UDP通信:简单高效,但不可靠
UDP在MAVLink里用得很多,尤其是机载计算机和地面站之间的通信。它的优势是速度快、开销小,没有TCP那种三次握手的延迟。但代价是什么?不保证送达,不保证顺序。
你想想看,如果无人机在飞行中,UDP丢了一个姿态数据包,飞控会怎样?其实问题不大,因为下一个包马上就到了。但如果你用UDP传参数写入指令,丢包就麻烦了——参数可能没写进去,而你浑然不知。
UDP通信的故障特征
| 故障现象 | 可能原因 | 排查方向 |
|---|---|---|
| 地面站收不到数据 | 端口未开放、防火墙拦截、IP地址错误 | 用netstat -uln 检查端口监听 |
| 数据时断时续 | 网络拥塞、Wi-Fi信号不稳定 | 用ping测试网络质量,观察丢包率 |
| 数据顺序错乱 | UDP本身不保证顺序,多路径路由 | 在应用层加序列号校验 |
| 单播正常,广播收不到 | 广播地址配置错误、路由器隔离广播域 | 检查子网掩码,确认广播地址 |
UDP通信的典型代码框架
// UDP 发送端示例
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("创建socket失败");
return -1;
}
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(14550); // MAVLink默认UDP端口
inet_pton(AF_INET, "192.168.1.100", &dest_addr.sin_addr);
// 发送MAVLink消息
uint8_t buffer[MAVLINK_MAX_PACKET_LEN];
// ... 填充MAVLink消息 ...
int len = sendto(sock, buffer, msg_len, 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr));
if (len < 0) {
perror("发送失败");
}
2.3 TCP通信:可靠,但有代价
TCP是面向连接的协议。它保证数据不丢、不乱序、不重复。听起来很完美,对吧?但在MAVLink场景里,TCP并不是万能的。
为什么?因为TCP的重传机制。如果网络不好,TCP会反复重传丢失的包。这在传文件时是好事,但在传实时飞控数据时,重传可能导致延迟飙升。我见过一个案例:无人机在信号遮挡严重的区域飞行,TCP连接不断重传,结果地面站的姿态显示滞后了整整2秒。
TCP通信的故障特征
| 故障现象 | 可能原因 | 排查方向 |
|---|---|---|
| 连接失败 | 服务端未启动、端口被占用、防火墙拦截 | 用telnet或nc测试端口连通性 |
| 连接频繁断开 | 心跳超时、网络闪断、Keep-Alive未配置 | 启用TCP Keep-Alive,缩短探测间隔 |
| 数据延迟大 | Nagle算法导致小包延迟、重传导致拥塞 | 禁用Nagle算法(TCP_NODELAY) |
| 粘包/拆包 | TCP是流协议,没有消息边界 | 在应用层添加帧头帧尾或长度字段 |
TCP通信的典型代码框架
// TCP 服务端示例
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(5760); // MAVLink TCP默认端口
addr.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 5);
// 接受客户端连接
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
// 禁用Nagle算法
int flag = 1;
setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
2.4 三种链路的对比与选型建议
说了这么多,到底该用哪种?我个人的经验是:
- 飞控 <-> 机载电脑: 首选串口。简单、可靠、延迟低。距离短(同一块载板),串口完全够用。
- 机载电脑 <-> 地面站(局域网): 首选UDP。延迟低,开销小。丢包?飞控数据更新频率高,丢几个包无所谓。
- 机载电脑 <-> 地面站(公网/跨网络): 首选TCP。公网环境复杂,UDP可能被运营商限速或直接丢弃。TCP的可靠性在这里是优势。
| 特性 | 串口 | UDP | TCP |
|---|---|---|---|
| 可靠性 | 中(依赖物理层) | 低(无确认机制) | 高(确认重传) |
| 实时性 | 高 | 高 | 中(重传导致延迟) |
| 传输距离 | 短(通常<5米) | 中(局域网) | 远(公网) |
| 实现复杂度 | 低 | 低 | 中 |
| MAVLink典型端口 | /dev/ttyS0, /dev/ttyUSB0 | 14550, 14540 | 5760 |