[AVR] Basic WS2811 interfacing (addressable RGB leds) - Page 1
Forum section: electronics / general

author: expertmax
author_rank: Regular Contributor
timestamp: November 23, 2014, 11:18:46 pm
content: Hi everyone!After seeing the FastLED library for the Arduino, I thought I would do my own version but using the WS2812B and using a barebone AVR.This is what my code looks like now :http://pastebin.com/d2ph6sHhThis is supposed to show on a 16x16 led grid a completely red output.Am I in the right path?Thanks(I don't have a WS2812B with me, eBay Chinese sellers ought to be slow)

author: hamster_nz
author_rank: Super Contributor
timestamp: November 23, 2014, 11:33:55 pm
content: Somewhat.The on-the-wire protocol for WS2812B has quite precise timing requirements - seehttp://www.adafruit.com/datasheets/WS2812B.pdfBits are sent as follows:- A zero bit is sent as 0.4us high, followed by a 0.85us low- A one bit is sent as 0.8us high, followed by a 0.45us lowThese have to be sent with +/- 0.15us accuracy, and all bits for all LEDs have to be sent back-to- back. For 255 LEDs this will involve sending 6,120 of data bits without a hiccup.You will most likely have to resort to assembler to achieve this, and you will definitely have to disable interrupts during the transfer.

author: expertmax
author_rank: Regular Contributor
timestamp: November 23, 2014, 11:46:24 pm
content: Would you happen to know what is the ASM code I should type in instead of that bloating C code?As a beginner in C, I cannot imagine learning ASM just for that particular project (anyway if it's not working, I'll have to use the FastLED library).Based on the datasheet, here are the changes I made to the code :http://pastebin.com/k793ZQm8

author: dannyf
author_rank: Super Contributor
timestamp: November 24, 2014, 12:07:27 am
content: QuoteYou would get there, but not as easily.Take a look at the datasheet and try to code to it at a logic level.For example: you will need to write a piece of code that send '0' and '1'; To do that, you will need to delay some, in about 0.4us blocks (on a 8MIPS avr, that's about 3 NOPs.so I would do this instead:Code:#define WS2822_DLY() {NOP(); NOP(); NOP();} //delay 0.4us//send '1' bit#define WS2822_SEND1() do {PIN_SET(WS2822_PORT, WS2822_PIN); WS2822_DLY(); WS2822_DLY(); PIN_CLR(WS2822_PORT, WS2822_PORT); WS2822_DLY();} while (0)//send '0' bit#define WS2822_SEND0() do {PIN_SET(WS2822_PORT, WS2822_PIN); WS2822_DLY(); PIN_CLR(WS2822_PORT, WS2822_PIN); WS2822_DLY(); WS2822_DLY();} while (0)//send 8-bitsvoid ws2822_send(uint8_t dat) {uint8_t mask = 0x80; //msb firstdo {//may need to disable interrupts hereif (dat & mask) WS2822_SEND1();else WS2822_SEND0();//if interrupt is previously disabled, enable it heremask = mask >> 1;} while (mask);}//send rgb#define ws2822_rgb(r, g, b) do {ws2822_send(g); ws2822_send(r); ws2822_send(b);} while (0)You can compare it vs. the datasheet and the structure would be quite logical.Additional high level routines can be written based on those little modules, like sending frames.

author: dannyf
author_rank: Super Contributor
timestamp: November 24, 2014, 12:08:56 am
content: That's the big-bang approach. You could also use other hardware modules, like spi, to send data, without having the mcu working so hard on each bit/byte/frame.

author: hamster_nz
author_rank: Super Contributor
timestamp: November 24, 2014, 12:19:23 am
content: Quote from: expertmax on November 23, 2014, 11:46:24 pmYou might be biting off more than you can chew at the moment, but here is how I would structure the code if I was writing in pure assembler:Code:; Send the reset pulseset output bit low;wait for 50us; - send the reset pulse; Set up for the transferdisable interruptsSet counter register to 0set mask register to 128jump start_sendingnext_bit:NOPs to wait for 0.45nsincrement counter registerroll mask one bit to the left ; set the mask to the next bit.start_sending:set output bit hightest if counter = 255*3If counter does = 255*3 then jump all_doneNOPs to wait for initial 0.4ns ; All bits have high for 0.4ns at the startAND mask with datajump one_bitzero_bit;set output bit low ; Have already sent the 0.4us of highNOPs to wait for 0.40nsjump next_bitone_bit:NOPs to wait for 0.40ns ; Keep the signal higher longerset output bit lowjump next_bitall_done:set output bit low ; Optional - start the reset pulse.enable interruptsreturnThe code is structured so that it is easy to ensure that all paths through the source will have constant time.I would then count cycles and tune the numbers of NOPs to get the timing correct, or maybe use a oscilloscope and tune by observation.

author: expertmax
author_rank: Regular Contributor
timestamp: November 24, 2014, 12:27:51 am
content: Ah, so basically the only difference between your code and my code is the use of NOPs...This project will have no interrupt enabled (as you may notice in the code), it will run at 16 Mhz, will display one "image" every minute (so FPS is far from being important here) and will not monitor external events (such as ADCs and Timers).The next thing to do is to hookup my logic analyser (Saleae to the rescue) and wait until the dreaded WS2812 arrives.

author: dannyf
author_rank: Super Contributor
timestamp: November 24, 2014, 12:48:56 am
content: QuoteThe use of nops is trivial and of zero importance.What's important is the way to code.

author: expertmax
author_rank: Regular Contributor
timestamp: November 24, 2014, 02:36:00 am
content: Sorry for my ignorance but I don't see how using macros will help my code.Again, sorry for my ignorance. I'm not a EE student I'm just a hobbyist.EDIT: I cannot for the life of me find my freakin' Logic Analyzer. I'll see what the timings are, if that full 3 channel 256 led framing is perfect (cannot detect any glitches though) it is going to stay like that, I'll die ignorant.

author: Jeroen3
author_rank: Super Contributor
timestamp: November 24, 2014, 08:37:52 am
content: If you indeed require a 0.15us accuracy you're in an range of 2 clock ticks.Use SPI instead, it is either 30% or 60% of set bits.NXP's SGPIO would be ideal for this.

author: BloodyCactus
author_rank: Frequent Contributor
timestamp: November 24, 2014, 01:24:35 pm
content: the timing is not as tight as the datasheet expects;here is an awesome writeup on the variantshttps://cpldcpu.wordpress.com/2014/01/14/light_ws2812-library-v2-0-part-i-understanding-the-ws2812/

author: expertmax
author_rank: Regular Contributor
timestamp: November 24, 2014, 04:10:58 pm
content: Quote from: Jeroen3 on November 24, 2014, 08:37:52 amCan you provide me with a small snippet of code that would initialise the SPI master and the divider + the code to send data?Thank you!

author: sca
author_rank: Regular Contributor
timestamp: November 24, 2014, 04:33:24 pm
content: The Signal Path has a recent vid on these addressable LEDs - worth watching.sca

author: hamster_nz
author_rank: Super Contributor
timestamp: November 24, 2014, 05:26:37 pm
content: I hauled out the oscilloscope and an Arduino last night and played around to generate an in-spec '1' and zero pulse, just to see how much slack there is.It would be quite easy, if only I could understand GCC's inline assembler options!All the code would have to be done in one function, as there isn't enough time for any call/return overhead...Code:void outputWS2812Bbits(void){asm volatile("cli"::); // Disable interrupts// Send a zero bit__asm__("sbi 5, 4\n\t"); // Set port b bit 4 Arduino pin 12__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("cbi 5, 4\n\t"); // clear port b bit 4__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // a pause// Send a one bit__asm__("sbi 5, 4\n\t"); // Set port b bit 4__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("cbi 5, 4\n\t"); // clear port b bit 4__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // A pause__asm__("nop\n\t"); // a pauseasm volatile("sei"::); // Enable interrupts}

author: free_electron
author_rank: Super Contributor
timestamp: November 24, 2014, 05:35:00 pm
content: i'm amazed nobody has stuffed that crap in a small cpld yet. you could feed data in parallel and have the cpld handle all the critical timing and other crap.Plus you could hook up mulitple chains to the cpld and write alternate bytes. the throughput could be phenomenal.

author: hamster_nz
author_rank: Super Contributor
timestamp: November 25, 2014, 08:11:48 am
content: So, the challenge got to me, and I spent the night learning AVRGCC and wrote a simple WS2812b driver - it is all in one file, and checking timing on the scope they are with +/- 50 ns.Here's the code - it is not the most beautiful or compact code out there, but it does weigh in at about 300 bytes.:Code:/******************************************* ws2812b_demo.c** Author : Mike Field <hamster@snap.net.nz<** A really simple driver for WS2812B LEDs for* the Arduino Uno. Must be running at 16MHz.**********************************************/#define OUTPUT_PIN 12unsigned char leds;void outputWS2812Bbytes(unsigned char (*leds), unsigned char length){delay(1);asm volatile(" cli \n\t" // Disable interrupts" mov 18,%1\n\t" // Copy length" add %1, 18\n\t" // Add it back" add %1, 18\n\t" // Add it back, so it is now x3 what it was"L_next%=:" "\n\t"///////////////////////////////////////////////////////////// Bit 7" SBI 5, 4 \n\t" // Set port b bit 4 Arduino pin 12" NOP \n\t" // A pause" LD 18, Z \n\t" // Load and post increment - two cycles" ANDI 18, 128 \n\t" // Test the bit - one cycle" BRNE L_bit7%= \n\t" // Skip the clear if the bit is set" CBI 5, 4 \n\t" // clear port b bit 4"L_bit7%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" cbi 5, 4 \n\t" // clear port b bit 4" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause///////////////////////////////////////////////////////////// Bit 6" SBI 5, 4 \n\t" // Set port b bit 4 Arduino pin 12" NOP \n\t" // A pause" LD 18, Z \n\t" // Load and post increment - two cycles" ANDI 18, 64 \n\t" // Test the bit - one cycle" BRNE L_bit6_0_%= \n\t" // Skip the clear if the bit is set" CBI 5, 4 \n\t" // clear port b bit 4" JMP L_bit6_1_%= \n\t""L_bit6_0_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause"L_bit6_1_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" cbi 5, 4 \n\t" // clear port b bit 4" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause///////////////////////////////////////////////////////////// Bit 5" SBI 5, 4 \n\t" // Set port b bit 4 Arduino pin 12" NOP \n\t" // A pause" LD 18, Z \n\t" // Load and post increment - two cycles" ANDI 18, 32 \n\t" // Test the bit - one cycle" BRNE L_bit5_0_%= \n\t" // Skip the clear if the bit is set" CBI 5, 4 \n\t" // clear port b bit 4" JMP L_bit5_1_%= \n\t""L_bit5_0_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause"L_bit5_1_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" cbi 5, 4 \n\t" // clear port b bit 4" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause///////////////////////////////////////////////////////////// Bit 4" SBI 5, 4 \n\t" // Set port b bit 4 Arduino pin 12" NOP \n\t" // A pause" LD 18, Z \n\t" // Load and post increment - two cycles" ANDI 18, 16 \n\t" // Test the bit - one cycle" BRNE L_bit4_0_%= \n\t" // Skip the clear if the bit is set" CBI 5, 4 \n\t" // clear port b bit 4" JMP L_bit4_1_%= \n\t""L_bit4_0_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause"L_bit4_1_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" cbi 5, 4 \n\t" // clear port b bit 4" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause///////////////////////////////////////////////////////////// Bit 3" SBI 5, 4 \n\t" // Set port b bit 4 Arduino pin 12" NOP \n\t" // A pause" LD 18, Z \n\t" // Load and post increment - two cycles" ANDI 18, 8 \n\t" // Test the bit - one cycle" BRNE L_bit3_0_%= \n\t" // Skip the clear if the bit is set" CBI 5, 4 \n\t" // clear port b bit 4" JMP L_bit3_1_%= \n\t""L_bit3_0_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause"L_bit3_1_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" cbi 5, 4 \n\t" // clear port b bit 4" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause///////////////////////////////////////////////////////////// Bit 2" SBI 5, 4 \n\t" // Set port b bit 4 Arduino pin 12" NOP \n\t" // A pause" LD 18, Z \n\t" // Load and post increment - two cycles" ANDI 18, 4 \n\t" // Test the bit - one cycle" BRNE L_bit2_0_%= \n\t" // Skip the clear if the bit is set" CBI 5, 4 \n\t" // clear port b bit 4" JMP L_bit2_1_%= \n\t""L_bit2_0_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause"L_bit2_1_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" cbi 5, 4 \n\t" // clear port b bit 4" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause///////////////////////////////////////////////////////////// Bit 1" SBI 5, 4 \n\t" // Set port b bit 4 Arduino pin 12" NOP \n\t" // A pause" LD 18, Z \n\t" // Load and post increment - two cycles" ANDI 18, 2 \n\t" // Test the bit - one cycle" BRNE L_bit1_0_%= \n\t" // Skip the clear if the bit is set" CBI 5, 4 \n\t" // clear port b bit 4" JMP L_bit1_1_%= \n\t""L_bit1_0_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause"L_bit1_1_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" cbi 5, 4 \n\t" // clear port b bit 4" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause///////////////////////////////////////////////////////////// Bit 0" SBI 5, 4 \n\t" // Set port b bit 4 Arduino pin 12" NOP \n\t" // A pause" LD 18, Z+ \n\t" // Load and post increment - two cycles" ANDI 18, 1 \n\t" // Test the bit - one cycle" BRNE L_bit0_0_%= \n\t" // Skip the clear if the bit is set" CBI 5, 4 \n\t" // clear port b bit 4" JMP L_bit0_1_%= \n\t""L_bit0_0_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause" NOP \n\t" // A pause"L_bit0_1_%=:" "\n\t"" NOP \n\t" // A pause" NOP \n\t" // A pause" cbi 5, 4 \n\t" // clear port b bit 4" SUBI %1,1 \n\t" // Decrement the count" BREQ L_done%= \n\t" // Exit when finished" JMP L_next%= \n\t" // Jump back if we still have more to do// All Finshed"L_done%=: \n\t"" sei \n\t": : "z" (leds), "d" (length));}void setup() {// put your setup code here, to run once:pinMode(OUTPUT_PIN,OUTPUT);}void loop() {// put your main code here, to run repeatedly:leds = 0x3F;leds = 0x3F;leds = 0x3F;leds = 0x6F;leds = 0;leds = 0;leds = 0;leds = 0x6F;leds = 0;leds = 0;leds = 0;leds = 0x6F;leds = 0x3F;leds = 0x3F;leds = 0x3F;outputWS2812Bbytes(leds,5);}

author: BurtyB
author_rank: Regular Contributor
timestamp: November 27, 2014, 12:16:38 am
content: Quote from: free_electron on November 24, 2014, 05:35:00 pmAnother option would be theTeensy 3.1 + OctoWS2811 [+ 74HCT245]with the associated library which uses PWM+DMA to handle the timings for 8 channels leaving the CPU mostly free for other tasks (I'm planning to use it for a WS2812B based 3D LED Cube).

author: aon
author_rank: Contributor
timestamp: November 28, 2014, 07:54:24 am
content: Quote from: BurtyB on November 27, 2014, 12:16:38 amThere's also0xWS2812for STM32F10x that uses a similar approach. I've got a "port" of it running on STM32F030 - the WS2812B driver and a DMX512 receiver seem to just about fit in a STM32F030F4P6 which is a pretty inexpensive way to drive 8 parallel strings of leds, although the level shifting parts and a 3.3V supply pretty much double the cost.Doing it in a CPLD did cross my mind, it's something I'll probably try to do as a learning experience once I get into learning a HDL.