1. 从零开始:认识你的测温搭档DS18B20

很多刚开始玩51单片机的朋友,一听到“传感器”、“单总线”、“时序”这些词,头就大了。我刚开始那会儿也一样,觉得这玩意儿肯定特别复杂。但后来我发现,DS18B20这个数字温度传感器,其实是单片机入门路上一个特别好的“陪练”。它功能强大,接线简单,但恰恰因为它只用一根数据线通信,逼着你去理解什么是“时序”,怎么写代码去“模拟”硬件操作。一旦你把它搞定了,单片机编程里“驱动外设”这个核心技能,你就掌握了一大半。

那么,DS18B20到底是个啥?你可以把它想象成一个自带“大脑”(ADC和数字逻辑)的温度计。普通的模拟温度传感器(比如热敏电阻)输出的是电压变化,单片机需要先用ADC(模数转换器)去读这个电压,再套用复杂的公式去算温度,中间误差环节多。而DS18B20厉害的地方在于,它自己内部就把温度测量和转换的活儿全干了,直接通过一根线,把代表温度的数字“告诉”单片机。这根线既要供电(它支持寄生供电模式,就是数据线偷电用),又要传数据,所以通信方式就得有严格的约定,这就是我们后面要死磕的“单总线协议”。

它的精度可以调,有9到12位四种分辨率。可能你对“位”没概念,这么说吧,12位分辨率时,它能感知的最小温度变化是0.0625℃。你用手捏着它,温度上升零点零几度它都能察觉到。我们上电默认就是12位模式,所以这次实战我们追求的就是这个“高精度”。它把转换好的温度值,存放在内部两个8位的寄存器里,一共16位。这里有个关键点:这16位数据是“补码”形式。如果温度是零上,这数据就是实际温度值的二进制表示;如果是零下,它存的是补码。所以我们的代码里,必须要有判断正负和处理补码的逻辑,否则冬天测室外温度就全显示乱码了。别担心,后面我会把这段代码掰开揉碎了讲,保证你能看懂。

2. 硬件连接: minimalist的极致,一根线的艺术

DS18B20的硬件连接,简单到让人怀疑人生,这也是它如此受欢迎的原因。它通常有三个引脚(也有封装成两个引脚的,那就是把VDD和GND内部短接了,完全靠数据线寄生供电):

  • VDD: 电源正极,接3.3V或5V。我实测下来,51单片机的5V系统下工作非常稳定。
  • GND: 电源地,接单片机系统地。
  • DQ: 数据输入/输出脚,开漏结构,必须接一个4.7KΩ到10KΩ的上拉电阻到VCC。这一点千万不能省!我当年就踩过这个坑,没加上拉电阻,数据死活读不出来,时序怎么调都没用,折腾了一整天。这个电阻的作用是,当总线空闲时,把它拉到高电平,保证一个稳定的空闲状态。

在51单片机上,你可以把DQ脚连接到任意一个I/O口,比如P3.7。我们的代码里就会用 sbit DSPORT = P3^7; 来定义它。整个系统连接起来就是:单片机、DS18B20、数码管模块(或者LCD1602,看你想怎么显示),共用电源和地。数码管的驱动我们选用最经典的74HC138译码器驱动8位数码管,或者直接用I/O口扫描,代码里都会体现。硬件搭建这块,真的没什么难度,焊接好,确保电源没问题,重点全在后面的软件时序上。

注意:如果你用的是开发板,通常板上已经为DS18B20的接口预留了上拉电阻。但如果是自己焊的电路,这个4.7KΩ的上拉电阻一定要记得焊上,它是通信稳定的基石。

3. 单总线协议:像对暗号一样与传感器对话

DS18B20的所有奥秘,都在这“单总线协议”里。你可以把它理解成一套非常严格的“对话暗号”。单片机作为主机(Master),DS18B20作为从机(Slave),所有对话都由主机发起。一次完整的温度获取流程,需要经历好几个“暗号”阶段:初始化(握手)、发ROM命令(找谁说话)、发RAM命令(让它干什么)、读数据(听它回答)。每一个阶段,主机都要严格按照特定的时间顺序,把数据线拉高、拉低,维持特定的时长。

3.1 初始化时序:关键的“喂,在吗?”

任何通信开始前,必须先初始化。这个过程是主机“打招呼”,检测从机是否存在。具体步骤,对照着代码看最清楚:

  1. 主机把DQ线(我们定义的DSPORT)拉低至少480微秒。这个“拉低”就是一个明确的复位脉冲。
  2. 主机释放DQ线(设置为高电平),并等待。此时,上拉电阻会把总线拉到高电平。
  3. 如果总线上有DS18B20,它会在等待15-60微秒后,主动把DQ线拉低60-240微秒,以此回应主机:“我在呢!”
  4. 主机在这段时间内去检测DQ线,如果读到低电平,说明初始化成功,传感器在线。

我代码里的 Ds18b20Init() 函数就干这个事。这里有个细节很重要:等待从机回应的那段时间不能无限等,必须做超时判断。比如我代码里用 while(DSPORT) 循环等待,同时计数,如果超过5ms还没等到低电平,就直接返回失败(返回0)。否则程序就会卡死在这里。这个超时处理是产品稳定性的保证,我建议你每次都加上。

3.2 写一位时序:如何精准地说“0”或“1”

主机要向从机发送命令,命令是由一个个二进制位(0或1)组成的。写一位的时序是基础。无论是写0还是写1,都以主机拉低DQ线开始,这个低电平维持约1微秒。之后,就分道扬镳了:

  • 写“0”:主机继续保持DQ线为低电平,持续约60微秒(最少48us),然后释放(变高)。整个“0”信号是一个较长的低脉冲。
  • 写“1”:主机在拉低1微秒后,很快(15微秒内)就把DQ线释放为高电平,并保持到60微秒周期结束。整个“1”信号是一个很短的低脉冲,接着是高电平。

在 Ds18b20WriteByte(uchar dat) 函数里,我们用一个循环,每次取数据 dat 的最低位(dat & 0x01),根据它是0还是1,来控制拉低后的延时长度(我代码里用 i=6; while(i--); 来实现约68us的延时,覆盖了写0所需的时长)。发送完一位后,把 dat 右移一位,准备发送下一位。这里的时间要求比较苛刻,需要用 while 循环空指令来精确延时,因为51单片机的指令周期是固定的,计算好循环次数就能得到大致时间。

3.3 读一位时序:如何听清传感器的“回答”

读数据的过程,是由主机“发起读取”,然后从机把数据放到总线上。步骤是:

  1. 主机拉低DQ线至少1微秒,然后释放。
  2. 主机必须在这个拉低操作后的15微秒内,完成对DQ线电平的采样。因为从机会在主机拉低后,很快(15微秒内)把要输出的数据位放到总线上,并保持15-60微秒。
  3. 主机采样到的电平,就是这一位的数据(0或1)。
  4. 主机完成采样后,需要等待至少45微秒(从拉低开始算起,总共60微秒周期),才能开始读下一位。

在 Ds18b20ReadByte() 函数里,我们同样循环8次。每次先拉低再释放,然后短暂延时(我代码里用几个 i++)等待电平稳定,立刻读取 DSPORT 引脚状态,存入变量。这里要注意数据拼接的顺序:DS18B20是先发送低位(LSB)。所以我们常用 byte = (byte >> 1) | (bi << 7); 这种写法,把新读到的位放在最高位,然后整体右移,相当于低位在先。

4. 核心代码实战:一步步读出温度值

理解了底层时序,我们就能拼装出完整的温度读取函数了。这个过程就像一套组合拳,顺序不能乱。

4.1 启动温度转换

我们首先要命令DS18B20:“去测一下现在的温度。” 这需要两个命令字节:

  1. 0xCC: 这是ROM命令中的“跳过ROM”命令。因为我们总线上通常只挂一个DS18B20,不用指定地址,直接对总线上的所有设备操作。发这个命令就是说:“在座的各位,都听好了!”
  2. 0x44: 这是RAM命令中的“开始温度转换”命令。意思是:“刚才听好的各位,现在开始测量温度!”

对应的函数就是 Ds18b20ChangTemp()。它先初始化,然后依次写入 0xCC 和 0x44。写入 0x44 后,DS18B20就开始吭哧吭哧干活了。对于12位分辨率,这个转换过程最长需要750毫秒。所以,如果你是在主循环里不断读取,问题不大,因为每次循环时间通常超过这个数。但如果是需要立刻读取结果,你就得加个延时等待转换完成,或者用 Ds18b20ReadByte() 去读它的状态寄存器,判断转换是否结束。我们这里用的简易方案,依赖主循环的周期,所以没加延时。

4.2 读取温度寄存器

转换完成后,温度数据就躺在DS18B20内部的暂存器里了。我们需要把它“搬”到单片机里来。同样需要两步:

  1. 0xCC: 再次“跳过ROM”,招呼大家。
  2. 0xBE: 这是RAM命令中的“读暂存器”命令。意思是:“把你们暂存器里的数据都吐出来。”

发完读命令后,就可以连续读取9个字节的数据(我们只关心前两个字节的温度值)。Ds18b20ReadTempCom() 函数负责发送这两个命令,然后 Ds18b20ReadByte() 会被调用两次,先读低字节 tmL,再读高字节 tmH。

4.3 数据拼接与处理:把原始值变成摄氏度

读上来的两个字节 tmH 和 tmL,组成了一个16位的有符号整数(补码形式)。我们先把它拼成一个整型变量 temp:

temp = tmH;
temp <<= 8;
temp |= tmL;

现在,temp 变量里存储的,就是原始的温度数据,单位是0.0625℃。比如,temp 的值是 0x0191(十进制401),那么实际温度就是 401 * 0.0625 = 25.0625℃。

但这里有个关键:正负判断。这个16位数据的最高5位(bit15-bit11)是符号扩展位。如果温度是正的,这5位全是0;如果是负的,这5位全是1。所以我们在处理函数 datapros(int temp) 里,首先判断 temp 是否小于0(在C语言中,对一个16位有符号数判断,正是利用了其最高位为1即为负数的特性)。如果是负数,先显示负号,然后对补码数据进行“减1取反”得到原码(对于负数,temp-1 再按位取反 ~),再进行温度计算。如果是正数,直接计算即可。

计算时,为了在数码管上显示两位小数,我们先将 temp 乘以0.0625得到浮点数温度,再乘以100,最后加0.5进行四舍五入,转换成整数。这样,整数部分就代表了“温度值乘以100”。例如25.0625℃就变成了2506。这个2506,就是我们后续要分解到数码管每一位去显示的数值。

5. 数码管显示:让温度“看得见”

温度值已经计算出来了,接下来就是把它漂亮地显示在数码管上。我们假设用的是6位共阴数码管,通过74HC138译码器进行位选,P0口段选。

5.1 数据分解

在 datapros 函数的后半部分,我们对处理好的整型温度值 temp(比如2506)进行分解:

  • temp / 10000: 获取万位数(对于温度,通常为0)。
  • temp % 10000 / 1000: 获取千位数(十位温度值,2)。
  • temp % 1000 / 100: 获取百位数(个位温度值,5)。这一位需要加上小数点,所以我们让它与 0x80 进行或运算(| 0x80),因为共阴数码管段码中,最高位(DP段)控制小数点。
  • temp % 100 / 10: 获取十位数(小数点后第一位,0)。
  • temp % 10: 获取个位数(小数点后第二位,6)。

把这些数字,通过查表 smgduan[](这是一个0-9的共阴数码管段码表)转换成对应的段码值,存入 DisplayData[] 数组。

5.2 动态扫描显示

DigDisplay() 函数负责动态扫描。它在一个快速循环中,依次点亮6位数码管中的每一位。通过控制P2.2、P2.3、P2.4(LSA、LSB、LSC)来选择哪一位数码管亮(位选),然后将 DisplayData 数组中对应的段码值送到P0口(段选)。每点亮一位,保持一小段时间(delay(100)),然后关闭(消隐,P0=0x00),再点亮下一位。由于人眼的视觉暂留效应,我们看到的是6位数码管同时稳定地显示“25.06”这样的数字。主函数 main() 里就是一个永不停止的循环:读取温度 -> 处理数据 -> 刷新显示。

6. 避坑指南与精度提升技巧

照着上面的代码做,基本功能肯定能实现。但想做得更稳定、更专业,这里有几个我踩过坑才总结出来的经验。

6.1 时序的稳定性是生命线

DS18B20最娇贵的就是时序。不同品牌、批次的51单片机,其晶振频率可能有细微差异(11.0592MHz vs 12MHz),这会导致 while(i--); 这种软件延时的实际时间不同。我建议你根据自己单片机的实际晶振,用示波器或者仿真器,校准一下关键延时:比如初始化拉低的480us、读位采样前的15us、写位保持的60us。如果没条件测量,就保守一点,把延时稍微留长一点,比如拉低初始化我用了70次循环(约642us),确保在480-960us的允许范围内。

6.2 中断干扰问题

如果你的系统里用了定时器中断来做其他事情(比如键盘扫描),一定要小心。单总线通信期间,如果被中断打断,导致延时时间错乱,通信极大概率会失败。解决办法有两种:一是在执行DS18B20的读、写、初始化函数时,暂时关闭全局中断(EA = 0;),操作完再打开(EA = 1;)。二是确保中断服务函数的执行时间非常短,远小于单总线通信中最小的时间单位(微秒级)。我个人的习惯是采用第一种,简单粗暴有效。

6.3 寄生供电模式的注意事项

为了省一根线,DS18B20可以工作在寄生供电模式:只接DQ和GND,VDD引脚也接到GND。此时,它通过DQ线内部的上拉电阻“偷电”来工作。但这种模式有个大问题:当DS18B20执行温度转换时,电流消耗较大(可达1.5mA),可能会把数据线电压拉得过低,导致芯片复位或通信失败。解决方法是:在启动温度转换(发0x44命令)后,用单片机的I/O口给DQ线一个强的上拉(例如,把I/O口设置为强推挽输出高电平),持续至少750ms,等转换完成后再切回开漏模式进行通信。我们的示例代码用的是独立供电,所以没这个问题。但如果你打算做超小型的探头,一定要研究透寄生供电的驱动方法。

6.4 多点测温与ROM搜索

我们的例子用了“跳过ROM”命令,只适合单点测温。如果一根总线上挂了多个DS18B20,你就必须用到“搜索ROM”命令(0xF0)来获取每个传感器的唯一64位ID,然后通过“匹配ROM”命令(0x55)对指定传感器操作。这是一个经典的算法,网上有成熟的代码(比如循环搜索算法)。当你需要同时监控多个点的温度时,这个功能就非常强大了。

更多推荐