SK6812 RGBW Test Fixture: Row-by-Row Color Mods

The vacuum tube LED firmware subtracts the minimum value from the RGB channels of the SK6812 RGBW LEDs and displays it in the white channel, thereby reducing the PWM value of the RGB LEDs by their common “white” component. The main benefit is reducing the overall power by about two LEDs. More or less, kinda sorta.

I tweaked the SK6812 test fixture firmware to show how several variations of the basic RGB colors appear:

      for (int col=0; col < NUMCOLS ; col++) {              // for each column
        byte Value[PIXELSIZE];                              // figure first row colors
        for (byte p=0; p < PIXELSIZE; p++) {                //  ... for each color in pixel
          Value[p] = StepColor(p,-col*Pixels[p].TubePhase);
        }
        // just RGB
        int row = 0;
        uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],0);
        strip.setPixelColor(col + NUMCOLS*row++,UniColor);

        byte MinWhite = min(min(Value[RED],Value[GREEN]),Value[BLUE]);

        // only common white
        UniColor = strip.Color(0,0,0,MinWhite);
        strip.setPixelColor(col + NUMCOLS*row++,UniColor);

        // RGB minus common white + white
        UniColor = strip.Color(Value[RED]-MinWhite,Value[GREEN]-MinWhite,Value[BLUE]-MinWhite,MinWhite);
        strip.setPixelColor(col + NUMCOLS*row++,UniColor);

         // RGB minus common white
        UniColor = strip.Color(Value[RED]-MinWhite,Value[GREEN]-MinWhite,Value[BLUE]-MinWhite,0);
        strip.setPixelColor(col + NUMCOLS*row++,UniColor);

        // inverse RGB
        UniColor = strip.Color(255 - Value[RED],255 - Value[GREEN],255 - Value[BLUE],0);
        strip.setPixelColor(col + NUMCOLS*row++,UniColor);

Which looks like this:

SK6812 Test Fixture - RGBW color variations - diffuser
SK6812 Test Fixture – RGBW color variations – diffuser

The pure RGB colors appear along the bottom row, with the variations proceeding upward to the inverse RGB in the top row. The dust specks show it’s actually in focus.

The color variations seem easier to see without the diffuser:

SK6812 Test Fixture - RGBW color variations - bare LEDs
SK6812 Test Fixture – RGBW color variations – bare LEDs

The white LEDs are obviously “warm white”, which seems not to make much difference.

Putting a jumper from D2 to the adjacent (on an Nano, anyway) ground pin selects the original pattern, removing the jumper displays the modified pattern:

SK6812 test fixture - pattern jumper
SK6812 test fixture – pattern jumper

For whatever it’s worth, those LEDs have been running at full throttle for two years with zero failures!

The Arduino source code as a GitHub Gist:

// SK6812 RGBW LED array exerciser
// Ed Nisley - KE4ANU - February 2017
// 2020-01-25 add row-by-row color modifications
#include <Adafruit_NeoPixel.h>
//----------
// Pin assignments
const byte PIN_NEO = A3; // DO - data out to first Neopixel
const byte PIN_HEARTBEAT = 13; // DO - Arduino LED
const byte PIN_SELECT = 2; // DI - pattern select input
//----------
// Constants
#define UPDATEINTERVAL 20ul
const unsigned long UpdateMS = UPDATEINTERVAL - 1ul; // update LEDs only this many ms apart minus loop() overhead
// number of steps per cycle, before applying prime factors
#define RESOLUTION 100
// phase difference between LEDs for slowest color
#define BASEPHASE (PI/16.0)
// LEDs in each row
#define NUMCOLS 5
// number of rows
#define NUMROWS 5
#define NUMPIXELS (NUMCOLS * NUMROWS)
#define PINDEX(row,col) (row*NUMCOLS + col)
//----------
// Globals
// instantiate the Neopixel buffer array
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUMPIXELS, PIN_NEO, NEO_GRBW + NEO_KHZ800);
uint32_t FullWhite = strip.Color(255,255,255,255);
uint32_t FullOff = strip.Color(0,0,0,0);
struct pixcolor_t {
byte Prime;
unsigned int NumSteps;
unsigned int Step;
float StepSize;
float TubePhase;
byte MaxPWM;
};
// colors in each LED
enum pixcolors {RED, GREEN, BLUE, WHITE, PIXELSIZE};
struct pixcolor_t Pixels[PIXELSIZE]; // all the data for each pixel color intensity
unsigned long MillisNow;
unsigned long MillisThen;
//-- Figure PWM based on current state
byte StepColor(byte Color, float Phi) {
byte Value;
Value = (Pixels[Color].MaxPWM / 2.0) * (1.0 + sin(Pixels[Color].Step * Pixels[Color].StepSize + Phi));
// Value = (Value) ? Value : Pixels[Color].MaxPWM; // flash at dimmest points
// printf("C: %d Phi: %d Value: %d\r\n",Color,(int)(Phi*180.0/PI),Value);
return Value;
}
//-- Helper routine for printf()
int s_putc(char c, FILE *t) {
Serial.write(c);
}
//------------------
// Set the mood
void setup() {
pinMode(PIN_HEARTBEAT,OUTPUT);
digitalWrite(PIN_HEARTBEAT,LOW); // show we arrived
pinMode(PIN_SELECT,INPUT_PULLUP);
Serial.begin(57600);
fdevopen(&s_putc,0); // set up serial output for printf()
printf("WS2812 / SK6812 array exerciser\r\nEd Nisley - KE4ZNU - February 2017\r\n");
/// set up Neopixels
strip.begin();
strip.show();
// lamp test: run a brilliant white dot along the length of the strip
printf("Lamp test: walking white\r\n");
strip.setPixelColor(0,FullWhite);
strip.show();
delay(250);
for (int i=1; i<NUMPIXELS; i++) {
digitalWrite(PIN_HEARTBEAT,HIGH);
strip.setPixelColor(i-1,FullOff);
strip.setPixelColor(i,FullWhite);
strip.show();
digitalWrite(PIN_HEARTBEAT,LOW);
delay(250);
}
strip.setPixelColor(NUMPIXELS - 1,FullOff);
strip.show();
delay(250);
// fill the array, row by row
printf(" ... fill\r\n");
for (int i=NUMROWS-1; i>=0; i--) { // for each row
digitalWrite(PIN_HEARTBEAT,HIGH);
for (int j=NUMCOLS-1; j>=0 ; j--) {
strip.setPixelColor(PINDEX(i,j),FullWhite);
strip.show();
delay(100);
}
digitalWrite(PIN_HEARTBEAT,LOW);
}
// clear to black, column by column
printf(" ... clear\r\n");
for (int j=NUMCOLS-1; j>=0; j--) { // for each column
digitalWrite(PIN_HEARTBEAT,HIGH);
for (int i=NUMROWS-1; i>=0; i--) {
strip.setPixelColor(PINDEX(i,j),FullOff);
strip.show();
delay(100);
}
digitalWrite(PIN_HEARTBEAT,LOW);
}
delay(1000);
// set up the color generators
MillisNow = MillisThen = millis();
printf("First random number: %ld\r\n",random(10));
Pixels[RED].Prime = 3;
Pixels[GREEN].Prime = 5;
Pixels[BLUE].Prime = 7;
Pixels[WHITE].Prime = 11;
printf("Primes: (%d,%d,%d,%d)\r\n",
Pixels[RED].Prime,Pixels[GREEN].Prime,Pixels[BLUE].Prime,Pixels[WHITE].Prime);
unsigned int PixelSteps = (unsigned int) ((BASEPHASE / TWO_PI) *
RESOLUTION * (unsigned int) max(max(max(Pixels[RED].Prime,Pixels[GREEN].Prime),Pixels[BLUE].Prime),Pixels[WHITE].Prime));
printf("Pixel phase offset: %d deg = %d steps\r\n",(int)(BASEPHASE*(360.0/TWO_PI)),PixelSteps);
Pixels[RED].MaxPWM = 255;
Pixels[GREEN].MaxPWM = 255;
Pixels[BLUE].MaxPWM = 255;
Pixels[WHITE].MaxPWM = 32;
for (byte c=0; c < PIXELSIZE; c++) {
Pixels[c].NumSteps = RESOLUTION * (unsigned int) Pixels[c].Prime;
Pixels[c].Step = (3*Pixels[c].NumSteps)/4;
Pixels[c].StepSize = TWO_PI / Pixels[c].NumSteps; // in radians per step
Pixels[c].TubePhase = PixelSteps * Pixels[c].StepSize; // radians per tube
printf("c: %d Steps: %5d Init: %5d",c,Pixels[c].NumSteps,Pixels[c].Step);
printf(" PWM: %3d Phi %3d deg\r\n",Pixels[c].MaxPWM,(int)(Pixels[c].TubePhase*(360.0/TWO_PI)));
}
}
//------------------
// Run the mood
void loop() {
MillisNow = millis();
if ((MillisNow - MillisThen) > UpdateMS) {
digitalWrite(PIN_HEARTBEAT,HIGH);
unsigned int AllSteps = 0;
for (byte c=0; c < PIXELSIZE; c++) { // step to next increment in each color
if (++Pixels[c].Step >= Pixels[c].NumSteps) {
Pixels[c].Step = 0;
printf("Color %d steps %5d at %8ld delta %ld ms\r\n",c,Pixels[c].NumSteps,MillisNow,(MillisNow - MillisThen));
}
AllSteps += Pixels[c].Step; // will be zero only when all wrap at once
}
if (0 == AllSteps) {
printf("Grand cycle at: %ld\r\n",MillisNow);
}
if (digitalRead(PIN_SELECT)) {
for (int col=0; col < NUMCOLS ; col++) { // for each column
byte Value[PIXELSIZE]; // figure first row colors
for (byte p=0; p < PIXELSIZE; p++) { // ... for each color in pixel
Value[p] = StepColor(p,-col*Pixels[p].TubePhase);
}
// just RGB
int row = 0;
uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],0);
strip.setPixelColor(col + NUMCOLS*row++,UniColor);
byte MinWhite = min(min(Value[RED],Value[GREEN]),Value[BLUE]);
// only common white
UniColor = strip.Color(0,0,0,MinWhite);
strip.setPixelColor(col + NUMCOLS*row++,UniColor);
// RGB minus common white + white
UniColor = strip.Color(Value[RED]-MinWhite,Value[GREEN]-MinWhite,Value[BLUE]-MinWhite,MinWhite);
strip.setPixelColor(col + NUMCOLS*row++,UniColor);
// RGB minus common white
UniColor = strip.Color(Value[RED]-MinWhite,Value[GREEN]-MinWhite,Value[BLUE]-MinWhite,0);
strip.setPixelColor(col + NUMCOLS*row++,UniColor);
// inverse RGB
UniColor = strip.Color(255 - Value[RED],255 - Value[GREEN],255 - Value[BLUE],0);
strip.setPixelColor(col + NUMCOLS*row++,UniColor);
}
}
else {
for (int k=0; k < NUMPIXELS; k++) { // for each pixel
byte Value[PIXELSIZE];
for (byte c=0; c < PIXELSIZE; c++) { // ... for each color
Value[c] = StepColor(c,-k*Pixels[c].TubePhase); // figure new PWM value
// Value[c] = (c == RED && Value[c] == 0) ? Pixels[c].MaxPWM : Value[c]; // flash highlight for tracking
}
uint32_t UniColor = strip.Color(Value[RED],Value[GREEN],Value[BLUE],Value[WHITE]);
strip.setPixelColor(k,UniColor);
}
}
strip.show();
MillisThen = MillisNow;
digitalWrite(PIN_HEARTBEAT,LOW);
}
}