按键召唤答的是按键前的旧帧:uiTask 还没消化键答案就发走了,新画面 只能等下一次周期召唤(280 ms/轮)才带得出——真机反馈 250-450 ms 的 主构成(根因账本 docs/子系统设计/屏板链路设计说明.md §7.1)。 改动(用户批准范围 A+B+C;屏板固件未动,决策三不变): - sb_link:按键召唤改为只入队不起发;uiTask 同拍重绘并组帧成功后交 凭据(sb_link_key_rendered),TIM3 1 kHz 的 sb_link_ms_tick 以 ≤1 ms 放行新帧。140 ms 截止(SB_KEY_RESP_MAX_MS,+59.6 ms 线上 <205 ms 屏板放弃线)保底发旧帧,"一召一答"契约不破;线忙时同旧丢单、 不留扣件。起发两中断源(USART1 IDLE/TIM3)共用 sb_tx_kick, "判忙→占位"在 PRIMASK 临界区原子完成。 - ui_task:排空注键挪到组帧之前,注键→即派即绘→组帧同拍闭环; 组帧返回 BSP_OK 才交重绘凭据(BUSY 拍帧还没反映按键)。 - ui_port:注键单槽改 8 深环(连按不再互相覆盖,满丢最旧+计数); 新增 ui_port_service_keys:同拍 lv_indev_read + lv_refr_now, 省掉 indev ≤20 ms + 刷新 ≤20 ms 两段等待。 - ui_menu_view:菜单移动选中只失效新旧两行带(黄金参考零像素变化), 滚动/异常回退整框失效。 - simulator:窗口循环 sleep 扣除本轮已耗时(原先无条件睡满 lv_timer_handler 返回值,叠加 Windows Sleep 15.6 ms 粒度放大观感)。 - 串口 TX 上报新增 dfok/dfold(凭据放行新帧数/超时保底旧帧数): dfold 连涨 = uiTask 被饿 ≥140 ms 的直接现场证据。 验证:test/host/sb_link 164/164(新增 18 条:不立刻起发/凭据放行/ 140 ms 截止保底/忙时不留扣件/陈旧凭据不放行等边界);菜单 8 状态 + 内容页 66 张黄金像素 PASS;Keil @build 0 错误 0 警告(两次); BOM 99 文件无漂移、代码规范、doc-links 本次文件零问题、cppcheck PASS。 文档:屏板规划文档已被主干收编进设计说明,延迟账本落 docs/子系统设计/屏板链路设计说明.md §7.1,并同步 §4 口径、§8 接口、 §9.1 单测台账 146→164、§10 坑账本补条。 RAM 台账(分支不跑 --write,并行分支提交约定):本笔盖戳行 RW_IRAM1 94,336 B (72.0%) / RW_CCM 57,376 B (87.9%),较上一台账行 +4,400 B——为距上次台账构建以来分支累计差(含批 3 状态灯等未盖戳 提交),非全部本笔引入;本次自身增量按对象计为 ui_port.o +12 B (8 深键环+丢计数+indev 指针)与 sb_link/o 数十字节级。 gate: only mem-budget/mem-numbers pending lead ledger backfill (合并落主干后 Lead 统一 --write 回填受管区)。
28 KiB
屏板链路设计说明(主机侧)
模块:
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,起发在中断、与任务心跳无关)。
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 熄灭也不是掉线(按键即复亮) |
| 日志口联动 | 控制台已迁 UART5(RS232#1),USART1 专用屏板;收口在 App/log/console_log.c 的 CON_UART* 单点宏 |
两条流共用同一个 USART1->DR 会互相插字节(症状 = 屏板偶发花屏 + 日志乱码);台架端子见 §3.2 |
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 的 ±电平可能打坏
U13TPT3232E 或适配器): 适配器TXD→CN1.10(板子RS232_RX1)、RXD→CN1.12(板子RS232_TX1)、GND→CN1.7或CN1.8(CGND)。 - 台架串口因此是
COM3,.serialtest/config.json的portName已按此配置,@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,任务态起发救不了。
- 断帧窗口:屏板按"总线空闲 1 个字符时间"断帧且无接收超时寄存器可调——间隙 ≥2 字符(≈28 µs)必把一帧
切成两半,两半都过不了 CRC。这是"必须整帧一次交 DMA"的全部理由:任务里逐字节轮询
- 按键上行:中断校验帧 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. 坑账本
@test的 forbidden 标记(FAIL;ERROR;HardFault;[ERR])大小写不敏感——日志字段名带 fail / error / abort 会把启动自检判红,本模块统计行用kerr=/abor=/off=命名。ReceiveToIdle_IT是一次性的:漏重挂 = 链路永久静默,且没有任何报错;ORE 不自己清,一次噪声打死接收。- NE/FE 非阻塞、ORE 阻塞:重挂判据用
RxState == READY而不是错误位;台架"NE 每帧出现 +rx_ok照涨" = 能用的告警,不是故障(无屏蔽杜邦线常态);真故障形态是 ORE(bits=0x08),本链路 6 B/280 ms 不该出现。 - CH340 收不了 700 kbaud 全帧(§3.3)——收不全 ≠ 固件没发,
tx == done自证,别据此改固件。 .ioc不保存 UART 波特率:改波特率只能在USER CODE区覆盖Init后重调HAL_UART_Init(§2)。- 全局 UART 回调是工程单例:新通道只在
rs485.c加转发行、不自写一份(§2)——"中断进来了但没人处理" 的表象是"屏板一直超时",极易误判成协议错。 - 帧缓冲不可上栈、不可进 CCM(§7)——放错段不报错,只是 DMA 读不到、发出去的是旧数据。
info[0..3]双解释:先判info[4..5]魔数再解释这 4 字节(普通图 = LED 掩码、LOGO = 宽高), 顺序反了会把 LED 掩码当宽高。- 纯注释改动也会让体积凭证报"不同码":先重建、再
ram_baseline.py --update。 - 按键召唤"立刻应答"发出去的必然是按键前的帧(2026-10-30 前的默认行为):屏板按键召唤来得比
uiTask消化键快一到两个数量级,"有答"不等于"有新画面"——排查"按了键屏不变化"先对dfok=计数,别顺着协议层找;延迟应答机制与账本见 §4 末条与 §7.1。