修复 I1 绘制缓冲缺 8 字节 palette 头导致的越界写与整帧错位,补存储维护文档

This commit is contained in:
2026-09-21 03:45:55 +08:00
parent b5dd94b834
commit 47b38f08ef
4 changed files with 96 additions and 12 deletions
+3 -2
View File
@@ -84,8 +84,9 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
- 所有 `net_*` socket 操作经 `netMsgQueue` 消息队列在 netTask 串行执行(线程安全);非 netTask 任务直接调 `net_*` API,勿直接碰 CH395F 驱动
- 网口配置:`/config.ini` [network] 段(工厂默认 192.168.1.100/24、gw .1;MAC 全零 = 芯片内置;UI 保存立即生效;旧档 `/cfg/netcfg.bin` 首启自动迁移)→ `docs/网络配置说明.md`
- **新 .c 文件必须手动注册到两处**才能双端编译:`MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt`(用 `new-source-file` skill)
- 测试构建:`test/test_config.h` 的 `TEST_SUITE_CH395F/STORAGE/GD5F/RTC` **互斥**(一次只能启用一个,`#error`);全部注释 = 产品构建;**破坏性用例默认关闭**:同文件 `ENABLE_DESTRUCTIVE_TESTS`(默认注释)控制写/擦 NAND、格式化、坏块注入与 FatFS 写类用例——关闭时套件为**只读模式**(开机横幅 `mode: READ-ONLY`,可随时在在用设备上跑、不动卷内容与容量),只在**产品首次上电/产线首启**或明确需要时打开(`mode: DESTRUCTIVE`);2026-09-21 实测一轮破坏性 GD5F 运行即消耗一个块(容量 95248 → 95200,TC-STO-04 恢复不回来);CH395F 套件 `test/ch395f_test.h`:硬件层 Phase 1~5 与 NET 层 Phase 6~11 互斥;CH395F 硬件层模式下 netTask(基础线程:无条件创建、全构建唯一实例)入口自挂起(不运行),testTask 独占 CH395F
- **改驱动前先读对应 `docs/问题记录/*_Trap_Records.md`**(CH395F / GD5F2GQ5UE / SD2506);各模块测试指南见 `docs/测试说明/`
- 测试构建:`test/test_config.h` 的 `TEST_SUITE_CH395F/STORAGE/GD5F/RTC` **互斥**(一次只能启用一个,`#error`);全部注释 = 产品构建;**破坏性用例默认关闭**:同文件 `ENABLE_DESTRUCTIVE_TESTS`(默认注释)控制写/擦 NAND、格式化、坏块注入与 FatFS 写类用例——关闭时套件为**只读模式**(开机横幅 `mode: READ-ONLY`,可随时在在用设备上跑、不动卷内容与容量),只在**产品首次上电/产线首启**或明确需要时打开(`mode: DESTRUCTIVE`);2026-09-21 实测一轮破坏性 GD5F 运行即让 dhara 多记一个坏块(容量 95248 → 95200;该计数持久化在 dhara journal checkpoint 头里,普通恢复模式清不掉,需全片擦除——见 `docs/测试说明/存储维护与容量恢复.md`);CH395F 套件 `test/ch395f_test.h`:硬件层 Phase 1~5 与 NET 层 Phase 6~11 互斥;CH395F 硬件层模式下 netTask(基础线程:无条件创建、全构建唯一实例)入口自挂起(不运行),testTask 独占 CH395F
- **改驱动前先读对应 `docs/问题记录/*_Trap_Records.md`**(CH395F / GD5F2GQ5UE / SD2506);各模块测试指南见 `docs/测试说明/`;**存储维护/容量恢复**(全片擦除恢复 dhara 坏块记账)见 `docs/测试说明/存储维护与容量恢复.md`
- **LVGL I1 绘制缓冲含 palette 头(2026-09-21 实证)**:索引色格式 draw buffer 布局 = `[palette 8B][pixel data stride×h]`(`lv_draw_buf_create` 加 8B、`lv_draw_buf_goto_xy`/`lv_refr get_max_row` 都按此跳/扣),但 `lv_display_set_buffers`/`lv_draw_buf_init` 的容量校验**只看 `stride×h`**——只给 3200B 会被静默接受:末 8 个像素字节写到缓冲外(曾打脏相邻 `s_frame_box[0]` 指针 → HardFault),且 flush 收到的首地址含 palette(整帧错位 8 字节)。`ui_port.c` 按 `stride×h + palette` 供给并在 flush 时 `+palette` 取像素;详见 `docs/UI 设计/UI_设计架构.md` §1.4 第 8 条
## 串口助手 serial_tool
+15 -10
View File
@@ -15,15 +15,18 @@
/* 160x160 1bpp 帧 = 3200 字节 */
static uint8_t g_ui_fbuf[UI_FB_SIZE];
/* LVGL 绘制缓冲(整屏渲染)。LVGL 9.3.0 对 I1 格式的整帧渲染,会在其自身算出的
* stride*h(=3200) 之后再越界多写 1 个字节(实测台架金丝雀命中 draw_buf[3200]=0xFF)。
* 若紧邻其后的 .bss(本例 ui_menu_view.o 的 s_frame_box)恰有指针,其最低字节被
* 打成 0xFF → 野指针 → HardFault(PC=lv_obj_get_scroll_y,2026-09-20 存储套件实证,
* SRAM/CCM 两种堆布局下同一签名、与 CCM 无关)。物理多留 UI_DRAW_BUF_GUARD 字节
* 吸收该越界写;逻辑尺寸仍按 UI_FB_SIZE 上报 LVGL(渲染数学 stride*h 不变)。
* Lib/LVGL 为第三方库,不改其源码,越界容纳放在本移植层。 */
/* LVGL 9.3 索引色(I1)绘制缓冲的真实布局 = [palette 8B][pixel data stride*h]:
* - lv_draw_buf_create() 按 stride*h + palette 分配;
* - lv_draw.c:497 → lv_draw_buf_goto_xy() 取绘制指针时先 += palette;
* - lv_refr.c get_max_row() 也从 data_size 里扣掉 palette 才当可用像素。
* 但 lv_display_set_buffers()/lv_draw_buf_init() 的容量校验只看 stride*h(不含 palette),
* 所以"只给 stride*h 字节"会被静默接受 → 末 8 个像素字节写到缓冲之外
* (2026-09-21 金丝雀实测:越界 n=8,且 flush 回调收到的首地址前 8 字节是 palette)。
* 本端口据此按 stride*h + palette 提供缓冲,flush 时跳过 palette 取真实像素。
* UI_DRAW_BUF_GUARD 只给后续 LVGL 版本差异留余量,不再依赖它吸收越界。 */
#define UI_DRAW_BUF_PALETTE ((uint32_t)LV_COLOR_INDEXED_PALETTE_SIZE(LV_COLOR_FORMAT_I1) * 4u)
#define UI_DRAW_BUF_GUARD 32
static uint8_t g_ui_draw_buf[UI_FB_SIZE + UI_DRAW_BUF_GUARD];
static uint8_t g_ui_draw_buf[UI_FB_SIZE + UI_DRAW_BUF_PALETTE + UI_DRAW_BUF_GUARD];
/* 单键缓冲:菜单无需多键队列,仅保存最近一次待处理键 */
static volatile uint8_t g_pending_key = 0;
@@ -83,7 +86,8 @@ static void ui_keypad_read(lv_indev_t * indev, lv_indev_data_t * data) {
static void ui_flush_cb(lv_display_t * disp, const lv_area_t * area, uint8_t * color_p) {
(void)disp;
(void)area;
memcpy(g_ui_fbuf, color_p, UI_FB_SIZE);
/* color_p = 绘制缓冲首地址(含 palette 头)→ 跳过 palette 取真实像素 */
memcpy(g_ui_fbuf, color_p + UI_DRAW_BUF_PALETTE, UI_FB_SIZE);
lv_display_flush_ready(disp);
}
@@ -105,7 +109,8 @@ int ui_port_init(void) {
return -1;
}
lv_display_set_color_format(disp, LV_COLOR_FORMAT_I1);
lv_display_set_buffers(disp, g_ui_draw_buf, NULL, UI_FB_SIZE,
lv_display_set_buffers(disp, g_ui_draw_buf, NULL,
UI_FB_SIZE + UI_DRAW_BUF_PALETTE,
LV_DISPLAY_RENDER_MODE_FULL);
lv_display_set_flush_cb(disp, ui_flush_cb);
+7
View File
@@ -34,6 +34,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓
|---|---|---|
| 屏幕 | 160×160,1bpp(`LV_COLOR_FORMAT_I1`) | `ui_port.h` |
| 帧缓冲 | 3200 B(`g_ui_fbuf`,flush 回调整帧 memcpy) | `ui_port.c` |
| 绘制缓冲 | **3200 + palette 8 B**(+32 B 余量):I1 索引色缓冲布局为 `[palette 8B][pixel data]`,见 1.4 第 8 条 | `ui_port.c` |
| LVGL 内存池 | 40 KB(`LV_MEM_SIZE`) | `Lib/lv_conf.h` |
| 刷新周期 | 10 ms(`ui_port_step(10)` + `osDelay(10)`) | `ui_task.c` |
| 推流端口 | 7003(TCP,与 DTU-RemoteLCD 客户端对齐) | `ui_net_sink.h` |
@@ -49,6 +50,12 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓
5. 模拟器与 MCU **共用** UI 源码(`ui_menu*.c` / `ui_font_cn_12.c`),禁止在 `simulator/` 下复制副本;新增文件须同时注册进 Keil `.uvprojx` 与模拟器 CMakeLists(双端注册)
6. 验证顺序:模拟器预览 → `@build`(0 错 0 警)→ `@test`(Required 加 `UI port init: OK;UI menu init: OK`)
7. **`LV_DRAW_SW_COMPLEX` 必须为 1**(两端均显式声明):主界面顶部"开口浅 V"装饰含斜线,`lv_line` 的斜段仅在复杂渲染器下绘制(`draw_line_skew`),关闭则只画水平线、两腿消失。且 `LV_DRAW_SW_CIRCLE_CACHE_SIZE` 须非零(默认 4),否则零长数组编译报错(C2466)
8. **I1 绘制缓冲必须含 palette 头(2026-09-21 实证,别再踩)**:LVGL 9.3 索引色格式的 draw buffer 布局是 `[palette 8B][pixel data stride×h]`——
`lv_draw_buf_create()` 按 `stride×h + LV_COLOR_INDEXED_PALETTE_SIZE(cf)×4` 分配,`lv_draw.c:497 → lv_draw_buf_goto_xy()` 取绘制指针时先 `+= palette`,`lv_refr.c get_max_row()` 也从 `data_size` 里扣掉 palette。
但 `lv_display_set_buffers()` / `lv_draw_buf_init()` 的容量校验**只看 `stride×h`(不含 palette)** → 只给 3200 B 会被静默接受,后果有两条:
① 末 8 个像素字节写到缓冲之外(曾把紧邻 `.bss` 的 `s_frame_box[0]` 指针打脏 → 野指针 HardFault;金丝雀实测越界 n=8);
② flush 回调收到的首地址**含 palette**,照原样整帧拷贝会让推流画面**整体错位 8 字节**。
故 `ui_port.c` 按 `stride×h + palette` 供给缓冲,上报尺寸含 palette,flush 时 `color_p + UI_DRAW_BUF_PALETTE` 取像素。
---
@@ -0,0 +1,71 @@
# STM32F4-Base 存储维护与容量恢复
> 版本:v1.0 日期:2026-09-21
> 适用工程:STM32F4-Base(STM32F407ZGTx + GD5F2GQ5UE SPI NAND + dhara FTL + FatFs)
> 关联:`docs/测试说明/GD5F2GQ5UE_Test_Guide.md`(破坏性用例清单)、`docs/内存管理/内存内容分配策略.md`(DMA 可达性守卫)
## 1. 三个维护动作,别混用
| 目标 | 开关组合 | 做了什么 | 代价 |
|---|---|---|---|
| **A. 卷/BBT 恢复** | `TEST_SUITE_STORAGE` + `ENABLE_STORAGE_RECOVERY_TESTS` | 重建 BBT(重扫出厂坏块并持久化)+ `nand_ftl_format()` 清 dhara map | 卷内容全失;**不恢复容量** |
| **B. 容量恢复** | A 再叠加 `STORAGE_RECOVERY_FULL_WIPE` | 擦净 dhara 可见区 **2044 块**(连 journal checkpoint 一起)+ 重建 BBT;随后**断电重启** | 卷内容全失;2044 次块擦除(实测约 4 秒) |
| **C. 日常回归** | 只开 `TEST_SUITE_*`,**不开** `ENABLE_DESTRUCTIVE_TESTS` | 只跑只读/参数校验用例,不写不擦不改坏块表 | 无 |
三个开关都在 `test/test_config.h`(A/B 的根开关在 `test/storage_test.h`),**默认全关**;维护完请重新注释并编译产品构建。
## 2. 为什么"容量"需要单独恢复(根因)
- 健康满容量 = **96064 pages(187 MB)**(`No valid map found, creating fresh` 分支下的满值)。
- dhara 的坏块计数 `bb_current` / `bb_last` **持久化写在 journal checkpoint 头里**
(`Lib/dhara/journal.c` 的 `hdr_set_bb_current/hdr_set_bb_last`,随 checkpoint 一起落盘)。
容量按 `good_blocks = num_blocks − max(bb_last, bb_current) − 1` 递减。
- `dhara_journal_clear()`(即 `nand_ftl_format()` 走的路)只做 `bb_last = bb_current`,**不归零**——
所以动作 A 清不掉被扣掉的容量,断电重扫也一样(计数从 checkpoint 读回)。
- 只有让下次启动 `dhara_map_resume` **失败**、走 `dhara_map_clear` 的**全新初始化**分支
(此时 `bb_last = bb_current = 0`)才能回满 → 这就是动作 B 的原理。
- 记账的触发:一次 `program verify` 失败(日志 `[ERR][NAND_FTL] program verify: data mismatch, blk N`)
就让 dhara 多记一个坏块;破坏性套件(写完擦、坏块注入、格式化)会累积这个数字。
## 3. 操作步骤(以容量恢复为例)
1. `test/test_config.h`:取消注释 `TEST_SUITE_STORAGE`;
2. `test/storage_test.h`:取消注释 `ENABLE_STORAGE_RECOVERY_TESTS` 与 `STORAGE_RECOVERY_FULL_WIPE`;
3. `@build`(要求 0 错误 0 警告);
4. 采集顺序 `open` → `clear` → 烧录 → `wait-idle`(擦除 2044 块约 4 秒,静默判据给 20~30s);
5. 确认日志:
```
=== Storage Test: TC-STO-04 maintenance (recovery / full wipe) ===
[PASS] TC-STO-04 bbt rebuild
wipe progress: ... 2044/2044
wiped 2044 blocks (N skipped as bad, 2044 visible)
[PASS] TC-STO-04 full wipe (2044 blocks erased)
[PASS] TC-STO-04 persisted BBT version=N
=== Storage Recovery: 3/3 PASSED (failed=0) ===
```
6. **断电重启**(`MDK-ARM/reset.bat` 无参即可)——本次启动才会重建干净 journal;
7. 恢复产品构建(两个头文件重新注释)→ `@build` → 烧录 → `@test`,确认:
```
[NAND_FTL] No valid map found, creating fresh (err=3)
[NAND_FTL] NAND FTL ready (capacity: 96064 pages = 187 MB)
[FS] no FAT volume, formatting NAND... ← 首次会重建 FAT
[FS] FATFS mounted
```
## 4. 注意事项
- **单写者**:套件运行期间存储由 testTask 独占(defaultTask 打印 `mount/cfg skipped (STORAGE suite owns storage)`),勿并发访问。
- **卷内容全失**:A/B 都会丢掉 `/log` 历史与 `/config.ini`(配置回落工厂默认,FAT 由启动流程自动重建)。
- **坏块跳过**:全片擦除会自动跳过 BBT 中已记录的坏块(`skipped as bad`),保留块池(BBT 存储区)不动。
- **别用 A 期待容量回来**:A 只重建 BBT + 格式化,容量仍是被扣掉的值——这是 2026-09-21 实测确认过的。
- **别为了"省一次擦除"而手工擦单块**:容量是否恢复取决于 checkpoint 是否全部失效,只擦单块无法保证(`resume` 会锚定到更早的 checkpoint)。
- 恢复后建议跑一次只读套件确认(GD5F 只读 16/16 或存储只读 3/3)。
## 5. 变更记录
| 日期 | 变更 |
|---|---|
| 2026-09-21 | 新建:A/B/C 三个维护动作;容量恢复原理(dhara checkpoint 坏块计数不清零)与全片擦除恢复路径(实测 95200 → 96064) |