Building a High-Power RGB-LED Driver

In this tutorial I show you how to build a 100W RGB-LED driver board based on the LM3404 constant current buck regulator.

Features

  • Power input: 36VDC @ 4A
  • Reverse polarity protection on power input connector
  • Forward voltage on each channel: 22 ‒ 34V (automatically adjusted)
  • Forward current on each channel: 0.9A (set by a resistor)
  • Enable/disable LM3404 drivers
  • PWM color dimming on each channel
  • +5V power supply for external microcontroller
  • +12V power supply for cooling fan
  • Selectable fan configuration (2/3/4-pin)
  • PWM fan control
  • RPM signal output from fan (3/4-pin only)
  • LM35 temperature sensor connectivity
  • Can be controlled with any PWM-capable 5V/3.3V microcontroller (Arduino / Raspberry Pi / ESP32)

Compatible 100W RGB-LED

  • Forward voltage:
    • red: 22 ‒ 26V
    • green: 33 ‒ 37V
    • blue: 33 ‒ 37V
  • Forward current: 0.9A / color

Alternatively you can use single color 30W LEDs per channel that have roughly the same forward voltages and exactly 0.9A forward current.

RGB-LEDs on AliExpress
Single color LEDs on AliExpress

Schematic

Schematic (V1.32)

If you want to use an RGB-LED chip with different power rating, use this online calculator to get proper component values for the driving circuit:
http://www.nomad.ee/micros/lm3404/
The BOM file also contains untested part list for 3-50W RGB-LEDs.

PCB

PCB top render (V1.31)
PCB bottom render (V1.31)
Physical dimensions (V1.31)

PCB images are rendered using tracespace view:
https://tracespace.io/view/
This is probably the best open source PCB render engine on the internet.

PCB assembly

PCB assembly top 01 (V1.31)
PCB assembly top 02 (V1.31)
PCB assembly bottom 01 (V1.31)
PCB assembly bottom 02 (V1.31)

Connector description

  • Power input (J1): reverse polarity protected screw terminal
  • LED red-channel output (J2): screw terminal
  • LED green-channel output (J3): screw terminal
  • LED blue-channel output (J4): screw terminal
  • Cooling fan output (J5): standard +12V 3/4-pin fan connector
    • -: ground
    • +: +12V
    • S: sense (RPM)
    • C: control (PWM)
  • Fan pin-selector (J6): a jumper here routes the F-PWM signal
    • 4P & x: 4-pin fan
    • 2P/3P & x: 2/3-pin fan
  • +5V output (J7): standard 2.54 mm pin header to power external microcontroller
  • Control pins (J8): standard 2.54 mm pin header
    • EN: pull this pin low to enable all LM3404 drivers or use a jumper to connect with the ground pin above it
    • RED: logic level PWM-signal (3.3V or 5V) to the red-channel
    • GREEN: logic level PWM-signal (3.3V or 5V) to the green-channel
    • BLUE: logic level PWM-signal (3.3V or 5V) to the blue-channel
    • F-PWM: logic level PWM-signal (3.3V or 5V) to the cooling fan for RPM control, no signal here means 100% RPM
    • F-RPM: fan RPM feedback (5V logic, not safe for 3.3V devices, two pulses per rotation, 3/4-pin fans only)
    • TEMP: analog signal routed from J9 (3.3V safe)
  • LM35 temperature sensor (J9): if you want to measure heat sink temperature
    • + : +5V
    • T : analog signal (0 ‒ 1V)
    • ‒ : ground

Operation

  • Don’t hotswap on the J1-J4 connectors! Power down the system before you connect/disconnect high-current wires.
  • Pull EN pin low externally or put a jumper on it to enable all LM3404 drivers. This pin is a simple ON/OFF switch, don’t PWM it!
  • Apply logic level PWM signal (3.3V or 5V, 100-2000 Hz) to RED, GREEN and BLUE pins to display colors and change brightness. If no control signal is present the RGB-LED won’t turn on.
  • Apply logic level PWM signal (3.3V or 5V, 100-2000 Hz) to F-PWM pin to control the cooling fan RPM. Leave this pin disconnected if you want 100% RPM by default. Make sure to put the jumper in the right place on the J6 connector!
  • Read F-RPM pin to verify cooling fan RPM. This only works with 3/4-pin fans. Leave this pin disconnected if not used. This signal is NOT 3.3V safe (two 5V pulses every rotation).
  • Read TEMP pin to get LM35 temperature sensor analog value (ideally mounted on the heat sink). Voltage/temperature conversion: 0mV = 0°C, 250mV = 25°C, 618mV = 61.8°C (10mV/°C). Leave this pin disconnected if not used. The analog signal is 3.3V safe (0-1V).

LED assembly

You can use a prepared LED heat sink too, if it has a hole pattern of 34×34 mm with M3 threading. I used this CPU heat sink because it was more accessible to me.

Drilling positions marked (pre-applied thermal paste is removed)
Drilling Ø2.5 mm holes
Tapping M3 threads

RGB-LED chips usually come in common anode configuration (positive pins are connected). The board handles channels separately and generates different forward voltages automatically so you have to split the anode pin with a small wire cutter to get 6 pins in total.
Solder wires to the pins and cover the joints with heat shrink tubes to avoid short circuits.
Finally when mounting the chip to the heat sink apply thermal paste between the chip and heat sink! Make sure it’s spread thinly across the whole chip surface. This way heat moves to the aluminum block more efficiently.

100W RGB-LED chip mounted on a CPU heat sink
Arctic Alpine 11 Plus heat sink

Power supply

Mean Well LRS-150-36 150W/36V/0-4,3A switching power supply

Test

Arduino UNO wiring diagram (100W)
When the 100W RGB-LED chip lights up

Example Arduino code (UNO)

[code language="cpp"]

// Default PWM-frequency for Timer 1 and 2 is 490.2 Hz

// 8-bit gamma correction table for linear brightness control
const byte PROGMEM gamma8[] =
{
      0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
      0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,
      1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,
      2,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  5,  5,  5,
      5,  6,  6,  6,  6,  7,  7,  7,  7,  8,  8,  8,  9,  9,  9, 10,
     10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16,
     17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
     25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36,
     37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50,
     51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68,
     69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89,
     90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114,
    115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142,
    144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175,
    177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213,
    215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255
};

byte frpmPin = 2;   // Fan RPM sense input
byte fpwmPin = 3;   // Fan PWM control output
byte enPin = 12;    // Enable LED drivers
byte redPin = 11;   // Red-channel: Timer 2 "A" output
byte greenPin = 10; // Green-channel: Timer 1 "B" output
byte bluePin = 9;   // Blue-channel: Timer 1 "A" output
byte temp = A0;     // Heat sink temperature sensor analog input

int  LEDTemp = 0;        // Heat sink temperature
byte calculatedPWM = 0;  // Control fan RPM with this PWM-value
volatile int fanRPM = 0; // Calculated fan RPM

void setup()
{ 
    Serial.begin(115200);
    
    pinMode(frpmPin, INPUT_PULLUP);
    pinMode(fpwmPin, OUTPUT);
    pinMode(enPin, OUTPUT);
    pinMode(redPin, OUTPUT);
    pinMode(greenPin, OUTPUT);
    pinMode(bluePin, OUTPUT);
    pinMode(temp, INPUT);
    pinMode(LED_BUILTIN, OUTPUT);

    attachInterrupt(digitalPinToInterrupt(frpmPin), calculateFanRPM, RISING);

    updateColor(redPin, 0, true);
    updateColor(greenPin, 0, true);
    updateColor(bluePin, 0, true);
    
    digitalWrite(enPin, LOW); // enable all LM3404 drivers
    digitalWrite(LED_BUILTIN, LOW); // turn off Arduino UNO's built-in LED
}

void loop()
{
    //readHeatSinkTemp();
    //changeFanRPM();
    //reportFanRPM();
    fadeAllColors(5); // wait 5 milliseconds between brightness changes
}

void updateColor(byte color, byte dutyCycle, bool gammaCorrection)
{
    if (gammaCorrection)
    {
        analogWrite(color, pgm_read_byte(&gamma8[dutyCycle])); // use the lookup table to get corrected duty cycle value
    }
    else
    {
        analogWrite(color, dutyCycle); // use original duty cycle value
    }
}

void fadeAllColors(int wait)
{
    for (int i = 0; i <= 255; i++) // fade in
    {
        updateColor(redPin, i, true);
        delay(wait);
    }
    
    for (int i = 255; i >= 0; i--) // fade out
    {
        updateColor(redPin, i, true);
        delay(wait);
    }

    for (int i = 0; i <= 255; i++) // fade in
    {
        updateColor(greenPin, i, true);
        delay(wait);
    }
    
    for (int i = 255; i >= 0; i--) // fade out
    {
        updateColor(greenPin, i, true);
        delay(wait);
    }

    for (int i = 0; i <= 255; i++) // fade in
    {
        updateColor(bluePin, i, true);
        delay(wait);
    }
    
    for (int i = 255; i >= 0; i--) // fade out
    {
        updateColor(bluePin, i, true);
        delay(wait);
    }
}

void readHeatSinkTemp(void)
{
    LEDTemp = analogRead(temp);
    //calculatedPWM = 0; // TODO
}

void changeFanRPM(void)
{
    //analogWrite(fpwmPin, calculatedPWM);
}

void calculateFanRPM(void) // ISR
{
    //fanRPM = 0; // TODO
}

void reportFanRPM(void)
{
    // TODO
    //Serial.print("Fan RPM: ");
    //Serial.println(fanRPM);
}

[/code]

Example Arduino code (ESP32-DevKitC)

This example uses BLE (Bluetooth Low Energy) to receive commands from a smartphone app called Bluefruit (Adafruit). After connecting to the ESP32 module navigate to Controller / Color Picker, select a color and tap the Select button. The RGB-LED should change its color immediately.
To control the cooling fan RPM send a text message via UART menu starting with the letter "F" following a single character. This character’s byte value is used as a PWM value. Example: F0, F1, F2, …, FA, FB, FC, …, Fa, Fb, Fc, …, Fz. I will rewrite this method to be more easy to use.

[code language="cpp"]

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// 8-bit gamma correction table for linear brightness control
const byte PROGMEM gamma8[] =
{
      0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
      0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,
      1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,
      2,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  5,  5,  5,
      5,  6,  6,  6,  6,  7,  7,  7,  7,  8,  8,  8,  9,  9,  9, 10,
     10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16,
     17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
     25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36,
     37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50,
     51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68,
     69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89,
     90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114,
    115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142,
    144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175,
    177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213,
    215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255
};

byte redChannel = 0;     // red-channel number
byte redPin = 25;        // red-channel output pin
byte greenChannel = 1;   // green-channel number
byte greenPin = 26;      // green-channel output pin
byte blueChannel = 2;    // blue-channel number
byte bluePin = 27;       // blue-channel output pin
int redPotValue = 0;     // red potentiometer
int greenPotValue = 0;   // green potentiometer
int bluePotValue = 0;    // blue potentiometer
int pwmFrequency = 500;  // Hz
byte pwmResolution = 8;  // bit
byte fanPwmPin = 14;
byte fanPwmChannel = 3;

// TODO: add LM3404 enable pin, fan PWM pin and temperature reading pin (analog)
// Fan RPM input pin can't be connected directly because it is pulled up to +5V and the signal would damage the 3.3V ESP32 device. Additional logic level converter circuit must be used.

BLECharacteristic *pCharacteristic;
bool deviceConnected = false;
uint8_t txValue = 0; // txValue is global, rxValue is local for now...

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID           "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID (fixed)
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

void updateColor(byte color, byte dutyCycle, bool gammaCorrection)
{
    if (gammaCorrection)
    {
        ledcWrite(color, pgm_read_byte(&gamma8[dutyCycle])); // use the lookup table to get corrected duty cycle value
    }
    else
    {
        ledcWrite(color, dutyCycle); // use original duty cycle value
    }
}

void fadeAllColors(int wait)
{
    for (int i = 0; i <= 255; i++) // fade in
    {
        updateColor(redChannel, i, true);
        delay(wait);
    }

    for (int i = 255; i >= 0; i--) // fade out
    {
        updateColor(redChannel, i, true);
        delay(wait);
    }

    for (int i = 0; i <= 255; i++) // fade in
    {
        updateColor(greenChannel, i, true);
        delay(wait);
    }

    for (int i = 255; i >= 0; i--) // fade out
    {
        updateColor(greenChannel, i, true);
        delay(wait);
    }

    for (int i = 0; i <= 255; i++) // fade in
    {
        updateColor(blueChannel, i, true);
        delay(wait);
    }

    for (int i = 255; i >= 0; i--) // fade out
    {
        updateColor(blueChannel, i, true);
        delay(wait);
    }
}

class MyServerCallbacks: public BLEServerCallbacks
{
    void onConnect(BLEServer* pServer)
    {
        deviceConnected = true;
    };

    void onDisconnect(BLEServer* pServer)
    {
        deviceConnected = false;
    }
};

class MyCallbacks: public BLECharacteristicCallbacks 
{
    // when something arrives to this sketch it triggers the onWrite callback!!!
    void onWrite(BLECharacteristic *pCharacteristic) 
    {
        // this is a local variable, can only be used within this function!!!
        std::string rxValue = pCharacteristic->getValue();

        if (rxValue.length() > 0) 
        {
            Serial.print("RX: ");
            for (int i = 0; i < rxValue.length(); i++)
            {
                if (rxValue[i] < 16) Serial.print("0");
                Serial.print(rxValue[i], 16);
                Serial.print(" ");
            }
            Serial.println();

            if ((rxValue[0] == 0x21) && (rxValue[1] == 0x43)) // these two conditions refer to the "Color Picker" tool in the Bluefruit smartphone app
            {
                updateColor(redChannel, rxValue[2], true);
                updateColor(greenChannel, rxValue[3], true);
                updateColor(blueChannel, rxValue[4], true);
            }
            else if (rxValue[0] == 0x46) // "F" for cooling fan PWM
            {
                ledcWrite(fanPwmChannel, rxValue[1]); // duty cycle is the second byte
            }
        }
    }
};

void setup()
{
    Serial.begin(115200);

    // Configure RGB-LED channels
    ledcSetup(redChannel, pwmFrequency, pwmResolution);
    ledcAttachPin(redPin, redChannel);
    ledcWrite(redChannel, 0);

    ledcSetup(greenChannel, pwmFrequency, pwmResolution);
    ledcAttachPin(greenPin, greenChannel);
    ledcWrite(greenChannel, 0);

    ledcSetup(blueChannel, pwmFrequency, pwmResolution);
    ledcAttachPin(bluePin, blueChannel);
    ledcWrite(blueChannel, 0);

    ledcSetup(fanPwmChannel, pwmFrequency, pwmResolution);
    ledcAttachPin(fanPwmPin, fanPwmChannel);
    ledcWrite(fanPwmChannel, 0);

    // Configure analog reading
    analogReadResolution(10); // Default of 12 is not very linear. Recommended to use 10 or 11 depending on needed resolution.
    analogSetAttenuation(ADC_11db); // Default is 11db which is very noisy. Recommended to use 2.5 or 6.

    // Create the BLE Device
    BLEDevice::init("RGB Mood Light");

    // Create the BLE Server
    BLEServer *pServer = BLEDevice::createServer();
    pServer->setCallbacks(new MyServerCallbacks());

    // Create the BLE Service
    BLEService *pService = pServer->createService(SERVICE_UUID);

    // Create a BLE Characteristic
    pCharacteristic = pService->createCharacteristic(
                        CHARACTERISTIC_UUID_TX,
                        BLECharacteristic::PROPERTY_NOTIFY
                      );
                      
    pCharacteristic->addDescriptor(new BLE2902());

    BLECharacteristic *pCharacteristic = pService->createCharacteristic(
                                          CHARACTERISTIC_UUID_RX,
                                          BLECharacteristic::PROPERTY_WRITE
                                         );

    pCharacteristic->setCallbacks(new MyCallbacks());

    // Start the service
    pService->start();

    // Start advertising
    pServer->getAdvertising()->start();
    Serial.println("Waiting a client connection to notify...");
}

void loop()
{
    if (deviceConnected)
    {
        // Send a byte-value back to the smartphone
        //Serial.printf("*** Sent Value: %d ***\n", txValue);
        //pCharacteristic->setValue(&txValue, 1);
        //pCharacteristic->notify();
        //txValue++;
    }
    else
    {
        fadeAllColors(5); // wait 5 milliseconds between brightness changes
        
//        redPotValue = analogRead(32) / 4; // Convert 10-bit reading to 8-bit
//        greenPotValue = analogRead(33) / 4;
//        bluePotValue = analogRead(34) / 4;
//        delay(50);
//        
//        updateColor(redChannel, redPotValue, true);
//        updateColor(greenChannel, greenPotValue, true);
//        updateColor(blueChannel, bluePotValue, true);
    }
}

[/code]

Downloads

Order

I sell on Tindie

Changelog

V1.30: Original

V1.31:

  • thermal vias placed under IC1, IC2, U1, U2 and U3,
  • R22 resistor added to limit N1 gate current when 2/3-pin fan configuration is used, probably doesn’t matter but better safe than sorry,
  • routing and silkscreen modifications.

V1.32:

  • silkscreen modification: fan (J5) pinout moved outside of the connector footprint.

5 thoughts on “Building a High-Power RGB-LED Driver

  1. hello i would like to know you can make effects such as light police light strobe light flash light fire

    it’s possible?

    thanks

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s