串口协议 Lprotocol¶
本节介绍¶
Lcode/Lprotocol.py 负责上位机与飞控之间的串口双向通信,使用匿名数传 AA 帧协议。这是整个系统的数据动脉——T265 速度、飞控姿态、控制指令全部经过这里。
学习目标¶
- 知道
Serial_fc类的职责和三线程架构 - 理解 AA 帧协议的帧格式(帧头/帧ID/数据/校验/帧尾)
- 能说出每个发送/接收线程的频率和内容
- 知道 DRY_RUN 模式下串口如何降级
三线程架构¶
Lprotocol.py (Serial_fc)
├── 监听线程 飞控→Pi 50Hz(姿态/激光高度/解锁状态/调试帧)
├── T265发送 Pi→飞控 100Hz(速度+位置)
└── 指令发送 Pi→飞控 50Hz(速度指令)
| 线程 | 方向 | 频率 | 帧ID | 内容 |
|---|---|---|---|---|
监听 (listen_fc) |
飞控→Pi | 50Hz | 0x01 |
姿态角、解锁状态、激光高度、光流数据 |
| 监听 (调试帧) | 飞控→Pi | 2Hz | 0x02 |
飞控速度估计、光流IMU、电机PWM掩码 |
T265发送 (_send_t265_loop) |
Pi→飞控 | 100Hz | 0x01/0x03 |
T265速度(cm/s) + 位置(cm) |
指令发送 (_send_command_loop) |
Pi→飞控 | 50Hz | 0x02 |
任务状态 + 速度指令 |
AA 帧协议格式¶
飞控下行帧:
AA | frame_id | len | DATA[len] | checksum | FF
校验: checksum = (sum(frame_id + len + DATA) ) & 0xFF
上行帧 (Pi→飞控):
AA 01: T265速度帧 — vx_cm(2) + vy_cm(2) + yaw_x100(2) = 6字节数据
AA 02: 指令帧 — task_sta(1) + com_x/y/z/yaw(4) + lock_cmd(1) + padding(2) = 8字节数据
AA 03: T265位置帧 — x_cm(4) + y_cm(4) + z_cm(4) = 12字节数据
校验: 上行帧使用 XOR 校验 (frame_id 到最后1字节数据逐字节异或)
校验算法差异
下行帧(飞控→Pi)使用累加和校验:checksum = (sum(header) + sum(data)) & 0xFF。
上行帧(Pi→飞控)使用XOR校验:checksum = frame_id ^ data[0] ^ ... ^ data[n-1]。
两者算法不同,解析时需注意区分。
API 参考¶
class Serial_fc
飞控串口双向通信,三线程收发
构造方法¶
Serial_fc(port: str, baudrate: int)
打开串口并初始化线程控制标志。设置 read timeout=1.0s 和 write_timeout=1.0s 防止阻塞卡死。
| 参数 | 类型 | 说明 |
|---|---|---|
port |
str |
串口路径,默认 /dev/ttyS6 |
baudrate |
int |
波特率,默认 460800 |
生命周期方法¶
监听线程
.listen_start(rxbuffer: List[int]) → None
启动飞控监听线程(daemon),清空输入缓冲区后开始接收下行帧。数据写入传入的 rxbuffer 列表。
| 参数 | 类型 | 说明 |
|---|---|---|
rxbuffer |
List[int] |
共享缓冲区,监听线程将解析后的字段追加到此列表 |
.listen_end(timeout: float = 1.5) → None
停止监听线程。先设标志位,再调用 `cancel_read()` 唤醒阻塞的 read(),最后 join 线程。必须在线程退出后才能 close fd。
发送线程
.send_start(comlist: List[int] = None, t265_obj = None, vel_freq: int = 100, cmd_freq: int = 50) → None
启动 T265 发送线程和指令发送线程(均为 daemon)。两个线程可独立启动。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
comlist |
List[int] |
None |
指令帧共享列表,Mission 状态机实时写入速度指令 |
t265_obj |
t265_class |
None |
T265 对象,用于读取速度和位置发送给飞控 |
vel_freq |
int |
100 |
T265 速度帧发送频率 (Hz) |
cmd_freq |
int |
50 |
指令帧发送频率 (Hz) |
.send_end(timeout: float = 1.5) → None
停止两个发送线程,join 等待退出。
资源清理
.close() → None
兜底清理:先 send_end() 再 listen_end(),最后关闭串口 fd。重复调用是幂等的。
实例属性¶
| 属性 | 类型 | 说明 |
|---|---|---|
ser |
serial.Serial |
pyserial 串口对象 |
debug_data |
dict |
调试帧(0x02)最新数据:fc_vel/of_acc/of_gyr/motor_pwm_mask/motor_pwm_mask_t/land_timeout_gaveup |
_last_laser_height_cm |
float |
最后有效的激光高度(米),过滤异常值 |
使用示例¶
from Lcode.Lprotocol import Serial_fc
from Lcode.global_variable import lock
rxbuffer = []
comlist = [0xAA, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF] # 指令帧模板
fc = Serial_fc(port="/dev/ttyS6", baudrate=460800)
fc.listen_start(rxbuffer)
fc.send_start(comlist=comlist, t265_obj=t265, vel_freq=100, cmd_freq=50)
# ... 飞行过程中 Mission_GPT 实时更新 comlist 中的速度指令 ...
fc.send_end()
fc.listen_end()
fc.close()
环境变量¶
| 变量 | 默认值 | 说明 |
|---|---|---|
DRONE_FC_PORT |
/dev/ttyS6 |
飞控串口路径 |
DRONE_DRY_RUN |
0 |
1 = 模拟模式,不打开真实串口 |
常见问题¶
为什么 write_timeout 必须设置?
pyserial 默认无 write_timeout。如果飞控端串口线断开或缓冲区写不出去,ser.write() 会无限阻塞且不抛异常,两个发送线程会静默卡死,整个飞控失控。设置 write_timeout=1.0 后超时会抛 SerialTimeoutException,线程能正常退出并记录错误。
rxbuffer 为什么用 List 而不是单独变量?
监听线程和 Mission 主循环共享 rxbuffer。使用 List 配合 with lock: 临界区,可以原子性地 clear + append 全部字段,避免 Mission 读到"新 yaw + 旧时间戳"的撕裂快照。
调试帧 (0x02) 的 motor_pwm_mask 有什么用?
用于诊断 land() 假阳性锁桨问题(#22)。unlock_sta 可能读到 0 但电机实际未停转,motor_pwm_mask 的 bit0~3 对应 m1~m4 的 PWM 是否非零,是独立于飞控状态字段的硬件级佐证。
← 状态机流程 | PID控制器 Lpid →