LED matrix flickering

Good evening gentlemen,

I have a 24x7 LED matrix, with rows sourcing current from a MIC5891 and columns sinking it (through 470 ohm resistors) into 3 HC595's. The circuit is operating at 5V and I am using a Wemos D1 mini (3.3V logic), so all outputs are going through a TXS0108E logic level converter.

The issue I am having is that the rows' brightness is not consistent, which causes a kind of flickering effect. LED's of the same row always seem to have the same brightness, but this is not true across different rows. It's more noticeable when displaying a still image instead of scrolling text. I've attached a short video - look at the reflections and you'll see what I mean.

Can I fix this somehow or is it just a consequence of the whole persistence of vision trick?

Thank you in advance

Here is my code:

#include "msg.h"

typedef uint8_t Pin;
#define PIN_NONE 255

#define ANALOG_OUT_MAX 1023 // for Wemos D1 mini

#define P_ROW_ENABLE D0
#define P_ROW_DATA D7
#define P_ROW_CLOCK D5
#define P_ROW_LATCH D6
#define P_COL_DATA D4
#define P_COL_CLOCK D3
#define P_COL_LATCH D2

bool msg_get(uint8_t row, uint16_t col){
  if(col >= COLS_CNT) col -= COLS_CNT; // wraparound
  return msg[row * COLS_CNT + col];
}

bool bit_get(uint8_t x, uint8_t n){return (x >> n) & 1;}

struct ShiftReg{
  Pin p_data,p_clock,p_latch,p_enable;
  bool invert;
  
  ShiftReg(Pin data, Pin latch, Pin clock, Pin enable = PIN_NONE, bool _invert = false){
    p_data = data; p_clock = clock; p_latch = latch; p_enable = enable; invert = _invert;
    pinMode(p_data, OUTPUT); pinMode(p_clock, OUTPUT); pinMode(p_latch, OUTPUT);
    if(p_enable != PIN_NONE) pinMode(p_enable, OUTPUT);
  }
  
  void write(bool val){ // // write bit to ic mem
    digitalWrite(p_clock, LOW);
    digitalWrite(p_data, invert ? (val ? LOW : HIGH) : (val ? HIGH : LOW));
    digitalWrite(p_clock, HIGH);
  }

  void write(uint8_t val){ // write byte to ic mem
    for(uint8_t n=7;; n--){
      this->write(bit_get(val, n));
      if(n==0) break;
    }
  }
  
  void latch(){ // apply ic mem to output pins
    digitalWrite(p_latch, LOW);
    digitalWrite(p_latch, HIGH);
  }
  
  void duty(float val){ // set duty cycle; 1.0 = 100%
    if(p_enable == PIN_NONE) return;
    // ic uses negative logic (i.e. when OE is LOW, output is enabled)
    analogWrite(p_enable, round((1.0 - val) * ANALOG_OUT_MAX));
  }
};


ShiftReg rows(P_ROW_DATA, P_ROW_LATCH, P_ROW_CLOCK, P_ROW_ENABLE);
ShiftReg cols(P_COL_DATA, P_COL_LATCH, P_COL_CLOCK, PIN_NONE, true);

#define COL_INTERVAL 75 // ms; scroll speed
unsigned long col_ts_lst;
uint8_t row_cur = 0;
uint16_t col_cur = 0;

void setup(){
  col_ts_lst = millis();
}

void loop(){
  rows.duty(0.00); // disable while feeding data to prevent ghosting
  
  row_cur = (row_cur == 6) ? 0 : (row_cur+1); // 7 would refer to non-existent 8th row
  rows.write(uint8_t(1 << row_cur));
  rows.latch();

  unsigned long ts_cur = millis();
  if(row_cur == 0  &&  ts_cur - col_ts_lst > COL_INTERVAL){ // allow scroll only when drawing a new frame (row == 0)
      col_cur = (col_cur+1 == COLS_CNT) ? 0 : col_cur+1;
      col_ts_lst = ts_cur;
  }

  for(uint16_t n = 23;; n--){ // rightmost first
    cols.write(msg_get(row_cur, col_cur+n));
    if(n==0) break;
  }
  cols.latch();
  rows.duty(0.15);
  delayMicroseconds(1100);
}

VID_20200709_1564.mpg (1010 KB)

I'd probably have attempted to put the 4 shift registers in the same chain and maybe controlled them via a timer instead of a delay in the loop().
But that may not be your problem.
Important is that all the activities on a row are synchronised. Output on all 4 shift registers is disabled (vie OE etc.), all the data for that row is pumped out and ready, then the outputs are once again enabled. After X us, the whole thing repeats.
You may also have a beating effect with analogWrite() which is 1KHz on an ESP8266 by default and very close to your 1100us timing interval. You can make the frequency very high (around 40KHz) to minimise this effect using analogWriteFreq(new_frequency), or you can control the brightness by running blank cycles instead.

6v6gt:
I'd probably have attempted to put the 4 shift registers in the same chain and maybe controlled them via a timer instead of a delay in the loop().
But that may not be your problem.
Important is that all the activities on a row are synchronised. Output on all 4 shift registers is disabled (vie OE etc.), all the data for that row is pumped out and ready, then the outputs are once again enabled. After X us, the whole thing repeats.
You may also have a beating effect with analogWrite() which is 1KHz on an ESP8266 by default and very close to your 1100us timing interval. You can make the frequency very high (around 40KHz) to minimise this effect using analogWriteFreq(new_frequency), or you can control the brightness by running blank cycles instead.

Thank you, a lot of good info. I should've thought of chaining them all... I adapted the circuit from one that used separate transistors for the rows. Calling analogWriteFreq(40000) didn't help unfortunately, so the issue seems to be using delay for timing. To confirm I tried it without PWM and just set the OE pin to LOW - same result.

What timer would you use, and what frequency?

If you are multiplexing by 7 - as I gather you are - then 1100 µs seems to be similar in scale to the time it takes for the code to loop so it would be sensitive to code execution. Three milliseconds using millis() might be better, giving a cycle rate close to 50 Hz.

And I suspect you are always going to have trouble attempting to multiplex and use analogWrite.

For the timer, you can do something like:

Ticker ticker ;
. . .
setup() {
  . . . 
  ticker.attach_ms( 1, pushOut ) ;   //  1 mS
  . . . 
}

void pushOut() { 
  // handle display
  . . . 
  . . .
}

In the loop, attempt to prepare the bytes (all 4) to be pushed out to the shift register chain(s). In the timer callback, do the pushing out. You’ll have to experiment with the timings.

Tried using Ticker with all values from 1 to 5 ms... still the same issue. I then tried

void states_apply(){
  if(!states_ready) return;
  //rows.duty(0.00); // disable while feeding data to prevent ghosting
  digitalWrite(P_ROW_ENABLE, HIGH);
  rows.write(row_states);
  cols.write(col_states[2]); // rightmost first
  cols.write(col_states[1]);
  cols.write(col_states[0]);
  rows.latch();
  cols.latch();
  //rows.duty(0.15);
  digitalWrite(P_ROW_ENABLE, LOW);
  ts_multiplex_lst = micros();
  states_ready = false; // must generate new states before next call
}

void loop(){
  if(!states_ready) states_gen();
  if(micros() - ts_multiplex_lst >= MULTIPLEX_INTERVAL) states_apply();
}

where states_gen() prepares the 4 bytes as you suggested and MULTIPLEX_INTERVAL is 2100us. The problem is still there but (I think?) less severe.

Any other ideas? I'm no longer using delay() anywhere and the only time LED's are off is in states_apply()

You need to publish all the code then it should be easier to make further suggestions.

main:

//#include <Ticker.h>

#include "pins.h"
#include "msg.h"

typedef uint8_t Pin;
#define PIN_NONE 255

#define ANALOG_OUT_MAX 1023 // for Wemos D1 mini

bool msg_get_state(uint8_t row, uint16_t col){
  if(col >= COLS_CNT) col -= COLS_CNT; // wraparound
  return msg[row * COLS_CNT + col];
}

uint8_t bit_set(uint8_t x, uint8_t n, bool val = true){return val ? (x | (1 << n)) : (x & ~(1 << n));}
bool bit_get(uint8_t x, uint8_t n){return (x >> n) & 1;}

struct ShiftReg{
  Pin p_data,p_clock,p_latch,p_enable;
  bool invert;
  
  ShiftReg(Pin data, Pin latch, Pin clock, Pin enable = PIN_NONE, bool _invert = false){
    p_data = data; p_clock = clock; p_latch = latch; p_enable = enable; invert = _invert;
    pinMode(p_data, OUTPUT); pinMode(p_clock, OUTPUT); pinMode(p_latch, OUTPUT);
    if(p_enable != PIN_NONE) pinMode(p_enable, OUTPUT);
  }
  
  void write(bool val){ // // write bit to ic mem
    digitalWrite(p_clock, LOW);
    digitalWrite(p_data, invert ? (val ? LOW : HIGH) : (val ? HIGH : LOW));
    digitalWrite(p_clock, HIGH);
  }

  void write(uint8_t val){ // write byte to ic mem
    for(uint8_t n=7;; n--){
      this->write(bit_get(val, n));
      if(n==0) break;
    }
  }
  
  void latch(){ // apply ic mem to output pins
    digitalWrite(p_latch, LOW);
    digitalWrite(p_latch, HIGH);
  }
  
  void duty(float val){ // set duty cycle; 1.0 = 100%
    if(p_enable == PIN_NONE) return;
    // ic uses negative logic (i.e. when OE is LOW, output is enabled, so we correct via 1-x)
    analogWrite(p_enable, round((1.0 - val) * ANALOG_OUT_MAX));
  }
};


ShiftReg rows(P_ROW_DATA, P_ROW_LATCH, P_ROW_CLOCK, P_ROW_ENABLE);
ShiftReg cols(P_COL_DATA, P_COL_LATCH, P_COL_CLOCK, PIN_NONE, true);

void test_pattern(){ // to check all the LEDs are working
  rows.duty(0.00);
  cols.write(uint8_t(255));
  cols.write(uint8_t(255));
  cols.write(uint8_t(255));
  cols.latch();
  for(uint8_t row = 0; row < 7; row++){
    rows.write(bit_set(0, row));
    rows.latch();
    rows.duty(0.15);
    delay(1000);
    rows.duty(0.00);
  }
}


#define MULTIPLEX_INTERVAL 2100 // us
#define COL_INTERVAL 120 // scroll speed (frames); (1 frame = 7*MULTIPLEX_INTERVAL us)

unsigned long ts_multiplex_lst;
uint8_t row_cur = 0;
uint16_t col_cur = 0;
uint16_t frame_cnt = 0;
bool states_ready = false;
uint8_t row_states = 0;
uint8_t col_states[3] = {0,0,0};
//Ticker ticker;

void states_gen(){
  row_states = bit_set(0, row_cur);

  for(uint8_t byte_n=0; byte_n<3; byte_n++){
    col_states[byte_n] = 0;
    for(uint8_t bit_n=0; bit_n<8; bit_n++)
      col_states[byte_n] = bit_set(col_states[byte_n], bit_n, msg_get_state(row_cur, col_cur + byte_n*8+bit_n));
  }

  if(++row_cur == 7){ // 7 would refer to non-existent 8th row
    row_cur = 0;
    if(++frame_cnt == COL_INTERVAL){ // new frame will be drawn next and we are ready to scroll
      frame_cnt = 0;
      if(++col_cur == COLS_CNT) col_cur = 0;
    }
  }
  states_ready = true;
}

void states_apply(){
  if(!states_ready) return;
  //rows.duty(0.00); // disable while feeding data to prevent ghosting
  digitalWrite(P_ROW_ENABLE, HIGH);
  rows.write(row_states);
  cols.write(col_states[2]); // rightmost first
  cols.write(col_states[1]);
  cols.write(col_states[0]);
  rows.latch();
  cols.latch();
  //rows.duty(0.15);
  digitalWrite(P_ROW_ENABLE, LOW);
  ts_multiplex_lst = micros();
  states_ready = false; // must generate new states before next call
}

void setup(){
  analogWriteFreq(40000); //40kHz
  test_pattern();
  ts_multiplex_lst = micros();
  //ticker.attach_ms(MULTIPLEX_INTERVAL/1000, states_apply);
}

void loop(){
  if(!states_ready) states_gen();
  if(micros() - ts_multiplex_lst >= MULTIPLEX_INTERVAL) states_apply();
}

msg.h:

//"TO ADMIT DEFEAT IS TO BLASPHEME AGAINST THE EMPEROR --- "
bool msg[] = {
	1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,0,0,0,1,0,0,0,1,0,0,1,1,1,0,0,1,1,1,1,1,0,0,0,0,0,1,1,1,0,0,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,0,1,1,1,0,0,1,1,1,1,1,0,0,0,0,0,0,1,1,1,0,0,0,1,1,1,1,0,0,0,0,0,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,1,1,1,1,0,0,1,0,0,0,0,0,0,1,1,1,0,0,0,1,1,1,1,0,1,1,1,1,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,1,1,1,1,1,0,0,0,0,0,0,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,0,0,0,1,1,1,0,0,1,0,0,0,1,0,0,1,1,1,1,0,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,1,1,1,1,0,0,1,1,1,1,1,0,1,1,1,1,0,0,0,1,1,1,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,1,0,0,1,1,0,1,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,0,0,1,1,0,1,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,0,1,1,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,1,0,0,1,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,1,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,1,1,1,0,0,1,1,1,1,0,0,1,1,1,1,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,1,1,1,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,1,1,1,0,0,1,1,1,1,0,0,1,1,1,1,1,0,1,1,1,1,0,0,1,0,1,0,1,0,1,1,1,1,0,0,0,0,0,0,1,0,0,0,1,0,1,0,1,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,1,0,0,1,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,1,1,1,0,1,1,1,1,0,0,0,0,0,0,1,1,1,1,0,0,1,0,1,0,1,0,1,1,1,1,0,0,1,1,1,1,0,0,1,1,1,1,0,0,1,0,0,0,1,0,1,1,1,1,0,0,0,0,0,0,1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1,1,0,0,0,0,0,
	0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,1,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,1,1,1,1,1,0,0,0,1,0,0,0,1,0,0,1,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,1,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,1,0,0,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
	0,0,1,0,0,0,0,1,1,1,0,0,0,0,0,0,1,0,0,0,1,0,1,1,1,0,0,0,1,0,0,0,1,0,0,1,1,1,0,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,0,0,1,1,1,1,1,0,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,1,1,0,0,1,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,1,1,1,0,0,0,0,0,0,1,1,1,1,0,0,1,1,1,1,1,0,1,0,0,0,1,0,1,1,1,1,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,1,0,1,1,1,1,1,0,0,0,0,0,1,0,0,0,1,0,0,1,1,1,1,0,1,0,0,0,1,0,0,1,1,1,0,0,1,0,0,0,1,0,1,1,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,0,1,1,1,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};

#define COLS_CNT 316

pins.h:

#define P_ROW_ENABLE D0
#define P_ROW_DATA D7
#define P_ROW_CLOCK D5
#define P_ROW_LATCH D6
#define P_COL_DATA D4
#define P_COL_CLOCK D3
#define P_COL_LATCH D2

Can you add the new function test_pattern2() and replace your current loop() as below.
The idea is to display a constant test pattern with rows alternating between nearly full and nearly empty. Hopefully it will be without flicker and the brightness will be consistent.
If there is a problem with consistent brightness, then the power supply is inadequate or the shift registers are being over stressed. If there is flicker, adjust the constant timing value in the loop. Also attempt to find the largest value possible without causing flicker.

It is untested so maybe you have to fix something

void test_pattern2(){ // to check all the LEDs are working
  static uint8_t row = 0 ;   // initialised once !
  
  digitalWrite(P_ROW_ENABLE, HIGH);   // blank all leds
  
  if ( rowIndex % 2 == 0 ) {
      // even rows
      cols.write(uint8_t(0b11100111));
      cols.write(uint8_t(0b11100111));
      cols.write(uint8_t(0b11111111));
  }
  else {
      // odd rows
      cols.write(uint8_t(0b00000111));
      cols.write(uint8_t(0b00000111));
      cols.write(uint8_t(0b00000111));
  }
  cols.latch();
  
  rows.write(bit_set(0, row));   
  rows.latch();
  
  if ( ++row > 6 ) row = 0 ;  // row 0..6
  
  digitalWrite(P_ROW_ENABLE, LOW);  // free display
}


void loop(){
  static uint32_t lastIterUs = 0 ;  // initialised once !
  if ( micros() - lastIterUs > 1000 ) {    // adapt this to stop flicker (microseconds)
     test_pattern2()
     lastIterUs = micros() ;
  }
}

Alright, I tried your test pattern and unfortunately wasn't able to get rid of the flicker, but it did help me narrow it down some more.

I don't think it's the power supply or shift registers. When I disable multiplexing and just display a full row of 24 LEDs, they all light up fine. No flicker, consistent brightness. Even with a very sparse pattern - say, 3 LEDs on even rows, 1 on odd - it flickers.

I thought maybe shifting out data was causing noise, so I added a 1us delay when toggling the display on/off. Nothing.

Experimenting with the refresh frequency didn't improve matters either. I did some searching around and 60 - 100 Hz (~ 1-2ms interval) seems to be ideal.

I also added

if(micros() - ts_multiplex_lst > MULTIPLEX_INTERVAL + 200) Serial.println(micros() - ts_multiplex_lst);

to states_apply() to check how off the timing was in practice. Typical results when MULTIPLEX_INTERVAL is 2200us:

09:51:30.472 -> 3206
09:51:30.646 -> 2471
09:51:30.782 -> 2423
09:51:30.782 -> 2323
09:51:30.953 -> 2493
09:51:31.092 -> 2547
09:51:31.228 -> 2480
09:51:31.398 -> 2608
09:51:31.398 -> 2438
09:51:31.532 -> 2332
09:51:31.705 -> 2668
09:51:31.840 -> 2557
09:51:31.875 -> 2303
09:51:33.228 -> 2308
09:51:33.297 -> 2313
09:51:33.332 -> 2345
09:51:33.436 -> 3699
09:51:33.571 -> 2474
09:51:33.741 -> 2475
09:51:33.877 -> 2480
09:51:34.045 -> 2430
09:51:34.045 -> 2338
09:51:34.349 -> 2511
09:51:34.488 -> 2622
09:51:34.660 -> 2353
09:51:34.660 -> 2328
09:51:34.694 -> 2338
09:51:34.796 -> 2374
09:51:34.966 -> 2331

Those occasional 1ms deviations don't seem like they should matter, right? Anything else I can try?

I'd just toss this whole board and start over if I wasn't still waiting for a new batch of ESP's to ship.

OK. I've rewritten it the test sketch so that it is completely self standing and independent of the functions you have in those structs (classes). It uses shiftOut() instead. If that still flickers, then I fear the problem may be impossible to solve in software.

Again, it is untested, so you may have to tweak it.

#define P_ROW_ENABLE D0
#define P_ROW_DATA D7
#define P_ROW_CLOCK D5
#define P_ROW_LATCH D6
#define P_COL_DATA D4
#define P_COL_CLOCK D3
#define P_COL_LATCH D2


void test_pattern2() { 
  static uint8_t row = 0 ;   // initialised once !

  digitalWrite(P_ROW_ENABLE, HIGH);   // blank all leds
  digitalWrite(P_COL_LATCH, LOW );   // latch

  if ( row % 2 == 0 ) {
    // even rows
    shiftOut( P_COL_DATA, P_COL_CLOCK, MSBFIRST, 0b11100111 );
    shiftOut( P_COL_DATA, P_COL_CLOCK, MSBFIRST, 0b11100111 );
    shiftOut( P_COL_DATA, P_COL_CLOCK, MSBFIRST, 0b11111111 );
  }
  else {
    // odd rows
    shiftOut( P_COL_DATA, P_COL_CLOCK, MSBFIRST, 0b00000111 );
    shiftOut( P_COL_DATA, P_COL_CLOCK, MSBFIRST, 0b00000111 );
    shiftOut( P_COL_DATA, P_COL_CLOCK, MSBFIRST, 0b00000111 );
  }
  digitalWrite(P_COL_LATCH, HIGH );   // latch


  digitalWrite(P_ROW_LATCH, LOW );   // latch
  shiftOut( P_ROW_DATA, P_ROW_CLOCK, MSBFIRST, 1 << row );
  digitalWrite(P_ROW_LATCH, HIGH );   // latch

  if ( ++row > 6 ) row = 0 ;  // row 0..6

  digitalWrite(P_ROW_ENABLE, LOW);  // free display
}

void setup() {
  pinMode(  P_ROW_ENABLE , OUTPUT) ;
  pinMode(  P_ROW_DATA , OUTPUT) ;
  pinMode(  P_ROW_CLOCK , OUTPUT) ;
  pinMode(  P_ROW_LATCH , OUTPUT) ;
  pinMode(  P_COL_DATA , OUTPUT) ;
  pinMode(  P_COL_CLOCK , OUTPUT) ;
  pinMode(  P_COL_LATCH , OUTPUT) ;
}

void loop() {
  static uint32_t lastIterUs = 0 ;  // initialised once !
  if ( micros() - lastIterUs > 1000 ) {    // adapt this to stop flicker (microseconds)
    test_pattern2() ;
    lastIterUs = micros() ;
  }
}

I would have to still go to an adequate power supply. That is use the a 9VDC power supply and connect it to to the power supply plug. I had a similar flickering aspect, as well as wrong number displayed on a three digit multiplex display.

Could easily be explained by a lack of adequate decoupling - LED arrays are relatively high current and
when switched at logic speeds definitely require good decoupling, at least 0.1µF ceramic per chip + 10µF
or more electrolytic close by.

Lack of decoupling will cause the logic chips to glitch as the supply voltage is crow-barred on nanosecond
timescales.

[ BTW. On forums there are many people of all ages from all walks of life all round the world, your
opening remark is ill-considered (I'm biting my tongue here). ]

6v6gt, I tried the independent code and that didn't fix it. In the end I determined that the inconsistent timing I noticed earlier was throwing it off after all. I put an optocoupler between the circuit and my old Uno, sending pulses at 1kHz, and attached an interrupt calling states_apply -> problem solved. Thanks for helping me figure this out! Time to go learn how those 555 timers work.

Mark, my apologies. I'll use "good day ladies and gentlemen" in the future. And thanks for the advice, I had no idea that was even a factor. I'll definitely be adding those caps.

Also, since I'll be starting over anyway, can anyone recommend a replacement for the HC595 that has separate logic/load supplies like the MIC5891?

I would suggest you try to develop a clearer understanding of why it is failing now, because of the risk that you'll experience the same problem later. One unpredictable factor is the ESP8266 because it tends to do a lot of back ground activities which could cause inconsistent timing and power consumption.

You've already said that Ticker did not help. Maybe you could also switch off the Radio part of the ESP8266 by something like described here : https://circuits4you.com/2019/01/08/esp8266-turn-off-wifi-save-power/

Maybe also state what the forward voltage is of those leds and supply a wiring diagram and describe more how you integrated a Uno in all that, apparently successfully.

The tpic6b595 is much stronger that the SN74HC595, but the latter should be adequate for sinking 7 leds especially with a 470R column resistor.

It was so simple all along - WiFi.mode(WIFI_OFF); did the trick just as well. Kind of a shame it messes with the timing so much. Looks like I'll need to use external timers for anything wifi-enabled.

The forward voltage is 3V - 3.2V. I'm no expert on wiring diagrams, but here is what I did: connect the LED side of an OPI7010 optocoupler to an Uno output pin and through a 330 ohm resistor to the Uno's ground. Connect the phototransistor side of the OPI7010 like this: collector to the ESP's 3.3V supply, emitter to an ESP input pin. Now the two circuits are galvanically isoalted, but when the Uno does a digitalWrite(P_LED, HIGH), the LED will draw about 10mA and activate the phototransistor. The ESP will see a HIGH input as long as the Uno is providing a HIGH output.

OK. I'm not sure I completely follow your Uno integration method but, anyway, it is becoming clearer where the problem lies so that should no longer be an issue.

The Built-in led is on D4 (check this for your board) which you are also using as a clock for the column led drivers. I'm now also wondering if this was causing interference if the ESP was also attempting to activate the led.

The radio part may also be less of a problem once it has your WLAN credentials and is connected to a network, then it will not be doing regular scans.

You can also use chips like the MAX7219 to drive a LED matrix panel (you would need 4). These handle the display multiplexing also. You need only tell it what to display and it does the rest. This would isolate the display from any processing activity on the ESP8266, although scrolling would still be dependent on the ESP8266.

6v6gt:
You can also use chips like the MAX7219 to drive a LED matrix panel (you would need 4).

24 by 7 matrix? That would be three MAX7219s.

Really, using the MAX7219s is always going to be far more practical than multiplexing in code, it is properly rated for the purpose - 40 mA drive to the active column so nice bright uniform display with no flicker, only one current-setting resistor per MAX7219 and no other driver components.

It's pretty much a "no-brainer". :grinning:


Recommendation - just buy three or four of these kits:

Or these ones

which used to be more expensive but are now actually cheaper and more useful if you wish to stack matrix arrays.

Except that is that now Covid-19 has completely up-ended the supply chain and these prsently have ridiculous mailing costs. :astonished:

The point is that you do not install the matrix arrays from the kits themselves - or their socket pins, but just solder to the positions on the PCB and you have a durable and reliable assembly to drive your own matrix arrays.

Why did I say three or four? Well, you can fully assemble the first one as the matrix with which it comes and practice programming it. Then the rest for your current project and prior to Covid I always suggested getting more - for the next! :grinning:

I'll make D4 an input in the revised version. That damn blue LED was active while it did the switching and there doesn't seem to be a way to turn it off in software.

One resistor per MAX7219? How can they accomplish such magic? I will have to investigate this.

Recommendation taken, that'll be my next project. Aliexpress has a 4 pack for $3... ludicrous! Perhaps a 32x32 matrix this time...