A NeoPixel Driver using AVR Hardware [2]
20th August 2025
My previous article described a driver for NeoPixel (WS2812) LED displays using the SPI peripheral on an AVR processor in conjunction with a Timer/Counter and the Configurable Custom Logic (CCL).
This article shows how to go one step further, and eliminate most of the interconnections between the pins on that circuit by taking advantage of an additional feature of recent AVR processors, the Event System:
![]()
NeoPixel Driver using the hardware peripherals and Event System in an AVR128DA28.
1st November 2025: Added a Postscript explaining how to eliminate the wire between PA1 and PA6 by using the CCL.
Introduction
The article A NeoPixel Driver using AVR Hardware [1] described a program for AVR processors that created the stream of pulses needed by the NeoPixel protocol, using the SPI peripheral in conjunction with a Timer/Counter Type A and the Configurable Custom Logic. It used the following circuit based on an AVR128DA28:
![]()
Circuit of the NeoPixel Driver using an AV128DA28 showing
the wires that can be replaced by the Event System.
We can go one step further, and use another feature of the AVR processors, the Event System, to replace the wires shown in red in the above circuit by internal connections defined using event channels. This not only saves wires on the circuit board, but also frees up some of the pins so they could be used for other functions.
The Event System
Terminology
The Event System avoids using the terms "input" or "output" because this could get confusing. For example, the input to the Event System could be the output from a Timer/Counter. It therefore uses the terms "event generator" for the signal initiating the event, and "event user" for the signal or signals that will be changed by the event.
Event generator
In our application the signal coming from an I/O port input is the event generator. You can usually use any I/O port input as an event generator.
There can only be one event generator; it wouldn't make sense to have more than one because there would be a conflict if the two inputs had different logic levels.
Event users
There is one event user available per port. In the AVR128DA28 the available pins are PA2 or PA7 for EVOUTA, PC2 for EVOUTC, and PD2 or PD7 for EVOUTD. Where there are two alternatives for a port the output on bit 2 is the default, and you select the alternative output on bit 7 using the PORTMUX EVSYSROUTEA register.
Channels
Each patch lead, linking an event generator with one or more event users, is called a "channel". Depending on the processor there can be between 6 and 10 channels, numbered from CHANNEL1 to CHANNEL10.
In the DA, DB, and DD series there are constraints on which channels you can use with event generators in each port, as shown in the following table:
| Ports | Channels |
| PORTA and PORTB | CHANNEL0 and CHANNEL1 |
| PORTC and PORTD | CHANNEL2 and CHANNEL3 |
| PORTE and PORTF | CHANNEL4 and CHANNEL5 |
| PORTG | CHANNEL6 and CHANNEL7 |
So you can use at most two I/O pins from PORTA and PORTB, two from PORTC and PORTD, and so on.
If you try to use an I/O pin with a channel that doesn't support it you'll get a compile error, such as:
'EVSYS_CHANNEL2_PORTA_PIN0_gc' was not declared in this scope
Event configurations
The following sections describe the event configuration statements used in the NeoPixel Driver:
Connecting the TCA0 waveform outputs to the look-up table
In the original circuit there were connections between the Timer/Counter waveform outputs WO0 (PA0) and WO1 (PA1), and the LUT1 inputs IN0 (PC0) and IN1 (PC1). These can be eliminated by defining them in the LUT1 configuration as follows:
CCL.LUT1CTRLB = CCL_INSEL0_TCA0_gc | CCL_INSEL1_TCA0_gc;// TCA0 WO0 and WO1 CCL.LUT1CTRLC = CCL_INSEL2_EVENTA_gc; // LUT1 EVENTA CCL.TRUTH1 = 0b11001010; CCL.LUT1CTRLA = CCL_OUTEN_bm | CCL_ENABLE_bm; // Enable, output on PC3
This replaces the two wires between WO0 and PD0, and WO1 and PD1, by TCA0 events.
Each LUT has two possible event inputs, called EVENTA and EVENTB (nothing to do with the event users EVOUTA and EVOUTB). Here we are specifying that IN2 comes from LUT1 EVENTA instead of from the external input PC2.
Connecting the SPI MISO pin to LUT1
We will use event channel 0 to eliminate the wire between MISO (PA5) and IN2 (PC2). The first half of this connection was defined in the configuration of LUT1 above. The second half is defined as follows:
EVSYS.CHANNEL0 = EVSYS_CHANNEL0_PORTA_PIN5_gc; // Link PA5 ... EVSYS.USERCCLLUT1A = EVSYS_USER_CHANNEL0_gc; // ... to LUT1 EVENTA
Connecting PD4 to the SPI SS pin
Finally, we will use event channel 2 to eliminate the wire between SS (PA7) and the EN output PD4:
PORTMUX.EVSYSROUTEA = PORTMUX_EVOUTA_ALT1_gc; // EVOUTA on PA7 EVSYS.CHANNEL2 = EVSYS_CHANNEL2_PORTD_PIN4_gc; // Link PD4 ... EVSYS.USEREVSYSEVOUTA = EVSYS_USER_CHANNEL2_gc; // ... and PA7
Connecting the TCA0 WO1 output to the SPI SCK pin
In the original version of this article I I wrote that it was not possible to eliminate the external wire between WO1 (PA1) and SCK (PA6), as PA6 doesn't correspond to an event output. However, there is a way to do it using the CCL; see the Postscript.
The circuit
Here's the final circuit using the Event System to make the internal interconnections in the AVR128DA28 version of the NeoPixel Driver:
![]()
Simplified circuit of the NeoPixel Driver eliminating external connections
by using the Event System.
The performance is the same as the original circuit in the previous article, but because the pins PC0, PC1, and PC2 are no longer required as the look-up table inputs they could be used for another function.
Resources
Here's the version of the NeoPixel Driver for the AVR128DA28 using the Event System: NeoPixel Driver Program 2.
Further suggestions
After using the Event System to eliminate most of the interconnections between the pins in the AVR128DA28 version of the NeoPixel Driver I realised that it would be possible to fit the same functionality on an AVR family chip with half the number of pins, the AVR64DD14. I'll describe how to do this in the third and final article in this series.
Postscript
Prompted by a comment by Gavin Wahl I decided to have another look at the datasheet, and realised that it's possible to use the CCL to eliminate the final external wire between WO1 (PA1) and SCK (PA6). I've decided to leave it as a postscript as I'm not sure if the saving justifies the extra complexity. Here's the configuration:
Connecting the TCA0 WO1 output to the SPI SCK pin
First we specify the alternate position of the LUT0 output, on PA6:
PORTMUX.CCLROUTEA = PORTMUX_LUT0_ALT1_gc; // OUT on PA6
Next we mask IN0 and IN2, and take IN1 from PA2:
CCL.LUT0CTRLB = CCL_INSEL0_MASK_gc | CCL_INSEL1_IN1_gc; // IN0 masked, IN1 from PA1 CCL.LUT0CTRLC = CCL_INSEL2_MASK_gc; // IN2 masked
The truth table simply copies IN1 to OUT and ignores IN0 and IN2:
CCL.TRUTH0 = 0b11001100; // Just copy IN1 to OUT
Finally we enable the output on PA6 and enable LUT0:
CCL.LUT0CTRLA = CCL_OUTEN_bm | CCL_ENABLE_bm; // Enable, output on PA6
We also need to move the statement enabling the CCL to after these statements:
CCL.CTRLA = CCL_ENABLE_bm; // Enable CCL last
blog comments powered by Disqus
