Today, we will be examining those 4-pin computer fans, the ones that cool down our power supplies and heatsinks. We’ll learn how they work and how we can both control and monitor them using an ESP32. Then we’ll build a temperature-controlled fan controller with an OLED display and stall detection.
Introduction
You might not think that a computer fan is a complicated device. It’s just a motor attached to some blades, right?

Well, there is a lot more to computer fans than that! These useful devices utilize brushless DC motors and feature built-in electronic speed control. Some of them provide a tachometer output, and the 4-pin variety also allows for external speed control using Pulse Width Modulation(PWM).
Today, we will learn how these fans operate and how to use a microcontroller to interact with them. We’ll see how to change their speed and how to use their internal tachometers.
We then have a project, an ESP32-based temperature-sensitive fan control. This unit can operate in either manual or automatic mode and also protects against a stalled or obstructed fan.
So let’s get spinning!
PWM Fan Control
Being able to control the speed of a cooling fan is quite helpful. It can save energy, a crucial factor in battery-powered equipment, and also allows the fan to run quieter.
The fans used in computers and power supplies utilize “brushless motors”, a type of motor that employs electronic control instead of a commutator to switch current through its coils. On cheaper (2-pin or 3-pin) units, this electronic controller is self-contained and can’t be controlled externally. However, 4-pin fans can be externally controlled using PWM.
Pulse Width Modulation (PWM)
Pulse Width Modulation (PWM) is a widely used method for controlling various DC motors. Servo motors are a good example of PWM-controlled motors; the width of a pulse determines their shaft position. Standard DC motors can also be controlled using PWM.
As its name would imply, PWM works by modulating the width of a fixed-frequency pulse. In the case of a computer fan, the wider the pulse is, the faster the motor will spin.
To go at full speed, the pulse has a 100% width, or in other words, it is held HIGH. To stop the motor, the pulse is removed and the line is held LOW.
The frequency used by DC fan motors is significantly higher than that used with servo motors. DC fan motors typically use a PWM frequency of 25KHz.
Common DC Fans
DC Fans are available in a variety of sizes, ranging from tiny 25 x 25 mm babies to jumbo 200 x 200 mm beasts. A prevalent size used in computer power supplies is 120 x 120 mm.

They have airflow rates ranging from 30 CFM to 150 CFM. Typically, a larger motor will have a greater airflow.
As for operating voltage, the two most common voltages are 12 volts and 5 volts. However, you can also find 24 and 48-volt models, which are often used in large server cabinets.
However, not every DC fan motor can be controlled using PWM, a fact that you should be aware of when shopping for one. Fortunately, there is a straightforward method for determining if your motor is suitable for PWM control: simply count the number of pins on the connector. If it doesn’t have four wires, you can’t control it!
2-Wire Fans

A 2-wire fan is as basic as it gets. These fans simply have a positive and negative voltage connection, which runs to the internal electronic speed control (ESC).
Remember, you must observe the correct polarity when wiring these, or any, DC PWM computer fan. Reversing the polarity will NOT reverse the direction of rotation, unlike a brushed DC motor.
3-Wire Fans

On these fans, a third wire has been added. This wire is a tachometer output.
The tachometer sensor in a DC PWM computer fan usually outputs two pulses per rotation. So, if we count 1000 pulses in a minute, then the motor is spinning at 500 RPM.
The tachometer output is open-collector with one side connected to ground. You will need to use a pull-up resistor to pull the output up to your desired logic level. Many microcontrollers, such as the ESP32 that we will use later, have internal pull-ups that you can enable in code, saving the cost and space of a physical resistor.
4-Wire Fans

This is the type of fan we want. It features all the benefits of the 3-wire fin, plus a PWM speed control input.
Regardless of the operating voltage, all DC PWM fans use 5-volt logic levels. They are also compatible with 3.3-volt logic. Applying a 25KHz PWM signal within this range will allow you to control the motor’s speed.
You can also use the PWM input as a binary motor control, if that is what you want. Sending this line HIGH (or just leaving it disconnected, as it is internally pulled HIGH) will run the motor at full speed. Grounding (setting LOW) the PWM input will stop the motor.
Ensure that the DC computer fan you are using has four pins, and follow along to learn how to control and monitor it.
Simple Fan Control
We will begin our experiments by simply controlling the fans’ speed using PWM. In some cases, as when you need to run the fan constantly at a lower speed, this might be all you need.
So grab a fan and an ESP32 and let’s get started!
ESP32 Fan Hookup
We will be using an ESP32 as our fan controller. It features an efficient PWM generation system used for LED and motor control, with virtually every logic pin being PWM-capable. It can also generate PWM at 25KHz, something an ATMega328 (used in the Arduino Uno and original Nano) struggles to accomplish.
Virtually any ESP32 microcontroller board will work for these experiments, and for the project we will build at the end.
I’m using a Seeeduino XIAO ESP32-S3 board, as it meets many of the design requirements. It’s tiny, inexpensive, and widely available. And it has enough pins to do the job.
Here is how we will hook up the Seeduino XIAO (or any ESP32 board) to the PWM DC Motor:

Note that we are connecting both the PWM and tachometer leads to the ESP32. In our first experiment, we’ll ignore the tachometer; it will be used in later exercises.
Fan Speed Control Code
Here is the code we will use to control the speed of our DC PWM computer fan:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
/* PWM Fan Speed Control pwm-fan-speed-esp32.ino Control a 4-pin PWM computer fan Uses Seeeduino XIAO ESP32-S3 DroneBot Workshop 2025 https://dronebotworkshop.com */ // Pin definitions const int PWM_PIN = 3; // D2 on XIAO = GPIO 3 const int POT_PIN = A0; // A0 on XIAO = GPIO 26 // PWM settings const int PWM_FREQ = 25000; // 25 kHz frequency for computer fans const int PWM_RESOLUTION = 8; // 8-bit resolution (0-255) void setup() { // Start serial monitor Serial.begin(115200); Serial.println("Simple Fan Control Starting..."); // Configure PWM ledcAttach(PWM_PIN, PWM_FREQ, PWM_RESOLUTION); // Set initial fan speed to zero ledcWrite(PWM_PIN, 0); Serial.println("Fan controller ready!"); Serial.println("Turn potentiometer to adjust fan speed"); } void loop() { // Read potentiometer value (0-4095 for 12-bit ADC) int potValue = analogRead(POT_PIN); // Convert to PWM duty cycle (0-255 for 8-bit resolution) int fanSpeed = map(potValue, 0, 4095, 0, 255); // Apply PWM to fan ledcWrite(PWM_PIN, fanSpeed); // Calculate and display percentage int speedPercent = map(fanSpeed, 0, 255, 0, 100); Serial.print("Potentiometer: "); Serial.print(potValue); Serial.print(" | PWM Value: "); Serial.print(fanSpeed); Serial.print(" | Fan Speed: "); Serial.print(speedPercent); Serial.println("%"); delay(100); // Small delay for stable readings } |
Here is a breakdown of the code:
Step 1: Define the Pins
First, we define the pins used for PWM output and potentiometer input. In this case, we’re using pin 3 (D2 on the XIAO) for PWM output and pin A0 (GPIO 26) for potentiometer input.
Step 2: Configure PWM
In Setup, we configure the PWM signal with a frequency of 25 kHz and an 8-bit resolution (0-255). This is suitable for most computer fans.
Step 3: Read the Potentiometer Value
In the Loop, we read the potentiometer value using the analogRead() function. The value ranges from 0 to 4095 (12-bit ADC).
Step 4: Convert to PWM Duty Cycle
We convert the potentiometer value to a PWM duty cycle value (0-255) using the map() function. This value determines the fan speed.
Step 5: Apply PWM to the Fan
We apply the PWM signal to the fan using the ledcWrite() function.
Step 6: Calculate and Display Fan Speed
We calculate the fan speed as a percentage (0-100%) and display it on the serial monitor along with the potentiometer value and PWM duty cycle value.
Step 7: Repeat
The loop repeats every 100 milliseconds, allowing for smooth fan speed control.
Ensure everything is connected correctly and load the code onto the XIAO. Then give it a try.

Turning the potentiometer should change the fan speed, from stop to full speed.
Tachometer
Now let’s move on to the tachometer. As mentioned earlier, it is an open-collector output that is activated twice per motor rotation.
I conducted an experiment (which you can view in the accompanying video) in which I controlled the fan using a signal generator to produce the correct PWM signals. I also used two oscilloscopes, one to monitor the incoming pulses and the other to monitor the tachometer output. The tachometer output is a square wave whose frequency changes in proportion to the motor’s rotation speed.

We have already connected the tachometer to the ESP32, so we can proceed with the code required to utilize it.
Tachometer Code
Here is the code we will use to monitor the tachometer’s output:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
/* PWM Fan Speed and Tachometer pwm-fan-speed-tach-esp32.ino Control a 4-pin PWM computer fan with Tachometer Uses Seeeduino XIAO ESP32-S3 DroneBot Workshop 2025 https://dronebotworkshop.com */ // Pin definitions const int PWM_PIN = 3; // D2 on XIAO = GPIO 3 const int TACH_PIN = 2; // D1 on XIAO = GPIO 2 const int POT_PIN = A0; // A0 on XIAO = GPIO 26 // PWM settings const int PWM_FREQ = 25000; // 25 kHz frequency for computer fans const int PWM_RESOLUTION = 8; // 8-bit resolution (0-255) // Tachometer variables volatile unsigned long tachPulseCount = 0; unsigned long lastTachTime = 0; const unsigned long TACH_SAMPLE_TIME = 1000; // Sample period in milliseconds // Interrupt service routine for tachometer void IRAM_ATTR tachISR() { tachPulseCount = tachPulseCount + 1; } void setup() { // Start Serial Monitor Serial.begin(115200); Serial.println("Fan Control with Tachometer Starting..."); // Configure PWM ledcAttach(PWM_PIN, PWM_FREQ, PWM_RESOLUTION); // Configure tachometer pin with interrupt pinMode(TACH_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(TACH_PIN), tachISR, FALLING); // Set initial fan speed to zero ledcWrite(PWM_PIN, 0); // Initialize timing lastTachTime = millis(); Serial.println("Fan controller ready!"); Serial.println("Turn potentiometer to adjust fan speed"); } void loop() { // Read potentiometer value (0-4095 for 12-bit ADC) int potValue = analogRead(POT_PIN); // Convert to PWM duty cycle (0-255 for 8-bit resolution) int fanSpeed = map(potValue, 0, 4095, 0, 255); // Apply PWM to fan ledcWrite(PWM_PIN, fanSpeed); // Calculate speed percentage int speedPercent = map(fanSpeed, 0, 255, 0, 100); // Calculate RPM every second unsigned long currentTime = millis(); if (currentTime - lastTachTime >= TACH_SAMPLE_TIME) { // Calculate RPM (2 pulses per revolution for most fans) unsigned long rpm = (tachPulseCount * 60000) / (TACH_SAMPLE_TIME * 2); // Estimate target RPM based on fan speed (values for Noctua NF-A4x20) // This fan has max RPM of 5000, adjust for your specific fan unsigned long targetRPM = map(speedPercent, 0, 100, 0, 5000); // Display results Serial.print("Pot: "); Serial.print(potValue); Serial.print(" | PWM: "); Serial.print(fanSpeed); Serial.print(" | Speed: "); Serial.print(speedPercent); Serial.print("% | Target: "); Serial.print(targetRPM); Serial.print(" RPM | Actual: "); Serial.print(rpm); Serial.println(" RPM"); // Reset counters tachPulseCount = 0; lastTachTime = currentTime; } delay(50); // Small delay for stable readings } |
How the Code Works
Here is a breakdown of the sketch operation:
1. New Pin and Tachometer Variables
We start by defining the pin for the tachometer and creating a few variables to help us count its pulses.
- const int TACH_PIN = 2;: We tell the ESP32 that the fan’s tachometer wire is connected to GPIO 2 (labeled D1 on the XIAO).
- volatile unsigned long tachPulseCount = 0;: This special variable will hold our pulse count. The volatile keyword is important because this variable will be modified by an “interrupt,” a process that happens outside the main loop(). It tells the compiler to always read the variable’s latest value.
- const unsigned long TACH_SAMPLE_TIME = 1000;: We define a sample period of 1000 milliseconds (1 second). This is how often we will calculate the RPM.
2. The Interrupt Service Routine (ISR)
This is the most important new piece of code. An Interrupt Service Routine (or ISR) is a special, super-fast function that the microcontroller runs immediately when a specific event happens on a pin.
- void IRAM_ATTR tachISR(): We create a function called tachISR.
- tachPulseCount = tachPulseCount + 1;: This is the only job of our ISR. Every single time a pulse from the fan’s tachometer wire is detected, this function runs and adds one to our tachPulseCount. Using an interrupt is much more accurate than trying to count pulses inside the main loop().
3. The setup() Function
In setup(), we configure the tachometer pin and tell the ESP32 to start listening for pulses.
- pinMode(TACH_PIN, INPUT_PULLUP);: We set the tachometer pin as an input. The _PULLUP is important as it activates an internal resistor on the ESP32, which is needed to get a clean signal from the fan’s tachometer.
- attachInterrupt(…): This powerful command tells the ESP32: “When you see a signal on TACH_PIN, immediately run the tachISR function. Trigger it specifically on the FALLING edge of the pulse (when the signal goes from high to low).”
4. The loop() Function
The main loop still controls the fan speed with the potentiometer, but now it also includes a timed section to calculate and display the RPM.
- Control the Fan: The first few lines are the same as before—they read the potentiometer, map the value, and set the fan’s PWM speed.
- Check the Time: The line if (currentTime – lastTachTime >= TACH_SAMPLE_TIME) checks if one second has passed since the last RPM calculation. This is a non-blocking way to time events.
- Calculate RPM: Inside the if statement, this formula is used: (tachPulseCount * 60000) / (TACH_SAMPLE_TIME * 2);
- It takes the number of pulses we counted (tachPulseCount).
- Multiplies by 60,000 to scale from milliseconds to a full minute.
- Divides by the sample time (1000ms).
- Divides by 2, because most computer fans produce two pulses for every single revolution. The result is the fan’s speed in RPM.
- Display and Reset: The code then prints all the values to the serial monitor. Finally, it resets tachPulseCount = 0; and updates lastTachTime to get ready for the next one-second sample period.

Load the code and observe the serial monitor as you turn the potentiometer. You should see results from the tachometer (check your wiring if you don’t). Try carefully stopping the fan to see what happens to the tach reading.
Temperature Controlled Fan
So now that we know how to both control and monitor our motor, we can put everything together and construct our project.
The project is a temperature controller for a 4-pin computer fan. The fan utilizes an external power supply and operates at either 12 or 5 volts.
The controller has an OLED display that indicates temperature, fan speed, and fan status. It also has a Stall LED, which is activated if the motor is obstructed. If the stall condition is detected, the motor PWM is stopped to protect the motor. After a 5-second countdown, the motor will spin again, provided the obstruction has been removed.
The controller can be operated in two modes:
- Manual Mode – The potentiometer can be used to set speed, regardless of sensor temperature.
- Auto Mode – The fan is controlled by the temperature sensor. If the temperature reaches a preset trigger point (35 degrees Celsius in the default design, which can be changed), the fan will be triggered at low speed. As the temperature rises, the speed will increase.
You select modes using the potentiometer. If the pot is turned entirely to the left, then the system will enter Auto mode. If you turn the pot clockwise (i.e., to increase the motor speed), then you will enter Manual mode. The current mode is displayed on the OLED.
Temperature Controlled Fan Hookup
We will start with the same circuit that we have been using for testing the PWM and tachometer functions. To this existing circuit, we will add the following components:
- A 128 x 64 I2C OLED Display.
- A TMP36 analog temperature sensor.
- An LED (to use as a stall indicator).
- A dropping resistor for the LED. I used a 150-ohm resistor.
Here is how we will hook this up:

You may wish to mount the TMP36 remotely from the circuit board, as it should be mounted close to the “hot spot” that you are trying to cool. If you need to extend it, you might need to shield its wires or provide a filter capacitor physically close to its power pins. This is especially true if you are operating in an area of high electrical noise.
Temperature Controlled Fan Code
Here is the code we will be using for our temperature-controlled fan controller:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
/* PWM Fan Controller with Temperature Sensor pwm-fan-temp-sense.ino Control a 4-pin PWM computer fan Regulate speed with temperature DroneBot Workshop 2025 https://dronebotworkshop.com */ // Include required libraries #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> // Pin definitions const int PWM_PIN = 3; // D2 = GPIO 3 const int TACH_PIN = 2; // D1 = GPIO 2 const int TEMP_PIN = A3; // A3 = GPIO 29 const int LED_PIN = 7; // D8 = GPIO 7 const int POT_PIN = A0; // A0 = GPIO 26 // OLED display settings #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // PWM settings const int PWM_FREQ = 25000; // 25 kHz frequency for computer fans const int PWM_RESOLUTION = 8; // 8-bit resolution (0-255) // Temperature control settings (configurable) float TEMP_FAN_ON = 35.0; // Temperature to start fan (Celsius) float TEMP_FULL_SPEED = 60.0; // Temperature for full speed (Celsius) // Tachometer variables volatile unsigned long tachPulseCount = 0; unsigned long lastTachTime = 0; const unsigned long TACH_SAMPLE_TIME = 2000; // Sample period in milliseconds unsigned long currentRPM = 0; // System variables float currentTemp = 0.0; int fanSpeedPercent = 0; bool fanFailure = false; bool manualMode = false; // Interrupt service routine for tachometer void IRAM_ATTR tachISR() { tachPulseCount = tachPulseCount + 1; } void setup() { // Start Serial Monitor Serial.begin(115200); Serial.println("Temperature-Controlled Fan System Starting..."); // Initialize I2C Wire.begin(4, 5); // SDA=4, SCL=5 // Initialize OLED display if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("SSD1306 allocation failed"); for (;;) ; } display.clearDisplay(); display.setTextSize(1); display.setTextColor(WHITE); display.setCursor(0, 0); display.println("Fan Controller"); display.println("Initializing..."); display.display(); // Configure PWM ledcAttach(PWM_PIN, PWM_FREQ, PWM_RESOLUTION); // Configure pins pinMode(TACH_PIN, INPUT_PULLUP); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // Setup tachometer interrupt attachInterrupt(digitalPinToInterrupt(TACH_PIN), tachISR, FALLING); // Set initial fan speed to zero ledcWrite(PWM_PIN, 0); // Initialize timing lastTachTime = millis(); Serial.println("System ready!"); Serial.println("Temperature thresholds:"); Serial.println("Fan ON: " + String(TEMP_FAN_ON) + "°C"); Serial.println("Full Speed: " + String(TEMP_FULL_SPEED) + "°C"); delay(2000); // Delay to show startup message } void loop() { // Read temperature from TMP36 // TMP36 outputs 10mV per degree Celsius int tempReading = analogRead(TEMP_PIN); float voltage = (tempReading * 3.3) / 4095.0; // Convert to voltage currentTemp = voltage * 100.0; // Convert to Celsius (10mV/°C) // Check for manual override mode using potentiometer int potValue = analogRead(POT_PIN); if (potValue > 100) { // If potentiometer is turned (accounting for noise) manualMode = true; fanSpeedPercent = map(potValue, 0, 4095, 0, 100); } else { manualMode = false; // Automatic temperature control if (currentTemp < TEMP_FAN_ON) { fanSpeedPercent = 0; // Fan off below threshold } else if (currentTemp >= TEMP_FULL_SPEED) { fanSpeedPercent = 100; // Full speed at or above upper threshold } else { // Proportional control between thresholds fanSpeedPercent = map(currentTemp * 10, TEMP_FAN_ON * 10, TEMP_FULL_SPEED * 10, 30, 100); } } // Convert percentage to PWM value int pwmValue = map(fanSpeedPercent, 0, 100, 0, 255); // Calculate RPM every 2 seconds unsigned long currentTime = millis(); if (currentTime - lastTachTime >= TACH_SAMPLE_TIME) { // Calculate RPM (2 pulses per revolution for most fans) currentRPM = (tachPulseCount * 60000) / (TACH_SAMPLE_TIME * 2); // Check for fan failure if (fanSpeedPercent > 20 && currentRPM < 500) { fanFailure = true; // Safety: turn off fan if it's not spinning properly pwmValue = 0; fanSpeedPercent = 0; } else { fanFailure = false; } // Reset counters tachPulseCount = 0; lastTachTime = currentTime; } // Apply PWM to fan only if no failure detected if (!fanFailure) { ledcWrite(PWM_PIN, pwmValue); } else { ledcWrite(PWM_PIN, 0); } // Control status LED(on if fan failure) digitalWrite(LED_PIN, fanFailure ? HIGH : LOW); // Update OLED display updateDisplay(); // Serial output for monitoring Serial.print("Temp: "); Serial.print(currentTemp, 1); Serial.print("°C | Speed: "); Serial.print(fanSpeedPercent); Serial.print("% | RPM: "); Serial.print(currentRPM); if (fanFailure) Serial.print(" | FAN FAILURE!"); if (manualMode) Serial.print(" | MANUAL"); Serial.println(); delay(500); } void updateDisplay() { display.clearDisplay(); // Title display.setTextSize(1); display.setCursor(0, 0); display.println("Fan Controller"); // Temperature display.setTextSize(1); display.setCursor(0, 16); display.print("Temp: "); display.print(currentTemp, 1); display.println("C"); // Fan speed display.setCursor(0, 26); display.print("Speed: "); display.print(fanSpeedPercent); display.println("%"); // RPM display.setCursor(0, 36); display.print("RPM: "); display.println(currentRPM); // Status indicators display.setCursor(0, 46); if (fanFailure) { display.println("STATUS: FAILURE!"); } else if (manualMode) { display.println("STATUS: MANUAL"); } else { display.println("STATUS: AUTO"); } // Temperature bar graph display.drawRect(0, 56, 128, 8, WHITE); int tempBarWidth = map(currentTemp * 10, 0, 500, 0, 126); // 0-50°C range if (tempBarWidth > 126) tempBarWidth = 126; if (tempBarWidth > 0) { display.fillRect(1, 57, tempBarWidth, 6, WHITE); } display.display(); } |
How the Code Works
This is the final code for our fan controller project. It brings everything together: temperature sensing, manual control with a potentiometer, fan speed measurement (RPM), and a visual display. Let’s walk through how it operates.
1. Libraries and Global Settings
At the very top, we include the necessary libraries for the OLED display. We then define all our pins and set up key variables.
- Configurable Settings: The most important variables for you are TEMP_FAN_ON and TEMP_FULL_SPEED. You can easily change these values to decide at what temperature the fan should turn on and at what temperature it should reach maximum speed.
- System Variables: We also have variables like currentTemp, fanSpeedPercent, fanFailure, and manualMode to keep track of the system’s status at all times.
2. The setup() Function
The setup() function runs once to prepare all the hardware.
- It starts the Serial Monitor and the I2C communication needed for the OLED display.
- It initializes the OLED screen and prints a startup message.
- It configures the ESP32’s PWM hardware with the standard 25 kHz frequency for our fan.
- Finally, it sets up the tachometer pin as an input and attaches the tachISR() interrupt to it, which will count the fan’s pulses in the background.
3. The loop()
The loop() is where the main logic happens, running over and over. It follows a clear sequence of tasks each time it runs.
- Read Sensors: The first thing it does is read the current temperature from the TMP36 sensor and the current position of the 10K potentiometer.
- Choose a Mode (Auto vs. Manual): It checks the potentiometer’s value. If you’ve turned the knob, the code switches to manualMode. In this mode, the fan’s speed is set directly by the potentiometer. If the pot is turned all the way down, the code switches back to automatic temperature control.
- Calculate Fan Speed:
- In automatic mode, it uses a series of if/else statements to compare the currentTemp to your TEMP_FAN_ON and TEMP_FULL_SPEED settings, calculating the appropriate fan speed percentage.
- In manual mode, it simply maps the potentiometer’s reading to a fan speed percentage from 0 to 100%.
4. Checking the Fan’s Health
Every two seconds, a special timed block of code runs to check the fan’s RPM and health.
- Calculate RPM: It uses the tachPulseCount (which has been updated in the background by our interrupt) to calculate the fan’s current speed in RPM.
- Detect Failure: This is a key safety feature. The code checks if the fan should be spinning (i.e., fanSpeedPercent > 20) but its actual RPM is very low (currentRPM < 500). If both are true, it assumes the fan has stalled, sets the fanFailure flag to true, and turns on the warning LED. As a precaution, it also cuts power to the fan.
5. Applying the Speed and Updating the Display
After all the checks and calculations, the code takes action.
- It converts the final fanSpeedPercent into a PWM value and sends it to the fan (unless a failure was detected).
- It then calls the updateDisplay() function to draw all the fresh information onto the OLED screen.
The updateDisplay() function is responsible for neatly formatting all our data—temperature, speed percentage, RPM, and system status (AUTO, MANUAL, or FAILURE!)—and even draws a nice temperature bar graph at the bottom of the screen.
Load the code onto the XIAO and try it out. The OLED should activate instantly; if it doesn’t, check your wiring to ensure you haven’t reversed SDA and SCL.

You should get a display of the temperature, fan speed, and mode. Move the pot and observe how it goes in and out of Manual mode.
Try carefully stopping the fan to verify that the stall detection is working. It should stop the PWM, light the LED, and show a short countdown on the OLED.

And find a source of heat to test the TMP36. Verify that the fan comes on when the first trip point is reached.
This controller would be excellent mounted on a small perfboard. If you go as far as to design a PCB for it, you could use the XIAO in surface-mount mode, as it has castellated pins for this purpose.
Conclusion
Cooling fans can be a vital component in computer or hardware design. The controlled temperature system constructed today can be used in conjunction with these fans to maintain temperatures within specifications efficiently.
Keep in mind that fans don’t need to be used just for electronics. A small 5-volt fan with a USB power input is a great way to build a personal cooling fan; you can reduce the trip point to about 20 degrees and the high point to 30 degrees for this application.
Whatever you use it for, you now know how to “keep your cool” in any situation by harnessing the power of a PWM-controlled fan!
Parts List
Here are some components you might need to complete the experiments in this article. Please note that some of these links may be affiliate links, and the DroneBot Workshop may receive a commission on your purchases. This does not increase the cost to you and is a method of supporting this ad-free website.
Seeeduino XIAO ESP32-S3 Digikey Amazon
SSD1306 128×64 OLED Amazon
Resources
Code Samples – All the code used in this article in one easy-to-use ZIP file!
Article PDF – A PDF version of this article in a ZIP file.







Hi. What is the positive power supply (or acting as such) to the esp32 in the wiring diagrams?
In the experiment the ESP32 is powered by the USB input from the computer, which provides the 5 and 3.3 volt outputs. When you use an ESP32 stand-alone, you can apply 5-volts to the 5V pin (it can also be an input).
On cheaper (2-pin or 3-pin) units, this electronic controller is self-contained and can’t be controlled externally. However, 4-pin fans can be externally controlled using PWM. Good, usefull in-depth info. I’m working on a similar external fan for a small computer (Intel NUC) that freezes (the linux OS, because cpu overheating?) reguarly in summer temperatures. I will change my program a bit after having read this article. I recently came across somewhere a way to control the speed of a 3-wire fan : apply PWM control in the positive power line of the fan, you will need a high-side driver or… Read more »
I would love to have the final code that includes the fan Stall/Retry feature as shown in the video 27:40 thru 27:55.