If you need closed-loop thermal management for a DIY electronics enclosure, 3D printer hotend, or server rack, a simple thermostat relay won't cut it. Relays cause temperature oscillation and mechanical wear. The definitive solution is a Proportional-Integral (PI) controller driving a Pulse Width Modulated (PWM) fan. This arduino code tutorial provides a complete, decision-forward blueprint to build a precision cooling system using the modern Arduino Uno R4 Minima, an I2C temperature sensor, and a logic-level MOSFET.
The direct answer for your hardware stack: Use the Arduino Uno R4 Minima (ABX00080) paired with an Adafruit SHT31-D breakout board and an IRLB8721 logic-level MOSFET. This combination guarantees 5V logic compatibility, high-accuracy I2C temperature readings, and zero-gate-drive overheating on the switching transistor.
Hardware Decision Matrix: Sensor and Switch Selection
Before writing code, you must select components that match your physical constraints. Use this decision table to lock in your exact part numbers.
| Component | Scenario / Constraint | Recommended Part | Why This Wins |
|---|---|---|---|
| Temp Sensor | Need high accuracy (<0.2°C) and fast I2C response for closed-loop PI tuning. | SHT31-D (Adafruit Breakout) | 16-bit ADC, integrated pull-ups, 3-5V tolerant. Avoid DHT22 (too slow/low-res for PI). |
| Temp Sensor | Wire runs exceed 3 meters; I2C capacitance will corrupt the signal. | DS18B20 (1-Wire) | Differential signaling over long distances, but requires OneWire library and parasitic power care. |
| MOSFET | Driving a 12V fan directly from the Arduino's 5V digital pin. | IRLB8721 (Infineon) | Logic-level gate. Rds(on) is 2.4mΩ at Vgs=4.5V. Stays cool without a heatsink at 5A. |
| MOSFET | Using a standard IRF520 or IRFZ44N from a generic kit. | REJECT / DO NOT USE | Requires 10V+ at the gate to fully open. At 5V, it operates in the linear region, dissipating massive heat and burning out. |
Bill of Materials and Pin Mapping
The Arduino Uno R4 Minima features a Renesas RA4M1 ARM Cortex-M4 processor. Unlike the older ATmega328P, it natively supports 5V logic but has a more complex timer architecture for PWM. Ensure your wiring matches this exact spec sheet.
| Module / Part | Pin / Terminal | Arduino R4 Minima Pin | Notes & Warnings |
|---|---|---|---|
| SHT31-D Breakout | VIN | 5V | Breakout has onboard regulator; 5V is required. |
| SHT31-D Breakout | GND | GND | Share common ground with MOSFET source. |
| SHT31-D Breakout | SDA | A4 | Primary I2C bus on R4 Minima. |
| SHT31-D Breakout | SCL | A5 | Primary I2C bus on R4 Minima. |
| IRLB8721 MOSFET | Gate | D9 | Hardware PWM capable. Add 10kΩ pulldown to GND. |
| IRLB8721 MOSFET | Drain | Fan Negative (-) | Switches the low side of the 12V fan circuit. |
| IRLB8721 MOSFET | Source | GND | Must tie to 12V PSU ground and Arduino GND. |
| 12V PC Fan | Positive (+) | 12V PSU (+) | Do NOT power the fan from the Arduino 5V/VIN pin. |
The Complete Arduino Code Tutorial: PI Fan Control
This code implements a custom Proportional-Integral (PI) controller. We avoid the standard PID_v1 library here to eliminate external dependencies and demonstrate exactly how the math maps to the PWM output. The code includes explicit error handling for I2C timeouts and sensor dropouts, defaulting to a 50% failsafe fan speed if the sensor goes offline.
Target Board: Arduino Uno R4 Minima. Core: Arduino UNO R4 Boards (version 1.2.0 or newer).
#include <Wire.h>
#include "Adafruit_SHT31.h"
// --- PIN DEFINITIONS ---
const int PIN_FAN_PWM = 9;
const int PIN_SHT_SDA = A4;
const int PIN_SHT_SCL = A5;
// --- PI CONTROLLER TUNING PARAMETERS ---
float Kp = 15.0; // Proportional gain (aggressive response to current error)
float Ki = 0.8; // Integral gain (eliminates steady-state offset)
float setpoint = 35.0; // Target temperature in Celsius
float integral = 0.0;
unsigned long lastTime = 0;
// Initialize the I2C sensor object
Adafruit_SHT31 sht31 = Adafruit_SHT31();
void setup() {
Serial.begin(115200);
// Explicitly set 8-bit PWM resolution for the R4 Minima ARM core
analogWriteResolution(8);
pinMode(PIN_FAN_PWM, OUTPUT);
analogWrite(PIN_FAN_PWM, 0); // Ensure fan is off at boot
Wire.begin(PIN_SHT_SDA, PIN_SHT_SCL);
// Attempt I2C handshake
if (!sht31.begin(0x44)) { // Default I2C address is 0x44
Serial.println("FATAL: Couldn't find SHT31 on I2C bus 0x44");
Serial.println("Check SDA/SCL wiring and pull-up resistors.");
while(1) { delay(10); } // Halt execution safely
}
Serial.println("System Online. PI Fan Control Active.");
lastTime = millis();
}
void loop() {
float temp = sht31.readTemperature();
// --- ERROR HANDLING: Sensor Dropout ---
if (isnan(temp)) {
Serial.println("ERROR: Sensor read failed (NaN). I2C NACK or timeout.");
analogWrite(PIN_FAN_PWM, 128); // Failsafe: Force 50% duty cycle
delay(1000);
return; // Skip PI math this cycle
}
// --- PI MATH ---
unsigned long now = millis();
float dt = (now - lastTime) / 1000.0; // Delta time in seconds
lastTime = now;
float error = temp - setpoint;
integral += error * dt;
// Anti-windup clamping prevents integral saturation during large spikes
if (integral > 50.0) integral = 50.0;
if (integral < -50.0) integral = -50.0;
float output = (Kp * error) + (Ki * integral);
// Clamp output to valid 8-bit PWM range (0-255)
int pwmVal = constrain((int)output, 0, 255);
analogWrite(PIN_FAN_PWM, pwmVal);
// --- TELEMETRY ---
Serial.print("Temp: "); Serial.print(temp, 1);
Serial.print("C | Error: "); Serial.print(error, 1);
Serial.print(" | PWM: "); Serial.println(pwmVal);
delay(500); // 2Hz control loop frequency
}
Debugging: First Three Things to Check When It Fails
Embedded hardware rarely works perfectly on the first power-up. If your fan isn't spinning or the serial monitor is throwing errors, follow this exact diagnostic sequence.
Compile-Time Error: Missing Library
Exact Error String: fatal error: Adafruit_SHT31.h: No such file or directory
- Cause 1: You haven't installed the library. Open Tools > Manage Libraries, search for
Adafruit SHT31, and install it. It will prompt you to install theAdafruit BusIOdependency; click 'Install All'. - Cause 2: Case-sensitivity typo. Linux and macOS compilers are strict.
adafruit_sht31.hwill fail. It must match the header exactly.
Run-Time Error: I2C Bus Collision
Exact Error String: FATAL: Couldn't find SHT31 on I2C bus 0x44
If you see this in the Serial Monitor, the Arduino is sending data but getting no acknowledgment (NACK). Check these three things first:
- Verify the I2C Address: The SHT31-D breakout defaults to
0x44. If theADDRpad on the breakout is bridged with solder, the address shifts to0x45. Run the standard Arduino I2C Scanner sketch to confirm the hex address on your specific board. - Check Logic Level Pull-ups: I2C requires pull-up resistors on SDA and SCL. The official Adafruit breakout includes 10kΩ onboard pull-ups. If you are using a raw SHT31 chip or a cheap clone board without pull-ups, the bus will float, and
Wire.hwill hang or fail. Add 4.7kΩ resistors from SDA/SCL to 5V. - Confirm Shared Grounds: The 12V power supply for the fan must share a common ground wire with the Arduino's GND pin. Without a shared reference ground, the MOSFET gate voltage is undefined, and I2C return currents have no path.
Hardware Symptom: Fan Runs at 100% or 0% Only
If the serial monitor shows PWM values changing (e.g., 40, 85, 120) but the fan only clicks or runs at full speed, your MOSFET is not switching correctly.
- Cause: You used a standard MOSFET (like IRF520) instead of a logic-level MOSFET. The 5V from Pin D9 is not enough to lower the Rds(on) resistance. Swap to the IRLB8721 or IRLZ44N.
- Fix: Add a 10kΩ pulldown resistor between the MOSFET Gate and Source (GND). This prevents the gate from floating and turning the fan on during Arduino boot-up before the pin is initialized.
Extending and Simplifying the Build
Once the baseline PI controller is stable, you can adapt the architecture to fit your specific project constraints.
How to Simplify: Drop the PI Math
If you are cooling a simple power supply and don't care about holding an exact temperature, strip out the integral math and use a 3-tier threshold system. This saves flash memory and eliminates tuning headaches.
// Simplified Threshold Logic
if (temp < 30.0) analogWrite(PIN_FAN_PWM, 0); // Off
else if (temp < 40.0) analogWrite(PIN_FAN_PWM, 100); // ~40% speed
else analogWrite(PIN_FAN_PWM, 255); // 100% speed
How to Extend: Upgrade to a 4-Wire PWM Fan
The IRLB8721 MOSFET setup is designed for standard 2-pin or 3-pin fans. However, modern 4-pin PC fans (Intel PWM spec) have a dedicated blue PWM control wire that expects a 25kHz carrier frequency. Chopping the 12V ground line with a MOSFET at standard Arduino frequencies (490Hz) causes audible whining in 4-wire fans.
The Upgrade Path:
- Ditch the MOSFET entirely.
- Connect the fan's 12V (Yellow) to 12V PSU, GND (Black) to PSU GND, and Tach (Green) to D2 for RPM counting.
- Connect the PWM wire (Blue) directly to Arduino Pin D9.
- Use the Arduino UNO R4 Minima Cheat Sheet to configure the AGT timer for a 25kHz output using the
analogWriteFrequency(9, 25000)function (specific to the R4 ARM core).
By moving to a 4-wire fan, you eliminate the MOSFET heat dissipation problem entirely, as the fan's internal controller handles the high-current switching. For high-reliability enclosures in 2026, the 4-wire Intel-spec fan is the definitive endpoint for thermal management.






