Adjustable data speed for output expander

Hey, I have a LED cube with an on-board Arduino controlling (4 x 4 x 4 = 64) RGB LEDs through a constant current driver. i.e. no neopixels or dotstars.

TLDR: I would like to have slower Data Speed options available for the Output Expander to allow custom expanders to utialise the PixelBlaze’s rendering engine.

I have also attached my Arduino sketch below.


The cube’s MCU is a AtMega32u4, running at 16MHz. It can technically receive at 2mbs baud. A lot of the received data gets dropped (I’m guessing from noise causing parity issues and the MCU not being able to keep up with the amount of data coming in).

With the PixelBlaze v3 configured with:

LED type: PixelBlaze Output Extender
Pixels: 64
Data Speed: 2mbs
Color Order: RGB

Output Expander Configuration:

Channel Type Start Index count Options
0 WS2812 0 64 RGB

The other channels have default settings


After some trial and error, I realised that the buffered data I was receiving looked like the following.
You can see the magic bytes(0x5550584C - “UPXL”), followed by the channel (0x00) and type (0x01 - ws2812). Then the number of elements (0x03), colour order (0x24 - RGB) and finally the pixel count (0x4000 - 64 in little endian).

annoyingly, there is never enough pixel data to fill out the expected (3 * 64 = 192) pixels and often the CRC is missing before the next magic bytes are sent through.

5550584C0001032440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009FC56CB5550584CDF5550584C000103244000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000055385550584C000103244000000000000000000000000000000000000000000000000000000000000000000000000000000000
5550584C0300000000000000000000000000000000000000000058DF555000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000505550584C00010324400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000CB025550584C00010324400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
5550584C00010324400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056FF555001032430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005550584C000103244000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FCFF5550584C000103244000000000000000000000000000000000000000000000
5550584C0001032440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000559B5550584C0001032440000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005550584C000103000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050DF
5550584C000103244000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050815550584C00010324400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005550584C000103000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050385550584C000103244000
5550584C00010324400000000000000000000000000000000000000000000000000000000000000050385550584C00010324400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005550584C000103244000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056FF5550584C0001032440000000000000000000000000000000000000000000000000000000000000000000
5550584C00010324400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005550584C00010324400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000056FF5550584C00010324400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000055500000000000000000000000000000000000000000000000000000000000
Here is my Arduino sketch

[!NOTE]
The sketch is compiled with the build flag -D SERIAL_RX_BUFFER_SIZE=256

// PixelBlase to Cube4 adapter using PixelBlase Output Extender protocol.
//
// Cube4 - https://www.freetronics.com.au/products/cube4-4x4x4-rgb-led-cube
//
#include "Cube.h"

// ==============================================================================
// The following snippets were copied from
// https://github.com/simap/PBDriverAdapter/blob/master/src/PBDriverAdapter.cpp

enum ChannelType {
    CHANNEL_WS2812 = 1, CHANNEL_DRAW_ALL, CHANNEL_APA102_DATA, CHANNEL_APA102_CLOCK
};

typedef struct {
    int8_t magic[4];
    uint8_t channel;
    uint8_t recordType; //set channel ws2812 opts+data, draw all
} PBFrameHeader;

typedef struct {
    uint8_t numElements; //0 to disable channel, usually 3 (RGB) or 4 (RGBW)
    union {
        struct {
            uint8_t redi :2, greeni :2, bluei :2, whitei :2; //color orders, data on the line assumed to be RGB or RGBW
        };
        uint8_t colorOrders;
    };
    uint16_t pixels;
} PBWS2812Channel;

static const uint32_t crc_table[16] = {
        0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
        0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
};

uint32_t crc_update(uint32_t crc, const void *data, size_t data_len) {
    const unsigned char *d = (const unsigned char *) data;
    unsigned int tbl_idx;

    while (data_len--) {
        tbl_idx = crc ^ *d;
        crc = crc_table[tbl_idx & 0x0f] ^ (crc >> 4);
        tbl_idx = crc ^ (*d >> 4);
        crc = crc_table[tbl_idx & 0x0f] ^ (crc >> 4);
        d++;
    }
    return crc & 0xffffffff;
}

// ==============================================================================

Cube cube;

union {
  struct {
    PBFrameHeader header;
    PBWS2812Channel channel;
    rgb_t pixels[64];
    uint32_t rx_crc;
  };
  uint8_t data[sizeof(PBFrameHeader) + sizeof(PBWS2812Channel) + sizeof(pixels) + sizeof(rx_crc)];
} frame;


void setup() {
  memcpy(frame.header.magic, "UPXL", 4);

  Serial.begin(115200);
  Serial1.begin(2000000);
  cube.begin(-1);
}

void loop() {
  uint32_t crc = 0xffffffff;

  // Discard all bytes received until the magic bytes are found.
  if (!Serial1.find((uint8_t*)frame.header.magic, sizeof(frame.header.magic))){
    // Timed out waiting for the magic bytes.
    Serial.println("No data received from PixelBlaze.");
    return;
  }

  // Read in the rest of the frame header.  
  Serial1.readBytes(&frame.data[4], sizeof(frame.header) - 4);

  if(frame.header.channel == 0xff && frame.header.recordType == CHANNEL_DRAW_ALL) {
    Serial1.readBytes((uint8_t *) &frame.rx_crc, sizeof(frame.rx_crc));

    // Only calculate teh checksum on the frame's header.
    crc = 0xFFFFFFFF;
    crc = crc_update(crc, &frame.header, sizeof(frame.header));
    crc = crc ^0xffffffff;
    
    if (crc != frame.rx_crc){
      return;
    }

    cube.set(0, 0, 0, frame.pixels[0]);
    for(uint16_t i = 1; i < 64; i++){
        cube.next(frame.pixels[i]);
    }
    
    return;
  } else if(frame.header.channel != 0 || frame.header.recordType != CHANNEL_WS2812){
    
    return;
  }

  memset(frame.pixels, 0, sizeof(frame.pixels));

  // Raed in the rest of the frame.
  size_t offset = sizeof(frame.header);
  while (offset < sizeof(frame.data)) {
    offset += Serial1.readBytes((uint8_t *) &frame.data[offset], sizeof(frame) - offset);
  }

  if (frame.channel.numElements != 3 || frame.channel.pixels != 64){
    return;
  }

  crc = 0xFFFFFFFF;
  crc = crc_update(crc, &frame.data, sizeof(frame) - 4);
  crc = crc ^ 0xFFFFFFFF;
  
  // Clear out the received pixels if the checksum is invalid.
  if (crc != frame.rx_crc) {
    // Reset the pixels in case a DRAW ALL frame is sent.
    memset(frame.pixels, 0, sizeof(frame.pixels));
    return;
  }
}

Is this a reasonable request or should I look using a faster micro instead of an Arduino?

I noticed that the structures representing the frame data format are not packed, and may contain padding that you don’t realize is there. For example, sizeof (PBFrameHeader) may end up being equal to 12 instead of 6 as you might be expecting.

Hey, I completely overlooked that, thank you!
Unfortunately, adding a static_assert and also verifying with print statements does verify the structs are the expected size without the explicit packing.

typedef struct {
    int8_t magic[4];
    uint8_t channel;
    uint8_t recordType; //set channel ws2812 opts+data, draw all
} PBFrameHeader;
static_assert(sizeof(PBFrameHeader) == 6);

typedef struct {
    uint8_t numElements; //0 to disable channel, usually 3 (RGB) or 4 (RGBW)
    union {
        struct {
            uint8_t redi :2, greeni :2, bluei :2, whitei :2; //color orders, data on the line assumed to be RGB or RGBW
        };
        uint8_t colorOrders;
    };
    uint16_t pixels;
} PBWS2812Channel;
static_assert(sizeof(PBWS2812Channel) == 4);

// ...

void loop() {
  Serial.print("sizeof(PBFrameHeader) = ");
  Serial.println(sizeof(PBFrameHeader)); // prints: sizeof(PBFrameHeader) = 6
  Serial.print("sizeof(PBWS2812Channel) = ");
  Serial.println(sizeof(PBWS2812Channel)); // prints: sizeof(PBWS2812Channel) = 4
 
 // ...
}

For the sake of clarity, I’ll be adding __attribute__((__packed__)) to both structs

1 Like

Would be 32bit word be default. It’s a lot to ask for the atmega.

You might consider using apa102 protocol, and the clock speed for that can be set very low.