Tưới cây tự động theo độ ẩm đất, xem và chỉnh bằng điện thoại

Cảm biến đo độ ẩm đất, khi đất khô mạch tự bật bơm mini tưới rồi tự tắt. Xem độ ẩm, chỉnh ngưỡng, tưới tay qua web trên điện thoại. Có giới hạn thời gian bơm và số lần tưới mỗi ngày để không cháy bơm hay ngập chậu.

⏱ 90 phút🔧 Biết hàn💰 Linh kiện ~174.400đ🧠 ESP32 DevKit V1⚡ Nguồn USB 5V

✅ Đã 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 · 11 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 74% bộ nhớesp32:esp32:esp32doit-devkit-v1
Xem chi tiết 11 phép đo

Đang bơm (GPIO25 mức cao)

  • Q1: dẫn 227.0mA, Vds=0.01V.
  • Chân GPIO25 cấp 0.3mA (an toàn ≤ 20.0mA).
  • Tổng dòng đỉnh từ USB 482.3mA (đủ với cục sạc 5V 1A).
  • Dòng chạy qua máy bơm: 227.0mA ✓
  • Áp rơi trên MOSFET (MOSFET mở hoàn toàn): 0.01V ✓
  • Dòng chân GPIO25 phải cấp: 0.3mA ✓
  • Áp cảm biến vào ESP32 (đất khô nhất): 3.00V ✓

Nghỉ (GPIO25 mức thấp)

  • Tổng dòng đỉnh từ USB 255.0mA (đủ với cục sạc 5V 1A).
  • Bơm tắt hẳn khi nghỉ: 0.0mA ✓

Lúc khởi động (GPIO25 thả nổi)

  • Tổng dòng đỉnh từ USB 255.0mA (đủ với cục sạc 5V 1A).
  • Trở 10kΩ giữ bơm tắt khi ESP32 đang khởi động: 0.0mA ✓
Kiểm lần cuối: 16:25:45 12/11/2026

Nguyên lý hoạt động

Cảm biến điện dung cho điện áp 1,2–2,8V tùy độ ẩm (đất càng khô áp càng cao). ESP32 đọc ở GPIO34, đổi ra % theo 2 mốc hiệu chuẩn khô/ướt. Khi độ ẩm dưới ngưỡng, GPIO25 lên 3,3V mở MOSFET IRLZ44N, bơm nhận ~0,23A từ cổng USB 5V; MOSFET gần như không tỏa nhiệt (áp rơi < 0,02V). Trở 10kΩ kéo cổng MOSFET xuống GND để bơm không tự chạy lúc ESP32 khởi động; đi-ốt 1N5819 dập xung điện ngược khi bơm tắt. Phần mềm giới hạn mỗi lần bơm tối đa N giây, nghỉ tối thiểu giữa 2 lần và số lần tưới mỗi ngày.

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.

5V5V3V33V3GNDGNDSOILSOILGATE_DRVGATE_DRVGATEGATEPUMP_NEGPUMP_NEGU1 · ESP32 DevKit V13V3VINGPIO25 (D25)GPIO34 (D34)GNDR1 · Điện trở 100Ω12Q1 · MOSFET kênh N logic-level IRLZ44NDGSR2 · Điện trở 10kΩ12U2 · Cảm biến độ ẩm đất điện dung v1.2VCCAOUTGNDM1 · Máy bơm nước mini 3–5V- · dây đen+ · dây đỏD1 · Đi-ốt 1N5819A (anode)K (vạch trắng)C1 · Tụ điện 470µF-+
#DâyNối các điểm
1đỏ
5V
U1 VIN ⟷ M1 + · dây đỏ ⟷ D1 K (vạch trắng) ⟷ C1 +
2cam
3V3
U1 3V3 ⟷ U2 VCC
3đen
GND
U1 GND ⟷ U2 GND ⟷ Q1 S ⟷ R2 2 ⟷ C1 -
4vàng
SOIL
U2 AOUT ⟷ U1 GPIO34 (D34)
5xanh lá
GATE_DRV
U1 GPIO25 (D25) ⟷ R1 1
6xanh dương
GATE
R1 2 ⟷ Q1 G ⟷ R2 1
7tím
PUMP_NEG
M1 - · dây đen ⟷ D1 A (anode) ⟷ Q1 D

Danh sách linh kiện

Ký hiệuLinh kiệnGiá tham khảo
U1ESP32 DevKit V1 30 chân (ESP32-WROOM-32)110.000đTìm mua
U2Cảm biến độ ẩm đất điện dung v1.2 (loại không gỉ, kèm dây 3 chân)25.000đTìm mua
Q1MOSFET IRLZ44N (TO-220, loại logic-level, có 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
M1Máy bơm chìm mini 3–5V DC kèm 1m ống silicon 8mm25.000đTìm mua
D1Đi-ốt Schottky 1N5819500đTìm mua
C1Tụ hóa 470µF 16V (470u)1.500đTìm mua
Tổng (chưa gồm board hàn, dây, thiếc)174.400đ

Các bước làm

Code nạp cho mạch

Chọn board DOIT ESP32 DEVKIT V1.

/*
  Mạch Gọn — Tưới cây tự động theo độ ẩm đất (ESP32)
  Board : DOIT ESP32 DEVKIT V1
  Chân  : GPIO34 <- AOUT cảm biến độ ẩm điện dung (cấp 3V3)
          GPIO25 -> R100 -> cổng MOSFET IRLZ44N (bơm 5V ở cực D)
  Lần đầu: WiFi "MachGon-Tuoi" (mật khẩu machgon123) -> 192.168.4.1
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Preferences.h>

// ---- Cấu hình ----
const uint8_t PIN_SOIL = 34;
const uint8_t PIN_PUMP = 25;
const uint8_t PIN_LED  = 2;
const char* AP_SSID  = "MachGon-Tuoi";
const char* HOSTNAME = "tuoicay";
const uint32_t SAMPLE_MS = 2000;          // đọc cảm biến mỗi 2 giây
const uint32_t DAY_MS = 24UL * 3600UL * 1000UL;
const uint16_t PUMP_HARD_MAX_S = 60;      // giới hạn cứng, không cài vượt được

struct Settings {
  int dryRaw = 2900;      // giá trị ADC khi khô (ngoài không khí)
  int wetRaw = 1300;      // giá trị ADC khi ngập nước
  int onBelow = 35;       // bật tưới khi độ ẩm < %
  int pumpSec = 8;        // mỗi lần bơm tối đa (giây)
  int restMin = 30;       // nghỉ tối thiểu giữa 2 lần (phút)
  int maxPerDay = 6;      // tối đa số lần tự tưới / 24h
  bool autoOn = true;
} S;

WebServer server(80);
Preferences prefs;
int rawNow = 0, pctNow = 0;
bool sensorOk = true;
bool pumping = false;
uint32_t pumpStart = 0, pumpLimitMs = 0, lastWater = 0, dayStart = 0, lastSample = 0;
int waterToday = 0;
bool everWatered = false;
String lastReason = "Chưa tưới";

void loadSettings() {
  S.dryRaw = prefs.getInt("dry", S.dryRaw);
  S.wetRaw = prefs.getInt("wet", S.wetRaw);
  S.onBelow = prefs.getInt("on", S.onBelow);
  S.pumpSec = prefs.getInt("sec", S.pumpSec);
  S.restMin = prefs.getInt("rest", S.restMin);
  S.maxPerDay = prefs.getInt("max", S.maxPerDay);
  S.autoOn = prefs.getBool("auto", S.autoOn);
}
void saveSettings() {
  prefs.putInt("dry", S.dryRaw); prefs.putInt("wet", S.wetRaw); prefs.putInt("on", S.onBelow);
  prefs.putInt("sec", S.pumpSec); prefs.putInt("rest", S.restMin); prefs.putInt("max", S.maxPerDay);
  prefs.putBool("auto", S.autoOn);
}

int readSoilRaw() {
  long sum = 0;
  for (int i = 0; i < 16; i++) { sum += analogRead(PIN_SOIL); delayMicroseconds(300); }
  return sum / 16;
}

int rawToPct(int raw) {
  if (S.dryRaw == S.wetRaw) return 0;
  long p = (long)(S.dryRaw - raw) * 100 / (S.dryRaw - S.wetRaw);
  return constrain((int)p, 0, 100);
}

void pumpOn(uint32_t seconds, const String& reason) {
  if (pumping) return;
  seconds = constrain(seconds, 1u, (uint32_t)PUMP_HARD_MAX_S);
  digitalWrite(PIN_PUMP, HIGH);
  digitalWrite(PIN_LED, HIGH);
  pumping = true; pumpStart = millis(); pumpLimitMs = seconds * 1000UL;
  lastReason = reason;
  Serial.printf("Bom BAT %lus (%s)\n", (unsigned long)seconds, reason.c_str());
}
void pumpOff() {
  digitalWrite(PIN_PUMP, LOW);
  digitalWrite(PIN_LED, LOW);
  if (pumping) { pumping = false; lastWater = millis(); everWatered = true; Serial.println("Bom TAT"); }
}

void controlLoop() {
  uint32_t now = millis();
  if (pumping && now - pumpStart >= pumpLimitMs) pumpOff();      // luôn cắt bơm khi hết giờ
  if (now - dayStart >= DAY_MS) { dayStart = now; waterToday = 0; }
  if (now - lastSample < SAMPLE_MS) return;
  lastSample = now;
  rawNow = readSoilRaw();
  // cảm biến tuột dây/đứt: giá trị dính sát 0 hoặc 4095
  sensorOk = rawNow > 200 && rawNow < 4000;
  pctNow = rawToPct(rawNow);
  if (!sensorOk) { if (pumping && lastReason.startsWith("Tự động")) pumpOff(); return; }
  if (!S.autoOn || pumping) return;
  bool rested = !everWatered || now - lastWater >= (uint32_t)S.restMin * 60000UL;
  if (pctNow < S.onBelow && rested && waterToday < S.maxPerDay) {
    waterToday++;
    pumpOn(S.pumpSec, "Tự động: đất " + String(pctNow) + "%");
  }
}

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>Tưới cây</title>
<style>
body{margin:0;font-family:system-ui,sans-serif;background:#052e16;color:#ecfdf5}
header{padding:14px;background:#064e3b;font-weight:700}
.card{margin:12px;padding:14px;background:#065f46;border-radius:14px}
.big{font-size:56px;font-weight:800;text-align:center}.sub{text-align:center;color:#a7f3d0}
.bar{height:14px;background:#022c22;border-radius:8px;overflow:hidden}.bar i{display:block;height:100%;background:#34d399}
label{display:flex;justify-content:space-between;align-items:center;margin:8px 0;gap:8px}
input[type=number]{width:90px;padding:7px;border-radius:8px;border:0;font:inherit}
button{font:inherit;border:0;border-radius:10px;padding:10px 14px;margin:4px 4px 0 0;background:#34d399;color:#022c22;font-weight:700}
button.g{background:#0f766e;color:#ecfdf5}.warn{background:#7f1d1d;padding:10px;border-radius:10px;display:none}
small{color:#a7f3d0}
</style></head><body><header>🌱 Tưới cây tự động <small id="ip"></small></header>
<div class="card"><div class="big" id="pct">--%</div><div class="bar"><i id="b" style="width:0"></i></div>
<p class="sub" id="st">Đang đọc…</p><div class="warn" id="err">⚠ Không đọc được cảm biến: kiểm tra dây AOUT/VCC/GND.</div>
<button onclick="water(3)">Tưới thử 3 giây</button><button class="g" onclick="post('/api/stop')">Dừng bơm</button></div>
<div class="card"><b>Cài đặt</b>
<label>Tự động tưới <input type="checkbox" id="auto"></label>
<label>Tưới khi độ ẩm dưới (%) <input type="number" id="on" min="5" max="90"></label>
<label>Mỗi lần bơm tối đa (giây) <input type="number" id="sec" min="1" max="60"></label>
<label>Nghỉ ít nhất giữa 2 lần (phút) <input type="number" id="rest" min="1" max="1440"></label>
<label>Tối đa số lần/ngày <input type="number" id="max" min="1" max="48"></label>
<button onclick="save()">Lưu cài đặt</button></div>
<div class="card"><b>Hiệu chuẩn cảm biến</b><br><small>Giá trị thô hiện tại: <span id="raw">-</span> (khô=<span id="d">-</span>, ướt=<span id="w">-</span>)</small><br>
<button class="g" onclick="post('/api/cal?m=dry')">Đặt mốc KHÔ (để ngoài không khí)</button>
<button class="g" onclick="post('/api/cal?m=wet')">Đặt mốc ƯỚT (nhúng nước)</button></div>
<div class="card"><b>WiFi nhà</b><br><small id="ws"></small>
<label>Tên <input id="ssid" style="width:60%;padding:7px;border-radius:8px;border:0"></label>
<label>Mật khẩu <input id="pass" type="password" style="width:60%;padding:7px;border-radius:8px;border:0"></label>
<button onclick="wifi()">Lưu & khởi động lại</button></div>
<script>
const $=i=>document.getElementById(i);let loaded=false;
async function post(u,b){const r=await fetch(u,{method:'POST',body:b});tick();return r}
function water(s){post('/api/water?s='+s)}
async function save(){const p=new URLSearchParams();['on','sec','rest','max'].forEach(k=>p.append(k,$(k).value));p.append('auto',$('auto').checked?1:0);await post('/api/settings',p);alert('Đã lưu')}
async function wifi(){await post('/api/wifi',new URLSearchParams({ssid:$('ssid').value,pass:$('pass').value}));alert('Đã lưu, mạch khởi động lại. Nối WiFi nhà rồi mở http://tuoicay.local')}
async function tick(){try{const s=await (await fetch('/api/state')).json();
$('pct').textContent=s.ok?s.pct+'%':'--';$('b').style.width=s.pct+'%';$('err').style.display=s.ok?'none':'block';
$('st').textContent=(s.pumping?'💧 Đang bơm… ':'')+'Lần cuối: '+s.reason+(s.ago>=0?' ('+s.ago+' phút trước)':'')+' · Hôm nay: '+s.today+'/'+s.max+' lần';
$('raw').textContent=s.raw;$('d').textContent=s.dry;$('w').textContent=s.wet;$('ip').textContent=s.ip;$('ws').textContent=s.sta?'Đã nối '+s.ssid:'Chưa nối WiFi nhà';
if(!loaded){['on','sec','rest','max'].forEach(k=>$(k).value=s[k]);$('auto').checked=s.auto;loaded=true}}catch(e){}}
tick();setInterval(tick,2000);
</script></body></html>)HTML";

void setupWeb() {
  server.on("/", []() { server.send_P(200, "text/html; charset=utf-8", PAGE); });
  server.on("/api/state", []() {
    bool sta = WiFi.status() == WL_CONNECTED;
    long ago = everWatered ? (long)((millis() - lastWater) / 60000UL) : -1;
    String j = "{\"pct\":" + String(pctNow) + ",\"raw\":" + rawNow + ",\"ok\":" + (sensorOk ? "true" : "false") +
      ",\"pumping\":" + (pumping ? "true" : "false") + ",\"reason\":\"" + lastReason + "\",\"ago\":" + ago +
      ",\"today\":" + waterToday + ",\"dry\":" + S.dryRaw + ",\"wet\":" + S.wetRaw + ",\"on\":" + S.onBelow +
      ",\"sec\":" + S.pumpSec + ",\"rest\":" + S.restMin + ",\"max\":" + S.maxPerDay + ",\"auto\":" + (S.autoOn ? "true" : "false") +
      ",\"sta\":" + (sta ? "true" : "false") + ",\"ssid\":\"" + prefs.getString("ssid", "") + "\",\"ip\":\"" +
      (sta ? WiFi.localIP().toString() : WiFi.softAPIP().toString()) + "\"}";
    server.send(200, "application/json", j);
  });
  server.on("/api/water", HTTP_POST, []() {
    if (!sensorOk && server.arg("s").toInt() > 10) return server.send(400, "text/plain", "cảm biến lỗi");
    pumpOn(server.arg("s").toInt(), "Tưới tay"); server.send(200, "text/plain", "ok");
  });
  server.on("/api/stop", HTTP_POST, []() { pumpOff(); server.send(200, "text/plain", "ok"); });
  server.on("/api/settings", HTTP_POST, []() {
    S.onBelow = constrain(server.arg("on").toInt(), 5, 90);
    S.pumpSec = constrain(server.arg("sec").toInt(), 1, (int)PUMP_HARD_MAX_S);
    S.restMin = constrain(server.arg("rest").toInt(), 1, 1440);
    S.maxPerDay = constrain(server.arg("max").toInt(), 1, 48);
    S.autoOn = server.arg("auto") == "1";
    saveSettings(); server.send(200, "text/plain", "ok");
  });
  server.on("/api/cal", HTTP_POST, []() {
    int r = readSoilRaw();
    if (server.arg("m") == "dry") S.dryRaw = r; else S.wetRaw = r;
    if (abs(S.dryRaw - S.wetRaw) < 200) { server.send(400, "text/plain", "2 mốc quá gần nhau"); return; }
    saveSettings(); server.send(200, "text/plain", "ok");
  });
  server.on("/api/wifi", HTTP_POST, []() {
    prefs.putString("ssid", server.arg("ssid")); prefs.putString("pass", server.arg("pass"));
    server.send(200, "text/plain", "ok"); delay(500); ESP.restart();
  });
  server.onNotFound([]() { server.sendHeader("Location", "/"); server.send(302); });
  server.begin();
}

void setup() {
  pinMode(PIN_PUMP, OUTPUT); digitalWrite(PIN_PUMP, LOW);   // tắt bơm ngay khi khởi động
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  analogSetPinAttenuation(PIN_SOIL, ADC_11db);
  prefs.begin("tuoi", false);
  loadSettings();
  WiFi.mode(WIFI_AP_STA);
  WiFi.softAP(AP_SSID, "machgon123");
  String ssid = prefs.getString("ssid", "");
  if (ssid.length()) {
    WiFi.begin(ssid.c_str(), prefs.getString("pass", "").c_str());
    uint32_t t = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - t < 15000) delay(250);
  }
  Serial.printf("AP %s %s | STA %s\n", AP_SSID, WiFi.softAPIP().toString().c_str(), WiFi.localIP().toString().c_str());
  if (MDNS.begin(HOSTNAME)) MDNS.addService("http", "tcp", 80);
  setupWeb();
  dayStart = millis();
}

void loop() {
  server.handleClient();
  controlLoop();
  static uint32_t lastCheck = 0;
  if (millis() - lastCheck > 30000) {
    lastCheck = millis();
    if (prefs.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.

+−+−55101015152020252530303535JIHGFEDCBAU1 · ESP32 DevKit V1 (30 chân)3425GNDVIN3V3U2Q1 IRLZ44NR1 100R2 10kM1D1 1N5819C1 470u+
  • 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 board61,0×63,5 mm46 lỗ khoan · mũi 0,8 / 0,9 / 1 / 1,1 / 1,2mm
Đi dây 1 mặt13 đoạn · 364,5 mm · 1 cầu nhảyCầu nhảy = đoạn dây hàn ở mặt linh kiện
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
U1U2Q1R1R2M1D1C1EN363934353233252627141213GNDVIN232213211918517164215GND23V3VCCAOUTGND+-
Cầu nhảy cần hàn (dây đơn ở mặt linh kiện):
  • net GND: U1.GND → R2.2
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: Tưới cây tự động theo độ ẩm đất, xem và chỉnh bằng điện thoại

Kích thước board: 61,0×63,5 mm · 46 lỗ khoan (mũi 0,8, 0,9, 1, 1,1 và 1,2 mm) · 13 đoạn mạch, tổng 364,5 mm · 1 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, 0,9mm, 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ẽ đỏ.
  • Mạch này còn 1 CẦU NHẢY: hàn đoạn dây đơn ở mặt linh kiện theo nét đỏ gạch trên bản vẽ:
  • cầu nhảy net GND: từ U1.GND sang R2.2
  • 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 0,9mm — 2 lỗmũi khoan 1mm — 35 lỗmũi khoan 1,1mm — 3 lỗmũi khoan 1,2mm — 2 lỗ

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

Giữa VCC và GND của cảm biến độ ẩm3,2 – 3,3V
Giữa AOUT và GND khi cảm biến ở ngoài không khíkhoảng 2,4 – 2,9V
Giữa AOUT và GND khi nhúng nướckhoảng 1,0 – 1,5V
Giữa chân D của MOSFET và GND khi đang bơmdưới 0,1V
Giữa 2 dây bơm khi đang bơm4,6 – 5,0V

Sửa lỗi thường gặp

Bơm chạy liên tục không tắt

Kiểm tra trở 10kΩ G–GND, MOSFET có thể đã chập (đo D–S). Phần mềm luôn cắt bơm sau số giây tối đa; nếu vẫn chạy là lỗi phần cứng.

Bơm không chạy

Thử "Tưới thử" trên web; đo áp chân G lúc bơm phải ~3,3V. Kiểm tra chiều MOSFET G-D-S và đi-ốt (vạch trắng về VIN; cắm ngược đi-ốt sẽ chập bơm).

Độ ẩm nhảy lung tung hoặc luôn 0%/100%

Hiệu chuẩn lại 2 mốc; kiểm tra dây AOUT vào đúng GPIO34; dùng dây ngắn dưới 1m.

ESP32 khởi động lại khi bơm chạy

Cục sạc yếu: dùng cục sạc 5V ≥1A và cáp ngắn; đảm bảo đã hàn tụ 470µF.

⚠️ An toàn

  • Chỉ dùng điện 5V từ cục sạc USB, an toàn khi chạm tay.
  • Đặt mạch trong hộp kín, cao hơn mặt nước; chỉ cảm biến và bơm được tiếp xúc nước.
  • Không dùng bơm 220V với mạch này.