- 25 kHz
- Fan PWM frequency
- 8 -bit
- PWM resolution
- 5
- Fan curve points
- 1,584
- Lines of C++
What it is
An AI-Thinker ESP32-CAM doing two jobs at once: streaming live video to a browser, and driving a PC case fan properly — real 25 kHz PWM, real tachometer feedback, and a stall check that notices when the rotor stops.
Why I built it
I wanted to see the thing I was cooling. A fan controller that reports RPM tells you a number; a fan controller with a camera pointed at the fan tells you whether the number is true. The camera started as a debugging aid and turned out to be the more interesting half.
Driving the fan correctly
A 4-pin fan’s PWM input is not a general-purpose speed knob. The specification asks for 25 kHz, and the reason is audible: drive it at a few hundred hertz and the fan whines at the switching frequency, because the coils are being pulsed inside the range human hearing is most sensitive to. Push the carrier up to 25 kHz and it moves above that band.
#define LEDC_FAN_FREQ 25000 // 25 kHz — per the 4-wire fan spec
#define LEDC_LED_FREQ 5000
#define LEDC_RES 8 // 8-bit: 0-255
The fan gets its own LEDC channel, and the flash LED gets another at a much lower frequency, because an LED does not care and a lower carrier gives smoother dimming at the bottom of the range.
The constraint that shapes the whole file: the OV2640 needs a clock, and on this board that clock is generated by LEDC too. The channel assignments are therefore not arbitrary — they are chosen so the fan and LED channels cannot collide with the camera’s XCLK. Get that wrong and the symptom is not a broken fan, it is a camera that fails to initialise.
Reading the tachometer
A 4-wire fan’s tach line pulses twice per revolution, open-collector. It is read
on GPIO2 as INPUT_PULLUP with a falling-edge interrupt, counting pulses in
the ISR and converting to RPM outside it.
GPIO2 is 3.3 V tolerant only. The tach line must not be pulled to 5 V — this
is written in the header as a warning because it is the kind of mistake that
costs a board and is invisible until it isn’t.
Stall detection
The useful failure to catch is a fan that is commanded to spin and isn’t: something jammed the blades, or a bearing seized. The check compares measured RPM against commanded PWM — if the ratio falls below a threshold and stays there past a delay, it is a stall rather than a transient.
30.0f, // stallRatio — RPM must track PWM by at least this much
3.0f, // stallDelay — sustained for this many seconds before it counts
The delay is the part that matters. Fans take time to spin up, and a threshold without hysteresis fires every time the curve steps. Three seconds is long enough to ride through spin-up and short enough to be worth having.
Concurrency
Streaming video and servicing a control loop on one dual-core chip means the work has to be split explicitly. Four FreeRTOS tasks, with the fan and tach loops at a higher priority than the web server and the stream:
#define PRI_WEB 5
#define PRI_STREAM 5
#define PRI_FAN 6
#define PRI_TACH 6
That ordering is deliberate. If the network gets busy, the stream should stutter — the fan control loop should not. Dropping frames is a cosmetic failure; missing a stall is a thermal one.
Limitations
- The stream is MJPEG, which is bandwidth-hungry and has no audio. It is the right choice for this board and the wrong choice for anything with a budget.
- One client at a time is the realistic limit for the stream.