Cảm biến lưu lượng nước YF-S201 với ESP32: nối dây 3,3 V, đếm xung, lít/phút, tổng lít, hiệu chuẩn
Dùng cảm biến lưu lượng YF-S201 với ESP32: nguyên lý tua bin Hall, F = 7,5 × Q, nối dây an toàn 3,3 V bằng diode, đếm xung bằng ngắt, tính lít/phút, tổng lít lưu flash, hiệu chuẩn bằng xô đo.
YF-S201 là cảm biến lưu lượng nước kiểu tua bin, ren 1/2 inch, rất hay dùng trong máy lọc nước, hệ tưới, đồng hồ đo nước tự làm. Cảm biến không cho ra số lít mà cho ra một chuỗi xung: vi điều khiển phải đếm xung rồi tự tính lưu lượng và tổng thể tích. Bài này giải thích nguyên lý, cách nối YF-S201 với ESP32 an toàn ở mức 3,3 V, chương trình đếm xung bằng ngắt, tính lít/phút mỗi giây, cộng dồn tổng lít có lưu vào flash, và cách hiệu chuẩn bằng một xô đo để số đọc sát thực tế.
1. Nguyên lý và thông số

Nước chảy qua làm quay một tua bin nhựa có gắn nam châm. Bên ngoài ống, một cảm biến Hall đổi mức mỗi khi nam châm đi qua, tạo ra xung vuông ở dây vàng. Thông số thường ghi cho YF-S201:
- Nguồn 5–18 V (dùng 5 V khi đi với ESP32); dây đỏ +, đen GND, vàng xung.
- Dải đo khoảng 1–30 L/phút; dưới 1 L/phút tua bin quay không đều hoặc đứng yên.
- Hệ số: F (Hz) = 7,5 × Q (L/phút), tức khoảng 450 xung cho mỗi lít.
- Sai số khi chưa hiệu chuẩn khoảng ±10 %; hệ số thật thay đổi theo từng con, theo áp lực và theo cách lắp.
- Áp lực tối đa khoảng 1,75 MPa; nước sạch, nhiệt độ dưới 80 °C. Bản YF-S201C có thân trong suốt, cùng cách dùng.
Lắp cảm biến đúng chiều mũi tên trên thân, đoạn ống trước cảm biến nên thẳng; ống có bọt khí hoặc cặn sẽ làm tua bin quay sai.
2. Nối dây với ESP32

YF-S201 cần nguồn 5 V, lấy từ chân VIN của ESP32 DevKit khi cắm USB (khoảng 4,7–5 V). Vấn đề nằm ở dây vàng: tùy lô hàng, đầu ra có thể là cực góp hở (chỉ kéo xuống GND) hoặc có sẵn điện trở kéo lên 5 V bên trong, khi đó mức cao lên tới 5 V, quá mức chịu của chân GPIO ESP32.

- Khi cảm biến kéo xuống: dòng từ 3,3 V qua R1 và diode về cảm biến, chân GPIO còn khoảng 0,3–0,6 V, dưới ngưỡng mức thấp 0,8 V của ESP32.
- Khi cảm biến nhả: diode bị phân cực ngược, R1 kéo chân GPIO lên 3,3 V. Dù bên trong cảm biến có kéo lên 5 V, điện áp đó không đi qua được diode.
- Dùng diode Schottky (1N5817, BAT43, BAT85) vì sụt áp nhỏ. Diode thường như 1N4148 sụt áp lớn hơn, mức thấp có thể sát ngưỡng.
- Cầu chia điện trở (10 kΩ / 20 kΩ) chỉ hợp với đầu ra đẩy kéo; với đầu ra có kéo lên bên trong, điện trở kéo lên đó cộng vào cầu chia làm mức cao tụt xuống dưới ngưỡng. Cách diode không phụ thuộc loại đầu ra.
Cách nối an toàn và các trường hợp cần chuyển mức khác có ở bài mức logic 3,3 V và 5 V.
3. Từ xung ra lít/phút

Với hệ số K xung/lít, nếu trong khoảng thời gian t giây đếm được n xung thì:
- lưu lượng Q = n × 60 / (K × t) L/phút;
- thể tích chảy qua trong khoảng đó là n / K lít, cộng dồn lại thành tổng.
Ở 30 L/phút tần số chỉ 225 Hz (chu kỳ 4,4 ms), nên ngắt GPIO đếm thoải mái, không cần bộ đếm phần cứng. Cửa sổ đếm 1 giây cho số đọc nhanh; ở lưu lượng nhỏ (1–2 L/phút) mỗi giây chỉ có 7–15 xung, sai một xung đã lệch vài phần trăm, khi đó tăng CHU_KY_MS lên 3–5 giây. Tổng lít không bị ảnh hưởng vì không xung nào bị bỏ. Cách dùng ngắt và chống nhiễu xung có ở bài ngắt GPIO trên ESP32.
4. Chương trình ESP32
Chuẩn bị: ESP32 DevKit V1 30 chân, cảm biến YF-S201, một diode Schottky, một điện trở 10 kΩ. Chương trình chỉ dùng thư viện có sẵn trong gói esp32 cho Arduino IDE.
// PYWORLD.VN - Đo lưu lượng nước bằng cảm biến YF-S201 với ESP32: đếm xung bằng ngắt, tính lít/phút mỗi giây và tổng số lít
// PYWORLD.VN - Hệ số mặc định 450 xung/lít (F = 7,5 × Q); hiệu chuẩn bằng xô đo: gõ "c" để bắt đầu, xả đúng thể tích, gõ "c5" (5 lít)
// PYWORLD.VN - Dây vàng -> diode Schottky (vạch về phía cảm biến) -> GPIO27, kéo lên 10 kΩ lên 3V3; dây đỏ VIN (5 V), dây đen GND
#include <Preferences.h>
const int CHAN_XUNG = 27;
const float XUNG_MOI_LIT_MAC_DINH = 450.0f; // 7,5 Hz cho mỗi L/phút × 60 giây
const uint32_t CHU_KY_MS = 1000; // cửa sổ đếm; lưu lượng nhỏ thì cửa sổ dài cho số ổn định hơn
const uint32_t XUNG_NGAN_NHAT_US = 1000; // 30 L/phút ≈ 225 Hz (chu kỳ 4,4 ms): xung cách nhau dưới 1 ms là nhiễu
const double LUU_MOI_LIT = 1.0; // ghi tổng vào flash khi tăng thêm 1 lít…
const uint32_t LUU_TOI_THIEU_MS = 60000; // …và cách lần ghi trước ít nhất 1 phút (flash có số lần ghi giới hạn)
const uint32_t CANH_BAO_CHAY_MS = 30UL * 60 * 1000; // nước chảy liên tục 30 phút: có thể rò rỉ hoặc quên khóa vòi
Preferences nvs;
volatile uint32_t demXung = 0;
volatile uint32_t lucXungTruoc = 0;
portMUX_TYPE khoaDem = portMUX_INITIALIZER_UNLOCKED;
float xungMoiLit = XUNG_MOI_LIT_MAC_DINH;
double tongLit = 0, tongDaLuu = 0;
uint32_t xungTruoc = 0, lucTinh = 0, lucLuu = 0, lucBatDauChay = 0;
bool dangChay = false, daCanhBao = false;
bool dangHieuChuan = false;
uint32_t xungDauHieuChuan = 0;
void IRAM_ATTR ngatXung() {
uint32_t bayGio = micros();
portENTER_CRITICAL_ISR(&khoaDem);
if (bayGio - lucXungTruoc >= XUNG_NGAN_NHAT_US) {
demXung = demXung + 1; // không viết demXung++ với biến volatile (C++20 báo cảnh báo)
lucXungTruoc = bayGio;
}
portEXIT_CRITICAL_ISR(&khoaDem);
}
uint32_t docDem() {
portENTER_CRITICAL(&khoaDem);
uint32_t n = demXung;
portEXIT_CRITICAL(&khoaDem);
return n;
}
void luuTong() {
nvs.putDouble("tong", tongLit);
tongDaLuu = tongLit;
lucLuu = millis();
}
void xuLyLenh() {
if (!Serial.available()) return;
String lenh = Serial.readStringUntil('\n');
lenh.trim();
if (lenh == "r") {
tongLit = 0;
luuTong();
Serial.println("Da dat tong ve 0");
} else if (lenh == "c") {
dangHieuChuan = true;
xungDauHieuChuan = docDem();
Serial.println("HIEU CHUAN: xa nuoc vao xo do, du the tich thi khoa voi va go c<so lit>, vi du c5");
} else if (lenh.startsWith("c") && dangHieuChuan) {
float lit = lenh.substring(1).toFloat();
uint32_t n = docDem() - xungDauHieuChuan;
if (lit <= 0 || n < 200) {
Serial.printf("Khong hieu chuan: %u xung, %.2f lit (can it nhat 200 xung)\n", (unsigned)n, lit);
} else {
xungMoiLit = n / lit;
nvs.putFloat("k", xungMoiLit);
Serial.printf("Dem duoc %u xung cho %.2f lit -> he so moi %.1f xung/L (da luu)\n", (unsigned)n, lit, xungMoiLit);
}
dangHieuChuan = false;
} else if (lenh.startsWith("k")) {
float k = lenh.substring(1).toFloat();
if (k > 50 && k < 5000) {
xungMoiLit = k;
nvs.putFloat("k", xungMoiLit);
Serial.printf("He so %.1f xung/L (da luu)\n", xungMoiLit);
}
} else {
Serial.println("Lenh: r = dat tong ve 0, c = bat dau hieu chuan, c5 = ket thuc voi 5 lit, k450 = dat he so");
}
}
void setup() {
Serial.begin(115200);
nvs.begin("luuluong", false);
xungMoiLit = nvs.getFloat("k", XUNG_MOI_LIT_MAC_DINH);
tongLit = tongDaLuu = nvs.getDouble("tong", 0);
pinMode(CHAN_XUNG, INPUT_PULLUP); // có thêm điện trở 10 kΩ ngoài cho dây dài
attachInterrupt(digitalPinToInterrupt(CHAN_XUNG), ngatXung, FALLING);
lucTinh = lucLuu = millis();
Serial.printf("YF-S201: he so %.1f xung/L, tong da luu %.3f L\n", xungMoiLit, tongLit);
}
void loop() {
xuLyLenh();
uint32_t bayGio = millis();
if (bayGio - lucTinh < CHU_KY_MS) return;
uint32_t xung = docDem();
uint32_t moi = xung - xungTruoc; // phép trừ không dấu vẫn đúng khi bộ đếm tràn
float giay = (bayGio - lucTinh) / 1000.0f;
xungTruoc = xung;
lucTinh = bayGio;
float tanSo = moi / giay;
float litPhut = tanSo * 60.0f / xungMoiLit;
tongLit += moi / (double)xungMoiLit;
bool chay = moi > 0;
if (chay && !dangChay) {
lucBatDauChay = bayGio;
daCanhBao = false;
}
if (chay && !daCanhBao && bayGio - lucBatDauChay >= CANH_BAO_CHAY_MS) {
Serial.println("CANH BAO: nuoc chay lien tuc 30 phut");
daCanhBao = true;
}
// Ghi flash khi đủ 1 lít và đủ 1 phút, hoặc ngay lúc nước vừa ngừng chảy
bool vuaDung = dangChay && !chay;
dangChay = chay;
if ((tongLit - tongDaLuu >= LUU_MOI_LIT && bayGio - lucLuu >= LUU_TOI_THIEU_MS) || (vuaDung && tongLit > tongDaLuu)) {
luuTong();
}
Serial.printf("Q %.2f L/phut | %.1f Hz | tong %.3f L%s\n", litPhut, tanSo, tongLit, dangHieuChuan ? " | dang hieu chuan" : "");
}
- Hàm ngắt
ngatXung()chỉ tăng bộ đếm, cóIRAM_ATTRvà khóaportMUXđể vòng lặp chính đọc bộ đếm không bị chen ngang. Xung cách nhau dưới 1 ms bị bỏ vì ở dải đo của YF-S201 chúng chỉ có thể là nhiễu. - Mỗi giây chương trình lấy hiệu số xung, tính Q theo thời gian thật đã trôi qua (không giả định đúng 1000 ms), cộng thể tích vào tổng.
- Lưu tổng vào flash bằng Preferences khi tăng thêm 1 lít và đã qua 1 phút, hoặc ngay lúc nước vừa ngừng chảy, nên mất điện chỉ mất phần lẻ dưới 1 lít mà flash không bị ghi liên tục. Cách dùng Preferences có ở bài lưu dữ liệu khi mất điện.
- Cảnh báo khi nước chảy liên tục 30 phút: dấu hiệu rò rỉ hoặc quên khóa vòi; sửa
CANH_BAO_CHAY_MStheo cách dùng thực tế. - Lệnh Serial:
rđặt tổng về 0,cvàc5để hiệu chuẩn,k450đặt hệ số bằng tay.
5. Hiệu chuẩn bằng xô đo

- Lắp cảm biến vào đúng đường ống sẽ dùng (áp lực và đoạn ống ảnh hưởng tới hệ số).
- Chuẩn bị xô hoặc can có vạch chia, càng lớn càng chính xác (5–20 lít).
- Gõ
ctrong Serial Monitor, mở vòi với lưu lượng thường dùng, khóa vòi khi nước đúng vạch. - Gõ
ckèm số lít, ví dục5. Chương trình tính hệ số mới = số xung / số lít và lưu lại. - Làm lại 2–3 lần, lấy trung bình rồi đặt bằng lệnh
knếu các lần lệch nhau.
Ở ví dụ trên, cảm biến thật cho 462 xung/lít; với hệ số mặc định 450, chương trình đọc 5,133 lít cho một xô 5 lít (cao hơn 2,7 %). Sau khi hiệu chuẩn, số đọc khớp. Hệ số có thể khác nhau giữa lưu lượng nhỏ và lớn; nếu dùng ở nhiều mức lưu lượng, hiệu chuẩn ở mức hay dùng nhất.
6. Lỗi hay gặp
| Hiện tượng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Luôn 0 L/phút | Thiếu nguồn 5 V, diode cắm ngược, lắp ngược chiều nước | Đo 5 V ở dây đỏ; vạch diode về phía dây vàng; xem mũi tên trên thân |
| Có số khi không có nước chảy | Nhiễu vào dây tín hiệu dài, chân thả nổi | Điện trở kéo lên 10 kΩ, dây xoắn hoặc cáp có lưới, đi xa dây 220 V |
| Lưu lượng nhỏ đọc 0 hoặc nhảy | Dưới 1 L/phút tua bin không quay đều; cửa sổ đếm ngắn | Tăng CHU_KY_MS; dùng cảm biến cỡ nhỏ hơn cho lưu lượng nhỏ |
| Tổng lít lệch so với đồng hồ nước | Chưa hiệu chuẩn, có bọt khí, áp lực khác lúc hiệu chuẩn | Hiệu chuẩn tại chỗ; xả khí trong ống |
| ESP32 khởi động lại khi bơm chạy | Nhiễu từ bơm, relay qua nguồn chung | Tụ lọc nguồn, nguồn riêng cho bơm, mass đi riêng |
| Mất tổng sau khi mất điện | Chưa kịp ghi flash (dưới 1 lít) | Bình thường theo thiết kế; giảm LUU_MOI_LIT nếu cần |
7. Xem thêm
- Cảm biến lưu lượng: tua bin, chênh áp, điện từ, siêu âm, Coriolis
- Cảm biến từ trường: Hall, từ trở, fluxgate, lưỡi gà
- Tưới cây tự động với ESP32: cảm biến độ ẩm đất, relay và bơm mini
- Đếm xung bằng bộ đếm phần cứng PCNT của ESP32
Bài cùng cụm chủ đề: đo mực nước bồn và tự bơm bằng ESP32.
Xem thêm