IoTLabs

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

Series 37 Module Cảm Biến – OLED SSD1306 0.96\” I2C: Frame Buffer, Adafruit GFX & Hiển Thị Dữ Liệu Thực Tế

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ải128 × 64 pixels
Kích thước màn0.96″ (đường chéo)
Màu sắcMonochrome — White, Blue, hoặc Yellow+Blue
Giao tiếpI2C (4 chân) hoặc SPI (7 chân, module khác)
Địa chỉ I2C0x3C hoặc 0x3D
Điện áp VCC3.3V hoặc 5V (có onboard LDO regulator)
Dòng tiêu thụ~20mA (toàn màn sáng)
Frame buffer RAM1024 bytes (trong SSD1306)
Tốc độ I2C100kHz (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ânMô tả
GNDMass
VCC3.3V hoặc 5V
SCLI2C Clock
SDAI2C 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″ SPI128×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″ I2C128×32 pixel (nửa chiều cao)
Yellow+Blue16 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 SSD1306 by Adafruit
  • Adafruit GFX Library by 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ụngMô tả
Đồng hồ thời gian thựcKết hợp DS3231 (Bài 37) hiển thị giờ/ngày
Trạm đo môi trườngNhiệt độ + độ ẩm (DHT22) + áp suất (BMP280)
Màn hình IoTHiể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ướngKế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ẹ.

Bài tiếp theo: