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ì?

Sơ đồ bus 1-Wire: ESP32 làm thiết bị chủ nối ba cảm biến DS18B20 song song trên một dây dữ liệu DQ có điện trở kéo lên 4,7 kilôôm và dây GND chung, mỗi cảm biến có một mã ROM riêng
Mọi thiết bị nối song song vào một dây dữ liệu DQ có điện trở kéo lên (mã ROM trong hình là số ví dụ).
  • 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

Bốn kiểu khe thời gian của 1-Wire: xung reset 480 micro giây và xung trả lời của thiết bị, ghi bit 1 bằng xung thấp 6 micro giây, ghi bit 0 bằng xung thấp 60 micro giây, đọc bit bằng cách lấy mẫu sau 9 micro giây
Reset, ghi bit 1, ghi bit 0 và đọc bit (hình vẽ minh họa, không theo tỉ lệ thời gian).
KheThiết bị chủ làm gìThiết bị tớ làm gì
ResetKéo dây xuống 480 µs rồi nhả, đọc dây sau 70 µsKéo dây xuống 60 đến 240 µs để báo có mặt
Ghi bit 1Ké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 0Ké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 bitKéo dây xuống 6 µs, nhả ra, đọc dây sau 9 µsGiữ 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

Trình tự đọc nhiệt độ DS18B20 qua 1-Wire: reset, lệnh 0xCC bỏ qua mã ROM, lệnh 0x44 đo nhiệt độ, chờ, rồi reset, 0xCC, lệnh 0xBE đọc chín byte kết quả gồm nhiệt độ, ngưỡng, cấu hình và CRC
Hai bước đọc nhiệt độ và ý nghĩa của 9 byte kết quả (giá trị là số ví dụ).

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ệnhMãÝ nghĩa
Read ROM0x33Đọc mã ROM; chỉ dùng khi trên dây có đúng một thiết bị
Skip ROM0xCCBỏ qua mã ROM, nói với mọi thiết bị trên dây
Match ROM0x55Gửi kèm 8 byte mã ROM để chọn đúng một thiết bị
Convert T0x44DS18B20 bắt đầu đo nhiệt độ; mất tối đa 750 ms ở độ phân giải 12 bit
Read Scratchpad0xBEĐọ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

Sơ đồ nối ESP32 DevKit V1 với cảm biến DS18B20: DQ nối D4, GND nối GND, VDD nối 3V3 và một điện trở 4,7 kilôôm nối giữa DQ với 3V3
DQ nối D4, nguồn 3,3 V, điện trở 4,7 kΩ nối giữa DQ và 3V3.
  • Đ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

Hình minh họa Serial Monitor của hai chương trình 1-Wire trên ESP32: mã ROM tám byte bắt đầu bằng 28 kèm kết quả CRC, và chín byte kết quả kèm nhiệt độ tính ra
Kết quả mong đợi trên Serial Monitor của hai chương trình (hình minh họa, mã ROM và nhiệt độ là số ví dụ).
  1. Chương trình 1: mở Tools > Serial Monitor ở 115200 baud. Mỗi 2 giây hiện mã ROM; byte đầu là 28 và CRC đúng.
  2. 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.
  3. 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ượngNguyên nhân thường gặpCách xử lý
Báo dây dữ liệu đang ở mức thấpThiếu điện trở kéo lên, hoặc DQ chạm GNDNố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 resetSai chân, cảm biến chưa có nguồn hoặc cắm ngượcKiểm tra ba dây và thứ tự chân của cảm biến
CRC sai, lúc được lúc khôngDây dài, điện trở kéo lên quá lớn, nguồn nhiễuRú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ếuGiữ đủ 750 ms chờ; cấp nguồn riêng vào chân VDD
Mã ROM toàn FFCảm biến có trả lời reset nhưng không gửi dữ liệuKiể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 28Thiế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


Tin tức liên quan

Bluetooth Low Energy (BLE) là gì: quảng bá, GATT, dịch vụ, đặc tính và ESP32
Bluetooth Low Energy (BLE) là gì: quảng bá, GATT, dịch vụ, đặc tính và ESP32

Giải thích Bluetooth Low Energy: quảng bá và kết nối, cấu trúc GATT gồm dịch vụ, đặc tính, mô tả, UUID. Hướng dẫn cho ESP32 làm thiết bị BLE có đặc tính đọc, ghi, thông báo và thử bằng điện thoại.

Điều khiển hồng ngoại hoạt động thế nào: sóng mang 38 kHz, giao thức NEC và ESP32
Điều khiển hồng ngoại hoạt động thế nào: sóng mang 38 kHz, giao thức NEC và ESP32

Giải thích điều khiển hồng ngoại: sóng mang 38 kHz, mắt thu và giao thức NEC với khung 32 bit, mã lặp. Hướng dẫn tự giải mã phím điều khiển và tự phát lệnh bằng ESP32, không dùng thư viện ngoài.

RF 433 MHz là gì: điều chế ASK/OOK và tự viết giao thức phát, thu bằng ESP32
RF 433 MHz là gì: điều chế ASK/OOK và tự viết giao thức phát, thu bằng ESP32

Giải thích sóng RF 433 MHz và điều chế ASK/OOK, cách mã hóa bit bằng độ rộng xung và khung tin 24 bit. Hướng dẫn tự viết chương trình phát và thu trên hai mạch ESP32, không dùng thư viện ngoài.

Vòng dòng 4-20 mA là gì và cách đọc cảm biến 4-20 mA bằng ESP32
Vòng dòng 4-20 mA là gì và cách đọc cảm biến 4-20 mA bằng ESP32

Giải thích vòng dòng 4-20 mA: vì sao dùng dòng điện, vì sao bắt đầu từ 4 mA, cách đổi ra giá trị đo và ngưỡng báo lỗi. Hướng dẫn đọc bằng ESP32 qua điện trở shunt 120 Ω, với ADC có sẵn và với ADS1115.

CAN bus (phần 2): bộ lọc nhận, tốc độ bit và xử lý lỗi bus-off trên ESP32
CAN bus (phần 2): bộ lọc nhận, tốc độ bit và xử lý lỗi bus-off trên ESP32

Phần 2 về CAN bus với ESP32: đặt bộ lọc nhận theo mã lọc và mặt nạ, chọn tốc độ bit theo chiều dài dây, hiểu ba trạng thái lỗi của nút và viết chương trình theo dõi bộ đếm lỗi, tự hồi phục sau bus-off.

CAN bus (phần 1): tín hiệu, khung tin, phân xử ưu tiên và gửi nhận bằng ESP32
CAN bus (phần 1): tín hiệu, khung tin, phân xử ưu tiên và gửi nhận bằng ESP32

Giải thích CAN bus: hai dây CAN_H, CAN_L, mức trội và mức lặn, khung tin có ID 11 bit, phân xử ưu tiên, CRC và ACK. Kèm hai chương trình ESP32 dùng bộ điều khiển TWAI: tự kiểm tra và hai nút gửi nhận.

Modbus TCP/IP (phần 2): ESP32 làm máy khách và cổng chuyển đổi Modbus TCP sang RTU
Modbus TCP/IP (phần 2): ESP32 làm máy khách và cổng chuyển đổi Modbus TCP sang RTU

Hướng dẫn dùng ESP32 làm máy khách Modbus TCP đọc thanh ghi của một thiết bị trong mạng, và làm cổng chuyển đổi Modbus TCP sang Modbus RTU qua RS-485: cách đổi khung, mã thiết bị, thời gian chờ, mã lỗi 0x0B.

Modbus TCP/IP (phần 1): phần đầu MBAP và ESP32 làm máy chủ qua Wi-Fi
Modbus TCP/IP (phần 1): phần đầu MBAP và ESP32 làm máy chủ qua Wi-Fi

Giải thích Modbus TCP/IP: phần đầu MBAP, cổng 502, điểm khác với Modbus RTU. Kèm chương trình ESP32 làm máy chủ Modbus TCP qua Wi-Fi với 10 thanh ghi giữ và 8 cuộn, tự phân tích khung, không dùng thư viện.


Bình luận
Đã thêm vào giỏ hàng