Màn hình OLED SSD1306 0.96″ là lựa chọn phổ biến nhất để hiển thị thông tin trong các dự án nhúng: không cần đèn nền, tiêu thụ điện thấp, hiển thị sắc nét trên 128×64 pixel. Bài này giải thích cơ chế frame buffer, cách khởi tạo đúng với Adafruit SSD1306, và viết code hiển thị text, hình khối, cuộn chữ cho cả Arduino Uno và ESP32.
Nguyên Lý Hoạt Động
1. Cấu Trúc Panel OLED
OLED (Organic Light-Emitting Diode) khác với LCD ở chỗ mỗi pixel tự phát sáng:
- LCD: cần đèn nền (backlight) chiếu qua filter màu → tiêu thụ điện cao
- OLED: pixel phát sáng độc lập → pixel tắt = đen tuyệt đối, tiêu thụ = 0
SSD1306 Module Cross-Section (nhìn ngang):
Mặt kính ─────────────────────────────────
██░░███░░██░░███░░ ← OLED pixels (sáng/tắt)
Organic layer (phát sáng khi có dòng)
──────────────────
Transistor backplane
──────────────────
PCB ──────────────────────────────────────
2. Controller SSD1306 — Frame Buffer
IC điều khiển SSD1306 quản lý 128×64 = 8192 pixel:
- Mỗi pixel = 1 bit (ON/OFF)
- Tổng: 8192 bit = 1024 byte = 1KB frame buffer
- Vi điều khiển ghi frame buffer vào GDDRAM của SSD1306 qua I2C
- SSD1306 tự động quét và điều khiển từng hàng/cột pixel
Frame Buffer Layout (1024 bytes):
Page 0 [128 bytes = 128 cột × 8 row bit]
Page 1 [128 bytes]
...
Page 7 [128 bytes]
─────────────────────────────────────
Col0 Col1 ... Col127
Tổng: 8 pages × 128 bytes = 1024 bytes
Quan trọng với Arduino Uno: RAM tổng = 2048 byte. Frame buffer 1024 byte chiếm 50% RAM. Không thể dùng nhiều mảng lớn song song với OLED.
3. Giao Tiếp I2C
SSD1306 dùng I2C 2 dây:
- SCL: Clock (400kHz fast mode)
- SDA: Data (bidirectional)
- Địa chỉ I2C: 0x3C (khi pin SA0=GND) hoặc 0x3D (khi SA0=VCC)
- Hầu hết module trên thị trường: 0x3C (mặc định)
I2C Transaction:
[START] [0x3C + W] [Control] [Data/Command] [STOP]
│
├─ 0x00 = Command byte
└─ 0x40 = Data byte (ghi GDDRAM)
Thông Số Kỹ Thuật
| Thông số | Giá trị |
|---|---|
| Độ phân giải | 128 × 64 pixels |
| Kích thước màn | 0.96″ (đường chéo) |
| Màu sắc | Monochrome — White, Blue, hoặc Yellow+Blue |
| Giao tiếp | I2C (4 chân) hoặc SPI (7 chân, module khác) |
| Địa chỉ I2C | 0x3C hoặc 0x3D |
| Điện áp VCC | 3.3V hoặc 5V (có onboard LDO regulator) |
| Dòng tiêu thụ | ~20mA (toàn màn sáng) |
| Frame buffer RAM | 1024 bytes (trong SSD1306) |
| Tốc độ I2C | 100kHz (standard) / 400kHz (fast) |
Sơ Đồ Chân (Pinout)
Module OLED 0.96" I2C — 4 chân:
┌──────────────────────────────┐
│ OLED SSD1306 0.96" I2C │
│ ┌─────────────────────┐ │
│ │ ████████████████ │ │
│ │ ████ HIỂN THỊ ████ │ │
│ │ ████████████████ │ │
│ └─────────────────────┘ │
│ GND VCC SCL SDA │
└──┬────┬────┬────┬────────────┘
│ │ │ │
GND VCC SCL SDA
| Chân | Mô tả |
|---|---|
| GND | Mass |
| VCC | 3.3V hoặc 5V |
| SCL | I2C Clock |
| SDA | I2C Data |
Thứ tự chân cảnh báo: Một số module có thứ tự GND-VCC-SCL-SDA, số khác là VCC-GND-SCL-SDA. Luôn đọc nhãn trên PCB trước khi cắm — cắm ngược VCC-GND có thể hỏng module.
Các Biến Thể
| Biến thể | Đặc điểm |
|---|---|
| 0.96″ I2C (phổ biến) | 128×64, 4 chân, 0x3C |
| 0.96″ SPI | 128×64, 7 chân, nhanh hơn, tốn nhiều GPIO |
| 1.3″ I2C (SSH1106) | 128×64, controller SSH1106 — cần thư viện khác |
| 0.91″ I2C | 128×32 pixel (nửa chiều cao) |
| Yellow+Blue | 16 hàng vàng (y=0..15), 48 hàng xanh (y=16..63) |
Phân biệt SSD1306 vs SSH1106: Hai IC nhìn ngoài giống hệt nhau. Cách nhận biết: AdafruitSSD1306 không hiển thị đúng → thử U8g2 với U8G2SH1106.
Cài Đặt Thư Viện
Trong Arduino IDE: Sketch → Include Library → Manage Libraries
Tìm và cài đặt cả 2:
Adafruit SSD1306by AdafruitAdafruit GFX Libraryby Adafruit
Kết Nối Phần Cứng
OLED với ESP32 DevKit V1
ESP32 DevKit V1 OLED SSD1306 0.96" I2C
───────────────────── ──────────────────────────
3.3V hoặc 5V ─────────→ VCC
GND ─────────────────→ GND
GPIO21 (SDA) ──────────→ SDA ← I2C default ESP32
GPIO22 (SCL) ──────────→ SCL ← I2C default ESP32
OLED với Arduino Uno
Arduino Uno OLED SSD1306 0.96" I2C
───────────────────── ──────────────────────────
5V ─────────────────→ VCC
GND ─────────────────→ GND
A4 (SDA) ─────────────→ SDA ← I2C trên Uno
A5 (SCL) ─────────────→ SCL ← I2C trên Uno
Code Arduino IDE
Code Hiển Thị Text Cơ Bản — Arduino Uno
/*
* OLED SSD1306 — Hiển thị text, kích thước, vị trí
* Board: Arduino Uno
* Kết nối: VCC→5V, GND→GND, SDA→A4, SCL→A5
* Thư viện cần: Adafruit SSD1306 + Adafruit GFX Library
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// Kích thước màn hình
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
// Reset pin (-1 = không dùng reset riêng)
#define OLED_RESET -1
// Khởi tạo đối tượng display
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void setup() {
Serial.begin(9600);
// Khởi động OLED — địa chỉ I2C 0x3C
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 không tìm thấy! Kiểm tra kết nối.");
while (true); // Dừng nếu không tìm thấy màn hình
}
// Xóa frame buffer (màn đen)
display.clearDisplay();
// --- Dòng 1: chữ nhỏ nhất (size 1 = 6×8 pixel/ký tự) ---
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0); // Vị trí: cột 0, hàng 0
display.println("Xin chao IoTLabs!");
// --- Dòng 2: chữ to hơn (size 2 = 12×16 pixel/ký tự) ---
display.setTextSize(2);
display.setCursor(0, 20); // Xuống 20 pixel
display.println("ESP32!");
// --- Dòng 3: ký tự đặc biệt, chữ nhỏ ---
display.setTextSize(1);
display.setCursor(0, 50);
display.print("T:");
display.print(27.5, 1); // In số thực
display.print((char)247); // Ký tự độ (°)
display.print("C");
// *** QUAN TRỌNG: gọi display() để đẩy buffer lên màn hình ***
// Mọi thao tác vẽ chỉ sửa buffer trong RAM — display() mới ghi ra OLED
display.display();
}
void loop() {
// Không làm gì — màn hình giữ nguyên
}
Code Vẽ Hình Khối — Arduino Uno
/*
* OLED SSD1306 — Vẽ đường thẳng, hình chữ nhật, hình tròn
* Board: Arduino Uno
* Kết nối: VCC→5V, GND→GND, SDA→A4, SCL→A5
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void drawShapes() {
display.clearDisplay();
// Đường thẳng ngang
display.drawLine(0, 0, 127, 0, SSD1306_WHITE); // Từ (0,0) đến (127,0)
// Hình chữ nhật rỗng: drawRect(x, y, width, height, color)
display.drawRect(0, 10, 40, 25, SSD1306_WHITE);
// Hình chữ nhật đặc: fillRect
display.fillRect(50, 10, 30, 25, SSD1306_WHITE);
// Hình tròn rỗng: drawCircle(x_center, y_center, radius, color)
display.drawCircle(105, 22, 15, SSD1306_WHITE);
// Chữ ở dưới
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 48);
display.println("Rect Fill Circle");
display.display(); // Đẩy buffer ra màn hình
}
void setup() {
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { while (true); }
drawShapes();
}
void loop() {}
Code Thanh Tiến Trình & Đồng Hồ — Arduino Uno
/*
* OLED SSD1306 — Progress bar + đếm thời gian
* Board: Arduino Uno
* Kết nối: VCC→5V, GND→GND, SDA→A4, SCL→A5
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Vẽ thanh tiến trình tại vị trí (x, y), chiều rộng w, chiều cao h, phần trăm 0-100
void drawProgressBar(int x, int y, int w, int h, int percent) {
display.drawRect(x, y, w, h, SSD1306_WHITE); // Khung ngoài
int filled = (w - 2) * percent / 100; // Chiều rộng phần đã lấp
display.fillRect(x + 1, y + 1, filled, h - 2, SSD1306_WHITE); // Lấp đặc bên trong
}
unsigned long startTime;
int progress = 0;
void setup() {
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { while (true); }
startTime = millis();
}
void loop() {
unsigned long elapsed = (millis() - startTime) / 1000; // Giây đã trôi qua
progress = (elapsed % 11) * 10; // 0% → 100% lặp mỗi 10 giây
display.clearDisplay();
// Tiêu đề
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 0);
display.println("Thanh Tien Trinh");
// Thanh tiến trình: x=0, y=20, width=128, height=12
drawProgressBar(0, 20, 128, 12, progress);
// Phần trăm ở giữa
display.setCursor(55, 23);
display.setTextColor(SSD1306_BLACK); // Chữ đen trên nền trắng
display.print(progress); display.print("%");
// Thời gian đã chạy
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 45);
display.print("Thoi gian: ");
display.print(elapsed);
display.print("s");
display.display();
delay(500);
}
Code ESP32 — Hiển Thị Nhiệt Độ Từ LM35
/*
* OLED SSD1306 + LM35 — Nhiệt kế hiển thị trực tiếp lên OLED
* Board: ESP32 DevKit V1
* Kết nối OLED: VCC→3.3V, GND→GND, SDA→GPIO21, SCL→GPIO22
* Kết nối LM35: VCC→VIN(5V), GND→GND, VOUT→GPIO34
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define LM35_PIN 34 // ADC1 Ch6 — input only
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
float readTempC() {
long sum = 0;
for (int i = 0; i < 20; i++) {
sum += analogRead(LM35_PIN);
delay(5);
}
float avgRaw = (float)sum / 20.0;
float voltage_mV = avgRaw * 3300.0 / 4095.0; // ESP32 ADC 12-bit, 3.3V ref
return voltage_mV / 10.0; // 10mV/°C
}
void drawThermometer(float tempC) {
display.clearDisplay();
// --- Tiêu đề ---
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(20, 0);
display.println("NHIET KE LM35");
// Đường kẻ ngang phân cách
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
// --- Nhiệt độ lớn ở giữa ---
display.setTextSize(3); // 18×24 pixel/ký tự
display.setCursor(10, 18);
display.print(tempC, 1); // 1 chữ số thập phân
display.setTextSize(2);
display.print((char)247); // Ký tự °
display.print("C");
// --- Fahrenheit nhỏ bên dưới ---
float tempF = tempC * 1.8 + 32.0;
display.setTextSize(1);
display.setCursor(30, 52);
display.print(tempF, 1);
display.print((char)247);
display.print("F");
display.display();
}
void setup() {
Serial.begin(115200);
analogSetAttenuation(ADC_11db); // ESP32: đọc đủ 0-3.3V
// Khởi động I2C với đúng pin ESP32
Wire.begin(21, 22); // SDA=GPIO21, SCL=GPIO22
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("OLED không tìm thấy!");
while (true);
}
// Màn hình khởi động
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(25, 25);
display.println("Khoi dong...");
display.display();
delay(1500);
}
void loop() {
float temp = readTempC();
drawThermometer(temp);
Serial.printf("Nhiet do: %.2f°C\n", temp);
delay(1000);
}
Kết Quả Mong Đợi
Màn hình OLED hiển thị:
┌─────────────────────────────┐
│ NHIET KE LM35 │
│─────────────────────────────│
│ │
│ 27.3°C │
│ │
│ 81.1°F │
└─────────────────────────────┘
Ứng Dụng Thực Tế
| Ứng dụng | Mô tả |
|---|---|
| Đồng hồ thời gian thực | Kết hợp DS3231 (Bài 37) hiển thị giờ/ngày |
| Trạm đo môi trường | Nhiệt độ + độ ẩm (DHT22) + áp suất (BMP280) |
| Màn hình IoT | Hiển thị dữ liệu từ server qua WiFi |
| Điều khiển tốc độ | Hiển thị RPM, công suất motor |
| Menu điều hướng | Kết hợp Rotary Encoder (Bài 22) chọn menu |
Lưu Ý Khi Sử Dụng
1. Luôn gọi display.display() sau khi vẽ
Mọi hàm drawLine(), fillRect(), println() chỉ sửa frame buffer trong RAM. Chỉ display.display() mới truyền buffer qua I2C xuống màn hình. Quên gọi display() → màn không cập nhật.
2. RAM Arduino Uno bị chiếm 50%
Frame buffer 1024 byte + Adafruit SSD1306 overhead ≈ 1200-1400 byte RAM. Trên Arduino Uno (2048 byte RAM), chỉ còn ~600-800 byte cho code. Tránh dùng String (cấp phát động) và mảng lớn.
3. Kiểm tra địa chỉ I2C nếu không hiển thị
Chạy I2C Scanner để tìm địa chỉ đúng:
#include <Wire.h>
void setup() {
Wire.begin(); Serial.begin(9600);
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr); Wire.endTransmission();
if (Wire.endTransmission() == 0) {
Serial.print("Tìm thấy I2C: 0x"); Serial.println(addr, HEX);
}
}
}
void loop() {}
4. Tuổi thọ OLED
OLED bị burn-in nếu hiển thị cùng nội dung liên tục hàng nghìn giờ. Thực tế: thêm screensaver (tắt sau 30 giây không tương tác), hoặc dịch chuyển nội dung nhẹ.


