1-Wire là gì: khe thời gian, mã ROM và cách đọc DS18B20 bằng ESP32
Giải thích giao tiếp 1-Wire: một dây dữ liệu, điện trở kéo lên, bốn kiểu khe thời gian, mã ROM 64 bit và CRC. Kèm hai chương trình ESP32 tự viết giao thức để đọc mã ROM và nhiệt độ DS18B20.
1-Wire là chuẩn giao tiếp chỉ cần một dây dữ liệu. Thiết bị quen thuộc nhất dùng chuẩn này là cảm biến nhiệt độ DS18B20: nhiều cảm biến mắc chung một dây, mỗi cảm biến có một mã riêng 64 bit. Vì không có dây clock, 1-Wire phân biệt bit 0 và bit 1 bằng thời gian, tính theo micro giây. Bài viết giải thích các khe thời gian đó và tự viết giao thức trên ESP32, không dùng thư viện, để đọc mã ROM và nhiệt độ của DS18B20.
1. Bus 1-Wire gồm những gì?
- Một dây dữ liệu (thường ghi DQ) dùng cho cả hai chiều, cộng với dây GND. Thiết bị có thể lấy nguồn riêng qua chân VDD.
- Dây DQ thuộc loại cực máng hở giống I2C: các thiết bị chỉ kéo dây xuống mức thấp, mức cao do một điện trở kéo lên 4,7 kΩ tạo ra.
- Có một thiết bị chủ (ESP32) khởi đầu mọi trao đổi. Các thiết bị tớ chỉ trả lời.
- Mỗi thiết bị tớ mang một mã ROM 64 bit ghi sẵn từ nhà máy: 8 bit mã họ thiết bị, 48 bit số sê-ri và 8 bit CRC. Mã họ của DS18B20 là 0x28.
- Tốc độ thấp, khoảng 16 kbit/s, nhưng dây đi được hàng chục mét. Chuẩn này hợp với các điểm đo nhiệt độ rải rác.
2. Bốn kiểu khe thời gian
| Khe | Thiết bị chủ làm gì | Thiết bị tớ làm gì |
|---|---|---|
| Reset | Kéo dây xuống 480 µs rồi nhả, đọc dây sau 70 µs | Kéo dây xuống 60 đến 240 µs để báo có mặt |
| Ghi bit 1 | Kéo dây xuống 6 µs rồi nhả đến hết khe | Đọc dây khoảng 30 µs sau sườn xuống: thấy mức cao |
| Ghi bit 0 | Kéo dây xuống 60 µs rồi nhả 10 µs | Đọc dây khoảng 30 µs sau sườn xuống: thấy mức thấp |
| Đọc bit | Kéo dây xuống 6 µs, nhả ra, đọc dây sau 9 µs | Giữ dây ở mức thấp nếu bit là 0, để yên nếu bit là 1 |
- Mỗi bit chiếm một khe khoảng 70 µs và luôn do thiết bị chủ mở đầu.
- Mỗi byte gửi bit thấp nhất trước.
- Thời gian phải chính xác đến vài micro giây. Chương trình tạm cấm ngắt trong lúc tạo từng khe để không bị việc khác chen vào.
3. Trình tự một lần trao đổi
Mọi trao đổi trên 1-Wire đi theo ba bước: xung reset, một lệnh về mã ROM để chọn thiết bị, rồi một lệnh chức năng.
| Lệnh | Mã | Ý nghĩa |
|---|---|---|
| Read ROM | 0x33 | Đọc mã ROM; chỉ dùng khi trên dây có đúng một thiết bị |
| Skip ROM | 0xCC | Bỏ qua mã ROM, nói với mọi thiết bị trên dây |
| Match ROM | 0x55 | Gửi kèm 8 byte mã ROM để chọn đúng một thiết bị |
| Convert T | 0x44 | DS18B20 bắt đầu đo nhiệt độ; mất tối đa 750 ms ở độ phân giải 12 bit |
| Read Scratchpad | 0xBE | Đọc 9 byte kết quả của DS18B20 |
Hai byte đầu của kết quả là nhiệt độ dạng số có dấu 16 bit, byte thấp trước, mỗi đơn vị bằng 1/16 °C. Byte thứ chín là CRC của 8 byte trước, dùng để phát hiện lỗi truyền.
4. Chuẩn bị
- Một mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340).
- Một cảm biến DS18B20 (dạng ba chân, dạng đầu dò có dây, hoặc module) và một điện trở 4,7 kΩ.
- Breadboard và dây cắm.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Không cần cài thêm thư viện.
5. Sơ đồ nối
- Điện trở 4,7 kΩ giữa DQ và 3V3 là bắt buộc. Module DS18B20 ba chân thường đã hàn sẵn điện trở này.
- Với cảm biến dạng ba chân, kiểm tra kỹ thứ tự chân trước khi cấp nguồn: cắm ngược cực làm cảm biến nóng lên rất nhanh.
- Đầu dò có dây thường dùng màu đỏ cho VDD, đen cho GND và vàng cho DQ; nên đối chiếu với thông tin của nơi bán.
6. Chương trình 1: reset và đọc mã ROM
Chương trình tạo xung reset, kiểm tra thiết bị có trả lời, gửi lệnh Read ROM rồi đọc 8 byte mã ROM và kiểm tra CRC. Bốn hàm datLai(), ghiBit(), docBit() ứng với bốn khe thời gian ở mục 2.
/*
PYWORLD.VN - 1-Wire trên ESP32: tự tạo xung reset, đọc từng bit và đọc mã ROM 64 bit của thiết bị
Không dùng thư viện OneWire; mỗi hàm ứng với một kiểu khe thời gian của giao thức
Dây dữ liệu nối D4 (GPIO4), có điện trở 4,7 kΩ kéo lên 3,3 V; trên dây chỉ có một thiết bị
*/
#define DQ_PIN 4
// Xung reset: kéo dây xuống 480 micro giây rồi nhả. Thiết bị có mặt sẽ kéo dây xuống để trả lời.
bool datLai() {
digitalWrite(DQ_PIN, LOW);
delayMicroseconds(480);
noInterrupts();
digitalWrite(DQ_PIN, HIGH);
delayMicroseconds(70);
bool coThietBi = digitalRead(DQ_PIN) == LOW;
interrupts();
delayMicroseconds(410);
return coThietBi;
}
// Ghi bit 1: kéo xuống thật ngắn (6 micro giây). Ghi bit 0: giữ thấp gần hết khe (60 micro giây).
void ghiBit(bool bit) {
noInterrupts();
digitalWrite(DQ_PIN, LOW);
delayMicroseconds(bit ? 6 : 60);
digitalWrite(DQ_PIN, HIGH);
delayMicroseconds(bit ? 64 : 10);
interrupts();
}
// Đọc bit: kéo xuống 6 micro giây, nhả ra rồi đọc dây sau 9 micro giây. Thiết bị giữ dây thấp nếu bit là 0.
bool docBit() {
noInterrupts();
digitalWrite(DQ_PIN, LOW);
delayMicroseconds(6);
digitalWrite(DQ_PIN, HIGH);
delayMicroseconds(9);
bool bit = digitalRead(DQ_PIN);
interrupts();
delayMicroseconds(55);
return bit;
}
// Mỗi byte gửi bit thấp trước
void ghiByte(uint8_t giaTri) {
for (int i = 0; i < 8; i++) {
ghiBit((giaTri >> i) & 1);
}
}
uint8_t docByte() {
uint8_t giaTri = 0;
for (int i = 0; i < 8; i++) {
if (docBit()) {
giaTri |= (1 << i);
}
}
return giaTri;
}
// CRC 8 bit của 1-Wire (đa thức x^8 + x^5 + x^4 + 1)
uint8_t tinhCrc8(const uint8_t *duLieu, int soByte) {
uint8_t crc = 0;
for (int i = 0; i < soByte; i++) {
uint8_t b = duLieu[i];
for (int k = 0; k < 8; k++) {
bool tron = (crc ^ b) & 0x01;
crc >>= 1;
if (tron) {
crc ^= 0x8C;
}
b >>= 1;
}
}
return crc;
}
void setup() {
Serial.begin(115200);
digitalWrite(DQ_PIN, HIGH);
pinMode(DQ_PIN, OUTPUT_OPEN_DRAIN); // Chỉ kéo xuống hoặc nhả; mức cao do điện trở kéo lên tạo ra
}
void loop() {
delay(2000);
if (digitalRead(DQ_PIN) == LOW) {
Serial.println("Day du lieu dang o muc thap: thieu dien tro keo len 4,7 kOhm hoac day bi cham GND");
return;
}
if (!datLai()) {
Serial.println("Khong co thiet bi tra loi xung reset");
return;
}
// Lệnh 0x33 (Read ROM): thiết bị gửi lại 8 byte mã ROM. Chỉ dùng khi trên dây có đúng một thiết bị.
ghiByte(0x33);
uint8_t maRom[8];
for (int i = 0; i < 8; i++) {
maRom[i] = docByte();
}
Serial.print("Ma ROM:");
for (int i = 0; i < 8; i++) {
Serial.printf(" %02X", maRom[i]);
}
bool crcDung = tinhCrc8(maRom, 7) == maRom[7] && maRom[0] != 0x00;
Serial.printf(" | ho thiet bi 0x%02X%s | CRC %s\n", maRom[0], maRom[0] == 0x28 ? " (DS18B20)" : "", crcDung ? "dung" : "SAI");
}
7. Chương trình 2: đọc nhiệt độ
Chương trình dùng lại các hàm mức thấp ở trên và thực hiện trình tự ở mục 3: ra lệnh đo, chờ 750 ms, đọc 9 byte, kiểm tra CRC rồi đổi hai byte đầu thành độ C.
/*
PYWORLD.VN - 1-Wire trên ESP32: đọc nhiệt độ từ cảm biến DS18B20 bằng giao thức tự viết
Trình tự: reset, bỏ qua mã ROM, ra lệnh đo, chờ 750 ms, reset, bỏ qua mã ROM, đọc 9 byte kết quả, kiểm tra CRC
Dây dữ liệu nối D4 (GPIO4), có điện trở 4,7 kΩ kéo lên 3,3 V; trên dây chỉ có một cảm biến
*/
#define DQ_PIN 4
const uint8_t LENH_BO_QUA_ROM = 0xCC; // Skip ROM: nói với mọi thiết bị trên dây, dùng khi chỉ có một thiết bị
const uint8_t LENH_DO_NHIET_DO = 0x44; // Convert T
const uint8_t LENH_DOC_KET_QUA = 0xBE; // Read Scratchpad
const int THOI_GIAN_DO_MS = 750; // Thời gian đo dài nhất ở độ phân giải 12 bit
bool datLai() {
digitalWrite(DQ_PIN, LOW);
delayMicroseconds(480);
noInterrupts();
digitalWrite(DQ_PIN, HIGH);
delayMicroseconds(70);
bool coThietBi = digitalRead(DQ_PIN) == LOW;
interrupts();
delayMicroseconds(410);
return coThietBi;
}
void ghiBit(bool bit) {
noInterrupts();
digitalWrite(DQ_PIN, LOW);
delayMicroseconds(bit ? 6 : 60);
digitalWrite(DQ_PIN, HIGH);
delayMicroseconds(bit ? 64 : 10);
interrupts();
}
bool docBit() {
noInterrupts();
digitalWrite(DQ_PIN, LOW);
delayMicroseconds(6);
digitalWrite(DQ_PIN, HIGH);
delayMicroseconds(9);
bool bit = digitalRead(DQ_PIN);
interrupts();
delayMicroseconds(55);
return bit;
}
void ghiByte(uint8_t giaTri) {
for (int i = 0; i < 8; i++) {
ghiBit((giaTri >> i) & 1);
}
}
uint8_t docByte() {
uint8_t giaTri = 0;
for (int i = 0; i < 8; i++) {
if (docBit()) {
giaTri |= (1 << i);
}
}
return giaTri;
}
uint8_t tinhCrc8(const uint8_t *duLieu, int soByte) {
uint8_t crc = 0;
for (int i = 0; i < soByte; i++) {
uint8_t b = duLieu[i];
for (int k = 0; k < 8; k++) {
bool tron = (crc ^ b) & 0x01;
crc >>= 1;
if (tron) {
crc ^= 0x8C;
}
b >>= 1;
}
}
return crc;
}
// Trả về true và ghi nhiệt độ vào *nhietDo nếu đọc được và CRC đúng
bool docNhietDo(float *nhietDo) {
if (!datLai()) {
Serial.println("Khong co cam bien tra loi xung reset");
return false;
}
ghiByte(LENH_BO_QUA_ROM);
ghiByte(LENH_DO_NHIET_DO);
delay(THOI_GIAN_DO_MS);
if (!datLai()) {
Serial.println("Mat cam bien sau khi ra lenh do");
return false;
}
ghiByte(LENH_BO_QUA_ROM);
ghiByte(LENH_DOC_KET_QUA);
uint8_t ketQua[9];
for (int i = 0; i < 9; i++) {
ketQua[i] = docByte();
}
Serial.print("9 byte:");
for (int i = 0; i < 9; i++) {
Serial.printf(" %02X", ketQua[i]);
}
if (tinhCrc8(ketQua, 8) != ketQua[8]) {
Serial.println(" | CRC SAI, bo ket qua");
return false;
}
// Hai byte đầu là nhiệt độ: byte thấp trước, số có dấu, mỗi đơn vị là 1/16 độ C
int16_t giaTriTho = (int16_t)(((uint16_t)ketQua[1] << 8) | ketQua[0]);
*nhietDo = giaTriTho / 16.0f;
return true;
}
void setup() {
Serial.begin(115200);
digitalWrite(DQ_PIN, HIGH);
pinMode(DQ_PIN, OUTPUT_OPEN_DRAIN);
}
void loop() {
float nhietDo = 0;
if (digitalRead(DQ_PIN) == LOW) {
Serial.println("Day du lieu dang o muc thap: thieu dien tro keo len 4,7 kOhm hoac day bi cham GND");
} else if (docNhietDo(&nhietDo)) {
Serial.printf(" | CRC dung | nhiet do %.2f do C\n", nhietDo);
}
delay(2000);
}
Lệnh Skip ROM chỉ đúng khi trên dây có một cảm biến. Với nhiều cảm biến, thiết bị chủ phải dò tìm mã ROM của từng cảm biến rồi dùng lệnh Match ROM để gọi riêng từng cái. Thuật toán dò tìm khá dài; trong ứng dụng thật nên dùng thư viện có sẵn cho phần này.
8. Kết quả mong đợi
- Chương trình 1: mở Tools > Serial Monitor ở 115200 baud. Mỗi 2 giây hiện mã ROM; byte đầu là
28và CRC đúng. - Chương trình 2: mỗi lần đọc hiện 9 byte và nhiệt độ. Nắm cảm biến trong tay vài giây: nhiệt độ tăng dần.
- Nếu nhiệt độ luôn là 85,00 °C: đó là giá trị mặc định của cảm biến khi chưa đo xong, xem bảng xử lý lỗi.
9. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Báo dây dữ liệu đang ở mức thấp | Thiếu điện trở kéo lên, hoặc DQ chạm GND | Nối điện trở 4,7 kΩ giữa DQ và 3V3; kiểm tra dây |
| Không có thiết bị trả lời xung reset | Sai chân, cảm biến chưa có nguồn hoặc cắm ngược | Kiểm tra ba dây và thứ tự chân của cảm biến |
| CRC sai, lúc được lúc không | Dây dài, điện trở kéo lên quá lớn, nguồn nhiễu | Rút ngắn dây, giảm điện trở xuống 2,2 kΩ, thêm tụ 100 nF sát cảm biến |
| Nhiệt độ luôn là 85,00 °C | Đọc kết quả khi cảm biến chưa đo xong, thường do nguồn yếu | Giữ đủ 750 ms chờ; cấp nguồn riêng vào chân VDD |
Mã ROM toàn FF | Cảm biến có trả lời reset nhưng không gửi dữ liệu | Kiểm tra tiếp xúc của dây DQ; thử cảm biến khác |
Byte đầu của mã ROM không phải 28 | Thiết bị không phải DS18B20 (ví dụ DS18S20 có mã họ 0x10) | Cách đổi nhiệt độ của từng loại khác nhau; xem tài liệu của đúng loại |
10. Xem thêm
- Giao tiếp I2C trên ESP32, chuẩn hai dây cũng dùng điện trở kéo lên.
- Ngắt GPIO trên ESP32.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm