4. 串口通信基础
各位同学,今天我们来聊聊串口通信。说实话,做无人机地面站开发,串口是你绕不开的第一道坎。我最早接触无人机遥测时,第一件事就是跟串口打交道——飞控把数据通过串口发出来,地面站通过串口收进来,就这么简单,也这么关键。
你想想看,一架无人机在天上飞,每秒都在往外吐数据:GPS坐标、姿态角、电池电压、空速……这些数据怎么从飞控的芯片里跑到你的电脑屏幕上?串口就是那条路。搞懂了串口,你就拿到了读懂无人机“黑匣子”的钥匙。
核心认知:串口通信是无人机遥测链路的最底层物理通道。无论上层用MavLink还是自定义协议,最终都要通过串口这根“水管”来传输。水管堵了,上层再花哨也没用。
4.1 串口通信原理:说白了就是两根线的事
串口通信,全称是串行通信接口。为什么叫“串行”?因为它一次只传一个bit,像一串珠子一样挨个儿发过去。与之对应的是并行通信,一次传8个bit或16个bit。串口虽然慢一点,但胜在简单、抗干扰、传输距离远——无人机上用的就是它。
串口通信需要三根线:TX(发送)、RX(接收)、GND(地线)。注意,两个设备连接时要交叉:A设备的TX接B设备的RX,A设备的RX接B设备的TX。我见过不少新手把TX接TX、RX接RX,结果死活收不到数据——嗯,这个坑我当年也踩过。
串口通信的流程大致是这样的:
- 空闲状态:TX线保持高电平(逻辑1)。
- 起始位:TX线拉低(逻辑0),告诉对方“我要发数据了”。
- 数据位:从低位到高位,依次发送5~9个bit的数据。
- 校验位(可选):用于检错,奇校验或偶校验。
- 停止位:TX线拉高(逻辑1),表示这一帧结束。
下面这张图把串口一帧数据的结构画清楚了,你一看就明白:
我的经验:调试串口通信时,先用示波器看TX脚的波形。如果能看到下降沿和跳变的方波,说明硬件层面没问题。如果波形是一条直线,先检查接线和波特率配置。我曾经花了一下午查代码,最后发现是TX和RX焊反了——这种低级错误,谁都会犯。
4.2 波特率与数据位:通信双方的“暗号”
串口通信有个前提:收发双方必须约定好“暗号”。这个暗号包括三样东西:波特率、数据位、停止位。任何一个对不上,收到的就是乱码。
4.2.1 波特率:每秒传多少bit
波特率(Baud Rate)表示每秒传输的符号数。在串口通信中,一个符号就是一个bit,所以波特率就等于比特率。常见的波特率有:9600、19200、38400、57600、115200。
无人机遥测最常用的是115200和57600。为什么?因为遥测数据量大,波特率低了会丢包。我做过一个项目,飞控默认波特率是9600,结果GPS数据刷新率只有2Hz,飞起来延迟感很明显。后来改成115200,刷新率直接拉到10Hz,手感完全不一样。
但波特率不是越高越好。波特率越高,对线路质量要求也越高。长距离传输时,高频信号衰减严重,反而容易出错。一般原则是:
- 短距离(1米以内):115200或更高
- 中等距离(1~10米):38400~57600
- 长距离(10米以上):9600~19200
4.2.2 数据位与停止位
数据位通常设为8位,因为一个字节就是8个bit。有些老设备用7位数据位(比如ASCII传输),但无人机遥测数据基本都是二进制,8位是标配。
停止位一般设1位。设2位停止位会增加传输开销,只在噪声严重的环境中使用。我做了这么多年无人机,99%的情况都是8数据位 + 1停止位 + 无校验。
下面这个表格把常见配置列出来了,方便你对照:
| 参数 | 常见值 | 无人机遥测推荐 | 说明 |
|---|---|---|---|
| 波特率 | 9600 / 57600 / 115200 | 115200 | 平衡速度与稳定性 |
| 数据位 | 7 / 8 | 8 | 8位可传输完整字节 |
| 停止位 | 1 / 2 | 1 | 1位足够,2位浪费带宽 |
| 校验位 | 无 / 奇校验 / 偶校验 | 无 | 上层协议自带校验,串口层可省 |
注意:飞控和地面站的串口参数必须完全一致。我曾经遇到过飞控设的是8N1(8数据位、无校验、1停止位),地面站代码里写的是8E1(偶校验),结果数据全对不上。排查了半小时才发现是校验位不匹配——这种问题,串口调试助手一开就能看出来。
4.3 Python串口编程入门:用代码跟飞控“对话”
理论讲完了,咱们来点实际的。Python里操作串口,最常用的库是pyserial。安装很简单:
pip install pyserial
装好之后,打开串口只需要几行代码。我习惯先写一个简单的测试脚本,确认串口能通:
import serial
# 打开串口
ser = serial.Serial(
port='COM3', # Windows下是COM口,Linux下是/dev/ttyUSB0
baudrate=115200, # 波特率
bytesize=8, # 数据位
parity='N', # 校验位:N=无,E=偶校验,O=奇校验
stopbits=1, # 停止位
timeout=1 # 超时时间(秒)
)
# 检查串口是否打开
if ser.is_open:
print(f"串口 {ser.port} 已打开,波特率 {ser.baudrate}")
else:
print("串口打开失败")
串口打开后,读写数据就简单了。读数据用ser.read(),写数据用ser.write()。注意,write()需要传字节数据,不能直接传字符串:
# 发送数据(必须转成bytes)
ser.write(b'\xFE\x01\x02\x03') # 发送16进制数据
ser.write('AT\r\n'.encode()) # 发送ASCII字符串
# 读取数据
data = ser.read(10) # 读取10个字节
print(data.hex()) # 以16进制打印
# 读取一行(以换行符结尾)
line = ser.readline()
print(line.decode().strip())
我的习惯:调试串口时,我从来不用readline(),因为飞控发的不一定是文本数据。我更喜欢用ser.read(ser.in_waiting),一次把缓冲区里的数据全读出来,然后自己解析。这样更灵活,也不容易丢数据。
下面是一个完整的串口收发示例,模拟从飞控读取遥测数据:
import serial
import time
def read_telemetry(port='COM3', baud=115200):
"""从飞控读取遥测数据"""
try:
ser = serial.Serial(port, baud, timeout=1)
print(f"连接飞控:{port} @ {baud} baud")
while True:
# 检查是否有数据可读
if ser.in_waiting > 0:
# 读取所有可用数据
raw_data = ser.read(ser.in_waiting)
print(f"收到 {len(raw_data)} 字节: {raw_data.hex()}")
# 这里后续可以解析MavLink协议
# 现在先简单打印
time.sleep(0.01) # 10ms轮询间隔
except serial.SerialException as e:
print(f"串口错误: {e}")
except KeyboardInterrupt:
print("用户中断")
finally:
if 'ser' in locals() and ser.is_open:
ser.close()
print("串口已关闭")
if __name__ == '__main__':
read_telemetry()
这段代码里有个关键点:timeout=1。如果不设超时,ser.read()会一直阻塞,程序就卡死了。设了超时后,如果1秒内没收到数据,read()会返回空字节串,程序可以继续往下走。
避坑指南:我曾经在项目里忘了设timeout,结果飞控断电后,地面站程序一直卡在ser.read()那里,界面假死。后来加了个超时和异常处理,才解决了这个问题。记住:串口编程一定要考虑异常情况——飞控可能掉线、可能重启、可能发乱码。
4.4 串口调试三板斧
最后分享几个调试串口的实用技巧,都是我踩坑踩出来的:
- 先用串口调试助手验证硬件:别一上来就写代码。先用SSCOM、Putty或Arduino的串口监视器,手动发几个字节,看飞控有没有回应。硬件通了,再写代码。
- 打印16进制原始数据:不要直接打印字符串。飞控发的是二进制数据,直接打印字符串会看到一堆乱码。用
data.hex()打印16进制,一目了然。 - 注意串口缓冲区溢出:如果飞控发数据的速度比你的读取速度快,缓冲区会满,后面的数据就丢了。解决方案是:要么提高读取频率,要么用多线程异步读取。