💾 Archived View for gemini.djph.net › users › gemini › projects › sk6812 captured on 2020-09-24 at 00:55:19. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

LED Driver - SK6812 / WS2812 / etc.

Little project to make some LEDs blink and stuff for a shadowbox my wife is in the process of making. This'll be a bit meandering, as I'm writing this as I go.

Background

SWMBO is one of those crafty types, who has one of those "Cricut" paper-cutters that she's used for various projects - {holiday, birthday, baby shower, etc} cards, banners, scrapbook accompaniment, what have you. As part of being stuck at home due to the current global dumpster fire, she came across someone selling patterns that could be stacked together in a shadowbox to make a scene as you stack the cutouts together. One of the examples was lit, so that's where I come in.

Finding some LED Strips

The panels are pretty close together - nominal 0.200" (foam board she's using is 5.5mm) - so "normal" width LED strips wouldn't fit in the space. Came across some SK6812 "clones" of the WS2812B strips at Sparkfun, which are mounted to a 4mm wide strip, which JUST fits in between the layers.

Show her the strips and she likes the idea, so shoot off the order to Sparkfun, and wait a few days to get them in. In the interim, I start poking around the web looking for the libraries in the wild. There are several promising ones (FastLED coming to the forefront), but they're all pretty massive - a basic example in FastLED was weighing in at around 4 KiB. It ... could fit, but it's honestly heavier than I like; so onward to writing it myself.

Controlling the LEDs

According to the datasheet of dubious quality (web link below), the LEDs have a pretty simplistic 1-wire control scheme of 3 Bytes / LED, in GRB order (which if you're familiar with WS2812Bs is somewhat out-of-order). They use a NRZ (Non-return to Zero) encoding scheme based around a bit-length of 1.2μs (± 0.3 μs total).

Finally, chip reset is handled via a minimum 50 μs logic low between data sets.

SK6812 Datasheet (pdf, HTTPS)

SK6812 Datasheet (pdf, Gemini)

This is "slow enough" that we can just bang the sequence out on a digital pin -- if we can tighten the loops so that we don't overrun any of the timing constraints. Even if you're writing everything in C, this still means a function's worth of inline assembly, as the C constructs just aren't fast enough on the '0' side (well, least that I could get out of AVR-GCC anyway). In my case, I'm trying to learn assembly anyway, so blinkenLEDs is probably a good place to start.

Dealing with timing

With three μs for a "zero", this means the internal CPU of an ATxxx microcontroller can only execute one-third (1/3) of an instruction before having to put a pin low again; and one of those has to be the "go low" instruction to the port / pin. That's impossible (at least for me). Good thing is, there is a DEAD easy fix for the microcontrollers -- disable the CKDIV8 fuse.

With CKDIV8 disabled, the microcontroller CPU is running at 8 MHz - giving us 8 clocks per microsecond. This gives us three (3) instructions to work with when sending a '0', a bit tight; but doable. I honestly had the hardest time with the loops, originally writing it out as a /LOT/ of lines, just to work with one LED. It worked, but it certainly wasn't extendable for more than that. So, I dug into FastLED and some of the other drivers to find out how they were looping things, and took some notes.

Unfortunately, due to a minor incident with a toddler and some water, the original PC I had that data on bit it. But at least I had a backup of the correct-with-a-loop version ... and this time I was "smart" enough to put it on GitHub. Looking at this again today, even though I KNOW it works is a bit brainbendy. I think my comments are off-by-one in counting.

; Display output code - output is PB0

; Static registers for input/output/etc

; r5 = 0x04 (PB0 low, PB3 high)

; r6 = 0x05 (PB0 high, PB3 high)

; r24 = array size (3 * LED string length)

; XREG = pointer into the LED Data array

; note the clock-ticks are "natural" counting, so "CT7" is the 7th

; clock tick (or 7/8

;

; Initialize r24:r25 to array size -- it can be used with

; the SBIW command (2 clocks) which saves a couple nops in the code.

; Also init XREG data pointer

ldi r24, 0x0F ; We're sending 15 bytes for 5 LEDs

lds XL, 0x0060 ; ATTiny25 RAMSTART = 0x0060

lds XH, 0x0061 ; Byte1 = highbyte of the pointer addr

strt: ldi r23, 7 ; loop start; runs 8 times (bits 7->0)

ld r2, X+ ; put data at 0x0060:61 into r2, X=0x0062

ck5: nop ; some no-ops to space out the high (ck 3-5)

ck6: nop ; second nop (ck 5-6)

out PORTB, r5 ; set PB0 low (ck 7 * 0.125 = 0.750 μs)

ct9: lsl r2 ; shift bit7 of r2 into Carry

out PORTB, r6 ; set PB0 high (clock 0)

brcs ck2 ; if Carry = 1, jump to "clock 3" (ck 0-1)

out PORTB, r5 ; C=0; 2 clocks @ 8 Mhz = 0.25 μs (ck 1-2)

ck2: dec r23 ; cpu tick 3, decrement bit counter (ck 0-2)

brne ck5 ; if r23>0, jump to clock 5 nop (ck 2-3)

lsl r2 ; else shift 8th bit into Carry (ck 3-4)

brcc bit8 ; jump ahead if 8th bit=0 (ck 4-5)

ldi r23,7 ; reload bit counter (ck 5-6)

out PORTB, r5 ; PB0 = low (ck 6-7) tot. 7 cks hi = logic 1

nop ; ct 1-2 for logic 1 low time

out PORTB, r6 ; start next bit (0.375 μs for logic 1 )

sbiw r24, 1 ; decrement r24 by 1 - 2 clocks

ld r2, X+ ; next byte

brne ct6 ; byte counter >0 go back to clock6

rjmp disend ; byte counter =0, jump to end

bit8: out PORTB, r5 ; making sure PB0 is low. test nop here

ldi r23, 7 ; reset bit counter

out PORTB, r6 ; enable PB0

nop ; hold PB0 high for a moment

out PORTB, r5 ; disable PB0 logic0 for bit8 sent

sbiw r24,1 ; decrement bytes (2 clocks)

ld r2, X+ ; load new data byte into R2, increment ptr

brne ct9 ; start over (9 ticks low = 1.00 μs)

disend: out PORTB, low ; ensure port down (reset pulse start)

ret ; return from subroutine

To be continued

This is as far as I've taken the project - it's a "static" display consisting of four LEDS that fades from red to blue to green. I still need to add some interrupt handling to allow a user to switch "programs" (e.g. to solid white or so).

Continued in part 2