Files
STM32F4-Base/docs/子系统设计/屏板链路设计说明.md
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

278 lines
28 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.
# 屏板链路设计说明(主机侧)
> **模块**:`Drivers/BSP/ScreenLink/`(`sb_proto` 协议层 + `sb_link` 传输层)· `App/algo/sb_led_map`(状态灯折算)· `App/task/ui_task.c`(宿主)。
> **需求与屏板侧契约**(这条链路要做什么、屏板行为逐字节)在 `docs/产品需求/产品需求设计文档.md` §5.16,本文不重抄其表;
> 本文是**主机侧实现事实**:接线、串口配置、协议口径、键/灯映射、资源、验证状态与未闭环项。
> **屏板固件事实源**:外部工程 `DTU-LCD`(不在本仓,本链路不改它)。
## 1. 链路与角色
- 屏板是 **Modbus 主站**:每 280 ms 发一帧 6 B 召唤(帧 A,`RT_CMD`),有键待报时插一帧带 key 的立即召唤(`KEY_CMD`)。
主机是**从站 `0x01`**:校验收下帧 A,回一帧 4208 B 图像应答(帧 B)。**主机从不主动推送。**
- **严格一召唤 → 一答 4208 B**:不提前发、不超发、不叠发。屏板接收溢出即**永久静默**且不可自恢复(它错误回调里的
串口重启被注释掉),只能断电重上或等 ≈83 min 自复位——多发是唯一风险源,busy 冻结在主机侧挡死。
- **一份显存、两个出口**:串口(本链路)为主,TCP 7003 网络为镜像;两出口读**同一份活显存**
(`ui_port_get_framebuffer()`),无第二份账可漂。
- **宿主 = `uiTask`(组帧 + 注键),起发全部在中断 = USART1 空闲中断(周期召唤直答)+ TIM3 1 kHz 节拍
(按键召唤的延迟应答放行,§7.1)**。不新建任务、不占堆、
不加看门狗位(产品 `required_mask` 维持 `0x1F7`,起发在中断、与任务心跳无关)。
```mermaid
flowchart TB
subgraph UITASK["uiTask(10 ms 一拍)"]
R["ui_port_step 渲染"] --> F["sb_link_frame_update(活显存, 灯掩码)"]
F -->|"点阵/掩码 memcmp 没变(~20 µs)"| R
F -->|TX busy → BSP_ERR_BUSY| R
F -->|变了 → 组帧 + 自校验| S["s_frame 就绪"]
K["sb_link_key_take 出队"] --> M["ui_task_board_key_map 映射"] --> Q["ui_port_queue_key 注键"]
Q --> SV["ui_port_service_keys 同拍立刻重绘"] --> F
P["sb_link_poll 统计 + 离线判定"]
end
subgraph ISRCTX["中断上下文(两个起发源)"]
A["sb_link_on_rx_event:校 6 B 帧 A"] -->|"key ≠ 0"| KQ["按键入队 + 扣住本答(不起发)"]
A -->|周期召唤 且 s_frame 就绪| D["sb_tx_kick 点 TX DMA"]
T["sb_link_ms_tick(TIM3 1 kHz)"] -->|"凭据→放新帧 / 扣满 140 ms→发旧帧"| D
A -->|无条件| A2["重挂 ReceiveToIdle_IT"]
end
S -.-> D
S -.->|"组帧 OK 且本拍注过键 → 交凭据"| T
KQ -.-> K
D --> TC["DMA2_Stream7 传完中断"] --> C["清 busy(攒了 dirty → 下一拍补组)"]
```
分层铁律:`sb_proto` 纯逻辑(不碰 HAL / 寄存器 / 任务),原样 include 进 `test/host/` 跑单测;
`sb_link` 依赖 HAL 与中断上下文,仅 MCU(`new-source-file` 双端注册时走 MCU-only 豁免)。
## 2. 串口与端口配置
| 项 | 值 | 说明 |
|---|---|---|
| 端口 | **USART1** PA9 / PA10(`U5` 子板连接器网络 `ST_TX0` / `ST_RX0`) | MCU 引脚直出 TTL,**不经任何收发器**,无 DE、全双工两根独立线 |
| 波特率 | 700000 8N1,实测 `BRR = 0x0078`(APB2 84 MHz → 7.5,误差 0 %) | 开机日志自证(`sb_link_init` 交叉核对,对不上报 `BSP_ERR_STATE`) |
| 波特率落点 | `Src/usart.c` 的 `USER CODE BEGIN USART1_Init 2` 覆盖 `huart1.Init` 后重调 `HAL_UART_Init` | **`.ioc` 不保存任何 UART 的波特率**,GUI 里改也不落盘 |
| RX | **16 B 缓冲 + IDLE 中断**(`HAL_UARTEx_ReceiveToIdle_IT`) | 屏板只发 6 B,别照抄它侧的 4208 B;**一次性 API,每次回调必须重挂**,漏一次 = 永久静默;**不开 RX DMA** |
| TX | **整帧交 TX DMA**:`DMA2_Stream7` CH4(NVIC 优先级 9,`Src/dma.c` 已配、`Src/stm32f4xx_it.c` 已接 IRQ) | CubeMX 预置、全工程原本没人用;`.ioc` 无需为此改动;帧缓冲必须在 SRAM(**CCM 对 DMA1/2 物理不可达**,放错段 = 静默发旧数据) |
| 全局 UART 回调 | `HAL_UARTEx_RxEventCallback` / `HAL_UART_ErrorCallback` / `HAL_UART_TxCpltCallback` 的**唯一实现处在 `Drivers/BSP/RS485/rs485.c`**,各含一行 `sb_link_on_*` 转发;按句柄过滤在 `sb_link` 内做 | 三个都是全局单例:再定义一份就是链接期 multiple definition;收在同一文件,下一个加通道的人一眼看到全部 |
| 错误处理 | 按 **SR → DR 顺序**清 ORE/NE/FE/PE;**重挂判据用 `RxState == READY` 而非错误位**(ORE 阻塞型须重挂,NE/FE 非阻塞、接收还在跑,再挂只拿 `HAL_BUSY`);TX 被错误打断时按 `gState != BUSY_TX` 清残留 busy | ORE 锁存 = 接收再也进不来;残留 busy = 之后每次召唤都被判"上一帧还在发"→ rx 涨 tx 冻结 |
| 链路监视 | `rx_ok` 计数 **≥10 s 不前进 → 判离线**(`SB_LINK_OFFLINE_MS`),恢复由下一次有效召唤;5 s 统计行只在计数有变化时打 | 屏板 ≈5000 s 自复位有 ≈1 s 空窗,按 500 ms 判会把"歇口气"判成离线反复跳;屏板无背光 100 s 熄灭也**不是**掉线(按键即复亮) |
## 3. 物理接线
### 3.1 主机 ↔ 屏板(TTL 直连,PA10 已接线、无需飞线)
| 主机侧(V0.1) | 屏板侧 | 方向 |
|---|---|---|
| `U5.A10`(网络 `ST_TX0`,PA9) | 屏板排针**丝印 `RX`** | 主机发 → 屏板收 |
| `U5.B10`(网络 `ST_RX0`,PA10) | 屏板排针**丝印 `TX`** | 屏板发 → 主机收 |
| 板地 `CGND`(或任一 GND 脚) | 屏板 `GND` | **两板各自供电必须接第三根共地线**,浮地的形态同样是彻底收不到 |
- 丝印 `TX/RX` 是**屏板自己发出/接收的视角**,所以主机侧**交叉同名接**:主机 TX 接丝印 `RX`、主机 RX 接丝印 `TX`。
- 屏板排针收发脚与屏板网表(PA2=发 / PA3=收)的对应关系未经屏板方确认(实测按网表接不通、对调才通)。
**唯一实操验收口径 = 接完看主机 `rx` 计数涨不涨**(涨 = 方向对,与帧内容无关),不涨当场对调两根信号线;
产品线束定版前向屏板方核实实际收发脚。**不要按屏板 readme 与"TX 对 TX"直连**——同边对接两端都静默且无任何报错可看。
- **别去 CN1 找 USART1 端子**:`ST_TX0` / `ST_RX0` 在网表里只连 `U5.A10` / `U5.B10` 与 MCU 101/102 脚;
CN1 上的 `RS232_*` 是 UART5 / USART2 经收发器之后的电平。同一个 `U5` 上还挂着 USART3 的 `ST_TX2` / `ST_RX2`
(`U5.C7` / `U5.C6`),别抓错针。3.3 V TTL 直连,不串电阻,没有方向控制脚可接。
### 3.2 台架日志口(RS232#1)
- 台架须用 **USB 转 RS-232 适配器**(TTL 的 CH340 直连不了,且 RS-232 的 ±电平可能打坏 `U13` TPT3232E 或适配器):
适配器 `TXD` → `CN1.10`(板子 `RS232_RX1`)、`RXD` → `CN1.12`(板子 `RS232_TX1`)、`GND` → `CN1.7` 或 `CN1.8`(`CGND`)。
- 台架串口因此是 **`COM3`**,`.serialtest/config.json` 的 `portName` 已按此配置,`/serial-boot-test` 与 `serial-assistant` 不必手工指端口。
- RS232#1 **没有 TTL 出口**(`ST_TX1`/`ST_RX1` 只连 `U9` 隔离器与 MCU),唯一外部出口是 CN1 的 RS-232 端子;
`U14` 是 RS232#3 的网口座,本通道用不上。
### 3.3 PC 顶替屏板(无屏板实物的台架验证)
- USB 转 TTL 适配器:`TXD` → `U5.B10`、`RXD` → `U5.A10`、`GND` → 板地;700000 8N1;
激励脚本 `test/host/screenlink_bench.py` 按屏板节拍发 6 B 帧 A(`--key-fixed` 持续改画面取组帧样本、
`--glitch-baud` 灌噪声验自愈、`--reply` 收应答)。
- ⚠ **CH340 收不了 700 kbaud 连续流**(RX FIFO 几十字节 + USB 每 1 ms 才搬一次,实测只收到 ≈2373 B / 9.5 kB/s):
**收不全 ≠ 固件没发**,同轮 `tx == done` 即自证;要收全 4208 B 帧验内容须换 FTDI / CP210x。
## 4. 协议实现口径
帧 A / 帧 B 的逐字段表与屏板时序常量在 `docs/产品需求/产品需求设计文档.md` §5.16.2,此处只记主机侧实现事实:
- **CRC-16/MODBUS**(`0xA001` 反射、初值 `0xFFFF`、16 位合并表):帧 A 覆盖前 4 B、帧 B 覆盖 `[0, 4205]`,
**一律低字节在前**(标准 Modbus RTU 线上序)。屏板源码 `Calculate_CRC()` 的返回值与写入处注释把高低**都写反了**
(`123456789` 它返回 `0x374B`,标准值 `0x4B37`;两处名字反、线上字节对)——照字面实现成"高字节在前",
屏板会把每一帧判成 CRC 错,而它超时判定只看"收没收到一整包"、坏帧也清超时计数,现场只表现为"画面不刷新"。
实测向量钉在 `test/host/sb_proto/sb_proto_test.c`(`01 03 01 00 F0 48`、`01 03 00 09 31 DE`)。
- **帧 B 正好 4208 B**:3200 显存 + 4 B 灯掩码(小端)+ `info[4] = info[5] = 0x00`(普通图分支)+ `info[6..7]` 保留 +
**998 B 填充固定 `0x00`(屏板不读但参与 CRC)** + 2 B CRC。短一字节屏板就在 `DR[4206]` 读到陈旧字节 → CRC 必失败
静默丢弃;多发会把它正在处理的那一帧撕裂(收满即回调、缓冲从头再填)。
- **LOGO 形态不实现、不下发**(出局):总线上不会出现 `info[4..5] = 0x89 0x45` 的帧;`sb_proto` 里既有的
`sb_frame_b_build_logo()` 与魔数常量为待删除遗留(§9.2),勿新增调用。
- **组帧**(`sb_link_frame_update`,uiTask 每拍):拿帧缓冲里的点阵段与活显存 memcmp,变了才组 + `sb_frame_b_verify`
自校验,不过不置 ready(宁可不发旧帧)。**不留显存影子副本**(省 3200 B SRAM 与每拍重算 CRC,代价只是每拍一次
~20 µs 的 memcmp)。**TX DMA busy 期间冻结帧缓冲**:DMA 读的就是 `s_frame` 本体,中途改写会发出半新半旧的"脏帧"
——CRC 仍对、屏板照收、画面撕裂,极难归因;忙就 `BSP_ERR_BUSY` 下一拍补组(双缓冲被否:最坏慢 10 ms,
280 ms 周期无感,不值再花 4208 B)。
- **时序事实**(屏板实跑 **705 882 bps**——它自己 36 MHz 时钟比标称高 0.84 %,判据换算按它、不按 700 000):
1 字符 ≈14.17 µs,整帧 4208 × 10 bit ≈ **59.6 ms**(8N1)。
- **断帧窗口**:屏板按"总线空闲 1 个字符时间"断帧且无接收超时寄存器可调——**间隙 ≥2 字符(≈28 µs)必把一帧
切成两半**,两半都过不了 CRC。这是"必须整帧一次交 DMA"的全部理由:任务里逐字节轮询 `DR`,一次几十 µs 的
ISR 抢占(TIM3 采样、SysTick、SPI/CH395F DMA)就毁帧,而屏板不报超时、不可归因。
- **起发留量**:距召唤帧末位 ≥30 µs(2 个字符时间,等屏板发完放开它自己的 DE 脚)再起发,无上限要求。
- **屏板的 205 ms 不是帧作废线**:到点只置"可以重发"标志,不清缓冲、不重启串口,**迟到的完整帧照样显示**
(帧里没序号);真硬的是下一轮召唤——无键 280 ms、**有键待报最早 ≈205 ms 就再来一帧**,上一答还在总线上
就是双向对撞。所以"≤200 ms 内发完"仍要做,理由是**防撞总线 + 不多一轮画面滞后**。
- **按键只发一次不重试**(屏板发完即清备份):错过那一轮 = 丢键——这是起发必须留在中断(µs 级)、
不能挂在任何任务态的直接原因。`uiTask` 属"数秒量级等待"那一族(netTask 忙等推导上界 ≈4 s、
对时 relisten 突发实测单轮 800 ms 以上缺席),屏板只给 205/280 ms,任务态起发救不了。
- **按键上行**:中断校验帧 A 后 `key ≠ 0` 入环形队列(可存 3 个,满了丢**最**新的一帧并计数——旧键已在路上),
`uiTask` 出队 → 映射 → 注键;**注键不在中断里**。
- **按键召唤延迟应答**(2026-10-30,`SB_KEY_RESP_MAX_MS = 140`):`key ≠ 0` 的帧 A 不再在 IDLE 中断里直接点
DMA 起发——召唤到达那一刻 `uiTask` 还没消化这个键,此刻发出必然是"按键前"的画面,白发一帧把反馈推给下一轮
召唤。现在中断只入队**并扣住本答**;`uiTask` 同拍排空注键 + 同步重绘(`ui_port_service_keys()`)+ 组帧返回
`BSP_OK` 后交 `sb_link_key_rendered()` 凭据;TIM3 1 kHz 的 `sb_link_ms_tick()` 见凭据即起发**带键的新帧**。
扣满 140 ms 保底发旧帧(140 + 59.6 线上 ≈ 199.6 ms < 205 ms 屏板放弃线,「一召一答」不破);线忙则同旧丢单、
**不留扣件**(扣一个没人应答过的召唤 = 之后凭空多发一帧)。起发从此**两个中断源**(IDLE 直答 + TIM3 放行),
`sb_tx_kick()` 的"判忙→占位"在 PRIMASK 临界区原子完成——交错起发 = 线上中段叠帧。完整延迟账本见 §7.1。
## 5. 键值映射(帧 A `key` → `UI_KEY_*`)
| 屏板 `key` | 语义 | 映射 | 备注 |
|---|---|---|---|
| 0 | 无键 | — | 不产生事件、不占队列 |
| 1 / 2 / 3 / 4 | ENTER / UP / DOWN / ESC | `0x20` / `0x02` / `0x40` / `0x01` | |
| 7 / 8 | LEFT / RIGHT | `0x10` / `0x08` | |
| 9 / 10 | RESET / FACTORY | **丢弃 + 一行日志**(`unmapped`) | 语义待产品裁定(映射复位 / 映射 ESC / 不映射);10 无物理入口理论收不到 |
| 5 / 6 | ADD / DEC | — | 屏板 `key_configs[]` 仅 7 项,**无物理入口**,不会出现——别按屏板 readme 的 8 键表实现 |
- 映射表落点在 `ui_task.c` 的 `ui_task_board_key_map()`;`sb_link_key_take()` 只吐**屏板原始键号**——键号是协议
语义、UI 键码是 UI 语义,汇合归 uiTask(那一层才知道当前哪个页面在接键)。
- 与 7003 网络的注入(`ui_net_sink_inject_key()`)**同一个输入队列**(都走 `ui_port_queue_key`),两路无需互斥。
## 6. 状态灯映射(帧 B `info[0..3]`,32 位小端)
权威定义 = `App/algo/sb_led_map.h` 位宏 + `sb_led_map_eval()`(**映射的唯一出处**,折算不散落调用点)。
`uiTask` 每拍现采四路状态源 → 掩码进 `sb_link_frame_update()`;掩码变了帧就重组,走 busy/dirty 既有通路,无新机制。
| 位 | 语义 | 来源 |
|---|---|---|
| bit1 | 运行心跳 | 500 ms 半周期翻转,与主板 LED1~3 同拍 |
| bit2 | 告警 | `prot_elm_active_mask() != 0` |
| bit3 | 通信(104 RUN) | `iec104_serv_link_run()` |
| bit4 | 采样故障 | `adc_health_flags()` 电平位(不选 sticky——灯要能灭) |
| bit5 | 保护启动 | `prot_elm_pick_mask() != 0` |
| bit0 | 恒 0 | ID 0 电源灯硬件常亮,屏板不看 bit0 |
| bit6~31 | 恒 0 | 保留,扩展从 ID 6 顺序取 |
- 折算做成**纯函数分支**而不是 const 表 + 谓词回调:5 路输入判据形态各异(电平 4 路 + 闪烁 1 路),
纯函数直白且可单测;主机单测 `test/host/sb_led_map/` 钉「掩码 & ~定义集 == 0」,改表漏测会被套件拦住。
- ⚠ 产品构建下 bit2 / bit5 **恒灭**(保护判据未标定闭锁,`ADC_CAL_F_VALID` 未置位)——验证这两路要等标准源
标定后或临时构建(与屏上「保护动作 N」同一已知口径)。
## 7. 资源与耗时(`.map` / 日志可复核)
| 项 | 值 | 说明 |
|---|---|---|
| `s_frame` | 4208 B @ `0x20006d98` | `Execution Region RW_IRAM1`(`0x20000000` 起),**不在 `RW_CCM`** |
| `s_rx_buf` / `s_stats` 等 | 16 + 84 B | 同段 |
| 整模块增量 | Code ≈ +1.5 KB(237,464 → 238,936 B) | `.map` |
| 全镜像 | RW_IRAM1 85,904 B(65.5 %)/ RW_CCM 57,376 B(87.9 %) | 凭证 `scripts/size_attestation.json`(门禁红线 92 %) |
| 任务 / 堆 / 看门狗 | **零新增** | uiTask 宿主、静态缓冲、`required_mask` 不变 |
| 组帧耗时 | **509 µs 典型 / 812 µs 峰值,预算 ≤1 ms**;没变只付 memcmp ≈20 µs | 台架 `us=` / `max=` 字段 |
| 耗时结构 | ≈480 µs 全在 CRC:每帧算满 4206 B,168 MHz 查表 ≈19~20 周期/字节,512 B 表驻 Flash(5 等待周期);清零+拷贝只 ≈20 µs。**占空比无约束**:一帧 280 ms ≈0.2 %,3.5 帧/s ≈0.17 % CPU | 压降两条路已评估**未实施**:表搬 SRAM(≈ −125 µs,+512 B SRAM)、998 B 零段 GF(2) 推进变换(≈ −89 µs,需长期维护矩阵代码)——这个占空比换不来收益;另循环变量用 `uint32_t` 防 ARMCC 每轮多一条 `UXTH`(`sb_crc16`) |
### 7.1 按键反馈延迟:根因账本与 2026-10-30 修复(延迟应答)
**背景**:用户报告真机 LCD 与模拟器按键反馈都慢(按下→看到画面变化)。模拟器插桩实测
(SDL 事件→键回调→REFR_READY,40 键)内部路径 **avg 0.68 ms / max 1.2 ms**——窗口自身的
代码路径不是量级来源;若现场 SDL 窗口仍感觉慢,剩余候选是旧构建 / vsync 呈现 /
远端查看器(7003 路径),另行取证。**真机的慢是结构性的**,账本如下。
**修复前(2026-10-29 基线)的账**:键 → 屏板扫描(外部,ms 级)→ 帧 A 立刻发出(屏板按键不等
周期轮)→ 主机 IDLE 中断入队并**立刻起发当前帧**——可此刻 `uiTask` 还没消化这个键(10 ms
节拍 + 渲染地板 61.3 ms 占住,见 §7 表)→ **这一答发出去的是按键之前的画面(白发)**;
新内容只能随下一次召唤带出。三个结构性主因:①键召唤答旧帧;②`uiTask` FULL 整屏渲染地板
61.3 ms;③召唤周期 ~275-280 ms(带键最早 205 ms)。放大器④:远端查看器在连时
`ui_net_sink_poll()` 可阻塞到秒级(登记簿「远端推流阻塞 uiTask」专项,本轮只裁案不做)。
典型 **~250-450 ms**、最坏 **~500 ms**;④ 发作时秒级。
**修复后(2026-10-30,用户裁定三处全做)**:
| 面 | 改动 | 省掉 |
|---|---|---|
| **延迟应答(核心)** | IDLE 中断收按键召唤 → 只入队、**扣住本答不发**;`uiTask` 本拍排空注键 + `ui_port_service_keys()`(立刻 `lv_indev_read`+`lv_refr_now`,各省一个 ≤20 ms 的 LVGL 定时轮)+ 组帧 OK 后交 `sb_link_key_rendered()` 凭据;TIM3 1 kHz 的 `sb_link_ms_tick()` 见凭据即起发**带键的新帧** | 整轮"答旧帧"≈140-280 ms |
| 闸门 | 起发从此**两个中断源**(IDLE 直答 + TIM3 放行)→ `sb_tx_kick()` 内 PRIMASK 临界区原子占位;交错起发 = 叠发毁帧,闸必须有 | — |
| 截止 | `SB_KEY_RESP_MAX_MS = 140`:uiTask 饿死(对时 reconcile 突发那类)扣满即发旧帧保底——140 + 59.6 线上 ≈ 199.6 ms < 205 ms 屏板放弃线,「一召一答」不破;线忙则同旧丢单不留扣件 | 协议安全边界不动 |
| 注键环 | `ui_port` 注键从单槽改 **8 深环**(满丢最旧+计数)——连按不覆盖 | 连按丢键 |
| 渲染收窄 | `view_apply_sel()` 选中移动且滚动未变时只 invalidate **两行带**(旧选中行+新选中行),整框 invalidate 退为滚动/首刷兜底 | 选中移动类的整屏重绘(61.3 ms 地板页里的大头) |
| 模拟器 | 主循环 sleep 扣掉本轮实际耗时(旧代码在 `lv_refr_now` 前算 sleep_ms,每轮多睡一段工作量) | 每轮一段过冲 |
**修复后的账**:召唤 → 下一拍 ≤10 ms → 注键+重绘(收窄后选中移动为几 ms 级;页面地板仍
61.3 ms)→ 组帧 ~0.5 ms → 放行 ≤1 ms → 线上 59.6 ms → **典型 ~70-135 ms**(修复前
250-450 ms);uiTask 饿死场景 **~200 ms 保底发旧帧**(修复前同场景秒级且白发一帧)。
统计口径:TX 上报行新增 `dfok=`(新帧放行次数)与 `dfold=`(扣满 140 ms 发旧帧次数——
**dfold 在涨 = uiTask 被饿 ≥140 ms 的直接证据**,与登记簿「reconcile 突发饿死 uiTask」对拍)。
**没动的**:屏板固件(外部工程 DTU-LCD,用户裁定不改)、召唤节奏(280 ms 是屏板的,主机
单方面把"下一次召唤"的期望值提前不了)、7003 解耦(只留方案在登记簿)。主机单测
`TC-SBLINK-030~033` 钉住扣件/放行/截止/四边界(套件 164/164)。
## 8. 对外接口(单一出处在各头文件,这里只列形状)
| 接口 | 上下文 | 作用 |
|---|---|---|
| `sb_link_init(&huart1)` | 启动,`app_main.c` 步骤 2b | 交叉核对波特率(对不上 `BSP_ERR_STATE`)+ 挂首轮接收 + 使能 DWT |
| `sb_link_receive_start()` | 中断/任务 | 重挂 IDLE 接收(一次性 API,漏一次永久静默) |
| `sb_link_on_rx_event(huart,size)` | 中断(rs485.c 转发) | 帧 A 校验、计数、按键入队、**周期召唤点 TX DMA 直答;按键召唤扣件延迟应答(§7.1)**、重挂 |
| `sb_link_on_error(huart)` | 中断(rs485.c 转发) | 按 SR→DR 清错、按 `RxState` 决定重挂 RX、按 `gState` 清被打断 TX 的残留 busy |
| `sb_link_on_tx_cplt(huart)` | 中断(rs485.c 转发) | 清 busy、计数;**不在中断里组帧** |
| `sb_link_frame_update(bmp,mask)` | uiTask | 按需组帧(memcmp 判变化),busy 返 `BSP_ERR_BUSY`,自校验不过不发 |
| `sb_link_key_take()` | uiTask | 出队屏板原始键号(0 = 空);映射归 uiTask |
| `sb_link_key_rendered()` | uiTask | 交"键已画进画面、帧==活显存"凭据(组帧 `BSP_OK` 拍才准交);延迟应答的放行钥匙 |
| `sb_link_ms_tick()` | 中断(TIM3 1 kHz,`Src/main.c` USER CODE) | 扣件的毫秒时钟:见凭据放新帧(`dfok`)、扣满 140 ms 发旧帧保底(`dfold`) |
| `sb_link_poll(now_ms)` | uiTask | 离线判定(每拍)+ 5 s 变化才打的统计行(RX/TX 两行自分频) |
| `sb_link_get_stats(&st)` | 任意上下文 | 计数快照(rx/ok/crc/len/fmt/keys/tx/done/drop/fb/bad/us/off/dfok/dfold…) |
| `sb_crc16` / `sb_frame_a_parse` / `sb_frame_b_build_image` / `sb_frame_b_verify` | 纯逻辑 | 协议层四件套,主机单测可回归 |
| `sb_frame_b_build_logo` | 纯逻辑 | **待删除**(LOGO 出局,勿新增调用,§9.2) |
## 9. 验证状态
### 9.1 已验证
| 层级 | 证据(计数名 = 台架/真机日志 `[SBLINK]` 行字段) |
|---|---|
| 主机单测 | `test/host/sb_proto/` **59/59**(屏板实测向量、CRC 表与屏板查表逐字节交叉验证);`test/host/sb_link/` **164/164**(ORE 的 SR→DR 顺序在桩里真执行、非阻塞错误不得重挂、busy 期间帧缓冲逐字节冻结、帧 B 逐字段、周期→µs 换算、离线 9.999 s/10 s 边界与恢复、按键 FIFO 与溢出丢新;2026-10-30 延迟应答 +18:不立刻起发/凭据放行新帧/140 ms 截止保底旧帧/忙时不留扣件/陈旧凭据不放行/首帧未备同旧丢单);`test/host/sb_led_map/` **17/17** |
| 台架(PC + USB 转 TTL 顶替屏板) | `rx == 发送帧数`、`ok == rx`、`crc=0 len=0 size=6`;错波特率灌 64 B 垃圾后**自动恢复**(`rearm=0`,一次都没真失败);杜邦线噪声形态 `bits=0x02`(NE)每帧 ~1.3 次但数据零错;激励停止 10 s 准点 OFFLINE |
| 真屏板冒烟(2026-10-29) | 5 s ≈18 帧 → 278 ms/帧,与屏板 280 ms 节拍吻合;`tx==done`、一召一答无双发;**屏板出图(用户目视确认)**——屏板收帧只有"CRC 过 → 刷屏 / 不过 → 静默丢弃"两种结局且无报错通道,**出图 = 屏板亲手端到端验收了帧长 4208、CRC 覆盖 `[0,4205]`、低字节在前、1 bpp 20 B/行 MSB 在前的布局**,这是任何主机侧计数都给不出的一条证据 |
| 真屏板 10 min 连续(2026-10-29) | `rx=4258 ok=4258 crc=0 len=0 fmt=0 err=0 bits=0x00 rearm=0`(**连错误中断都没进过**);`tx=4249 done=4248`(`drop=9` 全在开机头两秒帧未就绪期,窗口内零新增);`keys=53 kq=0`(逐键必到、零丢弃);`fb=902 bad=0`(每帧自校验通过);组帧峰值 `us=687 max=812` < 1 ms 预算 |
| 双通道并发(2026-10-30,真屏板在线) | 7003 侧 `test/host/remolcd_pull_test.py` INIT→LCDMEM 10 Hz 重压两轮 **1500/1500 + 700/700**、CRC 错 0、超时 0,往返 min 35 / avg 63 / max 131 ms;屏板侧 60 s 隔离窗口 rx +233 全收、**每召必答**、`drop` 零新增、`off=0`;画面一致性由"两出口同一份活显存"机制保证 |
| 离线与恢复 | 断接期间 `off=1`,恢复后 `off=0`——`offline` 标志**只有**恢复分支会清,1→0 的转变即恢复路径执行过的证明(全程未重启主机) |
### 9.2 未闭环
| 项 | 内容 |
|---|---|
| 2 h 长稳 | 跨一次屏板自复位(≈5000 s + 余量):零误判 OFFLINE、零 kerr/abort、自动续上 |
| 仪器项 | 逻辑分析仪:整帧期间无 ≥28 µs 间隙、召唤末位→应答首位 ≥30 µs、59.6 ms 整帧时长;FTDI/CP210x 收满 4208 B 后与 `g_ui_fbuf` 逐字节比对 |
| uiTask 拥塞面 | 制造 7003/netTask 拥塞(客户端不读、对时 relisten 突发),屏板侧应零超时、画面停最后一帧——"起发不在任务态"的直接证据 |
| 目视项 | 7 个物理键逐个按、动作对照 `docs/产品需求/产品需求设计文档.md` §8.2 菜单树 + 键 9 出 `unmapped` 日志;状态灯逐位(bit1 肉眼即见;bit3 挂 104 主站看亮灭;bit4 拔采样前端;bit2/bit5 见 §6 闭锁提示);主动拔插回归(含 7003 并发面) |
| 剖析对照 | 画面静止 1 min,开屏板链路前后对比 `[PROF] uiw`(memcmp ~20 µs 成本判据) |
| 待删除 | `sb_proto` 的 `sb_frame_b_build_logo()` + 魔数常量与对应单测用例(LOGO 出局遗留,现仅"勿新增调用") |
| 待确认 | 屏板排针实际收发脚方向(向屏板方,线束定版前);键 9/10 语义(产品侧) |
| 观察项 | 一次上电累计 `len=7` 共 7 次(IDLE 断帧尺寸 ≠ 6),出现窗口夹在"烧录后冒烟"与"首轮 150 s 重压结束"之间、不能排除与重压相关;但同固件上一上电周期 10 min 窗 `len=0`,第二轮 60 s 隔离窗口**零新增**,全程 `crc=0 fmt=0`、`ok` 增速与 `rx` 同频——功能影响为零。若再现首查:那包是否伴随 `err` 位(NE 噪声尾巴粘进下一包 = 无屏蔽杜邦线的正常形态) |
## 10. 坑账本
1. **`/serial-boot-test` 的 forbidden 标记(`FAIL;ERROR;HardFault;[ERR]`)大小写不敏感**——日志字段名带 fail / error / abort
会把启动自检判红,本模块统计行用 `kerr=` / `abor=` / `off=` 命名。
2. **`ReceiveToIdle_IT` 是一次性的**:漏重挂 = 链路永久静默,且没有任何报错;ORE 不自己清,一次噪声打死接收。
3. **NE/FE 非阻塞、ORE 阻塞**:重挂判据用 `RxState == READY` 而不是错误位;台架"NE 每帧出现 + `rx_ok` 照涨"
= 能用的告警,**不是**故障(无屏蔽杜邦线常态);真故障形态是 ORE(`bits=0x08`),本链路 6 B/280 ms 不该出现。
4. **CH340 收不了 700 kbaud 全帧**(§3.3)——收不全 ≠ 固件没发,`tx == done` 自证,别据此改固件。
5. **`.ioc` 不保存 UART 波特率**:改波特率只能在 `USER CODE` 区覆盖 `Init` 后重调 `HAL_UART_Init`(§2)。
6. **全局 UART 回调是工程单例**:新通道只在 `rs485.c` 加转发行、不自写一份(§2)——"中断进来了但没人处理"
的表象是"屏板一直超时",极易误判成协议错。
7. **帧缓冲不可上栈、不可进 CCM**(§7)——放错段不报错,只是 DMA 读不到、发出去的是旧数据。
8. **`info[0..3]` 双解释**:先判 `info[4..5]` 魔数再解释这 4 字节(普通图 = LED 掩码、LOGO = 宽高),
顺序反了会把 LED 掩码当宽高。
9. **纯注释改动也会让体积凭证报"不同码"**:先重建、再 `ram_baseline.py --update`。
10. **按键召唤"立刻应答"发出去的必然是按键前的帧**(2026-10-30 前的默认行为):屏板按键召唤来得比
`uiTask` 消化键快一到两个数量级,"有答"不等于"有新画面"——排查"按了键屏不变化"先对 `dfok=`
计数,别顺着协议层找;延迟应答机制与账本见 §4 末条与 §7.1。