Built-in project · Arduino Uno R3

LCD1602 in 8-bit mode on pins 2 to 12, on an Arduino Uno

A 16x2 LCD with RS on 12, RW on 11, E on 2 and all eight data lines on pins 3 to 10, driven byte by byte with no LiquidCrystal library. The whole build, an Arduino Uno and 1 more part, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 30 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $35. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • How an HD44780 LCD takes commands and characters
  • The roles of the RS, RW and E pins
  • Sending a byte on eight data lines, one bit per pin
  • Turning a number into text one digit at a time
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 3

How it works

A 16x2 LCD has a controller chip that takes one byte at a time. Here all eight data lines D0 to D7 are wired to pins 3 to 10, with RS on 12, RW on 11 and E on 2. No library is used, so the sketch does every step itself.

To send a byte it sets RS (low for a command, high for a character), pulls RW low, puts the eight bits on the data pins, then pulses E. At startup it shows two lines for three seconds. After that it prints Hello, world! on line one and the seconds since reset on line two.

Parts list

3 parts, plus the jumper wires. They retail for about $35; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Character LCD, 16x2 pin RSArduino Uno R3 pin 12
Character LCD, 16x2 pin RWArduino Uno R3 pin 11
Character LCD, 16x2 pin D7Arduino Uno R3 pin 10
Character LCD, 16x2 pin D6Arduino Uno R3 pin 9
Character LCD, 16x2 pin D5Arduino Uno R3 pin 8
Character LCD, 16x2 pin D4Arduino Uno R3 pin 7
Character LCD, 16x2 pin D3Arduino Uno R3 pin 6
Character LCD, 16x2 pin D2Arduino Uno R3 pin 5
Character LCD, 16x2 pin D1Arduino Uno R3 pin 4
Character LCD, 16x2 pin D0Arduino Uno R3 pin 3
Character LCD, 16x2 pin EArduino Uno R3 pin 2
Character LCD, 16x2 pin VDDArduino Uno R3 pin 5V
Character LCD, 16x2 pin AArduino Uno R3 pin 5V
Character LCD, 16x2 pin VSSArduino Uno R3 pin GND
Character LCD, 16x2 pin V0Arduino Uno R3 pin GND
Character LCD, 16x2 pin KArduino Uno R3 pin GND

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// A 16x2 LCD in 8-bit mode, with no library: every byte by hand.
//
//   LCD RS -> pin 12    RW -> pin 11    E -> pin 2
//   LCD D0 to D7 -> pins 3 to 10
//   LCD VSS, K -> GND   VDD, A -> 5V   V0 -> GND (full contrast)
//
// The display's controller (an HD44780) takes one byte at a time on D0 to
// D7. RS says what the byte is: LOW for a command (clear, move the cursor),
// HIGH for a character to print. RW LOW means the board is writing. A short
// pulse on E tells the controller to take the byte.
//
// The LiquidCrystal library does all this, usually over four data lines to
// save pins. Doing it by hand over eight shows what the library hides.

const int RS_PIN = 12;
const int RW_PIN = 11;
const int E_PIN = 2;
const int DATA_PINS[8] = {3, 4, 5, 6, 7, 8, 9, 10};  // D0 to D7

void lcdSend(byte value, bool isText) {
  digitalWrite(RS_PIN, isText ? HIGH : LOW);
  digitalWrite(RW_PIN, LOW);
  for (int bit = 0; bit < 8; bit++) {
    digitalWrite(DATA_PINS[bit], (value >> bit) & 1);
  }
  digitalWrite(E_PIN, HIGH);
  delayMicroseconds(1);
  digitalWrite(E_PIN, LOW);  // the controller takes the byte here
  delayMicroseconds(50);     // most commands are done in 40 us
}

void lcdCommand(byte command) {
  lcdSend(command, false);
  if (command <= 0x03) delay(2);  // clear and home take longer
}

void lcdPrint(const char *text) {
  while (*text) lcdSend(*text++, true);
}

// Move the cursor: row 0 starts at address 0x00, row 1 at 0x40.
void lcdGoTo(byte column, byte row) {
  lcdCommand(0x80 | (row ? 0x40 : 0x00) | column);
}

void setup() {
  pinMode(RS_PIN, OUTPUT);
  pinMode(RW_PIN, OUTPUT);
  pinMode(E_PIN, OUTPUT);
  for (int bit = 0; bit < 8; bit++) pinMode(DATA_PINS[bit], OUTPUT);
  Serial.begin(9600);
  Serial.println("Mokxi Uno: 16x2 LCD in 8-bit mode, no library");

  delay(50);           // let the display power up
  lcdCommand(0x38);    // 8-bit interface, two lines, 5x8 dots
  delay(5);
  lcdCommand(0x38);
  lcdCommand(0x38);
  lcdCommand(0x0C);    // display on, no cursor, no blink
  lcdCommand(0x06);    // move right after each character
  lcdCommand(0x01);    // clear

  lcdGoTo(1, 0);
  lcdPrint("Eight data pins");
  lcdGoTo(1, 1);
  lcdPrint("and no library");
  delay(3000);
  lcdCommand(0x01);
  lcdGoTo(0, 0);
  lcdPrint("Hello, world!");
}

void loop() {
  // The seconds since reset, as text: digits are the characters '0' to '9'.
  unsigned long seconds = millis() / 1000;
  char digits[11];
  int n = 0;
  do {
    digits[n++] = '0' + seconds % 10;
    seconds /= 10;
  } while (seconds > 0);
  lcdGoTo(0, 1);
  lcdPrint("Up ");
  while (n > 0) lcdSend(digits[--n], true);
  lcdPrint(" s");
  delay(250);
}

Code walkthrough

The key functions of the sketch, and what each one does.

lcdSend sketch.ino

The core of the sketch. It sets RS from isText, holds RW low to write, puts each bit of the byte on its data pin, then raises and lowers E. The controller reads the byte when E falls. A short wait follows so it can finish.

void lcdSend(byte value, bool isText) {
  digitalWrite(RS_PIN, isText ? HIGH : LOW);
  digitalWrite(RW_PIN, LOW);
  for (int bit = 0; bit < 8; bit++) {
    digitalWrite(DATA_PINS[bit], (value >> bit) & 1);
  }
  digitalWrite(E_PIN, HIGH);
  delayMicroseconds(1);
  digitalWrite(E_PIN, LOW);  // the controller takes the byte here
  delayMicroseconds(50);     // most commands are done in 40 us
}

lcdCommand sketch.ino

Sends a byte with RS low, so the LCD treats it as an instruction such as clear or display on. Commands up to 0x03 (clear and home) take longer, so it waits 2 ms after them.

void lcdCommand(byte command) {
  lcdSend(command, false);
  if (command <= 0x03) delay(2);  // clear and home take longer
}

lcdGoTo sketch.ino

Moves the cursor. Row 0 starts at address 0x00 and row 1 at 0x40. The command is 0x80 combined with the row's start address and the column.

// Move the cursor: row 0 starts at address 0x00, row 1 at 0x40.
void lcdGoTo(byte column, byte row) {
  lcdCommand(0x80 | (row ? 0x40 : 0x00) | column);
}

setup sketch.ino

Sets all pins as outputs, waits for the display to power up, then sends 0x38 three times to select the 8-bit interface with two lines. It turns the display on, sets the entry mode, clears, and shows the two-line greeting before Hello, world!.

void setup() {
  pinMode(RS_PIN, OUTPUT);
  pinMode(RW_PIN, OUTPUT);
  pinMode(E_PIN, OUTPUT);
  for (int bit = 0; bit < 8; bit++) pinMode(DATA_PINS[bit], OUTPUT);
  Serial.begin(9600);
  Serial.println("Mokxi Uno: 16x2 LCD in 8-bit mode, no library");

  delay(50);           // let the display power up
  lcdCommand(0x38);    // 8-bit interface, two lines, 5x8 dots
  delay(5);
  lcdCommand(0x38);
  lcdCommand(0x38);
  lcdCommand(0x0C);    // display on, no cursor, no blink
  lcdCommand(0x06);    // move right after each character
  lcdCommand(0x01);    // clear

  lcdGoTo(1, 0);
  lcdPrint("Eight data pins");
  lcdGoTo(1, 1);
  lcdPrint("and no library");
  delay(3000);
  lcdCommand(0x01);
  lcdGoTo(0, 0);
  lcdPrint("Hello, world!");
}

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The screen shows blocks or nothing This project
    Check V0 goes to ground for full contrast, and VDD and A go to 5 V. Also check the VSS and K wires.
  • Garbled characters This project
    Check D0 to D7 go to pins 3 to 10 in order, matching DATA_PINS.
  • The screen lights up but shows only a row of solid boxes, or nothing at all. Character LCD
    Contrast pin V0 needs a voltage. Connect the middle pin of a 10 k pot to V0, with its ends on 5 V and GND. Also confirm that the code calls lcd.begin(16, 2).
  • Garbled characters or a blank display even with good contrast. Character LCD
    In 4-bit mode only D4 to D7 are used, and D0 to D3 stay unconnected. Tie RW to GND. Match the pins in LiquidCrystal(rs, en, d4, d5, d6, d7) exactly to your wiring.
  • Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
    Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.
  • Pins misbehave or the board resets when you power motors or servos from it. Arduino Uno R3
    Each I/O pin gives about 20 mA, 40 mA max. Power motors and servos from their own supply and join its ground to the Uno's GND.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.