亚马逊AWS官方博客

AI 辅助嵌入式全流程开发:使用 Kiro 逐步构建智能温湿度监控系统

摘要:本文介绍如何使用 AI 编码工具 Kiro 辅助嵌入式系统的全流程开发。通过一个智能温湿度监控系统的实际案例,展示从需求分析、代码生成到调试部署的完整开发过程,帮助开发者了解如何借助 AI 提升嵌入式开发效率。


一、前言

嵌入式开发一直是软件工程中最“手工”的领域之一:工程师的大量时间并非花在创造性的架构设计上,而是消耗在翻手册、对寄存器、调参数这类机械劳动中。一个完整的嵌入式项目涉及时钟树配置、GPIO 复用、DMA 传输、RTOS 移植、GUI 渲染——每一层都有自己的“Datasheet 陷阱”。

举一个典型场景:要在 STM32H743 上驱动一颗 SDRAM,首先要在数千页的参考手册中找到 FMC 章节,再打开 SDRAM 的 Datasheet 核对时序参数。以 120 MHz 的 SDCLK 为例,72 ns 的 tXSR 需要向上取整为 9 个时钟周期;STM32H7 HAL 接口接收实际周期数,底层再将其转换为寄存器编码。时序单位、时钟来源或刷新公式中的任一环节处理不当,都可能导致 SDRAM 自检偶尔通过而显示仍然异常。

能否让 AI 辅助完成“查手册、整理参数和生成模板代码”等重复工作,让工程师把更多精力放在架构决策、参数复核和异常判断上?Kiro 提供了一套可行的人机协作方式。它的重点不是“一键生成代码”,而是通过三个机制组织开发过程:

  • Spec 驱动——以“需求 → 设计 → 任务”的结构化流程逐层拆解,而不是一次性 prompt;
  • Steering 约束——硬件参数与编码规范一次配置、全程生效;
  • Skills 沉淀——把踩过的坑固化为可复用的团队资产,下次不再重复踩坑。

本文将分 9 个步骤介绍:如何使用 Kiro 辅助在 STM32H743IIT6(Cortex-M7,最高主频 480MHz)上逐步构建一个带 800×480 LCD 曲线显示的温湿度监控系统。流程覆盖硬件连接信息整理、需求与设计拆解、代码实现、编译、烧录和调试。

[图1 系统实机运行效果:800×480 LCD 实时温湿度曲线]

二、前置准备

2.1 硬件清单

器件 型号 用途
开发板 STM32H743IIT6(176 引脚 LQFP) 主控 MCU,最高主频 480MHz Cortex-M7
温湿度传感器 DHT20(I2C,7 位地址 0x38) 数据采集
显示屏 800×480 RGB LCD(LTDC 接口) 曲线显示
外部内存 W9825G6KH-6I(32MB SDRAM) 帧缓冲 + LVGL Draw Buffer
调试器 DAPLink(CMSIS-DAP) SWD 烧录

2.2 软件环境

工具 版本 用途
Kiro kiro-ide-0.11.131-stable AI 开发环境
arm-none-eabi-gcc 12.x+ 交叉编译器
pyOCD 0.42+ CMSIS-DAP 烧录
串口终端 screen / minicom 115200 8N1 日志

[图2 STM32H743 开发板、DHT20 传感器与 RGB LCD 连接实物]

三、步骤 1:构建 Datasheet 知识库

嵌入式开发离不开查手册:SDRAM 的时序参数、DHT20 的 I2C 协议、LTDC 的像素时钟范围等信息散落在大量 PDF 中。通过 MCP 连接 Amazon Bedrock Knowledge Bases,可以让 Kiro 基于检索到的手册内容回答问题,减少仅凭模型记忆作答的风险。检索结果仍需工程师对照原始 Datasheet 复核。系统架构如下:

[图3 Datasheet 知识库系统架构]

3.1 Datasheet 文档管理全流程

3.1.1 阶段一:上传

User (Kiro) → DataSheet MCP → S3 Draft Bucket
  1. 用户通过 Kiro 的 DataSheet MCP 工具上传文档(PDF、Word、技术规格书等);
  2. MCP 将文档写入 Amazon S3 Draft Bucket(草稿桶),并附带元数据(上传者、时间、文档类型、版本号);
  3. 此时文档状态为“待审批”,不对外可见。

3.1.2 阶段二:事件驱动审批

S3 Draft Bucket → (S3 Event) → EventBridge → Step Functions → SNS 通知审批人
                                                             → 审批通过 → S3 Production
                                                             → 审批拒绝 → 通知作者修改
  1. 在 Draft Bucket 已启用向 Amazon EventBridge 投递事件后,PutObject 事件会发送至 EventBridge;该配置启用后通常需要数分钟生效;
  2. EventBridge 规则匹配后触发 AWS Step Functions 审批工作流;
  3. Step Functions 执行以下步骤:
    • 通过 Amazon SNS 向审批人发送通知;如需投递到协作软件,还需配置相应的订阅端点或集成服务;
    • 通过回调任务令牌等方式等待审批结果,并设置超时时间;
    • 审批通过:将文档从 Draft Bucket 复制到 Production Bucket,确认复制成功后再删除草稿副本,状态标记为“已发布”;
    • 审批拒绝:通过 SNS 将修改意见通知作者,文档保留在 Draft Bucket,状态改为“需修改”;
    • 超时未审批:按工作流配置升级通知或结束审批。

3.1.3 阶段三:自动入库 Knowledge Base

S3 Production → (S3 Event) → EventBridge → Lambda → Textract(文本提取)
                                                   → Bedrock Knowledge Base(向量化入库)
  1. 文档落入 S3 Production Bucket 后,在该 Bucket 同样启用 EventBridge 事件投递的前提下,S3 事件再次触发 EventBridge;
  2. EventBridge 触发 AWS Lambda 函数;
  3. Lambda 执行:
    • 按文档类型选择 Amazon Textract、Bedrock Knowledge Bases 支持的解析器或自定义解析流程;
    • 对提取的文本进行清洗与分块(chunking);
    • 通过与所选 Knowledge Base 数据源相匹配的摄取流程完成向量化和索引更新;
    • 将处理状态(成功、失败和重试次数)记录到 Amazon DynamoDB
  4. 入库并完成同步后,用户即可通过 Bedrock Knowledge Bases 的检索增强生成能力对 Datasheet 进行自然语言问答。

3.2 Kiro 查询知识库

User 提问 → Kiro → MCP → Knowledge Base 检索 → LLM 生成回答 → 返回用户

[图4 Kiro 查询知识库流程]

3.3 示例:SDRAM 初始化

下面以 SDRAM 初始化为例。Kiro 通过 DataSheet MCP 查询 STM32H743 FMC 控制器与 W9825G6KH-6I 的时序参数,并结合项目实际硬件配置推导出 FMC 寄存器值:

[图5 Kiro 调用 DataSheet MCP 查询 SDRAM 时序参数]

完整的 SDRAM 初始化代码如下:

/**
 * bsp_sdram.c — W9825G6KH-6I (32MB, 16-bit) via STM32H743 FMC
 *
 * 时序参数来源:
 *   - STM32H743 RM0433 Chapter 22 (FMC SDRAM controller)
 *   - W9825G6KH-6I Datasheet (speed grade -6I)
 *   - FMC_SDCLK = HCLK / 2 = 240MHz / 2 = 120MHz → 8.3ns per cycle
 */

#include "bsp_sdram.h"

SDRAM_HandleTypeDef hsdram;

/* ---- MPU 配置:Write-Through, Cacheable ---- */
static void SDRAM_MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    HAL_MPU_Disable();

    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = 0xC0000000;          /* SDRAM Bank 1 */
    MPU_InitStruct.Size             = MPU_REGION_SIZE_32MB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;  /* B=0 */
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_CACHEABLE;       /* C=1 */
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;             /* TEX=0 → Write-Through */
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

/* ---- SDRAM 初始化命令序列(按 W9825G6KH 规格) ---- */
static void SDRAM_InitSequence(void)
{
    FMC_SDRAM_CommandTypeDef Command = {0};

    /* Step 1: Clock Enable */
    Command.CommandMode       = FMC_SDRAM_CMD_CLK_ENABLE;
    Command.CommandTarget     = FMC_SDRAM_CMD_TARGET_BANK1;
    Command.AutoRefreshNumber = 1;
    Command.ModeRegisterDefinition = 0;
    HAL_SDRAM_SendCommand(&hsdram, &Command, 0xFFFF);

    /* Step 2: 等待时钟稳定 ≥100µs */
    HAL_Delay(1);

    /* Step 3: Precharge All */
    Command.CommandMode = FMC_SDRAM_CMD_PALL;
    HAL_SDRAM_SendCommand(&hsdram, &Command, 0xFFFF);

    /* Step 4: Auto Refresh ×8 */
    Command.CommandMode       = FMC_SDRAM_CMD_AUTOREFRESH_MODE;
    Command.AutoRefreshNumber = 8;
    HAL_SDRAM_SendCommand(&hsdram, &Command, 0xFFFF);

    /* Step 5: Load Mode Register
     *   Burst Length = 1
     *   Burst Type   = Sequential
     *   CAS Latency  = 3 (匹配 FMC CASLatency 配置)
     *   Operating Mode = Standard
     *   Write Burst  = Single
     */
    Command.CommandMode = FMC_SDRAM_CMD_LOAD_MODE;
    Command.AutoRefreshNumber = 1;
    Command.ModeRegisterDefinition =
        (0x0000 << 0) |  /* Burst Length = 1 */
        (0x0000 << 3) |  /* Burst Type = Sequential */
        (0x0003 << 4) |  /* CAS Latency = 3 */
        (0x0000 << 7) |  /* Operating Mode = Standard */
        (0x0001 << 9);   /* Write Burst = Single */
    HAL_SDRAM_SendCommand(&hsdram, &Command, 0xFFFF);
}

/* ---- 主初始化函数 ---- */
uint8_t BSP_SDRAM_Init(void)
{
    FMC_SDRAM_TimingTypeDef SDRAM_Timing = {0};

    /* 1. MPU 配置 */
    SDRAM_MPU_Config();

    /* 2. FMC 控制器参数 — W9825G6KH: 13-bit row, 9-bit column, 16-bit data, 4 banks */
    hsdram.Instance                = FMC_SDRAM_DEVICE;
    hsdram.Init.SDBank             = FMC_SDRAM_BANK1;
    hsdram.Init.ColumnBitsNumber   = FMC_SDRAM_COLUMN_BITS_NUM_9;   /* 512 columns */
    hsdram.Init.RowBitsNumber      = FMC_SDRAM_ROW_BITS_NUM_13;     /* 8192 rows */
    hsdram.Init.MemoryDataWidth    = FMC_SDRAM_MEM_BUS_WIDTH_16;    /* 16-bit */
    hsdram.Init.InternalBankNumber = FMC_SDRAM_INTERN_BANKS_NUM_4;  /* 4 banks */
    hsdram.Init.CASLatency         = FMC_SDRAM_CAS_LATENCY_3;
    hsdram.Init.WriteProtection    = FMC_SDRAM_WRITE_PROTECTION_DISABLE;
    hsdram.Init.SDClockPeriod      = FMC_SDRAM_CLOCK_PERIOD_2;      /* SDCLK = HCLK/2 = 120MHz */
    hsdram.Init.ReadBurst          = FMC_SDRAM_RBURST_ENABLE;
    hsdram.Init.ReadPipeDelay      = FMC_SDRAM_RPIPE_DELAY_1;

    /* 3. 时序参数 — 来自 W9825G6KH-6I Datasheet + Kiro Datasheet MCP 查询 */
    SDRAM_Timing.LoadToActiveDelay    = 2;  /* tMRD = 2 cycles */
    SDRAM_Timing.ExitSelfRefreshDelay = 7;  /* tXSR = 72ns → 9 cycles → reg=7 */
    SDRAM_Timing.SelfRefreshTime      = 4;  /* tRAS = 42ns → 6 cycles → reg=4 */
    SDRAM_Timing.RowCycleDelay        = 7;  /* tRC  = 60ns → 8 cycles → reg=7 */
    SDRAM_Timing.WriteRecoveryTime    = 2;  /* tWR  = 2 cycles */
    SDRAM_Timing.RPDelay              = 2;  /* tRP  = 15ns → 2 cycles */
    SDRAM_Timing.RCDDelay             = 2;  /* tRCD = 15ns → 2 cycles */

    /* 4. 初始化 FMC */
    if (HAL_SDRAM_Init(&hsdram, &SDRAM_Timing) != HAL_OK)
        return 1;

    /* 5. 执行 SDRAM 初始化命令序列 */
    SDRAM_InitSequence();

    /* 6. 设置刷新率
     *   W9825G6KH: 8192 rows, 64ms refresh period
     *   Refresh Rate = (64ms / 8192) × 120MHz - 20 = 917
     */
    if (HAL_SDRAM_ProgramRefreshRate(&hsdram, 917) != HAL_OK)
        return 2;

    return 0;
}

该示例中的 FMC 时序字段仍需按实际 FMC 时钟重新计算,并注意 STM32H7 HAL 参数传入的是实际周期数,而不是已经减 1 的寄存器编码值。

四、步骤 2:导入原理图——AI 辅助整理硬件连接

硬件原理图是电子元器件之间电气连接关系的完整描述。一张典型的 STM32H743 开发板原理图包含上百个元器件、数百条网络连接,如何让 AI 准确理解这些硬件信息?有两种可行的方案:

方案一:多模态视觉识别(PDF/图片)。直接将原理图 PDF 或截图交给 AI,利用多模态能力识别元器件和连接关系。这种方式对简单电路(几十个引脚的 MCU)效果尚可,但面对 STM32H743 这样 176 引脚的 MCU,加上 SDRAM 的 40 余根数据/地址线、LTDC 的 24 根 RGB 信号线,识别准确率明显下降——漏识别一个引脚复用关系,就可能导致后续驱动代码配错 GPIO。此外,不同模型的识别能力参差不齐,结果不够稳定。

方案二:网表文本解析(本文采用)。网表(Netlist)是从 EDA 工具(Altium Designer、KiCad、立创 EDA 等)导出的结构化文本,记录元器件、封装和网络连接关系。相比直接识别原理图图片,网表更便于检索、版本管理和程序化处理,但它不一定包含全部设计意图、电气约束或版图信息,AI 的解析结果也仍可能出现遗漏或误判。

具体做法:通过 Kiro 的 filesystem MCP 读取网表文件,由 AI 辅助整理 MCU 参数、外设清单和引脚映射表,并标记潜在冲突供工程师复核。两种方案的对比如下:

维度 多模态视觉识别 网表文本解析
输入格式 PDF/图片 .net 等结构化文本
可靠性 受图片清晰度、版面和模型能力影响 连接关系更易追溯,但解析结果仍需校验
适用范围 适合快速理解局部或较简单电路 更适合连接密集、需要批量核对的设计
可复现性 受模型和图片质量影响较大 输入可固定,输出仍可能受模型版本与提示影响
版本管理 图片不便直接 diff 文本文件便于 Git 管理和差异比较

⚠️ 重要提示:

无论采用哪种方案,AI 输出的硬件描述都必须经过人工检查确认。尤其是引脚复用关系和 I2C/SPI 总线上的设备地址,它们是后续驱动代码正确性的基础。确认无误后再锁定硬件配置,进入下一步。

4.1 Kiro 输出:外设清单

外设 芯片型号 接口 关键引脚
SDRAM W9825G6KH-6I FMC A0-A12、BA0-BA1、D0-D15
USB 转串口 CH340X USART1 PA9(TX)、PA10(RX)
温湿度传感器 DHT20 I2C PH4(SCL)、PH5(SDA)
RGB LCD TFT 40P LTDC R0-R7、G0-G7、B0-B7、HSYNC、VSYNC、DE、PCLK
LED 红、绿 GPIO PB1(红)、PB0(绿)

五、步骤 3:生成 Steering 约束文档

嵌入式开发有一个独特的挑战:同样的硬件,不同工程师写出的代码可能天差地别。同样是驱动一颗 SDRAM,有人把初始化代码写在 main.c 里,有人封装成 BSP_SDRAM_Init();有人使用硬编码的魔法数字 0xC0000000,有人用 #define SDRAM_BANK_ADDR 集中管理;有人在 HAL 初始化失败时默默忽略返回值,有人严格检查每一步并调用 Error_Handler()。经验丰富的工程师会自然地遵循分层架构、统一命名、防御性编程这些最佳实践,而初级工程师往往需要踩过几次坑之后才能形成这些习惯。

用 AI 生成代码时,这个问题同样存在——如果不加约束,AI 每次生成的代码风格可能都不一样:这次用 bsp_ 前缀,下次用 drv_ 前缀;这次把 GPIO 配置写在 MspInit 回调里,下次直接写在初始化函数中。代码能跑,但不一致、不可维护。

如何提高 AI 生成代码时的一致性?一种方式是使用 Steering。Steering 把硬件参数、编码规范和工具链约定写成持久化文档,并支持始终包含、按文件匹配、手动包含等加载方式。对于应影响整个项目的核心规范,可配置为始终包含;对于特定模块的详细规则,应选择更有针对性的加载方式。基于前两步的 Datasheet 查询和硬件连接信息整理,本文使用三份约束文档:

.kiro/steering/
├── hardware-spec.md      # 硬件规格:MCU 参数 / SDRAM 地址 / LCD 时序 / DHT20 引脚
├── coding-standards.md   # 编码规范:BSP_<Module>_<Verb>() 命名 / HAL 分层 / 错误处理
└── toolchain.md          # 工具链:GCC -mcpu=cortex-m7 / pyocd 烧录 / 115200 串口

coding-standards.md 为例,它约束了:

  • 文件组织:每个外设驱动成对出现在 bsp_xxx.hbsp_xxx.c
  • 接口命名:统一使用 BSP_<Module>_<Verb>() 格式,如 BSP_SDRAM_Init()BSP_LCD_FillScreen()
  • 错误处理:所有 HAL_xxx_Init() 的返回值必须检查,失败时返回错误码或调用 Error_Handler()
  • GPIO 配置:统一在 HAL_xxx_MspInit() 回调中完成,不散落在业务代码里;
  • 常量管理:硬件地址、引脚定义、时序参数全部以宏定义集中在头文件中,禁止魔法数字。

这些 Steering 有助于让后续生成的 BSP 驱动、FreeRTOS 任务和 LVGL 移植层保持一致。它的价值是把团队规范从口头约定或 Code Review 阶段前置到生成阶段,减少命名、分层和错误处理上的偏差。不过,Steering 不能替代代码审查、编译检查和硬件验证。

六、步骤 4:Spec 驱动——需求 → 设计 → 任务拆解

AI 编程助手已经能很好地完成局部任务:补全一个函数、修复一个 bug、解释一段代码。但嵌入式系统开发不是一个个孤立的函数——它是一项有严格依赖顺序的工程:SDRAM 必须在 LCD 之前初始化(因为帧缓冲位于 SDRAM);LVGL 必须在 FreeRTOS 之后启动(因为它依赖任务调度);DHT20 的采样周期必须与 UI 刷新率解耦(否则传感器阻塞会导致界面卡顿)。

这些系统级约束很难通过一条提示词完整表达。AI 可以生成一个 BSP_SDRAM_Init() 函数,但工程仍需明确它在 main() 中的调用位置、LVGL Draw Buffer 与 LCD 帧缓冲的边界,以及 FreeRTOS 任务之间的优先级和通信关系。

Kiro Spec 通过 Requirements → Design → Tasks 三层结构化文档,把“做一个温湿度监控系统”这类目标逐步细化为可执行任务,并在各阶段保留人工审查和确认的机会:

  • Requirements:定义“系统应该做什么”——用 EARS 等结构化方式写出可验证的验收标准;
  • Design:定义“系统怎么做”——包括架构分层、内存映射、任务模型和启动顺序;
  • Tasks:定义“具体实现哪些内容”——标明相关文件、接口、依赖关系和 Requirement。

这三层文档不应一次生成就直接定稿。Kiro 先产出初稿,工程师根据手册、原理图和资源约束提出修改意见,再持续迭代,直到关键参数和验收方法具备可追溯依据。这就是结构化协作与单次提示词生成的主要区别。

6.1 Requirements(EARS 格式)

本文案例生成了 12 条需求,覆盖从底层串口驱动到上层曲线 UI 的主要功能,并为每条需求附上 EARS 风格的验收标准(Acceptance Criteria)。以下内容保留原始 Spec 示例;其中工具链描述仍混有早期 Keil/RVDS 方案,本文最终实现路线以 GCC、GCC FreeRTOS port、newlib-nano _write 重定向和 ELF/HEX 产物为准:

# Requirements Document

## Introduction

本文档定义了在 STM32H743IIT6 开发板上构建「环境监测系统」的分阶段功能需求。
系统以渐进式点亮 (incremental bring-up) 方式依次启用 USART、LED、FreeRTOS、SDRAM、RGB LCD、LVGL、DHT20 传感器,最终在 800×480 LCD 上以示波器样式的曲线图展示实时温湿度,并在图表下方提供图例与实时数值。

每一阶段均以前一阶段的功能为基础,且必须通过 DAPLINK 下载并复位运行后通过可观测证据(串口打印、LED 行为、屏幕显示)自验证。所有驱动应遵循 `skills/` 目录下的指南实现,以保持代码风格与工程结构一致。

## Glossary

- **Firmware**: 在 STM32H743IIT6 上运行的完整二进制镜像,由本项目 Keil/MDK-ARM 工程编译产出。
- **Target_Board**: 基于 STM32H743IIT6 的目标硬件板,已连接 DAPLINK。
- **DAPLINK_Flasher**: 通过 CMSIS-DAP 协议将 Firmware 烧录到 Target_Board 并触发复位的工具链(由 Kiro Agent 通过命令行调用)。
- **BSP_USART_Driver**: `Drivers/BSP/Src/bsp_usart.c` + `Drivers/BSP/Inc/bsp_usart.h`,提供 `BSP_USART1_Init()` 与 `printf` 重定向。
- **BSP_LED_Driver**: `Drivers/BSP/Src/bsp_led.c` + `Drivers/BSP/Inc/bsp_led.h`,提供 `BSP_LED_Init`、`BSP_LED_On`、`BSP_LED_Off`、`BSP_LED_Toggle`。
- **BSP_SDRAM_Driver**: `Drivers/BSP/Src/bsp_sdram.c` + `Drivers/BSP/Inc/bsp_sdram.h`,基于 FMC 控制器驱动外部 SDRAM,基地址 `0xC0000000`。
- **BSP_LCD_Driver**: `Drivers/BSP/Src/bsp_lcd.c` + `Drivers/BSP/Inc/bsp_lcd.h`,基于 LTDC 的 800×480 RGB LCD 驱动,像素格式 ARGB8888,帧缓冲位于 SDRAM。
- **BSP_DHT20_Driver**: `Drivers/BSP/Src/bsp_dht20.c` + `Drivers/BSP/Inc/bsp_dht20.h`,DHT20 I2C 温湿度传感器驱动(SDA=PH5, SCL=PH4)。
- **FreeRTOS_Kernel**: FreeRTOS V10.x 内核,移植层选用 `portable/RVDS/ARM_CM7/r0p1`,内存管理采用 `heap_4`,配置文件为 `User/Inc/FreeRTOSConfig.h`。
- **LVGL_Stack**: LVGL v8.3.11 图形库及其移植层 `Middlewares/LVGL_Port/`,颜色深度 32 位 ARGB8888,启用 DMA2D 加速。
- **UI_Task**: FreeRTOS 任务 `LVGL_Task`,周期性调用 `lv_timer_handler()` 以驱动 LVGL 渲染。
- **Sensor_Task**: FreeRTOS 任务 `Sensor_Task`,周期性调用 `BSP_DHT20_Read()` 并将温湿度结果推送至 UI_Task。
- **Chart_Screen**: LVGL 主界面,包含温湿度曲线图 (Chart_Widget)、图例 (Legend) 与实时数值标签 (Value_Labels)。
- **Chart_Widget**: 基于 `lv_chart` 的曲线控件,显示温度与湿度两条折线,以环形缓冲方式追加最新采样点,形成类示波器的滚动效果。
- **Sample_Point**: 一次 DHT20 采样的结果 `{temperature: float °C, humidity: float %RH, timestamp: ms}`。
- **EARS_Pattern**: 本文档所有需求遵循的 EARS 语法模板(Ubiquitous / Event-driven / State-driven / Unwanted event / Optional feature / Complex)。

## Requirements

### Requirement 1: USART1 串口驱动与 printf 重定向

**User Story:** 作为嵌入式开发者,我希望通过 USART1 获得运行日志,以便在后续每一阶段验证代码是否按预期执行。

#### Acceptance Criteria

1. THE BSP_USART_Driver SHALL 提供函数 `BSP_USART1_Init()`,按 115200 bps、8N1、无硬件流控初始化 USART1。
2. THE BSP_USART_Driver SHALL 将 USART1 的 TX 配置为 PA9、RX 配置为 PA10,GPIO 复用号为 `GPIO_AF7_USART1`。
3. THE BSP_USART_Driver SHALL 实现 `fputc(int ch, FILE *f)`,使标准库 `printf` 通过 USART1 输出。
4. WHEN `BSP_USART1_Init()` 被调用且返回,THE BSP_USART_Driver SHALL 允许随后的 `printf("...")` 调用在串口终端上产生可读 ASCII 输出。
5. IF `HAL_UART_Init()` 返回 `HAL_ERROR`,THEN THE BSP_USART_Driver SHALL 调用 `Error_Handler()`。
6. THE BSP_USART_Driver SHALL 遵循 `skills/usart-driver.md` 中定义的文件结构与代码风格。

### Requirement 2: 板载 LED GPIO 驱动

**User Story:** 作为嵌入式开发者,我希望通过 LED 翻转作为最小可视心跳,确认 MCU 时钟与 GPIO 子系统正常工作。

#### Acceptance Criteria

1. THE BSP_LED_Driver SHALL 提供 `BSP_LED_Init()`、`BSP_LED_On(LED_TypeDef)`、`BSP_LED_Off(LED_TypeDef)`、`BSP_LED_Toggle(LED_TypeDef)` 四个接口。
2. THE BSP_LED_Driver SHALL 将 LED 引脚配置为推挽输出、无上下拉、低速。
3. WHEN `BSP_LED_Init()` 被调用且返回,THE BSP_LED_Driver SHALL 使 LED 初始状态为熄灭。
4. WHEN `BSP_LED_Toggle(LED_RED)` 被连续调用,THE BSP_LED_Driver SHALL 使红色 LED 的电平在每次调用后反转一次。
5. THE BSP_LED_Driver SHALL 遵循 `skills/led-driver.md` 中定义的文件结构与代码风格。

### Requirement 3: 通过 DAPLINK 下载固件并复位运行(阶段一验证)

**User Story:** 作为嵌入式开发者,我希望在阶段一完成后立即通过 DAPLINK 下载并复位运行固件,以便用 LED 心跳和串口输出验证板子与驱动是否工作。

#### Acceptance Criteria

1. THE Firmware SHALL 在 Keil/MDK-ARM 工程中完成对 `bsp_usart.c` 和 `bsp_led.c` 的编译,输出产物为 `.hex` 或 `.axf` 文件。
2. WHEN 阶段一的代码编写完成,THE DAPLINK_Flasher SHALL 使用项目可用的命令行工具(例如 `pyocd flash` 或 Keil CLI)将固件烧录至 Target_Board。
3. WHEN 烧录完成,THE DAPLINK_Flasher SHALL 向 Target_Board 发送复位命令(例如 `pyocd reset`)使固件立即开始运行。
4. WHEN 阶段一固件运行,THE Firmware SHALL 以 500 ms 周期翻转一个板载 LED,并通过 USART1 每秒打印一次心跳信息(例如 `"[heartbeat] tick=<n>"`)。
5. IF 烧录或复位命令返回非零退出码,THEN THE Firmware SHALL 被视为未部署成功,且应回显错误输出以便排错。

### Requirement 4: FreeRTOS 内核移植

**User Story:** 作为嵌入式开发者,我希望在工程中移植 FreeRTOS 内核,以便后续以任务方式组织传感器采集与 UI 渲染。

#### Acceptance Criteria

1. THE FreeRTOS_Kernel SHALL 位于 `Middlewares/FreeRTOS/Source/` 目录下,启用 `portable/RVDS/ARM_CM7/r0p1/port.c` 与 `portable/MemMang/heap_4.c`。
2. THE Firmware SHALL 在 `User/Inc/FreeRTOSConfig.h` 中声明 `configCPU_CLOCK_HZ = 480000000`、`configTICK_RATE_HZ = 1000`、`configTOTAL_HEAP_SIZE ≥ 64 × 1024`。
3. THE Firmware SHALL 在 `SysTick_Handler()` 中按 `skills/freertos-porting.md` 第四步同时调用 `HAL_IncTick()` 与 `xPortSysTickHandler()`(后者仅在调度器已启动时调用)。
4. THE Firmware SHALL 通过宏 `vPortSVCHandler = SVC_Handler`、`xPortPendSVHandler = PendSV_Handler` 将对应中断映射到 FreeRTOS 移植层。
5. THE Firmware SHALL 实现钩子函数 `vApplicationMallocFailedHook()` 与 `vApplicationStackOverflowHook()`。
6. WHEN `vTaskStartScheduler()` 被 `main()` 调用,THE Firmware SHALL 进入多任务调度状态且不返回 `main()`。
7. WHILE 调度器正在运行,THE Firmware SHALL 使至少一个 LED 任务以 FreeRTOS `vTaskDelay()` 的时间精度翻转 LED,作为调度正常的可见证据。
8. THE FreeRTOS_Kernel 的集成 SHALL 遵循 `skills/freertos-porting.md` 中的移植检查清单。

### Requirement 5: 外部 SDRAM 驱动

**User Story:** 作为嵌入式开发者,我希望启用板载 SDRAM,以便为 LCD 帧缓冲与 LVGL Draw Buffer 提供足够内存。

#### Acceptance Criteria

1. THE BSP_SDRAM_Driver SHALL 通过 FMC 控制器将 SDRAM 映射到 `0xC0000000` 起始地址,容量 `SDRAM_SIZE` ≥ `0x02000000`(32 MB)。
2. THE BSP_SDRAM_Driver SHALL 按 `skills/sdram-driver-guide.md` 中的命令序列执行 Clock Enable → Precharge All → Auto Refresh ×8 → Load Mode Register 初始化流程。
3. THE BSP_SDRAM_Driver SHALL 通过 `HAL_SDRAM_ProgramRefreshRate()` 配置刷新计数,使数据在静态保持状态下不丢失。
4. THE BSP_SDRAM_Driver SHALL 配置 MPU 区域 0 覆盖 SDRAM 地址空间,启用 Cacheable 与 Write-Through 策略。
5. THE BSP_SDRAM_Driver SHALL 提供 `BSP_SDRAM_Test()`,对 SDRAM 依次进行 32 位、16 位、8 位读写验证。
6. WHEN `BSP_SDRAM_Init()` 与 `BSP_SDRAM_Test()` 顺序调用,THE BSP_SDRAM_Driver SHALL 在 `BSP_SDRAM_Test()` 中返回 `0`,并通过 USART1 打印测试结果(例如 `"SDRAM OK: 32MB at 0xC0000000"`)。
7. IF `HAL_SDRAM_Init()` 或 `HAL_SDRAM_ProgramRefreshRate()` 返回非 `HAL_OK`,THEN THE BSP_SDRAM_Driver SHALL 返回非零错误码以便上层识别。

### Requirement 6: 800×480 RGB LCD 驱动

**User Story:** 作为嵌入式开发者,我希望点亮 800×480 RGB LCD 并验证帧缓冲,以便作为后续 LVGL 渲染的显示基础。

#### Acceptance Criteria

1. THE BSP_LCD_Driver SHALL 通过 LTDC 控制器驱动分辨率 800×480、像素格式 `LTDC_PIXEL_FORMAT_ARGB8888`、帧缓冲起始地址 `0xC0000000` 的 RGB LCD。
2. THE BSP_LCD_Driver SHALL 按 `skills/rgb-lcd-driver-guide.md` 中的默认时序配置:HSW=48、HBP=88、HFP=40、VSW=3、VBP=32、VFP=13。
3. THE BSP_LCD_Driver SHALL 通过 `LCD_ClockConfig()` 将 PLL3 配置为约 33 MHz 的像素时钟。
4. THE BSP_LCD_Driver SHALL 提供 `BSP_LCD_Init()`、`BSP_LCD_BacklightOn()`、`BSP_LCD_BacklightOff()`、`BSP_LCD_FillScreen(uint32_t color)`、`BSP_LCD_Clear()` 五个接口。
5. THE BSP_LCD_Driver SHALL 在 `BSP_LCD_Init()` 内部依次执行硬件复位、时钟配置、LTDC 控制器初始化、Layer 0 配置、清屏、打开背光。
6. THE Firmware SHALL 在 `main()` 中保证 `BSP_SDRAM_Init()` 在 `BSP_LCD_Init()` 之前被调用。
7. WHEN `BSP_LCD_Init()` 返回 `0` 后调用 `BSP_LCD_FillScreen(LCD_COLOR_RED)`、`LCD_COLOR_GREEN`、`LCD_COLOR_BLUE`,THE BSP_LCD_Driver SHALL 在屏幕上分别显示纯红、纯绿、纯蓝画面。

### Requirement 7: LVGL v8.3 移植与 DMA2D 加速

**User Story:** 作为嵌入式开发者,我希望将 LVGL 移植到工程并启用 DMA2D 加速,以便使用控件库构建温湿度界面。

#### Acceptance Criteria

1. THE LVGL_Stack SHALL 位于 `Middlewares/LVGL/`(源码)与 `Middlewares/LVGL_Port/`(移植层)目录下,版本为 v8.3.x。
2. THE LVGL_Stack SHALL 通过 `Middlewares/lv_conf.h` 设定 `LV_COLOR_DEPTH = 32`、`LV_MEM_SIZE ≥ 48 × 1024`、`LV_USE_GPU_STM32_DMA2D = 1`、`LV_SPRINTF_USE_FLOAT = 1`。
3. THE LVGL_Stack SHALL 通过 `lv_port_disp.c` 注册双 Draw Buffer,缓冲区位于 SDRAM 帧缓冲之后的连续区域(例如 `0xC0180000` 与 `0xC01A5800`,每块 ≥ 48 行)。
4. THE LVGL_Stack SHALL 在 `disp_flush_cb` 中使用 DMA2D 以内存到内存模式将 Draw Buffer 拷贝到 LCD 帧缓冲,并在 DMA2D 传输完成中断中调用 `lv_disp_flush_ready()`。
5. THE Firmware SHALL 创建 FreeRTOS 任务 `LVGL_Task`,栈大小 ≥ 4096 字,优先级为可配置常量(例如 `LVGL_TASK_PRIO`),任务体周期性调用 `lv_timer_handler()`,每次调用后 `vTaskDelay(pdMS_TO_TICKS(5))`。
6. WHEN `LVGL_Task` 成功启动并创建至少一个可见控件,THE LVGL_Stack SHALL 在 LCD 上渲染出该控件且无撕裂或花屏。
7. THE LVGL_Stack SHALL 遵循 `skills/lvgl-porting-guide.md` 中描述的目录结构、缓冲区布局与初始化顺序。

### Requirement 8: DHT20 温湿度传感器驱动

**User Story:** 作为嵌入式开发者,我希望读取 DHT20 的温湿度数据,以便为 UI 提供真实采样数据源。

#### Acceptance Criteria

1. THE BSP_DHT20_Driver SHALL 通过 I2C 接口与地址 `0x38` 的 DHT20 通信,SDA 引脚为 PH5、SCL 引脚为 PH4。
2. THE BSP_DHT20_Driver SHALL 提供 `BSP_DHT20_Init()` 与 `BSP_DHT20_Read(float *temperature, float *humidity)` 两个接口。
3. WHEN `BSP_DHT20_Init()` 被调用,THE BSP_DHT20_Driver SHALL 等待不少于 100 ms,然后读取状态字节并在 bit3 为 0 时写入校准寄存器 `0x1B`、`0x1C`、`0x1E`。
4. WHEN `BSP_DHT20_Read()` 被调用,THE BSP_DHT20_Driver SHALL 依次发送触发测量命令 `0xAC 0x33 0x00`、延时不少于 80 ms、读取 6 字节数据。
5. THE BSP_DHT20_Driver SHALL 按 `humidity = humi_raw / 2^20 × 100`、`temperature = temp_raw / 2^20 × 200 − 50` 的公式将原始寄存器值转换为物理量。
6. WHEN DHT20 响应字节的 bit7(忙标志)保持为 1 超过 `DHT20_MAX_RETRY = 3` 次重试,THEN THE BSP_DHT20_Driver SHALL 从 `BSP_DHT20_Read()` 返回 `DHT20_ERR_BUSY`。
7. WHEN `BSP_DHT20_Read()` 成功返回,THE BSP_DHT20_Driver SHALL 使 `*temperature` 落入区间 `[-40.0 °C, +85.0 °C]`、`*humidity` 落入区间 `[0.0 %RH, 100.0 %RH]`。
8. THE BSP_DHT20_Driver SHALL 遵循 `skills/dht20-driver-guide.md` 中的驱动架构与时序要求。

### Requirement 9: 温湿度采集任务

**User Story:** 作为嵌入式开发者,我希望有一个独立的 FreeRTOS 任务周期性采集 DHT20 数据,以便将数据稳定地提供给 UI 任务。

#### Acceptance Criteria

1. THE Firmware SHALL 创建 FreeRTOS 任务 `Sensor_Task`,栈大小 ≥ 512 字,优先级低于 `LVGL_Task`。
2. THE Sensor_Task SHALL 以周期 1000 ms 调用 `BSP_DHT20_Read()`。
3. THE Firmware SHALL 使用 FreeRTOS 队列或受互斥量保护的共享变量在 `Sensor_Task` 与 `LVGL_Task` 之间传递最新的 Sample_Point。
4. WHEN `BSP_DHT20_Read()` 返回 `DHT20_OK`,THE Sensor_Task SHALL 将新 Sample_Point 发布给 UI_Task 并通过 USART1 打印单行日志(例如 `"T=25.32C H=48.71%"`)。
5. IF `BSP_DHT20_Read()` 连续 5 次返回非 `DHT20_OK`,THEN THE Sensor_Task SHALL 通过 USART1 打印错误日志且不向 UI_Task 推送无效数据。

### Requirement 10: 示波器样式温湿度曲线 UI

**User Story:** 作为使用者,我希望在 LCD 上看到类似示波器的温湿度曲线,并在图表下方看到图例与当前数值,以便直观掌握环境变化。

#### Acceptance Criteria

1. THE Chart_Screen SHALL 在 800×480 分辨率下,将上半部分(≥ 60% 的屏幕高度)用于 Chart_Widget,下半部分用于 Legend 与 Value_Labels。
2. THE Chart_Widget SHALL 基于 `lv_chart` 的 `LV_CHART_TYPE_LINE` 类型,且在同一坐标系内同时绘制两条数据序列:温度线与湿度线。
3. THE Chart_Widget SHALL 维护一个环形采样缓冲(点数 ≥ 100、≤ 400),WHEN 新 Sample_Point 被 UI_Task 接收,Chart_Widget SHALL 以 `lv_chart_set_next_value()` 将最新温度与湿度分别追加到两条序列的末端,使旧点左移、新点从右侧进入,形成滚动效果。
4. THE Chart_Widget SHALL 为温度线配置温度量程(例如 `[-10 °C, 60 °C]`,可在源码常量集中修改)、为湿度线配置湿度量程 `[0 %RH, 100 %RH]`,两条线分别使用独立 Y 轴或独立归一化显示。
5. THE Legend SHALL 位于 Chart_Widget 下方,包含两行,每行由一段颜色与 Chart_Widget 中对应序列一致的色块与文本标签组成(例如 `"■ Temperature (°C)"`、`"■ Humidity (%RH)"`)。
6. THE Value_Labels SHALL 位于 Legend 附近或同一行内,以不小于 24 像素的字体显示当前温度与湿度的数值(保留小数点后一位,例如 `"T: 25.3 °C   H: 48.7 %RH"`)。
7. WHEN UI_Task 收到新的 Sample_Point,THE Value_Labels SHALL 使用 `lv_label_set_text_fmt()` 将数值更新为该 Sample_Point 的温度与湿度。
8. WHILE Sensor_Task 尚未产生第一个有效 Sample_Point,THE Value_Labels SHALL 显示占位符文本(例如 `"T: -- °C   H: -- %RH"`),且 Chart_Widget 不展示未定义数据点。
9. THE Chart_Screen SHALL 在每次新采样到达时完成一次视觉更新,完成时间不超过 `LV_DISP_DEF_REFR_PERIOD` 定义的刷新周期(默认 30 ms)。

### Requirement 11: 系统集成与启动顺序

**User Story:** 作为嵌入式开发者,我希望 `main()` 按确定顺序初始化各子系统,以便在任何一次复位后系统都能可靠进入稳定运行状态。

#### Acceptance Criteria

1. WHEN 固件从复位向量进入 `main()`,THE Firmware SHALL 依次执行 `HAL_Init()`、`SystemClock_Config()`、`BSP_LED_Init()`、`BSP_USART1_Init()`、`BSP_SDRAM_Init()`、`BSP_LCD_Init()`、任务创建、`vTaskStartScheduler()`。
2. THE Firmware SHALL 在 `LVGL_Task` 首次运行时(而非 `main()` 中)调用 `lv_init()` 与 `lv_port_disp_init()`,以保证 LVGL 内部分配发生在 FreeRTOS 内存管理可用之后。
3. THE Firmware SHALL 在 `main()` 中至少创建 `LVGL_Task` 与 `Sensor_Task` 两个任务,且仅当 `xTaskCreate()` 全部返回 `pdPASS` 时调用 `vTaskStartScheduler()`。
4. IF 任一 `xTaskCreate()` 返回非 `pdPASS`,THEN THE Firmware SHALL 调用 `Error_Handler()` 而不启动调度器。
5. WHILE 调度器运行,THE Firmware SHALL 在每次 Sensor_Task 成功采样时通过 USART1 打印一行日志,以便在 DAPLINK 下载后的串口终端中观察到持续的运行证据。

### Requirement 12: 阶段性 DAPLINK 下载与自验证

**User Story:** 作为嵌入式开发者,我希望在每个主要阶段完成后都通过 DAPLINK 下载并复位运行一次,以便在走向下一阶段之前先确认当前阶段正确。

#### Acceptance Criteria

1. WHEN 阶段一(USART + LED)代码完成,THE DAPLINK_Flasher SHALL 下载固件并复位 Target_Board,随后 Firmware SHALL 表现出 LED 周期翻转与 USART1 心跳打印。
2. WHEN 阶段二(FreeRTOS 移植)代码完成,THE DAPLINK_Flasher SHALL 重新下载并复位,随后 Firmware SHALL 由 FreeRTOS 任务驱动 LED 翻转,USART1 打印包含任务名的日志(例如 `"[LED_Task] toggle"`)。
3. WHEN 阶段三(SDRAM)代码完成,THE DAPLINK_Flasher SHALL 重新下载并复位,随后 Firmware SHALL 通过 USART1 打印 `BSP_SDRAM_Test()` 的返回值(期望为 `0`)。
4. WHEN 阶段四(LCD)代码完成,THE DAPLINK_Flasher SHALL 重新下载并复位,随后 LCD SHALL 依次显示红、绿、蓝全屏作为自检。
5. WHEN 阶段五(LVGL)代码完成,THE DAPLINK_Flasher SHALL 重新下载并复位,随后 LCD SHALL 显示 LVGL 渲染的占位界面(例如标题 `"ENV Monitor"` 与空的 Chart_Widget)。
6. WHEN 阶段六(DHT20 + UI)代码完成,THE DAPLINK_Flasher SHALL 重新下载并复位,随后 LCD SHALL 显示滚动的温湿度曲线、图例与实时数值,USART1 SHALL 每秒打印一次温湿度采样日志。
7. IF 阶段 N 的验证失败,THEN THE Firmware SHALL 被视为尚未进入阶段 N+1,开发流程 SHALL 返回阶段 N 修复后再重新烧录。

6.2 Design(架构设计)

Design 的核心思路是渐进可观测:系统被划分为 6 个阶段,每个阶段都定义串口日志、LED 行为或 LCD 显示等验证证据。工程师可以在每个阶段结束时烧录验证,而不是等全部代码完成后才进行首次集成。需要注意的是,后续阶段的验证不能反向证明早期参数必然正确,关键时序仍应依据 Datasheet 独立复核。

# Design Document

## Overview

本设计文档规定了在 STM32H743IIT6 开发板上实现「环境监测系统」的完整技术方案。系统采用「分阶段点亮 (incremental bring-up)」方法,自下而上依次构建六层能力:(1) 基础外设 (USART1 + LED) 与 DAPLINK 工作流;(2) FreeRTOS V10.x 内核;(3) 外部 SDRAM;(4) 800×480 RGB LCD;(5) LVGL v8.3.x 图形中间件(含 DMA2D 加速);(6) DHT20 温湿度传感器 + 示波器样式曲线 UI。

设计原则:

- **渐进可观测**:每一阶段结束必须能通过 DAPLINK 烧录后立即产生可观测证据(串口日志、LED 心跳、LCD 全屏色或 LVGL 控件、曲线滚动)。
- **一个职责一个模块**:每个外设在 `Drivers/BSP/` 下以 `bsp_xxx.c/.h` 成对组织,任务在 `User/Src/xxx_task.c` 独立成文件。
- **遵循 skills/ 指南**:USART、LED、FreeRTOS 移植、SDRAM、RGB LCD、LVGL 移植、DHT20 共 7 份 skill 文档是本设计的参考依据,本设计不重复其内部细节,只在必要处固化工程参数。
- **LVGL 渲染解耦传感器采集**:通过 FreeRTOS 队列让 `Sensor_Task` 和 `LVGL_Task` 在周期、速率、失败处理上彼此独立。

本文档覆盖六个阶段的整体架构、内存映射、目录结构、任务模型、启动顺序、DAPLINK 下载流程、错误处理、正确性属性与测试策略。

## Architecture

### 系统分层

系统分为五层:HAL 驱动层、BSP 板级驱动层、FreeRTOS 内核层、LVGL 图形中间件层、应用层。下图展示各层之间的依赖关系与数据流:

```mermaid
graph TB
    subgraph AppLayer["应用层 User/"]
        MAIN["main()<br/>启动顺序编排"]
        LVGL_TASK["LVGL_Task<br/>lv_timer_handler<br/>Chart UI 构建"]
        SENSOR_TASK["Sensor_Task<br/>1Hz DHT20 采样"]
        LED_TASK["LED_Task (可选)<br/>心跳翻转"]
    end

    subgraph LVGLLayer["LVGL v8.3.x + 移植层"]
        LVGL_CORE["LVGL Core<br/>lv_chart / lv_label"]
        LV_PORT_DISP["lv_port_disp<br/>双 Draw Buffer + flush_cb"]
        LV_DMA2D["DMA2D 加速<br/>disp_flush_cb"]
    end

    subgraph RTOSLayer["FreeRTOS V10.x 内核"]
        SCHED["Scheduler<br/>1kHz tick"]
        QUEUE["Queue<br/>Sample_Point"]
        HEAP["heap_4<br/>&ge;64 KB"]
    end

    subgraph BSPLayer["Drivers/BSP 板级驱动层"]
        BSP_USART["bsp_usart<br/>USART1 115200"]
        BSP_LED["bsp_led<br/>GPIO 输出"]
        BSP_SDRAM["bsp_sdram<br/>FMC 32MB @ 0xC0000000"]
        BSP_LCD["bsp_lcd<br/>LTDC 800x480 ARGB8888"]
        BSP_DHT20["bsp_dht20<br/>I2C 0x38 PH4/PH5"]
    end

    subgraph HALLayer["STM32 HAL + CMSIS"]
        HAL_CORE["HAL 核心<br/>SystemClock 480MHz"]
        HAL_FMC["HAL_SDRAM"]
        HAL_LTDC["HAL_LTDC / HAL_DMA2D"]
        HAL_I2C["HAL_I2C / Soft I2C"]
        HAL_UART["HAL_UART"]
        HAL_GPIO["HAL_GPIO"]
    end

    MAIN --> BSP_USART
    MAIN --> BSP_LED
    MAIN --> BSP_SDRAM
    MAIN --> BSP_LCD
    MAIN --> SCHED
    MAIN --> LVGL_TASK
    MAIN --> SENSOR_TASK
    MAIN --> LED_TASK

    LVGL_TASK --> LVGL_CORE
    LVGL_TASK --> QUEUE
    LVGL_CORE --> LV_PORT_DISP
    LV_PORT_DISP --> LV_DMA2D
    LV_DMA2D --> HAL_LTDC

    SENSOR_TASK --> BSP_DHT20
    SENSOR_TASK --> QUEUE
    SENSOR_TASK --> BSP_USART

    LED_TASK --> BSP_LED

    BSP_USART --> HAL_UART
    BSP_LED --> HAL_GPIO
    BSP_SDRAM --> HAL_FMC
    BSP_LCD --> HAL_LTDC
    BSP_DHT20 --> HAL_I2C

    SCHED --> HEAP
```

### 六阶段渐进路线

每阶段以上一阶段为前提,完成后立即通过 DAPLINK 烧录并做自检,见表:

| 阶段 | 核心交付 | 验证证据 | 依赖 skill |
|------|---------|---------|-----------|
| 1 | `bsp_usart.c` + `bsp_led.c` + `main()` 心跳 | LED 500ms 翻转 + USART1 每秒 `[heartbeat] tick=N` | usart-driver.md, led-driver.md |
| 2 | FreeRTOS 内核 + `LED_Task` | LED 由 `vTaskDelay` 驱动,日志含 `[LED_Task]` 标记 | freertos-porting.md |
| 3 | `bsp_sdram.c` + MPU 配置 | `BSP_SDRAM_Test()` 返回 0,串口打印 `SDRAM OK: 32MB @ 0xC0000000` | sdram-driver-guide.md |
| 4 | `bsp_lcd.c` + LTDC + 帧缓冲 | 屏幕依次显示纯红/绿/蓝全屏 | rgb-lcd-driver-guide.md |
| 5 | LVGL v8.3.x + DMA2D + `LVGL_Task` | 屏幕显示 `ENV Monitor` 标题 + 空 Chart 占位 | lvgl-porting-guide.md |
| 6 | `bsp_dht20.c` + `Sensor_Task` + Chart_Screen | 曲线滚动 + 图例 + 实时数值 + 每秒 `T=xx.xx H=yy.yy` | dht20-driver-guide.md |

## Components and Interfaces

### 目录与文件布局

Keil/MDK-ARM 工程采用如下目录结构(与现有 `Drivers/CMSIS/` 并列扩展):

```
Project_Root/
├── Drivers/
│   ├── CMSIS/                         # 已存在
│   ├── STM32H7xx_HAL_Driver/          # HAL 库(已存在)
│   └── BSP/
│       ├── Inc/
│       │   ├── bsp_usart.h
│       │   ├── bsp_led.h
│       │   ├── bsp_sdram.h
│       │   ├── bsp_lcd.h
│       │   └── bsp_dht20.h
│       └── Src/
│           ├── bsp_usart.c
│           ├── bsp_led.c
│           ├── bsp_sdram.c
│           ├── bsp_lcd.c
│           └── bsp_dht20.c
├── Middlewares/
│   ├── FreeRTOS/
│   │   └── Source/
│   │       ├── include/               # FreeRTOS.h, task.h, queue.h, ...
│   │       ├── tasks.c / queue.c / list.c / timers.c / event_groups.c / stream_buffer.c
│   │       └── portable/
│   │           ├── RVDS/ARM_CM7/r0p1/port.c
│   │           └── MemMang/heap_4.c
│   ├── LVGL/                          # v8.3.x 源码(src/、examples 可裁剪)
│   ├── LVGL_Port/
│   │   ├── lv_port_disp.c
│   │   └── lv_port_disp.h
│   └── lv_conf.h                      # 与 LVGL/ 同级,LVGL 根据宏 LV_CONF_INCLUDE_SIMPLE 查找
├── User/
│   ├── Inc/
│   │   ├── main.h
│   │   ├── FreeRTOSConfig.h
│   │   ├── lvgl_task.h
│   │   ├── sensor_task.h
│   │   ├── led_task.h                 # 可选
│   │   └── chart_ui.h
│   └── Src/
│       ├── main.c
│       ├── stm32h7xx_it.c
│       ├── stm32h7xx_hal_msp.c
│       ├── lvgl_task.c
│       ├── sensor_task.c
│       ├── led_task.c                 # 可选
│       └── chart_ui.c
└── MDK-ARM/
    └── Project.uvprojx                # Keil 工程文件
```

### BSP 驱动接口契约

所有 BSP 模块遵循统一命名风格:`BSP_<Module>_<Verb>()`。各模块的关键 API:

| 模块 | 初始化 | 操作 | skill 参考 |
|------|-------|------|-----------|
| `bsp_usart` | `BSP_USART1_Init()` | `printf` 重定向(`fputc`) | usart-driver.md |
| `bsp_led` | `BSP_LED_Init()` | `BSP_LED_On/Off/Toggle(LED_TypeDef)` | led-driver.md |
| `bsp_sdram` | `BSP_SDRAM_Init()` | `BSP_SDRAM_Test()` | sdram-driver-guide.md |
| `bsp_lcd` | `BSP_LCD_Init()` | `BSP_LCD_FillScreen(color)`, `BSP_LCD_Clear()`, `BSP_LCD_BacklightOn/Off()` | rgb-lcd-driver-guide.md |
| `bsp_dht20` | `BSP_DHT20_Init()` | `BSP_DHT20_Read(float *t, float *h)` 返回 `DHT20_OK` / `DHT20_ERR_BUSY` | dht20-driver-guide.md |

### LVGL 移植层接口

- `void lv_port_disp_init(void)`:在 `LVGL_Task` 启动后、`lv_init()` 完成后调用。初始化 DMA2D、注册双 draw buffer(地址见下一节内存映射)、注册 `lv_disp_drv`。
- `static void disp_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p)`:以 DMA2D M2M 模式将 draw buffer 的脏矩形拷贝到 LCD 帧缓冲,完成中断回调 `lv_disp_flush_ready()`。
- `void DMA2D_IRQHandler(void)`:转发到 `HAL_DMA2D_IRQHandler()`。

### 任务模型

FreeRTOS 任务表如下(优先级越大越高,`configMAX_PRIORITIES=7`):

| 任务 | 优先级 | 栈 (words) | 周期 / 触发 | 职责 |
|------|-------|-----------|-------------|-----|
| `LVGL_Task` | 3 | 4096 | 每轮 `vTaskDelay(5ms)` | 首次运行时 `lv_init()` + `lv_port_disp_init()` + 构建 Chart_Screen;主循环 `lv_timer_handler()` 并从队列读取 `Sample_Point` 更新图表与 Value_Labels |
| `Sensor_Task` | 2 | 512 | 每 1000ms | 调用 `BSP_DHT20_Read()`,成功则把 `Sample_Point` 发入队列并 `printf`,失败计数超阈值则打印错误 |
| `LED_Task` (可选) | 1 | 128 | 每 500ms | `BSP_LED_Toggle(LED_RED)` 作为心跳可视证据 |
| `IDLE` (FreeRTOS 自建) | 0 | `configMINIMAL_STACK_SIZE` | — | 空闲钩子(当前未启用) |

### 任务间通信:Sensor_Task → LVGL_Task

采用 FreeRTOS `QueueHandle_t` 传递 `Sample_Point`,满足「不丢最新值」策略:

- 类型:`xQueueCreate(length=4, item_size=sizeof(Sample_Point))`。
- 发送端(`Sensor_Task`):使用 `xQueueOverwrite()` 或在 `xQueueSend()` 满时先 `xQueueReceive()` 丢弃最旧再写入;首选方案是长度为 1 的 `xQueueOverwrite`,这样队列永远持有最新一次成功采样。为了兼顾调试时查看多帧数据,实际实现选 **长度 4 的 FIFO + 满时覆盖最旧** 方案:
  - `if (xQueueSend(sample_queue, &sp, 0) != pdTRUE) { Sample_Point drop; xQueueReceive(sample_queue, &drop, 0); xQueueSend(sample_queue, &sp, 0); }`
- 接收端(`LVGL_Task`):每次 `lv_timer_handler()` 循环前 `xQueueReceive(sample_queue, &sp, 0)` 以非阻塞方式排空队列,将所有新样本依次追加到 chart 并更新 Value_Labels(这样即使 LVGL 渲染慢,也能补齐滞留样本;实时数值标签始终反映最后一个 `sp`)。

该策略保证 **最新的采样点总是会被 UI 看到**(详见 Correctness Properties)。

### Chart UI 布局

在 800×480 屏幕上按功能区域切分:

```
 (0,0) ┌──────────────────────────────────────────────────────────┐ (800,0)
       │ Title Bar  "ENV Monitor"           (高度 40px)             │
 (0,40)├──────────────────────────────────────────────────────────┤ (800,40)
       │                                                            │
       │                    Chart_Widget (lv_chart)                 │
       │          800 × 320   (占屏幕高度 67%,满足 >=60%)          │
       │   左 Y 轴: Temperature (-10..60 °C)                         │
       │   右 Y 轴: Humidity    (0..100 %RH)                         │
       │                                                            │
 (0,360)├──────────────────────────────────────────────────────────┤ (800,360)
       │ Legend 区 (高度 40px)                                      │
       │   ■ Temperature (°C)           ■ Humidity (%RH)             │
 (0,400)├──────────────────────────────────────────────────────────┤ (800,400)
       │ Value_Labels 区 (高度 80px, 字体 >=24px)                    │
       │     T: 25.3 °C         H: 48.7 %RH                          │
       └──────────────────────────────────────────────────────────┘ (800,480)
```

`lv_chart` 配置要点:

- `lv_chart_set_type(chart, LV_CHART_TYPE_LINE)`
- `lv_chart_set_point_count(chart, 200)` — 环形缓冲 200 点(在需求允许的 100..400 范围内,1 Hz 采样下可覆盖 200 秒历史)。
- 两条 series:
  - `s_temp = lv_chart_add_series(chart, COLOR_TEMP, LV_CHART_AXIS_PRIMARY_Y)`(左 Y 轴)。
  - `s_humi = lv_chart_add_series(chart, COLOR_HUMI, LV_CHART_AXIS_SECONDARY_Y)`(右 Y 轴)。
- `lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, -100, 600)`(×10 以支持 0.1°C 精度的定点表示)。
- `lv_chart_set_range(chart, LV_CHART_AXIS_SECONDARY_Y, 0, 1000)`(×10 以支持 0.1%RH 精度)。
- `lv_chart_set_update_mode(chart, LV_CHART_UPDATE_MODE_SHIFT)` — 新点从右侧进入、旧点左移。
- 新样本到来时 `lv_chart_set_next_value(chart, s_temp, (int16_t)(t * 10))` / `lv_chart_set_next_value(chart, s_humi, (int16_t)(h * 10))`。
- Value_Labels 由两个 `lv_label` 组成,使用 `lv_label_set_text_fmt(lbl_t, "T: %.1f °C", t)`。
- 在第一个 `Sample_Point` 到达前:Value_Labels 显示占位符 `"T: -- °C   H: -- %RH"`,chart 两条 series 使用 `lv_chart_set_next_value` 塞入 `LV_CHART_POINT_NONE` 或完全不调用。

颜色常量(`ARGB8888`):

- `COLOR_TEMP = 0xFFFF5555`(红色系)
- `COLOR_HUMI = 0xFF5599FF`(蓝色系)
- 背景 `0xFF101020`,网格 `0xFF404050`。

## Data Models

### Sample_Point

一次 DHT20 采样的结果,在 `Sensor_Task` 和 `LVGL_Task` 之间传递:

```c
/* User/Inc/sensor_task.h */
typedef struct {
    float    temperature;   /* 单位 °C, 范围 [-40.0, +85.0] */
    float    humidity;      /* 单位 %RH, 范围 [0.0, 100.0] */
    uint32_t timestamp_ms;  /* xTaskGetTickCount() × 1000 / configTICK_RATE_HZ */
} Sample_Point;
```

不变式:

- `temperature ∈ [-40.0, 85.0]` 且 `humidity ∈ [0.0, 100.0]`(由 `BSP_DHT20_Read()` 成功路径保证)。
- `timestamp_ms` 单调不减(同一任务内 `xTaskGetTickCount()` 语义)。

### SDRAM 内存映射

SDRAM 32 MB,起始地址 `0xC0000000`。布局按「LCD 帧缓冲在最前,LVGL 双 draw buffer 紧随其后」原则,给后续扩展(文件系统缓存、图片资源)保留大量自由区:

| 区域 | 起始 | 结束 | 大小 | 用途 |
|------|------|------|------|------|
| LCD Framebuffer | `0xC000_0000` | `0xC017_6FFF` | 800×480×4 = 1,536,000 B ≈ 1.46 MB | LTDC Layer 0 扫描区 |
| 对齐填充 | `0xC017_7000` | `0xC017_FFFF` | 36,864 B | 4 KB 对齐余量 |
| LVGL Draw Buffer 1 | `0xC018_0000` | `0xC01A_57FF` | 800×48×4 = 153,600 B ≈ 150 KB | `lv_disp_draw_buf_t` 第一块 |
| LVGL Draw Buffer 2 | `0xC01A_5800` | `0xC01C_AFFF` | 153,600 B ≈ 150 KB | `lv_disp_draw_buf_t` 第二块 |
| Free / 预留 | `0xC01C_B000` | `0xC1FF_FFFF` | ≈ 30.2 MB | 保留(未来可用作图片资源、日志缓冲、fs) |

说明:

- Draw Buffer 48 行对 800×480 而言是「一个屏 10% 左右」的区域,能让 LVGL 以多次 flush 完成整屏渲染,同时 RAM 占用可控。
- 对齐到 `0xC018_0000` 是为了便于在 DMA2D 与 Cache 清理时采用 4 KB 粒度,且好看、易调试。
- 具体地址常量在 `User/Inc/main.h` 或 `Middlewares/LVGL_Port/lv_port_disp.h` 集中定义:

```c
#define LCD_FB_ADDR          ((uint32_t)0xC0000000)
#define DISP_BUF_ADDR1       ((uint32_t)0xC0180000)
#define DISP_BUF_ADDR2       ((uint32_t)0xC01A5800)
#define DISP_BUF_LINES       48
#define DISP_BUF_SIZE        (LCD_WIDTH * DISP_BUF_LINES)  /* in lv_color_t */
```

### 启动顺序(main())

```c
int main(void)
{
    /* 1. CMSIS / HAL 基础 */
    HAL_Init();
    SystemClock_Config();          /* 配置 SYSCLK = 480 MHz,外设总线,PLL3 = 33 MHz (LTDC) */

    /* 2. 最早的板级可观测:LED + 串口 */
    BSP_LED_Init();
    BSP_USART1_Init();
    printf("\r\n[boot] STM32H743 env-monitor starting...\r\n");

    /* 3. 外部大容量内存 */
    if (BSP_SDRAM_Init() != 0) { Error_Handler(); }
    if (BSP_SDRAM_Test() != 0) { printf("[boot] SDRAM TEST FAIL\r\n"); Error_Handler(); }
    printf("[boot] SDRAM OK: 32MB @ 0x%08X\r\n", SDRAM_BANK_ADDR);

    /* 4. 显示子系统(必须在 SDRAM 之后,因为帧缓冲位于 SDRAM) */
    if (BSP_LCD_Init() != 0) { Error_Handler(); }
    BSP_LCD_Clear();
    printf("[boot] LCD OK: 800x480 ARGB8888\r\n");

    /* 5. 传感器总线 —— 可以放在任务内做,但 I2C 的 GPIO 时钟/引脚在这里一次性初始化也可。
          按 skill 惯例放在 BSP_DHT20_Init() 内(在 Sensor_Task 里调用)。*/

    /* 6. 创建任务 —— 注意:lv_init() 放在 LVGL_Task 内完成,保证 FreeRTOS 堆管理已生效 */
    if (LVGL_Task_Create()   != pdPASS) Error_Handler();
    if (Sensor_Task_Create() != pdPASS) Error_Handler();
    /* LED_Task 可选 */
    LED_Task_Create();  /* 内部 xTaskCreate 失败走 Error_Handler */

    /* 7. 启动调度 */
    vTaskStartScheduler();
    while (1) { /* unreachable */ }
}
```

`LVGL_Task` 进入后首次执行的初始化顺序(参考 `skills/lvgl-porting-guide.md`):

```c
static void LVGL_Task_Entry(void *arg) {
    lv_init();
    lv_port_disp_init();        /* 注册 draw buffer + flush_cb + DMA2D 中断 */
    Chart_UI_Create();          /* 构建 Chart_Screen */
    for (;;) {
        /* 1) 排空传感器队列 */
        Sample_Point sp;
        while (xQueueReceive(sample_queue, &sp, 0) == pdTRUE) {
            Chart_UI_PushSample(&sp);   /* lv_chart_set_next_value + lv_label_set_text_fmt */
        }
        /* 2) LVGL 心跳 */
        lv_timer_handler();
        vTaskDelay(pdMS_TO_TICKS(5));
    }
}
```

### DAPLINK 烧录与自检工作流

以 `pyocd`(CMSIS-DAP)为例,每个阶段结束都执行如下序列。`{PROJECT_AXF}` 指 Keil 输出的 `.axf` 或 `.hex` 文件。

```
# 1. 在 Keil MDK-ARM 中编译
#    输出位置通常是 MDK-ARM/Objects/{target_name}.axf 或 .hex

# 2. 擦写 + 烧录
pyocd flash --target stm32h743xx MDK-ARM/Objects/env_monitor.axf

# 3. 复位使固件从 0x0800_0000 向量表重新启动
pyocd reset --target stm32h743xx

# 4. 打开串口(115200 8N1)查看日志
#    macOS:   screen /dev/tty.usbmodem* 115200
#    Linux:   minicom -D /dev/ttyACM0 -b 115200
#    Windows: PuTTY / Tera Term
```

每阶段的「可观测证据」如下:

| 阶段 | 串口 | LED | LCD |
|------|------|-----|-----|
| 1 | `[heartbeat] tick=N` 每秒一行 | 红色 LED 以 500ms 翻转 | 黑屏(未初始化) |
| 2 | `[LED_Task] toggle=N` 每 500ms(或每秒)一行 | 同上,但由 FreeRTOS 任务驱动 | 黑屏 |
| 3 | `[boot] SDRAM OK: 32MB @ 0xC0000000` | 同上 | 黑屏 |
| 4 | + `[boot] LCD OK: 800x480 ARGB8888` 以及阶段自检期间的 `FillScreen RED/GREEN/BLUE` 日志 | 同上 | 依次显示红/绿/蓝全屏各 1 秒 |
| 5 | + `[LVGL_Task] started` | 同上 | 显示 `ENV Monitor` 标题与空 Chart 占位 |
| 6 | + `T=25.32 H=48.71` 每秒一行 | 同上 | 温湿度曲线从右向左滚动,图例与数值实时更新 |

## Correctness Properties

*属性 (Property) 是对系统在所有合法执行上都应成立的特征或行为的形式化陈述。它把人类可读的验收标准与机器可验证的正确性保证连接起来:每个属性都是一条"对所有输入 X,性质 P(X) 成立"的全称命题。*

本节依据对 `requirements.md` 中 12 条需求的逐条分类(已通过 `prework` 工具完成),抽取出以下 7 条可通过基于属性的测试 (PBT) 验证的核心不变式。其余验收标准分类为 SMOKE / EXAMPLE / INTEGRATION / EDGE_CASE,将在 Testing Strategy 中以对应的例证性测试、集成测试或代码审查覆盖。

说明:
- 属性 1、2 涉及硬件但可通过「在目标板上对 SDRAM / 帧缓冲做多轮随机读写比对」的方式执行 PBT(即带硬件的属性测试),也可以在主机端以 mock 代替。
- 属性 3、4、6、7 为纯软件逻辑,完全可在主机 (host-side) 上以常规 PBT 框架 (例如 theft、Hypothesis、fast-check 的 C 版本或 Unity + Criterion + QuickCheck-for-C) 运行。
- 属性 5 涉及 FreeRTOS 任务 + 队列,推荐用桌面版 FreeRTOS Posix Port 或纯逻辑模型做 PBT。

### Property 1: SDRAM 读写一致性 (identity / round-trip)

*For all* 合法 SDRAM 偏移 `off ∈ [0, SDRAM_SIZE)` 与访问宽度 `w ∈ {8, 16, 32}` 位、以及任意对齐良好的数据值 `v ∈ [0, 2^w)`,在 `BSP_SDRAM_Init()` 成功且未发生断电/复位的前提下:先以宽度 `w` 将 `v` 写入 `(SDRAM_BANK_ADDR + off)`,再以相同宽度读取该地址,所读回的值必然等于 `v`。

**Validates: Requirements 5.3, 5.6**

### Property 2: LCD 帧缓冲全屏填充

*For any* 32 位 ARGB 颜色值 `c ∈ [0, 2^32)`,在 `BSP_LCD_Init()` 成功后调用 `BSP_LCD_FillScreen(c)`,对于任意有效像素坐标 `(x, y), 0 <= x < LCD_WIDTH, 0 <= y < LCD_HEIGHT`,内存地址 `LCD_FB_ADDR + (y * LCD_WIDTH + x) * 4` 所存储的 32 位值必然等于 `c`。

**Validates: Requirements 6.1, 6.7**

### Property 3: DMA2D flush_cb 地址计算公式

*For all* 合法 LVGL 脏矩形 `area = {x1, y1, x2, y2}` 满足 `0 <= x1 <= x2 < LCD_WIDTH` 且 `0 <= y1 <= y2 < LCD_HEIGHT`,`disp_flush_cb(area, color_p)` 计算得到的 DMA2D 目标地址 `dst_addr` 与输出行偏移 `output_offset` 必然满足:

- `dst_addr == LCD_FB_ADDR + (y1 * LCD_WIDTH + x1) * sizeof(uint32_t)`
- `output_offset == LCD_WIDTH - (x2 - x1 + 1)`

**Validates: Requirements 7.4**

### Property 4: DHT20 原始数据解析满足公式与量程

*For all* DHT20 成功响应的 6 字节数据包 `data[0..5]`,其中 `data[0]` 的 bit7 = 0(就绪)、bit3 = 1(已校准),令 `humi_raw = (data[1]<<12) | (data[2]<<4) | (data[3]>>4)` 且 `temp_raw = ((data[3] & 0x0F)<<16) | (data[4]<<8) | data[5]`,则解析函数 `BSP_DHT20_ParseRaw(data)` 输出的 `(temperature, humidity)` 必然满足:

- `humidity == (float)humi_raw / 1048576.0f * 100.0f` 且 `humidity ∈ [0.0, 100.0]`
- `temperature == (float)temp_raw / 1048576.0f * 200.0f - 50.0f` 且 `temperature ∈ [-50.0, 150.0]`

(其中 `humi_raw, temp_raw ∈ [0, 2^20)` 由位域抽取保证。)

**Validates: Requirements 8.5, 8.7**

### Property 5: Sensor_Task → Queue → LVGL_Task 保持 soundness 与 liveness

*For all* 有限长度的 `BSP_DHT20_Read` 调用序列(其中每次返回 `DHT20_OK` 或 `DHT20_ERR_BUSY`,成功时产出物理量程内的 `Sample_Point`),令 `S` 为所有成功返回值构成的子序列,令 `Q` 为 `LVGL_Task` 从 `sample_queue` 取出的 `Sample_Point` 序列,则必然同时满足:

- **soundness**:`Q` 中的每个元素都出现在 `S` 中(队列中永远不会出现来自失败路径的无效数据)。
- **liveness**:若 `S` 非空,则 `S` 的最后一个元素 `sN` 存在于 `Q` 中(最新的成功采样最终会被 UI 看到,无论中间丢弃了多少旧样本)。

**Validates: Requirements 9.3, 9.4, 9.5**

### Property 6: Chart 环形缓冲保留最后 N 个样本且顺序正确

*For all* 任意长度 `M` 的 `Sample_Point` 推送序列 `p[0], p[1], ..., p[M-1]` 与环形缓冲容量 `N = lv_chart_set_point_count` 所配置的点数(约定 `100 <= N <= 400`),在依次调用 `Chart_UI_PushSample(&p[i])` 后,`lv_chart` 内部温度序列从旧到新的值序列必然等于原序列最后 `min(M, N)` 个温度分量(湿度序列同理)。

**Validates: Requirements 10.3**

### Property 7: Value_Labels 反映最新 Sample_Point

*For all* `Sample_Point sp` 满足 `sp.temperature ∈ [-40.0, 85.0]` 且 `sp.humidity ∈ [0.0, 100.0]`,在 `Chart_UI_PushSample(&sp)` 返回后,温度标签 `lbl_t` 与湿度标签 `lbl_h` 的文本必然满足:

- `lv_label_get_text(lbl_t)` 等于 `snprintf("T: %.1f °C", sp.temperature)` 的结果。
- `lv_label_get_text(lbl_h)` 等于 `snprintf("H: %.1f %%RH", sp.humidity)` 的结果。

**Validates: Requirements 10.6, 10.7**

## Error Handling

错误处理贯穿每一层,总体策略是「尽早发现、显式返回、一律打印、不可恢复时进入 `Error_Handler()`」。

### 分层错误处理

| 层级    | 错误类型                   | 处理方式                                      | 可观测证据                         |
| -------------| ----------------------------------------------| ------------------------------------------------------------------------------------| ------------------------------------------------------------|
| HAL     | `HAL_StatusTypeDef != HAL_OK`        | BSP 封装层捕获,转换为自定义返回码                         | 上层 `printf` 或 `Error_Handler()`             |
| BSP_USART  | `HAL_UART_Init` 失败             | 调用 `Error_Handler()`(此时 printf 不可用,通过 LED 指示)            | 红 LED 常亮                        |
| BSP_SDRAM  | `HAL_SDRAM_Init` / `ProgramRefreshRate` 失败 | 返回 `1` / `2`                                   | `printf("[SDRAM] init fail code=%d")` 后 `Error_Handler()` |
| BSP_LCD   | `HAL_LTDC_Init` / `ConfigLayer` 失败     | 返回 `1` / `2`                                   | `printf("[LCD] init fail code=%d")` 后 `Error_Handler()`  |
| BSP_DHT20  | 忙标志连续 `DHT20_MAX_RETRY = 3` 次     | 返回 `DHT20_ERR_BUSY`                               | `Sensor_Task` 连续 5 次失败才打印错误           |
| FreeRTOS  | `xTaskCreate != pdPASS`           | `Error_Handler()` 不启动调度器                           | 红 LED 常亮 + 系统停止                   |
| FreeRTOS  | `vApplicationMallocFailedHook`        | `Error_Handler()`                                 | 同上                            |
| FreeRTOS  | `vApplicationStackOverflowHook`       | `Error_Handler()`                                 | 同上                            |
| LVGL    | DMA2D 传输冲突(`dma2d_cplt == 0`)     | `disp_flush_cb` 内自旋等待上一次完成                        | 渲染短暂降速但不崩溃                    |
| Sensor_Task | `BSP_DHT20_Read` 返回 `ERR_BUSY`       | 计失败次数,连续 5 次打印 `[Sensor_Task] DHT20 busy 5 times`,不向队列发送任何样本 | 串口日志 + UI 数值停更                   |
| Sensor_Task | 传感器首次未就绪               | Value_Labels 继续显示占位符 `"T: -- °C   H: -- %RH"`                | LCD 上"--"持续                       |

### Error_Handler 行为

```c
void Error_Handler(void) {
    __disable_irq();
    BSP_LED_On(LED_RED);
    /* 若 printf 已可用,打印寄存器与调用栈信息(调试构建)。*/
    for (;;) { }  /* 停机,等待看门狗或人工复位 */
}
```

### 错误恢复窗口

- **阶段 1–5 的 `Error_Handler()`** 会使板子停机且红 LED 常亮;此时需要手动按下复位按钮或 DAPLINK 发送 `pyocd reset` 重新启动,按阶段自检流程排查。
- **阶段 6 的 DHT20 失败** 不会导致 `Error_Handler()`,`Sensor_Task` 会持续重试;这是为了避免偶发 I2C 总线抖动让整个系统停机。

## Testing Strategy

本节定义四类测试的分工与执行方式。**由于本项目涉及硬件初始化、FreeRTOS 调度、LVGL 渲染**,大部分验收标准属于 INTEGRATION 或 SMOKE 类别;PBT 只用于可以被提取为纯函数或可 mock 的逻辑层。

### 为何 PBT 部分适用

**适用 PBT** 的层:
- DHT20 原始数据解析(纯字节到浮点的位运算 + 算术)
- LVGL `disp_flush_cb` 的地址/偏移计算(纯整数运算)
- Chart 环形缓冲语义(纯数据结构行为)
- 基于主机版 FreeRTOS 的 Sensor → Queue → LVGL 数据流模型(可 mock I2C)

**不适用 PBT** 的层:
- HAL 寄存器配置(ST 已测,我方只做配置正确性检查 → SMOKE)
- LCD 时序参数(配置值 → SMOKE,视觉验证 → 板上 INTEGRATION)
- FreeRTOS 内核本身(上游已测)
- LVGL 渲染的视觉正确性(无法程序化断言"不撕裂")
- DAPLINK 烧录与复位流程(操作性 INTEGRATION)

### 单元测试与例证测试 (EXAMPLE)

使用 Unity 或 CMock 在桌面构建中运行:

- `BSP_LED_Init` 后红 LED 默认熄灭(example)。
- `main()` 启动顺序代码审查点:`BSP_SDRAM_Init` 必须出现在 `BSP_LCD_Init` 之前(可用静态分析或编译期 `_Static_assert` 检查顺序标志的可选宏)。
- `BSP_DHT20_Init` 等待至少 100 ms 后读取状态字节(example,通过 mock HAL_Delay 记录调用参数)。
- Chart UI 初始化后 Value_Labels 文本为占位符 `"T: -- °C   H: -- %RH"`(example)。
- `Sensor_Task` 连续 5 次 `ERR_BUSY` 后触发错误日志(example,通过 mock DHT20_Read 返回 ERR_BUSY 注入)。

### 基于属性的测试 (PBT)

**库选择**:C 语言下推荐 `theft` (by Scott Vokes) 或 `Hypothesis-C`。如果项目已有 Unity,可搭配 `cmp` + 自定义生成器。所有 PBT 测试配置 **最少 100 轮迭代**。

**标签格式**:每个 PBT 测试必须在注释中注明:

```c
/* Feature: stm32h743-env-monitor, Property 4: DHT20 raw 数据解析满足公式与量程 */
TEST(test_dht20_parse_raw_satisfies_formula) {
    /* ... */
}
```

每个 Property 实现为「单一」PBT 测试函数:

| Property | 测试文件 | 生成器 | 断言 |
|----------|---------|-------|------|
| 1. SDRAM identity | `tests/hw/test_sdram_pbt.c`(在板上运行) | 随机 `offset + width + value` | 写后立即读回相等 |
| 2. LCD FillScreen | `tests/hw/test_lcd_fill_pbt.c`(在板上运行) | 随机 ARGB `c` + 随机 `(x,y)` | `FB[y*W+x] == c` |
| 3. flush_cb 地址计算 | `tests/host/test_disp_flush_pbt.c`(主机) | 随机 `lv_area_t` | `dst_addr` 与 `output_offset` 等于公式 |
| 4. DHT20 parse | `tests/host/test_dht20_parse_pbt.c`(主机) | 随机 6 字节 + 设置 `bit7=0, bit3=1` | 输出满足公式且落在量程 |
| 5. Queue soundness + liveness | `tests/host/test_sensor_queue_pbt.c`(主机 + FreeRTOS Posix 或纯模型) | 随机成功/失败序列 | 接收序列是发送成功子序列 + 最新值可见 |
| 6. Chart ring buffer | `tests/host/test_chart_ring_pbt.c`(主机,桩替 `lv_chart`) | 随机长度样本序列 | 缓冲等于尾部 N 个样本 |
| 7. Value_Labels 文本 | `tests/host/test_value_labels_pbt.c`(主机) | 随机合法 `Sample_Point` | `lv_label_get_text` 等于 sprintf 结果 |

### 集成测试 (INTEGRATION)

每个阶段结束执行一次人工或半自动的集成检查,流程见 Architecture 章的 DAPLINK 表格。建议准备:

- 一个串口日志抓取脚本 `scripts/capture.sh`,调用 `pyocd flash && pyocd reset` 后开启 `screen` 或 `pyserial` 监听,保存 30 秒输出供复盘。
- 一个 LED 翻转周期测量脚本(用逻辑分析仪 CSV 导出 + Python 脚本验证周期误差 < 5%)。
- LCD 全屏色自检:阶段 4 的代码在 `main()` 中插入 `FillScreen RED → 500ms → GREEN → 500ms → BLUE → 500ms` 序列,然后再进入正常流程;阶段 4 验证完成后删除此段测试代码。
- 阶段 6 完整验证:连续运行 >= 10 分钟,确认曲线滚动、数值与串口日志一致。

### 配置与冒烟测试 (SMOKE)

编译期或启动时检查一次性配置:

- 所有 `skills/*.md` 中列出的宏在对应头文件中均有定义(`grep` 脚本)。
- `FreeRTOSConfig.h` 的 `configCPU_CLOCK_HZ == 480000000`, `configTICK_RATE_HZ == 1000`, `configTOTAL_HEAP_SIZE >= 64*1024`, `configMAX_SYSCALL_INTERRUPT_PRIORITY == 5`。
- `lv_conf.h` 的 `LV_COLOR_DEPTH == 32`, `LV_USE_GPU_STM32_DMA2D == 1`, `LV_SPRINTF_USE_FLOAT == 1`, `LV_MEM_SIZE >= 48*1024`。
- BSP 驱动头文件中 API 符号存在性(链接通过)。
- SDRAM / LCD / LVGL 地址常量互不重叠(可在 `main.h` 中用 `_Static_assert` 自检):

```c
_Static_assert(DISP_BUF_ADDR1 >= LCD_FB_ADDR + LCD_FB_SIZE,
               "DISP_BUF_ADDR1 overlaps with LCD framebuffer");
_Static_assert(DISP_BUF_ADDR2 >= DISP_BUF_ADDR1 + DISP_BUF_SIZE * 4,
               "DISP_BUF_ADDR2 overlaps with DISP_BUF_ADDR1");
```

### 测试执行节奏

| 阶段 | 需跑的测试组 |
|------|-------------|
| 开发中(任意时刻) | 主机 PBT + 主机单元测试(CI 自动运行) |
| 每阶段烧录前 | 编译 + SMOKE 检查 |
| 每阶段烧录后 | 对应阶段的 INTEGRATION 检查(见 Architecture 表) |
| 合并前 | 全套 PBT (>= 100 iterations each) + 全套 SMOKE + 阶段 6 端到端 |

### 测试不覆盖的项

- 视觉正确性(不撕裂、颜色准确):需人工目视检查。
- DHT20 传感器硬件精度:依赖传感器规格,不在软件测试范围。
- 长时运行稳定性(>= 24h):作为独立的 soak test 单独安排。
- 电源抖动、ESD、温湿度极端环境:硬件测试,不在此范围。

---

## Appendix A: DAPLINK 烧录操作手册(运维参考)

### A.1 工具链

- **Keil MDK-ARM (ARM Compiler V5)**:构建 `env_monitor.axf`。
- **pyocd**(推荐):`pip install pyocd`,驱动 CMSIS-DAP,支持 `flash / reset / gdbserver`。
- **串口终端**:`screen`(macOS/Linux)、`minicom`、`PuTTY`、`Tera Term`。

### A.2 单次烧录-复位-日志流程

```bash
# 1) 构建(Keil IDE 或 armcc 命令行)
#    产出: MDK-ARM/Objects/env_monitor.axf

# 2) 烧录 + 验证
pyocd flash --target stm32h743xx MDK-ARM/Objects/env_monitor.axf

# 3) 复位
pyocd reset --target stm32h743xx

# 4) 监听串口
#    macOS:   screen /dev/tty.usbmodem<DAPLINK_SN> 115200
#    Linux:   screen /dev/ttyACM0 115200
#    Windows: PuTTY Session -> Serial, COMx, 115200 8N1
```

### A.3 阶段性自检一览(Kiro Agent 执行每个阶段结束后都按此检查)

| 阶段 | 预期串口首行 | 预期 LED 行为 | 预期 LCD 显示 |
|------|-------------|--------------|---------------|
| 1 | `[boot] STM32H743 env-monitor starting...` 然后每秒 `[heartbeat] tick=N` | 红 LED 500ms 翻转 | 黑屏 |
| 2 | 同上,但心跳由 `[LED_Task] toggle=N` 替换 | FreeRTOS 任务驱动的 500ms 翻转 | 黑屏 |
| 3 | + `[boot] SDRAM OK: 32MB @ 0xC0000000` | 继续心跳 | 黑屏 |
| 4 | + `[boot] LCD OK: 800x480 ARGB8888` + 三色自检日志 | 继续心跳 | 红/绿/蓝依次全屏 |
| 5 | + `[LVGL_Task] started` | 继续心跳 | `ENV Monitor` 标题 + 空 Chart_Widget |
| 6 | + `T=25.32 H=48.71` 每秒一行 | 继续心跳 | 温湿度曲线滚动 + 图例 + 实时数值 |

### A.4 烧录失败排查

| 现象         | 可能原因                 | 解决                             |
| ----------------------| -------------------------------------------| --------------------------------------------------------------|
| `No device found`  | DAPLINK 未连接 / 驱动未识别        | 插拔 USB;确认 `pyocd list` 能看到探针            |
| `Flash erase failed` | 写保护 / Option Bytes 问题        | `pyocd cmd -c "unlock"`                   |
| 复位后无串口     | 波特率错 / 未使能 USART1 时钟       | 核对 115200 8N1 与 `__HAL_RCC_USART1_CLK_ENABLE()`      |
| LCD 花屏       | SDRAM 未先初始化 / 时序不对        | 检查 `main()` 顺序、对照 `skills/sdram-driver-guide.md` 时序 |
| LVGL 崩溃      | `lv_init` 在 `vTaskStartScheduler` 前调用 | 将 `lv_init()` 迁入 `LVGL_Task` 首次运行处          |

实现前提:实际 LCD 时序与像素时钟必须以具体面板型号和 Datasheet 为准,PLL3 参数还取决于开发板 HSE 频率;LVGL v8 必须额外提供毫秒 Tick,例如在 FreeRTOS Tick Hook 中调用 lv_tick_inc(),或配置 LV_TICK_CUSTOM

Design 中最关键的三部分内容——六阶段渐进路线、SDRAM 内存映射与 FreeRTOS 任务模型——汇总如下。

6.2.1 六阶段渐进路线

阶段 核心交付 验证证据
Phase 1 USART1 + LED + main() 心跳 LED 500ms 翻转 + 串口 [heartbeat] tick=N
Phase 2 FreeRTOS 内核 + LED_Task 串口 [LED_Task] toggle=N
Phase 3 SDRAM + MPU 配置 串口 SDRAM OK: 32MB @ 0xC0000000
Phase 4 LTDC + 帧缓冲 屏幕红/绿/蓝全屏
Phase 5 LVGL + DMA2D + LVGL_Task 屏幕 “ENV Monitor” + 空 Chart
Phase 6 DHT20 + Sensor_Task + Chart 曲线滚动 + 串口 T=25.32 H=48.71

6.2.2 SDRAM 内存映射

区域 起始地址 大小 用途
LCD Framebuffer 0xC0000000 1.46 MB LTDC Layer 0 扫描区
LVGL Draw Buffer 1 0xC0180000 150 KB 双缓冲第一块
LVGL Draw Buffer 2 0xC01A5800 150 KB 双缓冲第二块
Free 0xC01CB000 约 30 MB 预留扩展

6.2.3 FreeRTOS 任务模型

任务 优先级 周期 职责
LVGL_Task 3 4096 words 5ms lv_timer_handler() 渲染 + 消费采样队列
Sensor_Task 2 512 words 1000ms DHT20 采样并写入队列
LED_Task 1 128 words 500ms LED 心跳翻转

6.3 Tasks(实现计划)

Design 随后被拆解为 40 余个 Task,各 Task 尽量明确文件路径、函数签名、依赖关系及其关联的 Requirement 编号。以下为原始任务清单,代码与命令内容按本轮要求保持不变;正式实施时应统一为 GCC 工具链,并修正其中已标记的时序、LVGL Tick、DMA2D 完成标志和时间戳处理问题:

# Implementation Plan: STM32H743 环境监测系统

## Overview

本文档依据 `design.md` 将系统分为七个阶段(含 Phase 0 预备阶段):每个阶段结束都必须通过 DAPLINK (pyocd) 烧录并复位 Target_Board,并以串口日志 / LED 行为 / LCD 画面作为可观测证据完成自检,然后才能进入下一阶段。Phase 7 为可选的主机端 PBT 测试编写。

所有代码任务都给出了具体的文件路径与函数签名,并在末尾以 `_Requirements: X.Y_` 方式引用 `requirements.md` 中的子验收标准。标记 `*` 的子任务为可选(主要是主机端 PBT 与硬件端 PBT),可在 MVP 阶段跳过。

## 工具链说明(macOS 实际执行)

原始规格假定 Keil/MDK-ARM + ARM Compiler V5 (RVDS)。本仓库在 macOS 上执行,已切换到等效的开源工具链:

- **编译器**:`arm-none-eabi-gcc`(Homebrew,≥ 10.x)。
- **构建系统**:顶层 `Makefile`(位于 `MDK-ARM/` 目录以复用规格中的路径约定;实际产出为 `.elf` / `.hex` / `.bin`,替代 Keil 的 `.axf`)。
- **烧录工具**:`pyocd 0.42+`(与规格原文一致)。
- **FreeRTOS 移植层**:使用 `portable/GCC/ARM_CM7/r0p1/port.c`(规格原文是 RVDS 版本,语义等价)。
- **LVGL 源码选型**:LVGL v8.3.x GCC 兼容分支。
- **printf 重定向**:用 `newlib-nano` 的 `_write` 系统调用替代 Keil 的 `fputc + MicroLIB`,功能等价。

每次 "Keil Rebuild" 在本仓库被替换为 `make -C MDK-ARM` 或仓库根目录的 `make`;产出文件由 `MDK-ARM/Objects/env_monitor.axf` 替换为 `build/env_monitor.elf` 与 `build/env_monitor.hex`。

## Tasks

### Phase 0: 工程骨架与目录初始化

- [x] 1. 建立 BSP / Middlewares / User 目录骨架
  - 在工程根目录下创建 `Drivers/BSP/Inc/` 与 `Drivers/BSP/Src/` 两个空目录(用于后续所有 `bsp_xxx.c/.h`)。
  - 创建 `Middlewares/FreeRTOS/`、`Middlewares/LVGL/`、`Middlewares/LVGL_Port/` 三个占位目录(暂不放源码,分别在 Phase 2、Phase 5 填充)。
  - 创建 `User/Inc/` 与 `User/Src/`,放置以下空文件:`User/Inc/main.h`、`User/Src/main.c`、`User/Src/stm32h7xx_it.c`、`User/Src/stm32h7xx_hal_msp.c`。
  - 约定 `Middlewares/lv_conf.h` 将在 Phase 5 创建(此时不动)。
  - _Requirements: 11.1_

- [x] 2. 建立 GCC + Makefile 构建目标
  - 在仓库根目录创建顶层 `Makefile`,目标 MCU `STM32H743xx`,使用 `arm-none-eabi-gcc -mcpu=cortex-m7 -mfpu=fpv5-d16 -mfloat-abi=hard -mthumb`。
  - 链接脚本放在 `MDK-ARM/STM32H743IITx_FLASH.ld`(Flash 原点 `0x08000000` 大小 2 MB、RAM 原点 `0x24000000` 大小 512 KB AXI SRAM)。
  - 启动文件使用 `Drivers/CMSIS/Device/ST/STM32H7xx/Source/Templates/gcc/startup_stm32h743xx.s`。
  - Include 路径集合:`Drivers/CMSIS/Include`、`Drivers/CMSIS/Device/ST/STM32H7xx/Include`、`Drivers/STM32H7xx_HAL_Driver/Inc`、`Drivers/BSP/Inc`、`User/Inc`、`Middlewares/FreeRTOS/Source/include`、`Middlewares/FreeRTOS/Source/portable/GCC/ARM_CM7/r0p1`、`Middlewares/LVGL`、`Middlewares/LVGL/src`、`Middlewares/LVGL_Port`、`Middlewares/`。
  - 源文件集合:`User/Src/*.c`、`Drivers/BSP/Src/*.c`、`Drivers/STM32H7xx_HAL_Driver/Src/*.c`(按需挑选,排除 `_template.c`)、`Middlewares/FreeRTOS/**.c`、`Middlewares/LVGL/src/**.c`、`Middlewares/LVGL_Port/*.c`。
  - 构建产出目录 `build/`,默认目标 `build/env_monitor.elf` 与 `build/env_monitor.hex`;提供 `make clean`、`make flash`(调用 `pyocd flash --target stm32h743xx build/env_monitor.elf`)、`make reset`(`pyocd reset --target stm32h743xx`)三个便捷目标。
  - printf 重定向使用 `newlib-nano`:`LDFLAGS += --specs=nano.specs -u _printf_float`,并在 `User/Src/syscalls.c` 里实现最小的 `_write` 把字节写到 USART1。
  - 本任务不产出可烧录固件,仅保证后续阶段每次 `make` 能成功链出空 `main()`。
  - _Requirements: 11.1_

- [x] 3. 配置工具链版本并验证空 main 可编译
  - 在 `User/Src/main.c` 中写入最小 `int main(void) { HAL_Init(); SystemClock_Config(); while (1) {} }` 骨架(`SystemClock_Config` 先写空实现,后续阶段替换为 480 MHz 配置)。
  - 在 `User/Src/stm32h7xx_it.c` 中实现 `NMI_Handler / HardFault_Handler / MemManage_Handler / BusFault_Handler / UsageFault_Handler / DebugMon_Handler` 空循环版本;`SVC_Handler / PendSV_Handler / SysTick_Handler` 暂保留空(后续 FreeRTOS 移植阶段修改)。
  - 运行 `make` 确认返回 `0 Error(s), 0 Warning(s)`(允许无关 warning),产出 `build/env_monitor.elf` 与 `build/env_monitor.hex`。
  - _Requirements: 11.1_

### Phase 1: USART1 串口 + LED 心跳 main()

- [x] 4. 实现 BSP_USART1 驱动
  - [x] 4.1 创建 `Drivers/BSP/Inc/bsp_usart.h`
    - 定义引脚宏 `USART1_TX_PIN=GPIO_PIN_9 / USART1_TX_GPIO_PORT=GPIOA / USART1_TX_AF=GPIO_AF7_USART1`,RX 同理使用 PA10。
    - 定义 `USART1_BAUDRATE = 115200`。
    - 声明 `void BSP_USART1_Init(void);`。
    - 参考 `skills/usart-driver.md` 的头文件模板。
    - _Requirements: 1.1, 1.2, 1.6_

  - [x] 4.2 创建 `Drivers/BSP/Src/bsp_usart.c` 实现 UART1 初始化
    - 定义 `UART_HandleTypeDef huart1`(全局)。
    - 实现 `BSP_USART1_Init()`:配置 `huart1.Init.BaudRate / WordLength=8B / StopBits=1 / Parity=NONE / Mode=TX_RX / HwFlowCtl=NONE / OverSampling=16 / ClockPrescaler=UART_PRESCALER_DIV1`;调用 `HAL_UART_Init`,失败走 `Error_Handler()`。
    - 实现 `HAL_UART_MspInit(UART_HandleTypeDef *huart)`:判断 `huart->Instance == USART1`,使能 `__HAL_RCC_USART1_CLK_ENABLE / __HAL_RCC_GPIOA_CLK_ENABLE`,按 `GPIO_MODE_AF_PP / PULLUP / SPEED_HIGH / Alternate=AF7_USART1` 配置 PA9、PA10。
    - _Requirements: 1.1, 1.2, 1.5, 1.6_

  - [x] 4.3 在 `bsp_usart.c` 实现 `fputc` 重定向
    - 实现 `int fputc(int ch, FILE *f)`:调用 `HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY)` 并返回 `ch`。
    - 确认 Keil 已启用 MicroLIB(Phase 0 Task 2 已完成)。
    - _Requirements: 1.3, 1.4_

- [x] 5. 实现 BSP_LED 驱动
  - [x] 5.1 创建 `Drivers/BSP/Inc/bsp_led.h`
    - 定义 `LED_RED_PIN=GPIO_PIN_1 / LED_GREEN_PIN=GPIO_PIN_0 / LED_GPIO_PORT=GPIOB`(按原理图确认;若电路不同在此集中修改)。
    - 定义枚举 `typedef enum { LED_RED = 0, LED_GREEN } LED_TypeDef;`。
    - 声明 `void BSP_LED_Init(void); void BSP_LED_On(LED_TypeDef); void BSP_LED_Off(LED_TypeDef); void BSP_LED_Toggle(LED_TypeDef);`。
    - _Requirements: 2.1, 2.5_

  - [x] 5.2 创建 `Drivers/BSP/Src/bsp_led.c` 实现 GPIO 输出
    - 定义 `static const uint16_t LED_PIN[2] = { LED_RED_PIN, LED_GREEN_PIN };`。
    - 实现 `BSP_LED_Init()`:使能 `__HAL_RCC_GPIOB_CLK_ENABLE()`,先调用 `HAL_GPIO_WritePin(..., GPIO_PIN_SET)` 使 LED 初始熄灭(共阳电路),随后按 `GPIO_MODE_OUTPUT_PP / NOPULL / SPEED_LOW` 配置两个引脚。
    - 实现 `BSP_LED_On/Off/Toggle`:使用 `HAL_GPIO_WritePin / HAL_GPIO_TogglePin`;`On = RESET`、`Off = SET`(共阳低电平有效)。
    - 参考 `skills/led-driver.md`。
    - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_

- [x] 6. 编写 Phase 1 main() 心跳循环
  - 在 `User/Src/main.c` 的 `main()` 中按顺序调用 `HAL_Init()` → `SystemClock_Config()`(实装 480 MHz 配置,启用 HSE、PLL1、并设置外设总线分频)→ `BSP_LED_Init()` → `BSP_USART1_Init()`。
  - 打印启动 banner `printf("\r\n[boot] STM32H743 env-monitor starting...\r\n");`。
  - 主循环:`uint32_t tick = 0; while (1) { BSP_LED_Toggle(LED_RED); HAL_Delay(500); if ((++tick) % 2 == 0) printf("[heartbeat] tick=%lu\r\n", tick / 2); }`,使 LED 500 ms 翻转、串口 1 Hz 打印。
  - _Requirements: 3.1, 3.4, 11.1_

- [x] 7. Phase 1 DAPLINK 烧录并复位验证(必做)
  - 在 Keil 中 `Rebuild`,产物位于 `MDK-ARM/Objects/env_monitor.axf`。
  - 执行 `pyocd flash --target stm32h743xx MDK-ARM/Objects/env_monitor.axf` 烧录。
  - 执行 `pyocd reset --target stm32h743xx` 复位板子。
  - 打开 115200 8N1 串口终端(macOS:`screen /dev/tty.usbmodem* 115200`;Linux:`screen /dev/ttyACM0 115200`;Windows:PuTTY),确认:
    - 首行出现 `[boot] STM32H743 env-monitor starting...`。
    - 之后每秒出现一行 `[heartbeat] tick=N`(N 递增)。
    - 红色 LED 以 500 ms 周期稳定翻转。
  - 若任一证据缺失或烧录命令返回非零退出码,回到上面任务修复后再次烧录,直到三项证据同时满足方可进入 Phase 2。
  - _Requirements: 3.2, 3.3, 3.4, 3.5, 12.1_

### Phase 2: FreeRTOS 内核移植 + LED_Task

- [x] 8. 导入 FreeRTOS V10.x 源码
  - 将 FreeRTOS V10.x 源码解压放入 `Middlewares/FreeRTOS/Source/`,保留 `include/`、`tasks.c`、`queue.c`、`list.c`、`timers.c`、`event_groups.c`、`stream_buffer.c`,以及 `portable/RVDS/ARM_CM7/r0p1/port.c` 和 `portable/MemMang/heap_4.c`;其余移植目录可删除以减少编译时间。
  - 在 Keil 工程的 `Middlewares/FreeRTOS` 分组中添加上述 `.c` 源文件。
  - 确认 Phase 0 Task 2 已把 `Middlewares/FreeRTOS/Source/include` 与 `Middlewares/FreeRTOS/Source/portable/RVDS/ARM_CM7/r0p1` 加入头文件搜索路径。
  - _Requirements: 4.1, 4.8_

- [x] 9. 创建 FreeRTOSConfig.h
  - 创建 `User/Inc/FreeRTOSConfig.h`,参照 `skills/freertos-porting.md` 模板,关键配置:
    - `configCPU_CLOCK_HZ = 480000000`
    - `configTICK_RATE_HZ = 1000`
    - `configMAX_PRIORITIES = 7`
    - `configTOTAL_HEAP_SIZE = (64 * 1024)`(≥ 64 KB)
    - `configUSE_PREEMPTION = 1 / configUSE_MUTEXES = 1 / configUSE_TIMERS = 1 / configSUPPORT_DYNAMIC_ALLOCATION = 1`
    - `configUSE_MALLOC_FAILED_HOOK = 1 / configCHECK_FOR_STACK_OVERFLOW = 2`
    - `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5`
    - 在文件末尾加入 `#define vPortSVCHandler SVC_Handler` 与 `#define xPortPendSVHandler PendSV_Handler`(`SysTick_Handler` 不在此映射,见下一任务)。
  - _Requirements: 4.2, 4.4_

- [x] 10. 修改 stm32h7xx_it.c 接管中断
  - 在 `User/Src/stm32h7xx_it.c` 顶部 `#include "FreeRTOS.h"` 与 `#include "task.h"`,并 `extern void xPortSysTickHandler(void);`。
  - 删除或注释原 `SVC_Handler` 与 `PendSV_Handler` 空实现,避免与 FreeRTOS port 层冲突。
  - 修改 `SysTick_Handler()` 为:先调用 `HAL_IncTick()`,然后 `if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) xPortSysTickHandler();`。
  - _Requirements: 4.3, 4.4_

- [x] 11. 实现 FreeRTOS 钩子函数
  - 在 `User/Src/stm32h7xx_it.c` 或新建 `User/Src/freertos_hooks.c` 中实现:
    - `void vApplicationMallocFailedHook(void) { Error_Handler(); }`
    - `void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void)xTask; (void)pcTaskName; Error_Handler(); }`
  - 确认 `Error_Handler()` 在 `main.c` 中已实现:`__disable_irq(); BSP_LED_On(LED_RED); for(;;){}`。
  - _Requirements: 4.5_

- [x] 12. 实现 LED_Task
  - [x] 12.1 创建 `User/Inc/led_task.h` 与 `User/Src/led_task.c`
    - 头文件仅导出 `BaseType_t LED_Task_Create(void);`。
    - 实现文件中定义 `#define LED_TASK_STACK 128 / #define LED_TASK_PRIO 1`。
    - 任务函数 `static void LED_Task_Entry(void *arg)`:循环 `BSP_LED_Toggle(LED_RED); printf("[LED_Task] toggle=%lu\r\n", ++tick); vTaskDelay(pdMS_TO_TICKS(500));`。
    - `LED_Task_Create()` 调用 `xTaskCreate(LED_Task_Entry, "LED_Task", LED_TASK_STACK, NULL, LED_TASK_PRIO, NULL)` 并返回其返回值。
    - _Requirements: 4.7, 12.2_

- [x] 13. 改造 main() 启动调度器
  - 删除 Phase 1 的 `while(1)` 心跳循环(LED 心跳改由 `LED_Task` 负责)。
  - 在 `main()` 中 `HAL_Init / SystemClock_Config / BSP_LED_Init / BSP_USART1_Init` 之后调用 `if (LED_Task_Create() != pdPASS) Error_Handler();`,然后 `vTaskStartScheduler();`,其后放置不可达的 `while(1){}`。
  - _Requirements: 4.6, 11.3, 11.4_

- [x] 14. Phase 2 DAPLINK 烧录并复位验证(必做)
  - Keil Rebuild → `pyocd flash` → `pyocd reset`。
  - 串口终端预期:保留 `[boot] STM32H743 env-monitor starting...`;不再出现 `[heartbeat] tick=N`,取而代之的是每 500 ms 一行 `[LED_Task] toggle=N`。
  - LED:红色 LED 继续按 500 ms 翻转,但由 `vTaskDelay` 而非 `HAL_Delay` 驱动,用万用表或逻辑分析仪观察周期误差应 < 5%。
  - 若未看到 `[LED_Task]` 日志或板子进入 HardFault(红 LED 常亮但无日志),回到 Task 9–13 检查 `FreeRTOSConfig.h`、中断映射与钩子函数实现。
  - _Requirements: 4.6, 4.7, 12.2_

### Phase 3: 外部 SDRAM + MPU 驱动

- [x] 15. 实现 BSP_SDRAM 驱动
  - [x] 15.1 创建 `Drivers/BSP/Inc/bsp_sdram.h`
    - 定义 `SDRAM_BANK_ADDR = 0xC0000000 / SDRAM_SIZE = 0x02000000`(32 MB)。
    - 按 `skills/sdram-driver-guide.md` 定义列/行/数据宽度/内部 Bank/CAS 延迟/时序常量(`SDRAM_TMRD / TXSR / TRAS / TRC / TWR / TRP / TRCD`)、模式寄存器位(`SDRAM_MODEREG_BURST_LENGTH_1` 等)、刷新参数 `SDRAM_REFRESH_COUNT / SDRAM_AUTO_REFRESH_NUM = 8 / SDRAM_TIMEOUT = 0xFFFF`。
    - 声明 `uint8_t BSP_SDRAM_Init(void); uint8_t BSP_SDRAM_Test(void);`。
    - _Requirements: 5.1_

  - [x] 15.2 创建 `Drivers/BSP/Src/bsp_sdram.c` 主体
    - 定义全局 `SDRAM_HandleTypeDef hsdram;`。
    - 实现 `static void SDRAM_MPU_Config(void)`:配置 MPU 区域 0 覆盖 `SDRAM_BANK_ADDR` 起 32MB,`IsCacheable = MPU_ACCESS_CACHEABLE`、`IsBufferable = MPU_ACCESS_NOT_BUFFERABLE`、`TypeExtField = MPU_TEX_LEVEL0`(Write-Through)。
    - 实现 `void HAL_SDRAM_MspInit(SDRAM_HandleTypeDef *hsdram)`:使能 GPIOC/D/E/F/G/H 时钟与 `__HAL_RCC_FMC_CLK_ENABLE()`,按原理图把全部 FMC 数据线/地址线/控制信号配置为 `GPIO_MODE_AF_PP / NOPULL / SPEED_VERY_HIGH / Alternate = GPIO_AF12_FMC`(引脚清单见 `skills/sdram-driver-guide.md`)。
    - _Requirements: 5.2, 5.4_

  - [x] 15.3 实现 SDRAM 初始化命令序列
    - 在 `bsp_sdram.c` 中实现 `static void SDRAM_InitSequence(void)`:依次调用 `HAL_SDRAM_SendCommand`:Clock Enable → `HAL_Delay(1)` → Precharge All → Auto Refresh ×8 → Load Mode Register(模式寄存器组合 `BURST_LENGTH_1 | BURST_TYPE_SEQUENTIAL | CAS_LATENCY_3 | OPERATING_MODE_STANDARD | WRITEBURST_MODE_SINGLE`)。
    - 实现 `BSP_SDRAM_Init()`:调用 `SDRAM_MPU_Config()` → 填充 `hsdram.Init` 与 `FMC_SDRAM_TimingTypeDef` → `HAL_SDRAM_Init` → `SDRAM_InitSequence` → `HAL_SDRAM_ProgramRefreshRate(&hsdram, SDRAM_REFRESH_COUNT)`。失败返回 `1`(FMC)或 `2`(刷新率)。
    - _Requirements: 5.2, 5.3, 5.7_

  - [x] 15.4 实现 BSP_SDRAM_Test 读写自检
    - 分别以 32 位、16 位、8 位三种宽度对 `SDRAM_BANK_ADDR` 起至少 1 KB 区域进行写-回读比对(模式参照 `skills/sdram-driver-guide.md`)。
    - 任何一轮不匹配返回对应错误码(`1 / 2 / 3`);全通过返回 `0`。
    - _Requirements: 5.5, 5.6_

- [x] 16. 在 main() 中集成 SDRAM 初始化
  - 在 `main()` 的 `BSP_USART1_Init()` 之后、任务创建之前按顺序调用:
    ```c
    if (BSP_SDRAM_Init() != 0) { printf("[boot] SDRAM INIT FAIL\r\n"); Error_Handler(); }
    if (BSP_SDRAM_Test() != 0) { printf("[boot] SDRAM TEST FAIL\r\n"); Error_Handler(); }
    printf("[boot] SDRAM OK: 32MB @ 0x%08X\r\n", (unsigned int)SDRAM_BANK_ADDR);
    ```
  - 确认该调用发生在 `LED_Task_Create()` 与 `vTaskStartScheduler()` 之前。
  - _Requirements: 5.6, 11.1_

- [ ]* 17. 编写硬件端 SDRAM 属性测试(可选)
  - **Property 1:SDRAM 读写一致性**
  - **Validates: Requirements 5.3, 5.6**
  - 创建 `tests/hw/test_sdram_pbt.c`,以 FreeRTOS 任务或裸机 loop 执行 ≥ 100 次随机 `(offset, width ∈ {8,16,32}, value)`:写入 `SDRAM_BANK_ADDR + offset` 后立即读回并断言相等,失败通过 `printf` 打印反例并 `vTaskSuspendAll`。
  - 此测试需在板上运行,仅在开发期启用,提交前通过编译开关屏蔽。

- [x] 18. Phase 3 DAPLINK 烧录并复位验证(必做)
  - Keil Rebuild → `pyocd flash` → `pyocd reset`。
  - 串口预期新增一行 `[boot] SDRAM OK: 32MB @ 0xC0000000`;其余 `[LED_Task] toggle=N` 日志继续输出。
  - 若出现 `SDRAM INIT FAIL` 或 `SDRAM TEST FAIL`,对照 `skills/sdram-driver-guide.md` 排查 GPIO / 时序 / 刷新计数。
  - 验证通过后方可进入 Phase 4。
  - _Requirements: 5.6, 12.3_

### Phase 4: RGB LCD + LTDC 驱动

- [x] 19. 实现 BSP_LCD 驱动
  - [x] 19.1 创建 `Drivers/BSP/Inc/bsp_lcd.h`
    - 定义 `LCD_WIDTH = 800 / LCD_HEIGHT = 480 / LCD_PIXEL_FORMAT = LTDC_PIXEL_FORMAT_ARGB8888 / LCD_PIXEL_SIZE = 4 / LCD_FB_SIZE = LCD_WIDTH*LCD_HEIGHT*4 / LCD_FB_ADDR = 0xC0000000`。
    - 定义时序常量 `LCD_HSW=48 / LCD_HBP=88 / LCD_HFP=40 / LCD_VSW=3 / LCD_VBP=32 / LCD_VFP=13`。
    - 定义颜色常量 `LCD_COLOR_RED = 0xFFFF0000 / LCD_COLOR_GREEN = 0xFF00FF00 / LCD_COLOR_BLUE = 0xFF0000FF / LCD_COLOR_BLACK = 0xFF000000 / LCD_COLOR_WHITE = 0xFFFFFFFF`。
    - 定义背光引脚 `LCD_BL_PIN / LCD_BL_GPIO_PORT` 与复位引脚 `LCD_RST_PIN / LCD_RST_GPIO_PORT`(按原理图确认)。
    - 声明 `uint8_t BSP_LCD_Init(void); void BSP_LCD_BacklightOn(void); void BSP_LCD_BacklightOff(void); void BSP_LCD_FillScreen(uint32_t color); void BSP_LCD_Clear(void);`。
    - _Requirements: 6.1, 6.2, 6.4_

  - [x] 19.2 实现像素时钟配置
    - 在 `bsp_lcd.c` 中实现 `static void LCD_ClockConfig(void)`,通过 `HAL_RCCEx_PeriphCLKConfig` 把 PLL3 配置为 `PLL3M=25 / PLL3N=132 / PLL3R=4`,使 LTDC 像素时钟 ≈ 33 MHz。失败走 `Error_Handler()`。
    - _Requirements: 6.3_

  - [x] 19.3 实现硬件复位与 GPIO MspInit
    - 实现 `static void LCD_Reset(void)`:`HAL_GPIO_WritePin(...RST..., RESET); HAL_Delay(10); HAL_GPIO_WritePin(...RST..., SET); HAL_Delay(120);`。
    - 实现 `void HAL_LTDC_MspInit(LTDC_HandleTypeDef *hltdc)`:使能 GPIOA/D/E/F/G/H/I 时钟与 `__HAL_RCC_LTDC_CLK_ENABLE()`,按 `skills/rgb-lcd-driver-guide.md` 中的引脚清单以 `GPIO_AF14_LTDC`(个别引脚 `GPIO_AF13_LTDC`)配置所有 RGB 数据/HSYNC/VSYNC/DE/PCLK 引脚;背光与复位引脚配置为 `GPIO_MODE_OUTPUT_PP / SPEED_LOW`。
    - _Requirements: 6.5_

  - [x] 19.4 实现 LTDC 初始化与 Layer 0 配置
    - 实现 `BSP_LCD_Init()`:`LCD_Reset() → LCD_ClockConfig() → 填充 hltdc.Init(极性 HSPolarity/VSPolarity/DEPolarity=AL、PCPolarity=IPC、按 skills 文档计算的累加时序)→ HAL_LTDC_Init`(失败返回 1)。
    - 配置 Layer 0:`WindowX0/X1 = 0/LCD_WIDTH`、`WindowY0/Y1 = 0/LCD_HEIGHT`、`PixelFormat = ARGB8888`、`Alpha=0xFF`、`FBStartAdress = LCD_FB_ADDR`、`ImageWidth = LCD_WIDTH`、`ImageHeight = LCD_HEIGHT`。`HAL_LTDC_ConfigLayer` 失败返回 2。
    - 最后调用 `BSP_LCD_Clear()` 与 `BSP_LCD_BacklightOn()`,返回 0。
    - _Requirements: 6.1, 6.2, 6.4, 6.5_

  - [x] 19.5 实现绘图与背光控制
    - `BSP_LCD_FillScreen(uint32_t color)`:以 `uint32_t *fb = (uint32_t *)LCD_FB_ADDR;` 循环写入 `LCD_WIDTH*LCD_HEIGHT` 个像素。
    - `BSP_LCD_Clear() = BSP_LCD_FillScreen(LCD_COLOR_BLACK)`。
    - `BSP_LCD_BacklightOn/Off`:用 `HAL_GPIO_WritePin` 翻转 `LCD_BL_PIN`。
    - _Requirements: 6.4, 6.7_

- [x] 20. 在 main() 集成 LCD 初始化与三原色自检
  - 在 `main()` 中 `BSP_SDRAM_Init/Test` 之后调用:
    ```c
    if (BSP_LCD_Init() != 0) { printf("[boot] LCD INIT FAIL\r\n"); Error_Handler(); }
    printf("[boot] LCD OK: 800x480 ARGB8888\r\n");
    /* 三原色自检:分别显示红/绿/蓝各 500 ms */
    BSP_LCD_FillScreen(LCD_COLOR_RED);   printf("[boot] FillScreen RED\r\n");   HAL_Delay(500);
    BSP_LCD_FillScreen(LCD_COLOR_GREEN); printf("[boot] FillScreen GREEN\r\n"); HAL_Delay(500);
    BSP_LCD_FillScreen(LCD_COLOR_BLUE);  printf("[boot] FillScreen BLUE\r\n");  HAL_Delay(500);
    BSP_LCD_Clear();
    ```
  - 该调用必须在 `BSP_SDRAM_Init()` 之后(帧缓冲位于 SDRAM)。三原色自检代码在 Phase 5 开始前可保留以继续自检,待 Phase 6 完成并稳定后可删除。
  - _Requirements: 6.6, 6.7, 11.1_

- [ ]* 21. 编写硬件端 LCD 帧缓冲属性测试(可选)
  - **Property 2:LCD FillScreen 帧缓冲正确性**
  - **Validates: Requirements 6.1, 6.7**
  - 创建 `tests/hw/test_lcd_fill_pbt.c`,调用 `BSP_LCD_FillScreen(c)` 后随机抽取 ≥ 100 组 `(x, y)`,断言 `((uint32_t*)LCD_FB_ADDR)[y*LCD_WIDTH + x] == c`。失败通过串口打印反例 `(c, x, y)`。
  - 与 Task 17 共同放在 `tests/hw/` 下,由编译开关控制。

- [x] 22. Phase 4 DAPLINK 烧录并复位验证(必做)
  - Keil Rebuild → `pyocd flash` → `pyocd reset`。
  - 屏幕预期:红屏约 500 ms → 绿屏约 500 ms → 蓝屏约 500 ms → 黑屏。
  - 串口预期:新增 `[boot] LCD OK: 800x480 ARGB8888` 以及三条 `FillScreen RED/GREEN/BLUE` 日志。
  - LED 与 `[LED_Task] toggle=N` 日志继续输出(在 LCD 自检期间 LED 仍应翻转,说明 HAL_Delay 与 LED 任务互不阻塞)。
  - 若屏幕花屏/无显示,对照 `skills/rgb-lcd-driver-guide.md` 检查 LTDC 时序、PLL3 配置与 GPIO AF。
  - _Requirements: 6.7, 12.4_

### Phase 5: LVGL v8.3 移植 + LVGL_Task 占位界面

- [x] 23. 导入 LVGL v8.3.x 源码
  - 将 LVGL v8.3.x 源码的 `src/` 放入 `Middlewares/LVGL/src/`,保留 `core/ / draw/ / extra/ / font/ / hal/ / misc/ / widgets/` 子目录;`examples/` 与 `demos/` 可删除以减小体积。
  - 在 Keil 工程 `Middlewares/LVGL` 分组中加入 `Middlewares/LVGL/src/` 下所有 `.c` 文件(或使用 Keil 的递归添加功能)。
  - 确认 Phase 0 Task 2 已把 `Middlewares/LVGL` 与 `Middlewares/` 加入头文件搜索路径。
  - _Requirements: 7.1_

- [x] 24. 创建 lv_conf.h
  - 在 `Middlewares/lv_conf.h` 写入 LVGL 配置,关键项按 `skills/lvgl-porting-guide.md`:
    - `LV_COLOR_DEPTH 32 / LV_COLOR_SCREEN_TRANSP 0`
    - `LV_MEM_CUSTOM 0 / LV_MEM_SIZE (48U*1024U) / LV_MEMCPY_MEMSET_STD 1`
    - `LV_DISP_DEF_REFR_PERIOD 30 / LV_INDEV_DEF_READ_PERIOD 30 / LV_DPI_DEF 130`
    - `LV_USE_GPU_STM32_DMA2D 1 / LV_GPU_DMA2D_CMSIS_INCLUDE "stm32h7xx.h"`
    - `LV_FONT_MONTSERRAT_14 1 / LV_FONT_MONTSERRAT_24 1 / LV_FONT_DEFAULT &lv_font_montserrat_14 / LV_SPRINTF_USE_FLOAT 1`
    - `LV_USE_LABEL 1 / LV_USE_CHART 1`
  - 在 Keil C/C++ 预处理器定义中追加 `LV_CONF_INCLUDE_SIMPLE`,使 LVGL 头文件能通过 `#include "lv_conf.h"` 找到 `Middlewares/lv_conf.h`。
  - _Requirements: 7.2_

- [x] 25. 实现 LVGL 显示移植层 lv_port_disp
  - [x] 25.1 创建 `Middlewares/LVGL_Port/lv_port_disp.h`
    - 定义 `DISP_BUF_ADDR1 = 0xC0180000 / DISP_BUF_ADDR2 = 0xC01A5800 / DISP_BUF_LINES = 48 / DISP_BUF_SIZE = (LCD_WIDTH * DISP_BUF_LINES)`(与 `design.md` Data Models 章的 SDRAM 内存映射一致)。
    - 声明 `void lv_port_disp_init(void);`。
    - _Requirements: 7.3_

  - [x] 25.2 实现 `Middlewares/LVGL_Port/lv_port_disp.c`
    - 定义 `static DMA2D_HandleTypeDef hdma2d; static volatile uint8_t dma2d_cplt = 1; static lv_disp_draw_buf_t draw_buf; static lv_disp_drv_t disp_drv;`。
    - 实现 `void lv_port_disp_init(void)`:使能 `__HAL_RCC_DMA2D_CLK_ENABLE`、设置 DMA2D 中断 `HAL_NVIC_SetPriority(DMA2D_IRQn, 6, 0); HAL_NVIC_EnableIRQ(DMA2D_IRQn);`、`hdma2d.XferCpltCallback = dma2d_xfer_cplt_cb;`;随后 `lv_disp_draw_buf_init(&draw_buf, (lv_color_t *)DISP_BUF_ADDR1, (lv_color_t *)DISP_BUF_ADDR2, DISP_BUF_SIZE);`;`lv_disp_drv_init(&disp_drv); disp_drv.hor_res=LCD_WIDTH; disp_drv.ver_res=LCD_HEIGHT; disp_drv.flush_cb=disp_flush_cb; disp_drv.draw_buf=&draw_buf; lv_disp_drv_register(&disp_drv);`。
    - 实现 `static void disp_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p)`:计算 `dst_addr = LCD_FB_ADDR + (area->y1 * LCD_WIDTH + area->x1) * 4`;`w = area->x2 - area->x1 + 1; h = area->y2 - area->y1 + 1;`;等待 `dma2d_cplt == 1`;填充 `hdma2d.Init.Mode=DMA2D_M2M / ColorMode=DMA2D_OUTPUT_ARGB8888 / OutputOffset = LCD_WIDTH - w`;`LayerCfg[1].InputColorMode=DMA2D_INPUT_ARGB8888 / InputOffset=0 / AlphaMode=DMA2D_NO_MODIF_ALPHA / InputAlpha=0xFF`;`HAL_DMA2D_Init / HAL_DMA2D_ConfigLayer(&hdma2d, 1) / HAL_DMA2D_Start_IT(&hdma2d, (uint32_t)color_p, dst_addr, w, h);`;设置 `dma2d_cplt = 0`。
    - 实现 `static void dma2d_xfer_cplt_cb(DMA2D_HandleTypeDef *hdma)`:`dma2d_cplt = 1; lv_disp_flush_ready(&disp_drv);`。
    - 实现 `void DMA2D_IRQHandler(void) { HAL_DMA2D_IRQHandler(&hdma2d); }`。
    - _Requirements: 7.3, 7.4_

  - [x] 25.3 在 main.h 集中定义 SDRAM 内存映射并加 _Static_assert
    - 在 `User/Inc/main.h` 中 `#include "bsp_lcd.h"` 与 `#include "lv_port_disp.h"` 之后加入:
      ```c
      _Static_assert(DISP_BUF_ADDR1 >= LCD_FB_ADDR + LCD_FB_SIZE, "DISP_BUF_ADDR1 overlaps with LCD framebuffer");
      _Static_assert(DISP_BUF_ADDR2 >= DISP_BUF_ADDR1 + DISP_BUF_SIZE * 4, "DISP_BUF_ADDR2 overlaps with DISP_BUF_ADDR1");
      ```
    - 目的:编译期保障 `design.md` 表格中的 LCD_FB / Draw Buffer 1 / Draw Buffer 2 互不重叠。
    - _Requirements: 7.3_

- [x] 26. 实现 LVGL_Task 与 Chart_Screen 占位界面
  - [x] 26.1 创建 `User/Inc/lvgl_task.h`
    - 仅导出 `BaseType_t LVGL_Task_Create(void);`。
    - _Requirements: 7.5, 11.2_

  - [x] 26.2 创建 `User/Src/lvgl_task.c`
    - 定义 `#define LVGL_TASK_STACK 4096 / #define LVGL_TASK_PRIO 3`。
    - 任务函数内部首次执行:`lv_init();` → `lv_port_disp_init();` → 构建占位界面(先创建一个标题 label:`lv_obj_t *title = lv_label_create(lv_scr_act()); lv_label_set_text(title, "ENV Monitor"); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);`,并预留一个空的 `lv_chart` 占位控件 `Chart_Widget`,不加入数据)。
    - 主循环:`for (;;) { lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); }`。首次进入任务时 `printf("[LVGL_Task] started\r\n");`。
    - `LVGL_Task_Create()` 调用 `xTaskCreate(LVGL_Task_Entry, "LVGL_Task", LVGL_TASK_STACK, NULL, LVGL_TASK_PRIO, NULL)`。
    - _Requirements: 7.5, 7.6, 11.2, 11.3_

- [x] 27. 改造 main() 集成 LVGL_Task
  - 在 `main()` 中 LCD 初始化完成、Phase 4 三原色自检之后,追加 `if (LVGL_Task_Create() != pdPASS) Error_Handler();`;保持在 `vTaskStartScheduler()` 之前。
  - 确保 `lv_init()` 仅在 `LVGL_Task` 内首次调用(不在 `main()` 中直接调用),满足 Requirement 11.2 关于 FreeRTOS 堆先启动的要求。
  - _Requirements: 11.2, 11.3, 11.4_

- [ ]* 28. 编写主机端 PBT:disp_flush_cb 地址计算
  - **Property 3:DMA2D flush_cb 地址计算公式**
  - **Validates: Requirements 7.4**
  - 创建 `tests/host/test_disp_flush_pbt.c`,抽出 `disp_flush_cb` 内纯整数地址计算部分(或封装为 `static inline void flush_cb_calc_addr(const lv_area_t *area, uint32_t *dst_addr, uint32_t *output_offset, uint32_t *w, uint32_t *h)`),用 theft 或类似框架以 ≥ 100 次随机 `lv_area_t`(满足 `0 <= x1 <= x2 < LCD_WIDTH` 且 `0 <= y1 <= y2 < LCD_HEIGHT`)做生成器,断言 `dst_addr == LCD_FB_ADDR + (area->y1 * LCD_WIDTH + area->x1) * 4` 且 `output_offset == LCD_WIDTH - (x2 - x1 + 1)`。
  - 该测试完全在主机上运行,不依赖硬件。

- [x] 29. Phase 5 DAPLINK 烧录并复位验证(必做)
  - Keil Rebuild → `pyocd flash` → `pyocd reset`。
  - 屏幕预期:Phase 4 三原色自检后,出现 `ENV Monitor` 标题与空 Chart_Widget 占位;整屏无撕裂、无闪烁。
  - 串口预期:新增 `[LVGL_Task] started`;`[LED_Task] toggle=N` 与 `[boot] ...` 日志继续输出。
  - 若屏幕黑屏或 LVGL 未启动(无 `[LVGL_Task] started`),检查:`lv_conf.h` 是否被正确 include(`LV_CONF_INCLUDE_SIMPLE` 宏)、`lv_init()` 是否在 `LVGL_Task` 内、DMA2D 中断优先级是否 ≥ `configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5`。
  - _Requirements: 7.6, 12.5_

### Phase 6: DHT20 驱动 + Sensor_Task + Chart_Screen 曲线 UI

- [x] 30. 实现 BSP_DHT20 驱动
  - [x] 30.1 创建 `Drivers/BSP/Inc/bsp_dht20.h`
    - 定义 `DHT20_ADDR = 0x38 / DHT20_ADDR_WRITE = 0x70 / DHT20_ADDR_READ = 0x71`。
    - 定义返回码 `DHT20_OK = 0 / DHT20_ERR_BUSY = 1 / DHT20_ERR_INIT = 2`。
    - 定义 I2C 引脚宏 `DHT20_SCL = PH4 / DHT20_SDA = PH5`(Soft I2C 方案,引脚按原理图)。
    - 声明 `uint8_t BSP_DHT20_Init(void); uint8_t BSP_DHT20_Read(float *temperature, float *humidity);`,并声明内部辅助 `void BSP_DHT20_ParseRaw(const uint8_t data[6], float *temperature, float *humidity);`(公开给主机端 PBT 使用)。
    - _Requirements: 8.1, 8.2_

  - [x] 30.2 实现软件 I2C(或 HAL 硬件 I2C)基础原语
    - 在 `Drivers/BSP/Src/bsp_dht20.c` 内实现 `Soft_I2C_Init / Start / Stop / SendByte / RecvByte(ack)`(参照 `skills/dht20-driver-guide.md`)。若采用 HAL 硬件 I2C,改为使用 `HAL_I2C_Master_Transmit / Receive`,并在 MspInit 中配置 PH4/PH5 为 `GPIO_MODE_AF_OD / OpenDrain / GPIO_AFx_I2Cx`。
    - Soft I2C 默认实现保证时钟 ≤ 400 kHz。
    - _Requirements: 8.1_

  - [x] 30.3 实现 BSP_DHT20_Init
    - 依次 `Soft_I2C_Init()` → 调用 `HAL_Delay(100)` / `vTaskDelay(pdMS_TO_TICKS(100))` 等待上电稳定 ≥ 100 ms → 读取状态字节,若 bit3=0(未校准)则按 skills 文档分别写入校准寄存器 `0x1B / 0x1C / 0x1E`。
    - _Requirements: 8.3, 8.8_

  - [x] 30.4 实现 BSP_DHT20_Read
    - Step 1: 发送触发测量 `0xAC 0x33 0x00`;Step 2: 等待 80 ms;Step 3: 循环最多 `DHT20_MAX_RETRY = 3` 次读取状态字节,若 bit7=1 则等 10 ms 重试;Step 4: 连续读取 6 字节;Step 5: 调用 `BSP_DHT20_ParseRaw(data, temperature, humidity)`。
    - 超出重试返回 `DHT20_ERR_BUSY`;成功返回 `DHT20_OK`。
    - `BSP_DHT20_ParseRaw` 内按公式:`humi_raw = (data[1]<<12) | (data[2]<<4) | (data[3]>>4); temp_raw = ((data[3] & 0x0F)<<16) | (data[4]<<8) | data[5]; *humidity = (float)humi_raw / 1048576.0f * 100.0f; *temperature = (float)temp_raw / 1048576.0f * 200.0f - 50.0f;`。
    - _Requirements: 8.4, 8.5, 8.6, 8.7, 8.8_

- [x] 31. 定义 Sample_Point 与 sample_queue
  - [x] 31.1 创建 `User/Inc/sensor_task.h`
    - 定义 `typedef struct { float temperature; float humidity; uint32_t timestamp_ms; } Sample_Point;`。
    - 声明 `extern QueueHandle_t sample_queue; BaseType_t Sensor_Task_Create(void);`。
    - _Requirements: 9.1, 9.3_

  - [x] 31.2 在 Sensor_Task_Create 内创建队列
    - 在 `User/Src/sensor_task.c` 中定义全局 `QueueHandle_t sample_queue = NULL;`;在 `Sensor_Task_Create()` 里 `sample_queue = xQueueCreate(4, sizeof(Sample_Point));` 并在失败时走 `Error_Handler()`。
    - _Requirements: 9.3_

- [x] 32. 实现 Sensor_Task
  - 定义 `#define SENSOR_TASK_STACK 512 / #define SENSOR_TASK_PRIO 2`(低于 LVGL_Task)。
  - 任务函数 `Sensor_Task_Entry`:首次运行调用 `BSP_DHT20_Init()`;循环:
    - `float t, h; if (BSP_DHT20_Read(&t, &h) == DHT20_OK) {`
      - 组装 `Sample_Point sp = { .temperature=t, .humidity=h, .timestamp_ms = xTaskGetTickCount() * 1000U / configTICK_RATE_HZ };`
      - 队列写入使用 FIFO + 满时覆盖最旧:`if (xQueueSend(sample_queue, &sp, 0) != pdTRUE) { Sample_Point drop; xQueueReceive(sample_queue, &drop, 0); xQueueSend(sample_queue, &sp, 0); }`
      - `printf("T=%.2f H=%.2f\r\n", t, h);`
      - 清零连续失败计数 `fail_count = 0;`
    - `} else { if (++fail_count >= 5) { printf("[Sensor_Task] DHT20 busy %u times\r\n", fail_count); fail_count = 0; } /* 不写队列 */ }`
    - `vTaskDelay(pdMS_TO_TICKS(1000));`
  - `Sensor_Task_Create()` 通过 `xTaskCreate` 启动该任务。
  - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 11.5_

- [x] 33. 实现 Chart_UI 曲线界面
  - [x] 33.1 创建 `User/Inc/chart_ui.h` 与 `User/Src/chart_ui.c`
    - 头文件导出 `void Chart_UI_Create(void); void Chart_UI_PushSample(const Sample_Point *sp);`。
    - 定义颜色常量 `#define COLOR_TEMP lv_color_hex(0xFF5555) / #define COLOR_HUMI lv_color_hex(0x5599FF)`。
    - 在 `.c` 内定义静态 `lv_obj_t *chart, *lbl_t, *lbl_h; lv_chart_series_t *s_temp, *s_humi;`。
    - _Requirements: 10.1_

  - [x] 33.2 实现 `Chart_UI_Create`
    - 按 `design.md` Chart UI 布局划分:Title Bar 0..40 / Chart 40..360 / Legend 360..400 / Value_Labels 400..480。
    - 创建标题 `lv_label "ENV Monitor"` 并居中顶部。
    - 创建 `chart = lv_chart_create(lv_scr_act()); lv_obj_set_size(chart, 800, 320); lv_obj_align(chart, LV_ALIGN_TOP_LEFT, 0, 40);`。
    - `lv_chart_set_type(chart, LV_CHART_TYPE_LINE); lv_chart_set_point_count(chart, 200); lv_chart_set_update_mode(chart, LV_CHART_UPDATE_MODE_SHIFT);`。
    - `lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, -100, 600);`(温度 ×10:-10.0°C..+60.0°C)。
    - `lv_chart_set_range(chart, LV_CHART_AXIS_SECONDARY_Y, 0, 1000);`(湿度 ×10:0.0..100.0 %RH)。
    - `s_temp = lv_chart_add_series(chart, COLOR_TEMP, LV_CHART_AXIS_PRIMARY_Y); s_humi = lv_chart_add_series(chart, COLOR_HUMI, LV_CHART_AXIS_SECONDARY_Y);`。
    - 创建 Legend 区两个带色块的 label 以及两条描述文字 `"■ Temperature (°C)"`、`"■ Humidity (%RH)"`。
    - 创建 `lbl_t, lbl_h`(Value_Labels),使用 `lv_font_montserrat_24`(至少 24 px),初始文本:`lv_label_set_text(lbl_t, "T: -- °C"); lv_label_set_text(lbl_h, "H: -- %RH");`。
    - _Requirements: 10.1, 10.2, 10.4, 10.5, 10.6, 10.8_

  - [x] 33.3 实现 `Chart_UI_PushSample`
    - 对 `sp->temperature` 与 `sp->humidity` 分别调用:`lv_chart_set_next_value(chart, s_temp, (int16_t)(sp->temperature * 10.0f)); lv_chart_set_next_value(chart, s_humi, (int16_t)(sp->humidity * 10.0f));`(滚动效果由 `LV_CHART_UPDATE_MODE_SHIFT` 保证)。
    - 更新 `lv_label_set_text_fmt(lbl_t, "T: %.1f °C", sp->temperature); lv_label_set_text_fmt(lbl_h, "H: %.1f %%RH", sp->humidity);`。
    - _Requirements: 10.3, 10.7, 10.9_

- [x] 34. 在 LVGL_Task 内消费 sample_queue
  - 修改 Phase 5 创建的 `LVGL_Task_Entry`:在 `lv_port_disp_init()` 之后调用 `Chart_UI_Create()` 替换原 Phase 5 的空占位;主循环改为:
    ```c
    Sample_Point sp;
    while (xQueueReceive(sample_queue, &sp, 0) == pdTRUE) {
        Chart_UI_PushSample(&sp);
    }
    lv_timer_handler();
    vTaskDelay(pdMS_TO_TICKS(5));
    ```
  - 注意:若 `sample_queue == NULL` 需先跳过(保险起见),防止 Phase 5 的精简构建被意外触发。
  - _Requirements: 10.3, 10.7, 11.2_

- [x] 35. 在 main() 集成 Sensor_Task 并移除 LCD 三原色自检
  - 在 `main()` 中 `LVGL_Task_Create()` 之后调用 `if (Sensor_Task_Create() != pdPASS) Error_Handler();`。
  - 删除 Phase 4 的三原色 `FillScreen RED/GREEN/BLUE` 自检代码(Chart_Screen 已提供更完整的 UI 可观测性),保留 `[boot] LCD OK` 行。
  - 确认 `main()` 顺序:`HAL_Init → SystemClock_Config → BSP_LED_Init → BSP_USART1_Init → BSP_SDRAM_Init/Test → BSP_LCD_Init → LED_Task_Create → LVGL_Task_Create → Sensor_Task_Create → vTaskStartScheduler`,符合 Requirement 11.1。
  - _Requirements: 11.1, 11.3, 11.4_

- [ ]* 36. 主机端 PBT:DHT20 原始数据解析
  - **Property 4:DHT20 原始数据解析满足公式与量程**
  - **Validates: Requirements 8.5, 8.7**
  - 创建 `tests/host/test_dht20_parse_pbt.c`:生成随机 6 字节 `data[0..5]`,强制 `data[0]` 的 bit7=0(就绪)、bit3=1(已校准),调用 `BSP_DHT20_ParseRaw(data, &t, &h)`(该函数在 Task 30.1 已声明),断言:`h == (float)humi_raw / 1048576.0f * 100.0f` 且 `h ∈ [0.0, 100.0]`;`t == (float)temp_raw / 1048576.0f * 200.0f - 50.0f` 且 `t ∈ [-50.0, 150.0]`。
  - ≥ 100 次迭代。

- [ ]* 37. 主机端 PBT:Sensor → Queue → LVGL 的 soundness + liveness
  - **Property 5:Sensor_Task → Queue → LVGL_Task 保持 soundness 与 liveness**
  - **Validates: Requirements 9.3, 9.4, 9.5**
  - 创建 `tests/host/test_sensor_queue_pbt.c`:用 FreeRTOS POSIX port 或纯数据结构模型模拟「长度 4 FIFO + 满时覆盖最旧」。生成器产出随机长度 ≥ 1 的 `BSP_DHT20_Read` 结果序列(每项为 `OK + 合法 Sample_Point` 或 `ERR_BUSY`),模拟 `Sensor_Task` 把 OK 项按本设计策略写入队列、LVGL 侧每轮排空队列。断言:(a) 接收序列是发送序列(仅 OK)子序列(soundness);(b) 若发送序列非空,则其最后一个 OK 元素出现在接收序列中(liveness)。
  - ≥ 100 次迭代。

- [ ]* 38. 主机端 PBT:Chart 环形缓冲
  - **Property 6:Chart 环形缓冲保留最后 N 个样本且顺序正确**
  - **Validates: Requirements 10.3**
  - 创建 `tests/host/test_chart_ring_pbt.c`:实现 `lv_chart_set_next_value / lv_chart_set_point_count` 的主机桩,生成随机长度 M 的 `Sample_Point` 序列与点数 `N ∈ [100, 400]`,依次 `Chart_UI_PushSample` 后从桩内部取出温度序列,断言等于原序列最后 `min(M, N)` 个温度值;湿度同理。
  - ≥ 100 次迭代。

- [ ]* 39. 主机端 PBT:Value_Labels 文本格式
  - **Property 7:Value_Labels 反映最新 Sample_Point**
  - **Validates: Requirements 10.6, 10.7**
  - 创建 `tests/host/test_value_labels_pbt.c`:实现 `lv_label_set_text_fmt / lv_label_get_text` 桩,生成随机合法 `Sample_Point`(`t ∈ [-40.0, 85.0]`, `h ∈ [0.0, 100.0]`),`Chart_UI_PushSample(&sp)` 后断言标签文本分别等于 `snprintf("T: %.1f °C", t)` 与 `snprintf("H: %.1f %%RH", h)`。
  - ≥ 100 次迭代。

- [x] 40. Phase 6 DAPLINK 烧录并复位验证(必做,端到端)
  - Keil Rebuild → `pyocd flash --target stm32h743xx MDK-ARM/Objects/env_monitor.axf` → `pyocd reset --target stm32h743xx`。
  - 串口预期:
    - `[boot] STM32H743 env-monitor starting...`
    - `[boot] SDRAM OK: 32MB @ 0xC0000000`
    - `[boot] LCD OK: 800x480 ARGB8888`
    - `[LVGL_Task] started`
    - 之后每秒一行 `T=xx.xx H=yy.yy`(DHT20 采样成功),`[LED_Task] toggle=N` 每 500 ms 一行。
  - 屏幕预期:
    - 顶部标题 `ENV Monitor`。
    - 中部 800×320 Chart 区:温度红色折线 + 湿度蓝色折线从右向左滚动;首次样本到达前 chart 无数据点。
    - 下方 Legend 显示 `■ Temperature (°C)` 与 `■ Humidity (%RH)`。
    - 底部 Value_Labels 以 ≥ 24 px 字体显示 `T: 25.3 °C` / `H: 48.7 %RH`(样例值);首次采样前显示 `T: -- °C` / `H: -- %RH`。
    - LED 持续 500 ms 翻转。
  - 稳定性:连续运行 ≥ 10 分钟,曲线持续滚动,数值与串口日志保持一致,无 HardFault / 无撕裂。
  - 若发现曲线不滚动、数值不更新或传感器常报 `busy`,对照 `skills/dht20-driver-guide.md` 排查 I2C 波形与地址;若 UI 卡顿检查 DMA2D 中断优先级与 `LVGL_TASK_STACK`。
  - _Requirements: 10.3, 10.6, 10.7, 10.9, 11.5, 12.6, 12.7_

### Phase 7 (可选): 主机端 PBT 测试基础设施与 CI

- [ ]* 41. 搭建主机端测试基础设施
  - 在仓库根目录新建 `tests/host/` 与 `tests/hw/` 目录,并在 `tests/host/Makefile` 或 `tests/host/CMakeLists.txt` 中配置:
    - 构建目标:`test_disp_flush_pbt / test_dht20_parse_pbt / test_sensor_queue_pbt / test_chart_ring_pbt / test_value_labels_pbt`。
    - 依赖:`theft` 或 `Hypothesis-C`(下载到 `tests/third_party/`)、Unity(可选)。
    - 对 `Drivers/BSP/Inc/` 与 `User/Inc/` 做条件 include,通过宏 `HOST_TEST` 隔离掉 HAL / FreeRTOS 调用,只编译纯逻辑函数(`BSP_DHT20_ParseRaw`、`flush_cb_calc_addr`、`Chart_UI_PushSample` 的逻辑抽取版本)。
  - 每个 PBT 测试配置 ≥ 100 轮迭代;失败时打印反例。
  - 提供 `make test` 或 `ctest` 入口。

- [ ]* 42. 配置 CI 自动执行主机端 PBT
  - 在仓库根目录新建 `.github/workflows/host-tests.yml`(或等效 CI 配置),在每次 PR 上执行 `make -C tests/host test`,全部 PBT 必须通过。
  - 在 README / CONTRIBUTING.md 中补充「本地跑 PBT」的说明。

- [ ]* 43. 最终检查点 - 确认全套测试通过
  - Ensure all tests pass, ask the user if questions arise.

## Notes

- 标记 `*` 的子任务为可选,主要覆盖主机端 PBT(Task 28、36–39、41–43)与硬件端 PBT(Task 17、21)。MVP 路径仅需完成未标星任务即可获得 Phase 6 端到端可观测的环境监测系统。
- 六个硬件阶段结束的 DAPLINK 烧录复位任务(Task 7、14、18、22、29、40)均为 **必做**:每个阶段的验证证据未全部满足前,不得跳到下一阶段开发;这是本设计「渐进可观测」原则的硬约束。
- 每个实现任务都指定了明确的文件路径、函数签名与关键常量,以便并行开发和代码审查。
- 所有 BSP 驱动都严格遵循 `skills/` 目录对应指南的文件结构与命名风格;如需偏离,请先在 design.md 中记录理由。

## 工作流完成提示

本 requirements-first 工作流只负责产出 `requirements.md`、`design.md`、`tasks.md` 三份规范文档。本次任务文档生成完成后,你可以:

1. 打开 `.kiro/specs/stm32h743-env-monitor/tasks.md`。
2. 点击任意任务旁的 **Start task** 开始按顺序实施 Phase 0 → Phase 6(可选 Phase 7)。
3. 每完成一个带有 "DAPLINK 烧录并复位验证" 字样的任务时,务必按任务内列出的串口/LED/LCD 证据逐项核对,再推进下一阶段。

可以看到kiro生成的任务里面除了必选的任务还有一些可选任务,可选任务用于辅助测试功能单元是否符合预期,并且kiro非常擅长编写脚本(如Python,Power Shell等)辅助程序的开发。

七、步骤 5:代码生成——Kiro 逐 Task 辅助实现

Spec 确认后,可以让 Kiro 按 Phase 顺序处理 Task:读取任务描述与关联的 Requirement,结合 Steering 约束生成或修改对应文件。工程师仍需逐 Task 审查,并在每个 Phase 结束后依据 tasks.md 中定义的串口日志、LED 行为和 LCD 显示完成验证。

八、步骤 6:GCC 编译

当用户指定用GCC编译代码时,kiro会根据系统环境变量检查工具链是否存在,若工具链不存在,kiro会指导用户完成工具链的安装,有效地降低用户部署开发环境的耗时。当kiro被给予相应的权限,部署开发环境的工作甚至可以在没有人的干预下独立完成。

传统嵌入式 IDE 通常通过图形界面管理头文件路径、预处理宏、链接脚本和浮点 ABI。切换到 GCC 开源工具链后,需要在 Makefile 或 CMakeLists.txt 中显式配置源文件、Include 路径、链接脚本、FPU 和浮点 ABI。若使用 newlib-nano,还需要通过 -u _printf_float 显式启用浮点格式化;--specs=nano.specs 的作用是选择 newlib-nano,二者不应混为一谈。

这些构建配置的复杂度在大型工程中尤为突出。本项目涉及 HAL 库(50 余个 .c 文件)、FreeRTOS 内核(8 个源文件 + portable 层)、LVGL 图形库(200 余个源文件)、BSP 驱动(5 个模块)和应用层(6 个源文件)。仅仅是把这些源文件和头文件路径正确地组织进 Makefile,对不熟悉 GCC 工具链的工程师来说就是不小的挑战。

Kiro 可以根据 Steering 中的工具链约束(toolchain.md)和 Design 中的目录结构生成 Makefile 初稿,工程师随后需要核对启动文件、链接脚本、源文件集合、库选项和目标芯片参数。本文最终采用 GCC 路线,核心配置如下:

Makefile
CPU       = -mcpu=cortex-m7
FPU       = -mfpu=fpv5-d16
FLOAT-ABI = -mfloat-abi=hard
LDSCRIPT  = MDK-ARM/STM32H743IITx_FLASH.ld
LDFLAGS  += --specs=nano.specs -u _printf_float

原始编译示例输出如下。具体文件大小取决于源码、编译器版本和优化选项,本轮按要求不修改代码块内容:

$ make
arm-none-eabi-gcc -c -mcpu=cortex-m7 -mfpu=fpv5-d16 ... User/Src/main.c
arm-none-eabi-gcc -c ... Drivers/BSP/Src/bsp_sdram.c
arm-none-eabi-gcc -c ... Drivers/BSP/Src/bsp_dht20.c
...
arm-none-eabi-size build/env_monitor.elf
   text    data     bss     dec     hex filename
 156832    1024   68480  226336   37420 build/env_monitor.elf

九、步骤 7:烧录与验证

嵌入式开发中,代码编译通过并不意味着程序可以在目标硬件上正常运行。烧录这件事看似简单,实际涉及一连串琐碎的配置:调试器驱动是否识别?目标芯片型号选对了没有?Flash 擦除算法是否匹配?烧录完要不要自动复位?串口波特率和引脚对不对?不同调试器(ST-Link、J-Link、DAPLink)各有各的工具和命令行参数,换一个调试器可能整套流程都要重新摸索。

在传统 IDE 中,这些配置通常分布在多个调试选项中。本文把烧录命令整理为 Makefile 目标:make flash 用于烧录,make reset 用于复位,底层调用 pyOCD(开源的 CMSIS-DAP 调试工具)。这样可以把命令和目标型号纳入版本管理,也便于在具备调试探针和目标板的自托管 CI 环境中集成。实际可复现性仍取决于工具版本、探针固件、目标板状态和权限配置。

$ pyocd flash --target stm32h743xx build/env_monitor.elf
 [====================================] 100%
 Programmed 156832 bytes (38 pages) in 3.21s

$ pyocd reset --target stm32h743xx
 Successfully reset device

示例串口日志如下。正式发布时应使用可追溯的原始日志替换或注明测试日期、固件版本与硬件版本:

[boot] STM32H743 env-monitor starting...
[boot] SDRAM OK: 32MB @ 0xC0000000
[boot] LCD OK: 800x480 ARGB8888
[LVGL_Task] started
[Sensor_Task] started
T=25.32 H=48.71
T=25.35 H=48.68
T=25.31 H=48.73
[LED_Task] toggle=1
T=25.33 H=48.70

十、步骤 8:AI 辅助调试——先观察,再验证

嵌入式开发通常需要多轮验证。这一步以两个场景说明如何使用 Kiro 汇总代码、原理图和 Datasheet 线索;最终根因仍应通过日志、寄存器状态、总线波形或对照实验确认。

10.1 场景 1:DHT20 I2C 通信异常

10.1.1 现象

[Sensor_Task] DHT20 busy 5 times
[Sensor_Task] DHT20 busy 5 times
...
LCD 上 Value_Labels 一直显示 "T: -- °C  H: -- %RH"

10.1.2 Kiro 诊断过程

  1. 查询 Datasheet:“DHT20 上电后需要等待多久?”——要求是不少于 100 ms;
  2. 检查代码:BSP_DHT20_Init() 中只写了 HAL_Delay(50),等待时间不足;
  3. 对照原理图确认 PH4/PH5 引脚和 4.7 kΩ 上拉电阻配置,随后仍需通过 I2C 波形或应答状态排除连线、供电和时序问题。

10.1.3 修复

/* 修复前 */
HAL_Delay(50);   // ← 不够,DHT20 需要 ≥100ms

/* 修复后 */
HAL_Delay(100);  // ← Kiro 根据 Datasheet 修正

重新编译、烧录后,如果串口持续输出合理的温湿度值,并且 I2C 状态与 CRC(如启用)均正常,才能确认该问题已解决。DHT20 的有效工作温度范围为 -40°C 至 +80°C,转换公式的理论输出范围不能替代器件工作范围校验。

10.2 场景 2:LCD 花屏

10.2.1 现象

LCD 显示花屏/雪花点;SDRAM 自检通过,但帧缓冲数据错乱。

10.2.2 Kiro 诊断过程

  1. 核对 SDRAM 数据手册中的刷新周期和各项最小时序;
  2. 确认 FMC 实际内核时钟、SDCLK 分频和刷新计数公式;
  3. 确认 STM32H7 HAL 时序结构体接收实际周期数,底层再写入减 1 后的寄存器编码。

修正刷新计数和不满足最小值的时序参数后,应重新执行全地址或抽样读写测试、长时间保持测试和 LCD 连续刷新测试。仅凭一次 SDRAM 自检通过,不能排除边界时序问题。

10.3 AI 辅助调试的作用

维度 常见人工流程 Kiro 可辅助的环节
I2C 异常 抓取波形并逐页查阅 Datasheet 检索相关时序、状态位和命令,生成核对清单
LCD 花屏 逐项核对时钟、SDRAM、LTDC 和缓存配置 关联代码、网表和 Datasheet,提出待验证假设
定位效率 取决于经验、证据完整度和问题复杂度 可缩短资料整理时间,但不能替代测量与复测

十一、步骤 9:Skills 沉淀——经验固化为团队资产

项目完成后,可以把开发过程中积累的经验整理为 7 个 Skill。下面的代码块按本轮要求保留原始逻辑清单;按照当前 Kiro Skills 规范,实际落盘时每个 Skill 应是一个独立目录,并在目录内包含必需的 SKILL.md,而不是直接把多个 Markdown 文件平铺在 .kiro/skills/ 下。

.kiro/skills/
├── usart-driver.md          # USART1:引脚配置 + printf 重定向
├── led-driver.md            # LED GPIO:共阳/共阴适配
├── freertos-porting.md      # FreeRTOS:中断接管 + 钩子 + 配置清单
├── sdram-driver-guide.md    # SDRAM:FMC 时序 + MPU + 刷新率
├── rgb-lcd-driver-guide.md  # RGB LCD:LTDC 时序 + PLL3 + 帧缓冲
├── lvgl-porting-guide.md    # LVGL:lv_conf.h + DMA2D + Draw Buffer
└── dht20-driver-guide.md    # DHT20:I2C 时序 + 校准 + 解析公式

跨项目复用

这些 Skill 可以作为后续项目的参考,但复用范围取决于 MCU、板卡、外设型号、工具链和软件版本。所有硬件参数在新项目中都应重新验证:

场景 可参考内容 需要补充或复核
同板子增加 BMP280 现有工程结构、I2C 调试流程、构建与烧录方法 新增 BMP280 Skill,并核对地址、时序和总线占用
更换为 STM32F4 DHT20 协议、FreeRTOS 与 LVGL 的通用方法 重新核对时钟、缓存、SDRAM、LCD、DMA 和移植层
新人接手维护 各 Skill 可作为操作与审查入口 补充项目架构、版本基线、已知限制和实测记录

十二、参考资料

十三、总结

13.1 流程对比

维度 未采用结构化 AI 流程时 Kiro 可辅助的环节
硬件理解 人工查阅原理图、网表和 Datasheet 整理连接关系并基于 Datasheet 检索相关段落
需求文档 由团队自行选择文档粒度 生成 Spec 初稿和 EARS 风格验收标准
架构设计 依赖人工设计与评审 辅助形成 Design 文档、架构图和内存映射草案
驱动编写 人工实现并持续查阅手册 生成代码初稿,由工程师审查、测试和修正
调试定位 结合仪器、日志和经验逐项排查 汇总证据并生成待验证的根因假设
经验沉淀 通过文档、代码评审和团队培训积累 把可复用流程整理为 Skills 并纳入版本管理

13.2 流程概览

Datasheet KB → 原理图/网表解析 → Steering 约束 → Spec(Requirements → Design → Tasks)
→ 代码生成 → 编译 → 烧录 → AI 辅助调试 → Skills 沉淀 → 跨项目复用

本文以 9 个步骤介绍了使用 Kiro 辅助完成硬件信息整理、Spec 拆解、代码实现、编译、烧录和调试的一套流程。核心不是让 AI 替代工程师,而是用结构化协作减少资料整理和模板性工作;架构选型、时序计算、代码审查、仪器测量和实机验证仍由工程师负责。

➡️ 下一步行动:

相关产品:

相关文章:

*前述特定亚马逊云科技生成式人工智能相关的服务目前在亚马逊云科技海外区域可用。亚马逊云科技中国区域相关云服务由西云数据和光环新网运营,具体信息以中国区域官网为准。

本篇作者

罗英明

亚马逊云科技解决方案架构师,负责企业客户应用在亚马逊云科技的架构咨询和设计。拥有 9 年大数据领域经验,曾主导数十 PB 级大数据平台的开发与运维,StarRocks 社区 Contributor。在大数据平台架构、数据仓库建设、OLAP 实时分析及 AI 应用等方向具备丰富的设计与落地经验。


AWS 架构师中心:云端创新的引领者

探索 AWS 架构师中心,获取经实战验证的最佳实践与架构指南,助您高效构建安全、可靠的云上应用