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.
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.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Full-size breadboard
- 1 × Character LCD, 16x2
Kits that can build it
These 7 starter kits have every part in the circuit. Each kit's page lists what else it can build.
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 pin | Connects to |
|---|---|
| Character LCD, 16x2 pin RS | Arduino Uno R3 pin 12 |
| Character LCD, 16x2 pin RW | Arduino Uno R3 pin 11 |
| Character LCD, 16x2 pin D7 | Arduino Uno R3 pin 10 |
| Character LCD, 16x2 pin D6 | Arduino Uno R3 pin 9 |
| Character LCD, 16x2 pin D5 | Arduino Uno R3 pin 8 |
| Character LCD, 16x2 pin D4 | Arduino Uno R3 pin 7 |
| Character LCD, 16x2 pin D3 | Arduino Uno R3 pin 6 |
| Character LCD, 16x2 pin D2 | Arduino Uno R3 pin 5 |
| Character LCD, 16x2 pin D1 | Arduino Uno R3 pin 4 |
| Character LCD, 16x2 pin D0 | Arduino Uno R3 pin 3 |
| Character LCD, 16x2 pin E | Arduino Uno R3 pin 2 |
| Character LCD, 16x2 pin VDD | Arduino Uno R3 pin 5V |
| Character LCD, 16x2 pin A | Arduino Uno R3 pin 5V |
| Character LCD, 16x2 pin VSS | Arduino Uno R3 pin GND |
| Character LCD, 16x2 pin V0 | Arduino Uno R3 pin GND |
| Character LCD, 16x2 pin K | Arduino 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.
// 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, matchingDATA_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.