Help with PROGMEM on 2D arrays

Beginner programmer here, slowly figuring stuff out but memory management has me stumped..

I have a WS2812B which I have folded so that it essentially represents a 24x12 matrix, very similar to Lazy Grid Clock V2 : 6 Steps (with Pictures) - Instructables

I have written a clock sketch which will reads the time from a DS3232 RTC, then display numbers on the LED matrix. The digit to be displayed corresponds to a line of a 2D array; in this line I store the index numbers of the LED's to light up to display that particular character in a self-designed font.

Finally these index numbers are translated to the actual LEDs to light up, taking into account the x,y position of the digit to be displayed (x 1>24, y 1>12) and then figuring out which position along the folded LED strip this pixel corresponds to. SO far, so good - works great!

However I'd like to be able to store multiple fonts, but am already using 83% of my nano's memory. I can figure out how to store the 'digits' array in flash using PROGMEM but can't figure out how to read it back.

So using PROGMEM my array looks like this:

const uint8_t digits[10][35] = {

  { 14, 26, 38, 50, 3, 15, 51, 63, 4, 40, 64, 5, 29, 41, 65, 6, 42, 66, 7, 31, 67, 8, 32, 44, 68, 9, 33, 69, 10, 70, 23, 35, 47, 59 },

  { 26, 38, 50, 15, 51, 4, 52, 5, 17, 29, 53, 18, 54, 19, 55, 20, 56, 9, 21, 57, 69, 10, 70, 11, 23, 35, 47, 59, 71 },

  { 14, 26, 38, 50, 3, 63, 4, 16, 28, 64, 29, 65, 18, 54, 7, 43, 8, 32, 44, 56, 68, 9, 69, 10, 70, 11, 23, 35, 47, 59 },

  { 14, 26, 38, 50, 3, 63, 4, 16, 28, 64, 29, 65, 18, 54, 19, 55, 8, 20, 32, 68, 9, 69, 10, 70, 23, 35, 47, 59 },

  { 26, 38, 50, 27, 51, 16, 52, 17, 53, 6, 42, 66, 7, 67, 8, 20, 32, 44, 68, 45, 69, 46, 70, 47, 59, 71 },

  { 2, 14, 26, 38, 50, 62, 3, 63, 4, 64, 5, 29, 41, 53, 6, 42, 19, 55, 32, 68, 9, 21, 33, 69, 10, 70, 23, 35, 47, 59 },

  { 26, 38, 50, 62, 15, 63, 16, 52, 64, 5, 41, 6, 42, 54, 7, 67, 8, 32, 68, 9, 33, 45, 69, 10, 70, 23, 35, 47, 59 },

  { 2, 14, 26, 38, 50, 62, 3, 63, 4, 16, 28, 40, 64, 41, 65, 42, 66, 31, 67, 32, 56, 21, 57, 22, 46, 23, 35, 47 },

  { 14, 26, 38, 50, 3, 63, 4, 28, 40, 64, 5, 41, 65, 18, 54, 7, 67, 8, 32, 44, 68, 9, 45, 69, 10, 70, 23, 35, 47, 59 },

  { 14, 26, 38, 50, 3, 63, 4, 28, 40, 64, 5, 41, 65, 6, 66, 19, 31, 67, 32, 68, 9, 21, 57, 10, 58, 11, 23, 35, 47 }

};

Not every line of the array uses the same amount of pixels, I understand the compiler just zero-fills the remaining index entries.

How do I now read back a particular position from this array? Normally I would just use:

uint8_t pixelnumber = digits[number][i];

'number' refers to the line in the array I want it to read. The resulting LED values are fed to the routine which figures out which LED to light up, and I just loop i from 0 to 34 to complete the entire figure.

But this doesnt work when the array is stored in flash. All I can find is how to read back position number i in a one-dimendional array, but how to do in in 2D? Is there a corresponding 2D lookup method?

Full sketch below in case it's needed to understand.


#include <FastLED.h>

#include <DS3232RTC.h>

#define NUM_LEDS 288

#define DATA_PIN 6

#define BRIGHTNESS 50

CRGB leds[NUM_LEDS];

#define columns 24

#define rows 12

DS3232RTC myRTC(false);

tmElements_t tm;

uint8_t col;

uint8_t row;

int r;

uint8_t number = 0;

uint8_t xpos = 1;  //x=column

uint8_t ypos = 1;  //y=row

uint8_t hr10;

uint8_t hr1;

uint8_t min10;

uint8_t min1;

const uint8_t digits[10][35] = {

  { 14, 26, 38, 50, 3, 15, 51, 63, 4, 40, 64, 5, 29, 41, 65, 6, 42, 66, 7, 31, 67, 8, 32, 44, 68, 9, 33, 69, 10, 70, 23, 35, 47, 59 },

  { 26, 38, 50, 15, 51, 4, 52, 5, 17, 29, 53, 18, 54, 19, 55, 20, 56, 9, 21, 57, 69, 10, 70, 11, 23, 35, 47, 59, 71 },

  { 14, 26, 38, 50, 3, 63, 4, 16, 28, 64, 29, 65, 18, 54, 7, 43, 8, 32, 44, 56, 68, 9, 69, 10, 70, 11, 23, 35, 47, 59 },

  { 14, 26, 38, 50, 3, 63, 4, 16, 28, 64, 29, 65, 18, 54, 19, 55, 8, 20, 32, 68, 9, 69, 10, 70, 23, 35, 47, 59 },

  { 26, 38, 50, 27, 51, 16, 52, 17, 53, 6, 42, 66, 7, 67, 8, 20, 32, 44, 68, 45, 69, 46, 70, 47, 59, 71 },

  { 2, 14, 26, 38, 50, 62, 3, 63, 4, 64, 5, 29, 41, 53, 6, 42, 19, 55, 32, 68, 9, 21, 33, 69, 10, 70, 23, 35, 47, 59 },

  { 26, 38, 50, 62, 15, 63, 16, 52, 64, 5, 41, 6, 42, 54, 7, 67, 8, 32, 68, 9, 33, 45, 69, 10, 70, 23, 35, 47, 59 },

  { 2, 14, 26, 38, 50, 62, 3, 63, 4, 16, 28, 40, 64, 41, 65, 42, 66, 31, 67, 32, 56, 21, 57, 22, 46, 23, 35, 47 },

  { 14, 26, 38, 50, 3, 63, 4, 28, 40, 64, 5, 41, 65, 18, 54, 7, 67, 8, 32, 44, 68, 9, 45, 69, 10, 70, 23, 35, 47, 59 },

  { 14, 26, 38, 50, 3, 63, 4, 28, 40, 64, 5, 41, 65, 6, 66, 19, 31, 67, 32, 68, 9, 21, 57, 10, 58, 11, 23, 35, 47 }

};

void setup() {

  Serial.begin(9600);

  FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);

  FastLED.setBrightness(BRIGHTNESS);

  FastLED.clear();

  delay(500);

}

void loop() {

  delay(100);

  FastLED.show();

  FastLED.clear();

  RTC.read(tm);

  displaytime();

  flashborder();

}

void displaytime() {

  hr10 = tm.Hour / 10;

  hr1 = tm.Hour % 10;

  min10 = tm.Minute / 10;

  min1 = tm.Minute % 10;

  Displaynumber(hr10, 1, 1);

  Displaynumber(hr1, 7, 1);

  Displaynumber(min10, 13, 1);

  Displaynumber(min1, 19, 1);

  if (xpos >= columns) xpos = 0;

  if (ypos > rows) ypos = 0;

}

void Displaynumber(uint8_t number, uint8_t xpos, uint8_t ypos) {

  for (uint8_t i = 0; i < 35; i++) {

    uint8_t pixelnumber = digits[number][i];

    if (pixelnumber == 0) return 32767;

    uint8_t x = (pixelnumber / 12) + xpos;

    uint8_t y = ((pixelnumber - 1) % 12) + ypos;

    if (x > columns) x = x - columns;

    if (y > rows) y = y - rows;

    r = { calcLEDposition(x, y) };

    leds[r] += CHSV(number * 25 + xpos * 5, 255, 255);

  }

}

void flashborder() {

  if (tm.Second % 2 == 0) {

    leds[276] = CRGB::White;

    leds[287] = CRGB::White;

    leds[11] = CRGB::White;

    leds[0] = CRGB::White;

  }

}

int calcLEDposition(uint8_t col, uint8_t row) {  //Calculate the position of the LED to light up. column & row 0 are not displayed, 'overscan'areas! This is used to hide away artefacts at position 0,0 which I got otherwise

  if (row == 0 || col == 0) {

    return 32767;

  }

  if (col % 2 == 0) {

    int r = (300 - ((col * 12) + row));

    return r;

  } else {

    int r = (300 - ((col * 12) + (13 - row)));

    return r;

  }

}

Welcome,

Try pgm_read_byte( &(digits[number][i]) );

And, you could save some progmem space by doing something like this

const uint8_t digit_0[] PROGMEM = { 14, 26, 38, 50, 3, 15, 51, 63, 4, 40, 64, 5, 29, 41, 65, 6, 42, 66, 7, 31, 67, 8, 32, 44, 68, 9, 33, 69, 10, 70, 23, 35, 47, 59 };
const uint8_t digit_1[] PROGMEM = { 26, 38, 50, 15, 51, 4, 52, 5, 17, 29, 53, 18, 54, 19, 55, 20, 56, 9, 21, 57, 69, 10, 70, 11, 23, 35, 47, 59, 71 };
...

const uint8_t * const digits[] PROGMEM = { digit_0, digit_1, ... };

But then you will need a way to know the length of each array, so maybe use the first byte of an array as the length, or have another array like

const uint8_t digitsLength[] = { sizeof( digit_0 ), sizeof( digit_1 ), ... };

that you could also put in progmem if you want..

Wow! Didn't think it would be that simple! Thanks very much.

I can't quite figure out what you're doing there ( I mean the syntax) need to think about that some.. Do you happen to know if there is any documentation out there for the pgm_read_byte line? WHat I could find is either too simple or way over my head..

So the second solution saves space in progmem because it stored only the actual digits used, not the zero filled rest of the array?

Not much documentation about progmem

Yes.. Another solution for the length thing, maybe easier, is to add a 0 at the end of each array. But are you sure that pixel 0 is not a valid pixel ?

Last thing, you do if (pixelnumber == 0) return 32767; but the return type of the function is void, you should have a compiler warning about that

Maybe late, but here is a better idea that you may find interesting

In your case, the greatest led number in your array is 71, so you could reduce memory usage to from ~350 bytes to 90 bytes by modifying your array so that instead of storing 35 bytes per digit where each byte is a led to turn on, you store only 9 bytes per digit where each bit is the state of one of the 72 leds (because there are 8 bits in one byte, so 9 bytes = 72 bits :slight_smile: )

There is your array converted to what I described

const uint8_t digits[][9] PROGMEM =
{
	{ 0b11111000, 0b11000111, 0b10000000, 0b10100100, 0b01001011, 0b10010111, 0b00001100, 0b10001000, 0b01111111 },
	{ 0b00110000, 0b10001110, 0b10111110, 0b00100100, 0b01001000, 0b10000000, 0b11111100, 0b00001011, 0b11100000 },
	{ 0b10011000, 0b01001111, 0b10000101, 0b00110100, 0b01001001, 0b10011000, 0b01000100, 0b10001001, 0b01110011 },
	{ 0b00011000, 0b01000111, 0b10011101, 0b00110100, 0b01001001, 0b10000000, 0b11000100, 0b10001000, 0b01110011 },
	{ 0b11000000, 0b00000001, 0b00010011, 0b00001100, 0b01000001, 0b11110100, 0b00111100, 0b00001000, 0b11111100 },
	{ 0b01111100, 0b01000110, 0b10101000, 0b00100100, 0b01001011, 0b10000110, 0b10100100, 0b11001000, 0b01110001 },
	{ 0b11100000, 0b10000111, 0b10000001, 0b00000100, 0b01001011, 0b10100110, 0b01010100, 0b11001000, 0b01111001 },
	{ 0b00011100, 0b01000000, 0b11100001, 0b10010100, 0b01001001, 0b11000111, 0b00000100, 0b11000011, 0b00001111 },
	{ 0b10111000, 0b01000111, 0b10000100, 0b00010100, 0b01001001, 0b10110011, 0b01000100, 0b10001000, 0b01111011 },
	{ 0b01111000, 0b01001110, 0b10101000, 0b10010100, 0b01001001, 0b10000011, 0b00000100, 0b10000110, 0b00011111 }
};

Then you just have to read each bit of these bytes, with bitRead, if it's a 1, turn the corresponding led on

( no I didn't write this array manually ! I used this little program l4n0DI - Online C++ Compiler & Debugging Tool - Ideone.com )

Can you tell how the ledstrip is going ? Where is the first led ? Are those two leds at the top or at the bottom ?

Thanks for the warning. I don't get any compiler error.
Did this because the top left pixel (0,0) was always lit up originally, since everything is zero-indexed. So the workaround I have now - I'm sure it's amateuristic - is to start counting at 1,1 and then anything that gives a zero just feed a number so big it will never display anyway. I think actually this is a remnant of earlier when I was still starting the row & column numbers form zero.

Thanks! For the time being now that I can actually store my fonts in flash I have plenty of memory again so I'll keep it as is for readability purposes (I've had a couple of cases where a typo was messing up the position, which was easy enough to figure out with the numbered list) but certainly if I run into new issues I will keep this in mind, great tip!

So I internally determine the position of the pixels with an x,y coordinate system (x starting at 1 => 24, y 1=> 12). This is translated to the position along the LED strip by the calcLEDposition subroutine.

The physical orientation is with the LED starting at bottom right (24,12), it goes upwards to (24,1), then loops downwards in the next row (from 23,1 to 23,12), left again to (22,12) and then in a zigzag pattern. The strip is held in a 3D printed holder and will take the strip in any orientation, just needs re-writing of the calcLEDposition code if it's changed.

Final complication is that the pixels making up the character to display are lined up differently - here top left is 1,1 (pixel number 1) downwards to 1,12 (corresponding to pixel number 12), then the next row starts at the top again 13,2 = pixel no. 13, etc.

So I read the pixel numbers from the array, these are translated to the x,y coordinates to light up in the displaynumber subroutine:

    uint8_t x = (pixelnumber / 12) + xpos;
    uint8_t y = ((pixelnumber - 1) % 12) + ypos;

And then that is again translated into the actual LED along the strip to light up.

Apologies if I've done a poor job of explaining. This all seemed logical to me at the time! I've tried several things and this actually works quite well for me, and makes it relatively easy to both define the shape of a character and then move it around the grid at will.

Sorry, but I can't figure out how to connect the NeoPixels in a Wokwi simulation. This is still your old sketch: https://wokwi.com/arduino/projects/300804599007150605

Hey, nice! That wokwi simulation works fine - it was displaying the last (minute) digit, except upside down, so I've corrected them and looks good now.