IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Series 37 Module Cảm Biến – LCD 1602 I2C: PCF8574 Expander, LiquidCrystal_I2C, Cuộn Chữ & Ký Tự Tùy Chỉnh

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ước16 ký tự × 2 hàng
ControllerHD44780 (hoặc ST7066U tương thích)
I2C ExpanderPCF8574 hoặc PCF8574A
Địa chỉ I2C0x27 (phổ biến) hoặc 0x3F
Điện áp VCC5V (LCD cần 5V — không dùng 3.3V trực tiếp)
Màu backlightXanh lá + chữ đen, Xanh + chữ trắng, vàng/xanh
Ký tự customTối đa 8 (lưu trong CGRAM)
Tốc độ I2C100kHz (standard mode)

Sơ Đồ Chân (Pinout)

Module LCD 1602 + I2C Backpack (4 chân):

┌──────────────────────────────────────┐
│  GND  VCC  SDA  SCL                 │
└──┬────┬────┬────┬─────────────────────┘
   │    │    │    │
  GND  5V  SDA  SCL
ChânMô tả
GNDMass
VCC5V (bắt buộc — không dùng 3.3V)
SDAI2C Data
SCLI2C 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ụngMô tả
Trạm thời tiếtHiển thị nhiệt độ + độ ẩm + áp suất
Đồng hồ thựcKết hợp DS3231 (Bài 37) hiển thị giờ/ngày
Menu điều hướngKết hợp nút bấm chọn menu cài đặt
Máy đo điệnHiể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ự

  1. Chạy I2C Scanner: địa chỉ thực là bao nhiêu?
  2. Kiểm tra hướng cắm module backpack lên LCD đúng chiều
  3. Vặn biến trở contrast trên backpack module (nếu có) cho đến khi thấy cursor
  4. 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).

Bài tiếp theo: