Crossfading several RGB LED strips simultaneously

Hi all,

I'm trying to accomplish something which I thought would be easy, but to my surprise it has caused some serious hair pulling.

What I want to do is to use ShiftPWM to make 7 RGB LED strips crossfade through a colour sequence simultaneously. They are supposed to change from white to amber to red and back to white through amber, but I want to have a delay of (for instance) 2 seconds between each strip -- so strip 1 should be 2 seconds ahead of strip 2, 4 seconds ahead of strip 3, and strip N should be 2*N ahead of strip 1.

I think the problem is that I'm using a series of while loops to change the colours. The crossfade subroutine looks like this:

void colourFade(int idex){
  
  int hueRed = 0;
  int hueAmber = 22;
  int satWhite = 190;
  int satFull = 255;
  int fadeTime = 3500;
  int waitTime = 1500;

  int fadeSteps1 = satFull - satWhite;
  int fadeSteps2 = hueAmber - hueRed;
  
  int interval1 = fadeTime/fadeSteps1;
  int interval2 = fadeTime/fadeSteps2;
  
  int sat = satWhite;
  while(sat < satFull){
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval1)
    {sat++; previousMillis = currentMillis; ShiftPWM.SetHSV(idex, hueAmber, sat, 255);}
  }

  int hue=hueAmber;
  while(hue>hueRed){
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval2)
    {hue--; previousMillis = currentMillis; ShiftPWM.SetHSV(idex, hue, satFull, 255);}
  }
  
  while(hue<hueAmber){
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval2)
    {hue++; previousMillis = currentMillis; ShiftPWM.SetHSV(idex, hue, satFull, 255);}
  }

  
  while(sat>satWhite){
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval1)
    {sat--; previousMillis = currentMillis; ShiftPWM.SetHSV(idex, hueAmber, sat, 255);}
  }
}

So what I was thinking that I could do was to start this subroutine for each strip with a delay between the start of each; I tested this using the following (very ugly) loop:

void loop(){
    colourFade(0);
    delay(2000);
    colourFade(1);
    delay(2000);
    colourFade(2);
    delay(2000);
    colourFade(3);
    delay(2000);
    colourFade(4);
    delay(2000);
    colourFade(5);
    delay(2000);
    colourFade(6);
    delay(2000);
}

But of course, this does not work. The while loops are blocking, so the result of this is merely that the first strip goes through the whole sequence, followed by a 2 second wait, then the next one starts, and so on. What I want is for the first one to start, then the second one to start two seconds after that, and so on.

Any ideas on how to accomplish this? I have a feeling that I'm overlooking something very fundamental, but I haven't managed to find out what. I have looked at examples that are supposed to be non-blocking (hence the use of millis), but this isn't non-blocking enough.

Any hints would be much appreciated!

The while loops are blocking,

No it is the delay function that is blocking.
You want to look at the blink without delay example in the IDE, that is the way to do it.
You need to set up three tasks one for each strip you are fading.

This system is known as a state machine and is explained here:-
http://www.thebox.myzen.co.uk/Tutorial/State_Machine.html

For other explanations just google the terms finite state machine

I'm afraid I'm having trouble wrapping my head around this. I have tried multiple variations of the state machine, and a couple of different scheduler libraries, but the end result is always the same: the second strip won't start crossfading before the first one has finished.

This is what my code looks like at the moment:

#include <Queue.h>

const int ShiftPWM_latchPin=8;
const bool ShiftPWM_invertOutputs = false; 
const bool ShiftPWM_balanceLoad = false;

#include <ShiftPWM.h>

unsigned char maxBrightness = 255;
unsigned char pwmFrequency = 75;
int numRegisters = 3;
int numRGBleds = numRegisters*8/3;

long int goTime0;
long int goTime1;
long int goTime2;
long int goTime3;

unsigned long previousMillis;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void colourCrossFade(int idex){
  
  int hueRed = 0;
  int hueAmber = 22;
  int satWhite = 190;
  int satFull = 255;

  int fadeTime = 5000;

  int fadeSteps1 = satFull - satWhite;
  int fadeSteps2 = hueAmber - hueRed;
  
  int interval1 = fadeTime/fadeSteps1;
  int interval2 = fadeTime/fadeSteps2;
  
  int sat = satWhite;
  while(sat < satFull){
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval1)
    {sat++; previousMillis = currentMillis; ShiftPWM.SetHSV(idex, hueAmber, sat, 255);}
  }

  int hue=hueAmber;
  while(hue>hueRed){
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval2)
    {hue--; previousMillis = currentMillis; ShiftPWM.SetHSV(idex, hue, satFull, 255);}
  }
  
  while(hue<hueAmber){
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval2)
    {hue++; previousMillis = currentMillis; ShiftPWM.SetHSV(idex, hue, satFull, 255);}
  }

  while(sat>satWhite){
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval1)
    {sat--; previousMillis = currentMillis; ShiftPWM.SetHSV(idex, hueAmber, sat, 255);}
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void setup(){
  ShiftPWM.SetAmountOfRegisters(numRegisters);
  ShiftPWM.Start(pwmFrequency,maxBrightness);
  goTime0 = millis();
  goTime1 = millis() + 2000;
  goTime2 = millis() + 4000;
  goTime3 = millis() + 6000;
}

void loop(){
  if(millis() >= goTime0) colourCrossFade(0);
  if(millis() >= goTime1) colourCrossFade(1);
  if(millis() >= goTime2) colourCrossFade(2);
  if(millis() >= goTime3) colourCrossFade(3);
  }
  while(sat < satFull){
    unsigned long currentMillis = millis();
    if(currentMillis - previousMillis > interval1)
    {sat++; previousMillis = currentMillis; ShiftPWM.SetHSV(idex, hueAmber, sat, 255);}
  }

This is blocking code. It's a fancy way of avoiding typing the dreaded d e l a y function, but it achieves EXACTLY the same results. You need to read, understand, and EMBRACE, the blink without delay example and methodology.

At any given time, you are in some state. The color is at some level and there is some amount of time that has elapsed since the color last changed. You need to determine, on each pass through loop, what to do next based on those two factors. Either it is time to change the color level, or it isn't. If it is, the level is either at the max or it isn't. It should be pretty obvious what to do if the comparison is true, and what to do (possibly nothing) if it isn't, in both cases.

NO delay() calls and NO while loops that emulate what delay() is doing.

Thanks a lot for the assistance so far -- I think I'm starting to get it. I got the strips to fade through the sequence as intended, and the delay works -- the next challenge is that I can't seem to find a suitable place to declare the variables for R, G and B values. (I changed from HSV to RGB because it makes the fading less jerky).

The thing is that if I put the int R, int G, int B stuff at the start of the program, they will obviously be global variables, and thus the strips change colour in sync, which defeats the purpose of the program. But if I declare them at the start of the subroutine, they reset every time the function loops, so the strips can't change the colour at all.

const int ShiftPWM_latchPin=8;
const bool ShiftPWM_invertOutputs = false; 
const bool ShiftPWM_balanceLoad = false;

#include <ShiftPWM.h>

unsigned char maxBrightness = 255;
unsigned char pwmFrequency = 100;
int numRegisters = 3;
int numRGBleds = numRegisters*8/3;

long int goTime0;
long int goTime1;
long int goTime2;
long int goTime3;

unsigned long previousMillisR;
unsigned long previousMillisG;
unsigned long previousMillisB;

int R0 = 255;
int G0 = 140;
int Bl0 = 50;

int R1 = 255;
int G1 = 90;
int Bl1 = 0;  

int R2 = 255;
int G2 = 8;
int Bl2 = 0;  

int fadeTime = 5000;

int fadeSteps01R = R1 - R0;
int fadeSteps01G = G0 - G1;
int fadeSteps01B = Bl0 - Bl1;

int intervalR01 = fadeTime/fadeSteps01R;
int intervalG01 = fadeTime/fadeSteps01G;
int intervalB01 = fadeTime/fadeSteps01B;

int fadeSteps12R = R2 - R1;
int fadeSteps12G = G1 - G2;
int fadeSteps12B = Bl2 - B1;

int intervalR12 = fadeTime/fadeSteps12R;
int intervalG12 = fadeTime/fadeSteps12G;
int intervalB12 = fadeTime/fadeSteps12B;

int R = R0;
int G = G0;
int B = Bl0;

int fadeStage = 0;

  
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void colourCrossFade(int idex){
  
  unsigned long currentMillis = millis(); 

  // start at white
  // reduce green and blue to get amber

  if(G > G1 && currentMillis - previousMillisG > intervalG01 && fadeStage == 0){
    previousMillisG = currentMillis;
    G--; ShiftPWM.SetRGB(idex, R, G, B);
  }

  if(B > Bl1 && currentMillis - previousMillisB > intervalB01 && fadeStage == 0){
    previousMillisB = currentMillis;
    B--; ShiftPWM.SetRGB(idex, R, G, B); 
  }
  
  if(fadeStage == 0 && G == G1 && B == Bl1){
    fadeStage = 1;
  }
  
  // reduce green to get red
  
  if(G > G2 && currentMillis - previousMillisG > intervalG12 && fadeStage == 1){
    previousMillisG = currentMillis;
    G--; ShiftPWM.SetRGB(idex, R, G, B);
  }
  
  if(fadeStage == 1 && G == G2){
    fadeStage = 2;
  }
  
  // increase green to get amber

  if(G < G1 && currentMillis - previousMillisG > intervalG12 && fadeStage == 2){
    previousMillisG = currentMillis;
    G++; ShiftPWM.SetRGB(idex, R, G, B);
  }
  
  if(fadeStage == 2 && G == G1){
    fadeStage = 3;
  }
  
  // increase green and blue to get white
  
  if(B < Bl0 && currentMillis - previousMillisB > intervalB01 && fadeStage == 3){
    previousMillisB = currentMillis;
    B++; ShiftPWM.SetRGB(idex, R, G, B); 
  }
  
  if(G < G0 && currentMillis - previousMillisG > intervalG01 && fadeStage == 3){
    previousMillisG = currentMillis;
    G++; ShiftPWM.SetRGB(idex, R, G, B);
  } 
  
  if(fadeStage == 3 && B == Bl0 && G == G0){
    fadeStage = 0;
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void setup(){
  ShiftPWM.SetAmountOfRegisters(numRegisters);
  ShiftPWM.Start(pwmFrequency,maxBrightness);
  goTime0 = millis();
  goTime1 = millis() + 5000;
  goTime2 = millis() + 10000;
  goTime3 = millis() + 15000;
}

void loop(){
  if(millis() >= goTime0) colourCrossFade(0);
  if(millis() >= goTime1) colourCrossFade(1);
  if(millis() >= goTime2) colourCrossFade(2);
  if(millis() >= goTime3) colourCrossFade(3);
  }

The thing is that if I put the int R, int G, int B stuff at the start of the program, they will obviously be global variables, and thus the strips change colour in sync, which defeats the purpose of the program.

Not if you have one variable for each color for each strip.

But if I declare them at the start of the subroutine, they reset every time the function loops, so the strips can't change the colour at all.

Not if you use the static keyword when declaring the variable.