# 存储访问协议

## 1. 协议定位

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

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

- 标准 Modbus RTU 使用从机地址、功能码和 Modbus CRC。
- 存储访问协议使用 `CMD + ADDR + LEN + DATA + CRC16-CCITT-FALSE`。
- 复位使用 `bit6=1` 的特殊指令帧；当前仅定义复位特殊指令。
- CodeInfo 同步先读取普通区域 `area=0x00` 的 codeinfo 描述符，再用 `addr32` 读取 TLV 信息块。

## 2. 字节序与 CRC

除 CRC 算法内部计算外，所有多字节协议控制字段均为大端序。当前目标内存变量的多字节值默认按大端解释和编辑。

```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      SPECIAL   特殊指令标志位
bit5~bit4 RSV       保留，普通读写保持 0
bit3      RW        普通读写标志，0=读，1=写
bit2~bit0 AREA      普通读写区域码
```

| 位 | 值 | 含义 |
|---|---:|---|
| ERR | 0 | 请求或正常响应 |
| ERR | 1 | 故障响应 |
| SPECIAL | 0 | 普通存储访问 |
| SPECIAL | 1 | 特殊指令，`bit0~bit5` 表示特殊指令码 |
| RSV | 0 | 当前版本固定为 0 |
| RW | 0 | 读操作 |
| RW | 1 | 写操作 |

普通内存命令生成规则：

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

特殊指令生成规则：

```text
特殊请求 : CMD = 0x40 | SPECIAL_CODE
异常响应 : CMD_ERR = CMD | 0x80
```

当前仅定义 `SPECIAL_CODE=0x01` 表示复位，因此复位请求 `CMD=0x41`，复位异常响应 `CMD=0xC1`。

## 4. AREA 定义

| AREA | 名称 | 读 | 写 | 用途 |
|---:|---|---|---|---|
| `0x00` | CODEINFO | 支持 | 禁止 | 同步时读取一次，返回 TLV 起始地址与长度 |
| `0x01` | DATA | 支持 | 支持 | 内部直接寻址 RAM 区 |
| `0x02` | IDATA | 支持 | 支持 | 内部间接寻址 RAM 区 |
| `0x03` | XDATA | 支持 | 支持 | 外部数据空间或扩展 RAM |
| `0x04` | CODE | 支持 | 禁止 | 程序存储区，16 位地址 |
| `0x05..0x06` | 保留 | 禁止 | 禁止 | 保留 |
| `0x07` | ADDR32 | 支持 | 支持 | 32 位统一地址模式 |

写 `CODEINFO` 或 `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..0x03: 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`。

## 6. 特殊指令帧

特殊指令使用 `CMD bit6=1`，`bit0~bit5` 表示特殊指令码。当前仅定义 `0x01` 复位。

```text
请求: CMD DATA... CRC_H CRC_L
响应: CMD DATA... CRC_H CRC_L
异常: CMD_ERR EXCEPTION_CODE CRC_H CRC_L
```

特殊指令执行失败时直接返回异常帧，成功响应不额外携带状态字节。

| 指令码 | CMD | 名称 | 请求 DATA | 成功响应 DATA | 说明 |
|---:|---:|---|---|---|---|
| `0x01` | `0x41` | RESET | 无 | 无 | 复位 |

## 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. 读取普通区域 `area=0x00 CODEINFO` 的描述符，响应 DATA 为 9 字节。
2. 按描述符返回的 TLV 起始地址、`len16`、地址位宽和最大包长，读取完整 CodeInfo TLV 信息块。

### 8.1 读取 CODEINFO 描述符

请求：

```text
00 CRC_H CRC_L
```

从机成功响应：

```text
00 TLV_ADDR_3 TLV_ADDR_2 TLV_ADDR_1 TLV_ADDR_0 TLV_LEN_H TLV_LEN_L ADDR_WIDTH MAX_PACKET_H MAX_PACKET_L CRC_H CRC_L
```

响应 DATA 字段：

| 字段 | 长度 | 说明 |
|---|---:|---|
| `TLV_ADDR` | 4 | CodeInfo TLV 信息块起始字节地址；`ADDR_WIDTH=16` 时读取帧只携带低 16 位 |
| `TLV_LEN` | 2 | CodeInfo TLV 信息块的字节长度 |
| `ADDR_WIDTH` | 1 | 读取 CodeInfo 信息块本体时使用的地址位宽，只允许 `16` 或 `32` |
| `MAX_PACKET` | 2 | 从机允许的最大完整协议帧长度，包含 CMD/ADDR/LEN/DATA/CRC；为 0 表示未声明 |

描述符区域 `CODEINFO` 只读，不支持写入。`CMD`、`ADDR`、普通读写 `LEN`、`TLV_LEN`、`MAX_PACKET` 等多字节协议控制字段始终按大端序解析；TLV `LEN` 为单字节字段，不涉及大小端。

### 8.2 读取完整 CodeInfo

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

```text
ADDR_WIDTH = 32: MODE = 0x07 ADDR32
ADDR_WIDTH = 16: MODE = 0x04 CODE
ADDR = TLV_ADDR
LEN  = TLV_LEN
```

如果 `TLV_LEN` 超过当前 BLE 包长可承载的数据长度，小程序按协议自动分片读取。

最大数据长度计算：

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

## 9. CodeInfo TLV 信息块

CodeInfo TLV 信息块位于 `CODEINFO` 描述符给出的地址。字段是否存在由 TLV `TYPE` 决定，信息块总长度由描述符返回的 `TLV_LEN` 决定。

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

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

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

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

### 9.1 固定内存入口 TLV

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

| TYPE | 名称 | 地址宽度 | 小程序处理 |
|---:|---|---:|---|
| `0x01` | DATA 结构体实例 | 16 位 | 创建结构体组；未导入定义时按 `00`、`01`、`02`... 字节占位 |
| `0x02` | DATA 单独变量 | 16 位 | 创建单变量组；`byte_len` 只定义长度，类型需在 UI 中配置为有符号/无符号整数、`float`，或导入 enum 后按枚举项显示 |
| `0x03` | IDATA 结构体实例 | 16 位 | 创建结构体组；未导入定义时按 `00`、`01`、`02`... 字节占位 |
| `0x04` | IDATA 单独变量 | 16 位 | 创建单变量组；类型由 UI/enum 配置，长度来自 `byte_len` |
| `0x05` | XDATA 结构体实例 | 16 位 | 创建结构体组 |
| `0x06` | XDATA 单独变量 | 16 位 | 创建单变量组；类型由 UI/enum 配置，长度来自 `byte_len` |
| `0x07` | ADDR32 结构体实例 | 32 位 | 创建结构体组，统一 32 位字节地址 |
| `0x08` | ADDR32 单独变量 | 32 位 | 创建单变量组，统一 32 位字节地址；类型由 UI/enum 配置，长度来自 `byte_len` |

16 位地址入口 `VALUE`：

| 字段 | 长度 | 说明 |
|---|---:|---|
| `byte_addr` | 2 | 结构体实例或单独变量所在区域的字节地址 |
| `byte_len` | 2 | 结构体实例或单独变量的字节长度 |
| `name` | 32 | 固定 32 字节名称字段，UTF-8 或 ASCII，建议 0 结尾或 0 填充；结构体定义名、变量名或 enum 类型名 |

因此 16 位地址固定内存入口 TLV 的 `LEN` 固定为 `0x24`。

32 位地址入口 `VALUE`：

| 字段 | 长度 | 说明 |
|---|---:|---|
| `byte_addr` | 4 | 结构体实例或单独变量所在统一地址空间内的字节地址 |
| `byte_len` | 2 | 结构体实例或单独变量的字节长度 |
| `name` | 32 | 固定 32 字节名称字段，UTF-8 或 ASCII，建议 0 结尾或 0 填充；结构体定义名、变量名或 enum 类型名 |

因此 32 位地址固定内存入口 TLV 的 `LEN` 固定为 `0x26`。

### 9.2 自定义 TLV

`0x20~0x3F` 为自定义 TLV 区域。上位机保留原始 TLV 项展示，当前不对自定义项做业务解析。

### 9.3 板卡信息 TLV

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

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

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

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

1. 按 `TYPE LEN VALUE` 遍历 TLV 项，未知类型跳过。
2. 解析 `0x40` 起的可选板卡信息，更新通讯页 CodeInfo 卡片；不存在的字段不显示、不进入转换公式上下文。
3. 遇到固定结构体入口 `TYPE=0x01/0x03/0x05/0x07` 创建结构体组；结构体定义未导入时按字节占位。
4. 遇到固定变量入口 `TYPE=0x02/0x04/0x06/0x08` 创建单独变量组，初始按 `byte_len` 显示原始字节并标记为未配置。
5. 按固定 TLV `TYPE` 解析内存区域和地址宽度。
6. 当前目标内存多字节值默认按大端解释；单字节值不受影响。
7. 根据 `byte_addr`、`byte_len`、`TYPE`、`name` 创建参数组。
8. 如果导入了 C 结构体和 enum 定义，只有结构体入口且 `name` 与结构体定义名一致、长度一致时才补全结构体字段；结构体字段类型为 enum 时，读回值按枚举项显示。
9. 单独变量入口需要在 UI 中选择与 `byte_len` 一致的有符号/无符号整数或 `float` 类型；如果 `name` 匹配 enum 类型名，或导入文件中存在 `EnumType variable_name;` 且变量名匹配 `name`，读回值按枚举项显示。
10. 如果当前已有同地址、同区域、同名称、同长度的结构体组，并且已经导入过结构体定义，则保留当前导入结构，只更新来源地址、区域、轮询开关等状态。
11. 如果存在相同 `name` 但区域或 `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 读取 CodeInfo

假设描述符返回：

```text
TLV_ADDR = 0x00123456
TLV_LEN  = 0x0120
ADDR_WIDTH = 32
MAX_PACKET = 0x0040
```

先读取 `CODEINFO` 描述符：

```text
00 CRC_H CRC_L
```

再按 `ADDR_WIDTH=32` 读取 TLV 信息块：

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

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

### 11.3 CodeInfo TLV 示例

结构体实例：

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

单独变量：

```text
TYPE = 0x08
LEN = 0x26
VALUE = 00 00 21 00 00 02 73 70 65 65 64 5F 72 65 66 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
含义 = 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` 为 CODEINFO 短描述符请求；`MODE=0x05..0x06` 保留。
3. 普通读写响应必须回显请求的 `CMD`、`ADDR`、`LEN`。
4. 上位机必须校验响应 `CMD`、`ADDR`、`LEN`、CRC，并确认数据长度等于 `LEN`。
5. `CODEINFO` 和 `CODE` 区只读，写入必须返回 `WRITE_PROTECT`。
6. 特殊指令正常响应不携带 `STATUS`，执行失败必须返回异常帧。
7. CodeInfo 同步必须先读 `area=0x00 CODEINFO` 描述符，再按描述符返回的 `TLV_ADDR/TLV_LEN/ADDR_WIDTH/MAX_PACKET` 读取信息块；`ADDR_WIDTH=16` 使用 `area=0x04 CODE`，`ADDR_WIDTH=32` 使用 `area=0x07 ADDR32`，内存入口地址宽度由 TLV `TYPE` 决定。
8. 固件更新 CodeInfo TLV 类型或内存入口格式时，必须同步更新本文档和小程序解析器。

## 13. 读写位宽说明

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

- `TYPE=0x01/0x03/0x05/0x07` 的结构体实例由导入的 C 结构体定义决定字段类型。
- `TYPE=0x02/0x04/0x06/0x08` 的单独变量由 `byte_len` 给出字节长度，UI 必须选择与该长度一致的显示/编辑类型；未配置前只显示原始字节，不允许写入。
- `enum` 不改变普通内存读写帧，只是在导入定义后给同长度整数值增加枚举映射；显示格式为 `枚举项 (数值)`，写入时可输入枚举项名称或数字。
- 参数页可在寄存器/变量配置中进一步设置公式、单位、范围和显示类型。

如果后续从机需要原子读写、硬件寄存器位宽或对齐语义，可以新增 `bit6=1` 的 typed 特殊指令，不改变普通内存访问帧。具体 `special_code` 和 DATA 格式需另行定义。

建议字段：

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