
OneWire 是什麼:一條線為什麼能掛好幾顆
OneWire(又稱 1-Wire)是 Dallas Semiconductor 的匯流排協定,它最誇張的地方是:資料、時序、甚至供電都可以只用一條線。加上地線,兩條線就能跑一顆感測器。
我第一次看到 DS18B20 的接線圖時的反應是「這樣真的能通訊?」。答案是能,而且很穩 —— 前提是你搞懂它的三個設計:
- 開漏匯流排:所有裝置只能「拉低」或「放開」,所以一定要有上拉電阻
- Master 發起一切:Slave 不會主動說話,每個時槽都由 Master 開頭
- 64-bit ROM ID:每顆出廠就燒好唯一序號,這是同一條線能分辨多顆的關鍵

這也是為什麼 OneWire 常常被拿來當「匯流排最小化」的教科書例子:接線成本幾乎為零,代價是軟體要非常守時。
通訊時序:所有操作都由 Master 發起
OneWire 沒有時脈線,所以「時間」就是它的時脈。每一個動作都有明確的時間窗口:

先看初始化。Master 把 DQ 拉低至少 480 µs 當作 Reset,放開之後,Slave 會在 15~60 µs 後拉低 60~240 µs 回一個 Presence Pulse —— 這就是在說「我在這裡」。
寫位元更有意思:Write-0 與 Write-1 的差別只差在 Master 拉低多久。

拉低 1~15 µs 然後放開 = 寫 1;拉低 60~120 µs = 寫 0。讀取則反過來:Master 拉低 1~15 µs 啟動時槽後放開,Slave 在 15 µs 內決定要不要拉低,Master 大約在 15 µs 時採樣。
看完這些數字就知道一件重要的事:整個位元只有 60~120 µs。軟體模擬 OneWire 時,任何一次中斷都可能把時序吃穿 —— 這是後面實作一定會遇到的坑。
常用 ROM 指令:先找到裝置,再跟它說話
| 指令 | 代碼 | 說明 |
|---|---|---|
| Search ROM | 0xF0 | 搜尋匯流排上所有 DS18B20 的 ROM ID |
| Read ROM | 0x33 | 讀取單顆裝置的 ROM ID(僅單顆時可用) |
| Match ROM | 0x55 | 指定特定 ROM ID 進行通訊 |
| Skip ROM | 0xCC | 跳過 ROM 比對(單顆或多顆一起控制時用) |
| Alarm Search | 0xEC | 搜尋觸發警報條件的裝置 |
ROM 指令負責「找裝置」。單顆時最常用的是 0xCC(Skip ROM),直接跳過位址比對;多顆時就得靠 0x55(Match ROM) 指定 ROM ID,或者用 0xF0(Search ROM) 把線上所有裝置點名一遍。
常用功能指令:找到之後要做什麼
| 指令 | 代碼 | 說明 |
|---|---|---|
| Convert T | 0x44 | 啟動溫度轉換 |
| Write Scratchpad | 0x4E | 寫入 Byte2~4(TH, TL, 設定) |
| Read Scratchpad | 0xBE | 讀取 9 bytes 暫存器 |
| Copy Scratchpad | 0x48 | 將暫存器複製到 EEPROM |
| Recall EEPROM | 0xB8 | 從 EEPROM 復原暫存器 |
| Read Power Supply | 0xB4 | 讀取供電模式(0=寄生, 1=外接) |
功能指令負責「做事」。讀溫度的標準兩步是 0x44(Convert T) 啟動轉換,等到轉換完成後再送 0xBE(Read Scratchpad) 把 9 個位元組的暫存器讀回來。12-bit 解析度下,轉換時間約 750 ms。
讀一次溫度的完整流程

注意流程裡的順序:轉換與讀取之間必須再 Reset 一次。這是很多人第一次寫會漏掉的步驟 —— 因為 Convert T 之後裝置就「忙」起來了,要重新建立通訊。
ESP32 實作:單顆 DS18B20
Arduino 生態裡有現成的 OneWire 與 DallasTemperature 函式庫,單顆的情況最簡單:
// ESP32 Arduino - DS18B20 溫度讀取
#include
#include
#define ONE_WIRE_BUS 4 // GPIO 4
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
void setup() {
Serial.begin(115200);
sensors.begin(); // 初始化匯流排
// 顯示匯流排上 DS18B20 的數量
int count = sensors.getDeviceCount();
Serial.printf("發現 %d 顆 DS18B20\n", count);
// 設定解析度(可選,預設 12-bit)
sensors.setResolution(12);
}
void loop() {
// 請求溫度轉換(非阻塞版本)
sensors.requestTemperatures();
// 讀取溫度(°C)
float tempC = sensors.getTempCByIndex(0);
if (tempC == DEVICE_DISCONNECTED_C) {
Serial.println("❌ DS18B20 連線中斷");
} else {
float tempF = tempC * 9.0 / 5.0 + 32.0;
Serial.printf("溫度: %.2f°C (%.2f°F)\n", tempC, tempF);
}
delay(1000);
}
這裡有個實務細節:轉換需要 750 ms,所以讀取前要等。多數函式庫提供 setWaitForConversion() 之類的開關,讓你自己控制等待時機,避免整個程式卡住。
多顆 DS18B20:靠 ROM ID 點名
同一條線上掛多顆時,流程會變成「先搜尋、再指定」:
// ESP32 - 多顆 DS18B20 + ROM ID 定址
#include
#include
#define ONE_WIRE_BUS 4
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
// 儲存所有發現的 ROM ID
DeviceAddress sensorList[10];
int sensorCount = 0;
void setup() {
Serial.begin(115200);
sensors.begin();
// 搜尋所有 DS18B20 的 ROM ID
DeviceAddress addr;
DallasTemperature::DeviceAddress devices[10];
sensorCount = sensors.getDeviceCount();
Serial.printf("發現 %d 顆感測器\n", sensorCount);
// 列印每顆的 ROM ID
for (int i = 0; i < sensorCount; i++) {
if (sensors.getAddress(devices[i], i)) {
Serial.printf("感測器 #%d: ", i);
for (int b = 0; b < 8; b++) {
Serial.printf("%02X", devices[i][b]);
if (b < 7) Serial.print(":");
}
Serial.println();
}
}
sensors.setResolution(12); // 所有感測器設為 12-bit
}
void loop() {
sensors.requestTemperatures();
for (int i = 0; i < sensorCount; i++) {
float temp = sensors.getTempCByIndex(i);
Serial.printf("Sensor #%d: %.2f°C\n", i, temp);
}
delay(2000);
}

Search ROM 的逐位元仲裁是 OneWire 最巧妙的地方:Master 丟出一個位元,所有裝置同時回答,靠「誰拉低」的差異就能一位一位推出完整位址。實務上通常只在開機時搜尋一次,把 ROM ID 存進陣列,之後直接用 Match ROM。
STM32 實作:自己刻時序
STM32 沒有內建 OneWire,所以要用 GPIO + 微秒級延時自己模擬:
// STM32 HAL - 軟體模擬 OneWire(DS18B20 讀取)
// 使用 PA0 作為 DQ 腳位,4.7kΩ 上拉到 3.3V
#define DQ_PORT GPIOA
#define DQ_PIN GPIO_PIN_0
// 微秒延遲(需 TIM 或 DWT 支援)
void delay_us(uint32_t us) {
// 使用 DWT 精確延遲,或 HAL_Delay 近似
for (uint32_t i = 0; i < us * 8; i++) __NOP();
}
// 匯流排操作
void DQ_Low() { HAL_GPIO_WritePin(DQ_PORT, DQ_PIN, GPIO_PIN_RESET); }
void DQ_High() { HAL_GPIO_WritePin(DQ_PORT, DQ_PIN, GPIO_PIN_SET); }
uint8_t DQ_Read(){ return HAL_GPIO_ReadPin(DQ_PORT, DQ_PIN); }
void DQ_Output() {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DQ_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // Open-Drain
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(DQ_PORT, &GPIO_InitStruct);
DQ_High();
}
void DQ_Input() {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = DQ_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(DQ_PORT, &GPIO_InitStruct);
}
// OneWire Reset
uint8_t onewire_reset() {
DQ_Output();
DQ_Low();
delay_us(480);
DQ_High();
delay_us(70);
DQ_Input();
uint8_t presence = !DQ_Read(); // 0 = Presence
delay_us(410);
return presence;
}
// OneWire Write Bit
void onewire_write_bit(uint8_t bit) {
DQ_Output();
DQ_Low();
if (bit) {
delay_us(6);
DQ_High();
delay_us(64);
} else {
delay_us(60);
DQ_High();
delay_us(10);
}
}
// OneWire Read Bit
uint8_t onewire_read_bit() {
DQ_Output();
DQ_Low();
delay_us(2);
DQ_Input();
delay_us(8);
uint8_t bit = DQ_Read();
delay_us(50);
return bit;
}
// OneWire Write Byte
void onewire_write_byte(uint8_t data) {
for (int i = 0; i < 8; i++) { onewire_write_bit(data & 0x01); data >>= 1;
}
}
// OneWire Read Byte
uint8_t onewire_read_byte() {
uint8_t data = 0;
for (int i = 0; i < 8; i++) { data >>= 1;
if (onewire_read_bit()) data |= 0x80;
}
return data;
}
// 讀取 DS18B20 溫度
float read_ds18b20() {
if (!onewire_reset()) return -999.0; // 無裝置
onewire_write_byte(0xCC); // Skip ROM
onewire_write_byte(0x44); // Convert T
HAL_Delay(750); // 等待轉換
if (!onewire_reset()) return -999.0;
onewire_write_byte(0xCC); // Skip ROM
onewire_write_byte(0xBE); // Read Scratchpad
uint8_t tempL = onewire_read_byte();
uint8_t tempH = onewire_read_byte();
// 組合成 16-bit 溫度值
int16_t raw = (tempH << 8) | tempL;
return raw * 0.0625f; // 12-bit 解析度
}
// 使用範例
void main() {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
float temp = read_ds18b20();
printf("溫度: %.2f°C\n", temp);
}
這個版本最能看出 OneWire 的本質:所有動作都是「拉低、等一下、放開、再等一下」。寫這一份的好處是,之後看任何 OneWire 裝置的 datasheet 都不再害怕。
CRC 校驗:長線上的救命機制

// OneWire CRC-8 計算
uint8_t onewire_crc8(const uint8_t *data, int len) {
uint8_t crc = 0;
for (int i = 0; i < len; i++) {
uint8_t byte = data[i];
for (int b = 0; b < 8; b++) { uint8_t fb = (crc ^ byte) & 0x01; crc >>= 1;
if (fb) crc ^= 0x8C; // 多項式: x^8 + x^5 + x^4 + 1
byte >>= 1;
}
}
return crc;
}
// 驗證 DS18B20 資料
uint8_t scratchpad[9];
// ... 讀取 9 bytes ...
if (onewire_crc8(scratchpad, 8) == scratchpad[8]) {
// CRC 正確,資料有效
float temp = ((scratchpad[1] << 8) | scratchpad[0]) * 0.0625f;
}
DS18B20 讀回來的 9 個位元組裡,最後一個是前面 8 個位元組的 CRC-8。驗證成本極低,但沒有它的話,一條拉很長的線上偶爾讀到 85.0 °C(開機預設值)或離譜數值時,你根本不知道該相信哪一筆。
多顆匯流排的注意事項
| 注意事項 | 說明 |
|---|---|
| Pull-Up 電阻 | 一顆 4.7kΩ 即可,多顆時可降為 2.2kΩ |
| 最大數量 | 理論上無限,但電容限制建議 < 20 顆 |
| 接線長度 | 建議 < 30m,過長需加強 Pull-Up |
| 寄生供電 | 多顆時不建議,轉換時電流不足 |
| ROM ID 衝突 | 每顆 DS18B20 有唯一 64-bit ID,不會衝突 |
| 轉換時間 | 9-bit: 93.75ms, 10-bit: 187.5ms, 11-bit: 375ms, 12-bit: 750ms |
關於供電方式,還有一件事值得單獨畫出來 —— 這是除錯時最常見的元凶:

寄生電源(只用兩條線)很方便,但轉換期間裝置是靠匯流排上的電荷在工作,需要更強的上拉。如果你的電路「單顆正常、三顆就出錯」,先懷疑這裡。
總結
OneWire 值得學的原因不是 DS18B20 很便宜,而是它把幾個通訊設計的核心問題攤開來給你看:
- 開漏 + 上拉:多裝置共用一條線的最小成本解法
- 時間即時脈:沒有時脈線,就必須靠嚴格的時間窗口
- 唯一 ID:定址不必靠額外腳位或跳線
- CRC 自檢:長線與多裝置環境下的必要保險
把這四點弄懂,再去看 I2C、SPI 甚至 CAN 的設計取捨,會發現它們處理的都是同一批問題,只是換了不同的答案。
文章評論