IR NeoPixel Controller
2nd June 2026
This is an IR NeoPixel Controller that lets you control a set of one or more high-power RGB LEDs with an infrared remote control:
![]()
The IR NeoPixel Controller, based on an AVR64DD14, controlling a high-power NeoPixel RGB LED.
It lets you control the hue, saturation, and brightness of each RGB LED independently, or alternatively set them all to the same settings. You can also switch off and on all the lights, or animate the lights with slowly-changing hue. It's based on an AVR64DD14 and a TSOP38238 IR receiver.
Introduction
I recently bought some smart colour bulbs, thinking it would be nice to be able to adjust the colour of the illumination in my living room. However it turned out that I also needed to buy a compatible smart hub, install a control app, create an account, and control the bulbs using my smartphone.
This is fine if you want to automate your home, but for a single light it was far more complicated than I really wanted. I therefore thought about building my own solution based on a NeoPixel interface, using an infrared remote control to control the brightness and colour of the LEDs. The advantage of using NeoPixel LEDs over dumb high-power RGB LEDs is that you can chain multiple lights together with just three wires.
This IR NeoPixel Controller will support up to nine lights, a number I chose because the IR remote provides keys 0 to 9. However, the main constraint will be how much power your power supply can handle. A typical USB-C output can provide 5V at 1.5A, which is enough for two or possibly three high-power 3W LEDs.
The IR NeoPixel Controller lets you control the hue, saturation, and brightness of each RGB LED independently, or alternatively set them all to the same settings. You can also switch off and on all the lights, or animate the lights with slowly-changing hue. The settings are stored in EEPROM, so if you disconnect and reconnect the power the LEDs remember their previous settings.
Components
The circuit consists of three components: the NeoPixel LEDs, the NeoPixel driver, and the IR remote control receiver.
High-power NeoPixel LEDs
For the LEDs I chose Adafruit's 3 watt chainable NeoPixel LEDs, that use WS2811 NeoPixel-protocol controllers to drive a high-power RGB LED mounted on an aluminium heatsink [1]. To use these set the #define at the start of the program to RGB.
Alternatively you can use the 4 watt RGBW versions [2], which are available in warm white, natural white, or cool white. To use these set the #define at the start of the program to RGBW:
![]()
The IR NeoPixel Controller controlling a high-power NeoPixel RGBW LED.
Warning! These high-power NeoPixel LEDs are extremely bright, so when testing the IR NeoPixel Controller I covered them with translucent bottle tops. When using it for room illumination I recommend using a reflector to direct it at the ceiling, or put it into a translucent globe light fitting [3]:

A high-power NeoPixel RGB LED in a translucent globe light fitting.
The high-power LEDs should be mounted on a heatsink to avoid overheating at maximum brightness. I used a square 40 x 40 x 11mm black [4] or silver [5] finned heatsink.
Note that although the high-power modules use the same connectors as Adafruit's LED strips, the polarity of the +5V and GND connections is reversed, so check before plugging them in!
This circuit will also drive standard WS2812 or WS2812B NeoPixel LEDs daisy-chained together, but these are each only about 0.3 watts. However, because perception of brightness is non linear they will look only about a third of the brightness of the high-power LEDs.
NeoPixel driver
To drive the LED modules I used my NeoPixel driver based on an AVR64DD14: NeoPixel Driver Program 3. This uses a Timer/Counter in the AVR processor to generate the waveforms needed by the NeoPixel protocol, and the SPI peripheral to output them as a serial stream, rather than the software approach used by many other NeoPixel interfaces.
IR remote control receiver
The IR remote control receiver is designed to work with remote controls that use the NEC protocol, like many low cost infrared remotes available from the Far East. The remote control I used is available from Adafruit [6], or their distributors such as The Pi Hut in the UK [7]. The NEC protocol is also used by the Apple Remote [8], so if you have one of those you could use it. If you want to use a different remote control you can find its key codes as described below: Finding the codes for an IR remote.
Using the IR NeoPixel Controller
The following diagram shows the function of each key on the Adafruit remote control:

Initially all the LEDs will be off, at minimum brightness, the saturation will be at maximum, and the remote control will control the first LED.
- To control an individual LED press the corresponding number key, 1 to 9. To control all the LEDs press 0.
- To control the brightness press or hold down ▲ or ▼.
- To change the hue (colour) press or hold down ◄ or ►. The hue goes in the sequence: red, yellow, green, cyan, blue, magenta, and back to red.
- To change the saturation press or hold down VOL- or VOL+. Reducing the saturation dilutes the colour with white.
- To turn off all the LEDs press STOP. Press it again to revert to their previous settings.
The settings are saved automatically every 10 seconds.
The circuit
Here's the circuit of the IR NeoPixel Controller:
![]()
Circuit for the IR NeoPixel Controller.
Infrared receiver
The circuit uses the popular TSOP38238 38kHz infrared receiver module in a 3-pin through-hole package, and available from Adafruit [9] or most component suppliers.
Because I am planning to build a surface mount version of the circuit on a PCB I also tested the circuit with a surface-mount equivalent, the TSOP36138, mounted on a SOIC-12 breakout board [10] (it wouldn't fit on an SOIC-8 breakout board):
![]()
A version of the circuit using a TSOP36138 surface-mount infrared receiver module
mounted on a breadboard.
Microcontroller
The project is controlled by a 14-pin AVR processor, the AVR64DD14. This has just enough I/O pins to perform all the functions, including an error light, provided you configure PF6 as an input to receive the IR signals from the IR receiver.
The program is small enough to fit on an AVR32DD14 or even an AVR16DD14, although I haven't tested them.
Error light
The error light flashes if a complex pattern cannot be evaluated in the 20ms between updates of the NeoPixels, but given the small number of LEDs in this application this will probably never happen, so I've also made it flash once to show that the IR remote control has set maximum or minimum brightness or saturation.
Construction
I built the circuit on a mini breadboard, available from SparkFun [11] or The Pi Hut in the UK [12]. Pre-cut colour-coded jumper wires make the interconnections easier [13].
The AVR64DD14 isn't available in a dual in-line package, so I mounted it on an SOIC-14 breakout board [14].
The program
IR remote control format
The NEC format was originally developed by the consumer electronics firm NEC. The data is encoded using pulse distance encoding. Each bit starts with 562.5µs of carrier. A zero has a total width of 1.125ms before the start of the next bit, and a one has a total width of 2.25ms before the next bit:

The carrier pulse consists of 21 cycles at 38kHz. The pulses usually have a mark/space ratio of 1:4, to reduce the current consumption:

Each code sequence starts with a 9ms pulse, known as the AGC pulse. This is followed by a 4.5ms silence:

The data consists of 32 bits: a command field consisting of an 8-bit id and an 8-bit key code, and a 16-bit address field:

The bits are transmitted low-order bit first and high-order bit last. Note that there needs to be one extra pulse at the end to terminate the last bit.
The address identifies the equipment being controlled, and the key identifies the key pressed.
In the original version of the NEC protocol the 8-bit id was just an inverted version of the key. The Apple Remote uses the full 16-bits of the address and command fields; the address is 0x87EE, and the id is used to allow a remote to be paired to a particular product.
IR receiver
The IR remote control receiver code is based on my earlier IR Remote Control Receiver, which is designed to work with the NEC protocol.
The timing of each component of the NEC protocol can conveniently be expressed in multiples of 112.5µs. The multiples are defined by the following constants:
const int ZeroBit = 10; // 10 * 112.5us = 1.125ms const int OneBit = 20; // 20 * 112.5us = 2.25ms const int RepeatPulse = 100; // 100 * 112.5us = 11.25ms const int AGCPulse = 120; // 120 * 112.5us = 13.5ms
I used Timer/Counter TCB to do the timing. First, it is configured to count in units of 112.5µs:
TCB0.CCMP = 2700-1; // 112.5us TCB0.CTRLA = TCB_CLKSEL_DIV1_gc | TCB_ENABLE_bm; // Divide timer by 1 TCB0.CTRLB = 0; // Periodic Interrupt mode TCB0.INTCTRL = TCB_CAPT_bm; // Overflow interrupt
Note that this assumes a 24MHz clock, and you will need to change the divisor for a different clock rate.
Next, the TCB interrupt service routine simply counts interrupts in the global variable Ticks:
ISR(TCB0_INT_vect) {
TCB0.INTFLAGS = TCB_CAPT_bm; // Clear interrupt flag
if (Ticks < 255) Ticks++;
}
The output from the IR receiver goes to input PF6, and a pin-change interrupt is set on this pin:
PORTF.PIN6CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;// Interrupt falling edge
The PORTF interrupt service routine is called on each falling edge of the IR input:
ISR (PORTF_PORT_vect) {
static uint32_t RecdData;
// Waiting for AGC pulse and gap
int Time = Ticks;
if (NextBit == -1) { // Waiting for AGC pulse
if ((Time > AGCPulse-10) && (Time <= AGCPulse+10)) { // Got AGC pulse
RecdData = 0; NextBit = 0;
} else if ((Time > RepeatPulse-10) && (Time <= RepeatPulse+10)) {
ProcessCode(RecdData, true); // Repeat code
}
} else { // Data bit
if ((Time > OneBit+5) || (Time == 255)) NextBit = -1; // Fail - restart
else {
if (Time > OneBit-5) { // Bit = '1'
RecdData = RecdData | ((unsigned long) 1<<NextBit);
}
NextBit++;
if (NextBit == 32) { // Got complete code
ProcessCode(RecdData, false); // Normal code
NextBit = -1; // Go back to waiting
}
}
}
Ticks = 0; // Clear counter
PORTF.INTFLAGS = PIN6_bm; // Clear interrupt flag
}
This uses the global variable NextBit to keep track of the progress of the decoding. Initially NextBit is set to -1, to indicate that the routine is waiting for the AGC start pulse. As the bits are received they are shifted into RecdData, and NextBit is incremented. When NextBit reaches 32 the complete IR code has been received, and the routine calls ProcessCode() to perform the required action.
The routine checks that the length of the AGC pulse and gap is within 10 ticks of the required 13.5ms, and if so sets NextBit to 0 to start reading the code. It also checks whether the AGC pulse and gap is within 10 ticks of the 11.25ms width of a repeat code, and if so calls ProcessCode() with the same code and the second parameter set to true.
For each bit the routine checks its length. If it is within 5 ticks of 2.25ms it is taken as a 1-bit. If shorter than this it is taken as a 0-bit. If the bit is too long, or Time = 255, it indicates that the code hasn't been received correctly, so the processing of the code is abandoned and the routine returns to the waiting state.
Processing the IR remote key codes
The IR remote key presses are processed by the routine ProcessCode(), which takes the 32-bit code and a flag to specify whether it is a repeat code; ie the key is being held down.
The code is first separated into the address, key, and id components, and the address is checked to ensure that the code is from the Adafruit remote:
void ProcessCode (uint32_t code, bool repeat) {
uint16_t address = code & 0xffff;
uint8_t key = code >> 16 & 0xff;
uint8_t id = code >> 24 & 0xff;
if (address != IRAddress) return; // Check for remote
uint8_t n = State.LED;
switch (key) {
case UP: if (State.Control[n][VALUE] <= 253) State.Control[n][VALUE]+=2;
else Error(true); break;
...
}
}
The routine then uses a switch statement to perform the appropriate action for each key. In most cases this involves incrementing or decrementing one of the elements of the array State.Control[][]. The first array index is the number of the LED, from 1 to 9, or 0 to set all LEDs to the same value. The second array index is the parameter being controlled: hue, saturation, or value (brightness). For simplicity I've only shown the processing for the UP key, which increases the brightness.
An earlier version of the program used a NeedsSaving flag to indicate whether the data had changed and needed to be saved to EPROM, but I realised that this was unnecessary because the Arduino EEPROM library EEPROM.update() function, and its wrapper EEPROM.put(), only write to the EEPROM if the value to be written is different from what's already there, so there's no wear on the EEPROM if the data hasn't changed.
NeoPixel driver
The NeoPixel driver code is identical to the program described in my earlier project A NeoPixel Driver using AVR Hardware [3]. The main change is that the number of channels is specified by a global variable Channels, to allow for RGBW or GRBW NeoPixels.
Actually, there is one other change. In my first prototype I found that the NeoPixels sometimes flickered when operating the IR remote. I tracked the problem down to the pin-change interrupt interfering with the timing of the SPI0 interrupt. The solution is to give the SPI0 interrupt priority by defining it as a level 1 high priority vector, with the statement:
CPUINT.LVL1VEC = 20; // Give SPI0_INT priority
The value 20 is the SPI0_INT vector number, which you can find in the datasheet Interrupt Vector Mapping table.
Pattern functions
My earlier Light Lab project provided 22 alternative patterns which included both static and animated displays that you could control with three potentiometers, and it supported strips of up 160 LEDs. To achieve this the pattern functions used fixed-point arithmetic, because floating point wasn't fast enough to support that number of LEDs.
This IR Controlled NeoPixel application has more modest ambitions: it only allows up to nine LEDs, and there is essentially one pattern which allows you to adjust the hue, saturation, and brightness of each or all LEDs with the remote control. The pattern functions are written using floating-point calculations, which is fast enough with this number of LEDs, and this makes them simpler than their equivalents in Light Lab.
For example, here's the main hsv() function for RGB or GRB displays:
float hsv (int k, float h, float s, float v) {
return clamp(2.0 - (3.0 * tri(h - k/3.0))) * v * s + v * (1 - s);
}
It takes an integer channel number k, from 0 to 2 for red, green, and blue, and floating-point values of hue, saturation, and value (brightness), of 0.0 to 1.0. It returns the intensity of that channel, from 0.0 to 1.0.
It uses the functions fract(), fractional part, clamp(), which clamps its parameter to the range 0.0 to 1.0, and dist(), which returns the the shortest distance around a circle between two points on a circle of circumference 1.0.
For RGBW or GRBW LEDs the additional channel number 3 indicates the white channel, and the function becomes:
float hsv (int k, float h, float s, float v) {
if (k == 3) return v * (1 - s); // White channel
else return clamp(2.0 - (3.0 * tri(h - k/3.0))) * v * s;
}
For an explanation of how these hsv() functions work see A Single-Line HSV Function.
Saving the state in EEPROM
After setting the LEDs to the colour settings you want to use, if you switch off and on again they are restores to the same settings. To do this the settings are saved in a struct called State:
struct {
uint8_t flag = NumPixels; // Check if changed
uint8_t Control[NumPixels+1][3]; // Control values
uint8_t LED = 1; // 1 to 9 or 0 = all
bool Animate = false; // Changing display
bool On = true; // Switch LEDs on/off
} State;
On startup the settings are loaded from EEPROM with the following statement in setup():
if (EEPROM.read(0) == NumPixels) EEPROM.get(0, State); // Only read if valid
The first time you run the program, or if the number of LEDs, NumPixels, has changed, the settings are initialised to their defaults by calling ResetState().
The settings are saved automatically every 10 seconds in the Refresh() loop.
Installing the program
First install Spence Konde's DxCore from GitHub: see DxCore - Installation. I used version 1.5.11.
Then, in the Arduino IDE:
- Choose the AVR DD-series (no bootloader) option under the DxCore heading on the Board menu.
- Check that the subsequent options are set as follows (ignore any other options):
Board: "AVR DD-series (no bootloader)"
Chip: "AVR64DD14"
Clock Speed: "24 MHz internal"
Reset and UPDI: "PF6: Input & PF7: UPDI (no reset pin, safe.)"
You can leave the other options at their defaults, the first option on each submenu.
- Connect a UPDI programmer to the UPDI, GND, and VDD pins.
To program the processor the recommended option is to use a 5V or 3.3V USB to Serial board, such as the SparkFun FTDI Basic board [15], or a USB to Serial cable [16], connected with a Schottky diode as follows. You can substitute a 4.7kΩ resistor for the Schottky diode:

- Set Programmer to the "SerialUPDI - Normal: 230400 baud (recommended)" option.
- Select the USB port corresponding to the USB to Serial board in the Port menu.
- Select Burn Bootloader to configure the fuse to make PF6 an input.
- Click Upload to upload the program to the AVR64DD14.
If the IR remote control doesn't work, check that you remembered to set the Reset and UPDI option to make PF6 an input.
Finding the codes for an IR remote
The following procedure allows you to use the IR NeoPixel Controller to display the address and key codes for any NEC-protocol IR remote control.
Connect a NeoPixel strip with at least 8 LEDs to the +5V, Data, and GND pins. I used an Adafruit eight NeoPixel strip [17].
Then modify the IR NeoPixel Controller program as follows:
- Set the #define FORMAT line to the NeoPixel format appropriate to the type of strip you are using.
- Comment out the following line in the ProcessCode() function, as shown, to respond to any address:
// if (address != 0xbf00) return;
- Replace the switch block below it by the lines:
ResetState(); for (int i=0; i<8; i++) State.Control[i+1][VALUE] = (key>>i & 1) * 16;
- Upload the program.
Now when you press any key on the remote control, its code will be displayed in binary on the eight LEDs.
Finding the remote control address
- To display the bottom eight bits of the remote control address when you press a key, change the second line to:
for (int i=0; i<8; i++) State.Control[i+1][VALUE] = (address>>i & 1) * 16;
- To display the top eight bits of the remote control address change the line to:
for (int i=0; i<8; i++) State.Control[i+1][VALUE] = (address>>(i+8) & 1) * 16;
Resources
Here's the IR NeoPixel Controller program: IR NeoPixel Controller Program
- ^ Ultra Bright 3 Watt Chainable NeoPixel LED on Adafruit.
- ^ Ultra Bright 4 Watt RGBW NeoPixel LED on Adafruit.
- ^ Firstlight Sphere 15cm Flush Ceiling Light on Lightbulbs Direct.
- ^ Aluminium Heatsink - Black on The Pi Hut.
- ^ Aluminium Heatsink - Silver on The Pi Hut.
- ^ Mini Remote Control on Adafruit.
- ^ Mini IR Remote Control on The Pi Hut.
- ^ Apple Remote on Wikipedia.
- ^ IR (Infrared) Receiver Sensor on Adafruit.
- ^ SMT Breakout PCB for SOIC-12 or TSSOP-12 on Adafruit.
- ^ Breadboard - Mini Modular (White) on SparkFun.
- ^ Mini Breadboard - White on The Pi Hut.
- ^ Jumper Wire Kit on The Pi Hut.
- ^ SMT Breakout PCB for SOIC-14 or TSSOP-14 on Adafruit.
- ^ SparkFun FTDI Basic Breakout - 5V on Sparkfun.
- ^ FTDI Serial TTL-232 USB Cable on Adafruit.
- ^ NeoPixel Stick - 8 x 5050 RGB LED on Adafruit.
blog comments powered by Disqus
