解读 151 · 半双工 UART 入门
解读对象:概念课 · 以
duck-control/src/bus.rs与microduck-replica/rl-series/scripts/hl2915_bench.py为教材
需要的前置:解读 46(总线架构)、解读 53(协议层)、解读 108(帧层 Rust 实现)
15 只舵机加一块 IMU 板,共用一根数据线聊天,还不许乱——这靠的是半双工 UART。代码里两个文件正好是它的"物理层"与"礼仪层"。
1. UART 与 8N1:把字节搬上线
UART 是最朴素的串口:一个字节一位一位地搬。怎么算"一位完了"?双方先约定格式——本项目全程用 1Mbps、8N1(bench 脚本文件头:"TTL 半双工,1 Mbps,8N1,低位在前")。8N1 翻译过来:每个字节前面加 1 个起始位、后面加 1 个停止位、中间 8 个数据位、不设校验位——10 位打包一个字节。1Mbps 即每微秒 1 位,所以每字节 10µs:一条 PING 指令 6 字节,应答也是 6 字节,线上往返约 120µs——你可以自己从 bench 的 packet() 数出来。
打开方式就在 bench 的 Bus.__init__:serial.Serial(port, baud, bytesize=8, parity="N", stopbits=1),与 8N1 逐字对应。要改就得两头一起改,这是串口通信"先约定后说话"的第一课。
2. 半双工:单车道,靠分时通行
全双工是双车道,收发各一根线;半双工是一根线,同一时刻只能一个方向——像对讲机,说完整句喊一声"over",再换对方。舵机总线就是这么一根 DATA 线,所以每个"回合"都是严格的两步,bench 的 transact() 写得明明白白:write() 发出指令 → flush() → 立刻转成收包等应答(等 0.1s,超时记一次失败)。说—听—说—听,谁也不能抢。
为什么一根线既能收又能发?三件事合起来:
- 分时:主机说完就闭嘴,从机才开口;广播指令(0xFE)约定不应答,bench 里
rx_params=None跳过收包,连"over"都省了。 - 硬件方向开关:MCU 的 TX 不能直接怼在总线上,中间隔一个缓冲器芯片,发时接通、收时断开;代价是发送时会听到自己的回显,固件按长度丢弃即可(接线表"使能极性说明"一节)。
- 排队秩序:bus.rs 顶部注释:"一次 sync_read 覆盖 IMU 板和全部 15 只舵机……IMU 排在第一个,让它赶在舵机轰炸前应答"——16 台设备在一根线上,连次序都是设计出来的。
3. 动手验证:从一条 PING 到十分钟拷机
hl2915_bench.py 1 发一条 PING,应答即通;hl2915_bench.py 4 --minutes 10 是终极验证:50Hz 连续收发 10 分钟统计丢包,PASS 线是丢包 <0.1%。bus.rs 那侧同样把"不听话怎么办"写死:主机超时 30ms(READ_TIMEOUT);连续 25 次读到一模一样的旧数据(半秒,50Hz)就要警告——线没断,但数据死了,这种"半故障"只有靠数出来的纪律才能抓住。
你带走的收获
- 8N1=10 位打包一个字节:1Mbps 下每字节 10µs,线上耗时可手算。
- 半双工=单车道分时:说—听严格交替,广播不许应答,连 16 台设备的应答次序都是排好的。
- 一根线收发兼得,靠"分时+方向缓冲器+回显处理"三件套,不是魔法。
- 可靠性是量出来的:0.1s 指令超时、30ms 主机超时、丢包 <0.1% 的 PASS 线。