FormLabs Form 1+ Laser Galvanometer Control

Despite some concerns, I’ve decided to poke around inside a broken FormLabs Form 1+ laser resin 3D printer while it is running. I think it’ll tell me more about how it was supposed to work, because I didn’t learn very much from just a roster of ICs. But it still wouldn’t tell me everything, because some of its current behavior would be as intended while other behavior would be wrong because the printer is broken. And as my only example of the species, it might be hard to tell which is which.

Before I powered it up, I probed around for all the ground wires. Since the printer is powered by 24V DC delivered through a standard barrel jack, it was least easy to probe for continuity to ground. There was even an exposed pad on the main processor board labeled GND. Knowing which wire is ground for each connector established a baseline for voltage measurements once I turned the printer on.

The first surprise was on the GALVO POWER connector. It had three wires colored red, black, and purple. Black was known to be ground. Red was expected to be +24V DC, which was confirmed. I didn’t know what to expect for purple, and it turned out to be -24V DC. Negative voltage! Something I rarely see but it would be consistent with the presence of a ST Micro L79 negative voltage regulator delivering -15V. Which is a mirror match for the L78 positive regulator taking +24V DC and delivering +15V DC. Separately there is an On Semi NCP1117 delivering 3.3V for digital logic.

As for GALVO SIGNAL, it had three wires colored black, white, and red. Again I knew black was ground from earlier probing. Once the system was powered on, I saw white wire voltage stayed pretty consistent at 2.5V DC. The red wire was where interesting things happened: It varied between 0V to 5V relative to ground (or -2.5V to +2.5V relative to white wire) while this printer went through the motion of printing, blissfully unaware its galvanometer control system was fried. Plotting X and Y galvo signals against each other on my oscilloscope’s XY mode, it sure looks like a laser draw path. That is, as long as we ignore that diagonal noise I blame on my beginner level oscilloscope skill. Anyway, noise aside, it shows this 0-5V (Or -2.5 to +2.5V) analog voltage is how mainboard commands galvo position. This scorched control board couldn’t translate those commands into galvanometer movement, but I wanted to see if anything is getting to those galvanometers at all.

Considerations Exploring FormLabs Form 1+ While Powered Up

Curious about what I could learn from a scorched laser galvanometer control board, I looked up all component markings I could find. That gave me a few pieces of a puzzle, but I’m missing many more pieces and I don’t have a guess as to how those pieces fit together. In the interest of getting more data, I thought I would probe around while this FormLabs Form 1+ printer is powered up.

The biggest danger is a laser at the heart of this laser resin printer. This whole printer is classified as a Class 1 laser device, which means it is safe to use without eye protection. But that guarantee of safety only applies when used as intended. If I’m going to probe its circuitry with panels removed, I lose protection provided by those panels. Bypassing such protection means I have to be careful not to damage my eyes. Anyone who plans to try this as well: please be careful!

Working with lasers is especially dangerous if they operate at a wavelength outside human visible spectrum. We have evolved a blink reflex to protect our vision, but it wouldn’t work for wavelengths we can’t see. Thankfully, 405nm is within human visible spectrum (it should show up as an intense blue or violet beam), so I still have my blink reflex as a final line of defense. Why 405nm? Apparently, this wavelength became popular because Blu-ray drives drove R&D and economies of scale. I don’t know about power levels, though. I would guess resin printing runs 405nm lasers at a higher power level than reading Blu-ray discs (60mW as per Wikipedia) but I have no data to support that guess.

There’s another interesting data point I want to investigate: the printer was happy to run through its printing process with a dead galvanometer control board. Such behavior tells me something is running as an open-loop process, operating without feedback that its laser beam wasn’t being steered. I knew stepper motors used in cheap FDM 3D printers are open-loop devices, where the printer control board sends motor pulses without knowing if there is actual physical motion. But laser galvanometers are closed-loop actuators, so something else is going on.

Despite my wariness of a laser operating at unknown power levels, my curiosity motivated me to power it up and poke around.

Form 1+ Galvanometer Control Board IC Roll Call

This Form 1+ laser resin printer has a scorched galvanometer control board, and replacements are no longer available from FormLabs. I don’t have any background to make heads or tails of a laser galvanometer control board, but I might get a vague idea and learn something by examining its collection of components. Here’s a roll call based on markings on those chips.

The board controls both X and Y axis galvanometers, probably why the middle section of this board a pair of identical assemblies.

Starting from the upper left, component U4 looks to be the brains of this operation: a ST Microelectronics STM32F030K6T6 with its ARM Cortex-M0 core running at up to 48MHz.

U5 in the upper right is an On Semiconductor NCP1117, a low-dropout (LDO) voltage regulator marked with 17-33G indicating it outputs 3.3V. This would supply power to the STM32 and other 3.3V digital logic.

Back towards the centerline, U6 is a Microchip MCP4802 digital-to-analog converter (DAC) with dual 8-bit resolution channels controlled via SPI.

Just below that is U7, a TI TL032 dual-channel op-amp.

On either side of U7 is a collection of 8 chips, four on each side. (U104, U105, U106, and U107 on the left. U204, U205, U206, and U207 on the right.) They are all marked with Microchip logo and 41H51103, which I found to be a MCP41HV51 SPI-controlled digital potentiometer. In section 11.1 Package Marking Information of the MCP41HVx1 datasheet, 41H51103 is identified as MCP41HV51-103E/ST variant.

There are a lot of surface mount passives in the center section with two copies of the layout left and right. Each copy gets a trio of chips marked with On Semiconductor logo and MC33274ADG. These MC33274A chips are each a quad-pack of op-amps. Each side has an additional component that is mounted vertically so we only see their top here.

Going back to an earlier picture focused on the burnt and melted connector, we can see one of those vertical standing components clearly on the right. They are On Semiconductor BD139 NPN power transistors.

Three components are bolted to a heat sink at the bottom. To the left is a single Texas Instruments LM1876. It is a dual 20W audio power amplifier. Adjacent to that amplifier are a mirror pair of ST Microelectronics voltage regulators. The L79 negative voltage regulator accept up to -35V DC and output a regulated -15V DC. L78 is its symmetric positive voltage sibling, accepts up to +35V DC and output a regulated +15V DC.

Those components are major pieces of my “how did this board work” puzzle, but I’m still fuzzy on how they fit together. If I want more data, I’ll have to probe around while the system is powered up and I’m not entirely sure I want to do that.

FormLabs Form 1+ Next Steps

Diagnosing why a FormLabs Form 1+ resin laser 3D printer wasn’t working; I found the smoke trail of an electrical fire on its galvanometer (galvo) control board. This board is cooked in a very literal sense. Even though the printer is long out of support, I contacted FormLabs hoping for a replacement board. While waiting for a response, I took the printer apart to get a closer look at the damaged board.

It looks far worse from this angle. A lot of charred and burnt plastic at the base of power connector, which had melted together into a single lump. It is no longer possible to (neatly) unplug this connector. A surface-mount capacitor C20 has been vaporized along with some of the circuit board creating a small pit.

Holding the board at a different angle, I saw charred residue of smoke trail running all the way up the board. This fire was more significant than I had previously thought. This is good news and bad news. I had been worried that I cooked the device with a nonstandard power supply but seeing extent of damage confirmed it happened before I got my hands on the device. If it happened on my watch, I would have definitely smelled dead electronics, a scent I was reminded of quite recently.

On the downside, severity of this damage puts repair out of reach of my current skill level. I don’t know anything about how galvanometer control circuits are supposed to work. My electronics engineering skills aren’t good enough to reverse engineer this board and fix it without technical information like schematics and diagnostics procedures.

After this (very discouraging) sight, FormLabs got back to me: no replacement galvo control boards are available. Unsurprising but at least it didn’t hurt to ask. I briefly considered buying a laser light show galvanometer setup from eBay or Amazon. But even if I could interface them, I don’t know how to test and tune them for good resin prints. Given these challenges, I’m not going to try to get this printer printing. I’ll focus on learning as much as I can from taking it (further) apart.

Switching to CPU Ticks Did Not Eliminate Wobble Because Fan Itself Was Wobbling

In order to obey constraints imposed by ESP32 hardware timer peripheral, I switched my fan strobe LED code to use a regularly polling interrupt service routine (ISR). I worried that I would be overly demanding to schedule it once every 10 microseconds, but I saw no obvious problems from that front. What I did see, though, is an irregular wobble at longer LED pulse delays indicating my LED strobe timing is drifting in and out of sync with fan blade position.

Because I had calculated timing for LED pulses by counting number of times my ISR is executed, I thought this wobble might be caused by unpredictable delays in ISR overhead. A multi-millisecond strobe delay (where I notice the wobble) would require my ISR to count several hundred times, allowing tiny errors to add up. To test this hypothesis, I switched my code to use a more consistent timing mechanism: the ESP32 CPU tick counter. It is a highly precise (though not necessarily accurate) source of timing information, one that I used for performance metrics in ESP_8_BIT_Composoite. But it comes with limitations including:

  • Tick counter value is only valid on the same core it is queried from, it is not valid to compare tick count across the two cores of an ESP32. Not a problem here because Arduino subsystem is pinned to a single core, so I am guaranteed to always be on the same core.
  • It goes up by one for each clock cycle of the CPU core. At 240MHz, an unsigned 32-bit value would overflow two or three times every minute. For this quick experiment I’m ignoring overflows, resulting in a brief dark pulse two or three times per minute. (This limitation was why I didn’t use it in my first draft.)

Switching my timer counter to CPU ticks did not eliminate the wobble, disproving my hypothesis. What else could it be? Thinking through possible explanations, I wished I could see exact time relationship between tachometer signal and my LED strobe pulses. Then I remembered I could do exactly that because I have a real oscilloscope now! In fact, I should have put it on scope before embarking on my CPU clock tick counter experiment. I guess I’m still not used to having this powerful tool at hand.

I set up the oscilloscope so I could see the fan tachometer pulse that would trigger my tachometer interrupt handler, where I calculate the ticks for turning LED on and off. I set up the oscilloscope to trigger on my resulting LED output pulse. I increased the LED pulse delay to roughly 6 milliseconds, which is a roughly 180 degree rotation. Placing that pulse close to the next fan tachometer pulse allowed me to easily compare their timing on oscilloscope screen.

As it turned out, my ESP32 code is completely blameless. The timing held steady between tachometer signal triggering pulse and LED output pulse. What changed are timing between consecutive fan tachometer pulses. In other words: my strobe light visual is wobbling because fan speed is wobbling. This is exactly why strobe lights are useful to diagnose certain problems with high-speed machinery: it makes subtle problems visible to the human eye.

Conclusion: the wobble is not a bug, it is a feature!


Custom component code using CPU tick counter. (Does not account for 32-bit tick overflow):

#include "esphome.h"
#include "esp_system.h"

volatile int evenOdd;
volatile int strobeDuration;
volatile int strobeDelay;
volatile uint32_t ccountTachometer;
volatile uint32_t ccountLEDOn;
volatile uint32_t ccountLEDOff;

const int gpio_led = 23;
const int gpio_tach = 19;

// How many cycles per microsecond. 240 for typical ESP32 speed of 240MHz.
const int clockSpeed = 240;

hw_timer_t* pulse_timer = NULL;

IRAM_ATTR void pulse_timer_handler() {
  uint32_t ccountNow = xthal_get_ccount();

  if (ccountNow >= ccountLEDOn) {
    digitalWrite(gpio_led, HIGH);
    ccountLEDOn = 0;
  }

  if (ccountNow >= ccountLEDOff) {
    digitalWrite(gpio_led, LOW);
    ccountLEDOff = 0;
  }
}

IRAM_ATTR void tach_pulse_handler() {
  if (0 == evenOdd) {
    evenOdd = 1;
  } else {
    if (strobeDuration > 0 && ccountLEDOn == 0 && ccountLEDOff == 0) {
      ccountTachometer = xthal_get_ccount();

      // Calculate time for turning LED on and off
      ccountLEDOn = ccountTachometer + strobeDelay * clockSpeed;
      ccountLEDOff = ccountLEDOn + strobeDuration * clockSpeed;
    }
    evenOdd = 0;
  }
}

class FanStrobeLEDSetupComponent : public Component {
  public:
    void setup() override {
      // Initialize variables
      strobeDelay = 10;
      strobeDuration = 20;
      ccountLEDOn = 0;
      ccountLEDOff = 0;

      // LED power transistor starts OFF, which is LOW
      pinMode(gpio_led, OUTPUT);
      digitalWrite(gpio_led, LOW);

      // Attach interrupt to tachometer wire
      pinMode(gpio_tach, INPUT_PULLUP);
      evenOdd = 0;
      attachInterrupt(digitalPinToInterrupt(gpio_tach), tach_pulse_handler, RISING);

      // Configure hardware timer
      pulse_timer = timerBegin(0, 80, true);
      timerAttachInterrupt(pulse_timer, &pulse_timer_handler, true);
      timerAlarmWrite(pulse_timer, 10, true /* == autoreload */);
      timerAlarmEnable(pulse_timer);
    }
};

class FanStrobeLEDEvenOddToggleComponent: public Component, public Switch {
  public:
    void write_state(bool state) override {
      evenOdd = !evenOdd;
      publish_state(state);
    }
};

class FanStrobeLEDDelayComponent: public Component, public FloatOutput {
  public:
    void write_state(float state) override {
      strobeDelay = 11000*state;
    }
};

class FanStrobeLEDDurationComponent: public Component, public FloatOutput {
  public:
    void write_state(float state) override {
      strobeDuration = 1000*state;
    }
};

Polling ESP32 Timer Shows Wobble

I wanted to use my fan strobe LED as a learning project to work with ESP32 hardware timers, but my original idea required modifying timer configuration from inside a timer alarm ISR. I have learned that is not allowed, and I have the crash stacks to prove it. The ESP32 Arduino Core timer API is geared towards timer alarm interrupts firing at regular intervals to run the same code repeatedly, so I should change my code structure to match.

Since the tachometer signal timing, strobe delay, and strobe duration could all change at runtime, I couldn’t use any of those factors as my hardware timer interval. Switching to a regular timing interval meant I would have to poll current status to determine what to do about state of LED illumination.

For the first draft, I kept a counter of how many times my ISR has executed. This variable is only updated by the ISR. Based on this counter value, each tachometer ISR calculates the time for LED to be turned on and back off in terms of timer ISR counter. Timer ISR compares those values to turn the LED on, then back off. When a particular pulse is complete, the timer ISR resets all counters to zero and waits for the next tachometer pulse to occur.

I tried a timer ISR duration of polling once every 20 microseconds. This meant a worst-case scenario of LED pulse starting as much as 20 microseconds later than specified, and a pulse duration as much as 20 microseconds longer than specified. When the ideal duration is around 200-250 microseconds, this is nearly 10% variation and resulted in visible flickering. I then tried 10 microseconds, and the flickering became less bothersome. On an ESP32 running at 240MHz, it meant I demand my ISR run once every 2400 clock cycles. I’m worried this is too demanding, but I don’t have a better idea at the moment. At least it hasn’t crashed my ESP32 after several minutes of running.

This new mechanism also allowed me to increase my LED strobe delay beyond a millisecond, but I noticed an irregular wobbling for longer delays. Those strobes of light are not happening at my calculated location. They vary by only a few degrees but very definitely noticeable. Is this a sign that my ISR is too demanding? For a multi-millisecond LED strobe delay, a 10-microsecond timer ISR would execute hundreds of times. If there’s a tiny but unpredictable amount of delay on each, they might add up to cause a visible wobble. Perhaps I should use a different counter with better consistency.


Custom component code with timer counter:

#include "esphome.h"
#include "esp_system.h"

volatile int evenOdd;
volatile int strobeDuration;
volatile int strobeDelay;
volatile int timerCount;
volatile int timerLEDOn;
volatile int timerLEDOff;

const int gpio_led = 23;
const int gpio_tach = 19;

hw_timer_t* pulse_timer = NULL;

IRAM_ATTR void timerCountersReset() {
  timerCount = 0;
  timerLEDOn = 0;
  timerLEDOff = 0;
}

IRAM_ATTR void pulse_timer_handler() {
  if (timerCount >= timerLEDOn) {
    digitalWrite(gpio_led, HIGH);
  }

  if (timerCount >= timerLEDOff) {
    digitalWrite(gpio_led, LOW);
    timerCountersReset();
  } else {
    timerCount++;
  }
}

IRAM_ATTR void tach_pulse_handler() {
  if (0 == evenOdd) {
    evenOdd = 1;
  } else {
    if (timerLEDOn == 0 && strobeDuration > 0) {
      // Calculate time for turning LED on and off
      timerLEDOn = timerCount + strobeDelay;
      timerLEDOff = timerLEDOn + strobeDuration;
    }
    evenOdd = 0;
  }
}

class FanStrobeLEDSetupComponent : public Component {
  public:
    void setup() override {
      // Initialize variables
      strobeDelay = 10;
      strobeDuration = 20;
      timerCountersReset();

      // LED power transistor starts OFF, which is LOW
      pinMode(gpio_led, OUTPUT);
      digitalWrite(gpio_led, LOW);

      // Attach interrupt to tachometer wire
      pinMode(gpio_tach, INPUT_PULLUP);
      evenOdd = 0;
      attachInterrupt(digitalPinToInterrupt(gpio_tach), tach_pulse_handler, RISING);

      // Configure hardware timer
      pulse_timer = timerBegin(0, 80, true);
      timerAttachInterrupt(pulse_timer, &pulse_timer_handler, true);
      timerAlarmWrite(pulse_timer, 10, true /* == autoreload */);
      timerAlarmEnable(pulse_timer);
    }
};

class FanStrobeLEDEvenOddToggleComponent: public Component, public Switch {
  public:
    void write_state(bool state) override {
      evenOdd = !evenOdd;
      publish_state(state);
    }
};

class FanStrobeLEDDelayComponent: public Component, public FloatOutput {
  public:
    void write_state(float state) override {
      strobeDelay = 1100*state;
    }
};

class FanStrobeLEDDurationComponent: public Component, public FloatOutput {
  public:
    void write_state(float state) override {
      strobeDuration = 100*state;
    }
};

ESP32 Timer ISR Not Allowed to Call Timer API

Conceptually I knew using delayMicroseconds() inside an interrupt service routine (ISR) was a bad idea. But I tried it anyway on an ESP32 and now I have a crash stack to prove it was indeed a bad idea. A solid reason to switch over to using hardware timers. My fan strobe LED project has two needs: the first delay is between tachometer wire signal and turning LED on (this delay may be zero) followed by a second delay before turning LED back off. Duration of both of these delays vary in order to produce desired visual effects. Translating this concept directly into timers results in this pseudocode:

  1. Interrupt handler triggered by tachometer signal. Starts timer that calls step 2 when first it ends.
  2. Interrupt handler triggered by expiration of first delay. Turn on LED and start timer that calls step 3 when it ends.
  3. Interrupt handler triggered by expiration of second delay, turn off LED.

Unfortunately, that direct translation doesn’t work. At least not with ESP32 Arduino Core Timer APIs. The timer alarm callback (“call step 2” and “call step 3” above) is specified at the time an alarm timer is configured, and not something I can change in between steps 2 and 3 above. Furthermore, I had no luck changing the timer duration in between calls, either. Either the change has no effect (duration doesn’t change) or the timer alarm interrupt stops triggering entirely. The only way I got something resembling my initial pseudocode is to tear down and rebuild a timer on every call to step 1, then do it again on step 2, and repeat.

That code felt wrong, and it didn’t take long for me to see concrete evidence it was wrong. I could adjust my delay parameters a few times, but within 2-3 minutes of running this sketch my ESP32 would crash with the following stack:

WARNING Found stack trace! Trying to decode it
WARNING Decoded 0x40088dcc: invoke_abort at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/esp32/panic.c:715
WARNING Decoded 0x40089049: abort at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/esp32/panic.c:715
WARNING Decoded 0x40089dbf: xQueueGenericReceive at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/queue.c:2038
WARNING Decoded 0x400f1001: removeApbChangeCallback at /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-cpu.c:224
WARNING Decoded 0x400817f9: timerEnd at /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-timer.c:174
WARNING Decoded 0x40081095: pulse_off_handler() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/gpio.h:62
WARNING Decoded 0x400814c9: __timerISR at /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-timer.c:174
WARNING Decoded 0x400874cd: _xt_lowint1 at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/xtensa_vectors.S:1154
WARNING Decoded 0x4008568d: esp_dport_access_int_abort at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/esp32/dport_access.c:216
WARNING Decoded 0x40087e63: spi_flash_enable_interrupts_caches_and_other_cpu at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/cache_utils.c:203
WARNING Decoded 0x4008857a: spi_flash_guard_end at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/flash_ops.c:172
 (inlined by) spi_flash_read at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/flash_ops.c:664
WARNING Decoded 0x401659c9: nvs::nvs_flash_read(unsigned int, void*, unsigned int) at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/nvs_flash/src/nvs_ops.cpp:76
WARNING Decoded 0x40164289: nvs::Page::readEntry(unsigned int, 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 0x401642fd: nvs::Page::eraseEntryAndSpan(unsigned int) at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/nvs_flash/src/nvs_page.cpp:871
WARNING Decoded 0x40164979: nvs::Page::eraseItem(unsigned char, nvs::ItemType, char const*, unsigned char, nvs::VerOffset) at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/nvs_flash/src/nvs_page.cpp:871
WARNING Decoded 0x4016337a: nvs::Storage::eraseMultiPageBlob(unsigned char, char const*, 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 0x40163751: 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 0x40162b3b: 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 0x400d8407: esphome::esp32::ESP32Preferences::sync() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/components/esp32/preferences.cpp:124
WARNING Decoded 0x400e35dd: esphome::preferences::IntervalSyncer::on_shutdown() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/gpio.h:62
WARNING Decoded 0x400e3665: 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 0x400e34e6: 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 0x400e1319: esphome::Application::loop() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/application.cpp:69
WARNING Decoded 0x400e391e: loop() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/gpio.h:62
WARNING Decoded 0x400f0e29: loopTask(void*) at /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/main.cpp:23
WARNING Decoded 0x4008a05a: vPortTaskWrapper at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/port.c:355 (discriminator 1)

I’m not sure what removeApbChangeCallback() meant. I found the code, but there were no code comments explaining what “Apb” is. A general web search found one potential explanation of “Advanced Peripheral Bus“. Whatever it is, it was called as part of a FreeRTOS queue receive. This was a surprise. I know FreeRTOS queues can be used from inside an ISR to schedule work to be performed outside an ISR, but that would be an ISR-safe queue send, not a receive. Hitting a receive path inside an ISR feels wrong.

Looking further down the call stack, I see NVS (nonvolatile storage) calls like I saw when I failed to use delayMicroseconds() on the stack. Why are storage APIs being called? I didn’t look into it earlier, hoping I could avoid the problem by moving to timers, but it’s clear I need to better understand what’s going on. Poking around in ESPHome code, I believe this is trying to save state of my parameters so it could be restored in case of system reset or shutdown. If so, I should be able to deactivate such behavior by setting restore_mode parameter to ALWAYS_OFF.

WARNING Found stack trace! Trying to decode it
WARNING Decoded 0x400f0ea8: initApbChangeCallback at /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-cpu.c:224
WARNING Decoded 0x400817e2: timerBegin at /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-timer.c:174
WARNING Decoded 0x4008110c: tach_pulse_handler() at /config/esphome/.esphome/build/fan-strobe-led/src/esphome/core/gpio.h:62
WARNING Decoded 0x40081185: __onPinInterrupt at /data/cache/platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-gpio.c:220
WARNING Decoded 0x400874dd: _xt_lowint1 at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/xtensa_vectors.S:1154
WARNING Decoded 0x40087c65: spi_flash_op_block_func at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/spi_flash/cache_utils.c:203
WARNING Decoded 0x40084f17: ipc_task at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/esp32/ipc.c:62
WARNING Decoded 0x4008a06a: vPortTaskWrapper at /home/runner/work/esp32-arduino-lib-builder/esp32-arduino-lib-builder/esp-idf/components/freertos/port.c:355 (discriminator 1)

The good news is that did indeed stop writes to nonvolatile storage. The bad news is that did not avoid crashing my ESP32. It’s still crashing on an Apb change callback, though this time without any NVS calls on the stack. (There’s still one line of spi_flash_op_block_func, though.) What’s going on? I decided to look deeper. Reading documentation for hardware timers in ESP-IDF (one layer below ESP32 Arduino Core) I saw the explanation: “It [ESP32 timer ISR callback] cannot call other timer APIs.” Aha! My timer ISR is not allowed to adjust and kick off another timer. This makes my initial pseudocode impossible to implement, and I have to devise a different approach.

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)

Wemos D1 Mini ESP32 Derivative

It was fun to build a LED strobe light pulsing in sync with a cooling fan’s tachometer wire. After the initial bare-bones prototype I used ESPHome to add some bells and whistles. My prototype board is built around a Wemos D1 Mini module, but I think I’ve hit a limit of hardware timers available within its onboard ESP8266 processor. The good news is that I could upgrade to its more powerful sibling ESP32 with this dev board, and its hardware compatibility means I don’t have to change anything on my prototype board to use it.

The most puzzling thing about this particular ESP32 dev format is that I’m not exactly sure where it came from. I found it as “Wemos D1 Mini ESP32” on Amazon where I bought it. (*) I also see it listed by that name as well as “Wemos D1 Mini32” on its Platform.IO hardware board support page, which helpfully links to Wemos as “Vendor”. Except, if we follow that link, we don’t see this exact board listed. The Wemos S2 Mini is very close, but I see fewer pins (32 vs. 40) and their labels indicate a different layout. Did Wemos originate this design, but since removed it for some reason? Or did someone else design this board and didn’t get credit for it?

Whatever the history of this design, putting a unit (right) next to the Wemos D1 Mini design (left) shows it is a larger board. ESP32 has a much greater number of I/O pins, so this module has 40 through-holes versus 16.

Another contributing factor for larger size is the fact all components are on a single side of the circuit board, as opposed to having components on both sides of the board. It leaves the backside open for silkscreened information for pins. Some of the pins were labeled with abbreviations I don’t understand, but probing those lines found the following:

  • Pins connected to onboard flash and not recommended for GPIO use: CMD (IO11), CLK (IO6) SD0/SDD (IO7) SD1 (IO8) SD2 (IO9) and SD3 (IO10).
  • TDI is IO12, TDO is IO15, TCK is IO13, and TMS is IO34. When not used as GPIO, these can be used as JTAG interface pins for testing and debugging.
  • SVP is IO36, and SVN is IO39. I haven’t figured out what “SVP” and “SVN” might mean. These are two of four pins on an ESP32 that are input-only. “GPI” and not “GPIO”, so to speak. (IO34 and IO35 are the other input-only pins.)

Out of these 40 pins, two are labeled NC meaning not connected, leaving 38 connections. An Espressif ESP32 DevKitC has 38 pins and it appears the same 38 are present on this module, including the aforementioned onboard flash pins and three grounds. But physical arrangement has been scrambled relative to the DevKitC to arrive at this four-column format. What was the logic behind this rearrangement? The key insight for me was that a subset of 16 pins were highlighted with white. They were arranged to be physically and electrically compatible with the Wemos D1 Mini ESP8266:

  • Reset pin RST is in the same place.
  • All power pins are at the same places: 5V/VCC, 3.3V, and one of the ground pins.
  • Serial communication lines up with UART TX/RX at the same places.
  • ESP32 can perform I2C on most of its GPIO pins. I see many examples use the convention of pins 21 for SDA and pin 22 for SCL, and they line up here with ESP8266 D1 Mini’s I2C pins D2 and D1.
  • Same deal with ESP32 SPI support: many pins are supported, but convention has uses four pins (5, 18, 19, and 23) so they’ve been lined up to their counterparts on ESP8266 D1 Mini.
  • ESP8266 has only a single pin for analog-to-digital conversion. ESP32 has more flexibility and one of several ADC-supported pins was routed to the same place.
  • ESP8266 supported hardware sleep/wake with a single pin. Again ESP32 is more flexible and one of the supported pins was routed to its place.
  • There’s a LED module hard-wired to GPIO2 onboard the ESP8266MOD. ESP-WROOM-32 has no such onboard LED, so there’s an external LED wired to GPIO2 adjacent to an always-on power LED. Another difference from ESP8266: this LED illuminates when GPIO2 is high instead of ESP8266’s onboard LED which shines when GPIO2 is low.

This is a nice piece of backwards-compatibility work. It means I can physically plug this board’s white-highlighted subset of 16 pins into any hardware expecting the Wemos D1 Mini ESP8266 board, like its ecosystem of compatible shields. Physically it will hang out the sides, but electrically things should work. Software will still have to be adjusted and recompiled for ESP32, changing GPIO numbers to match their new places. But at least those pins are all capable of the digital and analog IO pins in those places.

The only downside I see with this design? It is no longer breadboard-friendly. When all pins are soldered, all horizontally adjacent pins would be shorted together and that’s not going to work. I’m specifically eyeing one corner where reset (RST) would be connected to ground (GND). I suppose we could solder pins to just the compatibility subset of 16 pins and plug that into a breadboard, but this module is too wide for a standard breadboard. A problem shared with another ESP32 dev board format I’ve used.

And finally, like my ESP8266 Wemos D1 Mini board, these came without any pins soldered to the board. Three different types were included in the bag: pins, or sockets, or passthrough. However, the bag only included enough 20 pins of each type, which isn’t enough for all 40 pins. Strange, but no matter. I have my own collection of pins and sockets and passthrough connectors if I want to use them. And my most recent ESP32 project didn’t need these pins at all. In fact, I had to unsolder the pins that module came with. An extra step I wouldn’t need to do again, now I have these “Wemos D1 Mini ESP32” modules on hand for my experiments.


(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Fan Strobe LED Adjustments via ESPHome

After proving my fan blade LED strobe idea worked with a minimalist Arduino sketch, I ported that code over as an ESPHome custom component. I thought it would be good practice for writing ESPHome custom components and gain Home Assistant benefit of adding adjustments in software. For me it was easier to create an analog control with a few lines of YAML and C, than it would be to wire up a potentiometer. (I recognize it may be the reverse for people more comfortable with hardware than software.)

The first adjustment “Fan Speed Control” actually did not require custom component at all: making the fan speed adjustable utilized the built-in fan speed component. In my minimalist sketch the fan is always running at full speed, now I can adjust fan speed and verify that the strobe logic indeed stays in sync with the fan speed. Meaning the strobe keeps these fan blades visually frozen in the same place regardless of their rotational speed.

The first custom output component adjustment “Strobe Duration” changes the duration of LED strobe pulse, from zero to 1000 microseconds. For this LED array, I found values under 100 to be too dim to be useful. The dimmest usable value is around 100, and things seem to work well up to 300. I start detecting motion blur above 300, and things are pretty smeared out above 600.

The next addition is a toggle switch “Strobe Tachometer Toggle”. Because the tachometer signal pulses twice per revolution, I ignore every other pulse. But that means a 50% chance the code will choose to trigger on the wrong pulse, resulting in a visually upside-down image. When this happens, this toggle allows us to flip to trigger on the opposing pulse, flipping the frozen visual right-side up.

The final custom component adjustment “Strobe Delay” adds a delay between triggered by tachometer wire and illumination of LED strobe. This changes the point at which the fan is visually frozen by the strobe light. Dynamically adjusting this value makes it look like the fan blade is slowly rotating position even though it is actually rotating at ~1200RPM. I think it is a fun effect, but to fully take advantage of that effect I will need longer delays, which means finding how I could move that work outside of my interrupt service routine. Inside the ISR I should set up a hardware timer for this delay and turn on LED when timer expires. I can then use the same mechanism to set a timer for LED duration and turn off LED when that timer expires.

Unfortunately, there are only two hardware timers on an ESP8266, and they are both spoken for. One runs WiFi, the other runs Arduino core for things like millis(). To explore this idea further, I will need to move up to an ESP32 which has additional hardware timers exposed to its Arduino core. And if I choose to explore that path, I don’t even need to redo my circuit board: there exists a (mostly) drop-in ESP32 upgrade for anything that runs a Wemos D1 Mini.



YAML Excerpt:

esphome:
  includes:
    - led_strobe_component.h

output:
  - platform: esp8266_pwm
    pin: 14
    id: fan_pwm_output
    frequency: 1000 Hz
  - platform: custom
    type: float
    lambda: |-
      auto led_strobe = new LEDStrobeComponent();
      App.register_component(led_strobe);
      return {led_strobe};
    outputs:
      id: strobe_duration_output
  - platform: custom
    type: float
    lambda: |-
      auto led_strobe_delay = new LEDStrobeDelayComponent();
      App.register_component(led_strobe_delay);
      return {led_strobe_delay};
    outputs:
      id: strobe_delay_output

fan:
  - platform: speed
    output: fan_pwm_output
    id: fan_speed
    name: "Fan Speed Control"

light:
  - platform: monochromatic
    output: strobe_duration_output
    id: strobe_duration
    name: "Strobe Duration"
    gamma_correct: 1.0
  - platform: monochromatic
    output: strobe_delay_output
    id: strobe_delay
    name: "Strobe Delay"
    gamma_correct: 1.0

switch:
  - platform: custom
    lambda: |-
      auto even_odd_flip = new LEDStrobeEvenOddComponent();
      App.register_component(even_odd_flip);
      return {even_odd_flip};
    switches:
      name: "Strobe Tachometer Toggle"

ESPHome custom component file led_strobe_component.h:

#include "esphome.h"

volatile int evenOdd;
volatile int strobeDuration;
volatile int strobeDelay;

IRAM_ATTR void tach_pulse_handler() {
  if (0 == evenOdd) {
    evenOdd = 1;
  } else {
    delayMicroseconds(strobeDelay);
    digitalWrite(13, HIGH);
    delayMicroseconds(strobeDuration);
    digitalWrite(13, LOW);
    evenOdd = 0;
  }
}

class LEDStrobeComponent : public Component, public FloatOutput {
  public:
    void setup() override {
      // LED power transistor starts OFF, which is LOW
      pinMode(13, OUTPUT);
      digitalWrite(13, LOW);

      // Attach interrupt to tachometer wire
      pinMode(12, INPUT_PULLUP);
      evenOdd = 0;
      attachInterrupt(digitalPinToInterrupt(12), tach_pulse_handler, RISING);

      strobeDuration = 200;
    }

    void loop() override {
    }

    void write_state(float state) override {
      // Multiply by 1000 = strobe duration from 0 to 1ms.
      strobeDuration = 1000 * state;
    }
};

class LEDStrobeEvenOddComponent: public Component, public Switch {
  public:
    void write_state(bool state) override {
      evenOdd = !evenOdd;
      publish_state(state);
    }
};

class LEDStrobeDelayComponent: public Component, public FloatOutput {
  public:
    void write_state(float state) override {
      strobeDelay = 1000*state;
    }
};

LED Strobing to Fan Speed Signal

The reason I cared about power-on response time of a salvaged LED array is because I wanted to use it as a strobe light shining on a cooling fan pulsing once per revolution. Historically strobe lights used xenon bulbs for their fast response, as normal incandescent bulbs were too slow. This LED array used to be a battery-powered work light with no concern of reaction time, but LEDs are naturally faster than incandescent. Is it fast enough for the job? PC case fan specifications usually range from the hundreds to low thousands of RPM. Using 1200RPM as a convenient example, that means 1200/60 seconds per minute = 20 revolutions per second. Pulsing at 20Hz should be easy for any LED.

For the hardware side of controlling LED flashes, I used a 2N2222A transistor because I had a bulk bag of them. They are usually good for switching up to 0.8 Amps of current. I measured this LED array and it drew roughly 0.3 Amps at 11.3V, comfortably within limits. I just need to connect this transistor’s base to a microcontroller to toggle this light on and off. For this experiment I repurposed the board I had built for the first version of my bedstand fan project. I unsoldered the TMP36 sensor to free up space for 2N2222A and associated LED power wire connector.

This board also had the convenience of an already-connected fan tachometer wire. My earlier project used it for its original purpose of counting fan RPM, but now I will use those pulses to trigger a LED flash. Since timing is critical, I can’t just poll that signal wire and need a hardware interrupt instead. Within Arduino framework I could use attachInterrupt() for this purpose and run a small bit of code on every tachometer wire signal pulse. Using an ESP8266 for this job had an upside and a downside. The upside is that interrupts could be attached to any available GPIO pin, I’m not limited to specific pins like I would have been with an ATmega328P. The downside is that I have to use an architecture-specific keyword IRAM_ATTR to ensure this code lives in the correct part of memory, something not necessary for an ATmega328P.

Because it runs in a timing-critical state, ISR code is restricted in what it can call. ISR should do just what they absolutely need to do at that time, and exit allowing normal code to resume. So many time-related things like millis() and delay() won’t work as they normally would. Fortunately delayMicroseconds() can be used to control duration of each LED pulse, even though I’m not supposed to dawdle inside an ISR. Just for experiment’s sake, though, I’ll pause things just a bit. My understanding of documentation is as long as I keep the delay well under 1 millisecond (1000 microseconds) nothing else should be overly starved for CPU time. Which was enough for this quick experiment, because I started noticing motion blur if I keep the LED illuminated for more than ~750 microseconds. The ideal tradeoff between “too dim” and “motion blurred” seems to be around 250 microseconds for me. This tradeoff will be different for every different combination of fan, circuit, LED, and ambient light.

My minimalist Arduino sketch for this experiment (using delayMicroseconds() against best practices) is publicly available on GitHub, as fan_tach_led within my ESP8266Tests repository. Next step in this project is to move it over to ESPHome for bells and whistles.

NEXTEC Work Light LED Array

While experimenting with 5V power delivery over USB-C, I thought of an experiment that will utilize my new understanding of computer cooling fan tachometer wire. For this experiment I will need a light source in addition to the fan itself. I wanted a nice and bright array of many LEDs, and preferably something already set up to run at around 12V so I wouldn’t have to add current-limiting resistors. A few years ago, I took apart a Sears Craftsman NEXTEC work light for its battery compartment. Now it is the LEDs turn to shine. That battery pack used three lithium 18650 cells in series, so it is in the right voltage range.

I think there was only a single fastener involved in this LED array, and it was already gone from teardown earlier so now everything slid apart easily.

I like the LED housing and intend to use it, but I wanted to take a closer look at the LED array.

I confirm the 24 white LEDs visible before disassembly, and there’s nothing else hiding on this side of the board, just the power supply wires looping through for a bit of strain relief. We can also see that Chervon Group was the subcontractor who produced this device to be sold by Sears under their Craftsman branding.

Everything is on the backside of this circuit board. From here we can see the 24 LEDs are arranged in 12 parallel sets of 2 LEDs in series, each set with a 240 Ohm resistor between them. Beyond that, to lower left I see a cluster of components and I’m not sure what they do. My best guess is battery over-discharge protection. Perhaps the component marked ZD1 is a Zener diode to detect voltage threshold, working with power transistor Q1 to cut power if battery voltage drops too low.

The most important thing is that I don’t see a microcontroller that requires time to boot up. I will be pulsing this LED array rapidly and want minimal delay between power and illumination. If delay proves to be a problem, I’ll try bypassing those lower-left bits: Relocate the power supply wire (brown wire, connects between markings R1 and ZD1) so it connects directly to the LED supply plane. Either to the transistor tab adjacent to the Q1 marking, or directly to the high end of any of those 12 parallel LED strings. But I might not need to perform that bypass. I will try my experiment with this circuit board as-is.

Resistors Negotiate 5V Power in USB Type C

Thanks to prompting by a comment, I am picking up where I left off trying to supply power over a USB-C cable. I love the idea of USB Power Delivery, the latest version covers transferring up to 240W over a USB Type-C cable. But that power also comes with complexity, and I didn’t want to figure out how to establish a power delivery contract when my project really just wanted five volts. Fortunately, the specification also describes a low-complexity way to manage 5V power over USB Type-C. But I had to be confident I was dealing with the correct wires, so I probed wiring with a small breakout board. (*) I confirmed that the four red wires were VBUS, the green and white wires were indeed the differential data pairs, and the mystery yellow wire is the VCONN or CC (cable configuration) wire on the same side.

Ah, yes, that “same side” was an interesting find. USB Type-C is physically shaped so there’s no “upside-down” way to insert the plug, with symmetric wires. However, that also means each side has a set of D+/D-/CC wires, and a USB Type-A to Type-C adapter only connects to one side. It is up to the Type-C device to check both sides.

In my previous experiment I learned that just connecting +5V to red and ground to black was enough to be recognized as a power source by some Type-C device but not my Pixel 3a phone. I found multiple guides that said to connect a 56kΩ pull-up resistor between CC and VBUS, but I wanted to know a little bit more without diving into the deep end of USB specifications. I found a very accessible post on Digi-Key forums describing the details of 5V @ 3A = 15W power over Type-C. Which is itself a simplified version of a much more complex Digi-Key overview of USB power.

Like several other guides, it mentioned the resistors on both ends of the Type-C cable, but it also had this phrase: “Together they form a voltage divider” which was my “A-ha!” moment. It allowed components to negotiate 5V power delivery without a digital communication protocol. We just need a resistor on either side: one for the provider to indicate the amount available, and the other by the consumer to indicate its desired consumption. Then we just need an ADC to measure voltage value of the resulting voltage divider, and we’ll know the safe power level.

When I added the 56kΩ pull-up resistor to my circuit, my Pixel 3a lit up with “Charging slowly”. I thought it was successfully charging at 500mA, but it wasn’t. Over the next half hour, its battery level actually dropped! I put the circuit under a USB power meter(*) and found it was only drawing a feeble 40mA. That meter also told me why: my circuit had supplied only 4.3V because I had a transistor in the circuit for power control and it dropped 0.7V from collector to emitter. This was why the power level was so low: a pull-up resistor to 4.3V was below the voltage threshold for 500mA power.

In order to create a microcontroller-switchable 5V (not 4.3V) power supply, I went with my most expedient option of using another voltage regulator with an enable pin connected to what used to be the transistor base. This raised the divided voltage within 500mA range, and finally the Pixel 3a started charging at that rate as confirmed by the USB power meter. And as an experiment to confirm my understanding, I dropped pull-up resistance down to 22kΩ. This raised the resulting voltage at the divider, and USB power meter reported that my Pixel 3a started drawing 1.5A. My buck converter is rated to handle this output and this way the phone charges faster.


(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Notes on “Make: Bluetooth” by Allan, Coleman, and Mistry

As a part of a Humble Bundle package of books published by Maker Media, I had the chance to read through Make: Bluetooth (*) by Alasdair Allan, Don Coleman & Sandeep Mistry. This book covers a series of projects that can be built by the Make audience: by assembling development breakout boards and discrete components on prototype breadboards.

One of the first things this book covers is that these projects all use Bluetooth LE and not “Classic” Bluetooth. They share two things: (1) they both communicate over 2.4GHz range of RF spectrum, and (2) they are both administered by the Bluetooth Special Interest Group. Other than that, they are completely different wireless communication protocols named for maximum customer confusion.

For each project, this book provides a detailed step-by-step guide from beginning to end, covering just what we need for each project. This is both the book’s greatest strength and leads to my biggest criticism. Minimizing extraneous information not pertinent to the current project avoids confusing beginners, but if that beginner wants to advance beyond being a beginner, this book doesn’t provide much information to guide their future study. This problem gets worse as the book ages, because we’re not given the background information necessary to adapt. (The book is copyrighted 2016, this post is written in 2022.)

The first example is the Bluetooth LE module they used for most of the book: Adafruit product #1697, Bluefruit LE – Bluetooth Low Energy (BLE 4.0) – nRF8001 Breakout. The book never covers why this particular BLE module was chosen. What if we can’t get one and need to find a substitute? We’re not just talking about a global chip shortage. It’s been years since the book was written and Adafruit has discontinued product #1697. Fortunately, Adafruit is cool, and added a link to their replacement products built around the nRF51822 chip. But if Adafruit hadn’t done that, the reader would have been up a creek trying to figure out a suitable replacement.

Another example was the phone interaction side of this book, which is built using Adobe PhoneGap to produce apps for either iOS or Android phones. And guess what, Adobe has discontinued that product as well. While most of the codebase is also available in the open-source counterpart Apache Cordova, Adobe’s withdrawal from the project means a big cut of funding and support. A web search for Apache Cordova will return many links titled “Is Apache Cordova Dead?” Clearly the sentiment is not optimistic.

The Bluetooth LE protocol at the heart of every project in this book was given similarly superficial coverage. There were mentions of approved official BLE characteristics, and that we are free to define our own characteristic UUID. But nothing about how to find existing BLE characteristics, nor rules on defining our own UUID. This was in line with the simplified style of the rest of the book, but at least we have a “Further Reading” section at the back of the book pointing to two books:

  1. Getting Started with Bluetooth Low Energy (*) by Townsend, Cufí, Akiba, and Davidson.
  2. Bluetooth Low Energy: The Developer’s Handbook (*) by Heydon

I like the idea of a curated step-by-step guide to building projects with Bluetooth LE, but when details are out of date and there’s nothing to help the reader adapt, such a guide is not useful. I decided not to spend the time and money to build any of these projects.


(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Fan Blade Counter Success: Infrared LED Photovoltaic Effect

I wanted to rig up a fan blade counter as an oscilloscope exercise: set up an emitter and hook the receiver up to the oscilloscope and count the spinning blades via interruption between emitter and receiver. I had a box of consumer infrared remote-control emitter and receivers, but those receivers were too smart and sophisticated for my project. I needed something simpler.

Looking at what I (literally) have on hand, I wondered if the oscilloscope is sensitive enough to pick up a photovoltaic effect on these IR emitters. I remembered that every piece of silicon responds to light to some degree. This fact caused problems like the first run of Raspberry Pi 2 that resets when exposed to bright light, which is why they’re usually shrouded in black plastic to block light. I already had an IR emitter set up to pulse at 38kHz, so I placed another emitter pointed face-to-face wired directly to an oscilloscope probe: the probe connected to LED anode and corresponding ground pin connected to LED cathode.

The answer is: yes, the oscilloscope can pick up electrical activity on the IR emitter diode as it was stimulated by an identical IR emitter. It’s not a very clean square wave, with a sharp climb and a slower decay, but it’s clearly transmitting the 38kHz signal. I don’t know how to build a circuit that triggers behavior based on such small voltages, but right now I don’t have to. The exercise is measuring fan blades and see how it correlates to fan tachometer signal on a multichannel oscilloscope, and I have an effect strong enough to be picked up by said oscilloscope.

The emitter LED was removed from the 38kHz circuit. Now it lives on the breadboard power rail, so it is always on. The other emitter LED (acting as receiver) was placed on the other side of the fan. A separate set of oscilloscope probes were connected to the fan tachometer wire. I gave the fan power, and saw the graph I had hoped to get:

The yellow square wave is the fan tachometer signal, and the rougher purple wave is the receiving LED. There are seven blades on this particular fan, so seven purple cycles would correspond to one revolution of the fan. I count seven purple cycles for every two yellow cycles, finally confirming that the cooling fan tachometer signal goes through two full cycles on every fan revolution.

Fan Blade Counter Fail: IR Receiver is not Simple Phototransistor

After a successful Lissajous experiment with my new oscilloscope, I proceeded to another idea to explore multichannel capability: a fan blade counter. When I looked at the tachometer wire on a computer cooling fan, I could see a square wave on a single-channel oscilloscope. But I couldn’t verify how that corresponded to actual RPM, because I couldn’t measure the latter. I thought I could set up an optical interrupter and use the oscilloscope to see individual fan blades interrupt the beam as they spun. Plotting the tachometer wire on one oscilloscope channel and the interrupter on another would show how they relate to each other. However, my first implementation of this idea was a failure.

I needed a light source, plus something sensitive to that particular light, and they need to be fast. I have some light-sensitive resistors on hand, but their reaction times are too slow to count fan blades. A fan could spin up to a few thousand RPM and a fan has multiple blades. So, I need a sensor that could handle signals in the tens of kilohertz and up. Looking through my stock of hardware, I found a box of consumer infrared remote-control emitter and receiver modules (*) from my brief exploration into infrared. Since consumer IR usually modulate their signals with a carrier frequency in the ballpark of 38kHz, these should be fast enough. But trying to use them to count fan blades was a failure because I misunderstood how the receiver worked.

I set up an emitter LED to be always-on and pointed it at a receiver. I set up the receiver with power, ground, and its signal wire connected to the oscilloscope. I expected the signal wire to be at one voltage level when it sees the emitter, and at another voltage level when I stick an old credit card between them. Its actual behavior was different. The signal was high when it saw the emitter, and when I blocked the light, the signal is… still high. Maybe it’s only setup to work at 38kHz? I connected the emitter LED to a microcontroller to pulse it at 38kHz. With that setup, I can see a tiny bit of activity with my block/unblock experiment.

Immediately after I unblocked the light, I see a few brief pulses of low signal before it resumed staying high. If I gradually unblocked the light, these low signals stayed longer. Even stranger, if I do the opposite thing and gradually blocked the light, I also get longer pulses of low signal.

Hypothesis: this IR receiver isn’t a simple photoresistor changing signal high or low depending on whether it sees a beam or not. There’s a circuit inside looking for a change in intensity and the signal wire only goes low when it sees behavior that fits some criteria I don’t understand. That information is likely to be found in the datasheet for this component, but such luxuries are absent when we buy components off random Amazon lowest-bidder vendors instead of a reputable source like Digi-Key. Armed with microcontroller and oscilloscope, I could probably figure out the criteria for signal low. But I chose not to do that right now because, no matter the result, it won’t be useful for a fan blade counter. I prefer to stay focused on my original goal, and I have a different idea to try.


(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

LRWave Audio Under Multichannel Oscilloscope

When I read through the user’s guide for my new 4-channel oscilloscope, one of the features that jumped out at me was “XY mode”. Normally signals are displayed with their voltage on the vertical axis and time on the horizontal axis. But XY mode allows us to plot one channel on the vertical axis against another channel on the horizontal axis. Aside from its more technical applications, people have used this to display vector art on their oscilloscope. And the simplest vector art are Lissajous curves, which Emily Velasco introduced me to. We’ve had several projects for Lissajous curves including this old CRT projection TV tube.

Motivated by these Lissajous experiments, I created my software project LRWave to give us a basic function generator using our cell phones. Or really anything that has an audio output jack and a web browser. It’s not nearly as good as a real function generator instrument, but I didn’t really know how far away from “good” it is. Now that I have an oscilloscope, I can look closer.

Digging into my pile of discarded electronics, I found a set of stereo headphones. Cutting its cable, I pulled out three wires corresponding to left audio, right audio, and common ground reference.

These wire strands had an insulating coating that had to be removed. Hot solder seemed to work well melting them off, also conveniently giving me a surface to attach a short segment of wire for oscilloscope probes to hook onto. Now I can see what LRWave output looks like under an oscilloscope.

It’s not very pretty! Each vertical grid on this graph is 20mV according to the legend on the top right. The waveform is far from crisp, smearing across a range of about 50mV. This is very bad when the maximum and minimum levels are only separated by roughly 120mV. The narrow range was because my phone was set at very low audio volume.

Cranking my phone volume up to maximum increased the amplitude to about 1.5V, so the maximum and minimum levels are separated by about 3V. (Each vertical grid is now 500mV.) With this range, the 50mV variation is a lot less critical and we have a usable sine wave. Not as good as a real function generator, but usable. Also, actual performance will vary depending on the audio hardware. Different cell phones/tablets/computers will output audio to varying levels of fidelity.

This is as far as I could have gone with my cheap DSO-138 single-channel oscilloscope, but now that I have more than one channel, I can connect both stereo audio channels to the oscilloscope and activate XY mode to plot them against each other and get some nice Lissajous curves on my oscilloscope screen.

Yeah, that’s what I’m talking about! I expect this line would be finer (thinner) if I used a real wave generation instrument instead of the headphone output jack of a cell phone, but it’s more than enough for a fun graph. Onwards to my next multichannel oscilloscope experiment.

Notes on Siglent SDS1104X-E Oscilloscope User’s Guide

I have some basic ideas on how to use an oscilloscope, but I’ve never had one of my own until I bought one during this year’s Amazon Prime Day sale. Given its complexity and cost, I thought it would be a good idea to invest some time into Reading The Fine Manual. This did not start well, as there was only a Quick Start Guide in the box, featuring this particular gem:

These symbols may appear on the product: (But we won’t tell you what they mean!) Thanks, guys. Despite such minor mistakes, the quick start guide seems fine if perfunctory. I was moderately annoyed that they used the same manual for two-channel and four-channel versions of this scope, so I would occasionally look at something that made no sense until I realize it was about the two-channel. That annoyance aside, I learned valuable things like adjusting probe compensation as part of unpacking and initial setup (they were all slightly under-compensated but easily resolved with the procedure) but most of the other descriptions assumed I already knew how to use an oscilloscope. I was worried until I saw a note saying I could find more information in the User Manual.

Okay! A real User Manual exists, even if it isn’t in the box. I went hunting online and found my answer on Siglent NA (North America?) document repository where I could find the User Manual (and many other guides) in PDF format under the SDS1000X-E-Series section. It has the same annoyance of using one manual for both 2- and 4-channel versions, but now with a lot more useful detail.

  • One valuable thing I learned and need to keep in mind is that most knobs on this oscilloscope are like the quadrature encoder knob I took apart: there is a button press in addition to rotation. If I’m poking around looking for a feature, it might be a knob press.
  • I like the idea of the “Auto Setup” button. It is advertised to looks at the channel’s signal and choose an appropriate vertical and horizontal scaling. Sounds like a counterpart to “auto ranging” capability on a multimeter, I hope it will turn out to be as useful as it sounds.
  • These scopes came with probes that have a switch to toggle between 1X and 10X attenuation. It appears the probe has no way to communicate its current setting to the scope, I have to tell the scope. Something to keep in mind and check when things make no sense.
  • When I zoom out to a longer timescale, there’s a threshold where the cheap DSO-138 would automatically switch to showing data in a horizontal scrolling display. After reading this user’s guide I know it is called “Roll Mode” (Page 34) here and it’s something I can choose to toggle on/off with a button, independent of timescale.
  • I frequently try to adjust display timescale on the DSO-138 so I could zoom in and out to look at various features. Now, I have an actual zoom function (page 35) so I can keep the longer timescale waveform on screen simultaneously with a short timescale subset of the same wave.
  • DSO-138 would frequently fail to show fast blips. If I need to see peaks of very brief signals, I can choose to display “Peak Detection” mode. (page 46).
  • Typically having multiple channels mean multiple lines all graphed against time, but setting “Acquire” to “XY” (page 49) allows graphing one channel versus another instead of time. There will be some vector graphics fun with Lissajous curves in the near future.
  • It seems like half of the manual goes into depth on what each of the trigger modes do. I will need to re-read this section several times. Eventually I should be able to recognize which situations are best fit for certain trigger modes.
  • I was very excited to read about Video Trigger: it sounds like the oscilloscope knows what NTSC composite video signals should look like and can trigger on specific parameters or fields. Once I master this mode, I foresee it becoming extremely valuable for debugging my ESP32 composite video output library.
  • I had no idea “Measurements” (page 130) are something oscilloscopes can do now. So instead of reading the screen to see how much time is represented by an on-screen grid division, and calculating the period of a waveform, and from there calculating the frequency… now the scope has measurement tools to do all that math for us. Wow, fancy!

Judging by what I’ve learned from this User’s Guide, I’m very happy with the potential usefulness of my oscilloscope purchase. I hope it will prove to be actually useful as I learn to harness its abilities.

Finally Bought a Real Oscilloscope

An oscilloscope has been on my workbench wish list for years. I had been limping along with a degraded DSO-138 kit, occasionally wishing for something with more channels, or more bandwidth, or just the ability to measure voltage levels accurately. The key word being occasionally. I haven’t felt that I would use an oscilloscope enough to justify the expense. But when this year’s Amazon Prime Day rolled around, the memory of deciphering multi-channel signals was fresh on my mind, and I clicked “Buy” on a Siglent Technologies SDS1104X-E Oscilloscope. (*)

I had actually been eyeing a Rigol DS1054z(*), which had become a very popular entry-level oscilloscope for hobbyists. It is sold far and wide including my favorite vendor Adafruit, and its popularity meant plenty of online resources. From basic beginner’s “Getting Started” guides to hacks for unlocking features. Ah yes, those features. They were a big part of why I hadn’t bought the Rigol: it really sours me on a company when they would hold features for ransom even though all of the hardware is already present. Sure, I could visit questionable websites and generate codes to unlock those features without paying for them, but just the idea of buying from a company that would do such a thing turned me off.

While the Siglent oscilloscope did have a few paid upgrade features, they all involved additional hardware not already onboard. This made the concept more palatable for me. For reference, they were:

  • WiFi capability. The scope comes with an Ethernet port for network connectivity. Wireless comes at extra cost for the software upgrade in addition to the cost of a supported wireless adapter. I prefer wired Ethernet so I did not care.
  • AWG (arbitrary waveform generator) capability requires extra hardware in the form of Siglent SAG1021I. (~$175 *) So far, my waveform generation needs have been very basic. So basic, in fact, that I wrote a HTML app to cover my needs. I don’t think I’ll miss this feature.
  • MSO (multi-signal oscilloscope) capability requires a Siglent SLA1016 (~$330 *) which adds sixteen additional digital channels for logic analysis. Between the four channels already on board the oscilloscope (which already has logic analyzer functionality without paying to unlock as would a Rigol) and eight channels on my Saleae, I think I’ll be fine without the MSO add-on.

One thing that made me frown was that the AWG and MSO addons connect by something Siglent called “SBus”. Proprietary expansion ports are nothing new, but they chose to use a HDMI connector for the purpose. With a warning that plugging in actual HDMI devices would damage the oscilloscope. Gah! I see the economic advantage of using an existing high bandwidth connector already produced at high volume, but the resulting user experience sucks. Since I don’t plan on making any SBus upgrades, I will try my best to ignore that not-HDMI port.

This oscilloscope cost more than a Rigol DS1054z, though it is technically cheaper because many of Rigol’s paid add-ons were on the Siglent without extra charge. The Prime Day discount closed the price gap enough for me. Once it arrived, I dug into the manual eager to learn about my first real oscilloscope.


(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Asus Wireless Router (RT-N66R)

Taking apart a broken Ethernet switch reminded me that I have another piece of networking equipment that had been retired and sitting in a box. It was my Asus RT-N66R wireless router that I retired because its gigabit Ethernet ports started failing. After years of use I lost one port, and within two weeks I lost another port. I took those two consecutive port failures as a sign of impending total failure and quickly replaced it.

One thing that I remembered about this router was that it ran hot. Really, really hot. The power supply is rated at 19V DC @ 1.58A. That’s 30 watts of electricity pumped into a device without active cooling or even a metal case for passive heat dissipation. I wouldn’t be surprised if its failure can be traced to heat.

Four rubber feet on the bottom concealed four Philips-head fasteners. Once they were removed, though, the router was not inclined to come apart. Its top and bottom halves were held together by hooks inside these very robust loops. While undoing these assemblies, I noticed that plastic on one side of the router is much more brittle than the other side. Might this be a result of long-term heat exposure?

Removing the top exposed this aluminum heatsink up top, oddly situated far away from vents along the sides and bottom of the device. It explained why the top surface was so warm to the touch. Bare copper traces visible on the circuit board show signs of discoloration that may or may not be heat.

Towards one corner I saw two items of interest: a 4-pin header labeled with VCC, RX, TX, and GND that indicated an UART connection. And not far away, what looks like an empty microSD card slot. Asus routers run their fork of DD-WRT and it is possible to install custom builds of DD-WRT. I assume this UART and microSD would be handy for such enterprises. But we now live in the age of Raspberry Pi and BeagleBone so having a small network-capable Linux computer is not the novelty it once was. I’m not going to bother, especially as this hardware has started to fail.

Flipping the assembly over, I expected to see another finned heatsink for dissipating heat out of those ventilation slots on the bottom, but I only saw this sheet of metal. Likely aluminum.

And it’s not even a heatsink. There was no surface contact with any electronic components. It made contact only with six brass standoffs, none of which had any connection to the finned heatsink on the other side. If anything, the air trapped between it and the circuit board would have kept heat inside. I’m very mystified by the thermal engineering of this router.

Said heatsink were held on by four plastic retainers, two on each side. Here’s a closeup of one side. They have become very brittle and shattered when I tried to release them.

Once the heatsink was removed, we have our first sighting of thermal pads, but they sat on top of thin metal shields for radio-frequency (RF) isolation.

Prying off those shields revealed four more thermal pads, one on each of four important-looking chips.

The biggest thermal pad sits on the most important looking chip, a Broadcom BCM4706KPBG. A quick web search indicates this is a MIPS32 architecture CPU. Remaining three chips with thermal pads all have a Broadcom logo on top, but text information below that logo were very hard to read.

I saw no obvious damage that would explain why two out of four Ethernet ports failed, nor do I see anything I could conceivably salvage and reuse with my current skill level. Plastic enclosure will go to landfill, aluminum heat sink and sheet will head to metal recycle, and the circuit board will go to electronic waste disposal.