0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

OneWire 通訊協定完整教學:DS18B20 溫度感測器 ESP32/STM32 實作

2026 年 6 月 8 日 661點熱度 0人點贊 0條評論

OneWire 通訊協定與 DS18B20 完整教學封面

OneWire 是什麼:一條線為什麼能掛好幾顆

OneWire(又稱 1-Wire)是 Dallas Semiconductor 的匯流排協定,它最誇張的地方是:資料、時序、甚至供電都可以只用一條線。加上地線,兩條線就能跑一顆感測器。

我第一次看到 DS18B20 的接線圖時的反應是「這樣真的能通訊?」。答案是能,而且很穩 —— 前提是你搞懂它的三個設計:

  • 開漏匯流排:所有裝置只能「拉低」或「放開」,所以一定要有上拉電阻
  • Master 發起一切:Slave 不會主動說話,每個時槽都由 Master 開頭
  • 64-bit ROM ID:每顆出廠就燒好唯一序號,這是同一條線能分辨多顆的關鍵

OneWire 匯流排接線

這也是為什麼 OneWire 常常被拿來當「匯流排最小化」的教科書例子:接線成本幾乎為零,代價是軟體要非常守時。

通訊時序:所有操作都由 Master 發起

OneWire 沒有時脈線,所以「時間」就是它的時脈。每一個動作都有明確的時間窗口:

OneWire 五種時槽的時間

先看初始化。Master 把 DQ 拉低至少 480 µs 當作 Reset,放開之後,Slave 會在 15~60 µs 後拉低 60~240 µs 回一個 Presence Pulse —— 這就是在說「我在這裡」。

寫位元更有意思:Write-0 與 Write-1 的差別只差在 Master 拉低多久。

Write-0 與 Write-1 波形

拉低 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 校驗:長線上的救命機制

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 的設計取捨,會發現它們處理的都是同一批問題,只是換了不同的答案。

標籤: 教學
最後更新:2026 年 9 月 30 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang