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.
Default Pick: For 95% of bench and enclosure builds under 2 meters, lock in the SHT31-D and the IRLB8721. The rest of this tutorial assumes these exact parts.

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 the Adafruit BusIO dependency; click 'Install All'.
  • Cause 2: Case-sensitivity typo. Linux and macOS compilers are strict. adafruit_sht31.h will 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:

  1. Verify the I2C Address: The SHT31-D breakout defaults to 0x44. If the ADDR pad on the breakout is bridged with solder, the address shifts to 0x45. Run the standard Arduino I2C Scanner sketch to confirm the hex address on your specific board.
  2. 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.h will hang or fail. Add 4.7kΩ resistors from SDA/SCL to 5V.
  3. 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:

  1. Ditch the MOSFET entirely.
  2. Connect the fan's 12V (Yellow) to 12V PSU, GND (Black) to PSU GND, and Tach (Green) to D2 for RPM counting.
  3. Connect the PWM wire (Blue) directly to Arduino Pin D9.
  4. 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.