Do you know Arduino? – A PROGMEM Practical Example

A PROGMEM Practical Example

In previous article we've learnt all about the PROGMEM with simple code snippets. In this article, we will take a look at an LCD library for Arduino that I developed before and see how we could apply what we've learnt on PROGMEM to reduce the usage of SRAM.

The Simple LCD5110/PCD8544 Arduino Library is discussed in How to create Arduino library from Arduino sketch. I would suggest you read that Article first to get familiar about what we are going to talk about here.

The library that we are going to use can be download here, unzipped the file and add it to your Arduino Libraries folder on your computer.

The LCD library works as expected but when you compiling the example code that come with the library on Arduino IDE, you will noticed that the sketch uses over 1000 bytes of Arduino dynamic memory (SRAM), that's 50% of the total available memory of an Arduino. Let's start from the example code of using the library.

LCD5110_demo.pde

#include <LCD5110.h>

/* See README on https://github.com/e-tinkers/LCD-5110-Arduino-library for API documentation*/

#define DC 8           // pin use for LCD DC (Data/Command mode) control
#define BACKLIGHT 7    // pin use for control backlight

// Use https://www.e-tinkers.com/nokia5110-lcd-image-creator/ to create your own logo
const char eTinkersLogo[504] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 
  ...
  0x0f, 0x0f, 0x07, 0x01, 0x00
};

LCD5110 lcd;

void setup() {
  lcd.begin((DC, BACKLIGHT);
}

void loop() {
  lcd.cursor(2, 2);
  lcd.printStr("Hello World!!");
  lcd.cursor(4, 2);
  lcd.printStr("e-tinkers.com");
  lcd.inverse(ON);
  lcd.cursor(6,1);
  lcd.printStr("** Nov 2017 **");
  lcd.inverse(OFF);
  delay(5000);

  lcd.backlight(ON);
  lcd.printImage(eTinkersLogo);
  delay(5000);
  lcd.backlight(OFF);
  lcd.clear();
}

For the LCD5110, the screen size consists of 84x48 pixels, if you want to display a logo image, the logo image will require (84x48)/8=504 bytes of data to represent all the pixels you see on the screen. Obviously we should keep the eTinkersLogo[] array in the program memory by adding the PROGMEMattribute to it.

LCD5110_demo.pde

// const char eTinkersLogo[504] = {
const char eTinkersLogo[504] PROGMEM = {

Of course we also need to modify the function that using the array to be able to read the data out from program memory.

LCD5110.cpp
printImage() method in LCD5110.cpp

void LCD5110::printImage(const char *image) {
  cursor(1,1);
  for (int i = 0; i < (LCD_WIDTH * LCD_HEIGHT / 8); i++) {
    //_write(DATA, image[i]);
    _write(DATA, pgm_read_byte(image + i));
  }
}

The printImage() is PROGMEM-aware function, that means it is expecting the pointer to be passed in as the function argument is pointing to the program memory, if user pass in a pointer that point to data memory in SRAM, it will not be able to display correctly. In the way, we sort of make it mandatory that if you want to use the printImage(), the image data must be resided in program memory.

Within the loop(), there are several string literals to be passed into printStr() method so that it can be displayed on the LCD. Those string literals will be loaded into SRAM during execution and ideally those should be kept in the program memory. However, unlike the logo image which took up of 504 bytes, string to be print on the LCD is likely not very long, and therefore we want to give the user the choice for using plain string literal or PROGMEM string literal (i.e. wrapped with F() macro, or a PROGMEM variable. We already have the printStr() method that accepts a const char* str as the argument for handling normal string literal. We can use C++ class function overload to create a method with the same name but with different function prototype for handling the PROGMEM string literal and PROGMEM variable.

LCD5110.cpp

void LCD5110::printStr(const char *str) { 
  static const char FONT_TABLE [][5] = {
    { 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x20, space
    { 0x00, 0x00, 0x5f, 0x00, 0x00 }, // 0x21, !
    ...
    { 0x10, 0x08, 0x08, 0x10, 0x08 }, // 0x7e, ~
    { 0x78, 0x46, 0x41, 0x46, 0x78 }  // 0x7f, DEL
  };
  int p = 0;
  while (str[p]!='\0') {
    if ( (str[p] >= 0x20) & (str[p] <= 0x7f) ) {
      for (int i = 0; i < 5; i++) {
        _write(DATA, FONT_TABLE[str[p] - 32][i]);
      }
      _write(DATA, 0x00);
    }
    p++;
  }
}

void LCD5110::printStr(const __FlashStringHelper *strLiteral) {
  PGM_P p = reinterpret_cast(strLiteral);

  char *ptr = (char *)malloc(strlen_P(p)+1);
  if (ptr != NULL) {  // if memory allocation successful
    strcpy_P(ptr, p);
    printStr(ptr);    // function overload
    free(ptr);
  }
}

If the user calling the method with a plain string literal (it will be loaded into SRAM, but this is user's choice), the original method printStr(const char* str) will response to the calling. But if user is using a PROGMEM variable or string literal, the newly created printStr(const __FlashStringHelper *strLiterail) method will be executed.

This new method cast the pass-in __FLashStringHelper class string literal to a PROGMEM variable, and copy it into a dynamic allocated SRAM memory before passing it to our original printStr()method to print it on the LCD.

With this approach, we provide user multiple ways of using the printStr() method.

LCD5110_demo.pde

void loop() {
  static const website[] PROGMEM = "e-tinkers.com";

  lcd.cursor(2, 2);
  lcd.printStr("Hello World!");  // passing string literal. This take up SRAM space
  lcd.cursor(4, 2);
  lcd.printStr((__FlashStringHelper *) website);  // passing PROGMEM variable
  lcd.inverse(ON);
  lcd.cursor(6,1);
  lcd.printStr(F("** Nov 2017 **"));  // keep string literal in program space
  lcd.inverse(OFF);
  delay(5000);
  ...
}

Since we added a new overloading function in the LCD5110 class, we need to add the function prototype into the LCD5110.h header file.

LCD5110.h

class LCD5110
{
  public:
    LCD5110();
    void begin(const uint8_t dc, const uint8_t led);
    void clear(void);
    void cursor(uint8_t row, uint8_t col);
    void backlight(const uint8_t state);
    void inverse(const uint8_t inv);
    void printStr(const char *str);
    void printStr(const __FlashStringHelper *strLiteral);
    void printImage(const char *image);
  private:
    void _write(const uint8_t mode, char data);
    uint8_t _inverse = OFF;
    uint8_t _DC;
    uint8_t _BACKLIGHT;
};

LCD display library is a typical application where a large amount of data will be loaded from program memory into SRAM because of the font table which is required to generate the fonts used for the display. A typical font table will consists about 96 displayable font characters and each font is represented by 5 bytes data, so that will take up 480 bytes of memory.

LCD5110.cpp

static const char FONT_TABLE [][5] = {
  { 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x20, space
  { 0x00, 0x00, 0x5f, 0x00, 0x00 }, // 0x21, !
  ...
  { 0x10, 0x08, 0x08, 0x10, 0x08 }, // 0x7e, ~
  { 0x78, 0x46, 0x41, 0x46, 0x78 }  // 0x7f, DEL
};

We should definitely keep the FONT_TABLE in the program memory as what we did to the image data.

LCD5110.cpp

// static const char FONT_TABLE [][5] = {
static const char FONT_TABLE [][5] PROGMEM = {
memory usage before and after applying PROGMEM
Memory usage before and after applying PROGMEM

With all those changes, if you re-compiled the code in Arduino IDE, we have reduce the usage of SRAM from 1041 bytes to only 37 bytes on an Arduino Nano! It uses 37 bytes because the "Hello World!!" still take up 14 bytes of SRAM, if you wrapped the string literal with F() macro as F("Hello World!!")), memory usage would further reduced to only 23 bytes! Try it yourself...

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.