Daisy Chain Three 16x WS2812

I have a project I'm working on that uses an Arduino Pro Mini driving 3x 16 WS2812 NeoPixel rings. The LED data in are wired to individual data pins and the code currently works. What I would like is to be able to modify the sketch so the NeoPixels are driven by a single pin daisy chained together. The end goal is to be able to move from an Arduino based MCU to a Raspberry Pi.

I know you need to define the sketch with something like:
(index 0
led count 16)
(index 16
led count 16)
(index 32
led count 16)

But I know there's more involved than that. Can anyone help with this?

#include <Adafruit_NeoPixel.h>


#define NeoPixelStartupAnimationActive true  //Show Startup Animation for all Neopixels (true = activated / false = deactivated) !!Attention!! Animation will only be played if all NeoPixels have the same number of LEDs
#define COLOR(r, g, b) (((uint32_t)r << 16) | ((uint32_t)g <<  8) | b)

#define NumberNeoPixels 3

struct StructNeoPixelConfig {
  bool Active;
  uint32_t StartupAnimationColor;
  neoPixelType Type;
  uint16_t LEDs;
  uint16_t Pin;
  int8_t PixelOffset;
  float TempOffset;
  bool AnimationActive;
  bool AnimationReverse;
  uint8_t Brightness;
  bool AnimationMemoryActive;
  bool AnimationMemoryRunning;
  uint8_t AnimationMemoryPosition;
  uint8_t AnimationMemoryPosition_Memory;
  uint8_t AnimationMemoryRangeBegin;
  uint8_t AnimationMemoryRangeEnd;
  uint8_t AnimationMemoryAnimationColor;
};

StructNeoPixelConfig NeoPixelConfig[NumberNeoPixels] = {
  { // Neopixel 1
    Active                                : true,
    StartupAnimationColor                 : COLOR(0, 0, 255),
    Type                                  : NEO_GRB + NEO_KHZ800,
    LEDs                                  : 16,
    Pin                                   : 7,
    PixelOffset                           : 2,
    TempOffset                            : 0,
    AnimationActive                       : true,
    AnimationReverse                      : false,
    Brightness                            : 8,
  },
  { // Neopixel 2
    Active                                : true,
    StartupAnimationColor                 : COLOR(255, 0, 0),
    Type                                  : NEO_GRB + NEO_KHZ800,
    LEDs                                  : 16,
    Pin                                   : 6,
    PixelOffset                           : 0,
    TempOffset                            : 0,
    AnimationActive                       : true,
    AnimationReverse                      : false,
    Brightness                            : 8,
  },
  { // Neopixel 3
    Active                                : true,
    StartupAnimationColor                 : COLOR(0, 255, 0),
    Type                                  : NEO_GRB + NEO_KHZ800,
    LEDs                                  : 16,
    Pin                                   : 8,
    PixelOffset                           : 0,
    TempOffset                            : 0,
    AnimationActive                       : true,
    AnimationReverse                      : false,
    Brightness                            : 8,
  },
};


#define COLOR1(x) COLOR(x) // expand a single argument to 3 for COLOR

#define NeopixelRefreshSpeed 200

unsigned long NeoPixelTimerRefresh = millis();
uint8_t NeoPixelID;
uint8_t NeoPixelLEDID;

Adafruit_NeoPixel *NeoPixel_Device[NumberNeoPixels];

static int ConvertPosition2PixelIndex(int PixelCount, int PixelOffset, int Position, bool ReverseDirection) {
  int newposition;
  int newpixeloffset;
  if (ReverseDirection == false) {
    newpixeloffset = PixelOffset;
  }
  else{
    newpixeloffset = -PixelOffset;
  }
  if ((Position + newpixeloffset) > PixelCount) {
    newposition = (Position + newpixeloffset) - PixelCount;
  }
  else if ((Position + newpixeloffset) < 1) {
    newposition = PixelCount + (Position + newpixeloffset);
  }
  else {
    newposition = (Position + newpixeloffset);
  }  
  if (ReverseDirection == false) {
    return (PixelCount - newposition);
  }
  else {  
    return (newposition - 1);
  }  
}

// Initialize everything and prepare to start
void setup()
{
}

// Main loop
void loop()
{
   
  pinMode(LED_BUILTIN, OUTPUT);
  // Initialize Variables
  int16_t BrightnessID = 0;
  uint8_t NeoPixelAnimationStep;
  uint8_t NeoPixelAnimationCount;
  uint8_t NeoPixelAnimationID;
  uint32_t AnimiationColor[NumberNeoPixels];
  
  // Initialize Neopixels
  for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++){
    if (NeoPixelConfig[NeoPixelID].Active == true) {
      NeoPixel_Device[NeoPixelID] = new Adafruit_NeoPixel(NeoPixelConfig[NeoPixelID].LEDs, NeoPixelConfig[NeoPixelID].Pin, NeoPixelConfig[NeoPixelID].Type);      
      NeoPixel_Device[NeoPixelID]->begin(); 
      NeoPixel_Device[NeoPixelID]->show();
    }
  }  

  //Set start values
  for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++){
    NeoPixelConfig[NeoPixelID].AnimationMemoryPosition = 0;
    NeoPixelConfig[NeoPixelID].AnimationMemoryPosition_Memory = 0;
    NeoPixelConfig[NeoPixelID].AnimationMemoryRangeBegin = 0;
    NeoPixelConfig[NeoPixelID].AnimationMemoryRangeEnd = 0;
    NeoPixelConfig[NeoPixelID].AnimationMemoryRunning = false;
    NeoPixelConfig[NeoPixelID].AnimationMemoryAnimationColor = 0;
  }  
  
  // Startup-Animation
  int NeoPixelCount = 0;
  bool NeoPixelStartupAnimationActive_Consistency = true;
  if (NeoPixelStartupAnimationActive == true)
  {
    // Animation Consistency-Check
    for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++){
      if (NeoPixelConfig[NeoPixelID].Active == true) {
        if (NeoPixelCount == 0) {
          NeoPixelCount = NeoPixelConfig[NeoPixelID].LEDs;
        }  
        else {
          if (NeoPixelConfig[NeoPixelID].LEDs != NeoPixelCount) {
            NeoPixelStartupAnimationActive_Consistency = false;
          }
        }
      }
    } 
    //Activate startup sequence & Co. if all activated NeoPixels have the same number of LEDs
    if (NeoPixelCount > 0 && NeoPixelStartupAnimationActive_Consistency == true) {

      //Initialize Neopixels && Build AnimationColor-Array
      NeoPixelAnimationCount = 0;
      for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++){
        if (NeoPixelConfig[NeoPixelID].Active == true) { 
          NeoPixel_Device[NeoPixelID]->setBrightness(255);
          AnimiationColor[NeoPixelAnimationCount] = NeoPixelConfig[NeoPixelID].StartupAnimationColor;
          NeoPixelAnimationCount++;
        }  
      }

      //Startup Animation Phase #1
      for (NeoPixelAnimationStep = 0; NeoPixelAnimationStep < NeoPixelAnimationCount; NeoPixelAnimationStep++) {
        for (NeoPixelLEDID = 0; NeoPixelLEDID < NeoPixelCount; NeoPixelLEDID++) {
          NeoPixelAnimationID = NeoPixelAnimationStep;
          for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++) {
            if (NeoPixelConfig[NeoPixelID].Active == true) {
              NeoPixel_Device[NeoPixelID]->fill(COLOR(0, 0, 0));
              NeoPixel_Device[NeoPixelID]->setPixelColor(ConvertPosition2PixelIndex(NeoPixelConfig[NeoPixelID].LEDs,NeoPixelConfig[NeoPixelID].PixelOffset,(NeoPixelLEDID + 1), NeoPixelConfig[NeoPixelID].AnimationReverse), AnimiationColor[NeoPixelAnimationID]); 
              NeoPixel_Device[NeoPixelID]->show();
            }  
            NeoPixelAnimationID++;
            if (NeoPixelAnimationID >= NeoPixelAnimationCount) {
              NeoPixelAnimationID = 0;
            }  
          }
          delay(30);
        }
      }  

      //Startup Animation Phase #2
      for (NeoPixelAnimationStep = 0; NeoPixelAnimationStep < NeoPixelAnimationCount; NeoPixelAnimationStep++) {
        //Part A
        for(BrightnessID = 0; BrightnessID < 255; BrightnessID++) {
          NeoPixelAnimationID = NeoPixelAnimationStep;
          for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++) {
            if (NeoPixelConfig[NeoPixelID].Active == true) {
              NeoPixel_Device[NeoPixelID]->setBrightness(BrightnessID); 
              NeoPixel_Device[NeoPixelID]->fill(AnimiationColor[NeoPixelAnimationID]);
              NeoPixel_Device[NeoPixelID]->show();
            }
            NeoPixelAnimationID++;
            if (NeoPixelAnimationID >= NeoPixelAnimationCount) {
              NeoPixelAnimationID = 0;
            }
          }
          delay(1);
        }  
        //Part B
        for(BrightnessID = 255; BrightnessID >= 0; BrightnessID--) {
          NeoPixelAnimationID = NeoPixelAnimationStep;
          for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++) {
            if (NeoPixelConfig[NeoPixelID].Active == true) {
              NeoPixel_Device[NeoPixelID]->setBrightness(BrightnessID); 
              NeoPixel_Device[NeoPixelID]->fill(AnimiationColor[NeoPixelAnimationID]);
              NeoPixel_Device[NeoPixelID]->show();
            }
            NeoPixelAnimationID++;
            if (NeoPixelAnimationID >= NeoPixelAnimationCount) {
              NeoPixelAnimationID = 0;
            }
          }
          delay(1);
        }  
      }  
    } 
  }  
  
  //Initial Neopixel for Loop
  for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++){
    if (NeoPixelConfig[NeoPixelID].Active == true) {
      NeoPixel_Device[NeoPixelID]->clear(); 
      NeoPixel_Device[NeoPixelID]->setBrightness(NeoPixelConfig[NeoPixelID].Brightness); 
      NeoPixel_Device[NeoPixelID]->show();
    }
  }
  }

WS2812 can be cascaded. So instead of 3 pins with 16 LEDs, there will be 1 pin with 48 LEDs. You also need to think about how to connect the Raspberry Pi, because it seems to run at 3.3 volts but the Arduino Pro Mini and WS2812 runs at 5 volts - maybe you need a level shifter.

I understand that. I have a logic level shifter I will use but I'm not sure how to modify the code. That's my issue

This code seems to have been written to frustrate what should be a simple matter.

/* ... make one strip object */

# define NLAMPS     48
# define myDataPin  9

Adafruit_NeoPixel theOneTrueStrip = Adafruit_NeoPixel(NLAMPS, aDataPin, NEO_GRB + NEO_KHZ800);

and remove the neopixel object from the structure they are now in.


begin(); 
clear(); 
fill(AnimiationColor[NeoPixelAnimationID]);
fill(COLOR(0, 0, 0));
setBrightness(255);
setBrightness(BrightnessID); 
setBrightness(NeoPixelConfig[NeoPixelID].Brightness); 
setPixelColor(ConvertPosition2PixelIndex(NeoPixelConfig[NeoPixelID].LEDs,NeoPixelConfig[NeoPixelID].PixelOffset,(NeoPixelLEDID + 1), NeoPixelConfig[NeoPixelID].AnimationReverse), AnimiationColor[NeoPixelAnimationID]); 
show();

Those are all the ways you talk to the neopixels. Every call to one of the 6 functions has to be replaced with a call that will route the commands to the strip at the appropriate offset.

Understanding and modifying ConvertPosition2PixelIndex() will be the key to that.

So a physical pixel's index is the ConvertPosition2PixelIndex() the original program calculated, plus 16 if on logical strip 2 of 3, and 32 if on logical strip 3 of 3.

TBH it looks doable, but it will be quite a slog IMO.

For example


  for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++) {
	if (NeoPixelConfig[NeoPixelID].Active == true) {
	  NeoPixel_Device[NeoPixelID]->fill(COLOR(0, 0, 0));
	  NeoPixel_Device[NeoPixelID]->setPixelColor(ConvertPosition2PixelIndex(NeoPixelConfig[NeoPixelID].LEDs,NeoPixelConfig[NeoPixelID].PixelOffset,(NeoPixelLEDID + 1), NeoPixelConfig[NeoPixelID].AnimationReverse), AnimiationColor[NeoPixelAnimationID]); 
	  NeoPixel_Device[NeoPixelID]->show();
	}  

might become, when we have one physical 48 LED strip:

  theOneTrueStrip.fill(COLOR(0, 0, 0));
  
  for (NeoPixelID = 0; NeoPixelID < NumberNeoPixels; NeoPixelID++) {
	if (NeoPixelConfig[NeoPixelID].Active == true) {
	
	  int logicalIndex = ConvertPosition2PixelIndex(NeoPixelConfig[NeoPixelID].LEDs,NeoPixelConfig[NeoPixelID].PixelOffset,(NeoPixelLEDID + 1), NeoPixelConfig[NeoPixelID].AnimationReverse) + 16 * NeopixelID;

	  theOneTrueStrip.setPixelColor(logicalIndex, AnimiationColor[NeoPixelAnimationID]); 
	}  
  }
  
  theOneTrueStrip.show();  /not "fillshow"

The idea is to leave all the structure and how it informs pixel colors intact, but move the strips out and redirect all the calls that do anything to a portion of the one physical strip.

fill() will have to be replaced with loops of your own, as it applies to the entire strip. That's straightahead - your fill may be slightly slower, no problem.

If that makes sense, go for it. If it doesn't light up a few brain cells at least, you in over your head - still possible but even more challenging.

With only 48 pixels I can't imagine why someone didn't write to one physical strip in the first place. This clever code (I assume you did not write) almost seems like it was deliberately written to confuse and frustrate. I know I am confused and frustrated, color me tapped out on this one time-wise.

HTH

edits: I am finding problems with my little example, I will fix them, but take the code with a giant glass of vodka grain of salt, it's just the idea sketched yet.

a7

Original Code

Let me give some background. The original intention for the Neopixels was to be connected to an Arduino Pro Mini that is connected to a 3d printer through UART, acting like a serial sniffer it was looking for specific status', based on the status it would change the state of each of the 3 rings.

The project is moving to a different firmware that is hosted on a Raspberry Pi. We can use the built in API to drive the status changes for the Neopixels but the starting animation code is difficult to write. The idea was to strip the old code so it only contains the starting animation, than using Arduino IDE export it as a binary. Once the new code is written to run off the RPI we would include the exported binary to run once at startup.

It's entirely possible that when I stripped the code I left some extra coding

Hmm. Still thinking you didn’t write the code? There really is no good reason for there to have been multiple pins and multiple strips and multiple strip objects and multiple loops over them and and and.

In any event, the approach I outlined is valid, I have the code working to one strip just fine…

for Phase 1. Arguably the low lower hanging fruit.

I do not anticipate getting Phase 2 to function, that’s your trip. But it will be harder as I have suggested, as it relies on whole-strip methods. I would start by reading in the library code to see how strip brightness and other whole-strip functions are coded.

OR

Someone please tell me there is another smart LED library that will let us consider sub-sectional runs of pixels as separate virtual strip entities.

I’ve bookmarked a deep dive one day into FastLED, which may have some abstraction concepts that would makes this almost trivial.

I did my work on your problem at the lowest levels possible, keeping my mind clear of questions about how the code is structured, how it functions and what all those struct elements might mean and why they named so odd &c.

a7

Yes, FastLED allows partitioning the pixel array into "virtual strips" (my term, not FastLED's). It handles the indexing / offsetting from the sub-strips into the actual big pixel array. Then, when all the updates are done in memory, you push the entire pixel array into the physical strip at one time.

THX @gfvalvo, that will energize my dive and perhaps make it come sooner.

@dhusolo I suggest you look into switching libraries, Imma bet FastLED would be easier… depending on what you call easy, or maybe what kind of fun you want to have.

I would also recommend you consider simply dumping the (gratuitous?) animation, which has nothing to do with nothing and is perhaps a waste of time.

a7

Sorry thought I answered the initial question. No I did not write the original code(linked in my last post). The code I posted in the OP was the result after I removed what I thought was unnecessary from the original code.

I'd love to get rid of the starting animation but the person in charge of the project wants to keep it...

By the sounds of it due to the fact the way the code was written it might be easier if I start from scratch.

  1. Make sure I can get the 3 Neopixel rings to work daisy-chained together
  2. Figure out how to simplify the starting animations and add it to the new "from scratch" code.

If you talking those 16 x 3 = 48 LEDs on one pin, this will not be a problem.

You may be able to consider a different (FastLED library) or you may be locked in, no matter. It's all just a little bit of organization, bookkeeping and maths.

But yeah, starting from scratch looks real good to me - looked good after a few minutes poking around in the code. :expressionless:

a7

Had to look it up myself. The structure is called 'CPixelView'. This example simply shows how indexing into an array of CPixelView objects (one element for each ring) properly indexes into the real led array:

#include "Arduino.h"
#include <FastLED.h>

#define COLOR_ORDER GRB
#define CHIPSET WS2812

constexpr uint8_t numRings = 3;
constexpr uint8_t numPixelsPerRing = 16;
constexpr uint16_t numPixels = numRings * numPixelsPerRing;
constexpr uint8_t ledPin = 5;

CRGB leds[numPixels];
CPixelView<CRGB> ledRings[] = {
  {leds + 0 * numPixelsPerRing, numPixelsPerRing},
  {leds + 1 * numPixelsPerRing, numPixelsPerRing},
  {leds + 2 * numPixelsPerRing, numPixelsPerRing}
};

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("Starting");

  FastLED.addLeds<CHIPSET, ledPin, COLOR_ORDER>(leds, numPixels);

  for (auto &ring : ledRings) {
    Serial.print(&ring[0] - leds);
    Serial.print(" ... ");
    Serial.println(&ring[numPixelsPerRing-1] - leds);
  }
}

void loop() {
}

Serial Monitor Display:

Starting
0 ... 15
16 ... 31
32 ... 47

One thing I don't like about that code is that you have to manually define the indices for the 'ledRings' array. This is error prone. On a non-AVR processor with std::vector, you could be a little slicker and let the number of rings and leds / ring values do the work for you.

#include "Arduino.h"
#include <FastLED.h>
#include <vector>

#define COLOR_ORDER GRB
#define CHIPSET WS2812

constexpr uint8_t numRings = 3;
constexpr uint8_t numPixelsPerRing = 16;
constexpr uint16_t numPixels = numRings * numPixelsPerRing;
constexpr uint8_t ledPin = 5;

CRGB leds[numPixels];
std::vector<CPixelView<CRGB>> ledRings;

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.printf("Starting\n");

  FastLED.addLeds<CHIPSET, ledPin, COLOR_ORDER>(leds, numPixels);

  for (uint8_t r = 0; r < numRings; r++) {
    ledRings.emplace_back(leds + (uint16_t) r * numPixelsPerRing, numPixelsPerRing);
  }

  for (auto &ring : ledRings) {
    Serial.printf("%d ... %d\n", &ring[0] - leds, &ring[numPixelsPerRing - 1] - leds);
  }
}

void loop() {
}