Coding for an LCD display is the process of sending formatted byte sequences over a parallel or I2C bus to manipulate a character generator and Display Data RAM (DDRAM) matrix, translating microcontroller logic into visible alphanumeric text. In a real circuit, adding an LCD shifts your project from abstract serial monitor debugging to standalone, human-readable physical feedback without requiring the high-speed SPI bus or heavy framebuffer memory of a graphical OLED. Beginners commonly confuse the I2C address of the PCF8574 backpack (typically 0x27 or 0x3F) with the internal memory registers of the HD44780 LCD controller itself, leading to blank screens when the bus is actually functioning perfectly.

The HD44780 Architecture: DDRAM vs CGRAM

To write robust code for character LCDs, you must understand that the screen is not a pixel grid; it is a memory map. The ubiquitous HD44780 controller (and its modern clones like the SPLC780D) relies on two distinct memory areas:

  • DDRAM (Display Data RAM): This is the text buffer. Writing an ASCII byte to a specific DDRAM address tells the controller to fetch the corresponding 5x8 pixel font from the internal Character Generator ROM (CGROM) and illuminate those pixels on the glass.
  • CGRAM (Character Generator RAM): This is a tiny, volatile memory space (usually 64 bytes) where you can define up to eight custom 5x8 characters. This is how you create custom battery icons, thermometers, or progress bars.
The Memory Illusion: The physical layout of the LCD glass rarely matches the linear memory layout of the DDRAM. If you write a continuous string of 40 characters to a 16x2 display, the text will wrap in bizarre, non-sequential ways because the memory addresses jump between rows.

Understanding the DDRAM base addresses is the single most important theoretical concept for advanced LCD formatting. Here is the exact memory map for the two most common module sizes:

HD44780 DDRAM Base Address Map (Hexadecimal)
Display Size Row 1 Base Row 2 Base Row 3 Base Row 4 Base
16x2 0x00 0x40 N/A N/A
20x2 0x00 0x40 N/A N/A
16x4 0x00 0x40 0x10 0x50
20x4 0x00 0x40 0x14 0x54

Where You Meet This In Practice

You will encounter HD44780-based character LCDs in environments where graphical displays are too fragile, too expensive, or require too much processing overhead. In basic character LCD implementations, the display is driven by low-cost 8-bit microcontrollers that lack the RAM for a graphical framebuffer.

Common real-world applications include:

  • 3D Printer Control Panels: Older or budget-friendly Marlin firmware implementations use 20x4 LCDs with rotary encoders to navigate SD card menus and monitor hotend temperatures.
  • Bench Power Supplies: DIY linear power supplies use 16x2 displays to show constant voltage (CV) and constant current (CC) setpoints, updating the DDRAM only when the ADC detects a change to prevent screen flicker.
  • Reflow Ovens and Kilns: High-temperature environments favor character LCDs because the thick glass and simple backlight diffusers survive ambient heat better than the delicate polarizers and flexible printed circuits (FPC) of TFT or OLED screens.

I2C Backpacks and the PCF8574 Expander

Wiring a raw HD44780 in 4-bit parallel mode consumes six GPIO pins (RS, E, D4, D5, D6, D7). To save pins, the market standard is the I2C backpack, which uses a PCF8574 or PCF8574A I/O expander chip to translate I2C serial data back into the parallel signals the LCD expects.

Critical I2C Addresses: Backpacks with the PCF8574 chip default to 0x27. Backpacks with the PCF8574A chip default to 0x3F. If your I2C scanner finds 0x3F but your code initializes the LiquidCrystal_I2C library at 0x27, your screen will remain blank.

Worked Numeric Example: Manual Cursor Positioning

Most Arduino libraries abstract away cursor positioning with a simple lcd.setCursor(col, row) function. But if you are writing bare-metal C for an AVR or optimizing an ESP32 interrupt service routine, you need to calculate the DDRAM address manually.

The Scenario: You want to print the string "VCC:" starting at Row 3, Column 4 on a 20x4 LCD.

  1. Find the Row Base: According to the table above, Row 3 on a 20x4 display starts at 0x14.
  2. Add the Column Offset: Columns are 0-indexed. Column 4 is an offset of 0x03 (since Col 0 is +0, Col 1 is +1, etc.).
  3. Calculate Target Address: 0x14 + 0x03 = 0x17.
  4. Format the Command Byte: The HD44780 'Set DDRAM Address' instruction requires the most significant bit (MSB) to be 1. This means you bitwise OR your target address with 0x80.
  5. Final Calculation: 0x80 | 0x17 = 0x97.

Sending the command byte 0x97 to the instruction register moves the internal cursor exactly to Row 3, Column 4. The next data byte you send will render at that exact physical location.

The PCF8574 Pin Mapping

When coding the I2C backpack directly, you are actually writing to the PCF8574's 8-bit output latch. The standard mapping for the vast majority of Chinese-manufactured backpacks is:

  • P0: Register Select (RS)
  • P1: Read/Write (RW) - Usually tied to GND for write-only mode
  • P2: Enable (E)
  • P3: Backlight Control (Active HIGH or LOW depending on jumper)
  • P4-P7: Data pins D4-D7

Because the data pins are on the high nibble (P4-P7), the I2C library must split every 8-bit LCD command into two 4-bit I2C transmissions, pulsing the Enable (P2) pin between them.

Debugging the Blank Screen

Why is my I2C LCD completely blank, even with the backlight on?

This is almost always a contrast voltage issue, not a code issue. The HD44780 requires a negative voltage differential between VCC and the V0 (contrast) pin to make the liquid crystals opaque. If the blue potentiometer on the back of the backpack is set to 0 ohms, the screen will look blank or show solid white blocks. Turn the potentiometer counter-clockwise until you see the dark pixel grid, then back it off slightly.

Why does my ESP32 crash or fail to initialize the LCD?

The ESP32 operates at 3.3V logic, while the standard LCD backpack expects 5V. While the ESP32's I2C pins are somewhat 5V tolerant, the PCF8574 expander requires a solid 5V input to register a HIGH signal. If you are using 3.3V pull-up resistors on the I2C bus, the PCF8574 may not recognize the clock edges. Use a bidirectional logic level shifter (like the BSS138-based modules) between the ESP32 and the LCD backpack, and ensure the I2C pull-ups on the 5V side are tied to 5V.

Why is my text wrapping weirdly on a 16x4 display?

As shown in the DDRAM table, Row 3 on a 16x4 display starts at 0x10, not 0x20 or 0x30. If your code assumes a linear memory map and just increments the cursor position past the end of Row 1, the controller will wrap the text to Row 3, skipping Row 2 entirely. Always use explicit setCursor() commands or calculate the exact hex offsets rather than relying on continuous string printing to handle line breaks.