本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:DS18B20是一款基于1-Wire协议的数字温度传感器,支持单线组网、高精度测温和唯一序列号识别。本文介绍的“DS18B20组网程序”实现了多传感器环境下的温度采集系统,涵盖初始化、读写控制、序列号识别、精度配置及特定设备温度读取等功能。通过一线总线协议,程序可在Arduino或Raspberry Pi等平台上运行,利用OneWire和DallasTemperature等库实现稳定通信,并包含异常处理与电源管理机制,适用于工业监控、智能家居等分布式测温场景。

DS18B20与1-Wire总线:从原理到实战的深度解析

你有没有遇到过这样的场景?在做一个温控系统时,明明代码逻辑没问题,传感器也接上了,可就是读不出温度——要么数据乱跳,要么直接“设备无响应”。别急,这大概率不是你的问题,而是DS18B20这位“高冷选手”对通信时序太敏感了 😅。

我们今天要聊的可不是简单的“怎么接线、调库函数”的快餐式教程,而是一次彻底拆解DS18B20和它背后的 1-Wire协议 的硬核之旅。你会发现,这个看起来只有三根线的小玩意儿,背后藏着多少精巧的设计哲学和工程智慧。

准备好了吗?让我们从一颗小小的数字温度传感器开始,揭开单总线通信系统的神秘面纱 🧩!


一、DS18B20不只是个温度计,它是智能节点

先来认识一下主角: DS18B20 。这家伙可不简单,表面上看是个温度传感器,实际上是一个集成了感知、转换、存储、通信于一体的微型智能终端。

它通过一根数据线(DQ)就能完成所有操作,而且每个芯片出厂时都有一个全球唯一的64位ROM地址——这意味着你可以把几十个甚至上百个DS18B20挂在同一根线上,彼此互不干扰,各自安好 ✨。

它的核心能力有哪些?

  • 数字输出 :无需外部ADC,直接输出16位温度值;
  • 宽测温范围 :-55°C 到 +125°C,工业级适用;
  • 可编程分辨率 :9~12位可调,精度最高达±0.5°C;
  • 两种供电模式 :既可以用VDD独立供电,也能靠数据线“偷电”工作(寄生电源);
  • 支持多点组网 :一条总线挂多个设备,分布式部署毫无压力。

是不是有点像物联网时代的“微型IoT节点”?没错,正是因为它具备这些特性,才让它在智能家居、农业大棚、冷链监控等领域大放异彩。

但它也有“脾气”——对时序极其敏感,稍有偏差就可能罢工。所以我们必须深入理解它的通信协议: 1-Wire


二、1-Wire协议:一根线如何承载整个世界?

想象一下,只用一根线(加上地线),就能实现设备识别、命令下发、数据回传、甚至能量传输……听起来像是黑科技?但这就是Maxim(原Dallas Semiconductor)早在上世纪90年代就搞出来的 1-Wire技术

🔌 “One Wire, Many Functions.”
—— 这是1-Wire设计的核心理念。

2.1 单线双向通信的物理层奥秘

1-Wire最神奇的地方在于它的电气结构: 开漏(Open-Drain)或集电极开路(Open-Collector)输出

什么意思呢?就是所有设备只能主动把总线拉低(表示0),不能主动驱动高电平。那高电平怎么来的?靠一个 外部上拉电阻 连接到VDD!

这就形成了所谓的“ 线与(Wired-AND) ”逻辑:

  • 只要有任意一个设备拉低,总线就是低;
  • 所有设备都释放,总线才被上拉至高。

这种设计让多个设备可以安全共存于同一总线上,不会发生短路冲突。但代价是—— 没有独立时钟线 ,所有通信都依赖精确的时间窗口来判断逻辑状态。

也就是说, 时间就是时钟 ⏱️!

sequenceDiagram
    participant Master
    participant Bus(Line)
    participant Slave as DS18B20 (Slave)

    Master->>Bus: 拉低总线 (Reset Pulse, 480μs)
    Bus->>Slave: 下降沿触发检测
    Slave-->>Bus: 拉低应答脉冲 (60-240μs)
    Bus->>Master: 检测到低电平 → 存在设备
    Master->>Bus: 释放总线(上拉恢复高电平)
    Slave-->>Bus: 释放总线
    Master->>Bus: 发起写/读时序

看懂了吗?主机发起复位脉冲后,从机必须在规定时间内拉低作为回应。整个过程全靠“谁在什么时候拉低”来传递信息。

2.2 上拉电阻:小元件,大作用

别看这个4.7kΩ的电阻不起眼,它可是1-Wire能否稳定工作的关键!

它到底干啥用?
  • 在设备释放总线后,将DQ迅速拉回高电平;
  • 给寄生供电的DS18B20提供充电路径;
  • 影响信号上升沿速度,进而决定最大通信距离和速率。
应用条件 推荐阻值 原因说明
总线长度 < 30cm 4.7kΩ 标准推荐,平衡速度与功耗
总线长度 > 1m 2.2kΩ ~ 3.3kΩ 减少RC延迟,加快上升时间
多设备并联(>5个) ≤3.3kΩ 补偿负载电容增加的影响
寄生供电模式 必须使用强上拉 确保转换期间获得足够能量
典型电路对比
(1)外部供电模式
         VDD
          │
          └───┬───────┐
              │       │
            [4.7k]   ┌┴┐
              │     ─┤ ├─ DS18B20
              │      └┬┘
              │       │
             MCU ─────┘
              DQ       │
                       │
                      GND

这是最简单的接法,DS18B20自己有电源,DQ只需一个被动上拉即可。

(2)寄生供电模式

这时候问题来了:DS18B20没外接VDD,能量全靠DQ线“蹭电”。空闲时靠上拉电阻充电,工作时需要大电流怎么办?

普通4.7kΩ上拉根本供不上劲!所以必须引入 强上拉电路 (Strong Pull-up),比如用MOSFET临时把DQ接到VDD上。

#define STRONG_PULLUP_PIN 10

void enable_strong_pullup() {
  pinMode(STRONG_PULLUP_PIN, OUTPUT);
  digitalWrite(STRONG_PULLUP_PIN, HIGH); // MOSFET导通,DQ直连VDD
}

void disable_strong_pullup() {
  digitalWrite(STRONG_PULLUP_PIN, LOW);
  pinMode(STRONG_PULLUP_PIN, INPUT); // 高阻态,恢复普通模式
}

📌 使用技巧:
- 强上拉只在温度转换、EEPROM写入等高功耗阶段开启(持续750ms左右);
- 转换结束后立即关闭,避免影响后续通信;
- MOSFET建议选低阈值N沟道型号(如2N7002)。


三、时序控制:毫秒之差,天地之别

如果你觉得I²C已经够难调了,那恭喜你,1-Wire会让你更“清醒”——它所有的通信都是靠微秒级延时控制的。

没有硬件UART,没有SPI控制器,一切都得靠软件模拟波形。哪怕差几个微秒,就可能导致复位失败、数据错乱。

关键时序参数一览

时序类型 最小时间 最大时间 描述
复位脉冲(主机发出) 480μs 960μs 启动通信
应答脉冲(从机返回) 60μs 240μs 设备存在标志
写0时隙 60μs 120μs 主机拉低≥60μs表示写0
写1时隙 1μs 15μs(拉低)
60~120μs(总周期)
短暂拉低后释放
读采样点 ≥15μs处 ≤45μs 主机在此刻读取电平

看到没?写0和写1的区别就在于“拉低多久”,读的时候还得掐着时间去采样。这完全是 时间域编码 的典范!

实战代码:Arduino上的复位检测

uint8_t ow_reset() {
  uint8_t presence;
  noInterrupts();           // 关中断,防止被打断
  DDRD |= _BV(DDD2);        // PD2设为输出
  PORTD &= ~_BV(PORTD2);    // 拉低总线
  delayMicroseconds(480);   // 维持480μs以上

  DDRD &= ~_BV(DDD2);       // 改为输入,释放总线
  PORTD |= _BV(PORTD2);     // 启用内部上拉(若有)
  delayMicroseconds(70);    // 等待从机响应

  presence = !(PIND & _BV(PIND2)); // 若读到低,则设备存在
  delayMicroseconds(410);   // 补足到约960μs
  interrupts();
  return presence;
}

⚠️ 注意事项:
- noInterrupts() 很关键,否则定时器中断可能打乱节奏;
- AVR平台可用 _delay_us() ,但ESP32/STM32需用更高精度延时;
- 外部上拉强烈建议使用4.7kΩ,内部上拉通常太弱(20kΩ+)。


四、比特传输机制:每一个bit都在跳舞

1-Wire的数据传输是以“位”为单位进行的,每个bit在一个固定的时间槽(Slot)内完成。

四种基本操作

操作 主机行为 从机行为 判定条件
写0 拉低≥60μs,然后释放 在15μs内检测到低电平 认为收到0
写1 拉低1~15μs,然后释放 在15μs内检测到高电平 认为收到1
读0 拉低1~15μs,释放 在15μs前拉低总线 主机读得0
读1 拉低1~15μs,释放 不拉低(保持高) 主机读得1
timeline
    title 1-Wire Bit Time Slots
    section Write '0'
      Host pulls low : 0ms to 60μs
      Host releases  : 60μs
      Slave detects  : at 15μs → sees low → interprets as 0
    section Write '1'
      Host pulls low : 0ms to 2μs
      Host releases  : 2μs
      Slave detects  : at 15μs → sees high → interprets as 1
    section Read '0'
      Host pulls low : 0ms to 2μs
      Host releases  : 2μs
      Slave pulls low : after 2μs, before 15μs
      Host samples   : at 15μs → reads low → gets 0
    section Read '1'
      Host pulls low : 0ms to 2μs
      Host releases  : 2μs
      Slave does nothing
      Host samples   : at 15μs → reads high → gets 1

你看,每一次通信就像一场精准编排的双人舞,主从之间必须严格同步。

字节收发示例

字节传输由8个bit组成,低位先行(LSB First)。下面是写一个字节的实现:

void ow_write_byte(uint8_t data) {
  for (int i = 0; i < 8; ++i) {
    if (data & 0x01) {
      // 写1:短暂拉低
      PORTD &= ~_BV(PORTD2);
      delayMicroseconds(2);
      PORTD |= _BV(PORTD2);
      delayMicroseconds(60);
    } else {
      // 写0:长时间拉低
      PORTD &= ~_BV(PORTD2);
      delayMicroseconds(60);
      PORTD |= _BV(PORTD2);
      delayMicroseconds(10);
    }
    data >>= 1;
  }
}

对应的读取函数也要严格按照时序采样:

uint8_t ow_read_byte() {
  uint8_t value = 0;
  for (int i = 0; i < 8; ++i) {
    DDRD &= ~_BV(DDD2);           
    PORTD |= _BV(PORTD2);         
    delayMicroseconds(2);
    PORTD &= ~_BV(PORTD2);        
    delayMicroseconds(2);
    PORTD |= _BV(PORTD2);         
    delayMicroseconds(10);
    if (PIND & _BV(PIND2)) {      
      value |= (1 << i);
    }
    delayMicroseconds(50);        
  }
  return value;
}

为了提升可靠性,建议加入CRC-8校验,特别是在读取SCRATCHPAD或ROM时。


五、多设备管理:如何在一棵树上找到每一片叶子?

单个DS18B20还好说,但当你挂了十几个甚至几十个时,怎么知道哪个是哪个?总不能靠猜吧?

答案就是: SEARCH ROM算法

5.1 64位ROM地址的结构

每个DS18B20的ROM地址由三部分构成:

字段 长度 示例 功能
家族码(Family Code) 8位 0x28 标识设备类型(DS18B20固定为0x28)
序列号(Serial Number) 48位 0xA1B2C3D4E5F6 全球唯一编号
CRC校验码 8位 0x3F 验证地址完整性

只要读到首字节是 0x28 ,就知道这是个DS18B20 👍。

5.2 SEARCH ROM:二叉树搜索的艺术

当总线上有多个设备时,必须用 0xF0 命令启动搜索流程。其核心思想是利用“三态采样”发现冲突位,并通过优先级策略遍历所有分支。

整个过程就像在一颗满二叉树中寻找所有叶子节点:

graph TB
    Start[开始 SEARCH ROM] --> Reset
    Reset --> SendCommand[发送 SEARCH ROM 命令]
    SendCommand --> BitLoop{处理第i位}
    BitLoop --> ReadBit[读取两位样本]
    ReadBit --> Conflict{是否有冲突?}
    Conflict -->|无冲突| NoConflictBranch[记录位值]
    Conflict -->|有冲突| HasConflictBranch[标记冲突位]
    HasConflictBranch --> ChoosePath[选择较低优先级路径]
    ChoosePath --> NextBit((i++))
    NoConflictBranch --> NextBit
    NextBit --> Done{是否完成64位?}
    Done -->|否| BitLoop
    Done -->|是| CheckCRC[CRC校验]
    CheckCRC --> Valid{校验通过?}
    Valid -->|是| SaveAddr[保存设备地址]
    Valid -->|否| Discard[丢弃]
    SaveAddr --> MoreDevices{还有设备未发现?}
    MoreDevices -->|是| ContinueSearch[继续搜索]
    MoreDevices -->|否| End[结束枚举]

为了高效实现,通常维护以下状态变量:

typedef struct {
    uint8_t rom_no[8];           
    uint8_t last_discrepancy;    
    uint8_t last_family_discrepancy;
    uint8_t last_device_flag;    
} SearchState;

其中 last_discrepancy 记录最近一次出现冲突的位位置,用于下一轮翻转选择方向,避免重复搜索。

5.3 设备列表动态管理

搜索完成后,要把地址存起来,方便后续访问:

#define MAX_DEVICES 10
uint8_t device_list[MAX_DEVICES][8];
uint8_t device_count = 0;

void add_to_device_list(uint8_t *rom) {
    for (int i = 0; i < device_count; i++) {
        if (memcmp(device_list[i], rom, 8) == 0)
            return; 
    }
    if (device_count < MAX_DEVICES) {
        memcpy(device_list[device_count++], rom, 8);
    }
}

还可以定期扫描,检测设备插拔变化:

void check_device_changes() {
    uint8_t temp_list[MAX_DEVICES][8];
    uint8_t new_count = 0;

    perform_search(temp_list, &new_count);

    for (int i = 0; i < new_count; i++) {
        if (!find_in_list(temp_list[i], device_list, device_count)) {
            trigger_device_added(temp_list[i]);
        }
    }

    for (int i = 0; i < device_count; i++) {
        if (!find_in_list(device_list[i], temp_list, new_count)) {
            trigger_device_removed(device_list[i]);
        }
    }

    memcpy(device_list, temp_list, sizeof(temp_list));
    device_count = new_count;
}

甚至可以把已知地址缓存在EEPROM里,下次开机直接加载,省去漫长的枚举过程。


六、精度设置与组网优化:真正的高手懂得权衡

你以为分辨率越高越好?其实不然。DS18B20支持9~12位分辨率,但代价是 转换时间成倍增长

分辨率 LSB 值 (°C) 转换时间
9 bit 0.5 93.75 ms
10 bit 0.25 187.5 ms
11 bit 0.125 375 ms
12 bit 0.0625 750 ms

公式:$ T_{conv} = 2^{(res - 3)} \times 93.75\text{ms} $

所以在电池供电或高速采样场景中,完全可以降为10位,换取更快响应。

修改配置寄存器的方法

void setResolution(uint8_t* addr, int bits) {
    uint8_t config;
    switch(bits) {
        case 9:  config = 0x1F; break;
        case 10: config = 0x3F; break;
        case 11: config = 0x5F; break;
        case 12: config = 0x7F; break;
        default: return;
    }

    oneWire.reset();
    oneWire.select(addr);
    oneWire.write(0x4E); // WRITE SCRATCHPAD
    oneWire.write(0x00); // TH
    oneWire.write(0x00); // TL
    oneWire.write(config);

    delay(10);
    oneWire.reset();
    oneWire.select(addr);
    oneWire.write(0x48); // COPY TO EEPROM
    delay(10);
}

记得最后执行 COPY TO EEPROM ,否则掉电后又回到默认12位!


七、实战部署:从Arduino到云端的完整链路

终于到了动手环节!下面是一个完整的多点测温系统示例。

使用 DallasTemperature 库快速开发

#include <OneWire.h>
#include <DallasTemperature.h>

#define ONE_WIRE_BUS 2
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

DeviceAddress tempDeviceAddress;

void setup() {
  Serial.begin(9600);
  sensors.begin();

  int deviceCount = sensors.getDeviceCount();
  Serial.print("Found ");
  Serial.print(deviceCount);
  Serial.println(" devices.");

  for (int i = 0; i < deviceCount; i++) {
    if (sensors.getAddress(tempDeviceAddress, i)) {
      printAddress(tempDeviceAddress);
      sensors.setResolution(tempDeviceAddress, 12);
    }
  }
}

void loop() {
  sensors.requestTemperatures();
  delay(1000);

  for (int i = 0; i < sensors.getDeviceCount(); i++) {
    if (sensors.getAddress(tempDeviceAddress, i)) {
      float tempC = sensors.getTempC(tempDeviceAddress);
      if (tempC != DEVICE_DISCONNECTED_C) {
        Serial.print("Sensor ");
        printAddress(tempDeviceAddress);
        Serial.print(" = ");
        Serial.print(tempC);
        Serial.println("°C");
      }
    }
  }

  delay(5000);
}

void printAddress(DeviceAddress deviceAddress) {
  for (uint8_t i = 0; i < 8; i++) {
    if (deviceAddress[i] < 16) Serial.print("0");
    Serial.print(deviceAddress[i], HEX);
  }
}

输出示例:

Found 3 devices.
285E8A2B0700007A
28F34C2B0700009C
28A1B2D40700005F
Sensor 285E8A2B0700007A = 23.12°C
Sensor 28F34C2B0700009C = 24.69°C
Sensor 28A1B2D40700005F = 22.06°C
...

结合WiFi上传MQTT(ESP8266)

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

WiFiClient espClient;
PubSubClient client(espClient);

void sendToCloud(int id, float temp) {
  StaticJsonDocument<128> doc;
  doc["sensor_id"] = id;
  doc["temperature"] = temp;
  doc["timestamp"] = millis();

  char buffer[128];
  serializeJson(doc, buffer);

  client.publish("home/temp", buffer);
}

配合InfluxDB + Grafana,轻松实现可视化监控 dashboard 📊。


八、那些年踩过的坑:经验总结与避雷指南

最后分享一些血泪教训 💔:

  1. 总线太长导致通信失败?
    - 换屏蔽双绞线;
    - 缩短上拉电阻至2.2kΩ;
    - 加强上拉或使用中继芯片。

  2. 偶尔读数异常或CRC错误?
    - 检查电源稳定性;
    - 添加0.1μF去耦电容;
    - 使用重试机制 + 状态轮询。

  3. 寄生供电下转换失败?
    - 确保强上拉电路正常工作;
    - 不要在转换期间通信;
    - 避免总线电容过大。

  4. 设备重启后识别不到?
    - 缓存ROM地址;
    - 增加复位重试次数;
    - 初始化前先延时100ms让电源稳定。


九、结语:简单接口背后的复杂世界

DS18B20看似简单,实则凝聚了嵌入式系统设计的诸多精髓:

  • 软硬协同 :没有专用硬件,全靠软件模拟时序;
  • 资源节约 :一根线搞定通信与供电;
  • 可扩展性 :天生支持多节点组网;
  • 容错机制 :CRC、重试、枚举算法缺一不可。

它告诉我们: 真正的优雅,是在限制中创造自由

无论你是做智能硬件、工业自动化,还是DIY项目,掌握DS18B20和1-Wire协议,都将为你打开一扇通往可靠分布式传感系统的大门 🔑。

所以,下次当你再面对一堆乱七八糟的传感器时,不妨试试这条“单线奇迹”之路吧!🚀

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:DS18B20是一款基于1-Wire协议的数字温度传感器,支持单线组网、高精度测温和唯一序列号识别。本文介绍的“DS18B20组网程序”实现了多传感器环境下的温度采集系统,涵盖初始化、读写控制、序列号识别、精度配置及特定设备温度读取等功能。通过一线总线协议,程序可在Arduino或Raspberry Pi等平台上运行,利用OneWire和DallasTemperature等库实现稳定通信,并包含异常处理与电源管理机制,适用于工业监控、智能家居等分布式测温场景。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐