16x2 LCD Shows Nothing? The 4-Bit Wiring Checklist

📅
✍️ By CircuPilot Team
← Back to Blog Hub

A blank character LCD is one of the least helpful failures in electronics. There is no error, no serial output, no LED — just a screen that stays empty while your code runs perfectly.

The causes are few, and they can be separated without a scope if you go in the right order.

What the module actually is

Almost every 16x2 character display — LM016L, and the anonymous blue and green modules — is an HD44780 controller with a dot-matrix panel attached. Same commands, same timing, same protocol. Code written for one drives all of them.

It has a parallel interface, and you use half of it. In 4-bit mode each byte goes across as two nibbles on D4–D7, which is why the standard wiring is six lines and not eleven:

PinGoes to
RSa digital pin — low selects a command, high selects data
Ea digital pin — the strobe; data is latched on its falling edge
D4–D7four digital pins
RWground — you are only writing
D0–D3nothing
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);   // rs, e, d4, d5, d6, d7

void setup() {
  lcd.begin(16, 2);
  lcd.print("CircuPilot");
  lcd.setCursor(0, 1);
  lcd.print("LM016L works!");
}
void loop() { }

The checklist, in the order things actually go wrong

1. Contrast. On real hardware this is the most common cause by a wide margin, and it looks exactly like a wiring fault: the backlight is on, the screen is blank, and the code is fine. The V0 (or VEE) pin sets contrast and it is not optional — it wants a 10 kΩ potentiometer between 5 V and ground with the wiper on V0, or at minimum a resistor to ground. Floating, you get a blank screen or a row of solid blocks.

2. The constructor does not match the wiring. LiquidCrystal(rs, e, d4, d5, d6, d7) — that order, and it must be the order you wired. Swap any two and the display gets valid strobes carrying scrambled nibbles, which produces either nothing or garbage. This is the failure with no symptom to distinguish it, so check it early rather than late.

3. begin() was not called, or was called with the wrong size. lcd.begin(16, 2) runs the initialisation sequence. Without it the controller is in an undefined state. With begin(16, 1) it works — but only the first line, and setCursor(0, 1) silently goes nowhere.

4. RS and E are on pins the sketch is also using for something else. The serial pins 0 and 1 are the classic collision.

5. Nothing was printed. lcd.print() in loop() without a setCursor writes past the end of the display after sixteen characters and then keeps going into DDRAM you cannot see.

Why the second line is at 0x40

This one deserves its own note because it surprises everyone once.

The two rows are not contiguous in memory. Row 1 starts at DDRAM address 0x00; row 2 starts at 0x40, not at 0x10. Write seventeen characters starting at the top left and the seventeenth does not appear at the start of row 2 — it goes into 0x10, which is memory the panel does not show.

setCursor(0, 1) is a wrapper around Set DDRAM address 0x40, and begin(16, 2) is what tells the controller there are two lines at all. If the second line never works, it is nearly always because begin was told otherwise.

On a bare AVR there is no library

LiquidCrystal is an Arduino library. On an ATmega16 or any bare AVR you write the 4-bit driver, and it is about thirty lines:

static void lcd_strobe(void) {
    LCD_PORT |=  (1 << EN);
    _delay_us(2);
    LCD_PORT &= ~(1 << EN);      /* latched here, on the falling edge */
    _delay_us(50);
}

static void lcd_nibble(uint8_t n) {
    LCD_PORT = (LCD_PORT & ~(0x0F << D4)) | ((n & 0x0F) << D4);
    lcd_strobe();
}

static void lcd_byte(uint8_t b, uint8_t rs) {
    if (rs) LCD_PORT |=  (1 << RS);
    else    LCD_PORT &= ~(1 << RS);
    lcd_nibble(b >> 4);
    lcd_nibble(b & 0x0F);
    _delay_ms(2);
}

And the initialisation sequence, which is not optional and not obvious:

0x33, 0x32   — get the controller into 4-bit mode from any starting state
0x28         — 4-bit, 2 lines, 5x8 font
0x0C         — display on, cursor off
0x06         — entry mode: increment
0x01         — clear

Those first two exist because the controller might be in 8-bit mode, in 4-bit mode, or halfway through a nibble pair when your code starts. The sequence resynchronises it from any of those. Skipping it is why a driver that works after a power cycle fails after a reset.

Note _delay_ms rather than delay() if you are on an ATmega16 in a simulator — the Arduino delay() counts Timer0 overflows, and the register-level path does not set Timer0 up.

Seeing it without the hardware

The awkward thing about debugging a blank LCD is that all five causes look identical. In a simulator they separate, because the wiring and the constructor are both in front of you and the display either shows text or does not — with no contrast pot in the way, since a simulated panel has nothing to adjust.

Both forms above run in the browser simulator: the LiquidCrystal version on an Arduino Uno, the register-level version on an ATmega16, both driving the same 16x2 module. Place the parts, wire six lines, press Run.

For the wider embedded picture, simulating an Arduino circuit covers digital I/O and the compile-and-run loop, and the ATmega16 walkthrough covers the register-level side including what _delay_ms is doing there.

Frequently asked questions

Why is my 16x2 LCD blank with the backlight on?

On real hardware the usual cause is contrast — the V0 pin needs a potentiometer or a resistor to ground, and with it floating you get a lit but empty screen, or a row of solid blocks. After contrast, check that RS and E reach the pins your LiquidCrystal constructor names.

Which pins does an LCD need in 4-bit mode?

Six: RS, E, and D4 to D7. RW ties to ground because you are only writing, and D0 to D3 stay unconnected. That is why almost every Arduino LCD tutorial uses six wires and not eleven.

Why does setCursor(0, 1) not go to the second line?

It does, if begin() was told there are two lines. The second row starts at DDRAM address 0x40, not immediately after the first, and begin(16, 2) is what sets that up. Calling begin(16, 1), or not calling it at all, leaves the controller in one-line mode.

Can I print a float on an LCD?

Yes, lcd.print(value, 2) prints two decimal places. The catch is that the display does not clear behind you: printing a shorter string over a longer one leaves the tail of the old one, so pad with spaces or clear the line first.

What is the difference between LM016L and a generic 16x2 LCD?

Nothing that matters to your code. LM016L is the part designation used in schematic tools for a standard HD44780-compatible 16x2 module, and the same commands and the same 4-bit protocol drive all of them.