IoT 实现记录
Vanling 自有 ESP32 固件(apps/iot)的实现判断记录。AI 产出,未经人类审阅,见上级说明。
对应的系统说明在开发文档 / IoT 固件。
发现速查
下面这张表是回答「某个结论是什么、凭多少数字」的地方。绝大多数问题到这一层就结束,不必读全文。 依据标注沿用各记录正文的约定:结论 + 仍然生效的数字 + 被否方案及原因 + 未决项。
摄像头(GC2145 + ST7789)
| 发现 | 结论 | 数字 |
|---|---|---|
| 视野怎么算 | 输出宽 ÷ 阵列宽,与寄存器取值无关。ratio 5 整除,320×240 就是满视野 | 320/1600 = 1/5 |
| bins 与帧率 | 同一个开关的两面。本仓库写 0x00 | 0x00 4.5 fps / 691 KB/s;0x01 10 fps / 1558 KB/s |
| ratio 档位 | ratio 5 是唯一读满整个阵列的档;1 不出帧,3/4 欠采样 | ratio 3 = 3444 KB/s,ratio 1 = 0 帧 |
标量模式 0xfd | 帧率翻倍但视野更窄,本驱动不写(留上电表 0x00)但读回 | 5 → 10 fps |
| 环形描述符链 | 已撤回,保留线性链。外设不走完第一圈就停 | ring 28 KB/s vs 线性 645 KB/s |
| 线性链丢字节 | ring 必须整帧大小,代价是重建降到每秒 4 次 | 丢 1 字节后每行错开一行(移动条带) |
| PSRAM ring | 永远写乱。esp-hal 无法表达 internal SRAM only,只能停在官方拒绝的配置上 | 150 KB ring |
| 帧率上限 | 来自数据量,不是 AEC 曝光。想更快只能缩窗口或换芯片 | 5.2 fps;exposure 486 lines vs 读出 ~1200 |
| 像素时钟 | 固定 24 MHz。改 20 MHz 时 PLL 不锁,输出结构对、全是噪声且寄存器读回一切正常 | — |
| 反复进出 Camera 页 | 链每次重建且建起来就堵。render 侧 8% → 29–36% | 启动直入 11 repaints/窗口;反复进出 25–29 |
| 进页空窗 | 约 192 ms 无法缩短,用过渡帧 CAMERA WAIT 缓解 | 5.2 fps ⇒ ~192 ms |
| 面板读回 | 面板没有 MISO,寄存器读回全是 0x00,只能目视验证 | — |
| 帧缓冲持有者 | FrameOwner 三变体,第四种组合不可表示(曾用两个标志导致永久卡死) | 153,600 B |
音频(ES7210 + ZTS6216)
| 发现 | 结论 | 数字 |
|---|---|---|
| 采集平面可否节流 | 不可。环只有 sample() 排空,被批到 30 s 就是环撞满 + I2S 停机 | ring 60 ms 撞满 |
Off 的实现 | 必须在消费端主动 park 链,不能只是不 poll | 需求生效后 1 ms 停链 |
| 静音判定 | 必须读作「没变化」,否则整条 diff 链空转 | — |
| 平面边际代价 | 随别的任务醒着而变,不是常数 | 12.52%(独自跑)→ 15.73%(有旁侧) |
| 节流的效果 | 不省设备总量,只是把执行器从一处搬到另一处 | 94.8% → 94.0% |
| poll 侧缩放 | 按实际节奏缩放,两端都与实测对上 | +0.6% / −4.1%(缩放前高 40%) |
| 量化噪声 | 会喂出 65 dBA 的假底噪,dBA 表需对原始表换算 | 65 dBA |
| SPL 标定 | 0 dBFS = 102 dB SPL 是这一颗麦克风的值(397.8 mV/Pa → 满量程 2.51 Pa) | 101.98 dB |
运动(QMI8658A)
| 发现 | 结论 | 数字 |
|---|---|---|
| tap engine | 不可用(据称),敲击判定改在 core 侧;QMI8658_CAPABILITIES 不声明 TAP | — |
| 敲击门限 | 用原始幅值,不减重力估计 | 静止摆动 8–22 mG;门限 150 mG / 峰值 250 mG |
| aLPF 滤波器 | 实测无关,关掉。敲击是宽带瞬态,滤波只会削峰 | 静止噪声与敲击读数完全一致 |
| No-Motion | 是电平不是事件,只发布 0→1 上升沿,否则静止时按帧率持续报 Still | — |
| 摇晃判据 | 比较矢量残差并数过零次数,不是门限之上的时间 | 2.0 g;W3C 的 2.5 g/60 Hz 单轴幅值不适用 |
输入与触摸
| 发现 | 结论 | 数字 |
|---|---|---|
| 页面循环 | 硬件上正确。[DISPLAY] 的 2 秒窗口看不出页序,据此判断过一次「页面在乱跳」是错的 | 46 次跃迁,偏离 0、重复 0 |
| 四路轮询 | 只有 capture 可关;交互路径按设计不可关 | 稳定形态 button 0 / touch 1 / motion 3 / capture 16–20 ms |
| 交互地板 | 与需求无关的理论下限,不是调优空间 | 10.87% |
回声消除
| 发现 | 结论 | 数字 |
|---|---|---|
| 有没有公开的验证录音 | **没有。**四份都试过:两份乐鑫录音、Microsoft AEC-Challenge(按设计如此——消费级音频栈可能在发送侧自带 AEC/NS)、ITU-T G.168(附件 D 有显式冲激响应表,但誊录无法验证) | 相关 0.137 / 0.743 / ≤0.137;Wiener 上界 −4.97…−0.42 dB(40 个 GUID 最好值仍为负) |
| ERLE 是那份数据集的指标吗 | **不是。**论文原文「we can not use ERLE and PESQ」 | ERLE 与主观质量 PCC 0.31 |
| ITU-T G.168 的标准路径 | **方向正确但誊录不可验证。**8 条 m_i(k) 显式冲激响应表,无需任何音频文件 | 抽取丢首列;读序有歧义;两种读法都违反标准自身「幅度响应 ≤ 0 dB」不变式,超出 1–11 dB |
| 标准自己怎么说 | G.168 Test 12 注记:真实声学测试向量「for further study」 | 标准机构也确认它们不存在 |
| 验收基准改为 | 同一条路径上的离线最优(当场解最小二乘)。算出来的,不是誊来的 | 与最优差距 0.1–1.9 dB(4 条路径) |
| 15 dB 那道闸 | 是对路径说的,不是对算法说的。尾长于滤波器时离线最优本身只有 14.4 dB | 任何 128 抽线性滤波器都到不了 15 |
| 生成的路径必须有直达反射 | 否则没有起点,延迟搜索锁不住,报的是噪声峰 | 无直达反射:12 抽路径报 15、4 抽报 22,ERLE 0.4 dB;补上后延迟全部精确命中,61 dB |
| 冻结拦得住持续损伤,拦不住起始 | 相干度计量器 ~512 样本时间常数,滤波器已跑完那几百个样本 | 冻结覆盖 98%;路径外能量 涨 3.7e6 倍;冻结后不再增长。未修复 |
| CPU:先量时钟 | 当时整块板跑在 80 MHz,不是 240。CpuClock 的默认臂就是 _80MHz,Config::default() 从来不设最高频率 | esp_hal::clock::cpu_clock() = 80,000,000 Hz |
| 测量曾用错过时钟 | 第一版按 240 MHz 换算出的「219 cycles/MAC」是错的。参照循环揭穿了它——一条平凡整数循环本该更快,却慢 193 倍 | 参照 FIR 12 cycles/MAC @80 MHz |
| 分块自适应:过了 | **每 4 个样本更新一次系数。**稳态 ERLE 逐位不变,收敛变慢 | L=16 9,680 µs ≤ 10 ms;L=128 70,945 → 34,911 µs |
| 频域 MDF(最佳实践) | 写完 512 点定点 FFT 先测它再谈架构,量完就删了(产品路径零引用)。变换一项就超预算 1.8 倍,分区和滤波一分钱没花 | 3,046 µs/次 = 预算的 182%;RAM 3,064 B → 6 KB(省不了 RAM) |
| 第二核:数字逐位相同 | FROM_CPU_INTR1 是同一颗核的第二个中断源,不是第二核。产品全在 core 0,core 1 空着 | 两核 20 多项测量全部逐位相同 → 瓶颈是两个核共享的 |
| IRAM:也不是 | 蝶形放进 .rwtext(确认在 0x4037a4c4,非 flash) | 3,046 → 3,235 µs,反而慢 6%。取指不是瓶颈 |
| 四条循环 IPC 全在 0.1–0.33 | 取指否了、双核否了,剩下的只能是内存系统本身。最干净的一段代码(FFT 蝶形)IPC 最低 | 蝶形 0.09;参照 FIR 0.33 |
| 延迟搜索能单独撑爆 30 ms 的界 | 一次搜索约 32 ms,每 256 ms 一次,摊到每 tick 3.0 ms(占 L=16 的 31%)。栅格抽稀已否 | 宽带参考下相关峰只有一个样本宽——延迟 1 时相邻的 lag 0 和 4 都相关不到(146 vs 1000) |
| 时钟 240 MHz,控制台没坏 | esp-hal 那条「换 PLL 会影响 USB Serial/JTAG」是 [据称]。实测 240 MHz 跑通、控制台全程正常 | L=16 3,248 µs;L=128 11,639 µs;最坏 tick 21.6 ms,在 30 ms 界内 |
| cycles 一个都没变 | 时钟纯粹是除数:cycles/蝶形 112 → 112,参照 FIR 12 → 12 | @240 MHz 时间全部 ÷3 |
| 数据缓存走不通 | ESPHome/ESP-SR 的 AEC 要 DATA_CACHE_64KB + LINE_64B,没有就拒绝启动。esp-hal 无 API、esp-metadata 无 eFuse 字段 | [据称] 本板有没有,未读出 |
| IPC 谜题不是缓存 | 按读写排,不按工作集:只读 12 / 31.6,读+写 105.8 / 119。工作集最小的不是最快的 | write-back 缓存 + 每抽头回写同一位置 |
| 但翻出一个大得多的杠杆 | 整块板跑在上限的三分之一,现有的 17–25 ms 最坏 tick 同样被压在这里。esp-hal 注释称换 PLL 会影响 USB Serial/JTAG(本仓库控制台正是它) | @240 MHz:AEC L=32 → 7.6 ms(塞得下);最坏 tick → 5.7–8.3 ms |
| 这个 target 的 FPU 是关的 | 同形状 f32 循环 >120 s vs i32 孪生 140 ms,二进制带全套 libgcc 软浮点助手 | 浮点走软件模拟,「换浮点」不是捷径 |
| 代价 | 3,040 B RAM(板上) | state_bytes::<128>() = size_of::<Canceller<128>>();旧函数只数滤波器,低报 3.4× |
硬件约束
| 发现 | 结论 | 数字 |
|---|---|---|
| 电源管理 | 还没有 PM,所以没有时钟源锁是对的(加两把无持有者的锁就是死代码)。但顺序不能反 | — |
| light-sleep | 外设被时钟门控且不产生中断,依赖外设中断的驱动必须持 NO_LIGHT_SLEEP | DVP 与 I2S 链都在此列 |
| 共用 MCLK | 两个单元跑在各自时钟域,发送单元驱动引脚、采集被强制从属,方向不可交换 | with_signal_loopback 名字是唯一误导处 |
| 主任务栈 | 随摄像头进产品而变,以链接 ELF 符号为准 | _stack_start_cpu0 = 83,060 B |
| esp-hal 栈溢出 watchpoint | 会误触发,不能当溢出判据 | — |
| 数据缓存默认 64 KiB ⇒ reclaimed 区为空 | esp-hal 把 0x3FCF0000 以上「留给堆」,但要从数据缓存那一头扣。默认配置下 #[ram(reclaimed)] 无处可放 | 本仓既有 ELF 里 0x3FCF0000 以上零符号;压到 32 KiB 换回 32 KiB |
网络(Wi-Fi)
| 发现 | 结论 | 数字 |
|---|---|---|
| 本世代有无 TCP/IP 栈 | 没有 lwIP/esp-netif(crates.io 404 + 仓库无目录 + CHANGELOG 零命中)。替代物是 esp-radio(L2)+ embassy-net(smoltcp) | esp-radio 自带 embassy_net_driver::Driver 实现,接上就能拿 IP |
| 「连上」的定义 | 拿到地址才算。参考固件的 STA_CONNECTED 处理函数是空的,连接位在 GOT_IP 里置 | 关联 ≠ 连上 |
| 三个阶段的预算是三份 | 扫描归射频、关联归 AP 与重试、DHCP 归租约,合并成「连接中」就看不出哪一段慢 | 探针分开计时 |
| 连接默认值的坑 | 快速扫描取第一个匹配 ⇒ 强的同名 AP 一次认证失败就被换成弱的 | 参考固件实测:−73 dBm 在范围内,却连上 −79 dBm |
| DRAM 预算是板上最硬的一条 | dram_seg 只能再拿 12,344 B(主栈断言 65,536 B 之下只剩这些),装不下驱动缓冲 | 主栈 77,880 B;Wi-Fi 堆 8 KiB + 32 KiB reclaimed |
PHY 标定恢复在 S3 上是 todo!() | PROVIDE(misc_nvs_restore = __esp_radio_misc_restore) 把它绑到 #[cfg(xtensa)] 的 todo!(),而 blob 对它是未定义引用 | 规避手段:分区表不写 phy_init(代价:每次开机重标定) |
| 分区表 | espflash 默认 factory 1 MB,而固件加 Wi-Fi 前已 630 KB | factory 6 MB;16 MB 板 |
日志
| 发现 | 结论 | 数字 |
|---|---|---|
| 默认级别 | info 只留事件,稳态可以长时间零行,静默不是死机 | 169 行/43.8 s → 35 行/80 s;最后一行在第 6.2 s |
| 一个旋钮 | IOT_LOG_LEVEL,认不出的名字退回 info 而不是静音;级别只由 log::max_level() 持有 | 开机首行 [IOT] log level … 是唯一凭据 |
| 周期遥测 | 帧成本、栈水位、feed 节奏、链路心跳、扫描列表全在 debug;越界的那一次升 warn | 同源码 debug 构建 40 s 内 102 条 DEBUG |
| 删行买到什么 | 不是 CPU,是读的人的时间:USB CDC 的串口写不进执行器持有账 | 1426 → 1417 ms / 5 s 窗口 |
| 标签 | 一个模块一个标签,不是一种外设一个 | [MIC]→[AUDIO]、[SPEAKER]→[PLAY]、[TOUCH]→[INPUT]、[RADIO]→[WIFI]、[PSRAM]→[HEAP]、[BACKLIGHT]→[DISPLAY] |
探针
每个探针是独立 bin,把框架里的一层单独接到真硬件上。改驱动之前先看这里——「哪个问题跑哪个探针」在这一层就该结束。
它们不是产品的另一种形态:产品固件产生不出探针能产生的那些输入。整机五页四平面全是 Continuous,所以产品上永远拿不到 (配置 × 节奏) 矩阵里除默认格以外的任何一格。
| 探针 | 回答什么 | 报什么数 |
|---|---|---|
demand-probe | 运动平面每格配置 × 节奏,各值多少 | 每格先 settle 再测:总线事务/秒、执行器占用墙钟占比、测量的 poll 数(供前两者互相校验) |
camera-probe | 相机取到帧、面板逐像素写出,画面有没有被替换 | 帧字节数与校验;「画面看着不对」不是测量 |
audio-probe | 放音链路独占运行时的稳态 | 交替两个音效的 stall 与缺口 |
multicore-s3 | 启动第二核能否在主栈底下存活 | 栈顶是否被启动序例覆写(ThreadFlag 就住在那里) |
aec-probe | 线性回声消除跑一个音频 tick 要多少时间,L = 16/32/64/128 | 首/均/最坏 tick、占 20 ms 比例、延迟搜索/滤波/计量器分开计时 |
clock-probe | 运行期切 CPU 频率要多少、切完谁还准、切完核 1 怎么样 | 三档同一工作量的耗时比(不看 cpu_clock(),它读配置不读硬件)、切换墙钟、跨切换的 Instant 差、两核同量对比 |
wifi-probe | 射频与 IP 栈能不能立起来,这块板的 DRAM 预算够不够 | 扫描/关联/DHCP 三个分开的耗时、扫描到的条数、驱动堆每 2 秒的余量、开机标定代价 |
moon run iot:probe-demand-s3 && moon run iot:flash-probe-demand-s3
probe-*-s3 只构建,flash-probe-*-s3 才烧录;端口在 moon.yml 的命令里。wifi-probe 两个芯片都有(-s3 / -c6):同一份源码,C6 那条用普通复位而不是 -a watchdog-reset(原因见 @/development/iot/flashing.md)。
wifi-probe 还多一步:它的凭据是构建期输入,所以构建命令上带两个变量
IOT_WIFI_SSID=home IOT_WIFI_PSK=secret moon run iot:probe-wifi-s3
IOT_PORT=/dev/cu.usbmodem1422301 moon run iot:flash-probe-wifi-s3
# C6:同一份源码,普通复位启动
IOT_WIFI_SSID=home IOT_WIFI_PSK=secret moon run iot:probe-wifi-c6
IOT_PORT=/dev/cu.usbmodem14401 moon run iot:flash-probe-wifi-c6
不设这两个变量它照样能跑,只是会说「这个构建没有让我连任何东西」——那是「扫描本身通不通」的答案,而不会假装自己是一次连接测试。两个名字在 scripts/iot-xtensa.sh 的 FORWARDED_ENV 里,宿主测试 build_env.rs 会在「读了却没转发」时失败构建。
五个 bin 每次 check-s3 都编一遍,只编不链。 没有编译它们的探针会烂在原地:每个都自己调一遍 bring-up 或芯片 API,Board、Speaker、芯片层一改就会编译失败,而没人会注意到,直到某天在台架上要用。
探针与产品的分界线会无声地被挪动:两边各自都还能编译,所以编译期抓不到漂移。改动若触及探针调用的框架路径(poll_plan_of、MotionConfig::for_mode、驱动入口),把探针烧一次当作证据,不要只等编译通过。
检查任务
moon 按内容哈希缓存。不写 inputs 时默认把项目下的每个文件都算进去;写了 inputs 是替换而不是叠加。所以经仓库根脚本编译的任务(check-s3 / check-features / probe-*-s3)必须显式点名那个脚本,否则改分派脚本不会让缓存失效。详见检查任务与缓存记录。
| 发现 | 结论 | 数字 |
|---|---|---|
| 缓存键里有什么 | 任务文本 + 项目级 env: + 每个输入文件一条 sha1;默认输入是整项目 | check-host 的默认键 121 条 |
| 脚本在键外 | 经 scripts/iot-xtensa.sh 编译的任务要显式点名它,否则改脚本不失效 | 追加一行注释:键 0973f854 → 577401ca;还原后命中 |
check-features 曾经每次真跑 | 改前是 cache: false。现在同树重跑 0.09 s,改源码仍付全价 | 2m20s570ms(八套 S3 子集各 ~15 s) |
| C6 产品检查 | 改宿主 stable:该目标有预编译 core/alloc,固件本来也是宿主产的 | 1m12s → 1.65 s |
| 8 次容器合成 1 次 | 被否:容器启动实测 0.45 s,最多省 3.5 s,不值得加批量协议 | 3 × 0.45 s(5.4 s 是冷启读数) |
| 仍走分派脚本的守卫 | check-features 的 C6 子集特意保留:它是 S3-only ESP_HAL_CONFIG_DATA_CACHE_SIZE gating 在 CI 里唯一的见证 | — |
各记录正文
需要推导过程、被否方案全文或复现方式时再下钻。
| 章节 | 内容 |
|---|---|
| 放音实现记录 | Cp0Disabled 调查、被否方案、feed 节拍归属、ISR 拆分、音效配方 |
| 硬件约束记录 | 共享 MCLK、时钟源锁与 PM 的先后顺序、麦克风前端写序与标定、栈容量由来 |
| 摄像头实现记录 | GC2145 视野算术、bins 与标量实测、PSRAM 不可用由来、面板接线、反复进出导致链重建后停滞 |
| 音频实现记录 | 静音判定、执行器边际代价实测、采集链只有一处能停、标定走查受需求门控、积压深度、dBA 与原始列、量化噪声底噪 |
| Wi-Fi 实现记录 | 本世代没有 lwIP 只有 esp-radio+embassy-net、「连上」= 拿到地址、连接默认值抄自参考固件的三条、DRAM 预算怎么凑出来的、PHY 标定 todo!() 与分区表的规避手段 |
| 运动实现记录 | 敲击判定的被否方案、门限来源、No-Motion 是电平不是事件 |
| 输入与触摸实现记录 | 页面循环硬件实测、2 秒窗口看不出页序、四路轮询里只有一路可关 |
| 回声消除实现记录 | 乐鑫向量为何测不了 AEC、步长不能按残余缩放、相干度冻结判据、延迟搜索的置信度与限频、ERLE 稳态与全程的差 |
| 运行期改 CPU 频率记录 | esp-hal 的 Frozen Clock Frequencies、越界写寄存器实测比值 1 : 2.00 : 2.95、切频 1–50 µs、第二个核与外围无差异、表跟着芯片走、挡路的既有缺陷 |
| 检查任务与缓存记录 | moon 缓存键的组成与脚本盲点、check-features 开缓存、C6 检查改宿主、被否的容器合并 |
| 日志实现记录 | 默认 info 的实测依据、IOT_LOG_LEVEL 单点持有、事件/遥测/越界三档判据、标签统一、post-mortem 缺口 |
节流的完整推导、实测对照与方法在架构文档,不在这份记录里。