简介:这是基于51单片机的DS18B20温度采集实例,采用C语言编写,配套Proteus仿真工程与Keil源码,适合单片机初学者、电子爱好者学习,也可作为课程设计或毕业设计的参考。压缩包为RAR格式,共14个文件,大小仅14KB,内容紧凑。核心包括C语言源程序、可直接烧录的HEX文件、Proteus仿真工程(uv2/opt/plg)以及Keil启动文件,其余为编译生成的列表、目标文件和备份文件。目前已有400人浏览学习,虽然体积小巧,但麻雀虽小五脏俱全。通过这份资料,读者可以学习DS18B20的单总线通信时序与温度读取流程,在Proteus中运行仿真还可直观观察LCD显示效果;同时借助完整源码能理解51单片机C语言工程的编译、烧录与调试过程,节省自行搭建环境的时间。
1. 温度采集DS18B20:51单片机C语言实例里最该先跑通的单总线驱动
温度采集DS18B20是51单片机课程设计里最常见的一道题,也是Proteus仿真中被问得最多的案例之一。DS18B20只有一根数据线,却能在单总线上同时完成命令下发、温度读取和CRC校验,这让它在教学上比I2C和SPI更有“时序感”。多数初学者第一次把程序烧进去后看到85℃或一长串不正常数据,问题往往不在传感器本身,而在C语言驱动里那几十微秒的延时估算,以及Proteus和Keil之间晶振频率是否一致。下面把“DS18B20+51单片机”的最小工程拆开:从单总线时序原理,到可编译的C语言代码,再到Proteus仿真里从原理图到HEX加载的完整流程,最后讲几个实物调试才会用到的参数修正技巧。
2. DS18B20单总线时序与ROM命令:51单片机最小接法
2.1 没有时钟线的总线,靠什么对齐
DS18B20使用的1-Wire总线没有SCL时钟线,主从设备之间靠约定好的时间片来同步。每一位传输的标准时间片大约60~120us,位与位之间还需要至少1us的恢复时间。主机是总线上的唯一发起者,DS18B20只能被动响应:主机拉低线缆表示开始一个时间片,随后根据写0、写1或读操作来决定释放还是继续拉低。
和I2C对照着学会更容易理解:I2C有时钟线,主机在每个SCL上升沿可以稳定判断SDA数值;1-Wire的时钟信息则全部编码在电平跳变和持续时长里。所以DS18B20驱动代码中最要紧的不是“发指令”,而是把每次拉低和释放的持续时间卡准。对51单片机而言,这些时间由延时循环和_NOP_()指令共同实现,而延时是否准确又直接受编译器优化等级影响,这也是后面调试时第一个要检查的地方。
2.2 最小硬件接法与Proteus中的元件选择
2.2.1 电源接法:外部供电优先
DS18B20的供电范围是3.0~5.5V,51单片机开发板通常直接提供5V。最省事的接法是VDD接VCC、GND共地、DQ通过上拉电阻接VCC。上拉电阻的典型值是4.7kΩ,它的作用不是把电平拉高,而是在开漏结构的单总线上提供一个高电平默认态。没有这个电阻,复位脉冲释放后DQ一直浮空,读到的存在脉冲和温度值都会不稳定。
DS18B20也支持寄生供电,也就是只接DQ和GND两根线,靠数据线内部电容在空闲时存电。仿真和实物调试中都不建议一上来就用寄生供电:温度转换期间主机必须主动维持强上拉,否则电压跌落会让转换结果错乱。等外部供电方案跑通后,再尝试寄生供电不迟。
2.2.2 Proteus元件库里怎么找和怎么接
在Proteus的元件选择框里输入DS18B20,可以得到型号为DS18B20的仿真模型,引脚依次是DQ、VDD、GND。把AT89C51或STC89C52放入原理图后,DQ接在P3.7这类空闲I/O上;VDD接VCC;GND接GND;从DQ到VCC之间放一个阻值为4.7k的电阻。这个电阻在Proteus里输入RES后选择普通电阻即可,不需要专门找排阻。
需要注意,Proteus中的单片机模型自带电源引脚,默认已经连接到VCC/GND,原理图上不需要额外画出VCC和GND符号。要让仿真更接近实物,可以再补一个0.1uF去耦电容到单片机电源附近,但这不影响DS18B20的温度逻辑。
2.3 复位脉冲与存在脉冲:一次握手才算通信开始
DS18B20每次通信都以复位脉冲开头。主机先把DQ拉低并保持至少480us,然后释放总线;DS18B20检测到这个低电平后,会在15~60us内把DQ拉低60~240us,这个响应叫存在脉冲。主机在释放总线后等待一段时间再去读DQ,读到低电平就说明总线上有设备。
这个流程在C语言里至少要拆成四步:拉低、延时、释放、延时后读电平。最容易出错的是释放后读取的时机:如果主机释放后立刻读,存在脉冲还没出现,会把高电平误判为无设备。在12MHz标准模式下,释放后延时30us左右再读取比较稳妥。
// 复位脉冲与存在脉冲检测,返回1表示检测到DS18B20 bit ds18b20_reset(void) { bit presence = 1; DQ = 0; delay_us(250); // 拉低约500us,满足至少480us要求 delay_us(250); DQ = 1; // 释放总线 delay_us(30); // 等待DS18B20拉低DQ presence = DQ; // 读存在脉冲,低电平=应答 delay_us(200); // 等时间片结束 return presence; }这段代码里的两个delay_us(250)叠加是500us,为什么不用一个500us循环?因为在51单片机上,单次函数调用本身也要消耗机器周期,分成两段更容易在延时基准出错时单独修正。presence读取放在释放后30us,这个窗口正好落在DS18B20允许的15~60us响应区间内。如果换成11.0592MHz晶振,延时参数要按机器周期比例重算,不能直接照抄。
2.4 功能指令表与读写时间片:先查指令再写驱动
DS18B20的协议分两层:ROM指令负责设备寻址,功能指令负责温度操作。单设备场景下最常用的是跳过ROM和读暂存器两条指令,多设备并联时才需要读ROM和匹配ROM。
| 指令名 | 指令代码 | 典型用途 |
|---|---|---|
| 跳过ROM(Skip ROM) | 0xCC | 总线上只有一个DS18B20时直接操作 |
| 读ROM(Read ROM) | 0x33 | 读取64位序列号,用于枚举总线设备 |
| 匹配ROM(Match ROM) | 0x55 | 多设备时先发送目标序列号再操作 |
| 温度转换(Convert T) | 0x44 | 启动一次温度转换,结果存入暂存器 |
| 读暂存器(Read Scratchpad) | 0xBE | 读取温度、告警阈值、配置字节和CRC |
读写一位的过程看起来简单,实际卡住很多人的是“读时序”的采样点。写一位时,主机先把DQ拉低约6us:写1就释放让上拉电阻把总线拉高,写0则继续保持低到时间片结束。读一位时,主机同样先拉低,但必须在释放后的15us内完成采样,因为DS18B20只在这个窗口内把0拉到低电平,之后总线会被上拉电阻恢复成1。
// 读一位的采样窗口示意,DQ为输入方向 bit read_bit(void) { bit dat; DQ = 0; _nop_(); _nop_(); // 拉低持续约2us DQ = 1; // 释放 _nop_(); _nop_(); // 等待2us后采样 dat = DQ; delay_us(50); // 确保时间片总长60us以上 return dat; }这个示意代码把“释放后等待”放在两个NOP上,保证采样发生在DS18B20仍有效驱动总线的15us窗口内。实际驱动中,这几十微秒的时序还会受51单片机I/O翻转速度和Keil编译优化影响,因此“能仿真跑通”和“能实物跑通”之间通常就差这几个参数,后面章节会给出完整的字节级读写函数。
3. 51单片机的DS18B20驱动代码:C语言实现与延时参数调整
3.1 引脚定义与延时基准
写驱动前先确定两件事:DQ接在哪个引脚、单片机晶振频率和机器周期模式是什么。下面代码默认DQ接P3^7,晶振为12MHz,指令周期为标准12T模式,也就是一个机器周期1us。内部两个_NOP_()加一起约2us,delay_us(n)用while循环和NOP组合,每轮约2us时长的循环体。
#include <reg51.h> #include <intrins.h> #define uchar unsigned char #define uint unsigned int sbit DQ = P3^7; /***** 延时函数:n为循环次数,每次约2us(12MHz,12T) *****/ void delay_us(uchar n) { while (n--) { _nop_(); _nop_(); } } void delay_ms(uint ms) { uint i, j; for (i = 0; i < ms; i++) for (j = 0; j < 120; j++); }delay_us的参数是uchar,最大255次循环对应约510us。如果复制到其他工程时把参数改成uint,要警惕Keil在复杂表达式下的优化,避免延时被裁剪。delay_ms里内层循环120次是在12MHz下粗略凑出1ms,它只用于主循环节流,不参与DS18B20高速时序,所以精度要求不高。
3.2 三个基础函数:复位、写字节、读字节
在上一章的示意代码基础上,把位操作封装成字节函数,就是驱动的主体。下面这三个函数可以直接放进工程,main函数里调用读温度函数即可。
/***** DS18B20复位,返回0表示存在 *****/ bit ds18b20_reset(void) { bit presence; DQ = 0; delay_us(250); delay_us(250); // 总低电平时间约500us DQ = 1; delay_us(30); // 释放后等待存在脉冲 presence = DQ; // 读到0表示DS18B20应答 delay_us(200); return presence; } /***** 写一个字节,低位在前 *****/ void ds18b20_write_byte(uchar dat) { uchar i; for (i = 0; i < 8; i++) { DQ = 0; _nop_(); _nop_(); // 低电平约2us DQ = dat & 0x01; // 写1则释放,写0则继续拉低 delay_us(60); // 保持到时间片结束 DQ = 1; // 释放总线 _nop_(); dat >>= 1; } } /***** 读一个字节,低位在前 *****/ uchar ds18b20_read_byte(void) { uchar i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; DQ = 0; _nop_(); DQ = 1; // 释放后立即采样 _nop_(); if (DQ) dat |= 0x80; delay_us(50); // 等待当前时间片结束 } return dat; }逻辑说明:写字节函数里先看dat最低位,为1就释放总线由4.7k上拉电阻抬高,为0则继续保持低,这正好对应写1和写0的时间片。读字节函数把dat先右移一位再判断DQ,让最低位依次从bit7方向填进结果,整个过程低位在前,和DS18B20的数据格式一致。两个函数都加了时间片尾部的延时,避免连续位操作时位间隙太短。
参数说明:12MHz、12T模式下,_NOP_()约1us,所以拉低2us满足写时序的“至少1us”。如果改用6T内核或晶振频率变化,要把NOP组合改成定时器延时或查表修正,尤其是写0时的60us,不能靠编译器猜。
| 参数 | 12MHz推荐值 | 设计依据 |
|---|---|---|
| 复位低电平 | 480~500us | DS18B20要求最低480us |
| 写时间片拉低 | 约2us | 满足写位前至少1us要求 |
| 写0保持低 | 60us | 保证从机在15~60us采样窗口内读到0 |
| 读时序采样点 | 释放后约1~2us | 必须在15us内完成采样 |
3.3 温度读取主流程:转换、等待、读暂存器
DS18B20读一次温度需要两轮通信。第一轮发跳过ROM(0xCC)和温度转换(0x44),并等待转换完成;第二轮再次复位,发跳过ROM和读暂存器(0xBE),连续读取9个字节,其中前两个字节就是温度值。单设备场景用跳过ROM省去64位ROM号匹配,代码简单很多。
/***** 返回温度原始值,低12位有效 *****/ int ds18b20_read_temp_raw(void) { uchar tl, th; uint timeout = 0; int temp; ds18b20_reset(); ds18b20_write_byte(0xCC); // 跳过ROM ds18b20_write_byte(0x44); // 启动温度转换 while (DQ == 0) { // 转换期间DQ被拉低,完成后恢复高 if (++timeout > 60000) break; // 防止DS18B20故障时死循环 } ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读暂存器,从第0字节开始 tl = ds18b20_read_byte(); th = ds18b20_read_byte(); DQ = 1; temp = (th << 8) | tl; // 高字节是低字节的符号扩展 return temp; }while(DQ == 0)这一步值得多说:DS18B20在温度转换期间会主动把DQ拉低,转换完成后再释放。12位分辨率下转换时间最长750ms,主循环等在这里不会有问题,但严谨做法是加超时计数。上面timeout的60000次循环在仿真环境中足够判断故障;实物调试时如果出现转换时间不足,可以放大到200000再观察。
温度换算:原始值乘以0.0625就是摄氏度,比如0x0191对应25.0625°C。如果只做数码管显示,可以用移位和取余算出一位小数,避免在51上直接引入float库。负温度时原始值以补码形式存放在两个字节中,C51的int类型可以直接处理负数,但数码管显示时要单独处理负号。
3.4 优化等级、负温度和CRC校验:三个容易被忽略的坑
第一个坑是Keil的优化等级。默认优化下编译器可能把延时循环里的_NOP_()当成无副作用代码合并,结果实际延时只剩不到一半。调驱动时把Options for Target里的优化等级改成Level 0,再编译下载,很多“仿真正常、实物乱码”的问题会直接消失。调通后再慢慢恢复优化等级。
第二个坑是负温度。DS18B20在零下的原始值是0xFF90这类形式,因为C51的int是16位有符号数,直接读取th、tl组合后temp已经是负数补码。不少教材把temp声明成unsigned int,负温度会变成65424,显示时出现巨大正数,这个问题在北方冬天做实物最容易遇到。
第三个坑是CRC校验。读暂存器后第9个字节是CRC,校验失败说明总线上数据传输有误。Proteus仿真基本不会触发CRC错误,但实物线缆过长或干扰大时会读错温度。可以在驱动里加一个查表CRC函数,读满9个字节后校验一遍,失败就丢弃本次结果并重新发起转换。
4. Proteus仿真:从原理图、HEX加载到温度读数验证
4.1 元器件清单与最小电路
在Proteus 8 Professional里新建工程后,按下表放置元件。单片机选择AT89C51,DS18B20在元件搜索框输入即可找到。
| Proteus元件关键字 | 数量 | 说明 |
|---|---|---|
| AT89C51 | 1 | 51单片机,需要设置12MHz时钟 |
| DS18B20 | 1 | 温度传感器仿真模型 |
| RES | 1 | 4.7kΩ上拉电阻 |
| CAP | 2 | 22pF晶振负载电容(可选) |
| CAP-ELEC | 1 | 10μF电源滤波电容(可选) |
连线时把DS18B20的DQ连到P3^7,VDD接VCC,GND接GND。在DQ和VCC之间放电阻并设置Resistance为4.7k。如果希望看到温度值,可以在P1口接一个数码管或者直接使用P1口的LED观察二进制变化,后者验证驱动更直接。
4.2 晶振频率与Keil工程必须一致
双击AT89C51,在Edit Component对话框里把Clock Frequency设为12MHz。很多仿真时序错乱的原因不是代码写错,而是这里和Keil里的预设不一致。需要注意,Proteus中单片机模型有时会有默认频率,必须显式改成与驱动代码一致的数值,不能依赖默认值。
Keil工程里也要同步设置:Options for Target的目标晶振Xtal(MHz)填12。这个值虽然不参与实际编译结果,但Keil调试器在模拟运行时会按这个频率估算延时,两边不一致,单步调试时会发现时间计算对不上。
4.3 生成HEX文件并加载到Proteus
Keil里编译工程前,先勾选Output标签页的Create HEX File。编译成功后,在Proteus里双击单片机,在Program File一栏选择生成的.hex文件。需要注意HEX路径不要放在中文目录下,Proteus 8对中文路径的兼容性一般,很多加载失败都是路径引起的。
加载完点击左下角运行按钮。数码管或LCD显示当前温度前,可以在main函数里只做一次读取,然后把温度原始值的低字节送到P1口观察。Proteus的VIRTUAL TERMINAL组件也能直接显示字符串,但51单片机串口波特率要和虚拟终端一致,例如9600或4800。
4.4 温度调不下来?检查这四个仿真细节
运行后如果读到的温度一直不变,按顺序检查:DS18B20是否放在原理图里并与P3^7相连、上拉电阻是否真的生效、程序是否烧录成功、时钟频率是否一致。Proteus中的DS18B20模型默认会按照环境温度返回数据,点击元件后在属性窗口的Temperature一栏修改数值,就能模拟升温降温,这个交互对验证程序逻辑非常有用。
另外,Proteus仿真中不存在线缆电容和接触电阻,所以代码时序只要满足理论窗口就能通过。实物移植时若出问题,问题多半在延时基准和上拉电阻值,而不是代码里的逻辑。若仿真里固定读到85℃,说明程序没有真正完成温度转换,读到的只是DS18B20上电复位后的默认暂存器值。
5. 三个能让DS18B20温度采集更稳定的调试技巧
5.1 在Proteus里动态改温度,验证负温度和显示逻辑
运行仿真后,点击原理图中的DS18B20,在属性窗口的Temperature一栏直接输入新的温度值。把25改成-10,观察程序能不能正确区分正负。如果只改了整数部分,说明换算代码没有处理符号扩展。一个稳妥的显示换算方式是先判断正负,再对绝对值做temp * 10 / 16,得到十分之一度单位,最后用查表法把整数部分和小数位拆出来。
5.2 用逻辑探头看时序波形,快速定位延时基准
给DQ节点接一个Logic Probe或虚拟示波器,可以看到复位脉冲的低电平宽度。测量一下复位时序中的低电平是否接近500us,写时序每个低电平时间片是否在60us以上。这个方法能快速分辨“程序逻辑问题”和“延时基准问题”。虚拟示波器看到的时序再完美,也不能代表实物线缆上的真实波形,但至少能排除一半的软件错误。
5.3 实物移植时改这三个参数,比改逻辑更有效
上拉电阻从4.7k改成2.2k,适合数据线较长或供电电压偏低的场景;读时序采样点从两个NOP改成单个NOP,适合I/O翻转速度较快的STC系列;温度转换等待从轮询DQ改为固定延时750ms加一次查询,适合后续加入多任务调度,避免单总线一直占着CPU。再往后做闭环温度控制时,这套驱动可以直接作为采集层,把温度原始值传给PID或开环PWM模块,就扩展成51系列单片机闭环温度控制实验了。
一位小数查表法的实现也很短:先取出temp的低4位,用它查一个长度为16的常量表,得到0.0、0.1、0.1、0.2、0.3、0.3……对应的十分位。再把整数部分右移4位得到十位和个位。整个换算过程只用移位和查表,不调用乘除法,在51单片机的中断里也能稳定执行。
本文还有配套的精品资源,点击获取