Use Teensy as WS2812 emulator?

Status
Not open for further replies.

Pryankster

New member
I'm building an LED lighting project for my PC. My motherboard has a single addressible RGB port and knows "natively" how to drive WS2812 LEDs. I have several devices that I want to hook up that don't have the ability to "daisy chain", so I want to make a device that allows me to connect up multiple 2812 strands (or maybe even other kinds of ARGB, like DotStars. I envision a USB packet to configure the lengths of the output strips, and then using the teensy to direct the incoming pulse trains to the various outputs. This could be used, for example, to control the lights on multiple ARGB fans. My back-of-the-envelope design uses Paul's SERIAL/DMA based WS2812 code on the output side, but could the serial input be used on the input side?
Something like this:
Code:
int outputChainLength[8];
int numChains;

uint8_t *outputBuffer[2];

void init()
{
    // this would be configured VIA a USB control packet, here for illustration.
    outputChainLength[0] = 16; // case lights
    outputChainLength[1] = 8; // a fan
    outputChainLength[2] = 0; // nothing plugged in to output 2.
    outputChainLength[3] = 8; // another fan.
    numChains = 4;
    
    // count up total LED length ...
    for (int i = 0; i < numChains; i++ ) {
        numLEDs += outputChainLength[i];
    }
    
    // double buffer output. (maybe need triple buffer?) that could be a lot of RAM.
    outputBuffer[0] = calloc(numLEDs * 12);
    outputBuffer[1] = calloc(numLEDs * 12);
}
    
void recvRGB() 
{
    // forever, read LED data from motherboard -- it thinks it's talking to WS2812s.
    while(1) {
        // read in all RAW WS2812 control bytes, (12 bytes per LED @ 4Mbps)
        // waitDMARead will read in up to numLEDs*12 bytes, or until it gets a WS2812 'stop'.
        int bytes = waitDMARead(rgbBuffer[page], numLEDs * 12);
        // TODO: what if DMA still busy?  throw away? triple buffer?
	sendRGB(rgbBuffer[page], bytes);
	page ^= 1;
    }
}

volatile uint8_t *outputBuf;
volatile int outputChain;
volatile int outputBytes;

// 'buf' is raw serial data to send, bytes is number of raw bytes (LEDs * 12)
void sendRGB(uint8_t *buf, int bytes)
{
    outputChain = 0;
    outputBuf = buf;
    outputBytes = bytes;
    // make sure DMA is done.
    waitDMAComplete();
    // invoke DMA callback to start next dump to LED strips
    OnDMAComplete();
}

void OnDMAComplete()
{
    if (outputBytes == 0) return;	// out of bytes.. we're done.
    while( outputChain < numChains && outputChainLength[chain] == 0) chain++;
    if (chain == numChains) return; // we're done.
    
    SetOutputMUX(outputChain);
    int len = outputChainLength[chain] * 12;
    if (len > outputBytes) len = outputBytes;
    // TODO: swizzle bytes for RGB -> GBR, RBG, etc. 
    startSerialDMA( outputBuf, len, OnDMAComplete );		// bytes, length, on-complete-callback
    outputBuf += len;
    outputBytes -= len;
}

The reason that I want to buffer the whole chain is two fold: I may want/needs to swizzle the send bytes to compensate for different RGB ordering, and/or, direct some of those bytes into APA/Dotstar output. (which is much more forgiving than 2812)

Am I completely barking up the wrong tree? does this seem feasable?
-- pryankster
 
That's an interesting idea. Currently there is no code or library (as far as I know) which can receive WS2812 data. All the existing libraries only transmit.

Creating a library to reliably receive WS2812 protocol would be quite a challenge. I believe it could be done on Teensy 4.0. Here's 3 ways I can imagine it (maybe) working.

1: Timer input capture is probably the best way, probably using one of the FlexPWM timers. Maybe code in the FreqMeasure or PulsePosition libraries could be a good starting point. Capturing the counter at 150 MHz when the rising and falling edges occur would let software work out the exact timing of the waveform.

2: Serial receive might be simpler, but the only realistic approach I can see is running at just over 8 Mbit/sec speed, so the start bit, 8 data bits, and stop bit all fit in slightly less time than the 1.25us (800 kHz) bit time of WS2812. The serial port needs to be looking for the beginning of another start bit before the next WS2812 rising edge arrives. You'd also configure the serial port for inverse signal, since WS2812 is normally low and serial is normally high. Software-size this might be simpler, because you'd receive exactly 1 byte for each WS2812 bit. A simple lookup table could translate all 256 bit patterns into the decision of whether you received a 1 or 0. The main challenge with serial is we normally use the 24 MHz oscillator as the time base for all the serial ports. That gives a maximum baud rate of only 6 Mbit/sec. You'd need to reconfigure the serial ports to run at 80 MHz based on the USB PLL.

3: Software bitbang polling with digitalReadFast() might also be able to work, if you can be sure no other work is needed (and maybe all interrupts are disabled) while the WS2812 data arrives. You'd read the ARM_DWT_CYCCNT cycle counter each time you detect the change with digitalReadFast. You could probably do some minimal work while receiving, like subtracting the rising edge cycle count from the one read at the falling ege before you store it in a big array and go back to looking for the next rising edge. Software-wise, this is probably the easiest way, but of course it will fall apart if you're not running that tight loop while the WS2812 data arrives.

The first 2 ways use hardware to do the receiving, which probably means you're going to need DMA to move the data into memory fast enough. Or maybe Teensy 4.0 could manage 800,000 or 1.6 million interrupts per second if you keep the ISR code short and simple. Maybe.

So to answer you question...

does this seem feasable?

I would say it's probably possible, but a very ambitious expert level project. Well, unless someone has already done this very hard work and packaged it up nicely as a WS2812 receive library.
 
could be nicely done using a tinyFPGA AX1 to offload the teensy

the tinyFPGA needs a programmer but it can be built using a single pic16f1455 PDIP-14 USB
but then when I think about it the PIC16 also needs a programmer (maybe a teensy can be used for that)
 
Thinking about adding external circuitry, perhaps SPI could be used to receive if a 555 or similar one-shot timer is added. The WS2812 signal would go directly into MISO. The timer would delay the WS2812 rising edge by about 500ns, putting it right in the middle of the timing difference between a short/low versus long/high pulse.
 
but the easiest way is using the RT1060 input capture mode which can then be used to measure the high pulse time which is the the only one needed to determine if the receiving bit is a zero or one, the low part measure is only needed to determine if a reset pulse have been received.
Every time the pulse changes it generates a interrupt and then the timer value is compared against the different timing values.
which is the only way to do it without disturbing the teensy normal workflow.

The first thing to do is just to get the capture thingy working, and connect it to a ws2811 sender,
then just taking the values measured by the timer and send them to the serial port for easy determination of the different times needed.
 
Status
Not open for further replies.
Back
Top