Rdzleo fc07d3806d Phase 01 调试迭代: OV3660 人脸检测集成 + 23 条踩坑经验汇总
## 代码变更

### main/application.cc
修复 T01 probe 日志 %lld 格式 bug(改用 %lu + unsigned long)

### main/boards/common/esp32_camera.cc
- 修复 DVP V4L2 单 buffer 导致 DMA 饥饿:req.count 从 1 改为 2
- 修复 [T01] Probe 日志 elapsed=ldus 显示问题(同上格式修复)

### main/face_tracker.cc
多轮迭代:
- 新增 frame debug 诊断日志(打印 top-left/center 16B + zero_bytes 统计)
- pix_type 尝试路径:YUYV → RGB565LE → RGB565BE → YUYV → RGB888(手动转换)
- 手动实现 BT.601 公式 YUYV→RGB888 转换,绕过 ImagePreprocessor 黑盒
- face_tracker 任务从 Core 0 切换到 Core 1,避让 RMT/LED 死锁
- 新增 INFO 级限频日志(每秒 1 条 face 检测记录)
- 修复推理时长日志 %lld 格式 bug
- 连续 3 秒无人脸时打印 no face detected

### main/idf_component.yml
esp_video 升级 1.3.1 → ~1.4.1(手动 patch 修 xclk_freq bug)

### partitions/v2/16m.csv
OTA 分区扩容:3.94MB → 5MB,assets 缩到 5.875MB,支持 4.23MB 固件

### docs/phase-01-face-tracking/PROGRESS.md
更新 Phase 01 执行日志,记录实机调试细节

## 文档更新

### Coglet项目分析与开发指南.md 新增第六点五节

完整记录本轮调试的 23 个踩坑,分为:
1. 编译/配置类(5 个):板级重置、依赖冲突、bootloader 缓存、%lld 格式、xclk_freq bug
2. 摄像头数据链路(5 个):sensor driver 启用、V4L2 buffer 数量、分区扩容、镜头保护膜、光照
3. esp-dl 人脸检测(3 个):MSR letterbox 伪影、ESPDET OOD 默认输出、字节序判断
4. 任务调度(3 个):WDT 崩溃、GDMA ISR 崩溃、弱符号链接
5. RP2040 端(4 个):idle 回中、坐标累加撞限位、mpremote 阻塞、两分支代码差异
6. 硬件(3 个):飞线验证、360° 舵机误用、烧录生效验证

附调试方法论 6 条 + 未解决遗留问题 3 条

## 已解决问题

-  ESP-IDF 编译链路(依赖/分区/格式)
-  ESP32 + RP2040 端到端协议(face:x,y UART)
-  WDT 崩溃(face_tracker 切到 Core 1)
-  RP2040 眼球回中机制(idle 时回正)
-  V4L2 双 buffer(DMA 数据更新正常)

## 遗留问题(待解决)

-  face 检测 box 固定伪激活(无论 pix_type / 画面内容 / 模型选择)
-  GDMA ISR 每 ~30s 触发 InstrFetchProhibited 崩溃
- ⚠️ 端到端验收:眼球未真正跟随人脸

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:22:15 +08:00

12 KiB
Raw Permalink Blame History

Phase 01 执行进度追踪

由于本仓库非 git 仓库,用本文件替代 commit 作为原子进度追踪。 每完成一个任务追加一行;遇到偏差记录 [!] 条目。

任务状态表

  • [~] T01 摄像头硬件 V4L2 probe —— 代码完成,硬件验证待用户
  • T02 Kconfig 开关 + FPS choice
  • T03 esp-dl + human_face_detect 依赖
  • T04 Esp32Camera CaptureForDetection + 双超时 mutex
  • T05 face_tracker.{h,cc} 骨架 + CMake 条件编译
  • T06 集成 HumanFaceDetect 推理 + 坐标归一化(代码部分;实测待 T12
  • T07 uart_send_face + uart mutex
  • T08 RP2040 parse_face + static 去重
  • T09 RP2040 main.py incoming_commands 识别 face:
  • T10 RP2040 facetrack() 改造D-07 idle return
  • T11 application.cc 接入 face_tracker_start
  • T12 端到端联调
  • T13 性能调优
  • T14 关开关回归测试
  • T15 最终验收

执行日志

  • T01 代码部分完成2026-04-17

    • 新增 ProbeFrameCapture()main/boards/common/esp32_camera.{h,cc}
    • main/application.ccStart() 末尾插入 probe 调用(#ifndef CONFIG_IDF_TARGET_ESP32 守卫)
    • 硬件验证部分待用户接 USB 后在 T02/T03 通过后烧录验证
  • T02 完成2026-04-17

    • main/Kconfig.projbuild 的 Camera Configuration menu 末尾新增 XIAOZHI_ENABLE_FACE_TRACKING + FPS choice5/10/15
    • 采用 PLAN_CHECK NOTE-1 方案 Bdepends on IDF_TARGET_ESP32S3 只支持 S3与 CMake 排除逻辑对齐
  • [!] T03 偏差2026-04-17 — 依赖版本冲突 阻塞批次 1

    • 第一轮偏差PLAN 原定 esp-dl==3.2.0 + human_face_detect==0.4.1 不兼容 registry 数据显示 human_face_detect 0.4.1 实际依赖 esp-dl ~3.3.0
    • 自动修正为 esp-dl ~3.3.0
    • 第二轮偏差blockingesp-dl 3.3.0 要求 esp-dsp ==1.7.0 但项目已有 esp-sr ~2.2.0 要求 esp-dsp ==1.6.0,互斥
    • 此为真正的版本冲突,已停下汇报 orchestrator
  • T03 偏差已解决2026-04-17 —— 用户决策方案 A升级 esp-sr

    • idf_component.ymlesp-sr~2.2.0 升级为 ~2.3.1
    • esp-sr 2.3.x 已切换到 esp-dsp 1.7.0,与 esp-dl 3.3.0 兼容
    • idf.py reconfigure 通过esp-dl 3.3.x / esp-dsp 1.7.0 / esp-sr 2.3.1 / human_face_detect 0.4.1 全部就绪
    • 编译遇到 bootloader CMake 缓存不匹配(与 IDF 路径历史变更有关),已清理 build/bootloader* 目录后重新编译
  • T04 完成2026-04-17 - 修改文件: main/boards/common/esp32_camera.{h,cc}

    • esp32_camera.h: 新增公开结构体 FrameRefdata/len/width/height/format/buf_index
      • CaptureForDetection(FrameRef*) / ReleaseDetectionFrame(const FrameRef&) 声明
      • 私有成员 SemaphoreHandle_t capture_mutex_
    • esp32_camera.cc: 构造函数末尾 xSemaphoreCreateMutex(),析构函数 vSemaphoreDelete 实现 CaptureForDetection10ms timeout 拿不到锁即返回 false 跳帧,成功后不解锁) 实现 ReleaseDetectionFrameVIDIOC_QBUF 归还 + 释放 mutex Capture() 头部用栈上 RAII CaptureLockGuard 以 portMAX_DELAY 加锁,确保任何 return 路径都解锁
    • idf.py build 通过,固件 2.47MB / 剩余 1.47MB (37% free)
  • T05 完成2026-04-17 - 新增: main/face_tracker.{h,cc};修改: main/CMakeLists.txt

    • face_tracker.h: extern "C" 导出 3 个接口:face_tracker_start/stop/get_fps
    • face_tracker.cc: 三重保护
      1. Kconfig 层面(批次 1 已加 depends on IDF_TARGET_ESP32S3
      2. 代码层面 #if defined(CONFIG_XIAOZHI_ENABLE_FACE_TRACKING) && defined(CONFIG_IDF_TARGET_ESP32S3) 守卫
      3. 构建层面CMakeLists.txt if(NOT CONFIG_IDF_TARGET_ESP32S3) list(REMOVE_ITEM SOURCES "face_tracker.cc") 骨架任务 pin Core 0 / 优先级 2 / 栈 8KB每秒打印 hello from core 0
    • idf.py build 通过,固件 2.47MB / 剩余 1.47MB (face_tracker.cc.obj 已被编译链接)
  • [!] T06 偏差2026-04-17 - PLAN 中 T06 依赖 T07 的 uart_send_face 符号,但批次 2 未做 T07

    • 采取方案face_tracker.cc 中用 __attribute__((weak)) 前向声明 uart_send_face T07 完成后uart_component.cc 提供的 strong symbol 自动覆盖弱符号 调用处加 if (uart_send_face != nullptr) 判空(弱符号未定义时为 NULL
    • 此偏差属于"修复 T06 的前置依赖缺失",无需架构层面变更,已内联解决
  • T06 完成代码部分2026-04-17 - 修改: main/face_tracker.cc

    • 包含 human_face_detect.hpp / dl_image_define.hpp / dl_detect_define.hpp
    • 构造 HumanFaceDetect()(默认 model_type 由 CONFIG_DEFAULT_HUMAN_FACE_DETECT_MODEL 决定)
    • 任务主循环:vTaskDelayUntil(period) 按 Kconfig FPS → CaptureForDetection → 组装 img_t (YUYV) → detector->run(img) → ReleaseDetectionFrame → 坐标归一化
    • 坐标公式严格遵守 RESEARCH Pitfall 7cx * 224 / width - 112(匹配 RP2040 deadzone=20
    • PLAN 未定义多人脸排序,补充健壮性:遍历 list 挑 score 最高的 result避免多脸摇摆
    • 启动时打印 PSRAM after detector init 供 R2 OOM 风险追踪
    • 每 10 秒打印 face stats: hit/miss/fps
    • idf.py build 通过,固件 2.50MB / 剩余 1.46MB (36% free) — 相比 T05 +30KB (esp-dl 推理库 + human_face_detect 模型注册表代码被链接)
    • 实测部分待 T12:需烧录后将人脸对准摄像头验证 score / infer 时长 / FPS 若 score < 0.5 则进入决策点 D-B改为 DL_IMAGE_PIX_TYPE_RGB565LE
  • T07 完成2026-04-17 - 修改: main/uart_component.{h,cc}

    • uart_component.h: 新增 uart_send_face(int,int) 声明,用 extern "C" 包裹 以保证 C 链接名(匹配 face_tracker.cc 的 extern "C" __attribute__((weak)) 前置声明) 其他函数保持原 C++ 修饰名不变,不影响 main.cc/display.cc 现有调用
    • uart_component.cc:
      • 新增 static SemaphoreHandle_t s_uart_tx_mutex,在 uart_init_component() 末尾创建
      • uart_send_string() 整体加 mutex 保护(防止与 uart_send_face 并发撕包)
      • uart_signal_start/stop 经由 uart_send_string 间接加锁,无需重复保护
      • 新增 extern "C" void uart_send_face(int,int)snprintf 到 24 字节栈缓冲, 加锁后 uart_write_bytes(buf,n) + uart_write_bytes("\r\n",2),与现有格式一致
    • [!] 小偏差Rule 2PLAN 示例中 header 未用 extern "C",但 face_tracker.cc 的弱符号 前置声明是 C 链接strong 实现必须也是 C 链接才能覆盖 weak加 extern "C" 包裹解决
    • idf.py build 通过,固件 0x280760 = 2.51MB / 剩余 36% (1.46MB),相比 T06 几乎持平 (仅 +数百字节,符合 PLAN T07 "< 1KB" 预期)
    • nm 验证libmain.auart_send_face 为 Tstrong 定义),face_tracker.cc.obj 中为 wweak 引用)。弱符号覆盖链生效。最终 ELF 暂时没这些符号是因为 T11 未做, application 未调用 face_tracker_start触发链接器 DCE 把整个 face_tracker 子图剔除。 T11 接入后会自动拉入 uart_send_face 的 strong 实现。
    • 未添加 test hookPLAN DoD 中提到的 uart_send_face(42,-30) 临时调用), 留给 T12 端到端联调时用真实 face_tracker 数据验证
  • T08 完成2026-04-20 - 修改文件: /Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/coms.py

    • Comms.__init__ 末尾新增三个成员:
      • self.last_face_offset = NoneESP32 人脸坐标最新值main.py 消费后清空)
      • self.last_face_raw = Nonestatic 去重用的"上一次原始坐标"
      • self.FACE_STATIC_THRESHOLD = 3±3 像素阈值,经验值)
    • 类内新增 parse_face(line) 方法:
      • 兼容 bytes / str 输入(自动 decode + strip防御性处理
      • face: 前缀或解析失败返回 None不影响 staticflag
      • last_face_raw 对比:差异 ≤ 3 像素 → staticflag = True;否则 → staticflag = False 并更新 last_face_raw
      • 首次收到坐标视为"非静态"(确保眼球初始化时会响应)
    • 未新增 独立的 read_face() 方法PLAN T08 规范只要求 parse_face + last_face_offset,不要求单独的 UART 读取方法——ESP32 UART 数据仍由 esp_read() 统一读入 rx_buffermain.py 遍历 commands 时逐个调 parse_face(见 T09。如果单独再提供 read_face() 会导致 UART 缓冲区被两个方法争抢,造成行撕裂。
    • Python 语法检查:python3 -c "import ast; ast.parse(...)" 通过
  • T09 完成2026-04-20 - 修改文件: /Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/main.py

    • 主循环 for data in incoming_commands 分支改造:
      • 优先调 external.parse_face(data),成功则设 external.last_face_offsetanimation.grove_active = Trueanimation.grove_last_seen = time.ticks_ms()continue
      • 否则走原 action_map / state_map 分发(完全不影响现有 state 指令行为)
    • grove_active / grove_last_seen 在收到 face: 消息时立即 set True即时响应与 T10 的 facetrack() 兜底刷新配合,不产生状态污染
    • Python 语法检查通过
  • T10 完成2026-04-20 - 修改文件: /Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/main.py

    • facetrack() 按 PLAN T10 v1.1 完整重构,执行顺序:
      1. 始终读 Grovegrove_offset = external.grove_read()):即便无 Grove 硬件也要消费 UART 防止缓冲区溢出
      2. 读 ESP32 face 数据esp_offset = external.last_face_offset),消费后立即 last_face_offset = None
      3. 数据源优先级Grove 有效时用 Grove否则 fallback 到 ESP32
      4. D-07 修复if animation.current_state == "idle": returnidle 下只消费数据不驱动舵机
      5. 非 idle 分支:原有 eyl/eyr/pit/yaw 的 scale + set_target 逻辑一字不动
    • BLOCKER #2 修复staticflag 不再在 facetrack() 硬编码设置——完全由 T08 parse_face() / 原有 grove_read() 各自管理
    • grove_active 合并策略T09 在消息到达时 set True实时T10 兜底在 facetrack() 里根据本帧最终 offset 再次刷新并处理 3 秒超时。两边都 set 到"当前时间戳"或 set False语义一致避免 BLOCKER #2 所描述的"双处更新导致污染"问题——只要 T09 先 set 到 TrueT10 进入时 offset 非空也会刷到同一时间戳,不会发生回退。
    • Python 语法检查通过
  • [!] T10 偏差说明(轻度):

    • PLAN v1.1 在 T10 里写了"grove_active/grove_last_seen 不在本任务主动 set True",但 facetrack() 在 Grove 硬件活跃时仍需要把 Grove 自身的 offset 反映到 grove_active 时间戳——本次实现在 facetrack() 里保留了"if offset: grove_active=True"写法,原因是 T09 只在收到 ESP32 face: 消息时 set True不处理 Grove 来源。如果严格按 PLAN 字面删掉Grove 模式下 grove_active 永远为 False3 秒超时总是会触发,自动动画会不停抢夺 Grove 追踪权。
    • 处理方式T09 设 Grove_active 的职责是"ESP32 数据源"T10 保留的是"Grove 数据源"——两边是 OR 关系而非重复更新BLOCKER #2 所警告的是"两边都 set 到同一数据源",这里不是)。已在代码注释中记录。
    • 此偏差属于 Rule 2补完 PLAN 遗漏的 Grove 分支),不需要用户决策。
  • 批次 4 (T08/T09/T10) Python 语法检查结果:

    • coms.py OK
    • main.py OK
    • animation.py OK未改动做回归确认