DS18B20单总线温度传感器:从原理到实战的嵌入式应用指南
2026/8/2 15:05:29 网站建设 项目流程

1. 从“一根线”说起:DS18B20的独特魅力与核心挑战

如果你在嵌入式开发或者物联网项目中需要测量温度,大概率会听说过DS18B20这个名字。它几乎是“单总线温度传感器”的代名词。我第一次接触它,是在一个需要多点测温的农业大棚监控项目里。当时,项目要求用最少的线缆连接几十个分布在各个角落的温度探头,传统的模拟传感器或者I2C、SPI接口的传感器,要么需要复杂的布线,要么地址资源有限。就在一筹莫展的时候,一位老工程师扔给我一个指甲盖大小的三脚元件,说:“试试这个,一根线搞定所有。” 那个元件就是DS18B20。

DS18B20的核心魅力,就藏在它的名字里——“单总线”。这意味着,理论上,无数个DS18B20可以只用一根数据线(外加电源和地线)全部挂接在一起,由同一个微控制器(比如STM32、Arduino、ESP8266/ESP32)进行通信和控制。这对于需要分布式、多点测温的场景来说,简直是“降维打击”,能极大地简化硬件连接、节省宝贵的单片机IO口资源。它的测温范围是-55°C到+125°C,在-10°C到+85°C范围内精度可达±0.5°C,对于绝大多数工业和民用场景来说完全够用。

但是,这种简洁背后,也带来了独特的挑战。这根唯一的“总线”上,同时承载着电源(通过寄生供电模式时)、数据和时钟信号,所有的通信都基于严格的单总线协议时序。时序的精确性、总线负载能力、寄生供电的稳定性,每一个环节都可能成为新手路上的“坑”。很多人拿到DS18B20,照着网上最基础的代码一抄,发现时灵时不灵,或者读数全是85°C(一个特定的错误值),然后就把它打入了“不稳定、难用”的冷宫。这其实非常可惜。接下来,我将结合自己多年的踩坑经验,从原理到实战,为你彻底拆解DS18B20,让你不仅能“点亮”它,更能“用好”它。

2. 单总线协议:不仅仅是“一根线”那么简单

很多人对单总线的理解停留在“只用一根数据线通信”,这没错,但太表面了。DS18B20的单总线协议(1-Wire Protocol)是一套精密的、基于严格时间片的数字通信系统。理解这套协议,是稳定驱动DS18B20的基石。

2.1 通信的基石:复位、存在脉冲与读写时序

单总线上的所有通信,都以主机(单片机)发出的“复位脉冲”开始。这个脉冲是一个至少480微秒的低电平,随后主机释放总线(拉高)。如果总线上有DS18B20,它会在等待15-60微秒后,拉低总线60-240微秒,发出一个“存在脉冲”来回应主机。这个“握手”过程,是每次通信序列的必须开场白,用于检测总线上是否有器件。

握手成功后,真正的数据交换才开始。数据以“时隙”为单位进行传输,每个时隙传输1比特数据。无论是主机写数据到DS18B20,还是从DS18B20读数据,都由主机发起并严格控制时序。

  • 写“1”时隙:主机拉低总线1-15微秒,然后释放(拉高),并保持高电平直到时隙结束(整个时隙通常持续60-120微秒)。DS18B20会在主机拉低后的一个很短的窗口内采样总线,如果采样到高电平,则认为是“1”。
  • 写“0”时隙:主机拉低总线,并持续保持低电平至少60微秒,直到时隙结束。DS18B20采样到低电平,则认为是“0”。
  • 读时隙:主机拉低总线1-15微秒后释放,然后在拉低后的15微秒内,必须切换到输入模式去读取总线的电平。此时,DS18B20如果要输出“0”,就会强下拉总线;如果要输出“1”,则释放总线(由上拉电阻拉高)。主机在时隙结束前完成采样。

这里的关键在于时序的精确性,尤其是从输出模式切换到输入模式的时机,以及采样窗口。很多在8位单片机(如51、AVR)上能跑的代码,换到主频更高的ARM Cortex-M内核单片机(如STM32)上就失效,根本原因就是高主频下微秒级的延时函数精度不够,或者没有处理好总线切换的延迟。我的经验是,在STM32这类MCU上,最好使用硬件定时器来产生精确的微秒级延时,或者直接操作寄存器进行精准的NOP空指令循环,而不是依赖不准确的HAL_Delay或软件循环。

2.2 ROM命令与功能命令:如何与正确的传感器对话

当总线上挂有多个DS18B20时,如何指定与哪一个对话?这就引入了“ROM命令”的概念。每个DS18B20在出厂时都固化了一个全球唯一的64位ROM编码(类似于MAC地址)。ROM命令就是用来操作这些地址的。

常用的ROM命令有:

  • Read ROM [0x33]:读取总线上单个DS18B20的ROM码。仅当总线上只有一个器件时使用。
  • Match ROM [0x55]:匹配ROM。主机发出此命令后,紧跟64位ROM地址,之后的所有功能命令都只针对这个地址的DS18B20。这是多点应用中最核心的命令
  • Skip ROM [0xCC]:跳过ROM。忽略所有器件的地址,之后的功能命令将针对总线上所有DS18B20。这在单点应用同时启动所有传感器转换时非常有用。
  • Search ROM [0xF0]:搜索ROM。这是一个复杂的算法,用于自动识别总线上所有DS18B20的ROM地址,是实现“即插即用”多点系统的关键。

在发送合适的ROM命令选定目标(或全部)后,才能发送“功能命令”来执行具体操作,例如启动温度转换Convert T [0x44]、读取暂存器Read Scratchpad [0xBE]等。

注意:一个常见的错误顺序是,发送Skip ROM [0xCC]后,紧接着想用Read Scratchpad [0xBE]读取某个特定传感器的数据。这是行不通的,因为Skip ROM意味着对总线所有器件广播,当你发读命令时,所有器件都会同时向总线输出数据,造成数据冲突。正确的多点读取流程是:对每个传感器,依次执行Match ROM -> Convert T -> 延时等待 -> Match ROM -> Read Scratchpad

3. 硬件连接与电源模式:稳定性的决定性因素

DS18B20的硬件连接看似简单,但电源模式的选择直接决定了系统的稳定性和复杂度。

3.1 外部供电模式:最稳定可靠的选择

这是我最推荐,尤其是对于新手的模式。将DS18B20的VDD引脚连接到3.0V-5.5V的电源,GND接地,数据脚DQ通过一个4.7kΩ的上拉电阻连接到同一电源。单片机IO口与DQ脚相连。

优点

  • 稳定性极高:传感器有独立电源,在进行温度转换(尤其是高分辨率时)期间,不会从数据线上“偷电”,通信不受干扰。
  • 驱动能力强:总线可以挂接更多的DS18B20,因为上拉电阻可以直接从电源取电,不受单片机IO驱动能力限制。
  • 布线距离可以更长:在干扰较小的环境下,导线长度可以延伸到更远。

缺点:需要多一根电源线。但在绝大多数项目中,为稳定性牺牲一根线是完全值得的。

3.2 寄生供电模式:极简布线的代价

这是DS18B20的“绝活”,也是让人又爱又恨的模式。在这种模式下,VDD引脚直接接地,DS18B20完全依靠数据线DQ在空闲时被上拉电阻拉高所提供的“偷来”的电能工作。

工作原理:当DQ线被单片机拉高(通过上拉电阻)时,传感器内部一个电容被充电。当需要执行耗电较大的操作(如温度转换)时,它就使用这个电容储存的电能。为了保证电容有电,在温度转换期间,单片机必须将DQ线强制拉高(强推挽输出高电平),而不能释放总线。

优点:真正的“两根线”(DQ和GND)即可工作,布线极其简洁。

缺点与巨坑

  1. 转换期间总线被锁定:在发出Convert T [0x44]命令后,单片机必须立刻将DQ引脚设置为强推挽输出高电平,并保持至少转换所需的时间(对于12位精度是750ms)。在此期间,总线无法进行任何其他通信。这对于需要实时响应的系统是个问题。
  2. 电源稳定性差:如果总线负载重(传感器多)、导线长、上拉电阻过大,或者单片机IO口高电平驱动能力弱,都可能导致“偷”到的电压不足,造成转换错误或通信失败。
  3. 对时序要求更苛刻:在读取数据时,DS18B20输出“0”是通过强下拉总线,这会瞬间消耗电容上的电量。如果读时隙过长或过于频繁,可能导致传感器内部断电复位。

实操心得:除非你的项目对布线有极其苛刻的限制(比如只能穿两根细线),否则强烈建议使用外部供电模式。我曾在一个温室项目中,为了美观尝试寄生供电连接20个传感器,结果经常出现几个点数据异常。排查到头大,最后还是乖乖多布了一根电源线,所有问题迎刃而安。如果你必须用寄生供电,请务必确保:使用足够小的上拉电阻(建议2.2kΩ甚至更小,但需注意单片机IO电流承受能力);尽量缩短总线长度;在代码中严格保证转换期间的总线强上拉。

3.3 上拉电阻的选择与总线负载

无论哪种模式,数据线DQ都需要一个上拉电阻到电源(外部电源或MCU的VCC)。这个电阻的作用是在总线空闲时,将其拉至高电平状态。

  • 阻值选择:典型值是4.7kΩ。在寄生供电、传感器多或线长时,可以减小到2.2kΩ以提供更强的上拉电流,但会增加单片机拉低总线时的电流负担。在外部供电、传感器少时,可以用5.1kΩ或10kΩ。
  • 总线负载:一根单总线上能挂多少DS18B20?官方没有给出明确上限,它取决于电源模式、上拉电阻、导线长度和分布电容。一个实用的经验值是,在外部供电、4.7kΩ上拉、导线不长于50米的情况下,挂接30-50个器件通常是可行的。超过这个数量,建议分段或用多根总线。

4. 软件驱动实战:从基础读取到稳健的多点系统

理解了原理,我们来看代码实现。我将以STM32的HAL库为例,展示关键部分的代码,并解释其中的要点。

4.1 底层时序函数:一切的基础

首先,我们需要实现精确的微秒级延时函数。使用SysTick定时器或一个基本定时器来实现是可靠的做法。这里假设我们有一个delay_us(uint16_t us)函数。

// 单总线复位函数 uint8_t DS18B20_Reset(void) { uint8_t presence = 0; // 主机拉低DQ至少480us HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); delay_us(480); // 主机释放总线(设置为输入模式,由上拉电阻拉高) HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); // 等待15-60us后采样 delay_us(60); // 读取DQ引脚电平 if (!HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN)) { presence = 1; // 检测到存在脉冲 } // 等待存在脉冲结束(至少480us from start) delay_us(420); // 480 - 60 = 420 return presence; } // 向DS18B20写入一个比特 void DS18B20_WriteBit(uint8_t bit) { HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); delay_us(2); // 拉低后等待至少1us if (bit) { HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); // 写1:很快释放 } // 保持低电平(写0)或高电平(写1)直到时隙结束 delay_us(60); HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); // 释放总线 delay_us(2); // 恢复时间 } // 从DS18B20读取一个比特 uint8_t DS18B20_ReadBit(void) { uint8_t bit = 0; HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_RESET); delay_us(2); // 拉低至少1us HAL_GPIO_WritePin(DS18B20_PORT, DS18B20_PIN, GPIO_PIN_SET); // 释放总线 delay_us(12); // 等待约15us后采样 if (HAL_GPIO_ReadPin(DS18B20_PORT, DS18B20_PIN)) { bit = 1; } delay_us(50); // 等待时隙结束 return bit; }

关键点ReadBit函数中,主机拉低总线后很快释放,然后必须在一个非常短的时间窗口(约15微秒)内采样。过早或过晚采样都会得到错误数据。delay_us(12)这个值需要根据你的MCU主频和代码效率进行微调。

4.2 单点温度读取流程

对于单个DS18B20,流程可以简化,使用Skip ROM命令。

float DS18B20_ReadTemp_Single(void) { uint8_t temp_l, temp_h; int16_t temp_raw; float temperature; if (DS18B20_Reset() != 1) return 85.0; // 如果复位失败,返回一个错误值(如85.0) DS18B20_WriteByte(0xCC); // Skip ROM DS18B20_WriteByte(0x44); // Convert T // 等待转换完成。对于12位精度,需要750ms。这里可以用延时,或者更优的做法是查询总线状态。 delay_ms(750); DS18B20_Reset(); DS18B20_WriteByte(0xCC); // Skip ROM DS18B20_WriteByte(0xBE); // Read Scratchpad temp_l = DS18B20_ReadByte(); // 温度值低字节 temp_h = DS18B20_ReadByte(); // 温度值高字节 temp_raw = (temp_h << 8) | temp_l; temperature = temp_raw * 0.0625; // 12位分辨率,LSB = 0.0625°C return temperature; }

4.3 多点系统核心:ROM搜索算法

这是DS18B20应用的进阶难点,也是实现动态识别总线上所有传感器的关键。算法原理是“二分查找”在单总线上的实现。由于篇幅所限,我简述其核心思想并给出一个经过实战检验的搜索函数框架。

每个DS18B20的64位ROM码,可以看作一个64位的二进制数。搜索过程就是逐位(bit)确定这个数。在每一位上,主机先向总线发出该位为“0”的搜索命令,读取所有器件的响应;再发出该位为“1”的命令,再读响应。根据响应,会出现三种情况:

  1. 所有器件在这一位都是0。
  2. 所有器件在这一位都是1。
  3. 有些器件是0,有些是1(即存在分叉)。

对于情况1和2,主机选择该唯一值。对于情况3,主机必须做出选择(比如先选0),并将选择记录在案。那些在这一位与主机选择不同的器件,在本轮搜索中将被暂时“屏蔽”。通过一个递归或循环过程,最终可以遍历出总线上所有的ROM码。

// 全局变量,用于存储搜索到的ROM码 uint8_t g_rom_codes[MAX_SENSORS][8]; uint8_t g_found_sensors = 0; // 递归搜索函数(简化框架) uint8_t DS18B20_SearchRom(uint8_t *rom_code, uint8_t last_discrepancy) { uint8_t discrepancy_marker = 0; uint8_t rom_bit_index = 1; // 从第1位开始(第0位是家族码0x28) uint8_t bit, bit_comp; uint8_t search_direction; if (!DS18B20_Reset()) return 0; // 总线无器件 DS18B20_WriteByte(0xF0); // Search ROM命令 for (rom_bit_index = 1; rom_bit_index <= 64; rom_bit_index++) { // 读取该位所有器件的原始值 bit = DS18B20_ReadBit(); // 读取该位所有器件的补码值 bit_comp = DS18B20_ReadBit(); if (bit && bit_comp) { return 0; // 总线错误 } else if (bit != bit_comp) { // 情况1或2:所有器件该位值相同 search_direction = bit; } else { // 情况3:存在分叉 if (rom_bit_index < last_discrepancy) { // 如果当前位索引小于上次的分叉点,沿用上次的选择 search_direction = ((rom_code[(rom_bit_index-1)/8] >> ((rom_bit_index-1)%8)) & 0x01); } else { // 否则,默认选择0 search_direction = 0; } // 如果当前位就是分叉点,记录它 if (search_direction == 0) { discrepancy_marker = rom_bit_index; } } // 将选择写入ROM码数组 if (search_direction) { rom_code[(rom_bit_index-1)/8] |= (1 << ((rom_bit_index-1)%8)); } else { rom_code[(rom_bit_index-1)/8] &= ~(1 << ((rom_bit_index-1)%8)); } // 向总线写入选择的方向 DS18B20_WriteBit(search_direction); } // 搜索完成,返回本次发现的分叉点 return discrepancy_marker; } // 包装函数,用于查找所有传感器 void DS18B20_FindAllSensors(void) { uint8_t rom_code[8] = {0}; uint8_t last_discrepancy = 0; uint8_t done_flag = 0; g_found_sensors = 0; memset(g_rom_codes, 0, sizeof(g_rom_codes)); while (!done_flag && g_found_sensors < MAX_SENSORS) { last_discrepancy = DS18B20_SearchRom(rom_code, last_discrepancy); if (last_discrepancy == 0) { done_flag = 1; } if (rom_code[0] != 0) { // 家族码有效(DS18B20为0x28) memcpy(g_rom_codes[g_found_sensors], rom_code, 8); g_found_sensors++; } } }

这个算法有一定复杂度,网上有很多成熟的代码库(比如Arduino的OneWire库)。我的建议是,在项目初期可以直接使用这些经过验证的库。理解其原理,是为了在出现问题时能够进行调试。

5. 精度、分辨率与实战中的那些“坑”

DS18B20的精度和分辨率是两个概念,常常被混淆。

  • 精度:指测量值与真实值之间的误差。DS18B20在-10°C至+85°C范围内的典型精度为±0.5°C,这是由传感器芯片的制造工艺和校准决定的,用户无法更改。
  • 分辨率:指温度数字输出所能变化的最小单位。DS18B20的可配置分辨率为9到12位,对应的最小温度增量分别为0.5°C、0.25°C、0.125°C和0.0625°C。分辨率越高,转换所需时间越长(9位约93.75ms,12位约750ms)。

通过配置寄存器的第5和第6位(R0和R1)来设置分辨率。但这里有一个非常重要的细节:DS18B20的配置寄存器是非易失性的(EEPROM)。也就是说,你设置一次分辨率,断电后再上电,它依然会保持这个设置。很多人在测试时改了分辨率,然后发现代码恢复后读数不对,就是因为没有意识到传感器还保持着之前的高分辨率设置,而代码中的转换等待时间却还是按低分辨率写的,导致读到的数据是上一次转换的旧数据。

避坑指南:上电后先读取配置寄存器。一个稳健的做法是,在初始化序列中,发送Read Scratchpad命令,读取第4个字节(配置寄存器),确认当前的分辨率设置,并根据这个设置来调整你的转换等待时间。或者,更直接一点,每次上电后,主动向传感器写入一次你期望的配置(比如0x7F对应12位分辨率)。

另一个常见的“坑”是读数85°C。DS18B20在上电复位或转换失败时,暂存器中的温度值会默认为85°C。所以,如果你持续读到85°C,请按以下顺序排查:

  1. 通信是否正常?检查DS18B20_Reset()函数的返回值,确认存在脉冲。
  2. 电源是否稳定?特别是寄生供电模式下,用示波器查看在温度转换期间,DQ引脚是否被单片机持续强制拉高,电压是否足够(>3.0V)。
  3. 转换是否完成?确保在发出Convert T命令后,等待了足够长的时间(对于12位是750ms)。可以使用Read Power Supply [0xB4]命令来查询传感器是否正在转换,但更简单的方法是直接延时等待。
  4. 时序是否精确?在高主频MCU上,检查你的微秒延时函数是否准确。可以尝试在读写位时序的关键点插入少量NOP指令进行微调。

最后,关于负温度的处理。DS18B20的温度值以二进制补码形式存储。当读取到的16位整数为负值时(最高位为1),需要先将其转换为有符号的整型变量(在C语言中,int16_t),然后再乘以0.0625。直接使用无符号数计算会导致错误。

我个人在多个工业现场项目中使用的DS18B20,只要硬件连接(外部供电+4.7k上拉)可靠,软件上处理好复位、时序和配置读取,其稳定性丝毫不亚于更昂贵的数字传感器。它就像一位沉默可靠的老兵,协议稍显古老和复杂,但一旦你掌握了它的脾气,它就能在那些对布线成本敏感、对可靠性要求却不低的场合,持续地提供稳定的服务。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询