You are being burned by a hd44780 interface issue, LiquidCrystal documentation issue, or long standing LiquidCrystal library bug depending on your point of view.
There is no “fix” for this to work the way you are wanting/expecting it to work.
(you can work around it)
It is amazing that this issue does not come up more often. I believe it is due to shear luck on the part of the sketch authors that they are not seeing this issue. i.e. they just happen to write their code in a way that does not trigger the issue.
You were unlucky in that your code happens to expect an impossible behavior so it fails.
A hd44780 display has two types of RAM. Display data ram (DDRAM) and Character Gernarator RAM (CGRAM)
IMO, the hd44780 interface is sloppy with how you switch between.
The only way to switch between them is to issue a command that sets the target RAM type AND target ram address.
What this means is that you can’t simply switch RAMs, do some writes, then flip back to the other ram and continue on at the same previous ram location.
This can be an issue when loading custom characters since the LCD display remains in CGRAM write mode until you either explicitly set a DDRAM address, or do something like a clear or home command.
Now consider what happens with the LiquidCrystal library.
When you call createChar(charval, *ptr) the library uses charval to calculate the memory location in CGRAM, then it sends the command to set the display to accept all future writes to write to CGRAM starting at that memory location.
Then it writes 8 bytes to the display which writes the custom character data to CGRAM.
It then returns back to the sketch. This leaves the LCD display in CGRAM mode.
Until you do something to put the display back into DDRAM mode, all writes will continue to go to CGRAM.
The only way to get the display back into DDRAM mode, is to call setCursor(), home(), or clear().
setCursor() explicitly sets the DDRAM address which also sets the display to DDRAM mode, and home() and clear() commands cause the display to revert back to DDRAM mode as well.
But there is no way to get the display back to DDRAM mode at the same DDRAM location where it was.
In my view this is a complete oversight on the LiquidCrystal authors part, and I’d bet that they are completely unaware of the issue.
However, fixing this is not as easy as it would seem.
So here is the dilemma: the only way to put the display back to DDRAM mode and put it back to the exact same DDRAM address, is to explicitly set it to that previous address.
If the library supported reads, it could have read the address before switching to CGRAM mode and then put it back in the createChar() function.
But since reads are not supported, it can’t do this.
The library also is not tracking the DDRAM location either.
As a result, the library has no way of explicitly setting the DDRAM address back to where it was prior to updating CGRAM in createChar().
There is no way to fix it when reads are not supported.
The other alternative would be to have createChar() do a setCursor(0,0), home(), or clear() before returning to force the display back to DDRAM mode.
But this is not returning the display back to where it was.
Personally, I think that issuing a setCursor(0,0) (which is not the same as home()) command before returning would be better than leaving the display in CGRAM mode as the display would be able to display characters even if they were not in the correct position.
(I have a hd44780 library that does this)
setCursor(0,0), is the least intrusive thing that can be done to get the display back into DDRAM mode.
home() and clear() alter more than just the DDRAM location.
Any, to summarize, you can’t do this:
lcd.createChar(memOffset++, customChar);
lcd.write(uint8_t(memOffset));
The write() will write the data to CGRAM which is not what is intended/expected.
You will need to modify you code to do some sort of cursor positioning after setting the custom character.
You may want to modify you loop() code to create and write characters in separate routines.
But at a minimum, you must do a setCursor(), home(), or clear() after you create any custom character before you can write any new characters to the display.
— bill