RGB LED Matrix PWM - brightness timing

I have finally hooked up my RGB LED Matrix to my Arduino using 4 shift registers. I’m using 32 levels PWM and update the matrix from a TimerOne interrupt.

Now I’m in the process of improving the interrupt method to be as fast and accurate as possible.

There’s two problems I’m running into that I could use some suggestions for.

  1. the updating of column information is probably optimized to not do anything when a subpixel is off.

In my code I’m using lines like this:

    colROn |= (*(pixel++) > pwmCounter) ? _BV(0) : 0;
    colGOn |= (*(pixel++) > pwmCounter) ? _BV(0) : 0;
    colBOn |= (*(pixel++) > pwmCounter) ? _BV(0) : 0;

This is probably optimized to work like:

if(*(pixel++) > pwmCounter) { colROn = _BV(0); }
if(*(pixel++) > pwmCounter) { colGOn = _BV(0); }
if(*(pixel++) > pwmCounter) { colBOn = _BV(0); }

So when a subpixel is off, there are less instructions executed which results in a different brightness.

How can I solve this?

  1. The last row has different brightness.

After all the rows have been displayed I can either clear the last line or leave it on.
When I leave it on, it’s a little bit brighter than the others.
When I turn it off, it’s a little dimmer than the others.

Here I’m again looking for a way to display the last line equally long.

If you have any other suggestions to improve the speed or accuracy of the iDrawFrame method, they’re most welcome.

#include <TimerOne.h>

#include "Shifting.h"

#define DATA_PIN 7
#define LATCH_PIN 6
#define CLOCK_PIN 5

#define ROW_ON HIGH
#define ROW_OFF LOW

#define COL_ON LOW
#define COL_OFF HIGH

#define PWM_LEVELS 32

int timerDelay = 280;
byte pwmCounter = 0;

uint8_t rows[8*8*3];

void iDrawFrame() {
  byte r;
  uint8_t* pixel = rows;
  uint8_t rowOn = 1;
  uint8_t colROn = 0;
  uint8_t colGOn = 0;
  uint8_t colBOn = 0;
  
  for(r=8; r!=0; --r) {
    colROn = 0; colGOn = 0; colBOn = 0;
    
    colROn |= (*(pixel++) > pwmCounter) ? _BV(0) : 0;
    colGOn |= (*(pixel++) > pwmCounter) ? _BV(0) : 0;
    colBOn |= (*(pixel++) > pwmCounter) ? _BV(0) : 0;
    
    colROn |= (*(pixel++) > pwmCounter) ? _BV(1) : 0;
    colGOn |= (*(pixel++) > pwmCounter) ? _BV(1) : 0;
    colBOn |= (*(pixel++) > pwmCounter) ? _BV(1) : 0;
    
    colROn |= (*(pixel++) > pwmCounter) ? _BV(2) : 0;
    colGOn |= (*(pixel++) > pwmCounter) ? _BV(2) : 0;
    colBOn |= (*(pixel++) > pwmCounter) ? _BV(2) : 0;
    
    colROn |= (*(pixel++) > pwmCounter) ? _BV(3) : 0;
    colGOn |= (*(pixel++) > pwmCounter) ? _BV(3) : 0;
    colBOn |= (*(pixel++) > pwmCounter) ? _BV(3) : 0;
    
    colROn |= (*(pixel++) > pwmCounter) ? _BV(4) : 0;
    colGOn |= (*(pixel++) > pwmCounter) ? _BV(4) : 0;
    colBOn |= (*(pixel++) > pwmCounter) ? _BV(4) : 0;
    
    colROn |= (*(pixel++) > pwmCounter) ? _BV(5) : 0;
    colGOn |= (*(pixel++) > pwmCounter) ? _BV(5) : 0;
    colBOn |= (*(pixel++) > pwmCounter) ? _BV(5) : 0;
    
    colROn |= (*(pixel++) > pwmCounter) ? _BV(6) : 0;
    colGOn |= (*(pixel++) > pwmCounter) ? _BV(6) : 0;
    colBOn |= (*(pixel++) > pwmCounter) ? _BV(6) : 0;
    
    colROn |= (*(pixel++) > pwmCounter) ? _BV(7) : 0;
    colGOn |= (*(pixel++) > pwmCounter) ? _BV(7) : 0;
    colBOn |= (*(pixel++) > pwmCounter) ? _BV(7) : 0;

    shiftOutByte(DATA_PIN, CLOCK_PIN, MSBFIRST, ~colROn); // Red
    shiftOutByte(DATA_PIN, CLOCK_PIN, MSBFIRST, ~colBOn); // Blue
    shiftOutByte(DATA_PIN, CLOCK_PIN, LSBFIRST, ~colGOn); // Green
    shiftOutByte(DATA_PIN, CLOCK_PIN, MSBFIRST,   rowOn); // Row (ground)
    
    bitOut(LATCH_PIN, LOW);
    bitOut(LATCH_PIN, HIGH);
    
    rowOn <<= 1;
  }
  
  // Disable this, and the last row is a little brighter because it gets all the time until refresh to display
  // Enable it, and it'll be a little dimmer, because it didn't really take long enough to display :-(
  // TODO: find a way to display the last row equally long
//  shiftOutByte(DATA_PIN, CLOCK_PIN, MSBFIRST, ~0x00); // Red
//  shiftOutByte(DATA_PIN, CLOCK_PIN, MSBFIRST, ~0x00); // Blue
//  shiftOutByte(DATA_PIN, CLOCK_PIN, LSBFIRST, ~0x00); // Green
//  shiftOutByte(DATA_PIN, CLOCK_PIN, MSBFIRST,  0x00); // Row (ground)
//  bitOut(LATCH_PIN, LOW);
//  bitOut(LATCH_PIN, HIGH);
    
  if(pwmCounter == 0) pwmCounter = PWM_LEVELS;
  else --pwmCounter;
}

void setup() {
  Serial.begin(9600);
  pinMode(DATA_PIN, OUTPUT);
  pinMode(LATCH_PIN, OUTPUT);
  pinMode(CLOCK_PIN, OUTPUT);
  
  Timer1.initialize(timerDelay);
  Timer1.attachInterrupt(iDrawFrame);
}

unsigned long volatile now;
unsigned long volatile nextUpdate = 500;

byte state = 0;
uint8_t R = PWM_LEVELS;
uint8_t G = 0;
uint8_t B = 0;
uint8_t inc = 2;

void loop() {
  now = millis();
  if(now > nextUpdate) {
    nextUpdate = now + 200;
    
    switch(state) {
      case 0:
        G += inc;
        if(G >= PWM_LEVELS) {
          G = PWM_LEVELS;
          state = 1;
        }
        break;
      case 1:
        if(R <= inc) {
          R = 0;
          state = 2;
        } else {
          R -= inc;
        }
        break;
      case 2:
        B += inc;
        if(B >= PWM_LEVELS) {
          B = PWM_LEVELS;
          state = 3;
        }
        break;
      case 3:
        if(G <= inc) {
          G = 0;
          state = 4;
        } else {
          G -= inc;
        }
        break;
      case 4:
        R += inc;
        if(R >= PWM_LEVELS) {
          R = PWM_LEVELS;
          state = 5;
        }
        break;
      case 5:
        if(B <= inc) {
          B = 0;
          state = 0;
        } else {
          B -= inc;
        }
        break;
    }
    
    uint8_t* subpixP =&rows[7*8*3-1];
    uint8_t* subpix = &rows[8*8*3-1];
    for(int r=0; r<7; ++r) {
      for(int c=0; c<8; ++c) {
        *(subpix--) = *(subpixP--);
        *(subpix--) = *(subpixP--);
        *(subpix--) = *(subpixP--);
      }
    }
    
    subpix = rows;
    for(int c=0; c<8; ++c) {
      *(subpix++) = R * abs(8-c) / 8;
      *(subpix++) = G * abs(8-c) / 8;
      *(subpix++) = B * abs(8-c) / 8;
    }
  }
}

Shifting.h

#ifndef SHIFTING_H
#define SHIFTING_H

#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))

#define sbipin(pin) sbi((pin)<8 ? PORTD:PORTB, (pin) - ((pin)<8 ? 0:8))
#define cbipin(pin) cbi((pin)<8 ? PORTD:PORTB, (pin) - ((pin)<8 ? 0:8))

#define bitOut(pin, val) { \
  if ((val) == LOW) cbipin(pin); \
  else              sbipin(pin); \
}

#define shiftOutBit(dataPin, clockPin, val, bit) { \
  bitOut(dataPin, ((val) & (1 << (bit))) ? HIGH:LOW); \
  bitOut(clockPin, HIGH); \
  bitOut(clockPin, LOW); \
}

#define shiftOutByte(dataPin, clockPin, bitOrder, val) { \
  shiftOutBit(dataPin, clockPin, val, (bitOrder) == LSBFIRST ?0:7); \
  shiftOutBit(dataPin, clockPin, val, (bitOrder) == LSBFIRST ?1:6); \
  shiftOutBit(dataPin, clockPin, val, (bitOrder) == LSBFIRST ?2:5); \
  shiftOutBit(dataPin, clockPin, val, (bitOrder) == LSBFIRST ?3:4); \
  shiftOutBit(dataPin, clockPin, val, (bitOrder) == LSBFIRST ?4:3); \
  shiftOutBit(dataPin, clockPin, val, (bitOrder) == LSBFIRST ?5:2); \
  shiftOutBit(dataPin, clockPin, val, (bitOrder) == LSBFIRST ?6:1); \
  shiftOutBit(dataPin, clockPin, val, (bitOrder) == LSBFIRST ?7:0); \
}

#endif