Chỉnh độ sáng dây LED 12V bằng điện thoại (dimmer WiFi, hiệu ứng thở/nhấp nháy)
Điều chỉnh độ sáng dây LED 12V đơn màu (tới 1A, khoảng 2–4m tùy loại) bằng thanh trượt trên điện thoại, có hiệu ứng thở, nhấp nháy, nến. Dùng chung một adapter 12V cho cả dây LED và ESP32 nhờ module hạ áp 5V. MOSFET IRLZ44N đóng cắt êm, không nóng.
✅ Đã kiểm thử
Xem chi tiết 8 phép đo
Sáng 100% (GPIO25 mức cao liên tục)
- Q1: dẫn 997.2mA, Vds=0.03V.
- Chân GPIO25 cấp 0.3mA (an toàn ≤ 20.0mA).
- Dòng qua dây LED: 997.2mA ✓
- Sụt áp trên MOSFET (càng nhỏ càng mát): 0.03V ✓
- Áp ra module hạ áp cấp ESP32: 5.00V ✓
- Tổng dòng adapter 12V phải cấp (adapter 2A): 1.12A ✓
Tắt (GPIO25 mức thấp)
- Dây LED tắt hẳn: 0.0mA ✓
Lúc ESP32 đang khởi động (chân thả nổi)
- Trở 10kΩ kéo cổng G xuống, LED không chớp lúc cắm điện: 0.0mA ✓
Nguyên lý hoạt động
Adapter 12V cấp thẳng cho dây LED và cho module hạ áp; module hạ xuống 5V nuôi ESP32 qua chân VIN. Cực âm dây LED đi qua MOSFET IRLZ44N xuống GND. ESP32 bật tắt cổng G của MOSFET 5000 lần/giây (PWM); tỉ lệ thời gian bật quyết định độ sáng mắt thấy. IRLZ44N là loại logic-level nên mở hoàn toàn chỉ với 3,3V: ở 1A sụt áp chưa tới 0,1V (tỏa nhiệt <0,1W, không cần tản nhiệt). Trở 100Ω hạn dòng nạp cổng G, trở 10kΩ giữ MOSFET tắt khi ESP32 chưa chạy. Độ sáng được hiệu chỉnh theo mắt người (gamma) nên thanh trượt tăng đều.
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 | đỏ sẫm 12V | PS1 + ⟷ U2 IN+ ⟷ L1 + |
| 2 | đen GND | PS1 - ⟷ U2 IN- ⟷ U2 OUT- ⟷ U1 GND ⟷ Q1 S ⟷ R2 2 |
| 3 | đỏ 5V | U2 OUT+ ⟷ U1 VIN |
| 4 | vàng GATE_DRV | U1 GPIO25 (D25) ⟷ R1 1 |
| 5 | xanh lá GATE | R1 2 ⟷ Q1 G ⟷ R2 1 |
| 6 | xanh dương LED_NEG | L1 - ⟷ Q1 D |
Danh sách linh kiện
| Ký hiệu | Linh kiện | Giá tham khảo | |
|---|---|---|---|
| PS1 | Adapter 12V 2A jack DC 5.5×2.1mm | 65.000đ | Tìm mua |
| U2 | Module hạ áp DC-DC LM2596 (chỉnh ra 5V) hoặc mini-360 ra 5V | 25.000đ | Tìm mua |
| U1 | ESP32 DevKit V1 30 chân (ESP32-WROOM-32) | 110.000đ | Tìm mua |
| Q1 | MOSFET IRLZ44N (TO-220, loại logic-level chữ L) | 12.000đ | Tìm mua |
| R1 | Điện trở 100Ω 1/4W (nâu-đen-nâu) | 200đ | Tìm mua |
| R2 | Điện trở 10kΩ 1/4W (nâu-đen-cam) | 200đ | Tìm mua |
| L1 | Dây LED 12V đơn màu (tổng dòng ≤1A, xem nhãn W/m) | 40.000đ | Tìm mua |
| Tổng (chưa gồm board hàn, dây, thiếc) | 252.400đ |
Các bước làm
Code nạp cho mạch
Chọn board DOIT ESP32 DEVKIT V1.
/*
Mạch Gọn — Dimmer WiFi cho dây LED 12V
Board : ESP32 DevKit V1 (chọn "DOIT ESP32 DEVKIT V1"), lõi esp32 >= 3.0
Chân : GPIO25 -> R100 -> G của IRLZ44N (R10k kéo G xuống S)
Lần đầu: vào WiFi "MachGon-Den" (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 <math.h>
// ---- Cấu hình ----
const uint8_t PIN_PWM = 25;
const uint32_t PWM_FREQ = 5000; // 5kHz: không nhấp nháy khi quay phim điện thoại
const uint8_t PWM_BITS = 12; // 0..4095
const float GAMMA = 2.2;
const char* AP_SSID = "MachGon-Den";
const char* HOSTNAME = "den";
enum Effect { FX_STATIC, FX_BREATHE, FX_BLINK, FX_CANDLE, FX_SUNRISE, FX_COUNT };
const char* FX_NAMES[FX_COUNT] = {"Tĩnh", "Thở", "Nhấp nháy", "Ánh nến", "Bình minh 10 phút"};
int bright = 60; // 0..100 (%)
int effect = FX_STATIC;
int speed = 50; // 1..100
bool power = true;
uint32_t fxStart = 0;
WebServer server(80);
Preferences cfg;
void writeLevel(float level) { // level 0..1 theo cảm nhận mắt
level = constrain(level, 0.0f, 1.0f);
uint32_t duty = (uint32_t)lroundf(powf(level, GAMMA) * ((1 << PWM_BITS) - 1));
ledcWrite(PIN_PWM, duty);
}
void saveState() {
cfg.putInt("b", bright); cfg.putInt("fx", effect); cfg.putInt("sp", speed); cfg.putBool("pw", power);
}
void renderFx() {
float base = power ? bright / 100.0f : 0;
float t = (millis() - fxStart) / 1000.0f;
float period = map(speed, 1, 100, 6000, 400) / 1000.0f; // giây
switch (effect) {
case FX_STATIC: writeLevel(base); break;
case FX_BREATHE: writeLevel(base * (0.08f + 0.92f * (0.5f - 0.5f * cosf(2 * PI * t / period)))); break;
case FX_BLINK: writeLevel(fmodf(t, period) < period / 2 ? base : 0); break;
case FX_CANDLE: {
static uint32_t next = 0; static float lv = 1;
if (millis() > next) { lv = 0.65f + (esp_random() % 350) / 1000.0f; next = millis() + 40 + esp_random() % 120; }
writeLevel(base * lv); break;
}
case FX_SUNRISE: writeLevel(base * constrain(t / 600.0f, 0.0f, 1.0f)); break;
}
}
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>Đèn LED WiFi</title>
<style>
:root{--b:#0f172a;--c:#1e293b;--a:#facc15;--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:#111}
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:34px}.v{font-size:34px;font-weight:700;text-align:center}
.fx{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.fx button{font:inherit;padding:12px;border-radius:10px;border:1px solid #334155;background:#0b1220;color:var(--t)}
.fx button.on{background:var(--a);color:#111;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:#111;font-weight:600}
</style></head><body>
<header><b>💡 Đèn LED WiFi</b><button class="pw" id="pw">…</button></header>
<section><h3>Độ sáng</h3><div class="v" id="bv">–</div><input type="range" id="b" min="1" max="100"></section>
<section><h3>Hiệu ứng</h3><div class="fx" id="fx"></div><h3 style="margin-top:14px">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;
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;$('#bv').textContent=s.b+'%';$('#sp').value=s.sp;
$('#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://den.local)'):'Chưa nối WiFi nhà'}
async function set(q){draw(await api('/api/set?'+q,{method:'POST'}))}
$('#b').oninput=e=>{$('#bv').textContent=e.target.value+'%';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://den.local')}
api('/api/state').then(draw);
</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 += "]";
bool sta = WiFi.status() == WL_CONNECTED;
return String("{\"b\":") + bright + ",\"fx\":" + effect + ",\"sp\":" + speed + ",\"power\":" + (power ? "true" : "false") +
",\"names\":" + names + ",\"sta\":" + (sta ? "true" : "false") + ",\"ip\":\"" + (sta ? WiFi.localIP().toString() : WiFi.softAPIP().toString()) +
"\",\"ssid\":\"" + cfg.getString("ssid", "") + "\"}";
}
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("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("fx")) { effect = constrain(server.arg("fx").toInt(), 0, FX_COUNT - 1); fxStart = millis(); }
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);
if (!ledcAttach(PIN_PWM, PWM_FREQ, PWM_BITS)) Serial.println("Loi khoi tao PWM");
ledcWrite(PIN_PWM, 0);
cfg.begin("cfg", false);
bright = cfg.getInt("b", 60); effect = cfg.getInt("fx", FX_STATIC); speed = cfg.getInt("sp", 50); power = cfg.getBool("pw", true);
if (effect < 0 || effect >= FX_COUNT) effect = FX_STATIC;
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) { renderFx(); delay(20); }
}
if (MDNS.begin(HOSTNAME)) MDNS.addService("http", "tcp", 80);
setupWeb();
fxStart = millis();
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;
if (millis() - last >= 10) { last = millis(); renderFx(); }
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
| OUT+ và OUT− của module hạ áp | 4,9 – 5,1V |
| Chân G và S của MOSFET khi kéo thanh trượt 100% | khoảng 3,2V |
| Chân D và S khi 100% | dưới 0,2V (LED sáng tối đa) |
| Chân D và S khi tắt | khoảng 12V (LED tắt) |
Sửa lỗi thường gặp
LED sáng hoài, không tắt được
Kiểm tra chân MOSFET (G-D-S), có thể đấu D và S ngược hoặc MOSFET bị chập do hàn quá nóng. Đo D–S lúc tắt phải ~12V.
LED sáng yếu dù kéo 100%
Đang dùng IRFZ44N (không có chữ L) – loại này cần 10V ở cổng G. Đổi đúng IRLZ44N.
ESP32 không lên
Đo áp ra module hạ áp, phải 5V. Kiểm GND chung.
MOSFET nóng
Dây LED vượt 1A hoặc nhầm loại MOSFET. Tính tổng: công suất (W/m × số mét) ÷ 12 = dòng (A); quá 1A hãy gắn tản nhiệt.
⚠️ An toàn
- Toàn mạch chạy 12V DC, an toàn khi chạm tay.
- Dùng adapter có nhãn an toàn, không để dây LED cuộn tròn khi bật lâu (dễ nóng).
- Không dùng cho đèn LED 220V.