DS18B20温度传感器组网程序设计与实现
简介: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 📊。
八、那些年踩过的坑:经验总结与避雷指南
最后分享一些血泪教训 💔:
-
总线太长导致通信失败?
- 换屏蔽双绞线;
- 缩短上拉电阻至2.2kΩ;
- 加强上拉或使用中继芯片。 -
偶尔读数异常或CRC错误?
- 检查电源稳定性;
- 添加0.1μF去耦电容;
- 使用重试机制 + 状态轮询。 -
寄生供电下转换失败?
- 确保强上拉电路正常工作;
- 不要在转换期间通信;
- 避免总线电容过大。 -
设备重启后识别不到?
- 缓存ROM地址;
- 增加复位重试次数;
- 初始化前先延时100ms让电源稳定。
九、结语:简单接口背后的复杂世界
DS18B20看似简单,实则凝聚了嵌入式系统设计的诸多精髓:
- 软硬协同 :没有专用硬件,全靠软件模拟时序;
- 资源节约 :一根线搞定通信与供电;
- 可扩展性 :天生支持多节点组网;
- 容错机制 :CRC、重试、枚举算法缺一不可。
它告诉我们: 真正的优雅,是在限制中创造自由 。
无论你是做智能硬件、工业自动化,还是DIY项目,掌握DS18B20和1-Wire协议,都将为你打开一扇通往可靠分布式传感系统的大门 🔑。
所以,下次当你再面对一堆乱七八糟的传感器时,不妨试试这条“单线奇迹”之路吧!🚀
简介:DS18B20是一款基于1-Wire协议的数字温度传感器,支持单线组网、高精度测温和唯一序列号识别。本文介绍的“DS18B20组网程序”实现了多传感器环境下的温度采集系统,涵盖初始化、读写控制、序列号识别、精度配置及特定设备温度读取等功能。通过一线总线协议,程序可在Arduino或Raspberry Pi等平台上运行,利用OneWire和DallasTemperature等库实现稳定通信,并包含异常处理与电源管理机制,适用于工业监控、智能家居等分布式测温场景。
更多推荐




所有评论(0)