Reading PB serial output on Arduino

Hi all,

I’m new to the community (coming from the FastLED world) and just got my first PixelBlaze and output expander, which I’m already loving!

I’m working on a project that includes LEDs on 2 spinning wheels that are driven by a single DC motor (imagine something like this with LEDs. My plan is to use an Arduino Nano to directly control the motor speed via PWM (since PB does not directly support PWM output), but I would like to have the PB output the desired speed (and direction) to the Arduino.

To accomplish this, I’m hoping to dedicate one of the output expander channels to motor control, and setting fake RGB values in that channel, which the Arduino can then read in via the Rx pin and map to motor speed/direction outputs. However, I’m having trouble figuring out how to read RGB on the Arduino.

From searching the forums here, I’ve found @zranger1’s (C code for reading PB serial output on an ESP32), but I am having trouble implementing it for my setup (I am not very experienced with C). Does anyone have any shareable C code for Arduino that reads in PB serial data and extracts the RGB values?

Alternatively, I can describe what I’ve tried so far in case someone can identify what I might be doing wrong: I’ve set up a test where I am simply routing the PB DAT pin to Arduino RX. Grounds across both boards are connected. PB settings are set to output 1 WS2812 pixel. PB pattern calls for a single red LED:

export function render(index) {rgb(1, 1, 0)}

On the Arduino side, I’ve copied @zranger1’s C code (linked above), commented out all the parts for forwarding PB data over Wifi, reduced the BUFFER_SIZE to 500 (to be compatible with Arduino’s limited memory), and set the main loop to turn the Arduino built-in LED (pin 13) on if the magic word (“UPXL”) is received:

//#include <ESP8266WiFi.h>
//#include <WiFiUdp.h>
#include <SoftwareSerial.h>

// Serial setup
#define SSBAUD          115200            // log to console for debugging
#define BAUD            2000000           // bits/sec coming from pixelblaze
#define BUFFER_SIZE     500               // 2048 pixels plus a little
Stream* logger;

// Network setup
//#define _SSID "SSID"              // Your WiFi SSID goes here.  
//#define _PASS "WOWGREATPASSWORD"  // Your WiFi password goes here.
//#define LISTEN_PORT 8081          // UDP port on which pbxTeleporter listens for commands
//#define DATA_OUT_PORT 8082        // UDP port on client to which we send data
//
//WiFiUDP Udp;
//IPAddress targetIP;

// Datagram rate limiting - not currently used.  May be necessary
// eventually to support multple clients. For now, everything behaves well at 60+ fps,
// so the outgoing packet rate is determined by the client.
//
#define MIN_SEND_INTERVAL 17   // milliseconds - (16.6667ms == 60 fps)
uint64_t sendTimer = 0;

// TODO - per channel buffers for virtual wiring
// For now, we just concatenate pixel data and forward it all to the client
// on request.
// TODO -- in V2 support color order.
// For now, the user must set the  strand to be (ideally) a WS2812 w/RGB pixels 
// or (slightly less ideally) an APA102, which requires us to discard data to
// get RGB pixels. Someday, we may want the extra 5 bits of APA data, but for
// now it doesn't make any visisble difference on monitors.
// (Maybe eventually HDR if the client does some extra color calculation??)
//
#define MAX_PIXELS     2048                    // 8 channels x 256 pixels/channel
uint8_t pixel_buffer[BUFFER_SIZE];             // per-pixel RGB data for current frame
uint8_t incoming_buffer[256];                  // incoming requests for pixels
uint8_t *pixel_ptr;                            // current write position in buffer

///////////////////////////////////////////////////////////////////////////////////////////
// structures imported from pixelblaze expander source
// https://github.com/simap/pixelblaze_output_expander
//////////////////////////////////////////////////////////////////////////////////////////
enum RecordType {
  SET_CHANNEL_WS2812 = 1, DRAW_ALL, SET_CHANNEL_APA102_DATA, SET_CHANNEL_APA102_CLOCK
};

typedef struct {
//    int8_t magic[4];
    uint8_t channel;
    uint8_t command;
} __attribute__((packed)) 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;
} __attribute__((packed)) PBWS2812Channel ;

typedef struct {
    uint32_t frequency;
    union {
        struct {
            uint8_t redi :2, greeni :2, bluei :2; //color orders, data on the line assumed to be RGB
        };
        uint8_t colorOrders;
    };
    uint16_t pixels;
} __attribute__((packed)) PBAPA102DataChannel ;

typedef struct {
    uint32_t frequency;
}  __attribute__((packed)) PBAPA102ClockChannel;

/////////////////////////////////
// Utility Functions
/////////////////////////////////

// readBytes()
// reads the specified number of bytes into a buffer
// yields if bytes are not available
void readBytes(uint8_t *buf, uint16_t size) {
  int i = 0;
  while (i < size) {
    if (Serial.available()) {
      *buf++ = Serial.read();
      i++;
    }
    else {
      delay(0);
    }
  }  
}

// readOneByte()
// Read a single byte, yielding if the buffer is empty
uint8_t readOneByte() {
  while (!Serial.available()) {
    delay(0);
  }
  return Serial.read();
}

// readMagicWord
// returns true if we've found the magic word "UPXL"
// false otherwise. Clunky, but fast.
bool readMagicWord() {
  if (readOneByte() != 'U') return false;
  digitalWrite(13, LOW);
  if (readOneByte() != 'P') return false;
  if (readOneByte() != 'X') return false;
  if (readOneByte() != 'L') return false;
  
  return true;
}

// crcCheck()
// read and discard 32-bit CRC from data buffer 
// TODO -- for virtual wiring, we need to check this before passing
// channel data on to the clients.  For use with Processing, crc
// errors have been nonexistent, so it really isn't needed.
void crcCheck() { 
    uint32_t crc;
    readBytes((uint8_t *) &crc,sizeof(crc));
}

/////////////////////////////////
// Command Handlers
/////////////////////////////////

// read pixel data in WS2812 format
// NOTE: Only handles 3 byte RGB data for now.  Discards frame
// if it's any other size.
void doSetChannelWS2812() {
  PBWS2812Channel ch;
  uint16_t data_length;

  readBytes((uint8_t *) &ch,sizeof(ch));
  
// read pixel data if available
  if (ch.pixels && (ch.numElements == 3) && (ch.pixels <= MAX_PIXELS)) {
    data_length = ch.pixels * ch.numElements;
    readBytes(pixel_ptr,data_length);
    pixel_ptr += data_length;
  } 
  
  crcCheck();
}

// read pixel data in APA 102 format
void doSetChannelAPA102() {
  PBAPA102DataChannel ch;
  
  readBytes((uint8_t *) &ch,sizeof(ch));

// APA 102 data is always four bytes. The first byte
// contains a 3 bit flag and 5 bits of "extra" brightness data.
// We're gonna discard the "extra" APA bits and put 3-byte RGB
// data into the output buffer. 
  if (ch.frequency && (ch.pixels <= MAX_PIXELS)) {
    for (int i = 0; i < ch.pixels;i++) {
      readOneByte(); 
      readBytes(pixel_ptr,3);
      pixel_ptr += 3;
    }       
  } 
  
  crcCheck();  
}

// draw all pixels on all channels using current data
void doDrawAll() {
//  int packetSize = Udp.parsePacket();
//  if (packetSize) {   
//    uint16_t data_size = pixel_ptr - pixel_buffer;    
//    if (data_size) {
//      Udp.beginPacket(Udp.remoteIP(),DATA_OUT_PORT);
//      Udp.write(pixel_buffer,data_size);  
//      Udp.endPacket(); 
//    } 
//  }  
  pixel_ptr = pixel_buffer;      
}

// read APA 102 clock data.  
// TODO - For now, we ignore this. For full-on Pixel Telporter, we'll
// eventually have to do something with it...
void doSetChannelAPA102Clock() {
  PBAPA102ClockChannel ch;

  readBytes((uint8_t *) &ch,sizeof(ch));
  crcCheck();  
}

/////////////////////////////////
// Main loop & InitializationUtility Functions
/////////////////////////////////

// setup()
// Create software serial port for low speed logging and swap UART so it 
// speaks over GPIO.  Pins are: RX=GPIO13 TX=GPIO15
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);

  Serial.begin(BAUD);  
//  Serial.swap(); 
//  Serial.setRxBufferSize(1024);  
  
// use HardwareSerial0 pins so we can still log to the
// regular usbserial chips
//  SoftwareSerial* ss = new SoftwareSerial(3, 1);
//  ss->begin(SSBAUD);
//  ss->enableIntTx(false);
//  logger = ss;
//  logger->println();

//// Configure and connect WiFi
//  WiFi.mode(WIFI_STA);
//  WiFi.begin(_SSID,_PASS);
//
//  logger->print("\n\n\n");
//  logger->println("pbxTeleporter - Pixel Teleporter Bridge v1.1.2 for ESP8266");
//  logger->println("Connecting to wifi...");
//  while(WiFi.status() != WL_CONNECTED) {
//    logger->print('.');
//    delay(500);
//  }
//
//  logger->println("Connected!");
//  logger->print("IP address: ");
//  logger->println(WiFi.localIP());  
//  logger->printf("pbxTeleporter listening on %d, responding on %d\n",
//                 LISTEN_PORT,DATA_OUT_PORT);
//  
//  Udp.begin(LISTEN_PORT);    

  pixel_ptr = pixel_buffer;

//  logger->println("Setup: Success");
  pinMode(13, OUTPUT);
  digitalWrite(13, LOW);
}

// main loop
void loop() {
  PBFrameHeader hdr;
  
// read characters 'till we get the magic sequence
  if (readMagicWord()) {
    digitalWrite(13, HIGH);
    readBytes((uint8_t *) &hdr,sizeof(hdr));
    

    switch (hdr.command) {
      case SET_CHANNEL_WS2812:
       doSetChannelWS2812();
        break;
//      case DRAW_ALL:
//        doDrawAll();
//        break;       
      case SET_CHANNEL_APA102_DATA: 
        doSetChannelAPA102();       
        break;         
      case SET_CHANNEL_APA102_CLOCK:
        doSetChannelAPA102Clock();
        break;
      default:  
       break;        
    }
  }
}

The code uploads without error, but the LED never comes on, even if I update the RGB values on PB, so it seems the Arduino is not effectively reading the PB serial output.

Also, if anyone has suggestions for a better way to control my motor speed from the PB, then I’m very open to that as well. Thank you all for your help!

Hi and welcome!

I don’t have a Nano around to test, but I’d guess that problem is that pixel data is sent at very high speed. The PB/Output expander connection runs at 2,000,000 baud.

The Arduino software serial port won’t be able to keep up, and even though the Nano’s UART theoretically supports that data rate, once connected, there won’t be enough CPU time left over to do much of anything.

So you might think about approaching this a different way – maybe have the Pixelblaze talk to the Arduino via 4 GPIO pins which would give you 16 speeds - say, 8 forward, 8 reverse. The Nano could add graceful acceleration/deceleration on speed changes to its list of motor control tasks.

1 Like

Thanks so much for the insight! That makes sense that the Nano can’t keep up with the speed of PB pixel data output.

I was considering an approach using 2 GPIO pins to control the speed up or down as an alternative, but I like your suggestion because it communicates the speed in absolute terms between the controllers, so I’ll probably go with that. And definitely planning to use PID to control the motor speed smoothly. Thanks again for the suggestions!

2 Likes

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.