ledc causing WDT to reboot...
Posted: Sat Apr 04, 2026 1:34 am
I have some firmware that has been in production for 4 or 5 years and is running on ESP-IDF 5.1.2. I'm in the process of updating it to run on 5.5.4 and everything went smoothly except for the ledc code.
There is a task that runs on core 1 that changes the LED when the controller enters different states that are set by tasks running on Core 0. e.g. BLE connected, Wifi connected...etc. The code can fade a single color via ledc just fine, say green or blue by itself. But when it attempts to mix 2 colors (blue and red) via ledc fade a WDT exception is being triggered.
Here is the configuration of the ledc timer, I'm using low speed mode.
And here is the code that actually does the fading. As you see I'm following the guidance of the ledc_fade example and using xSemaphoreTake to take 3 semaphores, one for each color, and xSemaphoreGiveFromISR in the callback registered via ledc_cb_register to put them back. With the use of the semaphore there has never been an issue with changing the duty cycle before the previous one completes.
When the WDT is triggered the back trace shows Core 1 at:
Which is the empty while loop.
It seems that it is waiting for something to change with duty_start that isn't happening quickly enough and then Core 0 triggers the WTD (not sure why it is Core 0 that triggers it when the ledc code is running on Core 1). If I comment out the empty while call the code runs perfectly. The fact that I'm already waiting for the duty cycle to end before changing it seems to imply that possibly this is just a belt and suspenders situation, but having an empty while loop that waits for a condition to change before exiting is bad practice. It should at least have a vTaskDelay in the loop so it allows other tasks to run and not trigger a WTD event.
While debugging this I also learned that the fade length that I'm using seems to have some bearing on this. The LED fades in and out over the course of 4 seconds, 2 sec in and 2 sec out. If I shorten this to 200 ms, 100ms in and 100ms out it doesn't trigger the WDT. Perhaps that is because the WDT task length is 300ms by default.
All this being said, I have not yet been able to recreate my issue with the ledc_fade example. Though the example is very simple and runs on a single core. I will attempt to modify it to use a couple of tasks that simulate what my code is doing and see if I can recreate the error in an example simple enough to share.
Does anyone have any thoughts on the empty while loop? It wasn't there in 5.1.2 but does show up in 5.2.6 and later.
There is a task that runs on core 1 that changes the LED when the controller enters different states that are set by tasks running on Core 0. e.g. BLE connected, Wifi connected...etc. The code can fade a single color via ledc just fine, say green or blue by itself. But when it attempts to mix 2 colors (blue and red) via ledc fade a WDT exception is being triggered.
Here is the configuration of the ledc timer, I'm using low speed mode.
Code: Select all
// configure the timer for the PWM DML 4/2/2024
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE, // timer mode
.duty_resolution = LEDC_TIMER_13_BIT, // resolution of PWM duty
.timer_num = LEDC_TIMER_1, // timer index
.freq_hz = 5000, // frequency of PWM signal
.clk_cfg = LEDC_AUTO_CLK, // Auto select the source clock
};Code: Select all
void LEDTask::fadeLED(int r, int g, int b, int fadeTime){
xSemaphoreTake(counting_sem, portMAX_DELAY);
xSemaphoreTake(counting_sem, portMAX_DELAY);
xSemaphoreTake(counting_sem, portMAX_DELAY);
ledc_set_fade_with_time(this->ledc_channel[R_CHANNEL].speed_mode,this->ledc_channel[R_CHANNEL].channel, r, fadeTime);
ledc_set_fade_with_time(this->ledc_channel[G_CHANNEL].speed_mode,this->ledc_channel[G_CHANNEL].channel, g, fadeTime);
ledc_set_fade_with_time(this->ledc_channel[B_CHANNEL].speed_mode,this->ledc_channel[B_CHANNEL].channel, b, fadeTime);
ledc_fade_start(this->ledc_channel[R_CHANNEL].speed_mode, this->ledc_channel[R_CHANNEL].channel, LEDC_FADE_NO_WAIT);
ledc_fade_start(this->ledc_channel[G_CHANNEL].speed_mode, this->ledc_channel[G_CHANNEL].channel, LEDC_FADE_NO_WAIT);
ledc_fade_start(this->ledc_channel[B_CHANNEL].speed_mode, this->ledc_channel[B_CHANNEL].channel, LEDC_FADE_NO_WAIT);
}Code: Select all
--- 0x40094bff: ledc_ll_set_duty_start at C:/Users/david/OneDrive/Documents/.espressif/v5.5.4/esp-idf/components/hal/esp32/include/hal/ledc_ll.h:487Code: Select all
static inline void ledc_ll_set_duty_start(ledc_dev_t *hw, ledc_mode_t speed_mode, ledc_channel_t channel_num)
{
// wait until the last duty change took effect (duty_start bit will be self-cleared when duty update or fade is done)
// this is necessary on ESP32 only, otherwise, internal logic might mess up (later targets with SOC_LEDC_SUPPORT_FADE_STOP allow to re-configure parameters while last update is still in progress)
while (hw->channel_group[speed_mode].channel[channel_num].conf1.duty_start);
hw->channel_group[speed_mode].channel[channel_num].conf1.duty_start = 1;
}While debugging this I also learned that the fade length that I'm using seems to have some bearing on this. The LED fades in and out over the course of 4 seconds, 2 sec in and 2 sec out. If I shorten this to 200 ms, 100ms in and 100ms out it doesn't trigger the WDT. Perhaps that is because the WDT task length is 300ms by default.
All this being said, I have not yet been able to recreate my issue with the ledc_fade example. Though the example is very simple and runs on a single core. I will attempt to modify it to use a couple of tasks that simulate what my code is doing and see if I can recreate the error in an example simple enough to share.
Does anyone have any thoughts on the empty while loop? It wasn't there in 5.1.2 but does show up in 5.2.6 and later.