# 下位机 UART 发送协议 v2（带采样时间戳）

本文件描述 STM32 下位机通过 **USART3（ROS 口，波特率 921600）** 周期发送给上位机的
传感器数据帧格式。v2 在原 24 字节帧的基础上，于**帧尾追加**了会话 ID 与两个采样
时间戳字段，用于上位机做 IMU / 轮速 / 雷达的时间对齐。

- 对应源码：[HARDWARE/usartx.c](../HARDWARE/usartx.c)、[HARDWARE/usartx.h](../HARDWARE/usartx.h)
- 时间源实现：[HARDWARE/timer.c](../HARDWARE/timer.c) 的 `TIM7_Init()` / `mcu_time_us()`
- 设计背景与总体原则见 [时间戳.md](时间戳.md)

## 1. 帧总览

| 项目 | 值 |
| ---- | ---- |
| 帧头 `FRAME_HEADER` | `0x7B` |
| 帧尾 `FRAME_TAIL` | `0x7D` |
| 帧长 `SEND_DATA_SIZE` | **44 字节**（v1 为 24 字节） |
| 发送周期 | 200 Hz（`data_task`，USART3 非阻塞 DMA 发送） |
| 字节序 | 多字节字段一律 **大端**（高字节在前） |
| 校验 | 单字节 XOR，覆盖 byte 0..41 |

设计约束：byte 0..21 与 v1 完全一致，新增字段全部放在校验位之前的尾部，
使只读取旧字段的解析器仍然兼容。

## 2. 字节布局

| 偏移 | 长度 | 字段 | 类型 | 说明 |
| ---- | ---- | ---- | ---- | ---- |
| 0 | 1 | Frame_Header | u8 | 帧头 `0x7B` |
| 1 | 1 | Flag_Stop | u8 | 软件失能标志（1=停车） |
| 2 | 2 | X_speed | int16 | X 轴线速度，单位 mm/s，大端 |
| 4 | 2 | Y_speed | int16 | Y 轴线速度，单位 mm/s，大端 |
| 6 | 2 | Z_speed | int16 | Z 轴角速度，单位 mm/s（等效），大端 |
| 8 | 2 | Accel_X | int16 | 加速度计 X（ROS 坐标），大端 |
| 10 | 2 | Accel_Y | int16 | 加速度计 Y（ROS 坐标），大端 |
| 12 | 2 | Accel_Z | int16 | 加速度计 Z（ROS 坐标），大端 |
| 14 | 2 | Gyro_X | int16 | 陀螺仪 X（ROS 坐标），大端 |
| 16 | 2 | Gyro_Y | int16 | 陀螺仪 Y（ROS 坐标），大端 |
| 18 | 2 | Gyro_Z | int16 | 陀螺仪 Z（ROS 坐标），大端 |
| 20 | 2 | Power_Voltage | int16 | 电池电压×1000，大端 |
| **22** | **4** | **session_id** | **uint32** | 本次 MCU 启动会话 ID，大端 |
| **26** | **8** | **speed_sample_time_us** | **uint64** | 编码器采样时刻（μs），大端 |
| **34** | **8** | **imu_sample_time_us** | **uint64** | IMU 采样时刻（μs），大端 |
| 42 | 1 | Checksum | u8 | byte 0..41 的 XOR |
| 43 | 1 | Frame_Tail | u8 | 帧尾 `0x7D` |

加粗行为 v2 相对 v1 的新增字段。

## 3. 时间源

时间戳由自由运行的 **TIM7** 提供，见 [HARDWARE/timer.c](../HARDWARE/timer.c)：

- TIM7 挂在 APB1，定时器时钟 84 MHz，预分频 84 → **1 MHz 计数（1 tick = 1 μs）**。
- 16 位计数器每 65.536 ms 回绕一次，更新中断把高 32 位软件计数 `g_time_high` 加一。
- `mcu_time_us()` 返回 `(g_time_high << 16) | CNT`，即**自上电以来的单调递增微秒数**，
  可用位宽 48 位（约 8.9 年不回绕）。
- TIM2~TIM5 被四路编码器占用、TIM8 被航模遥控输入捕获占用，故选空闲的 TIM7。
- TIM7 更新中断优先级设为 5（数值大于 `configMAX_SYSCALL_INTERRUPT_PRIORITY`
  对应的抢占门限），ISR 内只做自增，不调用任何 FreeRTOS API。

> 注意：`mcu_time_us()` 只增不减，绝不因校时而跳变。上位机负责维护
> `T_host = a·t_mcu + b` 的时钟映射（见 [时间戳.md](时间戳.md) 第 7、8 节）。

## 4. 两个时间戳分别打在哪里

v1 只有"发送时刻"隐含语义，无法表达 IMU（100 Hz）与轮速（200 Hz）是**异步采集**的。
v2 在各自真实采样点打点，写入全局变量，`data_transition()` 打包时快照：

| 时间戳 | 打点位置 | 采样率 |
| ---- | ---- | ---- |
| `speed_sample_time_us` | [balance.c](../BALANCE/balance.c) `Get_Velocity_Form_Encoder()` 读编码器锁存计数器的瞬间 | 200 Hz |
| `imu_sample_time_us` | [MPU6050.c](../HARDWARE/MPU6050/MPU6050.c) `MPU6050_task` I2C 读取前 | 100 Hz |

因此一帧内同时带两个时间戳：上位机应把加速度/角速度对齐到 `imu_sample_time_us`，
把 X/Y/Z 速度对齐到 `speed_sample_time_us`，而不是共用一个发送时刻。

> IMU 打点在 I2C 突发读取之前，是在没有 DRDY（数据就绪）中断条件下能取到的、
> 最接近物理采样的时刻。若日后接入 MPU 的 DRDY 引脚中断，可把打点移到中断里进一步减小抖动。

## 5. session_id

- 定义在 [usartx.c](../HARDWARE/usartx.c)：`g_session_id`。
- 当前实现为**固定值 `0xA5A5A5A5`**。
- 用途：上位机据此判断下位机是否重启过（重启后 `mcu_time_us()` 从 0 重新计时，
  时钟映射 `a, b` 必须重新拟合）。
- 待办：如需真正区分每次上电，应在启动时用熵源（如未初始化 RAM、启动 tick 等）
  生成一次性随机值。当前固定值只能表明"协议版本一致"，不能区分不同启动会话。

## 6. 校验与解析建议

- 校验位 `buffer[42] = XOR(buffer[0..41])`，与 v1 的计算方式相同，只是范围从 22 扩到 42。
- 上位机解析步骤：
  1. 同步帧头 `0x7B`；
  2. 读满 44 字节；
  3. 校验 `buffer[43] == 0x7D` 且 `XOR(buffer[0..41]) == buffer[42]`；
  4. 按上表大端解出各字段。
- 多字节字段务必按大端手动组装，不要直接 `memcpy` 结构体（编译器填充/对齐/端序差异）。

## 7. 其他发送通道

| 通道 | 现状 |
| ---- | ---- |
| USART3（ROS 主口，DMA） | 已用 `SEND_DATA_SIZE`，自动发送完整 44 字节 |
| USART1 / USART5 | 已改用 `SEND_DATA_SIZE`，发送完整 44 字节 |
| CAN | 当前未被调用；`CAN_SEND()` 仍按 8×3=24 字节分包，**不含时间戳字段**。如需经 CAN 传时间戳需另行扩展分包 |

## 8. 上位机适配提醒

USART3 收到的帧已从 24 字节变为 **44 字节**。上位机（ROS 端）解析代码必须同步更新：
帧长、校验范围、以及尾部三个新字段的读取。否则会因帧长不符导致解析错位。

