diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index 09ffa929..8724fd5e 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -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`——堆已满(§1.3);采纳方案不新建任务,这条只是备着 | +| 静态任务分配 | `Inc/FreeRTOSConfig.h` `configSUPPORT_STATIC_ALLOCATION = 1` | 只有走退路(独立任务)时才用得上:仓内新建任务的先例是 `Src/freertos.c` 的 `USER CODE` 区里 `osThreadNew`(全工程没有一处调用 `xTaskCreateStatic`),**且不能建在 `app_main_init`**——它跑在 `osKernelStart` 之前,那里禁调任何创建类 API。采纳方案不新建任务,这条只是备着 | ### 1.2 缺什么(要新建) @@ -50,7 +50,7 @@ | `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 配置,**已满** | **不加任务则不动** | ⚠ 新增动态任务会创建失败 | +| RTOS 堆 | 57,344 B 配置;最近一次台架实测 `free` 16,400 / `min_ever` **8,464 B**(告警线 3,072 B,口径 `docs/内存管理/RAM使用分析与优化方案.md` §2.3) | **不加任务则不动** | ⚠ 是**紧张**,不是「必然创建失败」:768 字(3 KB)栈 + TCB 只剩几 KB 余量 → 仍首选不新建 | **DMA 不是可选项,是「整帧不断帧」的唯一保证。** 三个数别混: @@ -71,6 +71,11 @@ > `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**。 +> **三个收尾回调全工程都没人实现**:`HAL_UART_TxCpltCallback` / `HAL_UART_RxCpltCallback` 零实现, +> `HAL_UART_ErrorCallback` 只有一处、且只分发另一条通道的句柄(定位:全库搜这三个名字)→ 本链路要**自写这三个、 +> 按 `huart` 句柄分发**,错误分支里顺手清掉 USART1 的 ORE / NE / FE 再重挂接收(与 1-3 同一手法)。 +> **`.ioc` 在仓库根**(`STM32F407-Demo.ioc`,不在 `MDK-ARM/` 下):`Dma.Request0/1` 已列 `USART1_TX` / `USART1_RX`, +> `DMA2_Stream7` 的 NVIC 优先级 9——与 `Src/dma.c`、`Src/stm32f4xx_it.c` 三处互相自洽(换流会被打回优先级 0,须回 `Src/dma.c` 核对)。 --- @@ -106,22 +111,28 @@ ### 决策三:谁来跑这个链路(宿主)——**已裁定 2026-10-29:`uiTask` 组帧 + 中断点 DMA 起发,不新建任务** -**先把名目拆开**——原来把两件不同的事写进了同一格,方案就是在这儿走偏的: +**先把四个名目拆开**——它们是四件不同的事,混在一起就会把实现方式选错: | 数 | 它是什么 | 能不能避免 | |---|---|---| | **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 条 | +| **数秒量级的等待** | `uiTask` / `netTask` / `defaultTask` 里那一族**等别的任务回话或等外设**的阻塞(实测与推导见方案表后的注) | 与屏板链路本身无关,但**决定谁不能当起发者**:任务不在就绪态,抬优先级救不了 | | 方案 | 优点 | 缺点 | 结论 | |---|---|---|---| | **`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` 主循环里 | 改动最省 | `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);实测最坏见方案表后那组注。**这类阻塞是等另一个任务回话——任务不在就绪态,抬优先级救不了** | 否决 | +| 把起发挂在 `defaultTask` | 零新增任务 | 周期 100 ms 量级只是表面;它同属「数秒量级的等待」那一族——FTL 挂载、日志排空、对时突发都压在这条任务上 → 起发延迟不可控 | 否决 | -**采纳方案的分工——起发者不等于业务归属**(原来那条否决理由把这两件事混成一件了): +> **「数秒量级等待」那一行的实测锚点**(引用这一族时别把推导值当实测值): +> - **≈4 s 是推导上界,不是实测**:netTask 里 `ch395f_poll_cmd_status(4000U)` 的忙等上限(`Drivers/BSP/CH395F/ch395f.c`:651,:1088 / :1103 / :1118 / :1133 / :1148 六处调用)+ 调用方 `portMAX_DELAY`;`docs/UI设计/RemoDispBus协议与接收设计说明.md` 里那个「约 4 s」就是这个推导数,**没有实测样本**。 +> - **实测值**:7003 往返 `uis` 峰 183 ms;`uiTask` 常态渲染地板 `uip` 61.3 ms(`docs/子系统设计/看门狗设计说明.md` §11.4);对时 relisten 突发把 `uiTask` 压成**单轮 800 ms 以上缺席**(`docs/任务规划/登记簿.md` 条目「reconcile 突发饿死 uiTask」,`uip` 在途 307 → 1083 ms)。 +> - 结论不变:这一族的延迟量级是**百 ms 到秒**,屏板只给 205 / 280 ms → 起发不能挂在任何任务态。 + +**采纳方案的分工——起发者不等于业务归属**(这两件事分开看,否决理由才站得住): - **`uiTask`(10 ms 一拍)= 业务与组帧**:显存或 LED 掩码有变化时把 4208 B 帧拼好(3200 B 显存 + 4 B 灯 + `info[4..7]` + 998 B 填充 + 2 B CRC);消费中断留下的 `key` 字节 → `ui_net_sink_inject_key()` @@ -132,9 +143,13 @@ - **两条必须防**:① DMA 正在读时 `uiTask` 改帧 → dirty 标志(传完再重组)或双缓冲(仓里 `meas` 的双区 + `seq` 是现成先例);② **屏板一次接收溢出就永久收不到、且自己毫无指示**(它错误回调里的重启被注释掉) → 主机严格「一召唤 → 一答 4208 B」,**不提前发、不超发、不叠发**。 -- **不新建任务**:不动 `Src/freertos.c`、不动 `App/task/task_handles.h`、不碰已满的 RTOS 堆(§1.3); - 看门狗位掩码 `0x37` 不动——起发在中断里,与任务心跳无关。 -- **屏板那 200 ms 的真实语义**(决定为什么仍按 ≤200 ms 要求自己,但理由要换):它到点只置一个「可以重发」 +- **不新建任务**:不动 `Src/freertos.c`、不动 `App/task/task_handles.h`、不占 RTOS 堆(§1.3 的余量本来就紧); +- **看门狗一个位都不加**:产品构建的必需位掩码现值是 **`0x1F7`**(`App/util/task_watchdog.c` 的 `wd_required_mask()`: + default / net / ftp / ui / timesync / adc / iec104 / protect;口径 `docs/子系统设计/看门狗设计说明.md` §1, + default / net / ftp / ui / timesync / adc / iec104 / protect;口径见 `docs/子系统设计/看门狗设计说明.md` 的「必需掩码」行)。 + 本项不给屏板链路新增心跳位,也不改变「同一任务缺席 800 ms」那条判定。起发在中断里,与任务心跳无关。 + 也不改变「同一任务缺席 800 ms」那条判定。 +- **屏板那 200 ms 的真实语义**(它不是帧作废线,但主机仍按 ≤200 ms 要求自己,理由是下面这两条):它到点只置一个「可以重发」 的标志,不丢已收字节、不清缓冲、不重启串口;**迟到的完整帧照样会被显示**(收帧处理与超时判定相互独立, 帧里也没有序号)。真硬的是 280 ms 那轮召唤——而且**有按键待报时最早 205 ms 就会再来一帧**, 上一答还在总线上就是**双向对撞**。所以 ≤200 ms 内发完仍要做,理由是**防撞总线 + 不多一轮画面滞后**。 @@ -202,7 +217,7 @@ | 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-4 | **帧缓冲归属与 DMA 收尾** | **不新建任务**(决策三):DMA 传完中断(`DMA2_Stream7_IRQn`,`Src/stm32f4xx_it.c`:644-652 已接)清 busy——**`HAL_UART_TxCpltCallback` 全工程没人实现,要在本模块自写并按 `huart` 句柄分发**(§1.3);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 项) @@ -323,7 +338,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 | **一次噪声后链路永久静默** | 错误回调里不清 ORE | 屏板召唤再也收不到,症状与"协议不对"完全一样 | 1-3 判据含"拔插 TX 线制造噪声后自动恢复" | | **日志口又染上 ORE 锁存** | 新日志口照抄 `UART_MODE_TX_RX` | 控制台静默、看门狗误复位(老病复发) | 新日志口设 `UART_MODE_TX`;判据含"连续 10 min 无 ORE" | | 帧长按 3208 实现 | 照 readme 写 | CRC 全失败、屏板不亮 | 已写进 §4.2 警示;联调第一步就核 4208 | -| 中断回调漏加分支 | 只改自己模块,忘了现有全局 UART 回调里的句柄分发 | 收不到召唤帧,表象像协议错 | 批 1 的 1-3 已列为判据 | +| 中断回调漏加分支 | 只改自己模块,忘了那三个全局 UART 回调里唯一的一处实现只分发另一条通道的句柄(§1.3) | 收不到召唤帧,表象像协议错 | 批 1 的 1-3 已列为判据 | | 屏板自复位被误判离线 | 链路监视窗口设得太小 | 每 83 分钟误报一次 | 判据定为"连续 ≥10 s 无帧" | | 状态灯语义返工 | 映射表拍脑袋定的 | 后期改表要重测 | 把表做成一张 `const` 数组,改表不改逻辑 | | RAM 超门禁 | 误把 RX 缓冲也做成 4208 B | `RW_IRAM1` 逼近 92 % | RX 只要 16 B(§1.3) | @@ -332,6 +347,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 | **把 200 ms 当成「帧作废」线** | 误读屏板那个放弃计时 | 白加一层重试与加急逻辑;或反过来以为迟到无害,撞上 205 ms 的按键召唤 | 屏板真实规则:迟到的完整帧照样显示;**有按键待报时最早 205 ms 就再召唤** → 仍按 ≤200 ms 完成,理由换成「防撞 + 不滞后」(决策三) | | **按键被当成偶发失灵** | 那一轮窗口被错过 | 屏板的键**只发一次不重试**(发完立刻清备份)→ 该次按键直接丢失 | 起发留在中断里(µs 级);2-3 按「逐键必到」测,不看统计均值 | | **屏板背光 100 s 熄灭被当成掉线** | 现场长时间没人按键 | 误报链路故障 | 按任意键即复亮;判离线只看召唤帧有无(2-5 的 10 s 窗口) | +| **采纳方案继承 `uiTask` 的挤占面** | `uiTask` 是 BelowNormal,排在 defaultTask(Normal)之后,而后者一次日志排空就是 250 ms(`App/log/console_log.c` 的 `DBG_CON_DRAIN_BUDGET_MS`)、NAND 写数百 ms;对时 relisten 突发实测把 `uiTask` 单轮压到 800 ms 以上 | 只表现为**画面滞后与注键延迟**——起发在中断,不撞总线、不进屏板那套超时阶梯 | 与 7003 同一来源,属已知成本;现场不可接受就走退路(组帧挪独立任务),**起发仍留在中断里** | --- @@ -349,11 +365,13 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 7. 拔插屏板:7003 侧不受影响,插回自动恢复,不用复位主机。 8. LOGO 下发后断电重启仍在,且不改变 LED 状态。 -**另测两项**(属本方案的实现判据,不在 §5.16.10 编号内): +**另测三项**(属本方案的实现判据,不在 §5.16.10 编号内): - 逻辑分析仪量整帧发送期间**不得出现 ≥2 个字符时间(约 28 µs)的间隙**——这是断帧安全性的直接证据, 也是必须用 DMA 的理由(决策三);同时记下召唤帧末位到应答首位的实测值,确认 30 µs 的切换留量够用。 - 跑一遍 LOGO 下发(3-3)之后,验证「给屏板断电重上」仍能恢复通信:屏板在写它自己那块 Flash 时若被打断, +- **7003 拥塞时串口侧照常应答**:挂着 7003 客户端不读、或制造对时 / relisten 突发把 `uiTask` 卡住, + 屏板侧应**零超时**、画面停在最后一帧(这就是「起发不在任务态」的直接证据);`uiTask` 一醒画面立刻续上、总线无对撞。 可能进入它自己救不回来的状态(它的错误回调与重初始化都被注释掉了)。 ---