# 存储访问协议

## 1. 协议定位

存储访问协议用于上位机通过蓝牙透传链路按字节访问从机 MCU 的存储区域。协议为单主单从模型，不带从机地址字段，不属于标准 Modbus RTU。

小程序中该协议与标准 Modbus RTU、Bootloader 升级协议平级：

- 标准 Modbus RTU 使用从机地址、功能码和 Modbus CRC。
- 存储访问协议使用 `CMD + ADDR + LEN + DATA + CRC16-CCITT-FALSE`。
- 复位、启动、停止、控制参考值、CodeInfo 描述符读取使用特殊指令帧。
- CodeInfo 同步只使用特殊指令 `OP=0x05`，不保留旧同步区域入口。

## 2. 字节序与 CRC

除 CRC 算法内部计算外，所有多字节协议控制字段均为大端序。目标内存变量的多字节值不跟随协议控制字段字节序，而是按第一次同步 bootstrap 描述符中的 `MEMORY_ENDIAN` 解释和编辑。

```text
32 位地址 : ADDR_3 ADDR_2 ADDR_1 ADDR_0
16 位长度 : LEN_H LEN_L
CRC 输出  : CRC_H CRC_L
```

CRC 使用 `CRC16-CCITT-FALSE`：

| 参数 | 值 |
|---|---|
| 多项式 | `0x1021` |
| 初值 | `0xFFFF` |
| 输入反转 | 否 |
| 输出反转 | 否 |
| 结果异或 | `0x0000` |
| 输出顺序 | 高字节在前 |

CRC 覆盖除最后两个 CRC 字节外的整帧内容。

## 3. CMD 定义

每帧第 1 字节为 `CMD`。

```text
bit7      ERR       异常标志
bit6      CTL       特殊指令标志位
bit5~bit4 RSV       保留，普通读写保持 0
bit3      RW        读写标志，0=读，1=写
bit2~bit0 MODE      地址模式或存储区域
```

| 位 | 值 | 含义 |
|---|---:|---|
| ERR | 0 | 正常请求或正常响应 |
| ERR | 1 | 异常响应 |
| CTL | 0 | 普通内存读写 |
| CTL | 1 | 与 `bit3=1` 且 `MODE=0x7` 组合为特殊指令，即 `CMD=0x4F` |
| RSV | 0 | 当前版本固定为 0 |
| RW | 0 | 读操作 |
| RW | 1 | 写操作 |

普通内存命令生成规则：

```text
读请求   : CMD = MODE
写请求   : CMD = 0x08 | MODE
异常响应 : CMD_ERR = CMD | 0x80
```

`CMD=0x4F` 固定保留为特殊指令帧，不作为普通存储区域。

## 4. AREA 定义

| AREA | 名称 | 读 | 写 | 用途 |
|---:|---|---|---|---|
| `0x00` | 保留 | 禁止 | 禁止 | 保留 |
| `0x01` | DATA | 支持 | 支持 | 内部直接寻址 RAM 区 |
| `0x02` | IDATA | 支持 | 支持 | 内部间接寻址 RAM 区 |
| `0x03` | XDATA | 支持 | 支持 | 外部数据空间或扩展 RAM |
| `0x04` | CODE | 支持 | 禁止 | 程序存储区 |
| `0x05..0x06` | 保留 | 禁止 | 禁止 | 保留 |
| `0x07` | ADDR32 | 支持 | 支持 | 32 位统一地址模式 |

写 `CODE` 区必须返回写保护异常。

## 5. 普通内存访问帧

### 5.1 读请求

```text
MODE=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L CRC_H CRC_L
MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
```

长度分别为 9 字节或 7 字节。

### 5.2 写请求

```text
MODE=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L DATA... CRC_H CRC_L
MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
```

长度分别为 `9 + LEN` 或 `7 + LEN` 字节。

### 5.3 正常读响应

```text
MODE=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L DATA... CRC_H CRC_L
MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
```

长度分别为 `9 + LEN` 或 `7 + LEN` 字节。

响应必须回显请求中的 `CMD`、`ADDR`、`LEN`。

### 5.4 正常写响应

```text
MODE=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L CRC_H CRC_L
MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
```

长度分别为 9 字节或 7 字节。

响应必须回显请求中的 `CMD`、`ADDR`、`LEN`。

### 5.5 异常响应

```text
CMD_ERR EXCEPTION_CODE CRC_H CRC_L
```

长度固定 4 字节。`CMD_ERR` 为原请求 `CMD | 0x80`。特殊指令异常响应使用 `0xCF`。

## 6. 特殊指令帧

特殊指令使用固定 `CMD=0x4F`。

```text
请求: 4F OP DATA... CRC_H CRC_L
响应: 4F OP STATUS DATA... CRC_H CRC_L
异常: CF EXCEPTION_CODE CRC_H CRC_L
```

`STATUS=0x00` 表示指令执行成功；非 0 表示从机识别了指令，但拒绝执行或执行失败。

| OP | 名称 | 请求 DATA | 成功响应 DATA | 说明 |
|---:|---|---|---|---|
| `0x01` | RESET | 无 | 无 | 复位 |
| `0x02` | START | 无 | 无 | 启动 |
| `0x03` | STOP | 无 | 无 | 停止 |
| `0x04` | SET_CONTROL_REF | `REF_H REF_L` | 无 | 控制参考值，`int16_t`，大端序 |
| `0x05` | READ_CODE_INFO_DESCRIPTOR | 无 | `CODE_ADDR32 CODE_LEN16 ADDR_WIDTH8 MEMORY_ENDIAN16 MAX_PACKET16` | 读取 CodeInfo bootstrap 描述符 |

特殊指令状态码：

| STATUS | 名称 | 说明 |
|---:|---|---|
| `0x00` | OK | 成功 |
| `0x01` | UNSUPPORTED | 不支持的指令 |
| `0x02` | INVALID_PARAM | 参数无效 |
| `0x03` | BUSY | 设备忙 |
| `0x04` | FAILED | 执行失败 |

## 7. 异常码

| 异常码 | 名称 | 说明 |
|---:|---|---|
| `0x01` | ILLEGAL_COMMAND | 非法命令 |
| `0x02` | ILLEGAL_AREA | 非法区域 |
| `0x03` | ILLEGAL_ADDRESS | 非法地址 |
| `0x04` | ILLEGAL_LENGTH | 非法长度 |
| `0x05` | WRITE_PROTECT | 写保护 |
| `0x06` | DEVICE_BUSY | 设备忙 |
| `0x07` | FORMAT_ERROR | 格式错误 |
| `0x08` | ACCESS_DENIED | 访问拒绝 |
| `0x09` | INTERNAL_ERROR | 内部错误 |
| `0x0A` | ALIGNMENT_ERROR | 对齐错误 |
| `0x0B` | RANGE_OVERFLOW | 地址范围溢出 |
| `0x0C` | UNSUPPORTED_OPERATION | 不支持的操作 |

## 8. CodeInfo 同步流程

CodeInfo 同步分两步：

1. 使用特殊指令 `OP=0x05` 读取 bootstrap 描述符。
2. 按 bootstrap 返回的地址长度、目标内存字节序和最大包长读取完整 CodeInfo TLV 信息块。

### 8.1 读取 bootstrap 描述符

请求：

```text
4F 05 CRC_H CRC_L
```

从机成功响应：

```text
4F 05 00 CODE_ADDR_3 CODE_ADDR_2 CODE_ADDR_1 CODE_ADDR_0 CODE_LEN_H CODE_LEN_L ADDR_WIDTH ENDIAN_MARK_H ENDIAN_MARK_L MAX_PACKET_H MAX_PACKET_L CRC_H CRC_L
```

响应 DATA 字段：

| 字段 | 长度 | 说明 |
|---|---:|---|
| `CODE_ADDR` | 4 | CodeInfo TLV 信息块在 CODE 区内的字节地址 |
| `CODE_LEN` | 2 | CodeInfo TLV 信息块的字节长度 |
| `ADDR_WIDTH` | 1 | 读取 CodeInfo 信息块本体时使用的地址长度，只允许 `16` 或 `32` |
| `ENDIAN_MARK` | 2 | 目标内存变量字节序标记；原始字节 `55 AA` 表示大端，原始字节 `AA 55` 表示小端 |
| `MAX_PACKET` | 2 | 从机允许的最大完整协议帧长度，包含 CMD/ADDR/LEN/DATA/CRC；为 0 表示未声明 |

`ENDIAN_MARK` 只影响目标内存变量的多字节值显示和编辑，例如结构体字段、单独变量、enum 底层整数。`CMD`、`ADDR`、普通读写 `LEN`、`CODE_LEN`、`MAX_PACKET` 等多字节协议控制字段始终按大端序解析；TLV `LEN` 为单字节字段，不涉及大小端。

### 8.2 读取完整 CodeInfo

上位机继续发送普通读请求：

```text
ADDR_WIDTH = 32: MODE = 0x07 ADDR32
ADDR_WIDTH = 16: MODE = 0x04 CODE
ADDR = CODE_ADDR
LEN  = CODE_LEN
```

当 `ADDR_WIDTH=16` 时，下发普通读帧只携带 `CODE_ADDR` 的低 16 位。

如果 `CODE_LEN` 超过当前 BLE 包长可承载的数据长度，小程序按协议自动分片读取。后续分片使用 bootstrap 返回的 `ADDR_WIDTH` 与 `MAX_PACKET`；当 `MAX_PACKET` 非 0 时，设置页最大包长和设备声明包长取较小值。

最大数据长度计算：

```text
ADDR_WIDTH=32: 读响应单帧数据上限 = max_frame_bytes - 9
ADDR_WIDTH=16: 读响应单帧数据上限 = max_frame_bytes - 7
```

## 9. CodeInfo TLV 信息块

CodeInfo TLV 信息块位于 bootstrap 描述符给出的 CODE 区地址。该信息块不再定义固定 C 结构体头，也不再包含 `format_version`、`board_info_format`、`struct_entry_len` 或 `struct_table`。当前测试阶段只维护一种 TLV 字节格式，字段是否存在由 TLV `TYPE` 决定，信息块总长度由 `OP=0x05` bootstrap 返回的 `CODE_LEN` 决定。

TLV 项连续排列，直到累计长度等于 `CODE_LEN`：

```text
TYPE LEN VALUE...
```

| 字段 | 长度 | 说明 |
|---|---:|---|
| `TYPE` | 1 | TLV 类型 |
| `LEN` | 1 | `VALUE` 字节长度，单项最大 255 字节 |
| `VALUE` | `LEN` | 类型对应的数据 |

上位机解析到未知 `TYPE` 时跳过该项。任何 TLV 项声明长度超过 `CODE_LEN` 剩余字节时，整段 CodeInfo 视为格式错误。`CODE_LEN` 仍由 bootstrap 使用 16 位字段给出，因此整段 CodeInfo 可以超过 255 字节，只是单个 TLV 项不能超过 255 字节。

### 9.1 板卡信息 TLV

板卡信息全部为可选项。某块板不存在对应硬件信息时，不需要放该 TLV；小程序卡片和转换公式只使用实际同步到的字段。

| TYPE | 名称 | LEN | VALUE 格式 | 显示/用途 |
|---:|---|---:|---|---|
| `0x01` | `cave_freq` | 1 | `uint8_t` | 载波频率，单位 KHz |
| `0x02` | `ref_volt` | 1 | `uint8_t` | 基准电压实际值乘 10，显示时除以 10，单位 V |
| `0x03` | `amp_gain` | 1 | `uint8_t` | 运放倍数，无单位 |
| `0x04` | `rs_shunt` | 2 | `uint16_t` | 采样电阻，单位 mΩ |
| `0x05` | `bus_div` | 4 | `float32` | 母线电压分压比，大端序 |
| `0x06` | `along_div` | 4 | `float32` | 模拟输入电压分压比，大端序 |
| `0x07` | `chip_model` | 可变 | UTF-8 或 ASCII 字符串 | 芯片型号，建议 0 结尾或 0 填充 |
| `0x08` | `model` | 可变 | UTF-8 或 ASCII 字符串 | 电机型号，建议最多 30 字节，可容纳至少 7 个常见 UTF-8 汉字 |

### 9.2 内存入口 TLV

结构体实例和单独变量也是 TLV 项，不再额外保存 `entry_kind` 字段：

| TYPE | 类型 | 地址宽度 | 小程序处理 |
|---:|---|---:|---|
| `0x20` | 结构体实例 | 16 位 | 创建结构体组；未导入定义时按 `00`、`01`、`02`... 字节占位 |
| `0x21` | 单独变量 | 16 位 | 创建单变量组；1/2/4 字节默认按 `uint8_t`、`uint16_t`、`uint32_t` 显示，导入 enum 后可按枚举项显示 |
| `0x28` | 结构体实例 | 32 位 | 创建结构体组，统一 32 位字节地址 |
| `0x29` | 单独变量 | 32 位 | 创建单变量组，统一 32 位字节地址 |

内存入口 `VALUE` 的地址宽度由 TLV `TYPE` 决定，不再由 bootstrap 的 `ADDR_WIDTH` 决定。bootstrap `ADDR_WIDTH` 只表示“读取 CodeInfo 信息块本体”时使用 16 位 CODE 地址还是 32 位统一地址。名称字段为 UTF-8 或 ASCII，建议 0 结尾或 0 填充。

`TYPE=0x20/0x21` 的 16 位地址入口：

| 字段 | 长度 | 说明 |
|---|---:|---|
| `mem_type` | 1 | 所在存储区域，使用 AREA 编号 |
| `byte_addr` | 2 | 结构体实例或单独变量所在区域的字节地址 |
| `byte_len` | 2 | 结构体实例或单独变量的字节长度 |
| `type_name` | `LEN - 5` | 结构体定义名、变量名或 enum 类型名 |

`TYPE=0x28/0x29` 的 32 位地址入口：

| 字段 | 长度 | 说明 |
|---|---:|---|
| `byte_addr` | 4 | 结构体实例或单独变量所在统一地址空间内的字节地址 |
| `byte_len` | 2 | 结构体实例或单独变量的字节长度 |
| `type_name` | `LEN - 6` | 结构体定义名、变量名或 enum 类型名 |

32 位地址入口不携带 `mem_type`，小程序同步出的参数组统一使用 `MODE=0x07 ADDR32`。16 位地址入口按 `mem_type=0x01..0x04` 使用 DATA/IDATA/XDATA/CODE。手动存储访问卡片始终允许选择 `ADDR32/DATA/IDATA/XDATA/CODE`，不再因为 bootstrap `ADDR_WIDTH` 被锁定。

`mem_type`：

| 值 | 区域 |
|---:|---|
| `0x01` | DATA |
| `0x02` | IDATA |
| `0x03` | XDATA |
| `0x04` | CODE |

## 10. 小程序同步后的处理

小程序读取完整 CodeInfo TLV 信息块后：

1. 按 `TYPE LEN VALUE` 遍历 TLV 项，未知类型跳过。
2. 解析可选板卡信息，更新通讯页 CodeInfo 卡片；不存在的字段不显示、不进入转换公式上下文。
3. 遇到 `TYPE=0x20/0x28` 创建结构体组；结构体定义未导入时按字节占位。
4. 遇到 `TYPE=0x21/0x29` 创建单独变量组。
5. 按 TLV `TYPE` 解析内存入口地址；`0x20/0x21` 为 16 位地址，`0x28/0x29` 为 32 位地址。
6. 按 bootstrap `MEMORY_ENDIAN` 解释结构体字段、单独变量和 enum 底层整数的多字节值；单字节值不受影响。
7. 根据 `mem_type`、`byte_addr`、`byte_len`、`TYPE`、`type_name` 创建参数组。
8. 如果导入了 C 结构体和 enum 定义，只有结构体入口 `TYPE=0x20/0x28` 且 `type_name` 与结构体定义名一致、长度一致时才补全结构体字段；结构体字段类型为 enum 时，读回值按枚举项显示。
9. 单独变量入口 `TYPE=0x21/0x29` 如果 `type_name` 匹配 enum 类型名，或导入文件中存在 `EnumType variable_name;` 且变量名匹配 `type_name`，读回值按枚举项显示。
10. 如果当前已有同地址、同区域、同名称、同长度的结构体组，并且已经导入过结构体定义，则保留当前导入结构，只更新来源地址、区域、轮询开关等状态。
11. 如果存在相同 `type_name` 但 `mem_type` 或 `byte_addr` 不同的入口，应视为不同参数组。

## 11. 示例

### 11.1 读取 XDATA 中 64 字节结构体

```text
AREA = 0x03 XDATA
ADDR = 0x2000
LEN = 0x0040
读请求 = 03 20 00 00 40 CRC_H CRC_L
正常响应 = 03 20 00 00 40 DATA(64B) CRC_H CRC_L
```

### 11.2 读取 CODE 中 CodeInfo

假设描述符返回：

```text
CODE_ADDR = 0x00123456
CODE_LEN  = 0x0120
ADDR_WIDTH = 32
MEMORY_ENDIAN = big，bootstrap 原始标记 55 AA
```

读取请求：

```text
07 00 12 34 56 01 20 CRC_H CRC_L
```

`0x07` 表示 32 位统一地址模式。

### 11.3 CodeInfo TLV 示例

结构体实例：

```text
TYPE = 0x20
LEN = 0x14
VALUE = 03 20 00 00 40 4D 6F 74 6F 72 5F 52 75 6E 74 69 6D 65 5F 74
含义 = XDATA 0x2000 / 64 bytes / Motor_Runtime_t
```

单独变量：

```text
TYPE = 0x29
LEN = 0x0F
VALUE = 00 00 21 00 00 02 73 70 65 65 64 5F 72 65 66
含义 = ADDR32 0x00002100 / 2 bytes / speed_ref
```

## 12. 实现约束

1. 普通内存读写帧的 `LEN` 单位始终为字节，字段固定 16 位，有效范围为 `1..0xFFFF`；CodeInfo TLV 项内的 `LEN` 为 1 字节，只表示单项 `VALUE` 长度。
2. `ADDR` 单位始终为字节；`MODE=0x07` 时地址字段为 32 位，`MODE=0x01..0x04` 时地址字段为 16 位；`MODE=0x00/0x05/0x06` 保留。
3. 普通读写响应必须回显请求的 `CMD`、`ADDR`、`LEN`。
4. 上位机必须校验响应 `CMD`、`ADDR`、`LEN`、CRC，并确认数据长度等于 `LEN`。
5. `CODE` 区只读，写入必须返回 `WRITE_PROTECT`。
6. 特殊指令正常响应必须包含 `STATUS`。
7. CodeInfo 同步必须走 `OP=0x05` bootstrap 描述符，再按 bootstrap `ADDR_WIDTH` 读取信息块；内存入口地址宽度由 TLV `TYPE` 决定，目标变量值按 bootstrap `MEMORY_ENDIAN` 解释。
8. 固件更新 CodeInfo TLV 类型或内存入口格式时，必须同步更新本文档和小程序解析器。

## 13. 读写位宽说明

普通内存读写协议不额外携带变量位宽字段，只传输“字节地址 + 字节长度 + 字节数据”。变量显示和编辑位宽由以下信息决定：

- `TYPE=0x20/0x28` 的结构体实例由导入的 C 结构体定义决定字段类型。
- `TYPE=0x21/0x29` 的单独变量由 `byte_len` 默认推断：1 字节为 `uint8_t`，2 字节为 `uint16_t`，4 字节为 `uint32_t`。
- `enum` 不改变普通内存读写帧，只是在导入定义后给整数值增加枚举映射；显示格式为 `枚举项 (数值)`，写入时可输入枚举项名称或数字。
- 参数页可在寄存器/变量配置中进一步设置公式、单位、范围和显示类型。

如果后续从机需要原子读写、硬件寄存器位宽或对齐语义，可以新增 typed 特殊指令，不改变普通内存访问帧：

```text
读: 4F 06 AREA ADDR_3 ADDR_2 ADDR_1 ADDR_0 VALUE_WIDTH COUNT_H COUNT_L CRC_H CRC_L
写: 4F 07 AREA ADDR_3 ADDR_2 ADDR_1 ADDR_0 VALUE_WIDTH COUNT_H COUNT_L DATA... CRC_H CRC_L
```

建议字段：

| 字段 | 建议值 | 说明 |
|---|---|---|
| `ADDR` | 4 字节 | typed 扩展建议使用 32 位字节地址 |
| `VALUE_WIDTH` | `0x01`、`0x02`、`0x04` | 单个值 8/16/32 位 |
| `COUNT` | 16 位 | 值数量，不是字节数 |
| `DATA` | `VALUE_WIDTH * COUNT` 字节 | 按协议大端序 |
