1
\$\begingroup\$

I'm looking at controlling some WS2812 5050 RGB LED strips from a microcontroller. I've successfully played with the Adafruit Neopixel library and I've written some code on a PIC18F2455 which I've also gotten working for some simple things (progressively change colour from red, to green to blue, and back etc).

However, all of that was for a single string. Ideally, since I'm using PORTB on the PIC micro, it'd be great to use all 8 bits to drive up to 8 strings from the one chip.

Yes, I know I'm getting greedy here :)

My question is, what approach would you recommend to take given that I have at most the timing involved for sending a 1 or a 0 on any individual channel is a few instruction cycles and the signalling protocol is non-standard 1-wire type protocol so the PIC doesn't have a dedicated peripheral to offload the work to (sending a 1 involves 'write 1, nop, write 0' and sending a 0 involves 'write 1, write 0, nop').

Additionally, there's probably only at most a hundred instruction cycles between the end of one bit at the start of the next before you hit the 50us "end code" and everything you've been writing out gets latched onto the LEDs and the data protocol resets back to waiting for the first bit again.

For a single string I've just been taking the three bytes for Green, Red, and Blue (that's the order these things use) and doing 24 "if (green & 0x80) write1(); else write0();" etc statements. But clearly that exact same approach isn't going to work for 8 bits at once.

Some options I considered:

  1. Computing a byte based on the first bit of the green value, then using carefully crafted assembly to (a) write 0xff to the port, (b) write the computed byte to the port, and then (c) write a 0x00 to the port. Rinse repeat 23 more times for the first LED on each of the 8 strings, then repeat again for however long it takes to output the entire string. Only problem is all that computing takes a fair amount of cycles and it's quite possible to take so long it ends up interfering with your output.

  2. Instead of storing each of the led string data as an array of GRB byte values and then computing them at output time, store them as a "smeared" bit array (eg, first string's LED 1 data is stored across 24bytes of memory in the first bit of each byte, second string's across the second bit of each byte, etc). Advantage is outputting is dead easy and fast, disadvantage is the workload is moved to the creation part of the process and you now need functions to get or set individual values.

Thoughts? Anyone have any reasons why you would or wouldn't do any of the above? Anyone know of any clever hacks to quickly "flip" an array of 8 bytes so byte 1 becomes the first bit of the 8 bytes in the result? :)

\$\endgroup\$
  • \$\begingroup\$ Often hacks for bit-mashing use much wider registers, or many registers. For example, load the full three-byte value for one LED on each of the 8-strings of LEDs into 8 32-bit registers. Then get the sign-bit of each into a 9th register to build a parallel-port value. Shift all 8 registers to get at the next bit. The 8 register load might be a single load multiple register instruction. That would be relatively efficient on something like an ARM-Cortex-M3/4. It has bit-addressing, which would do this. Table look-up, typically needs 64kB spare flash to make a big difference. Buy a an ST Nucleo? \$\endgroup\$ – gbulmer Sep 5 '14 at 12:44
  • \$\begingroup\$ If you are running 8 LED strings from the MCU, it would be worth double checking the MCU power specs. Often, MCUs are spec'd for a max current per pin and a maximum total across all pins (which is always lower). If you run into power issues, consider a transistor switch circuit or a simple high current buffer IC. \$\endgroup\$ – Oliver Sep 5 '14 at 14:21
  • 1
    \$\begingroup\$ @Oliver - WS2812 aren't powered from pins of the MCU. The MCU is only driving one data pin on each WS2812, which according to the datasheet only require 1µA. \$\endgroup\$ – gbulmer Sep 5 '14 at 15:58
  • 1
    \$\begingroup\$ @gbulmer - My bad. \$\endgroup\$ – Oliver Sep 5 '14 at 16:00
1
\$\begingroup\$

If changes of the values are rare (relative to the running of the LED update protocol) the second approach is clearly lighter on the CPU (and hence results in faster communication).

You might want to google for "vertical counters", which uses a similar approach to spread the bits of a number of counters 'vertically' over a number of bytes, with the aim of making a fast set of counters. I think I first heared of this from http://www.dattalo.com/technical/software/pic/vertcnt.html

\$\endgroup\$
  • \$\begingroup\$ That's kind of more along the lines I was thinking... that link is fascinating. I don't know if it specifically solves this problem but it's worth studying it to see if something like it may help. Worst case is I just end up with 8 bytes as a temp latching array, and just do some variation on a huge array of "bit test f, skip if set; bit set f" instructions in memory. program memory is huge, it's the ram and CPU we're short on, so unloading loops into non space optimised code is probably okay here :) \$\endgroup\$ – Jon Kloske Sep 8 '14 at 4:49
0
\$\begingroup\$

The existing WS2812 single-string-output is fast to update (change 3 bytes), then the time to communicate to all LEDs is mostly determined by by the proprietary single-wire signalling protocol.

When considering dividing LEDs across 8 strings, you have selected two approaches:

  1. Fast RGB update (change 3 bytes), most work to output (assembling bits to create 8-bit port value), and
  2. Most work for RGB update ("store them as a "smeared" bit array (eg, first string's LED 1 data is stored across 24bytes of memory in the first bit of each byte, second string's across the second bit of each byte, etc)"), "Advantage is outputting is dead easy and fast"

The assumptions built into both approaches are not necessarily true.

In theory, the total communication duration of a zero or one are different. So, in theory a string of zeroes takes less time to transmit than the same number of ones.

I believe there is enough tolerance in the protocol to allow a zero or one to use the same duration. But that is pushing tolerances and removing flexibility, making it harder to code; specifically there is less flexibility for adding or removing instructions.

However, it gets worse. The first part of the protocol has the signal output for zero high for 2x longer than one. Simply copying 8bits at a time to the output port won't work, unless the entire port is all '0' or all '1'. For every other combination, it takes more work to manipulate the right part of the bit pattern to represent the shorter '0' timing.

The WS2812 one-wire protocol is real-time. The transition times are sub microsecond, with specified jitter less than +/- 1/6th µs. So building the output with a 12 MIPS PIC18F2455 translates to jitter of less than +/- 2 instructions, if it is to stay within spec.

Zero has the shortest signal time of 1/3rd µs, with jitter that means between 2 and 6 instructions.

So if eight string are driven in parallel, assembling the output must happen in less than 8 instructions. So I think assembling the ports bit pattern on the fly isn't feasible.

When there are 8 strings of LEDs, a single LED change only requires 1/8th of the LEDs to be updated. So, providing the protocol timing can be met, the time between updating the LED value, and completing output could take almost 8x more work than the existing single string, and it is still faster.

So dividing the single-string into 8 strings, and putting each string onto a port pin may have significant benefits, because the latency to communicate updates would be 1/8th of a single string, and hence may free a lot of extra CPU cycles.

However, trying to drive all 8 strings simultaneously might be well beyond the capability of the PIC18F2455; the only simple way to store the correct bit pattern uses a lot more memory, and there are very few instructions available to assemble patterns ion the fly.

I would start with a much bigger 'hammer' than a PIC18F2455 if this is important.

\$\endgroup\$
  • 1
    \$\begingroup\$ The timing is pretty important for how long the pins are driven high, but the low time is less important. The manual states a single bit takes approximately 1.25us +/- 0.6us. If you add the values together, it's nominally 1.15us for a 0, and 1.3us for a 1 (however some people think there's goofs in the datasheet on the timing and that really the length for both bits is the same). In practice however if you drive the pin high for only 0.4us the chip treats it as a 0, and for 0.8us a 1. You do need more low time after a 0 of course, but doing the lot in 1.2us (by 1, nop, 0, vs 1, 0, nop) is fine \$\endgroup\$ – Jon Kloske Sep 8 '14 at 4:37
  • 1
    \$\begingroup\$ However, you need a full 50us for a "stop" code. What happens if you delay between bits for somewhere in between the 0.8us of a 0 and 50us for a stop isn't defined, however it seems that the way the chips work is when the pin is driven high, the protocol starts, and if the pin goes low before about 0.6us, it's considered a zero, and after 0.6 a 1. Then it waits for the next driving of the pin high, and if 50us happens before then, it considers the transfer done and latches everything across. This means you actually have about 50us between bits to do calculations. \$\endgroup\$ – Jon Kloske Sep 8 '14 at 4:40
  • 1
    \$\begingroup\$ @JonKloske - Very interesting experiments. Thank you very much for sharing your insights. I am different. I try to work within specification because that is the 'guarantee' made by the manufacturer. \$\endgroup\$ – gbulmer Sep 8 '14 at 18:59
  • \$\begingroup\$ ~definitely~ a good idea in general :) I think it's since these devices are very "toy" and their spec feels very much like it was hacked together I'm more comfortable playing with the specs like this. \$\endgroup\$ – Jon Kloske Sep 9 '14 at 0:35
  • 1
    \$\begingroup\$ @JonKloske - I am still amazed at their price. The datasheet seems to have been written after a quick chat with an engineer using only the back on an envelope. Have you experimented with driving the data signal from a 3.3V MCU? \$\endgroup\$ – gbulmer Sep 9 '14 at 10:04
0
\$\begingroup\$

If one is running ten instructions per microsecond (40MHz), I think one could meet required timings using a loop like:

    movlw 0xFF
LP:
    movwf PORTB,c
    nop
    nop
    nop
    movff POSTINC0,PORTB
    nop
    clrf  PORTB,c
    infsnz Ctr1,f,c
    incfsz Ctr2,f,c
     goto LP

If I figured things right, a "1" will be 700ns+500ns, while a zero will be 400ns+800ns. All times will be within 150ns of nominal, which is the specified tolerance.

BTW, it would be more helpful if the datasheet specified input and output tolerance separately, since it's likely that if one drives the first device in a chain with a signal that's near the edge of the specified tolerance, some aspects of the output timing are apt to be outside the specified tolerance band, but the data sheet doesn't say how precise the output would be.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.