简介:面向51单片机开发者的DS18B20温度传感器源码项目,适合单片机入门学习者以及正在做温控类课程设计、电子竞赛或小项目的读者参考。资源采用C语言编写,压缩包中共4个文件,包含2个C源文件、1个头文件和1个可直接烧录的hex固件,压缩包大小仅6KB,结构十分精简;源码覆盖DS18B20初始化、单总线读写时序、温度转换、数据输出等关键环节,主程序与底层驱动分离,模块化较好,既可整体烧录验证硬件连接,也方便将温度获取部分独立移植到其他工程,对理解51单片机与传感器交互很有帮助。代码中的函数划分清晰,注释到位,能帮助初学者避开DS18B20读写时序中的常见坑点,缩短调试时间。目前已有9028人学习下载,对于需要快速实现温度采集、入门单总线通信或复用代码的学习者而言,是一份轻量且实用的参考资料。
1. 51单片机读取DS18B20温度传感器代码的核心在时序
拿到一块DS18B20,三根线接好,程序烧进去,串口助手打印出来的全是0xFF;换成另一份例程,读数又常驻85.0。这两个现象几乎是每个做51单片机温度采集的人都会撞上的坎,根因不在传感器本身,而在单总线(1-Wire)的时序没有对上。DS18B20是数字输出,直接给出12位分辨率、最小0.0625℃的温度数据,不需要像NTC热敏电阻那样做查表或曲线拟合;代价是数据线和时钟共用一根DQ线,复位、写位、读位都有严格的微秒级窗口。51单片机GPIO速度不高,反而适合驱动这类低速器件,难点是把每个延时段写准。这篇不绕弯:先把时序参数拆开讲,再给一份能直接编译的C代码,最后拆掉最常见的那几个故障现象。适合准备做课程设计或温控风扇项目的读者,也适合在Proteus里调仿真却始终读不到温度的工程师。
2. DS18B20时序详解:初始化、写时序与读时序的15个关键参数
2.1 单总线协议与51单片机的时序匹配
DS18B20由Dallas Semiconductor定义1-Wire单总线协议,所有数据交换都在一根DQ线上完成。51单片机上没有硬件单总线控制器,必须用普通GPIO模拟,但这门手艺正好落在51的舒适区:单总线要求的时隙最短1us,最长120us,而11.0592MHz晶振下STC89C52一个机器周期约1.085us,GPIO翻转速度和协议要求在一个数量级。
模拟单总线通信有一个先决条件:51的P1、P2、P3口是准双向IO,读外部电平前必须先向引脚写1,否则读到的可能被内部锁存器拉低。DS18B20的复位、写1、读时隙流程里主机本来就要释放总线(拉高DQ),所以代码里不会出现“先写1再读”这种额外节奏,直接按协议时序走就行。
2.2 复位脉冲与存在应答检测
每一次完整通信都以复位开始。主机把DQ拉低480us到960us,然后释放;DS18B20检测到上升沿后,等待15us到60us,主动将DQ拉低60us到240us,这段低电平就是存在应答脉冲(Presence Pulse)。主机在释放总线后60us左右采样,读到低电平说明器件在线。
复位时序里有三个参数不能写错:拉低时间不能短于480us,否则传感器可能还没反应过来你就松手了;释放后的采样点不能太早,要落在DS18B20拉低存在应答的时间窗内;整个复位周期结束后DQ必须恢复高电平,再留出至少1us恢复时间,才能发下一条命令。
2.3 写0与写1的时隙拆分
写时序把每个位拆成一个写时隙(Write Time Slot),时隙整体长度60us到120us。写0和写1的区别在于低电平持续时间:
写0时,主机把DQ从拉低一直保持60us到120us,期间不释放,再从低拉到高完成一个时隙。写1时,主机只把DQ拉低1us到15us,然后立刻释放,靠外部上拉电阻把总线拉回高电平,保持到时隙结束。
DS18B20在写时隙开始后的15us到60us窗口内对DQ采样。这条规则有一个反直觉的推论:写1时拉低时间一旦超过15us,传感器会把“1”误判成“0”。很多人写代码时习惯用一个统一的延时函数,结果写1的拉低时间被拖到30us以上,读数就乱了。
2.4 读时隙与15us采样窗口
读时隙的细节更考验代码功力。主机先把DQ拉低至少1us,然后释放,DS18B20检测到这个起始沿后,如果当前位是0,它会主动把总线拉低60us到120us;如果当前位是1,它不动作,总线被上拉电阻维持在高电平。
关键的采样窗口在起始沿之后的15us以内。主机释放总线后,如果等超过15us才去读DQ,无论传感器给出的是0还是1,此时总线很可能已经被上拉成高电平,结果全部读成0xFF。这也是前文提到的“串口打印全是0xFF”最隐蔽的原因——传感器是好的,时序窗口错过了。
2.5 DS18B20时序参数速查表
| 时序段 | 参数 | 最短 | 典型 | 最长 | 说明 |
|---|---|---|---|---|---|
| 复位 | 主机拉低 | 480us | 500us | 960us | 小于480us可能复位失败 |
| 存在应答 | 等待应答 | 15us | 60us | 60us | 释放后在60us左右采样 |
| 存在应答 | 应答低电平 | 60us | - | 240us | 采样点为低电平说明在线 |
| 写时隙 | 时隙总长 | 60us | - | 120us | 前后两个时隙间至少恢复1us |
| 写0 | 低电平保持 | 60us | - | 120us | 全程保持 |
| 写1 | 低电平保持 | 1us | - | 15us | 超过15us会被误判为0 |
| 读时隙 | 低电平预置 | 1us | - | - | 主机拉低后立即释放 |
| 温度转换 | 12位分辨率 | - | - | 750ms | 9位时93.75ms,10位187.5ms,11位375ms |
参数表不是背下来就完事。它对应到代码里就是每个函数的延时量,后面第4章的代码会逐行对应回来。记住一句话优先于所有细节:写1拉低不超过15us,读时隙采样不晚于15us,复位拉低不少于480us。
3. 硬件连接与最小系统:引脚分配、上拉电阻与Proteus仿真要点
3.1 DS18B20引脚定义与51单片机接线表
DS18B20常见封装是TO-92三脚,平面朝向自己时从左到右分别是GND、DQ、VDD。如果是买来的小模块,通常已经焊好上拉电阻和电源滤波电容,只引出VCC、GND、DQ三根线,接线更简单。
| DS18B20引脚 | 功能 | 接51单片机 |
|---|---|---|
| GND | 电源地 | GND |
| DQ | 数据线/1-Wire总线 | P3.7(可换任意IO) |
| VDD | 电源正极 3.3V~5.5V | VCC(与单片机同电源) |
我一般把DQ固定放P3.7,理由只有一个:P3口在STC89C52的DIP-40封装上靠外侧,走线方便。实际选哪个引脚都可以,但选定之后代码里sbit DQ = P3^7;要同步改,这个低级错误出现频率远比你想象的高。注意VDD必须和51单片机共地,供电取自同一路5V,不要用两个电源分别供。
3.2 4.7k上拉电阻为什么必须接
DS18B20的DQ引脚是漏极开路结构,只能主动拉低,不能主动输出高电平。总线上的高电平完全依靠外部上拉电阻提供。没有这个电阻,复位时主机释放总线后DQ处于悬空状态,电平不确定,存在应答和读时隙全部无法工作。
常规取4.7k,范围1k到10k都行。连接线短、干扰小时可以取大一点;连接线超过半米、工作环境有电机或开关电源时,建议取1k到2.2k并适当缩短走线。上拉电阻一端接DQ,另一端接VCC,这是必须的,不能省。
DS18B20还支持寄生供电模式:VDD引脚直接接地,依靠DQ线上的高电平给内部电容充电。51单片机驱动寄生供电没问题,但转换期间电流尖峰明显,电源纹波大的时候容易造成转换结果漂移。做课程设计或温控项目建议直接外部供电,省掉一类排查麻烦。
3.3 晶振选择、引脚复用和Proteus仿真注意事项
晶振选择直接牵涉到后面代码能不能跑通。推荐直接上11.0592MHz:这个频率下51的串口能精确产生9600bps波特率,而DS18B20对主机时钟并不敏感,两个场景共用一个晶振是最省心的方案。如果你坚持用12MHz,串口9600会引入约2.1%误差,短帧能通,长帧偶尔乱码。
Proteus仿真有个坑:仿真模型对漏极开路和上拉电阻的建模比实物宽松,DS18B20模型不上拉电阻也可能出正常读数,导致你把仿真搬到实物后第一个故障就是全0xFF。不要因为仿真能跑就略过上拉电阻,实物接线永远按“必须有4.7k上拉”来画图。仿真里晶振、器件选型随意,但实测时以第4章代码里注释的晶振和延时关系为准。
4. 51单片机读取DS18B20温度的C语言代码实现
4.1 代码框架与延时函数的设计方式
完整的测温流程分四段:初始化(复位+存在检测)、跳过ROM寻址(0xCC)、启动温度转换(0x44)、转换结束后读暂存器(0xBE)。单器件接线时ROM命令可以固定用0xCC跳过,不涉及搜索ROM和匹配ROM。
延时是这套代码的灵魂。空循环延时的实际时间受Keil优化级别影响,你把Optimization从0级调到9级,同样一段while(t--);可能快出几倍。稳妥写法是加入_nop_()并统一晶振为11.0592MHz:
#include <reg52.h> #include <intrins.h> #define uchar unsigned char #define uint unsigned int sbit DQ = P3^7; // DS18B20数据线,可换其他IO // 11.0592MHz晶振下,单条空循环约1us到2us void delay_us(uint t) { while (t--) { _nop_(); } } void delay_ms(uint t) { uchar i; while (t--) { for (i = 0; i < 120; i++); } }这段代码里延时单位不是绝对精确,只能保证量级正确。DS18B20时序的容差范围很宽(复位480us到960us、写时隙60us到120us),量级对了就能工作。真要精确到每个微秒,需要在Keil里关闭优化或使用#pragma OPTIMIZE(1),再用逻辑分析仪校准。
4.2 复位、写入和读取的时序函数实现
下面是协议层的核心函数。注意读位函数里采样点位置,这段代码比网上很多版本更强调“释放后立即采样”:
// 复位并检测存在脉冲,返回1表示检测到DS18B20 bit DS18B20_Reset(void) { bit ack; DQ = 0; delay_us(500); // 拉低500us,满足480us~960us DQ = 1; // 释放总线 delay_us(60); // 等待60us后采样 ack = DQ; // 采样存在应答,低电平为有效 delay_us(420); // 补足整个复位周期 return ~ack; } // 写一个位,val为1时走写1时隙,val为0时走写0时隙 void DS18B20_WriteBit(bit val) { DQ = 0; _nop_(); // 拉低保持1us if (val) { DQ = 1; // 写1:快速释放,让上拉电阻拉高 } delay_us(60); // 时隙总长60us DQ = 1; _nop_(); } // 读一个位,采样点必须靠近起始沿 bit DS18B20_ReadBit(void) { bit val; DQ = 0; _nop_(); // 拉低1us _nop_(); DQ = 1; // 释放总线 _nop_(); // 等待约2us val = DQ; // 在15us窗口内采样 delay_us(60); // 占满整个读时隙 return val; } // 写一个字节,低位在前 void DS18B20_WriteByte(uchar dat) { uchar i; for (i = 0; i < 8; i++) { DS18B20_WriteBit(dat & 0x01); dat >>= 1; } } // 读一个字节,低位在前 uchar DS18B20_ReadByte(void) { uchar i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; if (DS18B20_ReadBit()) { dat |= 0x80; } } return dat; }DS18B20_Reset返回值用了取反技巧:存在应答为低电平,ack = DQ读到0表示有设备,函数返回~ack即1,让调用处的if(!DS18B20_Reset())语义更直白。读写字节都坚持LSB first,这是单总线协议的规定,顺序反了会得到完全错误的温度值。读位函数里两个_nop_()约等于2us到3us,释放后只等一次_nop_()就采样,这个节奏是读时序不出问题的关键。
4.3 温度转换主流程与负数补码处理
温度寄存器的低字节是TL、高字节是TH,合成一个16位值。正数时直接乘0.0625就得到摄氏度;负数时高字节最高位为1,需要取反加一得到绝对值,再加负号。
为避免浮点运算,工程上更常用的做法是把返回值放大10倍或100倍。下面代码返回十分之一摄氏度,比如256表示25.6℃:
// 启动一次温度转换,单设备场景跳过ROM void DS18B20_Convert(void) { DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM,单设备不需要匹配64位序列号 DS18B20_WriteByte(0x44); // 启动温度转换 } // 读取温度值,返回十分之一摄氏度 // 设备未应答时返回0x7FFF,用于主函数区分故障 int DS18B20_ReadTempX10(void) { uchar tl, th; int raw, temp10; if (!DS18B20_Reset()) { return 0x7FFF; } DS18B20_WriteByte(0xCC); // 跳过ROM DS18B20_WriteByte(0xBE); // 读暂存器,从第0字节开始 tl = DS18B20_ReadByte(); // 温度低字节 th = DS18B20_ReadByte(); // 温度高字节 raw = (th << 8) | tl; if (raw & 0x8000) // 符号位为1,说明是负温度 { raw = ~raw + 1; // 取补码,得到绝对值 temp10 = raw * 625 / 1000; return -temp10; } else { return raw * 625 / 1000; } }0x7FFF作为错误码是安全的:DS18B20实际测温范围只有-55℃到+125℃,12位分辨率下最大正数不会超过2047,放大10倍是1279,远小于32767。实际项目里这个返回值就是“传感器不在线”的哨兵值,主函数拿到它做不上电告警。
乘625除1000的本质是把0.0625转化为整数运算:0.0625 * 10 = 0.625,raw * 625 / 1000就是raw * 0.625。之所以用625而不是6250除10000,是为了让raw最大2047时中间乘积不超过20位,51的int刚好放得下。
4.4 串口输出与主循环调度
转换启动后要等750ms才能读结果。有人用一个死循环在转换期间干等,更合理的做法是启动转换后让主循环去做别的事,下次循环再来读。不过课程设计里多数是轮询,先用延时方式把链路跑通:
void UART_Init(void) { SCON = 0x50; // 串口方式1,8位UART,允许接收 TMOD &= 0x0F; TMOD |= 0x20; // 定时器1为模式2 TH1 = 0xFD; TL1 = 0xFD; // 11.0592MHz下波特率9600 TR1 = 1; } void UART_SendByte(uchar dat) { SBUF = dat; while (!TI); TI = 0; } void main(void) { int t10; UART_Init(); EA = 0; // 温度采集过程中不响应中断,避免时序被拉偏 while (1) { DS18B20_Convert(); // 启动转换 delay_ms(760); // 等12位分辨率转换完成 t10 = DS18B20_ReadTempX10(); if (t10 == 0x7FFF) { UART_SendByte('X'); // 传感器未接或初始化失败 UART_SendByte('\r'); UART_SendByte('\n'); continue; } if (t10 < 0) { UART_SendByte('-'); t10 = -t10; } UART_SendByte('0' + t10 / 100); // 十位 UART_SendByte('0' + (t10 / 10) % 10); // 个位 UART_SendByte('.'); UART_SendByte('0' + t10 % 10); // 十分位 UART_SendByte('\r'); UART_SendByte('\n'); delay_ms(500); // 两次测温间隔,给系统留出喘息余地 } }主循环里先启动转换再延时,是刻意分解的。后续要做多路采集时,可以把delay_ms(760)换成对DQ轮询或把等待时间交给调度器,这样代码结构不用大改。关闭总中断EA = 0这条值得养成习惯:51没有硬件单总线控制器,任何中断服务程序里的串口发送、定时器重装都会把微秒级时序打断,导致采样点偏移。
5. 排错、迟滞控制与CRC校验:把测温代码用进温控项目
5.1 51单片机测温代码的常见故障排查对照表
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 串口打印全0xFF | 复位失败、存在脉冲没采到、DQ根本没上拉 | 测DQ对地电压,正常空闲时应为高电平;补4.7k上拉电阻 |
| 读数常驻85.0 | 未发0x44转换命令,或转换未完成就读取 | 检查主循环是否调用DS18B20_Convert,转换后等待时间是否≥750ms |
| 读数为0或数值乱跳 | 读时序采样点晚于15us,读到上拉高电平 | 检查ReadBit中释放DQ后到采样之间的延时,改成不超过两三个NOP |
| 温度精度退化为0.5℃ | 分辨率被写入的配置字改成9位 | 重新上电默认12位,或写0x4E重设配置寄存器 |
| 换STC15等1T单片机后全错 | 机器周期缩短,原空循环延时不成立 | 定时器延时或按新时钟重新标定NOP循环 |
排查时先看存在应答,再看写时序,最后看读时序,这个顺序别反。存在应答是后面所有命令的前提,复位都失败时不要去调读时隙。
5.2 温控风扇:迟滞比较替代直接阈值比较
读温度只是第一步,温控风扇或闭环温控实验才是多数人最终要做的。直接“大于32℃开风扇,小于28℃关风扇”会造成继电器在临界点反复吸合,解决办法是引入迟滞:
#define TEMP_ON 320 // 32.0℃开启 #define TEMP_OFF 280 // 28.0℃关闭 static bit fan_on = 0; void Fan_Control(int t10) { if (fan_on) { if (t10 < TEMP_OFF) // 开启状态下低于关断点才关 { fan_on = 0; } } else { if (t10 >= TEMP_ON) // 关闭状态下高于开启点才开 { fan_on = 1; } } FAN = fan_on; // FAN接继电器或PWM驱动 }迟滞量4℃,继电器吸合频率大幅降低。如果驱动的是小风扇而不是继电器,可以把FAN输出换成PWM调速,51用定时器输出PWM时注意把EA = 0改为只在读温度期间关中断,否则PWM输出和温度采集会互相干扰。
5.3 用CRC校验确保DS18B20暂存器数据可靠
DS18B20读暂存器命令0xBE会连续返回9个字节,第9字节是前8字节的CRC校验值。工业场景或长线传输时建议校验,忽略CRC的做法在读数偶尔漂移时会让你无从下手:
uchar OneWire_CRC8(uchar crc, uchar dat) { uchar i; crc ^= dat; for (i = 0; i < 8; i++) { if (crc & 0x01) { crc = (crc >> 1) ^ 0x8C; // CRC-8/Dallas多项式 } else { crc >>= 1; } } return crc; }0x8C来自DS18B20使用的Dallas CRC-8多项式x^8+x^5+x^4+1,按LSB-first方式移位时异或值为0x8C。使用时把读回的9个字节中前8个依次送入OneWire_CRC8,最终结果与第9字节比较,不一致就丢弃本次读数。校验函数只有几行,换来的是定位“传感器劣化”还是“传输干扰”的能力。手边有逻辑分析仪的话,把DQ引脚波形抓下来对照第2章参数表看一遍,所有时序疑点当场就能定案。
本文还有配套的精品资源,点击获取