ESP32 hardware faders (#P27)

Maybe my google-fu is rusty, or there’s simply no full example of this on the Interwebs…that’s pretty strange.

This is about the cherished ESP32 that will feature an extension/replacement of my #P7 series. This was an ESP8266 board that replaced a dumb lighting control, offering sensor logging to a database. I quickly found out that the limited pin count (and quirks) of the ESP8266 would require me to think about e.g. shift register extensions to make the entire project viable – which now got replaced with the large ESP32 that does have all the flexibility that I need. ESP32 prices have come down and I’m really sick of the accidental boot pin configuration crap of the ESP8266.

Anyway, one of the things required for kitchen (or hallway) lighting is fading. Sure, placing an interrupt triggered by some HC-SR501 PIR sensor would ideally cause the lights to fire right up. Depending on the amount of LED power, a quick fade would be kind of nice for the power supply, but it’s usually not required. However, when turning the lights off, a fair bit of warning is necessary. One simply does not suddenly shut off all the lights.

Even with the ESP8266 a traditional LED fade is not straight forward, as the aggressive watchdog will reset the entire thing when it takes too long to complete some loop. This can be mitigated on regular loops by calling yield() (or resetting the wdt manually, which I still haven’t figured out) during the fade routine. Doing so in interrupt functions however is usually not possible, as interrupt handlers are not designed to take a significant amount of time. Another interrupt could hit in the mean time and the ESP will crash. In the past, I have set immediate full throttle when triggered by PIR events, and made acknowledgement fades for touch inputs run from the main code by setting a variable during the interrupt function call.

The ESP32 has a different design when it comes to LED fading. In my understanding, you do not control pins directly and individually to your liking, but you rather control fader channels. There are several of them (0-7) that act independently (for the most part), and one can attach one or more output pins to them. Change the channel and it will do whatever is required to make the magic happen on the assigned pins.

This is well documented, but for the sake of easy comparison, here’s a simple random fader for the onboard LED of most ESP32 boards. It starts with a simple fade from 0 to 1023 (for strange reasons 1024 sometimes is a valid value as well?) and then generates random fade target values which are used in the next loop interation. Fading speed is constant at 5ms per tick, so the extremal 0 to 1023 rise would take five seconds.

const int Lonboardchannel = 6; // use channel 6
const int Lonboardpin = 2; //use pin 2, that's the onboard LED
const int pwmfrequency = 15000; //15 kHz frequency. change to sub-400Hz for visible artifacts
const int pwmresolution = 10; //Resolution bits - 8, 10, 12, 15
int i = 0;
volatile uint32_t timenow;
const int fadespeed = 5; //5 ms tick interval
volatile int fade1 = 0;
volatile int fade2 = 1023;

void setup()
    ledcAttachPin(Lonboardpin, Lonboardchannel); //attach pin to channel
    ledcSetup(Lonboardchannel, pwmfrequency, pwmresolution); // setup channel

void loop() {
  Serial.println("Fading from " + String(fade1) + " to " + String(fade2));
    for (i = fade1; i > fade2; i--) {
      ledcWrite(Lonboardchannel, i);
      timenow = millis();
      while (millis() < timenow + fadespeed)  {  yield();  }
    for (i = fade1; i < fade2; i++) {
      ledcWrite(Lonboardchannel, i);
      timenow = millis();
      while (millis() < timenow + fadespeed)  {  yield();  } 
  fade1 = fade2;
  fade2 = exp((float)random(0, 69)/10); // min to max-1

Compiled file size: 211 KiB
esp32_example_ledcwrite.ino / esp32_example_ledcwrite.bin

Classic Arduino-like code - quick setup similar to pinMode(pin, OUTPUT) and most of the code decides what to do with the start and end fade values. ledcWrite(channel, value) replaces digitalWrite(pin, value).

Now here's the thing: This is not the only way to handle fading on the ESP32 platform. There's hardware faders!

#include <driver/ledc.h>
const int pwmfrequency = 15000; //15 kHz frequency. change to sub-400Hz for visible artifacts
const int pwmresolution = 10; //Resolution bits - 8, 10, 12, 15
volatile int fade = 1023;
const int fadeduration = 1000; //1000 ms total fade duration

void setup()
  ledc_timer_config_t ledc_timer;  
  ledc_timer.speed_mode   = LEDC_LOW_SPEED_MODE;
  ledc_timer.timer_num    = LEDC_TIMER_1;
  ledc_timer.bit_num      = (ledc_timer_bit_t) pwmresolution;
  ledc_timer.freq_hz      = pwmfrequency;
  ledc_channel_config_t ledc_channel1;    = LEDC_CHANNEL_1;
  ledc_channel1.gpio_num   = 2;
  ledc_channel1.speed_mode = LEDC_LOW_SPEED_MODE;
  ledc_channel1.timer_sel  = LEDC_TIMER_1;
  ledc_channel1.duty       = 5;

void setfade(ledc_channel_t channel, uint32_t fadeTo, int duration, ledc_fade_mode_t wait) {  
  ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, channel, fadeTo, duration );
  ledc_fade_start(LEDC_LOW_SPEED_MODE, channel, wait );

void loop() {
  Serial.println("Fading to " + String(fade));
  setfade(LEDC_CHANNEL_1, fade, fadeduration, LEDC_FADE_WAIT_DONE);
  fade = exp((float)random(0, 69)/10); // min to max-1

Compiled file size: 221 KiB (+10 KiB)
esp32_example_hwfade.ino / esp32_example_hwfade.bin

Most of the sketch is now about setting the entire thing up. There's a ledc_channel_config_t struct similar to the previous ledcSetup, and there's a bigger ledc_channel_config_t struct for the channel setup as well. After that, the entire fader action is handled by two function calls ony: ledc_set_fade_with_time() (that sets up the channel) and ledc_fade_start() which, for obvious reasons, gets the thing going. As there's configuration involved in every call, I've even remapped this to a single custom function setfade() that does run those functions with my default preferences. Apparently this can be used to generate signals up to 40 MHz, which I do not need, so low speed modes will do for me.

Same sketch length (well, lines of code), totally different behaviour.

The kicker: There's this ledc_fade_mode_t variable that can have two settings: LEDC_FADE_WAIT_DONE or LEDC_FADE_NO_WAIT. Given the channels are independent and are executed in (or very close to) hardware, they can be run in arbitrary pieces of code with just those two instructions. If you set LEDC_FADE_WAIT_DONE, it'll return to the next line of code like a for or while loop with step-by-step fading instructions would do, but LEDC_FADE_NO_WAIT does start the fading process and then returns to the next line of code immediately. As your program does something else, the fading will continue! No explicit multicore/multithreading programming needed, this is executed in the background without any further input needed!

There's even more functionality present in the ledc.h, e.g. ledc_set_fade_with_step(), ledc_set_fade_with_time() and even thread-safe versions of them. I haven't had much success with these so I mostly played around with what worked for me, but I'll have another go at it. For example I get hpoint warning messages after setup, so I assume this needs to be addressed - but the help on hpoint is not exactly helpful. "LEDC channel hpoint value, the max value is 0xfffff" - thank you so much. Again, there's VERY little helpful information out there, so I'm glad to provide a sample Arduino sketch that someone can build on.

Hardware fading however has one important drawback that I need to mention: According to the documentation, it cannot be stopped - once the fading is in progress, you need to wait for it. Once it finishes, it can execute a new fading routine, but it cannot be stopped or reprogrammed, e.g. when a PIR movement interrupt hits and the slow fading down process needs to be stopped and reversed immediately.

I'm working on that one, too, maybe by implementing it in a more classic way so that the individual fade steps are much shorter and any interrupt can be handled within a few tens of milliseconds. We'll see.

Side note: Since I needed to edit MIME types once again to be able to upload the .ino sketch and .bin image files to my maximum security WordPress blog, I finally found the culprit of the buggy media viewer - Enhanced Media Library Version 2.7.2 By wpUXsolutions. Not only did it require me to do uploads in a separate window, but it also wasn't able to edit MIME types properly (which was the main reason for installing it way back). Well, hasn't been updated for the last three major WordPress revisions and people flagged it down, so I kicked it out. Replacement: WP Add Mime Version 2.5.5 By Kimiya Kitani. Easy interface and works fine. .ino and .bin now linked below the respective code blocks if you prefer a downloaded file instead of simple copy and paste.

Notify of
:mrgreen:  :neutral:  :twisted:  :arrow:  :shock:  :smile:  :???:  :cool:  :evil:  :grin:  :idea:  :oops:  :razz:  :roll:  ;-)  :cry:  :eek:  :lol:  :mad:  :sad:  :suspect:  :!:  :?:  :bye:  :good:  :negative:  :scratch:  :wacko:  :yahoo:  :heart:  B-)  :rose:  :whistle:  :yes:  :cry2:  :mail:  :-((  :unsure:  :wink: 
Inline Feedbacks
View all comments