2

I'm using the DuinoWitchery LCD library (https://github.com/duinoWitchery/hd44780) in a PlatformIO Arduino project with CLion.

The following code works if I stick it in main.cpp:

// near top of class...
const PROGMEM uint8_t decimalDigit[10][8] = {
        {0x07, 0x05, 0x05, 0x05, 0x07, 0x18, 0x18}, // .0
        {0x02, 0x02, 0x02, 0x02, 0x02, 0x18, 0x18}, // .1
        {0x07, 0x01, 0x07, 0x04, 0x07, 0x18, 0x18}, // .2
        {0x07, 0x01, 0x07, 0x01, 0x07, 0x18, 0x18}, // .3
        {0x5,  0x05, 0x07, 0x01, 0x01, 0x18, 0x18}, // .4
        {0x07, 0x04, 0x06, 0x01, 0x06, 0x00, 0x18, 0x18}, // .5
        {4,    4,    7,    5,    7,    0,    0x18, 0x18}, // .6
        {7,    1,    1,    1,    1,    0,    0x18, 0x18}, // .7
        {7,    5,    7,    5,    7,    0,    0x18, 0x18}, // .8
        {7,    5,    7,    1,    1,    0,    0x18, 0x18}, // .9
};

hd44780_I2Cexp lcd(0x27);

// ... snip ...

void setup() {
    for (int x=0; x<8; x++)
       lcd.createChar((uint8_t)x, decimalDigit[x]);
    // ...snip...
}

... but if I move the code into another class, like:

Renderer.h

class Renderer : BaseRenderer {
public:
    virtual void renderTo(hd44780* lcd) override;
    // ... snip ...
private:
const PROGMEM uint8_t decimalDigit[10][8] = {
        {0x07, 0x05, 0x05, 0x05, 0x07, 0x18, 0x18}, // .0
        {0x02, 0x02, 0x02, 0x02, 0x02, 0x18, 0x18}, // .1
        {0x07, 0x01, 0x07, 0x04, 0x07, 0x18, 0x18}, // .2
        {0x07, 0x01, 0x07, 0x01, 0x07, 0x18, 0x18}, // .3
        {0x5,  0x05, 0x07, 0x01, 0x01, 0x18, 0x18}, // .4
        {0x07, 0x04, 0x06, 0x01, 0x06, 0x00, 0x18, 0x18}, // .5
        {4,    4,    7,    5,    7,    0,    0x18, 0x18}, // .6
        {7,    1,    1,    1,    1,    0,    0x18, 0x18}, // .7
        {7,    5,    7,    5,    7,    0,    0x18, 0x18}, // .8
        {7,    5,    7,    1,    1,    0,    0x18, 0x18}, // .9
    };
}

Renderer.cpp

void Renderer::renderTo(hd44780lcd* lcd) {
    for (int x=0; x<8; x++)
       lcd->createChar((uint8_t)x, decimalDigit[x]);
    // ...snip...
}

... the custom characters end up with garbage data.

I think I might be breaking some esoteric rule that governs where you're allowed to put a const PROGMEM uint8_t[], and causing the array declared in Renderer.h to end up in SRAM. But I'm not sure how to confirm it OR fix it.


note: the for/next loop to create the chars is just for the sake of illustration & to demonstrate that something that works in main.setup() fails in Renderer.renderTo(). In real life, the sequence of characters getting rendered to the LCD owns one of the 8 custom characters & redefines it as necessary to be the decimal point and tenths value.

7
  • Does it help if you make the data member static const? Commented Jul 16, 2023 at 19:23
  • 1
    I'm pretty sure the first works only because it's in the RAM anyway because createChar doesn't support PROGMEM.
    – KIIV
    Commented Jul 16, 2023 at 21:07
  • 1
    I think the DuinoWitchery LCD library specifically does. I literally copied the code that worked in main.setup() from the library's example code .
    – Bitbang3r
    Commented Jul 16, 2023 at 21:54
  • 1
    Confirmation: hd44780::createChar(uint8_t location, const uint8_t* charmap) ABSOLUTELY treats charmap as a pointer to PROGMEM. I took advantage of CLion's ability to find the source for a method, and edited it to stick a bunch of Serial.print() statements in createChar to see what's happening. The values it's fetching via pgm_read_byte() are definitely NOT the values in my array. So, it looks like my theory that decimalDigit[10][8] is ending up in SRAM is probably correct. I still have no idea why, or how to fix it, though :-(
    – Bitbang3r
    Commented Jul 16, 2023 at 23:15
  • I also just verified that the decimalDigit[][] array in Renderer.h is DEFINITELY ending up in SRAM (by temporarily hacking createChar() to forcibly treat it like a pointer to SRAM instead of a pointer to flash). So... NOW, the big question is, what do I have to do to get Renderer.h's array to end up in PROGMEM instead of SRAM?
    – Bitbang3r
    Commented Jul 16, 2023 at 23:27

2 Answers 2

4

The PROGMEM qualifier can only be applied to statically-allocated constant data. When you move the decimalDigit array inside the class, it becomes a data member, i.e. you have one copy of the array per instance... in RAM. In this case, PROGMEM does not work. My compiler says:

warning: ‘__progmem__’ attribute ignored [-Wattributes]

If you want to keep the array inside the class, it must be given the static qualifier. It then becomes statically-allocated class data (not instance data):

// Renderer.h:
class Renderer : BaseRenderer {
public:
    virtual void renderTo(hd44780* lcd) override;
    // ... snip ...
private:
    static const PROGMEM uint8_t decimalDigit[10][8];
};

// Renderer.cpp:
const PROGMEM uint8_t Renderer::decimalDigit[10][8] = {
    {0x07, 0x05, 0x05, 0x05, 0x07, 0x18, 0x18}, // .0
    {0x02, 0x02, 0x02, 0x02, 0x02, 0x18, 0x18}, // .1
    // ... snip ...
};

Of course, putting the array within the class is not an obligation. If it is going to be private anyway, the outside world (the files that include Renderer.h) does not need to know about its existence.

1

OK, I ended up blindly stumbling over the solution, even though at this moment I have no idea why it actually works.

I moved the const PROGMEM uint8_t decimalDigit[10][8] = {...} declaration from Renderer.h to the almost-top of Renderer.cpp, so it sits between #include "Renderer.h" and constructor:

#include "Renderer.h"

const PROGMEM uint8_t decimalDigit[10][8] = {
        {0x07, 0x05, 0x05, 0x05, 0x07, 0x18, 0x18}, // .0
        {0x02, 0x02, 0x02, 0x02, 0x02, 0x18, 0x18}, // .1
        {0x07, 0x01, 0x07, 0x04, 0x07, 0x18, 0x18}, // .2
        {0x07, 0x01, 0x07, 0x01, 0x07, 0x18, 0x18}, // .3
        {0x5,  0x05, 0x07, 0x01, 0x01, 0x18, 0x18}, // .4
        {0x07, 0x04, 0x06, 0x01, 0x06, 0x00, 0x18, 0x18}, // .5
        {4,    4,    7,    5,    7,    0,    0x18, 0x18}, // .6
        {7,    1,    1,    1,    1,    0,    0x18, 0x18}, // .7
        {7,    5,    7,    5,    7,    0,    0x18, 0x18}, // .8
        {7,    5,    7,    1,    1,    0,    0x18, 0x18}, // .9
};

Renderer::Renderer(int col, int row, int customCharNumber) : BaseRenderer(col, row) {
    assignCustomChar(customCharNumber);
}

For now, I just have to chalk it up as 'black magic', because some subsequent experiments have forced me to question and doubt everything I ever thought I understood about how #include directives work.

2
  • 1
    That's not black magic. You just took it out of the class definition. If the class needs it as a member, then pass in a pointer or reference. As it is written now, the decimalDigit array is no longer a part of the class. It is existing at global scope.
    – Delta_G
    Commented Jul 17, 2023 at 3:15
  • 1
    It's nothing to do with black magic or #includes. An array of constants is, by definition, not going to change. Therefore there is no point in including it inside a class, unless marked as static, for scoping purposes.
    – Nick Gammon
    Commented Jul 17, 2023 at 10:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.