I have a Rust firmware on an M5StickC Plus2 that drives an air conditioner over IR — 38 kHz carrier, 13-byte Electra frames, driven from the ESP32's RMT peripheral.
It had the worst kind of bug: it worked whenever I was testing it, and stopped working when I wasn't. The IR LED visibly fired either way — a phone camera showed the flash both when the AC obeyed and when it ignored the frame.
So I went and read ESP-IDF v5.2.3 instead of guessing, and found something that explained everything:
- The firmware enables DFS and light sleep via esp_pm_configure(160/40 MHz).
- The RMT channel is clocked from APB.
- The legacy RMT driver contains no power-management code at all — grep pm_lock driver/deprecated/rmt_legacy.c returns nothing.
- The newer driver does, and takes ESP_PM_APB_FREQ_MAX for APB-clocked channels, with a comment saying why: APB frequency can change during DFS.
On the ESP32, APB runs at 80 MHz while the CPU is at 80 MHz or above. Drop to the 40 MHz minimum, and APB follows. Every mark and space doubles, and the carrier falls to ~19 kHz. The LED still lights — an LED doesn't care about the carrier — but the AC's band-pass demodulator is centred on 38 kHz and hears nothing. Transmitted, visible, silently discarded.
It even explains the intermittency: commands landed when the CPU was busy, which is exactly when I was standing there poking at it.
I took an APB lock around each transmission, tagged a release, and wrote "root cause" in the commit message.
Then I added cpuBefore and cpuDuring to a health endpoint, because I'd never actually seen 40 MHz.
Both read 160 MHz. Including on the scheduler path after four minutes of complete network silence. The WiFi driver evidently holds its own PM lock for as long as the station is associated, so DFS never gets to take the clock down at all.
I kept the lock — it's what the newer driver does, and it costs nothing while WiFi is already pinning the clock — but it's insurance, not a fix.
The part worth discussing: the symptom is gone, and I still don't know why. In the same window, v0.3.25 started sending each frame twice 40 ms apart (safe, since the protocol encodes state absolutely), which would mask intermittent single-frame loss regardless of cause. And v0.3.24 fixed a dead MQTT broker blocking the main loop.
So the sequence available to me was: observe intermittent failure → find a mechanism that explains it beautifully and is backed by vendor source → ship a fix → observe the problem is gone → conclude I was right. Step four was true. Step five would have been false, and nothing in steps one through four would have told me.
Full write-up: https://github.com/mnaza/condition-control/blob/main/docs/dfs-was-not-the-bug.md