3
\$\begingroup\$

I need to send a string of bits to a chained set of RGB LEDs, each of which have their own controller IC. The datasheet for that IC, the WS2811, is here.

The protocol is quite simple: each bit, both 1 and 0, are sent as a high and a low, with different mark-space ratios, and are then strung together.

At 400kbps, the mark-space is as follows:

  • Binary 1: 1.2us high followed by 1.3us low (each +/- 150ns)
  • Binary 0: 0.5us high followed by 2.0us low (each +/- 150ns)

The data can either be sent at 400kbps or 800kbps. I'm not bothered which one I use, but I expect 400kbps will be easier to implement. (At 800kbps, the above times are halved.)

My question: How could this best be implemented? I'll probably be using a 20MHz (5MIPs) 8-bit PIC MCU to determine what data is to be sent, and I'm looking for the equivalent of a USART IC that the MCU could rely on to transmit data with the above encoding.

Possible solutions

There are three ways I envisage sending that kind of data at that speed:

  1. Finding an IC / ASIC which is dedicated to this purpose.
  2. Using a second MCU dedicated to receiving data from the primary MCU, and converting it directly to the above PWM encoding.
  3. Building a dedicated transmitter out of TTL/CMOS logic ICs or similar.

For point 1, I can't find any IC that will do it. Do they exist?

For point 2, I have ideas for how it might work, but no solutions so far. One solution: Bit-banging would be very hard, having to be written in assembler given timing requirements. Another solution, although not capable of achieving the 100ns resolution for timing of the PWM signal to match the specs in the datasheet, could configure the USART on a PIC MCU to transmit synchronously - except each 4 bits transmitted would just be a hacky way of sending one PWM bit. So to send 400kbps PWM data, the USART would have to be capable of 400 x 4 = 1.6Mbps. Then, to send a PWM binary 1, the USART would send HHLL, and to send a PWM binary 0, the USART would send HLLL. This would only barely satisfy the +/-150ns timing requirements, a margin of 25ns. And I haven't found a PIC that can transmit that fast serially anyway.

So at the moment, option 3 is my only option: build dedicated hardware out of logic ICs. For example:

  • Construct a 25-bit shift register to output the PWM, required for a single bit. Then circuitry which fills the register accordingly depending on whether a binary 1 or 0 is to be transmitted. And since this would still be very intensive on the MCU, I would add perhaps an 8-bit latch which the MCU could write to, and allow further circuitry to process the 8 bits one at a time, for each one encoding the bit into the 25 bit shift register as a PWM cycle.
  • As above, but replace the 25 bit shift register with a monostable that outputs either a long or a short pulse, and another which loads a new bit every 2.5us.
  • Using a memory IC which acts as a lookup table, so perhaps (with extra circuitry) could be given a byte to transmit from the MCU and return data that a circuit could translate into serial data as the PWM output.

In short, I can't find a good solution! It seems like an awful lot of work for such a simple problem. How would you do it?

\$\endgroup\$
1
  • 1
    \$\begingroup\$ If you do want to go the bit-banging assembler route, that's exactly what I did in my first foray into the wonderful world of PIC. \$\endgroup\$ Dec 28, 2015 at 10:58

3 Answers 3

4
\$\begingroup\$

These are popular chips, and the very popular WS2812 RGB LEDs (aka Neopixels) use the same 800KHz signal timing and protocol, so you are not alone!

The chips are not as finicky as they seem and can easily be driven from an 8-bit microconroller...

http://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/

People typically bit-bang them directly from a normal GPIO pin, which does require a little bit of cycle counting to get the timing right, but does not require any additional hardware except for maybe a level shifter if you are running the MCU at low voltage. The above article includes the following very simple bit-banging code for AVR that should be trivial to convert to PIC...

// These are the timing constraints taken mostly from the WS2812 datasheets
// These are chosen to be conservative and avoid problems rather than for maximum throughput 

#define T1H  900    // Width of a 1 bit in ns
#define T1L  600    // Width of a 1 bit in ns

#define T0H  400    // Width of a 0 bit in ns
#define T0L  900    // Width of a 0 bit in ns

#define RES 7000    // Width of the low gap between bits to cause a frame to latch

// Here are some convenience defines for using nanoseconds specs to generate actual CPU delays

#define NS_PER_SEC (1000000000L) // Note that this has to be SIGNED since we want to be able to check for negative values of derivatives

#define CYCLES_PER_SEC (F_CPU)

#define NS_PER_CYCLE ( NS_PER_SEC / CYCLES_PER_SEC )

#define NS_TO_CYCLES(n) ( (n) / NS_PER_CYCLE )

#define DELAY_CYCLES(n) ( ((n)>0) ? __builtin_avr_delay_cycles( n ) : __builtin_avr_delay_cycles( 0 ) ) // Make sure we never have a delay less than zero

// Actually send a bit to the string. We turn off optimizations to make sure the compile does
// not reorder things and make it so the delay happens in the wrong place.

void sendBit(bool) __attribute__ ((optimize(0)));

void sendBit( bool bitVal ) {

    if ( bitVal ) {      // 1-bit

      bitSet( PIXEL_PORT , PIXEL_BIT );

      DELAY_CYCLES( NS_TO_CYCLES( T1H ) - 2 ); // 1-bit width less overhead for the actual bit setting
                                                     // Note that this delay could be longer and everything would still work
      bitClear( PIXEL_PORT , PIXEL_BIT );

      DELAY_CYCLES( NS_TO_CYCLES( T1L ) - 10 ); // 1-bit gap less the overhead of the loop

    } else {             // 0-bit

      cli();                                       // We need to protect this bit from being made wider by an interrupt 

      bitSet( PIXEL_PORT , PIXEL_BIT );

      DELAY_CYCLES( NS_TO_CYCLES( T0H ) - 2 ); // 0-bit width less overhead
                                                    // **************************************************************************
                                                    // This line is really the only tight goldilocks timing in the whole program!
                                                    // **************************************************************************
      bitClear( PIXEL_PORT , PIXEL_BIT );

      sei();

      DELAY_CYCLES( NS_TO_CYCLES( T0L ) - 10 ); // 0-bit gap less overhead of the loop

    }

    // Note that the inter-bit gap can be as long as you want as long as it doesn't exceed the 5us reset timeout (which is A long time)
    // Here I have been generous and not tried to squeeze the gap tight but instead erred on the side of lots of extra time.
    // This has thenice side effect of avoid glitches on very long strings becuase

}

void sendByte( unsigned char byte ) {

    for( unsigned char bit = 0 ; bit < 8 ; bit++ ) {

      sendBit( bitRead( byte , 7 ) ); // Neopixel wants bit in highest-to-lowest order
                                                     // so send highest bit (bit #7 in an 8-bit byte since they start at 0)
      byte <<= 1; // and then shift left so bit 6 moves into 7, 5 moves into 6, etc

    }
}

Other techniques are also possible. I've had successes getting a UART to drive them...

http://wp.josh.com/2014/09/03/inside-neouart-tricking-a-serial-port-into-being-a-signal-generator/

...which can relax the timing requirements, especially on chips that have large transmit buffers.

PJRC has also famously gotten a DMA driven PWM to generate the correct signal...

https://www.pjrc.com/teensy/td_libs_OctoWS2811.html

(scroll down to "Technical Details")

...which is very complicated, but very cool in that there is almost no CPU load.

These are all easy solutions and are there are plenty of code examples for all of them. The right choice really depends on the application and what factors are important to you.

\$\endgroup\$
1
  • \$\begingroup\$ Very useful - thank you. Helps to know what the LEDs are called! \$\endgroup\$
    – Jodes
    Dec 27, 2015 at 20:38
1
\$\begingroup\$

The WS2811 timing is this: -

enter image description here

And, it appears that some folk are using SPI like this: -

enter image description here

So maybe consider using SPI if your chip supports it.

\$\endgroup\$
1
\$\begingroup\$

I woild drive them with delays hung of a SPI or USART data and clock pins

run it at 400Khz clock shorten the clock pulse to 250ns when a the data is zero and stretch it to 1500 ns when the data is 1

have the bit clock drive a ramp generator and the use the data signal to set the threshold for a comparator that's watching the ramp, in this way you can get one bit of data out for every bit you stuff into the usart

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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