Files
RemoteMapper-ESP32/USB_UAC_BUG_POSTMORTEM.md
cuicuiV5 57e6e30578 chore(savepoint): rollback to stable LED and UAC state
- Implemented LED indicator feature (24 brightness)

- Migrated UAC microphone to hardware xfer_cb (fixed endpoint collision)

- Rolled back ATVV strict 0x03 check to prevent infinite ping-pong loop

- Rolled back HOGP keyboard release to fix modifier sticky keys

- Rolled back Volume keys to TAP with repeat to preserve stability
2026-08-29 23:40:59 +08:00

4.3 KiB
Raw Permalink Blame History

ESP32-S3 USB UAC 1.0 Isochronous IN Boundary Crossing Bug & Postmortem

Date: 2026-08-29
Platform: ESP32-S3 (Synopsys DWC2 USB Controller)
Framework: Arduino-ESP32 (TinyUSB)

1. 现象描述 (The Symptoms)

在将小米蓝牙语音遥控器的 16kHz BLE 音频通过 ESP32-S3 转发为 USB UAC 1.0 麦克风时,出现了以下严重问题:

  • 音频变慢且伴随严重锯齿/电流声:音调(Pitch)正常,但播放速度慢了一倍,且声音断断续续。
  • Ring Buffer 溢出:BLE 稳定以 16000 samples/sec 的速度写入音频流,但 USB IN Endpoint 只能以约 8000 samples/sec 的速度拉取数据,导致内存缓冲区积压崩溃。

2. 根本原因:SOF 边界跨越 Bug (The Boundary Crossing Bug)

ESP32-S3 硬件使用的是 Synopsys DWC2 USB 控制器,在预编译的 Arduino TinyUSB 环境中,Start-Of-Frame (SOF) 中断默认被关闭 (CFG_TUD_SOF_EN=0)。 因此,我们无法在 1ms 物理微帧的绝对起始点精确排队 USB 传输,只能依赖 FreeRTOS 的 vTaskDelay 或 xfer_cb 完成回调来排队数据。

这引发了致命的 奇偶帧边界跨越 (Even/Odd Frame Boundary Crossing) 缺陷:

  1. Windows 主机每 1ms (1000Hz) 发送一个 IN Token 轮询音频数据。
  2. 当 FreeRTOS 任务因为系统调度延迟,或者在 xfer_cb 中排队数据时,物理时间往往刚好跨过了 1.00ms 的边界(比如在 1.05ms 排队)。
  3. TinyUSB 底层的 dcd_dwc2.c 读取当前物理帧号(N),由于已经迟到,它为了安全,强制将端点的硬件奇偶校验位(EONUM)设定为下一帧(N+1)。
  4. 结果:当前这一帧被直接跳过,数据被推迟到了 2ms 后发送。
  5. 最终表现:USB UAC 传输率从 1000Hz 物理降频到了 500Hz,导致 16kHz 的音频变成了 8kHz 的传输率,引发一半的丢包和极端的锯齿电流音。

3. 失败的尝试 (Failed Attempts)

在锁定最终解法前,我们经历了以下惨烈的尝试:

  1. FreeRTOS vTaskDelayUntil(1ms):FreeRTOS 时钟和电脑 USB 硬件晶振存在相位漂移,当任务醒来早于传输完成时,端点处于忙碌状态,任务跳过该回合,导致 500Hz。
  2. xfer_cb 硬件回调排队:TinyUSB 会在调用 xfer_cb 之后才清除 ep_busy 标志。如果在回调里调用 usbd_edpt_xfer,会直接被拒绝(死锁静音)。
  3. Semaphore (信号量) 严格同步:通过回调释放信号量唤醒任务。由于唤醒+上下文切换依然需要 50-100us,完美必定跨越 SOF 边界,依然触发 500Hz DWC2 Bug。
  4. 强写硬件寄存器 DIEPCTL (Hardware Override):试图在传输提交后强行覆盖寄存器中的 EONUM 奇偶位。导致 DMA 控制器状态机彻底崩溃,麦克风完全死锁静音。
  5. 尝试增加包大小 (wMaxPacketSize = 64 且 bInterval = 1):Windows UAC 驱动执行严格的频宽数学校验。对于 16kHz 单声道 16-bit 音频,1ms 只能是 32 bytes。声明 64 bytes 导致 Windows 拒绝识别设备 (Error 43)。

4. 终极圣杯方案:2ms 降频缓冲策略 (The Holy Grail)

既然 1ms (1000Hz) 在 DWC2+FreeRTOS 环境下必定触发跨界丢帧,我们选择了“降维打击”——合规地改变主机轮询频率。

我们修改了 UAC Isochronous IN 端点描述符:

  • wMaxPacketSize = 64 (32 个采样)
  • bInterval = 2 (要求 Windows 每 2 毫秒轮询一次)

为什么这是完美的?

  1. 满足严格的频宽校验:64 bytes / 2ms = 32000 bytes/sec。完美匹配 16kHz 单声道 16-bit 的要求,Windows UAC 驱动毫无怨言地接受,不再报 Error 43。
  2. 消灭边界 Bug:FreeRTOS 任务只需设置为 2ms 周期的 vTaskDelayUntil。这给了硬件整整 1 毫秒的空闲冗余时间 (Slack Time) 来清空 ep_busy,无论怎样产生相位漂移,都绝对不会跨过下一个 2ms 边界!
  3. 完美的音频还原:缓冲填满了整整 2ms 的音频,一次性交给 DMA。没有任何的丢帧、断点或 CPU 100% 满载,从物理硬件层面彻底解决了抖动锯齿。

5. 结论

在没有 SOF 中断且受限于 Arduino 预编译栈的 ESP32-S3 项目中,开发 USB 1000Hz 实时同步音频设备,不要试图在软件层和 1ms 时序死磕。 利用 USB 规范中的 bInterval > 1 合理降频打包,是绕过芯片级 DMA 缺陷最优雅、最稳定的方法。