Giao thức truyền thông trên STM32 (Phần 8): 1-Wire và DS18B20 – khe thời gian, CRC8 và bit-bang GPIO
1-Wire trên STM32 với cảm biến DS18B20, từ các khe thời gian, lệnh ROM, scratchpad và CRC8 (ví dụ đã kiểm tra) đến hai cách cài đặt bằng GPIO và bằng UART. Phần 8 của series giao thức truyền thông trên STM32.

Ghi chú: sơ đồ trong bài do Capitlab vẽ để minh hoạ. Số liệu trong các bảng là giá trị tính theo công thức, cột "Đo thực tế" sẽ được cập nhật khi đo trên phần cứng. CRC8 và các giá trị nhiệt độ ví dụ đã được kiểm tra bằng chương trình. Mã nguồn soạn theo tài liệu HAL và datasheet, chưa chạy thử trên phần cứng, nên hãy kiểm tra lại trước khi dùng cho sản phẩm.
1-Wire do Dallas tạo ra (sau này thuộc Maxim, giờ là Analog Devices). Nó chỉ cần một dây dữ liệu và GND, thậm chí lấy nguồn được từ chính dây dữ liệu (parasite power). Thiết bị 1-Wire hay gặp nhất là cảm biến nhiệt DS18B20.
Ít dây thì phải trả giá ở chỗ khác: tốc độ thấp và yêu cầu thời gian rất chặt. Lệch vài micro giây là đọc sai bit. Bài này giải thích các khe thời gian, cách đọc và kiểm tra dữ liệu DS18B20, rồi đưa ra hai cách cài trên STM32: bit-bang GPIO với timer micro giây, và mượn UART để phần cứng lo phần thời gian.
1. Nguyên lý
Dây dữ liệu (DQ) được kéo lên nguồn qua một điện trở, thường 4,7 kΩ. Lúc rảnh, đường dây ở mức cao. Master và slave đều chỉ có transistor kéo xuống GND và nhả ra để đường dây tự lên cao, nên không bao giờ có hai bên cùng đẩy và đối đầu nhau.
Mỗi chip có một mã ROM 64 bit duy nhất: 8 bit mã họ (DS18B20 là 0x28), 48 bit số serial và 8 bit CRC. Nhờ vậy một dây mang được nhiều cảm biến, và master gọi từng cái theo mã ROM.
Hình 1. Hai DS18B20 dùng chung một dây DQ, điện trở kéo lên 4,7 kΩ. Chân GPIO cấu hình output open-drain.
1.1. Các khe thời gian
Hình 2. Bốn thao tác cơ bản. Đỏ là master kéo thấp, xanh là slave kéo thấp, xám là điện trở kéo lên đưa đường dây lên cao.
| Thao tác | Master làm gì | Thời gian |
|---|---|---|
| Reset | Kéo thấp rồi nhả | Kéo thấp ít nhất 480 µs |
| Presence | Slave kéo thấp để báo có mặt | 60 đến 240 µs, bắt đầu sau khoảng 15 đến 60 µs kể từ lúc nhả |
| Ghi bit 1 | Kéo thấp ngắn rồi nhả | Kéo thấp khoảng 1 đến 15 µs, cả khe từ 60 µs trở lên |
| Ghi bit 0 | Giữ thấp suốt khe | Ít nhất 60 µs |
| Đọc bit | Kéo thấp khoảng 1 µs rồi nhả, lấy mẫu trong khoảng 15 µs | Cả khe từ 60 µs trở lên |
Slave lấy mẫu đường dây trong khoảng 15 đến 60 µs sau cạnh xuống của khe ghi. Bit đi theo thứ tự LSB trước. Giữa hai khe cần ít nhất 1 µs để điện trở kéo đường dây về cao.
Một lần đọc scratchpad 9 byte gồm 72 khe. Mỗi khe khoảng 70 µs, nên riêng phần đọc mất khoảng 5 ms, chưa tính reset, lệnh ROM và lệnh chức năng. Chậm hơn I²C nhiều, nhưng với cảm biến nhiệt đo vài lần mỗi giây thì không thành vấn đề.
1.2. Lệnh
Mỗi giao dịch gồm ba bước: reset, lệnh ROM, lệnh chức năng.
| Lệnh ROM | Mã | Ý nghĩa |
|---|---|---|
| Read ROM | 0x33 | Đọc mã ROM, chỉ dùng khi có một slave duy nhất |
| Match ROM | 0x55 | Chọn đúng một slave theo mã ROM 64 bit |
| Skip ROM | 0xCC | Bỏ qua địa chỉ, nói với tất cả (dùng khi chỉ có một slave) |
| Search ROM | 0xF0 | Duyệt tìm toàn bộ thiết bị trên bus |
| Lệnh chức năng DS18B20 | Mã | Ý nghĩa |
|---|---|---|
| Convert T | 0x44 | Bắt đầu đo nhiệt độ |
| Read Scratchpad | 0xBE | Đọc 9 byte bộ nhớ đệm |
| Write Scratchpad | 0x4E | Ghi TH, TL và thanh ghi cấu hình |
Thời gian chuyển đổi tối đa phụ thuộc độ phân giải: khoảng 93,75 ms ở 9 bit, 187,5 ms ở 10 bit, 375 ms ở 11 bit và 750 ms ở 12 bit. Không cần độ phân giải cao thì hạ xuống 9 hoặc 10 bit để đo nhanh hơn.
1.3. Scratchpad và CRC8
Hình 3. Scratchpad 9 byte với giá trị ngay sau khi cấp nguồn. Nhiệt độ mặc định 0x0550 là 85 °C.
Nhiệt độ là số 16 bit có dấu chia 16. Ở 12 bit, mỗi bước là 0,0625 °C. Các giá trị mẫu trong datasheet, đã kiểm tra lại bằng chương trình:
| Giá trị thô (hex) | Nhiệt độ |
|---|---|
| 0x07D0 | +125 °C |
| 0x0550 | +85 °C (giá trị mặc định lúc cấp nguồn) |
| 0x0191 | +25,0625 °C |
| 0xFF5E | −10,125 °C |
| 0xFC90 | −55 °C |
Giá trị 85 °C cần chú ý: đó là giá trị mặc định lúc cấp nguồn. Đọc ra 85 °C thường có nghĩa là chưa đo xong, hoặc chưa hề gửi lệnh đo.
CRC8 của Dallas/Maxim dùng đa thức x⁸ + x⁵ + x⁴ + 1 (dạng đảo bit 0x8C), giá trị khởi tạo 0. Có thể đối chiếu bằng máy tính CRC của Capitlab, chọn CRC-8/MAXIM:
uint8_t ow_crc8(const uint8_t *p, uint8_t n)
{
uint8_t crc = 0;
while (n--) {
uint8_t b = *p++;
for (int i = 0; i < 8; i++) {
uint8_t mix = (crc ^ b) & 1;
crc >>= 1;
if (mix) crc ^= 0x8C;
b >>= 1;
}
}
return crc;
}
// Ví dụ: 50 05 4B 46 7F FF 0C 10 -> CRC = 0x1C (đã kiểm tra)
Tính CRC8 trên cả 9 byte, gồm cả byte CRC ở cuối, phải ra 0. Với mã ROM 8 byte cũng vậy: CRC của 7 byte đầu bằng byte thứ 8. 1-Wire không có cơ chế phát hiện lỗi nào khác, nên mọi lần đọc đều phải kiểm tra CRC.
2. Cách 1: bit-bang bằng GPIO và timer micro giây
Chân GPIO cấu hình output open-drain, dùng điện trở kéo lên bên ngoài. Cần một hàm trễ micro giây chính xác, thường là một timer 16 bit chạy 1 MHz:
// TIM6 chạy 1 MHz (prescaler = SYSCLK_MHz - 1), đã HAL_TIM_Base_Start
static inline void delay_us(uint16_t us)
{
__HAL_TIM_SET_COUNTER(&htim6, 0);
while (__HAL_TIM_GET_COUNTER(&htim6) < us) { }
}
#define OW_LOW() HAL_GPIO_WritePin(OW_GPIO_Port, OW_Pin, GPIO_PIN_RESET)
#define OW_REL() HAL_GPIO_WritePin(OW_GPIO_Port, OW_Pin, GPIO_PIN_SET) // open-drain: nhả
#define OW_READ() HAL_GPIO_ReadPin(OW_GPIO_Port, OW_Pin)
int ow_reset(void)
{
OW_LOW(); delay_us(480);
OW_REL(); delay_us(70);
int present = (OW_READ() == GPIO_PIN_RESET);
delay_us(410);
return present;
}
void ow_write_bit(int b)
{
__disable_irq();
OW_LOW();
delay_us(b ? 6 : 60);
OW_REL();
delay_us(b ? 64 : 10);
__enable_irq();
}
int ow_read_bit(void)
{
__disable_irq();
OW_LOW(); delay_us(2);
OW_REL(); delay_us(10);
int v = (OW_READ() == GPIO_PIN_SET);
__enable_irq();
delay_us(50);
return v;
}
void ow_write_byte(uint8_t v) { for (int i = 0; i < 8; i++) { ow_write_bit(v & 1); v >>= 1; } }
uint8_t ow_read_byte(void) { uint8_t v = 0; for (int i = 0; i < 8; i++) v |= ow_read_bit() << i; return v; }
Ngắt được tắt trong từng khe, vì lệch vài micro giây là đọc sai. Mỗi khe trong mã trên dài khoảng 70 µs. Cách này đơn giản nhưng nhạy với tải hệ thống: RTOS và các ngắt khác có thể chen vào, và việc tắt ngắt, dù ngắn, cũng ảnh hưởng đến các tác vụ thời gian thực khác.
2.1. Đọc nhiệt độ từ một DS18B20
Khi bus chỉ có một cảm biến, dùng Skip ROM:
// Trả về 0 nếu thành công, âm nếu lỗi. *t tính bằng °C.
int ds18b20_read(float *t)
{
uint8_t sp[9];
if (!ow_reset()) return -1; // không có cảm biến
ow_write_byte(0xCC); // Skip ROM
ow_write_byte(0x44); // Convert T
HAL_Delay(750); // 12 bit (nên thay bằng timer hoặc RTOS để không chặn)
if (!ow_reset()) return -2;
ow_write_byte(0xCC);
ow_write_byte(0xBE); // Read Scratchpad
for (int i = 0; i < 9; i++) sp[i] = ow_read_byte();
if (ow_crc8(sp, 9) != 0) return -3; // CRC sai: bỏ kết quả
int16_t raw = (int16_t)((sp[1] << 8) | sp[0]);
*t = raw / 16.0f;
return 0;
}
Đọc ra 0x0550 (85 °C) hay toàn 0xFF là dấu hiệu đọc sai, và CRC sẽ bắt được phần lớn trường hợp. Trong sản phẩm thật, đừng chặn CPU 750 ms bằng HAL_Delay: gửi Convert T, rồi để timer hoặc RTOS gọi hàm đọc sau.
Với nhiều cảm biến, dùng Match ROM (0x55) kèm mã ROM 8 byte của từng cái. Để tìm mã ROM, dùng Search ROM (0xF0). Thuật toán duyệt cây nhị phân từng bit: ở mỗi bit, các slave gửi bit và bù của nó, master chọn nhánh, rồi lặp lại cho đến khi đi hết cây. Các thư viện 1-Wire có sẵn đã cài thuật toán này.
3. Cách 2: dùng UART làm 1-Wire
Mẹo hay dùng là biến mỗi khe thời gian thành một byte UART. Nối chân TX (cấu hình open-drain) và chân RX vào cùng một đường 1-Wire có điện trở kéo lên. Byte nào gửi đi cũng được nhận lại ngay trên RX, và nhìn byte nhận về là biết trạng thái đường dây.
Hình 4. Mỗi thao tác được biểu diễn bằng một byte UART với tốc độ baud phù hợp.
Để reset, đặt baudrate 9600 và gửi 0xF0. Đường dây xuống thấp khoảng 520 µs (bit start cộng 4 bit 0). Nếu byte nhận về khác 0xF0 thì có slave đang kéo đường dây, tức là có presence.
Với từng bit dữ liệu, đặt baudrate 115200. Gửi 0xFF để ghi 1 hoặc để đọc: bit start tạo một xung thấp ngắn khoảng 8,7 µs. Gửi 0x00 để ghi 0: đường dây thấp khoảng 78 µs. Khi đọc, nhìn bit thấp nhất của byte nhận về: 1 là slave không kéo (bit 1), 0 là slave kéo thấp (bit 0).
// UART2 cấu hình: TX open-drain, RX cùng nối vào đường 1-Wire, 8N1
static void ow_uart_baud(uint32_t baud)
{
huart2.Init.BaudRate = baud;
HAL_UART_Init(&huart2);
}
static uint8_t ow_uart_slot(uint8_t out)
{
uint8_t in = 0xFF;
__HAL_UART_FLUSH_DRREGISTER(&huart2); // bỏ byte cũ (macro tùy dòng chip)
HAL_UART_Transmit(&huart2, &out, 1, 10);
HAL_UART_Receive(&huart2, &in, 1, 10); // byte vừa gửi dội về RX
return in;
}
int ow_uart_reset(void)
{
ow_uart_baud(9600);
uint8_t in = ow_uart_slot(0xF0);
ow_uart_baud(115200);
return in != 0xF0; // khác 0xF0 nghĩa là có presence
}
int ow_uart_bit(int b)
{
uint8_t in = ow_uart_slot(b ? 0xFF : 0x00);
return (in & 0x01); // dùng khi đọc: gửi b = 1
}
Ở 115200 baud, mỗi khe dài khoảng 87 µs (10 bit × 8,68 µs), nằm trong giới hạn của 1-Wire. Đọc 9 byte scratchpad tốn 72 khe, khoảng 6,25 ms, so với khoảng 5 ms của cách GPIO.
Thời gian ở đây do phần cứng UART tạo ra nên không phụ thuộc ngắt, không phải tắt ngắt, và kết hợp được với DMA. Đổi lại, cách này chiếm một UART, và việc đổi baudrate liên tục cũng tốn thời gian. Khi hệ thống có nhiều ngắt hoặc chạy RTOS, cách UART ổn định hơn.
4. Chế độ parasite power
DS18B20 có thể lấy nguồn từ chính đường DQ (chân VDD của cảm biến nối xuống GND) thông qua một tụ bên trong. Lúc đo, cảm biến cần dòng lớn hơn mức điện trở kéo lên cấp được, nên ngay sau lệnh Convert T phải kéo cứng đường dây lên cao (strong pull-up) bằng một MOSFET hoặc chân GPIO push-pull, giữ suốt thời gian chuyển đổi. Với sản phẩm cần chắc chắn, đi ba dây và cấp VDD riêng thì đơn giản hơn.
5. Lỗi thường gặp
| Triệu chứng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Không thấy presence | Thiếu điện trở kéo lên, sai chân, cấu hình push-pull thay vì open-drain | Kiểm tra điện trở 4,7 kΩ, chọn output open-drain |
| Đọc ra 85 °C hoài | Chưa chờ đủ thời gian chuyển đổi (750 ms ở 12 bit), hoặc quên gửi 0x44 |
Chờ đủ, kiểm tra thứ tự lệnh |
| Đọc ra −0,0625 °C (0xFFFF) hoặc toàn 0xFF | Mất kết nối cảm biến, đường dây bị kéo cao | Kiểm tra dây và nguồn, kiểm tra CRC |
| Đọc sai ngẫu nhiên | Ngắt xen vào giữa khe thời gian | Tắt ngắt trong từng khe, hoặc dùng cách UART |
| Dây dài hoặc nhiều cảm biến bị lỗi CRC | Điện dung dây lớn làm cạnh lên chậm | Giảm điện trở kéo lên, dùng dây xoắn đôi chất lượng, giảm số cảm biến mỗi nhánh |
| Chế độ parasite power trả sai | Điện trở kéo lên không đủ dòng khi đang đo | Dùng strong pull-up bằng MOSFET, hoặc cấp VDD riêng |
| Hai cảm biến trả lời cùng lúc | Dùng Skip ROM khi trên bus có nhiều cảm biến | Dùng Match ROM với mã ROM của từng cái |
6. Lưu ý khi đi dây nhiều cảm biến
Điện dung của dây là trở ngại lớn nhất với 1-Wire. Khi có nhiều cảm biến, đi dây theo một đường thẳng và tránh các nhánh rẽ dài.
Sau khi chạy Search ROM một lần, lưu mã ROM của từng cảm biến vào bộ nhớ, lần khởi động sau khỏi phải tìm lại.
DS18B20 loại đầu dò chống nước thường đi kèm dây dài. Khi đó dùng điện trở kéo lên khoảng 2,2 kΩ đến 4,7 kΩ tuỳ chiều dài, và thử với đúng chiều dài dây sẽ lắp thật.
7. Hỏi đáp
Sao lúc nào cũng đọc ra 85 °C? Đó là giá trị mặc định lúc cấp nguồn. Đọc scratchpad trước khi chuyển đổi xong, hoặc quên gửi Convert T, đều cho ra giá trị này.
Một dây gắn được bao nhiêu DS18B20? Về lý thuyết thì rất nhiều, vì mỗi chip có ROM riêng. Thực tế bị giới hạn bởi điện dung dây và dòng của điện trở kéo lên. Dây ngắn có thể chạy vài chục cảm biến, dây dài thì ít hơn nhiều. Phải thử trên bus thật.
1-Wire có nhanh hơn I²C không? Không, chậm hơn nhiều. Tốc độ chuẩn khoảng 15,4 kbit/s (khe 65 µs mỗi bit), chế độ overdrive khoảng 125 kbit/s. Bù lại chỉ cần một dây.
Nên dùng GPIO hay UART? GPIO đơn giản và không tốn UART nhưng nhạy với ngắt. UART ổn định hơn khi hệ thống bận ngắt hoặc chạy RTOS, đổi lại chiếm một UART.
Ngoài DS18B20 còn thiết bị 1-Wire nào? Có iButton (khoá điện tử), EEPROM DS2431, bộ ghép kênh, chip nhận dạng pin. Phần lớn dùng cùng khe thời gian và CRC8, chỉ khác lệnh chức năng và mã họ trong ROM.
8. Kế hoạch đo thực tế
Cột "Đo thực tế" để trống cho đến khi chạy trên phần cứng.
| Thử nghiệm | Cách đo | Giá trị tính toán | Đo thực tế |
|---|---|---|---|
| Độ rộng xung reset và presence | Logic analyzer hoặc oscilloscope | Reset ≥ 480 µs, presence 60 đến 240 µs | (điền sau) |
| Độ chính xác khe ghi 0, ghi 1, đọc (GPIO) | Oscilloscope, so với bảng timing | Ghi 0: 60 µs, ghi 1: 6 µs thấp, khe 70 µs | (điền sau) |
| Độ rộng reset bằng UART ở 9600 | Logic analyzer | Khoảng 520 µs | (điền sau) |
| Ổn định GPIO và UART khi có ngắt nền | Đọc 1000 lần, đếm số lần CRC lỗi | GPIO có thể lỗi nếu ngắt xen vào | (điền sau) |
| Thời gian đọc scratchpad 9 byte | Logic analyzer | Khoảng 5 ms (GPIO), 6,25 ms (UART) | (điền sau) |
| Thời gian chuyển đổi thực ở 9, 10, 11, 12 bit | Đo từ lệnh 0x44 đến khi có dữ liệu hợp lệ |
Tối đa 93,75, 187,5, 375, 750 ms | (điền sau) |
| Số cảm biến và chiều dài dây chạy ổn | Tăng dần, ghi nhận lúc xuất hiện lỗi CRC | Phụ thuộc điện dung dây | (điền sau) |
| Độ chính xác nhiệt độ | So sánh với nhiệt kế chuẩn tại vài điểm | Theo datasheet | (điền sau) |
Kết luận
Luôn kiểm tra CRC8, và nghi ngờ mỗi khi thấy 85 °C. Đừng để ngắt làm lệch khe thời gian. Khi hệ thống bận, dùng cách UART để phần cứng lo thời gian.
Phần 9 chuyển sang USB CDC: biến STM32 thành một cổng COM ảo cắm thẳng vào máy tính.
Để kiểm tra CRC8 hoặc giải mã giá trị nhiệt độ thô, có thể dùng máy tính CRC và Hex Converter của Capitlab.
Tài liệu tham khảo
- DS18B20 and STM32 (ControllersTech)
- STM32 1-Wire (One-Wire) Protocol DS18B20 Code Example (DeepBlueMbedded)
- Datasheet DS18B20 (Maxim/Analog Devices)