硬件约束记录
apps/iot 在真实硬件上摸出来的约束。AI 产出,未经人类审阅,见上级说明。
每条判断标注依据:[测试] 有对应单元测试,[实测] 有硬件测量并附复现方式,[据称] 未复现、仅留痕。
还没有电源管理,所以还没有时钟源锁——但顺序不能反
[据称](未实现,仅留痕)ESP-IDF 的 esp_pm 用三种锁表达组件对功耗的需求:ESP_PM_CPU_FREQ_MAX、ESP_PM_APB_FREQ_MAX、ESP_PM_NO_LIGHT_SLEEP。锁是递归的,持有几次就得释放几次。其文档给出两条本板将来会撞上的事实:
- Light-sleep 期间外设被时钟门控,且不会产生中断。 因此任何依赖外设中断的驱动必须持
NO_LIGHT_SLEEP。本板的 DVP 采集链与 I2S 采集/放音链都靠 DMA 完成传输,落在这一条里。 REF_TICK/XTAL/RC_FAST(即 RTC_8M)等外设时钟源不受 APB 降频影响,所以降频不等于全停,某些时钟能活过 APB 变化。
当前固件没有任何 PM 配置——没有 light-sleep、没有 esp_pm_configure、没有 WFI。喂数任务已在独立的 InterruptExecutor 上(lib.rs 的 SpeakerRunner::OnInterrupt),所以执行器争用这条路已经被结构解决,不需要锁。
因此没有实现锁是对的:加两把没有持有者的锁就是死代码,而死锁比死代码更难查。
但顺序不能反。要加 light-sleep,必须先有引用计数的锁集合,并让至少这些持有:
| 持有者 | 需要的原因 |
|---|---|
| 播放喂数 | 它的存在本身就是时钟来源;停它等于停掉 codec 的主时钟 |
| 采集链 | DMA 完成即释放环,DMA 被门控则环永远不排空 |
| DVP 采集链 | 同上,且其链的修复轮询住在 render 侧 |
InputSource::set_plane_collecting 已经是这个形状(平面关闭时不能靠下一次 poll 得知,必须有人显式说),所以接线位置已经对了,缺的只是锁本身。
Zephyr 的做法也印证这个分工:它的 device runtime PM 明确不管理设备依赖,「devices can manually get or put their dependencies」——依赖关系由消费者自己管,不是自动的。
共用一根 MCLK
[实测] 采集与放音两个 codec 挂同一组 MCLK / BCLK / LRCK。若各自用独立时钟声明,可以相差几 Hz 而没有任何东西会报告:面板会把采集计量对,喇叭却略微偏高——听起来像硬件故障,而它不是。
[实测] 两个故障各自都很安静、且像对方:喇叭吃到错位时钟是爆音,麦克风被错位时钟采样则返回恒定满刻度,看起来像"房间很吵"。
[实测] 哪个单元驱动那对引脚,两个方向都试过,两个都坏:
| 谁驱动引脚 | 发送侧听到的 | 采集侧看到什么 |
|---|---|---|
| 采集单元 | 持续爆音(发送单元在一个 ES8311 并未被其时钟的除数上移出采样) | 正常 |
| 发送单元 | 正常 | 恒定满刻度读数 + 一条死平的波形(在采集一个并非给 ES7210 定时的那只除数) |
两个单元跑在各自的时钟域里,谁都不能独自拥有时钟。所以发送单元驱动引脚、采集被强制为从属,方向不可交换。
代码里的对应约束(shared_tdm_config):with_signal_loopback 让接收单元从属跟随,其名是唯一误导之处——它不跨单元传样本,只共享时钟。
audio::tdm_config 与 audio_out::shared_tdm_config 是有意保留的两份拷贝,因为两个 feature 相互独立(只有喇叭的板、只有麦克风的板都是真实存在的),不能让任何一半拥有那个共同答案。采集侧那份必须关掉从属——没有发送单元就没有主。
麦克风前端的 DC 阻断写序
[测试] ES7210 每对输入的 DC 阻断滤波器占两个只差 bit 5 的寄存器(0x0A ^ 0x2A == 0x20),因此拐角是两者唯一共享的字段。有 MockI2c 的写序 trace 测试守着这次写序。
寄存器按它们携带的位命名,而非按级次顺序:没有公开文档支持级次命名——datasheet 是 "Everest Semiconductor Confidential" 并把位定义指向一份不公开的 guide,两个驱动家族连名字都不一致(esp-bsp 称 0x22 为 HPF2,esp-audio-dev 称 HPF1)。
[实测] 交换这一对不是外观问题:会抬高静音底噪。所以写序跟着参考驱动走,而不是跟着一个无文档支持的级次顺序。
0 dBFS = 102 dB SPL
[据称] 2026-09-28 以 30 cm 对着语音标定:底噪 66 dB SPL,峰值到 80。本次未复现此标定。
推导链(代码注释里有完整版):ES7210 满量程为 AVDD/3.3 Vrms,故在 ANALOG_POWER_RUN 下是 1.0 Vrms 而非"headroom"暗示的 2 Vrms;ZTS6216 的 −38 dBV/Pa 即 12.59 mV/Pa,经 GAIN_30DB 到转换器是 397.8 mV/Pa——于是满量程 2.51 Pa,对 20 µPa 参考压力即 101.98 dB。
重新标定的方法(换胶囊或换增益档时):设 CAL_SPL_LOG,在同一距离说话。
这些数字留在代码注释里是因为它们决定读数;要改数值必须同时改这里和代码。
核心侧如何消费这个偏移(为什么 spl() 把它当参数收)见音频实现记录。
QMI8658A 的 tap engine 不可用
[据称] 该器件的轻点引擎在使能瞬间锁住一个不释放的 tap 位和冻结的 TAP_NUM,无法分辨真实敲击,因此只 arm No-Motion,敲击判定改由 core 侧的识别器承担(recognizer.rs,[测试] 7 个用例覆盖敲击/转身/一次敲击计数)。本次未复现该现象。
完整的判定规则、门限来源与被否方案见运动实现记录。
主任务栈容量
[实测]栈的三个数字都已随摄像头进产品而变,以下是当前值(vanling,moon run iot:build-s3后读 ELF 符号):_stack_end = 0x3fcd529c (was 0x3fcd154c) _stack_start_cpu0 = 0x3fce9710 → 83 060 B = 81.1 KB (was 98 756 B)面板帧缓冲从堆上那份
Vec变成了板层的静态 arena(与相机 ring 共用,见camera.md),HEAP_MEM因此从 204,800 降到 65,536;净效果是.bss增加 14,336 B、主栈减少约 15 KB。FRAME_ARENA实测落在0x3fcaf900,% 64 == 0。
[实测] esp-hal 自带的 stack.x 把主任务栈顶设在 dram_seg 末尾,dram2_seg 那块无人认领,crates/app/linker/esp32s3-main-stack.x 把它接上,栈因此跨两块内存连续。
[实测] 从 ELF 符号读出的真实几何:
_stack_end = 0x3fcd154c
_stack_start_cpu0 = 0x3fce9710 → 98 756 B = 96.4 KB
[实测]这两个值是摄像头进产品之前测的,见本节开头。_stack_start_cpu0此前还记为0x3fced710/ 115 140 B,少算了 ROM 的两个 16 KB 引导栈:esp32s3-main-stack.x把栈顶设在dram2_seg上沿减ROM_BOOT_STACKS = 0x4000(0x3fced710 - 0x4000 = 0x3fce9710);ROM 的 PRO/APP 引导栈不能被主栈顶到。同一个漏算让camera.md一度得出"ring 进vanling必然 region overflow"的错误结论,那一节已注明。
为什么需要:绘制一屏渲染时 Diagnostics 快照与两个 [u16; ENVELOPE_COLUMNS] 列数组按值在栈上,Audio 页曾在这个栈上崩溃(异常的 A1 落在 _stack_end 之下)。
[实测] 当前占用 36 453 B(43.9%),余量约 46.6 KB(Ambient 页实测,vanling 烧录后 47 秒日志内的最深值)。固件自带水位测量([DISPLAY] stack peak),每重绘窗口采样一次,取最深值——不是估算。Camera 页的水位此前一直读 0 B:那条路径不经过任何采样点,是没测而不是水位低,现已补上。
一个未解的疑问:这个峰值从开机起几乎不变,多页切换也没有推高它。而历史崩溃点是 Audio 页的整帧 stamp。峰值到底属于哪条路径尚未定位;若它其实属于渲染,说明渲染比代码注释描述的还重,值得继续查。
esp-hal 的栈溢出 watchpoint 会误触发
[实测] esp-hal / esp-rtos 在硬件 STORE watchpoint 上做栈溢出保护,而被 arm 的窗口会压到合法写入,抛出 level-6 Debug 异常把 boot 干掉(曾在 esp32s3 的渲染 on_appearance 上观察到)。
因此 .cargo/config.toml 关闭硬件检测、开启软件 canary:
ESP_RTOS_CONFIG_HW_TASK_OVERFLOW_DETECTION = "false"
ESP_HAL_CONFIG_STACK_GUARD_MONITORING = "false"
ESP_RTOS_CONFIG_SW_TASK_OVERFLOW_DETECTION = "true"软件 canary 在每次上下文切换时检查栈底以下的哨兵,并以出错的任务名 panic——比静默破坏内存好。
面板的字模:Spleen 5×8,一套字([测试],2026-10-22)
字模在 apps/iot/crates/ui/src/font.rs:95 个可打印 ASCII,5 列 × 8 行、列优先、bit 0 是顶行;节距仍是 9×13(FONT_W 5 + OVERLAY_GAP 4、FONT_H 8 + OVERLAY_ROW_GAP 5)。
- 第一次换是因为开口:Adafruit
glcdfont的a/e/g/p/r/t/y在 5×7 里只剩 1 px 的开口,网络名和口令读不快。m5x7(Daniel Linssen,CC0)在同样的 5 列里画出 2 px 开口 —— 但它的m/M/w/W是 7 列、0与O同形、\缺字,这五处只能沿用 Adafruit,一句带m/w的话就混了两套笔画,上板后比换之前更难看(2026-10-22 的使用反馈)。 - 现在是 Spleen 5×8(Frederic Cambus,BSD-2-Clause):5×8 版本只含可打印 ASCII,字形原生就是 5 列 8 行,
m/M/w/W、0、\全在同一套里;不用拼字,也没有被裁掉的降部行。 - 只加一行:
FONT_H7→8、OVERLAY_ROW_GAP6→5,ROW_PITCH仍是 13。FPS 徽标232 + 8 = 240刚好填满页脚,音频页WAVE_TOP 42刚好落在第二行字下方 —— 两处都是编译期等号断言,改字号时先断在这里。 - 回归:
iot:test钉「非空格有墨」「形近字不同形」(l/1、I/l、0/O、c/e、u/v、g/q、p/q、y/v、m/w、M/W)与「降部到得了底行」(g/p/q/y/j)。换表时0/O真的同形,是第二条测试先报的。
字模在 flash:[u8; 95 × 5] = 475 B,与旧表同尺寸;静态墙前后都是 65,720 B(下限 65,536 B,余量 184 B)。