51单片机实战:DS18B20温度传感器的单总线通信与精准测温

📅 发布时间:2026/7/4 7:40:43 👁️ 浏览次数:
51单片机实战:DS18B20温度传感器的单总线通信与精准测温
1. 从零认识DS18B20你的第一个数字温度计如果你刚开始玩51单片机想做个温度计或者温控小风扇那DS18B20绝对是你绕不开的一个“神器”。我第一次用它的时候感觉特别神奇就这么一个小黑疙瘩三根线甚至两根线接上写几行代码就能读到精确到小数点后好几位的温度比那种需要接ADC的模拟传感器方便太多了。DS18B20本质上是一个数字温度传感器。什么叫数字呢就是说它内部自己就把温度测量好了并且转换成了我们单片机可以直接理解的二进制数据通过一根线传给我们。这就好比一个会说话的 thermometer你问它“现在多少度”它直接告诉你“26.5摄氏度”而不是给你一个需要你自己去换算的电阻值或者电压值。这对我们单片机开发者来说省去了设计模拟信号调理电路、进行模数转换的麻烦硬件电路极其简单抗干扰能力也强。它的核心卖点就是那个“单总线”1-Wire通信协议。顾名思义只用一根数据线通常叫DQ线就能完成双向通信既用它给传感器发命令也用它从传感器读数据。如果采用“寄生供电”模式连电源线都可以省掉直接数据线和地线两根线就能工作这在布线空间紧张或者需要远程测温的场景下非常有用。它的测温范围是-55°C 到 125°C精度在常温下能达到±0.5°C分辨率最高可以调到0.0625°C对于我们日常的电子制作和大多数工业监控场景完全够用。那么它适合谁呢我觉得最适合两类朋友一是正在学习51单片机的学生或爱好者想找一个有代表性、能实战的传感器项目来练手二是需要快速实现温度监测功能的电子工程师或创客DS18B20能让你用最小的硬件成本和时间成本得到一个稳定可靠的结果。接下来我就带你从硬件连接到代码调试完整体验一遍用51单片机“驾驭”这个传感器的全过程过程中我踩过的坑、总结的技巧都会毫无保留地分享给你。2. 硬件连接两种供电模式的实战选择拿到DS18B20第一步就是把它和我们的51单片机开发板连起来。别小看接线这里面的选择直接影响系统的稳定性和复杂度。DS18B20通常有三种引脚红色的VDD电源正极、黑色的GND地线、以及黄色或蓝色的DQ数据线。对应到我们的51单片机就是接电源通常是5V或3.3V、接地、以及接一个任意的I/O口比如P3^7。第一种接法是标准的外部供电模式。这也是我最推荐新手使用的模式因为它最稳定不容易出幺蛾子。接法很简单DS18B20的VDD引脚接到开发板的5V引脚GND接GNDDQ引脚接单片机的一个I/O口例如P3.7。关键一步来了必须在DQ引脚和电源VCC之间连接一个4.7kΩ左右的上拉电阻。这个电阻的作用至关重要因为DS18B20的数据口是“开漏输出”结构它自己只能把总线拉低输出0而不能主动拉高输出1。这个上拉电阻就是负责在传感器不拉低总线时将总线电平保持在高电平1为数据传输提供稳定的“高电平”基准。很多朋友第一次做发现通信不上十有八九是忘了焊这个电阻。第二种接法是炫酷的寄生供电模式。这种模式下DS18B20的VDD引脚直接和GND引脚接在一起都接地。它所需要的电能全部通过那根DQ数据线“偷”过来。具体怎么“偷”呢这需要我们的单片机配合在传感器进行温度转换这种耗电较大的操作时单片机需要把DQ引脚通过一个MOS管强行拉到电源称为“强上拉”给传感器内部的电容充电供其完成转换。转换结束后再恢复成普通的开漏模式加4.7kΩ上拉进行通信。这种模式省了一根电源线在需要多点测温、布线复杂的场合很有优势。但是它对时序和电源管理的要求更苛刻如果强上拉的时间不够或者电流不足可能导致转换失败。我个人的经验是新手项目或者单点测温老老实实用外部供电等玩熟了再挑战寄生供电。为了让你更清楚我画一个简单的对比表格特性外部供电模式寄生供电模式接线VDD接电源GND接地DQ接IO并上拉VDD与GND共接接地仅DQ接IO并上拉上拉电阻必需通常4.7kΩ必需通常4.7kΩ且需额外强上拉电路稳定性高电源独立不受通信影响中依赖总线供电大电流操作时需特别处理复杂度低接线简单程序无需特殊处理高需在代码中控制强上拉时序适用场景新手入门、单点测温、稳定性要求高的项目多点测温、布线受限、需要极致简化连线的场合在实际焊接时我习惯使用面包板先搭建电路确认一切工作正常后再焊接到万用板或者集成到自己的PCB上。一定要确保连接牢固虚焊是调试过程中最让人头疼的问题之一。另外如果你买的DS18B20是那种不锈钢封装探头型的它的三根线可能颜色不标准一定要用万用表测一下区分出VDD、GND和DQ通常红线是VDD黑线是GND黄/白/蓝线是DQ。3. 单总线通信协议像摩尔斯电码一样的对话规则硬件接好了接下来就要让单片机和DS18B20“对话”了。它们之间的语言就是单总线协议。你可以把它理解成一套非常严格的“摩尔斯电码”规则所有通信都通过控制DQ这根线电平的高低和持续时间来完成。协议规定了四种基本操作初始化复位、写一位、读一位、以及由它们衍生出的写一个字节和读一个字节。首先是初始化也叫复位脉冲。这是每次通信序列的开始目的是让总线上的所有DS18B20“醒一醒准备听命令”。具体过程是主机我们的单片机把DQ线拉低至少480微秒然后释放也就是让DQ线变回高电平靠那个4.7kΩ电阻拉上去。释放之后主机会等待大约15到60微秒然后去“听”总线。此时如果总线上有DS18B20它会在主机释放总线后的15到60微秒内主动把总线拉低大约60到240微秒以此来回应主机“嗨我在这儿呢”。主机检测到这个由从机产生的低电平脉冲就知道初始化成功有设备在线。如果超过一定时间没检测到那就是通信失败了。这个“一问一答”的过程是建立联系的基础。然后是发送一位数据。在单总线上数据是靠控制低电平的持续时间来区分的。要发送一个逻辑“0”主机需要把总线拉低60到120微秒然后释放。要发送一个逻辑“1”操作就“短促”得多主机把总线拉低仅仅1到15微秒然后马上释放。DS18B20会在主机拉低总线后大约30微秒的时刻去采样总线电平。如果主机发的是“1”此时总线早已恢复高电平从机就读到“1”如果发的是“0”此时总线还被主机拉着低电平从机就读到“0”。每个这样的“位时间片”总长度必须大于60微秒。接收一位数据则反过来。主机要读取从机发来的一个位需要先主动发起一个“读时隙”主机把总线拉低1到15微秒然后释放。释放之后主机必须赶紧在15微秒内去读取总线上的电平。这里有个关键DS18B20会在主机拉低总线的那一刻开始准备数据并在主机释放总线后将数据位保持到总线上一段时间。如果从机想输出“0”它就会持续拉低总线如果输出“1”它就释放总线由上拉电阻拉高。所以主机在释放总线后大约15微秒时读取读到低电平就是0读到高电平就是1。基于这一位一位的读写我们就能组合成字节的读写。发送一个字节就是循环8次“发送一位”的操作通常从字节的最低位LSB开始发送。接收一个字节则是循环8次“接收一位”的操作同样从最低位开始组装。理解了这个位时序再看代码就会豁然开朗。写代码时最要紧的就是精确的延时因为DS18B20对时序非常敏感。在12MHz晶振的51单片机上一个_nop_()空操作指令耗时1微秒这是我们构建所有延时函数的基础。我后面会给出用循环精确计算延时的代码这是确保通信稳定的核心。4. 驱动代码编写从底层时序到高层API理论懂了现在我们来动手写代码。我会按照自底向上的顺序先实现最核心的单总线底层时序函数然后基于它们实现DS18B20的驱动最后完成一个温度读取的主程序。这样结构清晰也方便你以后把代码移植到其他项目。我们先来写单总线协议的核心文件OneWire.c和OneWire.h。这里假设DS18B20接在单片机的P3.7引脚。// OneWire.h #ifndef __ONEWIRE_H__ #define __ONEWIRE_H__ unsigned char OneWire_Init(void); void OneWire_SendBit(unsigned char Bit); unsigned char OneWire_ReceiveBit(void); void OneWire_SendByte(unsigned char Byte); unsigned char OneWire_ReceiveByte(void); #endif头文件定义了五个函数初始化、发送一位、接收一位、发送一个字节、接收一个字节。下面是具体的实现我加了详细的注释特别是延时部分这是根据12MHz晶振计算出来的// OneWire.c #include REGX52.H #include INTRINS.H // 使用_nop_()函数 // 引脚定义 sbit OneWire_DQ P3^7; /** * brief 单总线初始化复位脉冲 * param 无 * retval 从机响应位0表示有设备响应1表示无设备响应 */ unsigned char OneWire_Init(void) { unsigned char i; unsigned char AckBit; OneWire_DQ 1; // 先确保释放总线 _nop_(); // 短暂延时 OneWire_DQ 0; // 主机拉低总线开始复位脉冲 i 247; while (--i); // 精确延时约500us远超480us最小值 OneWire_DQ 1; // 主机释放总线 i 32; while (--i); // 延时约70us等待15-60us后检测 AckBit OneWire_DQ; // 读取总线电平此时若从机存在会拉低 i 247; while (--i); // 再延时约500us等待从机释放总线 return AckBit; // 返回应答位0为成功 } /** * brief 向单总线发送一位数据 * param Bit 要发送的位0或1 * retval 无 */ void OneWire_SendBit(unsigned char Bit) { unsigned char i; OneWire_DQ 0; // 主机拉低总线开始一个时间片 i 4; while (--i); // 拉低约10us // 根据要发送的值决定释放总线的时机 OneWire_DQ Bit; // 如果Bit1则很快拉高如果Bit0则保持低电平 i 24; while (--i); // 保持约50us确保整个时间片60us OneWire_DQ 1; // 最终释放总线等待下一个时间片 } /** * brief 从单总线接收一位数据 * param 无 * retval 读取到的位0或1 */ unsigned char OneWire_ReceiveBit(void) { unsigned char i; unsigned char Bit; OneWire_DQ 0; // 主机拉低总线启动读时隙 i 2; while (--i); // 拉低约5us OneWire_DQ 1; // 主机迅速释放总线 i 2; while (--i); // 延时约5us等待总线稳定 Bit OneWire_DQ; // 在拉低后约10us处采样总线电平 i 24; while (--i); // 延时约50us等待该时间片结束 return Bit; } /** * brief 向单总线发送一个字节数据低位在先 * param Byte 要发送的字节 * retval 无 */ void OneWire_SendByte(unsigned char Byte) { unsigned char i; for(i0; i8; i) { // 依次发送字节的每一位从最低位开始 OneWire_SendBit(Byte (0x01 i)); } } /** * brief 从单总线接收一个字节数据低位在先 * param 无 * retval 接收到的字节 */ unsigned char OneWire_ReceiveByte(void) { unsigned char i; unsigned char Byte 0x00; for(i0; i8; i) { if(OneWire_ReceiveBit()) { Byte | (0x01 i); // 如果读到1则设置对应位 } } return Byte; }有了坚固的底层DS18B20的驱动就变得非常简单了。我们只需要按照它的命令集来操作。两个最常用的命令是0xCC跳过ROM适用于总线上只有一个传感器时0x44开始温度转换0xBE读取暂存器即温度数据。// DS18B20.h #ifndef __DS18B20_H__ #define __DS18B20_H__ void DS18B20_ConvertT(void); float DS18B20_ReadT(void); #endif// DS18B20.c #include REGX52.H #include OneWire.h // DS18B20指令定义 #define DS18B20_SKIP_ROM 0xCC #define DS18B20_CONVERT_T 0x44 #define DS18B20_READ_SCRATCHPAD 0xBE /** * brief 启动DS18B20进行一次温度转换 * param 无 * retval 无 */ void DS18B20_ConvertT(void) { OneWire_Init(); // 初始化总线 OneWire_SendByte(DS18B20_SKIP_ROM); // 发送跳过ROM命令 OneWire_SendByte(DS18B20_CONVERT_T); // 发送开始转换命令 // 注意发送完转换命令后DS18B20就开始转换了此时总线可以释放 // 对于寄生供电模式这里需要额外加强上拉操作本例为外部供电故省略 } /** * brief 从DS18B20读取温度值 * param 无 * retval 温度值单位为摄氏度浮点数 */ float DS18B20_ReadT(void) { unsigned char TLSB, TMSB; // 温度低字节温度高字节 int Temp; // 合并后的16位有符号整数 float T; OneWire_Init(); // 初始化总线 OneWire_SendByte(DS18B20_SKIP_ROM); // 发送跳过ROM命令 OneWire_SendByte(DS18B20_READ_SCRATCHPAD); // 发送读暂存器命令 TLSB OneWire_ReceiveByte(); // 先读低字节 TMSB OneWire_ReceiveByte(); // 再读高字节 // 将两个字节合并成一个16位有符号整数 Temp (TMSB 8) | TLSB; // DS18B20的温度数据是12位精度以1/16°C为LSB // 将16位整数除以16.0得到实际温度值 T Temp / 16.0; return T; }最后我们写一个主函数把温度显示在LCD1602液晶屏上这样就完成了一个完整的温度计项目。// main.c #include REGX52.H #include LCD1602.h #include DS18B20.h #include Delay.h // 需要一个毫秒级延时函数 float T; // 全局变量存储温度 void main() { // 上电后先启动一次温度转换并等待完成避免第一次读取到无效数据 DS18B20_ConvertT(); Delay(1000); // 等待转换完成12位精度下最多750ms LCD_Init(); // 初始化液晶屏 LCD_ShowString(1, 1, Temperature:); // 显示标题 while(1) { DS18B20_ConvertT(); // 启动新一轮温度转换 // 在实际应用中这里最好加个延时等待转换完成或者查询状态 // 简单起见我们直接用一个较长的延时确保转换完成 Delay(750); // 等待转换完成最坏情况 T DS18B20_ReadT(); // 读取温度值 // 处理并显示温度区分正负 if(T 0) // 温度为负 { LCD_ShowChar(2, 1, -); // 显示负号 T -T; // 取绝对值方便显示 } else // 温度为正或零 { LCD_ShowChar(2, 1, ); // 显示正号 } // 显示整数部分3位 LCD_ShowNum(2, 2, (unsigned int)T, 3); LCD_ShowChar(2, 5, .); // 显示小数点 // 显示小数部分将浮点数乘以10000取后四位 LCD_ShowNum(2, 6, (unsigned long)(T * 10000) % 10000, 4); Delay(500); // 每隔约0.5秒更新一次温度 } }5. 温度数据解析与精度处理技巧从DS18B20读回来的温度数据并不是一个简单的整数需要我们按照特定的格式进行解析才能得到正确的摄氏度值。这是很多新手容易出错的地方。DS18B20默认输出12位精度的温度数据这12位数据存储在两个字节共16位中。具体存储格式是这样的读回来的低字节LSB在前高字节MSB在后。合并成一个16位有符号整数后其低12位bit0-bit11是有效的温度数据高4位bit12-bit15是符号位。这12位数据本质上是一个以1/16°C即0.0625°C为最小单位的二进制补码数。如何计算实际温度呢公式很简单实际温度 (读取的16位有符号整数) / 16.0。举个例子如果读回来的两个字节是0x01和0x91即0x9101合并成16位整数是0x9101。注意这是一个有符号数其十进制值是-28415因为最高位是1表示负数。套用公式-28415 / 16.0 ≈ -1775.9375这显然不对。等等我们犯了一个错误DS18B20的数据是低字节在前所以正确的合并应该是TMSB8 | TLSB即0x01是高字节不对应该是先读的是低字节TLSB0x01后读的是高字节TMSB0x91。所以16位整数是(0x918) | 0x01 0x9101。没错就是这个数。但它的含义是这是一个负数的补码。我们需要将其转换为原码。更简单的做法是直接把这个16位整数当作有符号的short/int类型来处理然后除以16.0。在C语言中当我们把0x9101赋值给一个int型变量Temp时由于int是16位有符号的0x9101的最高位是1所以Temp自动被解释为一个负数。Temp / 16.0这个运算会先对这个负数进行整数除法再转换为浮点数得到的就是正确的负温度值。在我的代码float T Temp / 16.0;中编译器已经帮我们处理了符号问题。所以你不需要手动去判断符号位和取反加一直接使用有符号数运算即可这是最稳妥的方法。对于正温度比如读回0x50和0x00即0x0050计算80 / 16.0 5.0°C。对于负温度比如读回0xFF和0xF8即0xF8FF这是一个负数补码其值约为-1793计算-1793 / 16.0 -112.0625°C。关于精度选择DS18B20允许我们通过配置寄存器选择9位到12位的分辨率。分辨率越高转换时间越长12位最长达750ms。在大多数情况下默认的12位分辨率0.0625°C是最佳选择。如果你对刷新速度要求极高比如每秒要读很多次可以降低分辨率来换取更快的转换速度。配置方法是通过向传感器写入特定的命令来修改暂存器中的配置字节这涉及到更复杂的ROM匹配操作在单传感器且不常更改设置的场景下我们直接用默认值就好。6. 调试实战与常见问题排查代码写完了烧录进单片机满怀期待地通电——结果LCD上可能显示乱码、固定的错误温度或者干脆没反应。别慌这是嵌入式开发的常态。下面我结合自己踩过的坑给你梳理一套调试流程和常见问题排查清单。第一步检查硬件连接。这是所有问题的根源务必反复确认。电源和地用万用表测量DS18B20的VDD和GND之间是否有稳定的5V或3.3V电压电压不足会导致传感器工作异常。上拉电阻4.7kΩ的上拉电阻焊上了吗电阻值是否准确可以临时用个10kΩ的试试但4.7kΩ是最佳值。接线错误DQ线是否确实接到了你代码中定义的I/O口比如P3.7有没有虚焊或者接触不良用万用表通断档仔细检查。传感器方向如果是TO-92封装的直插芯片注意引脚顺序圆弧面通常朝自己从左到右是GND、DQ、VDD。接反了可能会烧坏传感器。第二步用示波器或逻辑分析仪抓取时序波形如果有条件。这是最直接的调试手段。把探头接到DQ线上观察单片机和DS18B20的通信波形。看复位脉冲主机拉低的时间是否够长480us释放后有没有看到一个短暂的、由从机产生的低电平脉冲60-240us如果没有从机应答脉冲说明初始化失败。看读写位时序主机拉低的时间是否符合协议规定发送“1”的时间是否足够短1-15us发送“0”的时间是否足够长60-120us读时隙中主机采样点是否在拉低后15us左右波形畸变、上升沿缓慢都可能是上拉电阻过大或总线电容过大导致的。第三步软件调试与代码排查。延时精度这是最常见的问题。你的单片机晶振是12MHz吗我的示例代码中的循环延时while (--i);是基于12MHz、12T模式计算的。如果你的晶振是11.0592MHz或者工作在6T模式延时时间会完全不同你必须根据自己单片机的实际机器周期重新计算延时循环次数。一个简单的验证方法是写一个让I/O口翻转的程序用示波器测量翻转周期来校准你的延时函数。初始化返回值在OneWire_Init()函数中检查返回值AckBit。如果是1说明DS18B20没有响应。可以在初始化失败时让一个LED闪烁便于诊断。命令顺序确保严格按照“初始化-跳过ROM(0xCC)-启动转换(0x44)”的顺序发送命令。读取时是“初始化-跳过ROM(0xCC)-读暂存器(0xBE)-连续读两个字节”。等待转换完成发送启动转换命令0x44后DS18B20需要时间进行模数转换。在12位分辨率下这个过程最长可能需要750ms。如果你发送完0x44后立即去读温度读到的将是上一次的转换结果或者无效数据。务必在发送0x44后等待足够长的时间或者使用更高级的“读忙”状态功能通过发送读时隙来检测转换是否完成。数据类型处理在DS18B20_ReadT()函数中确保Temp变量是int16位有符号类型而不是unsigned int。否则负温度会被错误地解释为一个很大的正数。第四步环境与传感器本身。尝试更换一个DS18B20传感器。虽然DS18B20很耐用但也有损坏的可能。检查总线长度。单总线协议不适合长距离通信一般建议在1-2米以内。线太长会导致信号衰减和畸变。注意电源噪声。如果系统中存在电机、继电器等大电流设备可能会在电源上产生噪声干扰DS18B20。可以在传感器电源引脚就近加一个0.1uF的瓷片电容进行滤波。我印象最深的一次调试是温度读数偶尔会跳变到一个极大的值。最后发现是主循环中启动转换和读取温度之间没有加延时在CPU运行很快时绝大部分时间转换都能及时完成但偶尔在临界点就会读到错误数据。加上一个750ms的延时后问题彻底解决。所以在嵌入式开发中对时序保持敬畏之心总是没错的。7. 项目进阶从单点到多点测温系统当你成功驱动了一个DS18B20后就可以玩点更高级的了在一根总线上挂载多个DS18B20实现多点测温。这正是单总线协议强大之处每个DS18B20在出厂时都拥有一个全球唯一的64位ROM序列号就像身份证号一样。主机可以通过这个序列号在总线上精准地“点名”某一个传感器进行操作。要实现多点测温你的代码需要增加以下几个关键功能ROM搜索算法这是一个经典的递归算法用于发现总线上所有DS18B20的64位ROM码并将它们存储到一个数组中。Dallas官方有标准的搜索算法代码稍微复杂但逻辑清晰。你需要实现OneWire_SearchRom之类的函数。匹配ROM命令在对特定传感器操作时不再使用0xCC跳过ROM而是使用0x55匹配ROM然后紧接着发送你要操作的那个传感器的64位ROM码。这样只有ROM码匹配的那个传感器才会响应后续的命令。读取ROM命令如果你总线上只有一个传感器但又不知道它的ROM码可以使用0x33读取ROM命令来获取。但注意这个命令只能在总线上有且仅有一个传感器时使用否则会发生数据冲突。流程会变成这样上电后先执行一次ROM搜索把所有传感器的ID存起来。然后你可以遍历这个列表对每一个传感器依次发送初始化 - 匹配ROM命令(0x55) - 发送该传感器的64位ID - 启动转换命令(0x44)。等所有传感器都转换完成后再依次用“匹配ROM”命令去读取每个传感器的温度值。这带来了新的挑战转换等待时间。如果你有10个传感器每个都串行地启动转换并等待750ms那读完一轮需要7.5秒刷新率太低了。这里有一个高级技巧DS18B20支持“寄生供电下的并行转换”。你可以使用0xCC跳过ROM命令同时向总线上的所有传感器发送0x44开始转换命令这样所有传感器会同时开始转换。然后你可以通过“读时隙”来查询任何一个传感器的转换状态读到一个0表示还在转换1表示完成。由于所有传感器同时工作你只需要等待最慢的那个完成还是750ms然后就可以依次去读取每个传感器的结果了大大提高了系统效率。当然多点测温的代码复杂度和对时序的要求更高。我建议你先在单点测温上做到滚瓜烂熟理解了每一个时序细节后再去找一些成熟的多点测温库代码来研究和移植。当你成功实现多点测温时你会发现之前所有的努力都是值得的因为你可以用极简的布线构建一个分布式的温度监控网络这正是很多实际应用如仓库温控、农业大棚的雏形。