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.
✅ Đã kiểm thử
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 ✓
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.
| # | Dây | Nối các điểm |
|---|---|---|
| 1 | đỏ 5V | U1 VIN ⟷ M1 + · dây đỏ ⟷ D1 K (vạch trắng) ⟷ C1 + |
| 2 | cam 3V3 | U1 3V3 ⟷ U2 VCC |
| 3 | đen GND | U1 GND ⟷ U2 GND ⟷ Q1 S ⟷ R2 2 ⟷ C1 - |
| 4 | vàng SOIL | U2 AOUT ⟷ U1 GPIO34 (D34) |
| 5 | xanh lá GATE_DRV | U1 GPIO25 (D25) ⟷ R1 1 |
| 6 | xanh dương GATE | R1 2 ⟷ Q1 G ⟷ R2 1 |
| 7 | tím PUMP_NEG | M1 - · dây đen ⟷ D1 A (anode) ⟷ Q1 D |
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 |
| U2 | Cả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 |
| Q1 | MOSFET 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 |
| M1 | Máy bơm chìm mini 3–5V DC kèm 1m ống silicon 8mm | 25.000đ | Tìm mua |
| D1 | Đi-ốt Schottky 1N5819 | 500đ | Tìm mua |
| C1 | Tụ 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.
- 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.
- 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
Đo kiểm bằng đồng hồ vạn năng
| Giữa VCC và GND của cảm biến độ ẩm | 3,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ước | khoảng 1,0 – 1,5V |
| Giữa chân D của MOSFET và GND khi đang bơm | dưới 0,1V |
| Giữa 2 dây bơm khi đang bơm | 4,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.