系列第 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;需要超时/流控时改用 pyserialserial.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 字节帧
    AA 55 | 误差高8位 | 误差低8位 (0.05cm 精度, 小端) | 速度 (int8, 0.1cm/s) | XOR 校验
    
    接收保持 5 字节任务帧;注释与实现严格对齐

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 → 误以为稳住了。
  • 解决(逐步升级):
    1. 丢帧发上一次有效误差 _last_error_cm
    2. 最终:丢帧不发_last_found 门控,断连/重复帧也不发,下位机靠超时感知);
    3. 任务切换(±5cm)用阶梯逼近TASK1_RAMP_STEP=0.18cm/帧,从 +5 平滑到 -5),避免 PID 猛打舵机;
    4. 速度隔 3 帧差分(4-1/5-2/6-3)。
  • 教训:“无有效数据” ≠ “误差为 0”,这是控制闭环里最伤下位机的坑。

12. error_cm 用旧目标计算,发送值与显示不一致(08-01)

  • 根因error_cmramp_step()/lock_target() 更新目标之前用旧 tg 计算,差 ≤0.18cm/帧。
  • 解决error_cm = offset - tg 移到 ramp/lock 之后(先更新目标再算误差)。

13. 串口位置放错:在重复帧 continue 之后,任务信号被跳过(08-01)

  • 现象:下位机发的任务帧偶尔没收到。
  • 根因(三选一的排查):重复帧 continue 把串口读跳过了。
  • 解决:串口移到 processor 第 0.5 步(循环前部、重复帧检查之前),每轮必读任务信号;error_cm = 0.0 循环外初始化供重复帧复用。

小结:串口篇避坑清单

  1. periphery.Serial 无 timeout 参数;需要超时换 pyserial。
  2. 半包用缓冲状态机,别逐字节 read(1);缓冲要设上限。
  3. read(64,0) 实测非阻塞(0.1ms),放心用。
  4. 发送限频 60fps 与摄像头同步;丢帧不发而不是发 0/上次值。
  5. 先更新目标再算误差;速度在计算处限幅。
  6. 串口读放在主循环最前面(重复帧 continue 之前)。
  7. 协议注释与实现必须一致,改协议时同步改下位机。

下一篇:05_工具链篇:Claude Code/DeepSeek/MCP、SSH/SCP、Git、VSCode 与推流

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐