Want pure hardware generation of a WS2811 signal? No funky buffers that take CPU time to setup or tricks to bend SPI to your will? Here’s a straight pure hardware way to generate a WS2811 stream with minimal CPU time on a Xmega8E5 by taking advantage of the XCL (XMEGA Custom Logic) module. By taking the XCL, DMA, and USART modules and mixing them all in a bowl with the Xmega Event system, we can effortlessly generate a WS2811 stream. In this example, I’ll use my PixelStick. The PixelStick is a Xmega8E5 based WS2811 pixel controller that is controlled wirelessly by a nRF24L01+ module. The complete firmware is available on GitHub, but I’ll extract the tasty bits here.
The first thing we need to do is setup the USART in SPI mode. Note that its running at 800kHz, no funky SPI tricks here. DMA transfers will be used to feed the USART module and is triggered by incoming nRF24L01 pixel data.
// Configure the USART module PORTD.DIRSET = PIN1_bm | PIN3_bm; /* Setup TX and Clock lines as outputs */ xusart_spi_init(&xusart_config, USART_UCPHA_bm); /* Initialize USART as Master SPI, Mode 1 */ xusart_spi_set_baudrate(xusart_config.usart, WS2811_BAUDRATE, F_CPU); /* Set baud rate */ xusart_enable_tx(xusart_config.usart);
Then we need to setup two PWM streams, representing the WS2811 Hi and Lo bits:
// Setup timers for WS2811 waveform generation PORTD.DIRSET = PIN4_bm | PIN5_bm; /* Enable output on PD4 & PD5 for compare channels */ TCD5.CTRLB = TC45_WGMODE_SINGLESLOPE_gc; /* Single Slope PWM */ TCD5.CTRLD = TC45_EVACT_RESTART_gc | TC45_EVSEL_CH7_gc; /* Restart on CH7 pulse - rising clock edge */ TCD5.CTRLE = TC45_CCAMODE_COMP_gc | TC45_CCBMODE_COMP_gc; /* Enable output compare on CCA & CCB */ TCD5.PER = 40 - 1; /* At 32MHz, 1 cycle = 31.25ns. Define top of counter for a 1250ns pulse: (32MHz / 800KHz) */ TCD5.CCA = 8; /* Compare for 0 bit @ 250ns (31.25ns * 8). Output is on PD4 */ TCD5.CCB = 32; /* Compare for 1 bit @ 1000ns (31.25ns * 32). Output is on PD5 */ TCD5.CTRLA = TC45_CLKSEL_DIV1_gc | TC5_EVSTART_bm | TC5_UPSTOP_bm; /* Start and stop the timer on each event occurrence, full speed clock */
And setup the XCL module as a MUX:
// Setup XCL PORTD.DIRSET = PIN0_bm; /* Enable output on PD0 for LUT OUT0 */ XCL.CTRLA = XCL_LUTOUTEN_PIN0_gc | XCL_PORTSEL_PD_gc | XCL_LUTCONF_MUX_gc; /* Setup LUT with output on PD0 as MUX */ XCL.CTRLD = 0b10100000; /* Truth Tables - Ignore 0 since its a MUX. Setup LUT1 to pass IN2. */
Everything is tied together via the Xmega Event system. Event Channel 7 links the SPI clock to the timer WS2811 stream timers so bit generation is always synchronized to the SPI stream.
// Setup event system channels EVSYS.CH7MUX = EVSYS_CHMUX_PORTD_PIN1_gc; /* SPI Clock (PD1) to CH7 */ PORTD.PIN1CTRL |= PORT_ISC_RISING_gc; /* Sense rising edge on PD1 */ EVSYS.CH1MUX = EVSYS_CHMUX_PORTD_PIN3_gc; /* TXD (PD3) to CH1 / LUT IN2 */ PORTD.PIN3CTRL |= PORT_ISC_LEVEL_gc; /* Sense level on PD3 */ EVSYS.CH6MUX = EVSYS_CHMUX_PORTD_PIN4_gc; /* CCA (PD4) Compare to CH6 / LUT IN0 */ PORTD.PIN4CTRL |= PORT_ISC_LEVEL_gc; /* Sense level on PD4 */ EVSYS.CH0MUX = EVSYS_CHMUX_PORTD_PIN5_gc; /* CCB (PD5) Compare to CH0 / LUT IN1 */ PORTD.PIN5CTRL |= PORT_ISC_LEVEL_gc; /* Sense level on PD5 */
In the end, we have an ISR to handle incoming nRF24L01 data and trigger DMA transfers:
ISR(PORTC_INT_vect) { LED_DATA_ON; xnrf_read_payload(&xnrf_config, rxbuff, xnrf_config.payload_width); /* Retrieve the payload */ xnrf_write_register(&xnrf_config, NRF_STATUS, (1 << RX_DR)); /* Reset nRF RX_DR status */ // Check if this is a frame we want. Set data ready flag to process the new buffer from rxbuff to pixelbuff. if ((rxbuff[RFSC_FRAME] >= frame_first) && (rxbuff[RFSC_FRAME] <= frame_last)) DFLAG = true; PORTC.INTFLAGS = PIN3_bm; /* Clear interrupt flag for PC3 */ LED_DATA_OFF; }
And a main() that just polls for new pixel data and updates our pixel buffer, triggering DMA transfers as needed to generate the WS2811 stream. The power conscious could sleep here if they wanted.
while(1) { while(!DFLAG); /* Poll until we have new data to process */ ATOMIC_BLOCK(ATOMIC_FORCEON) { /* Swap our buffers */ DFLAG = false; volatile uint8_t *swap = pbuff; pbuff = rxbuff; rxbuff = swap; } uint8_t frame = pbuff[RFSC_FRAME]; /* The frame for this buffer */ uint8_t start = 0; /* Start index for copying pbuff */ uint8_t stop = RFSC_FRAME_SIZE; /* Stop index for copying pbuff */ if (frame == frame_first) /* Shift start index for first frame */ start = channel_start - (frame * RFSC_FRAME_SIZE); if (frame == frame_last) /* Shift stop index for last frame */ stop = (channel_start + channel_count) - (frame * RFSC_FRAME_SIZE); /* Load our pixel buffer with the data we're interested in */ uint16_t channel = (frame * RFSC_FRAME_SIZE) + start - channel_start; for (uint8_t i = start; i < stop; i++) pixelbuff[channel++] = pbuff[i]; /* Once the pixel buffer has been refreshed, output the updated WS2811 stream */ if (frame == frame_last) EDMA.CH2.CTRLA |= EDMA_CH_ENABLE_bm | EDMA_CH_REPEAT_bm; /* Enable USART TX DMA channel to send the pixel stream */ }
You can see the PixelSticks in action here: