The 16x2 character LCD
Two rows of sixteen characters, on sixteen pins or on four. The HD44780 command set, the busy flag that catches everybody, and the I2C backpack.
The character LCD is the display in every kit, and it is not a screen in the sense the OLED is. There is no framebuffer and no graphics: a sketch sends it characters, and the controller has the shapes for them in a ROM. That makes it the cheapest way to put a number in front of somebody, and it is why it has outlived several generations of nicer displays.
Mokxi has it twice, because the shops sell it twice.
Two parts, one controller
Character LCD, 16x2 is the bare module: sixteen pins along one edge, and
your sketch drives the HD44780 directly. Six wires from the board is the usual
hookup: RS, E and four data lines, plus power, a contrast pin and the
backlight.
Character LCD with I2C backpack is the same panel with a little board
soldered to the back of it: a PCF8574 eight-bit port expander, a contrast
trimmer and a transistor for the backlight. It brings out four pins (GND,
VCC, SDA, SCL), and it is the one most kits ship now.
Behind them is one model
(crates/parts/src/parts/hd44780.rs): the same command decode, the same
memory, the same busy timing, the same pixels. Only the way a byte arrives
differs.
What the controller does
Everything the datasheet says, decoded from the highest set bit of the byte down: clear, home, entry mode, display and cursor and blink on and off, shift, function set, set the CGRAM address, set the DDRAM address.
Two of those are worth knowing about.
DDRAM is eighty bytes and the window is sixteen. Row 0 is addresses 0x00
to 0x27 and row 1 is 0x40 to 0x67, so each line is really forty
characters and the panel is a window onto sixteen of them. Print past column 16
and the characters go somewhere nobody can see until you shift the display,
which is the first surprise.
CGRAM is eight characters of your own. Write eight bytes of five bits each and print character code 0 to 7 to get them back. They are drawn by exactly the same code that draws the built-in font, so a custom degree sign or a battery icon looks like it belongs.
The busy flag, which is the thing that goes wrong
A write takes the controller 37 microseconds and a clear takes 1.52 milliseconds, and it ignores the bus entirely for that long. Mokxi models that: a byte clocked in while the controller is busy is dropped, exactly as it is on the bench.
That is not us being awkward. It is the bug behind every "my LCD prints
rubbish" question on every forum: a driver with no waits loses half a byte, the
four-bit nibble phase slips, and from then on every character is garbage. Our
own driver (firmware/lib/mokxi_hd44780.h) waits 50 microseconds after a byte
and 2 milliseconds after a clear, which is what the Arduino libraries do.
Blank displays, and which one you have
Both modules can show you a perfectly correct, perfectly invisible display, but for different reasons.
On the bare module it is contrast. V0 is an analog input, and at the
supply rail the characters vanish completely. That is what the little blue pot
on a real one is for, and it is why a first LCD so often looks dead. Wire V0
to ground for maximum contrast; leave it unwired and Mokxi treats it as ground,
which is how the cheap modules with a fixed resistor behave.
On the backpack the trimmer is a property rather than a pin, so contrast is
not the problem. The backlight is. It is bit 3 of the port byte and nothing
else, so a driver that never sets that bit leaves you with a working display in
a dark window. begin() in mokxi_lcd1602_i2c.h turns it on;
backlight(false) turns it off again, and the panel goes blank on the canvas
when it does.
The backpack's port byte
There is no display protocol on the I2C bus at all. The expander has one register (its eight output pins), and every byte after the address lands on them. The backpack wires them to the LCD in an order every Arduino library hard-codes, and so does Mokxi:
| bit | goes to |
|---|---|
P0 |
RS |
P1 |
RW |
P2 |
E |
P3 |
the backlight |
P4 to P7 |
D4 to D7 |
So one character is six writes to the expander: the high nibble with E low,
high, low again, then the same three for the low nibble. The controller latches
on the falling edge of E, exactly as it does on the bare module.
The address is 0x27 on a PCF8574 module and 0x3F on a PCF8574A one, and the
two are physically identical. It is a property, so you can set whichever your
board would have been.
Writing to one
Both drivers share their text half, so the calls are the same:
#include "mokxi_lcd1602_i2c.h"
Lcd1602I2c lcd(A5, A4); // SCL, SDA
void setup() {
lcd.begin(); // four-bit mode, backlight on, cleared
lcd.print("Hello");
lcd.setCursor(0, 1);
lcd.printFixed(215, 1); // "21.5"
lcd.padLine(); // wipe the tail of anything longer
}
printFixed takes a number scaled by a power of ten rather than a float,
which keeps the driver small on an Uno's 32 KB; lcd.print(21.5, 1) on a
float works too, through LiquidCrystal_I2C.h. padLine spaces
out to the end of the row, which is how a shorter reading wipes a longer one
without a clear() and its 1.52 millisecond wait.
For the six-wire module it is mokxi_lcd1602.h and
Lcd1602 lcd(rs, e, d4, d5, d6, d7), and everything after begin() is the
same.
The LiquidCrystal_I2C spelling
A tutorial for the backpack module compiles as it stands:
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
void setup() {
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Hello, world!");
}
LiquidCrystal_I2C.h is Mokxi's own header on the driver above, on Wire's
pins: A4 and A5 on an Uno, the board's own I2C pins elsewhere (each board's
help article says which), or wherever Wire.begin(sda, scl) put them. print
is the board's own, so a float prints with two places as it does on Serial.
init() turns the backlight on, where the upstream library leaves that to
backlight(). The six-wire LiquidCrystal.h is not shipped: use
mokxi_lcd1602.h, and the build says so if you forget.
What is not modeled
Reads of any kind. The busy flag, the address counter and DDRAM read back nothing, so a driver that polls the busy flag instead of waiting will hang. Wait instead; that is what every real library does anyway. Also not modeled: the 5x10 font a one-line module can select, and the viewing angle.
The busy timings are the datasheet's two typical numbers (37 microseconds for a write
and 1.52 milliseconds for clear and home), used as exact values. A real HD44780's
timing scales with whatever its internal oscillator happens to be running at, so those
figures move by tens of percent between parts and with temperature, and the 4-bit
nibble timing (the minimum E pulse width and the hold after it) is not checked here
at all: a driver can clock E as fast as it likes.
A byte arriving while the controller is busy is dropped. That is a choice the model makes so the fault is visible and repeatable; on a real part the outcome is undefined and more often a corrupted character than a lost one.
On the backpack, the PCF8574 never stretches the clock and acknowledges every byte sent to it. There is no read of the expander's port, no NACK and no bus error of any kind, and the backlight transistor is bit 3 and nothing else: no current, no brightness. Contrast on the backpack is a property rather than the trimmer's actual divider.
See it: Thermometer on an I2C LCD for the backpack, and Thermometer in the templates for the six-wire hookup.
If an I2C LCD stays blank, shows blocks or prints leftovers, Arduino I2C LCD not working goes through the causes by symptom.