DHT11 và DHT22 với ESP32: đọc nhiệt độ, độ ẩm không cần thư viện, giao thức 40 bit và điểm sương
So sánh DHT11 và DHT22, nối dây với ESP32, giải thích giao thức một dây 40 bit và checksum, chương trình đọc nhiệt độ, độ ẩm không cần thư viện, kèm điểm sương và bảng lỗi.
DHT11 và DHT22 là hai cảm biến nhiệt độ, độ ẩm rẻ và phổ biến nhất trong các dự án Arduino, ESP32: trạm thời tiết mini, theo dõi phòng ngủ, tủ chống ẩm, nhà kính. Cả hai chỉ cần một dây dữ liệu. Bài này giải thích giao thức của cảm biến, sau đó đọc nhiệt độ và độ ẩm bằng ESP32 không cần thư viện: tự đo độ dài từng xung, ghép thành 5 byte, kiểm tra checksum, đổi ra số đo và tính thêm điểm sương. Hiểu cách đọc này rồi thì gặp lỗi "không đọc được cảm biến" sẽ biết tìm ở đâu.
1. DHT11 và DHT22 khác nhau thế nào

Hai cảm biến dùng cùng một kiểu giao tiếp, cùng cách nối dây. Khác nhau ở dải đo, độ chính xác và giá:
| Thông số (theo datasheet) | DHT11 | DHT22 (AM2302) |
|---|---|---|
| Màu vỏ thường gặp | Xanh dương | Trắng |
| Điện áp cấp | 3,3–5,5 V | 3,3–6 V |
| Dải độ ẩm | 20–90 %RH | 0–100 %RH |
| Sai số độ ẩm | ±5 %RH | ±2 %RH (tối đa ±5 %RH) |
| Dải nhiệt độ | 0–50 °C | −40 đến 80 °C |
| Sai số nhiệt độ | ±2 °C | ±0,5 °C |
| Độ phân giải | 1 %RH, 1 °C (bản mới có 0,1 °C) | 0,1 %RH, 0,1 °C |
| Khoảng cách giữa hai lần đọc | Từ 1 giây | Từ 2 giây |
Chọn nhanh: đo trong nhà, cần biết "nóng hay mát, khô hay ẩm" thì DHT11 đủ dùng. Cần số đo sát thật, đo ngoài trời mùa lạnh hoặc độ ẩm trên 90 % (mùa nồm, nhà kính) thì dùng DHT22. Cả hai đều phản ứng chậm: thay đổi môi trường thì vài chục giây sau số đo mới ổn định.
2. Linh kiện cần chuẩn bị
- ESP32 DevKit V1 30 chân (bài dùng sơ đồ chân loại 30 chân; loại 38 chân nối theo tên chân GPIO4 cũng được).
- Module DHT11 hoặc DHT22 loại 3 chân (có sẵn điện trở kéo lên), hoặc cảm biến 4 chân trần kèm một điện trở 10 kΩ.
- Dây cắm test board và breadboard 830 lỗ.
- Dây USB nạp code loại có truyền dữ liệu.
- Nên có thêm một đồng hồ đo nhiệt độ, độ ẩm mini để so số đo khi lắp xong.
3. Sơ đồ nối dây

| Module DHT | ESP32 DevKit V1 | Ghi chú |
|---|---|---|
| + (VCC) | 3V3 | Cấp 3,3 V để chân dữ liệu cũng ở mức 3,3 V, an toàn cho ESP32 |
| out (DATA) | D4 (GPIO4) | Đổi chân khác thì sửa hằng CHAN_DHT |
| − (GND) | GND |
Thứ tự chân in trên đế mạch có thể khác nhau giữa các nhà sản xuất: luôn đọc chữ in cạnh chân trước khi cắm. Cảm biến 4 chân trần thì chân 1 là VCC, chân 2 là DATA, chân 3 bỏ trống, chân 4 là GND (nhìn từ mặt có lưới, chân hướng xuống), và cần thêm điện trở 10 kΩ từ DATA lên VCC.
Không cấp 5 V cho module rồi nối thẳng chân DATA vào ESP32: điện trở kéo lên trên module sẽ đưa 5 V vào GPIO. Lý do và cách chuyển mức an toàn có trong bài mức logic 3,3 V và 5 V.
4. Giao thức một dây của DHT

Đường dữ liệu bình thường ở mức cao nhờ điện trở kéo lên. Cả ESP32 lẫn cảm biến chỉ kéo đường dây xuống thấp rồi nhả ra, không bên nào đẩy lên cao. Một lần đọc gồm:
- Tín hiệu bắt đầu: ESP32 kéo dây xuống thấp ít nhất 18 ms (DHT11) hoặc 1 ms (DHT22), rồi nhả ra.
- Cảm biến trả lời: sau 20–40 µs, cảm biến kéo dây xuống thấp 80 µs rồi nhả lên cao 80 µs. Không thấy lời trả lời này nghĩa là sai dây, mất nguồn hoặc thiếu điện trở kéo lên.
- 40 bit dữ liệu: mỗi bit mở đầu bằng 50 µs mức thấp. Mức cao theo sau dài 26–28 µs là bit 0, dài khoảng 70 µs là bit 1. Bit gửi từ bit cao nhất của byte đầu tiên.
Giao thức này không phải 1-Wire của cảm biến DS18B20: DHT không có địa chỉ, mỗi chân GPIO chỉ nối được một cảm biến. Muốn so sánh hai cách làm, xem bài 1-Wire và cách đọc DS18B20.
5. Đọc 5 byte và kiểm tra checksum

40 bit ghép lại thành 5 byte. Cách đổi ra số đo khác nhau giữa hai loại:
- DHT11: byte 1 là phần nguyên độ ẩm, byte 2 là phần thập phân (thường bằng 0), byte 3 là phần nguyên nhiệt độ, 4 bit thấp của byte 4 là phần mười độ. Ví dụ
41 00 1D 03là 65 % và 29,3 °C. Một số bản DHT11 mới dùng bit cao nhất của byte 4 để báo nhiệt độ âm. - DHT22: byte 1–2 là độ ẩm nhân 10, byte 3–4 là nhiệt độ nhân 10. Bit cao nhất của nhiệt độ là dấu âm. Ví dụ
02 8C= 652 là 65,2 %,01 25= 293 là 29,3 °C, còn80 65là −10,1 °C. - Checksum: cộng 4 byte đầu, lấy byte thấp của tổng. Kết quả phải bằng byte thứ năm; khác thì gói tin bị nhiễu, bỏ đi và đọc lại.
6. Chương trình đọc DHT không cần thư viện
Mở Arduino IDE 2, chọn bo ESP32 Dev Module (hoặc DOIT ESP32 DEVKIT V1), dán chương trình dưới đây và nạp. Chưa cài gói ESP32 thì làm theo bài cài ESP32 vào Arduino IDE 2. Bài dùng gói esp32 phiên bản 3.3.10; chương trình không cần cài thêm thư viện nào. Dùng DHT22 thì sửa LOAI_CAM_BIEN thành 22.
// PYWORLD.VN - Đọc cảm biến nhiệt độ, độ ẩm DHT11 / DHT22 bằng ESP32, không cần thư viện
// Chân DATA nối D4 (GPIO4), điện trở kéo lên 10 kΩ lên 3V3 (module 3 chân thường có sẵn điện trở này)
// Đổi LOAI_CAM_BIEN thành 22 khi dùng DHT22 (AM2302); in thêm điểm sương tính theo công thức Magnus
const int CHAN_DHT = 4;
const int LOAI_CAM_BIEN = 11; // 11 = DHT11, 22 = DHT22
const uint32_t CHU_KY_DOC_MS = 2500; // DHT22 cần ít nhất 2 giây giữa hai lần đọc
portMUX_TYPE khoaNgat = portMUX_INITIALIZER_UNLOCKED;
enum KetQua { DOC_DUOC, KHONG_TRA_LOI, LOI_THOI_GIAN, SAI_CHECKSUM };
// Chờ chân DATA rời khỏi mức `muc`. Trả về số micro giây đã chờ, hoặc -1 nếu quá `toiDa` micro giây.
static int32_t choHetMuc(int muc, uint32_t toiDa) {
uint32_t batDau = micros();
while (digitalRead(CHAN_DHT) == muc) {
if (micros() - batDau > toiDa) return -1;
}
return (int32_t)(micros() - batDau);
}
// Đọc 5 byte: độ ẩm (2 byte), nhiệt độ (2 byte), checksum.
KetQua docDht(uint8_t du[5]) {
// Tín hiệu bắt đầu: kéo DATA xuống thấp (DHT11 ít nhất 18 ms, DHT22 ít nhất 1 ms) rồi nhả ra.
pinMode(CHAN_DHT, OUTPUT);
digitalWrite(CHAN_DHT, LOW);
delay(LOAI_CAM_BIEN == 11 ? 20 : 2);
pinMode(CHAN_DHT, INPUT_PULLUP);
uint16_t doDaiMucCao[40];
KetQua kq = DOC_DUOC;
// Cả gói tin dài khoảng 5 ms: tạm chặn ngắt để Wi-Fi, timer không làm lệch phép đo thời gian.
portENTER_CRITICAL(&khoaNgat);
// Cảm biến trả lời: chờ 20-40 us ở mức cao, kéo thấp 80 us, lên cao 80 us.
if (choHetMuc(HIGH, 100) < 0 || choHetMuc(LOW, 100) < 0 || choHetMuc(HIGH, 100) < 0) {
kq = KHONG_TRA_LOI;
}
for (int i = 0; i < 40 && kq == DOC_DUOC; i++) {
if (choHetMuc(LOW, 80) < 0) { // mỗi bit mở đầu bằng 50 us mức thấp
kq = LOI_THOI_GIAN;
break;
}
int32_t cao = choHetMuc(HIGH, 100); // rồi mức cao: khoảng 27 us là bit 0, khoảng 70 us là bit 1
if (cao < 0) {
kq = LOI_THOI_GIAN;
break;
}
doDaiMucCao[i] = (uint16_t)cao;
}
portEXIT_CRITICAL(&khoaNgat);
if (kq != DOC_DUOC) return kq;
for (int i = 0; i < 5; i++) du[i] = 0;
for (int i = 0; i < 40; i++) {
du[i / 8] = (du[i / 8] << 1) | (doDaiMucCao[i] > 48 ? 1 : 0); // ngưỡng 48 us nằm giữa 27 và 70
}
if (((du[0] + du[1] + du[2] + du[3]) & 0xFF) != du[4]) return SAI_CHECKSUM;
return DOC_DUOC;
}
// Đổi 5 byte thành độ ẩm (%) và nhiệt độ (độ C) theo loại cảm biến.
void doiGiaTri(const uint8_t du[5], float &doAm, float &nhietDo) {
if (LOAI_CAM_BIEN == 11) {
doAm = du[0] + du[1] * 0.1f;
nhietDo = du[2] + (du[3] & 0x0F) * 0.1f;
if (du[3] & 0x80) nhietDo = -nhietDo; // một số bản DHT11 mới báo nhiệt độ âm
} else {
doAm = ((du[0] << 8) | du[1]) * 0.1f;
nhietDo = (((du[2] & 0x7F) << 8) | du[3]) * 0.1f;
if (du[2] & 0x80) nhietDo = -nhietDo; // bit cao nhất là dấu âm
}
}
// Điểm sương theo công thức Magnus (b = 17,62; c = 243,12 độ C).
float diemSuong(float nhietDo, float doAm) {
float g = logf(doAm / 100.0f) + 17.62f * nhietDo / (243.12f + nhietDo);
return 243.12f * g / (17.62f - g);
}
void setup() {
Serial.begin(115200);
pinMode(CHAN_DHT, INPUT_PULLUP);
delay(1500); // cảm biến cần khoảng 1 giây ổn định sau khi cấp nguồn
Serial.printf("Doc DHT%d o chan GPIO%d\n", LOAI_CAM_BIEN, CHAN_DHT);
}
void loop() {
static uint32_t lanDocTruoc = 0;
if (millis() - lanDocTruoc < CHU_KY_DOC_MS) return;
lanDocTruoc = millis();
uint8_t du[5];
KetQua kq = docDht(du);
if (kq == KHONG_TRA_LOI) {
Serial.println("Cam bien khong tra loi: kiem tra day DATA, nguon, dien tro keo len");
return;
}
if (kq == LOI_THOI_GIAN) {
Serial.println("Mat dong bo giua chung: doc lai o lan sau");
return;
}
if (kq == SAI_CHECKSUM) {
Serial.printf("Sai checksum: %02X %02X %02X %02X %02X\n", du[0], du[1], du[2], du[3], du[4]);
return;
}
float doAm, nhietDo;
doiGiaTri(du, doAm, nhietDo);
Serial.printf("Byte: %02X %02X %02X %02X %02X | do am %.1f %%, nhiet do %.1f C, diem suong %.1f C\n",
du[0], du[1], du[2], du[3], du[4], doAm, nhietDo, diemSuong(nhietDo, doAm));
}
7. Giải thích các phần chính
choHetMuc(muc, toiDa)chờ chân DATA rời khỏi một mức và trả về thời gian đã chờ tính bằng micro giây. Quá thời gian thì trả về −1, nhờ vậy chương trình không treo khi rút dây cảm biến.portENTER_CRITICAL/portEXIT_CRITICALtạm chặn ngắt trong khoảng 5 ms đọc 40 bit. Không có đoạn này, ngắt của Wi-Fi hay timer có thể chen vào giữa một bit, làm bit 0 bị đo dài thành bit 1 và checksum sai. Đoạn chặn ngắt phải ngắn: không đặtdelay()haySerial.print()bên trong.- Ngưỡng 48 µs nằm giữa 27 µs (bit 0) và 70 µs (bit 1), nên vẫn đúng khi xung lệch vài micro giây do dây dài hay nguồn yếu.
doiGiaTri()đổi 5 byte ra độ ẩm và nhiệt độ theo đúng cách ở mục 5.diemSuong()tính điểm sương theo công thức Magnus (b = 17,62; c = 243,12 °C). Điểm sương là nhiệt độ mà hơi nước trong không khí bắt đầu đọng thành nước. Tường, sàn, đồ đạc lạnh hơn điểm sương sẽ bị đọng nước: đó là hiện tượng "nồm".CHU_KY_DOC_MS = 2500: đọc 2,5 giây một lần. Đọc DHT22 dày hơn 2 giây thì cảm biến trả về số cũ hoặc không trả lời.
8. Kết quả trên Serial Monitor

Mỗi dòng in 5 byte đọc được rồi mới tới giá trị đã đổi, nên khi số đo lạ có thể so lại từng byte với mục 5. Rút dây DATA ra thì chương trình báo "Cam bien khong tra loi" thay vì in số 0.
9. Đo cho đúng
- Đặt cảm biến cách xa ESP32, mạch nguồn và đèn ít nhất 5–10 cm: linh kiện nóng làm nhiệt độ đo cao lên, độ ẩm đo thấp xuống.
- Không đặt dưới nắng trực tiếp hay sát cửa gió máy lạnh. Ngoài trời thì cho vào hộp có khe thoáng, che mưa.
- Sau khi cấp nguồn hoặc mang cảm biến sang phòng khác, chờ vài phút cho số đo ổn định.
- So với đồng hồ nhiệt ẩm khác đặt cạnh nhau: DHT11 lệch 3–5 %RH là bình thường theo sai số của nó. Có thể cộng trừ một độ lệch cố định trong chương trình sau khi so.
- Muốn biết vì sao cảm biến độ ẩm điện dung, điện trở cho số khác nhau, đọc bài cấu tạo và nguyên lý cảm biến độ ẩm.
10. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| "Cam bien khong tra loi" mọi lần đọc | Sai chân, cắm DATA vào GND, thiếu điện trở kéo lên (cảm biến 4 chân trần) | Đọc lại chữ in cạnh chân; đo điện áp chân DATA lúc nghỉ phải gần 3,3 V |
| Thỉnh thoảng sai checksum | Dây dài, nhiễu từ relay, động cơ; nguồn 3,3 V sụt khi Wi-Fi phát | Dây ngắn dưới 1 m, xoắn dây DATA với GND, thêm tụ 100 nF sát chân VCC–GND của cảm biến |
| Đọc được DHT11 nhưng DHT22 không trả lời | Quên đổi LOAI_CAM_BIEN, đọc dày hơn 2 giây | Đặt LOAI_CAM_BIEN = 22, giữ CHU_KY_DOC_MS từ 2000 trở lên |
| Nhiệt độ cao hơn thực tế 2–5 °C | Cảm biến đặt sát ESP32 hoặc trong hộp kín | Đưa cảm biến ra xa, khoan lỗ thoáng cho hộp |
| Độ ẩm đứng yên ở 95–99 % | Cảm biến bị ướt hoặc đọng sương | Để khô ở nơi thoáng vài giờ; đặt cảm biến ở chỗ không bị nước bắn |
| Số đo DHT22 âm vô lý, ví dụ −3276,7 °C | Đổi byte nhiệt độ sai, coi bit dấu là bit giá trị | Tách bit cao nhất làm dấu như hàm doiGiaTri() |
11. Xem thêm
- ESP32 làm web server: hiển thị nhiệt độ, độ ẩm lên trang web trong mạng nhà.
- MQTT với ESP32: gửi số đo về máy chủ, điện thoại.
- Deep Sleep trên ESP32: trạm đo chạy pin, thức dậy đo rồi ngủ tiếp.
- Màn hình OLED 0.96 inch và LCD 1602 I2C để hiện số đo ngay trên mạch.
Xem thêm