用户裁定:文档只写现状,历史流水一律删掉,过程查 git log。 - 43 篇「变更记录 / 修订记录」整节删除:继电保护规划 12.6 KB、产品 需求 11.8 KB、项目部署环境 5.5 KB、BSD Socket 指南 5.3 KB、架构 总览 5.0 KB、实机测试实施指南 3.8 KB 等 - 23 篇别处仍有指向:只留节名 + 一行「本文件不再记流水账,过程查 git log」,节号与锚点不位移(doc-links 不因此断链) - 5 篇整表保留(被当唯一证据 / 活台账引用):看门狗设计说明、配置 文件设计说明、RAM预算台账、内容页架构、本地工具路径环境变量方案 - 规范同步:文档规范 §5.1 改新裁定;测试文档模板规范删示例节与 登记句;测试开发标准流程 / UI 两篇 / 实机测试实施指南 / 四篇单测 说明 / 并行任务工作树方案 / 编号规范 的「登记变更记录」改「写进 正文」 - 技能同步:stale-doc-count-sweep / ui-verify / id-family 三个 SKILL 正文不再要求追加流水行(顺带消掉 SKILL.md:121「产品需求 §12」 失效引用) - 修 5 处悬空指针:采样有效性校验目录锚点、BSD 指南「文末变更 记录」、docs/README 过期描述、网络自愈架构两处指向面 72 篇文档净减 600 余行;check_doc_links --all --sections-strict 可判 定比例 61% → 78%、失效引用 0;静态门禁 passed=50 failed=0;gitlint 10 条历史提交信息 0 failed。 未提交他人在飞改动:.dsh/skills/keil-build/*(%SystemDrive% 守卫)。
18 KiB
RemoDispBus 协议与接收设计说明
版本:v1.0 日期:2026-09-10 适用工程:STM32F4-Base(STM32F407ZGTx + CH395F 以太网 / LVGL UI) 范围:UI 远程显示(RemoDispBus)的协议设计与接收解析设计—— 帧格式、字段语义、命令集、交互时序、屏幕位图格式,以及服务端逐字节状态机解析的实现。 对应模块:
Drivers/BSP/UI/ui_net_sink.c(MCU 与模拟器共用同一份源码,协议与解析一致); 平台差异仅在网络后端(MCUnet_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 生成,浏览器打开可无损缩放):
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 典型交互时序
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 个状态,逐字节推进:
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 与轮询循环集成
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 |
要点:
- 单一解析实现:两端均用 §2 的逐字节状态机(
rm_parser_feed),对"帧跨 recv / 粘包 / 前导噪声"统一具备重同步能力,消除旧 MCU"整块解析"的脆弱性。 - 最小帧长判据统一:
remo_handle以n < 5拦截(最短合法帧 5 字节),两端一致——旧 MCUn < 6会丢 5 字节 KEEPLIVE/INIT 帧的隐患已随统一消除。 - 断线重连统一:
ui_net_sink_poll在客户端断开后用net_get_sock(g_listen) == NULL判监听是否失效(MCU 单连接模型下监听随客户端一起失效),失效即bind_listen()重建——两端一致,并修复了 MCU 单连接模型断线后无法重连的隐患。 - 协议兼容:线格式与命令语义不变,任一端都能与 DTU-RemoteLCD 参考客户端互通;双端行为一致已由 e2e 回归(
simulator/tests/check_remo_sink.py,0/25600 位差异)与双端构建(Keil + CMake 均 0 错误 0 警告)验证。 - 驱动者是宿主,不是协议自己:
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§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. 陷阱
- CRC 只覆盖数据段:不含 TAG/CMD/LEN;
LEN=0时 CRC 恒为0。收发两端必须一致,否则校验必败。 - TAG 是方向标识:服务端只接受
0xAA(客户端帧);收到0xBB视为非法(状态机在TAG态丢弃)。 - 大端字节序:
LEN及 payload 内 u16/u32 均大端;MCU(小端核)收发须显式转换(put_u16/put_u32)。 - 位图 MSB 优先:
bit_i = 7 - (x%8);搞反则画面左右镜像/错位。 - 连接后不主动发 INIT:避免污染客户端首帧读取(客户端按 LCDMEM 轮询或显式 INIT 获取)。
- 平台须齐备两个后端:
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)。 - 单连接:
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 |