Controlling high-power 12V COB (Chip-on-Board) LED strips with an Arduino seems straightforward until you point a smartphone camera at the light and see aggressive banding. This flicker happens because the default Arduino analogWrite() function runs at roughly 490Hz on most pins. To build a truly professional, flicker-free Arduino light controller, you need to manipulate the hardware timers to push the PWM frequency above 1kHz and drive the load with a proper logic-level MOSFET.

This guide walks through building a 60W-capable, serial-and-potentiometer-controlled dimmer. We will bypass the beginner trap of using IRF520 modules, wire a discrete IRLZ44N MOSFET, and write bare-metal timer code for smooth, high-frequency dimming.

Project Overview & Difficulty Rating

Difficulty: Intermediate (Requires basic soldering and understanding of PWM registers)
Estimated Time: 90 minutes
Estimated Cost: $45 - $55 (depending on LED strip length)

Exact Parts List

  • Microcontroller: Arduino Nano V3 (ATmega328P, 5V/16MHz) - Official or high-quality clone with CH340/FTDI USB-UART chip (~$6 - $22)
  • Switching MOSFET: IRLZ44N N-Channel Logic-Level MOSFET (TO-220 package). Do not use the IRF520; it requires 10V on the gate to fully turn on, which the Nano cannot provide. (~$1.50)
  • LED Load: 12V COB LED Strip, 5-meter roll, drawing ~4A max (e.g., BTF-Lighting or WAC Lighting) (~$25)
  • Power Supply: Mean Well LRS-60-12 (12V, 5A, 60W enclosed switching supply) (~$18)
  • Passives: 1kΩ gate resistor, 10kΩ gate-to-source pulldown resistor, 10kΩ potentiometer (~$1.00 total)
  • Wire: 18 AWG stranded copper for the 12V LED runs; 22 AWG solid for breadboard/perfboard logic.

Hardware Spec Sheet & Pin Mapping

Before wiring, it is critical to understand the electrical limits of your components. The IRLZ44N is chosen specifically for its low RDS(on) at 5V gate drive, ensuring it stays cool without a massive heatsink at 4A.

Table 1: Component Specifications & Operating Limits
Parameter IRLZ44N (MOSFET) Arduino Nano (ATmega328P) Mean Well LRS-60-12
Max Continuous Current 47A (at 25°C case) 40mA per I/O pin 5.0A (12V rail)
Gate Threshold / Logic Level VGS(th) 1.0V - 2.0V 5V HIGH output N/A
RDS(on) at VGS = 5V ~22 mΩ N/A N/A
Max Voltage Rating 55V (VDSS) 5V (VCC) 12V DC Output
Thermal Dissipation (No Heatsink) ~2W safe limit in free air N/A 60W max output

At 4A, the power dissipated by the IRLZ44N is I² × R = 16 × 0.022 = 0.352W. This is well within the ~2W free-air limit, meaning you do not need a heatsink for this specific Arduino light build. If you push past 8A, bolt it to a piece of aluminum.

Pin Mapping

Arduino Nano Pin Connects To Function
D9 (PWM) 1kΩ Resistor -> MOSFET Gate Timer1 Channel A (OC1A) High-Freq PWM Output
A0 Potentiometer Wiper Analog Input for manual dimming override
5V Potentiometer Pin 1 Reference voltage for analog reading
GND Potentiometer Pin 3, MOSFET Source, 10kΩ Pulldown Common logic and power ground

Step-by-Step Wiring & Assembly

Safety Callout: While 12V DC is not a shock hazard, a 60W power supply can deliver enough current to melt 22 AWG wire and start a fire if a short circuit occurs. Always use 18 AWG or thicker for the 12V LED power runs, and ensure the Mean Well supply is properly earthed (FG terminal to mains ground).
  1. Prepare the MOSFET Gate Drive: Solder the 1kΩ resistor to the Gate (middle pin) of the IRLZ44N. This resistor limits the inrush current from the Nano's I/O pin into the MOSFET's gate capacitance, protecting the ATmega328P from voltage spikes during switching (Source: All About Circuits).
  2. Install the Pulldown Resistor: Solder the 10kΩ resistor between the Gate and Source (right pin) of the MOSFET. Do not skip this. When the Arduino boots, its pins are high-impedance (floating) for a few hundred milliseconds. Without a pulldown, stray capacitance can turn the MOSFET partially on, causing the LEDs to flash brightly at boot.
  3. Wire the Load: Connect the 12V positive from the Mean Well supply directly to the positive pad of the COB LED strip. Connect the negative pad of the LED strip to the Drain (left pin) of the MOSFET.
  4. Complete the Power Circuit: Connect the Source (right pin) of the MOSFET to the negative terminal of the Mean Well power supply. Tie the Arduino Nano's GND to this same negative terminal to establish a common ground reference.
  5. Wire the Manual Override: Connect the 10kΩ potentiometer. Left pin to Nano 5V, right pin to Nano GND, middle wiper to Nano A0.

Complete Arduino Light Controller Code

This sketch targets the Arduino Nano V3 (ATmega328P). It bypasses the standard analogWrite() function to manually configure Timer1 for Fast PWM at ~7.8kHz. This frequency is completely invisible to smartphone cameras and eliminates acoustic whine from the LED drivers.

The code reads a potentiometer for local dimming but also accepts serial commands (0-100) for integration with external systems. It includes bounds-checking to prevent serial buffer overruns and invalid duty cycle errors.

// Target Board: Arduino Nano V3 (ATmega328P, 5V/16MHz)
// Project: High-Frequency Flicker-Free COB LED Controller

const int PWM_PIN = 9;    // OC1A - Must use Pin 9 or 10 for Timer1
const int POT_PIN = A0;   // Analog input for manual potentiometer

// Variables for serial parsing and state management
String serialBuffer = "";
int currentBrightness = 0; // 0 to 255
bool serialOverride = false;

void setup() {
  Serial.begin(115200);
  pinMode(PWM_PIN, OUTPUT);
  pinMode(POT_PIN, INPUT);
  
  // Ensure pin is LOW before configuring timer to prevent boot flash
  digitalWrite(PWM_PIN, LOW); 

  // --- Timer1 Configuration for ~7.8kHz Fast PWM ---
  // Clear Timer1 control registers
  TCCR1A = 0;
  TCCR1B = 0;
  
  // Set Fast PWM 8-bit mode (WGM11 and WGM10 in TCCR1A, WGM12 in TCCR1B)
  TCCR1A |= (1 << WGM10);
  TCCR1B |= (1 << WGM12);
  
  // Clear OC1A on Compare Match, set OC1A at BOTTOM (non-inverting mode)
  TCCR1A |= (1 << COM1A1);
  
  // Set prescaler to 8 (CS11 bit). 
  // Frequency = 16MHz / (Prescaler * 256) = 16,000,000 / (8 * 256) = 7812.5 Hz
  TCCR1B |= (1 << CS11);
  
  Serial.println("Arduino Light Controller Ready. Send 0-100 via Serial, or use Pot.");
}

void loop() {
  // 1. Handle Serial Input with Error Handling
  if (Serial.available() > 0) {
    char c = Serial.read();
    if (c == '\n' || c == '\r') {
      if (serialBuffer.length() > 0) {
        processSerialCommand(serialBuffer);
        serialBuffer = "";
      }
    } else {
      // Prevent buffer overflow attacks or garbage data lockups
      if (serialBuffer.length() < 5) {
        serialBuffer += c;
      }
    }
  }

  // 2. Read Potentiometer (Manual Override)
  // If pot is moved, it overrides serial commands
  int potVal = analogRead(POT_PIN);
  // Add deadband to prevent ADC jitter from flickering the light at low levels
  static int lastPotVal = 0;
  if (abs(potVal - lastPotVal) > 4) {
    lastPotVal = potVal;
    currentBrightness = map(potVal, 0, 1023, 0, 255);
    serialOverride = false;
    OCR1A = currentBrightness; // Update hardware register directly
  }

  // Small delay to stabilize ADC and reduce CPU heat
  delay(10); 
}

void processSerialCommand(String cmd) {
  cmd.trim();
  
  // Check if the string is a valid integer
  bool isNumber = true;
  for (unsigned int i = 0; i < cmd.length(); i++) {
    if (!isDigit(cmd[i])) {
      isNumber = false;
      break;
    }
  }
  
  if (!isNumber) {
    Serial.println("ERROR: Non-numeric input. Send integer 0-100.");
    return;
  }
  
  int percent = cmd.toInt();
  
  // Bounds checking
  if (percent < 0 || percent > 100) {
    Serial.println("ERROR: Value must be 0-100.");
    return;
  }
  
  // Map percentage to 8-bit PWM (0-255)
  currentBrightness = map(percent, 0, 100, 0, 255);
  OCR1A = currentBrightness;
  serialOverride = true;
  
  Serial.print("Brightness set to ");
  Serial.print(percent);
  Serial.println("%");
}

Debugging: First Three Things to Check When It Fails

When working with high-current PWM and bare-metal registers, things can go wrong in non-obvious ways. If your build fails, check these three items in order.

1. The LED stays fully ON at boot and ignores dimming

  • Cause A (Most Likely): The 10kΩ gate pulldown resistor is missing or soldered to the wrong pin. The Nano's D9 pin floats during the bootloader sequence, and the MOSFET's gate capacitance holds enough charge to turn the FET on.
  • Cause B: The MOSFET is wired backward. The IRLZ44N pinout (facing you, tab in back) is Gate, Drain, Source. If you swapped Drain and Source, the internal body diode will conduct 12V to the LEDs constantly, bypassing the gate entirely.

2. Upload fails with: avrdude: stk500_recv(): programmer is not responding

  • Cause A (Most Likely): You have a 12V short bleeding into the Nano. If your 12V LED positive accidentally touched the Nano's 5V or 3.3V pin, you have likely bricked the ATmega328P or the USB-UART bridge chip. Disconnect the 12V supply immediately and test the Nano standalone via USB.
  • Cause B: You are using a cheap clone Nano with a CH340 chip, but your PC lacks the CH340 driver. Install the latest CH340 drivers from the manufacturer, or select the correct COM port in the Arduino IDE.

3. The LED flickers on camera, or the MOSFET gets too hot to touch

  • Cause A (Flicker): You used analogWrite(PWM_PIN, val) instead of the custom Timer1 register code provided above. analogWrite on Pin 9 defaults to Phase Correct PWM at 490Hz. You must use the TCCR1A/B register manipulation to hit 7.8kHz (Source: Arduino Analog Output Docs).
  • Cause B (Heat): You used an IRF520 instead of an IRLZ44N. The IRF520 requires 10V on the gate to achieve its rated RDS(on). At 5V from the Nano, it operates in its linear (resistive) region, acting like a 2-ohm heater rather than a closed switch. Swap to a true logic-level FET (IRLZ44N, IRLB8721, or FQP30N06L).

Extending and Simplifying the Build

Depending on your end goal, you may want to scale this Arduino light project up for home automation or down for a quick prototype.

How to Simplify (No Bare-Metal Code)

If manipulating hardware timers and soldering TO-220 MOSFETs feels like overkill, swap the discrete components for an Adafruit 16-Channel 12-bit PWM/Servo Shield (PCA9685). The PCA9685 handles the high-frequency PWM generation via I2C, completely offloading the work from the ATmega328P. You will still need external MOSFETs for the 12V COB strips, but the shield provides clean, buffered gate drive signals and eliminates the need for Timer1 register math.

How to Extend (Smart Home Integration)

To integrate this dimmer into Home Assistant or a custom IoT dashboard:

  1. Swap the Brain: Replace the Arduino Nano with an ESP32-WROOM-32 DevKit v1. The ESP32's LEDC (LED Control) peripheral natively supports up to 20kHz PWM without complex register math.
  2. Add MQTT: Use the PubSubClient library to subscribe to an MQTT topic (e.g., home/livingroom/cob_light/set).
  3. Level Shifting Warning: The ESP32 is a 3.3V logic device. The IRLZ44N gate threshold is low enough that 3.3V might turn it on, but it will not fully saturate at high currents. Add a simple 2N2222 BJT or a TC4427 MOSFET driver between the ESP32 GPIO and the IRLZ44N gate to step the 3.3V logic up to a robust 12V gate drive.

By understanding the relationship between gate charge, PWM frequency, and logic levels, you can reliably drive high-power lighting loads without the flicker, heat, or boot-flash issues that plague beginner builds.