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

reverse polarity protected screw terminal LED red-channel output (J2): screw terminal

screw terminal LED green-channel output (J3): screw terminal

screw terminal LED blue-channel output (J4): screw terminal

screw terminal Cooling fan output (J5): standard +12V 3/4-pin fan connector -: ground +: +12V S: sense (RPM) C: control (PWM)

standard +12V 3/4-pin fan connector Fan pin-selector (J6): a jumper here routes the F-PWM signal 4P & x: 4-pin fan 2P/3P & x: 2/3-pin fan

a jumper here routes the signal +5V output (J7): standard 2.54 mm pin header to power external microcontroller

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)

standard 2.54 mm pin header LM35 temperature sensor (J9): if you want to measure heat sink temperature + : +5V T : analog signal (0 ‒ 1V) ‒ : ground

if you want to measure heat sink temperature

Operation Don’t hotswap on the J1-J4 connectors! Power down the system before you connect/disconnect high-current wires.

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!

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.

, and 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!

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 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).

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 ***

", 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

Changelog V1.30: Original V1.31: thermal vias placed under IC1 , IC2 , U1 , U2 and U3 ,

, , , and , 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,

resistor added to limit 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.