Testing ESP32's LEDC "non-blocking" fading
Problem statement
The ESP-IDF (SDK for ESP32 firmware development) LEDC documentation claims:
For ESP32, hardware does not support any duty change while a fade operation is running in progress on that channel. Other duty operations will have to wait until the fade operation has finished.
which – especially combined with ledc_fade_mode_t = LEDC_FADE_NO_WAIT
could
be interpreted at least two ways:
- Function call1 with
LEDC_FADE_NO_WAIT
returns, but op has to wait (is queued)2 - The function call blocks, despite
LEDC_FADE_NO_WAIT
In this quick post I’ll get to the bottom of it.
Solution
I created a simple test app, hoping to test the behavior3.
In a nutshell:
- Let’s fire long fade to max duty, then a bit later send fade to 0
- Send several fades (25% duty, 50%, 75%, 100%) in rapid succession
- Run multiple fades in parallel
In order to better see everything, I’ve added several signals that leave clues about timing:
Trigger
signal (GPIO) that triggers for the duration of the testSignal
signal that triggers for the duration of a given sectionMEASURE()
wrapper that uses ESP timer to get logs.
Even the result of the first test ended up showing that – sadly – the “other ops will have to wait” is Espressif speak for “function call will block”.
Check this out:
Signal graph of second test in Pulseview
On the Signal
trace you see how long it took for ledc_set_fade_time_and_start
to finish. So, yeah, blocking. Sigh.
Full log, if interesting:
I (297) LEDC_TEST: ==== LEDC Concurrent Fades Test ====
I (307) LEDC_TEST: Boot button config...
I (307) gpio: GPIO[9]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
I (317) LEDC_TEST: Trigger button (& GND+SIGNAL pins) config...
I (327) gpio: GPIO[3]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 1| Intr:0
I (337) gpio: GPIO[4]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 1| Intr:0
I (347) gpio: GPIO[7]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 1| Intr:0
W (357) LEDC_TEST: Punch the BOOT button to start test1 (sequential interrupted fades)...
I (6667) LEDC_TEST: Test 1: Sequential fades on one channel
I (6667) LEDC_TEST: ... took: 27μs
I (6667) LEDC_TEST: Initial duty: 0
I (6717) LEDC_TEST: Starting long fade to max duty
I (6717) LEDC_TEST: ... took: 169μs
I (6767) LEDC_TEST: Duty before interrupt: 1235
I (6767) LEDC_TEST: Interrupting with fade to 0
I (7037) LEDC_TEST: ... took: 278274μs
I (7337) LEDC_TEST: Final duty: 0 (expected to be close to 0)
W (7337) LEDC_TEST: Punch the BOOT button to start test2 (multiple rapid fade changes)...
I (15637) LEDC_TEST: Test 2: Rapid successive fades on one channel
I (15637) LEDC_TEST: ... took: 22μs
I (15687) LEDC_TEST: Starting fade to 25% duty
I (15687) LEDC_TEST: ... took: 31μs
I (15737) LEDC_TEST: Quickly overriding with fade to 50% duty
I (16097) LEDC_TEST: ... took: 359966μs
I (16147) LEDC_TEST: Quickly overriding with fade to 75% duty
I (16297) LEDC_TEST: ... took: 155153μs
I (16347) LEDC_TEST: Finally overriding with fade to 100% duty
I (16397) LEDC_TEST: ... took: 57768μs
I (16697) LEDC_TEST: Final duty: 8191 (expected to be close to max: 8191)
W (16697) LEDC_TEST: Punch the BOOT button to start test3 (concurrent fades on two channels)...
I (18097) LEDC_TEST: Test 3: Concurrent fades on different channels
I (18097) LEDC_TEST: ... took: 38μs
I (18147) LEDC_TEST: Initial duties - Channel 1: 0 (expected near 0), Channel 2: 8191 (expected near max)
I (18147) LEDC_TEST: Starting concurrent fades in opposite directions
I (18147) LEDC_TEST: ... took: 79μs
I (18307) LEDC_TEST: Mid-fade duties - Channel 1: 3825 (increasing), Channel 2: 4366 (decreasing)
I (18507) LEDC_TEST: Final duties - Channel 1: 8191 (expected near max), Channel 2: 0 (expected near 0)
W (18507) LEDC_TEST: Punch the BOOT button to start test1 (sequential interrupted fades)...
And full PulseView sessions:
- test1.sr, test1.pvs, test1-long.png
- test2.sr, test2.pvs, test2-long.png
- test3.sr, test3.pvs, test3-long.png
Closing words
Hardly Earth shattering, but shines some light on an annoying behavior I need to take into consideration for the Zigbee light.
If you can’t tell by this, the custom Hue light driver is still chugging along. Dotting the i-s, crossing the t-s. Watch this space. ;)
-
For example ledc_set_fade_time_and_start ↩
-
But if true, what if two duty updates trigger in the meantime? Or six? How deep is your love (queue), ESP-IDF? ↩
-
Originally I tried using Claude (
For esp-idf, write me a short test app that verifies behavior of LEDC controller when dimming. I want no interrupts.
,no need to explain what it does.
,ok, but I want to test the behavior of concurrent fades (situation when multiple additional fades trigger before the current one finishes. adjust the app to test this, and this time explain your strategy
), but the result had a subtle bug (see if you can spot it) and several falsehoods in comments. Then I extended it beyond recognition anyway – mainly to add the button trigger, the signals, measurement. And to shorten the runtime. ↩