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.

Bedside Fan and Light V2

I took apart the Asiahorse Magic-i 120 V2 control hub and remote because I didn’t need them anymore: I could control its trio of fans with my own circuit board. How shall I wield such power? It was fun playing with 3D coordinate mapping with a Pixelblaze, but my most immediate need is a lot less exciting: combination bedside fan and light to replace my first bedside fan project. (Which didn’t have a light.)

For such a simple use, the power of a Pixelblaze is overkill. So, my board was modified to use an ESP8266 (Wemos D1 Mini module) as its brain, running ESPHome for integration with Home Assistant. In the Pixelblaze demo, the fans were always on. Now they will be controlled by that ESP8266 as well.

I’m still not settled enough on the idea to spend the time designing and 3D printing a proper frame, but at least I’ve put a bit more effort into this cardboard creation. I’m reusing a corner of a Newegg shipping box (I think it was the very box used to ship the Asiahorse fan bundle) and I’ve turned it inside out so at least I don’t have to stare at the Newegg logo every time I’m in bed.

Three large holes, one per fan, was cut from that cardboard for airflow. Twelve more holes, four per fan, were drilled into the cardboard for fan mounting screws. The physical assembly came together quickly, but there were a few more hiccups.

First problem was that FastLED, the addressable LED Arduino library used by ESPHome, is not compatible with the latest Arduino 3 framework at the time of this writing. Relevant ESPHome documentation page listed the issues as 1264 and 1322 filed against FastLED Github repository. Until they are resolved, ESPHome offers a workaround for compiling against Arduino framework 2.7.4. Which is what I did to run these WS2812 LEDs. For the first draft, I’m not going to use the addressable nature at all, just a single solid color. I think it would be cool to have a falling waterfall pattern on these lights, but that’ll be experimentation for later.

The second problem is that PWM control of fan speed results in an audible whine, probably an 1kHz whine which is the practical maximum speed for ESP8266 software PWM. The previous fan project removed the audible whine by adding a capacitor to smooth out voltage output. I could do that again, one capacitor per fan, but these fans run quietly enough at full speed I’m willing to skip PWM and have just on/off control.

The final problem is that I still want this fan to be responsive to temperature changes, turn itself off in the middle of the night when it got cool enough. I wasn’t happy with the TMP36 sensor I bought for the previous experiment, so now I’m going to try another sensor: the DS18B20.

RGB LED Fan Hub and Remote (Asiahorse Magic-i 120 V2)

I bought the Asiahorse Magic-i 120 V2 package from Newegg, which bundled three 120mm fans with embedded RGB LEDs with a hub and a remote to control those LEDs. Now that I have successfully created a control circuit for my own independent control of those fans and their LEDs, I no longer have any use for the hub and remote.

The remote has an array of 21 membrane buttons. Across the top, we can turn the LEDs “On” and “Off”. “Auto” will start running an animated pattern. Just below the “Off” button are brightness controls. S+ / S- controls the speed for animations, and M+ / M – cycles through different animated patterns. Bottom 12 buttons will show the selected solid color.

Top membrane is held on with moderately strong adhesive that could be peeled off, exposing the less interesting side of its circuit board.

Flipping the board over showed a single chip with its support components. There were no visible markings on the chip. Battery contact springs are at the bottom, the top features an infrared remote control LED emitter, and a few passive components in between.

After disassembling the remote, I started on the hub.

There were no exposed fasteners top or bottom. I pushed on the bottom sticker and felt the corners move.

The bottom sticker is glued on more tenaciously than the remote membrane keyboard and refused to come off cleanly. But at least those four Philips head fasteners are now exposed.

Not much to see on the bottom.

Flipping the circuit board over exposed… not many more chips than the remote. Most of the surface area are consumed by connectors all around the perimeter, and traces to connect them.

I’m glad to see fan connector pin labels are consistent with my reverse-engineered pinout table. A large component on this board appears to be a power transistor. I probed its pins and one of them is connected to all “F-” pins, so it is present for fan control. There are three sets of unused pads across the middle, provision for WS2812 LEDs wired in parallel with the fans. These three are chained together, left-to-right, with the leftmost LED receiving the same “DI” (data input) as all fans. When present, these three LEDs would act identically to first 3 out of 12 LEDs on board each fan.

There were a few other unpopulated pads on this circuit board, but there is one part I found fascinating for its absence: an infrared receiver like the one I found in a Roku. I don’t see one, and I don’t see solder pad provision for one. How could the hub receive IR remote signals without one? I know the remote and hub works together, so does this mean they communicate by radio frequency instead of infrared? I don’t know enough about RF circuits to look for components that would implement such a thing. I had thought all RF devices sold in the United States are required to have an FCC ID printed on it, but none are visible. Perhaps certain unlicensed frequency bands are exempt from FCC ID requirement? Shrug, doesn’t matter to me anymore as I won’t be needing this remote or hub to put their associated fans to use.

Orthogonal Fans with Pixelblaze 3D Mapping

I have built a control board for a trio of affordable PC case fans, replacing their bundled control hub of this Asiahorse Magic-i 120 V2 system. With my board I can control individual RGB LED inside these fans with help of a Pixelblaze LED controller. My initial tests used simple built-in linear patterns, but I wanted to use patterns that take advantage of 3D coordinate mapping. This is my favorite part of Pixelblaze and a core part of my Glow Flow project.

To get a 3D structure out of three fans, I did the easiest and most expedient thing: orient them orthogonal to each other with a few twist-ties. Now every fan motor axis lines up with one of three axes in 3D space. I have a Pixelblaze 3D coordinate test program already in hand, the RGB-XYZ 3D Sweep program I created during Glow Flow. All I had to do was create a 3D coordinate map in my Pixelblaze to describe LED layout in three-dimensional space.

I opened up the Pixelblaze mapping editor and… immediately got stuck. Where, exactly, should I map these LEDs? Physically, they are surface mounted on the central hub. However, their light diffused by translucent fan blade plastic, no longer fixed to a single location but distributed across entire fan diameter. Should I map the LEDs where they are physically? Or fan blade outer perimeter? Or somewhere in between? There really isn’t one single location for resulting output of a single LED.

I decided to experiment by writing my mapping code so a single variable controls how far from the center each location is. When pixelRadius is zero, everything is at the center and not very interesting. When set to 0.5, everything is mapped to the perimeter. I adjusted this value until the pattern “looked right” and that ended up at 0.4. I’m not satisfied with the empirical nature of this value, but I haven’t figured out a better way to account for diffusion.

Another problem with these LEDs is that they weren’t placed with precise coordinates in mind. I think LEDs were laid out on the circuit board relative to wiring bundle location, which is slightly offset from one of the four mounting arms. As a result, the LEDs aren’t aligned to any reference point on the exterior. To use an analog clock face as example, these LEDs are evenly placed but slightly offset from the hour numbers. Instead of lined up at 12, 1, 2, 3. They are at 12:10, 1:10, 2:10, etc.

These fans are physically squares with side lengths of 120mm. They can be installed in one of four orientations that are 90 degrees from each other and be mechanically fine. I usually choose my orientation based on whichever makes the wiring most convenient. But whatever the motivation, my 3D coordinate map would have to compensate for the resulting rotation.

The answer for both of the above rotation compensation concerns is an array fanRotationCorrection. One value for each fan, a sum of physical and LED offset rotations (in radians) added into coordinate map angle calculation for that fan.

Here is the result of my 3D pixel map running my RGB-XYZ sweep test program:

Here is my Pixelblaze Pixel Mapper code. [UPDATE: I later noticed that I got my Z-axis backwards. Be aware that both the video embedded in the tweet and this pixel mapper code are known to be flawed. At least they’re consistent with each other!]

function(pixelCount) {
  pixelPerFan = 12;
  pixelRadius = 0.4;
  fanRotationCorrection = [0.65, 0.65, 0.65];

  var map = [];
  for (i = 0; i < pixelCount; i++) {
    var ledNumber = i % pixelPerFan;
    var ledRadians = ((ledNumber/pixelPerFan) * Math.PI * 2);

    if (i < pixelPerFan) {
      ledRadians += Math.PI*fanRotationCorrection[0]
      map.push([0, 0.5 + Math.cos(ledRadians)*pixelRadius, 0.5 - Math.sin(ledRadians)*pixelRadius]);
    } else if (i < pixelPerFan*2) {
      ledRadians += Math.PI*fanRotationCorrection[1]
      map.push([0.5 - Math.cos(ledRadians)*pixelRadius, 0, 0.5 - Math.sin(ledRadians)*pixelRadius]);
    } else if (i < pixelPerFan*3) {
      ledRadians += Math.PI*fanRotationCorrection[2]
      map.push([0.5 - Math.cos(ledRadians)*pixelRadius, 0.5 + Math.sin(ledRadians)*pixelRadius, 0]);
    } else {
      // Unexpected pixels are placed at origin
      map.push(0,0,0);
    }
  }
  return map;
}

My Asiahorse investigation results were also posted to Pixelblaze forums. And now that I have full and complete control over these fans, I no longer need the hub that came in the box.

Control Board for Asiahorse 120mm Fans with RGB LED

I have a trio of PC cooling fans with embedded addressable RGB LED. They were designed to plug into a control hub that came in the bundle, but that hub had only a limited set of patterns and appeared to send the same signal to all fans. In order to run my own light show and control each fan individually, I determined the fan pinout and will now build my own control board for these fans.

The first problem was wiring. These fans came with a JST-PH style plug with 2.0mm pitch. (Distance between pins.) My perforated prototype circuit boards (and most connectors and components on hand) have a pitch of 0.1″ (~2.54mm) and would not fit. To work around this problem today, I cut off the factory connector and crimped on a replacement. These are JST-XH clones(*) with 0.1″ pitch. In the future, I might consider buying some perforated prototype boards with 2.0mm pitch (*) but that would have the problem of using components with 0.1″ pitch. The real solution is to make my own circuit boards that can accommodate whatever pitch I need, but that’s beyond my reach at the moment.

To generate individual control signals for these fans, I will be using the very awesome Pixelblaze controller. For power I will be using one of my salvaged 12V DC power bricks. It is rated for up to 1.5A which should be sufficient for a trio of fans and 12*3 = 36 LEDs. The barrel jack has a 5.5mm outer diameter and 2.1mm inner diameter, so I soldered a matching power jack (*) to the board. This will deliver power for the fans, with a decoupling capacitor to smooth things out. A buck converter (*) with convenient 5V preset feeds from that 12V rail to deliver 5V for Pixelblaze and LEDs.

I soldered some 2N2222A transistors for potential control of fan speed, but for this first iteration they’re pulled high so the fans spin all the time. It would have been easier to solder fan motor low wire directly to ground, but I have ambition of fan control in a future update.

The LEDs are connected in serial across all three fans. Pixelblaze data is connected to “data in” of the first fan, whose “data out” is connected to “data in” of the second fan, and onwards to the third fan. Configured for 12*3 = 36 WS2812-style LEDs, the Pixelblaze has individual control of every LED with a single data line. And for the first time, these three fans show patterns different from each other. With this new power I can make things even more interesting.


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

Pinout for Asiahorse 120mm Fan (Magic-i 120 V2)

The Asiahorse Magic-i 120 V2 bundle included three 120mm cooling fans with integrated addressable RGB LEDs. These fans have a six-wire connector designed to be plugged into a hub that was included in the bundle, along with a remote control to change the light shown performed by those LEDs. Most users just need to plug those fans into the included hub, but some users like myself want to bypass the hub and control each fan directly. For this audience, I present the fan connector pinout derived from an exploratory session on my electronics workbench.

Since this was reverse engineered without original design documents, I don’t know which side is considered “pin #1” by the engineers who designed this system. These connectors appear to be JST-PH, whose datasheet does point to one side as “Circuit #1”. But there’s no guarantee the engineers followed JST convention. To avoid potential confusion, I’ll call them only by name.

NameSystemComments
+12VFanHigh side of fan motor. Hub connects this wire directly to +12V power input.
Motor LowFanLow side of fan motor. Use a power transistor between this wire and ground to control fan speed.
GroundFan + LEDPower return for LED circuit and can be used for fan motor low side as well. Hub connects this wire directly to power input ground.
Data InLEDInput control signal for addressable RGB LED. Compatible with WS2812/”NeoPixel” protocol.
+5VLEDPower for LED circuit. Hub connects this wire directly to +5V power input.
Data OutLEDControl signal for addressable RGB LED beyond the end of LED string inside the fan. Useful for chaining multiple units together by connecting this wire to Data In of the next device in line.

Now that I understand its pinout, I will build my own control circuit to replace the default Asiahorse hub.

Exploring 6-Wire Connector of Asiahorse Magic-i 120 V2

I was curious about PC accessories with embedded addressable RGB LED, so I bought the cheapest item available on Newegg that day. I have verified it works as originally intended, and now I’m going to dig deeper. This Asiahorse Magic-i 120 V2 is a three-pack of 120mm fans with integrated LEDs. All three fans plug into a hub that has a corresponding remote control for me to select from a list of programmed patterns. This bundle is fine if I’m satisfied with those patterns, but I want display patterns of my own.

Each fan connects to the hub through this six-wire connector. The distance between each pin is 2mm. Judging by the pitch and physical appearance, I guess they are JST-PH or a clone. (I don’t have any 6-conductor JST-PH to verify.) This is mildly inconvenient because my workbench is setup to work with 0.1″ pitch (~2.54mm) connectors so it’s not very easy for me to probe those signals as-is.

The solution is a quick soldering project to give me an exploration board. I cut the bundle of six wires and inserted a small piece of perforated prototype board. Each of the six wires are then bridged with an exposed length of solid wire, easy for me to clip probes onto.

Trying the easy thing first, I probed for continuity between these six wires and the power input wires. This gave me location for +12V source, +5V source, and ground. Armed with this information, I soldered capacitors to smooth out both power rails, because the AC adapter I’m using is designed for far higher wattage than a few LEDs and it’s not unusual for switching power supplies to be noisy at low power levels. (And the cheap ones are always noisy at all power levels…)

With three out of six wires identified for power, this left me with three more wires to decipher. Here are my candidates:

  • Fan control: it may be one (or none) of the following:
    • Fan motor high side: the fan may be internally connected to the ground wire, and the high side wire is left exposed here for external PWM or on/off control with a power transistor.
    • Fan motor low side: the same idea but reverse: fan motor is internally connected to +12V and the low side is exposed here for external PWM or on/off control with a power transistor.
    • Fan motor PWM: Neither of the above. Instead of leaving either high or low side unconnected for external power transistor, a suitable power transistor is built into the fan and controlled with a 25kHz PWM signal as used in 4-wire fans.
  • Fan tachometer like the type I found in 3-wire fans.
  • LED Data In: addressable RGB signal input.
  • LED Data Out: signal that has passed through the LED string inside this device and ready to be passed on to other LEDs in other devices down the chain.

To decipher which wires are which of those candidate capabilities, I connected my Saleae Logic 8 to the three unknown wires. I started an analog waveform capture session and used the fan remote control to command that all fans show a solid green.

The top line in white stayed at 0V through the entire session. This may be the tachometer wire, or it may be fan motor low side. To determine which, I disconnected everything other than the +12V and ground wires. The fan did not move. I connected the wire corresponding to this white line to ground, and the fan started spinning. Conclusion: this wire is fan motor low side.

The middle line in brown shows a distinct repeating pattern. The bottom line in red shows the same repeating pattern, but delayed by 12 cycles. Since there are 12 LEDs in a fan, that means the middle brown line is LED Data In and the red line is LED Data Out.

To verify LED Data In, I connected +5V, ground, plus this wire to the data pin of a Pixelblaze. After I configured the Pixelblaze to emit control signal for a string of 12 x WS2812 (NeoPixel) LEDs, I saw them light up appropriately on the fan.

To verify LED Data Out, I connected it to the data input pin of an array of 64 WS2812 LEDs. I configured the Pixelblaze for 12 + 64 = 76 pixels. After colorful pixels cycle through the fan, they marched onwards to the array as appropriate for LED Data Out.

With these functions verified, I’m confident enough to describe this Asiahorse fan pinout.

Asiahorse Magic-i 120 V2

I wanted to play with a set of PC case cooling fans with embedded addressable RGB LEDs, with the intent of learning how to control them for a future project. For extra challenge, I got a multipack that combined both fan and LED controls into a single (probably proprietary) connector that plugged into a bundled hub. Using the selection criteria of “Lowest bidder of the day” I bought a three-pack of fans: the Asiahorse Magic-i 120 V2 and I look forward to seeing how it works.

Before I start cutting things up, I need to verify the product worked as originally designed. I won’t need a computer for this as this multipack came with a remote control for the hub that allows operation without a computer. This lets me explore its signals without the risk of damaging a computer. I just need to supply power in the 4-pin accessory format popular with pre-SATA hard drives and optical disks. I didn’t need a computer here, either, as I had an AC adapter with this plug that originally came in a kit that turned internal HDDs into USB HDDs.

There were no instructions in the box, but things were straightforward. Three fans plugged into the hub, and a power cable connected my AC adapter to the hub. As soon as I turned on the power, all three fans started spinning. The LED light show didn’t start until I pressed the “On” button on the remote.

RGB LEDs in this fan are mounted in the hub, on the outside perimeter of the motor control board. I count 12 LEDs and they aimed along motor axis upwards into the center portion of translucent fan blades. These colorful lights are then diffused along length of the blade, resulting in a colorful spinning disk. While shopping on Newegg I saw other arrangements. Some fans have LEDs around the outside perimeter instead, and some fans illuminate both the hub and the perimeter. Each manufacturer hoping to capture the attention of a buyer with novelty of their aesthetic.

This remote control allowed me to cycle between various colorful programs or choose from a set of solid colors. I had hoped the colorful programs would ripple across the fans, but all three fans appear to display identical light sequence. I could control LED brightness or turn all the lights off, but I didn’t seem to have any control over fan speed. I guess this is where an instruction manual would have been useful.

If I wanted to build something bright and colorful that circulates air, almost everything is already here and ready to go. I just have to wire up a power switch to turn everything on/off, and the remote can take care of the rest. But I didn’t buy this just to have some lights. I wanted full control and I’m not afraid to start cutting things up to get there.

Shopping for PC Cooling Fans with RGB LED

I’ve decided to investigate controlling the RGB LEDs embedded in aesthetics-based PC accessories. I’m not interested in using them for my PC, but as research for a yet to be determined future electronics project. I wanted something that is a standardized commodity with a large range of variety in the ecosystem and have some usefulness beyond just looking shiny. I settled on 120mm PC cooling fans.

There are many common sizes for cooling fans, but I’ve found 120mm to be the most common for aftermarket cooling. They’re larger than average for CPU cooling, but not too large especially for heat-pipe based cooling towers. But they’re typically installed for general cooling in tower cases, whose cooling vents are cut for 120mm fans. Covering both popular use cases mean more options.

Looking around on my NewEgg, I find that fans sold individually typically have two separate connectors. One for LED and one for fan control. To the rest of the computer, these fans look like two separate peripherals: the LED and the fan. They just happened to coexist in the same device. The fan control connector sometimes just have two wires for +12V and ground. Some have a third tachometer wire for reporting speed, and some have a fourth wire for built-in PWM control. Here’s an example of a CPU cooler the Vetroo V5 whose fan has two connectors: a 4-pin CPU cooler fan control connector and a JRAINBOW RGB LED plug. These should be simple and straightforward to interface.

More challenging are fans that use an intermediate hub. The hub has a connector for power and for JRAINBOW, consolidating those signals into a proprietary connector. I started contemplating this particular Rosewill RGBF-S12001 three-pack of fans which use such a design. I think I can decipher roles of each wire so I could bypass the hub and control each fan directly. This multipack also had a remote control for direct control of the hub without a computer. This is appealing to me, because independent control meant I didn’t need a PC involved as I probed how it worked. If I should make a fatal mistake (say, accidentally short-circuited something) it should only kill the hub or the fan and not an entire computer.

As I scrolled down, though, Newegg showed me several other items under “similar products”. I saw an even more discounted three-pack of fans: the Asiahorse Magic-i 120 V2. Three fans for fourteen bucks, well within my impulse buy range. I’ll buy the pack and see what it does.

Temperature Responsive Cereal Box Fan

This quick and easy project produces a quiet breeze for hot summer nights. I wanted something gentler than the fans already in the house, and I wanted it to automatically turn itself off in the middle of the night once things cooled down enough. It also let me apply lessons I’ve recently learned. Even though I’ve found that the TMP36 sensor isn’t a great fit for a ESP8266, it’s something already on hand for an ESP8266 to tell if it’s cool enough to turn the fan off.

The 3-wire fan is a PC cooling fan with a 200mm diameter, relatively large within that category. I bought it some years ago for my first Luggable PC project, it was just a bit too large for that purpose and sat idle until now. I thought about designing and 3D-printing a stand for this fan, but in the spirit of keeping things simple and quick, I mounted it in an empty cereal box instead. Cutting holes in the box to accommodate the fan took a tiny fraction of the time it would have taken to 3D-print something.

Primary air intake was the top of the box, left open.

I cut a smaller secondary air intake towards the bottom of the box, which also makes it easy to toss my control board in there and feed it power from a salvaged DC power supply. A TMP36 sensor was soldered in the farthest corner in this picture, visible sticking up vertically.

Results

Running ESPHome (YAML excerpt below) this project successfully controls fan speed via ESP8266 PWM. It was also able to read temperature via TMP36 sensor, but values were affected by ESP8266. Located 1/2 of the circuit board away plus the entire height of its legs was not enough distance from the ESP8266 heat island: temperature reading dropped noticeably whenever the fan is turning. Still, it’s enough for me to create a Home Assistant automation to turn off this fan whenever the temperature dropped below a certain threshold. Due to the heating from ESP8266, the temperature value rises a few degrees immediately after the fan was turned off. Thankfully there was no risk of system feedback oscillation, because I did not create an automation to turn the fan on — I do that manually when I’m ready for a light breeze.

This worked well sitting on my bedstand, creating a light cool breeze when I’m ready to fall asleep and turning itself off while I was asleep. But its physical footprint was a problem: it took up space that is ideally used for a bedstand light. The obvious solution was to pull some LEDs into the next version, which is an opportunity to tackle another item on my to-learn list: PC accessories with embedded RGB LEDs.


ESPHome YAML to read TMP36 temperature and fan speed every 5 minutes:

sensor:
  - platform: adc
    pin: A0
    name: "Fan Temperature"
    unit_of_measurement: "°C"
    update_interval: 1s
    accuracy_decimals: 2
    filters:
      - multiply: 100
      - offset: -50
      - sliding_window_moving_average:
          window_size: 450
          send_every: 300
          send_first_at: 15
  - platform: pulse_counter
    pin: 12
    id: fan_rpm_counter
    name: "Fan RPM"
    unit_of_measurement: "RPM"
    accuracy_decimals: 0
    update_interval: 300s
    filters:
      - multiply: 0.5 # 2 pulses per revolution

output:
  - platform: esp8266_pwm
    pin: 14
    id: fan_pwm_output
    frequency: 1000 Hz

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

High-Side Fan ESP8266 PWM Using Optocoupler

It was neat that I could control the speed of a 4-wire CPU cooling fan with just software a PWM signal from an ESP8266, but 4-pin fans with built-in power switching are in the minority. Most available fans have no built-in speed control and depend on external PWM circuitry to vary their input voltage level. If I wanted to control speed of such fans with an ESP8266, I’ll need to get a power transistor of some sort into the circuit.

I’ve found several tutorials online for fan speed control via PWM, but they all use a transistor on low side of the fan. They do this because it makes the circuit easier. We connect the black fan wire to collector of the transistor, connect emitter to ground, and finally connect PWM signal from our microcontroller to transistor base. This will work because the transistor and the microcontroller share a common ground. A transistor needs only a volt or so on the base pin, easily delivered from any microcontroller.

The problem with this approach is that we could no longer directly read the tachometer signal of 3-pin fans, because they are open-drain to the “ground” which is no longer ground but low side of the fan. Depending on implementation details on the fan, the voltage level on that pin may rise too high for the microcontroller.

There are many valid ways to resolve this situation, the path I chose is to use a PC817 optocoupler. (*) Internally it is a LED pointed at a receiver. This optical system transmits a signal while electrically separating LED side from receiver side. In my case this I no longer need a common ground.

From here, there are two options forward: I could use the optocoupler to read the tachometer signals, or I could use the optocoupler to put the transistor on the fan’s high side. I chose to switch the fan’s high side so the fan has a common ground with the microcontroller, and I could directly read tachometer signal. Of course, it is valid to use optocoupler on both motor power PWM and for tachometer feedback. This is necessary for electrically noisy motor systems, but the brushless fan of a computer cooling fan (usually) does not require such measures.

I did have to add a capacitor to smooth out my PWM output, but that would have been necessary in any case. Having full PWM control means I can now switch the fan off completely with a 0% duty cycle, but it also means I am responsible for avoiding low PWM levels where a fan could not turn and stalls. After I proved I had PWM control via Home Assistant UI, I thought it would be fun to have the option to control fan based on temperature, so I added a TMP36 sensor.


ESPHome YAML excerpt for basic PWM fan control and reading fan tachometer. Note: this simple example lacks intelligence to avoid low PWM levels that would stall a fan.

sensor:
  - platform: pulse_counter
    pin: 12
    id: fan_rpm_counter
    name: "Fan RPM"
    unit_of_measurement: "RPM"
    accuracy_decimals: 0
    update_interval: 300s
    filters:
      - multiply: 0.5 # 2 pulses per revolution

output:
  - platform: esp8266_pwm
    pin: 14
    id: fan_pwm_output
    frequency: 1000 Hz

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

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

ESP8266 Controlling 4-Wire CPU Cooling Fan

I got curious about how the 4 wires of a CPU cooling fan interfaced with a PC motherboard. After reading the specification, I decided to get hands-on.

I dug up several retired 4-wire CPU fans I had kept. All of these were in-box coolers bundled with various Intel CPUs. And despite the common shape and Intel brand sticker, they were made by three different companies listed at the bottom line of each label: Nidec, Delta, and Foxconn.

I will use an ESP8266 to control these fans running ESPHome, because all relevant code has already been built and ready to go:

  • Tachometer output can be read with the pulse counter peripheral. Though I do have to divide by two (multiply by 0.5) because the spec said there are two pulses per fan revolution.
  • The ESP8266 PWM peripheral is a software implementation with a maximum usable frequency of roughly 1kHz, slower than specified requirement. If this is insufficient, I can upgrade to an ESP32 which has hardware PWM peripheral capable of running 25kHz.
  • Finally, a PWM fan speed control component, so I can change PWM duty cycle from HomeAssistant web UI.

One upside of the PWM MOSFET built into the fan is that I don’t have to wire one up in my test circuit. The fan header pins were wired as follows:

  1. Black wire to circuit ground.
  2. Yellow wire to +12V power supply.
  3. Green wire is tachometer output. Connected to a 1kΩ pull-up resistor and GPIO12. (D6 on a Wemos D1 Mini.)
  4. Blue wire is PWM control input. Connected to a 1kΩ current-limiting resistor and GPIO14. (D5 on Wemos D1 Mini.)

ESPHome YAML excerpt:

sensor:
  - platform: pulse_counter
    pin: 12
    id: fan_rpm_counter
    name: "Fan RPM"
    update_interval: 5s
    filters:
      - multiply: 0.5 # 2 pulses per revolution

output:
  - platform: esp8266_pwm
    pin: 14
    id: fan_pwm_output
    frequency: 1000 Hz

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

Experimental observations:

  • I was not able to turn off any of these fans with a 0% duty cycle. (Emulating pulling PWM pin low.) All three kept spinning.
  • The Nidec fan ignored my PWM signal, presumably because 1 kHz PWM was well outside the specified 25kHz. It acted the same as when the PWM line was left floating.
  • The Delta fan spun slowed linearly down to roughly 35% duty cycle and was roughly 30% of full speed. Below that duty cycle, it remained at 30% of full speed.
  • The Foxconn fan spun down to roughly 25% duty cycle and was roughly 50% of the speed. I thought it was interesting that this fan responded to a wider range of PWM duty cycles but translated that to a narrower range of actual fan speeds. Furthermore, 100% duty cycle was not actually the maximum speed of this fan. Upon initial power up, this fan would spin up to a very high speed (judged by its sound) before settling down to a significantly slower speed that it treated as “100% duty cycle” speed. Was this intended as some sort of “blow out dust” cleaning cycle?
  • These are not closed-loop feedback devices trying to maintain a target speed. If I set 50% duty cycle and started reducing power supply voltage below 12V, the fan controller will not compensate. Fan speed will drop alongside voltage.

Playing with these 4-pin fans were fun, but majority of cooling fans in this market do not have built-in power transistors for PWM control. I went back to learn how to control those fans.

CPU Cooling 4-Wire Fan

Building a PC from parts includes keeping cooling in mind. It started out very simple: every cooling fan had two wires, one red and one black. Put +12V on the red wire, connect black go ground, done. Then things got more complex. Earlier I poked around with a fan that had a third wire, which proved to be a tachometer wire for reading current fan speed. The obvious follow-up is to examine cooling fans with four wires. I first saw this with CPU cooling fans and, as a PC builder, all I had to know was how to plug it in the correct orientation. But now as an electronics tinkerer I want to know more details about what those wires do.

A little research found the four-wire fan system was something Intel devised. Several sources cited URLs on http://FormFactors.org which redirects to Intel’s documentation site. Annoyingly, Intel does not make the files publicly available, blocking it with a registered login screen. I registered for a free account, and it still denied me access. (The checkmark next to the user icon means I’ve registered and signed in.)

Quite unsatisfying. But even if I can’t get the document from official source, there are unofficial copies floating around on the web. I found one such copy, which I am not going to link to because the site liberally slathered the PDF with advertisements and that annoys me. Here is the information on the title page which will help you find your own copy. Perhaps even a more updated revision!

4-Wire Pulse Width Modulation
(PWM) Controlled Fans
Specification
September 2005
Revision 1.3

Reading through the specification, I learned that the four-wire standard is backwards compatible with three-wire fans as those three wires are the same: GND, +12V supply, and tachometer output. The new wire is for a PWM control signal input. Superficially, this seems very similar to controlling fan speed by PWM modulating the +12V supply, except now the power supply stays fixed at +12V and the PWM MOSFET is built into the fan. How is this better? What real-world problems are solved by using an internal PWM MOSFET? The spec did not explain.

According to spec, the PWM control signal should be running at 25kHz. Fan manufacturers can specify a minimum duty cycle. Fan speed for duty cycle less than the minimum is open for interpretation by different implementations. Some choose to ignore lower duty cycles and stay running at minimum, some interpret it as a shutoff signal. The spec forbids pull-up or pull-down resistor on the PWM signal line external to the fan, but internal to the fan there is a pull-up resistor. I interpret this to mean that if the PWM line is left floating, it will be pulled up to emulate 100% duty cycle PWM.

Reading the specification gave me the theory of operation for this system, now it’s time to play with some of these fans to see how they behave in practice.

Computer Cooling Fan Tachometer Wire

When I began taking apart a refrigerator fan motor, I expected to see simplest and least expensive construction possible. The reality was surprisingly sophisticated, including a hall effect sensor for feedback on fan speed. Seeing it reminded me of another item on my to-do list: I’ve long been curious about how computer cooling fans report their speed through that third wire. The electrical details haven’t been important to build PCs, all I needed to know was to plug it the right way into a motherboard header. But now I want to know more.

I have a fan I modified for a homemade evaporator cooler project, removing its original motherboard connector so I could power it with a 12V DC power plug. The disassembled connector makes it unlikely to be used in future PC builds and also makes its wires easily accessible for this investigation.

We see an “Antec” sticker on the front, but the actual manufacturer had its own sticker on the back. It is a DF1212025BC-3 motor from the DF1212BC “Top Motor” product line of Dynaeon Industrial Co. Ltd. Nominal operating power draw is 0.38A at 12V DC.

Even though 12V DC was specified, the motor spun up when I connected 5V to the red wire and grounded the black wire. (Drawing only 0.08 A according to my bench power supply.) Probing the blue tachometer wire with a voltmeter didn’t get anything useful. Oscilloscope had nothing interesting to say, either.

To see if it might be an open collector output, I added a 1kΩ pull-up resistor between the blue wire and +5V DC on the red wire.

Aha, there it is. A nice square wave with 50% duty cycle and a period of about 31 milliseconds. If this period corresponds to one revolution of the fan, that works out to 1000/31 ~= 32 revolutions per second or just under 2000 RPM. I had expected only a few hundred RPM, so this is roughly quadruple my expectations. If this signal was generated by a hall sensor, it would be consistent with multiple poles on the permanent magnet rotor.

Increasing the input voltage to 12V sped up the fan as expected, which decreased the period down to about 9ms. (The current consumption went up to 0.22 A, lower than the 0.38 A on the label.) The fan is definitely spinning at some speed far lower than 6667 RPM. I think dividing by four (1666 RPM) is in the right ballpark. I wish I had another way to measure RPM, but regardless of actual speed the key observation today is that the tachometer wire is an open-collector output that generates a 50% duty cycle square wave whose period is a function of the RPM. I don’t know what I will do with this knowledge yet, but at least I now know what happens on that third wire!

[UPDATE: After buying a multichannel oscilloscope, I was able to compare fan tachometer signal versus fan behavior and concluded that a fan tachometer wire signals two cycles for each revolution. Implying this fan was spinning at 3333 RPM which still seems high.]