# 04 串口篇:UART 协议、半包处理、限频与丢帧策略
·
系列第 5 篇。泰山派 ↔ MSPM0G3507 下位机的串口通信坑:
periphery库限制、协议设计反复、半包丢帧、发送限频、丢帧策略。共 15+ 个真实事件,集中在 ball_balancer 阶段(07-31 ~ 08-01)。
一、periphery 库与权限
1. periphery.Serial 不支持 timeout 参数(07-19、07-21 重复)
- 现象:
TypeError: __init__() got an unexpected keyword argument 'timeout'。 - 根因:python-periphery 的 Serial 构造函数签名是
Serial(devpath, baudrate, databits=8, parity='none', stopbits=1, ...),没有 timeout 参数。 - 解决:只传
(devpath, baudrate),走默认 8N1;需要超时/流控时改用pyserial的serial.Serial(port, baud, timeout=...);读取侧自己写非阻塞轮询。
2. 打开 /dev/ttyS3 报 Permission denied(07-14)
- 解决:
sudo chmod 666 /dev/ttyS3或把用户加进dialout组;串口排针第 8 脚 TX、第 10 脚 RX;Debug 口占用串口 1,别和调试口冲突。
3. LCD 初始化写在串口 try 块内(07-25)
- 现象:串口打开失败时 LCD 也不初始化,屏幕永远不亮。
- 根因:
lcd = ST7789Streamer()写在try: ser = serial.Serial(...)的 try 块内,被 except 一起跳过。 - 解决:串口与 LCD 分开初始化:
try: ser = serial.Serial(SERIAL_PORT, SERIAL_BAUD, timeout=0.01); print("Serial OK") except Exception as e: print("Serial ERROR:", e); ser = None lcd = ST7789Streamer() # 独立出来,别放 try 里 - 教训:相互独立的硬件初始化不要耦合在一个 try 块里。
二、协议设计(反复摇摆的坑)
4. 协议字节数反复混淆:注释写 5 字节,实际 6 字节(07-31)
- 现象:打包函数注释"5 字节"但实际是 6 字节;在 4/5/6/7 字节之间反复核对。
- 根因:协议从旧打靶项目的 8 字节改造而来,从"只发误差 4 字节"演进到"误差 int16 + 速度 int8",注释没同步。
- 解决:定案 6 字节帧:
接收保持 5 字节任务帧;注释与实现严格对齐。AA 55 | 误差高8位 | 误差低8位 (0.05cm 精度, 小端) | 速度 (int8, 0.1cm/s) | XOR 校验
5. 速度打包时才限幅,导致跳变 + EMA 污染(08-01)
- 现象:速度算出 500cm/s,打包时被 int8 硬截成 127;
_vel_smooth被巨值污染几十帧。 - 根因:限幅发生在"打包"处而非"计算"处。
- 解决:计算后立即限幅
_vel_smooth = max(-12.7, min(12.7, _vel_smooth))(±12.7 cm/s = int8 上限),EMA 在限幅后的值上累积;protocol.py 保留兜底限幅。
三、收发可靠性(半包、乱序、阻塞)
6. 任务帧偶尔收不到:单字节逐帧读,半包直接丢(08-01,重复)
- 现象:
read_task偶尔收不到下位机任务帧。 - 根因:旧逻辑每次
read(1)逐字节找帧头,任务帧分几次到达时,读到 AA 后read(1)等 55 超时 → 整帧丢弃。 - 解决:缓冲累积 + 状态机解析:
# 每次读缓冲区全部字节累积到 _rx_buf,逐字节找 AA 帧头凑 5 字节校验 # 跨调用保留半包;校验失败继续找下一个 AA;无完整帧时只保留最后一个 AA def read_task(self): self._rx_buf += self.ser.read(64, 0) # 非阻塞读 while len(self._rx_buf) >= 5: if self._rx_buf[0] != 0xAA: self._rx_buf.pop(0); continue frame = self._rx_buf[:5] if check(frame): self._rx_buf = self._rx_buf[5:] return parse(frame) self._rx_buf.pop(0) return None
7. _rx_buf 无上限,噪声下只增不减(08-01,代码审查发现)
- 根因:缓冲只保留最后一个 AA,若对端发校验失败的噪声,缓冲无限增长 + O(n) 扫描。
- 解决:加
_RX_BUF_MAX=64上限,超限截断;单测覆盖半包/噪声/超限/完整帧全部通过。
8. read(64, 0.001) 阻塞拖慢主循环:uart 从 1.3ms 涨到 7.9ms(08-01)
- 现象:接上下位机后 uart 耗时翻几倍,帧率下降。
- 根因:
periphery.Serial.read(64, 0.001)无数据时也阻塞等超时;担心read(64,0)的 0 超时语义(可能=无限等)。 - 解决:板子实测
read(64,0)无数据时耗时 0.1ms(非阻塞 ✅),采用read(64,0)+ 缓冲状态机。
9. 重复帧把速度队列拉低到 0(08-01)
- 现象:处理循环比采集快时,同一帧被处理多次,往速度队列塞相同位置 → 速度被拉到 0。
- 根因:重复帧检查在速度队列 append 之后。
- 解决:重复帧检查提前到检测之前,重复帧直接跳过整轮(UART 只在真新帧发)。
四、发送策略(下位机友好)
10. 发送无限频狂发 / 100Hz 限频过快(07-31,三次调整)
- 演进:100Hz 限频 → 无限 → 最终 60fps 限频(~16.6ms),与摄像头帧率匹配。
- 解决:发送误差 60fps 限频(用
thread_loop._last_send_t记录上次发送时间,>=0.016s才发)。
11. 丢帧时发 (0,0) 导致下位机误判(07-16 起贯穿全项目,08-01 定案)
- 现象:丢帧发
offset_cm=0.0,任务 0/1 下error = 0 - 5 = -5→ 下位机猛动;任务 02 下error=0→ 误以为稳住了。 - 解决(逐步升级):
- 丢帧发上一次有效误差
_last_error_cm; - 最终:丢帧不发(
_last_found门控,断连/重复帧也不发,下位机靠超时感知); - 任务切换(±5cm)用阶梯逼近(
TASK1_RAMP_STEP=0.18cm/帧,从 +5 平滑到 -5),避免 PID 猛打舵机; - 速度隔 3 帧差分(4-1/5-2/6-3)。
- 丢帧发上一次有效误差
- 教训:“无有效数据” ≠ “误差为 0”,这是控制闭环里最伤下位机的坑。
12. error_cm 用旧目标计算,发送值与显示不一致(08-01)
- 根因:
error_cm在ramp_step()/lock_target()更新目标之前用旧 tg 计算,差 ≤0.18cm/帧。 - 解决:
error_cm = offset - tg移到 ramp/lock 之后(先更新目标再算误差)。
13. 串口位置放错:在重复帧 continue 之后,任务信号被跳过(08-01)
- 现象:下位机发的任务帧偶尔没收到。
- 根因(三选一的排查):重复帧 continue 把串口读跳过了。
- 解决:串口移到 processor 第 0.5 步(循环前部、重复帧检查之前),每轮必读任务信号;
error_cm = 0.0循环外初始化供重复帧复用。
小结:串口篇避坑清单
- periphery.Serial 无 timeout 参数;需要超时换 pyserial。
- 半包用缓冲状态机,别逐字节
read(1);缓冲要设上限。 read(64,0)实测非阻塞(0.1ms),放心用。- 发送限频 60fps 与摄像头同步;丢帧不发而不是发 0/上次值。
- 先更新目标再算误差;速度在计算处限幅。
- 串口读放在主循环最前面(重复帧 continue 之前)。
- 协议注释与实现必须一致,改协议时同步改下位机。
更多推荐




所有评论(0)