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.

⏱ 75 phút🔧 Biết hàn💰 Linh kiện ~252.400đ🧠 ESP32 DevKit V1⚡ Nguồn adapter

✅ Đã kiểm thử

Luật điệnKhông lỗiSai chân, chập nguồn, quá áp 5V vào chân 3,3V, thiếu đi-ốt, thiếu trở kéo…
Mô phỏng SPICE3/3 kịch bản · 8 phép đo đạtDòng/áp từng linh kiện ở trường hợp xấu nhất (ngspice)
Biên dịch codeĐạt · dùng 75% bộ nhớesp32:esp32:esp32doit-devkit-v1
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 ✓
Kiểm lần cuối: 17:07:19 12/11/2026

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.

⚡ Mô phỏng tương tácBấm thử — mạch được giải ngay trên máy bạn, điện áp và dòng hiện lên sơ đồ bên dưới.
Tình huống mẫu
Chân GPIO25điều khiển R1

Đây là mô phỏng một chiều (DC) ở trạng thái tĩnh: dùng để kiểm tra mức áp, dòng và công suất. Hiệu ứng nhấp nháy, PWM hay giao tiếp số do code quyết định nên không chạy ở đây.

12V12VGNDGND5V5VGATE_DRVGATE_DRVGATEGATELED_NEGLED_NEGU1 · ESP32 DevKit V1VINGPIO25 (D25)GNDR1 · Điện trở 100Ω12Q1 · MOSFET kênh N logic-level IRLZ44NDGSR2 · Điện trở 10kΩ12L1 · Dây LED 12V-+PS1 · Adapter 12V DC+-U2 · hạ áp DC-DCIN+OUT+IN-OUT-
#DâyNối các điểmÁp mô phỏng
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—
4vàng
GATE_DRV
U1 GPIO25 (D25) ⟷ R1 1—
5xanh lá
GATE
R1 2 ⟷ Q1 G ⟷ R2 1—
6xanh dương
LED_NEG
L1 - ⟷ Q1 D—

Danh sách linh kiện

Ký hiệuLinh kiệnGiá tham khảo
PS1Adapter 12V 2A jack DC 5.5×2.1mm65.000đTìm mua
U2Module hạ áp DC-DC LM2596 (chỉnh ra 5V) hoặc mini-360 ra 5V25.000đTìm mua
U1ESP32 DevKit V1 30 chân (ESP32-WROOM-32)110.000đTìm mua
Q1MOSFET 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
L1Dâ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();
  }
}

🔌 Cắm thử trên breadboard

Cắm nháp trên board cắm trước cho chắc, chạy đúng rồi hãy làm mạch in. Sơ đồ này do máy tự xếp từ chính netlist ở trên và đã đối chiếu lại: hai chân cùng một cột (cùng nửa board) là thông nhau, không có net nào bị chập.

+−+−5510101515202025253030JIHGFEDCBAU1 · ESP32 DevKit V1 (30 chân)25GNDVINPS1U2Q1 IRLZ44NR1 100R2 10kL1
  • Mỗi cột 5 lỗ trong cùng một nửa board thông nhau; hai nửa cách nhau bởi rãnh giữa.
  • Hai đường kẻ dọc mép board là đường nguồn: hàng + (vạch đỏ) và hàng − (vạch xanh). Nhớ bắc dây nối đường nguồn trên với đường nguồn dưới nếu dùng cả hai nửa.
  • Màu dây trên hình trùng với màu trong bảng nối dây ở mục Sơ đồ, cắm theo màu cho dễ dò.

🧲 Mạch in để ủi (PCB 1 mặt)

Bản mạch in một mặt đồng, tự xếp linh kiện và tự đi dây, đã kiểm quy tắc khoảng cách và đối chiếu đúng 100% với sơ đồ nguyên lý ở trên. In tỉ lệ 1:1 rồi ủi lên tấm đồng.

Kích thước board58,4×55,9 mm45 lỗ khoan · mũi 0,8 / 1 / 1,1 / 1,2mm
Đi dây 1 mặt12 đoạn · 245,1 mmKhông cần cầu nhảy nào
Kiểm tra bản mạchĐạt DRC · khớp 100% netlistĐường ≥0,8mm, khe ≥0,5mm; mọi net đối chiếu lại từ hình học thật
Mặt đồng — đã lật gương sẵn để in rồi ủi úp xuống tấm đồng
Mặt linh kiện — vị trí, chiều cắm và tên từng chân
PS1U2U1Q1R1R2L1+-IN+IN-OUT+OUT-EN363934353233252627141213GNDVIN232213211918517164215GND23V3+-
Hướng dẫn ủi – ăn mòn – khoan – hàn (đầy đủ)

Hướng dẫn làm mạch in bằng phương pháp ủi

Mạch: Chỉnh độ sáng dây LED 12V bằng điện thoại (dimmer WiFi, hiệu ứng thở/nhấp nháy)

Kích thước board: 58,4×55,9 mm · 45 lỗ khoan (mũi 0,8, 1, 1,1 và 1,2 mm) · 12 đoạn mạch, tổng 245,1 mm · 0 cầu nhảy.

Quy tắc đã kiểm: đường mạch rộng 0,8mm (nguồn và GND 1mm), khe cách tối thiểu 0,5mm. Mạch in đã được máy đối chiếu đúng 100% với sơ đồ nguyên lý.

1. In đúng tỉ lệ 1:1

  • In trang "MẶT ĐỒNG" bằng máy in LASER, đặt tỉ lệ 100%, KHÔNG chọn "Fit to page" hay "Scale to fit".
  • Dùng thước trên bản in đo lại đoạn 100mm: phải đúng 100mm mới dùng được.
  • In trên giấy in ảnh bóng, giấy decal bóc, hoặc giấy tạp chí láng. Mực càng dày càng tốt.
  • Trang "MẶT ĐỒNG" đã được LẬT GƯƠNG sẵn: úp mặt có mực vào tấm đồng là đúng chiều.

2. Chuẩn bị tấm đồng

  • Cắt phíp đồng (board đồng một mặt) lớn hơn kích thước mạch mỗi bề khoảng 5mm.
  • Chà mặt đồng bằng giấy nhám mịn (P800–P1200) rồi rửa sạch, lau khô. Mặt đồng phải sáng, không vết dầu mỡ, không chạm tay vào.

3. Ủi mực sang đồng

  • Úp mặt có mực vào tấm đồng, dùng băng keo giấy dán cố định một cạnh.
  • Bàn ủi để mức nóng nhất (cotton/linen), KHÔNG dùng chế độ phun hơi nước.
  • Ủi đè mạnh và đều 3–5 phút, tập trung từng vùng nhỏ, chú ý các góc và mép.
  • Để nguội hẳn rồi ngâm cả tấm vào nước ấm 5–10 phút, bóc giấy thật nhẹ. Giấy decal thì bóc khi còn ấm.
  • Soi kỹ: chỗ nào mực bị thiếu hoặc đứt đường thì tô lại bằng bút lông dầu (bút CD) rồi để khô.

4. Ăn mòn (etching)

  • Pha dung dịch sắt(III) clorua FeCl3 theo hướng dẫn trên bao bì, hoặc dung dịch oxy già + muối ăn + a-xít; dùng hộp nhựa, KHÔNG dùng đồ kim loại.
  • Nhiệt độ dung dịch khoảng 40–50°C thì ăn nhanh hơn. Lắc nhẹ hộp cho đều, thường 10–25 phút là xong.
  • An toàn: đeo găng tay cao su và kính bảo hộ, làm ở nơi thoáng. Dung dịch ăn mòn làm hỏng quần áo, bồn rửa và gây bỏng rát da.
  • Xong thì rửa board bằng nước sạch, lau khô, dùng xăng thơm hoặc axeton lau sạch lớp mực.
  • Dung dịch đã dùng KHÔNG đổ xuống bồn rửa hay cống; đựng vào can kín và bỏ đúng nơi thu gom chất thải.

5. Khoan lỗ

  • Đối chiếu trang "BẢN VẼ LỖ KHOAN" để biết lỗ nào dùng mũi nào.
  • Dùng mũi khoan 0,8mm, 1mm, 1,1mm và 1,2mm, khoan từ MẶT ĐỒNG vào để mũi tự định tâm giữa pad.
  • Khoan tốc độ cao, đẩy nhẹ; mũi nhỏ rất dễ gãy. Khoan xong chà nhẹ bavia hai mặt.

6. Hàn linh kiện

  • Linh kiện cắm ở MẶT KHÔNG CÓ ĐỒNG, hàn ở mặt đồng. Theo trang "MẶT LINH KIỆN" để đặt đúng vị trí và đúng chiều.
  • Hàn linh kiện thấp trước (điện trở, đi-ốt), cao sau (tụ, hàng chân, board vi điều khiển).
  • Chú ý chiều: LED chân dài là (+); đi-ốt có vạch trắng là K; tụ hoá có vạch là (−); transistor theo mặt chữ như nét vẽ đỏ.
  • Trước khi cấp điện: đo thông mạch giữa chân nguồn và GND — KHÔNG được kêu (không được chập).

Lưu ý với board vi điều khiển

  • Board nhái có thể khác thứ tự chân. Đối chiếu tên chân in cạnh từng lỗ trên trang "MẶT LINH KIỆN" với chữ in trên board thật TRƯỚC KHI ăn mòn. Nếu khác, đừng hàn mà báo lại để sửa bản vẽ.
Bản vẽ lỗ khoan
BẢN VẼ LỖ KHOAN (nhìn từ mặt linh kiện)mũi khoan 0,8mm — 4 lỗmũi khoan 1mm — 34 lỗmũi khoan 1,1mm — 3 lỗmũi khoan 1,2mm — 4 lỗ

Đo kiểm bằng đồng hồ vạn năng

OUT+ và OUT− của module hạ áp4,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ắtkhoả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.