Inconsistent behaviour with custom chars in LiquidCrystal

Hello everyone. My project is using a lot more than 8 icons. So I thought, I could simply swap custom chars out on the fly in my code, which allows me to use a larger total number of icons than 8. I've come up with this test script, but it's exhibiting a confusing behaviour:

#include <LiquidCrystal.h>
#include "icons.h"

const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 7, d7 = 8;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

int counter = 0;

void setup() {

  lcd.begin(16, 2);

  pinMode(6, OUTPUT); // Turn on LCD backlight
  analogWrite(6, 255);

}

void loop() {

  lcd.setCursor(6, 0);
  lcd.print(counter);
  lcd.setCursor(3, 0);

  //if(counter > 1){
    swapIcon(0);
  //}
  lcd.write(byte(0));
  delay(500);

  swapIcon(1);
  lcd.write(byte(0));
  delay(500);

  counter += 1;

}

void swapIcon(int icon){
  byte icon_buf[8];
  memcpy_P(icon_buf, icon_lookup[icon], 8);
  lcd.createChar(0, icon_buf);
}

Notice the commented if statement in loop(). When this if statement is commented out, no icons are ever rendered to the display while the code is running, even though the counter increments. But when the if statement is uncommented, the code works perfectly, and the icon displayed alternates.

Here is icons.h:

#ifndef ICONS_H
#define ICONS_H

const byte I_BACK[8] PROGMEM = {
	0b00000,
	0b00100,
	0b01000,
	0b11111,
	0b01000,
	0b00100,
	0b00000,
	0b00000
};

const byte I_BACK_INV[8] PROGMEM = {
	0b11111,
	0b11011,
	0b10111,
	0b00000,
	0b10111,
	0b11011,
	0b11111,
	0b11111
};

const uint8_t icon_lookup[2] = {I_BACK, I_BACK_INV};

#endif

Any ideas what's going on here?

What you are storing in this array is the addresses in program memory of the icons. uint8_t won't be adequate to hold those addresses.

Are you aware that the minute you upload a new bitmap for one of those custom char, whatever is on screen with that reference will change to the new bitmap ?

➜ you can't have more than 8 visible bitmaps at a given moment.

Yes, I'm aware only 8 unique icons can be displayed on screen at once. I want the icon to change as soon as I update the data within the custom char slot.

OK

the hd44780 library knows how to handle custom chars in progmem so no coding is really required

I don't understand what you mean?

I updated it to use int now. Should I always use int for pointers? The issue still persists as well.

I guess you should use pointers for pointers, then you can't get the size wrong.

Maybe

const uint8_t* icon_lookup[2]

It might be a good idea to get the code working without using PROGMEM etc. There should be enough memory for a few icons. Once you have it working, you can then work on using PROGMEM.

Sure, I tried updating the code

#include <LiquidCrystal.h>
//#include "icons.h"

const byte I_BACK[8]= {
	0b00000,
	0b00100,
	0b01000,
	0b11111,
	0b01000,
	0b00100,
	0b00000,
	0b00000
};

const byte I_BACK_INV[8]= {
	0b11111,
	0b11011,
	0b10111,
	0b00000,
	0b10111,
	0b11011,
	0b11111,
	0b11111
};

const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 7, d7 = 8;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

int counter = 0;

void setup() {

  lcd.begin(16, 2);

  pinMode(6, OUTPUT); // Turn on LCD backlight
  analogWrite(6, 255);

}

void loop() {

  lcd.setCursor(6, 0);
  lcd.print(counter);
  lcd.setCursor(3, 0);

  //if(counter > 1){
    swapIcon(0);
  //}
  lcd.write(byte(0));
  delay(500);

  swapIcon(1);
  lcd.write(byte(0));
  delay(500);

  counter += 1;

}

void swapIcon(int icon){
  if(icon == 0){
    lcd.createChar(0, I_BACK);
  }else if(icon == 1){
    lcd.createChar(0, I_BACK_INV);
  }
}

Now with icons stored in normal memory in the main script. The icons still don't render while the if statement is commented out, but work fine when it's active

I mean something like this

where the screen will blink between


and this

by just changing the charset

click to see the code

/* ============================================
  code is placed under the MIT license
  Copyright (c) 2024 J-M-L
  For the Arduino Forum : https://forum.arduino.cc/u/j-m-l

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in
  all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  THE SOFTWARE.
  ===============================================
*/
#include <Wire.h>
#include <hd44780.h>                        // main hd44780 header
#include <hd44780ioClass/hd44780_I2Cexp.h>  // i2c expander i/o class header

const uint8_t nbCols = 20;
const uint8_t nbRows = 4;
hd44780_I2Cexp lcd;

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

const PROGMEM uint8_t otherIcons[10][8] = {
  {0b00000, 0b01010, 0b11111, 0b11111, 0b01110, 0b00100, 0b00000, 0b00000}, // HEART
  {0b00100, 0b01110, 0b01110, 0b01110, 0b11111, 0b00000, 0b00100, 0b00000}, // BELL
  {0b00000, 0b00000, 0b01010, 0b00000, 0b10001, 0b10001, 0b01110, 0b00000}, // SMILEY
  {0b00000, 0b00100, 0b11111, 0b11111, 0b11111, 0b01110, 0b01010, 0b01010}, // MAN
  {0b00000, 0b11111, 0b11111, 0b10101, 0b10101, 0b11111, 0b01010, 0b11011}, // CREATURE
  {0b00000, 0b00000, 0b00100, 0b01110, 0b11111, 0b01110, 0b01110, 0b01110}, // HOUSE
  {0b00001, 0b00011, 0b00101, 0b01001, 0b01001, 0b01011, 0b11011, 0b11000}, // MUSIC
  {0b01110, 0b01010, 0b01010, 0b01010, 0b11011, 0b10001, 0b01010, 0b00100}, // DOWN
};


void setOne() {
  for (uint8_t i = 0; i < 8; i++)
    lcd.createChar(i, minidigit[i]);
}

void setTwo() {
  for (uint8_t i = 0; i < 8; i++)
    lcd.createChar(i, otherIcons[i]);
}

void setup() {
  Serial.begin(115200);
  int result = lcd.begin(nbCols, nbRows);
  if (result) {
    Serial.print("LCD initialization failed: ");
    Serial.println(result);
    hd44780::fatalError(result);
  }

  lcd.setCursor(0, 0);
  for (uint8_t i = 0; i < 8; i++) lcd.write(i); // print the 8 custom chars on line one
}

void loop() {
  setOne(); // upload first charset
  delay(1000);
  setTwo(); // upload second charset
  delay(1000);
}

I was told recently that many LCD libraries have bugs which cause strange things to happen when user defined characters are changed. Maybe you should try using the <hd44780.h> library instead of the <LiquidCrystal.h> library.

that's what I have in the wokwi simulation.

We both know that, but @lumiobyte may not have realised that there may be significant differences between those two libraries which might appear equivalent at first glance.

Ah — OK - thanks for clarifying.

the Arduino Liquid Crystal only supports the byte arrays in SRAM not in PROGMEM:

leave away the PROGMEM keywords for your arrays when using the Arudino LiquidCrystal.

The issue of the original post is solved when using HD44780 LCD library. But either way, I thought that in my original post's code, this function

void swapIcon(int icon){
  byte icon_buf[8];
  memcpy_P(icon_buf, icon_lookup[icon], 8);
  lcd.createChar(0, icon_buf);
}

loaded the icon from progmem to ram first, so as to bypass that limitation?

You had an issue in the way you defined

It should have been an array of pointers not uint8_t

1 Like

had a second view on it - you are right.
When you want your characters in an array, I would store them in an array already.
See the modified icons.h:

/**
   Arduino Calculator

   Copyright (C) 2020, Uri Shaked.
   Released under the MIT License.

   https://forum.arduino.cc/t/inconsistent-behaviour-with-custom-chars-in-liquidcrystal/1330537/19
*/

#include <LiquidCrystal.h>

#include "icons.h"

const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 7, d7 = 8;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

int counter = 0;

void setup() {
  Serial.begin(115200);
  lcd.begin(16, 2);
  pinMode(6, OUTPUT); // Turn on LCD backlight
  analogWrite(6, 255);
}

void loop() {
  lcd.setCursor(6, 0);
  lcd.print(counter);
  lcd.setCursor(3, 0);

  if(counter > 1) {
    swapIcon(0);
  }
  lcd.write(byte(0));
  delay(500);

  swapIcon(1);
  lcd.write(byte(0));
  delay(500);

  counter += 1;
}

void swapIcon(int icon){
  byte icon_buf[8];
  memcpy_P(icon_buf, icon_lookup[icon], 8);
  //Serial.println(icon_buf[0], BIN);
  lcd.createChar(0, icon_buf);
}
//

icons.h

#ifndef ICONS_H
#define ICONS_H

const byte icon_lookup[2][8] PROGMEM = {
  {
    0b00000,
    0b00100,
    0b01000,
    0b11111,
    0b01000,
    0b00100,
    0b00000,
    0b00000
  },
  {
    0b11111,
    0b11011,
    0b10111,
    0b00000,
    0b10111,
    0b11011,
    0b11111,
    0b11111
  }
};

#endif
1 Like

@lumiobyte
The "bugs" @PaulRB mentioned related to creating custom characters is likely the issue related to createChar().
This issue/mis-feature is not in the hd44780 library but is in every hd44780 LCD library I've seen (and I've seen MANY), including the LiquidCrystal library.

The issue is that the libraries leave the LCD in CGRAM mode when createChar() returns.
Because of this, any attempt by the sketch to write() or print() to the LCD will not write those characters to the LCD display (DDRAM) but instead will continue to write to CGRAM - which will corrupt CGRAM.

IMO, the hd4480 instruction set is kind of broken/limiting in how to get back and forth between the two modes (DDRAM and CGRAM).
The only way to get between modes is to set the address counter within the specific RAM. And since there is limited h/w the LCD only has a single counter that is used for both modes. This means that when the LCD changes modes, the address counter for the previous mode is lost.
in other words there is no non-destructive way to return to the same counter value that was in DDRAM before you changed to CGRAM.

The only way to get from CGRAM mode back to DDRAM mode is to set the address counter to a location in DDRAM.
This can be done using clear(), home(), setCursor()
Until you do one of those, any attempts to write to the display will write to CGRAM instead.

Anyway, this createChar() issue may create an issue/confusion in your example code (would not be an issue with the hd44780 library)
Here is what is happening:
swapIcon() calls createChar() - which will define the custom character
createChar() leaves the LCD in CGRAM mode.
swapIcon() returns and the LCD will still be in CGRAM mode.
This code:

lcd.write(byte(0));

will not print the custom character on the display but rather will write the 0 byte to the first byte of custom character 1 so the 0 code character never makes to DDRAM to be displayed.
The counter prints because in loop() when the code rolls around it sets the currsor position. This puts the LCD back into DDRAM ram so printing works again.

The reason that if makes it work, is that it allows the write(0) to happen the first time through the loop and it allows the 0 byte to be written to DDRAM that first time since the LCD is in DDRAM if swapIcon() is not run.
After that first time, every write(0) will be writing to CGRAM rather than DDRAM.

Another thing to keep in mind is that you don't need to re-write the custom character to get the new character definition. It will "magically" change as soon as you redefine it. i.e. if you have custom character 0 on the display or even multiple ones, as soon you change the definition for it, ALL them will instantly change to the new glyph definition.

To fix this,

You can either do something to get the LCD back into DDRAM mode after creatChar() is called or switch to the hd44780 library and let it do it for you.
If you have LCD read capability, the hd44780 library will read the cursor position and restore it in createChar(). If read capability is not supported, the the hd44780 library will set the cursor position to 0,0 when createChar() returns.
i2c backpacks support reads, for LCDs using direct pin control, it would require hooking an additional pin to the R/W pin on the LCD.

A quick and dirty hack would be to put a setCursor(0,0) at the bottom of swapIcon()
setCursor() is the fastest way to get back to DDRAM as it is faster than home() and MUCH faster than clear()

--- bill

4 Likes

Super interesting writeup, thanks! I am indeed using your hd44780 library now, but this is great information to know regardless