docs(屏板链路): 决策三改判为 uiTask 组帧 + 中断点 DMA 起发

用户问「那 200 ms 的阻塞一定存在吗」→ 逐条核屏板固件与主机侧观测面后改判。

- 拆名目:约 60 ms 是整帧的线上占用时间(硬),任务态轮询才是实现选择(可归零),
  200 / 280 ms 是屏板自己的计时器,不是成本
- §1.3:DMA 从「不是必须的」改为「整帧不断帧的唯一保证」,补 USART1 TX DMA 现成、
  全工程无人用、Stream7 无冲突的证据;缓冲必须落 SRAM 不落 CCM
- 决策三:采纳 uiTask 组帧 + USART1 空闲中断点 DMA 起发;不新建任务、不占堆,
  看门狗位掩码不动。原否决理由换成「起发不能挂在有 portMAX_DELAY 等待的循环里,
  抬优先级无效」——业务归属 uiTask 是对的,错的只是把起发挂在它的循环里
- 批 1 / 批 2 判据同步:起发在中断、整帧 59.6 ms 且无 2 字符以上间隙、组帧 ≤0.3 ms,
  2-4 由建任务改为帧缓冲归属与 DMA 收尾
- §4.4 补三条「不要做」、§6 补五行风险、§7 补两项实测;周期口径 275 → 280 ms
- 需求文档 §5.16.2:200 ms 由「硬指标」改为软放弃计时,硬约束改为零间隙 /
  正好 4208 B / 绝不让屏板溢出;补屏板实跑 705 882 bps(BRR=0x33,+0.84 %)
- 需求文档:时序表补第 41 / 56 拍与「有按键待报时最早 205 ms 再召唤」的撞总线窗口,
  判据 8、FR-UI 判据与 §9.13 批次表同步 280 ms 与主机侧计数口径
This commit is contained in:
2026-10-29 12:52:40 +08:00
parent 4da31c93c3
commit 93e057abf2
2 changed files with 127 additions and 41 deletions
+33 -11
View File
@@ -656,7 +656,7 @@ sequenceDiagram
- 实现:LVGL 160×160 1bpp 离屏渲染到 `g_ui_fbuf`(3200 B)→ `ui_net_sink` 经 RemoDispBus 推流;
远端按键经协议注入。**串口屏板通道尚未实现**(§5.16.9 第 11 条)。
- 判据:DTU-RemoteLCD 连 7003 出图(160×160 1bpp)+ 远端菜单导航可用;
屏板实物接入后 275 ms 节拍出图 + 状态灯正确(§5.16.10 第 8~15 条)。
屏板实物接入后 280 ms 节拍出图 + 状态灯正确(§5.16.10 第 8~15 条)。
**FR-UI-2 菜单与内容页**
- 需求:42 节点层级菜单(最大 4 级)+ 内容页(装置信息/交流量/直流量等)。
@@ -1041,6 +1041,11 @@ sequenceDiagram
>
> 若改时钟树或改用 HSI,**必须重算**——700 kbaud 下 2 % 误差已足以丢帧。
>
> **屏板自己那侧不是零误差**:它的串口挂 APB1 = 36 MHz,`UART_BRR_SAMPLING16(36e6, 700000)` →
> `BRR = 0x33`(USARTDIV = 3.1875)→ 实跑 **705 882 bps,比标称高 0.84 %**;它源码里「手动写精确 BRR」
> 那段注释后面**没有对应代码**。与主机侧的 0 % 合起来 0.84 %,8 位数据 / 1 停止位(10 bit/帧)下在容差内;
> 但**位宽与整帧耗时的实测判据要按 705 882 算,不是按 700 000**(1 字符 ≈14.17 µs、整帧 ≈59.6 ms)。
>
> **端口裁定(用户决定,2026-10-28):屏板走 USART1,日志口让位。**
> 屏板链路改用 **USART1(PA9/PA10)**——这条通道是 **MCU 引脚直出 TTL,不经过板上任何收发器**,
> 因此**收发器速率上限问题不复存在**:本板 485 通道上的 `U15 = TP8485E-SR` 只有 250 kbps
@@ -1099,15 +1104,31 @@ CRC-16/MODBUS(多项式 `0xA001` 反转式、初值 `0xFFFF`、查表法 `auch
| 参数 | 屏板侧常量 | 实际时间 | 含义 |
|---|---|---|---|
| 基准节拍 | 5 ms 定时器 | — | `Process_Count()` 每 5 ms 递增 |
| 周期召唤 | `RS485_RT_COUNT_MAX = 55` | **275 ms** | 无按键时每 275 ms 召唤一次 |
| 应答超时 | `RS485_ACK_COUNT_MAX = 40` | **200 ms** | 发帧后 200 ms 未收齐 → 本次超时 |
| 周期召唤 | `RS485_RT_COUNT_MAX = 55`,判据 `RT_count > 55` | **280 ms**(第 56 拍 × 5 ms) | 无按键时每 280 ms 召唤一次;屏板 readme 与常见口径写的 275 ms 是按 55×5 算的 |
| 应答超时 | `RS485_ACK_COUNT_MAX = 40`,判据 `ACK_count > 40` | **205 ms**(第 41 拍 × 5 ms) | 到点只置「可以重发」标志 + 累计一次超时——**不丢已收字节、不清缓冲、不重启串口**;计时起点是召唤帧**最后一位的 TC** |
| 迟到的应答 | 收帧处理与超时判定**相互独立**,帧里没有序号 | — | 超过 205 ms 才到的完整帧**照样显示**(当成下一轮的应答,无从区分);代价 = 多一轮画面滞后 + 一次超时计数 |
| 有按键待报时 | 收到帧或超时即打开发送闸门 | 最早 **约 205 ms** 就可能再来一帧 | 上一答还在总线上 → **双向对撞**;且键**只发一次不重试**(发完立刻清备份)→ 错过那一轮 = **丢键** |
| 收到任意一包 | 无论 CRC 是否通过 | — | 超时阶梯全部清零(`ACK_count` / `ACK_OverTimeCnt`)→ **主机把帧发断,屏板也不报超时**,只表现为画面不更新 |
| 连续超时 | `RS485_ACK_OVERTIME_MAX = 5` | 5 次 | 连续 5 次超时 → 强制允许重发(**不重启串口**,重初始化被注释) |
| 屏板自复位 | `sysRunTime > 1 000 000` | **≈5000 s(83 min)** | 屏板**自己**无条件 `HAL_NVIC_SystemReset()` |
**200 ms 是硬指标**:700 kbaud 8N1 → 10 bit/字节 → 4208 B ≈ 42 080 bit ≈ **60.1 ms**,
占预算 **30 %**,留给"取显存快照 + 组帧"约 140 ms,宽裕。
但若主机把整帧拆成多次 `HAL_UART_Transmit` 且中间被高优先级任务抢占,很容易越线——
**建议一次性 DMA 发送 + 发送完成中断,不要逐块阻塞发**。
**真正的硬约束不是 200 ms,是下面三条**(按风险排序,均出自屏板源码):
1. **整帧零间隙**:屏板按「总线空闲 1 个字符时间」断帧,而它的串口 IP **没有接收超时寄存器**可调
(该系列 USART 只有 SR / DR / BRR / CR1 / CR2 / CR3 / GTPR)。705 882 bps 下 1 字符 ≈14.17 µs,
**间隙 ≥2 字符(约 28 µs)必把一帧切成两帧**,两半都过不了 CRC——**这条才是卡主机实现方式的约束**。
2. **必须正好 4208 B**:CRC 固定取 `DR[4206]/DR[4207]`、覆盖 `DR[0..4205]`,实收长度被丢弃 →
**短帧永远通不过**(998 B 填充是承重的);超过 4208 B 会让它把正在处理的那一帧撕裂(收满即回调、缓冲从头再填)。
3. **绝不让它溢出**:一旦 ORE 就**永久收不到**(它错误回调里的重启被注释掉),而屏幕上没有任何通信状态指示
(运行灯翻转被注释、故障显示函数无人调用)→ 唯一症状是「画面与状态灯一起冻死」,只能断电重上或等
83 min 自复位(自复位后是**白屏**,没有开机 LOGO)。此外它下发 LOGO 时要写 Flash + 4×500 ms 延时,
那段时间既不能处理也不能应答,是最容易撞上本条的窗口。
**所以主机侧整帧必须一次性交给 DMA + 发送完成中断,不要逐块阻塞发**:任务态轮询随时可能被几十 µs 的
ISR 抢占,一抢就撞第 1 条,而后果在屏板侧完全不可见。时间预算 4208 × 10 bit ÷ 705 882 ≈ **59.6 ms**
(按标称 700 000 算是 60.1 ms),占 280 ms 周期约 21 %;「≤200 ms 内发完」仍是我们对自己的要求——
理由是**不与下一轮召唤撞总线、不多一轮画面滞后**,不是因为屏板会把迟到的帧判废。宿主与起发方式见
`docs/任务规划/屏板链路.md` 决策三(`uiTask` 组帧 + USART1 空闲中断点 DMA 起发)。
**主机侧端口重排(2026-10-28 用户裁定)**
@@ -1245,7 +1266,7 @@ for (i = 1; i < LED_COUNT; i++) /* LE
| 机制 | 实现 | 对本产品的影响 |
|---|---|---|
| CRC 错计数 | 连续 ≥3 次错 → 计数清零(**重初始化串口被注释掉**) | 错帧静默丢弃,链路**不会自愈**,只能靠下一次召唤重试 |
| 应答超时 | 200 ms × 连续 5 次 → 强制允许重发 | 主机长时间不应答**不会锁死**屏板,恢复后自动续上 |
| 应答超时 | 第 41 拍 = **205 ms**;连续 5 次 → 只是打开重发闸门(**重初始化被注释掉**) | 主机长时间不应答**不会锁死**屏板,恢复后自动续上。但两个坑:① **收到任意一包就清零计数、不看 CRC** → 主机把帧发断时屏板也不报超时,只表现为画面不更新;② 真会永久锁死的是**接收溢出**(§5.16.2 硬约束 3),不是超时 |
| 系统自复位 | `sysRunTime > 1 000 000`(≈5000 s)→ `HAL_NVIC_SystemReset()` | **每 83 分钟屏板重启一次**,约 1 s 空窗 |
| 看门狗 | 5 ms 定时器内 `Clear_Watchdog()` | 主循环卡死 → 屏板复位 |
| DMA 接收 | 屏板提交记录明写"DMA 接收的问题没有解决" | 长帧接收可靠性**未经充分验证**,台架必须压测 |
@@ -1422,8 +1443,9 @@ for (i = 1; i < LED_COUNT; i++) /* LE
**串口屏板通道(与上面 7 条并列,待建后验收)**:
8. 主机上电后,屏板按 **275 ms** 节拍发帧 A(6 B,形如 `01 03 01 xx CRC_H CRC_L`);
主机每帧在 **≤200 ms** 内回帧 B(**4208 B**,CRC 覆盖 `[0, 4205]`),屏板**连续 10 分钟无超时**。
8. 主机上电后,屏板按 **280 ms** 节拍发帧 A(6 B,形如 `01 03 01 xx CRC_H CRC_L`);主机每帧在
**≤200 ms** 内**发完**帧 B(**4208 B**,整帧无 2 个字符时间以上的间隙,CRC 覆盖 `[0, 4205]`),
**主机侧**连续 10 分钟零超时、零 CRC 错帧(屏板不指示通信状态,计数只能在主机侧取)。
9. 帧 B 前 3200 B 与主机 `g_ui_fbuf` **逐字节一致**(160 行 × 20 B,1 bpp,MSB 在前,bit = 1 → 白)。
10. 屏板 7 个物理键逐个按下 → 主机收到的 `key` 依次为 `1/2/3/4/7/8/9`
(ENTER / UP / DOWN / ESC / LEFT / RIGHT / RESET),界面动作与 §8.2 菜单树一致;
@@ -2483,7 +2505,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5
| 软压板 | 9 面全部可投退且行为可观测(投退后对应功能确实停走/恢复) |
| 出口与开入 | 保护动作 → 出口继电器动作 → 开入回读确认,全链路 ≤ 120 ms |
| 标定解卡 | 8 通道两点标定后 `ADC_CAL_F_VALID` 全置位;注入标准源,保护动作值与定值误差 ≤ 3 % |
| 屏板链路 | **串口通道**:屏板 275 ms 召唤、主机 ≤200 ms 回 **4208 B**,连续 10 min 无超时;显存 3200 B 与 `g_ui_fbuf` 逐字节一致;7 个物理键(`1/2/3/4/7/8/9`)动作正确;**状态灯位掩码逐位一致、LED 0 恒亮**;屏板 ≈5000 s 自复位不误判离线;LOGO 下发后断电重启仍在;**网络通道**:INIT 应答 **16 B** 逐字段一致且无颜色字段、`LCDMEM` 偏移 0 得 3204 B、重复召唤无应答、偏移 ≥3200 无应答、`KEEPLIVE` 无应答且 1 分钟不掉链、8 键(含 `F1\|ENT` 组合)动作正确(§5.16.10 十五条) |
| 屏板链路 | **串口通道**:屏板 280 ms 召唤、主机 ≤200 ms 内**发完** **4208 B**(整帧无 2 个字符时间以上的间隙),连续 10 min **主机侧**零超时、零 CRC 错帧;显存 3200 B 与 `g_ui_fbuf` 逐字节一致;7 个物理键(`1/2/3/4/7/8/9`)动作正确;**状态灯位掩码逐位一致、LED 0 恒亮**;屏板 ≈5000 s 自复位不误判离线;LOGO 下发后断电重启仍在;**网络通道**:INIT 应答 **16 B** 逐字段一致且无颜色字段、`LCDMEM` 偏移 0 得 3204 B、重复召唤无应答、偏移 ≥3200 无应答、`KEEPLIVE` 无应答且 1 分钟不掉链、8 键(含 `F1\|ENT` 组合)动作正确(§5.16.10 十五条) |
---
+94 -30
View File
@@ -34,7 +34,7 @@
| 注入按键 | 同上,`ui_net_sink_inject_key(uint8_t)` | **直接复用**,串口与 7003 走同一入口,无需改 `ui_net_sink.c` |
| 1bpp 显存 | `g_ui_fbuf`(`ui_port.c`),`UI_FB_SIZE = 3200` | 160×160÷8,与屏板 `Picture[3200]` 逐字节同构 |
| UART 外设已初始化 | `Src/usart.c`:USART1/2/3、UART4/5 全部 `MX_*_Init()` 已调用(`Src/main.c:130-137`) | 端口是现成的,只需改波特率(USART1 侧 TTL 直连,没有方向切换) |
| 静态任务分配 | `Inc/FreeRTOSConfig.h` `configSUPPORT_STATIC_ALLOCATION = 1` | 可用 `xTaskCreateStatic`,**不占 RTOS 堆**(堆已满,见 §1.3) |
| 静态任务分配 | `Inc/FreeRTOSConfig.h` `configSUPPORT_STATIC_ALLOCATION = 1` | 走退路(独立任务)时**必须** `xTaskCreateStatic`——堆已满(§1.3);采纳方案不新建任务,这条只是备着 |
### 1.2 缺什么(要新建)
@@ -47,20 +47,34 @@
| 项 | 现值 | 加屏板链路后 | 判断 |
|---|---|---|---|
| `RW_CCM` | 57,376 / 65,280 = **87.9 %**(门禁 92 %) | 不动(用静态任务,不占堆) | ✅ |
| `RW_IRAM1` | 81,536 / 131,072 = **62.2 %** | +4208 B(发送缓冲)≈ **65.4 %** | ✅ 宽裕 |
| `RW_CCM` | 57,376 / 65,280 = **87.9 %**(门禁 92 %) | 不动(不新建任务、不占堆) | ✅ |
| `RW_IRAM1` | 81,536 / 131,072 = **62.2 %** | +4208 B(发送缓冲)≈ **65.4 %**;双缓冲 8416 B ≈ **68.6 %** | ✅ 宽裕 |
| 接收缓冲 | — | **16 B 足够**(屏板只发 6 B 召唤帧) | ✅ 别照抄屏板的 4208 B |
| RTOS 堆 | 57,344 B 配置,**已满** | **不加任务则不动** | ⚠ 新增动态任务会创建失败 |
**DMA 不是必须的**。4208 B 在 700 kbaud 下只占 60 ms,放在**独立任务里阻塞发送**完全可行
(屏板 275 ms 才问一次,200 ms 内答完即可)。省掉 DMA 就省掉了一堆流冲突:
**DMA 不是可选项,是「整帧不断帧」的唯一保证。** 三个数别混:
> **USART1 的 DMA 是现成的**:`.ioc` 里已配 `DMA2_Stream7`(TX,NORMAL)/ `DMA2_Stream2`(RX,CIRCULAR),
> 控制台从没用过(它直接轮询 `USART1->DR`),所以这两条**空着**,要用直接开,不必重新分配流。
| 数 | 是什么 | 硬 / 软 |
|---|---|---|
| **59.6 ms** | 4208 B 的**线上占用时间**(8N1 → 10 bit/字节;屏板实跑 705 882 bps,按标称 700 000 算是 60.1 ms) | 硬(协议 + 物理,省不掉) |
| **约 14 µs** | 屏板的**断帧判据**:它的串口 IP 没有接收超时寄存器,整帧靠「总线空闲 1 个字符时间」收尾;间隙 ≥2 字符(约 28 µs)**必把一帧切成两帧**,两半都过不了 CRC | 硬——**这条才是真正卡实现方式的** |
| **205 ms / 280 ms** | 屏板的**放弃计时**与**下一轮召唤**(第 41 / 56 拍 × 5 ms) | 截止期,不是成本;见决策三与 §7 第 1 条 |
所以「在任务里逐字节轮询 `USART1->DR` 发 60 ms」这种写法,问题不只是占住一个任务——**它随时可能被 ISR 抢走
几十 µs**(本板有 TIM3 1 kHz 采样节拍、SysTick、SPI1/SPI2 DMA 完成、CH395F 中断),一抢就毁帧;而屏板侧
不报超时(它的超时判定只看「收到一整包」、不看 CRC),现场只表现为「画面偶尔不刷新」,极难归因。
**整帧一次性交给 TX DMA、由中断起发,才能同时拿到「CPU 零占用」和「不可能有间隙」。**
> **USART1 的 TX DMA 是现成的、且全工程没人用**:`Src/usart.c`:267-282 已配 `DMA2_Stream7`
> (`DMA_CHANNEL_4` / `DMA_NORMAL` / 字节宽度 / `MINC`)并 `__HAL_LINKDMA` 挂到 `huart1`;
> NVIC 优先级 9(`Src/dma.c`:65-67,满足「外设中断 ≥5」);`Src/stm32f4xx_it.c`:644-652 已接
> `HAL_DMA_IRQHandler(&hdma_usart1_tx)`。在用的是 SPI1(DMA2 Stream0/3)、SPI2(DMA1)、ADC1(Stream4)
> ——**与 Stream7 不冲突**,不必重新分配流,`.ioc` 也不用为 DMA 改任何东西(1-2 只改波特率)。
> 主机侧 RX 仍然只要 16 B + 空闲中断(屏板只发 6 B),**不要开 RX DMA**。
---
## 2. 四个决策(一、二已由用户裁定,三、四待定)
## 2. 四个决策(一、二、三已由用户裁定,四待定)
### 决策一:波特率与物理介质(**已裁定 2026-10-28**)
@@ -90,18 +104,47 @@
> 症状是"屏板偶尔花屏 + 日志偶尔乱码",很容易误判成 CRC 或时序问题);
> ② 屏板链路的错误回调里**要清 ORE**(overrun 一旦锁存,接收再也进不来),否则一次噪声就永久锁死。
### 决策三:谁来跑这个链路(宿主)
### 决策三:谁来跑这个链路(宿主)——**已裁定 2026-10-29:`uiTask` 组帧 + 中断点 DMA 起发,不新建任务**
| 方案 | 优点 | 缺点 |
**先把名目拆开**——原来把两件不同的事写进了同一格,方案就是在这儿走偏的:
| 数 | 它是什么 | 能不能避免 |
|---|---|---|
| **独立任务(`xTaskCreateStatic`)** | 与 `uiTask` 隔离,60 ms 阻塞不拖渲染;不占堆 | 多一个任务要登记;`Src/freertos.c` 是 CubeMX 生成物,**不在那里加** |
| 塞进 `uiTask` | 零新增任务 | 7003 的 `LCDMEM` 应答最坏 ≈4 s,会把 200 ms 应答窗口撑爆 → **违反"双通道互不拖累"判据** |
| 塞进 `defaultTask` | 零新增任务 | 该任务周期 100 ms 量级,6 B 召唤帧可能被延到 >200 ms → 超时 |
| **59.6 ms** | 4208 B 的**线上占用时间**(§1.3) | 不能(协议 + 物理);只有改屏板砍掉 998 B 填充才省 14.3 ms,本项不改屏板 |
| **约 60 ms 的任务阻塞** | 「在任务里逐字节轮询发送」这个**实现选择**的成本 | **能**:整帧交 TX DMA 后,这 60 ms 里 CPU 占用为零 |
| **205 ms / 280 ms** | 屏板的**放弃计时**与**下一轮召唤** | 不是成本,是截止期;真实边界见下面的说明与 §7 第 1 条 |
**建议:独立任务,静态分配**。理由:唯一能满足 200 ms 硬窗口、又不吃堆(堆已满)的做法。
- 栈 **768 B** 足够(调用链只有 `memcpy` + `HAL_UART_Transmit` + 少量局部变量,4208 B 缓冲是静态的,不在栈上)。
- 优先级 **低于 `uiTask`、高于 `defaultTask`**(屏幕是辅助功能,不该抢保护/采集)。
- **不加入看门狗必需位掩码**(`0x37` 不动):屏板链路挂了只是没画面,不该复位整机。
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| **`uiTask` 组帧 + USART1 空闲中断点 DMA 起发** | 这条链路的人机交互语义(组帧、状态灯映射、注键、在离线、LOGO)**全留在 UI 上下文**,与 7003 同一份数据源、同一把笔;零新增任务、不占堆;起发延迟 = 中断延迟(µs 级),与 `uiTask` 这一轮卡不卡无关 | 组帧要占 `uiTask` 一次 `memcpy` + CRC(约 0.15 ms/帧,3.6 帧/s 约 0.05 %);多一个 4208 B 的 SRAM 发送缓冲;帧缓冲与 DMA 读取之间要做 dirty 或双缓冲 | **采纳** |
| 独立任务里**阻塞**发送 | 与 `uiTask` 隔离;不占堆 | 那 60 ms 的 CPU 轮询本身就是断帧风险(§1.3);起发仍要等任务被调度到;多一个任务要登记栈、句柄、台账 | 退路:仅当要求「状态灯在 `uiTask` 卡死期间仍刷新」时才值得 |
| 把起发挂在 `uiTask` 主循环里 | 改动最省 | `uiTask` 主循环现在就有**无上界**的等待:`ui_net_sink_poll()` → `net_accept` → `net_wait_result()` 走到 `xTaskNotifyWait(portMAX_DELAY)`(`App/task/ui_task.c`:505-511、`Drivers/BSP/NET/net_socket.c`:846-861);实测样本 `uisn` 154 ms 仍在段内,残余暴露见 `docs/UI设计/RemoDispBus协议与接收设计说明.md` 的「残留暴露」注(约 4 s)。**这类阻塞是等另一个任务回话——任务不在就绪态,抬优先级救不了** | 否决 |
| 把起发挂在 `defaultTask` | 零新增任务 | 该任务周期 100 ms 量级,还挂 FTL 挂载 / 日志写入等无上界路径 → 起发延迟不可控 | 否决 |
**采纳方案的分工——起发者不等于业务归属**(原来那条否决理由把这两件事混成一件了):
- **`uiTask`(10 ms 一拍)= 业务与组帧**:显存或 LED 掩码有变化时把 4208 B 帧拼好(3200 B 显存 + 4 B 灯 +
`info[4..7]` + 998 B 填充 + 2 B CRC);消费中断留下的 `key` 字节 → `ui_net_sink_inject_key()`
(**注键不能在中断里做**);外加在离线判定、LOGO 一次性下发。
- **USART1 空闲中断 = 唯一起发者**:校 6 B 召唤帧(4 字节 CRC 约 1 µs)→ 错帧丢弃并计数 → 取走 `key` →
点 TX DMA(帧已备好)→ 立刻退出;DMA 传完中断清 busy,攒了 dirty 就让 `uiTask` 重组。
距召唤帧末位**留 ≥30 µs 再起发**(屏板发完才放开它自己那根方向控制脚,按 2 个字符时间留量),无上限要求。
- **两条必须防**:① DMA 正在读时 `uiTask` 改帧 → dirty 标志(传完再重组)或双缓冲(仓里 `meas` 的双区 +
`seq` 是现成先例);② **屏板一次接收溢出就永久收不到、且自己毫无指示**(它错误回调里的重启被注释掉)
→ 主机严格「一召唤 → 一答 4208 B」,**不提前发、不超发、不叠发**。
- **不新建任务**:不动 `Src/freertos.c`、不动 `App/task/task_handles.h`、不碰已满的 RTOS 堆(§1.3);
看门狗位掩码 `0x37` 不动——起发在中断里,与任务心跳无关。
- **屏板那 200 ms 的真实语义**(决定为什么仍按 ≤200 ms 要求自己,但理由要换):它到点只置一个「可以重发」
的标志,不丢已收字节、不清缓冲、不重启串口;**迟到的完整帧照样会被显示**(收帧处理与超时判定相互独立,
帧里也没有序号)。真硬的是 280 ms 那轮召唤——而且**有按键待报时最早 205 ms 就会再来一帧**,
上一答还在总线上就是**双向对撞**。所以 ≤200 ms 内发完仍要做,理由是**防撞总线 + 不多一轮画面滞后**。
- **失败模式说清楚**(别当成零成本):`uiTask` 真卡死时画面本来就没人重绘,谁来发都一样冻;本方案的区别是
**不撞总线、不丢键、不进屏板那套超时阶梯,`uiTask` 一醒立刻恢复**。独立任务方案唯一独占的价值是
**状态灯**(来自业务状态、不来自 LVGL)能在 `uiTask` 卡死期间继续刷;真要这条就把组帧挪回独立任务,
但**起发仍留在中断里**——别让那 60 ms 回到任务态。
**裁定经过**:用户提问「那 200 ms 的阻塞一定存在吗、为什么会存在、有没有办法避免」→ 逐条核屏板源码与
主机侧观测面之后,于 2026-10-29 批准本口径(屏板侧事实源是外部工程 DTU-LCD,本项不改它的固件)。
### 决策四:屏板键值 9 / 10 的语义
@@ -146,7 +189,7 @@
|---|---|---|---|
| 1-1 | **协议层模块(纯逻辑,可主机单测)** | 新建 `Drivers/BSP/ScreenLink/sb_proto.c/h`:CRC-16/MODBUS(表与屏板 `auchCRCHi/Lo` 逐字节一致)、帧 A 解析、帧 B 组帧 | 主机单测:给定 6 B 召唤帧能算出屏板同样的 CRC;组出的 4208 B 帧 CRC 自校验通过 |
| 1-2 | **波特率与格式落地** | `STM32F407-Demo.ioc` 里把 **USART1** 改 **700000 / 8N1 / 16 倍过采样**(改 `.ioc` 而不是只改 `Src/usart.c`,否则 CubeMX 重生成会丢);`Src/usart.c` 只写 USER CODE | 示波器/逻辑分析仪实测位宽误差 **< 1 %**;理论值 `USARTDIV = 7.5`、`BRR = 0x0078`(APB2 84 MHz),误差 0 % |
| 1-3 | **USART1 收中断与全局回调分支** | 开 IDLE 中断接收(缓冲 **16 B**);`HAL_UARTEx_RxEventCallback` 里**加 USART1 分支**(现有回调只把事件分发给另一条在用通道的句柄,不加分支会静默收不到);`HAL_UART_ErrorCallback` 同样要加,且**必须清 ORE/NE/FE 并重挂接收**(`__HAL_UART_CLEAR_OREFLAG` 或读 SR 再读 DR) | 屏板每 275 ms 发来的 6 B 能稳定收到,`Size == 6`,CRC 通过率 100 %;**拔插一次 TX 线制造噪声后能自动恢复**(不清 ORE 会永久锁死) |
| 1-3 | **USART1 收中断与全局回调分支** | 开 IDLE 中断接收(缓冲 **16 B**);`HAL_UARTEx_RxEventCallback` 里**加 USART1 分支**(现有回调只把事件分发给另一条在用通道的句柄,不加分支会静默收不到);`HAL_UART_ErrorCallback` 同样要加,且**必须清 ORE/NE/FE 并重挂接收**(`__HAL_UART_CLEAR_OREFLAG` 或读 SR 再读 DR) | 屏板每 280 ms 发来的 6 B 能稳定收到,`Size == 6`,CRC 通过率 100 %;**拔插一次 TX 线制造噪声后能自动恢复**(不清 ORE 会永久锁死) |
> ⚠ **`HAL_UARTEx_RxEventCallback` / `HAL_UART_ErrorCallback` 是全局单例**——全工程只有一处实现,
> 现在只按它自己那个 `huart` 句柄分发(定位:全库搜这两个回调名就能找到实现处)。
@@ -156,10 +199,10 @@
| 序 | 任务 | 改动点 | 判据 |
|---|---|---|---|
| 2-1 | **帧 B 组帧** | `sb_link.c`:取 `ui_net_sink_get_lcdmem()` 的 3200 B → 拼 4 B 状态灯 → `info[4..5]` 填非 LOGO 值 → `info[6..7]` 保留 → **998 B 填充(固定 0x00)** → 2 B CRC(覆盖 `[0, 4205]`) | 长度 **4208 B**;CRC 覆盖范围正确;填充字节全 0 |
| 2-2 | **应答时序** | 收到帧 A 后**立即**回帧 B,不排队、不攒批 | 逻辑分析仪测:从帧 A 末字节到帧 B 首字节 **< 5 ms**;整帧 < 70 ms |
| 2-3 | **按键上行** | 帧 A 的 `key` 字节按屏板 `KEY_TYPE` 查表 → `ui_net_sink_inject_key(UI_KEY_*)`;`0` = 无键直接返回 | 7 个物理键逐个按下,UI 动作与 `docs/产品需求/产品需求设计文档.md` §8.2 菜单树一致 |
| 2-4 | **宿主任务** | `App/app/app_main_init()` 里 `xTaskCreateStatic` 建任务(栈 768 B 静态数组);句柄登记进 `App/task/task_handles.h` 并注明"非 CubeMX 创建" | 任务稳定运行 30 min,栈高水位余量 > 40 % |
| 2-1 | **帧 B 组帧(在 `uiTask` 里做)** | `sb_link.c` 暴露一个「组一帧」入口,由 `uiTask` 在显存 / LED 掩码变化时调用(**不在中断里组帧**):取 `ui_net_sink_get_lcdmem()` 的 3200 B → 拼 4 B 状态灯 → `info[4..5]` 填非 LOGO 值 → `info[6..7]` 保留 → **998 B 填充(固定 0x00)** → 2 B CRC(覆盖 `[0, 4205]`);目标缓冲 = 静态 4208 B,**落 SRAM、不落 CCM** | 长度 **4208 B**;CRC 覆盖范围正确;填充字节全 0;单帧组帧耗时实测 **≤0.3 ms**;`.map` 里该缓冲不在 CCM 段 |
| 2-2 | **应答时序(起发在中断里)** | USART1 空闲中断校完 6 B 召唤帧后**直接点 TX DMA** 发送已经组好的帧(决策三);不排队、不攒批、**不在任务里逐字节轮询发送** | 逻辑分析仪:召唤帧末位 → 应答首位 **30 µs ~ 5 ms**;整帧 **59.6 ms ±0.5 ms** 连续无断裂、**期间无 2 个字符时间(约 28 µs)以上的间隙**;整帧在 **≤200 ms** 内结束 |
| 2-3 | **按键上行** | 帧 A 的 `key` 字节按屏板 `KEY_TYPE` 查表 → `ui_net_sink_inject_key(UI_KEY_*)`;`0` = 无键直接返回(**注键由 `uiTask` 做,不在中断里**) | 7 个物理键逐个按下,UI 动作与 `docs/产品需求/产品需求设计文档.md` §8.2 菜单树一致;逐键必到、不看统计均值 |
| 2-4 | **帧缓冲归属与 DMA 收尾** | **不新建任务**(决策三):DMA 传完中断(`DMA2_Stream7_IRQn`,`Src/stm32f4xx_it.c` 已接)清 busy;busy 期间冻结帧缓冲,`uiTask` 有新内容就置 dirty、传完再重组(或双缓冲);缓冲、起发入口、收尾全收在 `sb_link.c` | 连续 30 min:无双发、无脏帧、组帧自校验 100 %;`[PROF]` 里 `uiTask` 单轮工作耗时增量 **≤0.3 ms**;拔掉屏板后不再触发、无残留 busy |
| 2-5 | **链路监视与容错** | 连续 **≥10 s** 无召唤帧才判"屏板离线"(**不能按 500 ms 判**——屏板 ≈5000 s 会自复位一次,约 1 s 空窗);CRC 错帧静默丢弃并计数 | 拔掉屏板 10 s 后置离线标志;插回自动恢复;屏板自复位不被误判 |
### 批 3 · 状态灯与 LOGO(3 项)
@@ -190,7 +233,7 @@
```
Drivers/BSP/ScreenLink/
├── sb_proto.c/h 纯逻辑:CRC-16、帧 A 解析、帧 B 组帧 ← 可主机单测
└── sb_link.c/h 传输:USART1、任务宿主、状态灯映射表 ← 依赖 HAL,仅 MCU
└── sb_link.c/h 传输:USART1 中断起发 + TX DMA、组帧入口、状态灯映射表 ← 依赖 HAL,仅 MCU
```
**为什么拆两层**:协议层不碰 HAL 才能在 `test/host/` 里跑单测(项目既有做法:原样 include 生产 `.c`,
@@ -240,8 +283,16 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉
和屏板链路的 4208 B 共用同一个数据寄存器,两条流一混,症状是"屏板偶尔花屏 + 日志偶尔乱码",
查起来会误以为是 CRC 或时序问题。批 0 的 0-2 就是干这个的。
- ❌ **不要在 `Src/freertos.c` 里加任务**——那是 CubeMX 生成物,重生成会丢。
- ❌ **不要把 4208 B 缓冲放到栈上**。
- ❌ **不要把 4208 B 缓冲放到栈上,也不要放到 CCM**——放错段就是 DMA 读不到,发出去的是旧数据(§1.3)。
- ❌ **不要按屏板 readme 的 3208 B 组帧**(见 §4.2 的警示)。
- ❌ **不要在任务里逐字节轮询 `USART1->DR` 发这 4208 B**——屏板按「总线空闲 1 个字符」断帧,一次几十 µs 的
ISR 抢占就把帧切断了,而屏板侧不报超时(§6)→ 整帧交 DMA 是唯一稳的写法(决策三、§1.3)。
- ❌ **不要提前发、超发、叠发**:屏板收帧是「一次 IDLE 一包」,多出来的字节会让它把正在处理的那一帧撕裂;
它一旦接收溢出就**永久收不到东西**(错误回调里的重启被注释掉),现场表现为「画面与状态灯一起冻死」,
只能给屏板断电重上或等它 83 min 自复位(自复位后是白屏)。严格「一召唤 → 一答 4208 B」。
- ❌ **不要指望屏板告诉你它没收到**:它的超时判定只看「收到一整包」、不看 CRC(坏帧也清掉超时计数),
屏幕上也没有任何通信状态指示(运行灯翻转被注释、故障显示函数无人调用)→
**观测面只有主机侧计数**(2-5 的 CRC 错帧计数 + 召唤间隔统计)。
---
@@ -276,6 +327,11 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉
| 屏板自复位被误判离线 | 链路监视窗口设得太小 | 每 83 分钟误报一次 | 判据定为"连续 ≥10 s 无帧" |
| 状态灯语义返工 | 映射表拍脑袋定的 | 后期改表要重测 | 把表做成一张 `const` 数组,改表不改逻辑 |
| RAM 超门禁 | 误把 RX 缓冲也做成 4208 B | `RW_IRAM1` 逼近 92 % | RX 只要 16 B(§1.3) |
| **整帧被 ISR 切断** | 在任务里轮询发送,中途被采样 / 网口 / 系统节拍 ISR 抢走 ≥2 个字符时间 | 一帧拆两包、两半 CRC 全错;症状只是「画面偶尔不刷新」,而**屏板不报超时** | 决策三:整帧 TX DMA + 中断起发;2-2 判据含「无 28 µs 以上间隙」 |
| **屏板接收溢出后永久静默** | 主机提前发 / 超发 / 叠发,或 LOGO 帧撞上屏板自己的 Flash 写 | 屏板再也收不到,画面与状态灯一起冻死,**它自己毫无指示**,只能断电重上 | 严格一召唤一应答、正好 4208 B;LOGO 只走出厂 / 联调流程(3-3),下发前先确认能给屏板断电 |
| **把 200 ms 当成「帧作废」线** | 误读屏板那个放弃计时 | 白加一层重试与加急逻辑;或反过来以为迟到无害,撞上 205 ms 的按键召唤 | 屏板真实规则:迟到的完整帧照样显示;**有按键待报时最早 205 ms 就再召唤** → 仍按 ≤200 ms 完成,理由换成「防撞 + 不滞后」(决策三) |
| **按键被当成偶发失灵** | 那一轮窗口被错过 | 屏板的键**只发一次不重试**(发完立刻清备份)→ 该次按键直接丢失 | 起发留在中断里(µs 级);2-3 按「逐键必到」测,不看统计均值 |
| **屏板背光 100 s 熄灭被当成掉线** | 现场长时间没人按键 | 误报链路故障 | 按任意键即复亮;判离线只看召唤帧有无(2-5 的 10 s 窗口) |
---
@@ -284,7 +340,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉
做完这一项,要能同时满足(逐条可测,对应 `docs/产品需求/产品需求设计文档.md` §5.16.10 第 8~15 条):
0. **腾位置**:日志在 RS-232 侧可读、内容与迁移前一致;USART1 已无任何控制台写入;连续 10 min 无 ORE 锁存。
1. 屏板按 275 ms 召唤,主机每帧在 **≤200 ms** 内回 **4208 B**,连续 10 min 零超时。
1. 屏板按 **280 ms** 召唤(判据是 RT_count > 55 → 第 56 拍 × 5 ms;屏板侧常写的 275 ms 是 55×5 的算法),主机每帧在 **≤200 ms** 内**发完** **4208 B**,连续 10 min **主机侧**零超时、零 CRC 错帧。
2. 帧 B 前 3200 B 与 `g_ui_fbuf` **逐字节一致**。
3. 屏板 7 个物理键逐个按下 → UI 动作正确;无键时 `key = 0` 不产生事件。
4. **状态灯**:掩码逐位对应,LED 0 恒亮、bit0 不影响任何灯。
@@ -293,6 +349,13 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉
7. 拔插屏板:7003 侧不受影响,插回自动恢复,不用复位主机。
8. LOGO 下发后断电重启仍在,且不改变 LED 状态。
**另测两项**(属本方案的实现判据,不在 §5.16.10 编号内):
- 逻辑分析仪量整帧发送期间**不得出现 ≥2 个字符时间(约 28 µs)的间隙**——这是断帧安全性的直接证据,
也是必须用 DMA 的理由(决策三);同时记下召唤帧末位到应答首位的实测值,确认 30 µs 的切换留量够用。
- 跑一遍 LOGO 下发(3-3)之后,验证「给屏板断电重上」仍能恢复通信:屏板在写它自己那块 Flash 时若被打断,
可能进入它自己救不回来的状态(它的错误回调与重初始化都被注释掉了)。
---
## 8. 与其它工作的关系
@@ -300,9 +363,10 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉
- **与日志/控制台工作强耦合**:批 0 的 0-2 要改 `App/log/console_log.c`——那是看门狗喂狗节奏的唯一写者
(机制见 `docs/子系统设计/控制台单写者日志设计说明.md`)。这项改动**不能与其它动控制台的活儿并行**,
而且必须台架长稳验证过才算完;**它比屏板链路本身风险更高**,建议单独排一轮、单独回滚。
- **与 UI 补齐无关**:本项只消费 `ui_net_sink_get_lcdmem()` / `ui_net_sink_inject_key()`,
不改任何页面。UI 页面已在 2026-10-28 收口,正好可以直接拿来当被测对象。
- **不占用并行工作树名额的额外理由**:改的是新目录 + `app_main_init` 一处 + `task_handles.h` 一处 +
`.ioc` 一处,与主线其它工作写域几乎不重叠;但 `.ioc` 改动属**单点事项**(CubeMX 重生成),
必须与其它改 `.ioc` 的工作串行。
- **与 UI 的耦合只有一处**:决策三把组帧与注键消费放进 `uiTask`——只用 `ui_net_sink_get_lcdmem()` /
`ui_net_sink_inject_key()` 这两个现成接口,**不改任何页面与绘制逻辑**。UI 页面已在 2026-10-28 收口,
正好可以直接拿来当被测对象;`uiTask` 单轮耗时的增量由 2-4 的判据盯着(≤0.3 ms)。
- **不占用并行工作树名额的额外理由**:改的是新目录 + `.ioc` 一处(决策三不新建任务 → 不再动
`Src/freertos.c` / `App/task/task_handles.h` / `App/app/app_main_init()`),与主线其它工作写域几乎
不重叠;但 `.ioc` 改动属**单点事项**(CubeMX 重生成),必须与其它改 `.ioc` 的工作串行。
- **需求侧销项**:完成后回填 `docs/产品需求/产品需求设计文档.md` §9.13 与 §5.16.2 的待复核项。