Dây LED RGB WS2812 đổi màu, hiệu ứng bằng điện thoại (30 bóng)
Dây LED 7 màu WS2812B 30 bóng (0,5m dây 60 bóng/m hoặc 1m dây 30 bóng/m) chọn màu và 10 hiệu ứng (cầu vồng, sao băng, thở, lửa…) trên điện thoại qua WiFi. Code tự giới hạn tổng dòng dưới 0,6A nên chạy được bằng cục sạc điện thoại 5V 1A.
✅ Đã kiểm thử
Xem chi tiết 3 phép đo
Đang gửi dữ liệu (DIN mức cao), dây LED sáng ở mức giới hạn
- Tổng dòng đỉnh 850.1mA: dùng cục sạc điện thoại 5V ≥1A, cổng USB máy tính có thể không đủ.
- Mức tín hiệu tới chân DIN: 3.29V ✓
- Dòng chân GPIO13: 0.0mA ✓
- Áp nuôi dây LED: 5.00V ✓
- LS1.DIN cần mức cao ≥3.5V, ESP32 DevKit V1 chỉ xuất 3.3V. Thường vẫn chạy với dây ngắn; nếu chập chờn hãy thêm IC chuyển mức 74AHCT125.
- Tổng dòng đỉnh 850.1mA: dùng cục sạc điện thoại 5V ≥1A, cổng USB máy tính có thể không đủ.
Nguyên lý hoạt động
Mỗi bóng WS2812B có chip riêng, nhận chuỗi dữ liệu màu qua một dây DIN rồi chuyển tiếp cho bóng sau. ESP32 gửi dữ liệu từ GPIO13 qua trở 330Ω (chống dội tín hiệu, bảo vệ bóng đầu). Tụ 1000µF đặt ở đầu dây LED dập xung dòng lúc bật. Mỗi bóng trắng tối đa ăn 60mA, 30 bóng có thể tới 1,8A – vượt sức cục sạc; vì vậy code tính tổng dòng mỗi khung hình và tự giảm độ sáng để luôn dưới 0,6A. Mức 3,3V của ESP32 thấp hơn chuẩn 3,5V của WS2812 nhưng với dây tín hiệu ngắn (<30cm) chạy ổn định.
Sơ đồ đấu dây
Mỗi màu là một dây. Chấm tròn là điểm hàn. Chạm vào tên dây trong bảng để làm nổi bật.
| # | Dây | Nối các điểm |
|---|---|---|
| 1 | đỏ 5V | U1 VIN ⟷ LS1 5V ⟷ C1 + |
| 2 | đen GND | U1 GND ⟷ LS1 GND ⟷ C1 - |
| 3 | vàng DIN_DRV | U1 GPIO13 (D13) ⟷ R1 1 |
| 4 | xanh lá DIN | R1 2 ⟷ LS1 DIN |
Danh sách linh kiện
| Ký hiệu | Linh kiện | Giá tham khảo | |
|---|---|---|---|
| U1 | ESP32 DevKit V1 30 chân (ESP32-WROOM-32) | 110.000đ | Tìm mua |
| LS1 | Dây LED WS2812B 5V (30 bóng; loại 60 bóng/m cắt 0,5m) | 45.000đ | Tìm mua |
| R1 | Điện trở 330Ω 1/4W (cam-cam-nâu) | 200đ | Tìm mua |
| C1 | Tụ hóa 1000µF 10V (hoặc 16V) (1000u) | 3.000đ | Tìm mua |
| Tổng (chưa gồm board hàn, dây, thiếc) | 158.200đ |
Các bước làm
Code nạp cho mạch
Cài thư viện: Adafruit NeoPixel (Arduino IDE → Library Manager). Chọn board DOIT ESP32 DEVKIT V1.
/*
Mạch Gọn — Dây LED RGB WS2812B điều khiển qua WiFi
Board : ESP32 DevKit V1 (chọn "DOIT ESP32 DEVKIT V1")
Thư viện: Adafruit NeoPixel
Chân : GPIO13 -> trở 330Ω -> DIN dây LED; dây LED cấp từ VIN (5V USB)
Lần đầu: vào WiFi "MachGon-RGB" (mật khẩu machgon123) -> mở 192.168.4.1
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Preferences.h>
#include <Adafruit_NeoPixel.h>
// ---- Cấu hình ----
const uint8_t PIN_DATA = 13;
const uint16_t NUM_LEDS = 30;
const uint16_t MAX_MA = 550; // giới hạn tổng dòng dây LED (mA), chừa phần cho ESP32
const float MA_PER_CH = 20.0; // mA mỗi kênh màu ở mức 255
const char* AP_SSID = "MachGon-RGB";
const char* HOSTNAME = "rgb";
Adafruit_NeoPixel strip(NUM_LEDS, PIN_DATA, NEO_GRB + NEO_KHZ800);
WebServer server(80);
Preferences cfg;
enum { FX_SOLID, FX_RAINBOW, FX_RAINBOW_RUN, FX_BREATHE, FX_COMET, FX_TWINKLE, FX_FIRE, FX_CHASE, FX_WIPE, FX_POLICE, FX_COUNT };
const char* FX_NAMES[FX_COUNT] = {"Màu tĩnh", "Cầu vồng", "Cầu vồng chạy", "Thở", "Sao băng", "Lấp lánh", "Lửa", "Đuổi nhau", "Quét màu", "Đèn còi"};
uint8_t buf[NUM_LEDS][3];
uint8_t colR = 255, colG = 80, colB = 0;
int effect = FX_RAINBOW_RUN, bright = 60, speed = 50;
bool power = true;
uint32_t frame = 0;
uint16_t lastMa = 0;
void setPx(int i, uint8_t r, uint8_t g, uint8_t b) { if (i >= 0 && i < NUM_LEDS) { buf[i][0] = r; buf[i][1] = g; buf[i][2] = b; } }
void fillAll(uint8_t r, uint8_t g, uint8_t b) { for (int i = 0; i < NUM_LEDS; i++) setPx(i, r, g, b); }
void wheel(uint8_t pos, uint8_t& r, uint8_t& g, uint8_t& b) {
uint32_t c = Adafruit_NeoPixel::ColorHSV((uint16_t)pos * 256);
r = c >> 16; g = c >> 8; b = c;
}
void fadeAll(uint8_t k) { for (int i = 0; i < NUM_LEDS; i++) for (int j = 0; j < 3; j++) buf[i][j] = (uint16_t)buf[i][j] * k / 256; }
void renderEffect() {
uint8_t r, g, b;
switch (effect) {
case FX_SOLID: fillAll(colR, colG, colB); break;
case FX_RAINBOW: wheel(frame, r, g, b); fillAll(r, g, b); break;
case FX_RAINBOW_RUN: for (int i = 0; i < NUM_LEDS; i++) { wheel(frame * 2 + i * 256 / NUM_LEDS, r, g, b); setPx(i, r, g, b); } break;
case FX_BREATHE: { float k = 0.05f + 0.95f * (0.5f - 0.5f * cosf(frame * 0.05f)); fillAll(colR * k, colG * k, colB * k); break; }
case FX_COMET: { fadeAll(170); int p = frame % (NUM_LEDS + 8); setPx(p, colR, colG, colB); break; }
case FX_TWINKLE: { fadeAll(220); if (esp_random() % 3 == 0) { wheel(esp_random(), r, g, b); setPx(esp_random() % NUM_LEDS, r, g, b); } break; }
case FX_FIRE: for (int i = 0; i < NUM_LEDS; i++) { uint8_t h = 120 + esp_random() % 136; setPx(i, h, h / 3 + esp_random() % 20, 0); } break;
case FX_CHASE: for (int i = 0; i < NUM_LEDS; i++) (i + frame) % 3 == 0 ? setPx(i, colR, colG, colB) : setPx(i, 0, 0, 0); break;
case FX_WIPE: { int p = frame % (NUM_LEDS * 2); for (int i = 0; i < NUM_LEDS; i++) (p < NUM_LEDS ? i <= p : i > p - NUM_LEDS) ? setPx(i, colR, colG, colB) : setPx(i, 0, 0, 0); break; }
case FX_POLICE: { bool a = (frame / 4) % 2; for (int i = 0; i < NUM_LEDS; i++) (i < NUM_LEDS / 2) == a ? setPx(i, 255, 0, 0) : setPx(i, 0, 0, 255); break; }
}
}
// Áp độ sáng và giới hạn dòng rồi xuất ra dây LED
void showFrame() {
float k = power ? bright / 100.0f : 0;
float sum = 0;
for (int i = 0; i < NUM_LEDS; i++) sum += buf[i][0] + buf[i][1] + buf[i][2];
float ma = sum * k / 255.0f * MA_PER_CH + NUM_LEDS * 1.0f; // ~1mA/bóng cho chip
if (ma > MAX_MA) { k *= (MAX_MA - NUM_LEDS) / (ma - NUM_LEDS); ma = MAX_MA; }
lastMa = (uint16_t)ma;
for (int i = 0; i < NUM_LEDS; i++) strip.setPixelColor(i, buf[i][0] * k, buf[i][1] * k, buf[i][2] * k);
strip.show();
}
const char PAGE[] PROGMEM = R"HTML(<!doctype html><html lang="vi"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>LED RGB WiFi</title>
<style>
:root{--b:#0f172a;--c:#1e293b;--a:#a855f7;--t:#e2e8f0;--m:#94a3b8}*{box-sizing:border-box}
body{margin:0;font-family:system-ui,sans-serif;background:var(--b);color:var(--t)}
header{padding:12px 14px;background:var(--c);display:flex;justify-content:space-between;align-items:center}
.pw{border:0;border-radius:24px;padding:8px 18px;font:inherit;font-weight:700;background:#334155;color:var(--t)}.pw.on{background:var(--a);color:#fff}
section{margin:14px;padding:14px;background:var(--c);border-radius:12px}h3{margin:0 0 10px;font-size:16px}
input[type=range]{width:100%;accent-color:var(--a);height:32px}input[type=color]{width:100%;height:56px;border:0;background:none}
.sw{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}.sw b{width:36px;height:36px;border-radius:50%;display:inline-block;border:2px solid #334155}
.fx{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.fx button{font:inherit;padding:11px;border-radius:10px;border:1px solid #334155;background:#0b1220;color:var(--t)}
.fx button.on{background:var(--a);border-color:var(--a)}small{color:var(--m)}
input:not([type]),input[type=password]{width:100%;font:inherit;padding:9px;margin:4px 0 8px;border-radius:8px;border:1px solid #475569;background:#0b1220;color:var(--t)}
.s{font:inherit;border:0;border-radius:8px;padding:9px 14px;background:var(--a);color:#fff;font-weight:600}
</style></head><body>
<header><b>🌈 LED RGB WiFi</b><button class="pw" id="pw">…</button></header>
<section><h3>Màu</h3><input type="color" id="col"><div class="sw" id="sw"></div></section>
<section><h3>Hiệu ứng</h3><div class="fx" id="fx"></div></section>
<section><h3>Độ sáng <small id="ma"></small></h3><input type="range" id="b" min="1" max="100"><h3>Tốc độ</h3><input type="range" id="sp" min="1" max="100"></section>
<section><h3>Cài đặt WiFi nhà</h3><small id="wst"></small><input id="ssid" placeholder="Tên WiFi"><input id="pass" type="password" placeholder="Mật khẩu"><button class="s" onclick="wifi()">Lưu & khởi động lại</button></section>
<script>
const $=s=>document.querySelector(s);let S={},tm;const SW=['#ff0000','#ff6a00','#ffd000','#00ff40','#00d0ff','#0040ff','#a000ff','#ff00a0','#ffffff'];
async function api(p,o){const r=await fetch(p,o);return r.json()}
function draw(s){S=s;$('#pw').textContent=s.power?'BẬT':'TẮT';$('#pw').className='pw'+(s.power?' on':'');$('#b').value=s.b;$('#sp').value=s.sp;$('#col').value=s.col;
$('#ma').textContent='~'+s.ma+'mA (giới hạn '+s.max+'mA)';
$('#fx').innerHTML=s.names.map((n,i)=>`<button class="${i==s.fx?'on':''}" onclick="set('fx=${i}')">${n}</button>`).join('');
$('#wst').textContent=s.sta?('Đã nối "'+s.ssid+'" — IP '+s.ip+' (hoặc http://rgb.local)'):'Chưa nối WiFi nhà'}
async function set(q){draw(await api('/api/set?'+q,{method:'POST'}))}
$('#sw').innerHTML=SW.map(c=>`<b style="background:${c}" onclick="set('c=${c.slice(1)}')"></b>`).join('');
$('#col').oninput=e=>{clearTimeout(tm);tm=setTimeout(()=>set('c='+e.target.value.slice(1)),80)};
$('#b').oninput=e=>{clearTimeout(tm);tm=setTimeout(()=>set('b='+e.target.value),80)};$('#sp').onchange=e=>set('sp='+e.target.value);
$('#pw').onclick=()=>set('pw='+(S.power?0:1));
async function wifi(){await fetch('/api/wifi',{method:'POST',body:new URLSearchParams({ssid:$('#ssid').value,pass:$('#pass').value})});alert('Đã lưu, mạch khởi động lại. Nối lại WiFi nhà rồi mở http://rgb.local')}
api('/api/state').then(draw);setInterval(()=>api('/api/state').then(s=>$('#ma').textContent='~'+s.ma+'mA (giới hạn '+s.max+'mA)'),3000);
</script></body></html>)HTML";
String stateJson() {
String names = "[";
for (int i = 0; i < FX_COUNT; i++) { if (i) names += ','; names += '"'; names += FX_NAMES[i]; names += '"'; }
names += "]";
char col[8]; snprintf(col, sizeof(col), "#%02x%02x%02x", colR, colG, colB);
bool sta = WiFi.status() == WL_CONNECTED;
return String("{\"b\":") + bright + ",\"sp\":" + speed + ",\"fx\":" + effect + ",\"power\":" + (power ? "true" : "false") +
",\"col\":\"" + col + "\",\"ma\":" + lastMa + ",\"max\":" + MAX_MA + ",\"names\":" + names +
",\"sta\":" + (sta ? "true" : "false") + ",\"ip\":\"" + (sta ? WiFi.localIP().toString() : WiFi.softAPIP().toString()) +
"\",\"ssid\":\"" + cfg.getString("ssid", "") + "\"}";
}
void saveState() {
cfg.putInt("fx", effect); cfg.putInt("b", bright); cfg.putInt("sp", speed); cfg.putBool("pw", power);
cfg.putUInt("col", ((uint32_t)colR << 16) | (colG << 8) | colB);
}
void setupWeb() {
server.on("/", []() { server.send_P(200, "text/html; charset=utf-8", PAGE); });
server.on("/api/state", HTTP_GET, []() { server.send(200, "application/json", stateJson()); });
server.on("/api/set", []() {
if (server.hasArg("fx")) effect = constrain(server.arg("fx").toInt(), 0, FX_COUNT - 1);
if (server.hasArg("b")) bright = constrain(server.arg("b").toInt(), 1, 100);
if (server.hasArg("sp")) speed = constrain(server.arg("sp").toInt(), 1, 100);
if (server.hasArg("pw")) power = server.arg("pw") == "1";
if (server.hasArg("c")) { uint32_t c = strtoul(server.arg("c").c_str(), nullptr, 16); colR = c >> 16; colG = c >> 8; colB = c; }
saveState();
server.send(200, "application/json", stateJson());
});
server.on("/api/wifi", HTTP_POST, []() {
cfg.putString("ssid", server.arg("ssid")); cfg.putString("pass", server.arg("pass"));
server.send(200, "application/json", "{\"ok\":true}"); delay(500); ESP.restart();
});
server.onNotFound([]() { server.sendHeader("Location", "/"); server.send(302); });
server.begin();
}
void setup() {
Serial.begin(115200);
strip.begin(); strip.clear(); strip.show();
cfg.begin("cfg", false);
effect = constrain(cfg.getInt("fx", FX_RAINBOW_RUN), 0, FX_COUNT - 1);
bright = cfg.getInt("b", 60); speed = cfg.getInt("sp", 50); power = cfg.getBool("pw", true);
uint32_t c = cfg.getUInt("col", 0xFF5000); colR = c >> 16; colG = c >> 8; colB = c;
memset(buf, 0, sizeof(buf));
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(AP_SSID, cfg.getString("appass", "machgon123").c_str());
String ssid = cfg.getString("ssid", "");
if (ssid.length()) {
WiFi.begin(ssid.c_str(), cfg.getString("pass", "").c_str());
uint32_t t = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t < 15000) delay(250);
}
if (MDNS.begin(HOSTNAME)) MDNS.addService("http", "tcp", 80);
setupWeb();
Serial.printf("San sang. AP %s IP %s\n", AP_SSID, WiFi.softAPIP().toString().c_str());
}
void loop() {
server.handleClient();
static uint32_t last = 0;
uint32_t interval = map(speed, 1, 100, 120, 8);
if (millis() - last >= interval) { last = millis(); frame++; renderEffect(); showFrame(); }
static uint32_t lastCheck = 0;
if (millis() - lastCheck > 30000) {
lastCheck = millis();
if (cfg.getString("ssid", "").length() && WiFi.status() != WL_CONNECTED) WiFi.reconnect();
}
}
Đo kiểm bằng đồng hồ vạn năng
| +5V và GND ở đầu dây LED khi đang sáng | 4,5 – 5,1V |
| GPIO13 và GND | 1 – 2V (đang truyền dữ liệu liên tục, đồng hồ đọc trung bình) |
Sửa lỗi thường gặp
Dây LED không sáng bóng nào
Hay gặp nhất: hàn vào đầu DO thay vì DIN. Kiểm mũi tên. Kiểm GND chung.
Màu sai (đỏ ra xanh)
Dây của bạn là loại thứ tự RGB. Trong code đổi NEO_GRB thành NEO_RGB.
Nhấp nháy loạn, bóng đầu sáng sai màu
Dây tín hiệu quá dài hoặc thiếu trở 330Ω. Rút ngắn dây; nếu vẫn lỗi thêm IC chuyển mức 74AHCT125.
ESP32 tự khởi động lại
Cục sạc yếu. Dùng loại 5V 2A, hoặc giảm MAX_MA trong code.
⚠️ An toàn
- Mạch chạy 5V, an toàn.
- Muốn dùng dây dài (>60 bóng) phải có nguồn 5V riêng công suất lớn, không cấp qua USB.