{"code":1,"msg":"成功","time":"1793358293","data":{"title":"〖2026嵌入式大赛〗基于 RT-Thread 与 Edgi-Talk PSoC E84 的边缘 AI 人形检测报警系统设计与实现","content":"##1. 系统概述\r\n源代码仓库[Gitee：hahahcd\/asc](https:\/\/gitee.com\/hahahcd\/asc.git)\r\n演示视频[Bilibili：人员检测报警系统演示](https:\/\/b23.tv\/CVoQYsj)\r\n\r\n### 1.1 建设目标\r\n\r\n系统以 Edgi-Talk PSoC E84 为边缘计算平台，通过 USB Host 接入 UVC 摄像头，在设备本地完成视频采集、人形检测、画面显示和报警决策。报警事件可驱动 LED、扬声器、SD 卡记录及局域网上报，核心检测流程不依赖云端服务。\r\n\r\n### 1.2 系统边界\r\n\r\n系统输入包括：\r\n\r\n- UVC 摄像头输出的 YUYV 视频帧；\r\n- 串口 msh 管理命令；\r\n- Wi-Fi 网络和 NTP 响应；\r\n- SD 卡上的配置文件和 WAV 音频文件。\r\n\r\n系统输出包括：\r\n\r\n- LCD 摄像头画面、检测框和状态信息；\r\n- 人形报警 LED；\r\n- 扬声器报警音；\r\n- SD 卡 CSV 日志和 BMP 抓拍；\r\n- UDP 或 HTTP JSON 报警消息；\r\n- 串口运行日志和诊断统计。\r\n\r\n### 1.3 当前配置基线\r\n\r\n| 项目           | 当前值                         |\r\n| -------------- | ------------------------------ |\r\n| 摄像头输入     | YUYV 424x240                   |\r\n| 显示视频尺寸   | 320x240，置于左侧 400x480 区域 |\r\n| AI 输入        | INT8 RGB 320x320               |\r\n| AI 调度周期    | 每 5 个摄像头帧推理一次        |\r\n| 模型候选数     | 6300                           |\r\n| 最大输出框数   | 8                              |\r\n| 模型框筛选阈值 | 0.45                           |\r\n| NMS IoU 阈值   | 0.45                           |\r\n| 默认报警置信度 | 60%                            |\r\n| 报警确认次数   | 连续命中 3 次推理              |\r\n| 报警解除次数   | 连续未命中 5 次推理            |\r\n| RT-Thread Tick | 1000 Hz                        |\r\n\r\n## 2. 总体架构\r\n\r\n### 2.1 系统上下文\r\n\r\n```mermaid\r\nflowchart LR\r\n    Camera[UVC 摄像头] -->|USB YUYV| Device[Edgi-Talk 设备]\r\n    Operator[维护人员] -->|UART \/ msh| Device\r\n    Device -->|RGB565| LCD[LCD]\r\n    Device -->|I2S PCM| Speaker[扬声器]\r\n    Device -->|GPIO| LED[报警 LED]\r\n    Device <-->|FAT 文件| SD[SD 卡]\r\n    Device -->|UDP JSON \/ HTTP POST| Server[局域网接收端]\r\n    NTP[NTP 服务器] -->|UTC 时间| Device\r\n```\r\n\r\n### 2.2 端到端处理链\r\n\r\n```mermaid\r\nflowchart LR\r\n    A[USB 等时传输] --> B[UVC 帧缓冲池]\r\n    B --> C[帧消费线程]\r\n    C --> D[显示预处理]\r\n    D --> E[LCD 左侧视频区]\r\n    C -->|每 5 帧| F[AI 预处理]\r\n    F --> G[Ethos-U55 推理]\r\n    G --> H[候选框解码与 NMS]\r\n    H --> I[报警状态机]\r\n    I --> J[检测框与 LVGL 状态]\r\n    I --> K[报警事件队列]\r\n    K --> L[SD \/ 网络处理]\r\n    K --> M[音频队列]\r\n    M --> N[WAV \/ 内置提示音]\r\n```\r\n\r\n### 2.3 设计原则\r\n\r\n1. **本地闭环**：视频、推理和报警判断均在设备端完成，断网不影响本地检测和声音报警。\r\n2. **实时路径隔离**：SD 写入、网络连接和音频播放不在 UVC 帧处理线程中执行。\r\n3. **有限资源控制**：视频帧池、事件队列、声音队列和 AI 输出均使用固定上限。\r\n4. **状态去抖**：报警以连续推理结果为依据，避免单帧误检引起抖动。\r\n5. **可观测性**：通过串口输出帧率、丢帧、USB 错误、AI 耗时及外设状态。\r\n6. **功能降级**：AI、SD 或网络异常不应阻止摄像头预览；WAV 不可用时退回内置提示音。\r\n\r\n## 3. 硬件架构\r\n\r\n### 3.1 硬件组成\r\n\r\n| 硬件单元               | 作用                                         | 当前使用情况     |\r\n| ---------------------- | -------------------------------------------- | ---------------- |\r\n| Cortex-M55             | RT-Thread、USB、视频、AI 调度、UI 和报警业务 | 使用             |\r\n| Cortex-M33             | 安全启动、系统初始化和 M55 拉起              | 启动依赖         |\r\n| Ethos-U55 NPU          | INT8 Person Detector 推理加速                | 使用             |\r\n| USB-OTG \/ DWC2         | UVC Host 数据传输                            | 使用             |\r\n| 480x800 LCD            | 视频和状态显示                               | 使用             |\r\n| VG-Lite GPU            | LVGL 绘制及显示处理                          | 使用             |\r\n| HyperRAM \/ SOCMEM      | 视频帧、模型、AI Arena 和工作缓冲            | 使用             |\r\n| SDIO \/ SD 卡           | 配置、日志、离线事件、WAV 和 BMP             | 使用             |\r\n| Wi-Fi 模块             | UDP、HTTP、NTP                               | 使用             |\r\n| I2S \/ 音频 Codec       | 报警音播放                                   | 使用             |\r\n| RTC                    | UTC 时间保存                                 | 使用             |\r\n| `P16_6` LED            | 主循环运行指示                               | 使用             |\r\n| `P16_7` LED            | 报警状态指示，低电平有效                     | 使用             |\r\n| PDM、IMU、温湿度传感器 | 板级扩展资源                                 | 当前主流程未使用 |\r\n\r\n### 3.2 多核启动关系\r\n\r\nM55 固件不是独立启动镜像。系统启动依赖关系如下：\r\n\r\n```text\r\nSecure M33 固件\r\n       |\r\n       v\r\nM33 Non-Secure 固件（启用 CM55 Core）\r\n       |\r\n       v\r\nM55 RT-Thread 应用固件\r\n```\r\n\r\n若 M33 工程未启用 CM55，M55 固件即使烧录成功也不会进入 `main()`。M55 启动后初始化 CherryUSB Host，并通过 `P16_6` 周期翻转提供存活指示。\r\n\r\n### 3.3 显示硬件组织\r\n\r\nLCD 物理分辨率为 480x800，软件使用旋转后的 800x480 逻辑画布：\r\n\r\n```text\r\n+--------------------------+--------------------------+\r\n| x = 0..399               | x = 400..799             |\r\n| UVC 视频与检测框         | LVGL 状态界面            |\r\n| 400x480                  | 400x480                  |\r\n+--------------------------+--------------------------+\r\n```\r\n\r\n摄像头图像转换为 RGB565 后写入左半区；LVGL 仅管理右半区。最终帧缓冲经旋转后送入 LCD Scanout，避免两个模块分别直接控制显示控制器。\r\n\r\n## 4. 软件架构\r\n\r\n### 4.1 软件分层\r\n\r\n```mermaid\r\nflowchart TB\r\n    L1[应用层<br\/>person_ai_app \/ person_alarm_io \/ person_ui]\r\n    L2[业务服务层<br\/>UVC App \/ UVC Display \/ Person AI]\r\n    L3[中间件层<br\/>CherryUSB \/ LVGL \/ TFLM \/ WLAN \/ DFS]\r\n    L4[设备驱动层<br\/>USB \/ LCD \/ VG-Lite \/ SDIO \/ I2S \/ RTC \/ GPIO]\r\n    L5[RT-Thread 5.0.2]\r\n    L6[PSoC E84 硬件]\r\n    L1 --> L2 --> L3 --> L4 --> L5 --> L6\r\n```\r\n\r\n### 4.2 主要软件组件\r\n\r\n| 组件        | 路径                              | 主要职责                                        |\r\n| ----------- | --------------------------------- | ----------------------------------------------- |\r\n| 系统入口    | `applications\/main.c`             | USB Host 初始化和运行 LED                       |\r\n| AI 应用适配 | `applications\/person_ai_app.c`    | 固定采集模式、AI 调度、状态去抖、画框和报警通知 |\r\n| 报警 I\/O    | `applications\/person_alarm_io.c`  | 音频、SD、BMP、网络、RTC 和配置                 |\r\n| 状态 UI     | `applications\/person_ui.c`        | LVGL 状态面板和运行信息                         |\r\n| UVC 应用    | `usbh_uvc_app.c`                  | 帧池、帧线程、FPS 线程和 msh 命令               |\r\n| UVC 流      | `usbh_uvc_stream.c`               | USB 视频流收发和缓冲队列                        |\r\n| 显示服务    | `usbh_uvc_display.c`              | YUYV\/MJPEG 转换、缩放和左屏合成                 |\r\n| AI 核心     | `deepcraft_ai\/src\/person_ai.c`    | 预处理、NPU 调用、解码和 NMS                    |\r\n| RTC 驱动    | `libraries\/HAL_Drivers\/drv_rtc.c` | RT-Thread RTC 设备适配                          |\r\n| LCD 驱动    | `libraries\/HAL_Drivers\/drv_lcd.c` | 帧缓冲、VG-Lite 和显示控制器                    |\r\n\r\n### 4.3 中间件与基础设施\r\n\r\n| 软件                   | 用途                        |\r\n| ---------------------- | --------------------------- |\r\n| CherryUSB 1.6.0        | USB Host、UVC 和 MSC 类支持 |\r\n| LVGL 9.2               | 右侧状态 UI                 |\r\n| TensorFlow Lite Micro  | INT8 模型运行时             |\r\n| Infineon ML Middleware | TFLM 和 Ethos-U55 集成      |\r\n| RT-Thread WLAN \/ WHD   | Wi-Fi 管理和自动重连        |\r\n| SAL \/ lwIP 2.0.3       | Socket、UDP 和 TCP          |\r\n| DFS \/ Elm-FAT          | SD 卡 FAT 文件访问          |\r\n\r\n## 5. 模块详细设计\r\n\r\n### 5.1 系统入口模块\r\n\r\n入口函数完成以下操作：\r\n\r\n1. 输出 M55 启动日志；\r\n2. 调用 `usbh_initialize(0, USBHS_BASE, NULL)` 初始化 USB Host；\r\n3. 配置 `P16_6` 为输出；\r\n4. 每 500 ms 翻转一次 LED，作为系统存活指示。\r\n\r\nUSB 摄像头插入后由 CherryUSB 完成枚举，并调用应用层 `usbh_video_run()` 输出设备支持的格式和分辨率。\r\n\r\n### 5.2 UVC 视频模块\r\n\r\n#### 5.2.1 启动流程\r\n\r\n`usbh_uvc_start` 的处理顺序如下：\r\n\r\n1. 解析格式、宽度和高度参数；\r\n2. `uvc_ai_app_enforce_mode()` 将当前模式强制为 YUYV 424x240；\r\n3. 初始化 8 个 UVC 帧缓冲；\r\n4. 初始化 AI 模型；AI 初始化失败时仅记录错误，摄像头预览继续；\r\n5. 创建 `uvc_fps` 和 `uvc_frm` 线程；\r\n6. 启动 UVC 数据流并提交帧缓冲。\r\n\r\n#### 5.2.2 单帧处理顺序\r\n\r\n```mermaid\r\nsequenceDiagram\r\n    participant USB as UVC Stream\r\n    participant Frame as uvc_frm\r\n    participant AI as Person AI\r\n    participant DISP as Display\r\n    participant Pool as Frame Pool\r\n\r\n    USB->>Frame: dequeue(frame)\r\n    Frame->>Frame: 丢弃积压旧帧，仅保留最新帧\r\n    Frame->>Frame: 复制到片上暂存区\r\n    Frame->>Pool: enqueue(原 USB 缓冲)\r\n    Frame->>AI: process_frame(暂存帧)\r\n    AI-->>Frame: 检测结果\/报警状态\r\n    Frame->>Frame: 424x240 缩放为 320x240\r\n    Frame->>DISP: YUYV 转 RGB565 并合成\r\n```\r\n\r\n先归还 USB 缓冲、再执行耗时的 AI 和显示处理，可降低有限帧池被应用层长期占用的概率。原始数据已经复制到独立暂存区，因此归还后不会出现 USB 覆盖正在处理的数据。\r\n\r\n#### 5.2.3 运行监控\r\n\r\n`uvc_fps` 每秒检查 USB 完成计数：连续 2 秒没有增长时调用传输中止回调以触发恢复；每 3 秒输出一次采集、显示、丢帧和错误计数。\r\n\r\n### 5.3 人形检测模块\r\n\r\n#### 5.3.1 输入预处理\r\n\r\nAI 输入接口为 424x240 YUYV。预处理执行以下步骤：\r\n\r\n- 将 YUYV 转换为 RGB；\r\n- 按宽度缩放至 320x181；\r\n- 在 320x320 输入中进行上下填充，填充值为 114；\r\n- 转换为模型所需的 INT8 数据；\r\n- 将输入送入 Ethos-U55 执行推理。\r\n\r\n#### 5.3.2 输出后处理\r\n\r\n模型输出大小为 `6300 x 6`。后处理先以 0.45 分数阈值筛选候选框，最多保留 32 个高分候选，再以 0.45 IoU 阈值执行 NMS，最终最多输出 8 个框。坐标从 320x320 模型空间反变换至 424x240 摄像头空间。\r\n\r\nAI 对外结果结构包含：\r\n\r\n```c\r\ntypedef struct {\r\n    uint8_t valid;\r\n    uint8_t count;\r\n    float max_confidence;\r\n    float inference_ms;\r\n    float preprocessing_ms;\r\n    uvc_ai_box_t boxes[8];\r\n} uvc_ai_result_t;\r\n```\r\n\r\n#### 5.3.3 应用接口\r\n\r\n| 接口                         | 说明                                |\r\n| ---------------------------- | ----------------------------------- |\r\n| `uvc_ai_app_enforce_mode()`  | 固定摄像头格式与尺寸                |\r\n| `uvc_ai_app_start()`         | 初始化模型、结果缓存和 Overlay 回调 |\r\n| `uvc_ai_app_process_frame()` | 按抽帧策略执行推理和状态更新        |\r\n| `uvc_ai_app_stop()`          | 注销回调、释放模型并解除报警        |\r\n\r\n### 5.4 报警状态机\r\n\r\n报警条件由持久化配置给出：\r\n\r\n```text\r\ndetected = result.valid\r\n        && result.count >= people_threshold\r\n        && result.max_confidence >= threshold \/ 100\r\n```\r\n\r\n状态转换如下：\r\n\r\n```mermaid\r\nstateDiagram-v2\r\n    [*] --> Armed\r\n    Armed --> Verifying: detected \/ hit_count++\r\n    Verifying --> Armed: !detected \/ hit_count=0\r\n    Verifying --> Alarm: hit_count >= 3\r\n    Alarm --> Alarm: detected \/ miss_count=0\r\n    Alarm --> Clearing: !detected \/ miss_count++\r\n    Clearing --> Alarm: detected \/ miss_count=0\r\n    Clearing --> Armed: miss_count >= 5\r\n    Alarm --> Armed: 停止 UVC\r\n```\r\n\r\n进入或解除报警时才调用 `person_alarm_io_notify()`，持续处于同一状态不会重复产生事件。进入报警时事件编号加一；对应的解除事件沿用同一编号，并附带本次报警持续时间。\r\n\r\n### 5.5 显示与 UI 模块\r\n\r\n显示模块向 AI 应用提供 Overlay 回调。AI 将检测框坐标映射到视频目标区域，普通框使用绿色 `RGB565 0x07e0`，报警框使用红色 `0xf800`，线宽为 3 像素。\r\n\r\nUI 模块不在 UVC 或 AI 线程中直接操作 LVGL 对象。业务线程只更新 `volatile` 状态变量，LVGL 定时器读取这些变量并刷新控件。界面状态定义如下：\r\n\r\n| 状态       | 条件                   | 显示文本          |\r\n| ---------- | ---------------------- | ----------------- |\r\n| 摄像头关闭 | `valid == 0`           | `CAMERA OFF`      |\r\n| 布防       | 有效且人数为 0         | `ARMED`           |\r\n| 确认中     | 人数大于 0、未进入报警 | `VERIFYING`       |\r\n| 报警       | `alarm_active != 0`    | `PERSON DETECTED` |\r\n\r\n### 5.6 报警 I\/O 模块\r\n\r\n报警 I\/O 模块使用两个后台线程，将慢速外设操作从视频路径中隔离：\r\n\r\n- `alarm_io`：CSV、BMP、离线消息、UDP 和 HTTP；\r\n- `alarm_snd`：WAV 校验、读取和音频播放。\r\n\r\n报警进入时设置抓拍请求。随后到达的第一帧合格 YUYV 数据被复制到动态缓冲，交由 `alarm_io` 转换为 24 位 BMP。该设计避免在 AI 回调内完成逐像素文件写入。\r\n\r\n## 6. 并发与同步设计\r\n\r\n### 6.1 线程配置\r\n\r\nRT-Thread 数值越小优先级越高。\r\n\r\n| 线程        |       栈大小 |     优先级 |   时间片 | 职责                    |\r\n| ----------- | -----------: | ---------: | -------: | ----------------------- |\r\n| `main`      |       2048 B |         10 | 系统默认 | USB 初始化和运行 LED    |\r\n| `uvc_fps`   |       1024 B |         20 |       10 | FPS 统计和 UVC 停滞监控 |\r\n| `uvc_frm`   |      65536 B |         22 |       10 | 帧消费、AI 和显示       |\r\n| `alarm_io`  |       4096 B |         20 |       10 | SD 与网络事件处理       |\r\n| `alarm_snd` |       8192 B |         21 |       10 | 音频播放                |\r\n| LVGL 任务   | 由端口层创建 | 由端口配置 |        - | UI 绘制与刷屏           |\r\n\r\n### 6.2 IPC 对象\r\n\r\n| 对象     | 类型     |     容量 | 用途                |\r\n| -------- | -------- | -------: | ------------------- |\r\n| `al_evt` | 消息队列 | 8 条事件 | AI 状态机到报警 I\/O |\r\n| `al_snd` | 消息队列 | 2 条事件 | 报警 I\/O 到音频线程 |\r\n| `al_net` | Mutex    |        - | 保护网络目标配置    |\r\n| `al_cap` | Mutex    |        - | 保护抓拍缓冲所有权  |\r\n\r\n事件队列满时，新事件被丢弃并输出 `event queue full`。声音队列满时跳过额外播放请求，但不影响日志和网络处理。\r\n\r\n### 6.3 共享数据约束\r\n\r\n- UVC 原始缓冲归 CherryUSB 帧池所有，应用复制后尽快归还。\r\n- 抓拍缓冲在视频线程分配，在 `alarm_io` 线程取得所有权后释放。\r\n- LVGL 对象仅由 LVGL 上下文访问，业务线程不得直接调用控件 API。\r\n- AI 结果由帧线程更新，Overlay 在同一显示处理链中读取。\r\n- 网络目标和抓拍指针分别由独立 Mutex 保护。\r\n\r\n## 7. 外部接口设计\r\n\r\n### 7.1 msh 命令接口\r\n\r\n| 命令                  | 参数                                | 功能                                       |\r\n| --------------------- | ----------------------------------- | ------------------------------------------ |\r\n| `usbh_uvc_start`      | `[fmt] [w] [h]`                     | 启动 UVC；当前 AI 构建会强制为 `0 424 240` |\r\n| `usbh_uvc_stop`       | 无                                  | 停止视频流并释放 AI                        |\r\n| `person_alarm_test`   | `on\\|off`                           | 注入测试事件，不改变检测框和报警 LED       |\r\n| `person_alarm_config` | `threshold\\|people\\|snapshot value` | 修改并保存报警配置                         |\r\n| `person_alarm_status` | 无                                  | 输出事件号、时间和外设状态                 |\r\n| `person_net_set`      | `<ipv4> <port>`                     | 设置 UDP 目标                              |\r\n| `person_net_clear`    | 无                                  | 关闭 UDP 上报                              |\r\n| `person_http_set`     | `<ipv4> <port> <path>`              | 设置 HTTP 目标                             |\r\n| `person_http_clear`   | 无                                  | 关闭 HTTP 上报                             |\r\n| `person_ntp_sync`     | `[numeric_ipv4]`                    | NTP 校时，默认服务器为 `129.6.15.28`       |\r\n\r\n### 7.2 应用内部接口\r\n\r\n| 接口                                 | 调用方     | 被调用方 | 语义                         |\r\n| ------------------------------------ | ---------- | -------- | ---------------------------- |\r\n| `uvc_ai_app_process_frame()`         | UVC 帧线程 | AI 应用  | 提交一帧用于抽帧推理         |\r\n| `uvc_display_set_overlay_callback()` | AI 应用    | 显示模块 | 注册检测框绘制回调           |\r\n| `person_alarm_io_notify()`           | 报警状态机 | 报警 I\/O | 发布进入\/解除事件            |\r\n| `person_alarm_io_capture()`          | UVC 帧线程 | 报警 I\/O | 满足请求时复制下一帧         |\r\n| `buddy_ui_update_detection()`        | AI 应用    | UI       | 更新人数、报警、置信度和耗时 |\r\n| `buddy_ui_update_fps()`              | UVC 应用   | UI       | 更新显示 FPS                 |\r\n\r\n### 7.3 网络消息格式\r\n\r\nUDP 数据报和 HTTP Body 使用相同 JSON 对象：\r\n\r\n```json\r\n{\r\n  \"event_id\": 1,\r\n  \"active\": 1,\r\n  \"people\": 2,\r\n  \"confidence\": 87,\r\n  \"epoch\": 1760500000,\r\n  \"duration_ms\": 0,\r\n  \"test\": 0\r\n}\r\n```\r\n\r\n| 字段          | 类型   | 说明                                 |\r\n| ------------- | ------ | ------------------------------------ |\r\n| `event_id`    | uint32 | 报警周期编号，进入时递增，解除时沿用 |\r\n| `active`      | uint8  | `1` 进入报警，`0` 解除报警           |\r\n| `people`      | uint8  | 检测人数；解除事件为 0               |\r\n| `confidence`  | uint8  | 最高置信度百分数                     |\r\n| `epoch`       | time_t | UTC Unix 时间戳；未校时为 0          |\r\n| `duration_ms` | uint32 | 解除事件中的报警持续时间             |\r\n| `test`        | uint8  | `1` 为人工测试，`0` 为真实检测       |\r\n\r\nHTTP 使用 `POST`、`Content-Type: application\/json` 和 `Connection: close`。当前实现优先尝试 UDP；UDP 未成功时才尝试 HTTP。两者均失败时，将 JSON 追加到离线文件。\r\n\r\n## 8. 数据与文件设计\r\n\r\n### 8.1 SD 卡文件\r\n\r\n| 路径                                 | 格式             | 说明                      |\r\n| ------------------------------------ | ---------------- | ------------------------- |\r\n| `\/sdcard\/person_alarm.cfg`           | `key=value` 文本 | 持久化报警和网络配置      |\r\n| `\/sdcard\/person_alarm.csv`           | CSV              | 报警进入与解除记录        |\r\n| `\/sdcard\/person_alarm_offline.jsonl` | JSON Lines       | 网络失败时的待发送事件    |\r\n| `\/sdcard\/alarm.wav`                  | PCM WAV          | 1 人报警音                |\r\n| `\/sdcard\/alarm2.wav`                 | PCM WAV          | 2 人报警音                |\r\n| `\/sdcard\/alarm3.wav`                 | PCM WAV          | 3 人及以上报警音          |\r\n| `\/sdcard\/alarm_NNNNNN.bmp`           | 24 位 BMP        | 报警抓拍，编号对应事件 ID |\r\n\r\n### 8.2 配置文件格式\r\n\r\n配置文件示例：\r\n\r\n```ini\r\nudp_ip=192.168.1.100\r\nudp_port=5005\r\nhttp_ip=192.168.1.100\r\nhttp_port=8080\r\nhttp_path=\/person\/alarm\r\nthreshold=60\r\npeople=1\r\nsnapshot=1\r\n```\r\n\r\n无 SD 卡时配置仅保存在 RAM，掉电后丢失。配置加载失败时使用以下默认值：置信度 60%、最少人数 1、抓拍开启、HTTP 路径 `\/person\/alarm`。\r\n\r\n### 8.3 CSV 格式\r\n\r\n```csv\r\nevent_id,time,epoch,tick,active,people,confidence,duration_ms,test,snapshot\r\n```\r\n\r\n每条事件写入后立即关闭文件。该策略优先保证异常掉电后的记录完整性，但频繁事件会增加文件系统操作次数。\r\n\r\n### 8.4 WAV 格式约束\r\n\r\n外部报警音必须满足：\r\n\r\n- RIFF\/WAVE 容器；\r\n- PCM 编码；\r\n- 16000 Hz；\r\n- 单声道；\r\n- 16 bit；\r\n- 非空且不超过 10 秒。\r\n\r\n不支持 MP3、浮点 WAV 或立体声 WAV。文件校验失败后自动使用内置提示音，不中断报警事件的其他处理。\r\n\r\n### 8.5 BMP 格式\r\n\r\n抓拍使用 424x240、24 位、Top-down BMP。视频线程复制完整 YUYV 帧，I\/O 线程逐像素转换为 BGR 并写入文件。单张图像数据约为 305 KB，不包含文件系统分配开销。\r\n\r\n## 9. 内存设计\r\n\r\n### 9.1 链接区域\r\n\r\n| 区域                      | 起始地址     |     大小 | 用途                     |\r\n| ------------------------- | ------------ | -------: | ------------------------ |\r\n| `m55_code_INTERNAL`       | `0x00000000` |   256 KB | ITCM 性能敏感代码        |\r\n| `m55_data_INTERNAL`       | `0x20000000` |   256 KB | DTCM 栈和快速数据        |\r\n| `m55_nvm`                 | `0x60580000` |     8 MB | 应用代码和模型加载镜像   |\r\n| `m55_code_secondary`      | `0x26000000` |   384 KB | 扩展代码区               |\r\n| `m55_data_secondary`      | `0x26060000` | 1.375 MB | 堆和 AI Arena 等数据     |\r\n| `m33_m55_shared`          | `0x261C0000` |   256 KB | 双核共享 SOCMEM          |\r\n| `gfx_mem`                 | `0x26200000` |     3 MB | LCD 和视频图形缓冲       |\r\n| `m55_hyperram`            | `0x64200000` |     2 MB | M55 HyperRAM 区域        |\r\n| `m33_m55_shared_hyperram` | `0x64400000` |    12 MB | USB 帧、模型及 AI 工作区 |\r\n\r\n### 9.2 关键段部署\r\n\r\n| 链接段                | 位置                              | 内容                    |\r\n| --------------------- | --------------------------------- | ----------------------- |\r\n| `.cy_ml_arena`        | Secondary Data                    | 1 MB TFLM Tensor Arena  |\r\n| `.cy_ml_model_data`   | Shared HyperRAM，加载镜像在 Flash | Vela INT8 Person 模型   |\r\n| `.cy_ml_input_data`   | Shared HyperRAM                   | 320x320x3 模型输入      |\r\n| `.cy_ml_working_data` | Shared HyperRAM                   | AI 预处理和后处理工作区 |\r\n| `.cy_uvc_native_buf`  | GFX Memory                        | RGB565 视频转换缓冲     |\r\n| `.cy_uvc_yuyv_stage`  | GFX Memory                        | 424x240 YUYV 片上暂存帧 |\r\n\r\n模型放入共享 HyperRAM是因为其体积超过片上 SOCMEM 余量；视频转换缓冲放入 GFX Memory，是因为 VG-Lite AXI 主设备不能可靠读取当前 HyperRAM 数据路径。\r\n\r\n### 9.3 动态内存\r\n\r\n动态内存主要用于：\r\n\r\n- 报警抓拍的 424x240x2 字节 YUYV 副本；\r\n- RT-Thread 线程栈、队列和中间件对象；\r\n- 网络 Socket 和文件系统运行对象。\r\n\r\n抓拍缓冲分配失败时跳过本次抓拍，但报警状态、日志、声音和网络流程继续执行。\r\n\r\n## 10. 可靠性与异常处理\r\n\r\n| 异常场景                 | 处理策略                            |\r\n| ------------------------ | ----------------------------------- |\r\n| AI 初始化失败            | 输出错误，保持摄像头预览            |\r\n| UVC 帧积压               | 丢弃旧帧，仅处理最新帧              |\r\n| USB URB 连续 2 秒无进展  | 中止当前传输并触发恢复              |\r\n| 显示初始化失败           | 跳过显示，UVC 和 AI 路径继续运行    |\r\n| SD 卡不存在              | 跳过配置持久化、日志和抓拍          |\r\n| WAV 不存在或格式非法     | 播放内置提示音                      |\r\n| Wi-Fi 离线或网络发送失败 | 本地报警继续，并尝试记录离线 JSONL  |\r\n| 报警事件队列满           | 丢弃新事件并输出诊断日志            |\r\n| 声音队列满               | 跳过额外声音，不影响其他报警动作    |\r\n| RTC 未校时               | UI 显示不可用，事件 `epoch` 为 0    |\r\n| 停止 UVC                 | 停止流、释放 AI、清除报警状态和 LED |\r\n\r\n离线消息恢复时，系统将 JSONL 内容通过 UDP 逐行补发，随后删除离线文件。当前协议没有应用层确认，因此文件删除表示“已提交给 Socket”，不代表远端业务系统已经确认接收。\r\n\r\n## 11. AI 模型开发与部署\r\n\r\n### 11.1 模型工程与数据来源\r\n\r\n当前固件使用 `PresenceDetectionVision` 模型工程。该工程基于 DEEPCRAFT Studio 的人员检测示例，以 YOLOv5n 为基础网络，通过迁移学习训练单类别 `person` 目标检测模型。基础数据来自 Roboflow People Detection Dataset；项目实施过程中在基础数据集上加入了自行拍摄并标注的实际场景图像，用于针对目标摄像头视角、人物距离、背景和光照条件进行微调。\r\n\r\n当前工程的数据划分如下：\r\n\r\n| 数据子集   | 图像数 | 用途                   |\r\n| ---------- | -----: | ---------------------- |\r\n| Train      |    935 | 参数更新和数据增强     |\r\n| Validation |     63 | 训练过程评估和模型选择 |\r\n| Test       |     43 | 独立结果评估           |\r\n\r\n\r\n### 11.2 训练配置\r\n\r\n模型工程文件 `PresenceDetectionVision\/PresenceDetectionVision.improj` 记录的主要训练配置如下：\r\n\r\n| 参数         | 配置值                       |\r\n| ------------ | ---------------------------- |\r\n| 任务类型     | 单类别目标检测               |\r\n| 基础网络     | YOLOv5n Legacy               |\r\n| 输入尺寸     | 320x320                      |\r\n| Epoch        | 100                          |\r\n| Batch Size   | 32                           |\r\n| 优化器       | SGD                          |\r\n| 初始学习率   | 0.01                         |\r\n| Momentum     | 0.937                        |\r\n| Weight Decay | 0.0005                       |\r\n| 冻结层数     | 10                           |\r\n| 训练模式     | Training \/ Transfer Learning |\r\n\r\n训练启用了平移、缩放、水平翻转、Mosaic 和 HSV 色彩增强。冻结部分预训练层后，在基础数据与自采数据组成的混合训练集上继续优化，使模型保留通用人员特征，同时适应本项目的部署场景。\r\n\r\n\r\n### 11.3 量化与 NPU 转换\r\n\r\n模型部署链如下：\r\n\r\n```text\r\nYOLOv5n 迁移学习权重 model.pt\r\n              |\r\n              v\r\n全整数量化 model_int8.tflite\r\n              |\r\n              v\r\nVela 编译 model_int8_vela.tflite\r\n              |\r\n              v\r\nperson_model_data.c 通过 .incbin 嵌入固件\r\n              |\r\n              v\r\nmtb-ml \/ TFLM \/ Ethos-U55 执行推理\r\n```\r\n\r\nVela 产物针对 `Ethos_U55_128`，模型大小为 1,659,824 字节。编译摘要给出的理论推理时间为 28.029 ms、吞吐率为 35.677 inference\/s；该数值是 Vela 性能估算，设备端实际耗时以串口输出的 `infer` 统计为准。\r\n\r\n固件构建通过 `libraries\/Common\/deepcraft_ai\/model\/person_model_data.c` 的 `.incbin` 引用 Vela TFLite 文件，并将模型放入 `.cy_ml_model_data` 段。运行时 `person_ai.c` 校验 TFLite 文件头、输入输出张量类型和元素数量，再通过 `mtb_ml_model_invoke()` 调用 Ethos-U55。\r\n\r\n### 11.4 模型与固件接口约束\r\n\r\n固件适配层要求模型输入为 `320x320x3` INT8 RGB，输入零点为 `-128`；输出包含 6300 个候选，每个候选 6 个字段。更换网络结构、输入尺寸、量化方式或输出排列后，必须同步修改 `uvc_ai.h` 和 `person_ai.c`，不能只替换模型文件。\r\n\r\n\r\n\r\n## 12. 构建与部署\r\n\r\n### 12.1 关键编译开关\r\n\r\n| 宏                                | 作用                           |\r\n| --------------------------------- | ------------------------------ |\r\n| `RT_USING_CHERRYUSB`              | 启用 CherryUSB                 |\r\n| `RT_CHERRYUSB_HOST`               | 启用 USB Host                  |\r\n| `RT_CHERRYUSB_HOST_DWC2_INFINEON` | 使用 Infineon DWC2 Host 控制器 |\r\n| `RT_CHERRYUSB_HOST_VIDEO`         | 启用 UVC Host 类               |\r\n| `BSP_USING_DEEPCRAFT_AI`          | 启用 Person AI 模块            |\r\n| `BSP_USING_LVGL`                  | 启用 LVGL UI                   |\r\n| `BSP_USING_LCD`                   | 启用 LCD                       |\r\n| `BSP_USING_AUDIO_PLAY`            | 启用音频播放                   |\r\n| `BSP_USING_SDCARD`                | 启用 SD 卡                     |\r\n| `RT_USING_WIFI`                   | 启用 Wi-Fi                     |\r\n| `BSP_USING_RTC`                   | 启用 RTC 驱动                  |\r\n\r\n\r\n\r\n## 13. 关键代码实现\r\n\r\n本章代码节选自已提交的固件工程，省略了参数检查、日志和部分资源释放代码，重点展示从 UVC 视频帧到人物报警及证据保存的主流程。完整实现见 [Gitee 源代码仓库](https:\/\/gitee.com\/hahahcd\/asc.git)。\r\n\r\n### 13.1 UVC 视频流与 AI 任务启动\r\n\r\n文件 `libraries\/Common\/board\/ports\/usb\/usbh_uvc_app.c` 中的 `cmd_usbh_uvc_start()` 创建视频帧池，初始化人员检测模型，再启动帧处理线程和 UVC 数据流。模型初始化失败时仍保留摄像头预览，避免 AI 故障阻塞基本视频功能。\r\n\r\n```c\r\nstatic int cmd_usbh_uvc_start(int argc, char **argv)\r\n{\r\n    uint8_t fmt = USBH_VIDEO_FORMAT_UNCOMPRESSED;\r\n    uint16_t w = UVC_CAPTURE_WIDTH;\r\n    uint16_t h = UVC_CAPTURE_HEIGHT;\r\n\r\n    \/* 当前人员检测模型固定使用 YUYV 424x240 输入。 *\/\r\n    uvc_ai_app_enforce_mode(&fmt, &w, &h);\r\n\r\n    for (uint32_t i = 0; i < UVC_FRAME_BUF_COUNT; i++) {\r\n        frame_pool[i].frame_buf = frame_buffer[i];\r\n        frame_pool[i].frame_bufsize = (uint32_t)w * h * 2U;\r\n    }\r\n    if (usbh_video_stream_create(frame_pool, UVC_FRAME_BUF_COUNT) < 0) {\r\n        return -RT_ERROR;\r\n    }\r\n\r\n    (void)uvc_ai_app_start(w, h);\r\n    uvc_app_running = 1;\r\n\r\n    rt_thread_t thread = rt_thread_create(\"uvc_frm\",\r\n        uvc_frame_thread_entry, NULL, 65536, 22, 10);\r\n    if (thread != RT_NULL) rt_thread_startup(thread);\r\n\r\n    usbh_video_stream_start(w, h, fmt);\r\n    return RT_EOK;\r\n}\r\n```\r\n\r\n帧线程从 CherryUSB 队列取出最新一帧，先交给 AI，再缩放并显示到 LCD：\r\n\r\n```c\r\nwhile (uvc_app_running) {\r\n    if (usbh_video_stream_dequeue(&frame, RT_TICK_PER_SECOND) < 0)\r\n        continue;\r\n\r\n    uvc_frame_keep_latest(&frame);\r\n    memcpy(stage_buf, frame->frame_buf, frame->frame_size);\r\n    display_frame = *frame;\r\n    display_frame.frame_buf = stage_buf;\r\n    usbh_video_stream_enqueue(frame);\r\n\r\n    uvc_ai_app_process_frame(&display_frame);\r\n    uvc_display_frame(&display_frame, stream_w, stream_h);\r\n}\r\n```\r\n\r\n### 13.2 YUYV 图像预处理\r\n\r\n文件 `libraries\/Common\/deepcraft_ai\/src\/person_ai.c` 将摄像头的 `424x240 YUYV` 图像转换为模型需要的 `320x320 INT8 RGB`。有效画面缩放为 `320x181`，上下使用像素值 114 填充，以保持原始宽高比。\r\n\r\n```c\r\n#define PERSON_RESIZED_HEIGHT 181U\r\n#define PERSON_PAD_TOP \\\r\n    ((UVC_AI_MODEL_HEIGHT - PERSON_RESIZED_HEIGHT) \/ 2U)\r\n#define PERSON_PAD_PIXEL 114\r\n\r\nstatic void person_prepare_input(const uint8_t *yuyv, int8_t *input)\r\n{\r\n    memset(input, (int8_t)(PERSON_PAD_PIXEL - 128),\r\n           PERSON_MODEL_INPUT_COUNT);\r\n\r\n    for (uint16_t dst_y = 0; dst_y < PERSON_RESIZED_HEIGHT; dst_y++) {\r\n        uint16_t src_y = (uint16_t)\r\n            ((uint32_t)dst_y * g_src_height \/ PERSON_RESIZED_HEIGHT);\r\n        int8_t *dst = input +\r\n            (uint32_t)(dst_y + PERSON_PAD_TOP) * UVC_AI_MODEL_WIDTH * 3U;\r\n\r\n        for (uint16_t dst_x = 0; dst_x < UVC_AI_MODEL_WIDTH; dst_x++) {\r\n            uint16_t src_x = (uint16_t)\r\n                ((uint32_t)dst_x * g_src_width \/ UVC_AI_MODEL_WIDTH);\r\n            int32_t y, u, v;\r\n            person_read_yuyv(yuyv, src_x, src_y, &y, &u, &v);\r\n            person_yuv_to_rgb_int8(y, u, v, dst);\r\n            dst += UVC_AI_MODEL_CHANNELS;\r\n        }\r\n    }\r\n}\r\n```\r\n\r\nRGB 三通道在写入张量前减去 128，与模型输入零点 `-128` 保持一致：\r\n\r\n```c\r\ndst[0] = (int8_t)((int16_t)person_sat_u8(r) - 128);\r\ndst[1] = (int8_t)((int16_t)person_sat_u8(g) - 128);\r\ndst[2] = (int8_t)((int16_t)person_sat_u8(b) - 128);\r\n```\r\n\r\n### 13.3 Ethos-U55 推理与 YOLO 后处理\r\n\r\n预处理完成后，通过 Infineon ML Middleware 调用 Ethos-U55 NPU。输出张量经反量化、置信度筛选和 NMS 后写入统一检测结果结构。\r\n\r\n```c\r\nint uvc_ai_process_yuyv(const uint8_t *yuyv, uint32_t yuyv_size,\r\n                        uvc_ai_result_t *result)\r\n{\r\n    int8_t *input = (int8_t *)g_person_model->input;\r\n    int8_t *output = (int8_t *)g_person_model->output;\r\n\r\n    rt_tick_t prep_start = rt_tick_get();\r\n    person_prepare_input(yuyv, input);\r\n    rt_tick_t prep_end = rt_tick_get();\r\n\r\n    if (mtb_ml_model_invoke(g_person_model) != CY_RSLT_SUCCESS)\r\n        return -RT_ERROR;\r\n    rt_tick_t inference_end = rt_tick_get();\r\n\r\n    uvc_ai_reset_result(result);\r\n    person_decode_output(output, result);\r\n    result->valid = 1U;\r\n    result->preprocessing_ms =\r\n        (float)(prep_end - prep_start) * 1000.0f \/ RT_TICK_PER_SECOND;\r\n    result->inference_ms =\r\n        (float)(inference_end - prep_end) * 1000.0f \/ RT_TICK_PER_SECOND;\r\n    return RT_EOK;\r\n}\r\n```\r\n\r\nYOLOv5n 输出的每个候选包含边界框、目标置信度和 `person` 类别置信度。固件先计算组合分数，再保留高分候选并执行 NMS：\r\n\r\n```c\r\nfor (uint32_t i = 0; i < UVC_AI_MODEL_CANDIDATES; i++) {\r\n    const int8_t *row = output + i * UVC_AI_MODEL_FIELDS;\r\n    float objectness = person_dequantize(row[4]);\r\n    float class_score = person_dequantize(row[5]);\r\n    float score = objectness * class_score;\r\n\r\n    if (score < PERSON_SCORE_THRESHOLD) continue;\r\n    float center_x = person_dequantize(row[0]);\r\n    float center_y = person_dequantize(row[1]);\r\n    float width = person_dequantize(row[2]);\r\n    float height = person_dequantize(row[3]);\r\n    if (width <= 0.0f || height <= 0.0f) continue;\r\n\r\n    person_candidate_t candidate = {\r\n        .x1 = center_x - width * 0.5f,\r\n        .y1 = center_y - height * 0.5f,\r\n        .x2 = center_x + width * 0.5f,\r\n        .y2 = center_y + height * 0.5f,\r\n        .score = score,\r\n    };\r\n    person_insert_candidate(candidates, &candidate_count, &candidate);\r\n}\r\n\r\n\/* 对按置信度排序后的候选框执行 NMS。 *\/\r\nif (person_iou(&candidates[i], &candidates[j]) > PERSON_NMS_THRESHOLD)\r\n    suppressed[j] = 1U;\r\n```\r\n\r\n### 13.4 抽帧调度与报警状态机\r\n\r\n文件 `applications\/person_ai_app.c` 每隔 5 个摄像头帧执行一次推理。业务报警默认要求人数不少于 1、最高置信度不低于 60%，并使用“连续 3 次命中、连续 5 次未命中”的去抖策略。\r\n\r\n```c\r\nstatic void person_alarm_update(const uvc_ai_result_t *result)\r\n{\r\n    uint8_t threshold = person_alarm_io_threshold();\r\n    uint8_t people_threshold = person_alarm_io_people_threshold();\r\n    uint8_t detected = result != RT_NULL && result->valid != 0U &&\r\n        result->count >= people_threshold &&\r\n        result->max_confidence >= (float)threshold \/ 100.0f;\r\n\r\n    if (detected != 0U) {\r\n        g_person_alarm_misses = 0U;\r\n        if (g_person_alarm_hits < PERSON_ALARM_CONFIRM_COUNT)\r\n            g_person_alarm_hits++;\r\n        if (g_person_alarm_hits >= PERSON_ALARM_CONFIRM_COUNT)\r\n            person_alarm_set(1U, result->count,\r\n                (uint8_t)(result->max_confidence * 100.0f + 0.5f));\r\n    } else {\r\n        g_person_alarm_hits = 0U;\r\n        if (g_person_alarm_active != 0U &&\r\n            ++g_person_alarm_misses >= PERSON_ALARM_CLEAR_COUNT)\r\n            person_alarm_set(0U, 0U, 0U);\r\n    }\r\n}\r\n```\r\n\r\n报警状态改变时，代码同步驱动报警 LED，并把事件交给后台 I\/O 线程处理：\r\n\r\n```c\r\nstatic void person_alarm_set(uint8_t active, uint8_t count,\r\n                             uint8_t confidence)\r\n{\r\n    if (active == g_person_alarm_active) return;\r\n    g_person_alarm_active = active;\r\n    rt_pin_write(PERSON_ALARM_LED,\r\n                 active ? PERSON_ALARM_LED_ON : PERSON_ALARM_LED_OFF);\r\n    person_alarm_io_notify(active, count, confidence);\r\n}\r\n```\r\n\r\n### 13.5 报警事件、日志与自动抓拍\r\n\r\n文件 `applications\/person_alarm_io.c` 的默认配置已经启用自动抓拍，因此正常检测流程不需要额外执行保存命令：\r\n\r\n```c\r\nstatic void alarm_config_defaults(void)\r\n{\r\n    rt_memset(&g_config, 0, sizeof(g_config));\r\n    g_config.threshold = 60U;\r\n    g_config.people_threshold = 1U;\r\n    g_config.snapshot = 1U;\r\n}\r\n```\r\n\r\n进入报警状态时生成新的事件编号并设置抓拍请求，解除报警时沿用同一事件编号并记录持续时间：\r\n\r\n```c\r\nevent.id = (active != 0U) ? ++g_event_id : g_event_id;\r\nevent.active = active ? 1U : 0U;\r\nevent.count = person_count;\r\nevent.confidence = confidence_percent;\r\nevent.snapshot = (active != 0U && g_config.snapshot != 0U) ? 1U : 0U;\r\nif (event.snapshot != 0U) g_snapshot_requested = 1U;\r\nrt_mq_send(g_alarm_queue, &event, sizeof(event));\r\n```\r\n\r\n后续视频帧被复制为独立抓拍缓冲，后台线程将报警记录写入 CSV，将 YUYV 数据转换成 24 位 BMP，同时处理声音和网络上报：\r\n\r\n```c\r\nvoid person_alarm_io_capture(const struct usbh_videoframe *frame)\r\n{\r\n    if (!g_snapshot_requested || frame == RT_NULL ||\r\n        frame->frame_size < ALARM_SNAPSHOT_MAX)\r\n        return;\r\n\r\n    uint8_t *copy = rt_malloc(ALARM_SNAPSHOT_MAX);\r\n    if (copy == RT_NULL) return;\r\n    rt_memcpy(copy, frame->frame_buf, ALARM_SNAPSHOT_MAX);\r\n\r\n    rt_mutex_take(&g_snapshot_lock, RT_WAITING_FOREVER);\r\n    g_snapshot_buf = copy;\r\n    g_snapshot_size = ALARM_SNAPSHOT_MAX;\r\n    g_snapshot_requested = 0U;\r\n    rt_mutex_release(&g_snapshot_lock);\r\n}\r\n\r\nstatic void person_alarm_io_thread(void *parameter)\r\n{\r\n    struct alarm_event event;\r\n    while (1) {\r\n        if (rt_mq_recv(g_alarm_queue, &event, sizeof(event),\r\n                       RT_WAITING_FOREVER) != sizeof(event))\r\n            continue;\r\n\r\n        alarm_log_event(&event);                    \/* person_alarm.csv *\/\r\n        if (event.snapshot != 0U)\r\n            alarm_save_snapshot(&event);            \/* alarm_XXXXXX.bmp *\/\r\n        if (!alarm_send_udp(&event) && !alarm_send_http(&event))\r\n            alarm_offline_append(&event);\r\n        if (event.active != 0U)\r\n            rt_mq_send(g_audio_queue, &event, sizeof(event));\r\n    }\r\n}\r\n```","updatetime":"2026-10-29 21:25:20","author":"xxx_234"}}