Dải LED WS2812B với ESP32: điều khiển từng bóng bằng RMT không cần thư viện, tính dòng và chọn nguồn
Bên trong bóng WS2812B, tín hiệu một dây 24 bit GRB, cách tính dòng để chọn nguồn, nối dải LED với ESP32 và tạo tín hiệu bằng bộ RMT không cần thư viện, kèm ba hiệu ứng.
Dải LED WS2812B (hay gặp với tên "LED NeoPixel", "LED RGB địa chỉ") cho phép chỉnh màu và độ sáng của từng bóng riêng chỉ bằng một dây tín hiệu. Đây là loại LED dùng cho đèn trang trí, bảng hiệu chữ chạy, đèn hắt màn hình, đèn báo trạng thái thiết bị. Bài này giải thích bên trong một bóng WS2812B, tín hiệu điều khiển, cách tính dòng điện để chọn nguồn, rồi viết chương trình ESP32 tạo tín hiệu bằng bộ RMT có sẵn trong chip, không cần thư viện, chạy ba hiệu ứng: chấm màu chạy, cầu vồng, sáng tắt như hơi thở.
1. WS2812B là gì

Mỗi bóng WS2812B cỡ 5 × 5 mm chứa ba chip LED đỏ, xanh lá, xanh dương và một chip điều khiển nhỏ. Chip điều khiển nhận màu qua chân DIN, giữ lại phần dành cho nó, rồi chuyển phần còn lại sang chân DO để bóng sau nhận tiếp. Nhờ cách nối nối tiếp này, cả dải hàng trăm bóng chỉ cần ba dây: 5 V, GND và dữ liệu.
| Thông số | WS2812B (theo datasheet) |
|---|---|
| Điện áp | 5 V (3,5–5,3 V) |
| Màu | Mỗi màu 256 mức, 24 bit một bóng, khoảng 16,7 triệu màu |
| Dòng tối đa mỗi màu | Khoảng 20 mA, tức khoảng 60 mA một bóng khi sáng trắng hết cỡ |
| Tín hiệu | Một dây, 800 kbit/s, mỗi bit 1,25 µs |
| Loại dải thường gặp | 30, 60, 144 bóng mỗi mét; có loại bọc silicon chống nước |
2. Linh kiện cần chuẩn bị
- ESP32 DevKit V1 30 chân (hoặc loại 38 chân).
- Dải LED WS2812B (bài dùng 30 bóng), một điện trở 330 Ω, một tụ hóa 1000 µF 10 V trở lên.
- Nguồn 5 V đủ dòng (xem mục 5). Có adapter 12 V thì hạ xuống 5 V bằng mạch hạ áp LM2596 (dòng tối đa khoảng 3 A, nên dùng cho dải ngắn).
- Dây cắm, breadboard, dây USB nạp code.
3. Sơ đồ nối dây

| Dải LED | Nối vào |
|---|---|
| DIN (đầu có mũi tên chỉ vào) | D13 (GPIO13) qua điện trở 330 Ω |
| +5V | Cực dương nguồn 5 V riêng |
| GND | Cực âm nguồn 5 V và GND của ESP32 |
- Đúng đầu vào: trên dải có mũi tên in cạnh chân DIN chỉ chiều tín hiệu. Nối nhầm vào đầu DO thì không bóng nào sáng.
- Tụ 1000 µF giữa +5V và GND ở đầu dải giúp nguồn không sụt khi cả dải đổi màu cùng lúc.
- Mức tín hiệu 3,3 V: WS2812B cấp 5 V cần mức cao tối thiểu khoảng 3,5 V, cao hơn mức 3,3 V của ESP32 một chút. Phần lớn dải vẫn nhận đúng khi dây dữ liệu ngắn. Gặp hiện tượng nhấp nháy sai màu thì thêm IC chuyển mức 74AHCT125 hoặc 74HCT245; lý do có trong bài mức logic 3,3 V và 5 V.
4. Tín hiệu điều khiển

Mỗi bit dài khoảng 1,2 µs. Bit 0 có mức cao ngắn (0,4 µs), bit 1 có mức cao dài (0,8 µs). Mỗi bóng nhận 24 bit theo thứ tự xanh lá, đỏ, xanh dương (không phải đỏ trước), bit cao nhất gửi trước. Gửi xong cả dải thì giữ dây ở mức thấp hơn 280 µs: các bóng hiểu đó là hết khung và cùng lúc đổi sang màu mới.
Thời gian tính bằng phần mười micro giây nên không thể bật tắt chân GPIO bằng digitalWrite() rồi chờ: chỉ một ngắt Wi-Fi chen vào là sai bit. ESP32 có bộ RMT (Remote Control Transceiver) vốn làm ra để phát tín hiệu hồng ngoại: nó nhận một bảng "mức cao bao lâu, mức thấp bao lâu" rồi tự phát ra chân bằng phần cứng, chính xác tới 0,1 µs, CPU không phải làm gì.
5. Dải LED tốn bao nhiêu dòng

Đây là phần hay bị bỏ qua nhất. Mỗi màu sáng hết cỡ ăn khoảng 20 mA, nên một bóng trắng hết cỡ khoảng 60 mA. 30 bóng trắng là gần 2 A, cuộn 5 m loại 30 bóng/m sáng trắng cần khoảng 9 A. Cổng USB máy tính chỉ cho 0,5–0,9 A. Vì vậy:
- Chọn nguồn 5 V chịu được dòng lớn hơn dòng ước tính 20–30 %.
- Hiệu ứng cầu vồng tốn ít dòng hơn tưởng: mỗi bóng chỉ sáng tổng cộng một "phần màu" (tổng đỏ + xanh lá + xanh dương luôn bằng 255), nên chỉ bằng một dải toàn màu đỏ.
- Dải dài trên 1–2 m thì điện áp tụt dần về cuối dải, các bóng cuối ngả vàng đỏ. Cách xử lý là cấp nguồn thêm ở cuối dải (hoặc cả hai đầu và giữa) bằng dây to.
- Chương trình có biến
DONG_NGUON_MA: khi dòng ước tính vượt mức này, độ sáng chung tự giảm cho vừa nguồn.
6. Chương trình điều khiển WS2812B bằng RMT
Sửa SO_LED đúng số bóng của dải và DONG_NGUON_MA theo nguồn đang dùng, chọn bo ESP32 Dev Module rồi nạp. Bài dùng gói esp32 phiên bản 3.3.10, chỉ dùng hàm RMT có sẵn.
// PYWORLD.VN - Dải LED WS2812B với ESP32: tạo tín hiệu bằng bộ RMT, không cần thư viện; chạy màu, cầu vồng, thở
// DIN của dải LED nối D13 qua điện trở 330 Ω; dải LED cấp 5 V từ nguồn riêng, GND chung với ESP32
// Tự giảm độ sáng khi dòng ước tính vượt DONG_NGUON_MA để không quá tải nguồn
const int CHAN_DIN = 13;
const int SO_LED = 30;
const uint32_t DONG_NGUON_MA = 1500; // dòng nguồn 5 V chịu được (mA), chừa dư 20-30 %
const uint8_t DO_SANG_TOI_DA = 128; // 0-255, giới hạn chung cho dịu mắt
uint8_t mau[SO_LED][3]; // màu từng LED theo thứ tự R, G, B
rmt_data_t kyHieu[SO_LED * 24]; // mỗi LED 24 bit, mỗi bit một ký hiệu RMT
// RMT chạy 10 MHz: 1 nhịp = 0,1 us. Bit 0: cao 0,4 us, thấp 0,8 us. Bit 1: cao 0,8 us, thấp 0,4 us.
const rmt_data_t BIT_0 = {{4, 1, 8, 0}};
const rmt_data_t BIT_1 = {{8, 1, 4, 0}};
// Dòng ước tính (mA): mỗi màu sáng hết cỡ khoảng 20 mA, cộng 1 mA cho mạch điều khiển của mỗi LED.
uint32_t dongUocTinh(uint8_t doSang) {
uint32_t tong = 0;
for (int i = 0; i < SO_LED; i++) tong += mau[i][0] + mau[i][1] + mau[i][2];
return tong * doSang / 255 * 20 / 255 + SO_LED;
}
uint32_t hienThi() {
uint8_t doSang = DO_SANG_TOI_DA;
uint32_t dong = dongUocTinh(doSang);
if (dong > DONG_NGUON_MA) { // quá dòng: hạ độ sáng chung cho vừa nguồn
doSang = (uint32_t)doSang * (DONG_NGUON_MA - SO_LED) / (dong - SO_LED);
dong = dongUocTinh(doSang);
}
int k = 0;
for (int i = 0; i < SO_LED; i++) {
uint8_t g = mau[i][1] * doSang / 255, r = mau[i][0] * doSang / 255, b = mau[i][2] * doSang / 255;
uint32_t grb = ((uint32_t)g << 16) | ((uint32_t)r << 8) | b; // WS2812B nhận thứ tự G, R, B
for (int bit = 23; bit >= 0; bit--) kyHieu[k++] = (grb >> bit) & 1 ? BIT_1 : BIT_0;
}
rmtWrite(CHAN_DIN, kyHieu, SO_LED * 24, RMT_WAIT_FOR_EVER);
delayMicroseconds(300); // giữ mức thấp trên 280 us: dải LED chốt màu mới
return dong;
}
void datMau(int i, uint8_t r, uint8_t g, uint8_t b) {
mau[i][0] = r;
mau[i][1] = g;
mau[i][2] = b;
}
// Bánh xe màu: vi_tri 0-255 chạy qua đỏ, xanh lá, xanh dương rồi về đỏ.
void banhXeMau(int i, uint8_t viTri) {
if (viTri < 85) datMau(i, 255 - viTri * 3, viTri * 3, 0);
else if (viTri < 170) datMau(i, 0, 255 - (viTri - 85) * 3, (viTri - 85) * 3);
else datMau(i, (viTri - 170) * 3, 0, 255 - (viTri - 170) * 3);
}
void setup() {
Serial.begin(115200);
if (!rmtInit(CHAN_DIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 10000000)) {
Serial.println("Loi khoi dong RMT");
}
for (int i = 0; i < SO_LED; i++) datMau(i, 0, 0, 0);
hienThi();
Serial.printf("%d LED o chan GPIO%d, nguon %lu mA\n", SO_LED, CHAN_DIN, (unsigned long)DONG_NGUON_MA);
}
void loop() {
static uint32_t batDauCheDo = 0;
static int cheDo = -1;
static uint32_t buoc = 0;
if (cheDo < 0 || millis() - batDauCheDo > 10000) { // 10 giây đổi hiệu ứng
cheDo = (cheDo + 1) % 3;
batDauCheDo = millis();
buoc = 0;
Serial.printf("Hieu ung %d: %s\n", cheDo, cheDo == 0 ? "chay mau" : cheDo == 1 ? "cau vong" : "tho trang");
}
if (cheDo == 0) { // một chấm xanh chạy dọc dải, có đuôi mờ dần
for (int i = 0; i < SO_LED; i++) {
int khoangCach = (int)(buoc % SO_LED) - i;
if (khoangCach < 0) khoangCach += SO_LED;
uint8_t sang = khoangCach < 5 ? 255 >> (khoangCach * 2) : 0;
datMau(i, 0, sang / 3, sang);
}
} else if (cheDo == 1) { // cầu vồng trôi
for (int i = 0; i < SO_LED; i++) banhXeMau(i, (i * 256 / SO_LED + buoc * 3) & 255);
} else { // cả dải trắng sáng lên tắt xuống như hơi thở
uint8_t sang = (uint8_t)(127.5f * (1 - cosf(buoc * 0.05f)));
for (int i = 0; i < SO_LED; i++) datMau(i, sang, sang, sang);
}
uint32_t dong = hienThi();
if (buoc % 50 == 0) Serial.printf("Dong uoc tinh %lu mA\n", (unsigned long)dong);
buoc++;
delay(20);
}
7. Giải thích các phần chính
rmtInit(CHAN_DIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 10000000)cho bộ RMT chạy 10 MHz, mỗi nhịp 0,1 µs.BIT_0,BIT_1là hai ký hiệu RMT: mức cao 4 hoặc 8 nhịp, mức thấp 8 hoặc 4 nhịp. Mỗi bóng cần 24 ký hiệu, nên mảngkyHieucóSO_LED × 24phần tử.hienThi()ghép từng bóng thành số 24 bit theo thứ tự G, R, B, biến từng bit thành ký hiệu, gọirmtWrite()rồi chờ 300 µs để các bóng chốt màu.dongUocTinh()cộng độ sáng ba màu của mọi bóng để ước tính dòng. Nếu vượtDONG_NGUON_MA, hệ số độ sáng chung được hạ xuống theo tỉ lệ.banhXeMau()đổi một số 0–255 thành màu đi vòng đỏ → xanh lá → xanh dương → đỏ; mỗi bóng lệch một chút nên thành dải cầu vồng.- Hiệu ứng thở dùng hàm cosin để độ sáng tăng giảm mềm, mắt nhìn tự nhiên hơn tăng giảm đều.
Thư viện Adafruit NeoPixel hoặc FastLED cũng dùng RMT trên ESP32 và có sẵn nhiều hiệu ứng. Chương trình tự viết giúp hiểu tín hiệu và kiểm soát được dòng điện.
8. Kết quả trên Serial Monitor

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ý |
|---|---|---|
| Không bóng nào sáng | Nối vào đầu DO; thiếu dây GND chung; nguồn chưa bật | Nối vào đầu có mũi tên chỉ vào; nối GND nguồn với GND ESP32 |
| Bóng đầu sáng sai màu, nhấp nháy ngẫu nhiên | Mức 3,3 V không đủ, dây dữ liệu dài, thiếu điện trở 330 Ω | Rút ngắn dây; thêm điện trở; dùng IC chuyển mức 74AHCT125 |
| Màu đỏ hiện thành xanh lá | Gửi sai thứ tự màu | WS2812B nhận G, R, B; một số loại LED khác nhận R, G, B |
| Cuối dải ngả vàng, đỏ khi bật trắng | Sụt áp dọc dải | Cấp nguồn thêm ở cuối dải; giảm độ sáng |
| ESP32 khởi động lại khi bật cả dải | Dải LED lấy chung nguồn USB với ESP32, nguồn yếu | Nguồn riêng cho dải; tụ 1000 µF; hạ DONG_NGUON_MA |
| Một bóng hỏng, các bóng sau không sáng | Chip điều khiển của bóng đó hỏng nên không chuyển tín hiệu | Cắt bỏ bóng hỏng, nối DO của bóng trước sang DIN của bóng sau |
10. Xem thêm
- ESP32 làm web server: chọn màu dải LED từ điện thoại.
- Cảm biến chuyển động PIR: dải LED tự sáng khi có người đi qua.
- PWM trên ESP32: cách chỉnh độ sáng LED thường, so với LED có địa chỉ.
- Bài dự án đèn LED nháy theo nhạc trong cùng loạt dùng dải WS2812B với micro I2S.
Xem thêm