LCD 1602 (16 cột × 2 hàng) là màn hình ký tự phổ biến nhất trong các dự án nhúng từ thập niên 1980 đến nay. Phiên bản I2C rút gọn chỉ còn 4 dây thay vì 16 dây song song nhờ module PCF8574. Bài này giải thích cơ chế IC mở rộng I/O, các lệnh LiquidCrystal_I2C cần biết, và cách tạo ký tự tùy chỉnh 5×8 pixel.
Nguyên Lý Hoạt Động
1. Controller HD44780 — Giao Tiếp Song Song
LCD 1602 dùng controller HD44780 (hoặc tương thích ST7066U):
- Giao tiếp 8-bit hoặc 4-bit song song
- Chế độ 8-bit: cần 8 + 3 GPIO = 11 dây điều khiển
- Chế độ 4-bit: cần 4 + 3 GPIO = 7 dây điều khiển
- Cả 2 đều nhiều GPIO → vấn đề trong dự án thực
LCD 1602 — 16 chân nguyên bản:
1: VSS (GND) 9: D2
2: VDD (5V) 10: D3
3: V0 (contrast) 11: D4 ←┐
4: RS 12: D5 ← 4 bit mode
5: R/W 13: D6 ← chỉ dùng 4 chân này
6: E (enable) 14: D7 ←┘
7: D0 15: A (backlight +)
8: D1 16: K (backlight -)
2. Module PCF8574 — Mở Rộng I/O Qua I2C
Giải pháp: gắn thêm IC PCF8574 (8-bit I/O expander I2C) lên LCD:
- PCF8574 nhận lệnh qua I2C (2 dây) → output 8 bit song song → điều khiển LCD 4-bit mode
- Kết quả: LCD 16 dây → 4 dây (GND, VCC, SDA, SCL)
Sơ đồ kết nối PCF8574 ↔ LCD:
PCF8574 → LCD HD44780
P0 → RS
P1 → R/W (thường nối GND vì chỉ ghi)
P2 → E (Enable)
P3 → Backlight transistor
P4 → D4
P5 → D5
P6 → D6
P7 → D7
3. Địa Chỉ I2C PCF8574
Địa chỉ base: 0x20 (PCF8574) hoặc 0x38 (PCF8574A)
- 3 jumper A0, A1, A2 trên module → cộng thêm 0-7 vào base address
- Cấu hình phổ biến nhất (A0=A1=A2=GND): 0x27 (PCF8574A) hoặc 0x20 (PCF8574)
Cách tìm địa chỉ: chạy I2C Scanner (xem code bên dưới).
Địa chỉ theo jumper:
A2 A1 A0 │ PCF8574 │ PCF8574A
0 0 0 │ 0x20 │ 0x38
0 0 1 │ 0x21 │ 0x39
0 1 0 │ 0x22 │ 0x3A
1 0 0 │ 0x24 │ 0x3C
1 1 1 │ 0x27 │ 0x3F
Phổ biến: 0x27 (PCF8574A, A0-A2=GND) hoặc 0x3F (PCF8574A, A0-A2=VCC)
4. Cấu Trúc DDRAM — Bộ Nhớ Ký Tự
HD44780 có DDRAM (Display Data RAM) — mỗi byte = 1 ký tự:
- Hàng 1: địa chỉ 0x00 – 0x27 (chỉ 0x00-0x0F hiển thị trong 16 cột)
- Hàng 2: địa chỉ 0x40 – 0x67 (chỉ 0x40-0x4F hiển thị)
lcd.setCursor(col, row): col=0-15, row=0-1
5. Ký Tự Tùy Chỉnh CGRAM
Có thể lưu tối đa 8 ký tự tùy chỉnh trong CGRAM:
- Mỗi ký tự: ma trận 5×8 pixel (5 cột × 8 hàng)
- Mỗi hàng = 1 byte (chỉ 5 bit thấp dùng)
- Gọi với index 0-7
Thông Số Kỹ Thuật
| Thông số | Giá trị |
|---|---|
| Kích thước | 16 ký tự × 2 hàng |
| Controller | HD44780 (hoặc ST7066U tương thích) |
| I2C Expander | PCF8574 hoặc PCF8574A |
| Địa chỉ I2C | 0x27 (phổ biến) hoặc 0x3F |
| Điện áp VCC | 5V (LCD cần 5V — không dùng 3.3V trực tiếp) |
| Màu backlight | Xanh lá + chữ đen, Xanh + chữ trắng, vàng/xanh |
| Ký tự custom | Tối đa 8 (lưu trong CGRAM) |
| Tốc độ I2C | 100kHz (standard mode) |
Sơ Đồ Chân (Pinout)
Module LCD 1602 + I2C Backpack (4 chân):
┌──────────────────────────────────────┐
│ GND VCC SDA SCL │
└──┬────┬────┬────┬─────────────────────┘
│ │ │ │
GND 5V SDA SCL
| Chân | Mô tả |
|---|---|
| GND | Mass |
| VCC | 5V (bắt buộc — không dùng 3.3V) |
| SDA | I2C Data |
| SCL | I2C Clock |
Cài Đặt Thư Viện
Trong Arduino IDE: Sketch → Include Library → Manage Libraries
Tìm và cài: LiquidCrystal I2C by Frank de Brabander
Kết Nối Phần Cứng
LCD 1602 I2C với Arduino Uno
Arduino Uno LCD 1602 I2C Module
───────────────────── ──────────────────────────
5V ─────────────────→ VCC
GND ─────────────────→ GND
A4 (SDA) ──────────────→ SDA
A5 (SCL) ──────────────→ SCL
LCD 1602 I2C với ESP32
ESP32 DevKit V1 LCD 1602 I2C Module
───────────────────── ──────────────────────────
VIN (5V) ─────────────→ VCC ← 5V từ USB power
GND ─────────────────→ GND
GPIO21 (SDA) ──────────→ SDA ← 3.3V I2C — xem lưu ý
GPIO22 (SCL) ──────────→ SCL
Lưu ý ESP32: LCD cần 5V nhưng ESP32 I2C là 3.3V. Hầu hết module PCF8574 trên thị trường chấp nhận tín hiệu I2C 3.3V khi nguồn 5V (input threshold ~1.5V) → thường hoạt động trực tiếp. Nếu không nhận: dùng level shifter bi-directional I2C.
Code Arduino IDE
Code I2C Scanner — Tìm Địa Chỉ LCD
/*
* I2C Scanner — tìm địa chỉ của LCD 1602 I2C
* Chạy 1 lần, đọc Serial Monitor để biết địa chỉ
* Board: Arduino Uno hoặc ESP32
*/
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(9600);
Serial.println("Quét thiết bị I2C...");
byte found = 0;
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.print("Tìm thấy: 0x");
Serial.println(addr, HEX);
found++;
}
}
if (found == 0) Serial.println("Không tìm thấy thiết bị I2C!");
else Serial.print("Tổng: "); Serial.println(found);
}
void loop() {}
Code Hiển Thị Text Cơ Bản — Arduino Uno
/*
* LCD 1602 I2C — Hiển thị text 2 hàng
* Board: Arduino Uno
* Kết nối: GND→GND, VCC→5V, SDA→A4, SCL→A5
* Thư viện: LiquidCrystal I2C by Frank de Brabander
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// Khởi tạo: địa chỉ I2C, số cột, số hàng
// Đổi 0x27 thành 0x3F nếu scanner tìm ra địa chỉ khác
LiquidCrystal_I2C lcd(0x27, 16, 2);
void setup() {
lcd.init(); // Khởi tạo LCD
lcd.backlight(); // Bật đèn nền
// Hàng 1: tên project
lcd.setCursor(0, 0); // Cột 0, hàng 0
lcd.print("IoTLabs Sensor");
// Hàng 2: thông tin thêm
lcd.setCursor(0, 1); // Cột 0, hàng 1
lcd.print("Bai 35: LCD I2C");
}
void loop() {
// Màn hình giữ nguyên, không cần làm gì
}
Code Hiển Thị Dữ Liệu Cập Nhật — Arduino Uno
/*
* LCD 1602 I2C — Hiển thị nhiệt độ (giả lập) cập nhật mỗi giây
* Board: Arduino Uno
* Kết nối: GND→GND, VCC→5V, SDA→A4, SCL→A5
*
* Kỹ thuật: chỉ xóa vùng dữ liệu thay đổi thay vì lcd.clear()
* lcd.clear() có thể gây nhấp nháy nếu gọi thường xuyên
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
unsigned long lastUpdate = 0;
int fakeTemp = 250; // Giả lập nhiệt độ × 10 (25.0°C ban đầu)
void setup() {
lcd.init();
lcd.backlight();
// Tiêu đề cố định — chỉ in 1 lần
lcd.setCursor(0, 0);
lcd.print("Nhiet do:");
lcd.setCursor(0, 1);
lcd.print("Trang thai: OK ");
}
void updateDisplay(float temp) {
// Chỉ cập nhật phần giá trị, không xóa toàn màn hình
lcd.setCursor(10, 0); // Sau chữ "Nhiet do:"
lcd.print(temp, 1);
lcd.print((char)223); // Ký tự ° (LCD có sẵn code 223)
lcd.print("C "); // Khoảng trắng thừa để xóa số cũ dài hơn
}
void loop() {
if (millis() - lastUpdate >= 1000) {
lastUpdate = millis();
// Giả lập nhiệt độ dao động quanh 27°C
fakeTemp += random(-5, 6); // ±0.5°C
fakeTemp = constrain(fakeTemp, 200, 400); // Giới hạn 20-40°C
float temp = fakeTemp / 10.0;
updateDisplay(temp);
}
}
Code Cuộn Chữ Ngang — Arduino Uno
/*
* LCD 1602 I2C — Cuộn chữ ngang (ticker tape)
* Board: Arduino Uno
* Kết nối: GND→GND, VCC→5V, SDA→A4, SCL→A5
*
* Phương pháp: dùng hardware scroll (scrollDisplayLeft) của HD44780
* Hoặc software scroll qua DDRAM (linh hoạt hơn)
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Cuộn văn bản dài qua màn hình 16 cột (software method)
void scrollText(int row, String message, int delayMs) {
String paddedMsg = " " + message + " ";
// 16 khoảng trắng đầu: cho chữ cuộn vào từ bên phải
// 16 khoảng trắng cuối: cho chữ cuộn ra ngoài bên trái
for (unsigned int i = 0; i < paddedMsg.length() - 15; i++) {
lcd.setCursor(0, row);
lcd.print(paddedMsg.substring(i, i + 16)); // Cắt 16 ký tự để hiển thị
delay(delayMs);
}
}
void setup() {
lcd.init();
lcd.backlight();
// Hàng 0: cố định
lcd.setCursor(0, 0);
lcd.print("IoTLabs.vn News:");
}
void loop() {
// Hàng 1: cuộn tin tức
scrollText(1, "ESP32 ra mat ban moi hon! BLE5 + WiFi6 toc do gap doi!", 200);
delay(500);
scrollText(1, "Kit hoc IoT gia re, ket noi thiet bi voi cloud de dang!", 200);
delay(500);
}
Code Ký Tự Tùy Chỉnh — Arduino Uno
/*
* LCD 1602 I2C — Tạo ký tự tùy chỉnh 5×8 pixel
* Board: Arduino Uno
* Kết nối: GND→GND, VCC→5V, SDA→A4, SCL→A5
*
* CGRAM: 8 ký tự tùy chỉnh, index 0-7
* Mỗi ký tự: 8 byte, mỗi byte = 1 hàng (5 bit thấp)
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Trái tim — index 0
byte heart[8] = {
0b00000,
0b01010, // _ _
0b11111, // X X X
0b11111, // X X X
0b01110, // X X
0b00100, // X
0b00000,
0b00000
};
// Mặt cười — index 1
byte smile[8] = {
0b00000,
0b01010, // X X
0b01010, // X X
0b00000,
0b10001, // X X
0b01110, // X X
0b00000,
0b00000
};
// Nhiệt kế — index 2
byte thermometer[8] = {
0b00100, // X
0b01010, // X X
0b01010, // X X
0b01110, // XXX
0b01110, // XXX
0b11111, // XXXXX
0b11111, // XXXXX
0b01110 // XXX
};
// Sóng WiFi — index 3
byte wifi[8] = {
0b00000,
0b01110, // XXX
0b10001, // X X
0b00100, // X
0b01010, // X X
0b00000,
0b00100, // X
0b00000
};
void setup() {
lcd.init();
lcd.backlight();
// Đăng ký ký tự tùy chỉnh vào CGRAM
lcd.createChar(0, heart);
lcd.createChar(1, smile);
lcd.createChar(2, thermometer);
lcd.createChar(3, wifi);
// Hàng 1
lcd.setCursor(0, 0);
lcd.write(0); // Trái tim (index 0)
lcd.print(" IoTLabs ");
lcd.write(1); // Mặt cười (index 1)
// Hàng 2
lcd.setCursor(0, 1);
lcd.write(2); // Nhiệt kế
lcd.print("27.5C ");
lcd.write(3); // WiFi
lcd.print("Online");
}
void loop() {}
Code ESP32 — LCD + Đồng Hồ Đơn Giản
/*
* LCD 1602 I2C + ESP32 — Đồng hồ dùng millis()
* Board: ESP32 DevKit V1
* Kết nối: VCC→VIN(5V), GND→GND, SDA→GPIO21, SCL→GPIO22
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
unsigned long startTime;
unsigned long lastUpdate = 0;
// Định dạng số thành chuỗi 2 chữ số (thêm "0" trước nếu < 10)
String twoDigit(int n) {
return n < 10 ? "0" + String(n) : String(n);
}
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // SDA=GPIO21, SCL=GPIO22
lcd.init();
lcd.backlight();
startTime = millis();
lcd.setCursor(0, 0);
lcd.print("Dong ho dem gian");
lcd.setCursor(0, 1);
lcd.print("00:00:00");
}
void loop() {
if (millis() - lastUpdate >= 1000) {
lastUpdate = millis();
unsigned long elapsed = (millis() - startTime) / 1000;
int hours = elapsed / 3600;
int minutes = (elapsed % 3600) / 60;
int seconds = elapsed % 60;
lcd.setCursor(4, 1); // Bắt đầu từ cột 4 hàng 2
lcd.print(twoDigit(hours) + ":" + twoDigit(minutes) + ":" + twoDigit(seconds));
Serial.printf("Uptime: %02d:%02d:%02d\n", hours, minutes, seconds);
}
}
Kết Quả Mong Đợi
Màn hình LCD (hàng 1 / hàng 2):
┌────────────────┐
│IoTLabs Sensor │
│Bai 35: LCD I2C │
└────────────────┘
Ứng Dụng Thực Tế
| Ứng dụng | Mô tả |
|---|---|
| Trạm thời tiết | Hiển thị nhiệt độ + độ ẩm + áp suất |
| Đồng hồ thực | Kết hợp DS3231 (Bài 37) hiển thị giờ/ngày |
| Menu điều hướng | Kết hợp nút bấm chọn menu cài đặt |
| Máy đo điện | Hiển thị điện áp, dòng, công suất |
| Máy pha cà phê | Hiển thị chế độ, thời gian, nhiệt độ nước |
Lưu Ý Khi Sử Dụng
1. LCD bắt buộc dùng 5V — không phải 3.3V
HD44780 được thiết kế cho 5V. Cấp 3.3V → contrast thấp (có thể không hiển thị) hoặc hoạt động không ổn định. Với ESP32: lấy 5V từ pin VIN (USB power), dùng 3.3V chỉ cho tín hiệu I2C.
2. Không tìm thấy LCD — kiểm tra theo thứ tự
- Chạy I2C Scanner: địa chỉ thực là bao nhiêu?
- Kiểm tra hướng cắm module backpack lên LCD đúng chiều
- Vặn biến trở contrast trên backpack module (nếu có) cho đến khi thấy cursor
- Kiểm tra kết nối SDA/SCL không bị nhầm
3. Màn hình tối nhưng cursor thấy được
lcd.backlight() chưa được gọi, hoặc transistor backlight trên PCF8574 không nhận lệnh. Thử: lcd.backlight() trong setup() sau lcd.init().
4. Tránh lcd.clear() trong loop() nhanh
lcd.clear() gửi lệnh HD44780 cần 1.52ms. Gọi trong vòng lặp nhanh → nhấp nháy rõ. Thay bằng: ghi đè chỉ vùng thay đổi với khoảng trắng thừa: lcd.print("27.5C ") (thêm space cuối).


