STC89C52单片机驱动DS18B20温度传感器:单总线通信协议详解与实战

发布时间:2026/7/31 6:17:55
STC89C52单片机驱动DS18B20温度传感器:单总线通信协议详解与实战 1. 项目概述从“点灯”到“测温”的进阶玩过51单片机的朋友大多都是从点亮一个LED灯开始的。当流水灯、数码管、按键这些基础外设都玩转之后下一个让你既兴奋又有点头疼的挑战往往就是与各种传感器打交道。而DS18B20这款数字温度传感器绝对是这个阶段绕不开的“经典科目”。它以其独特的“单线通信”协议成为了无数单片机初学者从“控制”走向“感知”世界的第一道坎。今天我们就来彻底拆解这个项目如何用经典的STC89C52单片机通过一根线稳定、准确地读取DS18B20的温度数据。这个项目的核心价值在于它不仅仅是一个温度读取功能。它是一次对单片机底层时序操作的深度训练是对“单总线”这种特殊通信协议的完整实践。理解了它你就能触类旁通为后续学习更复杂的I2C、SPI等通信协议打下坚实的基础。无论是想做一个简单的室内温度计还是为更复杂的智能家居、环境监控系统做技术储备这个项目都是绝佳的练手素材。接下来我将以一个过来人的身份带你从原理到代码从接线到调试完整走一遍这个流程并分享那些在官方数据手册里不会写的“踩坑”经验。2. 核心器件与通信协议深度解析2.1 主角登场STC89C52与DS18B20首先我们得认识一下两位“主角”。STC89C52可以说是国内单片机教育的“国民芯片”。它是8051内核的增强型拥有8K字节的Flash程序存储器512字节的RAM以及32个通用I/O口。对于本项目而言它的优势在于资源完全够用开发环境如Keil成熟资料极其丰富。我们只需要其中一个普通的I/O口例如P3.7就能完成与DS18B20的通信。DS18B20则是Dallas现Maxim Integrated公司出品的单总线数字温度传感器。它的“神奇”之处在于单线接口仅需一根数据线DQ即可实现单片机与传感器之间的双向通信同时这根线还能通过“寄生电源”方式为传感器供电真正做到最少连线。数字输出直接将温度值转换为9~12位的数字量通过单总线串行传送抗干扰能力强无需额外的A/D转换器。精度可选测温范围-55°C到125°C在-10°C到85°C范围内精度为±0.5°C。用户可编程设定9~12位的分辨率分辨率越高温度转换所需时间越长。唯一64位光刻ROM序列号每个DS18B20有唯一的ID这使得一根总线上可以挂载多个传感器通过寻址来区分。2.2 单总线协议一根线上的“精密舞蹈”单总线协议是DS18B20工作的核心它规定了严格的时序。你可以把它想象成两个人用一盏灯在黑暗里发莫尔斯电码必须严格遵守“亮多久代表开始灭多久代表等待闪烁一下代表‘0’闪烁长一下代表‘1’”的规则。任何一方节奏错了通信就会失败。协议流程主要分为几个阶段初始化复位脉冲存在脉冲、ROM命令、功能命令、数据读写。其中最基础、最关键的是复位脉冲、存在脉冲以及读写时序。初始化过程复位与检测主机拉低总线单片机将DQ线拉低至少480µs这个“长低电平”就是一个复位脉冲相当于对所有挂在总线上的DS18B20喊“注意我要开始通信了”主机释放总线单片机将DQ引脚设置为高电平或输入模式靠上拉电阻拉高等待15-60µs。从机回应DS18B20检测到复位脉冲结束上升沿后会等待约15-60µs然后主动将总线拉低60-240µs发出一个“存在脉冲”意思是“我在呢准备好了”主机检测单片机在这段时间内检测DQ线如果检测到一个低电平脉冲说明总线上有DS18B20设备初始化成功。注意这个等待和检测的时机非常关键。如果主机释放总线后等待时间太短就去检测可能抓不到从机的回应如果等待时间太长从机的回应脉冲已经结束也会检测失败。通常采用延时60µs后检测的方式比较稳妥。写时序主机发送‘0’或‘1’写‘0’时序主机拉低总线并持续保持低电平至少60µs一般保持到整个写时隙结束约60-120µs然后在两个写时隙之间至少提供1µs的恢复时间。写‘1’时序主机拉低总线但必须在15µs内释放总线拉高然后依靠上拉电阻将总线保持在高电平直到写时隙结束。读时序主机读取数据主机拉低总线至少1µs然后释放。主机释放后必须尽快在15µs内读取总线电平。因为DS18B20会在主机发起读时隙后在15µs内将有效数据送到总线上。读取的电平如果为低代表数据‘0’为高代表数据‘1’。整个读时隙从开始到结束应持续至少60µs之后需要至少1µs的恢复时间才能进行下一次操作。实操心得很多初学者代码调不通90%的问题出在时序上。STC89C52在12MHz晶振下一个_nop_()空操作指令的延时大约是1µs。但编译器优化、函数调用开销都会影响延时精度。最可靠的方法是使用示波器或逻辑分析仪抓取DQ线上的波形与数据手册的时序图逐个对比。这是排查单总线问题最直接有效的手段。3. 硬件电路设计与关键细节一个稳定可靠的硬件电路是软件正确运行的前提。DS18B20的接线有标准电源模式和寄生电源模式两种我强烈建议初学者使用标准电源模式。3.1 推荐电路标准电源模式VCC (5V) | | | | 4.7KΩ 上拉电阻 (R1) | | | ------------------- 到单片机I/O口 (如 P3.7) | GND | DS18B20 ----- | VDD|---- 接 VCC (5V) | DQ |---- 接上拉电阻节点 | GND |---- 接 GND -----为什么推荐标准电源模式在寄生电源模式下DS18B20在执行温度转换或拷贝数据到EEPROM等大电流操作时会通过DQ线“偷电”。这要求总线在特定时刻必须提供足够的强上拉例如用MOS管将总线直接拉到VCC否则可能导致电源电压不足芯片工作异常甚至复位导致读取失败或一直返回85°C上电默认值。标准电源模式单独供电彻底避免了电源问题让调试集中在通信时序上大大降低了入门难度。关键元件选择上拉电阻R1典型值为4.7KΩ范围在2KΩ~5.1KΩ之间。它的作用是在主机释放总线设置为高电平或输入时将总线电平稳定地拉到高电平为数据传输提供确定的电平状态。阻值太小会增大功耗阻值太大会导致上升沿过慢在长导线时可能影响通信。3.2 布线注意事项电源去耦在DS18B20的VCC和GND引脚之间尽量靠近芯片放置一个0.1µF的瓷片电容用于滤除电源噪声。总线长度单总线对分布电容敏感。如果通信线超过1米或者环境干扰较大可以考虑降低上拉电阻阻值如2.2KΩ或者使用屏蔽线。多点挂载如果总线上有多个DS18B20每个传感器的VDD和GND都必须连接好。总线DQ是并联的。此时搜索ROM、匹配ROM等命令就变得至关重要。4. 软件驱动编写与代码逐行解析理解了协议和硬件我们就可以动手写代码了。下面我将用一个经过实际项目验证的驱动代码为例逐函数解析其实现要点。4.1 基础宏定义与延时函数#include REG52.H #include INTRINS.H // 用于_nop_()函数 // 定义DS18B20数据线连接的单片机引脚 sbit DQ P3^7; // 根据你的实际接线修改 // 微秒级延时函数基于12MHz晶振粗略调整 void Delay_us(unsigned int t) { while(t--); }注意Delay_us是一个非常不精确的延时函数它受编译器优化等级影响很大。这里仅用于示意。在实际项目中为了获得更精确的时序通常会使用定时器中断来产生基准延时或者针对特定编译器如Keil和优化等级通过实验结合示波器校准出一个相对准确的延时函数。例如对于STC89C5212MHz一个while(i--);循环i与微秒的对应关系需要实测。4.2 核心时序函数实现4.2.1 初始化函数复位与检测bit DS18B20_Init(void) { bit ack 0; DQ 1; // 先拉高 Delay_us(8); DQ 0; // 主机拉低发起复位脉冲 Delay_us(480); // 保持低电平480us以上推荐480-960us DQ 1; // 主机释放总线 Delay_us(60); // 等待15-60us后检测 ack DQ; // 读取存在脉冲低电平有效 Delay_us(420); // 等待存在脉冲结束总共延时约480us return ~ack; // 如果ack为0检测到低电平则返回1初始化成功 }解析这个函数严格遵循了初始化时序。Delay_us(60)后检测正好落在DS18B20拉低总线存在脉冲的时间窗口内。最后Delay_us(420)是为了补足从开始拉低到结束的总时间确保时序完整。返回值用~ack这样当检测到存在脉冲ack0时函数返回1真更符合逻辑习惯。4.2.2 写一个字节函数void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for(i0; i8; i) { DQ 0; // 拉低开始写时隙 _nop_(); _nop_(); // 极短延时约2us保证拉低被识别 DQ dat 0x01; // 送出数据位的最低位 Delay_us(60); // 保持60-120us DQ 1; // 释放总线 dat 1; // 准备下一个数据位 // 这里可以加一个很短的延时如几微秒作为恢复时间 } }解析写‘0’和写‘1’的差异在于主机拉低后是持续拉低写0还是很快释放写1。这里采用了一种简化的写法无论写0写1都拉低总线并保持60us区别在于拉低后立即将总线电平设置为要写的值。对于写‘1’DQ dat 0x01这一句在拉低后很快几个指令周期内又将总线置高模拟了“快速释放”的效果。这种方法代码简洁在12MHz下通常能工作。4.2.3 读一个字节函数unsigned char DS18B20_ReadByte(void) { unsigned char i, dat 0; for(i0; i8; i) { dat 1; // 先右移低位在先 DQ 0; // 主机拉低启动读时隙 _nop_(); _nop_(); // 短暂保持大于1us即可 DQ 1; // 主机释放总线 _nop_(); _nop_(); // 等待约5-10us让DS18B20输出稳定 if(DQ) { // 在15us内读取总线电平 dat | 0x80; // 如果为高则该位为1 } Delay_us(50); // 等待读时隙结束总时长约60us } return dat; }解析读时序的关键在于“主机拉低后迅速释放并立即采样”。_nop_(); _nop_();用于产生一个极短的延时确保拉低动作被识别然后立即释放。释放后再等待几个_nop_()这是为了避开总线从低到高的上升沿并留出时间让DS18B20驱动总线电平稳定。随后读取DQ的值。dat的组装采用“先右移后或”的方式因为数据是从低位开始传送的。4.3 温度读取流程函数有了底层的读写函数我们就可以按照DS18B20的命令序列来读取温度了。float DS18B20_ReadTemp(void) { unsigned char LSB, MSB; int temp_raw; float temperature; if(!DS18B20_Init()) // 1. 初始化 return 999.9; // 初始化失败返回一个错误值如999.9 DS18B20_WriteByte(0xCC); // 2. 跳过ROM命令总线上只有一个传感器时 DS18B20_WriteByte(0x44); // 3. 启动温度转换命令 // 等待转换完成。对于12位分辨率最大转换时间为750ms。 // 这里可以用查询DS18B20忙状态的方式更高效。简单起见我们延时等待。 Delay_us(100000); // 延时约100ms假设分辨率不是最高实际项目需根据分辨率调整或查询 if(!DS18B20_Init()) // 4. 再次初始化准备读数据 return 999.9; DS18B20_WriteByte(0xCC); // 5. 跳过ROM命令 DS18B20_WriteByte(0xBE); // 6. 读暂存器命令 // 7. 读取温度数据前两个字节 LSB DS18B20_ReadByte(); MSB DS18B20_ReadByte(); // 8. 数据转换 temp_raw (MSB 8) | LSB; // 合成16位有符号整数 // 判断正负温度 if(temp_raw 0x8000) { // 温度为负 temp_raw ~temp_raw 1; // 取反加1得到原码的绝对值补码转原码 temperature - (temp_raw * 0.0625); // 12位分辨率时LSB为0.0625°C } else { // 温度为正 temperature temp_raw * 0.0625; } return temperature; }解析跳过ROM (0xCC)当总线上只有一个DS18B20时可以用此命令跳过地址匹配直接操作设备简化流程。启动转换 (0x44)发送此命令后DS18B20开始进行温度A/D转换。这里是一个常见的坑点转换需要时间分辨率越高时间越长12位时最长750ms。代码中用了简单的延时在实际产品中这会阻塞CPU。更好的做法是启动转换后单片机可以去执行其他任务然后通过定时器或者查询DQ线发送“读电源模式”命令后读位的方式来判断转换是否完成。读暂存器 (0xBE)此命令用于读取DS18B20内部暂存器的内容共9个字节。我们只关心前两个字节温度值。数据解析读到的16位数据中高5位bit15-bit11是符号位。如果温度是负值这5位全为1。数据以补码形式存放。计算时先将其视为16位有符号整数然后乘以分辨率默认12位时为0.0625即可得到实际温度值。上面的代码展示了补码转原码再计算的过程。更简洁的方法是temperature (float)((short)temp_raw) * 0.0625;利用C语言的有符号短整型转换直接得到带符号的十进制值。5. 系统集成与主程序框架将驱动函数集成到一个完整的系统中例如一个每2秒刷新一次温度的简易温度计。#include REG52.H #include stdio.h // 用于sprintf // 假设数码管显示函数已存在 extern void DisplayString(char *str); void main() { float temp; char disp_buf[10]; while(1) { temp DS18B20_ReadTemp(); if(temp 150.0) { // 简单的错误判断999.9是错误值 sprintf(disp_buf, Err); } else { sprintf(disp_buf, %4.1fC, temp); // 格式化为XX.XC } DisplayString(disp_buf); // 在数码管或LCD上显示 Delay_ms(2000); // 延时2秒再读 } }6. 调试技巧与常见问题排查实录即使代码看起来正确第一次成功读取温度也常常需要一番周折。下面是我总结的“踩坑”记录和排查指南。6.1 问题一永远返回85.0°C或0.0°C这是最常见的问题。返回85.0°C这是DS18B20的上电默认温度值。出现此问题几乎可以断定单片机根本没有成功启动DS18B20进行温度转换或者读取的是默认值。请按以下步骤检查检查初始化复位/存在脉冲用示波器看DQ线。主机拉低480us后释放是否能看到一个60-240us的低电平脉冲存在脉冲如果没有说明硬件连接或电源有问题DS18B20没有响应。重点检查VCC和GND是否接反、虚焊上拉电阻是否接好电源电压是否足够4.5V-5.5V。检查启动转换命令确认0x44命令是否成功发送。发送完这个命令后必须等待足够长的转换时间。如果你延时太短比如只延时了1ms然后立刻去读读到的就是未完成转换的旧数据很可能是85。尝试将等待转换的延时增加到800ms以上。寄生电源模式问题如果你使用的是寄生电源模式在温度转换期间电流需求增大如果总线无法提供足够电流芯片可能复位。强烈建议切换到标准电源模式调试。返回0.0°C可能的原因有读到的数据全为0检查读时序特别是采样点是否在DS18B20驱动有效的时间内主机拉低释放后的15us内。采样过早或过晚都可能读到总线空闲状态高电平被上拉电阻拉高或不确定状态。数据拼接错误确认读取的两个字节LSB和MSB的顺序是否正确。DS18B20是低位先送。LSB ReadByte(); MSB ReadByte();这个顺序是对的。计算错误检查温度计算公式。temp_raw是16位有符号数直接乘以0.0625。可以用一个已知值测试比如读取到的十六进制是0x0191十进制401那么温度就是401*0.062525.0625°C。6.2 问题二温度值跳动大不稳定电源噪声确保电源稳定在DS18B20的VCC和GND引脚就近并联一个0.1µF的瓷片电容。总线干扰通信线过长或靠近干扰源。尽量缩短连线使用双绞线或屏蔽线。可以适当减小上拉电阻如从4.7KΩ改为2.2KΩ增强驱动能力。软件滤波这是最常用的方法。连续读取N次如10次然后排序去掉最大最小值再取平均。或者采用一阶滞后滤波本次温度 上次温度 * 0.9 本次读数 * 0.1。#define FILTER_NUM 10 float TempFilter(void) { float temp_arr[FILTER_NUM], sum0, temp; int i,j; float min, max; // 采集 for(i0; iFILTER_NUM; i) { temp_arr[i] DS18B20_ReadTemp(); Delay_ms(10); } // 冒泡排序找最大最小简单示例 // ... 排序代码 ... // 去掉头尾各一个求平均 for(i1; iFILTER_NUM-1; i) { sum temp_arr[i]; } return sum/(FILTER_NUM-2); }6.3 问题三通信时好时坏偶尔失败时序临界你的延时函数可能处于临界状态。稍微的环境变化温度、电压或编译器优化等级改变就可能导致时序不满足。务必用示波器校准关键延时特别是初始化等待时间和读/写时隙的保持时间。确保所有时间参数都在数据手册规定的最小值和最大值之间并留有一定余量。中断干扰如果单片机在操作单总线期间被高优先级中断打断可能导致时序拉长通信失败。在DS18B20_Init、DS18B20_WriteByte、DS18B20_ReadByte等关键函数中可以临时关闭全局中断操作完成后再打开。void DS18B20_WriteByte(unsigned char dat) { EA 0; // 关闭总中断 // ... 原有的写字节代码 ... EA 1; // 打开总中断 }6.4 高级调试工具逻辑分析仪如果你有逻辑分析仪即使是几十元的简易款调试单总线协议会变得无比轻松。将探头连接到DQ线设置合适的采样率如4MHz触发条件设为下降沿。然后运行你的程序你就能清晰地看到复位脉冲的宽度是否够480us。是否存在一个明显的存在脉冲低电平。每一个读/写时隙的波形是否符合规范写1是否快速释放读采样点是否在正确位置 通过对比抓取的波形和数据手册的时序图可以精准定位是哪个环节的时序出现了偏差。7. 项目优化与扩展思路当基础功能稳定后可以考虑以下优化和扩展这会让你的项目更接近实际产品。7.1 优化一非阻塞式温度读取如前所述等待温度转换的几百毫秒是巨大的CPU资源浪费。我们可以实现一个状态机来管理温度读取流程。enum TempState {TEMP_IDLE, TEMP_START_CONV, TEMP_WAITING, TEMP_READ_DATA}; enum TempState temp_state TEMP_IDLE; unsigned long conv_start_time; float current_temp; void TempTask(void) { switch(temp_state) { case TEMP_IDLE: if(DS18B20_Init()) { DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); // 启动转换 conv_start_time GetSystemTick(); // 获取当前系统滴答计数 temp_state TEMP_WAITING; } break; case TEMP_WAITING: // 假设12位分辨率需要750ms if(GetSystemTick() - conv_start_time 750) { temp_state TEMP_READ_DATA; } break; case TEMP_READ_DATA: if(DS18B20_Init()) { DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // ... 读取并计算温度存入current_temp ... temp_state TEMP_IDLE; // 可以设置一个标志通知主程序温度已更新 temp_updated 1; } else { // 初始化失败可能重试或回到IDLE temp_state TEMP_IDLE; } break; } } // 在主循环中定期调用TempTask而不需要阻塞延时 void main() { while(1) { TempTask(); // 这里可以处理显示、按键等其他任务 if(temp_updated) { DisplayTemp(current_temp); temp_updated 0; } // ... 其他代码 ... } }7.2 扩展一连接多个DS18B20单总线的优势在于可以挂载多个设备。每个DS18B20有唯一的64位ROM ID。流程变为搜索ROM使用“搜索ROM”算法一种二叉树搜索列出总线上所有设备的ROM ID。匹配ROM发送“匹配ROM”命令0x55后跟指定的64位ROM ID之后的操作就只针对这个设备。轮流读取依次匹配每个传感器启动转换、读取温度。注意温度转换命令可以同时发给所有设备使用“跳过ROM”命令然后等待足够时间后再轮流匹配并读取每个传感器的结果这样效率最高。7.3 扩展二设置报警阈值与分辨率DS18B20的暂存器中有高温TH和低温TL报警阈值寄存器以及配置寄存器用于设置分辨率。你可以通过写暂存器命令0x4E来配置它们。例如设置12位分辨率向配置寄存器写入0x7F二进制01111111高5位保留低2位为11代表12位分辨率。这些配置掉电后丢失如果想保存需要执行“拷贝暂存器到EEPROM”命令0x48。通过这个完整的项目实践你收获的不仅仅是一个温度读数。你深入理解了严格的硬件时序在嵌入式通信中的重要性掌握了用软件模拟复杂协议的方法积累了宝贵的调试和排查硬件问题的经验。这些能力在你未来面对SPI Flash、OLED屏幕、无线模块等更复杂的器件时都将成为你快速上手的底气。