Migrating Fan Strobe LED Project to ESP32

My fan strobe LED project has a flaw where I used a time delay inside an interrupt service routine (ISR), a bad idea. Because that blocks other housekeeping task from occurring, which leads to other hard-to-diagnose problems. I didn’t know of a good way to resolve that problem with the ESP8266 microcontroller on board the Wemos D1 Mini module I had used, but I thought the more powerful ESP32 might do the job. Fortunately, I found an ESP32 dev board electrically compatible with the Wemos D1 Mini. Hardware-wise, I just had to solder a compatibility subset of 16 out of 40 pins to make a drop-in upgrade.

The software side needed more work, but thanks to ESPHome and the Arduino framework it is built upon, most of the changes are minor. Aside from updating GPIO pin numbers in ESPHome YAML and the custom component written in Arduino C, I switched fan control from ESP8266 software PWM to ESP32’s LEDC hardware PWM peripheral.

That leaves the star of the show: the ISR that turns the LED on and off triggered by fan tachometer signal. For this initial migration, I kept all the code as-is, including those problematic calls to delayMicroseconds(). Even though I intend to replace that code, I wanted to run it as-is at first to see any problems firsthand. I had thought that perhaps I would have more leeway for my ISR abuse with dual cores, because there’d a second core available to do work while I hoarded one.

Bad news: it did not. If anything, it appears that ESP32 is even more sensitive to my abuse. Or possibly I notice it more because the ESP32 had better crash information. It’s possible my ESP8266 sketch frequently crashed the controller and rebooted without my ever noticing something went wrong. With the ESP32, I get a lot more information with fully decoded stack trace, which takes nonzero time to generate and lets me notice the crashes. I love the fact that ESPHome incorporated all of this debugging infrastructure! It’s actually less effort for me to get a decoded crash stack via ESPHome than from within Arduino IDE. Sure, I had to connect a USB cable to see crash stack over serial, losing the wireless advantage of ESPHome, but that’s no worse than using Arduino IDE.

Judging by the crashing stack trace, my call to delayMicroseconds() was translated to esphome::delay_microseconds_safe(). And the fact it crashed told me that… well, it isn’t as safe as the author intended. Further down on the call stack, I see calls to ESP32 NVS subsystem, something I have learned was an ESP32 feature absent from the ESP8266. At first glance it implies delay_microseconds_safe() is not safe when called in the middle of a NVS write operation. No matter, the objective of this exercise is to get away from using delayMicroseconds() at all, so I should start working on that.

WARNING Found stack trace! Trying to decode it
WARNING Decoded 0x400e2cac: esphome::delay_microseconds_safe(unsigned int) at /data/cache/platformio/packages/toolchain-xtensa32/xtensa-esp32-elf/include/c++/5.2.0/bits/basic_string.h:195
WARNING Decoded 0x400810a0: pulse_timer_handler() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/gpio.h:62
WARNING Decoded 0x4008147d: __timerISR at /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-timer.c:174
WARNING Decoded 0x40087459: _xt_lowint1 at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/xtensa_vectors.S:1154
WARNING Decoded 0x4008f127: esp_rom_spiflash_read_status at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/spi_flash_rom_patch.c:662
WARNING Decoded 0x4008f162: esp_rom_spiflash_wait_idle at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/spi_flash_rom_patch.c:662
WARNING Decoded 0x4008f499: esp_rom_spiflash_program_page_internal at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/spi_flash_rom_patch.c:662
WARNING Decoded 0x4008f6c9: esp_rom_spiflash_write at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/spi_flash_rom_patch.c:662
WARNING Decoded 0x40087ec1: spi_flash_write_inner at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/flash_ops.c:155
WARNING Decoded 0x40088151: spi_flash_write at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/flash_ops.c:416
WARNING Decoded 0x40165a35: nvs::nvs_flash_write(unsigned int, void const*, unsigned int) at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/nvs_flash/src/nvs_ops.cpp:72
WARNING Decoded 0x40163e8e: nvs::Page::writeEntry(nvs::Item const&) at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/nvs_flash/src/nvs_page.cpp:871
WARNING Decoded 0x401641f9: nvs::Page::writeItem(unsigned char, nvs::ItemType, char const*, void const*, unsigned int, unsigned char) at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/nvs_flash/src/nvs_page.cpp:871
WARNING Decoded 0x40163220: nvs::Storage::writeMultiPageBlob(unsigned char, char const*, void const*, unsigned int, nvs::VerOffset) at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/nvs_flash/src/nvs_storage.cpp:579
WARNING Decoded 0x401637b6: nvs::Storage::writeItem(unsigned char, nvs::ItemType, char const*, void const*, unsigned int) at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/nvs_flash/src/nvs_storage.cpp:579
WARNING Decoded 0x40162bbb: nvs_set_blob at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/nvs_flash/src/nvs_api.cpp:547
WARNING Decoded 0x400d8413: esphome::esp32::ESP32Preferences::sync() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/components/esp32/preferences.cpp:124
WARNING Decoded 0x400e3621: esphome::preferences::IntervalSyncer::on_shutdown() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/gpio.h:62
WARNING Decoded 0x400e36a9: std::_Function_handler<void (), esphome::preferences::IntervalSyncer::setup()::{lambda()#1}>::_M_invoke(std::_Any_data const&) at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/gpio.h:62
WARNING Decoded 0x400e352a: std::function<void ()>::operator()() const at /data/cache/platformio/packages/toolchain-xtensa32/xtensa-esp32-elf/include/c++/5.2.0/functional:1710
 (inlined by) esphome::Scheduler::call() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/scheduler.cpp:204
WARNING Decoded 0x400e1325: esphome::Application::loop() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/application.cpp:69
WARNING Decoded 0x400e399e: loop() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/gpio.h:62
WARNING Decoded 0x400f0ea9: loopTask(void*) at /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/main.cpp:23
WARNING Decoded 0x40089ff6: vPortTaskWrapper at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/port.c:355 (discriminator 1)

4 thoughts on “Migrating Fan Strobe LED Project to ESP32

  1. Roger,

    I found using Stefan Staub’s Ticker library helps a lot with converting from blocking to non-blocking code. It takes a little while to get your head around the concept, but basically Ticker grabs one of the hardware timers and uses the stream of interrupts to trigger events, allowing you to have multiple tasks which will be called on a regular basis. You can use the Ticker to define how frequently a task will be called:-

    Ticker timer1(mainloop, 2000, 0); // RTC (slow) loop.
    Ticker timer2(ntp_loop, 1000, 0); // NTP (1-sec) loop.
    Ticker timer3(tstr_loop, 5, 0); // TelnetStream (5ms fast) loop.
    Ticker timer4(loopTicker, 10, 0); // LoopTicker (10ms, so fast-ish) loop.

    The one thing you must do when using Ticker is to remove -all- calls to delay() from your code (with the exception of start-up, before Ticker is actually invoked). This sounds easy, until you consider the number of third-party libraries which use hidden delays.

    I use the loopTicker (timer4) just to increment a counter to make it easy to implement a generic, non-blocking drop-in delay() replacement for those times where you do want to block a specific task, without blocking everything.

    Hope this might be useful,



    1. Do you know if it is safe to use from inside an ISR? A quick perusal of Ticker documentation on GitHub didn’t mention ISR one way or another. In this particular project, I would have wanted to call ticker start/resume from inside an ISR.


      1. Roger,

        I can’t give you a definitive answer on that, because I haven’t actually tried it, but I would imagine that it is safe. If your ticker is already defined outside of the ISR, then the call to start or resume should return immediately, without blocking the ISR itself.



Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s