diff --git a/AGENTS.md b/AGENTS.md index 283be213..0d39b211 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/Drivers/BSP/UI/ui_port.c b/Drivers/BSP/UI/ui_port.c index 38d8ae82..631230eb 100644 --- a/Drivers/BSP/UI/ui_port.c +++ b/Drivers/BSP/UI/ui_port.c @@ -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); diff --git a/docs/UI 设计/UI_设计架构.md b/docs/UI 设计/UI_设计架构.md index 4024d770..146c8683 100644 --- a/docs/UI 设计/UI_设计架构.md +++ b/docs/UI 设计/UI_设计架构.md @@ -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` 取像素。 --- diff --git a/docs/测试说明/存储维护与容量恢复.md b/docs/测试说明/存储维护与容量恢复.md new file mode 100644 index 00000000..4e6e6af8 --- /dev/null +++ b/docs/测试说明/存储维护与容量恢复.md @@ -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) |