LED ma trận MAX7219 và LED 7 đoạn TM1637 với ESP32: chữ chạy 32 × 8 và đồng hồ, không cần thư viện
So sánh LED ma trận MAX7219 và LED 7 đoạn TM1637, nối với ESP32 an toàn mức điện áp, giải thích khung 16 bit và ba lệnh TM1637, hai chương trình chữ chạy và đồng hồ không cần thư viện.
Màn hình LED đỏ có ưu điểm mà OLED, LCD không có: sáng rõ, nhìn được từ xa và cả dưới nắng. Hai loại phổ biến nhất là LED ma trận 8 × 8 dùng chip MAX7219 (ghép thành bảng chữ chạy 32 × 8) và LED 7 đoạn 4 số dùng chip TM1637 (đồng hồ, bộ đếm, hiện nhiệt độ). Bài này giải thích hai chip nhận dữ liệu thế nào, rồi viết cho ESP32 hai chương trình không cần thư viện: chữ chạy "PYWORLD.VN" trên ma trận 32 × 8 và đồng hồ đếm phút:giây trên LED 7 đoạn.
1. Hai loại màn hình LED
| MAX7219 ma trận 8 × 8 | TM1637 LED 7 đoạn 4 số | |
|---|---|---|
| Hiển thị | 64 điểm mỗi khối; ghép 4 khối thành 32 × 8 | 4 chữ số, dấu hai chấm ở giữa |
| Hợp với | Chữ chạy, biểu tượng, hình đơn giản | Giờ, số đếm, nhiệt độ |
| Giao tiếp | 3 dây kiểu SPI: DIN, CS, CLK | 2 dây: CLK, DIO (không phải I2C) |
| Nguồn | 5 V; 4 khối sáng hết cỡ tới khoảng 1 A | Datasheet ghi 5 V; phần lớn module chạy được ở 3,3 V, sáng hơi yếu hơn |
| Độ sáng | 16 mức | 8 mức |
Cả hai chip đều tự quét LED liên tục. ESP32 chỉ cần gửi nội dung mới khi muốn đổi, không phải bật tắt từng LED như khi nối LED trực tiếp vào chân GPIO.
2. Linh kiện cần chuẩn bị
- ESP32 DevKit V1 30 chân (hoặc loại 38 chân).
- Module LED ma trận MAX7219 loại 4 khối (32 × 8) và/hoặc module TM1637 4 số.
- Dây cắm cái–đực, breadboard, dây USB nạp code.
3. Sơ đồ nối dây

| MAX7219 (đầu DIN) | ESP32 | TM1637 | ESP32 |
|---|---|---|---|
| VCC | VIN (5 V khi cắm USB) | VCC | 3V3 |
| GND | GND | GND | GND |
| DIN | D23 | CLK | D22 |
| CS | D5 | DIO | D21 |
| CLK | D18 |
- Module MAX7219 có hai đầu: đầu ghi DIN là đầu vào, đầu ghi DOUT là đầu ra để nối thêm khối. Nối ESP32 vào đầu DIN.
- Vì sao MAX7219 cấp 5 V được mà TM1637 thì không: ESP32 chỉ gửi tín hiệu sang MAX7219, không đọc ngược lại, nên chân ESP32 không bao giờ nhận 5 V. TM1637 thì có điện trở kéo lên tới VCC trên chân DIO và ESP32 phải đọc chân này (bit ACK), nên cấp 5 V sẽ đưa 5 V vào GPIO. Muốn TM1637 sáng hơn ở 5 V thì cần mạch chuyển mức; xem bài mức logic 3,3 V và 5 V.
- MAX7219 cấp 5 V cần mức cao tối thiểu khoảng 3,5 V, cao hơn 3,3 V của ESP32 một chút. Phần lớn module vẫn nhận đúng; nếu chữ hiện lộn xộn thì thêm IC chuyển mức 74HCT245.
4. MAX7219 nhận dữ liệu thế nào

Mỗi lần ghi gồm 16 bit: 8 bit đầu là địa chỉ thanh ghi, 8 bit sau là dữ liệu. Với module ma trận phổ biến, thanh ghi 1 đến 8 là 8 hàng, mỗi bit của dữ liệu là một điểm trong hàng. Các thanh ghi cài đặt: 0x09 (giải mã, đặt 0 để tự điều khiển từng điểm), 0x0A (độ sáng 0–15), 0x0B (số hàng quét, đặt 7), 0x0C (bật màn), 0x0F (thử đèn, đặt 0).
Khi nối 4 khối, dữ liệu đi qua từng khối như qua một đường ống: 16 bit gửi trước bị đẩy tới khối xa nhất. Sau khi gửi đủ 64 bit, sườn lên của chân CS làm cả 4 khối cùng nhận phần dữ liệu đang nằm trong chúng.
5. Chương trình chữ chạy MAX7219
// PYWORLD.VN - LED ma trận MAX7219 (module 4 khối 32 x 8) với ESP32: chữ chạy, không cần thư viện
// DIN vào D23, CS vào D5, CLK vào D18; VCC 5 V, GND chung. Chữ ngược, lật hay sai thứ tự khối thì đổi ba cờ DAO_...
// Font 5 x 7 lấy từ bộ chữ chuẩn của thư viện Adafruit GFX (chữ hoa, số, dấu cơ bản)
const int CHAN_DIN = 23;
const int CHAN_CS = 5;
const int CHAN_CLK = 18;
const int SO_KHOI = 4; // số module 8 x 8 nối tiếp
const int DO_SANG = 3; // 0-15
const bool DAO_COT = true; // chữ bị lật ngang: đổi cờ này
const bool DAO_HANG = false; // chữ lộn ngược: đổi cờ này
const bool DAO_KHOI = false; // các khối hiện sai thứ tự: đổi cờ này
const char *NOI_DUNG = "PYWORLD.VN - ESP32 + MAX7219 ";
// Thanh ghi MAX7219
const uint8_t TG_GIAI_MA = 0x09, TG_DO_SANG = 0x0A, TG_SO_HANG = 0x0B, TG_TAT = 0x0C, TG_THU_DEN = 0x0F;
const uint8_t FONT[][5] = {
0x00, 0x00, 0x00, 0x00, 0x00, // dau cach
0x00, 0x00, 0x5F, 0x00, 0x00, // !
0x00, 0x07, 0x00, 0x07, 0x00, // "
0x14, 0x7F, 0x14, 0x7F, 0x14, // #
0x24, 0x2A, 0x7F, 0x2A, 0x12, // $
0x23, 0x13, 0x08, 0x64, 0x62, // %
0x36, 0x49, 0x56, 0x20, 0x50, // &
0x00, 0x08, 0x07, 0x03, 0x00, // '
0x00, 0x1C, 0x22, 0x41, 0x00, // (
0x00, 0x41, 0x22, 0x1C, 0x00, // )
0x2A, 0x1C, 0x7F, 0x1C, 0x2A, // *
0x08, 0x08, 0x3E, 0x08, 0x08, // +
0x00, 0x80, 0x70, 0x30, 0x00, // ,
0x08, 0x08, 0x08, 0x08, 0x08, // -
0x00, 0x00, 0x60, 0x60, 0x00, // .
0x20, 0x10, 0x08, 0x04, 0x02, // /
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0
0x00, 0x42, 0x7F, 0x40, 0x00, // 1
0x72, 0x49, 0x49, 0x49, 0x46, // 2
0x21, 0x41, 0x49, 0x4D, 0x33, // 3
0x18, 0x14, 0x12, 0x7F, 0x10, // 4
0x27, 0x45, 0x45, 0x45, 0x39, // 5
0x3C, 0x4A, 0x49, 0x49, 0x31, // 6
0x41, 0x21, 0x11, 0x09, 0x07, // 7
0x36, 0x49, 0x49, 0x49, 0x36, // 8
0x46, 0x49, 0x49, 0x29, 0x1E, // 9
0x00, 0x00, 0x14, 0x00, 0x00, // :
0x00, 0x40, 0x34, 0x00, 0x00, // ;
0x00, 0x08, 0x14, 0x22, 0x41, // <
0x14, 0x14, 0x14, 0x14, 0x14, // =
0x00, 0x41, 0x22, 0x14, 0x08, // >
0x02, 0x01, 0x59, 0x09, 0x06, // ?
0x3E, 0x41, 0x5D, 0x59, 0x4E, // @
0x7C, 0x12, 0x11, 0x12, 0x7C, // A
0x7F, 0x49, 0x49, 0x49, 0x36, // B
0x3E, 0x41, 0x41, 0x41, 0x22, // C
0x7F, 0x41, 0x41, 0x41, 0x3E, // D
0x7F, 0x49, 0x49, 0x49, 0x41, // E
0x7F, 0x09, 0x09, 0x09, 0x01, // F
0x3E, 0x41, 0x41, 0x51, 0x73, // G
0x7F, 0x08, 0x08, 0x08, 0x7F, // H
0x00, 0x41, 0x7F, 0x41, 0x00, // I
0x20, 0x40, 0x41, 0x3F, 0x01, // J
0x7F, 0x08, 0x14, 0x22, 0x41, // K
0x7F, 0x40, 0x40, 0x40, 0x40, // L
0x7F, 0x02, 0x1C, 0x02, 0x7F, // M
0x7F, 0x04, 0x08, 0x10, 0x7F, // N
0x3E, 0x41, 0x41, 0x41, 0x3E, // O
0x7F, 0x09, 0x09, 0x09, 0x06, // P
0x3E, 0x41, 0x51, 0x21, 0x5E, // Q
0x7F, 0x09, 0x19, 0x29, 0x46, // R
0x26, 0x49, 0x49, 0x49, 0x32, // S
0x03, 0x01, 0x7F, 0x01, 0x03, // T
0x3F, 0x40, 0x40, 0x40, 0x3F, // U
0x1F, 0x20, 0x40, 0x20, 0x1F, // V
0x3F, 0x40, 0x38, 0x40, 0x3F, // W
0x63, 0x14, 0x08, 0x14, 0x63, // X
0x03, 0x04, 0x78, 0x04, 0x03, // Y
0x61, 0x59, 0x49, 0x4D, 0x43, // Z
};
uint8_t manHinh[SO_KHOI * 8]; // mỗi phần tử là một cột, bit 0 là hàng trên cùng
// Gửi cùng một cặp (thanh ghi, giá trị) tới mọi khối, hoặc giá trị riêng từng khối nếu giaTri != NULL.
void ghiTatCa(uint8_t thanhGhi, uint8_t chung, const uint8_t *giaTri = nullptr) {
digitalWrite(CHAN_CS, LOW);
for (int k = 0; k < SO_KHOI; k++) { // byte gửi trước bị đẩy tới khối xa nhất
int khoi = DAO_KHOI ? SO_KHOI - 1 - k : k;
shiftOut(CHAN_DIN, CHAN_CLK, MSBFIRST, thanhGhi);
shiftOut(CHAN_DIN, CHAN_CLK, MSBFIRST, giaTri ? giaTri[khoi] : chung);
}
digitalWrite(CHAN_CS, HIGH); // sườn lên của CS: mọi khối chốt dữ liệu cùng lúc
}
void capNhat() {
uint8_t hang[SO_KHOI];
for (int r = 0; r < 8; r++) {
int hangThat = DAO_HANG ? 7 - r : r;
for (int k = 0; k < SO_KHOI; k++) {
uint8_t b = 0;
for (int c = 0; c < 8; c++) {
if (manHinh[k * 8 + c] >> hangThat & 1) b |= DAO_COT ? (1 << c) : (0x80 >> c);
}
hang[k] = b;
}
ghiTatCa(r + 1, 0, hang); // thanh ghi 1-8 là 8 hàng (với module loại phổ biến)
}
}
// Cột thứ i của dòng chữ (mỗi ký tự 5 cột + 1 cột trống).
uint8_t cotCuaChu(const char *chu, int i) {
int kyTu = i / 6, cot = i % 6;
char c = chu[kyTu];
if (c >= 'a' && c <= 'z') c -= 32; // font chỉ có chữ hoa
if (cot == 5 || c < 0x20 || c > 0x5A) return 0;
return FONT[c - 0x20][cot];
}
void setup() {
pinMode(CHAN_DIN, OUTPUT);
pinMode(CHAN_CLK, OUTPUT);
pinMode(CHAN_CS, OUTPUT);
digitalWrite(CHAN_CS, HIGH);
ghiTatCa(TG_THU_DEN, 0); // tắt chế độ thử đèn
ghiTatCa(TG_SO_HANG, 7); // quét đủ 8 hàng
ghiTatCa(TG_GIAI_MA, 0); // không giải mã BCD: tự điều khiển từng điểm
ghiTatCa(TG_DO_SANG, DO_SANG);
ghiTatCa(TG_TAT, 1); // thoát chế độ tắt, bắt đầu hiển thị
}
void loop() {
static int viTri = 0;
int tongCot = strlen(NOI_DUNG) * 6;
for (int x = 0; x < SO_KHOI * 8; x++) manHinh[x] = cotCuaChu(NOI_DUNG, (viTri + x) % tongCot);
capNhat();
viTri = (viTri + 1) % tongCot;
delay(60); // tốc độ chạy chữ
}

- Font 5 × 7 lấy từ bộ chữ chuẩn của thư viện Adafruit GFX: mỗi ký tự 5 cột, mỗi cột một byte, bit 0 là điểm trên cùng. Bảng chỉ có chữ hoa, số và vài dấu nên chương trình đổi chữ thường thành chữ hoa; chữ có dấu tiếng Việt không hiện được.
manHinh[]là bộ đệm 32 cột.loop()chép 32 cột của dòng chữ bắt đầu từviTrivào bộ đệm, gửi ra, rồi tăngviTri: chữ dịch sang trái mỗi 60 ms.capNhat()ghép 8 cột của mỗi khối thành từng byte hàng rồi gửi 8 lần (8 hàng).- Ba cờ
DAO_COT,DAO_HANG,DAO_KHOI: các nhà sản xuất nối LED vào MAX7219 theo hướng khác nhau. Nạp xong mà chữ bị lật ngang, lộn ngược hoặc các khối hiện sai thứ tự thì đổi cờ tương ứng rồi nạp lại.
6. TM1637: mã 7 đoạn và ba lệnh

Mỗi chữ số có 7 đoạn đặt tên a đến g. Mỗi đoạn là một bit: bit 0 là đoạn a, bit 6 là đoạn g. Ví dụ số 1 chỉ sáng đoạn b và c nên mã là 0x06. Với module đồng hồ, bit 7 của ô số thứ hai bật dấu hai chấm.
TM1637 dùng 2 dây nhưng không phải I2C: không có địa chỉ, bit thấp gửi trước. Mỗi lần hiển thị gửi ba lệnh: 0x40 (ghi dữ liệu, tự tăng ô), 0xC0 (bắt đầu từ ô 0) kèm 4 byte mã số, và 0x88 cộng độ sáng (bật màn). Sau mỗi byte có xung thứ 9 để chip kéo DIO xuống báo đã nhận (ACK).
7. Chương trình đồng hồ TM1637
// PYWORLD.VN - LED 7 đoạn 4 số TM1637 với ESP32: đồng hồ đếm phút:giây, dấu hai chấm nhấp nháy, không cần thư viện
// CLK vào D22, DIO vào D21; VCC vào 3V3 (module có điện trở kéo lên tới VCC: cấp 5 V sẽ đưa 5 V vào chân ESP32), GND chung
// Bấm nút BOOT để đổi độ sáng 0-7
const int CHAN_CLK = 22;
const int CHAN_DIO = 21;
const int CHAN_BOOT = 0;
// Mã 7 đoạn cho số 0-9: bit 0 = đoạn a ... bit 6 = đoạn g, bit 7 = dấu hai chấm (ở ô số 2)
const uint8_t MA_SO[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};
// TM1637 không phải I2C: hai chân chỉ kéo xuống thấp hoặc nhả ra để điện trở kéo lên.
void thap(int chan) { pinMode(chan, OUTPUT); digitalWrite(chan, LOW); }
void nha(int chan) { pinMode(chan, INPUT); }
void cho() { delayMicroseconds(5); }
void batDau() { // DIO xuống thấp trong lúc CLK đang cao
nha(CHAN_CLK); nha(CHAN_DIO); cho();
thap(CHAN_DIO); cho();
}
void ketThuc() { // DIO lên cao trong lúc CLK đang cao
thap(CHAN_CLK); thap(CHAN_DIO); cho();
nha(CHAN_CLK); cho();
nha(CHAN_DIO); cho();
}
bool guiByte(uint8_t b) { // bit thấp gửi trước, xung thứ 9 đọc ACK
for (int i = 0; i < 8; i++) {
thap(CHAN_CLK); cho();
if (b & 1) nha(CHAN_DIO); else thap(CHAN_DIO);
cho();
nha(CHAN_CLK); cho();
b >>= 1;
}
thap(CHAN_CLK); nha(CHAN_DIO); cho();
nha(CHAN_CLK); cho();
bool coAck = digitalRead(CHAN_DIO) == LOW; // TM1637 kéo DIO xuống để báo đã nhận
thap(CHAN_CLK); cho();
return coAck;
}
bool hienThi(const uint8_t ma[4], uint8_t doSang) {
batDau();
bool ok = guiByte(0x40); // lệnh dữ liệu: ghi, tự tăng địa chỉ
ketThuc();
batDau();
ok &= guiByte(0xC0); // lệnh địa chỉ: bắt đầu từ ô số 0
for (int i = 0; i < 4; i++) ok &= guiByte(ma[i]);
ketThuc();
batDau();
ok &= guiByte(0x88 | (doSang & 7)); // lệnh hiển thị: bật màn, độ sáng 0-7
ketThuc();
return ok;
}
void setup() {
Serial.begin(115200);
pinMode(CHAN_BOOT, INPUT_PULLUP);
uint8_t thu[4] = {0x7F | 0x80, 0x7F | 0x80, 0x7F | 0x80, 0x7F | 0x80};
Serial.println(hienThi(thu, 7) ? "TM1637 tra loi ACK" : "Khong co ACK: kiem tra CLK, DIO, nguon");
delay(1000); // sáng hết các đoạn 1 giây để kiểm tra
}
void loop() {
static uint8_t doSang = 4;
static bool nutTruoc = HIGH;
bool nut = digitalRead(CHAN_BOOT);
if (nutTruoc == HIGH && nut == LOW) {
doSang = (doSang + 1) % 8;
Serial.printf("Do sang %u\n", doSang);
}
nutTruoc = nut;
uint32_t giay = millis() / 1000;
uint32_t phut = (giay / 60) % 100, s = giay % 60;
uint8_t ma[4] = {MA_SO[phut / 10], MA_SO[phut % 10], MA_SO[s / 10], MA_SO[s % 10]};
if ((millis() / 500) % 2 == 0) ma[1] |= 0x80; // dấu hai chấm sáng nửa giây, tắt nửa giây
hienThi(ma, doSang);
delay(50);
}
thap(),nha(): chân không bao giờ được đẩy lên cao bằngdigitalWrite(HIGH). Muốn mức cao thì chuyển chân sangINPUTđể điện trở kéo lên làm việc. Như vậy TM1637 kéo DIO xuống để báo ACK mà không gây chập với ESP32.guiByte()gửi 8 bit (bit thấp trước), rồi tạo xung thứ 9 và đọc DIO: mức thấp là chip đã nhận.- Kiểm tra khi khởi động: chương trình bật hết các đoạn trong 1 giây và in ra Serial Monitor có ACK hay không: cách nhanh để biết nối dây đúng chưa.
- Đồng hồ tính phút, giây từ
millis(); dấu hai chấm sáng nửa giây, tắt nửa giây. Bấm nút BOOT để đổi độ sáng.
8. Kết quả trên Serial Monitor

Muốn đồng hồ chạy đúng giờ thật thay vì đếm từ lúc bật nguồn, lấy giờ từ module thời gian thực DS3231 (bài cùng loạt) hoặc từ Internet như bài đồng hồ Internet với ESP32 và NTP.
9. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Ma trận sáng hết hoặc tắt hết, không hiện chữ | Nối vào đầu DOUT; quên nối CS; module ở chế độ thử đèn | Nối vào đầu DIN; kiểm tra dây CS |
| Chữ bị lật, lộn ngược, sai thứ tự khối | Module nối LED theo hướng khác | Đổi các cờ DAO_COT, DAO_HANG, DAO_KHOI |
| Chữ chập chờn, có điểm lạ | Mức 3,3 V sát ngưỡng, dây dài, nguồn yếu | Dây ngắn; giảm DO_SANG; thêm IC chuyển mức |
| ESP32 khởi động lại khi ma trận sáng nhiều | 4 khối ăn dòng lớn qua cổng USB | Giảm độ sáng; cấp 5 V riêng cho module, nối chung GND |
| TM1637 "Khong co ACK" | Đảo CLK với DIO; chưa cấp nguồn | Đổi lại hai dây; đo VCC trên module |
| TM1637 sáng mờ | Cấp 3,3 V; độ sáng thấp | Bấm BOOT tăng độ sáng; cần sáng hơn thì cấp 5 V kèm chuyển mức cho DIO |
10. Xem thêm
- SPI là gì: kiểu giao tiếp mà MAX7219 dùng.
- Đồng hồ Internet với ESP32, NTP: lấy giờ thật để hiện lên LED 7 đoạn.
- LCD 1602, 2004 I2C và OLED 0.96 inch của shop: khi cần hiện nhiều chữ hơn.
Xem thêm