Automatic fan
A small DC motor fan that speeds up as the room gets brighter, or warmer with a DS18B20. The ESP32 drives it through a transistor with smooth, silent PWM.
Wiring
| From | To board | Note |
|---|---|---|
| Motor + | 5V | From USB or a separate 5 V supply, never from a GPIO pin |
| Motor - | Transistor collector | |
| Transistor emitter | GND | Check your transistor's pin order before wiring: a PN2222A is emitter, base, collector with the flat side facing you, but many parts sold as 2N2222 (such as P2N2222A) are collector, base, emitter |
| Transistor base, through 470 Ω | GPIO 25 | About 5 mA of base current, enough to switch the motor fully on |
| 1N4001 across the motor | Stripe to motor +, other end to motor - | Catches the voltage spike when the motor switches off. Backwards it shorts the supply |
| Photoresistor | 3V3 to GPIO 34 | One leg to 3V3, the other to the pin |
| 10 kΩ resistor | GPIO 34 to GND | With the photoresistor, brighter light gives a higher reading |
| Optional DS18B20 data | GPIO 4 | VCC to 3V3, GND to GND, 4.7 kΩ from data to 3V3. Then set SOURCE to USE_TEMPERATURE |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32 Dev Module
// Automatic fan: a small DC motor spins faster as the light gets brighter (a
// sunny window warms a room), or as a DS18B20 reads warmer if you wire one and
// set SOURCE to USE_TEMPERATURE. The motor runs through an NPN transistor with
// a flyback diode; the ESP32 pin only sends the PWM signal.
#include <OneWire.h>
#include <DallasTemperature.h>
const int MOTOR_PIN = 25; // to the transistor's base, through 470 Ω
const int LDR_PIN = 34; // ADC1 pin: photoresistor and 10 kΩ divider
const int TEMP_PIN = 4; // optional DS18B20 data line (4.7 kΩ pull-up to 3V3)
enum Source { USE_LIGHT, USE_TEMPERATURE };
const Source SOURCE = USE_LIGHT;
const int PWM_HZ = 20000; // above hearing, so the motor does not whine
const int PWM_BITS = 8; // duty from 0 to 255
const int MIN_DUTY = 90; // below this the motor hums but does not turn; tune for yours
// Light mode: fan off in the dark, full speed in bright light (percent of the ADC range).
const int LIGHT_OFF = 20, LIGHT_FULL = 80;
// Temperature mode: fan off below TEMP_OFF, full speed at TEMP_FULL (°C).
const float TEMP_OFF = 24.0, TEMP_FULL = 30.0;
OneWire oneWire(TEMP_PIN);
DallasTemperature probe(&oneWire);
int lastDuty = 0;
// Duty from 0 (off) to 1 (full) for a reading between the off and full points.
float fraction(float value, float off, float full) {
return constrain((value - off) / (full - off), 0.0f, 1.0f);
}
void setDuty(float f) {
int duty = f <= 0 ? 0 : MIN_DUTY + (int)roundf(f * (255 - MIN_DUTY));
if (duty > 0 && lastDuty == 0) {
ledcWrite(MOTOR_PIN, 255); // a short kick gets a stopped motor turning
delay(150);
}
ledcWrite(MOTOR_PIN, duty);
lastDuty = duty;
}
void setup() {
Serial.begin(115200);
ledcAttach(MOTOR_PIN, PWM_HZ, PWM_BITS); // Arduino-ESP32 core 3.x LEDC API
ledcWrite(MOTOR_PIN, 0);
analogSetPinAttenuation(LDR_PIN, ADC_11db);
if (SOURCE == USE_TEMPERATURE) {
probe.begin();
if (probe.getDeviceCount() == 0) Serial.println("No DS18B20 found: check the data wire and the 4.7 kΩ pull-up");
}
Serial.println(SOURCE == USE_LIGHT ? "Auto fan: following the light" : "Auto fan: following the temperature");
}
void loop() {
float f;
if (SOURCE == USE_LIGHT) {
long sum = 0;
for (int i = 0; i < 16; i++) sum += analogRead(LDR_PIN);
const int light = (int)roundf(sum / 16.0f * 100.0f / 4095.0f);
f = fraction(light, LIGHT_OFF, LIGHT_FULL);
setDuty(f);
Serial.printf("light = %d%%, duty = %d%%\n", light, lastDuty * 100 / 255);
} else {
probe.requestTemperatures();
const float c = probe.getTempCByIndex(0);
if (c == DEVICE_DISCONNECTED_C) {
setDuty(1); // fail safe: run the fan if the sensor is lost
Serial.println("sensor lost: fan on full");
} else {
f = fraction(c, TEMP_OFF, TEMP_FULL);
setDuty(f);
Serial.printf("temp_c = %.1f, duty = %d%%\n", c, lastDuty * 100 / 255);
}
}
delay(500);
}Wiring
| From | To board | Note |
|---|---|---|
| Motor + | 5V | From USB or a separate 5 V supply, never from a GPIO pin |
| Motor - | Transistor collector | |
| Transistor emitter | GND | Check your transistor's pin order before wiring: a PN2222A is emitter, base, collector with the flat side facing you, but many parts sold as 2N2222 (such as P2N2222A) are collector, base, emitter |
| Transistor base, through 470 Ω | GPIO 5 | About 5 mA of base current, enough to switch the motor fully on |
| 1N4001 across the motor | Stripe to motor +, other end to motor - | Catches the voltage spike when the motor switches off. Backwards it shorts the supply |
| Photoresistor | 3V3 to GPIO 2 | One leg to 3V3, the other to the pin |
| 10 kΩ resistor | GPIO 2 to GND | With the photoresistor, brighter light gives a higher reading |
| Optional DS18B20 data | GPIO 4 | VCC to 3V3, GND to GND, 4.7 kΩ from data to 3V3. Then set SOURCE to USE_TEMPERATURE |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32S3 Dev Module
// Automatic fan: a small DC motor spins faster as the light gets brighter (a
// sunny window warms a room), or as a DS18B20 reads warmer if you wire one and
// set SOURCE to USE_TEMPERATURE. The motor runs through an NPN transistor with
// a flyback diode; the ESP32 pin only sends the PWM signal.
#include <OneWire.h>
#include <DallasTemperature.h>
const int MOTOR_PIN = 5; // to the transistor's base, through 470 Ω
const int LDR_PIN = 2; // ADC1 pin: photoresistor and 10 kΩ divider
const int TEMP_PIN = 4; // optional DS18B20 data line (4.7 kΩ pull-up to 3V3)
enum Source { USE_LIGHT, USE_TEMPERATURE };
const Source SOURCE = USE_LIGHT;
const int PWM_HZ = 20000; // above hearing, so the motor does not whine
const int PWM_BITS = 8; // duty from 0 to 255
const int MIN_DUTY = 90; // below this the motor hums but does not turn; tune for yours
// Light mode: fan off in the dark, full speed in bright light (percent of the ADC range).
const int LIGHT_OFF = 20, LIGHT_FULL = 80;
// Temperature mode: fan off below TEMP_OFF, full speed at TEMP_FULL (°C).
const float TEMP_OFF = 24.0, TEMP_FULL = 30.0;
OneWire oneWire(TEMP_PIN);
DallasTemperature probe(&oneWire);
int lastDuty = 0;
// Duty from 0 (off) to 1 (full) for a reading between the off and full points.
float fraction(float value, float off, float full) {
return constrain((value - off) / (full - off), 0.0f, 1.0f);
}
void setDuty(float f) {
int duty = f <= 0 ? 0 : MIN_DUTY + (int)roundf(f * (255 - MIN_DUTY));
if (duty > 0 && lastDuty == 0) {
ledcWrite(MOTOR_PIN, 255); // a short kick gets a stopped motor turning
delay(150);
}
ledcWrite(MOTOR_PIN, duty);
lastDuty = duty;
}
void setup() {
Serial.begin(115200);
ledcAttach(MOTOR_PIN, PWM_HZ, PWM_BITS); // Arduino-ESP32 core 3.x LEDC API
ledcWrite(MOTOR_PIN, 0);
analogSetPinAttenuation(LDR_PIN, ADC_11db);
if (SOURCE == USE_TEMPERATURE) {
probe.begin();
if (probe.getDeviceCount() == 0) Serial.println("No DS18B20 found: check the data wire and the 4.7 kΩ pull-up");
}
Serial.println(SOURCE == USE_LIGHT ? "Auto fan: following the light" : "Auto fan: following the temperature");
}
void loop() {
float f;
if (SOURCE == USE_LIGHT) {
long sum = 0;
for (int i = 0; i < 16; i++) sum += analogRead(LDR_PIN);
const int light = (int)roundf(sum / 16.0f * 100.0f / 4095.0f);
f = fraction(light, LIGHT_OFF, LIGHT_FULL);
setDuty(f);
Serial.printf("light = %d%%, duty = %d%%\n", light, lastDuty * 100 / 255);
} else {
probe.requestTemperatures();
const float c = probe.getTempCByIndex(0);
if (c == DEVICE_DISCONNECTED_C) {
setDuty(1); // fail safe: run the fan if the sensor is lost
Serial.println("sensor lost: fan on full");
} else {
f = fraction(c, TEMP_OFF, TEMP_FULL);
setDuty(f);
Serial.printf("temp_c = %.1f, duty = %d%%\n", c, lastDuty * 100 / 255);
}
}
delay(500);
}Wiring
| From | To board | Note |
|---|---|---|
| Motor + | 5V | From USB or a separate 5 V supply, never from a GPIO pin |
| Motor - | Transistor collector | |
| Transistor emitter | GND | Check your transistor's pin order before wiring: a PN2222A is emitter, base, collector with the flat side facing you, but many parts sold as 2N2222 (such as P2N2222A) are collector, base, emitter |
| Transistor base, through 470 Ω | GPIO 5 | About 5 mA of base current, enough to switch the motor fully on |
| 1N4001 across the motor | Stripe to motor +, other end to motor - | Catches the voltage spike when the motor switches off. Backwards it shorts the supply |
| Photoresistor | 3V3 to GPIO 3 | One leg to 3V3, the other to the pin |
| 10 kΩ resistor | GPIO 3 to GND | With the photoresistor, brighter light gives a higher reading |
| Optional DS18B20 data | GPIO 4 | VCC to 3V3, GND to GND, 4.7 kΩ from data to 3V3. Then set SOURCE to USE_TEMPERATURE |
Tested on this board
Not run on this board yet. Treat the wiring and code as a first draft.
Firmware, Arduino sketch for ESP32C3 Dev Module
// Automatic fan: a small DC motor spins faster as the light gets brighter (a
// sunny window warms a room), or as a DS18B20 reads warmer if you wire one and
// set SOURCE to USE_TEMPERATURE. The motor runs through an NPN transistor with
// a flyback diode; the ESP32 pin only sends the PWM signal.
#include <OneWire.h>
#include <DallasTemperature.h>
const int MOTOR_PIN = 5; // to the transistor's base, through 470 Ω
const int LDR_PIN = 3; // ADC1 pin: photoresistor and 10 kΩ divider
const int TEMP_PIN = 4; // optional DS18B20 data line (4.7 kΩ pull-up to 3V3)
enum Source { USE_LIGHT, USE_TEMPERATURE };
const Source SOURCE = USE_LIGHT;
const int PWM_HZ = 20000; // above hearing, so the motor does not whine
const int PWM_BITS = 8; // duty from 0 to 255
const int MIN_DUTY = 90; // below this the motor hums but does not turn; tune for yours
// Light mode: fan off in the dark, full speed in bright light (percent of the ADC range).
const int LIGHT_OFF = 20, LIGHT_FULL = 80;
// Temperature mode: fan off below TEMP_OFF, full speed at TEMP_FULL (°C).
const float TEMP_OFF = 24.0, TEMP_FULL = 30.0;
OneWire oneWire(TEMP_PIN);
DallasTemperature probe(&oneWire);
int lastDuty = 0;
// Duty from 0 (off) to 1 (full) for a reading between the off and full points.
float fraction(float value, float off, float full) {
return constrain((value - off) / (full - off), 0.0f, 1.0f);
}
void setDuty(float f) {
int duty = f <= 0 ? 0 : MIN_DUTY + (int)roundf(f * (255 - MIN_DUTY));
if (duty > 0 && lastDuty == 0) {
ledcWrite(MOTOR_PIN, 255); // a short kick gets a stopped motor turning
delay(150);
}
ledcWrite(MOTOR_PIN, duty);
lastDuty = duty;
}
void setup() {
Serial.begin(115200);
ledcAttach(MOTOR_PIN, PWM_HZ, PWM_BITS); // Arduino-ESP32 core 3.x LEDC API
ledcWrite(MOTOR_PIN, 0);
analogSetPinAttenuation(LDR_PIN, ADC_11db);
if (SOURCE == USE_TEMPERATURE) {
probe.begin();
if (probe.getDeviceCount() == 0) Serial.println("No DS18B20 found: check the data wire and the 4.7 kΩ pull-up");
}
Serial.println(SOURCE == USE_LIGHT ? "Auto fan: following the light" : "Auto fan: following the temperature");
}
void loop() {
float f;
if (SOURCE == USE_LIGHT) {
long sum = 0;
for (int i = 0; i < 16; i++) sum += analogRead(LDR_PIN);
const int light = (int)roundf(sum / 16.0f * 100.0f / 4095.0f);
f = fraction(light, LIGHT_OFF, LIGHT_FULL);
setDuty(f);
Serial.printf("light = %d%%, duty = %d%%\n", light, lastDuty * 100 / 255);
} else {
probe.requestTemperatures();
const float c = probe.getTempCByIndex(0);
if (c == DEVICE_DISCONNECTED_C) {
setDuty(1); // fail safe: run the fan if the sensor is lost
Serial.println("sensor lost: fan on full");
} else {
f = fraction(c, TEMP_OFF, TEMP_FULL);
setDuty(f);
Serial.printf("temp_c = %.1f, duty = %d%%\n", c, lastDuty * 100 / 255);
}
}
delay(500);
}Parts
| Quantity | Part | Where to buy |
|---|---|---|
| 1× | ESP32 dev board | Buy at Amazon |
| 1× | Small DC motor (130 size) with a propeller or fan blade | |
| 1× | NPN transistor, PN2222 or 2N2222 | Buy at Adafruit |
| 1× | 1N4001 diode | Buy at Adafruit |
| 1× | 470 Ω resistor, for the transistor's base | |
| 1× | Photoresistor (light sensor) | Buy at Adafruit |
| 1× | 10 kΩ resistor, for the light sensor | |
| 1× | Optional: DS18B20 temperature probe | Buy at Adafruit |
| 1× | Optional: 4.7 kΩ resistor for the DS18B20 | Buy at Amazon |
| 8× | Jumper wires |
Amazon buy links may include an Associates tag, and this site may earn a commission from qualifying Amazon purchases at no extra cost to you. Adafruit links are direct.
Libraries
OneWire, DallasTemperature
Load it
- In the Arduino IDE, install the esp32 board package and pick the board named above the sketch.
- Install the libraries listed above from the Library Manager.
- Paste the sketch, choose the board's port and upload. Hold BOOT if the upload stalls at Connecting.
- Close the IDE's serial monitor, then open the bench here to watch the output.
Field notes
- Never run a motor straight from a GPIO pin. The transistor switches the motor's current from the 5 V supply; the pin only drives the transistor's base through 470 Ω. With 1 kΩ the transistor may not switch fully on and runs warm.
- The diode matters: a motor is a coil, and when the transistor switches it off the coil kicks back a voltage spike. The diode, stripe towards 5 V, gives that spike somewhere harmless to go.
- A PN2222 handles a small 130 motor. A bigger motor, or one that stalls, can draw more than its 600 mA limit and get hot: use a logic-level MOSFET instead.
- PWM runs at 20 kHz so the motor does not whine. Below MIN_DUTY a motor hums without turning; tune it until the slowest setting still spins, and the sketch gives a stopped motor a short full-power kick.
- A 9 V battery is a poor supply for this: it sags quickly under a motor's load. A USB power bank or a 5 V adapter lasts far longer.
- Light mode suits a sunny window that warms a room. For a real temperature fan, wire the DS18B20 and set SOURCE to USE_TEMPERATURE; if the sensor is lost the fan runs at full speed to be safe.
Not tested yet
- Not tested on hardware yet. Compiles for every board listed with Arduino-ESP32 core 3.3.12.
Community builds
Loading build reports from people who made this.
Built it? Send a build report with your board and what the serial port printed. Working reports become the "Tested on this board" entries above.