If you need a variable Arduino controlled power supply for bench testing DC loads—like dimming high-power LED strips, driving small 12V DC motors, or testing heating elements—you do not need a complex linear regulator that dissipates massive heat. The most efficient, reliable approach for hobbyist and prototyping loads is a Pulse Width Modulation (PWM) driver using a logic-level MOSFET. By pairing an Arduino Nano v3 with an IRLZ44N MOSFET, you can switch up to 12V at 20A+ with minimal heat generation, effectively creating a variable DC output controller.

This guide provides the exact hardware topology, a pin mapping table, production-ready firmware with ADC noise filtering, and a concrete debugging path for when your output gets stuck at full voltage.

Project Overview & Topology Decision Path

When designing a variable voltage or variable speed controller, the first mistake makers make is choosing the wrong switching topology. Linear regulators waste power as heat, while raw PWM without proper gate driving leads to MOSFET overheating and microcontroller brownouts. Use the decision matrix below to select the right topology for your specific load.

Decision Tree: Variable DC Topology Selection
Topology Best For Drawbacks Verdict
Linear (LM317) Ultra-low noise analog audio circuits (<1A) Massive heat dissipation; requires huge heatsinks for >2A. Reject for high-current bench loads.
PWM + RC Filter Creating a true analog DC voltage (DAC) High output impedance; voltage sags under load; slow transient response. Reject for driving motors or heaters.
PWM + Logic-Level MOSFET DC motors, LED strips, resistive heaters (up to 20A) Output is square-wave PWM, not pure DC (fine for 95% of DC loads). DEFAULT PICK: Use IRLZ44N for 12V/20A loads.

Assumption for this build: We are driving a DC load (motor, LED, heater) that tolerates PWM. If you strictly need a pure, smooth DC analog voltage for sensitive op-amp circuits, you must add an LC low-pass filter and a unity-gain op-amp buffer to the output, which is outside the scope of this direct-drive build.

Parts List & Hardware Specifications

Do not substitute the MOSFET. The commonly sold IRF520 module requires 10V on the gate to fully turn on (Vgs = 10V). Since the Arduino Nano outputs 5V logic, the IRF520 will only partially open, acting as a resistor and burning up at high currents. You must use a logic-level MOSFET like the IRLZ44N, which fully saturates at Vgs = 4.5V.

Spec Sheet & Bill of Materials
Component Exact Variant / Part Number Key Specification Approx. Cost (2026)
Microcontroller Arduino Nano v3 (ATmega328P) 5V logic, 10-bit ADC, 490Hz PWM on D3 $6.00
MOSFET IRLZ44N (Logic-Level N-Channel) Vgs(th) = 1-2V, Rds(on) = 22mΩ @ 5V $1.50
Input Control 10kΩ Linear Taper Potentiometer B10K (Linear, NOT Audio/Log taper) $1.00
Display 0.96" I2C OLED (SSD1306 driver) 128x64 resolution, 3.3V-5V tolerant $4.50
Gate Resistor 100Ω 1/4W Carbon Film Prevents high-frequency ringing on gate $0.10
Pull-down Resistor 10kΩ 1/4W Carbon Film Keeps MOSFET OFF during Nano boot-up $0.10

Pin Mapping & Assembly Steps

Wiring a MOSFET directly to a microcontroller requires two critical passive components: a gate series resistor to limit the inrush current from charging the MOSFET's internal gate capacitance, and a pull-down resistor to ensure the load stays off while the Arduino's bootloader is running.

Pin Mapping Table
Arduino Nano Pin Destination Notes
D3 (PWM) 100Ω Resistor -> MOSFET Gate D3 defaults to 490Hz PWM frequency.
A0 Potentiometer Wiper (Middle) Reads 0-5V analog control signal.
A4 (SDA) OLED SDA I2C Data line.
A5 (SCL) OLED SCL I2C Clock line.
5V Potentiometer Pin 1, OLED VCC Power for control logic.
GND MOSFET Source, Pot Pin 3, OLED GND Common ground reference.
Callout Tip: The Boot-Up Flutter Fix
When the Arduino Nano powers on or resets, its GPIO pins float as high-impedance inputs before the firmware initializes. This floating gate can pick up stray noise, causing the MOSFET to rapidly switch on and off, which can destroy inductive loads like motors. The 10kΩ pull-down resistor wired between the MOSFET Gate and Source (Ground) guarantees the gate is held at 0V until the Nano explicitly drives it HIGH.

Numbered Assembly Steps

  1. Mount the MOSFET: Secure the IRLZ44N to a small heatsink. Even at 22mΩ Rds(on), a 10A load will dissipate roughly 2.2W (P = I²R), which will burn your fingers without a heatsink.
  2. Wire the Gate Network: Connect Nano D3 to one leg of the 100Ω resistor. Connect the other leg to the IRLZ44N Gate. Wire the 10kΩ resistor between the Gate and Source (GND).
  3. Connect the Load: Wire your 12V power supply positive to the Load Positive. Wire Load Negative to the MOSFET Drain. Wire MOSFET Source to Power Supply GND.
  4. Wire the Inputs: Connect the 10kΩ pot outer legs to 5V and GND. Connect the wiper to A0.
  5. Verify with a Multimeter: Before applying 12V to the load, set your meter to continuity mode. Check for shorts between the 12V rail and GND. Measure the resistance between the MOSFET Drain and Source; it should read open-loop (OL) when the Nano is unpowered.

Complete Firmware (Targets: Arduino Nano v3)

The following code targets the Arduino Nano v3 (ATmega328P, Old Bootloader or standard). It includes a software exponential moving average (EMA) filter. Raw analogRead() values from cheap potentiometers often jitter by ±3 bits. Without filtering, this jitter causes the PWM output to audibly whine or visibly flicker at low duty cycles. The EMA filter smooths this without the latency of a simple averaging array.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// --- PIN DEFINITIONS ---
#define POT_PIN A0
#define PWM_PIN 3      // Must be a PWM-capable pin on Nano
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

// --- FILTER & CONTROL CONSTANTS ---
const float ALPHA = 0.15; // EMA smoothing factor (0.0 to 1.0). Lower = smoother but slower.
const int DEADZONE_LOW = 15;  // ADC value below which output is forced to 0
const int DEADZONE_HIGH = 1008; // ADC value above which output is forced to 255

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

float filteredADC = 0.0;
int lastPWM = -1;

void setup() {
  Serial.begin(115200);
  pinMode(PWM_PIN, OUTPUT);
  digitalWrite(PWM_PIN, LOW); // Ensure load is OFF immediately

  // Initialize OLED with error handling
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed. Check I2C wiring."));
    // Halt execution to prevent unmonitored operation
    while(true) { delay(1000); } 
  }
  
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.println("Variable Arduino");
  display.println("PWM Controller");
  display.display();
  delay(1000);
  
  // Prime the filter with an initial reading
  filteredADC = analogRead(POT_PIN);
}

void loop() {
  // 1. Read raw ADC
  int rawADC = analogRead(POT_PIN);
  
  // 2. Apply Exponential Moving Average (EMA) filter
  filteredADC = (ALPHA * rawADC) + ((1.0 - ALPHA) * filteredADC);
  
  // 3. Map 10-bit ADC (0-1023) to 8-bit PWM (0-255)
  int targetPWM = map((int)filteredADC, 0, 1023, 0, 255);
  
  // 4. Apply deadzones to eliminate jitter at extremes
  if (rawADC < DEADZONE_LOW) targetPWM = 0;
  if (rawADC > DEADZONE_HIGH) targetPWM = 255;
  
  // 5. Update hardware only if value changes (reduces I2C bus spam)
  if (targetPWM != lastPWM) {
    analogWrite(PWM_PIN, targetPWM);
    lastPWM = targetPWM;
    
    // Calculate pseudo-voltage for display (Assumes 12V rail)
    float pseudoVoltage = (targetPWM / 255.0) * 12.0;
    
    // Update OLED
    display.clearDisplay();
    display.setCursor(0, 0);
    display.setTextSize(1);
    display.println("Target Duty Cycle:");
    display.setTextSize(2);
    display.print(map(targetPWM, 0, 255, 0, 100));
    display.println(" %");
    display.setTextSize(1);
    display.print("Est. Voltage: ");
    display.print(pseudoVoltage, 1);
    display.println(" V");
    display.display();
  }
  
  delay(20); // 50Hz loop rate
}

Debugging: First Three Checks & Common Errors

When a variable PWM circuit fails, the issue is almost always in the gate drive network or a macro definition typo. If your build is not behaving, follow this strict diagnostic sequence.

The First Three Things to Check When It Fails

  1. Measure Vgs (Gate-to-Source Voltage): Put your multimeter in DC voltage mode. Put the black probe on the MOSFET Source (GND) and red on the Gate. Turn the pot to max. You must read ~4.8V to 5.0V. If you read 3.3V, your Nano is running on 3.3V logic (wrong board variant) or the USB cable is dropping voltage. If you read 0V, your D3 pin is dead or the 100Ω resistor is unseated.
  2. Check the Pull-Down Resistor: If the load turns on instantly when you plug in the USB, even before the code runs, your 10kΩ pull-down resistor is missing or broken. The gate is floating and picking up ambient EMI.
  3. Verify the Potentiometer Taper: If the output stays at 0V for half the dial rotation and then suddenly jumps to 100%, you accidentally bought an Audio Taper (A10K) potentiometer instead of a Linear Taper (B10K). Replace it with a B10K.

Compiler Error: "expected unqualified-id before numeric constant"

If the Arduino IDE throws this exact error string during compilation:

error: expected unqualified-id before numeric constant

Ranked Causes & Fixes:

  1. Reversed #define syntax (90% of cases): You wrote #define 3 PWM_PIN instead of #define PWM_PIN 3. The preprocessor replaces the number with the text, breaking the code. Fix the order.
  2. Missing Semicolon on Previous Line (10% of cases): A missing semicolon on a variable declaration above the #define block can cause the parser to misinterpret the macro. Check the lines immediately preceding your pin definitions.

Hardware Symptom: "Output Stuck at 12V (Full ON)"

If the load runs at 100% regardless of the potentiometer position, the MOSFET is either being held HIGH or it has failed short-circuit.

  • Cause A: The 100Ω gate resistor is shorted or bypassed, and a previous wiring mistake fed 12V back into the Nano's D3 pin, destroying the ATmega328P GPIO. Fix: Test D3 with a blink sketch. If dead, replace Nano.
  • Cause B: The IRLZ44N has experienced a Gate-Source overvoltage event (Vgs max is ±20V) or thermal runaway, welding the internal silicon into a short. Fix: Desolder MOSFET, test Drain-Source with diode mode on multimeter. If it reads 0.00V in both directions, it is dead. Replace and add a 15V Zener diode between Gate and Source for protection.

Extending and Simplifying the Build

Depending on your bench needs, you can strip this project down to its bare essentials or scale it up into a professional-grade lab tool.

How to Simplify (The 'Just Make It Work' Route)

If you do not have an I2C OLED display and just need to test a motor immediately, delete the Wire.h, Adafruit_GFX.h, and Adafruit_SSD1306.h includes. Remove all display.* lines. Replace the OLED feedback with Serial.println(targetPWM); and use the Arduino IDE's Serial Plotter tool to visualize your duty cycle in real-time. This reduces the code footprint by 80% and eliminates I2C bus hangups.

How to Extend (Adding Constant Current / CC Mode)

To turn this from a simple PWM switcher into a true laboratory power supply with overcurrent protection, add an INA219 I2C Current Sensor module between the 12V source and the load.

In the firmware, read the INA219 shunt voltage every loop iteration. If ina219.getCurrent_mA() exceeds your defined threshold (e.g., 2000mA), override the potentiometer input and force targetPWM = 0, triggering a software trip. This mimics the Constant Current (CC) foldback protection found in expensive bench supplies like the Rigol DP800 series, protecting your delicate prototypes from burning up if you accidentally short the output leads.

For deeper reading on PWM frequency limitations and ADC sampling rates on the ATmega328P, refer to the official Arduino analogWrite() documentation. For wiring and I2C address configuration for the display module, consult the Adafruit SSD1306 OLED guide.