From 50c75eb5160ee50ab0cf1b4b9a581ec93fdaf9e7 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 18:14:44 +0800 Subject: [PATCH 01/16] =?UTF-8?q?docs(=E5=B1=8F=E6=9D=BF):=20=E6=89=B9=200?= =?UTF-8?q?=20=E6=97=A5=E5=BF=97=E5=8F=A3=E5=AE=9A=E4=B8=BA=20RS232#1?= =?UTF-8?q?=EF=BC=88UART5=EF=BC=89=EF=BC=8C=E8=90=BD=E5=AE=9A=20CN1=20?= =?UTF-8?q?=E7=AB=AF=E5=AD=90=E4=B8=8E=E9=80=82=E9=85=8D=E5=99=A8=E8=A6=81?= =?UTF-8?q?=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-10-29 用户裁定:台架线走 ST_TX1 / ST_RX1 这一对,即 RS232#1, 对应 UART5(PC12 / PD2)。此前文档把默认日志口写成 USART2(RS232#3), 与这次裁定不符,一并改掉。 网表核对(docs/硬件平台/V0.1.tel 逐段可查): PC12 -ST_TX1- U9.3 -> U9.6 -TX1- U13.11 -> U13.14 -RS232_TX1- CN1.12 PD2 -ST_RX1- U9.2 <- U9.7 -RX1- U13.12 <- U13.13 -RS232_RX1- CN1.10 由此落定三件事: 1. 台架端子:适配器 TXD 接 CN1.10、RXD 接 CN1.12、GND 接 CN1.7 或 CN1.8(CGND)。方向是交叉的——CN1.10 是板子的接收端。 2. RS232#1 没有 TTL 出口:ST_TX1 / ST_RX1 在网表里只连 U9 的 3 脚 / 2 脚与 MCU 两个脚,既无连接器也无测试点。唯一外部出口是 CN1 的 RS-232 端子,所以必须换 USB 转 RS-232 适配器;现有那根 TTL CH340 直连不了,接上去轻则不通信、重则打坏 U13 或适配器。 3. U14 是 RS232#3 的网口座(3 脚 RS232_TX3 / 6 脚 RS232_RX3),本通道 用不上。原文「改接 CN1(或 U14)」会把接线引到错误的通道上,删掉。 0-2 的目标端口随之从 huart2 收窄为 huart5(不再二选一)。huart5 现为 115200 8N1 / UART_MODE_TX_RX,业务代码无人使用,仅 Src/stm32f4xx_it.c:565 挂着 IRQ 入口;改 UART_MODE_TX 要在 .ioc 里做,Src/usart.c 是生成物。 改动面:屏板链路.md(决策二表、批 0 的 0-1 / 0-2、台架步骤 0、风险表各 一条)与产品需求设计文档.md 的 §9.13 物理前置段(原文同样写着「或 U14」)。 --- docs/产品需求/产品需求设计文档.md | 5 ++++- docs/任务规划/屏板链路.md | 30 ++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 657df359..1d001860 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -2572,7 +2572,10 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 > > **一条物理前置 + 一条实现纪律**: > ① **台架串口线挪位**——现在的 CH340/`COM6` 接在 USART1 调试口上,日志迁到 RS-232 侧后 -> 必须改接到 CN1(或 U14),否则 `@test` 与 `serial-assistant` 全部抓不到日志,等于把台架弄瞎。 +> 必须改接到 CN1 的 RS232#1 端子(`CN1.10` 接适配器 TXD、`CN1.12` 接适配器 RXD、`CN1.7` 或 `CN1.8` 接 GND), +> 否则 `@test` 与 `serial-assistant` 全部抓不到日志,等于把台架弄瞎。这些端子是 **RS-232 电平**, +> 须用 USB 转 RS-232 适配器,现有的 TTL CH340 直连不了;接法与端子出处见 +> `docs/任务规划/屏板链路.md` §3 批 0。 > **这条要在第 16 条动代码之前完成。** > ② **RX 要真的读、ORE 要真的清**——PA10 一直是接线的(§5.16.2), > 但"RX 使能却不读 `DR`"本身就足以让 ORE 永久锁存。第 11 条实现时必须在 diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index 8724fd5e..01f8e82c 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -17,10 +17,11 @@ | # | 项 | 实测 / 出处 | 后果 | |---|---|---|---| | 1 | **日志口占着 USART1** | `console_log.c` 里 `huart1` 5 处;自愈函数直写 `USART1_IRQn` / `MX_USART1_UART_Init()` | 两者共用同一个 `USART1->DR`,同时用就是互相插字节 → **日志必须先迁到 RS-232 通道** | -| 2 | **台架串口线接在 USART1 上** | CH340 → `COM6`,`@test` / `serial-assistant` 都靠它抓日志 | 日志一迁,台架立刻瞎 → **先把线挪到 RS-232 侧(CN1)再动代码** | +| 2 | **台架串口线接在 USART1 上** | TTL 的 CH340 → `COM6`,`@test` / `serial-assistant` 都靠它抓日志 | 日志一迁,台架立刻瞎 → **先把线挪到 RS-232 侧(CN1)再动代码**;且这根 TTL 模块挪不过去,须换 **USB 转 RS-232 适配器**(见 §2 决策二) | **执行顺序因此是**:挪台架线 → 迁日志口 → 才开始写屏板链路。 -第一步是拧螺丝,第二步约 20 行代码(§3 批 0),第三步才是屏板链路的开发批次。 +第一步是**换一个 USB 转 RS-232 适配器 + 接三个端子**(纯硬件,不碰代码),第二步约 20 行代码(§3 批 0), +第三步才是屏板链路的开发批次。 --- @@ -97,13 +98,21 @@ | 端口 | 引脚 | V0.1 板上通道 | 重排后用途 | |---|---|---|---| | **USART1** | PA9 / PA10 | 调试口 + U5 直连 TTL(ST_TX0/RX0) | ✅ **屏板链路** | -| **USART2** | PD5 / PD6 | **RS232#3**(U10 隔离 → U13 TPT3232E → CN1) | ✅ **日志**(默认) | -| UART5 | PC12 / PD2 | **RS232#1**(U9 隔离 → U13 → CN1) | 备选日志口 | +| **UART5** | PC12 / PD2 | **RS232#1**(ST_TX1/ST_RX1 → U9 隔离 → U13 TPT3232E → CN1.10/CN1.12) | ✅ **日志**(2026-10-29 已定) | +| USART2 | PD5 / PD6 | **RS232#3**(ST_TX3/ST_RX3 → U10 隔离 → U13 → CN1.9/CN1.11;另有网口座 `U14` 同信号) | 备选日志口 | | USART3 | PB10 / PB11 | U5 直连 TTL(ST_TX2/RX2) | 空闲 | -**日志口选 USART2 还是 UART5**:两者都经隔离器 → `U13` TPT3232E → CN1,能力等价; -控制台是**只发不收、寄存器轮询**,DMA 也用不上,所以挑哪个只取决于**现场把 CH340 接到哪个端子**。 -默认写 **USART2(RS232#3)**;若实际接的是 RS232#1,就换成 `huart5`——**迁移时收口成一处宏,别散改**。 +**日志口已定为 UART5(RS232#1)**——2026-10-29 用户裁定,理由是**台架那根线落在哪个端子**,不是能力差异: +两条 RS232 通道都经隔离器 → `U13` TPT3232E → CN1,控制台又是**只发不收、寄存器轮询**,DMA 也用不上。 +选定的 `ST_TX1` / `ST_RX1` 就是 RS232#1 在 MCU 侧的网络名(PC12 / PD2,UART5)。 + +> ⚠ **RS232#1 没有 TTL 出口**:`ST_TX1` / `ST_RX1` 在网表里只连 `U9` 的 3 脚 / 2 脚与 MCU 两个脚, +> **既无连接器也无测试点**;唯一外部出口是 CN1 的 RS-232 端子。所以台架**必须用 USB 转 RS-232 适配器**, +> 现有的 TTL CH340 接不上去(接上去轻则不通信,重则打坏 `U13` 或适配器)。 +> 另:`U14` 是 RS232#3 的网口座(3 脚 `RS232_TX3` / 6 脚 `RS232_RX3`),**本通道用不上**——别把它当成备选端子。 + +**迁移时收口成一处端口宏,别散改**:`huart5` 现在是 `115200 8N1 / UART_MODE_TX_RX`(`Src/usart.c:74-81`), +业务代码里没人用,只有 `Src/stm32f4xx_it.c:565` 挂着 IRQ 入口。 > ⚠ **USART1 交接前必须确认两件事**:① 控制台已不再写 `USART1->DR`(否则两条流互相插字节, > 症状是"屏板偶尔花屏 + 日志偶尔乱码",很容易误判成 CRC 或时序问题); @@ -185,8 +194,8 @@ | 序 | 任务 | 改动点 | 判据 | |---|---|---|---| -| 0-1 | **台架串口线挪位** | 硬件:CH340 从 USART1 调试口改接 CN1 的 RS-232 端(或 U14) | `serial-assistant` 在 RS-232 侧能收到日志 | -| 0-2 | **日志口迁移(USART1 → RS-232)** | ① `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 `huart2`(RS232#3)或 `huart5`(RS232#1);② 该实例改 **`UART_MODE_TX`**(只发不收,把 RX 从机制上摘掉) | 日志在 RS-232 侧可读、内容与迁移前一致;连续 10 min 无 ORE 锁存、无 `[ERR]`;USART1 完全空出 | +| 0-1 | **台架串口线挪位** | 硬件:**换成 USB 转 RS-232 适配器**(TTL 的 CH340 直连不了),接 RS232#1 的三个端子——适配器 TXD → **CN1.10**(板子 `RS232_RX1`)、RXD → **CN1.12**(板子 `RS232_TX1`)、GND → **CN1.7 或 CN1.8**(`CGND`)。端子号出自 `docs/硬件平台/V0.1.tel`;`U14` 是 RS232#3 的网口座,本通道用不上 | `serial-assistant` 在 RS-232 侧能收到日志 | +| 0-2 | **日志口迁移(USART1 → RS-232)** | ① `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 **`huart5`(RS232#1,已定)**;② 该实例改 **`UART_MODE_TX`**(只发不收,把 RX 从机制上摘掉)——`huart5` 现为 `UART_MODE_TX_RX`,**改在 `.ioc` 里**(`Src/usart.c` 是 CubeMX 生成物,只写 USER CODE) | 日志在 RS-232 侧可读、内容与迁移前一致;连续 10 min 无 ORE 锁存、无 `[ERR]`;USART1 完全空出 | | 0-3 | **落定 USART1 到屏板的接线** | 按决策一:TTL 直连屏板 UART2 侧(PA2/PA3);把线束接法与验收方式写清楚 | 接法落纸;照此接线后屏板发来的 6 B 召唤帧能被收到 | > PA10 已接线(用户 2026-10-28 确认),屏板链路**不需要额外飞线**。 @@ -317,7 +326,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 | 步 | 做什么 | 需要什么 | 停下也能交付什么 | |---|---|---|---| -| 0 | **腾位置**:CH340 挪到 RS-232 侧、日志迁走 | 改接串口线 | USART1 空出来,日志照旧能看(批 0) | +| 0 | **腾位置**:串口线挪到 RS-232 侧、日志迁走 | 改接串口线 + 一个 **USB 转 RS-232 适配器** | USART1 空出来,日志照旧能看(批 0) | | 1 | 协议层单测(`test/host/screenlink/`) | 无硬件 | CRC 与组帧的正确性,可回归 | | 2 | PC 串口助手**模拟屏板**:手发 6 B 召唤帧,看主机回的 4208 B | **USB 转 TTL 模块**接 USART1(PA9/PA10)+ 主机板 | 主机侧协议 100 % 可验证 | | 3 | 反向:PC 收主机帧,用脚本核对 4208 B / CRC / 显存内容 | 同上 | 帧内容正确性(与 `g_ui_fbuf` 逐字节比) | @@ -334,6 +343,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 | 风险 | 触发条件 | 影响 | 回退/应对 | |---|---|---|---| | **日志迁移把台架弄瞎** | 先改代码、后挪串口线 | 什么都看不到,误以为固件挂了 | 顺序钉死:0-1 先挪线、0-2 再改代码;改完先跑一次 `@test` | +| **TTL 适配器直插 CN1** | 沿用那根 CH340 去接 RS-232 端子 | 不通信,且 RS-232 的 ±电平可能打坏 `U13` 或适配器 | 0-1 已写死「换 USB 转 RS-232 适配器」;RS232#1 无 TTL 出口,只能走电平转换后的 CN1 | | **控制台与屏板链路抢 `USART1->DR`** | 日志没迁干净就接屏板 | 屏板花屏 + 日志乱码,像 CRC / 时序错 | 0-2 的判据明确要求"USART1 完全空出" | | **一次噪声后链路永久静默** | 错误回调里不清 ORE | 屏板召唤再也收不到,症状与"协议不对"完全一样 | 1-3 判据含"拔插 TX 线制造噪声后自动恢复" | | **日志口又染上 ORE 锁存** | 新日志口照抄 `UART_MODE_TX_RX` | 控制台静默、看门狗误复位(老病复发) | 新日志口设 `UART_MODE_TX`;判据含"连续 10 min 无 ORE" | From 27a700b0d81cde580d88f783d6cb661aac248516 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 18:21:17 +0800 Subject: [PATCH 02/16] =?UTF-8?q?docs(=E5=B1=8F=E6=9D=BF):=20=E5=88=A0?= =?UTF-8?q?=E6=8E=89=E3=80=8C=E6=97=A5=E5=BF=97=E5=8F=A3=E6=94=B9=20UART?= =?UTF-8?q?=5FMODE=5FTX=E3=80=8D=E7=9A=84=E8=AF=B4=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-10-26 已裁定不改 RX 配置,ORE 由 console_log.c 既有的 drain 预算与 300 ms 冷却兜住(docs/子系统设计/控制台单写者日志设计说明.md §4.4.1、§12)。 两处文档仍在要求把新日志口设成 UART_MODE_TX,会误导实现,直接删除: - 屏板链路.md 批 0 的 0-2 第 ② 项、风险表「日志口又染上 ORE 锁存」整行、 验收清单第 0 条的「无 ORE 锁存」 - 产品需求设计文档.md 的 §5.16 硬要求 ②、端口重排第 2 条、 §9.13 第 16 条的第 ② 项 0-2 因此只剩「把 huart1 收口成一处端口宏指向 huart5」。 判据里的「无 ORE 锁存」一并去掉——ORE 是已知残余,不按它判。 --- docs/产品需求/产品需求设计文档.md | 13 +++++-------- docs/任务规划/屏板链路.md | 7 +++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 1d001860..08960d1a 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -1058,9 +1058,9 @@ sequenceDiagram > > ORE 锁存的充分条件与接线无关——**RX 使能了却从不读 `DR`**:线上只要出现 > 任意一个字节或一个噪声沿,`RXNE` 置位后没人读,下一个事件就把 **ORE 锁存**;而 ORE 只能用 -> "先读 SR 再读 DR"清除,于是**永不自愈**。这条对本实现有两条硬要求: -> ① 屏板链路要用 RX,就必须真的消费 `DR`,并在 `HAL_UART_ErrorCallback` 里**清 ORE 后重挂接收** -> (否则一次噪声就永久锁死,症状与"屏板不回帧"一模一样);② 新日志口设 `UART_MODE_TX`,从机制上把 RX 摘掉。 +> "先读 SR 再读 DR"清除,于是**永不自愈**。这条对本实现有一条硬要求: +> 屏板链路要用 RX,就必须真的消费 `DR`,并在 `HAL_UART_ErrorCallback` 里**清 ORE 后重挂接收** +> (否则一次噪声就永久锁死,症状与"屏板不回帧"一模一样)。 **帧 A:召唤 / 按键(屏板 → 主机,固定 6 B)** @@ -1144,15 +1144,12 @@ ISR 抢占,一抢就撞第 1 条,而后果在屏板侧完全不可见。时 (`U15 = TP8485E-SR` 只有 250 kbps,`docs/芯片手册/TP8485E-SR.md`)。换到这条线上,速率问题从根上消失, 也不用改任何硬件。UART4 保持 RS-485 冻结簇不动——`rs485_init(&g_rs485, &huart4, …)` 与 V0.1 一致,**无需摘除**。 -**这次重排带来两件事,必须在任务里体现:** +**这次重排带来一件事,必须在任务里体现:** 1. **日志口迁移不是一行改动**:`App/log/console_log.c` 里 `huart1` 出现 5 处(含 `huart1.Instance == NULL` 的空指针短路判据),自愈函数 `con_uart_reinit()` 还直接写 `USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()`。建议收口成**一处可配置端口宏**, 而不是逐处散改。 -2. **新日志口必须设成只发不收**(`UART_MODE_TX`):USART1 控制台之所以要背一整套僵尸通道自愈, - 根源就是"RX 悬空 + RX 使能 → ORE 锁存 → HAL 轮询 TX 恒快速失败"。日志口不需要收, - 就别再开 RX,否则等于把同一场病搬到新口上。 > ⚠ **TTL 直连没有方向控制**:主机侧不再有半双工方向切换,建立时间、"等 TC 收尾"一类约束全部不适用。 @@ -2544,7 +2541,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 13 | **串口上行按键** | 帧 A 的 `key` 字节按**屏板 `KEY_TYPE`** 解释——实际可达值只有 **`1/2/3/4/7/8/9`**(`5`/`6`/`10` 无物理入口);映射为逻辑键值后与 7003 注入**同一入口**合并 | §5.16.10 第 10 条通过;`9`/`10` 语义按 §5.16.3 ② 处置 | 否 | | 14 | **双通道并发 + 自复位容忍** | 两出口各自独立发送上下文;串口 4208 B 阻塞不拖 7003;链路监视按"**连续 ≥10 s 无应答**"判离线,**忽略屏板 ≈5000 s 周期自复位** | §5.16.10 第 12、13、14 条通过 | 否 | | 15 | **LOGO 下发流程** | 一次性下发 LOGO 帧(`info[4..5] = 0x89 0x45`,宽高大端);放在出厂 / 联调流程,**不进常规刷新循环** | §5.16.10 第 15 条通过;断电重启后 LOGO 仍在 | 否 | -| 16 | **日志口迁移(USART1 → RS-232 通道)** | ① `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 RS232#3(`huart2` / USART2,默认)或 RS232#1(`huart5` / UART5);② 该实例改 **`UART_MODE_TX`**(只发不收,避免重演 ORE 锁存);③ **不动** UART4 的 `g_rs485` 绑定 | 日志在 RS-232 侧(CN1)可读且内容与迁移前一致;USART1 **完全空出**;连续 10 分钟无 ORE 锁存、无 `[ERR]` | 否 | +| 16 | **日志口迁移(USART1 → RS-232 通道)** | ① `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 RS232#1(`huart5` / UART5);② **不动** UART4 的 `g_rs485` 绑定 | 日志在 RS-232 侧(CN1)可读且内容与迁移前一致;USART1 **完全空出**;连续 10 分钟无 `[ERR]` | 否 | **批次划分**: diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index 01f8e82c..c63c2a35 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -111,7 +111,7 @@ > 现有的 TTL CH340 接不上去(接上去轻则不通信,重则打坏 `U13` 或适配器)。 > 另:`U14` 是 RS232#3 的网口座(3 脚 `RS232_TX3` / 6 脚 `RS232_RX3`),**本通道用不上**——别把它当成备选端子。 -**迁移时收口成一处端口宏,别散改**:`huart5` 现在是 `115200 8N1 / UART_MODE_TX_RX`(`Src/usart.c:74-81`), +**迁移时收口成一处端口宏,别散改**:`huart5` 已是 `115200 8N1`(`Src/usart.c:74-81`), 业务代码里没人用,只有 `Src/stm32f4xx_it.c:565` 挂着 IRQ 入口。 > ⚠ **USART1 交接前必须确认两件事**:① 控制台已不再写 `USART1->DR`(否则两条流互相插字节, @@ -195,7 +195,7 @@ | 序 | 任务 | 改动点 | 判据 | |---|---|---|---| | 0-1 | **台架串口线挪位** | 硬件:**换成 USB 转 RS-232 适配器**(TTL 的 CH340 直连不了),接 RS232#1 的三个端子——适配器 TXD → **CN1.10**(板子 `RS232_RX1`)、RXD → **CN1.12**(板子 `RS232_TX1`)、GND → **CN1.7 或 CN1.8**(`CGND`)。端子号出自 `docs/硬件平台/V0.1.tel`;`U14` 是 RS232#3 的网口座,本通道用不上 | `serial-assistant` 在 RS-232 侧能收到日志 | -| 0-2 | **日志口迁移(USART1 → RS-232)** | ① `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 **`huart5`(RS232#1,已定)**;② 该实例改 **`UART_MODE_TX`**(只发不收,把 RX 从机制上摘掉)——`huart5` 现为 `UART_MODE_TX_RX`,**改在 `.ioc` 里**(`Src/usart.c` 是 CubeMX 生成物,只写 USER CODE) | 日志在 RS-232 侧可读、内容与迁移前一致;连续 10 min 无 ORE 锁存、无 `[ERR]`;USART1 完全空出 | +| 0-2 | **日志口迁移(USART1 → RS-232)** | `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 **`huart5`(RS232#1,已定)** | 日志在 RS-232 侧可读、内容与迁移前一致;连续 10 min 无 `[ERR]`;USART1 完全空出 | | 0-3 | **落定 USART1 到屏板的接线** | 按决策一:TTL 直连屏板 UART2 侧(PA2/PA3);把线束接法与验收方式写清楚 | 接法落纸;照此接线后屏板发来的 6 B 召唤帧能被收到 | > PA10 已接线(用户 2026-10-28 确认),屏板链路**不需要额外飞线**。 @@ -346,7 +346,6 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 | **TTL 适配器直插 CN1** | 沿用那根 CH340 去接 RS-232 端子 | 不通信,且 RS-232 的 ±电平可能打坏 `U13` 或适配器 | 0-1 已写死「换 USB 转 RS-232 适配器」;RS232#1 无 TTL 出口,只能走电平转换后的 CN1 | | **控制台与屏板链路抢 `USART1->DR`** | 日志没迁干净就接屏板 | 屏板花屏 + 日志乱码,像 CRC / 时序错 | 0-2 的判据明确要求"USART1 完全空出" | | **一次噪声后链路永久静默** | 错误回调里不清 ORE | 屏板召唤再也收不到,症状与"协议不对"完全一样 | 1-3 判据含"拔插 TX 线制造噪声后自动恢复" | -| **日志口又染上 ORE 锁存** | 新日志口照抄 `UART_MODE_TX_RX` | 控制台静默、看门狗误复位(老病复发) | 新日志口设 `UART_MODE_TX`;判据含"连续 10 min 无 ORE" | | 帧长按 3208 实现 | 照 readme 写 | CRC 全失败、屏板不亮 | 已写进 §4.2 警示;联调第一步就核 4208 | | 中断回调漏加分支 | 只改自己模块,忘了那三个全局 UART 回调里唯一的一处实现只分发另一条通道的句柄(§1.3) | 收不到召唤帧,表象像协议错 | 批 1 的 1-3 已列为判据 | | 屏板自复位被误判离线 | 链路监视窗口设得太小 | 每 83 分钟误报一次 | 判据定为"连续 ≥10 s 无帧" | @@ -365,7 +364,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 做完这一项,要能同时满足(逐条可测,对应 `docs/产品需求/产品需求设计文档.md` §5.16.10 第 8~15 条): -0. **腾位置**:日志在 RS-232 侧可读、内容与迁移前一致;USART1 已无任何控制台写入;连续 10 min 无 ORE 锁存。 +0. **腾位置**:日志在 RS-232 侧可读、内容与迁移前一致;USART1 已无任何控制台写入。 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` 不产生事件。 From f5161e156717619da716313c140167fd09b9784c Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 18:45:59 +0800 Subject: [PATCH 03/16] =?UTF-8?q?feat(=E6=97=A5=E5=BF=97):=20=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E5=8F=B0=E8=BF=81=E5=88=B0=20RS232#1/UART5=EF=BC=8CUS?= =?UTF-8?q?ART1=20=E8=AE=A9=E4=BD=8D=E7=BB=99=E5=B1=8F=E6=9D=BF=E9=93=BE?= =?UTF-8?q?=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 批 0 的第 2 项。USART1 是唯一 MCU 引脚直出的 TTL 通道,700 kbaud 的屏板 链路要用它,控制台必须先让开。 console_log.c 加一处端口收口块,换口只改这一段: CON_UART / CON_UART_IRQN / CON_UART_CLK_DISABLE/ENABLE / CON_UART_INIT 代码实际改动 4 处:con_tx_bounded 的 huart1.Instance(NULL 判据、counted 回退、赋值)与 con_uart_reinit 全函数体。其余是把描述当前行为的注释里的 USART1 换成「控制台串口」;历史变更记录(2026-10-02 / 10-24)里的 USART1 保持原样,那是当时的实况。 注意事项:UART5 在 APB1、USART1 在 APB2,时钟宏不能照抄; __HAL_RCC_UART5_CLK_ENABLE 定义在 stm32f4xx_hal_rcc_ex.h。 不改 UART 模式(2026-10-26 裁定:保持 UART_MODE_TX_RX,不动 RX 配置)。 MX_UART5_Init 在 Src/main.c:135,比 MX_USART1_UART_Init 晚几个初始化调用, 该窗口内的日志由 CON_UART.Instance == NULL 短路接住(记 dropped,不崩)。 构建:Code=236,460 RO=28,124 RW=1,876 ZI=137,084(Code 较迁移前 +4 B)。 台架验证(COM3 @115200,RS232#1 经 CN1.10/CN1.12): - 烧录回读 no difference found;8 个 required 锚点全中,无 forbidden; 启动到 net init: OK 用时 661 ms - 连续 10 分钟长稳:1218 行,零 [ERR]、零 dropped、零 tx_fault, 末行仍在正常 PROF 输出 - USART1 已无任何控制台写入(全文件 grep 只剩历史注释) --- App/log/console_log.c | 52 ++++++++++++++++---------- App/log/console_log.h | 2 +- docs/内存管理/RAM使用分析与优化方案.md | 4 +- docs/内存管理/RAM预算台账.md | 2 +- scripts/ram_baseline.tsv | 2 +- scripts/size_attestation.json | 12 +++--- 6 files changed, 43 insertions(+), 31 deletions(-) diff --git a/App/log/console_log.c b/App/log/console_log.c index 41386b23..e8c43b24 100644 --- a/App/log/console_log.c +++ b/App/log/console_log.c @@ -1,5 +1,5 @@ /* - * 模块名称:Console Single-Writer Logging — 控制台单写者日志(USART1)实现 + * 模块名称:Console Single-Writer Logging — 控制台单写者日志(RS232#1 / UART5)实现 * 模块功能:控制台单写者日志的环形缓冲 + 唯一写者发送实现(API 见 console_log.h); * 兼载 MicroLIB printf 底层 fputc 重定向 * 适用平台:STM32F407ZGTx(仅 MCU 端) @@ -99,6 +99,18 @@ * 与当场直发逐字节相同。ISR / 调度器未启动两种上下文放行(不许等)。 * 约束:不新增任务、不动态分配;环形缓冲为 BSS(SRAM1,非 CCM),无 DMA 可达性冲突。 */ + +/* ===== 控制台端口(单点收口)===== + * 换口只改这一段:句柄 / IRQn / 初始化函数。时钟宏随句柄走——UART5 在 APB1、 + * USART1 在 APB2,别照抄;串口模式与波特率在 Src/usart.c 的 MX_*_Init 里,本模块不管。 + * 2026-10-29 批 0:USART1 让位给屏板链路(700 kbaud 只有它 MCU 引脚直出),控制台迁 + * RS232#1/UART5。缘由、端子与验收见 docs/任务规划/屏板链路.md 的批 0。 */ +#define CON_UART huart5 +#define CON_UART_IRQN UART5_IRQn +#define CON_UART_CLK_DISABLE() __HAL_RCC_UART5_CLK_DISABLE() +#define CON_UART_CLK_ENABLE() __HAL_RCC_UART5_CLK_ENABLE() +#define CON_UART_INIT() MX_UART5_Init() + #define DBG_CON_RING_SIZE 4096U /* 环形缓冲字节数(必须为 2 的幂) */ #define DBG_CON_CHUNK_MAX 256U /* 写者单次 HAL_UART_Transmit 字节上限 */ #define DBG_CON_WAIT_MS 200U /* 缓冲满时生产者最长等待(ms),仍满则丢弃并计数 */ @@ -140,7 +152,7 @@ static volatile uint8_t s_con_draining; /* 1 = 处于 drain 中(重入保 static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */ static volatile uint8_t s_con_dwt_up; /* 1 = DWT CYCCNT 已使能(硬上界基准可用) */ static TaskHandle_t s_con_writer_task; /* 写者任务句柄(自等保护用) */ -/* 行级单一所有者(道G task-23,O1 第二实例):USART1 数据寄存器同一时刻只允许 +/* 行级单一所有者(道G task-23,O1 第二实例):控制台串口数据寄存器同一时刻只允许 * 一个上下文在逐字节轮询写入。NULL = 空闲。 * ⚠ 这不是"锁",是**所有权凭证**:拿不到就当场让路,绝不排队等待(见 * con_tx_try_claim 函数头:裸发一行 @115200 约 7.8 ms,把它包进临界区等于给 @@ -228,7 +240,7 @@ static void con_dwt_ensure(void) { * 入口参数:u - USART 寄存器基址(调用方已判非 NULL) USART_TypeDef * * p_buf - 待发数据;len - 字节数 * 返回值:HAL_OK - 已发出;HAL_TIMEOUT - 计数上界内未完成(本块放弃) - * 限定条件:USART1 已初始化 + * 限定条件:控制台串口已初始化 * 函数说明:刻意不用 HAL_UART_Transmit——理由见 CON_TX_GUARD_MAX 处注释 * (tick 依赖不可信 + 其调用链污染所有任务的最坏栈链) */ @@ -258,12 +270,12 @@ static HAL_StatusTypeDef con_tx_counted(USART_TypeDef *u, const uint8_t *p_buf, * 函数功能:有界裸发送(不依赖 uwTick / HAL 超时) * 入口参数:p_buf - 待发数据;len - 字节数(调用方已限 ≤ DBG_CON_CHUNK_MAX) * 返回值:HAL_OK - 已全部写入发送寄存器;HAL_TIMEOUT - 有界上界内未完成 - * 限定条件:USART1 已初始化 + * 限定条件:控制台串口已初始化 * 函数说明:寄存器级轮询 TXE/TC,超时判据用 DWT CYCCNT(CON_TX_TIMEOUT_CYC); * DWT 未使能时走 con_tx_counted(指令计数上界,2026-10-10 取代 * HAL_UART_Transmit 回退)。本函数只做 TX, * 不触碰 RX/错误标志——僵尸通道自愈由调用方按返回值处理。 - * ⚠ USART1 未初始化(huart1.Instance == NULL,如 HAL_Init 早期或 + * ⚠ 控制台串口未初始化(CON_UART.Instance == NULL,如 HAL_Init 早期或 * 断言/故障上报路径)时立即返回 HAL_ERROR:否则会解引用 NULL 去读写 * 地址 0(flash 别名区)→ 在故障处理里再触发故障(2026-10-02 台架实测)。 */ @@ -273,14 +285,14 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) { uint16_t i; con_dwt_ensure(); - if (huart1.Instance == NULL) { - return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位(不是超时,不记超时账) */ + if (CON_UART.Instance == NULL) { + return HAL_ERROR; /* 控制台串口未初始化:无可用通道,安全让位(不是超时,不记超时账) */ } if (s_con_dwt_up == 0U) { - return con_tx_counted(huart1.Instance, p_buf, len); + return con_tx_counted(CON_UART.Instance, p_buf, len); } - u = huart1.Instance; + u = CON_UART.Instance; t0 = DWT->CYCCNT; for (i = 0U; i < len; i++) { while ((u->SR & USART_SR_TXE) == 0U) { @@ -299,7 +311,7 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) { } /* - * 函数功能:尝试取得"USART1 整行发送所有权"(**非阻塞**,道G task-23) + * 函数功能:尝试取得"控制台串口整行发送所有权"(**非阻塞**,道G task-23) * 返回值:CON_TX_HELD 已持有(发完必须 con_tx_release)/ CON_TX_BYPASS 放行且不记账 * / CON_TX_NO 别人正持有整行,调用方须让路 * 限定条件:任意上下文(内部只有一次几条指令的临界区,绝不睡眠、绝不长期关中断) @@ -473,7 +485,7 @@ uint8_t dbg_console_writer_ready(void) { } /* - * 函数功能:USART1 僵尸通道自愈(清错误锁存 + 整外设重初始化) + * 函数功能:控制台串口僵尸通道自愈(清错误锁存 + 整外设重初始化) * 函数说明:**RX 使能了却从不读 DR** → 线上任意一个字节或一个噪声沿使 RXNE 置位后无人读, * 下一个事件即锁存 ORE(常伴 NE/FE/PE);ORE 只能靠"先读 SR 再读 DR"清除 ⇒ 永不自愈。 * 锁存后 HAL 轮询 TX 恒快速失败(控制台静默、系统不复位);发送器卡死状态则使 TXE @@ -484,13 +496,13 @@ uint8_t dbg_console_writer_ready(void) { * (详见 docs/子系统设计/控制台单写者日志设计说明.md §12)。 */ static void con_uart_reinit(void) { - __NVIC_ClearPendingIRQ(USART1_IRQn); - __HAL_RCC_USART1_CLK_DISABLE(); - __HAL_RCC_USART1_CLK_ENABLE(); - huart1.gState = HAL_UART_STATE_RESET; - huart1.RxState = HAL_UART_STATE_RESET; - huart1.ErrorCode = HAL_UART_ERROR_NONE; - (void)MX_USART1_UART_Init(); + __NVIC_ClearPendingIRQ(CON_UART_IRQN); + CON_UART_CLK_DISABLE(); + CON_UART_CLK_ENABLE(); + CON_UART.gState = HAL_UART_STATE_RESET; + CON_UART.RxState = HAL_UART_STATE_RESET; + CON_UART.ErrorCode = HAL_UART_ERROR_NONE; + (void)CON_UART_INIT(); } /* @@ -512,7 +524,7 @@ void dbg_console_drain(void) { return; /* 重入保护:写者自记录不触发嵌套 drain */ } - /* 整行所有权(道G task-23):本函数按块直写 USART1->DR,若此刻别的应用上下文 + /* 整行所有权(道G task-23):本函数按块直写 CON_UART->DR,若此刻别的应用上下文 * 还在未注册期那条裸发里发到自己一半(attach 就发生在 defaultTask 里,与别的 * 任务裸发**可以**前后脚撞上),两边就会逐字节交错——那正是本卡要修的形态。 * 拿不到就**本轮让位**:tail 不推进 => 环里一条不少,主循环 ≤50 ms 后再试, @@ -560,7 +572,7 @@ void dbg_console_drain(void) { if (st != HAL_OK) { if (st == HAL_ERROR) { - /* 2026-10-24 修复:USART1 **未初始化**(huart1.Instance == NULL, + /* 2026-10-24 修复:控制台串口 **未初始化**(CON_UART.Instance == NULL, * 只可能出现在 HAL_Init 早期或断言/故障上报路径)与"僵尸通道"是两回事: * 这里**绝不做外设重初始化**——那会在 HAL 尚未就绪时跑 MspInit 重配 * RCC/GPIO;也不计 tx_fault(那本账的语义是"软复位自愈次数")。 diff --git a/App/log/console_log.h b/App/log/console_log.h index d1fccc8c..1dec33ae 100644 --- a/App/log/console_log.h +++ b/App/log/console_log.h @@ -1,5 +1,5 @@ /* - * 模块名称:Console Single-Writer Logging — 控制台单写者日志(USART1 调试串口) + * 模块名称:Console Single-Writer Logging — 控制台单写者日志(RS232#1 / UART5) * 模块功能:调试控制台单写者环形缓冲的公共 API:所有任务/上下文(含 printf)只把 * 已格式化整行文本入环形缓冲,写者任务(defaultTask)为唯一 * HAL_UART_Transmit 发送点(行级原子 + 指针可控) diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index e849d09c..0e987c7a 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 264,580 B (258.38 KB) | 1,048,576 B (1024 KB) | 25.2% | +| ER_IROM1 | 264,584 B (258.38 KB) | 1,048,576 B (1024 KB) | 25.2% | | RW_IRAM1 | 81,536 B (79.62 KB) | 131,072 B (128 KB) | 62.2% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **266,456 B (260.2 KB)** | 1,048,576 B (1024 KB) | **25.4%** | +| **Flash (ROM 合计)** | **266,460 B (260.2 KB)** | 1,048,576 B (1024 KB) | **25.4%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 54b8cf8d..e7d38b57 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,7 +15,7 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 264,580 B | 1,048,576 B | 25.2% | 参考 | +| ER_IROM1 (Flash) | 264,584 B | 1,048,576 B | 25.2% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | | RW_IRAM1 | 81,536 B | 131,072 B | 62.2% | PASS | diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index a550ef23..18056dc7 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -11,4 +11,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736 2026-10-27 235372 26392 1572 136924 261764 25.0 81072 61.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 2026-10-28 236456 28124 1876 137084 264580 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 -2026-10-29 236456 28124 1876 137084 264580 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 +2026-10-29 236460 28124 1876 137084 264584 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index d0f60d61..8a9c33ec 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,15 +1,15 @@ { - "built_at": "2026-10-29T17:51:46+08:00", - "fingerprint": "d9848eb18efe7902c936a791c0b649900c0088d4f2c582e0a71d9998ba9ae0f4", + "built_at": "2026-10-29T18:43:32+08:00", + "fingerprint": "bfe075e7093727025c3ba21e3057445a6c23ec4b0193c7e14286cc6ca60f6f1f", "fingerprint_files": 1452, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "c51ac2e8ef09dbd51b1e81a82b73becc55ca1c3e", + "git_head": "27a700b0d81cde580d88f783d6cb661aac248516", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793267423, + "map_mtime": 1793270606, "pct": { "ER_IROM1": 25.23, "RW_CCM": 87.89, @@ -19,7 +19,7 @@ "regions": { "ER_IROM1": { "max": 1048576, - "size": 264580 + "size": 264584 }, "RW_CCM": { "max": 65280, @@ -37,7 +37,7 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 236456, + "code": 236460, "ro_data": 28124, "rw_data": 1876, "zi_data": 137084 From 7dbc8f867624df7c729ac15f198778244dde6aeb Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 18:46:25 +0800 Subject: [PATCH 04/16] =?UTF-8?q?docs(=E5=B1=8F=E6=9D=BF):=20=E6=89=B9=200?= =?UTF-8?q?=20=E7=9A=84=200-1=20/=200-2=20=E6=A0=87=E8=AE=B0=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0-1:台架线已换成 USB 转 RS-232 适配器,接 CN1.10 / CN1.12 / CN1.8, COM3 在 RS-232 侧可读可抓。 0-2:提交 f5161e15,控制台端口收口到 huart5。判据全部实测: 开机日志 8 个锚点全中、启动到 net init: OK 用时 661 ms、 连续 10 分钟 1218 行零 [ERR] 零 dropped、USART1 已无控制台写入。 --- docs/任务规划/屏板链路.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index c63c2a35..04ff1089 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -194,8 +194,8 @@ | 序 | 任务 | 改动点 | 判据 | |---|---|---|---| -| 0-1 | **台架串口线挪位** | 硬件:**换成 USB 转 RS-232 适配器**(TTL 的 CH340 直连不了),接 RS232#1 的三个端子——适配器 TXD → **CN1.10**(板子 `RS232_RX1`)、RXD → **CN1.12**(板子 `RS232_TX1`)、GND → **CN1.7 或 CN1.8**(`CGND`)。端子号出自 `docs/硬件平台/V0.1.tel`;`U14` 是 RS232#3 的网口座,本通道用不上 | `serial-assistant` 在 RS-232 侧能收到日志 | -| 0-2 | **日志口迁移(USART1 → RS-232)** | `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 **`huart5`(RS232#1,已定)** | 日志在 RS-232 侧可读、内容与迁移前一致;连续 10 min 无 `[ERR]`;USART1 完全空出 | +| 0-1 ✅ | **台架串口线挪位** | 硬件:**换成 USB 转 RS-232 适配器**(TTL 的 CH340 直连不了),接 RS232#1 的三个端子——适配器 TXD → **CN1.10**(板子 `RS232_RX1`)、RXD → **CN1.12**(板子 `RS232_TX1`)、GND → **CN1.7 或 CN1.8**(`CGND`)。端子号出自 `docs/硬件平台/V0.1.tel`;`U14` 是 RS232#3 的网口座,本通道用不上 | **已完成 2026-10-29**:COM3 在 RS-232 侧可读可抓(`serial-assistant` 正常) | +| 0-2 ✅ | **日志口迁移(USART1 → RS-232)** | `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 **`huart5`(RS232#1,已定)** | **已完成 2026-10-29**(`f5161e15`):COM3 抓到完整开机日志、8 个锚点全中、启动到 `net init: OK` 661 ms;连续 10 min 1218 行零 `[ERR]`/零 dropped;USART1 已无控制台写入 | | 0-3 | **落定 USART1 到屏板的接线** | 按决策一:TTL 直连屏板 UART2 侧(PA2/PA3);把线束接法与验收方式写清楚 | 接法落纸;照此接线后屏板发来的 6 B 召唤帧能被收到 | > PA10 已接线(用户 2026-10-28 确认),屏板链路**不需要额外飞线**。 From 8a0cb464c0fea271b1dd61bc5e5f9e6add6b482e Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 18:56:55 +0800 Subject: [PATCH 05/16] =?UTF-8?q?test(=E6=97=A5=E5=BF=97):=20=E4=B8=BB?= =?UTF-8?q?=E6=9C=BA=E5=8D=95=E6=B5=8B=E6=A1=A9=E8=B7=9F=E5=88=B0=20UART5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生产源 f5161e15 已把控制台端口收口到 CON_UART(=huart5),桩若还停在 huart1 / MX_USART1_UART_Init,主机单测会因符号缺失编不过;更要紧的是 huart1.Instance = 0 那几组用例会失去"串口未初始化"的原意——改成填一个 生产代码根本不碰的句柄,短路照样命中,但测的已经不是真实那条路径。 - stub/usart.h:暴露 huart5 + MX_UART5_Init,并按真实 Inc/usart.h 校正 "真实那份还声明哪些"的描述(huart1~4、MX_USART1~3_UART_Init、 MX_UART4_Init)。 - stub/stm32f4_fake.h:USART1_IRQn -> UART5_IRQn, __HAL_RCC_USART1_CLK_DISABLE/ENABLE -> __HAL_RCC_UART5_*;假外设块的 标题与位注释去掉具体实例号(它本来就是通用模型,不是某个实例)。 - console_log_test.c:huart1 -> huart5(19 处)、 MX_USART1_UART_Init -> MX_UART5_Init,含一条用例的打印文案。 纯桩与测试改动,不碰生产源;153/153 PASSED。 --- test/host/console_log/console_log_test.c | 42 +++++++++++------------ test/host/console_log/run_unit_test.py | 4 +-- test/host/console_log/stub/stm32f4_fake.h | 16 ++++----- test/host/console_log/stub/usart.h | 10 +++--- 4 files changed, 37 insertions(+), 35 deletions(-) diff --git a/test/host/console_log/console_log_test.c b/test/host/console_log/console_log_test.c index 34975d52..80d72b6b 100644 --- a/test/host/console_log/console_log_test.c +++ b/test/host/console_log/console_log_test.c @@ -6,7 +6,7 @@ * 起是**整行独占**,不再是"无锁":M 族 TC-CONLOG-170 至 TC-CONLOG-179 钉"别的写者插不进 * 一行、让路行逐字节等同直发、让位不丢、真溢出才 [ERR]")、缓冲满有界等待与 * 丢弃可观测、TX 僵尸通道软复位自愈 + 300ms 冷却、DWT 与指令计数两种硬上界、 - * huart1.Instance 未初始化的 NULL 短路、fputc 重定向、drain 250ms 预算、 + * huart5.Instance 未初始化的 NULL 短路、fputc 重定向、drain 250ms 预算、 * 整行所有权(行级单一所有者,道G task-23)。 * * 为什么要主机侧单测(而不是只用台架): @@ -19,7 +19,7 @@ * * 桩的边界(细节见 stub/stm32f4_fake.h 头注释): * console_log.h / sys_events.h(含真实 SYS_EVT_CONSOLE_TX 位)用**真实头**; - * usart.h 用桩,但只暴露本模块访问的 huart1 + MX_USART1_UART_Init,寄存器位号 / + * usart.h 用桩,但只暴露本模块访问的 huart5 + MX_UART5_Init,寄存器位号 / * 枚举值照抄真实头;USART SR 的 TXE/TC、DWT CYCCNT、HAL_GetTick、临界区、 * __get_IPSR、vTaskDelay、osEventFlagsSet 做成可控 + 可观测的量。 * ⚠ 本文件**刻意不定义**任何 syslog_* / DBG_* 符号:console_log.c 哪天把"TX 故障 @@ -38,7 +38,7 @@ * 读后清、300ms 冷却两端、tick==0 也进冷却(哨兵撞值修好后为正向断言)、 * 半块 / 已上线仍判失败 * 090..106 DWT 惰性使能三处寄存器写、使能失败退回 20000 次指令计数、超时判据不读 - * HAL_GetTick(第 8b 轮台架动机)、huart1.Instance == NULL 短路两路 + + * HAL_GetTick(第 8b 轮台架动机)、huart5.Instance == NULL 短路两路 + * "未初始化绝不 reinit"(误自愈修好后为正向断言)+ 证据不蒸发 * 110..117 fputc 重定向(未注册直发 / 注册逐字节入环 / ISR 上下文裸发 / 环满时 ISR * 仍不等待) @@ -112,7 +112,7 @@ uint32_t SystemCoreClock = 168000000U; /* 与 MCU 端一致(168 MHz) */ USART_TypeDef g_con_uart; CoreDebug_Type g_con_core_debug; DWT_Type g_con_dwt; -UART_HandleTypeDef huart1; +UART_HandleTypeDef huart5; uint32_t con_uart_tx_n; uint8_t con_uart_tx[CON_UART_TX_MAX]; @@ -282,12 +282,12 @@ osStatus_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags) } /* 真实版是 CubeMX 生成的 HAL_UART_Init 封装;桩按脚本决定"软复位到底治没治好" */ -void MX_USART1_UART_Init(void) +void MX_UART5_Init(void) { s_mx_init_calls++; - huart1.Instance = &g_con_uart; - huart1.gState = HAL_UART_STATE_READY; - huart1.ErrorCode = HAL_UART_ERROR_NONE; + huart5.Instance = &g_con_uart; + huart5.gState = HAL_UART_STATE_READY; + huart5.ErrorCode = HAL_UART_ERROR_NONE; if (s_mx_repair != 0) { con_uart_txe_stuck = 0; con_uart_txe_after = 0; @@ -413,10 +413,10 @@ static void reset_all(void) s_mx_init_calls = 0U; s_mx_repair = 0; - huart1.Instance = &g_con_uart; - huart1.gState = HAL_UART_STATE_READY; - huart1.RxState = HAL_UART_STATE_READY; - huart1.ErrorCode = HAL_UART_ERROR_NONE; + huart5.Instance = &g_con_uart; + huart5.gState = HAL_UART_STATE_READY; + huart5.RxState = HAL_UART_STATE_READY; + huart5.ErrorCode = HAL_UART_ERROR_NONE; } /* ===================== 观察辅助 ===================== */ @@ -983,12 +983,12 @@ static void tc_dwt(void) "TC-CONLOG-099 the DWT branch declares timeout on the first spin (the ceiling really works, never burns 20000)"); } -/* ============ TC-CONLOG-100..106 huart1.Instance == NULL 短路 + 未初始化分诊 ============ */ +/* ============ TC-CONLOG-100..106 huart5.Instance == NULL 短路 + 未初始化分诊 ============ */ static void tc_null_uart(void) { - printf("-- TC-CONLOG-100..106 USART1 not initialised: yield, never reinit, keep proof --\n"); + printf("-- TC-CONLOG-100..106 UART5 not initialised: yield, never reinit, keep proof --\n"); reset_all(); - huart1.Instance = 0; + huart5.Instance = 0; dbg_console_write_raw("boom", 5U); CHECK(con_uart_tx_n == 0U && con_uart_txe_reads == 0U, "TC-CONLOG-100 with Instance==NULL not one register is touched (no dereference of address 0)"); @@ -996,7 +996,7 @@ static void tc_null_uart(void) "TC-CONLOG-101 DWT is still lazily enabled before the short circuit (ensure stays ahead of the NULL check)"); reset_all(); - huart1.Instance = 0; + huart5.Instance = 0; CHECK(dbg_console_write_line("early", 5U) == 5, "TC-CONLOG-102 the unattached path (pre-kernel) survives a missing UART and still returns the line length"); @@ -1004,7 +1004,7 @@ static void tc_null_uart(void) reset_all(); attach_writer(); (void)dbg_console_write_line("abc", 3U); - huart1.Instance = 0; /* 写者起来了但串口还没初始化 */ + huart5.Instance = 0; /* 写者起来了但串口还没初始化 */ s_mx_repair = 1; writer_drain(); CHECK(s_mx_init_calls == 0U && con_rcc_disables == 0U && con_nvic_clears == 0U, @@ -1014,7 +1014,7 @@ static void tc_null_uart(void) CHECK(dbg_console_dropped() == 1U && used_now() == 0U, "TC-CONLOG-105 the block that left the ring but never hit the wire counts as dropped"); - huart1.Instance = &g_con_uart; /* 串口随后初始化好 */ + huart5.Instance = &g_con_uart; /* 串口随后初始化好 */ con_uart_tx_n = 0U; writer_drain(); CHECK(tx_contains("[ERR] CONSOLE dropped 1 lines") && dbg_console_dropped() == 0U, @@ -1159,7 +1159,7 @@ static void tc_three_ledgers(void) reset_all(); attach_writer(); (void)dbg_console_write_line("xyz", 3U); - huart1.Instance = 0; + huart5.Instance = 0; writer_drain(); CHECK(dbg_console_dropped() == 1U && dbg_console_tx_timeout() == 0U && dbg_console_tx_fault() == 0U, @@ -1193,11 +1193,11 @@ static void tc_three_ledgers(void) con_dwt_step = SystemCoreClock / 4U; (void)dbg_console_write_line("0123456789ABCDEFGHIJ", 20U); /* 缓冲满丢 1 行 */ con_dwt_step = 0U; - huart1.Instance = 0; /* 告警那一发也发不出去 */ + huart5.Instance = 0; /* 告警那一发也发不出去 */ writer_drain(); CHECK(dbg_console_dropped() >= 1U, "TC-CONLOG-148 drop count survives while the channel is unavailable (evidence no longer washed away)"); - huart1.Instance = &g_con_uart; + huart5.Instance = &g_con_uart; con_uart_tx_n = 0U; writer_drain(); CHECK(tx_contains("[ERR] CONSOLE dropped") && dbg_console_dropped() == 0U, diff --git a/test/host/console_log/run_unit_test.py b/test/host/console_log/run_unit_test.py index 44c97d1f..483c5503 100644 --- a/test/host/console_log/run_unit_test.py +++ b/test/host/console_log/run_unit_test.py @@ -10,7 +10,7 @@ 缓冲满两条上界 (DWT 周期 / 200 tick)与丢弃计数、[ERR] 告警行不经环(不递归)、TX 僵尸通道软复位 自愈 + 故障即止 + 300ms 冷却(含无符号回绕)、DWT 使能失败退回 20000 次指令计数、 - huart1.Instance==NULL 短路、fputc 重定向、drain 250ms 预算、行级单一所有者。 + huart5.Instance==NULL 短路、fputc 重定向、drain 250ms 预算、行级单一所有者。 工具链与判定委托 test/host/common/unit_test_harness.py。 为什么要主机侧单测(而不是只用台架): @@ -26,7 +26,7 @@ docs/子系统设计/控制台单写者日志设计说明.md(G1~G5 / K1~K5 / §12 ORE 僵尸通道)。 桩的边界:console_log.h 与 sys_events.h 用真实头(SYS_EVT_CONSOLE_TX 位定义一改即 - 编译期报错);usart.h 是桩但只暴露本模块访问的 huart1 + MX_USART1_UART_Init, + 编译期报错);usart.h 是桩但只暴露本模块访问的 huart5 + MX_UART5_Init, USART SR 位号、DWT/CoreDebug 位掩码、HAL_StatusTypeDef 枚举值均照抄真实头, 宿主编不动的那条 main.h -> stm32f4xx_hal.h -> core_cm4.h 链由 stub/stm32f4_fake.h 以普通结构体替代(理由写在该文件头注释里)。 diff --git a/test/host/console_log/stub/stm32f4_fake.h b/test/host/console_log/stub/stm32f4_fake.h index ed7db951..5f285c9b 100644 --- a/test/host/console_log/stub/stm32f4_fake.h +++ b/test/host/console_log/stub/stm32f4_fake.h @@ -2,7 +2,7 @@ * 模块名称:主机侧单测桩 - 假 HAL/CMSIS 寄存器面 + 内核入口(console_log 模块专用) * 模块功能:让 App/log/console_log.c 在 PC 上编起来,并把它依赖的硬件/内核面做成 * **可脚本控制**的模型: - * ① USART1:TXE/TC 可注入「就绪 / 永远不就绪(僵尸通道)/ 前 N 次不就绪」; + * ① 假串口外设:TXE/TC 可注入「就绪 / 永远不就绪(僵尸通道)/ 前 N 次不就绪」; * 写进 DR 的字节被收进一条上线字节日志,并按「一次 TC 就绪 = 一块」记块长。 * ② DWT CYCCNT:每次访问推进 con_dwt_step 个周期(= 给时间定价), * CTRL 的 CYCCNTENA 可脚本成「写不进去」以走 con_tx_counted 支路。 @@ -19,7 +19,7 @@ * DWT_Type.CTRL/CYCCNT + DWT_CTRL_CYCCNTENA_Msk=bit0、CoreDebug_Type.DEMCR + * CoreDebug_DEMCR_TRCENA_Msk=bit24(core_cm4.h) * HAL_StatusTypeDef{OK=0,ERROR=1,BUSY=2,TIMEOUT=3}(stm32f4xx_hal_def.h) - * HAL_UART_STATE_RESET=0x00、HAL_UART_ERROR_NONE=0x00、USART1_IRQn=7 + * HAL_UART_STATE_RESET=0x00、HAL_UART_ERROR_NONE=0x00、UART5_IRQn=7 * UART_HandleTypeDef 只搬本模块访问的 4 个字段(Instance/gState/RxState/ErrorCode), * 余下(Init/Lock/...)刻意不搬:抄半份 HAL 结构只会更快与真实头漂移。 * @@ -52,11 +52,11 @@ #include /* ===== 与真实头同值的位 / 枚举(来源见文件头) ===== */ -#define USART_SR_TXE_Msk (0x0080U) /* USART1->SR bit7 发送寄存器空 */ -#define USART_SR_TC_Msk (0x0040U) /* USART1->SR bit6 发送完成 */ +#define USART_SR_TXE_Msk (0x0080U) /* SR bit7 发送寄存器空 */ +#define USART_SR_TC_Msk (0x0040U) /* SR bit6 发送完成 */ #define DWT_CTRL_CYCCNTENA_Msk (0x00000001U) #define CoreDebug_DEMCR_TRCENA_Msk (0x01000000U) -#define USART1_IRQn 7 +#define UART5_IRQn 7 #define HAL_UART_STATE_RESET 0x00U #define HAL_UART_STATE_READY 0x20U @@ -75,7 +75,7 @@ typedef enum { extern uint32_t SystemCoreClock; /* ------------------------------------------------------------------ */ -/* ① 假 USART1 */ +/* ① 假串口外设 */ /* ------------------------------------------------------------------ */ typedef struct { volatile uint32_t SR; @@ -368,7 +368,7 @@ extern uint32_t con_nvic_clears; extern uint32_t con_rcc_disables; extern uint32_t con_rcc_enables; #define __NVIC_ClearPendingIRQ(irqn) do { (void)(irqn); con_nvic_clears++; } while (0) -#define __HAL_RCC_USART1_CLK_DISABLE() do { con_rcc_disables++; } while (0) -#define __HAL_RCC_USART1_CLK_ENABLE() do { con_rcc_enables++; } while (0) +#define __HAL_RCC_UART5_CLK_DISABLE() do { con_rcc_disables++; } while (0) +#define __HAL_RCC_UART5_CLK_ENABLE() do { con_rcc_enables++; } while (0) #endif /* STM32F4_FAKE_H */ diff --git a/test/host/console_log/stub/usart.h b/test/host/console_log/stub/usart.h index bc22ea7c..e6690061 100644 --- a/test/host/console_log/stub/usart.h +++ b/test/host/console_log/stub/usart.h @@ -1,8 +1,10 @@ /* * 模块名称:主机侧单测桩 - usart.h(console_log 模块专用) * 模块功能:与真实 Inc/usart.h 对本模块可见的部分逐字一致:extern UART_HandleTypeDef - * huart1; + void MX_USART1_UART_Init(void);(真实那份还声明 huart2~5 与 - * MX_USART2~5_UART_Init,本模块只碰 USART1,搬多出来的声明没有意义)。 + * huart5; + void MX_UART5_Init(void);(真实那份还声明 huart1~4 与 + * MX_USART1~3_UART_Init / MX_UART4_Init,本模块只碰 UART5,搬多出来的声明 + * 没有意义)。 + * 2026-10-29 批 0:控制台从 USART1 迁到 RS232#1/UART5,本桩随之改。 * ⚠ 仅供 test/host/console_log/ 使用,不参与 MCU 构建。 * 真实 Inc/usart.h 的 include 链(main.h -> stm32f4xx_hal.h -> stm32f4xx.h -> * core_cm4.h)在宿主上编不动,寄存器面由 stm32f4_fake.h 提供(理由见该文件头)。 @@ -14,7 +16,7 @@ #include #include "stm32f4_fake.h" -extern UART_HandleTypeDef huart1; -void MX_USART1_UART_Init(void); +extern UART_HandleTypeDef huart5; +void MX_UART5_Init(void); #endif /* USART_H */ From 6531c4a4bc86d358976f28ede9f4b2c1193f6d57 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:04:08 +0800 Subject: [PATCH 06/16] =?UTF-8?q?chore(=E5=8F=B0=E6=9E=B6):=20=E5=8F=B0?= =?UTF-8?q?=E6=9E=B6=E4=B8=B2=E5=8F=A3=E9=BB=98=E8=AE=A4=E5=8F=A3=E6=94=B9?= =?UTF-8?q?=20COM3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 日志口迁到 RS232#1/UART5 之后,台架那根线接在 CN1 的 RS-232 端子上,枚举 出来是 COM3;.serialtest/config.json 还写着 COM6,@test / @build / serial-assistant 不显式指端口时就会去开一个已经没人说话的口,抓到空日志。 - .serialtest/config.json:portName COM6 -> COM3。 - 产品需求 §9.13:物理前置改成已完成口径,写明台架口由 COM6 变 COM3。 - 数据可信:资源图例里的台架串口同步成 COM3。 docs/测试说明/m2_data/README.md 里的 COM6 是 2026-09-22 那次运行记录,按当时 事实保留不改。 --- .serialtest/config.json | 2 +- docs/产品需求/产品需求设计文档.md | 12 ++++++------ docs/任务规划/数据可信.md | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.serialtest/config.json b/.serialtest/config.json index 59a7b3ef..6e22a911 100644 --- a/.serialtest/config.json +++ b/.serialtest/config.json @@ -1,5 +1,5 @@ { - "portName": "COM6", + "portName": "COM3", "baudRate": 115200, "flash_cmd": ".venv\\Scripts\\python.exe .dsh\\skills\\stlink-flash\\scripts\\stlink_flash.py flash", diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index 08960d1a..e5868878 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -2567,12 +2567,12 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 > 第 11 条的帧长仍有一处待实测复核——代码实参是 **4208 B**,屏板 readme 写 **3208 B**(§5.16.2); > 台架抓到第一帧真实报文后**立即固化**,在此之前以代码实参为准,不得按 readme 实现。 > -> **一条物理前置 + 一条实现纪律**: -> ① **台架串口线挪位**——现在的 CH340/`COM6` 接在 USART1 调试口上,日志迁到 RS-232 侧后 -> 必须改接到 CN1 的 RS232#1 端子(`CN1.10` 接适配器 TXD、`CN1.12` 接适配器 RXD、`CN1.7` 或 `CN1.8` 接 GND), -> 否则 `@test` 与 `serial-assistant` 全部抓不到日志,等于把台架弄瞎。这些端子是 **RS-232 电平**, -> 须用 USB 转 RS-232 适配器,现有的 TTL CH340 直连不了;接法与端子出处见 -> `docs/任务规划/屏板链路.md` §3 批 0。 +> **一条物理前置(已完成 2026-10-29)+ 一条实现纪律**: +> ① **台架串口线已挪位**——日志口迁到 RS-232 侧后,台架那根线改接 CN1 的 RS232#1 端子 +> (`CN1.10` 接适配器 TXD、`CN1.12` 接适配器 RXD、`CN1.7` 或 `CN1.8` 接 GND),台架日志口 +> 因此由 `COM6` 变为 **`COM3`**,`.serialtest/config.json` 的 `portName` 已同步,`@test` 与 +> `serial-assistant` 不必再手工指端口。这些端子是 **RS-232 电平**,TTL 的 CH340 直连不了, +> 用的是 USB 转 RS-232 适配器;接法与端子出处见 `docs/任务规划/屏板链路.md` §3 批 0。 > **这条要在第 16 条动代码之前完成。** > ② **RX 要真的读、ORE 要真的清**——PA10 一直是接线的(§5.16.2), > 但"RX 使能却不读 `DR`"本身就足以让 ORE 永久锁存。第 11 条实现时必须在 diff --git a/docs/任务规划/数据可信.md b/docs/任务规划/数据可信.md index 384f7959..835eb67c 100644 --- a/docs/任务规划/数据可信.md +++ b/docs/任务规划/数据可信.md @@ -350,7 +350,7 @@ typedef struct { ## 5. 任务卡(完成即销项删行;标识列用语义短语,不配编号) -> 资源图例:🔴 台架(COM6 / ST-LINK,`bench.lock` 独占)· 🟡 外部(标准源 / 子板原理图)· 🟢 纯软件。 +> 资源图例:🔴 台架(COM3 / ST-LINK,`bench.lock` 独占)· 🟡 外部(标准源 / 子板原理图)· 🟢 纯软件。 > 状态:⬜ 未开始 · 🟦 进行中。**已闭环 = 直接删行**。 > 本表**只写判据**;执行步骤在 `docs/测试说明/ADC前端注入验证流程.md`,实测结论在 > `docs/硬件平台/ADC通道映射说明.md` §8.1(动态实测结论开在其后)。 From 71b6a5fa14db85ed9c95bba5b3b6106d1f20d223 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:04:20 +0800 Subject: [PATCH 07/16] =?UTF-8?q?docs(=E5=B1=8F=E6=9D=BF):=20=E6=89=B9=200?= =?UTF-8?q?=20=E7=9A=84=200-3=20=E6=8E=A5=E6=B3=95=E8=90=BD=E7=BA=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0-3 的交付物是"接法与验收方式写清楚"。网表核对结果:ST_TX0 / ST_RX0 只连 U5.A10 / U5.B10 与 MCU 的 101/102 脚(U17 就是 MCU 本体,101/102 = PA9/PA10), CN1 上没有任何 USART1 端子——USART1 的 TTL 唯一出口是 U5 子板连接器。 - 决策一下方新增「接法与验收」:三行接线表(U5.A10 -> 屏板 PA3、 U5.B10 -> 屏板 PA2、共地),加三条易错点(必须交叉、别去 CN1 找、 别抓错同一连接器上 USART3 的 ST_TX2 / ST_RX2)。 - 验收方式写死:无实物时用 PC + USB 转 TTL 顶替屏板发 6 B RT_CMD 帧, 判据 Size==6 / CRC 100% / 10 min 无丢帧 / 拔插 TX 线后自愈。 "6 B 能被收到"这条要等 1-3 的接收中断落地,实测随 1-3 做。 - 0-3 标记完成(落纸部分),§0 的执行顺序段落改成已完成口径。 --- docs/任务规划/屏板链路.md | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index 04ff1089..e2d88ffc 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -12,7 +12,8 @@ ## 0. 先看结论:裁定已定,三条流程前置顶上来 **2026-10-28 用户裁定:屏板走 USART1,日志口让位给 RS-232 通道。** 700 kbaud 只有 USART1 这一条 MCU -引脚直出的通道吃得到(其余串口都要过板上的收发器);代价是换成几条**流程**前置——都不是软件能解决的。 +引脚直出的通道吃得到(其余串口都要过板上的收发器);代价是换成几条**流程**前置——都不是软件能解决的 +(①②已于 2026-10-29 解除,见 §3 批 0)。 | # | 项 | 实测 / 出处 | 后果 | |---|---|---|---| @@ -20,8 +21,9 @@ | 2 | **台架串口线接在 USART1 上** | TTL 的 CH340 → `COM6`,`@test` / `serial-assistant` 都靠它抓日志 | 日志一迁,台架立刻瞎 → **先把线挪到 RS-232 侧(CN1)再动代码**;且这根 TTL 模块挪不过去,须换 **USB 转 RS-232 适配器**(见 §2 决策二) | **执行顺序因此是**:挪台架线 → 迁日志口 → 才开始写屏板链路。 -第一步是**换一个 USB 转 RS-232 适配器 + 接三个端子**(纯硬件,不碰代码),第二步约 20 行代码(§3 批 0), -第三步才是屏板链路的开发批次。 +前两步**已于 2026-10-29 完成**(§3 批 0 的 0-1 / 0-2:台架线接 CN1 的 RS232#1,日志口迁到 UART5, +台架串口由 `COM6` 变成 `COM3`,`.serialtest/config.json` 已同步);第三项 0-3 的接法也已落纸。 +**批 0 到此结束,下一步进批 1。** --- @@ -93,6 +95,35 @@ | 方向控制 | **不需要**——TTL 直连是 TX / RX 两根独立线(全双工),没有方向切换 | | 剩余子决策 | **USART1 那端怎么接**:TTL 直连屏板 UART2 侧(PA2/PA3,绕过屏板自己的收发器)。**台架按这条做**;产品形态的线束长度与隔离需求届时单独评估 | +**接法与验收(0-3 的交付物,2026-10-29 落纸)** + +主机侧取 U5 子板连接器上的两个网络(出处 `docs/硬件平台/V0.1.tel`:`ST_TX0 ; U5.A10 U17.101`、 +`ST_RX0 ; U5.B10 U17.102`;`U17` 就是 MCU 本体,101/102 = PA9/PA10): + +| 主机侧(V0.1) | 屏板侧 | 方向 | +|---|---|---| +| `U5.A10`(网络 `ST_TX0`,PA9) | 屏板 `PA3`(屏板 USART2_RX) | 主机发 → 屏板收 | +| `U5.B10`(网络 `ST_RX0`,PA10) | 屏板 `PA2`(屏板 USART2_TX) | 屏板发 → 主机收 | +| 板地(`CGND`,或板上任一 GND 脚) | 屏板 `GND` | 共地,必须接 | + +三条容易接错的地方: + +- **必须交叉**:TX 对 RX。同边对接(`U5.A10` 接屏板 `PA2`)两端口都静默,且没有任何报错可看。 +- **USART1 的 TTL 只从 U5 出去**:`ST_TX0` / `ST_RX0` 在网表里只连 `U5.A10` / `U5.B10` 与 MCU 的 + 101/102 脚,**CN1 上没有任何 USART1 端子**——CN1 的 `RS232_*` 是 UART5 / USART2 经收发器之后的电平, + 别去 CN1 找。同一个 `U5` 上还挂着 USART3 的 `ST_TX2` / `ST_RX2`(`U5.C7` / `U5.C6`),**别抓错针**。 +- **不串电阻、没有方向控制**:3.3 V TTL 直连,全双工两根独立线,没有 DE 脚可接。 + +**验收方式**(判据落在 1-3,0-3 只把方法写死): + +1. **无屏板实物时用 PC + USB 转 TTL 顶替屏板**(`docs/任务规划/README.md` 已注明"软件可先用 PC + + USB 转 TTL 台架验证,不必等屏板实物"):适配器 `TXD` → `U5.B10`、`RXD` → `U5.A10`、`GND` → 板地, + 700000 8N1,按屏板的周期召唤节拍(约 275~280 ms,口径见 `docs/产品需求/产品需求设计文档.md` §5.16.2) + 发 6 B 帧 `01 03 01 00 CRC_H CRC_L`(`RT_CMD`,按键时第 4 字节换成键值;CRC-16/MODBUS 只覆盖前 4 B、高字节在前)。 +2. **判据**:`Size == 6`、CRC 通过率 100 %、连续 10 min 无丢帧;拔插一次 TX 线制造噪声后能自动恢复 + (`HAL_UART_ErrorCallback` 里清 ORE 并重挂接收,否则一次噪声就永久静默)。 +3. **有实物后**重复第 2 步,并确认屏板画面按召唤节拍刷新。 + ### 决策二:用哪个 UART(**已裁定 2026-10-28**) | 端口 | 引脚 | V0.1 板上通道 | 重排后用途 | @@ -196,7 +227,7 @@ |---|---|---|---| | 0-1 ✅ | **台架串口线挪位** | 硬件:**换成 USB 转 RS-232 适配器**(TTL 的 CH340 直连不了),接 RS232#1 的三个端子——适配器 TXD → **CN1.10**(板子 `RS232_RX1`)、RXD → **CN1.12**(板子 `RS232_TX1`)、GND → **CN1.7 或 CN1.8**(`CGND`)。端子号出自 `docs/硬件平台/V0.1.tel`;`U14` 是 RS232#3 的网口座,本通道用不上 | **已完成 2026-10-29**:COM3 在 RS-232 侧可读可抓(`serial-assistant` 正常) | | 0-2 ✅ | **日志口迁移(USART1 → RS-232)** | `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 **`huart5`(RS232#1,已定)** | **已完成 2026-10-29**(`f5161e15`):COM3 抓到完整开机日志、8 个锚点全中、启动到 `net init: OK` 661 ms;连续 10 min 1218 行零 `[ERR]`/零 dropped;USART1 已无控制台写入 | -| 0-3 | **落定 USART1 到屏板的接线** | 按决策一:TTL 直连屏板 UART2 侧(PA2/PA3);把线束接法与验收方式写清楚 | 接法落纸;照此接线后屏板发来的 6 B 召唤帧能被收到 | +| 0-3 ✅ | **落定 USART1 到屏板的接线** | 按决策一:TTL 直连屏板 UART2 侧(PA2/PA3);接法与验收方式见 §2 决策一下方的「接法与验收」 | **接法已落纸 2026-10-29**:`U5.A10` / `U5.B10` ↔ 屏板 `PA3` / `PA2` + 共地,交叉接。后半条"照此接线后 6 B 召唤帧能被收到"要等 **1-3** 的接收中断落地才测得到,实测随 1-3 一起做 | > PA10 已接线(用户 2026-10-28 确认),屏板链路**不需要额外飞线**。 From b25d70e96271ebee22998df89dbf9f266bd49a62 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:17:28 +0800 Subject: [PATCH 08/16] =?UTF-8?q?feat(=E5=B1=8F=E6=9D=BF):=20=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E5=B1=82=20sb=5Fproto=EF=BC=88=E5=B8=A7=20A=20?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=20/=20=E5=B8=A7=20B=20=E7=BB=84=E5=B8=A7=20/?= =?UTF-8?q?=20CRC-16=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 批 1 的 1-1:新建 Drivers/BSP/ScreenLink/sb_proto.c/h,纯逻辑、无 HAL 依赖, 可主机侧单测。屏板侧的协议事实源是 DTU-LCD 的 CRC16.c / MODBUS.c / rs485.h。 - CRC-16/MODBUS 用一张 16 位合并表(512 B,与屏板两张 8 位表同量)。可逐项 验证的关系:s_crc_table[i] == (auchCRCLo[i] << 8) | auchCRCHi[i]——屏板把 低字节表叫 Hi、高字节表叫 Lo,这正是下面那个字节序陷阱的根源。 - 帧 A 解析:先校 CRC 再看字段。地址/功能码/命令类型不符报 BSP_ERR_PARAM, CRC 不符报 BSP_ERR_CRC(调用方据此决定"计数"还是"静默丢")。RT_CMD 带 非 0 key 不报错、按 0 收——为这一项丢一帧等于丢一次刷新,代价不对等。 - 帧 B 组帧:普通图(掩码小端)+ LOGO(宽高大端 + 魔数 0x89 0x45),整帧 4208 B 重写(998 B 填充固定 0x00 且参与 CRC),组完即自校验。 LOGO 容量按字节数判(((w+7)/8)*h <= 3200)而不是像素数:161x159 只有 25599 像素,却要 3339 B 才装得下。 - 主机单测 59/59 PASSED(test/host/sb_proto/),覆盖率 74/74 行、6/6 函数。 其中两条向量是屏板自己算出来的线上字节(01 03 01 00 F0 48 与 01 03 00 09 31 DE),并按屏板 Check_CRC 的口径反查"它会认这一帧"。 - 双端注册:uvprojx 加 File + IncludePath;模拟器侧走 check_registration.py 的 MCU-only 豁免(纯逻辑,模拟器无消费路径)。 - Keil 构建 0 错误 0 警告;Code 不变——尚无调用者,链接器按未用节丢弃。 文档侧的 CRC 字节序订正见下一个提交。 --- Drivers/BSP/ScreenLink/sb_proto.c | 273 ++++++++++++++++ Drivers/BSP/ScreenLink/sb_proto.h | 164 ++++++++++ MDK-ARM/STM32F407-Demo.uvprojx | 7 +- scripts/check_registration.py | 1 + scripts/test_coverage_baseline.json | 108 ++++-- test/host/sb_proto/run_unit_test.py | 59 ++++ test/host/sb_proto/sb_proto_test.c | 490 ++++++++++++++++++++++++++++ 7 files changed, 1081 insertions(+), 21 deletions(-) create mode 100644 Drivers/BSP/ScreenLink/sb_proto.c create mode 100644 Drivers/BSP/ScreenLink/sb_proto.h create mode 100644 test/host/sb_proto/run_unit_test.py create mode 100644 test/host/sb_proto/sb_proto_test.c diff --git a/Drivers/BSP/ScreenLink/sb_proto.c b/Drivers/BSP/ScreenLink/sb_proto.c new file mode 100644 index 00000000..53ea50e3 --- /dev/null +++ b/Drivers/BSP/ScreenLink/sb_proto.c @@ -0,0 +1,273 @@ +/* + * 模块名称:屏板链路协议层实现(帧 A 解析 / 帧 B 组帧 / CRC-16) + * 模块功能:见 sb_proto.h 的模块头。本文件只做字节级编解码,不碰寄存器、DMA、任务与时间。 + * 适用平台:STM32F4 系列(ARMCC v5);仅依赖 stdint.h 与 string.h,主机侧可编译。 + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建:按屏板固件 CRC16.c / MODBUS.c 对齐,并订正 CRC 字节序口径 + * + * ============================================================================ + * CRC 的字节序:这里有一个会让人白查一天的口径陷阱,先看这段 + * ============================================================================ + * 屏板固件 Middlewares/Modbus/CRC16.c 的 Calculate_CRC() 返回的**不是**教科书上那个 + * CRC-16/MODBUS 值,而是它高低字节互换后的「线上字」。举实测值: + * + * 数据 "123456789" 屏板返回 0x374B CRC-16/MODBUS 标准值 0x4B37 + * 数据 01 03 00 00 00 0A 屏板返回 0xC5CD 标准值 0xCDC5 + * + * 于是屏板 RS485_RefreshScreenCMD() 里那句「cmdbuf[4] = crc >> 8」(注释写"CRC校验高字节") + * 实际写出去的是**标准 CRC 的低字节**——两处命名都是反的,但**线上字节是对的**: + * 上面第二帧的线上 CRC 就是 C5 CD(Modbus RTU 的低字节在前)。 + * + * 结论(本模块采用的写法): + * · sb_crc16() 返回**标准 CRC-16/MODBUS 值**(可用教科书校验值 0x4B37 直接对); + * · 写帧时一律 **低字节在前**:帧 A 偏移 4 = 低、偏移 5 = 高;帧 B 偏移 4206 = 低、4207 = 高。 + * 若照屏板注释的字面意思写成「高字节在前」,两侧算出的值差一次字节交换,屏板会把每一帧都 + * 判成 CRC 错(它的超时判定只看"收没收到一整包"、不看 CRC,所以现场只表现为画面不刷新), + * 极易误判成波特率或时序问题。主机单测里钉了屏板算法的实测向量,改这两行会立刻红。 + * + * 表:屏板用两张 8 位表(auchCRCHi / auchCRCLo),本模块用一张 16 位表(同样 512 B 常量)。 + * 两者关系是可逐项验证的恒等式:s_crc_table[i] == (auchCRCLo[i] 左移 8) 或 auchCRCHi[i] + * ——即屏板把**低字节表叫 Hi、高字节表叫 Lo**,这也是上面那个反相的根源。 + */ + +#include + +#include "sb_proto.h" + +/* ============================================================================ + * CRC-16/MODBUS 查表(多项式 0xA001 反转式,初值 0xFFFF) + * 表的生成方式:对 i = 0..255,取 crc = i,做 8 次「最低位为 1 则右移后异或 0xA001, + * 否则只右移」,所得 crc 即表项。主机单测逐项复核前 8 项与末项,并复算屏板的实测向量。 + * ==========================================================================*/ +static const uint16_t s_crc_table[256] = +{ + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 +}; + +/* ============================================================================ + * 私有工具 + * ==========================================================================*/ + +/* + * 函数功能:把 CRC 写到帧尾(低字节在前,见文件头那段说明) + * 入口参数:p_frame - 帧首地址 + * off - CRC 域起始偏移 + * crc - sb_crc16() 返回的标准 CRC 值 + * 返回值:无 + * 限定条件:p_frame 至少长 off + 2。 + * 函数说明:帧 A 与帧 B 共用,两处字节序必须一致——写反了屏板会把每一帧都判成 CRC 错。 + */ +static void sb_put_crc(uint8_t *p_frame, uint16_t off, uint16_t crc) +{ + p_frame[off] = (uint8_t)(crc & 0xFFU); /* 低字节在前 */ + p_frame[off + 1U] = (uint8_t)((crc >> 8) & 0xFFU); +} + +/* ============================================================================ + * 公开接口实现 + * ==========================================================================*/ + +uint16_t sb_crc16(const uint8_t *p_data, uint16_t len) +{ + uint16_t crc = 0xFFFFU; + + while (len > 0U) + { + crc = (uint16_t)((crc >> 8) ^ s_crc_table[(uint8_t)(crc ^ *p_data)]); + p_data++; + len--; + } + return crc; +} + +/* + * 函数功能:解析一帧帧 A(屏板发来的 6 B 召唤 / 按键帧) + * 入口参数:p_buf - 收到的字节首地址 + * len - 收到的字节数 + * p_out - 解析结果输出(成功时写 cmd / key,失败时不改) + * 返回值:BSP_OK / BSP_ERR_CRC(帧被破坏,计数后丢弃)/ BSP_ERR_PARAM(不是合法召唤帧,直接丢) + * 限定条件:只认固定 6 B;先校 CRC 再看字段。 + * 函数说明:细节与"为什么地址/功能码不符也报 PARAM"见 sb_proto.h 的同名声明。 + */ +bsp_err_t sb_frame_a_parse(const uint8_t *p_buf, uint16_t len, sb_frame_a_t *p_out) +{ + uint16_t crc; + uint8_t cmd; + + if ((p_buf == 0) || (p_out == 0)) + { + return BSP_ERR_PARAM; + } + if (len != SB_FRAME_A_LEN) + { + return BSP_ERR_PARAM; + } + + /* 先校 CRC:字节已经乱了的话,再去解释地址/功能码没有意义 */ + crc = sb_crc16(p_buf, (uint16_t)SB_FRAME_A_CRC_OFF); + if ((p_buf[SB_FRAME_A_CRC_OFF] != (uint8_t)(crc & 0xFFU)) || + (p_buf[SB_FRAME_A_CRC_OFF + 1U] != (uint8_t)((crc >> 8) & 0xFFU))) + { + return BSP_ERR_CRC; + } + + if (p_buf[0] != SB_ADDR) + { + return BSP_ERR_PARAM; + } + if (p_buf[1] != SB_FUNC_READ_HOLDING) + { + return BSP_ERR_PARAM; + } + + cmd = p_buf[2]; + if ((cmd != SB_CMD_KEY) && (cmd != SB_CMD_RT)) + { + return BSP_ERR_PARAM; + } + + p_out->cmd = cmd; + /* 周期召唤的 key 字段按协议恒为 0;真收到非 0 也按 0 处理(不为此丢一帧) */ + p_out->key = (cmd == SB_CMD_KEY) ? p_buf[3] : (uint8_t)0U; + return BSP_OK; +} + +/* + * 函数功能:组一帧帧 B「普通图」(点阵 + 32 位 LED 掩码) + * 入口参数:p_frame - 输出缓冲(>= SB_FRAME_B_LEN) + * p_bmp - 点阵源(>= SB_FRAME_B_BMP_LEN) + * led_mask - 状态灯位掩码,小端填进 info[0..3] + * 返回值:BSP_OK / BSP_ERR_PARAM(指针为空) + * 限定条件:整帧重写(含 998 B 填充与 CRC),调用方不必预先清零。 + * 函数说明:掩码的位语义不归本模块管(见任务规划批 3)。 + */ +bsp_err_t sb_frame_b_build_image(uint8_t *p_frame, const uint8_t *p_bmp, uint32_t led_mask) +{ + if ((p_frame == 0) || (p_bmp == 0)) + { + return BSP_ERR_PARAM; + } + + /* 整帧重写:998 B 填充与 info[4..7] 一并归零,调用方不必预先清零 */ + (void)memset(p_frame, 0, (size_t)SB_FRAME_B_LEN); + (void)memcpy(p_frame, p_bmp, (size_t)SB_FRAME_B_BMP_LEN); + + /* info[0..3]:LED 位掩码,小端(info[0] = bit0..7) */ + p_frame[SB_FRAME_B_INFO_OFF + 0U] = (uint8_t)(led_mask & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 1U] = (uint8_t)((led_mask >> 8) & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 2U] = (uint8_t)((led_mask >> 16) & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 3U] = (uint8_t)((led_mask >> 24) & 0xFFU); + + sb_put_crc(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF, sb_crc16(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF)); + return BSP_OK; +} + +/* + * 函数功能:组一帧帧 B「LOGO」(点阵 + 宽高 + 魔数 0x89 0x45) + * 入口参数:p_frame - 输出缓冲(>= SB_FRAME_B_LEN) + * p_bmp - 点阵源(>= SB_FRAME_B_BMP_LEN) + * width - 图片宽度(像素),大端填进 info[0..1] + * height - 图片高度(像素),大端填进 info[2..3] + * 返回值:BSP_OK / BSP_ERR_PARAM(指针空 / 宽高为 0 / 点阵装不下) + * 限定条件:按字节数判容量(((width + 7) / 8) * height <= 3200),不是按像素数。 + * 函数说明:本帧会覆盖屏板 Flash 扇区 126,发送时机由调用方决定。 + */ +bsp_err_t sb_frame_b_build_logo(uint8_t *p_frame, const uint8_t *p_bmp, uint16_t width, uint16_t height) +{ + uint32_t need_bytes; + + if ((p_frame == 0) || (p_bmp == 0)) + { + return BSP_ERR_PARAM; + } + if ((width == 0U) || (height == 0U)) + { + return BSP_ERR_PARAM; + } + + /* 按字节数判容量而不是像素数:161x159 只有 25599 像素,却要 21x159 = 3339 B 才装得下 */ + need_bytes = ((uint32_t)width + 7U) / 8U; + need_bytes *= (uint32_t)height; + if (need_bytes > (uint32_t)SB_FRAME_B_BMP_LEN) + { + return BSP_ERR_PARAM; + } + + (void)memset(p_frame, 0, (size_t)SB_FRAME_B_LEN); + (void)memcpy(p_frame, p_bmp, (size_t)SB_FRAME_B_BMP_LEN); + + /* info[0..1] 宽、info[2..3] 高,均大端 */ + p_frame[SB_FRAME_B_INFO_OFF + 0U] = (uint8_t)((width >> 8) & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 1U] = (uint8_t)(width & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 2U] = (uint8_t)((height >> 8) & 0xFFU); + p_frame[SB_FRAME_B_INFO_OFF + 3U] = (uint8_t)(height & 0xFFU); + /* info[4..5] LOGO 魔数;info[6..7] 保留(已归零) */ + p_frame[SB_FRAME_B_INFO_OFF + 4U] = SB_LOGO_MAGIC_HI; + p_frame[SB_FRAME_B_INFO_OFF + 5U] = SB_LOGO_MAGIC_LO; + + sb_put_crc(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF, sb_crc16(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF)); + return BSP_OK; +} + +/* + * 函数功能:自校验一帧帧 B(重算 CRC 并与帧尾比对) + * 入口参数:p_frame - 待校验帧首地址 + * len - 帧长度,必须等于 SB_FRAME_B_LEN + * 返回值:BSP_OK / BSP_ERR_CRC(不一致)/ BSP_ERR_PARAM(指针空或长度不对) + * 限定条件:只验 CRC,不检查填充段是否全 0、也不检查掩码位语义。 + * 函数说明:供组帧路径自查(2-4 的「组帧自校验 100%」接这里)。 + */ +bsp_err_t sb_frame_b_verify(const uint8_t *p_frame, uint16_t len) +{ + uint16_t crc; + + if (p_frame == 0) + { + return BSP_ERR_PARAM; + } + if (len != SB_FRAME_B_LEN) + { + return BSP_ERR_PARAM; + } + + crc = sb_crc16(p_frame, (uint16_t)SB_FRAME_B_CRC_OFF); + if ((p_frame[SB_FRAME_B_CRC_OFF] != (uint8_t)(crc & 0xFFU)) || + (p_frame[SB_FRAME_B_CRC_OFF + 1U] != (uint8_t)((crc >> 8) & 0xFFU))) + { + return BSP_ERR_CRC; + } + return BSP_OK; +} diff --git a/Drivers/BSP/ScreenLink/sb_proto.h b/Drivers/BSP/ScreenLink/sb_proto.h new file mode 100644 index 00000000..0682f73e --- /dev/null +++ b/Drivers/BSP/ScreenLink/sb_proto.h @@ -0,0 +1,164 @@ +/* + * 模块名称:屏板链路协议层(帧 A 解析 / 帧 B 组帧 / CRC-16) + * 模块功能:把「主机与屏板之间那两种帧」的编解码收成一处纯逻辑: + * - CRC-16/MODBUS(表与屏板固件逐字节一致,见 Drivers/BSP/ScreenLink/sb_proto.c) + * - 帧 A(屏板 → 主机,固定 6 B)解析:校验后吐出「命令类型 + 按键值」 + * - 帧 B(主机 → 屏板,固定 4208 B)组帧:普通图(LED 掩码)/ LOGO(宽高 + 魔数) + * - 帧 B 自校验(组完立刻能验,供组帧路径自查) + * 本模块**不碰寄存器、不碰 DMA、不碰任务**:串口怎么配、谁来发、什么时候发都不归它管 + * (那是批 1 的 1-2 / 1-3 与批 2 的事)。 + * 适用平台:STM32F4 系列(ARMCC v5);不依赖 HAL,主机侧可直接编译(故可主机单测)。 + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建:按屏板固件 D:\Code\DTU Code\DTU-LCD 的 CRC16.c / MODBUS.c 逐字节对齐 + * + * 协议事实源:屏板固件工程(不在本仓库) + * Middlewares/Modbus/CRC16.c —— 两张 256 字节查表 + Calculate_CRC() / Check_CRC() + * Middlewares/Modbus/MODBUS.c —— RS485_RefreshScreenCMD()(帧 A 组帧,6 B) + * RS485_Process()(帧 B 解析:3200 点阵 + 8 字节 info + CRC 在帧尾) + * Drivers/BSP/RS485/rs485.h —— UART_RX_LEN = 4208(帧 B 的线上长度,注释写 3208 是错的) + * 需求侧口径见 docs/产品需求/产品需求设计文档.md §5.16.2(帧结构)与 §5.16.3(按键枚举)。 + * + * ⚠ 两个长度别混:屏板 `BMP_LEN` 的 3208 是**写 Flash 的记录长度**(点阵 3200 + 宽 4 + 高 4), + * 不是帧长;帧长以 `Check_CRC(..., UART_RX_LEN)` 的 **4208** 为准。本模块只认 4208。 + */ + +#ifndef SB_PROTO_H +#define SB_PROTO_H + +#include + +#include "bsp_err.h" /* bsp_err_t / BSP_OK / BSP_ERR_*:本模块只报统一错误码 */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* ============================================================================ + * 一、帧长与字段偏移(单一出处,别在调用方另抄一份) + * ==========================================================================*/ + +/* 帧 A:屏板 → 主机,固定 6 B。CRC 只覆盖前 4 B,高字节在前。 */ +#define SB_FRAME_A_LEN (6U) +#define SB_FRAME_A_CRC_OFF (4U) + +/* 帧 B:主机 → 屏板,固定 4208 B。CRC 覆盖 [0, 4205],高字节在前。 */ +#define SB_FRAME_B_LEN (4208U) +#define SB_FRAME_B_BMP_LEN (3200U) /* 点阵:160 行 x 20 B/行,1 bpp,MSB 在前 */ +#define SB_FRAME_B_INFO_LEN (8U) /* info[0..7] */ +#define SB_FRAME_B_INFO_OFF (3200U) +#define SB_FRAME_B_PAD_OFF (3208U) /* 保留填充起点 */ +#define SB_FRAME_B_PAD_LEN (998U) /* [3208, 4205]:屏板不读,但参与 CRC,须固定填 0 */ +#define SB_FRAME_B_CRC_OFF (4206U) + +/* 帧 A 的固定字段值 */ +#define SB_ADDR (0x01U) /* 从站地址:**主机的地址**,不是屏板的 */ +#define SB_FUNC_READ_HOLDING (0x03U) /* 功能码:读保持寄存器 */ +#define SB_CMD_KEY (0x00U) /* 命令类型:按键事件(立即召唤) */ +#define SB_CMD_RT (0x01U) /* 命令类型:周期刷新 */ + +/* 帧 B 的 LOGO 魔数(info[4] / info[5]) */ +#define SB_LOGO_MAGIC_HI (0x89U) +#define SB_LOGO_MAGIC_LO (0x45U) + +/* 屏幕几何:LOGO 的宽高要在这一片点阵装得下 */ +#define SB_SCREEN_W (160U) +#define SB_SCREEN_H (160U) + +/* ============================================================================ + * 二、帧 A 的解析结果 + * ==========================================================================*/ + +/* 解析成功后的有效字段。addr / func 恒为 SB_ADDR / SB_FUNC_READ_HOLDING, + * 不再回吐(校验不过直接报错,能走到这里就一定是这两个值)。 */ +typedef struct +{ + uint8_t cmd; /* SB_CMD_KEY / SB_CMD_RT */ + uint8_t key; /* cmd == SB_CMD_KEY 时有效;cmd == SB_CMD_RT 时恒为 0(见 sb_frame_a_parse 说明) */ +} sb_frame_a_t; + +/* ============================================================================ + * 三、公开接口 + * ==========================================================================*/ + +/* + * 函数功能:计算 CRC-16/MODBUS(多项式 0xA001 反转式、初值 0xFFFF、查表法) + * 入口参数:p_data - 待校验数据首地址 + * len - 参与校验的字节数(不含 CRC 本身) + * 返回值:16 位**标准 CRC-16/MODBUS 值**(教科书校验值:数据 "123456789" → 0x4B37) + * 限定条件:p_data 必须非空且 len >= 1;本函数是纯算术,不做空指针检查(与相量系纯算法函数同口径)。 + * 写进帧里一律**低字节在前**——屏板固件那个同名函数返回的是高低互换后的「线上字」, + * 两处命名都是反的,但线上字节是对的。陷阱与实测向量见 sb_proto.c 文件头,改前务必看。 + * 函数说明:查表法而非逐位法。帧 B 一次要算 4206 B:逐位法约 13 万拍(毫秒量级), + * 查表法约 2.5 万拍(0.15 ms 量级,168 MHz 估算),后者才留得住 2-1 的 0.3 ms + * 组帧预算——而那份预算还要分给 3200 B 的点阵拷贝,2-1 实测时两者要一起算。 + */ +uint16_t sb_crc16(const uint8_t *p_data, uint16_t len); + +/* + * 函数功能:解析一帧帧 A(屏板发来的 6 B 召唤 / 按键帧) + * 入口参数:p_buf - 收到的字节首地址 + * len - 收到的字节数(空闲中断断出来的实际长度) + * p_out - 解析结果输出(成功时写 cmd / key,失败时不改) + * 返回值:BSP_OK 解析成功,*p_out 有效 + * BSP_ERR_CRC CRC 不符:帧被破坏,应计数后静默丢弃(不要回任何东西) + * BSP_ERR_PARAM 不是一个合法的召唤帧:指针空 / len != 6 / 地址不是 0x01 / + * 功能码不是 0x03 / 命令类型不是 0 或 1 —— 丢弃即可,不必计数 + * 限定条件:只认固定 6 B 的帧 A;屏板的帧 B 应答方向相反,不经过本函数。 + * 函数说明: + * 1. **先校 CRC 再看字段**:CRC 不过说明字节已经乱了,再去解释地址/功能码没有意义。 + * 2. 地址与功能码不符也报 BSP_ERR_PARAM 而不是新造错误码——bsp_err.h 的码是跨模块的 + * 「能不能重试」语义,不是协议细节;对调用方而言这几种都等于「这不是给本机的合法召唤帧」。 + * 要区分细节就比对原始字节(屏板这两个字段是写死的,出现不符说明已经错位)。 + * 3. cmd == SB_CMD_RT 时若 key 非 0 **不报错**,按 0 忽略:屏板的周期召唤本应带 0, + * 但为这一项丢一帧就等于丢一次刷新(280 ms 的画面不动),代价不对等。 + */ +bsp_err_t sb_frame_a_parse(const uint8_t *p_buf, uint16_t len, sb_frame_a_t *p_out); + +/* + * 函数功能:组一帧帧 B「普通图」(点阵 + 32 位 LED 掩码) + * 入口参数:p_frame - 输出缓冲,长度必须 >= SB_FRAME_B_LEN + * p_bmp - 点阵源,长度必须 >= SB_FRAME_B_BMP_LEN(160x160 @1bpp) + * led_mask - 状态灯位掩码,小端填进 info[0..3](info[0] = bit0..7) + * 返回值:BSP_OK 组帧完成,整帧(含 CRC)已就绪 + * BSP_ERR_PARAM 指针为空 + * 限定条件:p_frame 会被**整帧重写**(含 998 B 填充与 CRC),调用方不必预先清零; + * 组好的帧在下次调用前有效,本模块不持有任何缓冲。 + * 函数说明:填充段固定填 0x00(屏板不读,但参与 CRC)。掩码的位语义不归本模块管 + * (bit0 恒 0 / bit20~31 恒 0 这类业务约束属状态灯映射表,见任务规划批 3)。 + */ +bsp_err_t sb_frame_b_build_image(uint8_t *p_frame, const uint8_t *p_bmp, uint32_t led_mask); + +/* + * 函数功能:组一帧帧 B「LOGO」(点阵 + 宽高 + 魔数 0x89 0x45) + * 入口参数:p_frame - 输出缓冲,长度必须 >= SB_FRAME_B_LEN + * p_bmp - 点阵源,长度必须 >= SB_FRAME_B_BMP_LEN + * width - 图片宽度(像素),大端填进 info[0..1] + * height - 图片高度(像素),大端填进 info[2..3] + * 返回值:BSP_OK 组帧完成 + * BSP_ERR_PARAM 指针为空 / 宽高为 0 / 超出 160x160 点阵容量 + * 限定条件:宽高必须满足 ((width + 7) / 8) * height <= 3200 —— 屏板拿这两个数直接显示, + * 装不下的宽高会让它读越 3200 B 的点阵缓冲,故本函数按字节数而非像素数判容量。 + * 函数说明:LOGO 帧会把屏板 Flash 0x0803F000(扇区 126)整块覆盖,且屏板不校验是否同一张图; + * 本模块只负责把帧组对,**什么时候发、能不能重复发由调用方决定**。 + */ +bsp_err_t sb_frame_b_build_logo(uint8_t *p_frame, const uint8_t *p_bmp, uint16_t width, uint16_t height); + +/* + * 函数功能:自校验一帧帧 B(重算 CRC 并与帧尾比对) + * 入口参数:p_frame - 待校验帧首地址 + * len - 帧长度,必须等于 SB_FRAME_B_LEN + * 返回值:BSP_OK CRC 与帧尾一致 + * BSP_ERR_CRC 不一致(帧被改写 / 组帧时漏算) + * BSP_ERR_PARAM 指针空或 len 不是 4208 + * 限定条件:只验 CRC,不检查填充段是否全 0、也不检查掩码位语义。 + * 函数说明:给组帧路径自查用(2-4 的「组帧自校验 100%」直接接这里)。 + */ +bsp_err_t sb_frame_b_verify(const uint8_t *p_frame, uint16_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* SB_PROTO_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index e591b41c..3c0a6c72 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -337,7 +337,7 @@ --diag_suppress=1,68,111,188,191,546,1293 USE_HAL_DRIVER,STM32F407xx,NDEBUG - ../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/protocol/iec104;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib + ../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/ScreenLink;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/protocol/iec104;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib @@ -1623,6 +1623,11 @@ 1 ..\Drivers\BSP\RS485\rs485.c + + sb_proto.c + 1 + ..\Drivers\BSP\ScreenLink\sb_proto.c + net_select.c 1 diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 5f501dfc..6460f15e 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -135,6 +135,7 @@ SIM_EXEMPT = [ ("Drivers/BSP/NET/net_pending.c", "MCU 挂起 recv 模型(依赖 ch395f_* SPI 读出与 FreeRTOS 通知),模拟器另有 net_socket_sim.c 按外部语义模拟"), ("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"), ("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"), + ("Drivers/BSP/ScreenLink/sb_proto.c", "屏板链路协议层纯逻辑(帧 A 解析 / 帧 B 组帧 / CRC-16),消费者 = MCU 侧 USART1 收发起发(批 1/2),模拟器无消费路径;主机侧纯逻辑单测覆盖于 test/host/sb_proto"), ("Drivers/BSP/TPAFE5160/tpafe5160.c", "MCU 计量芯片驱动"), ("Drivers/BSP/UI/ui_port.c", "MCU UI 端口层(显示/触摸硬件),模拟器另有 sim/sim_port.c"), ] diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index bc586c0a..799f1b45 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -344,6 +344,14 @@ "lines_covered": 108, "lines_valid": 112 }, + "Drivers/BSP/ScreenLink/sb_proto.c": { + "func_rate": 1.0, + "funcs_covered": 6, + "funcs_valid": 6, + "line_rate": 1.0, + "lines_covered": 74, + "lines_valid": 74 + }, "Drivers/BSP/UI/ui_data.c": { "func_rate": 0.9767, "funcs_covered": 42, @@ -607,6 +615,13 @@ "lines_total": 961, "ok": true }, + "sb_proto": { + "files": { + "Drivers/BSP/ScreenLink/sb_proto.c": 74 + }, + "lines_total": 74, + "ok": true + }, "sha256": { "files": { "App/util/sha256.c": 114 @@ -741,22 +756,23 @@ "Drivers/BSP/NET/net_socket.c", "Drivers/BSP/NETCFG/ftp_cfg.c", "Drivers/BSP/NETCFG/net_cfg.c", + "Drivers/BSP/ScreenLink/sb_proto.c", "Drivers/BSP/UI/ui_data.c", "Drivers/BSP/UI/ui_net_sink.c", "Drivers/BSP/UI/ui_w_soe.c" ], "totals": { "files_generated_excluded": 5, - "files_tested": 46, - "files_untested": 151, - "func_rate": 0.9317, - "funcs_covered": 532, - "funcs_valid": 571, - "line_rate": 0.8983, - "lines_covered": 7058, - "lines_valid": 7857, - "modules_ok": 41, - "modules_total": 41 + "files_tested": 47, + "files_untested": 164, + "func_rate": 0.9324, + "funcs_covered": 538, + "funcs_valid": 577, + "line_rate": 0.8993, + "lines_covered": 7132, + "lines_valid": 7931, + "modules_ok": 42, + "modules_total": 42 }, "untested_sources": [ { @@ -768,7 +784,7 @@ "path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c" }, { - "code_lines": 407, + "code_lines": 420, "path": "App/app/ui_data_feed.c" }, { @@ -796,7 +812,7 @@ "path": "Drivers/BSP/SD2506/sd2506.c" }, { - "code_lines": 249, + "code_lines": 263, "path": "App/task/adc_task.c" }, { @@ -815,6 +831,10 @@ "code_lines": 206, "path": "Drivers/BSP/CH395F/ch395f.h" }, + { + "code_lines": 206, + "path": "Drivers/BSP/UI/ui_data.h" + }, { "code_lines": 205, "path": "Drivers/BSP/UI/ui_page_net_cfg.c" @@ -827,14 +847,14 @@ "code_lines": 200, "path": "Drivers/BSP/UI/ui_w_ytview.c" }, - { - "code_lines": 196, - "path": "Drivers/BSP/UI/ui_data.h" - }, { "code_lines": 157, "path": "Drivers/BSP/TPAFE5160/tpafe5160.c" }, + { + "code_lines": 152, + "path": "Drivers/BSP/UI/ui_page_runreport.c" + }, { "code_lines": 150, "path": "Drivers/BSP/UI/ui_page_password.c" @@ -844,12 +864,12 @@ "path": "Drivers/BSP/UI/ui_page_device_info.c" }, { - "code_lines": 134, - "path": "App/task/protect_task.c" + "code_lines": 135, + "path": "App/app/app_main.c" }, { - "code_lines": 132, - "path": "App/app/app_main.c" + "code_lines": 134, + "path": "App/task/protect_task.c" }, { "code_lines": 129, @@ -999,6 +1019,10 @@ "code_lines": 40, "path": "Drivers/BSP/UI/ui_page_deadband.c" }, + { + "code_lines": 36, + "path": "Drivers/BSP/ScreenLink/sb_proto.h" + }, { "code_lines": 36, "path": "Drivers/BSP/UI/ui_w_records.h" @@ -1115,6 +1139,18 @@ "code_lines": 22, "path": "Drivers/BSP/NETCFG/ftp_cfg.h" }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_accident.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_alarm.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_oper.c" + }, { "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_runlog.c" @@ -1127,6 +1163,14 @@ "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_soe.c" }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_start.c" + }, + { + "code_lines": 22, + "path": "Drivers/BSP/UI/ui_page_yk.c" + }, { "code_lines": 20, "path": "Drivers/BSP/UI/ui_net_sink.h" @@ -1339,6 +1383,14 @@ "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_ac.h" }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_accident.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_alarm.h" + }, { "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_dc.h" @@ -1347,6 +1399,22 @@ "code_lines": 5, "path": "Drivers/BSP/UI/ui_page_device_info.h" }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_oper.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_runreport.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_start.h" + }, + { + "code_lines": 5, + "path": "Drivers/BSP/UI/ui_page_yk.h" + }, { "code_lines": 4, "path": "Drivers/BSP/UI/ui_page_runlog.h" diff --git a/test/host/sb_proto/run_unit_test.py b/test/host/sb_proto/run_unit_test.py new file mode 100644 index 00000000..47809879 --- /dev/null +++ b/test/host/sb_proto/run_unit_test.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""屏板链路协议层(sb_proto)的主机侧纯逻辑单元测试运行器(纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/sb_proto/sb_proto_test.c(它以 #include 纳入**生产源文件** + Drivers/BSP/ScreenLink/sb_proto.c),在 PC 上离线验证帧 A 解析 / 帧 B 组帧 / + CRC-16/MODBUS:教科书校验值、屏板实测的召唤帧字节、坏帧分支、4208 B 帧的 + 点阵-掩码-填充-CRC 落位、LOGO 宽高容量守卫、以及"按屏板 Check_CRC 的口径复核 + 本模块组的帧会不会被屏板接受"。零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用台架): + 屏板不认帧时,它的超时判定只看"收没收到一整包"、不看 CRC,现场只表现为**画面不刷新**—— + 这条现象与波特率错、时序错、线接反完全同形,台架上无法归因。把 CRC 与字节序钉在主机侧, + 台架上剩下的就只可能是物理层问题。另一处只能在这里测:CRC 的**字节序**口径。 + 屏板固件 Calculate_CRC() 返回的是高低互换后的「线上字」,它自己的注释把低字节叫"高字节"; + 照字面实现会让每一帧都差一次字节交换,而现象又是"画面不刷新"——这里钉了两条屏板实测向量。 + +本层为**纯逻辑**:无寄存器、无 RTOS、无文件/外设依赖,故**不需要任何桩目录**。 + +用法(仓库根目录): + python test/host/sb_proto/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/sb_proto/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:docs/产品需求/产品需求设计文档.md §5.16.2(帧结构); + docs/任务规划/屏板链路.md §3 批 1(1-1 的判据); + 协议事实源 = 屏板固件工程 D:\\Code\\DTU Code\\DTU-LCD 的 CRC16.c / MODBUS.c。 +创建日期:2026-10-29 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 无桩目录:被测代码只依赖 stdint.h / string.h。 +# 两个 include 目录各管一个头:ScreenLink 找 sb_proto.h,Drivers/BSP 找 bsp_err.h。 +BUILD = TestBuild( + name="ScreenLink Proto Tests", + src=os.path.join(HERE, "sb_proto_test.c"), + include_dirs=[ + os.path.join(ROOT, "Drivers", "BSP", "ScreenLink"), + os.path.join(ROOT, "Drivers", "BSP"), + HERE, + ], + summary_marker="=== ScreenLink Proto Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/sb_proto/sb_proto_test.c b/test/host/sb_proto/sb_proto_test.c new file mode 100644 index 00000000..7f226db3 --- /dev/null +++ b/test/host/sb_proto/sb_proto_test.c @@ -0,0 +1,490 @@ +/* + * 模块名称:屏板链路协议层(sb_proto)主机侧单元测试 + * 模块功能:在 PC 上对 Drivers/BSP/ScreenLink/sb_proto.c 做离线验证。 + * + * 被测对象:真实源文件(以 #include 原样纳入,保证测的是生产代码而非副本): + * ../../../Drivers/BSP/ScreenLink/sb_proto.c + * 本层为**纯逻辑**(只依赖 stdint.h 与 string.h),故**无需任何桩**。 + * + * 为什么要主机侧单测(而不是只用台架): + * 1. 协议层只有两种结局:屏板认 / 屏板不认。屏板不认时它的超时判定只看「收没收到一整包」、 + * 不看 CRC,现场只表现为**画面不刷新**——在台架上这条现象与波特率错、时序错、线接反 + * 完全同形,无法归因。主机侧把 CRC 与字节序钉死,才谈得上"发出去的就是对的"。 + * 2. 帧 A 的坏帧分支(长度不对 / 地址不对 / 功能码不对 / CRC 错)在台架上要造噪声才可能出现, + * 且一出现就被复位冲掉;主机侧是几行断言。 + * 3. 帧 B 4208 B 的边界(填充段参与 CRC、LOGO 宽高装不下点阵)不占 MCU 的 RAM,毫秒级。 + * + * 覆盖点(对应 docs/产品需求/产品需求设计文档.md §5.16.2 帧结构与任务规划 1-1 判据): + * TC-SBPROTO-001 CRC-16/MODBUS 教科书校验值:"123456789" -> 0x4B37 + * TC-SBPROTO-002 CRC 与 Modbus RTU 教科书帧一致:01 03 00 00 00 0A -> 0xCDC5(线上 C5 CD) + * TC-SBPROTO-003 查表法 == 逐位法(300 组伪随机长度/内容,钉住表被改错) + * TC-SBPROTO-004 查表法边界:len 个字节与逐字节累积等价(前 1 字节 / 全 0 / 全 FF) + * TC-SBPROTO-010 帧 A 实测向量:屏板发的周期召唤 01 03 01 00 F0 48 -> RT_CMD / key 0 + * TC-SBPROTO-011 帧 A 实测向量:按键召唤 01 03 00 09 31 DE -> KEY_CMD / key 9 + * TC-SBPROTO-012 帧 A 的 key:RT 命令下非 0 也按 0 收(不为此丢一帧) + * TC-SBPROTO-013 帧 A 坏帧:CRC 错 / 长度 5 与 7 / 地址 0x02 / 功能码 0x04 / 命令类型 0x02 + * TC-SBPROTO-014 帧 A 参数守卫:空指针两路;失败时不动 *p_out + * TC-SBPROTO-020 帧 B 普通图:点阵逐字节、掩码小端、info[4..7] 为 0、填充段全 0、CRC 自校验 + * TC-SBPROTO-021 帧 B 普通图:整帧重写(预填 0xAA 的缓冲被清干净)、重复组帧结果一致 + * TC-SBPROTO-022 帧 B LOGO:宽高大端、魔数 0x89 0x45、info[6..7] 为 0、CRC 自校验 + * TC-SBPROTO-023 帧 B LOGO 宽高守卫:161x159 与 160x161 被拒(按字节数判容量)、宽或高为 0 被拒 + * TC-SBPROTO-024 帧 B 参数守卫:空指针三处 + * TC-SBPROTO-030 帧 B 自校验:改点阵 / 改填充段 / 改 CRC 自身 / 长度不对 + * TC-SBPROTO-040 屏板侧视角:用屏板 Check_CRC 的口径(高低互换后比对)复核本模块组的两种帧 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-29 + */ + +#include +#include +#include + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* ===================== 被测代码 ===================== */ +/* 原样纳入生产源文件(纯逻辑,无需桩) */ +#include "sb_proto.c" + +/* ===================== 工具 ===================== */ + +/* 伪随机(自持实现,不依赖 rand() 的实现差异,换编译器也复现同一组向量) */ +static uint32_t s_rng = 0x20261029U; + +static uint32_t rng_next(void) +{ + s_rng = (s_rng * 1103515245U) + 12345U; + return (s_rng >> 8) & 0x00FFFFFFU; +} + +/* 逐位法 CRC-16/MODBUS:与被测查表法互为独立实现,用来抓表项被改错 */ +static uint16_t crc16_bitwise(const uint8_t *p, uint16_t len) +{ + uint16_t crc = 0xFFFFU; + uint16_t i; + uint8_t b; + + for (i = 0U; i < len; i++) + { + crc ^= (uint16_t)p[i]; + for (b = 0U; b < 8U; b++) + { + if ((crc & 1U) != 0U) + { + crc = (uint16_t)((crc >> 1) ^ 0xA001U); + } + else + { + crc = (uint16_t)(crc >> 1); + } + } + } + return crc; +} + +/* + * 屏板 Check_CRC() 的等价物(照屏板固件 Middlewares/Modbus/CRC16.c 的口径): + * 屏板的 Calculate_CRC() 返回的是高低字节互换后的「线上字」,所以它把帧尾两字节按 + * 「前一个是高」拼起来再比。本函数只用 sb_crc16 的标准值复刻这一口径, + * 用来回答"屏板会不会认这一帧"。 + */ +static int screen_board_accepts(const uint8_t *buf, uint16_t len) +{ + uint16_t wire; + uint16_t crc; + + if (len < 2U) + { + return 0; + } + wire = (uint16_t)(((uint16_t)buf[len - 2U] << 8) | (uint16_t)buf[len - 1U]); + crc = sb_crc16(buf, (uint16_t)(len - 2U)); + /* 标准值高低互换 == 屏板 Calculate_CRC() 的返回值 */ + return ((uint16_t)((crc << 8) | (crc >> 8)) == wire) ? 1 : 0; +} + +/* 按给定 4 字节组一帧帧 A(用生产代码自己的 CRC,再按线上顺序摆) */ +static void make_frame_a(uint8_t *p_out, uint8_t addr, uint8_t func, uint8_t cmd, uint8_t key) +{ + uint16_t crc; + + p_out[0] = addr; + p_out[1] = func; + p_out[2] = cmd; + p_out[3] = key; + crc = sb_crc16(p_out, (uint16_t)SB_FRAME_A_CRC_OFF); + p_out[4] = (uint8_t)(crc & 0xFFU); + p_out[5] = (uint8_t)((crc >> 8) & 0xFFU); +} + +static uint8_t s_frame[SB_FRAME_B_LEN]; /* 4208 B,帧 B 输出缓冲 */ +static uint8_t s_bmp[SB_FRAME_B_BMP_LEN]; /* 3200 B,点阵源 */ + +static void fill_bmp_pattern(void) +{ + uint16_t i; + + for (i = 0U; i < SB_FRAME_B_BMP_LEN; i++) + { + s_bmp[i] = (uint8_t)((i * 37U) & 0xFFU); + } +} + +/* ===================== 用例 ===================== */ + +static void tc_crc_vectors(void) +{ + static const uint8_t k_check[] = { '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + static const uint8_t k_modbus[] = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x0A }; + uint16_t crc; + + crc = sb_crc16(k_check, (uint16_t)sizeof(k_check)); + CHECK(crc == 0x4B37U, "TC-SBPROTO-001 CRC-16/MODBUS check value of '123456789' is 0x4B37"); + + crc = sb_crc16(k_modbus, (uint16_t)sizeof(k_modbus)); + CHECK(crc == 0xCDC5U, + "TC-SBPROTO-002 the textbook Modbus RTU frame yields 0xCDC5, so the wire bytes are C5 CD (low first)"); +} + +static void tc_crc_table(void) +{ + uint8_t buf[64]; + uint16_t n; + uint16_t i; + int mismatch = 0; + + for (n = 0U; n < 300U; n++) + { + uint16_t len = (uint16_t)(rng_next() % 65U); + + for (i = 0U; i < len; i++) + { + buf[i] = (uint8_t)(rng_next() & 0xFFU); + } + if (sb_crc16(buf, len) != crc16_bitwise(buf, len)) + { + mismatch++; + } + } + CHECK(mismatch == 0, + "TC-SBPROTO-003 the table walk agrees with the bitwise reference on 300 pseudo-random buffers"); + + buf[0] = 0x00U; + CHECK(sb_crc16(buf, 1U) == crc16_bitwise(buf, 1U), + "TC-SBPROTO-004 a single 0x00 byte: table and bitwise agree (initial value is really 0xFFFF)"); + + for (i = 0U; i < 32U; i++) + { + buf[i] = 0xFFU; + } + CHECK(sb_crc16(buf, 32U) == crc16_bitwise(buf, 32U), + "TC-SBPROTO-004b 32 bytes of 0xFF: table and bitwise agree"); +} + +static void tc_frame_a_vectors(void) +{ + /* 屏板 RS485_RefreshScreenCMD() 在 700000 8N1 上真实发出去的字节: + * 周期召唤(type=RT_CMD=1, key=0)与按键召唤(type=KEY_CMD=0, key=9)。 + * 这两条是拿屏板自己的算法算出来的线上字节,**不是**本模块算的。 */ + static const uint8_t k_rt[] = { 0x01, 0x03, 0x01, 0x00, 0xF0, 0x48 }; + static const uint8_t k_key[] = { 0x01, 0x03, 0x00, 0x09, 0x31, 0xDE }; + sb_frame_a_t out; + + memset(&out, 0, sizeof(out)); + CHECK(sb_frame_a_parse(k_rt, (uint16_t)sizeof(k_rt), &out) == BSP_OK, + "TC-SBPROTO-010 the screen board periodic summon 01 03 01 00 F0 48 parses"); + CHECK((out.cmd == SB_CMD_RT) && (out.key == 0U), + "TC-SBPROTO-010b it reports RT_CMD with key 0"); + + memset(&out, 0, sizeof(out)); + CHECK(sb_frame_a_parse(k_key, (uint16_t)sizeof(k_key), &out) == BSP_OK, + "TC-SBPROTO-011 the screen board key summon 01 03 00 09 31 DE parses"); + CHECK((out.cmd == SB_CMD_KEY) && (out.key == 9U), + "TC-SBPROTO-011b it reports KEY_CMD with key 9"); + + /* 屏板会认本模块自己组出来的这两帧吗(同一口径反查) */ + CHECK(screen_board_accepts(k_rt, (uint16_t)sizeof(k_rt)) == 1, + "TC-SBPROTO-011c the board side accepts the periodic summon frame"); + CHECK(screen_board_accepts(k_key, (uint16_t)sizeof(k_key)) == 1, + "TC-SBPROTO-011d the board side accepts the key summon frame"); +} + +static void tc_frame_a_lenient_key(void) +{ + uint8_t buf[SB_FRAME_A_LEN]; + sb_frame_a_t out; + + /* 屏板周期召唤本应带 key = 0;真带了非 0 也收下(丢一帧就等于丢一次刷新) */ + make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0x2AU); + memset(&out, 0, sizeof(out)); + CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_OK, + "TC-SBPROTO-012 an RT frame carrying a non-zero key is still accepted"); + CHECK(out.key == 0U, "TC-SBPROTO-012b its key is normalised to 0 (the key field only means something for KEY_CMD)"); + + /* KEY_CMD 的 key 原样透出,包括 0 与 0xFF */ + make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_KEY, 0x00U); + CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_OK && out.key == 0x00U, + "TC-SBPROTO-012c a KEY_CMD with key 0 is accepted and keeps 0"); + make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_KEY, 0xFFU); + CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_OK && out.key == 0xFFU, + "TC-SBPROTO-012d a KEY_CMD with key 0xFF is accepted and keeps 0xFF"); +} + +static void tc_frame_a_reject(void) +{ + uint8_t buf[SB_FRAME_A_LEN]; + sb_frame_a_t out; + sb_frame_a_t sentinel; + + /* CRC 错:改 CRC 低字节 / 改数据字节 */ + make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U); + buf[4] ^= 0x01U; + CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_CRC, + "TC-SBPROTO-013 a flipped CRC byte is reported as CRC error"); + make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U); + buf[2] ^= 0x02U; + CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_CRC, + "TC-SBPROTO-013b a flipped payload byte is reported as CRC error"); + + /* 长度不对 */ + make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U); + CHECK(sb_frame_a_parse(buf, (uint16_t)(SB_FRAME_A_LEN - 1U), &out) == BSP_ERR_PARAM, + "TC-SBPROTO-013c a 5-byte frame is rejected"); + CHECK(sb_frame_a_parse(buf, (uint16_t)(SB_FRAME_A_LEN + 1U), &out) == BSP_ERR_PARAM, + "TC-SBPROTO-013d a 7-byte frame is rejected (the idle interrupt may hand over a longer burst)"); + + /* 地址 / 功能码 / 命令类型不符(CRC 都是对的,纯粹是内容不对) */ + make_frame_a(buf, 0x02U, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U); + CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_PARAM, + "TC-SBPROTO-013e slave address 0x02 is rejected even with a good CRC"); + make_frame_a(buf, SB_ADDR, 0x04U, SB_CMD_RT, 0U); + CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_PARAM, + "TC-SBPROTO-013f function code 0x04 is rejected even with a good CRC"); + make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, 0x02U, 0U); + CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_PARAM, + "TC-SBPROTO-013g command type 0x02 is rejected even with a good CRC"); + + /* 失败时不动 *p_out */ + sentinel.cmd = 0x5AU; + sentinel.key = 0xA5U; + out = sentinel; + make_frame_a(buf, 0x02U, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U); + (void)sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, &out); + CHECK((out.cmd == 0x5AU) && (out.key == 0xA5U), + "TC-SBPROTO-014 a failed parse leaves the caller result struct untouched"); +} + +static void tc_frame_a_guard(void) +{ + uint8_t buf[SB_FRAME_A_LEN]; + sb_frame_a_t out; + + make_frame_a(buf, SB_ADDR, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U); + CHECK(sb_frame_a_parse(0, (uint16_t)SB_FRAME_A_LEN, &out) == BSP_ERR_PARAM, + "TC-SBPROTO-014b a null buffer is rejected"); + CHECK(sb_frame_a_parse(buf, (uint16_t)SB_FRAME_A_LEN, 0) == BSP_ERR_PARAM, + "TC-SBPROTO-014c a null result pointer is rejected"); +} + +static void tc_frame_b_image(void) +{ + uint16_t i; + uint16_t crc; + int bmp_ok = 1; + int pad_ok = 1; + + fill_bmp_pattern(); + CHECK(sb_frame_b_build_image(s_frame, s_bmp, 0xDEADBEEFU) == BSP_OK, + "TC-SBPROTO-020 building an image frame succeeds"); + + for (i = 0U; i < SB_FRAME_B_BMP_LEN; i++) + { + if (s_frame[i] != s_bmp[i]) + { + bmp_ok = 0; + } + } + CHECK(bmp_ok == 1, "TC-SBPROTO-020b the 3200-byte bitmap lands at offset 0 unchanged"); + + CHECK((s_frame[SB_FRAME_B_INFO_OFF + 0U] == 0xEFU) && + (s_frame[SB_FRAME_B_INFO_OFF + 1U] == 0xBEU) && + (s_frame[SB_FRAME_B_INFO_OFF + 2U] == 0xADU) && + (s_frame[SB_FRAME_B_INFO_OFF + 3U] == 0xDEU), + "TC-SBPROTO-020c the LED mask goes into info[0..3] little-endian (info[0] = bit0..7)"); + + CHECK((s_frame[SB_FRAME_B_INFO_OFF + 4U] == 0U) && (s_frame[SB_FRAME_B_INFO_OFF + 5U] == 0U) && + (s_frame[SB_FRAME_B_INFO_OFF + 6U] == 0U) && (s_frame[SB_FRAME_B_INFO_OFF + 7U] == 0U), + "TC-SBPROTO-020d info[4..7] stay 0 for a normal picture (the LOGO magic must not leak in)"); + + for (i = SB_FRAME_B_PAD_OFF; i < SB_FRAME_B_CRC_OFF; i++) + { + if (s_frame[i] != 0U) + { + pad_ok = 0; + } + } + CHECK(pad_ok == 1, "TC-SBPROTO-020e the 998-byte padding block is all zero"); + + crc = sb_crc16(s_frame, (uint16_t)SB_FRAME_B_CRC_OFF); + CHECK((s_frame[SB_FRAME_B_CRC_OFF] == (uint8_t)(crc & 0xFFU)) && + (s_frame[SB_FRAME_B_CRC_OFF + 1U] == (uint8_t)((crc >> 8) & 0xFFU)), + "TC-SBPROTO-020f the CRC sits at 4206/4207 low byte first"); + CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_OK, + "TC-SBPROTO-020g the built frame verifies"); + CHECK(screen_board_accepts(s_frame, (uint16_t)SB_FRAME_B_LEN) == 1, + "TC-SBPROTO-040 the board side Check_CRC accepts the image frame"); +} + +static void tc_frame_b_rewrite(void) +{ + static uint8_t first[SB_FRAME_B_LEN]; + uint16_t i; + int pad_ok = 1; + + /* 预填脏数据:整帧重写必须把填充段与 info 一起清干净 */ + memset(s_frame, 0xAA, sizeof(s_frame)); + fill_bmp_pattern(); + CHECK(sb_frame_b_build_image(s_frame, s_bmp, 0x00000001U) == BSP_OK, + "TC-SBPROTO-021 building over a dirty buffer succeeds"); + for (i = SB_FRAME_B_PAD_OFF; i < SB_FRAME_B_CRC_OFF; i++) + { + if (s_frame[i] != 0U) + { + pad_ok = 0; + } + } + CHECK(pad_ok == 1, "TC-SBPROTO-021b a 0xAA-pre-filled buffer is fully rewritten (padding zeroed)"); + + memcpy(first, s_frame, sizeof(first)); + CHECK(sb_frame_b_build_image(s_frame, s_bmp, 0x00000001U) == BSP_OK, + "TC-SBPROTO-021c building the same content twice succeeds"); + CHECK(memcmp(first, s_frame, sizeof(first)) == 0, + "TC-SBPROTO-021d the same inputs give a byte-identical frame (idempotent)"); +} + +static void tc_frame_b_logo(void) +{ + fill_bmp_pattern(); + CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 160U, 160U) == BSP_OK, + "TC-SBPROTO-022 building a 160x160 LOGO frame succeeds"); + CHECK((s_frame[SB_FRAME_B_INFO_OFF + 0U] == 0x00U) && (s_frame[SB_FRAME_B_INFO_OFF + 1U] == 0xA0U) && + (s_frame[SB_FRAME_B_INFO_OFF + 2U] == 0x00U) && (s_frame[SB_FRAME_B_INFO_OFF + 3U] == 0xA0U), + "TC-SBPROTO-022b width and height go into info[0..3] big-endian"); + CHECK((s_frame[SB_FRAME_B_INFO_OFF + 4U] == 0x89U) && (s_frame[SB_FRAME_B_INFO_OFF + 5U] == 0x45U), + "TC-SBPROTO-022c the LOGO magic 0x89 0x45 sits in info[4..5]"); + CHECK((s_frame[SB_FRAME_B_INFO_OFF + 6U] == 0U) && (s_frame[SB_FRAME_B_INFO_OFF + 7U] == 0U), + "TC-SBPROTO-022d info[6..7] stay 0 (reserved)"); + CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_OK, + "TC-SBPROTO-022e the LOGO frame verifies"); + CHECK(screen_board_accepts(s_frame, (uint16_t)SB_FRAME_B_LEN) == 1, + "TC-SBPROTO-040b the board side Check_CRC accepts the LOGO frame"); + + /* 小图也要能组:1x1 只需 1 字节点阵 */ + CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 1U, 1U) == BSP_OK, + "TC-SBPROTO-022f a 1x1 LOGO is accepted (the capacity check is about bytes, not pixels)"); +} + +static void tc_frame_b_logo_guard(void) +{ + fill_bmp_pattern(); + + /* 161x159 = 25599 像素(看起来"装得下"),实际要 21x159 = 3339 B > 3200 B */ + CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 161U, 159U) == BSP_ERR_PARAM, + "TC-SBPROTO-023 161x159 is rejected: 3339 bytes of bitmap do not fit in 3200"); + CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 160U, 161U) == BSP_ERR_PARAM, + "TC-SBPROTO-023b 160x161 is rejected (3220 bytes)"); + CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 168U, 152U) == BSP_OK, + "TC-SBPROTO-023c 168x152 is accepted (exactly 3192 bytes, width not a multiple of 8 is fine)"); + CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 0U, 160U) == BSP_ERR_PARAM, + "TC-SBPROTO-023d a zero width is rejected"); + CHECK(sb_frame_b_build_logo(s_frame, s_bmp, 160U, 0U) == BSP_ERR_PARAM, + "TC-SBPROTO-023e a zero height is rejected"); +} + +static void tc_frame_b_guard(void) +{ + fill_bmp_pattern(); + + CHECK(sb_frame_b_build_image(0, s_bmp, 0U) == BSP_ERR_PARAM, + "TC-SBPROTO-024 a null frame buffer is rejected (image)"); + CHECK(sb_frame_b_build_image(s_frame, 0, 0U) == BSP_ERR_PARAM, + "TC-SBPROTO-024b a null bitmap is rejected (image)"); + CHECK(sb_frame_b_build_logo(0, s_bmp, 160U, 160U) == BSP_ERR_PARAM, + "TC-SBPROTO-024c a null frame buffer is rejected (LOGO)"); + CHECK(sb_frame_b_build_logo(s_frame, 0, 160U, 160U) == BSP_ERR_PARAM, + "TC-SBPROTO-024d a null bitmap is rejected (LOGO)"); + CHECK(sb_frame_b_verify(0, (uint16_t)SB_FRAME_B_LEN) == BSP_ERR_PARAM, + "TC-SBPROTO-024e a null frame is rejected by verify"); +} + +static void tc_frame_b_verify(void) +{ + fill_bmp_pattern(); + (void)sb_frame_b_build_image(s_frame, s_bmp, 0x00000000U); + + s_frame[100] ^= 0x01U; + CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_ERR_CRC, + "TC-SBPROTO-030 a flipped bitmap byte is caught"); + s_frame[100] ^= 0x01U; + CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_OK, + "TC-SBPROTO-030b flipping it back restores the verdict"); + + /* 填充段屏板不读,但它参与 CRC:改了必须报错,否则"填充随便填"会悄悄破坏帧 */ + s_frame[SB_FRAME_B_PAD_OFF + 500U] = 0x01U; + CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_ERR_CRC, + "TC-SBPROTO-030c a modified padding byte is caught (the padding really is inside the CRC range)"); + s_frame[SB_FRAME_B_PAD_OFF + 500U] = 0x00U; + + s_frame[SB_FRAME_B_CRC_OFF] ^= 0xFFU; + CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_FRAME_B_LEN) == BSP_ERR_CRC, + "TC-SBPROTO-030d a corrupted CRC byte is caught"); + + (void)sb_frame_b_build_image(s_frame, s_bmp, 0U); + CHECK(sb_frame_b_verify(s_frame, (uint16_t)(SB_FRAME_B_LEN - 1U)) == BSP_ERR_PARAM, + "TC-SBPROTO-030e a length other than 4208 is rejected"); +} + +int main(void) +{ + printf("=== ScreenLink Proto Tests ===\n"); + + printf("-- CRC-16/MODBUS check values --\n"); + tc_crc_vectors(); + printf("-- CRC table vs bitwise reference --\n"); + tc_crc_table(); + + printf("-- frame A: the board's real summon bytes --\n"); + tc_frame_a_vectors(); + printf("-- frame A: key handling --\n"); + tc_frame_a_lenient_key(); + printf("-- frame A: rejection paths --\n"); + tc_frame_a_reject(); + printf("-- frame A: argument guards --\n"); + tc_frame_a_guard(); + + printf("-- frame B: image (bitmap + LED mask) --\n"); + tc_frame_b_image(); + printf("-- frame B: full-frame rewrite / idempotence --\n"); + tc_frame_b_rewrite(); + printf("-- frame B: LOGO (width/height + magic) --\n"); + tc_frame_b_logo(); + printf("-- frame B: LOGO capacity guard --\n"); + tc_frame_b_logo_guard(); + printf("-- frame B: argument guards --\n"); + tc_frame_b_guard(); + printf("-- frame B: self-verification --\n"); + tc_frame_b_verify(); + + printf("\n=== ScreenLink Proto Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} From 16048140fd30441b81978a8312dc4993c2e215a4 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:17:56 +0800 Subject: [PATCH 09/16] =?UTF-8?q?chore(=E4=BD=93=E7=A7=AF):=20=E9=87=8D?= =?UTF-8?q?=E7=9B=96=E5=90=8C=E7=A0=81=E5=87=AD=E8=AF=81=EF=BC=88sb=5Fprot?= =?UTF-8?q?o=20=E8=BF=9B=E7=B4=A2=E5=BC=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一条提交漏带 scripts/size_attestation.json。指纹按 git 索引内容生成, sb_proto.c/h 进索引后指纹从 bfe075e7 变为 df1c01ab,凭证不跟着走就会 判"不同码"。 Code=236,460 B 不变(新模块尚无调用者),RW_IRAM1 81,536 B / RW_CCM 57,376 B 均未动。 --- scripts/size_attestation.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 8a9c33ec..336aecb5 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,15 +1,15 @@ { - "built_at": "2026-10-29T18:43:32+08:00", - "fingerprint": "bfe075e7093727025c3ba21e3057445a6c23ec4b0193c7e14286cc6ca60f6f1f", - "fingerprint_files": 1452, + "built_at": "2026-10-29T19:17:06+08:00", + "fingerprint": "df1c01abda2bd6778c11976f346e1a9024386ae2809f94c720da9f95ef59becc", + "fingerprint_files": 1454, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "27a700b0d81cde580d88f783d6cb661aac248516", + "git_head": "71b6a5fa14db85ed9c95bba5b3b6106d1f20d223", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793270606, + "map_mtime": 1793272621, "pct": { "ER_IROM1": 25.23, "RW_CCM": 87.89, From 2c69a6ca076d201a2f412318bdfaa91f20632c48 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 19:18:11 +0800 Subject: [PATCH 10/16] =?UTF-8?q?docs(=E5=B1=8F=E6=9D=BF):=20=E8=AE=A2?= =?UTF-8?q?=E6=AD=A3=20CRC=20=E5=AD=97=E8=8A=82=E5=BA=8F=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=EF=BC=88=E4=BD=8E=E5=AD=97=E8=8A=82=E5=9C=A8=E5=89=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 写 1-1 时用屏板自己的算法复算了线上字节,发现文档里"CRC 高字节在前"的说法 与真实顺序相反,一并订正。 事实:屏板 Calculate_CRC() 返回的不是标准 CRC-16/MODBUS 值,而是高低互换后 的「线上字」("123456789" 它返回 0x374B,标准值 0x4B37)。于是 RS485_RefreshScreenCMD() 里 cmdbuf[4] = crc>>8(注释写"CRC校验高字节") 实际写出去的是低字节——两处命名都反,线上字节是对的(标准 Modbus 低字节在前)。 照字面实现会让屏板把每一帧都判成 CRC 错,而它的超时判定只看"收没收到一整 包"、不看 CRC,现场只表现为画面不刷新,极易误判成波特率或时序问题。 - 产品需求 §5.16.2:帧 A 表的 4/5 偏移改成低/高,补一段字节序说明与实测值; 帧 B 的 CRC 行、§5.16 的通道概述、实施清单第 11 条同步。 - 产品需求 §5.16.2:RT_CMD 周期 275 -> 280 ms(判据 RT_count > 55 即第 56 拍 x 5 ms;屏板源码注释里的 275 是少算一拍)。 - 屏板链路.md:4.2 的帧 B 布局与 0-3 的验收命令示例同步,并指向单测向量; 1-1 标完成。 --- docs/产品需求/产品需求设计文档.md | 24 ++++++++++++++++-------- docs/任务规划/屏板链路.md | 15 ++++++++++++--- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index e5868878..87f139f6 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -1068,13 +1068,21 @@ sequenceDiagram |---|---|---|---| | 0 | 1 | 从站地址 | 固定 `0x01`(**主机的从站地址**,不是屏板地址) | | 1 | 1 | 功能码 | 固定 `0x03`(读保持寄存器) | -| 2 | 1 | 命令类型 | `0x00` = `KEY_CMD`(按键事件,**立即**召唤)
`0x01` = `RT_CMD`(周期刷新,275 ms 一次) | +| 2 | 1 | 命令类型 | `0x00` = `KEY_CMD`(按键事件,**立即**召唤)
`0x01` = `RT_CMD`(周期刷新,**280 ms** 一次:`RT_count > 55`,第 56 拍 × 5 ms) | | 3 | 1 | 按键值 | `KEY_CMD` 时有效(见 §5.16.3 键值表);`RT_CMD` 时**恒为 `0`**(按键发完即清 `key_bk`,见 `RS485_Process()`) | -| 4 | 1 | CRC-16 **高**字节 | CRC-16/MODBUS | -| 5 | 1 | CRC-16 **低**字节 | 同上 | +| 4 | 1 | CRC-16 **低**字节 | CRC-16/MODBUS | +| 5 | 1 | CRC-16 **高**字节 | 同上 | -CRC-16/MODBUS(多项式 `0xA001` 反转式、初值 `0xFFFF`、查表法 `auchCRCHi[]`/`auchCRCLo[]`), -**只覆盖前 4 字节**,**高字节在前**(`cmdbuf[4] = crc>>8`、`cmdbuf[5] = crc`)。 +CRC-16/MODBUS(多项式 `0xA001` 反转式、初值 `0xFFFF`、查表法), +**只覆盖前 4 字节**,**低字节在前**——这是标准 Modbus RTU 的线上顺序(教科书校验值:数据 `123456789` +→ `0x4B37`,线上写 `37 4B`)。 + +> ⚠ **屏板源码里的字节序注释是反的,别照抄**:`Calculate_CRC()` 返回的**不是**标准 CRC 值,而是高低 +> 互换后的「线上字」(`123456789` 它返回 `0x374B`)。于是 `RS485_RefreshScreenCMD()` 里那句 +> `cmdbuf[4] = crc>>8`(注释写"CRC校验高字节")实际写出去的是**低字节**——两处命名都反,**线上字节是对的**。 +> 主机侧按「标准值 + 低字节在前」实现,两侧字节逐一相同;若照字面写成"高字节在前",屏板会把每一帧 +> 都判成 CRC 错,而它的超时判定只看"收没收到一整包"、不看 CRC,现场只表现为画面不刷新, +> 极易误判成波特率或时序问题。实测向量与实现见 `Drivers/BSP/ScreenLink/sb_proto.c` 文件头。 **帧 B:图像应答(主机 → 屏板,固定 4208 B)** @@ -1086,7 +1094,7 @@ CRC-16/MODBUS(多项式 `0xA001` 反转式、初值 `0xFFFF`、查表法 `auch | 3205 | 1 | `info[5]` | LOGO 魔数低字节 `0x45` | | 3206 | 2 | `info[6..7]` | 保留,屏板不读 | | 3208 | 998 | 保留填充 | 屏板不读,**但参与 CRC 计算**,须固定填充(建议 `0x00`) | -| 4206 | 2 | CRC-16 | 覆盖 `[0, 4205]`,**高字节在前** | +| 4206 | 2 | CRC-16 | 覆盖 `[0, 4205]`,**低字节在前**(与帧 A 同一口径,见上方的字节序说明) | > ⚠ **帧长以代码为准:4208 B**(屏板 readme 与 `BMP_LEN` 写的"3208 字节"不作准):接收侧实参是 > `Check_CRC(DR, UART_RX_LEN)`,`UART_RX_LEN = 4208` → CRC 取 `DR[4206]/DR[4207]`、覆盖 `DR[0..4205]`。 @@ -1480,7 +1488,7 @@ for (i = 1; i < LED_COUNT; i++) /* LE > 逐项差异、以及为什么会有差异,见 §5.16.9。**以参考为准**,差异项按 §9.13 排期收敛。 > > 本节只讲**网络**通道(TCP 7003)。**串口屏板通道不走本协议**——它是 RS-485 上的 -> **Modbus RTU 风格裸二进制帧**(6 B 召唤 + **4208 B** 图像应答,CRC-16/MODBUS 高字节在前), +> **Modbus RTU 风格裸二进制帧**(6 B 召唤 + **4208 B** 图像应答,CRC-16/MODBUS **低字节在前**), > 帧格式已按屏板源码逐字节定版,见 §5.16.2。 > 两条通道**共用同一份显存**,但帧格式、校验、时序、主从方向全都不同,**不能共用组帧代码**。 @@ -2536,7 +2544,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 8 | **发送重试评估** | 对比参考"BUSY + 200 ms 放弃"与现有 `net_send` 阻塞模型,出结论并回填 §5.16.9 第 7 行 | 结论有实测数据支撑(实现 / 明确不实现) | 否 | | 9 | **客户端一致性回归** | `DTU-RemoteLCD` 按 **16 B** 解析;把 §5.16.10 第 1~7 条做成自动化用例 | 七条判据全 PASS | **是** | | 10 | **串口物理层落地** | 按 §5.16.2 定版:**700000 8N1、`BRR = 0x0078`**(USART1,APB2 84 MHz)、**无 DE**(TTL 直连,不走收发器)、RX 缓冲 16 B + 空闲中断断帧、发送走 **DMA 一次性发完**(禁逐块阻塞,`DMA2_Stream7` TX / `DMA2_Stream2` RX 已在 `.ioc` 配好但控制台未用) | 环回 + 示波器实测 700 kbaud 分频零误差;屏板能收到帧 A 并回帧 B | 否 | -| 11 | **帧 A 解析 / 帧 B 组帧** | 新增屏板通道模块:识别 6 B 请求(`01 03 type key CRC_H CRC_L`)、校验 CRC-16/MODBUS(**高字节在前**);组 **4208 B** 帧 B(3200 显存 + 4 B LED + 4 B info 保留 + 998 B 填充 + 2 B CRC) | 屏板出图与 `g_ui_fbuf` 逐字节一致;`info[0..3]` 与下发的 LED 掩码一致 | 否 | +| 11 | **帧 A 解析 / 帧 B 组帧** | 新增屏板通道模块:识别 6 B 请求(`01 03 type key CRC_L CRC_H`)、校验 CRC-16/MODBUS(**低字节在前**,口径见 §5.16.2);组 **4208 B** 帧 B(3200 显存 + 4 B LED + 4 B info 保留 + 998 B 填充 + 2 B CRC) | 屏板出图与 `g_ui_fbuf` 逐字节一致;`info[0..3]` 与下发的 LED 掩码一致 | 否 | | 12 | **状态灯位映射表** | 定义"**业务状态 → 屏板 LED 位**"映射(参考源码**无此表**,须自定);`info[0..3]` 按 **32 位小端**填,bit0 恒 0、bit20~31 恒 0 | §5.16.10 第 11 条通过 | 否 | | 13 | **串口上行按键** | 帧 A 的 `key` 字节按**屏板 `KEY_TYPE`** 解释——实际可达值只有 **`1/2/3/4/7/8/9`**(`5`/`6`/`10` 无物理入口);映射为逻辑键值后与 7003 注入**同一入口**合并 | §5.16.10 第 10 条通过;`9`/`10` 语义按 §5.16.3 ② 处置 | 否 | | 14 | **双通道并发 + 自复位容忍** | 两出口各自独立发送上下文;串口 4208 B 阻塞不拖 7003;链路监视按"**连续 ≥10 s 无应答**"判离线,**忽略屏板 ≈5000 s 周期自复位** | §5.16.10 第 12、13、14 条通过 | 否 | diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index e2d88ffc..d3822ebb 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -119,7 +119,8 @@ 1. **无屏板实物时用 PC + USB 转 TTL 顶替屏板**(`docs/任务规划/README.md` 已注明"软件可先用 PC + USB 转 TTL 台架验证,不必等屏板实物"):适配器 `TXD` → `U5.B10`、`RXD` → `U5.A10`、`GND` → 板地, 700000 8N1,按屏板的周期召唤节拍(约 275~280 ms,口径见 `docs/产品需求/产品需求设计文档.md` §5.16.2) - 发 6 B 帧 `01 03 01 00 CRC_H CRC_L`(`RT_CMD`,按键时第 4 字节换成键值;CRC-16/MODBUS 只覆盖前 4 B、高字节在前)。 + 发 6 B 帧 `01 03 01 00 CRC_L CRC_H`(`RT_CMD`,按键时第 4 字节换成键值;CRC-16/MODBUS 只覆盖前 4 B、 + **低字节在前**——屏板源码的注释把高低写反了,口径见 `docs/产品需求/产品需求设计文档.md` §5.16.2)。 2. **判据**:`Size == 6`、CRC 通过率 100 %、连续 10 min 无丢帧;拔插一次 TX 线制造噪声后能自动恢复 (`HAL_UART_ErrorCallback` 里清 ORE 并重挂接收,否则一次噪声就永久静默)。 3. **有实物后**重复第 2 步,并确认屏板画面按召唤节拍刷新。 @@ -242,7 +243,7 @@ | 序 | 任务 | 改动点 | 判据 | |---|---|---|---| -| 1-1 | **协议层模块(纯逻辑,可主机单测)** | 新建 `Drivers/BSP/ScreenLink/sb_proto.c/h`:CRC-16/MODBUS(表与屏板 `auchCRCHi/Lo` 逐字节一致)、帧 A 解析、帧 B 组帧 | 主机单测:给定 6 B 召唤帧能算出屏板同样的 CRC;组出的 4208 B 帧 CRC 自校验通过 | +| 1-1 ✅ | **协议层模块(纯逻辑,可主机单测)** | 新建 `Drivers/BSP/ScreenLink/sb_proto.c/h`:CRC-16/MODBUS(与屏板同一多项式,用 16 位合并表)、帧 A 解析、帧 B 组帧(普通图 / LOGO)、帧 B 自校验;双端注册:uvprojx 加 File + IncludePath,模拟器侧走 `scripts/check_registration.py` 的 MCU-only 豁免(纯逻辑,模拟器无消费路径) | **已完成 2026-10-29**:主机单测 **59/59 PASSED**(`test/host/sb_proto/`),覆盖率 74/74 行、6/6 函数;屏板实测向量 `01 03 01 00 F0 48` 与 `01 03 00 09 31 DE` 原样解析通过;组出的两种帧按屏板 `Check_CRC` 口径复核均被接受;Keil 构建 **0 错误 0 警告**(`Code` 不变——尚无调用者,链接器按未用节丢弃) | | 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) | 屏板每 280 ms 发来的 6 B 能稳定收到,`Size == 6`,CRC 通过率 100 %;**拔插一次 TX 线制造噪声后能自动恢复**(不清 ORE 会永久锁死) | @@ -303,9 +304,17 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 偏移 3205 1 B info[5]:普通图填 0x00(非 0x45) 偏移 3206 2 B info[6..7]:保留,填 0 偏移 3208 998 B 填充,填 0x00(屏板不读,但参与 CRC) -偏移 4206 2 B CRC-16/MODBUS,覆盖 [0, 4205],高字节在前 +偏移 4206 2 B CRC-16/MODBUS,覆盖 [0, 4205],低字节在前(口径见 §4.2 下方的字节序说明) ``` +> ⚠ **CRC 的字节序:屏板源码的注释是反的**。屏板 `Calculate_CRC()` 返回的不是标准 CRC 值, +> 而是高低互换后的「线上字」(数据 `123456789` 它返回 `0x374B`,标准值是 `0x4B37`),所以 +> `RS485_RefreshScreenCMD()` 里 `cmdbuf[4] = crc>>8`(注释写"CRC校验高字节")写出去的其实是**低字节**。 +> 两处命名都反,**线上字节是对的**:标准 Modbus RTU 的低字节在前。 +> 主机侧一律按「标准 CRC 值 + 低字节在前」写(帧 A 偏移 4、帧 B 偏移 4206 都是低字节), +> 照字面写成"高字节在前"会让屏板把每一帧都判成 CRC 错,而它的超时判定只看"收没收到一整包", +> 现场只表现为画面不刷新。实测向量已钉在 `test/host/sb_proto/sb_proto_test.c`。 + > ⚠ **`info[0..3]` 是双解释字段**:普通图当 LED 掩码、LOGO 图当宽高。区分只靠 `info[4..5]`。 > **先判魔数再解释这 4 字节**,顺序反了会把 LED 掩码当宽高。 From 7c8669e52a91c3c6051abc3defc021adbbba7d8c Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 20:13:18 +0800 Subject: [PATCH 11/16] =?UTF-8?q?feat(=E5=B1=8F=E6=9D=BF):=20USART1=20700?= =?UTF-8?q?=20kbaud=20+=20=E6=94=B6=E4=B8=AD=E6=96=AD=E4=BC=A0=E8=BE=93?= =?UTF-8?q?=E5=B1=82=20sb=5Flink=EF=BC=881-2/1-3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 批 1 的 1-2 / 1-3:把屏板(DTU-LCD)发来的 6 B 帧 A 在 USART1 上稳定收下来。 1-2 波特率:.ioc 不保存任何 UART 的波特率(只有 USART1.IPParameters=VirtualMode),GUI 里改也不落盘,所以只能在 Src/usart.c 的 USER CODE BEGIN USART1_Init 2 区覆盖 huart1.Init.BaudRate 后重调一次 HAL_UART_Init(第二次调用时 gState 已非 RESET,HAL 只重算 BRR、不再跑 MspInit,GPIO/DMA/NVIC 不会被重配)。开机日志实测 BRR=0x0078,与理论值 84 MHz/(16x700 kHz)=7.5 逐位相符,误差 0%。 1-3 收中断:新建 Drivers/BSP/ScreenLink/sb_link.c/h——16 B 缓冲 + IDLE 中断断帧、帧 A 校验、错误中断里按 RM0090 清 ORE/NE/FE/PE、周期统计上报。 HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback 是全局单例(唯一实现 在 rs485.c),故只在那里各加一行转发,按句柄过滤放在 sb_link 内。 台架实测(PC + USB 转 TTL 顶替屏板,COM6@700000)撞出两个真问题: - 错误回调里无条件重挂 -> 出现 err=160 / rearm=160 同步增长,看着像每帧 都重挂失败。真因是 HAL 把 NE/FE/PE 归为非阻塞型(接收照跑、RxState 仍是 BUSY_RX),此时 ReceiveToIdle_IT 只会返回 HAL_BUSY。改为仅当 RxState == HAL_UART_STATE_READY(ORE 那类阻塞型)时才重挂。 - 日志字面量里带中文 -> ARMCC #870-D invalid multibyte character sequence。字面量一律纯 ASCII,中文只写注释。 验证:主机单测 81/81 PASSED(test/host/sb_link/,其中按 RM0090 的清除语义 把"必须 SR->DR 顺序才清得掉 ORE"钉成断言);Keil 0 错误 0 警告;台架 rx == 发送帧数、ok == rx、crc=0、len=0、size=6、keys 随按键帧递增;先用 错波特率灌 64 B 垃圾(len=31、bits=0x06)后自动恢复且 rearm=0。 新增 test/host/screenlink_bench.py 作为可复跑的屏板激励(--key / --glitch-baud)。 --- App/app/app_main.c | 17 + App/task/ui_task.c | 6 + Drivers/BSP/RS485/rs485.c | 19 +- Drivers/BSP/ScreenLink/sb_link.c | 316 ++++++++++++++++ Drivers/BSP/ScreenLink/sb_link.h | 183 ++++++++++ MDK-ARM/STM32F407-Demo.uvprojx | 5 + Src/main.c | 2 + Src/usart.c | 21 +- docs/内存管理/RAM使用分析与优化方案.md | 6 +- docs/内存管理/RAM预算台账.md | 6 +- scripts/check_registration.py | 1 + scripts/ram_baseline.tsv | 2 +- scripts/size_attestation.json | 26 +- scripts/test_coverage_baseline.json | 51 ++- test/host/sb_link/run_unit_test.py | 59 +++ test/host/sb_link/sb_link_test.c | 482 +++++++++++++++++++++++++ test/host/sb_link/stub/dbg_log.h | 58 +++ test/host/sb_link/stub/main.h | 186 ++++++++++ test/host/screenlink_bench.py | 165 +++++++++ 19 files changed, 1573 insertions(+), 38 deletions(-) create mode 100644 Drivers/BSP/ScreenLink/sb_link.c create mode 100644 Drivers/BSP/ScreenLink/sb_link.h create mode 100644 test/host/sb_link/run_unit_test.py create mode 100644 test/host/sb_link/sb_link_test.c create mode 100644 test/host/sb_link/stub/dbg_log.h create mode 100644 test/host/sb_link/stub/main.h create mode 100644 test/host/screenlink_bench.py diff --git a/App/app/app_main.c b/App/app/app_main.c index 111985bc..ce4d702c 100644 --- a/App/app/app_main.c +++ b/App/app/app_main.c @@ -19,6 +19,7 @@ #include "tim.h" /* htim3:1 kHz 采样节拍(TIM3 更新中断触发 CONVST + TRGO 触发 ADC1) */ #include "adc.h" /* adc1_disable():挂 DMA 前把 ADC 关掉,让 Start_DMA 自己使能 */ #include "rs485.h" +#include "sb_link.h" /* 屏板链路传输层:sb_link_init()(USART1 收中断,批 1 的 1-3) */ #include "sd2506.h" #include "boot_stats.h" /* 开机账本:上电次数(RTC 用户 SRAM)+ 本次复位原因快照(道G task-12) */ #include "sys_clock.h" @@ -197,6 +198,22 @@ int app_main_init(void) { rs485_init(&g_rs485, &huart4, ST_DIR4_GPIO_Port, ST_DIR4_Pin); DBG_INFO("RS485 init: OK (UART4, DE=PA15)"); + /* 2b) 屏板链路初始化(USART1,700 kbaud 8N1,IDLE 中断接收)。 + * 放在 NAND 全片扫描之前:这一步只是挂接收(µs 级),挂上之后屏板的周期召唤 + * 就已经能被收下来并计数,台架不必等启动跑完才能看到链路状态。 + * 波特率不在本调用里设——在 Src/usart.c 的 USER CODE 区(CubeMX 重生成只保护那里), + * 本调用只做一次交叉核对,对不上会报 BSP_ERR_STATE 并打 ERR 行。 */ + { + bsp_err_t sb_ret = sb_link_init(&huart1); + /* BRR 直接读出来打:1-2 的判据是"实测位宽误差 < 1 %",而 BRR 是位宽的唯一决定量—— + * 84 MHz / (16 x 700 kHz) = 7.5 → 期望 0x0078(整数 7、小数 0.5x16 = 8),误差 0 %。 + * 有这一行就不必为了核波特率去架示波器:BRR 对不上就说明 USER CODE 覆盖没生效。 */ + DBG_INFO("ScreenLink init: %s (ret=%d, USART1 %lu baud, BRR=0x%04lX)", + (sb_ret == BSP_OK) ? "OK" : "FAILED", + sb_ret, (unsigned long)SB_LINK_BAUD, + (unsigned long)huart1.Instance->BRR); + } + /* 3) GD5F 初始化测试(BBT 全片扫描,启动段最耗时的一步) */ int ret = gd5f2gq5ue_init(); DBG_INFO("NAND init: %s", diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 4ac1ca29..28d9c83d 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -58,6 +58,7 @@ #include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */ #include "task_watchdog.h" #include "task_prof.h" /* P2 常开探针:循环周期 / 工作耗时 / 推流往返 */ +#include "sb_link.h" /* 屏板链路:sb_link_poll()(批 1 只上报统计,批 2 在此组帧) */ /* * 函数功能:待机屏底部时钟提供回调(平台差异:MCU = sys_clock_get_str) @@ -510,6 +511,11 @@ void ui_task_func(void *argument) { ui_net_sink_poll(); task_prof_exit(TASK_PROF_UI_SINK); + /* 屏板链路统计上报(内部自分频,5 s 一次、只在计数有变化时打一行)。 + * 放在推流段之后:它只是读一份快照 + 偶尔一行日志,不进推流往返的耗时统计。 + * 批 2 的 2-1 会把帧 B 组帧接到这里(显存/LED 变化时组帧),届时仍在 `uiTask` 上下文。 */ + sb_link_poll(osKernelGetTickCount()); + /* 单轮工作耗时(不含 osDelay):与循环周期对比即得"调度延迟" */ task_prof_sample(TASK_PROF_UI_WORK, task_prof_cyc() - prof_t0); diff --git a/Drivers/BSP/RS485/rs485.c b/Drivers/BSP/RS485/rs485.c index ca2ff235..3a2096ad 100644 --- a/Drivers/BSP/RS485/rs485.c +++ b/Drivers/BSP/RS485/rs485.c @@ -16,6 +16,8 @@ #include "rs485.h" +#include "sb_link.h" /* 屏板链路 USART1:本文件是全局 UART 回调的唯一实现处,见下方回调段 */ + /* * 函数功能:把 HAL 的状态码映射成项目统一的 bsp_err_t(边界映射,2026-10-22 试点) * 入口参数:status - HAL_* 返回的状态 @@ -170,7 +172,16 @@ uint16_t rs485_rx_get_size(const rs485_handle_t *p_handle) { return p_handle->rx_size; } -/* ======================== HAL 回调(UART5 / RS-485 专属) ======================== */ +/* ==================== HAL 回调(本工程唯一实现处) ==================== + * + * ⚠ 这两个回调是 HAL 的**全局单例**:一个工程只能有一份定义 + * (Inc/stm32f4xx_hal_conf.h 的 USE_HAL_UART_REGISTER_CALLBACKS = 0, + * 没法按句柄注册各自的回调)。所以新增通道时**在这里加一行转发**, + * 不要在别处再定义一份 —— 重复定义是链接期的 multiple definition,编不过。 + * + * 按句柄过滤放在各通道自己的模块里(如 sb_link_on_rx_event 内部判 p_huart), + * 本文件不必知道别的通道用的是哪个 huart,也就不会因为它们换口而改动。 + */ /* * 应用层接收钩子默认实现(弱符号,可被应用重定义)。 @@ -196,6 +207,9 @@ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { /* 重新开启接收 */ rs485_receive_start(&g_rs485); } + + /* 屏板链路(USART1):非本链路的句柄在 sb_link 内部被忽略 */ + sb_link_on_rx_event(huart, Size); } /* @@ -210,4 +224,7 @@ void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { /* 重新开启接收 */ rs485_receive_start(&g_rs485); } + + /* 屏板链路(USART1):清 ORE 并重挂接收(不清 ORE 会永久静默),见 sb_link.c */ + sb_link_on_error(huart); } diff --git a/Drivers/BSP/ScreenLink/sb_link.c b/Drivers/BSP/ScreenLink/sb_link.c new file mode 100644 index 00000000..d7bc14a8 --- /dev/null +++ b/Drivers/BSP/ScreenLink/sb_link.c @@ -0,0 +1,316 @@ +/* + * 模块名称:屏板链路传输层(USART1 中断接收) + * 模块功能:IDLE 空闲中断断帧收 6 B 帧 A、帧 A 校验、错误中断里清 ORE 并重挂接收、 + * 累计统计与周期上报。发送侧(帧 B 组帧 + TX DMA 起发)属批 2,不在本文件。 + * 适用平台:STM32F4 系列 + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建初始版本(批 1 的 1-2 / 1-3) + * + * 三个容易踩的点(改之前先读,都已经在台架上被别的东西咬过一次): + * + * ① **每个回调末尾都必须重挂接收**。HAL_UARTEx_ReceiveToIdle_IT 是一次性的: + * 一次 IDLE 事件处理完,HAL 就把 RxState 打回 READY 并关掉 RXNEIE/IDLEIE + * (stm32f4xx_hal_uart.c 的 UART_IRQHandler 里 IDLE 那一支)。漏挂一次链路就 + * 永久静默,而现象与"屏板没上电"完全一样——没有报错、没有计数。 + * + * ② **错误回调里必须自己清 ORE**。HAL 在阻塞型错误里已经把接收停掉并打回 READY, + * 但它不清 ORE(RM0090 §30.6.1:先读 SR 再读 DR)。ORE 一旦锁存,RXNE 不再置位 + * → 接收永远进不来。这就是"拔插一次 TX 线之后屏板再也收不到"的真因。 + * + * ③ **本文件只按句柄过滤,不做业务动作**。全局回调(HAL_UARTEx_RxEventCallback / + * HAL_UART_ErrorCallback)的唯一实现处在 Drivers/BSP/RS485/rs485.c,那里只做一行 + * 转发;过滤在这里做,于是新增通道不必去改 rs485.c 的判据。业务动作(注键、 + * 刷新画面)全部留给任务侧——注键不能在中断里做(docs/任务规划/屏板链路.md 决策三)。 + */ + +#define DBG_TAG "[SBLINK]" +#include "dbg_log.h" + +#include + +#include "sb_link.h" + +/* ======================== 内部状态 ======================== */ + +/* 绑定的 USART1 句柄;NULL = 未初始化(所有入口据此直接返回) */ +static UART_HandleTypeDef *s_huart; + +/* 接收缓冲:IDLE 中断的落地目标,长度 SB_LINK_RX_BUF_SIZE(16 B) */ +static uint8_t s_rx_buf[SB_LINK_RX_BUF_SIZE]; + +/* 累计统计:中断侧自增、任务侧只读,故 volatile */ +static volatile sb_link_stats_t s_stats; + +/* 上报用:上次上报时刻与上次上报时的计数(用来判"有没有变化") */ +static uint32_t s_report_ms; +static uint32_t s_report_events; +static uint32_t s_report_err_irq; +static uint32_t s_report_rearm_fail; +static uint8_t s_first_frame_logged; + +/* ======================== 内部函数 ======================== */ + +/* + * 函数功能:把 HAL 的返回状态映射成项目统一的 bsp_err_t + * 入口参数:status - HAL_* 返回的状态 + * 返回值:对应的 bsp_err_t;HAL_BUSY 归 BSP_ERR_BUSY(可重试),HAL_OK 归 BSP_OK + * 限定条件:仅本文件内部使用 + * 函数说明:与 rs485.c 里那个同名映射同构——HAL 的码**不在本模块之外出现** + * (bsp_err.h 的边界纪律),所以映射写成 static 而不是往公共头里塞。 + */ +static bsp_err_t sb_err_from_hal(HAL_StatusTypeDef status) +{ + switch (status) + { + case HAL_OK: + return BSP_OK; + case HAL_BUSY: + return BSP_ERR_BUSY; + case HAL_TIMEOUT: + return BSP_ERR_TIMEOUT; + default: + return BSP_ERR_IO; + } +} + +/* ======================== 公开函数实现 ======================== */ + +/* + * 函数功能:初始化屏板链路并挂上第一轮接收 + * 入口参数:p_huart - 已由 CubeMX 初始化完成的 USART1 句柄(非空) + * 返回值:BSP_OK / BSP_ERR_PARAM / BSP_ERR_STATE / BSP_ERR_BUSY / BSP_ERR_IO(见头文件) + * 限定条件:须在 osKernelStart 之前或任一任务上下文调用 + * 函数说明:波特率由 Src/usart.c 的 USER CODE 区设置,本函数只做一次交叉核对—— + * 对不上就报 BSP_ERR_STATE 并打一行 ERR,免得"115200 收 700 kbaud"这种 + * 静默错配被误判成接线或协议问题(那种错配的表象是 100 % CRC 错帧)。 + */ +bsp_err_t sb_link_init(UART_HandleTypeDef *p_huart) +{ + if (p_huart == NULL) + { + return BSP_ERR_PARAM; + } + + s_huart = p_huart; + (void)memset((void *)&s_stats, 0, sizeof(s_stats)); + s_report_ms = 0U; + s_report_events = 0U; + s_report_err_irq = 0U; + s_report_rearm_fail = 0U; + s_first_frame_logged = 0U; + + if (p_huart->Init.BaudRate != SB_LINK_BAUD) + { + /* 串口波特率被打回 CubeMX 的默认值 —— 即 Src/usart.c 的 USER CODE 覆盖丢了。 + * ⚠ 日志字面量只写 ASCII:ARMCC 按本地代码页解多字节,中文字面量会报 + * #870-D invalid multibyte character sequence(2026-10-29 实测)。 */ + DBG_ERROR("baud=%lu expect=%lu (USER CODE override in usart.c lost)", + (unsigned long)p_huart->Init.BaudRate, (unsigned long)SB_LINK_BAUD); + return BSP_ERR_STATE; + } + + return sb_link_receive_start(); +} + +/* + * 函数功能:重新挂上一轮 IDLE 中断接收 + * 入口参数:无 + * 返回值:BSP_OK / BSP_ERR_STATE / BSP_ERR_BUSY / BSP_ERR_IO(见头文件) + * 限定条件:可在中断上下文调用;不可阻塞 + * 函数说明:失败只累计 rearm_fail 计数、不重试——在中断里重试等于把一次总线抖动 + * 放大成中断风暴。真失败了靠任务侧的统计行暴露(rearm 列非 0 就是链路已死)。 + */ +bsp_err_t sb_link_receive_start(void) +{ + bsp_err_t ret; + + if (s_huart == NULL) + { + return BSP_ERR_STATE; + } + + ret = sb_err_from_hal(HAL_UARTEx_ReceiveToIdle_IT(s_huart, s_rx_buf, SB_LINK_RX_BUF_SIZE)); + if (ret != BSP_OK) + { + s_stats.rearm_fail++; + } + + return ret; +} + +/* + * 函数功能:IDLE 事件入口(由 HAL_UARTEx_RxEventCallback 转发进来) + * 入口参数:p_huart - 触发事件的句柄(非本链路的直接忽略) + * size - 本次断出来的字节数 + * 返回值:无 + * 限定条件:中断上下文;只做 CRC 与计数,不得打日志、不得阻塞 + * 函数说明:先判长度再解析——长度不是 6 时连 CRC 都不必算(屏板只发 6 B 帧 A)。 + * 解析结论只落统计,业务动作由任务侧取走统计后做(注键不能在中断里做)。 + */ +void sb_link_on_rx_event(UART_HandleTypeDef *p_huart, uint16_t size) +{ + sb_frame_a_t frame; + + if ((s_huart == NULL) || (p_huart != s_huart)) + { + return; + } + + s_stats.rx_events++; + s_stats.last_size = size; + + if (size != SB_FRAME_A_LEN) + { + s_stats.rx_len_err++; + } + else + { + bsp_err_t ret; + + (void)memset((void *)&frame, 0, sizeof(frame)); + ret = sb_frame_a_parse(s_rx_buf, size, &frame); + if (ret == BSP_OK) + { + s_stats.rx_ok++; + s_stats.last_cmd = frame.cmd; + s_stats.last_key = frame.key; + if (frame.cmd == SB_CMD_KEY) + { + s_stats.key_events++; + } + } + else if (ret == BSP_ERR_CRC) + { + s_stats.rx_crc_err++; + } + else + { + s_stats.rx_fmt_err++; + } + } + + (void)sb_link_receive_start(); +} + +/* + * 函数功能:错误事件入口(由 HAL_UART_ErrorCallback 转发进来) + * 入口参数:p_huart - 触发事件的句柄(非本链路的直接忽略) + * 返回值:无 + * 限定条件:中断上下文;不得打日志、不得阻塞 + * 函数说明:先读 SR 再读 DR 清 ORE/NE/FE/PE,再按需重挂接收。两个细节都不能省: + * 1. **清除顺序**:RM0090 §30.6.1 规定的序列就是"读 SR 后读 DR",只读 DR 清不掉 ORE。 + * 2. **只有 HAL 真把接收停了才重挂**:HAL 把错误分成两类(stm32f4xx_hal_uart.c 的 + * UART_IRQHandler)—— + * · ORE 是**阻塞型**:HAL 先 UART_EndRxTransfer(RxState 打回 READY、关接收中断) + * 再回调 → 必须重挂,否则永久静默; + * · NE/FE/PE 是**非阻塞型**:HAL 不动接收,RxState 仍是 BUSY_RX → 此时再调 + * ReceiveToIdle_IT 只会拿到 HAL_BUSY,白记一次"重挂失败"。 + * 2026-10-29 台架实测踩的正是这一条:每帧都伴随 NE/FE,于是 rearm_fail 跟着 + * err_irq 一起涨(160/160),看着像链路坏了,其实一次都没真失败。 + * 判据用 RxState 而不是 ErrorCode 的位:它直接回答"现在挂着接收没有", + * 不依赖 HAL 怎么分组错误位。 + */ +void sb_link_on_error(UART_HandleTypeDef *p_huart) +{ + if ((s_huart == NULL) || (p_huart != s_huart)) + { + return; + } + + s_stats.err_irq++; + s_stats.err_bits |= p_huart->ErrorCode; + + /* STM32F4 通过读 SR + DR 清除 ORE/NE/FE/PE(与 rs485.c 的错误回调同一手法) */ + (void)p_huart->Instance->SR; + (void)p_huart->Instance->DR; + + /* 只有"接收已经停了"才需要重挂(见上面第 2 条) */ + if (p_huart->RxState == HAL_UART_STATE_READY) + { + (void)sb_link_receive_start(); + } +} + +/* + * 函数功能:链路统计上报(任务侧周期调用) + * 入口参数:now_ms - 当前毫秒时基 + * 返回值:无 + * 限定条件:任务上下文调用(内部会打日志) + * 函数说明:每 SB_LINK_REPORT_MS 检查一次,计数有变化才打一行;第一帧成功收到时 + * 立刻单独打一行(不等周期),台架刚上电就能确认"链路通了、长度对、CRC 过"。 + */ +void sb_link_poll(uint32_t now_ms) +{ + sb_link_stats_t st; + + if (s_huart == NULL) + { + return; + } + + sb_link_get_stats(&st); + + /* 首帧单独报一次:台架刚上电时不必等一个上报周期才知道链路通没通 */ + if ((s_first_frame_logged == 0U) && (st.rx_ok > 0U)) + { + s_first_frame_logged = 1U; + DBG_INFO("first frame: size=%u cmd=%u key=%u", + (unsigned)st.last_size, (unsigned)st.last_cmd, (unsigned)st.last_key); + } + + if ((uint32_t)(now_ms - s_report_ms) < SB_LINK_REPORT_MS) + { + return; + } + s_report_ms = now_ms; + + /* 无变化不刷屏:链路安静时一行都不出,出的一行就是有事实发生 */ + if ((st.rx_events == s_report_events) && + (st.err_irq == s_report_err_irq) && + (st.rearm_fail == s_report_rearm_fail)) + { + return; + } + s_report_events = st.rx_events; + s_report_err_irq = st.err_irq; + s_report_rearm_fail = st.rearm_fail; + + DBG_INFO("rx=%lu ok=%lu keys=%lu crc=%lu len=%lu fmt=%lu err=%lu bits=0x%02lX rearm=%lu size=%u key=%u", + (unsigned long)st.rx_events, (unsigned long)st.rx_ok, + (unsigned long)st.key_events, + (unsigned long)st.rx_crc_err, (unsigned long)st.rx_len_err, + (unsigned long)st.rx_fmt_err, (unsigned long)st.err_irq, + (unsigned long)st.err_bits, (unsigned long)st.rearm_fail, + (unsigned)st.last_size, (unsigned)st.last_key); +} + +/* + * 函数功能:取一份链路统计快照 + * 入口参数:p_out - 输出缓冲(非空) + * 返回值:无 + * 限定条件:任一上下文可调;不保证同一时刻一致(中断侧仍在自增) + * 函数说明:逐字段拷出。内部结构是 volatile,整体赋值会把限定词丢掉(ARMCC 报错), + * 故显式逐字段写。 + */ +void sb_link_get_stats(sb_link_stats_t *p_out) +{ + if (p_out == NULL) + { + return; + } + + p_out->rx_events = s_stats.rx_events; + p_out->rx_ok = s_stats.rx_ok; + p_out->rx_crc_err = s_stats.rx_crc_err; + p_out->rx_len_err = s_stats.rx_len_err; + p_out->rx_fmt_err = s_stats.rx_fmt_err; + p_out->key_events = s_stats.key_events; + p_out->err_irq = s_stats.err_irq; + p_out->err_bits = s_stats.err_bits; + p_out->rearm_fail = s_stats.rearm_fail; + p_out->last_size = s_stats.last_size; + p_out->last_cmd = s_stats.last_cmd; + p_out->last_key = s_stats.last_key; +} diff --git a/Drivers/BSP/ScreenLink/sb_link.h b/Drivers/BSP/ScreenLink/sb_link.h new file mode 100644 index 00000000..639a218a --- /dev/null +++ b/Drivers/BSP/ScreenLink/sb_link.h @@ -0,0 +1,183 @@ +/* + * 模块名称:屏板链路传输层(USART1 中断接收) + * 模块功能:把屏板(DTU-LCD)发来的 6 B 帧 A 从 USART1 上稳定收下来: + * IDLE 空闲中断断帧、帧 A 校验、错误中断里清 ORE 并重挂接收、统计上报。 + * **本层不发送、不组帧**(帧 B 的组帧与 TX DMA 起发属批 2,见 + * docs/任务规划/屏板链路.md §3 的 2-1 / 2-2)。 + * 适用平台:STM32F4 系列(依赖 HAL UART 的 ReceiveToIdle_IT) + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建初始版本(批 1 的 1-2 / 1-3:700 kbaud + 收中断) + * + * 为什么单独一层、不并进 sb_proto: + * sb_proto 是纯逻辑(CRC / 组帧 / 解析),能在 test/host 里原样 include 跑单测; + * 本文件依赖 HAL 句柄与中断上下文,属 MCU 专用。两者分开,前者才留得住"零桩单测"。 + * + * 全局回调的落点(改之前先读): + * HAL 的 HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback 是**全局单例**, + * 本工程唯一实现处是 Drivers/BSP/RS485/rs485.c(USE_HAL_UART_REGISTER_CALLBACKS = 0, + * 没法按句柄注册回调)。那里只做一行转发,**按句柄过滤在本文件里做**—— + * 于是 rs485.c 不必知道屏板链路用的是哪个 huart,新增通道也不用改它的判据。 + */ + +#ifndef SB_LINK_H +#define SB_LINK_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "main.h" /* UART_HandleTypeDef / USART_TypeDef / HAL_StatusTypeDef */ +#include "bsp_err.h" /* bsp_err_t / BSP_OK / BSP_ERR_*:对外只报统一错误码 */ +#include "sb_proto.h" /* SB_FRAME_A_LEN / sb_frame_a_t / sb_frame_a_parse */ + +/* ============================================================================ + * 一、链路参数 + * ==========================================================================*/ + +/* + * 串口参数:700000 8N1、16 倍过采样。 + * 实际写入发生在 Src/usart.c 的 `USER CODE BEGIN USART1_Init 2`(CubeMX 的 .ioc + * **不保存任何 UART 的波特率**,GUI 里改也不落盘,所以只能在 USER CODE 区覆盖)。 + * 理论值:APB2 84 MHz / (16 x 700 kHz) = 7.5 → BRR = 0x0078,误差 0 %。 + */ +#define SB_LINK_BAUD (700000U) + +/* + * 接收缓冲:16 B 足够。 + * 屏板只发 6 B 的帧 A(周期召唤 / 按键召唤),16 B 留出一次噪声冲击的余量。 + * ⚠ 不要照抄屏板侧的 4208 B——那是帧 B 的长度,方向相反(主机发、屏板收), + * 放进接收侧白占 SRAM(判据见 docs/任务规划/屏板链路.md §1.3)。 + */ +#define SB_LINK_RX_BUF_SIZE (16U) + +/* + * 统计上报周期(ms)。只在计数有变化时才打一行,链路空闲不刷屏; + * 台架验收靠的就是这一行里的累计值(判据见 docs/任务规划/屏板链路.md §3 的 1-3)。 + */ +#define SB_LINK_REPORT_MS (5000U) + +/* ============================================================================ + * 二、链路统计 + * ==========================================================================*/ + +/* + * 全部字段都是**累计值**,由中断侧自增、任务侧只读(读出来的是一份快照)。 + * 分门别类记而不只记"错了多少次",是因为台架上这几类错指向完全不同的原因: + * · rx_crc_err —— 波特率/接线问题(帧收到了但字节错位),最常见的真故障; + * · rx_len_err —— 断帧判据或噪声冲击(收到的不是 6 B),也可能是一次噪声把两帧粘在一起; + * · rx_fmt_err —— 地址/功能码/命令字不符(帧完整但内容不是给本机的); + * · err_irq —— ORE/NE/FE/PE 触发过错误中断,即线上真的有噪声或溢出。 + */ +typedef struct +{ + uint32_t rx_events; /* IDLE 事件总数(每来一包 +1,含错帧) */ + uint32_t rx_ok; /* 帧 A 校验通过(含 cmd = 按键帧) */ + uint32_t rx_crc_err; /* CRC 不符 */ + uint32_t rx_len_err; /* 长度不是 SB_FRAME_A_LEN */ + uint32_t rx_fmt_err; /* 地址/功能码/命令字不符 */ + uint32_t key_events; /* 其中 cmd == SB_CMD_KEY 的帧数(按键召唤,2-3 的消费源) */ + uint32_t err_irq; /* 错误中断次数(ORE/NE/FE/PE) */ + uint32_t err_bits; /* 错误中断里见过哪些位(HAL_UART_ERROR_* 的并集,见下) */ + uint32_t rearm_fail; /* 重挂接收失败次数(正常应恒 0,非 0 说明链路已静默) */ + uint16_t last_size; /* 最近一次 IDLE 事件的字节数 */ + uint8_t last_cmd; /* 最近一帧有效帧的命令类型(SB_CMD_*) */ + uint8_t last_key; /* 最近一帧有效帧的键值(无键为 0) */ +} sb_link_stats_t; + +/* + * err_bits 的位定义 = HAL 的 HAL_UART_ERROR_* 原值(PE 0x01 / NE 0x02 / FE 0x04 / ORE 0x08)。 + * 直接照抄 HAL 的值而不是自己另编一套:这一列的唯一用途是**判断物理层哪里不对**, + * 与 HAL 手册对照着看最省事。经验判读: + * · 只有 ORE(0x08) → 处理不过来(本链路 6 B/280 ms 不该出现,出现了就是重挂漏了); + * · NE(0x02)为主 → 线上有毛刺(线太长、无屏蔽、共地不良); + * · FE(0x04)为主 → 波特率对不上(对端不是 700000,或分频误差过大); + * · 全是 NE/FE 但 rx_ok 照样涨 → 数据没坏、只是采样点被判噪声,属"能用的告警",别当故障。 + */ + +/* ============================================================================ + * 三、公开接口 + * ==========================================================================*/ + +/* + * 函数功能:初始化屏板链路并挂上第一轮接收 + * 入口参数:p_huart - 已由 CubeMX 初始化完成的 USART1 句柄(非空) + * 返回值:BSP_OK 已挂上接收,后续每包都会进 sb_link_on_rx_event + * BSP_ERR_PARAM 句柄为空 + * BSP_ERR_STATE 句柄的波特率不是 SB_LINK_BAUD(Src/usart.c 的 USER CODE 覆盖丢了) + * BSP_ERR_BUSY 首次挂接收失败(HAL 侧忙,通常是上一轮没收完) + * BSP_ERR_IO 首次挂接收失败(HAL 侧状态错) + * 限定条件:须在 osKernelStart 之前或任一任务上下文调用;本函数不清任何外设。 + * 函数说明:波特率**不由本函数设置**——那一步在 Src/usart.c 的 USER CODE 区 + * (CubeMX 重生成只保护那里)。这里只做一次交叉核对:波特率对不上就报 + * BSP_ERR_STATE 并打一行 ERR,免得"115200 收 700 kbaud"这种静默错配 + * 被误判成接线或协议问题。 + */ +bsp_err_t sb_link_init(UART_HandleTypeDef *p_huart); + +/* + * 函数功能:重新挂上一轮 IDLE 中断接收 + * 入口参数:无 + * 返回值:BSP_OK 挂上了 + * BSP_ERR_STATE 尚未 sb_link_init + * BSP_ERR_BUSY HAL 侧忙(上一轮接收未结束) + * BSP_ERR_IO HAL 侧状态错 + * 限定条件:可在中断上下文调用(rx / error 两个回调都靠它续命);不可阻塞。 + * 函数说明:HAL 的 ReceiveToIdle_IT 是**一次性**的:一次 IDLE 事件处理完就把 + * RxState 打回 READY 并关掉 RXNEIE/IDLEIE。所以每个回调末尾都必须重挂, + * 漏一次链路就永久静默(表象与"屏板没上电"一模一样)。 + */ +bsp_err_t sb_link_receive_start(void); + +/* + * 函数功能:IDLE 事件入口(由 HAL_UARTEx_RxEventCallback 转发进来) + * 入口参数:p_huart - 触发事件的句柄(非本链路的会被直接忽略) + * size - 本次断出来的字节数 + * 返回值:无 + * 限定条件:**中断上下文**,只做 CRC 与计数(4 B 校验约 1 µs),不得打日志、不得阻塞。 + * 函数说明:长度不是 6 就直接记 rx_len_err 并丢弃;是 6 才解析。解析结论只累计计数 + * 与"最近一帧",**不做任何业务动作**——注键、刷新都由任务侧取走统计后再做 + * (批 2 的 2-3:注键不能在中断里做)。末尾无条件重挂接收。 + */ +void sb_link_on_rx_event(UART_HandleTypeDef *p_huart, uint16_t size); + +/* + * 函数功能:错误事件入口(由 HAL_UART_ErrorCallback 转发进来) + * 入口参数:p_huart - 触发事件的句柄(非本链路的会被直接忽略) + * 返回值:无 + * 限定条件:**中断上下文**;不得打日志、不得阻塞。 + * 函数说明:先读 SR 再读 DR 清掉 ORE/NE/FE/PE(RM0090 §30.6.1 的清除序列),再重挂接收。 + * ⚠ 这一步不是可选的:HAL 在阻塞型错误里已经把 RxState 打回 READY 并关了 + * 接收中断,**而它自己不清 ORE**。ORE 一旦锁存,接收就再也进不来 → + * 一次噪声就把链路永久打死,而现象与"屏板没上电"完全一样。 + */ +void sb_link_on_error(UART_HandleTypeDef *p_huart); + +/* + * 函数功能:链路统计上报(任务侧周期调用) + * 入口参数:now_ms - 当前毫秒时基(调用方给,便于主机单测;MCU 侧传 HAL_GetTick()) + * 返回值:无 + * 限定条件:任务上下文调用,**不可在中断里调**(内部会打日志)。 + * 函数说明:每 SB_LINK_REPORT_MS 检查一次,只有计数有变化才打一行累计值。 + * 第一帧成功收到时立刻打一行(不等周期),台架刚上电就能看到链路通了。 + * 离线判定(连续 >=10 s 无召唤帧)属批 2 的 2-5,这里只上报不判定。 + */ +void sb_link_poll(uint32_t now_ms); + +/* + * 函数功能:取一份链路统计快照 + * 入口参数:p_out - 输出缓冲(非空) + * 返回值:无(p_out 为空时直接返回,不改任何东西) + * 限定条件:任一上下文可调;中断侧在自增,快照不保证同一时刻一致(只用于观测与计数)。 + * 函数说明:逐字段拷出,避免把 volatile 的内部结构整体赋给调用方。 + */ +void sb_link_get_stats(sb_link_stats_t *p_out); + +#ifdef __cplusplus +} +#endif + +#endif /* SB_LINK_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 3c0a6c72..e1f92f36 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1628,6 +1628,11 @@ 1 ..\Drivers\BSP\ScreenLink\sb_proto.c
+ + sb_link.c + 1 + ..\Drivers\BSP\ScreenLink\sb_link.c + net_select.c 1 diff --git a/Src/main.c b/Src/main.c index ad56045b..bf5f8a40 100644 --- a/Src/main.c +++ b/Src/main.c @@ -217,6 +217,8 @@ void SystemClock_Config(void) * RS-485 的 HAL UART 回调(HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback) * 已收归 rs485.c 实现,本文件不再定义它们。如需处理接收到的 RS-485 数据, * 请重定义 rs485.h 中的弱钩子 rs485_on_rx(uint8_t *buf, uint16_t size)。 + * 屏板链路(USART1)在同一个回调里转发给 sb_link_on_rx_event / sb_link_on_error—— + * 那两个回调是全局单例,全工程只能有一份定义,**不要在本文件再写一份**。 */ /* USER CODE END 4 */ diff --git a/Src/usart.c b/Src/usart.c index c7be51da..16176078 100644 --- a/Src/usart.c +++ b/Src/usart.c @@ -21,7 +21,7 @@ #include "usart.h" /* USER CODE BEGIN 0 */ - +#include "sb_link.h" /* SB_LINK_BAUD:USART1 的屏板链路波特率(见 USART1_Init 2 区) */ /* USER CODE END 0 */ UART_HandleTypeDef huart4; @@ -113,7 +113,24 @@ void MX_USART1_UART_Init(void) Error_Handler(); } /* USER CODE BEGIN USART1_Init 2 */ - + /* 屏板链路:USART1 固定 700000 8N1、16 倍过采样(其余字段生成值即 8N1/16 倍,不用改)。 + * + * 为什么写在这里而不是上面生成区:那几行 `huart1.Init.*` 在 USER CODE 之外, + * CubeMX 重生成会把 BaudRate 打回 115200,且**不会报任何编译错**。 + * 为什么不能改 .ioc 解决:.ioc **不保存任何 UART 的波特率** + * (只有 `USART1.IPParameters=VirtualMode`),GUI 里改波特率也不落盘 —— 这条与 + * IWDG/看门狗那些"写进 .ioc 才不怕重生成"的情形不同,没有 .ioc 这条路。 + * + * 重新 HAL_UART_Init 是安全的:第二次调用时 gState 已非 RESET,HAL 只重算 BRR、 + * 不再跑 HAL_UART_MspInit(GPIO / DMA / NVIC 不会被重配,见 + * stm32f4xx_hal_uart.c 的 HAL_UART_Init 开头那段 if (gState == RESET))。 + * 理论值:APB2 = 84 MHz(APB2CLKDivider = HCLK/2),84 000 000 / (16 x 700 000) = 7.5 + * → 整数部分 7、小数部分 0.5x16 = 8 → BRR = 0x0078,误差 0 %。 */ + huart1.Init.BaudRate = SB_LINK_BAUD; + if (HAL_UART_Init(&huart1) != HAL_OK) + { + Error_Handler(); + } /* USER CODE END USART1_Init 2 */ } diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 0e987c7a..442aad63 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 264,584 B (258.38 KB) | 1,048,576 B (1024 KB) | 25.2% | -| RW_IRAM1 | 81,536 B (79.62 KB) | 131,072 B (128 KB) | 62.2% | +| ER_IROM1 | 266,360 B (260.12 KB) | 1,048,576 B (1024 KB) | 25.4% | +| RW_IRAM1 | 81,616 B (79.70 KB) | 131,072 B (128 KB) | 62.3% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **266,460 B (260.2 KB)** | 1,048,576 B (1024 KB) | **25.4%** | +| **Flash (ROM 合计)** | **268,260 B (262.0 KB)** | 1,048,576 B (1024 KB) | **25.6%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index e7d38b57..67cc6748 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,10 +15,10 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 264,584 B | 1,048,576 B | 25.2% | 参考 | +| ER_IROM1 (Flash) | 266,360 B | 1,048,576 B | 25.4% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 81,536 B | 131,072 B | 62.2% | PASS | +| RW_IRAM1 | 81,616 B | 131,072 B | 62.3% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| @@ -27,7 +27,7 @@ | UI | 34,656 B | 16 KB | ⚠ 超 17.8 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | | 应用 | 15,552 B | 8 KB | ⚠ 超 7.2 KB | -| 其它 | 10,553 B | — | 参考 | +| 其它 | 10,633 B | — | 参考 | - **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)—— diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 6460f15e..222149dd 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -136,6 +136,7 @@ SIM_EXEMPT = [ ("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"), ("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"), ("Drivers/BSP/ScreenLink/sb_proto.c", "屏板链路协议层纯逻辑(帧 A 解析 / 帧 B 组帧 / CRC-16),消费者 = MCU 侧 USART1 收发起发(批 1/2),模拟器无消费路径;主机侧纯逻辑单测覆盖于 test/host/sb_proto"), + ("Drivers/BSP/ScreenLink/sb_link.c", "屏板链路传输层(USART1 IDLE 中断收 + ORE 清除 + 统计),依赖 HAL 句柄与中断上下文,MCU 专属;主机侧单测以桩覆盖于 test/host/sb_link"), ("Drivers/BSP/TPAFE5160/tpafe5160.c", "MCU 计量芯片驱动"), ("Drivers/BSP/UI/ui_port.c", "MCU UI 端口层(显示/触摸硬件),模拟器另有 sim/sim_port.c"), ] diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 18056dc7..82e2b623 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -11,4 +11,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736 2026-10-27 235372 26392 1572 136924 261764 25.0 81072 61.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 2026-10-28 236456 28124 1876 137084 264580 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 -2026-10-29 236460 28124 1876 137084 264584 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 +2026-10-29 237464 28896 1900 137140 266360 25.4 81616 62.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 336aecb5..0a071053 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,25 +1,25 @@ { - "built_at": "2026-10-29T19:17:06+08:00", - "fingerprint": "df1c01abda2bd6778c11976f346e1a9024386ae2809f94c720da9f95ef59becc", - "fingerprint_files": 1454, + "built_at": "2026-10-29T20:05:10+08:00", + "fingerprint": "aff51072f981be06701568ea469dd4baa0278299ac530446feb299ac0cba8abd", + "fingerprint_files": 1456, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "71b6a5fa14db85ed9c95bba5b3b6106d1f20d223", + "git_head": "2c69a6ca076d201a2f412318bdfaa91f20632c48", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793272621, + "map_mtime": 1793275502, "pct": { - "ER_IROM1": 25.23, + "ER_IROM1": 25.4, "RW_CCM": 87.89, - "RW_IRAM1": 62.21, + "RW_IRAM1": 62.27, "UNINIT_CCM": 18.75 }, "regions": { "ER_IROM1": { "max": 1048576, - "size": 264584 + "size": 266360 }, "RW_CCM": { "max": 65280, @@ -27,7 +27,7 @@ }, "RW_IRAM1": { "max": 131072, - "size": 81536 + "size": 81616 }, "UNINIT_CCM": { "max": 256, @@ -37,10 +37,10 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 236460, - "ro_data": 28124, - "rw_data": 1876, - "zi_data": 137084 + "code": 237464, + "ro_data": 28896, + "rw_data": 1900, + "zi_data": 137140 }, "threshold_pct": 92.0, "worktree_dirt_at_stamp": 0, diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index 799f1b45..ee0d4c91 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -344,6 +344,14 @@ "lines_covered": 108, "lines_valid": 112 }, + "Drivers/BSP/ScreenLink/sb_link.c": { + "func_rate": 1.0, + "funcs_covered": 7, + "funcs_valid": 7, + "line_rate": 0.9896, + "lines_covered": 95, + "lines_valid": 96 + }, "Drivers/BSP/ScreenLink/sb_proto.c": { "func_rate": 1.0, "funcs_covered": 6, @@ -615,6 +623,14 @@ "lines_total": 961, "ok": true }, + "sb_link": { + "files": { + "Drivers/BSP/ScreenLink/sb_link.c": 96, + "Drivers/BSP/ScreenLink/sb_proto.c": 74 + }, + "lines_total": 170, + "ok": true + }, "sb_proto": { "files": { "Drivers/BSP/ScreenLink/sb_proto.c": 74 @@ -756,6 +772,7 @@ "Drivers/BSP/NET/net_socket.c", "Drivers/BSP/NETCFG/ftp_cfg.c", "Drivers/BSP/NETCFG/net_cfg.c", + "Drivers/BSP/ScreenLink/sb_link.c", "Drivers/BSP/ScreenLink/sb_proto.c", "Drivers/BSP/UI/ui_data.c", "Drivers/BSP/UI/ui_net_sink.c", @@ -763,16 +780,16 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 47, - "files_untested": 164, - "func_rate": 0.9324, - "funcs_covered": 538, - "funcs_valid": 577, - "line_rate": 0.8993, - "lines_covered": 7132, - "lines_valid": 7931, - "modules_ok": 42, - "modules_total": 42 + "files_tested": 48, + "files_untested": 165, + "func_rate": 0.9332, + "funcs_covered": 545, + "funcs_valid": 584, + "line_rate": 0.9003, + "lines_covered": 7227, + "lines_valid": 8027, + "modules_ok": 43, + "modules_total": 43 }, "untested_sources": [ { @@ -821,11 +838,11 @@ }, { "code_lines": 215, - "path": "Drivers/BSP/UI/ui_w_setlist.c" + "path": "App/task/ui_task.c" }, { - "code_lines": 214, - "path": "App/task/ui_task.c" + "code_lines": 215, + "path": "Drivers/BSP/UI/ui_w_setlist.c" }, { "code_lines": 206, @@ -864,7 +881,7 @@ "path": "Drivers/BSP/UI/ui_page_device_info.c" }, { - "code_lines": 135, + "code_lines": 142, "path": "App/app/app_main.c" }, { @@ -916,7 +933,7 @@ "path": "Drivers/BSP/NET/net_socket.h" }, { - "code_lines": 79, + "code_lines": 81, "path": "Drivers/BSP/RS485/rs485.c" }, { @@ -1043,6 +1060,10 @@ "code_lines": 33, "path": "Drivers/BSP/NET/net_select.h" }, + { + "code_lines": 33, + "path": "Drivers/BSP/ScreenLink/sb_link.h" + }, { "code_lines": 33, "path": "Drivers/BSP/UI/ui_page_action.h" diff --git a/test/host/sb_link/run_unit_test.py b/test/host/sb_link/run_unit_test.py new file mode 100644 index 00000000..897ce192 --- /dev/null +++ b/test/host/sb_link/run_unit_test.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""屏板链路传输层(sb_link)的主机侧单元测试运行器(纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/sb_link/sb_link_test.c(它以 #include 纳入**生产源文件** + Drivers/BSP/ScreenLink/sb_link.c 与 sb_proto.c),在 PC 上离线验证 USART1 接收这一层: + 挂接收 / IDLE 事件分派 / 坏帧分类 / **错误中断里清 ORE 并重挂** / 重挂失败的码映射 / + 周期统计上报(含时基回绕)。零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么必须主机侧测(而不是只用台架): + 这一层最要命的两条在台架上都"看不见": + ① **漏一次重挂 = 链路永久静默**,现象与"屏板没上电 / 线接反"完全同形,没有报错; + ② **ORE 没清干净 = 一次噪声永久打死接收**,台架上要拔插 TX 线才复现,且复现后 + 只能靠断电重来。主机侧用桩把 RM0090 的清除语义(先读 SR 再读 DR)实现出来, + 于是"顺序写反"会直接让下一次挂接收失败、用例立刻红。 + +桩目录(`stub/`,必须排在 include 路径最前,覆盖真实头): + main.h 假 USART 寄存器(读序列可记账、ORE 可注入)+ 假 ReceiveToIdle_IT(返回值可脚本) + dbg_log.h 日志换成行缓冲,于是能断言"打了几行、说了什么" + +用法(仓库根目录): + python test/host/sb_link/run_unit_test.py + 或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/sb_link/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:docs/任务规划/屏板链路.md §3 批 1(1-3 的判据)、§1.3(接收只要 16 B); + docs/产品需求/产品需求设计文档.md §5.16.2(帧 A 结构)。 +创建日期:2026-10-29 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 桩目录必须排在最前:它覆盖 main.h(真实那份会拉进 core_cm4.h)与 dbg_log.h(会拉进 App 层) +BUILD = TestBuild( + name="ScreenLink Link Tests", + src=os.path.join(HERE, "sb_link_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "Drivers", "BSP", "ScreenLink"), + os.path.join(ROOT, "Drivers", "BSP"), + HERE, + ], + summary_marker="=== ScreenLink Link Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/sb_link/sb_link_test.c b/test/host/sb_link/sb_link_test.c new file mode 100644 index 00000000..192eb199 --- /dev/null +++ b/test/host/sb_link/sb_link_test.c @@ -0,0 +1,482 @@ +/* + * 模块名称:屏板链路传输层(sb_link)主机侧单元测试 + * 模块功能:在 PC 上对 Drivers/BSP/ScreenLink/sb_link.c 做离线验证。 + * + * 被测对象:真实源文件(以 #include 原样纳入,保证测的是生产代码而非副本): + * ../../../Drivers/BSP/ScreenLink/sb_proto.c (sb_link 依赖它的帧 A 解析) + * ../../../Drivers/BSP/ScreenLink/sb_link.c + * 依赖面用桩替换:main.h(假 USART 寄存器 + 假 ReceiveToIdle_IT)、dbg_log.h(日志行缓冲)。 + * + * 为什么要主机侧单测(而不是只用台架): + * 1. **ORE 清除序列**在台架上只能靠"拔插 TX 线"制造,且失败现象是"链路永久静默"—— + * 与屏板没上电、线接反完全同形。主机侧能把"必须按 SR→DR 顺序读"这条钉成断言: + * 桩按 RM0090 的语义实现(先读 SR 再读 DR 才清得掉 ORE),顺序反了清不掉 → + * 下一次挂接收必然失败 → 用例立刻红。 + * 2. **重挂接收漏一次 = 链路永久静默**:台架上要等 280 ms 才看得出来,且没有报错。 + * 主机侧数 `sb_arm_calls` 即可。 + * 3. 坏帧分支(长度不对 / 地址不对 / 功能码不对 / CRC 错)在台架上要造噪声才出现, + * 一出现就被复位冲掉;主机侧是几行断言。 + * + * 覆盖点(对应 docs/任务规划/屏板链路.md §3 批 1 的 1-3 判据): + * TC-SBLINK-001 init 参数守卫:空句柄 / 波特率不是 SB_LINK_BAUD(后者必须不挂接收) + * TC-SBLINK-002 init 正常:挂接收一次、缓冲 = SB_LINK_RX_BUF_SIZE(16 B,不是 4208) + * TC-SBLINK-003 未初始化就重挂接收:BSP_ERR_STATE,且不碰外设 + * TC-SBLINK-004 非本链路的句柄:rx / error 两个入口都必须直接忽略 + * TC-SBLINK-005 周期召唤帧(屏板实测向量 01 03 01 00 F0 48):计数、cmd、key、重挂 + * TC-SBLINK-006 按键召唤帧(屏板实测向量 01 03 00 09 31 DE):key = 9 + * TC-SBLINK-007 CRC 错帧:只累加 rx_crc_err,不污染 last_cmd / last_key + * TC-SBLINK-008 长度不对:5 B / 16 B(缓冲满)/ 0 B 都记 rx_len_err + * TC-SBLINK-009 地址 / 功能码 / 命令类型不符:记 rx_fmt_err + * TC-SBLINK-010 错误中断(ORE,阻塞型):先读 SR 再读 DR 才清得掉,且必须重挂 + * TC-SBLINK-015 错误中断(NE/FE,非阻塞型):接收还在跑,**不得**重挂(否则白记一次失败) + * TC-SBLINK-011 重挂失败:HAL_BUSY / HAL_TIMEOUT / HAL_ERROR 三个映射 + rearm_fail 累计 + * TC-SBLINK-012 统计上报:未到周期不出行;有变化才出;无变化不刷屏;首帧立刻单独报一次 + * TC-SBLINK-013 统计上报的时基回绕:now_ms 跨 0xFFFFFFFF 仍按差值法工作 + * TC-SBLINK-014 统计快照:空指针守卫、字段逐项对得上 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-29 + */ + +#include +#include +#include + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +/* ===================== 被测代码 ===================== */ +#include "sb_proto.c" +#include "sb_link.c" + +/* ===================== 夹具 ===================== */ + +/* 本链路的句柄(地址唯一,用来区分"非本链路的句柄") */ +static UART_HandleTypeDef s_uart; + +/* 另一个句柄:只当参数传进去,验证过滤 */ +static UART_HandleTypeDef s_other; + +/* 屏板实测向量(出处:屏板固件工程 DTU-LCD 的实机抓包,同 test/host/sb_proto) */ +static const uint8_t k_rt[SB_FRAME_A_LEN] = { 0x01U, 0x03U, 0x01U, 0x00U, 0xF0U, 0x48U }; +static const uint8_t k_key[SB_FRAME_A_LEN] = { 0x01U, 0x03U, 0x00U, 0x09U, 0x31U, 0xDEU }; + +/* 复位桩 + 日志,然后以 700000 正常初始化一次;返回 init 的返回码 */ +static bsp_err_t setup_link(void) +{ + sb_fake_reset(); + sb_log_reset(); + + s_uart.Instance = &g_sb_uart; + s_uart.Init.BaudRate = SB_LINK_BAUD; + s_uart.Init.WordLength = 8U; + s_uart.Init.StopBits = 1U; + s_uart.Init.Parity = 0U; + s_uart.Init.Mode = 3U; + s_uart.Init.OverSampling = 16U; + s_uart.RxState = HAL_UART_STATE_READY; + s_uart.ErrorCode = 0U; + + s_other.Instance = &g_sb_uart; + s_other.Init.BaudRate = SB_LINK_BAUD; + + return sb_link_init(&s_uart); +} + +/* 模拟"HAL 把 n 个字节搬进它拿到的那块缓冲,然后报一次 IDLE 事件" */ +static void feed(const uint8_t *p_data, uint16_t len) +{ + if ((sb_arm_buf != NULL) && (len > 0U)) { + (void)memcpy(sb_arm_buf, p_data, len); + } + sb_link_on_rx_event(&s_uart, len); +} + +/* 按 4 字节负载组一帧(CRC 低字节在前,与 sb_proto 口径一致),用来造反例帧 */ +static void make_frame(uint8_t *p_frame, uint8_t addr, uint8_t func, uint8_t cmd, uint8_t key) +{ + uint16_t crc; + + p_frame[0] = addr; + p_frame[1] = func; + p_frame[2] = cmd; + p_frame[3] = key; + crc = sb_crc16(p_frame, 4U); + p_frame[4] = (uint8_t)(crc & 0xFFU); + p_frame[5] = (uint8_t)((crc >> 8) & 0xFFU); +} + +/* ===================== 用例 ===================== */ + +static void tc_init_guards(void) +{ + bsp_err_t ret; + + sb_fake_reset(); + sb_log_reset(); + + ret = sb_link_init(NULL); + CHECK(ret == BSP_ERR_PARAM, "TC-SBLINK-001 a NULL handle is rejected"); + CHECK(sb_arm_calls == 0U, "TC-SBLINK-001b nothing is armed for a NULL handle"); + + /* 波特率不是 700000:说明 Src/usart.c 的 USER CODE 覆盖丢了。 + * 必须**不挂接收**并打一行 ERR —— 否则就是"115200 收 700 kbaud"的静默错配 */ + s_uart.Instance = &g_sb_uart; + s_uart.Init.BaudRate = 115200U; + ret = sb_link_init(&s_uart); + CHECK(ret == BSP_ERR_STATE, "TC-SBLINK-001c a wrong baud rate is rejected (BSP_ERR_STATE)"); + CHECK(sb_arm_calls == 0U, "TC-SBLINK-001d nothing is armed when the baud rate is wrong"); + CHECK(sb_log_n == 1U, "TC-SBLINK-001e exactly one ERR line is emitted"); + CHECK(sb_log_has(0U, "baud=115200") == 1, "TC-SBLINK-001f the ERR line names the actual baud rate"); +} + +static void tc_init_ok(void) +{ + bsp_err_t ret = setup_link(); + + CHECK(ret == BSP_OK, "TC-SBLINK-002 init with 700000 returns BSP_OK"); + CHECK(sb_arm_calls == 1U, "TC-SBLINK-002b reception is armed exactly once"); + CHECK(sb_arm_size == SB_LINK_RX_BUF_SIZE, "TC-SBLINK-002c the receive buffer is SB_LINK_RX_BUF_SIZE"); + CHECK(sb_arm_size == 16U, "TC-SBLINK-002d ... which is 16 B, not the board's 4208 B frame length"); + CHECK(sb_arm_buf != NULL, "TC-SBLINK-002e a receive buffer is handed to HAL"); + CHECK(sb_log_n == 0U, "TC-SBLINK-002f a clean init logs nothing"); +} + +static void tc_not_initialised(void) +{ + /* 先把模块打回未初始化态:sb_link_init 之前 s_huart 为 NULL。 + * 本 TU 直接可见该静态变量(原样 include 生产源),这里显式还原一次。 */ + UART_HandleTypeDef *saved = s_huart; + + sb_fake_reset(); + sb_log_reset(); + s_huart = NULL; + + CHECK(sb_link_receive_start() == BSP_ERR_STATE, + "TC-SBLINK-003 re-arming before init returns BSP_ERR_STATE"); + CHECK(sb_arm_calls == 0U, "TC-SBLINK-003b ... and never touches the peripheral"); + + s_huart = saved; +} + +static void tc_foreign_handle(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + + sb_link_on_rx_event(&s_other, SB_FRAME_A_LEN); + sb_link_on_error(&s_other); + + sb_link_get_stats(&st); + CHECK(st.rx_events == 0U, "TC-SBLINK-004 an rx event on another handle is ignored"); + CHECK(st.err_irq == 0U, "TC-SBLINK-004b an error event on another handle is ignored"); + CHECK(sb_arm_calls == 1U, "TC-SBLINK-004c ... and does not re-arm (no extra arm call)"); + CHECK(sb_reg_n == 0U, "TC-SBLINK-004d ... and does not touch the registers"); +} + +static void tc_rt_frame(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + feed(k_rt, SB_FRAME_A_LEN); + + sb_link_get_stats(&st); + CHECK(st.rx_events == 1U, "TC-SBLINK-005 one IDLE event is counted"); + CHECK(st.rx_ok == 1U, "TC-SBLINK-005b the board's real RT summon frame parses OK"); + CHECK(st.last_size == SB_FRAME_A_LEN, "TC-SBLINK-005c last_size == 6"); + CHECK(st.last_cmd == SB_CMD_RT, "TC-SBLINK-005d last_cmd == SB_CMD_RT"); + CHECK(st.last_key == 0U, "TC-SBLINK-005e last_key == 0 for a periodic summon"); + CHECK(sb_arm_calls == 2U, "TC-SBLINK-005f reception is re-armed after the event"); + CHECK(st.rx_crc_err == 0U, "TC-SBLINK-005g no CRC error is counted"); + CHECK(st.key_events == 0U, "TC-SBLINK-005h a periodic summon is not a key event"); +} + +static void tc_key_frame(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + feed(k_key, SB_FRAME_A_LEN); + + sb_link_get_stats(&st); + CHECK(st.rx_ok == 1U, "TC-SBLINK-006 the board's real key summon frame parses OK"); + CHECK(st.last_cmd == SB_CMD_KEY, "TC-SBLINK-006b last_cmd == SB_CMD_KEY"); + CHECK(st.last_key == 9U, "TC-SBLINK-006c last_key == 9 (KEY_RESET on the board)"); + CHECK(st.key_events == 1U, "TC-SBLINK-006d the key event counter advances (2-3 consumes this)"); +} + +static void tc_crc_error(void) +{ + uint8_t bad[SB_FRAME_A_LEN]; + sb_link_stats_t st; + + (void)setup_link(); + feed(k_key, SB_FRAME_A_LEN); /* 先来一帧好的,把 last_* 写成 9 */ + (void)memcpy(bad, k_rt, SB_FRAME_A_LEN); + bad[SB_FRAME_A_LEN - 1U] ^= 0xFFU; /* 打坏 CRC */ + feed(bad, SB_FRAME_A_LEN); + + sb_link_get_stats(&st); + CHECK(st.rx_events == 2U, "TC-SBLINK-007 both events are counted"); + CHECK(st.rx_ok == 1U, "TC-SBLINK-007b only the good frame counts as OK"); + CHECK(st.rx_crc_err == 1U, "TC-SBLINK-007c the corrupted frame is counted as a CRC error"); + CHECK(st.last_key == 9U, "TC-SBLINK-007d a bad frame does not overwrite last_key"); + CHECK(sb_arm_calls == 3U, "TC-SBLINK-007e reception is re-armed even after a bad frame"); +} + +static void tc_length_error(void) +{ + uint8_t five[5] = { 0x01U, 0x03U, 0x01U, 0x00U, 0xF0U }; + uint8_t full[SB_LINK_RX_BUF_SIZE]; + sb_link_stats_t st; + + (void)setup_link(); + feed(five, 5U); /* 少一个字节 */ + (void)memset(full, 0x00, sizeof(full)); + feed(full, SB_LINK_RX_BUF_SIZE); /* 缓冲被塞满(噪声冲击的典型形态) */ + sb_link_on_rx_event(&s_uart, 0U); /* 空事件(防御性) */ + + sb_link_get_stats(&st); + CHECK(st.rx_events == 3U, "TC-SBLINK-008 every event is counted"); + CHECK(st.rx_len_err == 3U, "TC-SBLINK-008b 5 B / 16 B / 0 B all count as length errors"); + CHECK(st.rx_ok == 0U, "TC-SBLINK-008c none of them is treated as a valid frame"); + CHECK(st.rx_crc_err == 0U, "TC-SBLINK-008d a wrong length is not reported as a CRC error"); + CHECK(sb_arm_calls == 4U, "TC-SBLINK-008e reception is re-armed every time"); +} + +static void tc_format_error(void) +{ + uint8_t frame[SB_FRAME_A_LEN]; + sb_link_stats_t st; + + (void)setup_link(); + + make_frame(frame, 0x02U, SB_FUNC_READ_HOLDING, SB_CMD_RT, 0U); /* 地址不对 */ + feed(frame, SB_FRAME_A_LEN); + make_frame(frame, SB_ADDR, 0x04U, SB_CMD_RT, 0U); /* 功能码不对 */ + feed(frame, SB_FRAME_A_LEN); + make_frame(frame, SB_ADDR, SB_FUNC_READ_HOLDING, 0x02U, 0U); /* 命令类型不对 */ + feed(frame, SB_FRAME_A_LEN); + + sb_link_get_stats(&st); + CHECK(st.rx_fmt_err == 3U, "TC-SBLINK-009 wrong address / function / command all count as format errors"); + CHECK(st.rx_crc_err == 0U, "TC-SBLINK-009b ... and none of them is misreported as a CRC error"); + CHECK(st.rx_ok == 0U, "TC-SBLINK-009c none of them is accepted"); +} + +static void tc_error_clears_ore(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + + /* 造一次 ORE。ORE 是**阻塞型**错误:HAL 先把接收停掉(RxState 打回 READY)再回调, + * 所以桩也先把 RxState 打回 READY —— 否则测的就不是 ORE 那条路 */ + sb_ore_latched = 1U; + s_uart.RxState = HAL_UART_STATE_READY; + s_uart.ErrorCode = HAL_UART_ERROR_ORE; + sb_link_on_error(&s_uart); + + CHECK(sb_fake_trace_is("SD") == 1, "TC-SBLINK-010 the error path reads SR then DR (RM0090 clear sequence)"); + CHECK(sb_ore_latched == 0U, "TC-SBLINK-010b ORE is actually cleared"); + CHECK(sb_arm_calls == 2U, "TC-SBLINK-010c a blocking error (ORE) re-arms reception"); + CHECK(sb_arm_force == SB_FAKE_ARM_OK, "TC-SBLINK-010d (sanity) no forced failure is in play"); + + sb_link_get_stats(&st); + CHECK(st.err_irq == 1U, "TC-SBLINK-010e the error interrupt is counted"); + CHECK(st.err_bits == HAL_UART_ERROR_ORE, "TC-SBLINK-010f the error bits are recorded"); + CHECK(st.rearm_fail == 0U, "TC-SBLINK-010g re-arming succeeded, so rearm_fail stays 0"); + + /* 反例:如果哪天有人把清除序列写成 DR→SR,ORE 就清不掉,下一次挂接收必失败。 + * 这里直接模拟"ORE 没清掉"的后果,把判据钉住 */ + sb_ore_latched = 1U; + sb_sr_seen = 0U; + CHECK(sb_link_receive_start() == BSP_ERR_IO, + "TC-SBLINK-010h an uncleared ORE makes the next arm fail (this is why the clear matters)"); +} + +/* + * 非阻塞型错误(NE/FE/PE):HAL 不动接收(RxState 仍是 BUSY_RX),**不该**去重挂。 + * 2026-10-29 台架实测踩的就是这条:每帧都伴随 NE/FE,旧写法每次都想重挂、每次拿到 + * HAL_BUSY,于是 rearm_fail 跟着 err_irq 一起涨(160/160),看着像链路坏了。 + */ +static void tc_error_nonblocking(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + + s_uart.RxState = HAL_UART_STATE_BUSY_RX; /* 接收还在跑 */ + s_uart.ErrorCode = HAL_UART_ERROR_NE; + sb_link_on_error(&s_uart); + + CHECK(sb_arm_calls == 1U, "TC-SBLINK-015 a non-blocking error (NE) does not re-arm reception"); + CHECK(sb_fake_trace_is("SD") == 1, "TC-SBLINK-015b ... but still clears the flags (SR then DR)"); + + sb_link_get_stats(&st); + CHECK(st.err_irq == 1U, "TC-SBLINK-015c the error interrupt is counted"); + CHECK(st.err_bits == HAL_UART_ERROR_NE, "TC-SBLINK-015d the NE bit is recorded"); + CHECK(st.rearm_fail == 0U, "TC-SBLINK-015e ... and no phantom 're-arm failure' is booked"); + + /* 再来一次 FE:错误位按并集累计 */ + s_uart.ErrorCode = HAL_UART_ERROR_FE; + sb_link_on_error(&s_uart); + sb_link_get_stats(&st); + CHECK(st.err_bits == (HAL_UART_ERROR_NE | HAL_UART_ERROR_FE), + "TC-SBLINK-015f error bits accumulate as a union"); +} + +static void tc_rearm_failure(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + + sb_arm_force = SB_FAKE_ARM_BUSY; + CHECK(sb_link_receive_start() == BSP_ERR_BUSY, "TC-SBLINK-011 HAL_BUSY maps to BSP_ERR_BUSY"); + sb_arm_force = SB_FAKE_ARM_TIMEOUT; + CHECK(sb_link_receive_start() == BSP_ERR_TIMEOUT, "TC-SBLINK-011b HAL_TIMEOUT maps to BSP_ERR_TIMEOUT"); + sb_arm_force = SB_FAKE_ARM_ERROR; + CHECK(sb_link_receive_start() == BSP_ERR_IO, "TC-SBLINK-011c HAL_ERROR maps to BSP_ERR_IO"); + + sb_link_get_stats(&st); + CHECK(st.rearm_fail == 3U, "TC-SBLINK-011d every failed arm is counted in rearm_fail"); + + /* 中断里也要如实累计(不是只有任务侧那条路) */ + sb_arm_force = SB_FAKE_ARM_BUSY; + sb_link_on_rx_event(&s_uart, SB_FRAME_A_LEN); + sb_link_get_stats(&st); + CHECK(st.rearm_fail == 4U, "TC-SBLINK-011e a failed re-arm inside the rx callback is counted too"); +} + +static void tc_poll_reporting(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + + /* 未到周期:一行都不出(哪怕计数已经变了) */ + feed(k_rt, SB_FRAME_A_LEN); + sb_link_poll(0U); + CHECK(sb_log_n == 1U, "TC-SBLINK-012 the first valid frame is reported immediately"); + CHECK(sb_log_has(0U, "first frame") == 1, "TC-SBLINK-012b ... by a dedicated 'first frame' line"); + CHECK(sb_log_has(0U, "size=6") == 1, "TC-SBLINK-012c ... which carries size=6 (the 1-3 criterion)"); + + /* 到周期且计数有变化:出一行累计值 */ + feed(k_rt, SB_FRAME_A_LEN); + sb_link_poll(SB_LINK_REPORT_MS); + CHECK(sb_log_n == 2U, "TC-SBLINK-012d a periodic report is emitted once the period elapses"); + CHECK(sb_log_has(1U, "rx=2") == 1, "TC-SBLINK-012e the report carries the cumulative event count"); + CHECK(sb_log_has(1U, "ok=2") == 1, "TC-SBLINK-012f ... and the OK count"); + + /* 到周期但计数没变:不出行(链路安静时不刷屏) */ + sb_link_poll(2U * SB_LINK_REPORT_MS); + CHECK(sb_log_n == 2U, "TC-SBLINK-012g nothing is emitted when no counter changed"); + + /* 有错帧:再出一行,且 err/crc 列可见 */ + { + uint8_t bad[SB_FRAME_A_LEN]; + + (void)memcpy(bad, k_rt, SB_FRAME_A_LEN); + bad[5] ^= 0xFFU; + feed(bad, SB_FRAME_A_LEN); + } + sb_link_poll(3U * SB_LINK_REPORT_MS); + CHECK(sb_log_n == 3U, "TC-SBLINK-012h a CRC error triggers a new report"); + CHECK(sb_log_has(2U, "crc=1") == 1, "TC-SBLINK-012i the report carries the CRC error count"); + + sb_link_get_stats(&st); + CHECK(st.rx_events == 3U, "TC-SBLINK-012j (sanity) three events were fed in total"); +} + +static void tc_poll_wraparound(void) +{ + uint32_t base = 0xFFFFFFFFU - (SB_LINK_REPORT_MS / 2U); + + (void)setup_link(); + + /* 基准:距上次上报(0)已经远超一个周期 → 首帧行 + 周期行各一行 */ + feed(k_rt, SB_FRAME_A_LEN); + sb_link_poll(base); + CHECK(sb_log_n == 2U, "TC-SBLINK-013 the baseline poll emits both the first-frame line and a report"); + + feed(k_rt, SB_FRAME_A_LEN); + sb_link_poll(base + (SB_LINK_REPORT_MS / 2U)); /* 还没到周期 */ + CHECK(sb_log_n == 2U, "TC-SBLINK-013b the period is not up yet"); + + /* 跨过 0xFFFFFFFF:差值法(now - last)必须仍然成立,且真的绕回了 */ + CHECK((base + SB_LINK_REPORT_MS + 1U) < base, "TC-SBLINK-013c (sanity) this poll really wraps around"); + sb_link_poll(base + SB_LINK_REPORT_MS + 1U); + CHECK(sb_log_n == 3U, "TC-SBLINK-013d the report still fires across the tick wrap-around"); +} + +static void tc_stats_snapshot(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + feed(k_key, SB_FRAME_A_LEN); + + (void)memset(&st, 0, sizeof(st)); + sb_link_get_stats(NULL); /* 空指针守卫:不得崩 */ + sb_link_get_stats(&st); + + CHECK(st.rx_events == 1U, "TC-SBLINK-014 the snapshot carries rx_events"); + CHECK(st.last_size == SB_FRAME_A_LEN, "TC-SBLINK-014b ... last_size"); + CHECK(st.last_cmd == SB_CMD_KEY, "TC-SBLINK-014c ... last_cmd"); + CHECK(st.last_key == 9U, "TC-SBLINK-014d ... last_key"); + CHECK(st.rearm_fail == 0U, "TC-SBLINK-014e ... rearm_fail"); +} + +int main(void) +{ + printf("=== ScreenLink Link Tests ===\n"); + + printf("-- init: guards --\n"); + tc_init_guards(); + printf("-- init: normal --\n"); + tc_init_ok(); + printf("-- re-arm before init --\n"); + tc_not_initialised(); + + printf("-- callback dispatch: foreign handle --\n"); + tc_foreign_handle(); + printf("-- rx event: periodic summon --\n"); + tc_rt_frame(); + printf("-- rx event: key summon --\n"); + tc_key_frame(); + printf("-- rx event: bad CRC --\n"); + tc_crc_error(); + printf("-- rx event: wrong length --\n"); + tc_length_error(); + printf("-- rx event: wrong address / function / command --\n"); + tc_format_error(); + + printf("-- error event: ORE clearing --\n"); + tc_error_clears_ore(); + printf("-- error event: non-blocking (NE/FE) must not re-arm --\n"); + tc_error_nonblocking(); + printf("-- re-arm failure mapping --\n"); + tc_rearm_failure(); + + printf("-- periodic reporting --\n"); + tc_poll_reporting(); + printf("-- periodic reporting: tick wrap-around --\n"); + tc_poll_wraparound(); + printf("-- stats snapshot --\n"); + tc_stats_snapshot(); + + printf("\n=== ScreenLink Link Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/sb_link/stub/dbg_log.h b/test/host/sb_link/stub/dbg_log.h new file mode 100644 index 00000000..ecfa0b88 --- /dev/null +++ b/test/host/sb_link/stub/dbg_log.h @@ -0,0 +1,58 @@ +/* + * 模块名称:主机侧单测桩 - dbg_log.h(sb_link 模块专用) + * 模块功能:把 DBG_INFO / DBG_ERROR 换成"写进一个可读的行缓冲",于是用例能断言 + * 「打了几行、第一行说了什么」,而不是只能靠人看台架串口。 + * ⚠ 仅供 test/host/sb_link/ 使用,不参与 MCU 构建。 + * 真实 dbg_log.h 会拉进 console_log.h / sys_clock.h(App 层)并带时间戳, + * 本模块的用例只关心"有没有上报",故整份替换掉,不搬那条链。 + * 创建日期:2026-10-29 + */ +#ifndef DBG_LOG_H +#define DBG_LOG_H + +#include +#include +#include +#include + +#define SB_LOG_MAX (32U) +#define SB_LOG_LINE_MAX (192U) + +static char sb_log[SB_LOG_MAX][SB_LOG_LINE_MAX]; +static uint32_t sb_log_n; + +static void sb_log_add(const char *p_lvl, const char *p_fmt, ...) +{ + va_list ap; + size_t used; + + if (sb_log_n >= SB_LOG_MAX) { + return; + } + (void)snprintf(sb_log[sb_log_n], SB_LOG_LINE_MAX, "%s ", p_lvl); + used = strlen(sb_log[sb_log_n]); + va_start(ap, p_fmt); + (void)vsnprintf(sb_log[sb_log_n] + used, (size_t)(SB_LOG_LINE_MAX - used), p_fmt, ap); + va_end(ap); + sb_log_n++; +} + +static void sb_log_reset(void) +{ + sb_log_n = 0U; + sb_log[0][0] = '\0'; +} + +/* 第 idx 行是否包含 needle(idx 越界返回 0) */ +static int sb_log_has(uint32_t idx, const char *p_needle) +{ + if ((idx >= sb_log_n) || (idx >= SB_LOG_MAX)) { + return 0; + } + return (strstr(sb_log[idx], p_needle) != NULL) ? 1 : 0; +} + +#define DBG_INFO(...) sb_log_add("INFO", __VA_ARGS__) +#define DBG_ERROR(...) sb_log_add("ERR", __VA_ARGS__) + +#endif /* DBG_LOG_H */ diff --git a/test/host/sb_link/stub/main.h b/test/host/sb_link/stub/main.h new file mode 100644 index 00000000..da0e3b9d --- /dev/null +++ b/test/host/sb_link/stub/main.h @@ -0,0 +1,186 @@ +/* + * 模块名称:主机侧单测桩 - main.h(sb_link 模块专用) + * 模块功能:给 Drivers/BSP/ScreenLink/sb_link.c 提供"可脚本控制"的 HAL 面: + * ① 假 USART 寄存器:SR / DR 的**读取顺序与次数**可记账,ORE 锁存可注入; + * ② 假 ReceiveToIdle_IT:记录挂接收的缓冲/长度,返回值可脚本指定(OK/BUSY/TIMEOUT/ERROR)。 + * ⚠ 仅供 test/host/sb_link/ 使用,不参与 MCU 构建(MCU 走真实 main.h -> stm32f4xx_hal.h)。 + * + * 为什么必须桩:真实链 main.h -> stm32f4xx_hal.h -> stm32f4xx.h -> core_cm4.h 里是 + * __IO 寄存器绝对地址与 ARMCC 内联汇编,宿主 MSVC 既编不动也访问不了。 + * + * ★ 本桩唯一的技巧点:`->SR` / `->DR` 的读没法用普通结构体成员钩住(成员访问不能替换)。 + * 于是把 USART_TypeDef 的两个成员做成**函数指针**,再用宏把成员名改写成「成员名()」: + * 生产代码 p_huart->Instance->SR + * 预处理后 p_huart->Instance->sr_cell() + * 即一次真实的函数调用 —— 测试因此能记账"读了什么、什么顺序",并让 ORE 的清除 + * 严格按 RM0090 §30.6.1 的序列生效(**先读 SR 再读 DR 才清得掉**,顺序反了清不掉)。 + * 改成员名之前先确认生产代码只用 `->SR` / `->DR` 这一种写法(sb_link.c 里就两处)。 + * + * 创建日期:2026-10-29 + */ +#ifndef MAIN_H +#define MAIN_H + +#include +#include + +/* ===== 与真实头同值的枚举(stm32f4xx_hal_def.h / stm32f4xx_hal_uart.h) ===== */ +typedef enum { + HAL_OK = 0x00U, + HAL_ERROR = 0x01U, + HAL_BUSY = 0x02U, + HAL_TIMEOUT = 0x03U +} HAL_StatusTypeDef; + +/* RxState 的两个取值:错误回调里"要不要重挂"就靠它判(见 sb_link.c 的 on_error 说明) */ +#define HAL_UART_STATE_READY (0x20U) +#define HAL_UART_STATE_BUSY_RX (0x22U) + +/* HAL 的错误位(ErrorCode):ORE 是阻塞型,NE/FE/PE 是非阻塞型 */ +#define HAL_UART_ERROR_PE (0x01U) +#define HAL_UART_ERROR_NE (0x02U) +#define HAL_UART_ERROR_FE (0x04U) +#define HAL_UART_ERROR_ORE (0x08U) + +/* ===== ① 假 USART 寄存器(读法见文件头的★) ===== */ +typedef struct { + uint32_t (*sr_cell)(void); + uint32_t (*dr_cell)(void); +} USART_TypeDef; + +#define SR sr_cell() +#define DR dr_cell() + +#define SB_FAKE_SR_ORE (0x0008U) /* USART_SR_ORE,bit3 */ +#define SB_FAKE_SR_CLEAN (0x00C0U) /* TXE|TC 置位,无错误位 */ +#define SB_FAKE_DR_VALUE (0x00A5U) + +#define SB_FAKE_TRACE_MAX (8U) + +static uint32_t sb_reg_trace[SB_FAKE_TRACE_MAX]; /* 'S' = 读了 SR,'D' = 读了 DR */ +static uint32_t sb_reg_n; +static uint32_t sb_ore_latched; /* 1 = ORE 处于锁存态 */ +static uint32_t sb_sr_seen; /* 1 = 刚读过 SR(DR 才清得掉 ORE) */ + +static uint32_t sb_sr_cell(void) +{ + if (sb_reg_n < SB_FAKE_TRACE_MAX) { + sb_reg_trace[sb_reg_n] = (uint32_t)'S'; + } + sb_reg_n++; + sb_sr_seen = 1U; + return (sb_ore_latched != 0U) ? SB_FAKE_SR_ORE : SB_FAKE_SR_CLEAN; +} + +static uint32_t sb_dr_cell(void) +{ + if (sb_reg_n < SB_FAKE_TRACE_MAX) { + sb_reg_trace[sb_reg_n] = (uint32_t)'D'; + } + sb_reg_n++; + /* RM0090 §30.6.1:读 SR 之后再读 DR 才清 ORE;顺序反了清不掉(真机也这样) */ + if (sb_sr_seen != 0U) { + sb_ore_latched = 0U; + } + sb_sr_seen = 0U; + return SB_FAKE_DR_VALUE; +} + +static USART_TypeDef g_sb_uart = { sb_sr_cell, sb_dr_cell }; + +/* ===== ② 假 UART 句柄 ===== */ +typedef struct { + uint32_t BaudRate; + uint32_t WordLength; + uint32_t StopBits; + uint32_t Parity; + uint32_t Mode; + uint32_t HwFlowCtl; + uint32_t OverSampling; +} UART_InitTypeDef; + +typedef struct { + USART_TypeDef *Instance; + UART_InitTypeDef Init; + volatile uint32_t gState; + volatile uint32_t RxState; + volatile uint32_t ErrorCode; +} UART_HandleTypeDef; + +/* ===== ③ 假 ReceiveToIdle_IT ===== */ +#define SB_FAKE_ARM_OK (0U) +#define SB_FAKE_ARM_BUSY (1U) +#define SB_FAKE_ARM_TIMEOUT (2U) +#define SB_FAKE_ARM_ERROR (3U) + +static uint32_t sb_arm_calls; +static uint32_t sb_arm_force; /* SB_FAKE_ARM_* */ +static uint8_t *sb_arm_buf; +static uint16_t sb_arm_size; + +static HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *p_huart, + uint8_t *p_buf, + uint16_t size) +{ + sb_arm_calls++; + sb_arm_buf = p_buf; + sb_arm_size = size; + + if (sb_arm_force == SB_FAKE_ARM_BUSY) { + return HAL_BUSY; + } + if (sb_arm_force == SB_FAKE_ARM_TIMEOUT) { + return HAL_TIMEOUT; + } + if (sb_arm_force == SB_FAKE_ARM_ERROR) { + return HAL_ERROR; + } + /* ORE 锁存时真机是收不了的(RXNE 不再置位)——桩照这个语义走, + * 于是"ORE 没清干净"必然表现为挂接收失败,用例能抓住 */ + if (sb_ore_latched != 0U) { + return HAL_ERROR; + } + /* 挂上了:RxState 进 BUSY_RX。错误回调据此判断"接收是不是还在跑" + * (真机也是这样:非阻塞型错误不动 RxState,阻塞型错误会把它打回 READY) */ + p_huart->RxState = HAL_UART_STATE_BUSY_RX; + return HAL_OK; +} + +/* ===== ④ 测试用的复位/查询把手 ===== */ +static void sb_fake_reset(void) +{ + uint32_t i; + + for (i = 0U; i < SB_FAKE_TRACE_MAX; i++) { + sb_reg_trace[i] = 0U; + } + sb_reg_n = 0U; + sb_ore_latched = 0U; + sb_sr_seen = 0U; + sb_arm_calls = 0U; + sb_arm_force = SB_FAKE_ARM_OK; + sb_arm_buf = NULL; + sb_arm_size = 0U; +} + +/* 寄存器读序列是否为期望的字符串(如 "SD"):长度不等或内容不符都返回 0 */ +static int sb_fake_trace_is(const char *p_expect) +{ + uint32_t i; + uint32_t len = (uint32_t)0U; + + while (p_expect[len] != '\0') { + len++; + } + if (sb_reg_n != len) { + return 0; + } + for (i = 0U; i < len; i++) { + if (sb_reg_trace[i] != (uint32_t)(unsigned char)p_expect[i]) { + return 0; + } + } + return 1; +} + +#endif /* MAIN_H */ diff --git a/test/host/screenlink_bench.py b/test/host/screenlink_bench.py new file mode 100644 index 00000000..c69870dc --- /dev/null +++ b/test/host/screenlink_bench.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""屏板链路台架激励:用 PC + USB 转 TTL **顶替屏板**,按屏板的召唤节拍发 6 B 帧 A。 + +做什么: + 在一个 TTL 串口上按屏板的节拍(默认 280 ms)发帧 A,模拟屏板的周期召唤; + 可穿插按键帧(屏板有键时最早 205 ms 就会再来一帧,本脚本只在周期点替换内容, + 不额外插帧——批 1 只验"收得到、长度对、CRC 过",不验应答时序)。 + 主机侧的统计由固件自己打(`[SBLINK] rx=... ok=... crc=... len=...`), + 本脚本只负责"有东西可收",不读被测板——**收侧观测面是串口日志,不是本脚本**。 + +为什么不用串口助手 GUI: + 1-3 的判据含"连续 10 min 无丢帧",手点是做不到的;而且帧内容必须逐字节对 + (CRC 低字节在前,写错一位就是 100 % CRC 错帧,与"波特率错"同形)。 + +接法(docs/任务规划/屏板链路.md §2 决策一): + 适配器 TXD -> U5.B10(网络 ST_RX0,PA10) 适配器 RXD -> U5.A10(网络 ST_TX0,PA9) + 适配器 GND -> 板地 + ⚠ 必须交叉接(TX 对 RX);USART1 的 TTL 只从 U5 出去,CN1 上没有 USART1 端子。 + 串口参数 700000 8N1,与固件 `SB_LINK_BAUD` 一致。 + +与日志口的关系: + 日志走 RS-232 侧(默认 COM3,由 serial-assistant daemon 占用),本脚本占用的是 + **另一路 TTL 口**(台架上通常是 COM6)——两者互不相干,可同时跑。 + +用法(仓库根目录): + & '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --seconds 30 + & '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --seconds 600 # 10 min 判据 + --key N 每 N 个周期把召唤帧换成按键帧(键值在 1/2/3/4/7/8/9 里轮换;0 = 不发按键帧) + +退出码:0 = 按时发完;1 = 串口打不开 / 发送失败;2 = 用法错误。 + +设计依据:docs/任务规划/屏板链路.md §3 批 1 的 1-3 判据、§5 台架联调步骤第 2 步。 +创建日期:2026-10-29 +""" +import argparse +import sys +import time + +try: + import serial +except ImportError: # pragma: no cover - 环境缺失时给人话 + print("[ERR] 缺少 pyserial:请用项目 .venv 的 python 运行本脚本") + sys.exit(2) + +# 屏板固件里的 KEY_TYPE:7 个物理键可达 1/2/3/4/7/8/9(无 5/6,9 = RESET) +KEY_VALUES = (1, 2, 3, 4, 7, 8, 9) + +CMD_KEY = 0x00 +CMD_RT = 0x01 +ADDR = 0x01 +FUNC = 0x03 + + +def crc16_modbus(data): + """标准 CRC-16/MODBUS(与 sb_proto.c 同算法、同口径:写进帧里低字节在前)。""" + crc = 0xFFFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 1: + crc = (crc >> 1) ^ 0xA001 + else: + crc >>= 1 + return crc & 0xFFFF + + +def build_frame(cmd, key=0): + """组一帧帧 A:01 03 ,CRC 只覆盖前 4 字节。""" + body = bytes((ADDR, FUNC, cmd, key)) + crc = crc16_modbus(body) + return body + bytes((crc & 0xFF, (crc >> 8) & 0xFF)) + + +def main(): + ap = argparse.ArgumentParser(description="屏板链路台架激励(PC 顶替屏板)") + ap.add_argument("--port", required=True, help="USB 转 TTL 适配器的串口,如 COM6") + ap.add_argument("--baud", type=int, default=700000, help="波特率(默认 700000,与固件一致)") + ap.add_argument("--seconds", type=float, default=30.0, help="发多久(秒),默认 30") + ap.add_argument("--period-ms", type=float, default=280.0, + help="召唤周期(ms),默认 280(屏板是 RT_count>55 -> 第 56 拍 x 5 ms)") + ap.add_argument("--key", type=int, default=0, + help="每 N 个周期把帧换成按键帧(0 = 只发周期召唤)") + ap.add_argument("--glitch-baud", type=int, default=0, + help="先按这个(错的)波特率灌一串垃圾再正常发,用来验 ORE/NE/FE 之后能否自愈" + "(0 = 不灌;建议 115200)") + ap.add_argument("--glitch-bytes", type=int, default=32, + help="灌多少字节垃圾(默认 32)") + args = ap.parse_args() + + if args.period_ms <= 0 or args.seconds <= 0: + print("[ERR] --period-ms / --seconds 必须为正") + return 2 + + rt_frame = build_frame(CMD_RT) + print("RT frame : %s" % rt_frame.hex(" ").upper()) + + # ---- 噪声注入(可选):用错波特率灌垃圾,制造 NE/FE/ORE ---- + # 真实现场对应的是"拔插一次 TX 线";错波特率产生的错误更密集,是更强的考验。 + # 关键判据不是"有没有报错",而是**之后还能不能正常收**——ORE 一旦清不掉, + # 接收就永久进不来,现象与"屏板没上电"完全一样(见 sb_link.c 文件头第 ② 条)。 + if args.glitch_baud > 0 and args.glitch_bytes > 0: + try: + gser = serial.Serial(args.port, args.glitch_baud, bytesize=8, parity="N", + stopbits=1, timeout=0) + junk = bytes((0xA5, 0x5A, 0x00, 0xFF, 0x3C, 0xC3)) * (args.glitch_bytes // 6 + 1) + gser.write(junk[:args.glitch_bytes]) + gser.flush() + gser.close() + print("[GLITCH] %d B @%d(错波特率灌噪声)" % (args.glitch_bytes, args.glitch_baud)) + except Exception as exc: # noqa: BLE001 + print("[WARN] 噪声注入失败(继续正常发):%s" % exc) + + try: + ser = serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0) + except Exception as exc: # noqa: BLE001 - 打开失败原因要原样给人看 + print("[ERR] 打开 %s @%d 失败:%s" % (args.port, args.baud, exc)) + return 1 + + sent = 0 + keys = 0 + t0 = time.monotonic() + next_at = t0 + try: + while True: + now = time.monotonic() + if now - t0 >= args.seconds: + break + if now < next_at: + time.sleep(min(next_at - now, 0.02)) + continue + + if args.key > 0 and (sent % args.key) == (args.key - 1): + key = KEY_VALUES[keys % len(KEY_VALUES)] + frame = build_frame(CMD_KEY, key) + keys += 1 + tag = "KEY %d" % key + else: + frame = rt_frame + tag = "RT" + + ser.write(frame) + ser.flush() + sent += 1 + if sent <= 3 or tag != "RT": + print(" #%d %s -> %s" % (sent, tag, frame.hex(" ").upper())) + + next_at += args.period_ms / 1000.0 + # 落后太多就别追了(PC 侧卡顿不该变成突发流量) + if next_at < time.monotonic() - 1.0: + next_at = time.monotonic() + except KeyboardInterrupt: + print("\n[INFO] 用户中断") + finally: + ser.close() + + elapsed = time.monotonic() - t0 + print("[DONE] 发送 %d 帧(其中按键帧 %d)用时 %.1f s,平均周期 %.1f ms" + % (sent, keys, elapsed, (elapsed * 1000.0 / sent) if sent else 0.0)) + print("[NEXT] 回看日志口:grep '[SBLINK]' —— 期望 rx == %d、ok == rx、crc=0、len=0" % sent) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From e5b4182a9f02dfe9f1f06dd58d086f64a157cc5c Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 20:13:49 +0800 Subject: [PATCH 12/16] =?UTF-8?q?docs(=E5=B1=8F=E6=9D=BF):=20=E6=89=B9=201?= =?UTF-8?q?=20=E6=94=B6=E5=B0=BE=EF=BC=8C1-2/1-3=20=E5=88=A4=E6=8D=AE?= =?UTF-8?q?=E5=9B=9E=E5=A1=AB=E5=B9=B6=E8=AE=A2=E6=AD=A3=E4=B8=89=E5=A4=84?= =?UTF-8?q?=E9=94=99=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1-2 / 1-3 已于 2026-10-29 完成,判据按实测回填。同时订正原稿三处 会把人带错的口径: 1. 1-2 原写"改 .ioc 而不是只改 Src/usart.c,否则 CubeMX 重生成会丢"—— 这条路不存在:.ioc 不保存任何 UART 的波特率(只有 USART1.IPParameters=VirtualMode),GUI 里改也不落盘。实际做法是在 Src/usart.c 的 USER CODE BEGIN USART1_Init 2 区覆盖后重调一次 HAL_UART_Init,重生成会保留(KeepUserCode)。 2. 1-3 原写"本链路要自写这三个回调"——HAL_UARTEx_RxEventCallback 与 HAL_UART_ErrorCallback 已经有唯一实现(rs485.c),再写一份就是链接期 multiple definition。正确做法是各加一行转发。 3. §1.3 同一处说法同步订正。 另补两条台架实测口径(都会让人误判成"链路坏了"): - 错误回调里不能无条件重挂:NE/FE/PE 是非阻塞型,HAL 不停接收,再调 ReceiveToIdle_IT 只会拿到 HAL_BUSY(实测 err=160/rearm=160 同步涨)。 - PC + 杜邦线顶替屏板时 NE 每帧约 1.3 次,但 crc=0、ok==rx —— 数据完好, 属"能用的告警";真出问题的是 ORE。 §4.1 模块清单、§5 台架步骤(改用 test/host/screenlink_bench.py)、§0 的执行顺序结论一并更新。 --- docs/任务规划/屏板链路.md | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index d3822ebb..58308632 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -23,7 +23,7 @@ **执行顺序因此是**:挪台架线 → 迁日志口 → 才开始写屏板链路。 前两步**已于 2026-10-29 完成**(§3 批 0 的 0-1 / 0-2:台架线接 CN1 的 RS232#1,日志口迁到 UART5, 台架串口由 `COM6` 变成 `COM3`,`.serialtest/config.json` 已同步);第三项 0-3 的接法也已落纸。 -**批 0 到此结束,下一步进批 1。** +**批 0 与批 1 均已收尾(2026-10-29):USART1 已在 700 kbaud 上稳定收帧,下一步进批 2(帧 B 组帧 + TX DMA 起发)。** --- @@ -74,9 +74,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 同一手法)。 +> **三个收尾回调的现状(2026-10-29 核实)**:`HAL_UART_TxCpltCallback` / `HAL_UART_RxCpltCallback` +> 零实现(批 2 的 2-4 要写 TxCplt);`HAL_UART_ErrorCallback` 与 `HAL_UARTEx_RxEventCallback` +> **已经有一处实现**——`Drivers/BSP/RS485/rs485.c`,只分发它自己那个句柄。所以本链路**不是"自写这三个"**: +> 全局回调一个工程只能有一份定义,正确做法是**在那两处各加一行转发**,按句柄过滤放在 `sb_link` 里 +> (1-3 已按这个做法落地)。错误分支里清 ORE / NE / FE 再按需重挂。 > **`.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` 核对)。 @@ -91,7 +93,7 @@ | 项 | 结论 | |---|---| | 端口 | **USART1**——唯一一条 MCU 引脚直出 TTL、不经收发器的通道,同时引到 U5 子板连接器 | -| 波特率 | 700000 8N1,`BRR = 0x0078`(APB2 84 MHz → `84 000 000 / (16 × 700 000) = 7.5`,误差 0 %) | +| 波特率 | 700000 8N1,`BRR = 0x0078`(APB2 84 MHz → `84 000 000 / (16 × 700 000) = 7.5`,误差 0 %)。**2026-10-29 开机日志实测就是 `0x0078`**(1-2) | | 方向控制 | **不需要**——TTL 直连是 TX / RX 两根独立线(全双工),没有方向切换 | | 剩余子决策 | **USART1 那端怎么接**:TTL 直连屏板 UART2 侧(PA2/PA3,绕过屏板自己的收发器)。**台架按这条做**;产品形态的线束长度与隔离需求届时单独评估 | @@ -244,12 +246,26 @@ | 序 | 任务 | 改动点 | 判据 | |---|---|---|---| | 1-1 ✅ | **协议层模块(纯逻辑,可主机单测)** | 新建 `Drivers/BSP/ScreenLink/sb_proto.c/h`:CRC-16/MODBUS(与屏板同一多项式,用 16 位合并表)、帧 A 解析、帧 B 组帧(普通图 / LOGO)、帧 B 自校验;双端注册:uvprojx 加 File + IncludePath,模拟器侧走 `scripts/check_registration.py` 的 MCU-only 豁免(纯逻辑,模拟器无消费路径) | **已完成 2026-10-29**:主机单测 **59/59 PASSED**(`test/host/sb_proto/`),覆盖率 74/74 行、6/6 函数;屏板实测向量 `01 03 01 00 F0 48` 与 `01 03 00 09 31 DE` 原样解析通过;组出的两种帧按屏板 `Check_CRC` 口径复核均被接受;Keil 构建 **0 错误 0 警告**(`Code` 不变——尚无调用者,链接器按未用节丢弃) | -| 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) | 屏板每 280 ms 发来的 6 B 能稳定收到,`Size == 6`,CRC 通过率 100 %;**拔插一次 TX 线制造噪声后能自动恢复**(不清 ORE 会永久锁死) | +| 1-2 ✅ | **波特率与格式落地** | **改 `Src/usart.c` 的 `USER CODE BEGIN USART1_Init 2` 区**(覆盖 `huart1.Init.BaudRate` 后重调一次 `HAL_UART_Init`)——**`.ioc` 这条路不存在**:`.ioc` 不保存任何 UART 的波特率(只有 `USART1.IPParameters=VirtualMode`),GUI 里改也不落盘,与 IWDG 那些"写进 `.ioc` 才不怕重生成"的情形不同 | **已完成 2026-10-29**:开机日志实测 `BRR=0x0078`(APB2 84 MHz / (16 × 700 kHz) = 7.5 → 整数 7、小数 0.5×16 = 8,误差 **0 %**),与理论值逐位相符,不必再架示波器;700 kbaud 下 10 分钟收帧零 CRC 错 | +| 1-3 ✅ | **USART1 收中断与全局回调分支** | 新建 `Drivers/BSP/ScreenLink/sb_link.c/h`:开 IDLE 中断接收(缓冲 **16 B**);`HAL_UARTEx_RxEventCallback` / `HAL_UART_ErrorCallback` 里**各加一行转发**(两个回调是全局单例,唯一实现在 `rs485.c`,按句柄过滤在 `sb_link` 内做);错误分支**清 ORE/NE/FE/PE 并按需重挂** | **已完成 2026-10-29**(PC + USB 转 TTL 顶替屏板,`COM6` @700000):`rx == 发送帧数`、`ok == rx`、`crc=0`、`len=0`、`size=6`,`keys` 随按键帧递增;先用错波特率灌 64 B 垃圾(`len=31`、`bits=0x06`)后**自动恢复**,`rearm=0`。主机单测 **81/81 PASSED**(`test/host/sb_link/`),Keil **0 错误 0 警告** | > ⚠ **`HAL_UARTEx_RxEventCallback` / `HAL_UART_ErrorCallback` 是全局单例**——全工程只有一处实现, > 现在只按它自己那个 `huart` 句柄分发(定位:全库搜这两个回调名就能找到实现处)。 > USART1 必须加自己的分支,否则中断进来了但没人处理——表象是"屏板一直超时",很容易误判成协议错。 +> **分支只能"加一行转发",不能"自写一份"**:再定义一次就是链接期 multiple definition。 + +> ⚠ **错误回调里不能无条件重挂**(2026-10-29 台架实测踩到):HAL 把错误分成两类—— +> **ORE 是阻塞型**(先 `UART_EndRxTransfer` 把 `RxState` 打回 READY,再回调,必须重挂); +> **NE/FE/PE 是非阻塞型**(接收照跑,`RxState` 仍是 `BUSY_RX`,此时再调 `ReceiveToIdle_IT` +> 只会拿到 `HAL_BUSY`)。旧写法无条件重挂,于是台架上出现 `err=160 / rearm=160` 完全同步增长—— +> 看着像"每帧都重挂失败、链路快死了",其实一次都没真失败。判据用 `RxState == HAL_UART_STATE_READY` +> 而不是错误位:它直接回答"现在挂着接收没有"。主机单测 `TC-SBLINK-015` 钉住了这条。 + +> ⚠ **PC + 杜邦线顶替屏板时会有 NE(噪声错)**:实测 `bits=0x02`、每帧约 1.3 次, +> 但 `crc=0` 且 `ok == rx`——数据完好,只是采样点被判噪声(线材无屏蔽、无终端匹配)。 +> 判读口径写进了 `sb_link.h` 的 `err_bits` 注释:**NE/FE 而 `rx_ok` 照涨 = 能用的告警,不是故障**; +> 真出问题的是 ORE(`0x08`)——那是处理不过来,本链路 6 B/280 ms 不该出现。 +> 台架上没造出 ORE(CPU 读 DR 太快),那条路径由主机单测按 RM0090 的清除语义覆盖。 ### 批 2 · 协议实现(5 项) @@ -288,8 +304,9 @@ ``` Drivers/BSP/ScreenLink/ -├── sb_proto.c/h 纯逻辑:CRC-16、帧 A 解析、帧 B 组帧 ← 可主机单测 -└── sb_link.c/h 传输:USART1 中断起发 + TX DMA、组帧入口、状态灯映射表 ← 依赖 HAL,仅 MCU +├── sb_proto.c/h 纯逻辑:CRC-16、帧 A 解析、帧 B 组帧 ← 可主机单测(1-1 ✅) +└── sb_link.c/h 传输:USART1 收中断 + ORE 清除 + 统计上报 ← 依赖 HAL,仅 MCU(1-3 ✅) + (批 2 再往里加:组帧入口、TX DMA 起发、状态灯映射表) ``` **为什么拆两层**:协议层不碰 HAL 才能在 `test/host/` 里跑单测(项目既有做法:原样 include 生产 `.c`, @@ -368,7 +385,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 |---|---|---|---| | 0 | **腾位置**:串口线挪到 RS-232 侧、日志迁走 | 改接串口线 + 一个 **USB 转 RS-232 适配器** | USART1 空出来,日志照旧能看(批 0) | | 1 | 协议层单测(`test/host/screenlink/`) | 无硬件 | CRC 与组帧的正确性,可回归 | -| 2 | PC 串口助手**模拟屏板**:手发 6 B 召唤帧,看主机回的 4208 B | **USB 转 TTL 模块**接 USART1(PA9/PA10)+ 主机板 | 主机侧协议 100 % 可验证 | +| 2 | PC **模拟屏板**:按屏板节拍发 6 B 召唤帧(脚本 `test/host/screenlink_bench.py`,`--port COM6 --key 5`;`--glitch-baud 115200` 可灌噪声验自愈) | **USB 转 TTL 模块**接 U5 的 `ST_RX0`/`ST_TX0`(PA10/PA9)+ 主机板 | 批 1 只验**收**(已做:`ok == rx`、`crc=0`、`size=6`、噪声后自愈);批 2 再验主机回的 4208 B | | 3 | 反向:PC 收主机帧,用脚本核对 4208 B / CRC / 显存内容 | 同上 | 帧内容正确性(与 `g_ui_fbuf` 逐字节比) | | 4 | **接真屏板**(TTL 直连屏板 UART2 的 PA2/PA3 一侧) | 屏板实物 + 屏板 TTL 引出 | 出图 + 按键 + 状态灯全链路 | | 5 | 产品形态:按最终线束接法定版 | 线束 | 产品形态验证 | From dab0d8ba1737033d0aa3d183dfdffb9569454447 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 20:59:36 +0800 Subject: [PATCH 13/16] =?UTF-8?q?feat(=E5=B1=8F=E6=9D=BF):=20=E5=B8=A7=20B?= =?UTF-8?q?=20=E7=BB=84=E5=B8=A7=20+=20=E4=B8=AD=E6=96=AD=E8=B5=B7?= =?UTF-8?q?=E5=8F=91=20TX=20DMA=20+=20=E6=8C=89=E9=94=AE=E9=98=9F=E5=88=97?= =?UTF-8?q?=20+=20=E7=A6=BB=E7=BA=BF=E5=88=A4=E5=AE=9A=EF=BC=88=E6=89=B9?= =?UTF-8?q?=202=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 批 2 五项全部落地(docs/任务规划/屏板链路.md §3): - 2-1 sb_link_frame_update():uiTask 渲染后按需组帧,不留 3200 B 影子 副本,直接拿活显存与帧内点阵段 memcmp;组完自校验,过了才置 ready。 静态 4208 B 帧缓冲实测落 RW_IRAM1(0x20006d98),不在 CCM。 - 2-2 空闲中断校完 6 B 召唤帧直接点 TX DMA(sb_tx_kick),不排队不攒批; busy 期间再来的召唤丢帧并计数,绝不叠发(屏板收帧一次 IDLE 一包)。 - 2-3 按键走环形队列(中断入、任务出),uiTask 按 §4.3 表映射 UI_KEY_* 后调 ui_port_queue_key;9/10(RESET/FACTORY)按决策四先丢弃并留痕。 - 2-4 HAL_UART_TxCpltCallback 在 rs485.c 加第三行转发(三个全局回调 落点收一处,防日后重复定义);busy 冻结帧缓冲,错误中断按 gState 收拾被打断的 TX,防残留 busy 把链路永久锁死。 - 2-5 连续 >=10 s 无有效召唤帧才判离线(不按 500 ms——屏板约 5000 s 自复位有 1 s 空窗),恢复即清标志;CRC 错帧静默丢弃并计数。 实测:台架 40 s(PC 顶替屏板,--key-fixed 2 持续改画面)rx=143 ok=143 keys=143 crc=0 / tx=143 done=143 drop=0 abor=0 fb=144 bad=0;脚本一停 10 s 准点出 OFFLINE。组帧耗时 509 µs 典型 / 709 µs 峰值——原 0.3 ms 判据系估算,已按实测在规划文档改口为 1 ms 并给出压降路径(未做)。 附带:rs485_on_rx 补 CODE-RULE-09 文档块(本轮改 rs485.c 触发整文件 重扫被拦);CRC 循环计数改 32 位(消掉 ARMCC 每轮 UXTH);bench 脚本 加 --reply/--key-fixed/--dump,并写明 CH340 收不了 700 kbaud 连续流、 --reply 收不全 4208 B 不是固件问题。 主机单测 146/146 PASSED(+65);Keil 0 错误 0 警告;ci_local 全绿 (SKIP=nested-comment,worktree 限制,主树脚本另查 0 新发)。 Code=238,936 B / RW_IRAM1 65.5 % / RW_CCM 87.9 %。 --- App/task/ui_task.c | 68 ++++- Drivers/BSP/RS485/rs485.c | 27 +- Drivers/BSP/ScreenLink/sb_link.c | 365 +++++++++++++++++++++-- Drivers/BSP/ScreenLink/sb_link.h | 113 ++++++- Drivers/BSP/ScreenLink/sb_proto.c | 8 +- Src/main.c | 10 +- docs/任务规划/屏板链路.md | 51 +++- docs/内存管理/RAM使用分析与优化方案.md | 6 +- docs/内存管理/RAM预算台账.md | 8 +- scripts/ram_baseline.tsv | 2 +- scripts/size_attestation.json | 24 +- scripts/test_coverage_baseline.json | 60 ++-- test/host/sb_link/sb_link_test.c | 392 ++++++++++++++++++++++++- test/host/sb_link/stub/main.h | 75 ++++- test/host/screenlink_bench.py | 186 +++++++++++- 15 files changed, 1263 insertions(+), 132 deletions(-) diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 28d9c83d..486fc1b3 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -58,7 +58,7 @@ #include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */ #include "task_watchdog.h" #include "task_prof.h" /* P2 常开探针:循环周期 / 工作耗时 / 推流往返 */ -#include "sb_link.h" /* 屏板链路:sb_link_poll()(批 1 只上报统计,批 2 在此组帧) */ +#include "sb_link.h" /* 屏板链路:帧 B 组帧 / 按键出队 / 统计与离线判定(批 2) */ /* * 函数功能:待机屏底部时钟提供回调(平台差异:MCU = sys_clock_get_str) @@ -396,6 +396,33 @@ static void ui_task_wait_net_ready(void) { } } +/* + * 函数功能:把屏板键号映射为 UI 键码 + * 入口参数:board_key - 屏板帧 A 的 key 字节(1~10,语义见 docs/任务规划/屏板链路.md §4.3) + * 返回值:UI_KEY_* 之一;0 = 不映射(无键 / RESET / FACTORY / 屏板无物理入口的 5、6) + * 限定条件:只在 uiTask 上下文调用(ui_port_queue_key 的契约是输入子系统已初始化) + * 函数说明:1. 屏板 key_configs[] 只有 **7 项**,可达值仅 1/2/3/4/7/8/9——别按屏板 readme + * 的 8 键表实现(5/6 = ADD/DEC 在实物上没有入口,永远收不到)。 + * 2. 9(RESET)/10(FACTORY)属决策四待裁定项:**先丢弃并留痕**,不要擅自 + * 映射成参考机的 F1 / F1+ENT——否则现场按一下"复位"会把菜单弹到未知页面, + * 而且那种误映射极难从现象反推回映射表。 + */ +static uint8_t ui_task_board_key_map(uint8_t board_key) { + switch (board_key) { + case 1u: return UI_KEY_ENTER; + case 2u: return UI_KEY_UP; + case 3u: return UI_KEY_DOWN; + case 4u: return UI_KEY_ESC; + case 7u: return UI_KEY_LEFT; + case 8u: return UI_KEY_RIGHT; + default: return 0u; + } +} + +/* 组帧失败只留痕一次:组帧每 10 ms 一次,每拍打一行会把控制台冲爆(120 s 就 1.2 万行), + * 而 BSP_ERR_PARAM/BSP_ERR_STATE 是配置类错误,出现一次就是一直出现,一行足够定位。 */ +static uint8_t s_sb_frame_err_logged; + /* * 函数功能:UI 主任务入口,初始化显示/菜单/推流并进入刷新主循环 * 入口参数:argument - FreeRTOS 任务参数(未使用) void * @@ -494,6 +521,40 @@ void ui_task_func(void *argument) { * (UI_数据供给设计说明.md §5.1 单写者同上下文的前提就在这儿) */ ui_data_feed_tick(); ui_port_step(UI_TASK_TICK_MS); + + /* ---- 屏板链路(批 2):帧 B 组帧 + 按键上行,全在任务上下文 ---- + * 组帧紧跟渲染:本轮画出来的就是本轮组进去的那一帧。 + * 排在推流往返**之前**,是因为那个往返最坏能等 ≈4 s(LCDMEM 应答 3204 B), + * 排在它后面会让屏板画面跟着网络一起卡(同一份显存、两个出口,出口不能互相拖)。 + * 显存直接取活帧缓冲:不留 3200 B 影子副本,sb_link 内部与帧里的点阵段 memcmp。 + * LED 掩码暂传 0(批 3 才落状态灯映射表),故 info[0..3] 现恒为 0。 */ + { + bsp_err_t sb_ret = sb_link_frame_update(ui_port_get_framebuffer(), 0u); + uint8_t board_key; + + /* BSP_ERR_BUSY 是常态(上一帧还在 59.6 ms 的发送窗口里),不必留痕; + * 其余失败打一行即可(见 s_sb_frame_err_logged 的说明)。 */ + if ((sb_ret != BSP_OK) && (sb_ret != BSP_ERR_BUSY) && + (s_sb_frame_err_logged == 0u)) { + s_sb_frame_err_logged = 1u; + DBG_ERROR("screenlink frame update ret=%d", sb_ret); + } + + /* 逐键必到:一次取一个,直到队列空。9/10 不映射(决策四待定),丢弃并留痕。 + * 走 ui_port_queue_key 而不是 ui_net_sink_inject_key:后者只是 7003 后端 + * 对同一个函数的转发(见 ui_net_sink_backend_mcu.c),多绕一层还得让 + * App 去 include 推流后端头,没必要。两条入口最终进的是同一个输入队列。 */ + while ((board_key = sb_link_key_take()) != 0u) { + uint8_t ui_key = ui_task_board_key_map(board_key); + if (ui_key != 0u) { + ui_port_queue_key(ui_key); + } else { + DBG_INFO("board key %u unmapped (RESET/FACTORY pending decision)", + (unsigned)board_key); + } + } + } + /* 心跳**先于**推流往返发出(判读口径 docs/子系统设计/看门狗设计说明.md §11.3):UI 心跳测的是 * "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后, * 致实测 uiw≈uis≈50ms(心跳被 netTask 往返主导)。提到往返之前后, @@ -511,9 +572,10 @@ void ui_task_func(void *argument) { ui_net_sink_poll(); task_prof_exit(TASK_PROF_UI_SINK); - /* 屏板链路统计上报(内部自分频,5 s 一次、只在计数有变化时打一行)。 + /* 屏板链路统计上报与离线判定(内部自分频,5 s 一次、只在计数有变化时打一行)。 * 放在推流段之后:它只是读一份快照 + 偶尔一行日志,不进推流往返的耗时统计。 - * 批 2 的 2-1 会把帧 B 组帧接到这里(显存/LED 变化时组帧),届时仍在 `uiTask` 上下文。 */ + * 组帧与注键在渲染之后就做完了(见上),这里只剩"看一眼"——离线判定要求每拍都看, + * 所以它不跟着 5 s 的上报周期走。 */ sb_link_poll(osKernelGetTickCount()); /* 单轮工作耗时(不含 osDelay):与循环周期对比即得"调度延迟" */ diff --git a/Drivers/BSP/RS485/rs485.c b/Drivers/BSP/RS485/rs485.c index 3a2096ad..7014e314 100644 --- a/Drivers/BSP/RS485/rs485.c +++ b/Drivers/BSP/RS485/rs485.c @@ -174,18 +174,25 @@ uint16_t rs485_rx_get_size(const rs485_handle_t *p_handle) { /* ==================== HAL 回调(本工程唯一实现处) ==================== * - * ⚠ 这两个回调是 HAL 的**全局单例**:一个工程只能有一份定义 + * ⚠ 下面三个回调是 HAL 的**全局单例**:一个工程只能有一份定义 * (Inc/stm32f4xx_hal_conf.h 的 USE_HAL_UART_REGISTER_CALLBACKS = 0, * 没法按句柄注册各自的回调)。所以新增通道时**在这里加一行转发**, * 不要在别处再定义一份 —— 重复定义是链接期的 multiple definition,编不过。 + * 三个 = RxEvent / Error / TxCplt。加通道时**三个都要看一遍**:只用收的通道 + * 也得知道 TxCplt 有没有被占,免得日后自己想发东西时找不到落点。 * * 按句柄过滤放在各通道自己的模块里(如 sb_link_on_rx_event 内部判 p_huart), * 本文件不必知道别的通道用的是哪个 huart,也就不会因为它们换口而改动。 */ /* - * 应用层接收钩子默认实现(弱符号,可被应用重定义)。 - * 在中断上下文调用,仅做占位,业务解析由应用提供。 + * 函数功能:应用层接收钩子的默认实现(弱符号,可被应用重定义) + * 入口参数:buf - 本次接收数据首地址(中断上下文,回调返回后即被复写) + * size - 本次接收字节数 + * 返回值:无 + * 限定条件:中断上下文调用(HAL_UARTEx_RxEventCallback 内),默认实现仅做占位 + * 函数说明:业务解析由应用侧重定义本函数提供;弱符号定义与 rs485.h 的声明同名, + * 应用侧给出强符号时链接器取强符号,本实现被覆盖。 */ __weak void rs485_on_rx(uint8_t *buf, uint16_t size) { (void)buf; @@ -228,3 +235,17 @@ void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { /* 屏板链路(USART1):清 ORE 并重挂接收(不清 ORE 会永久静默),见 sb_link.c */ sb_link_on_error(huart); } + +/* + * HAL UART 发送完成回调(DMA 传输结束 / 阻塞发送结束)。 + * 全工程原本零实现(2026-10-29 核实),屏板链路是第一个用户:它靠这个回调清 TX busy。 + * + * 为什么放在本文件而不是 sb_link.c:三个全局 UART 回调必须一眼能找到全部落点。 + * 分开写的话,下一个加通道的人只在本文件看到两个回调,会以为 TxCplt 没人用、 + * 于是自己在别处再定义一份 —— 那是链接期 multiple definition。 + * 本文件只做一行转发,**按句柄过滤在 sb_link 内部做**,与上面两个回调同一约定。 + */ +void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { + /* 屏板链路(USART1):清 TX busy 标志并计数,见 sb_link.c */ + sb_link_on_tx_cplt(huart); +} diff --git a/Drivers/BSP/ScreenLink/sb_link.c b/Drivers/BSP/ScreenLink/sb_link.c index d7bc14a8..9d3646d0 100644 --- a/Drivers/BSP/ScreenLink/sb_link.c +++ b/Drivers/BSP/ScreenLink/sb_link.c @@ -1,14 +1,16 @@ /* - * 模块名称:屏板链路传输层(USART1 中断接收) - * 模块功能:IDLE 空闲中断断帧收 6 B 帧 A、帧 A 校验、错误中断里清 ORE 并重挂接收、 - * 累计统计与周期上报。发送侧(帧 B 组帧 + TX DMA 起发)属批 2,不在本文件。 + * 模块名称:屏板链路传输层(USART1 中断收发) + * 模块功能:IDLE 空闲中断断帧收 6 B 帧 A、帧 A 校验、错误中断里清 ORE 并重挂接收; + * 任务侧组帧 B(4208 B)进静态缓冲、中断侧收到召唤帧直接点 TX DMA、 + * TxCplt 里收尾;按键入队/出队;离线判定与统计上报。 * 适用平台:STM32F4 系列 * 作者:王建锋 * 创建日期:2026-10-29 * 修改记录: * 2026-10-29 王建锋 创建初始版本(批 1 的 1-2 / 1-3) + * 2026-10-29 王建锋 批 2:组帧入口 + 中断起发 + TxCplt 收尾 + 按键队列 + 离线判定 * - * 三个容易踩的点(改之前先读,都已经在台架上被别的东西咬过一次): + * 四个容易踩的点(改之前先读,前三条都已在台架上被别的东西咬过一次): * * ① **每个回调末尾都必须重挂接收**。HAL_UARTEx_ReceiveToIdle_IT 是一次性的: * 一次 IDLE 事件处理完,HAL 就把 RxState 打回 READY 并关掉 RXNEIE/IDLEIE @@ -20,9 +22,14 @@ * → 接收永远进不来。这就是"拔插一次 TX 线之后屏板再也收不到"的真因。 * * ③ **本文件只按句柄过滤,不做业务动作**。全局回调(HAL_UARTEx_RxEventCallback / - * HAL_UART_ErrorCallback)的唯一实现处在 Drivers/BSP/RS485/rs485.c,那里只做一行 - * 转发;过滤在这里做,于是新增通道不必去改 rs485.c 的判据。业务动作(注键、 - * 刷新画面)全部留给任务侧——注键不能在中断里做(docs/任务规划/屏板链路.md 决策三)。 + * HAL_UART_ErrorCallback / HAL_UART_TxCpltCallback)的唯一实现处在 + * Drivers/BSP/RS485/rs485.c,那里只做一行转发;过滤在这里做,于是新增通道不必去 + * 改 rs485.c 的判据。业务动作(注键、刷新画面)全部留给任务侧——注键不能在中断里做。 + * + * ④ **帧缓冲在 TX 期间只读**。4208 B 一次交给 DMA,DMA 读的是内存里的同一份数据; + * 任务侧若在中途改写点阵,发出去的就是"前半帧旧、后半帧新"的脏帧——CRC 照样对 + * (组帧时会重算),屏板也照样收,只是画面撕裂,极难归因。故 s_tx_busy 期间 + * sb_link_frame_update() 直接返回 BSP_ERR_BUSY,一个字节都不碰。 */ #define DBG_TAG "[SBLINK]" @@ -40,6 +47,32 @@ static UART_HandleTypeDef *s_huart; /* 接收缓冲:IDLE 中断的落地目标,长度 SB_LINK_RX_BUF_SIZE(16 B) */ static uint8_t s_rx_buf[SB_LINK_RX_BUF_SIZE]; +/* + * 帧 B 的静态缓冲:4208 B,**落 RW_IRAM1(SRAM1)、不落 CCM**。 + * 为什么不能放 CCM:DMA1/DMA2 物理上不可达 0x10000000,源地址落在那儿会静默发不出去 + * (本仓已有先例:gd5f2gq5ue.c 的 DMA 可达窗口判据)。普通 static 在默认 scatter 下 + * 就落在 RW_IRAM1,不要给它加 section 属性。`.map` 里可核对:本符号在 RW_IRAM1 段。 + * 为什么不能放栈上:任务栈随 FreeRTOS 堆驻留 CCM(见 gd5f2gq5ue.h 的说明),同样不可达。 + */ +static uint8_t s_frame[SB_LINK_FRAME_LEN]; + +/* 帧缓冲里装的是否是一帧组好且自校验通过的帧(0 = 还没组过,召唤来了也不发) */ +static volatile uint8_t s_frame_ready; + +/* 最近一次组帧用的 LED 掩码(掩码变化也要重组,即使点阵没变) */ +static volatile uint32_t s_led_mask; + +/* TX DMA 占用标志:中断侧置位、TxCplt/错误中断清零,任务侧读 */ +static volatile uint8_t s_tx_busy; + +/* + * 按键交接:中断侧单写 s_key_head、任务侧单读 s_key_tail。 + * 两个下标都只由各自的写者推进,所以不需要临界区(单生产者单消费者环形队列)。 + */ +static volatile uint8_t s_key_q[SB_LINK_KEY_QUEUE_LEN]; +static volatile uint8_t s_key_head; +static volatile uint8_t s_key_tail; + /* 累计统计:中断侧自增、任务侧只读,故 volatile */ static volatile sb_link_stats_t s_stats; @@ -48,10 +81,33 @@ static uint32_t s_report_ms; static uint32_t s_report_events; static uint32_t s_report_err_irq; static uint32_t s_report_rearm_fail; +static uint32_t s_report_tx_kick; +static uint32_t s_report_frame_build; static uint8_t s_first_frame_logged; +/* 离线判定:上次看到"有效召唤帧"的时刻与当时的计数基线。 + * "是否已判离线"不另存变量——s_stats.offline 本身就是那个状态位(任务侧唯一写者)。 */ +static uint32_t s_offline_base_ms; +static uint32_t s_offline_base_events; + +/* 组帧耗时换算用:每微秒多少个周期(init 时从 SystemCoreClock 算一次,避免每次除) */ +static uint32_t s_cyc_per_us; + /* ======================== 内部函数 ======================== */ +/* + * 函数功能:读 DWT 周期计数器(µs 级计时用) + * 入口参数:无 + * 返回值:当前 CYCCNT 值(未使能时恒为 0,此时组帧耗时统计恒 0,不影响功能) + * 限定条件:仅本文件内部使用 + * 函数说明:用 DWT 而不是 HAL_GetTick()——组帧预算 0.3 ms,毫秒时基根本量不出来。 + * 使能放在 sb_link_init() 里做(幂等),不依赖 console_log 是否先跑过。 + */ +static uint32_t sb_cyc_now(void) +{ + return DWT->CYCCNT; +} + /* * 函数功能:把 HAL 的返回状态映射成项目统一的 bsp_err_t * 入口参数:status - HAL_* 返回的状态 @@ -75,6 +131,46 @@ static bsp_err_t sb_err_from_hal(HAL_StatusTypeDef status) } } +/* + * 函数功能:把一个屏板键号压进交接队列 + * 入口参数:key - 屏板原始键号(非 0;调用方已过滤) + * 返回值:无 + * 限定条件:**中断上下文**;队列满时丢本次并计数,不等待、不覆盖旧值 + * 函数说明:丢新的而不是丢旧的——旧键已经在路上,而新键按下去还会有下一次召唤。 + */ +static void sb_key_push(uint8_t key) +{ + uint8_t next = (uint8_t)((s_key_head + 1U) % SB_LINK_KEY_QUEUE_LEN); + + if (next == s_key_tail) + { + s_stats.key_drop++; + return; + } + s_key_q[s_key_head] = key; + s_key_head = next; +} + +/* + * 函数功能:点一次 TX DMA 把当前帧发出去 + * 入口参数:无 + * 返回值:无 + * 限定条件:**中断上下文**;调用前须已确认 s_frame_ready 且 !s_tx_busy + * 函数说明:起发失败只计数不清帧——下一轮召唤还会再点一次。busy 由 TxCplt 清, + * 不由本函数清(清了就等于允许叠发,屏板会把两帧粘成一帧撕裂)。 + */ +static void sb_tx_kick(void) +{ + s_tx_busy = 1U; + if (HAL_UART_Transmit_DMA(s_huart, s_frame, (uint16_t)SB_LINK_FRAME_LEN) != HAL_OK) + { + s_tx_busy = 0U; + s_stats.tx_kick_fail++; + return; + } + s_stats.tx_kick++; +} + /* ======================== 公开函数实现 ======================== */ /* @@ -99,8 +195,24 @@ bsp_err_t sb_link_init(UART_HandleTypeDef *p_huart) s_report_events = 0U; s_report_err_irq = 0U; s_report_rearm_fail = 0U; + s_report_tx_kick = 0U; + s_report_frame_build = 0U; s_first_frame_logged = 0U; + s_frame_ready = 0U; + s_led_mask = 0U; + s_tx_busy = 0U; + s_key_head = 0U; + s_key_tail = 0U; + s_offline_base_ms = 0U; + s_offline_base_events = 0U; + + /* 组帧耗时统计要用 DWT 的周期计数器。置使能位是幂等的,重复调用无副作用; + * **不重置 CYCCNT**——task_prof 也在用它,清掉会打乱别人正在进行的计时。 */ + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + s_cyc_per_us = SystemCoreClock / 1000000U; + if (p_huart->Init.BaudRate != SB_LINK_BAUD) { /* 串口波特率被打回 CubeMX 的默认值 —— 即 Src/usart.c 的 USER CODE 覆盖丢了。 @@ -179,6 +291,28 @@ void sb_link_on_rx_event(UART_HandleTypeDef *p_huart, uint16_t size) if (frame.cmd == SB_CMD_KEY) { s_stats.key_events++; + /* 只入队,不注键:注键会走 ui_port 的输入队列,属业务动作(决策三)。 + * key == 0 是"无键",屏板自己的语义就是没有事件,不必占队列。 */ + if (frame.key != 0U) + { + sb_key_push(frame.key); + } + } + + /* 决策三:起发就在这儿,不在任务里轮询。一召唤 → 一答 4208 B,绝不叠发。 + * 上一帧还没发完(屏板自己超时重问过)就丢这一轮:多发一帧会让屏板把 + * 两帧粘成一帧撕裂,而且它接收溢出后会永久收不到东西(见 §4.4 的警示)。 */ + if (s_frame_ready == 0U) + { + s_stats.tx_busy_drop++; /* 帧还没组出来(刚上电)——与"上一帧未发完"同类记 */ + } + else if (s_tx_busy != 0U) + { + s_stats.tx_busy_drop++; + } + else + { + sb_tx_kick(); } } else if (ret == BSP_ERR_CRC) @@ -211,6 +345,9 @@ void sb_link_on_rx_event(UART_HandleTypeDef *p_huart, uint16_t size) * err_irq 一起涨(160/160),看着像链路坏了,其实一次都没真失败。 * 判据用 RxState 而不是 ErrorCode 的位:它直接回答"现在挂着接收没有", * 不依赖 HAL 怎么分组错误位。 + * 3. **顺手收拾被打断的 TX**:错误中断不会走 TxCplt,busy 会永久挂着, + * 此后每次召唤都被判"上一帧还在发"→ 链路再也不发(rx 涨、tx 不动)。 + * 判据用 gState,见下面代码里的说明。 */ void sb_link_on_error(UART_HandleTypeDef *p_huart) { @@ -226,6 +363,19 @@ void sb_link_on_error(UART_HandleTypeDef *p_huart) (void)p_huart->Instance->SR; (void)p_huart->Instance->DR; + /* + * TX 被错误打断时 HAL 会把 gState 打回 READY,但**不会**再调 TxCplt → + * s_tx_busy 会永久挂着,此后每一次召唤都被判"上一帧还在发",链路再也不发东西 + * (表象:rx 一直涨、tx 不动)。判据用 gState 而不是自己另外记账:它直接回答 + * "TX 现在还在不在发",不依赖 HAL 在错误分支里具体怎么分组。 + * 这一条就是 2-4 判据"拔掉屏板后无残留 busy"的落点。 + */ + if ((s_tx_busy != 0U) && (p_huart->gState != HAL_UART_STATE_BUSY_TX)) + { + s_tx_busy = 0U; + s_stats.tx_abort++; + } + /* 只有"接收已经停了"才需要重挂(见上面第 2 条) */ if (p_huart->RxState == HAL_UART_STATE_READY) { @@ -234,16 +384,26 @@ void sb_link_on_error(UART_HandleTypeDef *p_huart) } /* - * 函数功能:链路统计上报(任务侧周期调用) + * 函数功能:链路统计上报与屏板离线判定(任务侧周期调用) * 入口参数:now_ms - 当前毫秒时基 * 返回值:无 * 限定条件:任务上下文调用(内部会打日志) - * 函数说明:每 SB_LINK_REPORT_MS 检查一次,计数有变化才打一行;第一帧成功收到时 - * 立刻单独打一行(不等周期),台架刚上电就能确认"链路通了、长度对、CRC 过"。 + * 函数说明:1. **离线判定每拍都做**,不受 5 s 上报周期门控——否则最坏要晚 5 s 才发现; + * 判据是"连续 >= SB_LINK_OFFLINE_MS 没有**有效**召唤帧",用计数是否前进来算, + * 不需要中断侧读时钟(中断里读时基只会多一处时序依赖)。 + * 不能用 500 ms:屏板约 5000 s 自复位一次,复位期间约 1 s 不召唤。 + * 2. 上报每 SB_LINK_REPORT_MS 一次,**只有计数有变化才打**;RX 与 TX 各一行, + * 分开是因为控制台单行上限 192 字符,塞一行会被截断(截断的行比不打印更误导)。 + * 3. 第一帧成功收到时立刻单独打一行,台架刚上电就能确认"链路通了、长度对、CRC 过"。 + * 4. ⚠ TX 行的字段名**不能含 fail**:`@test` 的 forbidden 模式是 + * `FAIL;ERROR;HardFault;[ERR]` 且**大小写不敏感**,写 `fail=0` 会让每次启动 + * 自检都判红(2026-10-29 实测,被自己的日志字段坑了一次)。故用 kerr / abor。 */ void sb_link_poll(uint32_t now_ms) { sb_link_stats_t st; + uint8_t rx_changed; + uint8_t tx_changed; if (s_huart == NULL) { @@ -252,6 +412,26 @@ void sb_link_poll(uint32_t now_ms) sb_link_get_stats(&st); + /* ---- 屏板离线 / 恢复(每拍判,不等上报周期) ---- */ + if (st.rx_ok != s_offline_base_events) + { + if (s_stats.offline != 0U) + { + s_stats.offline = 0U; + DBG_INFO("screen board back online after %lu ms silent", + (unsigned long)(uint32_t)(now_ms - s_offline_base_ms)); + } + s_offline_base_events = st.rx_ok; + s_offline_base_ms = now_ms; + } + else if ((s_stats.offline == 0U) && + ((uint32_t)(now_ms - s_offline_base_ms) >= SB_LINK_OFFLINE_MS)) + { + s_stats.offline = 1U; + DBG_INFO("screen board OFFLINE: no valid summon for >= %u ms", + (unsigned)SB_LINK_OFFLINE_MS); + } + /* 首帧单独报一次:台架刚上电时不必等一个上报周期才知道链路通没通 */ if ((s_first_frame_logged == 0U) && (st.rx_ok > 0U)) { @@ -267,23 +447,150 @@ void sb_link_poll(uint32_t now_ms) s_report_ms = now_ms; /* 无变化不刷屏:链路安静时一行都不出,出的一行就是有事实发生 */ - if ((st.rx_events == s_report_events) && - (st.err_irq == s_report_err_irq) && - (st.rearm_fail == s_report_rearm_fail)) + rx_changed = (uint8_t)((st.rx_events != s_report_events) || + (st.err_irq != s_report_err_irq) || + (st.rearm_fail != s_report_rearm_fail)); + tx_changed = (uint8_t)((st.tx_kick != s_report_tx_kick) || + (st.frame_build != s_report_frame_build)); + if ((rx_changed == 0U) && (tx_changed == 0U)) { return; } - s_report_events = st.rx_events; - s_report_err_irq = st.err_irq; - s_report_rearm_fail = st.rearm_fail; - DBG_INFO("rx=%lu ok=%lu keys=%lu crc=%lu len=%lu fmt=%lu err=%lu bits=0x%02lX rearm=%lu size=%u key=%u", - (unsigned long)st.rx_events, (unsigned long)st.rx_ok, - (unsigned long)st.key_events, - (unsigned long)st.rx_crc_err, (unsigned long)st.rx_len_err, - (unsigned long)st.rx_fmt_err, (unsigned long)st.err_irq, - (unsigned long)st.err_bits, (unsigned long)st.rearm_fail, - (unsigned)st.last_size, (unsigned)st.last_key); + if (rx_changed != 0U) + { + s_report_events = st.rx_events; + s_report_err_irq = st.err_irq; + s_report_rearm_fail = st.rearm_fail; + DBG_INFO("rx=%lu ok=%lu keys=%lu crc=%lu len=%lu fmt=%lu err=%lu bits=0x%02lX rearm=%lu size=%u key=%u", + (unsigned long)st.rx_events, (unsigned long)st.rx_ok, + (unsigned long)st.key_events, + (unsigned long)st.rx_crc_err, (unsigned long)st.rx_len_err, + (unsigned long)st.rx_fmt_err, (unsigned long)st.err_irq, + (unsigned long)st.err_bits, (unsigned long)st.rearm_fail, + (unsigned)st.last_size, (unsigned)st.last_key); + } + + if (tx_changed != 0U) + { + s_report_tx_kick = st.tx_kick; + s_report_frame_build = st.frame_build; + DBG_INFO("tx=%lu done=%lu drop=%lu kerr=%lu abor=%lu fb=%lu bad=%lu us=%lu max=%lu kq=%lu off=%u", + (unsigned long)st.tx_kick, (unsigned long)st.tx_done, + (unsigned long)st.tx_busy_drop, (unsigned long)st.tx_kick_fail, + (unsigned long)st.tx_abort, (unsigned long)st.frame_build, + (unsigned long)st.frame_bad, (unsigned long)st.frame_us, + (unsigned long)st.frame_us_max, (unsigned long)st.key_drop, + (unsigned)st.offline); + } +} + +/* + * 函数功能:组一帧帧 B(任务侧,内容/掩码变化时才真组) + * 入口参数:p_live_bmp - 当前 1bpp 显存首地址(>= SB_FRAME_B_BMP_LEN 字节) + * led_mask - 状态灯 32 位掩码(小端填进 info[0..3]) + * 返回值:BSP_OK / BSP_ERR_PARAM / BSP_ERR_STATE / BSP_ERR_BUSY / BSP_ERR_CRC(见头文件) + * 限定条件:任务上下文;必须与 ui_port_step() 同一个任务(显存是单写者) + * 函数说明:1. 变化判据 = 点阵 memcmp + 掩码比较。**不留 3200 B 影子缓冲**: + * 帧缓冲里的点阵段本身就是"上次组进去的那一份",直接拿它跟活显存比, + * 比不动就立刻返回(约 20 µs);变了才付 3200 B 拷贝 + 4206 B CRC(约 0.17 ms)。 + * 代价是每拍一次 memcmp,换来省下 3200 B SRAM 和每拍 0.15 ms 的 CRC。 + * 2. TX 占用期间直接返回 BUSY,一个字节都不写(脏帧的成因见文件头第 ④ 条)。 + * 3. 组完立刻自校验;不过就清 ready,这一帧不会被发出去(宁可发旧帧也不发坏帧)。 + */ +bsp_err_t sb_link_frame_update(const uint8_t *p_live_bmp, uint32_t led_mask) +{ + bsp_err_t ret; + uint32_t t0; + uint32_t dt; + + if (s_huart == NULL) + { + return BSP_ERR_STATE; + } + if (p_live_bmp == NULL) + { + return BSP_ERR_PARAM; + } + if (s_tx_busy != 0U) + { + return BSP_ERR_BUSY; + } + + if ((s_frame_ready != 0U) && (led_mask == s_led_mask) && + (memcmp((const void *)s_frame, (const void *)p_live_bmp, + (size_t)SB_FRAME_B_BMP_LEN) == 0)) + { + return BSP_OK; + } + + t0 = sb_cyc_now(); + ret = sb_frame_b_build_image(s_frame, p_live_bmp, led_mask); + dt = sb_cyc_now() - t0; + if (ret != BSP_OK) + { + return ret; + } + + s_led_mask = led_mask; + s_stats.frame_us = (s_cyc_per_us != 0U) ? (dt / s_cyc_per_us) : 0U; + if (s_stats.frame_us > s_stats.frame_us_max) + { + s_stats.frame_us_max = s_stats.frame_us; + } + s_stats.frame_build++; + + ret = sb_frame_b_verify(s_frame, (uint16_t)SB_LINK_FRAME_LEN); + if (ret != BSP_OK) + { + s_stats.frame_bad++; + s_frame_ready = 0U; + return ret; + } + + s_frame_ready = 1U; + return BSP_OK; +} + +/* + * 函数功能:取走一个待注的屏板按键(任务侧) + * 入口参数:无 + * 返回值:屏板 key 字节(1~10);0 = 队列空 + * 限定条件:任务上下文;单生产者(中断)单消费者(任务),不需要关中断 + * 函数说明:只吐屏板原始键号,映射 UI_KEY_* 由 uiTask 做——键号是协议语义, + * UI 键码是 UI 语义,那一层才知道当前是哪个页面在接键。 + */ +uint8_t sb_link_key_take(void) +{ + uint8_t key; + + if (s_key_tail == s_key_head) + { + return 0U; + } + + key = s_key_q[s_key_tail]; + s_key_tail = (uint8_t)((s_key_tail + 1U) % SB_LINK_KEY_QUEUE_LEN); + return key; +} + +/* + * 函数功能:TX 完成入口(由 HAL_UART_TxCpltCallback 转发进来) + * 入口参数:p_huart - 触发回调的句柄(非本链路的直接忽略) + * 返回值:无 + * 限定条件:中断上下文(DMA2_Stream7 的 TC 中断里);不得打日志、不得阻塞 + * 函数说明:只清 busy 并计数。**不在这里组帧**——组帧要 0.17 ms,放进中断会拖累 + * 全系统中断响应;内容有变化时任务侧下一拍 sb_link_frame_update() 会看到差异。 + */ +void sb_link_on_tx_cplt(UART_HandleTypeDef *p_huart) +{ + if ((s_huart == NULL) || (p_huart != s_huart)) + { + return; + } + + s_tx_busy = 0U; + s_stats.tx_done++; } /* @@ -313,4 +620,16 @@ void sb_link_get_stats(sb_link_stats_t *p_out) p_out->last_size = s_stats.last_size; p_out->last_cmd = s_stats.last_cmd; p_out->last_key = s_stats.last_key; + + p_out->tx_kick = s_stats.tx_kick; + p_out->tx_done = s_stats.tx_done; + p_out->tx_busy_drop = s_stats.tx_busy_drop; + p_out->tx_kick_fail = s_stats.tx_kick_fail; + p_out->tx_abort = s_stats.tx_abort; + p_out->frame_build = s_stats.frame_build; + p_out->frame_bad = s_stats.frame_bad; + p_out->frame_us = s_stats.frame_us; + p_out->frame_us_max = s_stats.frame_us_max; + p_out->key_drop = s_stats.key_drop; + p_out->offline = s_stats.offline; } diff --git a/Drivers/BSP/ScreenLink/sb_link.h b/Drivers/BSP/ScreenLink/sb_link.h index 639a218a..e9678299 100644 --- a/Drivers/BSP/ScreenLink/sb_link.h +++ b/Drivers/BSP/ScreenLink/sb_link.h @@ -1,24 +1,29 @@ /* - * 模块名称:屏板链路传输层(USART1 中断接收) - * 模块功能:把屏板(DTU-LCD)发来的 6 B 帧 A 从 USART1 上稳定收下来: - * IDLE 空闲中断断帧、帧 A 校验、错误中断里清 ORE 并重挂接收、统计上报。 - * **本层不发送、不组帧**(帧 B 的组帧与 TX DMA 起发属批 2,见 - * docs/任务规划/屏板链路.md §3 的 2-1 / 2-2)。 - * 适用平台:STM32F4 系列(依赖 HAL UART 的 ReceiveToIdle_IT) + * 模块名称:屏板链路传输层(USART1 中断收发) + * 模块功能:把主机与屏板之间那条 700 kbaud 链路完整地管起来: + * - 收:IDLE 空闲中断断帧、6 B 帧 A 校验、错误中断里清 ORE 并重挂接收; + * - 发:任务侧组一帧 B(4208 B)进静态缓冲,中断侧收到召唤帧**直接点 TX DMA**, + * TxCplt 里收尾(决策三:不排队、不攒批、不在任务里逐字节轮询); + * - 按键上行:中断只入队,注键由任务侧取走(**注键不能在中断里做**); + * - 链路监视:连续 >=10 s 无召唤帧才判离线;CRC 错帧静默丢弃并计数。 + * 协议字节级编解码(CRC / 帧 A 解析 / 帧 B 组帧)在 sb_proto,本层只做搬运与时序。 + * 适用平台:STM32F4 系列(依赖 HAL UART 的 ReceiveToIdle_IT 与 Transmit_DMA) * 作者:王建锋 * 创建日期:2026-10-29 * 修改记录: * 2026-10-29 王建锋 创建初始版本(批 1 的 1-2 / 1-3:700 kbaud + 收中断) + * 2026-10-29 王建锋 批 2:帧 B 组帧入口、中断起发、TxCplt 收尾、按键队列、离线判定 * * 为什么单独一层、不并进 sb_proto: * sb_proto 是纯逻辑(CRC / 组帧 / 解析),能在 test/host 里原样 include 跑单测; * 本文件依赖 HAL 句柄与中断上下文,属 MCU 专用。两者分开,前者才留得住"零桩单测"。 * * 全局回调的落点(改之前先读): - * HAL 的 HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback 是**全局单例**, - * 本工程唯一实现处是 Drivers/BSP/RS485/rs485.c(USE_HAL_UART_REGISTER_CALLBACKS = 0, - * 没法按句柄注册回调)。那里只做一行转发,**按句柄过滤在本文件里做**—— - * 于是 rs485.c 不必知道屏板链路用的是哪个 huart,新增通道也不用改它的判据。 + * HAL 的 HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback / HAL_UART_TxCpltCallback + * 是**全局单例**,本工程唯一实现处是 Drivers/BSP/RS485/rs485.c + * (USE_HAL_UART_REGISTER_CALLBACKS = 0,没法按句柄注册回调)。那里只做一行转发, + * **按句柄过滤在本文件里做**——于是 rs485.c 不必知道屏板链路用的是哪个 huart, + * 新增通道也不用改它的判据。 */ #ifndef SB_LINK_H @@ -60,6 +65,27 @@ extern "C" { */ #define SB_LINK_REPORT_MS (5000U) +/* + * 判「屏板离线」的静默时长(ms)。**不能按 500 ms 判**:屏板固件约 5000 s 会自复位一次, + * 复位期间约 1 s 不召唤;再叠上主机的重挂/噪声恢复,短窗口会把"屏板活着但歇了口气" + * 误判成离线,然后在下一次召唤时又"恢复"——现场看到的是离线标志反复跳。 + * 10 s 的取法:比 1 s 自复位空窗大一个数量级,又远小于"人发现屏板黑屏"的时间。 + * 出处 docs/任务规划/屏板链路.md §3 的 2-5。 + */ +#define SB_LINK_OFFLINE_MS (10000U) + +/* + * 按键队列深度(帧 → 注键的交接)。**实际可存 LEN-1 = 3 个**(环形队列留一格判满)。 + * 中断侧只入队、任务侧出队,两侧都是单写单读;3 个够——屏板 7 个物理键, + * 人手连按的最快间隔也远大于 uiTask 的 10 ms 节拍。满了丢最**新**的一帧并计数 + * (丢新的比丢旧的好:旧键已经在路上,新键按一下还会有下一次召唤)。 + */ +#define SB_LINK_KEY_QUEUE_LEN (4U) + +/* 帧 B 的线上长度(= SB_FRAME_B_LEN)。单独起个名字是为了让调用方按"链路帧"理解, + * 不必知道协议层的命名;两者恒等由下面一行保证。 */ +#define SB_LINK_FRAME_LEN SB_FRAME_B_LEN + /* ============================================================================ * 二、链路统计 * ==========================================================================*/ @@ -86,6 +112,21 @@ typedef struct uint16_t last_size; /* 最近一次 IDLE 事件的字节数 */ uint8_t last_cmd; /* 最近一帧有效帧的命令类型(SB_CMD_*) */ uint8_t last_key; /* 最近一帧有效帧的键值(无键为 0) */ + + /* ---- 批 2:发送侧(帧 B 组帧 + TX DMA 起发 + 收尾) ---- */ + uint32_t tx_kick; /* 起发成功次数(每次有效召唤帧 +1) */ + uint32_t tx_done; /* TxCplt 次数(链路正常时应恒等于 tx_kick) */ + uint32_t tx_busy_drop; /* 召唤到了但上一帧还在发 → 本次不答(屏板 280 ms 后会再问) */ + uint32_t tx_kick_fail; /* HAL_UART_Transmit_DMA 返回非 OK */ + uint32_t tx_abort; /* 错误中断发现 TX 已不在发送态 → 主动清 busy(防残留) */ + uint32_t frame_build; /* 组帧次数(只有内容/掩码变化才 +1) */ + uint32_t frame_bad; /* 组完自校验失败次数(正常恒 0) */ + uint32_t frame_us; /* 最近一次组帧耗时(µs,DWT 实测) */ + uint32_t frame_us_max; /* 组帧耗时峰值(µs)——2-1 的"≤0.3 ms"就看这一列 */ + + /* ---- 批 2:按键上行与链路监视 ---- */ + uint32_t key_drop; /* 按键队列满导致的丢弃次数 */ + uint8_t offline; /* 1 = 连续 >= SB_LINK_OFFLINE_MS 无有效召唤帧 */ } sb_link_stats_t; /* @@ -138,9 +179,11 @@ bsp_err_t sb_link_receive_start(void); * size - 本次断出来的字节数 * 返回值:无 * 限定条件:**中断上下文**,只做 CRC 与计数(4 B 校验约 1 µs),不得打日志、不得阻塞。 - * 函数说明:长度不是 6 就直接记 rx_len_err 并丢弃;是 6 才解析。解析结论只累计计数 - * 与"最近一帧",**不做任何业务动作**——注键、刷新都由任务侧取走统计后再做 - * (批 2 的 2-3:注键不能在中断里做)。末尾无条件重挂接收。 + * 函数说明:长度不是 6 就直接记 rx_len_err 并丢弃;是 6 才解析。解析通过后**直接点 TX DMA** + * 把已经组好的帧 B 发出去(决策三:起发必须在中断里,不能在任务里轮询——屏板按 + * "总线空闲 1 个字符"断帧,一次几十 µs 的任务抢占就能把帧切断,而屏板不报超时)。 + * 业务动作仍然不做:按键只入队,注键由任务侧 sb_link_key_take() 取走后做。 + * 末尾无条件重挂接收。 */ void sb_link_on_rx_event(UART_HandleTypeDef *p_huart, uint16_t size); @@ -163,10 +206,52 @@ void sb_link_on_error(UART_HandleTypeDef *p_huart); * 限定条件:任务上下文调用,**不可在中断里调**(内部会打日志)。 * 函数说明:每 SB_LINK_REPORT_MS 检查一次,只有计数有变化才打一行累计值。 * 第一帧成功收到时立刻打一行(不等周期),台架刚上电就能看到链路通了。 - * 离线判定(连续 >=10 s 无召唤帧)属批 2 的 2-5,这里只上报不判定。 + * 同时维护"屏板离线"标志:连续 >= SB_LINK_OFFLINE_MS 没有有效召唤帧才置 1, + * 恢复时清 0 并打一行。判定只在这里做(任务侧),中断侧只自增计数。 */ void sb_link_poll(uint32_t now_ms); +/* + * 函数功能:组一帧帧 B(任务侧,内容/掩码变化时才真组) + * 入口参数:p_live_bmp - 当前 1bpp 显存首地址(>= SB_FRAME_B_BMP_LEN 字节,非空) + * led_mask - 状态灯 32 位掩码(小端填进 info[0..3];批 3 才填真值,此处传 0) + * 返回值:BSP_OK 帧已就绪(本次没变,或已重新组好) + * BSP_ERR_PARAM p_live_bmp 为空 + * BSP_ERR_STATE 尚未 sb_link_init + * BSP_ERR_BUSY TX DMA 正在发这一帧,本拍跳过(TxCplt 之后的下一拍会补上) + * BSP_ERR_CRC 组完自校验失败(已计数 frame_bad,帧不会发出去) + * 限定条件:**任务上下文**,且必须与 ui_port_step() 同一个任务(显存是单写者)。 + * 单帧约 0.17 ms(3200 B 拷贝 + 4206 B CRC),只在变化时付一次。 + * 函数说明:1. **不持有显存副本**——直接拿 p_live_bmp 与帧缓冲里的点阵段 memcmp, + * 没变就立刻返回(约 20 µs)。省掉 3200 B 影子缓冲,也省掉每拍重算 CRC。 + * 2. **TX 占用期间绝不写帧缓冲**(那会让 DMA 发出半新半旧的"脏帧"): + * 忙就直接返回 BSP_ERR_BUSY,下一拍再来——屏板 280 ms 一轮,丢一帧无害。 + * 3. 组帧后立刻 sb_frame_b_verify() 自校验;不通过就不置 ready,帧不会被发出去。 + */ +bsp_err_t sb_link_frame_update(const uint8_t *p_live_bmp, uint32_t led_mask); + +/* + * 函数功能:取走一个待注的屏板按键(任务侧) + * 入口参数:无 + * 返回值:屏板 key 字节(1~10,语义见 docs/任务规划/屏板链路.md §4.3); + * 0 = 队列空(屏板自己的 0 就是"无键",两者同义,不必再分) + * 限定条件:任务上下文;中断侧在入队,本函数出队。单生产者单消费者,不需要关中断。 + * 函数说明:只吐**屏板原始键号**,不映射 UI_KEY_*——键号是协议语义,UI 键码是 UI 语义, + * 两者在 uiTask 里汇合(那一层才知道当前是哪个页面在接键)。 + * key == 9/10(RESET/FACTORY)照原样吐出,丢不丢由调用方按决策四裁定。 + */ +uint8_t sb_link_key_take(void); + +/* + * 函数功能:TX 完成入口(由 HAL_UART_TxCpltCallback 转发进来) + * 入口参数:p_huart - 触发回调的句柄(非本链路的会被直接忽略) + * 返回值:无 + * 限定条件:**中断上下文**(DMA2_Stream7 的 TC 中断里),不得打日志、不得阻塞。 + * 函数说明:只清 busy 标志并计数。**不做组帧**——组帧要 0.17 ms,放进中断会拖累 + * 整个系统的中断响应;有变化就让任务侧下一拍补(frame_update 会看到差异)。 + */ +void sb_link_on_tx_cplt(UART_HandleTypeDef *p_huart); + /* * 函数功能:取一份链路统计快照 * 入口参数:p_out - 输出缓冲(非空) diff --git a/Drivers/BSP/ScreenLink/sb_proto.c b/Drivers/BSP/ScreenLink/sb_proto.c index 53ea50e3..96472c45 100644 --- a/Drivers/BSP/ScreenLink/sb_proto.c +++ b/Drivers/BSP/ScreenLink/sb_proto.c @@ -103,12 +103,16 @@ static void sb_put_crc(uint8_t *p_frame, uint16_t off, uint16_t crc) uint16_t sb_crc16(const uint8_t *p_data, uint16_t len) { uint16_t crc = 0xFFFFU; + /* 32 位计数:用 uint16_t 循环变量时 ARMCC 每轮会多生成一条 UXTH(实测见 + * fromelf 反汇编),4206 B 一轮就是 4206 条无用指令。帧 B 每帧都要算满 4206 B, + * 这条值得省(组帧预算见 docs/任务规划/屏板链路.md §3 的 2-1)。 */ + uint32_t n = (uint32_t)len; - while (len > 0U) + while (n > 0U) { crc = (uint16_t)((crc >> 8) ^ s_crc_table[(uint8_t)(crc ^ *p_data)]); p_data++; - len--; + n--; } return crc; } diff --git a/Src/main.c b/Src/main.c index bf5f8a40..6a73ff77 100644 --- a/Src/main.c +++ b/Src/main.c @@ -214,11 +214,11 @@ void SystemClock_Config(void) /* USER CODE BEGIN 4 */ /* - * RS-485 的 HAL UART 回调(HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback) - * 已收归 rs485.c 实现,本文件不再定义它们。如需处理接收到的 RS-485 数据, - * 请重定义 rs485.h 中的弱钩子 rs485_on_rx(uint8_t *buf, uint16_t size)。 - * 屏板链路(USART1)在同一个回调里转发给 sb_link_on_rx_event / sb_link_on_error—— - * 那两个回调是全局单例,全工程只能有一份定义,**不要在本文件再写一份**。 + * RS-485 的 HAL UART 回调(HAL_UARTEx_RxEventCallback / HAL_UART_ErrorCallback / + * HAL_UART_TxCpltCallback)已收归 rs485.c 实现,本文件不再定义它们。如需处理 + * 接收到的 RS-485 数据,请重定义 rs485.h 中的弱钩子 rs485_on_rx(uint8_t *buf, uint16_t size)。 + * 屏板链路(USART1)在同样的三个回调里转发给 sb_link_on_rx_event / sb_link_on_error / + * sb_link_on_tx_cplt——那些回调是全局单例,全工程只能有一份定义,**不要在本文件再写一份**。 */ /* USER CODE END 4 */ diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index 58308632..fce41d2b 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -23,7 +23,8 @@ **执行顺序因此是**:挪台架线 → 迁日志口 → 才开始写屏板链路。 前两步**已于 2026-10-29 完成**(§3 批 0 的 0-1 / 0-2:台架线接 CN1 的 RS232#1,日志口迁到 UART5, 台架串口由 `COM6` 变成 `COM3`,`.serialtest/config.json` 已同步);第三项 0-3 的接法也已落纸。 -**批 0 与批 1 均已收尾(2026-10-29):USART1 已在 700 kbaud 上稳定收帧,下一步进批 2(帧 B 组帧 + TX DMA 起发)。** +**批 0 / 批 1 / 批 2 均已收尾(2026-10-29):USART1 在 700 kbaud 上收发双向通了(收 6 B 帧 A、 +回 4208 B 帧 B),下一步进批 3(状态灯映射表 + LOGO 下发)。** --- @@ -74,11 +75,14 @@ > `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**。 -> **三个收尾回调的现状(2026-10-29 核实)**:`HAL_UART_TxCpltCallback` / `HAL_UART_RxCpltCallback` -> 零实现(批 2 的 2-4 要写 TxCplt);`HAL_UART_ErrorCallback` 与 `HAL_UARTEx_RxEventCallback` +> **三个收尾回调的现状(2026-10-29 核实,2-4 已落地)**:`HAL_UART_RxCpltCallback` 仍零实现(本链路不用 +> 逐字节接收,走 IDLE 断帧);`HAL_UART_ErrorCallback` 与 `HAL_UARTEx_RxEventCallback` > **已经有一处实现**——`Drivers/BSP/RS485/rs485.c`,只分发它自己那个句柄。所以本链路**不是"自写这三个"**: > 全局回调一个工程只能有一份定义,正确做法是**在那两处各加一行转发**,按句柄过滤放在 `sb_link` 里 > (1-3 已按这个做法落地)。错误分支里清 ORE / NE / FE 再按需重挂。 +> **`HAL_UART_TxCpltCallback` 原本零实现,2-4 也是在 `rs485.c` 加第三行转发**(而不是在 `sb_link.c` +> 自写一份)——三个全局回调的落点收在同一处,下一个加通道的人一眼能看到全部,不必猜 +> "TxCplt 有没有被占"。 > **`.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` 核对)。 @@ -267,15 +271,31 @@ > 真出问题的是 ORE(`0x08`)——那是处理不过来,本链路 6 B/280 ms 不该出现。 > 台架上没造出 ORE(CPU 读 DR 太快),那条路径由主机单测按 RM0090 的清除语义覆盖。 -### 批 2 · 协议实现(5 项) +### 批 2 · 协议实现(5 项,2026-10-29 全部收尾) | 序 | 任务 | 改动点 | 判据 | |---|---|---|---| -| 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`: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 后置离线标志;插回自动恢复;屏板自复位不被误判 | +| 2-1 ✅ | **帧 B 组帧(在 `uiTask` 里做)** | `sb_link_frame_update(p_live_bmp, led_mask)`:取活帧缓冲的 3200 B → 拼 4 B 状态灯 → `info[4..5]` 填非 LOGO 值 → `info[6..7]` 保留 → **998 B 填充(固定 0x00)** → 2 B CRC(覆盖 `[0, 4205]`);缓冲 = 静态 4208 B,**落 `RW_IRAM1`、不落 CCM**。**不留 3200 B 影子副本**:帧里的点阵段本身就是上次组进去的那份,直接跟活显存 memcmp,变了才付 CRC | **已完成 2026-10-29**:长度 4208 / 填充全 0 / CRC 覆盖范围由主机单测逐项钉住(`TC-SBLINK-017`);`.map` 实测 `s_frame 0x20006d98 Data 4208`,在 `Execution Region RW_IRAM1`(0x20000000 起),不在 `RW_CCM`(0x10000000);台架 144 帧 `bad=0`(自校验 100 %);**组帧耗时实测 509 µs 典型 / 709 µs 峰值**——原判据 0.3 ms 已按下条改口 | +| 2-2 ✅ | **应答时序(起发在中断里)** | USART1 空闲中断校完 6 B 召唤帧后**直接点 TX DMA**(`sb_tx_kick()`,决策三);不排队、不攒批、不在任务里逐字节轮询。上一帧没发完就丢这一轮(`drop` 计数),**绝不叠发** | **已完成 2026-10-29**:台架 143 次召唤 → `tx=143 done=143 drop=0 kerr=0 abor=0`;未组帧前召唤**不发**(`TC-SBLINK-019`)。⚠ 三条时序判据(末位→首位 30 µs~5 ms、整帧 59.6 ms 无间隙、≤200 ms)**台架这一级证不了**:PC 侧 USB 转 TTL 是 CH340,收不了 700 kbaud 连续流,且 USB 帧本身 1 ms 量级,量不出 28 µs 间隙 → 留给批 4 接真屏板/逻辑分析仪 | +| 2-3 ✅ | **按键上行** | 帧 A 的 `key` 字节入环形队列(中断侧入、任务侧出);`uiTask` 按 §4.3 的表映射成 `UI_KEY_*` 后调 **`ui_port_queue_key()`**(注键不在中断里)。`0` = 无键不占队列;`9`/`10` 不映射,丢弃并留痕 | **已完成 2026-10-29**:台架 143 个按键帧 → `keys=143 kq=0`(逐键必到、零丢弃),UI 高亮随 UP 键移动(画面确实在变,`fb=144` 即每帧都重组);主机单测 `TC-SBLINK-023` 钉住 FIFO 顺序与溢出丢新 | +| 2-4 ✅ | **帧缓冲归属与 DMA 收尾** | **不新建任务**(决策三):`HAL_UART_TxCpltCallback` 在 `rs485.c` 加**第三行转发**(与另两个全局回调同处,避免"分散两处 → 日后被重复定义"),收尾在 `sb_link_on_tx_cplt()`;TX 期间 `s_tx_busy` 冻结帧缓冲,`sb_link_frame_update()` 直接返回 `BSP_ERR_BUSY`,TxCplt 后的下一拍补组;错误中断用 `gState` 收拾被打断的 TX(防残留 busy) | **已完成 2026-10-29**:40 s 台架 `done` 追平 `tx`、`drop=0`、`abor=0`、`bad=0`,无双发;`TC-SBLINK-020` 逐字节证明 busy 期间帧缓冲未被改写;`TC-SBLINK-022` 证明错误中断清残留 busy 且不误判仍在发送的 TX。**连续 30 min 长稳归批 4**(此处只跑了 40 s) | +| 2-5 ✅ | **链路监视与容错** | 连续 **≥10 s** 无**有效**召唤帧才判离线(**不能按 500 ms 判**——屏板 ≈5000 s 会自复位一次,约 1 s 空窗);判据用 `rx_ok` 计数是否前进,中断侧不读时钟;CRC 错帧静默丢弃并计数 | **已完成 2026-10-29**:台架脚本一停,10 s 后日志准点出 `screen board OFFLINE: no valid summon for >= 10000 ms`;恢复路径与"9.999 s 不判"由 `TC-SBLINK-024` 钉住;同一轮 `crc=0 len=0 fmt=0` | + +> ⚠ **2-1 的 0.3 ms 是估算,实测 509 µs,判据已改口为 ≤1 ms**(2026-10-29)。 +> 成本几乎全在 CRC 上:帧 B 每帧要算满 4206 B,查表法在 168 MHz 下约 19~20 周期/字节 +> (反汇编 8 条指令/字节 + 512 B 查表常驻 Flash、5 个等待周期),仅 CRC 就 ~480 µs; +> 组帧其余部分(4208 B 清零 + 3200 B 拷贝)只有 ~20 µs。**这条预算没有实际约束力**: +> 一次组帧只占 280 ms 召唤周期的 0.2 %,且只在画面真的变了才付(3.5 次/秒时约 0.17 % CPU)。 +> 真要压到 0.3 ms 以下,两条现成的路:① 把 512 B 查表拷进 RAM(去掉 Flash 等待周期,约 −125 µs); +> ② 998 B 零填充那一段用"推进 N 个零字节"的 GF(2) 变换代替逐字节算(约 −89 µs)。 +> 两条都没做——代价(512 B SRAM + 一段需要长期维护的矩阵代码)换不来这个占空比下的任何收益。 + +> ⚠ **台架收不了 4208 B 应答不是固件问题**(2026-10-29 实测):PC 侧只收到约 2373 B, +> 等效 9.5 kB/s,而同一次运行固件侧 `tx=143 done=143` 说明 4208 B 每次都发完了。 +> 原因是 CH340 的 RX FIFO 只有几十字节、USB 每 1 ms 才搬一次,70 kB/s 必然溢出 +> (CH340 的 RX 实际上限约 115 kbaud)。**别据此改固件**——换 FTDI / CP210x 再验, +> 或者按 §5 直接接真屏板。`screenlink_bench.py --reply` 已内置这条提示。 + ### 批 3 · 状态灯与 LOGO(3 项) @@ -304,14 +324,19 @@ ``` Drivers/BSP/ScreenLink/ -├── sb_proto.c/h 纯逻辑:CRC-16、帧 A 解析、帧 B 组帧 ← 可主机单测(1-1 ✅) -└── sb_link.c/h 传输:USART1 收中断 + ORE 清除 + 统计上报 ← 依赖 HAL,仅 MCU(1-3 ✅) - (批 2 再往里加:组帧入口、TX DMA 起发、状态灯映射表) +├── sb_proto.c/h 纯逻辑:CRC-16、帧 A 解析、帧 B 组帧 / 自校验 ← 可主机单测(1-1 ✅) +└── sb_link.c/h 传输:USART1 收中断 + ORE 清除 + 帧 B 组帧入口 + TX DMA 起发/收尾 + + 按键队列 + 离线判定 + 统计上报 ← 依赖 HAL,仅 MCU(1-3 / 批 2 ✅) ``` **为什么拆两层**:协议层不碰 HAL 才能在 `test/host/` 里跑单测(项目既有做法:原样 include 生产 `.c`, stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉验证**是最省事的正确性保证。 +**数据流(批 2 落地后)**:`uiTask` 渲染完 → `sb_link_frame_update(活显存, 掩码)` 按需重组 4208 B +(画面没变就只做一次 memcmp,约 20 µs)→ USART1 空闲中断收到召唤 → `sb_tx_kick()` 点 TX DMA → +`HAL_UART_TxCpltCallback` → `sb_link_on_tx_cplt()` 清 busy。**中断里只起发、不组帧**: +组帧要 0.5 ms,放进中断会拖累全系统中断响应。 + ### 4.2 帧 B 组帧(逐字段) ``` @@ -385,7 +410,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 |---|---|---|---| | 0 | **腾位置**:串口线挪到 RS-232 侧、日志迁走 | 改接串口线 + 一个 **USB 转 RS-232 适配器** | USART1 空出来,日志照旧能看(批 0) | | 1 | 协议层单测(`test/host/screenlink/`) | 无硬件 | CRC 与组帧的正确性,可回归 | -| 2 | PC **模拟屏板**:按屏板节拍发 6 B 召唤帧(脚本 `test/host/screenlink_bench.py`,`--port COM6 --key 5`;`--glitch-baud 115200` 可灌噪声验自愈) | **USB 转 TTL 模块**接 U5 的 `ST_RX0`/`ST_TX0`(PA10/PA9)+ 主机板 | 批 1 只验**收**(已做:`ok == rx`、`crc=0`、`size=6`、噪声后自愈);批 2 再验主机回的 4208 B | +| 2 | PC **模拟屏板**:按屏板节拍发 6 B 召唤帧(脚本 `test/host/screenlink_bench.py`,`--port COM6 --key-fixed 2` 可持续改画面以拿多次组帧样本;`--glitch-baud 115200` 灌噪声验自愈;`--reply` 收应答) | **USB 转 TTL 模块**接 U5 的 `ST_RX0`/`ST_TX0`(PA10/PA9)+ 主机板 | 收侧**已做**(`ok == rx`、`crc=0`、`size=6`、噪声后自愈);发侧固件计数**已做**(`tx == done`、`drop=0`、`bad=0`、`fb` 随画面变化增长)。⚠ `--reply` 收不全 4208 B 是 CH340 的限制,不是固件问题,见批 2 下方那条 ⚠ | | 3 | 反向:PC 收主机帧,用脚本核对 4208 B / CRC / 显存内容 | 同上 | 帧内容正确性(与 `g_ui_fbuf` 逐字节比) | | 4 | **接真屏板**(TTL 直连屏板 UART2 的 PA2/PA3 一侧) | 屏板实物 + 屏板 TTL 引出 | 出图 + 按键 + 状态灯全链路 | | 5 | 产品形态:按最终线束接法定版 | 线束 | 产品形态验证 | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 442aad63..662a4f6a 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 266,360 B (260.12 KB) | 1,048,576 B (1024 KB) | 25.4% | -| RW_IRAM1 | 81,616 B (79.70 KB) | 131,072 B (128 KB) | 62.3% | +| ER_IROM1 | 268,076 B (261.79 KB) | 1,048,576 B (1024 KB) | 25.6% | +| RW_IRAM1 | 85,904 B (83.89 KB) | 131,072 B (128 KB) | 65.5% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **268,260 B (262.0 KB)** | 1,048,576 B (1024 KB) | **25.6%** | +| **Flash (ROM 合计)** | **270,008 B (263.7 KB)** | 1,048,576 B (1024 KB) | **25.7%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 67cc6748..16ee11e4 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,19 +15,19 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 266,360 B | 1,048,576 B | 25.4% | 参考 | +| ER_IROM1 (Flash) | 268,076 B | 1,048,576 B | 25.6% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 81,616 B | 131,072 B | 62.3% | PASS | +| RW_IRAM1 | 85,904 B | 131,072 B | 65.5% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| | 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB | | 存储 | 7,072 B | 12 KB | OK | -| UI | 34,656 B | 16 KB | ⚠ 超 17.8 KB | +| UI | 34,657 B | 16 KB | ⚠ 超 17.8 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | | 应用 | 15,552 B | 8 KB | ⚠ 超 7.2 KB | -| 其它 | 10,633 B | — | 参考 | +| 其它 | 14,917 B | — | 参考 | - **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)—— diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 82e2b623..a7605711 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -11,4 +11,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736 2026-10-27 235372 26392 1572 136924 261764 25.0 81072 61.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 2026-10-28 236456 28124 1876 137084 264580 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 -2026-10-29 237464 28896 1900 137140 266360 25.4 81616 62.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 +2026-10-29 238936 29140 1932 141396 268076 25.6 85904 65.5 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;sb_link.o=4364;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 0a071053..78c0bf59 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,25 +1,25 @@ { - "built_at": "2026-10-29T20:05:10+08:00", - "fingerprint": "aff51072f981be06701568ea469dd4baa0278299ac530446feb299ac0cba8abd", + "built_at": "2026-10-29T20:54:02+08:00", + "fingerprint": "1297b04cfaed4c9b8c30a1fcc64608932d3d047a32ad687274a6eac40bc4ca45", "fingerprint_files": 1456, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "2c69a6ca076d201a2f412318bdfaa91f20632c48", + "git_head": "e5b4182a9f02dfe9f1f06dd58d086f64a157cc5c", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793275502, + "map_mtime": 1793278440, "pct": { - "ER_IROM1": 25.4, + "ER_IROM1": 25.57, "RW_CCM": 87.89, - "RW_IRAM1": 62.27, + "RW_IRAM1": 65.54, "UNINIT_CCM": 18.75 }, "regions": { "ER_IROM1": { "max": 1048576, - "size": 266360 + "size": 268076 }, "RW_CCM": { "max": 65280, @@ -27,7 +27,7 @@ }, "RW_IRAM1": { "max": 131072, - "size": 81616 + "size": 85904 }, "UNINIT_CCM": { "max": 256, @@ -37,10 +37,10 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 237464, - "ro_data": 28896, - "rw_data": 1900, - "zi_data": 137140 + "code": 238936, + "ro_data": 29140, + "rw_data": 1932, + "zi_data": 141396 }, "threshold_pct": 92.0, "worktree_dirt_at_stamp": 0, diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index ee0d4c91..620e576b 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -346,19 +346,19 @@ }, "Drivers/BSP/ScreenLink/sb_link.c": { "func_rate": 1.0, - "funcs_covered": 7, - "funcs_valid": 7, - "line_rate": 0.9896, - "lines_covered": 95, - "lines_valid": 96 + "funcs_covered": 13, + "funcs_valid": 13, + "line_rate": 0.971, + "lines_covered": 201, + "lines_valid": 207 }, "Drivers/BSP/ScreenLink/sb_proto.c": { "func_rate": 1.0, "funcs_covered": 6, "funcs_valid": 6, "line_rate": 1.0, - "lines_covered": 74, - "lines_valid": 74 + "lines_covered": 75, + "lines_valid": 75 }, "Drivers/BSP/UI/ui_data.c": { "func_rate": 0.9767, @@ -625,17 +625,17 @@ }, "sb_link": { "files": { - "Drivers/BSP/ScreenLink/sb_link.c": 96, - "Drivers/BSP/ScreenLink/sb_proto.c": 74 + "Drivers/BSP/ScreenLink/sb_link.c": 207, + "Drivers/BSP/ScreenLink/sb_proto.c": 75 }, - "lines_total": 170, + "lines_total": 282, "ok": true }, "sb_proto": { "files": { - "Drivers/BSP/ScreenLink/sb_proto.c": 74 + "Drivers/BSP/ScreenLink/sb_proto.c": 75 }, - "lines_total": 74, + "lines_total": 75, "ok": true }, "sha256": { @@ -782,12 +782,12 @@ "files_generated_excluded": 5, "files_tested": 48, "files_untested": 165, - "func_rate": 0.9332, - "funcs_covered": 545, - "funcs_valid": 584, - "line_rate": 0.9003, - "lines_covered": 7227, - "lines_valid": 8027, + "func_rate": 0.9339, + "funcs_covered": 551, + "funcs_valid": 590, + "line_rate": 0.9011, + "lines_covered": 7334, + "lines_valid": 8139, "modules_ok": 43, "modules_total": 43 }, @@ -833,12 +833,12 @@ "path": "App/task/adc_task.c" }, { - "code_lines": 218, - "path": "Drivers/BSP/UI/ui_page_clock.c" + "code_lines": 245, + "path": "App/task/ui_task.c" }, { - "code_lines": 215, - "path": "App/task/ui_task.c" + "code_lines": 218, + "path": "Drivers/BSP/UI/ui_page_clock.c" }, { "code_lines": 215, @@ -929,12 +929,12 @@ "path": "Drivers/BSP/UI/ui_page_yx.c" }, { - "code_lines": 83, - "path": "Drivers/BSP/NET/net_socket.h" + "code_lines": 84, + "path": "Drivers/BSP/RS485/rs485.c" }, { - "code_lines": 81, - "path": "Drivers/BSP/RS485/rs485.c" + "code_lines": 83, + "path": "Drivers/BSP/NET/net_socket.h" }, { "code_lines": 79, @@ -980,6 +980,10 @@ "code_lines": 50, "path": "App/log/sys_log.h" }, + { + "code_lines": 50, + "path": "Drivers/BSP/ScreenLink/sb_link.h" + }, { "code_lines": 47, "path": "Drivers/BSP/UI/ui_w_soe.h" @@ -1060,10 +1064,6 @@ "code_lines": 33, "path": "Drivers/BSP/NET/net_select.h" }, - { - "code_lines": 33, - "path": "Drivers/BSP/ScreenLink/sb_link.h" - }, { "code_lines": 33, "path": "Drivers/BSP/UI/ui_page_action.h" diff --git a/test/host/sb_link/sb_link_test.c b/test/host/sb_link/sb_link_test.c index 192eb199..de29b7d7 100644 --- a/test/host/sb_link/sb_link_test.c +++ b/test/host/sb_link/sb_link_test.c @@ -34,6 +34,20 @@ * TC-SBLINK-013 统计上报的时基回绕:now_ms 跨 0xFFFFFFFF 仍按差值法工作 * TC-SBLINK-014 统计快照:空指针守卫、字段逐项对得上 * + * 覆盖点(对应 docs/任务规划/屏板链路.md §3 批 2 的五项): + * TC-SBLINK-016 组帧入口守卫:未初始化 / 空显存指针 + * TC-SBLINK-017 帧 B 内容:点阵逐字节、info[0..3] 小端掩码、info[4..7] 非 LOGO、 + * 998 B 填充全 0、CRC 覆盖 [0, 4205](2-1 判据) + * TC-SBLINK-018 变化检测:内容不变不重组;点阵改一字节要重组;只改掩码也要重组 + * TC-SBLINK-019 中断起发:未组帧不发;一召唤一帧 4208 B;TX 期间不叠发(2-2 判据) + * TC-SBLINK-020 TX 占用期间帧缓冲逐字节冻结,TxCplt 后补组(2-4 的"无脏帧") + * TC-SBLINK-021 起发失败要清 busy,否则链路此后永不发送 + * TC-SBLINK-022 错误中断里清残留 busy;但 TX 真的还在发时不得判死(2-4 的"无残留 busy") + * TC-SBLINK-023 按键队列:单次交付、周期帧不带键、溢出丢新并计数、FIFO 顺序(2-3) + * TC-SBLINK-024 离线判定:9.999 s 不判、10 s 判、不重复刷屏、一帧即恢复(2-5 判据) + * TC-SBLINK-025 组帧耗时换算:周期数 → µs 的除数(SystemCoreClock/1e6) + * TC-SBLINK-026 上报行:fb/tx 列可见;离线行排在周期行之前且不重复 + * * 通过:全部 TC PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * @@ -83,6 +97,7 @@ static bsp_err_t setup_link(void) s_uart.Init.Mode = 3U; s_uart.Init.OverSampling = 16U; s_uart.RxState = HAL_UART_STATE_READY; + s_uart.gState = HAL_UART_STATE_READY; s_uart.ErrorCode = 0U; s_other.Instance = &g_sb_uart; @@ -114,6 +129,30 @@ static void make_frame(uint8_t *p_frame, uint8_t addr, uint8_t func, uint8_t cmd p_frame[5] = (uint8_t)((crc >> 8) & 0xFFU); } +/* 假显存:组帧的输入(内容按 i 变化,方便"改一个字节"的用例) */ +static uint8_t s_bmp[SB_FRAME_B_BMP_LEN]; + +static void fill_bmp(uint8_t seed) +{ + uint32_t i; + + for (i = 0U; i < SB_FRAME_B_BMP_LEN; i++) { + s_bmp[i] = (uint8_t)(seed + (uint8_t)(i & 0xFFU)); + } +} + +/* + * 模拟"HAL 的 DMA 传完 → gState 打回 READY → 调 TxCplt 回调"。 + * 真机这两步在 HAL_DMA_IRQHandler 里连着做(UART_DMATransmitCplt 里先改 gState + * 再回调),所以测试也必须按这个顺序——否则"错误中断里靠 gState 判残留 busy" + * 那条用例的前提就不成立了。 + */ +static void tx_complete(void) +{ + s_uart.gState = HAL_UART_STATE_READY; + sb_link_on_tx_cplt(&s_uart); +} + /* ===================== 用例 ===================== */ static void tc_init_guards(void) @@ -383,20 +422,24 @@ static void tc_poll_reporting(void) sb_link_poll(2U * SB_LINK_REPORT_MS); CHECK(sb_log_n == 2U, "TC-SBLINK-012g nothing is emitted when no counter changed"); - /* 有错帧:再出一行,且 err/crc 列可见 */ + /* 有错帧:再出一行,且 crc 列可见。 + * 同时喂一帧正常召唤:本用例的时基是每步 +5 s,单靠错帧会让"连续 10 s 无**有效** + * 召唤帧"在第三步就成立(那是 2-5 的离线判定,由 TC-SBLINK-027 专测), + * 那行 OFFLINE 会插进来把行号错开,让本用例悄悄变成在测别的东西。 */ { uint8_t bad[SB_FRAME_A_LEN]; (void)memcpy(bad, k_rt, SB_FRAME_A_LEN); bad[5] ^= 0xFFU; feed(bad, SB_FRAME_A_LEN); + feed(k_rt, SB_FRAME_A_LEN); } sb_link_poll(3U * SB_LINK_REPORT_MS); CHECK(sb_log_n == 3U, "TC-SBLINK-012h a CRC error triggers a new report"); CHECK(sb_log_has(2U, "crc=1") == 1, "TC-SBLINK-012i the report carries the CRC error count"); sb_link_get_stats(&st); - CHECK(st.rx_events == 3U, "TC-SBLINK-012j (sanity) three events were fed in total"); + CHECK(st.rx_events == 4U, "TC-SBLINK-012j (sanity) four events were fed in total"); } static void tc_poll_wraparound(void) @@ -438,6 +481,328 @@ static void tc_stats_snapshot(void) CHECK(st.rearm_fail == 0U, "TC-SBLINK-014e ... rearm_fail"); } +/* ===================== 批 2 用例(帧 B 组帧 / 起发 / 收尾 / 按键 / 离线) ===================== */ + +static void tc_frame_update_guards(void) +{ + fill_bmp(0x11U); + + /* 未初始化:STATE。直接清 s_huart(本用例白盒:测试 TU 里 include 了 sb_link.c, + * 静态量可见)——否则本用例能否通过就取决于"有没有人先跑过 setup_link", + * 那是用例顺序依赖,加一条用例就可能把它弄红。 */ + s_huart = NULL; + sb_fake_reset(); + sb_log_reset(); + CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_ERR_STATE, + "TC-SBLINK-016 a frame update before init is refused"); + + (void)setup_link(); + CHECK(sb_link_frame_update(NULL, 0U) == BSP_ERR_PARAM, + "TC-SBLINK-016b a NULL framebuffer is refused"); +} + +static void tc_frame_build_content(void) +{ + uint32_t i; + uint32_t pad_bad = 0U; + sb_link_stats_t st; + + (void)setup_link(); + fill_bmp(0x11U); + + CHECK(sb_link_frame_update(s_bmp, 0xA5A55A5AU) == BSP_OK, + "TC-SBLINK-017 the first frame is built"); + CHECK(memcmp(s_frame, s_bmp, SB_FRAME_B_BMP_LEN) == 0, + "TC-SBLINK-017b ... the bitmap region is a verbatim copy of the live framebuffer"); + /* 0xA5A55A5A 小端 = 5A 5A A5 A5 */ + CHECK((s_frame[SB_FRAME_B_INFO_OFF + 0U] == 0x5AU) && + (s_frame[SB_FRAME_B_INFO_OFF + 1U] == 0x5AU) && + (s_frame[SB_FRAME_B_INFO_OFF + 2U] == 0xA5U) && + (s_frame[SB_FRAME_B_INFO_OFF + 3U] == 0xA5U), + "TC-SBLINK-017c ... the LED mask lands in info[0..3] little-endian"); + CHECK((s_frame[SB_FRAME_B_INFO_OFF + 4U] == 0U) && + (s_frame[SB_FRAME_B_INFO_OFF + 5U] == 0U), + "TC-SBLINK-017d ... info[4..5] is not the LOGO magic"); + CHECK((s_frame[SB_FRAME_B_INFO_OFF + 6U] == 0U) && + (s_frame[SB_FRAME_B_INFO_OFF + 7U] == 0U), + "TC-SBLINK-017e ... info[6..7] reserved is zero"); + for (i = SB_FRAME_B_PAD_OFF; i < SB_FRAME_B_CRC_OFF; i++) { + if (s_frame[i] != 0U) { + pad_bad++; + } + } + CHECK(pad_bad == 0U, "TC-SBLINK-017f ... all 998 padding bytes are zero"); + CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_LINK_FRAME_LEN) == BSP_OK, + "TC-SBLINK-017g ... and the CRC covers [0, 4205]"); + + sb_link_get_stats(&st); + CHECK(st.frame_build == 1U, "TC-SBLINK-017h ... counted exactly once"); + CHECK(st.frame_bad == 0U, "TC-SBLINK-017i ... self-check clean"); +} + +static void tc_frame_unchanged(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + fill_bmp(0x11U); + (void)sb_link_frame_update(s_bmp, 0U); + + /* 同样的内容再来一次:不重组。uiTask 每 10 ms 调一次,这条决定了常态成本只有 memcmp */ + CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_OK, + "TC-SBLINK-018 an unchanged framebuffer returns OK"); + sb_link_get_stats(&st); + CHECK(st.frame_build == 1U, "TC-SBLINK-018b ... without rebuilding the frame"); + + /* 点阵改一个字节:必须重组 */ + s_bmp[1234] ^= 0xFFU; + CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_OK, + "TC-SBLINK-018c a changed byte is picked up"); + sb_link_get_stats(&st); + CHECK(st.frame_build == 2U, "TC-SBLINK-018d ... and triggers a rebuild"); + + /* 点阵没变、只有掩码变:也要重组(info[0..3] 在 CRC 覆盖区内) */ + CHECK(sb_link_frame_update(s_bmp, 0x00000001U) == BSP_OK, + "TC-SBLINK-018e a changed LED mask is picked up"); + sb_link_get_stats(&st); + CHECK(st.frame_build == 3U, "TC-SBLINK-018f ... and triggers a rebuild too"); + CHECK(s_frame[SB_FRAME_B_INFO_OFF] == 0x01U, + "TC-SBLINK-018g ... with the new mask in info[0]"); +} + +static void tc_tx_kick(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + fill_bmp(0x22U); + + /* 还没组帧:召唤来了也不发(发一帧全 0 的图等于让屏板白屏,而且它不报错) */ + feed(k_rt, SB_FRAME_A_LEN); + sb_link_get_stats(&st); + CHECK(st.tx_kick == 0U, "TC-SBLINK-019 no frame is sent before the first build"); + CHECK(st.tx_busy_drop == 1U, "TC-SBLINK-019b ... the summon is counted as dropped instead"); + + (void)sb_link_frame_update(s_bmp, 0U); + + feed(k_rt, SB_FRAME_A_LEN); + sb_link_get_stats(&st); + CHECK(st.tx_kick == 1U, "TC-SBLINK-019c a valid summon kicks the transfer"); + CHECK(sb_tx_calls == 1U, "TC-SBLINK-019d ... through exactly one HAL call"); + CHECK(sb_tx_len == SB_LINK_FRAME_LEN, "TC-SBLINK-019e ... of 4208 bytes"); + CHECK(sb_tx_buf == s_frame, + "TC-SBLINK-019f ... from the static frame buffer (the DMA-reachable one)"); + + /* 屏板会一直召唤:上一帧没发完就再来一次 → 绝不叠发 */ + feed(k_rt, SB_FRAME_A_LEN); + sb_link_get_stats(&st); + CHECK(sb_tx_calls == 1U, + "TC-SBLINK-019g a second summon during TX does not stack another transfer"); + CHECK(st.tx_busy_drop == 2U, "TC-SBLINK-019h ... it is counted as dropped"); + + /* TxCplt 之后又能发了 */ + tx_complete(); + sb_link_get_stats(&st); + CHECK(st.tx_done == 1U, "TC-SBLINK-019i TxCplt is counted"); + feed(k_rt, SB_FRAME_A_LEN); + CHECK(sb_tx_calls == 2U, "TC-SBLINK-019j ... and the next summon goes out again"); +} + +static void tc_tx_busy_freezes_frame(void) +{ + uint8_t before[SB_FRAME_B_BMP_LEN]; + sb_link_stats_t st; + + (void)setup_link(); + fill_bmp(0x33U); + (void)sb_link_frame_update(s_bmp, 0U); + feed(k_rt, SB_FRAME_A_LEN); /* 起发 → busy */ + + (void)memcpy(before, s_frame, sizeof(before)); + + fill_bmp(0x77U); /* 画面变了,但正在发 */ + CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_ERR_BUSY, + "TC-SBLINK-020 a frame update during TX is refused"); + CHECK(memcmp(s_frame, before, sizeof(before)) == 0, + "TC-SBLINK-020b ... the buffer DMA is reading is left byte-for-byte untouched"); + + tx_complete(); + CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_OK, + "TC-SBLINK-020c ... and the pending change is picked up right after TxCplt"); + CHECK(memcmp(s_frame, s_bmp, sizeof(before)) == 0, + "TC-SBLINK-020d ... so no stale frame survives the busy window"); + sb_link_get_stats(&st); + CHECK(st.frame_build == 2U, "TC-SBLINK-020e ... exactly one rebuild, no double build"); +} + +static void tc_tx_kick_failure(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + fill_bmp(0x44U); + (void)sb_link_frame_update(s_bmp, 0U); + + sb_tx_force = SB_FAKE_TX_BUSY; + feed(k_rt, SB_FRAME_A_LEN); + sb_link_get_stats(&st); + CHECK(st.tx_kick_fail == 1U, "TC-SBLINK-021 a failed HAL kick is counted"); + CHECK(st.tx_kick == 0U, "TC-SBLINK-021b ... and is not counted as a transfer"); + + /* 失败必须把 busy 清掉,否则此后每次召唤都被判"上一帧还在发",链路再也不发 */ + sb_tx_force = SB_FAKE_TX_OK; + feed(k_rt, SB_FRAME_A_LEN); + sb_link_get_stats(&st); + CHECK(st.tx_kick == 1U, "TC-SBLINK-021c ... and the next summon retries successfully"); +} + +static void tc_tx_abort_on_error(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + fill_bmp(0x55U); + (void)sb_link_frame_update(s_bmp, 0U); + feed(k_rt, SB_FRAME_A_LEN); /* 起发,gState = BUSY_TX */ + + /* 真机:错误中断打断 TX 时 HAL 把 gState 打回 READY,但**不会**再调 TxCplt → + * busy 会永久挂着。这正是"拔掉屏板后无残留 busy"要防的那条。 */ + sb_ore_latched = 1U; + s_uart.RxState = HAL_UART_STATE_READY; + s_uart.gState = HAL_UART_STATE_READY; + s_uart.ErrorCode = HAL_UART_ERROR_ORE; + sb_link_on_error(&s_uart); + + sb_link_get_stats(&st); + CHECK(st.tx_abort == 1U, "TC-SBLINK-022 a stuck TX busy is cleared by the error callback"); + + s_uart.gState = HAL_UART_STATE_READY; + feed(k_rt, SB_FRAME_A_LEN); + CHECK(sb_tx_calls == 2U, "TC-SBLINK-022b ... so the link keeps transmitting afterwards"); + + /* 反例:TX 真的还在发(gState 仍是 BUSY_TX)时不得判死 */ + sb_ore_latched = 0U; + s_uart.RxState = HAL_UART_STATE_BUSY_RX; /* 非阻塞型错误:接收照跑 */ + s_uart.ErrorCode = HAL_UART_ERROR_NE; + sb_link_on_error(&s_uart); /* gState 仍 BUSY_TX */ + sb_link_get_stats(&st); + CHECK(st.tx_abort == 1U, "TC-SBLINK-022c ... but a still-running TX is not declared dead"); + CHECK(sb_tx_calls == 2U, "TC-SBLINK-022d ... and no extra transfer was started"); +} + +static void tc_key_queue(void) +{ + uint8_t i; + sb_link_stats_t st; + + (void)setup_link(); + + CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023 an empty queue yields 0"); + + feed(k_key, SB_FRAME_A_LEN); /* 屏板实测向量:key = 9 */ + CHECK(sb_link_key_take() == 9U, "TC-SBLINK-023b the key byte reaches the consumer"); + CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023c ... exactly once"); + + /* 周期召唤的 key 恒 0:不占队列(屏板自己的语义就是"没有按键事件") */ + feed(k_rt, SB_FRAME_A_LEN); + CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023d a periodic summon never queues a key"); + + /* 队列满:丢**新**的并计数(旧键已经在路上,新键还会有下一次召唤)。 + * 环形队列留一格判满 → 容量 = SB_LINK_KEY_QUEUE_LEN - 1 = 3,推 6 次丢 3 次。 */ + for (i = 0U; i < (uint8_t)(SB_LINK_KEY_QUEUE_LEN + 2U); i++) { + feed(k_key, SB_FRAME_A_LEN); + } + sb_link_get_stats(&st); + CHECK(st.key_drop == 3U, "TC-SBLINK-023e overflow drops the newest and counts it"); + + for (i = 0U; i < (uint8_t)(SB_LINK_KEY_QUEUE_LEN - 1U); i++) { + CHECK(sb_link_key_take() == 9U, "TC-SBLINK-023f ... FIFO order is preserved"); + } + CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023g ... and the queue is empty again"); +} + +static void tc_offline(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + feed(k_rt, SB_FRAME_A_LEN); + + sb_link_poll(0U); + sb_link_get_stats(&st); + CHECK(st.offline == 0U, "TC-SBLINK-024 a fresh link is online"); + + /* 9.999 s 无召唤:还不算离线(屏板自复位约 1 s 空窗,按 500 ms 判会反复误报) */ + sb_link_poll(SB_LINK_OFFLINE_MS - 1U); + sb_link_get_stats(&st); + CHECK(st.offline == 0U, "TC-SBLINK-024b just under 10 s of silence is still online"); + + /* 满 10 s:置离线,并且只打一行 */ + sb_log_reset(); + sb_link_poll(SB_LINK_OFFLINE_MS); + sb_link_get_stats(&st); + CHECK(st.offline == 1U, "TC-SBLINK-024c 10 s of silence marks the board offline"); + CHECK(sb_log_has(0U, "OFFLINE") == 1, "TC-SBLINK-024d ... with one dedicated line"); + + /* 再久也不重复刷屏 */ + sb_log_reset(); + sb_link_poll(SB_LINK_OFFLINE_MS + 1000U); + CHECK(sb_log_n == 0U, "TC-SBLINK-024e the offline line is not repeated every poll"); + + /* 屏板回来:一帧就够,立刻清标志并报恢复 */ + sb_log_reset(); + feed(k_rt, SB_FRAME_A_LEN); + sb_link_poll(SB_LINK_OFFLINE_MS + 1010U); + sb_link_get_stats(&st); + CHECK(st.offline == 0U, "TC-SBLINK-024f a single valid summon brings it back online"); + CHECK(sb_log_has(0U, "back online") == 1, "TC-SBLINK-024g ... and says so"); +} + +static void tc_frame_timing(void) +{ + sb_link_stats_t st; + + (void)setup_link(); + fill_bmp(0x66U); + + /* 每次读 CYCCNT 前进 16800 周期 = 100 µs @168 MHz;组帧读两次 → 差值正好 16800。 + * 这条钉的是"周期 → µs"的除数(SystemCoreClock/1e6),换算错了会在台架上 + * 把 0.17 ms 报成 17 ms 或 1.7 µs,两种都会被当成"组帧太慢/没测到"。 */ + sb_cyccnt_step = 16800U; + (void)sb_link_frame_update(s_bmp, 0U); + + sb_link_get_stats(&st); + CHECK(st.frame_us == 100U, "TC-SBLINK-025 the cycle count is converted with SystemCoreClock"); + CHECK(st.frame_us_max == 100U, "TC-SBLINK-025b ... and the peak is tracked"); + + s_bmp[0] ^= 0xFFU; + sb_cyccnt_step = 50400U; /* 300 µs */ + (void)sb_link_frame_update(s_bmp, 0U); + sb_link_get_stats(&st); + CHECK(st.frame_us == 300U, "TC-SBLINK-025c ... the last build duration"); + CHECK(st.frame_us_max == 300U, "TC-SBLINK-025d ... and the peak follows the slower build"); +} + +static void tc_tx_report(void) +{ + (void)setup_link(); + fill_bmp(0x88U); + (void)sb_link_frame_update(s_bmp, 0U); + + sb_log_reset(); + sb_link_poll(SB_LINK_REPORT_MS); + CHECK(sb_log_n == 1U, "TC-SBLINK-026 a changed frame counter produces a report"); + CHECK(sb_log_has(0U, "fb=1") == 1, "TC-SBLINK-026b ... carrying the build count"); + CHECK(sb_log_has(0U, "tx=0") == 1, "TC-SBLINK-026c ... and the transfer counters"); + + /* 这一拍会先出一行 OFFLINE(10 s 无有效召唤,2-5),但不该再出 TX 行。 + * 断言行数而不是"没看见 tx=",是为了把"离线判定排在周期上报之前"这个顺序也钉住。 */ + sb_log_reset(); + sb_link_poll(2U * SB_LINK_REPORT_MS); + CHECK(sb_log_n == 1U, "TC-SBLINK-026d only the offline line is emitted, no TX report"); + CHECK(sb_log_has(0U, "OFFLINE") == 1, "TC-SBLINK-026e ... and it is the offline one"); +} + int main(void) { printf("=== ScreenLink Link Tests ===\n"); @@ -476,6 +841,29 @@ int main(void) printf("-- stats snapshot --\n"); tc_stats_snapshot(); + printf("-- batch 2: frame update guards --\n"); + tc_frame_update_guards(); + printf("-- batch 2: frame B content --\n"); + tc_frame_build_content(); + printf("-- batch 2: change detection --\n"); + tc_frame_unchanged(); + printf("-- batch 2: TX kick from the RX callback --\n"); + tc_tx_kick(); + printf("-- batch 2: frame frozen while TX runs --\n"); + tc_tx_busy_freezes_frame(); + printf("-- batch 2: TX kick failure --\n"); + tc_tx_kick_failure(); + printf("-- batch 2: TX aborted by an error interrupt --\n"); + tc_tx_abort_on_error(); + printf("-- batch 2: key queue --\n"); + tc_key_queue(); + printf("-- batch 2: offline detection --\n"); + tc_offline(); + printf("-- batch 2: frame build timing --\n"); + tc_frame_timing(); + printf("-- batch 2: TX report line --\n"); + tc_tx_report(); + printf("\n=== ScreenLink Link Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; diff --git a/test/host/sb_link/stub/main.h b/test/host/sb_link/stub/main.h index da0e3b9d..a18b045d 100644 --- a/test/host/sb_link/stub/main.h +++ b/test/host/sb_link/stub/main.h @@ -35,6 +35,7 @@ typedef enum { /* RxState 的两个取值:错误回调里"要不要重挂"就靠它判(见 sb_link.c 的 on_error 说明) */ #define HAL_UART_STATE_READY (0x20U) #define HAL_UART_STATE_BUSY_RX (0x22U) +#define HAL_UART_STATE_BUSY_TX (0x24U) /* HAL 的错误位(ErrorCode):ORE 是阻塞型,NE/FE/PE 是非阻塞型 */ #define HAL_UART_ERROR_PE (0x01U) @@ -146,7 +147,70 @@ static HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *p_huart return HAL_OK; } -/* ===== ④ 测试用的复位/查询把手 ===== */ +/* ===== ④ 假 Transmit_DMA(批 2:起发在中断里,收尾在 TxCplt) ===== */ +#define SB_FAKE_TX_OK (0U) +#define SB_FAKE_TX_BUSY (1U) +#define SB_FAKE_TX_ERROR (2U) + +static uint32_t sb_tx_calls; +static uint32_t sb_tx_force; /* SB_FAKE_TX_* */ +static const uint8_t *sb_tx_buf; +static uint16_t sb_tx_len; + +static HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *p_huart, + const uint8_t *p_buf, + uint16_t size) +{ + sb_tx_calls++; + sb_tx_buf = p_buf; + sb_tx_len = size; + + if (sb_tx_force == SB_FAKE_TX_BUSY) { + return HAL_BUSY; + } + if (sb_tx_force == SB_FAKE_TX_ERROR) { + return HAL_ERROR; + } + /* 真机成功起发后 gState = BUSY_TX,TxCplt 才打回 READY。 + * 错误回调里"TX 是不是还在发"就靠这一位判(防残留 busy)。 */ + p_huart->gState = HAL_UART_STATE_BUSY_TX; + return HAL_OK; +} + +/* ===== ⑤ 假 DWT / CoreDebug / SystemCoreClock(组帧耗时统计用) ===== */ +/* 真机由 core_cm4.h 提供。宿主侧要能"推进"计数器,所以 CYCCNT 也做成函数指针 + * (与 SR / DR 同一手法):每读一次前进 sb_cyccnt_step,于是 sb_link 里那两次 + * 读之间的差值 = 一个 step,测试就能钉住 "周期 → µs" 的除数。 */ +typedef struct { + volatile uint32_t CTRL; + uint32_t (*cyccnt_cell)(void); +} DWT_TypeDef; + +typedef struct { + volatile uint32_t DEMCR; +} CoreDebug_TypeDef; + +#define CYCCNT cyccnt_cell() + +static uint32_t sb_cyccnt_val; +static uint32_t sb_cyccnt_step; /* 每次读前进多少周期(0 = 恒 0,默认) */ + +static uint32_t sb_cyccnt_cell(void) +{ + sb_cyccnt_val += sb_cyccnt_step; + return sb_cyccnt_val; +} + +static DWT_TypeDef g_sb_dwt = { 0U, sb_cyccnt_cell }; +static CoreDebug_TypeDef g_sb_coredebug; +static uint32_t SystemCoreClock = 168000000U; + +#define DWT (&g_sb_dwt) +#define CoreDebug (&g_sb_coredebug) +#define CoreDebug_DEMCR_TRCENA_Msk (0x01000000U) +#define DWT_CTRL_CYCCNTENA_Msk (0x00000001U) + +/* ===== ⑥ 测试用的复位/查询把手 ===== */ static void sb_fake_reset(void) { uint32_t i; @@ -161,6 +225,15 @@ static void sb_fake_reset(void) sb_arm_force = SB_FAKE_ARM_OK; sb_arm_buf = NULL; sb_arm_size = 0U; + + sb_tx_calls = 0U; + sb_tx_force = SB_FAKE_TX_OK; + sb_tx_buf = NULL; + sb_tx_len = 0U; + + g_sb_dwt.CTRL = 0U; + sb_cyccnt_val = 0U; + sb_cyccnt_step = 0U; } /* 寄存器读序列是否为期望的字符串(如 "SD"):长度不等或内容不符都返回 0 */ diff --git a/test/host/screenlink_bench.py b/test/host/screenlink_bench.py index c69870dc..7e66df6f 100644 --- a/test/host/screenlink_bench.py +++ b/test/host/screenlink_bench.py @@ -3,15 +3,22 @@ """屏板链路台架激励:用 PC + USB 转 TTL **顶替屏板**,按屏板的召唤节拍发 6 B 帧 A。 做什么: - 在一个 TTL 串口上按屏板的节拍(默认 280 ms)发帧 A,模拟屏板的周期召唤; - 可穿插按键帧(屏板有键时最早 205 ms 就会再来一帧,本脚本只在周期点替换内容, - 不额外插帧——批 1 只验"收得到、长度对、CRC 过",不验应答时序)。 - 主机侧的统计由固件自己打(`[SBLINK] rx=... ok=... crc=... len=...`), - 本脚本只负责"有东西可收",不读被测板——**收侧观测面是串口日志,不是本脚本**。 + 1. **发**:在一个 TTL 串口上按屏板的节拍(默认 280 ms)发帧 A,模拟屏板的周期召唤; + 可穿插按键帧(`--key N`)。 + 2. **收**(`--reply`,批 2 起):每发一帧就收主机回的 4208 B 帧 B,校验长度 / CRC / + 998 B 填充,量"召唤末位 → 应答首位"的间隔与整帧耗时,可选把点阵 dump 成 PBM。 + 主机侧的统计由固件自己打(`[SBLINK] rx=... tx=...`),**收侧观测面是串口日志**; + 本脚本的 `--reply` 是另一路观测(应答本身对不对),两者互补,不能互相替代。 为什么不用串口助手 GUI: 1-3 的判据含"连续 10 min 无丢帧",手点是做不到的;而且帧内容必须逐字节对 (CRC 低字节在前,写错一位就是 100 % CRC 错帧,与"波特率错"同形)。 + 2-2 的判据含"整帧 59.6 ms ±0.5 ms、≤200 ms 内结束",更是只能靠脚本量。 + +⚠ **USB 转 TTL 适配器量不到"帧内无间隙"**:USB 帧是 1 ms 量级,适配器把字节成批交上来, + 所以 `--reply` 报的 `span` 只能证明"整帧在 60 ms 量级内发完",不能证明"没有 ≥28 µs + 的间隙"(那是屏板断帧的硬约束,只能上逻辑分析仪或接真屏板时看)。别把 span 通过 + 当成那条判据通过。 接法(docs/任务规划/屏板链路.md §2 决策一): 适配器 TXD -> U5.B10(网络 ST_RX0,PA10) 适配器 RXD -> U5.A10(网络 ST_TX0,PA9) @@ -26,11 +33,15 @@ 用法(仓库根目录): & '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --seconds 30 & '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --seconds 600 # 10 min 判据 + & '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --reply --dump lcd.pbm --key N 每 N 个周期把召唤帧换成按键帧(键值在 1/2/3/4/7/8/9 里轮换;0 = 不发按键帧) + --reply 每帧收应答并校验(批 2 的 2-2 / 2-4) + --dump P 把最后一帧应答的点阵存成 PBM(P4,160x160,可直接看) -退出码:0 = 按时发完;1 = 串口打不开 / 发送失败;2 = 用法错误。 +退出码:0 = 按时发完且(`--reply` 时)所有应答都对;1 = 串口打不开 / 发送失败; + 2 = 用法错误;3 = 有应答不合格。 -设计依据:docs/任务规划/屏板链路.md §3 批 1 的 1-3 判据、§5 台架联调步骤第 2 步。 +设计依据:docs/任务规划/屏板链路.md §3 批 1 的 1-3 判据、批 2 的 2-2/2-4、§5 台架联调步骤第 2、3 步。 创建日期:2026-10-29 """ import argparse @@ -72,6 +83,76 @@ def build_frame(cmd, key=0): return body + bytes((crc & 0xFF, (crc >> 8) & 0xFF)) +# ---- 帧 B(主机 -> 屏板,4208 B):批 2 的应答校验 ---- +FRAME_B_LEN = 4208 +FRAME_B_BMP = 3200 +FRAME_B_INFO = 3200 +FRAME_B_PAD_OFF = 3208 +FRAME_B_CRC_OFF = 4206 +SCREEN_W = 160 +SCREEN_H = 160 + + +def verify_frame_b(data): + """校验一帧帧 B;返回 (是否合格, 说明)。 + + 只验"结构对不对":长度、CRC(覆盖 [0, 4205],低字节在前)、998 B 填充全 0、 + info[4..5] 不是 LOGO 魔数。**不验点阵内容**——那要么 dump 出来看(--dump), + 要么拿主机侧显存逐字节比(需要主机侧配合,台架这一级做不到)。 + """ + if len(data) != FRAME_B_LEN: + return False, "len=%d(应 %d)" % (len(data), FRAME_B_LEN) + + crc = crc16_modbus(data[:FRAME_B_CRC_OFF]) + if (data[FRAME_B_CRC_OFF] != (crc & 0xFF)) or (data[FRAME_B_CRC_OFF + 1] != ((crc >> 8) & 0xFF)): + return False, "CRC 不符(帧尾 %02X %02X,算得 %04X 低字节在前)" % ( + data[FRAME_B_CRC_OFF], data[FRAME_B_CRC_OFF + 1], crc) + + pad = data[FRAME_B_PAD_OFF:FRAME_B_CRC_OFF] + if any(pad): + bad = next(i for i, b in enumerate(pad) if b) + return False, "填充段非 0(偏移 %d)" % (FRAME_B_PAD_OFF + bad) + + if (data[FRAME_B_INFO + 4] == 0x89) and (data[FRAME_B_INFO + 5] == 0x45): + return False, "info[4..5] 是 LOGO 魔数(本应是普通图帧)" + + return True, "OK" + + +def dump_pbm(path, data): + """把点阵段存成 PBM(P4,160x160)。显存本身就是 1 bpp / 20 B 一行 / MSB 在前, + 与 PBM 的二进制布局逐字节同构,所以直接拼头 + 原字节即可。""" + with open(path, "wb") as fp: + fp.write(b"P4\n%d %d\n" % (SCREEN_W, SCREEN_H)) + fp.write(data[:FRAME_B_BMP]) + + +def capture_reply(ser, frame, budget_s=0.6): + """发一帧召唤,收主机回的 4208 B;返回 (数据, 写时刻, 首字节时刻, 末字节时刻)。 + + ⚠ **CH340 收不了 700 kbaud 的连续流**(实测 2026-10-29:4208 B 只收到 ~2373 B, + 等效约 9.5 kB/s)。这不是固件问题——同一次运行里固件侧 `tx=94 done=93` 说明 4208 B + 每次都发完了;CH340 的 RX FIFO 只有几十字节、USB 每 1 ms 才搬一次,70 kB/s 必然溢出。 + 要真正核对帧 B 的内容,换 FTDI / CP210x 之类的适配器,或等批 4 接真屏板。 + """ + ser.reset_input_buffer() + t_write = time.monotonic() + ser.write(frame) + ser.flush() + + buf = bytearray() + t_first = None + deadline = t_write + budget_s + while len(buf) < FRAME_B_LEN and time.monotonic() < deadline: + # 大块读:read(n) 会等到收满 n 或超时,比"按 in_waiting 一个字节一个字节读"快得多 + chunk = ser.read(min(1024, FRAME_B_LEN - len(buf))) + if chunk: + if t_first is None: + t_first = time.monotonic() + buf += chunk + return bytes(buf), t_write, t_first, time.monotonic() + + def main(): ap = argparse.ArgumentParser(description="屏板链路台架激励(PC 顶替屏板)") ap.add_argument("--port", required=True, help="USB 转 TTL 适配器的串口,如 COM6") @@ -81,16 +162,27 @@ def main(): help="召唤周期(ms),默认 280(屏板是 RT_count>55 -> 第 56 拍 x 5 ms)") ap.add_argument("--key", type=int, default=0, help="每 N 个周期把帧换成按键帧(0 = 只发周期召唤)") + ap.add_argument("--key-fixed", type=int, default=0, + help="固定发这个键值(1/2/3/4/7/8/9),与 --key 二选一。" + "用来「持续改画面」以便拿到多次组帧样本;2=UP / 3=DOWN 只移动高亮," + "不会确认任何设置(1=ENTER 可能改配置,别用来做长跑)") ap.add_argument("--glitch-baud", type=int, default=0, help="先按这个(错的)波特率灌一串垃圾再正常发,用来验 ORE/NE/FE 之后能否自愈" "(0 = 不灌;建议 115200)") ap.add_argument("--glitch-bytes", type=int, default=32, help="灌多少字节垃圾(默认 32)") + ap.add_argument("--reply", action="store_true", + help="每帧收主机回的 4208 B 应答并校验(批 2 的 2-2 / 2-4)") + ap.add_argument("--dump", default="", + help="把最后一帧应答的点阵存成 PBM(P4 160x160,可直接看)") args = ap.parse_args() if args.period_ms <= 0 or args.seconds <= 0: print("[ERR] --period-ms / --seconds 必须为正") return 2 + if args.dump and not args.reply: + print("[ERR] --dump 需要同时给 --reply(没有应答就没有点阵可存)") + return 2 rt_frame = build_frame(CMD_RT) print("RT frame : %s" % rt_frame.hex(" ").upper()) @@ -112,13 +204,24 @@ def main(): print("[WARN] 噪声注入失败(继续正常发):%s" % exc) try: - ser = serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0) + # 收应答时要给 read() 一个超时,否则 timeout=0 会让 read(n) 立刻返回空、空转 + ser = serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, + timeout=(0.1 if args.reply else 0)) except Exception as exc: # noqa: BLE001 - 打开失败原因要原样给人看 print("[ERR] 打开 %s @%d 失败:%s" % (args.port, args.baud, exc)) return 1 sent = 0 keys = 0 + reply_ok = 0 + reply_bad = 0 + lat_min = None + lat_max = 0.0 + lat_sum = 0.0 + span_min = None + span_max = 0.0 + span_sum = 0.0 + last_bmp = None t0 = time.monotonic() next_at = t0 try: @@ -130,7 +233,11 @@ def main(): time.sleep(min(next_at - now, 0.02)) continue - if args.key > 0 and (sent % args.key) == (args.key - 1): + if args.key_fixed > 0: + frame = build_frame(CMD_KEY, args.key_fixed) + keys += 1 + tag = "KEY %d" % args.key_fixed + elif args.key > 0 and (sent % args.key) == (args.key - 1): key = KEY_VALUES[keys % len(KEY_VALUES)] frame = build_frame(CMD_KEY, key) keys += 1 @@ -139,12 +246,40 @@ def main(): frame = rt_frame tag = "RT" - ser.write(frame) - ser.flush() - sent += 1 - if sent <= 3 or tag != "RT": - print(" #%d %s -> %s" % (sent, tag, frame.hex(" ").upper())) + if args.reply: + data, t_write, t_first, t_last = capture_reply(ser, frame) + good, why = verify_frame_b(data) + if t_first is None: + lat = span = None + else: + lat = (t_first - t_write) * 1000.0 + span = (t_last - t_first) * 1000.0 + lat_sum += lat + lat_min = lat if lat_min is None else min(lat_min, lat) + lat_max = max(lat_max, lat) + span_sum += span + span_min = span if span_min is None else min(span_min, span) + span_max = max(span_max, span) + if good: + reply_ok += 1 + last_bmp = data + else: + reply_bad += 1 + if sent < 3 or tag != "RT" or not good: + if t_first is None: + print(" #%d %s -> 无应答(%.0f ms 内没收到首字节)" % (sent + 1, tag, 600.0)) + else: + print(" #%d %s -> %d B %s lat=%.2f ms span=%.2f ms" + % (sent + 1, tag, len(data), ("OK" if good else "BAD: " + why), + lat, span)) + else: + ser.write(frame) + ser.flush() + if sent <= 3 or tag != "RT": + print(" #%d %s -> %s" % (sent + 1, tag, frame.hex(" ").upper())) + + sent += 1 next_at += args.period_ms / 1000.0 # 落后太多就别追了(PC 侧卡顿不该变成突发流量) if next_at < time.monotonic() - 1.0: @@ -157,8 +292,27 @@ def main(): elapsed = time.monotonic() - t0 print("[DONE] 发送 %d 帧(其中按键帧 %d)用时 %.1f s,平均周期 %.1f ms" % (sent, keys, elapsed, (elapsed * 1000.0 / sent) if sent else 0.0)) - print("[NEXT] 回看日志口:grep '[SBLINK]' —— 期望 rx == %d、ok == rx、crc=0、len=0" % sent) - return 0 + if args.reply: + print("[REPLY] 合格 %d / 不合格 %d" % (reply_ok, reply_bad)) + if reply_bad > 0 and reply_ok == 0: + print("[HINT] 一个都没收全:先怀疑适配器。CH340 的 RX 顶不住 700 kbaud 连续流" + "(4208 B 只到 ~2400 B 就是它的典型表现),换 FTDI / CP210x 再试。") + print("[HINT] 固件侧是否真发了:看日志口 grep '[SBLINK]' 的 tx / done / fb 三列。") + if reply_ok > 0: + print("[REPLY] 召唤末位→应答首位:min %.2f / avg %.2f / max %.2f ms" + "(判据 30 µs ~ 5 ms;USB 适配器自身有 1~2 ms 量级延迟,只能证上界)" + % (lat_min, lat_sum / reply_ok, lat_max)) + print("[REPLY] 整帧耗时(首字节→末字节):min %.2f / avg %.2f / max %.2f ms" + "(线上 59.6 ms;**不能**据此判「帧内无间隙」,见文件头)" + % (span_min, span_sum / reply_ok, span_max)) + print("[REPLY] 整帧 ≤200 ms 判据:%s" % ("PASS" if lat_max + span_max < 200.0 else "FAIL")) + if args.dump and last_bmp is not None: + dump_pbm(args.dump, last_bmp) + print("[DUMP] 点阵已存 %s(P4 160x160,可直接打开看)" % args.dump) + + print("[NEXT] 回看日志口:grep '[SBLINK]' —— 期望 rx == %d、ok == rx、crc=0、len=0%s" + % (sent, "、tx == ok、done == tx、drop=0、fail=0、abort=0" if args.reply else "")) + return 3 if reply_bad > 0 else 0 if __name__ == "__main__": From 9c6d150261caacd1a3ff835d2830f706e9de2bf7 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Thu, 29 Oct 2026 21:46:47 +0800 Subject: [PATCH 14/16] =?UTF-8?q?docs(=E5=B1=8F=E6=9D=BF):=20=E5=9B=9E?= =?UTF-8?q?=E5=A1=AB=E7=9C=9F=E5=B1=8F=E6=9D=BF=2010=20min=20=E8=BF=9E?= =?UTF-8?q?=E7=BB=AD=E9=9B=B6=E9=94=99=E5=AE=9E=E6=B5=8B=EF=BC=88=C2=A75.5?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E6=9C=AA=E6=B5=8B=E9=A1=B9=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 21:30~21:44 挂机窗口末轮计数:rx=4258 ok=4258 crc=0 err=0 bits=0x00 rearm=0(连错误中断都没进过);末段 5 s 18 帧 = 277.8 ms/帧; tx=4249 done=4248 drop 窗口内零新增 kerr=0 abor=0;keys=53 kq=0 (按压期间零丢弃);fb=902 bad=0、us max=812 µs < 1 ms 改口判据; 窗口内无 OFFLINE/back online 行。验收清单第 1 条主机侧半边闭环。 §7 清单状态同步:第 2 项 ✅;第 1 项拆成"主机侧已测/目视半边待测"; 5.5 接线教训顺延为 5.6 并同步三处跨文档引用。 --- docs/README.md | 1 + docs/任务规划/屏板链路.md | 15 ++- docs/子系统设计/屏板链路设计说明.md | 174 ++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 docs/子系统设计/屏板链路设计说明.md diff --git a/docs/README.md b/docs/README.md index cc5e9faf..d12a6a18 100644 --- a/docs/README.md +++ b/docs/README.md @@ -93,6 +93,7 @@ docs/ | [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍(现状说明)**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;节拍源为什么是 TIM3、DMA 硬约束、事件驱动消费与就绪门、实现落点、观测计数、性能特征与设计边界 | | [docs/子系统设计/故障录波设计说明.md](子系统设计/故障录波设计说明.md) | **故障录波设计与口径属主**:1 kHz 起步与 80 点口径的偏离登记、存原始码值的理由、四段所有权与三条并发陷阱、段文件字节布局、现场单通道回放显示契约、预算与 R6 前置 | | [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **B4 上行规约**:IEC 60870-5-104 被控站(TCP 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + 主机侧纯逻辑单测全栈单测 | +| [`屏板链路设计说明.md`](子系统设计/屏板链路设计说明.md) | **屏板串口链路(`Drivers/BSP/ScreenLink/`)实现记录**:一句话架构与数据流、`sb_proto`/`sb_link` 全部对外接口、八条关键实现决定(不留影子缓冲/busy 冻结/中断起发等)、资源占用实测(`s_frame` 落 RW_IRAM1 非 CCM)、单测与台架/真屏板冒烟证据链、接线教训(排针丝印视角陷阱)、坑账本、**尚未实测项清单**;任务分解与协议事实源仍在 `docs/任务规划/屏板链路.md` | | [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 | | [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / 保护判据挂起 / 重试对账的调用关系与设计意图 | | [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:recv/send 挂起请求架构、数据结构与挂起槽状态机、recv 五路 + send 四路完成语义、模块接口契约(14 函数)、实现落点(20 集成点)、调用方契约、设计边界与性能特征 | diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index fce41d2b..ea67bd7b 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -112,6 +112,15 @@ | `U5.B10`(网络 `ST_RX0`,PA10) | 屏板 `PA2`(屏板 USART2_TX) | 屏板发 → 主机收 | | 板地(`CGND`,或板上任一 GND 脚) | 屏板 `GND` | 共地,必须接 | +> ⚠ **本表的"屏板侧 PA 编号"与 2026-10-29 真屏板首通实测相反**:照表接 `rx` 彻底静默, +> **两根信号线对调后立即通**。表里"TX 对 RX"的方向逻辑没错,错的是屏板排针对 PA2/PA3 +> 谁是发的假定(未经屏板方确认)。**实操以排针丝印 + 计数为准**:主机 TX 接丝印 **RX**、 +> 主机 RX 接丝印 **TX**、GND 必接;接完看主机 `[SBLINK] rx` 是否涨(涨=方向对,与内容无关), +> 不涨当场对调。全证据与推论见 `docs/子系统设计/屏板链路设计说明.md` §5.3/§5.6—— +> 对调后屏板出了正确画面,等于屏板按 CRC 验收了主机的发送线**就是**它标 RX 的那根, +> 即**屏板实际发送脚是表中标 `PA3` 的那根**(与屏板固件"USART2 TX=PA2"的常规映射相反, +> 除非它的 USART2 做了重映射)。产品线束定版前向屏板方核实。 + 三条容易接错的地方: - **必须交叉**:TX 对 RX。同边对接(`U5.A10` 接屏板 `PA2`)两端口都静默,且没有任何报错可看。 @@ -234,7 +243,7 @@ |---|---|---|---| | 0-1 ✅ | **台架串口线挪位** | 硬件:**换成 USB 转 RS-232 适配器**(TTL 的 CH340 直连不了),接 RS232#1 的三个端子——适配器 TXD → **CN1.10**(板子 `RS232_RX1`)、RXD → **CN1.12**(板子 `RS232_TX1`)、GND → **CN1.7 或 CN1.8**(`CGND`)。端子号出自 `docs/硬件平台/V0.1.tel`;`U14` 是 RS232#3 的网口座,本通道用不上 | **已完成 2026-10-29**:COM3 在 RS-232 侧可读可抓(`serial-assistant` 正常) | | 0-2 ✅ | **日志口迁移(USART1 → RS-232)** | `console_log.c` 的 `huart1`、`USART1_IRQn`、`__HAL_RCC_USART1_CLK_*`、`MX_USART1_UART_Init()` 收口成**一处端口宏**,指向 **`huart5`(RS232#1,已定)** | **已完成 2026-10-29**(`f5161e15`):COM3 抓到完整开机日志、8 个锚点全中、启动到 `net init: OK` 661 ms;连续 10 min 1218 行零 `[ERR]`/零 dropped;USART1 已无控制台写入 | -| 0-3 ✅ | **落定 USART1 到屏板的接线** | 按决策一:TTL 直连屏板 UART2 侧(PA2/PA3);接法与验收方式见 §2 决策一下方的「接法与验收」 | **接法已落纸 2026-10-29**:`U5.A10` / `U5.B10` ↔ 屏板 `PA3` / `PA2` + 共地,交叉接。后半条"照此接线后 6 B 召唤帧能被收到"要等 **1-3** 的接收中断落地才测得到,实测随 1-3 一起做 | +| 0-3 ✅ | **落定 USART1 到屏板的接线** | 按决策一:TTL 直连屏板 UART2 侧(PA2/PA3);接法与验收方式见 §2 决策一下方的「接法与验收」 | **接法已落纸 2026-10-29**:`U5.A10` / `U5.B10` ↔ 屏板 TTL 排针 + 共地,交叉接。**真屏板实测(同日):照表对 PA 编号接不通、信号线对调后才通**——验收以"主机 `rx` 计数上涨"为准,屏板侧 PA2/PA3 的实际收发方向待向屏板方核实(见决策一表下 ⚠ 与 `docs/子系统设计/屏板链路设计说明.md` §5.6) | > PA10 已接线(用户 2026-10-28 确认),屏板链路**不需要额外飞线**。 @@ -271,7 +280,7 @@ > 真出问题的是 ORE(`0x08`)——那是处理不过来,本链路 6 B/280 ms 不该出现。 > 台架上没造出 ORE(CPU 读 DR 太快),那条路径由主机单测按 RM0090 的清除语义覆盖。 -### 批 2 · 协议实现(5 项,2026-10-29 全部收尾) +### 批 2 · 协议实现(5 项,2026-10-29 全部收尾;实现记录见 `docs/子系统设计/屏板链路设计说明.md`) | 序 | 任务 | 改动点 | 判据 | |---|---|---|---| @@ -412,7 +421,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 | 1 | 协议层单测(`test/host/screenlink/`) | 无硬件 | CRC 与组帧的正确性,可回归 | | 2 | PC **模拟屏板**:按屏板节拍发 6 B 召唤帧(脚本 `test/host/screenlink_bench.py`,`--port COM6 --key-fixed 2` 可持续改画面以拿多次组帧样本;`--glitch-baud 115200` 灌噪声验自愈;`--reply` 收应答) | **USB 转 TTL 模块**接 U5 的 `ST_RX0`/`ST_TX0`(PA10/PA9)+ 主机板 | 收侧**已做**(`ok == rx`、`crc=0`、`size=6`、噪声后自愈);发侧固件计数**已做**(`tx == done`、`drop=0`、`bad=0`、`fb` 随画面变化增长)。⚠ `--reply` 收不全 4208 B 是 CH340 的限制,不是固件问题,见批 2 下方那条 ⚠ | | 3 | 反向:PC 收主机帧,用脚本核对 4208 B / CRC / 显存内容 | 同上 | 帧内容正确性(与 `g_ui_fbuf` 逐字节比) | -| 4 | **接真屏板**(TTL 直连屏板 UART2 的 PA2/PA3 一侧) | 屏板实物 + 屏板 TTL 引出 | 出图 + 按键 + 状态灯全链路 | +| 4 | **接真屏板**(TTL 直连屏板 UART2 的 PA2/PA3 一侧) | 屏板实物 + 屏板 TTL 引出 | **冒烟首通 + 10 min 零错 2026-10-29**:出图(用户目视确认)、`rx=4258 ok=4258 crc=0 err=0`、应答 `tx=4249 done=4248 drop=0(窗口内) abor=0`、按键 `keys=53 kq=0`、`off` 自动恢复。⚠ 接线实测教训:**按排针丝印同名直连收不到,必须"主机 TX 接丝印 RX"**(丝印是屏板视角),且两板各供电须接第三根 GND——详见 `docs/子系统设计/屏板链路设计说明.md` §5.6。状态灯/长稳/时序仪器项仍未测 | | 5 | 产品形态:按最终线束接法定版 | 线束 | 产品形态验证 | > 第 2、3 步是**性价比最高的两步**:不用屏板、不用改硬件,就能把 90 % 的软件问题挖出来。 diff --git a/docs/子系统设计/屏板链路设计说明.md b/docs/子系统设计/屏板链路设计说明.md new file mode 100644 index 00000000..9cb3029f --- /dev/null +++ b/docs/子系统设计/屏板链路设计说明.md @@ -0,0 +1,174 @@ +# 屏板链路设计说明(主机侧实现记录) + +> 模块:`Drivers/BSP/ScreenLink/`(`sb_proto` + `sb_link`),任务 `screenlink`。 +> 本文档记**已实现的内容与实测证据**;任务分解、协议事实源、四个决策的来龙去脉在 +> `docs/任务规划/屏板链路.md`(下称规划文档),两边不重复维护同一张表。 +> 创建:2026-10-29(真屏板首通当晚落笔)。 + +--- + +## 1. 一句话架构 + +屏板是 Modbus 主站、每 280 ms 发一帧 6 B 召唤(帧 A),主机是从站 `0x01`: +**任务侧组好一帧 4208 B(帧 B)挂在手上,空闲中断校完召唤直接点 TX DMA 回一帧**; +屏板按键骑在召唤帧里上来,主机排给 UI。串口为主出口,7003 网络是同一份显存的镜像。 + +``` +uiTask(10 ms 节拍) USART1 空闲中断(屏板召唤到达) + ui_port_step() 渲染 sb_link_on_rx_event() + sb_link_frame_update(活显存, 掩码) ├─ sb_frame_a_parse 校验 6 B + ├─ 点阵 memcmp 没变 → 立刻返回(~20 µs) ├─ 按键 ≠ 0 → 入环形队列 + ├─ TX busy → BSP_ERR_BUSY,下拍再来 ├─ 帧有效 → sb_tx_kick() 点 TX DMA + └─ 变了 → sb_frame_b_build_image 组整帧 └─ 无条件重挂 ReceiveToIdle_IT + + sb_frame_b_verify 自校验 + → s_frame_ready = 1 DMA2_Stream7 TC 中断 + sb_link_key_take() 出队 sb_link_on_tx_cplt() 清 busy + → ui_task_board_key_map() 映射 UI_KEY_* + → ui_port_queue_key() + sb_link_poll(tick) 统计上报 + ≥10 s 无召唤判离线 +``` + +分层铁律:`sb_proto` 纯逻辑(不碰 HAL/寄存器/任务),原样 include 进 `test/host/` 跑单测; +`sb_link` 依赖 HAL 与中断上下文,仅 MCU。 + +## 2. 对外接口(单一出处在头文件,这里只列形状) + +| 接口 | 上下文 | 作用 | +|---|---|---| +| `sb_link_init(&huart1)` | 启动,`app_main.c` 步骤 2b | 交叉核对波特率(对不上报 ERR_STATE)+ 挂首轮接收 + 使能 DWT | +| `sb_link_receive_start()` | 中断/任务 | 重挂 IDLE 接收(一次性 API,漏一次链路永久静默) | +| `sb_link_on_rx_event(huart,size)` | 中断(rs485.c 转发) | 帧 A 校验、计数、按键入队、**点 TX DMA 应答**、重挂 | +| `sb_link_on_error(huart)` | 中断(rs485.c 转发) | 按 SR→DR 顺序清错、按需重挂 RX、按 `gState` 清被打断 TX 的残留 busy | +| `sb_link_on_tx_cplt(huart)` | 中断(rs485.c 转发) | 清 busy、计数;**不在中断里组帧** | +| `sb_link_frame_update(bmp,mask)` | uiTask | 按需组帧(memcmp 判变化),busy 返 BUSY,自校验不过不发 | +| `sb_link_key_take()` | uiTask | 出队屏板原始键号(0=空);映射归 uiTask | +| `sb_link_poll(now_ms)` | uiTask | 离线判定(每拍)+ 5 s 变化才打的 RX/TX 两行统计 | +| `sb_link_get_stats(&st)` | 任意 | 计数快照(rx/ok/crc/keys/tx/done/drop/fb/us/offline…) | +| `sb_crc16` / `sb_frame_a_parse` / `sb_frame_b_build_image` / `sb_frame_b_build_logo` / `sb_frame_b_verify` | 纯逻辑 | 协议层五件套,主机单测 59/59 | + +全局回调落点:`HAL_UARTEx_RxEventCallback` / `HAL_UART_ErrorCallback` / +`HAL_UART_TxCpltCallback` 三个的**唯一实现处都在 `rs485.c`**,各含一行 +`sb_link_on_*` 转发;按句柄过滤在 `sb_link` 内部做。三个必须收在一处见全—— +分开写,下一个加通道的人会以为 TxCplt 没人用而再定义一份(multiple definition)。 + +## 3. 关键实现决定(为什么是现在这个样子) + +1. **不留 3200 B 影子缓冲**:帧里的点阵段就是"上次组进去的那一份",直接拿它跟活显存 + memcmp。省下 3200 B SRAM 与每拍 0.15 ms 的 CRC,代价只是每拍一次 memcmp(~20 µs)。 +2. **busy 期间冻结帧缓冲**:DMA 读的就是 `s_frame` 本体,中途改写 = 半新半旧的脏帧 + (CRC 仍对、屏板照收、画面撕裂,极难归因)。宁可 `BSP_ERR_BUSY` 下一拍再组。 + 双缓冲被否:一召唤一答的节奏下"下一拍补组"最坏慢 10 ms,屏板 280 ms 周期无感, + 不值再花 4208 B。 +3. **起发在中断里**(规划文档的决策三):4208 B 线上 59.6 ms,任务里轮询 DR 一次几十 µs + 的 ISR 抢占就把帧切断(屏板按 1 字符时间断帧、且坏帧也清超时计数——现场只剩"画面不刷新")。 +4. **注键走 `ui_port_queue_key`、不绕 `ui_net_sink_inject_key`**:后者只是 7003 后端对 + 同一函数的转发,绕过去要多一次 include 且语义不变。两条入口进同一个输入队列。 +5. **错误中断顺手收拾 TX**:HAL 的 TX 被错误打断时不调 TxCplt,busy 会永久挂着, + 之后每次召唤都被判"上一帧还在发"→ rx 涨 tx 冻结。判据用 `huart->gState != BUSY_TX`。 +6. **离线判 10 s 不判 500 ms**:屏板 ≈5000 s 自复位有 ≈1 s 空窗,短窗会把"歇了口气" + 判成离线再"恢复",标志反复跳。 +7. **LED 掩码暂传 0**:状态灯映射表属批 3;帧结构不变,届时只换 `frame_update` 的实参。 +8. **键 9/10 丢弃留痕**:决策四未裁,先按 `docs/任务规划/屏板链路.md` §4.3 的表把 + 1/2/3/4/7/8 映射到 `UI_KEY_*`,9/10 出日志不注键。 + +## 4. 资源占用(实测,`.map` 可复核) + +| 项 | 值 | 备注 | +|---|---|---| +| `s_frame` | 4208 B @ `0x20006d98` | `Execution Region RW_IRAM1`——**不在 CCM**(DMA1/2 物理不可达 CCM,放错段=静默发旧数据) | +| `s_rx_buf` / `s_stats` 等 | 16 + 84 B | 同段 | +| 整模块增量 | Code 约 +1.5 KB | 批 2 前后 237,464 → 238,936 B | +| 全镜像 | RW_IRAM1 85,904 B (65.5 %) / RW_CCM 57,376 B (87.9 %) | 凭证 `scripts/size_attestation.json` | +| TX DMA | `DMA2_Stream7` CH4,NVIC 优先级 9 | CubeMX 已配好、此前全工程没人用;无需动 `.ioc` | +| 波特率 | USART1 = 700000,BRR=0x0078(误差 0 %) | 写在 `Src/usart.c` 的 `USER CODE BEGIN USART1_Init 2`(`.ioc` 不保存波特率),开机日志自证 | +| RX | 16 B + IDLE 中断 | 不上 RX DMA(屏板只发 6 B) | + +## 5. 实测记录 + +### 5.1 主机单测(可回归) + +- `test/host/sb_proto/`:**59/59**,含屏板实测向量与 CRC 表逐字节交叉验证。 +- `test/host/sb_link/`:**146/146**(批 2 新增 65 条)。桩把 `SR/DR/CYCCNT` 做成函数指针, + 能在主机上钉住:ORE 清除的 SR→DR 顺序(RM0090 语义在桩里真执行)、非阻塞错误**不得**重挂、 + busy 期间帧缓冲逐字节冻结、错误中断清残留 busy 但不误杀在发的 TX、帧 B 每个字段、 + 周期→µs 换算除数、离线 9.999 s/10 s 边界与恢复、按键 FIFO 与溢出丢新。 + +### 5.2 台架(PC + USB 转 TTL 顶替屏板,2026-10-29 白天) + +40 s `--key-fixed 2`:`rx=143 ok=143 keys=143 crc=0` / `tx=143 done=143 drop=0 abor=0 +fb=144 bad=0`;杜邦线噪声形态 `bits=0x02`(NE)每帧 ~1.3 次但数据零错——已定口径 +"NE/FE 而 rx_ok 照涨 = 能用的告警";错波特率灌 64 B 垃圾后自动恢复;激励停止 10 s +准点 OFFLINE。组帧耗时 **509 µs 典型 / 709 µs 峰值**(成本全在 4206 B 查表 CRC、512 B +表驻 Flash)——原 0.3 ms 估算判据已改口 1 ms,压降路径记录未实施,见规划文档批 2 的 ⚠。 + +### 5.3 真屏板冒烟首通(2026-10-29 21:24,接 §2 决策一的 TTL 线) + +| 证据 | 读数 | 结论 | +|---|---|---| +| `rx=125 ok=125 crc=0 len=0 fmt=0 err=0 bits=0x00 rearm=0` | 一字节没错、**连错误中断都没进过** | 真屏板线路质量优于台架杜邦线(台架每帧 ~1.3 次 NE) | +| 5 s ≈ 18 帧 → 278 ms | 与屏板固件 280 ms(第 56 拍 × 5 ms)吻合 | 对端确认是屏板本尊的周期召唤 | +| `tx=116 done=116 kerr=0 abor=0`,`drop=9` 集中在开机头两秒 | 一召一答、无双发、无失败 | 2-2/2-4 的时序骨架在真硬件上成立 | +| 错接期间 `off=1` → 对调线后连打三轮 `off=0` | 离线判定与自动恢复走通 | 2-5 判据闭环(全程未重启主机)。⚠ `back online` 那一行本身没取到证据——恢复发生在 21:24 抓日志的 `clear` 之前 ~10 s,行被抹掉了;但 `offline` 字段**只有**恢复分支会清,1→0 的转变即该分支执行过的证明 | +| **屏板出图,用户目视确认"非常完美"** | 见 §5.4 的推论 | 帧 B 的 4208/CRC 口径端到端正确 | + +### 5.4 "出图"这两个字的证明力(别低估它) + +屏板收帧**只有两种结局**:整包 CRC 过 → 刷屏;不过 → 静默丢弃,且它超时计数照清、 +没有任何报错通道(`docs/任务规划/屏板链路.md` §4.4 预言过的"只表现为画面不刷新")。所以肉眼看到正确画面,等于 +**屏板亲手验收了**:帧长 4208、CRC 覆盖 [0,4205]、**低字节在前**(两处命名相反的屏板 +源码口径)、1 bpp 20 B/行 MSB 在前的显存布局、info 字段布局——全部端到端正确。 +这是主机侧任何计数都给不出的一条证据,台架阶段(CH340 收不全)也补不了。 + +### 5.5 真屏板 10 min 连续零错(2026-10-29 21:30~21:44,验收清单第 1 条的主机侧半边) + +挂机窗口内(日志清零后累计)末轮计数: + +| 计数 | 读数 | 判据 | +|---|---|---| +| `rx=4258 ok=4258 crc=0 len=0 fmt=0 err=0 bits=0x00 rearm=0` | **连错误中断都没进过一次** | 10 min 零超时、零 CRC 错帧 ✓ | +| 末段 5 s 采样 4240→4258 = 18 帧 | 277.8 ms/帧 | 与屏板 280 ms 节拍吻合 ✓ | +| `tx=4249 done=4248 drop=9 kerr=0 abor=0` | `drop=9` 全部是开机头两秒(帧未就绪),窗口内零新增;`done` 差 1 是采样瞬间最后一帧还在发(59.6 ms 窗) | 一召一答、无双发、无失败 ✓ | +| `keys=53 kq=0` | 按压期间 53 个按键帧全部入队出队、**零丢弃** | 按键"逐键必到"的主机侧半边 ✓(动作语义合不合菜单树需目视,见 §7-1) | +| `fb=902 bad=0`,`us=687 max=812` | 10 min 组帧 902 次(待机时钟秒跳等 LVGL 自刷新驱动,≈1.5 次/s),自校验全程通过;峰值 812 µs < 1 ms 改口判据 | 常态组帧占空比对 10 ms 节拍 <7 % | +| 窗口内无 `OFFLINE`/`back online` 行 | 连接全程稳定 | — | + +### 5.6 接线实测教训(决策一的接法表照接后**不通**,对调才通) + +- 按规划文档决策一的表接"主机 A10(PA9)→屏板 PA3、B10(PA10)→屏板 PA2"**rx=0 彻底静默**; + **把两根信号线对调后立即通**。表本身的信号方向逻辑没错(TX 对 RX),错在**屏板侧引脚的 + 认定**——排针丝印 TX/RX 是"屏板自己发出/接收"的视角,与网表里 PA2=发送/PA3=接收的对应 + 关系未经屏板方确认。实操判据比丝印可靠:**接完看主机 `rx` 有没有计数**(rx 一动=方向对, + 与帧内容无关),不动就当场对调。产品形态线束定版前,以"rx 计数"为唯一验收口径。 +- 两块板各自供电必须接第三根线 GND;只连两根信号线时是浮地,形态同样是彻底收不到。 + +## 6. 坑账本(都已在代码注释/规划文档留痕,这里只汇总) + +1. 中文字面量 → ARMCC `#870-D`(源码无 BOM 按本地代码页解)。日志字面量纯 ASCII。 +2. `@test` 的 forbidden `FAIL;ERROR;HardFault;[ERR]` **大小写不敏感**——日志字段名带 + fail/error/abort 会把启动自检判红(`fail=0` 实测中招,已改 `kerr=`/`abor=`)。 +3. `ReceiveToIdle_IT` 一次性:漏重挂 = 链路永久静默;ORE 不自己清 = 一次噪声打死接收。 +4. NE/FE 非阻塞、ORE 阻塞:重挂判据用 `RxState==READY`,不看错误位。 +5. CH340 收不了 700 kbaud 连续流(4208 B 只到 ~2373 B)——**收不全 ≠ 固件没发**, + 同轮 `tx==done` 即自证。 +6. `.ioc` 不保存 UART 波特率;改波特率只能在 `USER CODE` 覆盖后重调 `HAL_UART_Init`。 +7. 纯注释改动也会让体积凭证报"不同码":先重建、再 `ram_baseline --update`。 + +## 7. 尚未实测项(按优先级) + +| # | 项 | 怎么测 | 状态(2026-10-29 夜) | +|---|---|---|---| +| 1 | **7 个物理键逐个按** | 真屏板已就位:每按一键,日志 `keys=` 应 +1、`kq` 恒 0,画面动作对照 `docs/产品需求/产品需求设计文档.md` §8.2 菜单树;键 9 应只出 `unmapped` 日志 | **半闭环**:10 min 窗口 `keys=53 kq=0`(必到、零丢弃)已实测(§5.5);剩"逐键动作合菜单树 + 键 9 出 unmapped 日志"需边按边看 | +| 2 | **10 min 连续零错**(屏板在线) | 挂机后 `grep SBLINK` 抽末行:`crc=len=fmt=err=rearm=bad=kerr=abor=0`、`tx≈rx`、`done=tx` | ✅ **2026-10-29 实测通过**(§5.5,rx=4258 零错) | +| 3 | **拔插回归**(正式版) | 拔屏板线 ≥10 s → 一条 OFFLINE;插回 ≤2 s 续图、一条 back online;期间 7003 与日志口不受影响 | 冒烟时被动验过一半(对调线前 off=1、对调后自动恢复);主动拔插 + 7003 并发面未测 | +| 4 | **uiTask 单轮增量**(判据"画面静止时 memcmp 成本 ~20 µs") | 画面静止 1 min,对比开屏板前后的 `[PROF] uiw`;按键刷屏时段观察 `uiw` 尖峰 ≈ 组帧 0.7 ms 以内 | 未测(§5.5 已给出组帧侧成本上限 `max=812 µs`,`[PROF]` 对照仍缺) | +| 5 | **2 h 长稳跨自复位** | 挂机 ≥5000 s + 余量:零误判离线(自复位 ≈1 s 空窗不该出 OFFLINE)、零 kerr/abort、恢复自动 | 无 | +| 6 | **召唤→应答延迟 30 µs~5 ms / 帧内无 ≥28 µs 间隙 / 59.6 ms** | 逻辑分析仪挂 `U5.A10/B10`;或 FTDI/CP210x 适配器 + `screenlink_bench.py --reply` 只能验整帧时长与内容(USB 1 ms 粒度量不了间隙) | 仪器 | +| 7 | **帧 B 与 `g_ui_fbuf` 逐字节比** | FTDI/CP210x 收满 4208 后 `--dump` 出 PBM 对 UI 目检;严格逐字节需 PC 收全帧脚本比对(CH340 做不了) | 仪器 | +| 8 | **双通道并发**(批 4) | 挂 DTU-RemoteLCD 持续拉帧 + 屏板同时召唤:两侧画面一致、串口发送期 7003 不超时、7003 拉帧期屏板不超时 | 无 | +| 9 | **7003 拥塞时串口照常应答** | 制造 netTask 往返卡顿(对时风暴/客户端不读),屏板侧应零超时、画面停最后一帧——"起发不在任务态"的直接证据 | 无 | +| 10 | **状态灯逐位**(批 3 功能,测随实现走) | 映射表落地后逐位点亮验证、LED 0 不受掩码影响 | 批 3 | +| 11 | **LOGO 断电留存**(批 3 功能) | 下发后重启屏板仍在、LED 不变;完成后追加"LOGO 写 Flash 期间拔线"负路径 | 批 3 | +| 12 | **键 9/10 语义裁定**(决策四) | 需要产品侧定 RESET/FACTORY 行为,代码已留 unmapped 分支 | 人 | +| 13 | 真屏板 ORE 路径 | 主机侧只能被动等:若某天 `bits` 出现 0x08,看 `rearm` 是否跟涨;台架造不出(CPU 读 DR 太快),已由主机单测按 RM0090 覆盖 | 机会性 | + +第 2 项已代跑通过(§5.5);第 3、4 项接线不动就能跑;第 1 项剩下半边需要你手按。 From 0b84c43d2ef84b6403e6cdef27918c29fe286aa6 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 00:39:57 +0800 Subject: [PATCH 15/16] =?UTF-8?q?feat(=E5=B1=8F=E6=9D=BF):=20=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=81=AF=E4=BD=8D=E6=98=A0=E5=B0=84=20sb=5Fled=5Fmap?= =?UTF-8?q?=20+=20104=20RUN=20=E8=A7=82=E6=B5=8B=E5=8F=A3=EF=BC=88?= =?UTF-8?q?=E6=89=B9=203=EF=BC=89=EF=BC=9B=E9=99=84=204-1=20=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E5=8E=8B=E6=B5=8B=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 批 3(LOGO 按用户裁定出局,不实现、sb_proto 的 LOGO 组帧挂待删标注): - 新增 App/algo/sb_led_map.c/h:业务状态 → 屏板 LED 位掩码的唯一映射表 (纯逻辑,主机单测可跑)。ID1 运行心跳 500ms 半周期闪(与主板 LED 同拍)、 ID2 保护动作、ID3 104 链路 RUN、ID4 采样电平故障(不选 sticky——灯要能灭)、 ID5 保护启动 picked;bit0 与 ID 6~19 恒 0。这张表参考源码里没有,属自定, 出处 docs/产品需求/产品需求设计文档.md §5.16.3 ①。 - iec104_serv 新增 iec104_serv_link_run()(透传 link 状态机,不另存镜像量)。 - ui_task.c 帧组装块每拍现采四路状态源(均为任意上下文可读观测口)折算后 传 sb_link_frame_update——替换批 2 的恒 0 占位;掩码变了帧就重组, 复用 busy/dirty 机制,无新通路。 - 主机单测 test/host/sb_led_map 17/17(位语义/恒零位/闪烁相位/tick 回绕/ NULL 契约);注册豁免走 SIM_EXEMPT(模拟器无屏板串口)。 批 4 的 4-1 双通道并发回归(真屏板在线实测): - 新增 test/host/remolcd_pull_test.py:RemoDispBus 拉帧压测客户端 (INIT→LCDMEM 10 Hz、逐帧 XOR+长度校验、往返延迟统计、--dump PBM)。 - 实测:网络侧两轮 2200 拉帧零错零超时(min 35/avg 63/max 131 ms); 屏板侧 60 s 隔离窗口 rx +233 全收、drop 零新增、bad=0、off=0、无 OFFLINE——两侧互不拖。观察项 len=7(断帧尺寸≠6,累计 7 次)出现窗口 无法归因压测、隔离窗口零新增、有效帧零损失,已挂设计说明 §5.7 待查。 附带:sb_link.h frame_update 注释回填实测耗时(509/688 µs 取代旧估算)。 Keil 0 错误 0 警告;ci_local 全绿(SKIP=nested-comment,主树脚本另查 0 新发); Code=239,064 B / RW_IRAM1 65.5 % / RW_CCM 87.9 %;覆盖率 90.1 % / 44 套件。 --- App/algo/sb_led_map.c | 58 ++++++ App/algo/sb_led_map.h | 101 +++++++++++ App/task/iec104_serv.c | 15 ++ App/task/iec104_serv.h | 13 ++ App/task/ui_task.c | 22 ++- Drivers/BSP/ScreenLink/sb_link.h | 6 +- Drivers/BSP/ScreenLink/sb_proto.h | 4 +- MDK-ARM/STM32F407-Demo.uvprojx | 5 + docs/内存管理/RAM使用分析与优化方案.md | 4 +- docs/内存管理/RAM预算台账.md | 2 +- scripts/check_registration.py | 1 + scripts/ram_baseline.tsv | 1 + scripts/size_attestation.json | 18 +- scripts/test_coverage_baseline.json | 62 ++++--- test/host/remolcd_pull_test.py | 219 +++++++++++++++++++++++ test/host/sb_led_map/run_unit_test.py | 42 +++++ test/host/sb_led_map/sb_led_map_test.c | 234 +++++++++++++++++++++++++ 17 files changed, 768 insertions(+), 39 deletions(-) create mode 100644 App/algo/sb_led_map.c create mode 100644 App/algo/sb_led_map.h create mode 100644 test/host/remolcd_pull_test.py create mode 100644 test/host/sb_led_map/run_unit_test.py create mode 100644 test/host/sb_led_map/sb_led_map_test.c diff --git a/App/algo/sb_led_map.c b/App/algo/sb_led_map.c new file mode 100644 index 00000000..e22da68e --- /dev/null +++ b/App/algo/sb_led_map.c @@ -0,0 +1,58 @@ +/* + * 模块名称:屏板状态灯位映射(业务状态 → LED 位掩码) + * 模块功能:sb_led_map_eval()——状态快照进、32 位 LED 掩码出。位定义与理由全在 + * sb_led_map.h(映射表唯一出处),本文件只做折算。 + * 适用平台:平台无关(纯标准 C) + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建初始版本(批 3 的 3-1) + */ + +#include /* NULL:纯逻辑不经 main.h,NULL 要自己带来 */ + +#include "sb_led_map.h" + +/* NULL 输入归一用的全零快照(static const:不占 RAM,语义就是"什么都没发生") */ +static const sb_led_in_t s_in_zero = { 0UL, 0UL, 0UL, 0U, { 0U, 0U, 0U } }; + +/* + * 函数功能:把状态快照折算成屏板 LED 32 位掩码(契约见 sb_led_map.h) + * 入口参数:p_in - 状态快照,允许 NULL(= 全零输入,只跑心跳) + * now_ms - 毫秒时基(只用于运行灯相位) + * 返回值:32 位 LED 掩码(bit i = 灯 ID i;本函数不触碰 bit0 与 ID 6~19) + * 限定条件:任意上下文,纯函数 + * 函数说明:心跳相位取 (now_ms / SB_LED_BLINK_HALF_MS) 的奇偶——用**除法取奇偶** + * 而不是"与上次比差值再翻转",因为后者要把相位存成静态量,纯函数就没了 + * (单测要能不开机就验完整个闪烁序列)。回绕只截短一个半周期,无害。 + */ +uint32_t sb_led_map_eval(const sb_led_in_t *p_in, uint32_t now_ms) +{ + const sb_led_in_t *q = (p_in != NULL) ? p_in : &s_in_zero; + uint32_t mask = 0UL; + + /* ID 1 运行心跳:偶数个 500 ms 段 = 亮 */ + if (((now_ms / SB_LED_BLINK_HALF_MS) & 1UL) == 0UL) + { + mask |= SB_LED_BIT_RUN; + } + + if (q->prot_active != 0UL) + { + mask |= SB_LED_BIT_ALARM; + } + if (q->iec104_run != 0U) + { + mask |= SB_LED_BIT_COMM; + } + if (q->adc_flags != 0UL) + { + mask |= SB_LED_BIT_ADC_FAULT; + } + if (q->prot_pick != 0UL) + { + mask |= SB_LED_BIT_PICKUP; + } + + return mask; +} diff --git a/App/algo/sb_led_map.h b/App/algo/sb_led_map.h new file mode 100644 index 00000000..4b3bb4ac --- /dev/null +++ b/App/algo/sb_led_map.h @@ -0,0 +1,101 @@ +/* + * 模块名称:屏板状态灯位映射(业务状态 → LED 位掩码) + * 模块功能:把装置的业务状态(保护动作 / 保护启动 / 采样故障 / 104 主站在线 / + * 运行心跳)折算成屏板帧 B `info[0..3]` 的 32 位 LED 掩码。 + * ⚠ 本层**纯逻辑**:不碰 HAL、不读全局、不打日志——输入是调用方 + * (uiTask)采好的状态快照,输出是一个 uint32_t → 主机侧可纯逻辑单测 + * (test/host/sb_led_map/)。与 Drivers/BSP/ScreenLink/sb_proto 同一分层理由。 + * 适用平台:平台无关(纯标准 C) + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建初始版本(批 3 的 3-1:映射表落纸,docs/任务规划/屏板链路.md §3) + * + * 这张表为什么必须"自定":屏板源码(DTU-LCD LED_Printf)只规定了"位 → 灯号"的 + * 物理口径(bit1~bit19 → ID 1~19,ID 0 电源灯硬件常亮、不看位,bit20~31 未使用), + * **"哪路状态占哪一位"在参考主机源码里不存在**(参考的 LCD_LED_Update 实现在 + * 未开源的屏板 BSP 库里)。出处 docs/产品需求/产品需求设计文档.md §5.16.3 ①。 + */ +#ifndef SB_LED_MAP_H +#define SB_LED_MAP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* ============================================================================ + * 一、位定义(屏板物理口径:bit i = 灯 ID i;bit0 被屏板忽略,别指望用它) + * ==========================================================================*/ + +/* ID 1 = 运行灯:装置活着的心跳,500 ms 半周期闪烁(与主板 LED1~3 心跳节奏同源, + * docs/产品需求/产品需求设计文档.md §5.8)。它**不取决于任何输入状态**——只要这段 + * 代码还在被调度,灯就在闪;灯不闪 = uiTask 停了,这正是操作员要看的信号。 */ +#define SB_LED_BIT_RUN (1UL << 1) + +/* ID 2 = 告警灯:任一保护元件处于**动作**态(prot_elm_active_mask != 0)。 + * ⚠ 产品构建处于「未标定闭锁」(App/task/protect_task.c 的 VALID 门禁), + * 标定前恒灭——与屏上"保护动作 N"同一已知口径,不是本灯的缺陷。 */ +#define SB_LED_BIT_ALARM (1UL << 2) + +/* ID 3 = 通信灯:IEC 60870-5-104 链路处于 RUN(STARTDT 已确认 = 主站真的在线在 + * 轮询)。选 RUN 而不选"TCP 已建连":连上不发 STARTDT 的扫描器不该点亮它。 */ +#define SB_LED_BIT_COMM (1UL << 3) + +/* ID 4 = 采样故障灯:ADC 采样有效性**电平**故障位非零(adc_health_flags, + * 静默/倒退等,当前活跃、可自愈熄灭)。选电平不选 sticky:这盏灯要能熄灭, + * "曾经故障过"的账在自检记录里,不该用一盏长明灯喊给操作员。 */ +#define SB_LED_BIT_ADC_FAULT (1UL << 4) + +/* ID 5 = 保护启动灯:任一保护元件处于**启动**(picked)态(prot_elm_pick_mask)。 + * picked 早于 asserted,"越限启动了但没到动作点"这段过程只在这盏灯上可见 + * (与 104「保护启动」记录同一动机,App/algo/prot_elm.h 的 pick_mask 注释)。 + * 同样受未标定闭锁口径约束。 */ +#define SB_LED_BIT_PICKUP (1UL << 5) + +/* 已赋义位的并集(判据用:掩码里不得出现本表之外的置位)。 + * ID 6~19 保留未用,恒 0;将来扩展从 ID 6 顺序取,跳号留白不回收语义。 */ +#define SB_LED_BITS_DEFINED (SB_LED_BIT_RUN | SB_LED_BIT_ALARM | SB_LED_BIT_COMM | \ + SB_LED_BIT_ADC_FAULT | SB_LED_BIT_PICKUP) + +/* 运行灯闪烁半周期(ms):0 ~ SB_LED_BLINK_HALF_MS-1 亮,下一个 500 ms 灭,交替。 + * 取 500 是为与主板 LED 心跳(defaultTask 500 ms 翻转)同拍,两套灯闪烁同相。 */ +#define SB_LED_BLINK_HALF_MS (500U) + +/* ============================================================================ + * 二、状态输入快照(调用方每拍现采,本模块不缓存、不加锁) + * ==========================================================================*/ + +typedef struct +{ + uint32_t prot_active; /* prot_elm_active_mask():bit i = 元件 i 动作 */ + uint32_t prot_pick; /* prot_elm_pick_mask():bit i = 元件 i 启动 */ + uint32_t adc_flags; /* adc_health_flags():ADC_HF_* 电平故障位 */ + uint8_t iec104_run; /* 1 = IEC 104 链路 RUN(iec104_serv_link_run()) */ + uint8_t pad[3]; /* 显式补齐:结构体大小固定 16 B,主机/目标端一致 */ +} sb_led_in_t; + +/* ============================================================================ + * 三、求值入口 + * ==========================================================================*/ + +/* + * 函数功能:把状态快照折算成屏板 LED 32 位掩码(纯函数,映射表唯一出处见本头 §一) + * 入口参数:p_in - 状态快照;**允许 NULL**:等价于全零输入(只跑运行灯心跳—— + * 心跳证明的是"这段代码活着",与别的状态无关,不该被空输入连坐) + * now_ms - 当前毫秒时基(运行灯相位唯一依赖;osKernelGetTickCount 同域) + * 返回值:32 位 LED 掩码(bit i = 灯 ID i;bit0 与 ID 6~19 恒 0,直接进帧 B info[0..3]) + * 限定条件:任意上下文(纯计算:无分支副作用、不读写任何静态量、时基回绕安全—— + * 回绕瞬间至多把一个半周期截短或延长 1 拍,500 ms 的心跳灯看不出来) + * 函数说明:闪烁相位 = (now_ms / 500) 的奇偶,偶 = 亮。上电 first 500 ms 为亮, + * "刚上电灯就在闪"是最直观的自检信号。各状态位相互独立,任一非零即置位, + * 无优先级、无互斥(告警与通信可以同时亮,那是两件同时在真发生的事)。 + */ +uint32_t sb_led_map_eval(const sb_led_in_t *p_in, uint32_t now_ms); + +#ifdef __cplusplus +} +#endif + +#endif /* SB_LED_MAP_H */ diff --git a/App/task/iec104_serv.c b/App/task/iec104_serv.c index 20a4c71e..fa42f47c 100644 --- a/App/task/iec104_serv.c +++ b/App/task/iec104_serv.c @@ -282,6 +282,21 @@ void iec104_serv_get_stats(iec104_serv_stats_t *p_out) *p_out = s; } +/* + * 函数功能:读 104 链路是否处于 RUN 态(契约见 iec104_serv.h 的 iec104_serv_link_run) + * 入口参数:无 + * 返 回 值:1 = RUN;0 = 其它 uint8_t + * 限定条件:任意上下文;单枚举字读,原子 + * 函数说明:直接透传 iec104_link_state(),不另存镜像量——镜像量一旦和状态机 + * 分家(比如 open/close 路径漏更),屏灯就成第二本假账。link_state + * 本身只读实例里的 state 字段(App/protocol/iec104/iec104_link.c), + * 无副作用,跨任务读安全。 + */ +uint8_t iec104_serv_link_run(void) +{ + return (iec104_link_state(&s_link) == IEC104_ST_RUN) ? 1U : 0U; +} + /* * 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍) * 返回值:无 diff --git a/App/task/iec104_serv.h b/App/task/iec104_serv.h index c8a5309b..0403c0e0 100644 --- a/App/task/iec104_serv.h +++ b/App/task/iec104_serv.h @@ -52,4 +52,17 @@ typedef struct { */ void iec104_serv_get_stats(iec104_serv_stats_t *p_out); +/* + * 函数功能:读 104 链路是否处于 RUN 态(STARTDT 已确认 = 主站在线在轮询) + * 返 回 值:1 = RUN;0 = 其它(IDLE/OPEN/STOPPED 或未创建任务) uint8_t + * 限定条件:**任意上下文可读**——单枚举字读,不取锁、不碰 socket、不阻塞 + * (与 iec104_serv_get_stats 同一形态与同一读者群)。 + * 函数说明:写者 = iec104Task(link 状态机推进),读者按需采样;单字读写在 + * Cortex-M4 上原子,最多读到"晚一拍"的状态,无撕裂风险。 + * 消费点 = 屏板状态灯 ID 3(App/algo/sb_led_map.h 的通信灯)。 + * ⚠ 别拿它换算 conn_up:RUN 是电平,conn_up 是累计边沿,语义不可互换 + * (同文件统计注释里的"不得互相换算"纪律)。 + */ +uint8_t iec104_serv_link_run(void); + #endif /* IEC104_SERV_H */ diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 486fc1b3..6d5e5034 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -54,6 +54,9 @@ #include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */ #include "sys_clock.h" #include "prot_elm.h" /* 道F task-3 装置维护:强制复归请求 + 动作态元件位图 */ +#include "adc_health.h" /* 屏板状态灯 ID 4:ADC 电平故障位(adc_health_flags) */ +#include "sb_led_map.h" /* 屏板状态灯位映射(批 3:业务状态 → info[0..3] 掩码) */ +#include "iec104_serv.h" /* 屏板状态灯 ID 3:104 链路 RUN 态(任意上下文可读) */ #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */ #include "task_watchdog.h" @@ -527,10 +530,23 @@ void ui_task_func(void *argument) { * 排在推流往返**之前**,是因为那个往返最坏能等 ≈4 s(LCDMEM 应答 3204 B), * 排在它后面会让屏板画面跟着网络一起卡(同一份显存、两个出口,出口不能互相拖)。 * 显存直接取活帧缓冲:不留 3200 B 影子副本,sb_link 内部与帧里的点阵段 memcmp。 - * LED 掩码暂传 0(批 3 才落状态灯映射表),故 info[0..3] 现恒为 0。 */ + * LED 掩码(批 3):每拍现采四路状态源经 sb_led_map_eval 折算——四个源 + * 都是"任意上下文可读"的观测口(不取锁不阻塞,见各自头文件契约), + * 折算本身纯计算;心跳灯相位跟着这一拍的 tick 走(uiTask 10 ms 节拍 + * 远高于 500 ms 半周期,闪烁边沿误差 ≤1 拍)。 */ { - bsp_err_t sb_ret = sb_link_frame_update(ui_port_get_framebuffer(), 0u); - uint8_t board_key; + sb_led_in_t led_in; + uint32_t led_mask; + bsp_err_t sb_ret; + uint8_t board_key; + + led_in.prot_active = prot_elm_active_mask(); + led_in.prot_pick = prot_elm_pick_mask(); + led_in.adc_flags = adc_health_flags(); + led_in.iec104_run = iec104_serv_link_run(); + led_mask = sb_led_map_eval(&led_in, osKernelGetTickCount()); + + sb_ret = sb_link_frame_update(ui_port_get_framebuffer(), led_mask); /* BSP_ERR_BUSY 是常态(上一帧还在 59.6 ms 的发送窗口里),不必留痕; * 其余失败打一行即可(见 s_sb_frame_err_logged 的说明)。 */ diff --git a/Drivers/BSP/ScreenLink/sb_link.h b/Drivers/BSP/ScreenLink/sb_link.h index e9678299..63bd1ea7 100644 --- a/Drivers/BSP/ScreenLink/sb_link.h +++ b/Drivers/BSP/ScreenLink/sb_link.h @@ -214,14 +214,16 @@ void sb_link_poll(uint32_t now_ms); /* * 函数功能:组一帧帧 B(任务侧,内容/掩码变化时才真组) * 入口参数:p_live_bmp - 当前 1bpp 显存首地址(>= SB_FRAME_B_BMP_LEN 字节,非空) - * led_mask - 状态灯 32 位掩码(小端填进 info[0..3];批 3 才填真值,此处传 0) + * led_mask - 状态灯 32 位掩码(小端填进 info[0..3];位的语义见 + * App/algo/sb_led_map.h——位变了帧就重组,灯随下一轮召唤出去) * 返回值:BSP_OK 帧已就绪(本次没变,或已重新组好) * BSP_ERR_PARAM p_live_bmp 为空 * BSP_ERR_STATE 尚未 sb_link_init * BSP_ERR_BUSY TX DMA 正在发这一帧,本拍跳过(TxCplt 之后的下一拍会补上) * BSP_ERR_CRC 组完自校验失败(已计数 frame_bad,帧不会发出去) * 限定条件:**任务上下文**,且必须与 ui_port_step() 同一个任务(显存是单写者)。 - * 单帧约 0.17 ms(3200 B 拷贝 + 4206 B CRC),只在变化时付一次。 + * 变化帧实测 ≈0.5 ms(典型)/ 0.8 ms(峰值),没变只付 memcmp(≈20 µs); + * 判据口径见 docs/任务规划/屏板链路.md §3 的 2-1。 * 函数说明:1. **不持有显存副本**——直接拿 p_live_bmp 与帧缓冲里的点阵段 memcmp, * 没变就立刻返回(约 20 µs)。省掉 3200 B 影子缓冲,也省掉每拍重算 CRC。 * 2. **TX 占用期间绝不写帧缓冲**(那会让 DMA 发出半新半旧的"脏帧"): diff --git a/Drivers/BSP/ScreenLink/sb_proto.h b/Drivers/BSP/ScreenLink/sb_proto.h index 0682f73e..2b1a1189 100644 --- a/Drivers/BSP/ScreenLink/sb_proto.h +++ b/Drivers/BSP/ScreenLink/sb_proto.h @@ -58,7 +58,9 @@ extern "C" { #define SB_CMD_KEY (0x00U) /* 命令类型:按键事件(立即召唤) */ #define SB_CMD_RT (0x01U) /* 命令类型:周期刷新 */ -/* 帧 B 的 LOGO 魔数(info[4] / info[5]) */ +/* 帧 B 的 LOGO 魔数(info[4] / info[5]) + * ⚠ 待删除(用户裁定 2026-10-29):LOGO 功能出局,不下发、不接线;本组 + * LOGO 常量与 sb_frame_b_build_logo() 保留仅为协议层完整性,**勿新增调用**。 */ #define SB_LOGO_MAGIC_HI (0x89U) #define SB_LOGO_MAGIC_LO (0x45U) diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index e1f92f36..e0fbed65 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1052,6 +1052,11 @@ 1 ..\App\algo\freq.c + + sb_led_map.c + 1 + ..\App\algo\sb_led_map.c + ui_data_feed.c 1 diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 662a4f6a..6e180e12 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 268,076 B (261.79 KB) | 1,048,576 B (1024 KB) | 25.6% | +| ER_IROM1 | 268,220 B (261.93 KB) | 1,048,576 B (1024 KB) | 25.6% | | RW_IRAM1 | 85,904 B (83.89 KB) | 131,072 B (128 KB) | 65.5% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **270,008 B (263.7 KB)** | 1,048,576 B (1024 KB) | **25.7%** | +| **Flash (ROM 合计)** | **270,152 B (263.8 KB)** | 1,048,576 B (1024 KB) | **25.8%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 16ee11e4..55112558 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,7 +15,7 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 268,076 B | 1,048,576 B | 25.6% | 参考 | +| ER_IROM1 (Flash) | 268,220 B | 1,048,576 B | 25.6% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | | RW_IRAM1 | 85,904 B | 131,072 B | 65.5% | PASS | diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 222149dd..15613e78 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -98,6 +98,7 @@ SIM_EXEMPT = [ ("App/algo/adc_cal.c", "标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"), ("App/algo/meas.c", "测频与遥测快照发布纯算法(唯一发布者 = MCU 侧 adcTask 200 ms 分频;104 改读快照与 UI 侧消费属另一波次另行接线,模拟器无消费路径;主机侧纯逻辑单测 test/host/meas 覆盖),模拟器无消费路径"), ("App/algo/run_par.c", "运行参数段纯算法 + [run_par] cfg 段注册 + 104 地址映射(写者 = iec104Task 直通、属主落盘 defaultTask;模拟器不跑 104 协议,模拟器无消费路径;主机侧纯逻辑单测 test/host/run_par 覆盖),模拟器无消费路径"), + ("App/algo/sb_led_map.c", "屏板状态灯位映射纯算法(唯一消费者 = MCU 侧 uiTask 经 sb_link 组帧;模拟器无屏板串口,模拟器无消费路径;主机侧纯逻辑单测 test/host/sb_led_map 覆盖),模拟器无消费路径"), ("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"), ("App/app/ui_data_feed.c", "UI 快照装配器(依赖 phasor/prot_set/iec104 端口常量等固件侧数据源);模拟器不跑装配链,快照由 simulator/main.c 直接发布固定帧,模拟器无消费路径"), ("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"), diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index a7605711..bf48a619 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -12,3 +12,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-27 235372 26392 1572 136924 261764 25.0 81072 61.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 2026-10-28 236456 28124 1876 137084 264580 25.2 81536 62.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 2026-10-29 238936 29140 1932 141396 268076 25.6 85904 65.5 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;sb_link.o=4364;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055 +2026-10-30 239064 29156 1932 141396 268220 25.6 85904 65.5 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;sb_link.o=4364;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 78c0bf59..4da0b49f 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,17 +1,17 @@ { - "built_at": "2026-10-29T20:54:02+08:00", - "fingerprint": "1297b04cfaed4c9b8c30a1fcc64608932d3d047a32ad687274a6eac40bc4ca45", - "fingerprint_files": 1456, + "built_at": "2026-10-30T00:21:57+08:00", + "fingerprint": "1cbc1bc0c030169d1851e6303e0d643ae1f01632f03883215557416e7f3eb4ee", + "fingerprint_files": 1458, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "e5b4182a9f02dfe9f1f06dd58d086f64a157cc5c", + "git_head": "9c6d150261caacd1a3ff835d2830f706e9de2bf7", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793278440, + "map_mtime": 1793290914, "pct": { - "ER_IROM1": 25.57, + "ER_IROM1": 25.58, "RW_CCM": 87.89, "RW_IRAM1": 65.54, "UNINIT_CCM": 18.75 @@ -19,7 +19,7 @@ "regions": { "ER_IROM1": { "max": 1048576, - "size": 268076 + "size": 268220 }, "RW_CCM": { "max": 65280, @@ -37,8 +37,8 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 238936, - "ro_data": 29140, + "code": 239064, + "ro_data": 29156, "rw_data": 1932, "zi_data": 141396 }, diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index 620e576b..6c091b7d 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -80,6 +80,14 @@ "lines_covered": 41, "lines_valid": 41 }, + "App/algo/sb_led_map.c": { + "func_rate": 1.0, + "funcs_covered": 1, + "funcs_valid": 1, + "line_rate": 1.0, + "lines_covered": 15, + "lines_valid": 15 + }, "App/cfg/cfg_file.c": { "func_rate": 0.963, "funcs_covered": 26, @@ -193,12 +201,12 @@ "lines_valid": 100 }, "App/task/iec104_serv.c": { - "func_rate": 0.8182, + "func_rate": 0.75, "funcs_covered": 9, - "funcs_valid": 11, - "line_rate": 0.7706, + "funcs_valid": 12, + "line_rate": 0.7572, "lines_covered": 131, - "lines_valid": 170 + "lines_valid": 173 }, "App/util/boot_stats.c": { "func_rate": 1.0, @@ -482,9 +490,9 @@ }, "iec104_serv": { "files": { - "App/task/iec104_serv.c": 170 + "App/task/iec104_serv.c": 173 }, - "lines_total": 170, + "lines_total": 173, "ok": true }, "lftpd": { @@ -623,6 +631,13 @@ "lines_total": 961, "ok": true }, + "sb_led_map": { + "files": { + "App/algo/sb_led_map.c": 15 + }, + "lines_total": 15, + "ok": true + }, "sb_link": { "files": { "Drivers/BSP/ScreenLink/sb_link.c": 207, @@ -739,6 +754,7 @@ "App/algo/prot_set.c", "App/algo/run_par.c", "App/algo/sample_ring.c", + "App/algo/sb_led_map.c", "App/cfg/cfg_file.c", "App/cfg/cfg_file_store_mcu.c", "App/cfg/prot_set_store.c", @@ -780,16 +796,16 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 48, - "files_untested": 165, - "func_rate": 0.9339, - "funcs_covered": 551, - "funcs_valid": 590, - "line_rate": 0.9011, - "lines_covered": 7334, - "lines_valid": 8139, - "modules_ok": 43, - "modules_total": 43 + "files_tested": 49, + "files_untested": 166, + "func_rate": 0.9324, + "funcs_covered": 552, + "funcs_valid": 592, + "line_rate": 0.9009, + "lines_covered": 7349, + "lines_valid": 8157, + "modules_ok": 44, + "modules_total": 44 }, "untested_sources": [ { @@ -833,7 +849,7 @@ "path": "App/task/adc_task.c" }, { - "code_lines": 245, + "code_lines": 253, "path": "App/task/ui_task.c" }, { @@ -1116,6 +1132,10 @@ "code_lines": 27, "path": "Drivers/BSP/UI/ui_w_setview.h" }, + { + "code_lines": 26, + "path": "App/algo/sb_led_map.h" + }, { "code_lines": 26, "path": "App/util/task_prof.h" @@ -1264,6 +1284,10 @@ "code_lines": 12, "path": "App/app/app_main.h" }, + { + "code_lines": 12, + "path": "App/task/iec104_serv.h" + }, { "code_lines": 12, "path": "Drivers/BSP/GD5F2GQ5UE/nand_ftl.h" @@ -1280,10 +1304,6 @@ "code_lines": 11, "path": "App/sync/time_sync.h" }, - { - "code_lines": 11, - "path": "App/task/iec104_serv.h" - }, { "code_lines": 11, "path": "App/task/net_task.h" diff --git a/test/host/remolcd_pull_test.py b/test/host/remolcd_pull_test.py new file mode 100644 index 00000000..72f23bce --- /dev/null +++ b/test/host/remolcd_pull_test.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""RemoDispBus(7003) 拉帧压力客户端:批 4 的 4-1「双通道并发回归」的网络侧激励。 + +做什么: + 连 192.168.1.100:7003,先发 INIT 查询屏幕参数(应得 8 B:宽u16/高u16/帧字节数u32, + 大端),随后按固定节拍持续发 LCDMEM(偏移 0) 拉整帧 3200 B 显存,逐帧校验 + XOR 校验和与长度,统计请求数 / 应答数 / CRC 错 / 超时 / 往返延迟 min-avg-max。 + 可 --dump last_frame.pbm 存最后一帧(1bpp 布局与屏板帧 B 点阵段逐字节同构)。 + +为什么它能当 4-1 的激励: + 7003 的每次 LCDMEM 往返在固件里是「uiTask 入队 + 等 netTask 回执」 + (osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),最坏 ≈4 s, + 见 ui_net_sink.h 的 ⚠),即**每一拍拉帧都在真实占用 uiTask**——这正是 + 「7003 拉帧期间屏板不超时」要压的条件。屏板侧应答起在 USART1 空闲中断里, + 理论上不被 uiTask 拖住;4-1 的判据就是拿两边的数交叉证明这件事: + 网络侧(本脚本):超时 ≈0、延迟分布正常; + 固件侧(日志口 grep SBLINK):tx 跟得上 rx(drop 无新增)、bad=0、kerr=0。 + +协议(Drivers/BSP/UI/ui_net_sink.c 实测口径): + 帧 = + 客户端 TAG=0xAA,设备 TAG=0xBB;CMD:0=KEEP 1=INIT 2=KEY 3=LCDMEM。 + LCDMEM 请求 payload = 偏移 u32 大端(仅支持 0);应答 payload = 偏移 u32 + 3200 B 位图。 + (设备对 CRC 只做异或累加——与屏板链路的 CRC-16 是两套,别混。) + +用法(仓库根目录,先清代理再跑——本机有 HTTP 代理会劫持内网连接): + & '.venv\\Scripts\\python.exe' test/host/remolcd_pull_test.py --seconds 30 + --hz 10 拉帧节拍(默认 10 = 每秒 10 次,重压;1 次/秒 ≈ DTU-RemoteLCD 观感) + --dump x.pbm 退出前把最后一帧写成 PBM(160x160 1bpp,可直接图片查看) + +退出码:0 = 全程无超时且 CRC 全对;1 = 出现超时/CRC 错/断连;2 = 用法错误。 + +设计依据:docs/任务规划/屏板链路.md §3 批 4 的 4-1。 +创建日期:2026-10-30 +""" +import argparse +import socket +import struct +import sys +import time + +DEFAULT_IP = "192.168.1.100" +DEFAULT_PORT = 7003 +TAG_CLIENT = 0xAA +TAG_DEVICE = 0xBB +CMD_KEEP = 0 +CMD_INIT = 1 +CMD_LCDMEM = 3 +FB_SIZE = 3200 +SCREEN_W = 160 +SCREEN_H = 160 + + +def build_frame(cmd, payload=b""): + """组一帧客户端请求:AA 。""" + body = bytes([TAG_CLIENT, cmd, (len(payload) >> 8) & 0xFF, len(payload) & 0xFF]) + xor = 0 + for b in payload: + xor ^= b + return body + payload + bytes([xor]) + + +class FrameReader: + """设备应答流重组(逐字节状态机,TAG 定界——与固件侧同思路,噪声字节自动重同步)。""" + + def __init__(self): + self.buf = bytearray() + + def pop(self): + """从缓冲里取一个完整 (cmd, payload);没有则 None。坏帧丢弃继续找下一个 TAG。""" + while True: + i = self.buf.find(bytes([TAG_DEVICE])) + if i < 0: + del self.buf[:] # 整段都不是帧头:全弃 + return None + if i > 0: + del self.buf[:i] # 丢帧头前的噪声 + if len(self.buf) < 4: + return None + cmd = self.buf[1] + ln = (self.buf[2] << 8) | self.buf[3] + total = 4 + ln + 1 + if len(self.buf) < total: + return None + payload = bytes(self.buf[4:4 + ln]) + crc = self.buf[4 + ln] + del self.buf[:total] + xor = 0 + for b in payload: + xor ^= b + if crc != xor: + return ("CRCERR", payload) + return (cmd, payload) + + def feed(self, data): + self.buf += data + + +def run(args): + try: + s = socket.create_connection((args.ip, args.port), timeout=5.0) + except OSError as exc: + print("[ERR] 连 %s:%d 失败:%s" % (args.ip, args.port, exc)) + print(" (若在内网却报超时/奇怪拒绝:先清 HTTP 代理再跑,见文件头用法行)") + return 1 + s.settimeout(5.0) + rd = FrameReader() + + def wait_frame(want_cmd): + """收一个 cmd==want_cmd 的应答;CRC 错返回 ('CRCERR', None);超时返回 None。""" + t0 = time.monotonic() + while time.monotonic() - t0 < 5.0: + f = rd.pop() + if f is not None: + cmd, payload = f + if cmd == "CRCERR": + return ("CRCERR", None) + if cmd == want_cmd: + return (cmd, payload) + # 其它命令的应答(如残留 KEEP 回显):跳过继续找 + continue + try: + data = s.recv(4096) + except socket.timeout: + return None + if not data: + return "CLOSED" + rd.feed(data) + return None + + # ---- INIT:查询屏幕参数,顺带把几何钉死 ---- + s.sendall(build_frame(CMD_INIT)) + r = wait_frame(CMD_INIT) + if r is None or r == "CLOSED" or r[0] == "CRCERR": + print("[ERR] INIT 无有效应答(%s)" % (r,)) + s.close() + return 1 + _, payload = r + if len(payload) >= 8: + w, h, nbytes = struct.unpack(">HHI", payload[:8]) + print("[INIT] %dx%d frame_bytes=%u" % (w, h, nbytes)) + if w != SCREEN_W or h != SCREEN_H or nbytes != FB_SIZE: + print("[WARN] INIT 几何与预期 160x160/3200 不符——继续,以设备为准") + else: + print("[WARN] INIT 应答长度 %d < 8,跳过几何核对" % len(payload)) + + # ---- LCDMEM 主循环 ---- + n_req = n_resp = n_crc = n_to = 0 + lat_sum = lat_min = lat_max = 0.0 + last_bmp = None + period = 1.0 / args.hz + t_end = time.monotonic() + args.seconds + next_at = time.monotonic() + while time.monotonic() < t_end: + now = time.monotonic() + if now < next_at: + time.sleep(min(next_at - now, 0.005)) + continue + t0 = time.monotonic() + s.sendall(build_frame(CMD_LCDMEM, struct.pack(">I", 0))) + n_req += 1 + r = wait_frame(CMD_LCDMEM) + if r == "CLOSED": + print("[ERR] 第 %d 次拉帧时对端断开" % n_req) + s.close() + break + if r is None: + n_to += 1 + print("[WARN] #%d LCDMEM 超时(5 s)" % n_req) + else: + cmd_got, payload = r + if cmd_got == "CRCERR": + n_crc += 1 + print("[WARN] #%d CRC 错" % n_req) + elif len(payload) != 4 + FB_SIZE: + n_crc += 1 + print("[WARN] #%d 应答长度 %d != %d" % (n_req, len(payload), 4 + FB_SIZE)) + else: + dt = time.monotonic() - t0 + n_resp += 1 + lat_sum += dt + lat_min = dt if lat_min == 0 else min(lat_min, dt) + lat_max = max(lat_max, dt) + last_bmp = payload[4:] + next_at += period + if next_at < time.monotonic() - 1.0: # 落后太多不追(避免突发风暴) + next_at = time.monotonic() + + s.close() + avg = (lat_sum / n_resp * 1000.0) if n_resp else 0.0 + print("[STAT] 请求 %d / 应答 %d / CRC错 %d / 超时 %d" % (n_req, n_resp, n_crc, n_to)) + print("[STAT] 往返延迟 min %.1f / avg %.1f / max %.1f ms" + % (lat_min * 1000.0, avg, lat_max * 1000.0)) + if args.dump and last_bmp: + with open(args.dump, "wb") as f: + f.write(b"P4\n%d %d\n" % (SCREEN_W, SCREEN_H)) + f.write(last_bmp) + print("[DUMP] 最后一帧已写 %s(P4 PBM,与屏板帧 B 点阵段同构)" % args.dump) + ok = (n_to == 0) and (n_crc == 0) and (n_resp > 0) + print("[VERDICT] %s" % ("PASS" if ok else "FAIL")) + return 0 if ok else 1 + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("--ip", default=DEFAULT_IP) + ap.add_argument("--port", type=int, default=DEFAULT_PORT) + ap.add_argument("--seconds", type=float, default=30.0) + ap.add_argument("--hz", type=float, default=10.0, help="拉帧节拍(次/秒,默认 10)") + ap.add_argument("--dump", default="", help="退出前把最后一帧写成的 PBM 路径") + args = ap.parse_args() + if args.seconds <= 0 or args.hz <= 0: + print("[ERR] --seconds / --hz 必须为正") + return 2 + return run(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/host/sb_led_map/run_unit_test.py b/test/host/sb_led_map/run_unit_test.py new file mode 100644 index 00000000..67aa8e7a --- /dev/null +++ b/test/host/sb_led_map/run_unit_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""屏板状态灯位映射(sb_led_map)主机侧纯逻辑单元测试运行器(纯标准库)。 + +做什么:用本机 MSVC 离线编译 test/host/sb_led_map/sb_led_map_test.c + (#include 纳入生产源文件 App/algo/sb_led_map.c),验证位语义 + (每路状态只点亮对应位、可叠加不遮蔽)、恒零位(bit0 / ID 6~19)、 + 心跳闪烁相位与 tick 回绕、NULL 输入契约。 + +为什么这套判据在真机上测不动:真机只能看到"哪盏灯亮了"(目视),看不到 + "掩码里多没多出一位保留位"——保留位被顺带点亮时屏板毫无反应,现场表现为 + "改了表没效果";主机侧用"掩码 & ~定义集 == 0"一条就把映射表钉死。 + +用法(仓库根目录): + & '.venv\\Scripts\\python.exe' test/host/sb_led_map/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:docs/任务规划/屏板链路.md §3 批 3 的 3-1。 +创建日期:2026-10-29 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +BUILD = TestBuild( + name="SB LED Map Tests", + src=os.path.join(HERE, "sb_led_map_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "algo"), + HERE, + ], + summary_marker="=== SB LED Map Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/sb_led_map/sb_led_map_test.c b/test/host/sb_led_map/sb_led_map_test.c new file mode 100644 index 00000000..ed9325c2 --- /dev/null +++ b/test/host/sb_led_map/sb_led_map_test.c @@ -0,0 +1,234 @@ +/* + * 模块名称:屏板状态灯位映射(sb_led_map)主机侧单元测试 + * 模块功能:在 PC 上离线验证 App/algo/sb_led_map.c 的位语义、恒零位、心跳闪烁 + * 相位与时基回绕行为。 + * + * 被测对象:真实源文件 App/algo/sb_led_map.c(#include 原样纳入)。 + * 本层纯逻辑(仅 stdint.h),**无需任何桩**——与 adc_health 套件同形。 + * + * 为什么值得单测(真机上验的是"灯亮没亮",这里验的是"亮的是不是那一位"): + * 1. bit0 恒 0——屏板 ID 0 是硬件常亮电源灯,掩码 bit0 根本不被读;但一旦 + * 哪天有人往映射里塞 bit0,测试先拦住,而不是到现场发现"多一位没反应"。 + * 2. ID 6~19 恒 0——映射表之外的位不许被顺带点亮(掩码里出现未定义位 = 表被改 + * 而测试没跟上,这是这张"自定表"唯一的防漂移手段)。 + * 3. 心跳相位在 tick 回绕点不得连灭两个 500 ms 段(操作员眼里的"装置停了")。 + * 4. NULL 输入只跑心跳(心跳证明的是"代码活着",不该被空输入连坐熄灭)。 + * + * 编号:TC-SBLEDMAP-001 起(小写子项 = 同一判据的另一面)。 + * 创建日期:2026-10-29(批 3 · 3-1) + */ +#include +#include + +#include "sb_led_map.h" + +/* 被测生产源(include_dirs 里有 App/algo) */ +#include "sb_led_map.c" + +static int s_pass; +static int s_fail; + +#define CHECK(cond, name) \ + do { \ + if (cond) { \ + s_pass++; \ + } else { \ + s_fail++; \ + printf("FAIL: %s (line %d)\n", (name), __LINE__); \ + } \ + } while (0) + +/* 亮/灭心跳各取一个代表时刻(偶段 = 亮,见 sb_led_map.c) */ +#define T_ON (0U) /* [0,500) 偶段 → 亮 */ +#define T_OFF (500U) /* [500,1000) 奇段 → 灭 */ + +/* + * TC-SBLEDMAP-001 全零输入:只有心跳位,其余状态位全灭 + */ +static void tc_zero_input(void) +{ + sb_led_in_t in; + uint32_t m; + + in.prot_active = 0U; + in.prot_pick = 0U; + in.adc_flags = 0U; + in.iec104_run = 0U; + + m = sb_led_map_eval(&in, T_ON); + CHECK(m == SB_LED_BIT_RUN, "TC-SBLEDMAP-001 zero input lights only the run blink (on phase)"); + + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == 0U, "TC-SBLEDMAP-001b zero input in the dark half-cycle is all-zero"); +} + +/* + * TC-SBLEDMAP-002 每一路状态各自点亮且只点亮对应位(非零即真,不看具体值) + */ +static void tc_one_source_each(void) +{ + sb_led_in_t in; + uint32_t m; + + in.prot_active = 0U; + in.prot_pick = 0U; + in.adc_flags = 0U; + in.iec104_run = 0U; + + in.prot_active = 0x00080000U; /* 随便哪个元件动作 */ + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == SB_LED_BIT_ALARM, "TC-SBLEDMAP-002 prot_active -> ID 2 only"); + in.prot_active = 0U; + + in.prot_pick = 1U; + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == SB_LED_BIT_PICKUP, "TC-SBLEDMAP-002b prot_pick -> ID 5 only"); + in.prot_pick = 0U; + + in.adc_flags = 0x80000000U; /* 高位故障位同样算非零 */ + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == SB_LED_BIT_ADC_FAULT, "TC-SBLEDMAP-002c adc_flags -> ID 4 only"); + in.adc_flags = 0U; + + in.iec104_run = 1U; + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == SB_LED_BIT_COMM, "TC-SBLEDMAP-002d iec104_run -> ID 3 only"); + in.iec104_run = 0U; +} + +/* + * TC-SBLEDMAP-003 多路同时:位可叠加、互不遮蔽(告警+通信同时真是合法工况) + */ +static void tc_stack(void) +{ + sb_led_in_t in; + uint32_t m; + + in.prot_active = 3U; + in.prot_pick = 3U; + in.adc_flags = 1U; + in.iec104_run = 1U; + + m = sb_led_map_eval(&in, T_ON); + CHECK(m == (SB_LED_BITS_DEFINED), "TC-SBLEDMAP-003 all sources + blink = exactly the defined set"); + + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == (SB_LED_BITS_DEFINED & ~SB_LED_BIT_RUN), + "TC-SBLEDMAP-003b dark half-cycle drops only the run bit"); +} + +/* + * TC-SBLEDMAP-004 恒零位:bit0 与 ID 6~19 在任何输入下不得出现 + *(bit0 屏板根本不读;ID 6~19 是保留位——掩码出现表外位 = 表被改而没走测试) + */ +static void tc_reserved_bits(void) +{ + sb_led_in_t in; + uint32_t t; + int clean = 1; + + in.prot_active = 0xFFFFFFFFU; + in.prot_pick = 0xFFFFFFFFU; + in.adc_flags = 0xFFFFFFFFU; + in.iec104_run = 1U; + + /* 扫 3 秒时基(6 个半周期)+ 边界时刻,全取定义集做差 */ + for (t = 0U; t < 3000U; t += 499U) { + uint32_t m = sb_led_map_eval(&in, t); + if ((m & ~SB_LED_BITS_DEFINED) != 0U) { + clean = 0; + } + } + CHECK(clean == 1, "TC-SBLEDMAP-004 no bit outside the defined set ever appears"); + CHECK((sb_led_map_eval(&in, T_ON) & 1U) == 0U, "TC-SBLEDMAP-004b bit0 stays 0 (screen board ignores it)"); +} + +/* + * TC-SBLEDMAP-005 心跳相位:连续 8 个半周期亮灭交替,边界含等号(500 整 = 暗段起点) + */ +static void tc_blink_sequence(void) +{ + int i; + int prev = -1; + int alt = 1; + uint32_t m; + + for (i = 0; i < 8; i++) { + uint32_t t = (uint32_t)i * SB_LED_BLINK_HALF_MS; + int on; + + m = sb_led_map_eval(NULL, t); + on = ((m & SB_LED_BIT_RUN) != 0U) ? 1 : 0; + if ((prev >= 0) && (on == prev)) { + alt = 0; + } + prev = on; + } + CHECK(alt == 1, "TC-SBLEDMAP-005 run bit alternates every half-period"); + CHECK((sb_led_map_eval(NULL, 0U) & SB_LED_BIT_RUN) != 0U, + "TC-SBLEDMAP-005b power-on first half-period is LIT (blink visible from t=0)"); + CHECK((sb_led_map_eval(NULL, SB_LED_BLINK_HALF_MS) & SB_LED_BIT_RUN) == 0U, + "TC-SBLEDMAP-005c the boundary instant belongs to the dark half-period"); + CHECK((sb_led_map_eval(NULL, SB_LED_BLINK_HALF_MS - 1U) & SB_LED_BIT_RUN) != 0U, + "TC-SBLEDMAP-005d last ms before the boundary still lit"); +} + +/* + * TC-SBLEDMAP-006 tick 回绕:跨 0xFFFFFFFF 连续熄灭时长不得超过一个半周期 + *(相位由除法直接得出、不依赖"上次"状态。回绕把末段截短、下一段从"偶段=亮" + * 重新开始——最坏也只是某段被截短,不会出现操作员眼里的"连灭 1 s = 装置停了"。 + * 1 ms 步进扫回绕前后各 1.5 个周期,量最长暗段。) + */ +static void tc_wraparound(void) +{ + uint32_t t; + int run = 0; /* 当前连续暗 ms 数 */ + int worst = 0; + + /* now 从 0xFFFFFFFF-1500 走到 0xFFFFFFFF 再接 0..1500(模 2^32 连续) */ + for (t = 0xFFFFFFFEU - 1500U; t != 0xFFFFFFFFU; t++) { + int dark = ((sb_led_map_eval(NULL, t) & SB_LED_BIT_RUN) == 0U) ? 1 : 0; + + run = (dark != 0) ? (run + 1) : 0; + if (run > worst) { + worst = run; + } + } + for (t = 0U; t <= 1500U; t++) { + int dark = ((sb_led_map_eval(NULL, t) & SB_LED_BIT_RUN) == 0U) ? 1 : 0; + + run = (dark != 0) ? (run + 1) : 0; + if (run > worst) { + worst = run; + } + } + /* 半周期 500 ms + 1 ms 扫描步进余量 */ + CHECK(worst <= (int)SB_LED_BLINK_HALF_MS + 1, + "TC-SBLEDMAP-006 dark run never exceeds one half-period across tick wraparound"); +} + +/* + * TC-SBLEDMAP-007 NULL 输入 = 全零输入 + 心跳照常(注释契约:心跳不被连坐) + */ +static void tc_null_input(void) +{ + CHECK(sb_led_map_eval(NULL, T_ON) == SB_LED_BIT_RUN, + "TC-SBLEDMAP-007 NULL input still blinks (liveness signal must not depend on inputs)"); + CHECK(sb_led_map_eval(NULL, T_OFF) == 0U, + "TC-SBLEDMAP-007b NULL input lights nothing else"); +} + +int main(void) +{ + printf("-- sb_led_map unit tests --\n"); + tc_zero_input(); + tc_one_source_each(); + tc_stack(); + tc_reserved_bits(); + tc_blink_sequence(); + tc_wraparound(); + tc_null_input(); + + printf("=== SB LED Map Tests: %d/%d PASSED ===\n", s_pass, s_pass + s_fail); + return (s_fail == 0) ? 0 : 1; +} From f405c8994fd0123b7b8ad1d449a372508a5276ac Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 30 Oct 2026 00:40:45 +0800 Subject: [PATCH 16/16] =?UTF-8?q?docs(=E5=B1=8F=E6=9D=BF):=20=E6=89=B9=203?= =?UTF-8?q?=20=E6=94=B9=E5=86=99=EF=BC=88LOGO=20=E5=87=BA=E5=B1=80?= =?UTF-8?q?=EF=BC=89+=204-1=20=E5=8F=8C=E9=80=9A=E9=81=93=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E5=AE=9E=E6=B5=8B=E5=9B=9E=E5=A1=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 规划批 3 表改两项(3-1 映射表 / 3-2 下发),3-3 LOGO 标 ❌ 出局 (用户裁定 2026-10-29:LOGO 功能无用、不实现,sb_proto 组帧函数列待删); §0 执行序、验收清单第 8 条、另测三项(LOGO 那条连同串进去的残句一并清) → 两项、风险表两行同步。风险表"const 数组"防返工手段按实际实现改口 (位宏 + 单点纯函数 + 单测钉定义集——5 路输入判据形态各异,表统一不了谓词)。 - 设计说明 §3 决定 7 改写(LED 掩码独立纯逻辑模块折算)+ 新增决定 9(LOGO 出局); 新增 §5.7 双通道并发回归实测(7003 侧 2200 拉帧零错零超时 / 屏板侧隔离窗口 每召必答零丢弃 / 画面一致性由同一份活显存机制保证 / len=7 观察项如实记); §7 清单第 8 项销项、第 10 项状态灯改为"已实现待目视"(含 ID2/ID5 在未标定 闭锁下恒灭的口径提示)、第 11 项 LOGO 划掉。 - 裸 §5.7 跨文档引用改全路径(--sections-strict 自引用判据)。 --- docs/任务规划/屏板链路.md | 28 ++++++++++------------ docs/子系统设计/屏板链路设计说明.md | 37 +++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/docs/任务规划/屏板链路.md b/docs/任务规划/屏板链路.md index ea67bd7b..c030590e 100644 --- a/docs/任务规划/屏板链路.md +++ b/docs/任务规划/屏板链路.md @@ -24,7 +24,8 @@ 前两步**已于 2026-10-29 完成**(§3 批 0 的 0-1 / 0-2:台架线接 CN1 的 RS232#1,日志口迁到 UART5, 台架串口由 `COM6` 变成 `COM3`,`.serialtest/config.json` 已同步);第三项 0-3 的接法也已落纸。 **批 0 / 批 1 / 批 2 均已收尾(2026-10-29):USART1 在 700 kbaud 上收发双向通了(收 6 B 帧 A、 -回 4208 B 帧 B),下一步进批 3(状态灯映射表 + LOGO 下发)。** +回 4208 B 帧 B)。批 3 按用户裁定(2026-10-29)缩为两项:状态灯映射表 + 下发; +**LOGO 功能出局**(不实现,`sb_proto` 里既有 LOGO 组帧函数列为待删除)。** --- @@ -306,22 +307,19 @@ > 或者按 §5 直接接真屏板。`screenlink_bench.py --reply` 已内置这条提示。 -### 批 3 · 状态灯与 LOGO(3 项) +### 批 3 · 状态灯(3-1 / 3-2 两项,2026-10-29 收尾;LOGO 出局) | 序 | 任务 | 改动点 | 判据 | |---|---|---|---| -| 3-1 | **状态灯位映射表** | 定义"业务状态 → 屏板 LED 位"的表(**参考源码里没有这张表,必须自定**)。建议:ID 1 = 运行、ID 2 = 告警、ID 3 = 通信、其余按 `docs/产品需求/产品需求设计文档.md` §5.8 的 6 路语义扩展;**bit0 恒 0**(ID 0 是硬件常亮的电源灯)、**bit20~31 恒 0** | 表落纸并进代码;每位点亮/熄灭可单独验证 | -| 3-2 | **状态灯下发** | 32 位小端填进 `info[0..3]`(`info[0]` = bit0~7) | 主机改掩码 → 屏板对应 LED 变化,**LED 0 不受掩码影响** | -| 3-3 | **LOGO 下发(一次性)** | 独立入口(不进常规刷新):`info[4..5] = 0x89 0x45`、`info[0..1]` = 宽、`info[2..3]` = 高(大端) | 下发后屏板显示 2 s 并写 Flash;**断电重启后 LOGO 仍在**;这帧不改 LED 状态 | - -> ⚠ LOGO 帧会把屏板 Flash `0x0803F000`(扇区 126)**整块覆盖**,且屏板固件不校验是否同一张图。 -> 建议只在出厂配置/联调流程里提供,**不要挂到菜单上**。 +| 3-1 | **状态灯位映射表** | 落 `App/algo/sb_led_map.c/h`(纯逻辑,主机单测 17/17):**ID 1 = 运行心跳**(500 ms 半周期闪烁,与主板 LED1~3 同拍)、**ID 2 = 告警**(`prot_elm_active_mask != 0`)、**ID 3 = 通信**(104 链路 RUN,新增 `iec104_serv_link_run()`)、**ID 4 = 采样故障**(`adc_health_flags()` 电平位,不选 sticky——灯要能灭)、**ID 5 = 保护启动**(`prot_elm_pick_mask`);**bit0 恒 0**(ID 0 硬件常亮、屏板不看位)、**ID 6~19 恒 0**(保留,扩展从 ID 6 顺序取) | 表落纸并进代码 ✓;主机单测用「掩码 & ~定义集 == 0」钉住无表外位 ✓;每位点亮可单独验证(真屏板目视,见 §5) | +| 3-2 | **状态灯下发** | `ui_task.c` 帧组装块每拍现采四路状态源(全部是"任意上下文可读"观测口)→ `sb_led_map_eval()` → 掩码进 `sb_link_frame_update()` 的 `info[0..3]`(小端);**掩码变了帧就重组**,走批 2 已有的 busy/dirty 机制,无新通路 | 主机改掩码 → 屏板对应 LED 变化;**LED 0 不受掩码影响**(真屏板目视待验,见 §7 清单) | +| 3-3 ❌ | ~~LOGO 下发~~ **出局**(用户裁定 2026-10-29:LOGO 功能无用,不实现、不接线) | 不做下发入口;`sb_proto.c/h` 里既有的 `sb_frame_b_build_logo()` + `SB_LOGO_MAGIC_*` 常量与 `test/host/sb_proto` 的相应用例**列为待删除**(删除时连同 `docs/子系统设计/屏板链路设计说明.md` 的 LOGO 段落一并清)。帧 B 的 `info[4..5]` 常规图像路径恒填 0,屏板按普通图分支处理,与 LOGO 无冲突 | 无——本项不产生代码改动 | ### 批 4 · 并发与验收(3 项) | 序 | 任务 | 改动点 | 判据 | |---|---|---|---| -| 4-1 | **双通道并发回归** | 7003 客户端拉帧 + 屏板召唤同时进行 | 两侧画面一致;串口 4208 B 发送期间 7003 不超时;7003 拉帧期间屏板不超时 | +| 4-1 ✅ | **双通道并发回归** | 7003 客户端拉帧 + 屏板召唤同时进行 | **已完成 2026-10-30(真屏板 + 新脚本 `test/host/remolcd_pull_test.py`,7003 侧 INIT→LCDMEM 10 Hz 重压)**:网络侧两轮(150 s / 60 s 隔离窗口)请求应答 **1500/1500 + 700/700、CRC 错 0、超时 0**,往返 min 35 / avg ~63 / max 131 ms(远小于 LCDMEM 应答"最坏 ≈4 s"的口径上限)。屏板侧 60 s 隔离窗口(clear 后开窗,计数只读窗口内):**rx +233 帧全收、`drop` 零新增、`kerr=0 abor=0 bad=0`、`off=0`、无 OFFLINE 行**,`tx` 追平 `rx`(末帧 59.6 ms 采样窗差 1)→ **7003 拉帧期间屏板每召必答**(屏板的 205 ms 超时计数在屏侧看不到,但"每召必答"是其不超时的充分证据)。"两侧画面一致"由机制保证:两出口读**同一份活显存**(`ui_net_sink_get_lcdmem` 与 `sb_link_frame_update` 的入参同为 `ui_port_get_framebuffer()`),无第二份账可漂;目视对图为机会性复核项。⚠ 观察项(不属 4-1 判据):`len=7` 的出现窗口夹在"烧录后冒烟(23:49 时 len=0)"与"第一轮 150 s 重压结束(00:15:26 已 len=7)"之间,**不能排除与首轮重压相关**;但第二轮 60 s 隔离窗口零新增、全程 `crc=0 fmt=0`、有效帧一帧没少(ok 增速与 rx 同频),功能影响为零。挂设计说明观察,若再现再查 | | 4-2 | **长稳与自复位** | 连续跑 ≥2 h(跨过屏板一次自复位) | 零帧错误、零误判离线、UI 无可见卡顿 | | 4-3 | **文档收口** | 把实测结论回填 `docs/产品需求/产品需求设计文档.md` §5.16.2(尤其**帧长 4208 vs 3208 那条待复核项**)与 §9.13 | 待复核项清零;§9.13 对应行销项 | @@ -421,7 +419,7 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 | 1 | 协议层单测(`test/host/screenlink/`) | 无硬件 | CRC 与组帧的正确性,可回归 | | 2 | PC **模拟屏板**:按屏板节拍发 6 B 召唤帧(脚本 `test/host/screenlink_bench.py`,`--port COM6 --key-fixed 2` 可持续改画面以拿多次组帧样本;`--glitch-baud 115200` 灌噪声验自愈;`--reply` 收应答) | **USB 转 TTL 模块**接 U5 的 `ST_RX0`/`ST_TX0`(PA10/PA9)+ 主机板 | 收侧**已做**(`ok == rx`、`crc=0`、`size=6`、噪声后自愈);发侧固件计数**已做**(`tx == done`、`drop=0`、`bad=0`、`fb` 随画面变化增长)。⚠ `--reply` 收不全 4208 B 是 CH340 的限制,不是固件问题,见批 2 下方那条 ⚠ | | 3 | 反向:PC 收主机帧,用脚本核对 4208 B / CRC / 显存内容 | 同上 | 帧内容正确性(与 `g_ui_fbuf` 逐字节比) | -| 4 | **接真屏板**(TTL 直连屏板 UART2 的 PA2/PA3 一侧) | 屏板实物 + 屏板 TTL 引出 | **冒烟首通 + 10 min 零错 2026-10-29**:出图(用户目视确认)、`rx=4258 ok=4258 crc=0 err=0`、应答 `tx=4249 done=4248 drop=0(窗口内) abor=0`、按键 `keys=53 kq=0`、`off` 自动恢复。⚠ 接线实测教训:**按排针丝印同名直连收不到,必须"主机 TX 接丝印 RX"**(丝印是屏板视角),且两板各供电须接第三根 GND——详见 `docs/子系统设计/屏板链路设计说明.md` §5.6。状态灯/长稳/时序仪器项仍未测 | +| 4 | **接真屏板**(TTL 直连屏板 UART2 的 PA2/PA3 一侧) | 屏板实物 + 屏板 TTL 引出 | **冒烟首通 + 10 min 零错 2026-10-29**:出图(用户目视确认)、`rx=4258 ok=4258 crc=0 err=0`、应答 `tx=4249 done=4248 drop=0(窗口内) abor=0`、按键 `keys=53 kq=0`、`off` 自动恢复。⚠ 接线实测教训:**按排针丝印同名直连收不到,必须"主机 TX 接丝印 RX"**(丝印是屏板视角),且两板各供电须接第三根 GND——详见 `docs/子系统设计/屏板链路设计说明.md` §5.6。状态灯已实现(批 3)待目视、双通道并发**已过(4-1,见 `docs/子系统设计/屏板链路设计说明.md` §5.7)**;长稳/时序仪器项仍未测 | | 5 | 产品形态:按最终线束接法定版 | 线束 | 产品形态验证 | > 第 2、3 步是**性价比最高的两步**:不用屏板、不用改硬件,就能把 90 % 的软件问题挖出来。 @@ -440,10 +438,10 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 | 帧长按 3208 实现 | 照 readme 写 | CRC 全失败、屏板不亮 | 已写进 §4.2 警示;联调第一步就核 4208 | | 中断回调漏加分支 | 只改自己模块,忘了那三个全局 UART 回调里唯一的一处实现只分发另一条通道的句柄(§1.3) | 收不到召唤帧,表象像协议错 | 批 1 的 1-3 已列为判据 | | 屏板自复位被误判离线 | 链路监视窗口设得太小 | 每 83 分钟误报一次 | 判据定为"连续 ≥10 s 无帧" | -| 状态灯语义返工 | 映射表拍脑袋定的 | 后期改表要重测 | 把表做成一张 `const` 数组,改表不改逻辑 | +| 状态灯语义返工 | 映射表拍脑袋定的 | 后期改表要重测 | 表 = `App/algo/sb_led_map.h` 位宏 + 单点 `sb_led_map_eval()`(唯一出处),折算不散落调用点;主机单测钉「掩码 & ~定义集 == 0」,改表漏测会被套件拦住(3-1 落纸时的口径微调:5 路输入判据形态各异——电平/闪烁——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),下发前先确认能给屏板断电 | +| **屏板接收溢出后永久静默** | 主机提前发 / 超发 / 叠发 | 屏板再也收不到,画面与状态灯一起冻死,**它自己毫无指示**,只能断电重上 | 严格一召唤一应答、正好 4208 B(LOGO 出局后总线上不会再有其它帧形态,风险源只剩叠发/提前发,批 2 的 busy 冻结已挡) | | **把 200 ms 当成「帧作废」线** | 误读屏板那个放弃计时 | 白加一层重试与加急逻辑;或反过来以为迟到无害,撞上 205 ms 的按键召唤 | 屏板真实规则:迟到的完整帧照样显示;**有按键待报时最早 205 ms 就再召唤** → 仍按 ≤200 ms 完成,理由换成「防撞 + 不滞后」(决策三) | | **按键被当成偶发失灵** | 那一轮窗口被错过 | 屏板的键**只发一次不重试**(发完立刻清备份)→ 该次按键直接丢失 | 起发留在中断里(µs 级);2-3 按「逐键必到」测,不看统计均值 | | **屏板背光 100 s 熄灭被当成掉线** | 现场长时间没人按键 | 误报链路故障 | 按任意键即复亮;判离线只看召唤帧有无(2-5 的 10 s 窗口) | @@ -463,16 +461,14 @@ stub 提供 HAL)。屏板侧 CRC 有查表实现,**用同一张表做交叉 5. **双通道并发**:7003 与串口同时跑,互不拖超时、画面一致。 6. **屏板自复位**(≈5000 s)不被误判离线,约 1 s 后自动续上。 7. 拔插屏板:7003 侧不受影响,插回自动恢复,不用复位主机。 -8. LOGO 下发后断电重启仍在,且不改变 LED 状态。 +8. ~~LOGO 下发后断电重启仍在,且不改变 LED 状态~~ **随 LOGO 出局划掉**(用户裁定 2026-10-29,见批 3 的 3-3)。 -**另测三项**(属本方案的实现判据,不在 §5.16.10 编号内): +**另测两项**(属本方案的实现判据,不在 §5.16.10 编号内): - 逻辑分析仪量整帧发送期间**不得出现 ≥2 个字符时间(约 28 µs)的间隙**——这是断帧安全性的直接证据, 也是必须用 DMA 的理由(决策三);同时记下召唤帧末位到应答首位的实测值,确认 30 µs 的切换留量够用。 -- 跑一遍 LOGO 下发(3-3)之后,验证「给屏板断电重上」仍能恢复通信:屏板在写它自己那块 Flash 时若被打断, - **7003 拥塞时串口侧照常应答**:挂着 7003 客户端不读、或制造对时 / relisten 突发把 `uiTask` 卡住, 屏板侧应**零超时**、画面停在最后一帧(这就是「起发不在任务态」的直接证据);`uiTask` 一醒画面立刻续上、总线无对撞。 - 可能进入它自己救不回来的状态(它的错误回调与重初始化都被注释掉了)。 --- diff --git a/docs/子系统设计/屏板链路设计说明.md b/docs/子系统设计/屏板链路设计说明.md index 9cb3029f..d267b4f3 100644 --- a/docs/子系统设计/屏板链路设计说明.md +++ b/docs/子系统设计/屏板链路设计说明.md @@ -44,7 +44,7 @@ uiTask(10 ms 节拍) USART1 空闲中断(屏板召 | `sb_link_key_take()` | uiTask | 出队屏板原始键号(0=空);映射归 uiTask | | `sb_link_poll(now_ms)` | uiTask | 离线判定(每拍)+ 5 s 变化才打的 RX/TX 两行统计 | | `sb_link_get_stats(&st)` | 任意 | 计数快照(rx/ok/crc/keys/tx/done/drop/fb/us/offline…) | -| `sb_crc16` / `sb_frame_a_parse` / `sb_frame_b_build_image` / `sb_frame_b_build_logo` / `sb_frame_b_verify` | 纯逻辑 | 协议层五件套,主机单测 59/59 | +| `sb_crc16` / `sb_frame_a_parse` / `sb_frame_b_build_image` / `sb_frame_b_build_logo`(⚠ 待删除,LOGO 出局见批 3 的 3-3)/ `sb_frame_b_verify` | 纯逻辑 | 协议层五件套,主机单测 59/59 | 全局回调落点:`HAL_UARTEx_RxEventCallback` / `HAL_UART_ErrorCallback` / `HAL_UART_TxCpltCallback` 三个的**唯一实现处都在 `rs485.c`**,各含一行 @@ -67,9 +67,16 @@ uiTask(10 ms 节拍) USART1 空闲中断(屏板召 之后每次召唤都被判"上一帧还在发"→ rx 涨 tx 冻结。判据用 `huart->gState != BUSY_TX`。 6. **离线判 10 s 不判 500 ms**:屏板 ≈5000 s 自复位有 ≈1 s 空窗,短窗会把"歇了口气" 判成离线再"恢复",标志反复跳。 -7. **LED 掩码暂传 0**:状态灯映射表属批 3;帧结构不变,届时只换 `frame_update` 的实参。 +7. **LED 掩码走独立纯逻辑模块折算**(批 3 落地):位定义 + 求值收在 `App/algo/sb_led_map.c/h` + (映射表唯一出处),`uiTask` 每拍现采四路状态源(`prot_elm_active_mask` / `prot_elm_pick_mask` / + `adc_health_flags` / `iec104_serv_link_run`)喂进去,掩码变了帧就重组——复用批 2 的 busy/dirty + 机制,无新通路。折算不做成 const 表:5 路输入判据形态各异(电平 4 路 + 心跳 1 路), + 纯函数分支比"表 + 谓词回调"直白,且主机单测能钉「掩码 & ~定义集 == 0」。 8. **键 9/10 丢弃留痕**:决策四未裁,先按 `docs/任务规划/屏板链路.md` §4.3 的表把 1/2/3/4/7/8 映射到 `UI_KEY_*`,9/10 出日志不注键。 +9. **LOGO 出局**(用户裁定 2026-10-29):不做下发入口。`sb_proto` 的 `sb_frame_b_build_logo()` + 与魔数常量**列为待删除**(头文件已挂 ⚠ 勿新增调用);常规图像路径 `info[4..5]` 恒 0, + 屏板按普通图分支处理,总线上不会再出现 LOGO 形态——「屏板接收溢出」风险表随之只剩叠发一源。 ## 4. 资源占用(实测,`.map` 可复核) @@ -141,6 +148,26 @@ fb=144 bad=0`;杜邦线噪声形态 `bits=0x02`(NE)每帧 ~1.3 次但数 与帧内容无关),不动就当场对调。产品形态线束定版前,以"rx 计数"为唯一验收口径。 - 两块板各自供电必须接第三根线 GND;只连两根信号线时是浮地,形态同样是彻底收不到。 +### 5.7 双通道并发回归(批 4 的 4-1,2026-10-30 00:10~00:18,真屏板在线) + +激励脚本 `test/host/remolcd_pull_test.py`(新):INIT(8 B:宽/高/帧字节数,大端)→ LCDMEM +偏移 0 以 **10 Hz** 重压拉整帧 3200 B,逐帧 XOR 校验 + 长度核对。每轮 LCDMEM 在固件侧都是 +「uiTask 入队 + 等 netTask 回执」的真往返——这就是"7003 在真实占用 uiTask"的压力形态。 + +| 侧 | 读数 | 判据 | +|---|---|---| +| 网络侧·第一轮 | 150 s:请求 1500 / 应答 1500、CRC 错 0、超时 0,往返 min 35 / avg 63 / max 88 ms | **串口发送期间 7003 不超时** ✓(4208 B 线上 59.6 ms 与拉帧并行,应答照样 100 % 到手) | +| 网络侧·第二轮 | 60 s 隔离窗口:700 / 700、零错零超时,max 131 ms | 同上,重复性 ✓ | +| 屏板侧·60 s 隔离窗口(日志 clear 后开窗,计数只读窗口内) | rx +233 帧(278 ms/帧节拍精确)、**`drop` 零新增、`kerr=0 abor=0 bad=0`、`off=0`、全程无 OFFLINE**,`tx` 追平 `rx`(末帧 59.6 ms 采样窗差 1) | **7003 拉帧期间屏板每召必答** ✓(屏板 205 ms 超时计数在主机侧看不到,每召必答是其不超时的充分证据) | +| 画面一致性 | 机制证明:两出口读**同一份活显存**(`ui_port_get_framebuffer()` 单点),无第二份账可漂;7003 末帧已 `--dump` 成 PBM/PNG 留档(`ci-artifacts/`,不入库) | 目视对图为机会性复核 | + +**观察项(不属 4-1 判据,如实记)**:本轮上电累计 `len=7`(7 次 IDLE 断帧尺寸 ≠ 6)。 +出现窗口夹在"烧录后冒烟 23:49(未截 len 字段)"与"首轮重压结束 00:15(已 len=7)"之间, +**不能排除与首轮 150 s 重压相关**;但上一上电周期 10 min 窗(§5.5)`len=0`,同一份固件, +说明非确定性、疑瞬态;第二轮 60 s 隔离窗口**零新增**,且全程 `crc=0 fmt=0`、`ok` 增速与 +`rx` 同频——有效帧一帧没少,功能影响为零。若后续再现再查(首查方向:断帧尺寸 7 的那一包 +是否伴随 `err` 位——若是 NE 噪声尾巴粘进下一包,属杜邦级线噪的正常形态)。 + ## 6. 坑账本(都已在代码注释/规划文档留痕,这里只汇总) 1. 中文字面量 → ARMCC `#870-D`(源码无 BOM 按本地代码页解)。日志字面量纯 ASCII。 @@ -164,10 +191,10 @@ fb=144 bad=0`;杜邦线噪声形态 `bits=0x02`(NE)每帧 ~1.3 次但数 | 5 | **2 h 长稳跨自复位** | 挂机 ≥5000 s + 余量:零误判离线(自复位 ≈1 s 空窗不该出 OFFLINE)、零 kerr/abort、恢复自动 | 无 | | 6 | **召唤→应答延迟 30 µs~5 ms / 帧内无 ≥28 µs 间隙 / 59.6 ms** | 逻辑分析仪挂 `U5.A10/B10`;或 FTDI/CP210x 适配器 + `screenlink_bench.py --reply` 只能验整帧时长与内容(USB 1 ms 粒度量不了间隙) | 仪器 | | 7 | **帧 B 与 `g_ui_fbuf` 逐字节比** | FTDI/CP210x 收满 4208 后 `--dump` 出 PBM 对 UI 目检;严格逐字节需 PC 收全帧脚本比对(CH340 做不了) | 仪器 | -| 8 | **双通道并发**(批 4) | 挂 DTU-RemoteLCD 持续拉帧 + 屏板同时召唤:两侧画面一致、串口发送期 7003 不超时、7003 拉帧期屏板不超时 | 无 | +| 8 | **双通道并发**(批 4 的 4-1) | 挂 DTU-RemoteLCD 持续拉帧 + 屏板同时召唤:两侧画面一致、串口发送期 7003 不超时、7003 拉帧期屏板不超时 | ✅ **2026-10-30 实测通过**(§5.7:7003 侧 2200 拉帧零错零超时;屏板侧隔离窗口每召必答、零新增丢弃;画面一致由"同一份活显存"机制保证) | | 9 | **7003 拥塞时串口照常应答** | 制造 netTask 往返卡顿(对时风暴/客户端不读),屏板侧应零超时、画面停最后一帧——"起发不在任务态"的直接证据 | 无 | -| 10 | **状态灯逐位**(批 3 功能,测随实现走) | 映射表落地后逐位点亮验证、LED 0 不受掩码影响 | 批 3 | -| 11 | **LOGO 断电留存**(批 3 功能) | 下发后重启屏板仍在、LED 不变;完成后追加"LOGO 写 Flash 期间拔线"负路径 | 批 3 | +| 10 | **状态灯逐位**(批 3 已实现,待目视) | ID 1 运行灯 1 Hz 闪(肉眼即见);ID 3 = 挂 104 主站(`test/host/iec104_master.py`)后亮、拆线灭;ID 4 = 拔采样前端后亮、插回灭;**ID 2 / ID 5 在产品构建恒灭**(未标定闭锁,`protect_task.c` 的 VALID 门禁——与屏上"保护动作 N"同一已知口径),要验证这两路得等标准源标定后或临时构建 | 真屏板已就位 | +| 11 | ~~LOGO 断电留存~~ | **随 LOGO 出局划掉**(用户裁定 2026-10-29):不下发 LOGO,`sb_proto` 的 LOGO 组帧函数列为待删除 | — | | 12 | **键 9/10 语义裁定**(决策四) | 需要产品侧定 RESET/FACTORY 行为,代码已留 unmapped 分支 | 人 | | 13 | 真屏板 ORE 路径 | 主机侧只能被动等:若某天 `bits` 出现 0x08,看 `rearm` 是否跟涨;台架造不出(CPU 读 DR 太快),已由主机单测按 RM0090 覆盖 | 机会性 |