Files
STM32F4-Base/docs/UI设计/RemoDispBus协议与接收设计说明.md
wandering1 f754d59218 docs(测试说明): 按用途分六个子目录并同步全库引用
分层:流程与规范/ 单元测试/ 板级测试/ 专项验证/ 测试工具/ 测试数据/,
顶层只留新增的目录导航 README.md(归层判据 + 硬约束 + 本次处置记录)。

- 搬迁 28 个路径:测试数据三个目录 58 个文件为纯改名(R100,内容逐字节不变)
- 全库引用同步 124 个文件:AGENTS / docs 索引 / skills / 门禁注释 / scripts
- 门禁面:scripts/check_test_traceability.py 的 glob 改 rglob
  (分层后不递归扫描=追溯门禁直接瞎掉)
- scripts/rename_ref.py:收尾的 ℹ 提示在 GBK 代码页抛 UnicodeEncodeError,
  让进程以退出码 1 收场而改写其实已落盘(RC 与真实结果相反);改为把
  stdout/stderr reconfigure 成 UTF-8,与 check_doc_links.py 等同法
- docs/文档规范.md §1.1 补「子目录文件过多时再分一层」;§3.6 补 rename_ref
  搬整目录的两个实测坑:① 会改写目录内部文件的内容 ② 内部文件的 ../ 出链
  不补层级(本次 23 个被误改的证据文件已按 HEAD 原始字节逐字节还原)
- docs/README.md §6 改为与六个子目录一一对应,并补登原先漏登记的
  「启动日志原子性复验流程」一行
- .cubemx_survival_baseline.json:App/util/task_watchdog.c 的注释路径也随分层更新,
  该文件是存活审计的 watchdog-module 漂移锚点,按脚本自己的提示跑 --write 重建基线
  (实测重建后仅 watchdog-module 一条哈希变化)
- 铁律 4 例外(本次在任务树里改了单写者文件):AGENTS.md 与
  .pre-commit-config.yaml 只被 rename_ref 改写路径字面量(后者全在注释行),
  任务表块与钩子声明一字未动

验证:check_doc_links.py --sections-strict 覆盖全部 88 个改动 .md 全绿;
check_test_traceability.py 输出与主树逐行一致;196 个文本文件无 BOM、无新增
CR、无 U+FFFD、无行尾空白;陈旧引用面由主树 25 处降到 6 处(无新增断链)。
2026-10-30 07:16:07 +08:00

306 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RemoDispBus 协议与接收设计说明
> 版本:v1.0 日期:2026-09-10
> 适用工程:STM32F4-Base(STM32F407ZGTx + CH395F 以太网 / LVGL UI)
> 范围:UI 远程显示(RemoDispBus)的**协议设计**与**接收解析设计**——
> 帧格式、字段语义、命令集、交互时序、屏幕位图格式,以及服务端逐字节状态机解析的实现。
> 对应模块:`Drivers/BSP/UI/ui_net_sink.c`(MCU 与模拟器**共用同一份**源码,协议与解析一致);
> 平台差异仅在网络后端(MCU `net_socket.c`/CH395F vs 模拟器 `net_socket_sim.c`/Winsock)
> 与 LCDMEM/按键注入的平台 backend(`ui_net_sink_backend_mcu.c` / `ui_net_sink_backend_sim.c`)。
---
## 0. 速览
| 项目 | 内容 |
|---|---|
| **协议名** | RemoDispBus(UI 远程显示总线) |
| **用途** | 远端查看设备 UI 画面 + 远端导航按键 |
| **传输** | TCP,端口 **7003**(须与 DTU-RemoteLCD 客户端默认端口一致) |
| **连接模型** | 单连接(服务端 `backlog=1`,同一时刻一个客户端) |
| **帧格式** | `[TAG(1)][CMD(1)][LEN_H(1)][LEN_LO(1)][DATA(n)][CRC(1)]` |
| **方向标识** | TAG `0xAA`=客户端→服务端,`0xBB`=服务端→客户端 |
| **命令集** | KEEPLIVE(0) / INIT(1) / KEY(2) / LCDMEM(3) |
| **校验** | CRC = 数据段逐字节**异或**(取低 8 位;不含 TAG/CMD/LEN) |
| **字节序** | 多字节字段**大端**(网络字节序) |
| **最短帧** | 5 字节(LEN=0 时) |
| **接收方式** | 逐字节有限状态机(TAG 定界 + 长度定长 + CRC 校验,自动重同步) |
---
## 1. 协议设计
### 1.1 定位与角色
RemoDispBus 让远端(PC 上的 DTU-RemoteLCD 客户端 / 浏览器页面)实时查看设备 UI 的 1bpp 画面,并下发导航按键。角色:
| 角色 | 进程 | 说明 |
|---|---|---|
| **服务端** | `ui_net_sink`(设备 / 模拟器) | 监听 7003,收命令、回送帧缓冲 |
| **客户端** | DTU-RemoteLCD(PC) | 连接服务端,拉取画面、下发按键 |
请求—响应模型:客户端发请求帧(TAG `0xAA`),服务端回应答帧(TAG `0xBB`)。
### 1.2 帧格式
| 偏移 | 字段 | 大小(B) | 说明 |
|---|---|---|---|
| 0 | `TAG` | 1 | 方向标识:`0xAA` 客户端帧 / `0xBB` 服务端帧 |
| 1 | `CMD` | 1 | 命令码(见 §1.5) |
| 2 | `LEN_H` | 1 | 数据段长度高字节(大端) |
| 3 | `LEN_LO` | 1 | 数据段长度低字节 |
| 4 | `DATA` | `LEN` | 数据段(命令相关,见 §1.5) |
| 4+LEN | `CRC` | 1 | 数据段逐字节异或(低 8 位) |
- 整帧长度 = `4 + LEN + 1`;`LEN=0` 时最短为 5 字节。
- `LEN` 为 16 位大端,理论最大 65535 数据字节。
**图形版**(SVG 矢量,字段宽 ∝ 字节数;由 `tools/arch_diagram/gen_frame_svg.py` 生成,浏览器打开可无损缩放):
![RemoDispBus 帧格式](../images/RemoDispBus帧格式.svg)
### 1.3 字段语义
- **TAG(方向)**:`0xAA` = 客户端→服务端(服务端只接受此标签的帧);`0xBB` = 服务端→客户端。TAG 标识**方向**,不承载命令。
- **CMD(命令)**:同一命令在两个方向复用同一编码,靠 TAG 区分方向(见 §1.5)。
- **CRC(校验)**:`CRC = DATA[0] ^ DATA[1] ^ … ^ DATA[LEN-1]`(低 8 位)。**仅覆盖数据段**,不含 TAG/CMD/LEN;`LEN=0` 时 CRC 为 `0`。与 DTU-HMI 协议一致。
### 1.4 字节序
多字节字段一律**大端**(网络字节序):`LEN`(u16)、INIT 应答的宽/高(u16)与帧大小(u32)、LCDMEM 的偏移(u32)。
### 1.5 命令集与数据段
| CMD | 名称 | 请求数据段(客户端→服务端) | 应答数据段(服务端→客户端) | 说明 |
|---|---|---|---|---|
| 0 | KEEPLIVE | 无(LEN=0) | 无(LEN=0) | 保活/心跳,服务端原样回显 |
| 1 | INIT | 无(LEN=0,查询) | `[宽u16][高u16][帧字节数u32]`(**8B**) | 客户端查询屏幕几何与显存大小 |
| 2 | KEY | `[按键码u8]`(1B) | 无(LEN=0,确认) | 远端导航按键,服务端注入 UI |
| 3 | LCDMEM | `[起始偏移u32]`(4B) | `[偏移u32][1bpp位图 3200B]`(3204B) | 读取显存(画面);偏移 0 = 整帧 |
> **按键码**(`KEY` 数据段,与 `ui_port.h` 一致):`UP=0x02`、`DOWN=0x40`、`LEFT=0x10`、`RIGHT=0x08`、`ENTER=0x20`、`ESC=0x01`。
> **注意**:客户端(如 Python 参考实现)可不使用 `INIT`(直接按 160×160 处理);协议保留该命令供需查询几何的客户端使用。
>
> ⚠ **INIT 应答长度勘误(2026-09-22 M3 台架实测修复,D17)**:本表原写"6B",而字段定义
> `[宽u16][高u16][帧字节数u32]` 合计为 **8B**。旧实现按 6 发送 → u32 只剩高半字 →
> 客户端读到的"帧字节数"**恒为 0**(真机与 PC 模拟器一致复现)。现已改为发满 8B
> (`remo_send_init`),客户端按 `LEN>=8` 读 u32;若需兼容旧固件,可按 `LEN==6` 时
> 只取前 4B(宽高)。
### 1.6 屏幕位图格式(LCDMEM 数据段)
- **1bpp** 单色,**MSB 优先**(每字节 8 像素,bit7 为该字节第一个像素)。
- 像素 (x,y) 定位:`byte_i = (y*W + x) / 8`,`bit_i = 7 - (x % 8)`。
- 位值 `1` = 白(255),`0` = 黑(0)。
- 160×160 → `160*160/8 = 3200` 字节。
### 1.7 典型交互时序
```mermaid
sequenceDiagram
participant C as 客户端 (DTU-RemoteLCD)
participant S as 服务端 (ui_net_sink)
C->>S: TCP 连接 :7003
opt 查询屏幕参数(可选)
C->>S: INIT 请求 (0xAA, CMD=1, len=0)
S-->>C: INIT 应答 (0xBB, CMD=1, 宽/高/帧大小)
end
loop 屏幕刷新(约 100ms)
C->>S: LCDMEM 请求 (0xAA, CMD=3, 偏移=0)
S-->>C: LCDMEM 应答 (0xBB, CMD=3, 偏移+位图 3200B)
end
opt 远端按键
C->>S: KEY 请求 (0xAA, CMD=2, 按键码)
S-->>C: KEY 应答 (0xBB, CMD=2, len=0)
end
opt 显式保活
C->>S: KEEPLIVE (0xAA, CMD=0)
S-->>C: KEEPLIVE (0xBB, CMD=0)
end
```
> 连接后服务端**不主动上报 INIT**(避免污染客户端首帧读取);由客户端按需请求(`INIT`)或按 LCDMEM 轮询拉取。
---
## 2. 接收解析设计(逐字节状态机)
### 2.1 设计动机
TCP 是**字节流**,不保留消息边界:一帧可能被拆到多次 `recv()`,一次 `recv()` 也可能带多帧或前导噪声。整块解析(假设"一次 recv = 一完整帧")在这种流上不可靠。因此服务端采用**逐字节有限状态机**:
| 关注点 | 整块解析 | 逐字节状态机 |
|---|---|---|
| 帧跨多次 recv | ✗ 会解析半帧 | ✓ 状态跨 recv 保持 |
| 一次 recv 多帧(粘包) | ✗ 只处理首帧 | ✓ 顺序处理每帧 |
| 前导噪声/失步 | ✗ 从头按帧头假设 | ✓ 扫描到 0xAA 重新同步 |
| 长度字段损坏 | ✗ 按错误长度读飞 | ✓ 越界即复位 |
| 内存 | 需容纳整帧 | 小块暂存 + 单帧重组缓冲 |
### 2.2 状态机
6 个状态,逐字节推进:
```mermaid
stateDiagram-v2
[*] --> TAG
TAG --> CMD : 0xAA
TAG --> TAG : 其他字节(丢弃,重同步)
CMD --> LEN_H : 任意字节
LEN_H --> LEN_L : 任意字节
LEN_L --> DATA : 0 < plen <= 256
LEN_L --> CRC : plen == 0
LEN_L --> TAG : plen > 256 (失步复位)
DATA --> DATA : 未收满
DATA --> CRC : 收满 plen
CRC --> TAG : 校验通过→分发 / 校验错→丢弃
```
| 状态 | 含义 | 收到字节动作 | 迁移 |
|---|---|---|---|
| `TAG` | 等帧头 | 仅收 `0xAA`,否则丢弃 | →`CMD`(命中)/ 停留(未命中) |
| `CMD` | 等命令码 | 存入帧 | →`LEN_H` |
| `LEN_H` | 等长度高字节 | 存入帧 | →`LEN_L` |
| `LEN_L` | 等长度低字节 | 存入帧,算 `plen` | →`DATA` / `CRC` / `TAG`(越界) |
| `DATA` | 等数据段 | 存入帧,计数 | 收满 → `CRC` |
| `CRC` | 等校验字节 | 存入帧,校验 | →`TAG`(分发或丢弃) |
### 2.3 各状态处理要点
- **TAG**:只认 `TAG_CLIENT(0xAA)`。非帧头字节一律丢弃并停留在 `TAG`——这是**重同步**的核心:任意噪声后,下一个真帧头 `0xAA` 即恢复。
- **LEN_L**:`plen = (LEN_H<<8) | LEN_LO`。若 `plen > RM_MAX_RX_PAYLOAD(256)` 判为**长度损坏/失步**,立即复位回 `TAG`(防止按错误长度"读飞"吞掉后续帧);否则进入 `DATA`(`plen>0`)或 `CRC`(`plen==0`)。
- **DATA**:逐字节存入并按 `didx` 计数;`didx` 达到 `plen` 转 `CRC`。**数据段内的 `0xAA` 只当数据**(长度定界保证不误判为帧头)。
- **CRC**:存入校验字节后,重算 `XOR(数据段)` 与收到的 CRC 比较;**相等才回调 `remo_handle` 分发**,随后复位回 `TAG`(半帧/坏帧均丢弃)。
### 2.4 重同步与健壮性
| 机制 | 作用 |
|---|---|
| TAG 门控 | 非 `0xAA` 字节在 `TAG` 态丢弃,噪声后自动重新锁定帧头 |
| 长度越界复位 | `plen` 超上限 → 判损坏,丢弃半帧回 `TAG`,不被坏长度带偏 |
| CRC 校验 | 只有校验通过的整帧才分发,坏帧静默丢弃 |
| 长度定界 | 数据段内的 `0xAA` 不误判为帧头,避免假重同步 |
### 2.5 状态持久化(跨 recv)
解析器状态(`g_rm`:当前状态 + 帧缓冲 + `plen`/`didx`)是**全局常驻**,跨 `poll()` 调用保持——一帧可跨多次 `recv()` 收齐。**复位点**:新连接 `accept`、连接断开、长度越界失步。
### 2.6 缓冲设计
| 缓冲 | 大小 | 用途 |
|---|---|---|
| `g_rx[RM_RX_BUF]` | 64 B | `net_recv()` 暂存区(每次 poll 的到达字节,喂入后即弃) |
| `g_rm.frame[]` | 4+256+1=261 B | 帧重组缓冲(任一时刻只装一帧,分发后复位) |
| `RM_MAX_RX_PAYLOAD` | 256 | 接收数据段上限(客户端命令帧都很小,此为防损坏护栏 + 定缓冲尺寸) |
> 客户端→服务端 的命令帧数据段都很小(KEY=1、LCDMEM 请求=4、其余=0),256 上限有充足余量,同时把重组缓冲压到最小。
### 2.7 与轮询循环集成
```c
int n = net_recv(g_client, g_rx, sizeof(g_rx), NET_MSG_DONTWAIT); /* 非阻塞 */
if (n > 0) {
for (int i = 0; i < n; i++) {
rm_parser_feed(g_rx[i]); /* 逐字节喂入状态机 */
}
}
```
全程非阻塞,不拖慢 LVGL 刷新;`recv` 无数据(`WOULDBLOCK`)时直接返回。
### 2.8 边界情况
| 场景 | 状态机行为 |
|---|---|
| 一帧拆到多次 recv | 状态跨 recv 保持,收齐后分发 |
| 一次 recv 含多帧 | 顺序解析、逐帧分发 |
| 前导噪声/垃圾字节 | `TAG` 态丢弃,直到下一个 `0xAA` |
| 数据段内含 `0xAA` | 当数据处理,不误判帧头 |
| 长度字段损坏(超大) | 判失步,复位回 `TAG` 重新扫描 |
| CRC 校验失败 | 整帧丢弃,复位回 `TAG` |
| `LEN=0` 的空数据段 | 跳过 `DATA` 态,期望 CRC=0 |
---
## 3. 双端共用实现
协议两端一致(线格式相同,可互通),且**服务端实现已统一**:MCU 与模拟器编译**同一份**
`Drivers/BSP/UI/ui_net_sink.c`(§2 逐字节状态机 + `remo_handle` 分发)。平台差异被隔离到两处 backend:
| 关注点 | MCU(Keil / CH395F) | 模拟器(CMake / Winsock) | 说明 |
|---|---|---|---|
| 帧收发 | `net_socket.c`(CH395F 中断 + 消息队列) | `net_socket_sim.c`(Winsock 非阻塞) | 同一套 `net_*` BSD API(`socket`/`bind`/`listen`/`accept`/`recv`/`send`/`close`),仅底层不同;模拟器以 `NET_NO_OS` 剥离 FreeRTOS 部分 |
| LCDMEM 取帧 | `ui_net_sink_backend_mcu.c` → `ui_port_get_framebuffer()` | `ui_net_sink_backend_sim.c` → `sim_port_get_screen_mono()` | 同一 `ui_net_sink_get_lcdmem()` 接口,各端提供 1bpp 帧缓冲来源 |
| KEY 注入 | `ui_net_sink_backend_mcu.c` → `ui_port_queue_key()` | `ui_net_sink_backend_sim.c` → `lv_group_send_data()` | 同一 `ui_net_sink_inject_key()` 接口,各端注入 UI |
要点:
1. **单一解析实现**:两端均用 §2 的逐字节状态机(`rm_parser_feed`),对"帧跨 recv / 粘包 / 前导噪声"统一具备重同步能力,消除旧 MCU"整块解析"的脆弱性。
2. **最小帧长判据统一**:`remo_handle` 以 `n < 5` 拦截(最短合法帧 5 字节),两端一致——旧 MCU `n < 6` 会丢 5 字节 KEEPLIVE/INIT 帧的隐患已随统一消除。
3. **断线重连统一**:`ui_net_sink_poll` 在客户端断开后用 `net_get_sock(g_listen) == NULL` 判监听是否失效(MCU 单连接模型下监听随客户端一起失效),失效即 `bind_listen()` 重建——两端一致,并修复了 MCU 单连接模型断线后无法重连的隐患。(术语统一补注:本文「远程显示」与「远程画面推送」同义,均指 RemoDispBus 监听 7003 的 UI 帧推送;代码标识 RemoDispBus 不随文案改名而变动。)
4. **协议兼容**:线格式与命令语义不变,任一端都能与 DTU-RemoteLCD 参考客户端互通;双端行为一致已由 e2e 回归(`simulator/tests/check_remo_sink.py`,0/25600 位差异)与双端构建(Keil + CMake 均 0 错误 0 警告)验证。
5. **驱动者是宿主,不是协议自己**:`ui_net_sink_poll()` 由宿主周期调用——MCU 端目前 = **uiTask 主循环 10 ms**,模拟器 = `main.c` 主循环。协议本身不含节拍、不阻塞。
> ✅ **门控已落地(2026-10-10)**:`ui_net_sink_poll()` 现在**先查后动**——`net_can_accept()` /
> `net_can_read()` 纯 RAM 读控制块判"有没有活",无活直接返回,不再每 10 ms 发一次
> `net_accept`(原 100 msg/s 队列压力,与 FTP 抢深度 8 的队列)。门控判"无数据"时仍
> **每 32 轮无条件 recv 一次**兜底(`readable` 只由 RECV_OK 中断置位,事件未收到时纯门控
> 会把数据永久藏住)。模拟器后端恒返回"就绪",行为逐位不变。
>
> ⚠ **残留暴露(已知,刻意不处理)**:门控只消除"无谓的往返",消除不了"往返本身无上界"——
> 客户端在连且 netTask 拥塞时,3204 B 的 LCDMEM 应答仍可能让宿主任务等最长 ≈4 s。
> "把推流搬出 uiTask(独立任务 + 帧 seqlock)"已完整评估并**决定不做**:实测刷新最坏周期
> (`uip` ≈61 ms)是 **LVGL 帧周期地板**、与网络无关,搬任务不会让刷新变快;成本却含
> **必改 `.ioc`** 的重生成与全套回归,收益只剩极端并发下的观感隔离。若日后它成为实际问题,
> 正确方向是先治 netTask 排队公平性(见 `../网络/Net层阻塞模型架构设计.md`),不是把 UI 搬走。
> 观测手段:`[PROF]` 的 `uis`(往返最坏)与 `uisn`(此刻仍在推流段内多久)——
> **`uisn` 持续非 0 就是这一格真发生的铁证**;字段语义与判读见
> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md) §11.4。
---
## 4. 关键常量与文件索引
| 常量 / 文件 | 值 / 职责 |
|---|---|
| `UI_NET_SINK_PORT` | `7003`(`ui_net_sink.h`,须与客户端默认端口一致) |
| `UI_REMO_TAG_CLIENT` / `UI_REMO_TAG_DEVICE` | `0xAA` / `0xBB` |
| `UI_REMO_CMD_KEEP / INIT / KEY / LCDMEM` | `0` / `1` / `2` / `3` |
| `RM_MAX_RX_PAYLOAD` | `256`(接收数据段上限,防长度字段损坏引发失步) |
| `RM_RX_BUF` | `64`(`net_recv` 暂存区) |
| `Drivers/BSP/UI/ui_net_sink.c` | **双端共用服务端**(§2 逐字节状态机;MCU/CH395F 与模拟器/Winsock 共同编译) |
| `Drivers/BSP/UI/ui_net_sink.h` | 共享头:协议常量 + `ui_net_sink_start`/`poll` 声明 |
| `Drivers/BSP/UI/ui_net_sink_backend.h` | 平台 backend 接口:`ui_net_sink_get_lcdmem()` / `ui_net_sink_inject_key()` |
| `Drivers/BSP/UI/ui_net_sink_backend_mcu.c` | MCU 实现:`ui_port_get_framebuffer()`(LCDMEM)/ `ui_port_queue_key()`(KEY) |
| `simulator/sim/ui_net_sink_backend_sim.c` | 模拟器实现:`sim_port_get_screen_mono()`(LCDMEM)/ `lv_group_send_data()`(KEY) |
| `simulator/sim/net_socket_sim.c` | 模拟器网络后端(Winsock 实现 `net_*` BSD API,`NET_NO_OS` 剥离 FreeRTOS) |
| `Drivers/BSP/NET/net_socket.c` | MCU 网络后端(CH395F 中断 + 消息队列实现 `net_*`) |
| DTU-RemoteLCD `remo_disp_server.py` | 参考客户端(Flask + WebSocket,协议编解码) |
**对外 API**(`ui_net_sink.h`):`ui_net_sink_start(port)` / `ui_net_sink_poll()`(无 `stop`;监听随网络代次变化 / 客户端断线自动重建)。
---
## 5. 陷阱
1. **CRC 只覆盖数据段**:不含 TAG/CMD/LEN;`LEN=0` 时 CRC 恒为 `0`。收发两端必须一致,否则校验必败。
2. **TAG 是方向标识**:服务端只接受 `0xAA`(客户端帧);收到 `0xBB` 视为非法(状态机在 `TAG` 态丢弃)。
3. **大端字节序**:`LEN` 及 payload 内 u16/u32 均大端;MCU(小端核)收发须显式转换(`put_u16`/`put_u32`)。
4. **位图 MSB 优先**:`bit_i = 7 - (x%8)`;搞反则画面左右镜像/错位。
5. **连接后不主动发 INIT**:避免污染客户端首帧读取(客户端按 LCDMEM 轮询或显式 INIT 获取)。
6. **平台须齐备两个后端**:`ui_net_sink.c` 依赖 `net_*` BSD Socket API 与 `ui_net_sink_backend`(`ui_net_sink_get_lcdmem` / `ui_net_sink_inject_key`);接入新平台时二者缺一不可(MCU:`net_socket.c` + `ui_net_sink_backend_mcu.c`;模拟器:`net_socket_sim.c` + `ui_net_sink_backend_sim.c`)。
7. **单连接**:`backlog=1`,同一时刻仅一个客户端;新连接需旧连接先断开。
---
## 6. 相关文档
| 主题 | 文档 |
|---|---|
| 网口配置(含远程显示服务自愈) | `docs/子系统设计/网络配置说明.md` |
| 项目分层架构 | `docs/架构设计/项目架构总览.md` |
| 启动顺序 / 就绪屏障 | `docs/架构设计/项目启动顺序管理.md` |
| UI 设计架构 | `docs/UI设计/UI_设计架构.md` |
| LVGL 模拟器部署 | `docs/UI设计/LVGL模拟器部署与使用说明.md` |
| CH395F 网络测试 | `docs/测试说明/板级测试/CH395F_Test_Guide.md` |