目录
[5 硬件设计](#5 硬件设计)
[6 软件设计](#6 软件设计)
[6.1 动态数码管显示](#6.1 动态数码管显示)
[6.2 DS18B20 底层驱动](#6.2 DS18B20 底层驱动)
[① 复位函数](#① 复位函数)
[② 应答检测函数](#② 应答检测函数)
[③ 初始化函数](#③ 初始化函数)
[④ 启动温度转换函数](#④ 启动温度转换函数)
[⑤ 温度读取函数](#⑤ 温度读取函数)
实验设计
5 硬件设计
本次实验需要用到动态数码管、DS18B20
从下图模块电路可以看到:
传感器的单总线管脚接至 P3.7 IO口,为了让单总线默认为高电平,通常外接上拉电阻
但图中没有看到上拉电阻,这是因为单片机IO口外接了 10K 上拉电阻
当单片机 IO 口连接到传感器的总线管脚时即相当于他们外接了上拉电阻

6 软件设计
实验目的:插上 DS18B20 温度传感器,数码管显示检测的温度值
6.1 动态数码管显示
作用:
把解析后的温度数据(以段选码形式存在数组里),从指定位置开始逐位显示在 8 位数码管上
知识回顾:
① 板载动态数码管一般是共阴极连接,且阴极连接到 74HC138 驱动芯片(用于扩展单片机IO口)上,只需控制该芯片的 1,2,3 管脚(分别连接 P2.4,P2.3,P2.2)就可控制动态数码管的位选端,即控制哪一个数码管点亮
② 动态数码管另一端(段选端)连接 74HC245 芯片(用于驱动电路)(且连接在单片机的 P0 端),控制点亮的数码管显示的内容,一般使用数组存储将要显示的数字 / 字符
③ 阴极为低电平,阳极为高电平即可
④ 利用人眼 "视觉暂留"(约 24ms),快速逐个选中每一位数码管并显示对应内容,在时间差24ms 内再次点亮,就会看起来像 "同时亮"
以下是代码:
public.h
cpp
#ifndef _public_H
#define _public_H
#include <at89c51RC2.h>
typedef unsigned char u8;
typedef unsigned int u16;
//延时函数声明
void Delay10us(u16 um);
#endif
cpp
#include "public.h"
//声明存储数码管段选的数组
extern u8 gsmg_code[17];
sbit LSC = P2^4;
sbit LSB = P2^3;
sbit LSA = P2^2;
sbit SMG_A_DP_PORT = P0;
cpp
#include "public.h"
#include "smg.h"
/*知识回顾:
位选"(控制哪一个数码管点亮)和"段选"(控制点亮的数码管显示什么数字 / 符号)
SMG_A_DP_PORT是段选端口( P0 口)
动态数码管显示
dat(date): 要显示的温度对应的段选码数据
pos(position):从左边开始第几个数码管显示温度*/
void smg_display(u8 dat[],u8 pos)
{
u8 i = 0;
for(i= pos-1;i<8;i++)
{
//位选:控制哪一个数码管亮
switch(i)
{
case 0:LSC=1;LSB=1;LSA=1;break;
case 1:LSC=1;LSB=1;LSA=0;break;
case 2:LSC=1;LSB=0;LSA=1;break;
case 3:LSC=1;LSB=0;LSA=0;break;
case 4:LSC=0;LSB=1;LSA=1;break;
case 5:LSC=0;LSB=1;LSA=0;break;
case 6:LSC=0;LSB=0;LSA=1;break;
case 7:LSC=0;LSB=0;LSA=0;break;
}
/*SMG_A_DP_PORT是段选端口 P0,dat数组里的段选码送到这个端口
i-pos_temp:对齐数组索引 */
SMG_A_DP_PORT = dat[i+1-pos];//控制显示内容
Delay10us(300);//延时一段时间,等待显示稳定
SMG_A_DP_PORT = 0X00;//消影
}
}
① u8 dat[]:要显示的温度段选码数组
比如显示温度 20.5,分别对应:2 --- 0x5B ,5 --- 0x6D ,.5 --- 0xBF
dat[0] = 0x5B , dat[1] = 0x6D ,dat[2] = 0xBF
② u8 pos:起始显示位置(从左边数第几个数码管开始显示)
比如温度是 2 位(25℃),想从左边第 3 位开始显示,就传 pos = 3;温度是 3 位(125℃),想从左边第 2 位开始显示,就传 pos=2 ------ 适配不同长度的温度值(比如 - 5℃、25℃、125.℃)
③ 变量初始化:把 "位置" 转 "代码索引"
cpp
u8 pos_temp = pos -1;
数码管的位选逻辑是 "索引 0~7",但实际对应左边第 1~8 位
比如 pos=2(左边第 2 位)→pos_temp=1(代码里的索引 1),后续循环从索引 1 开始。
④ 段选:让选中的数码管显示温度
cpp
SMG_A_DP_PORT = dat[i-pos_temp];//传送段选数据
SMG_A_DP_PORT是数码管段选的端口(比如 P0 口),把dat数组里的段选码送到这个端口,选中的数码管就会显示对应数字 / 符号;
i-pos_temp:对齐数组索引 ------dat数组从 0 开始(比如dat[0]是温度的第一位),而循环从pos_temp开始,所以用i-pos_temp取dat的第 0、1、2... 位,对应显示到数码管的pos_temp、pos_temp+1...位。举例:pos=2(pos_temp=1),i=1时→i-pos_temp=0→取dat[0](比如 2 的段选码),让左边第 2 位显示 2;i=2时→i-pos_temp=1→取dat[1](5 的段选码),让左边第 3 位显示 5
6.2 DS18B20 底层驱动
该函数实现了 "传感器初始化(握手检测)→启动温度转换→读取并解析温度" 的全流程
使用了以下函数
DS18B20_PORT:DQ 引脚对应的 IO 口(P3.7),是主机和传感器通讯的唯一接口;
Delay10us(n):精准的 10μs 级延时函数(n=1就是 10μs,n=75就是 750μs);
ds18b20_write_byte(u8 dat):向 DS18B20 写 1 字节数据(模拟单总线写时序);
ds18b20_read_byte(void):从 DS18B20 读 1 字节数据(模拟单总线读时序)。
①--③ 部分容易混淆,这里做个区分

分成测温前准备 进行测温 测温后 三个方面进行叙述
测温前准备
可以把主机(单片机)比作 "老师",传感器比作 "学生":
-
第一步:等学生 "举手回应"(传感器拉低 DQ)
- 老师喊 "上课"(主机发 reset 复位)后,会盯着学生(检测 DQ 引脚);
- 正常学生(传感器)会在 15~60μs 内举手(主动拉低 DQ 引脚);
check函数做的事:最多等 200μs(留余量),如果学生一直没举手(DQ 始终高电平),判定 "学生不在教室(传感器不存在)",返回 1;如果举手了,进入第二步。
-
第二步:等学生 "放下手"(传感器释放 DQ)
- 学生举手后,不会一直举着,正常会举 60~240μs 后放下(传感器主动释放 DQ,引脚回到高电平);
check函数做的事:重置计数器,最多再等 200μs,如果学生一直举手不放(DQ 始终低电平),判定 "学生卡住了(传感器异常)",返回 1;如果学生正常放手,判定 "通讯正常",返回 0。
cpp
程序上电 → ds18b20_init(确认传感器在线)
↓
需要测温 → 执行ds18b20_start():
复位→check(喊醒传感器,确认在线)
→ 发0xcc(跳过地址)
→ 发0x44(启动测温)
→ 延时750ms(等温度测完)
↓
执行ds18b20_read_temperture():
复位→check(再次喊醒,确认在线)
→ 发0xcc(跳过地址)
→ 发0xbe(读温度数据)
→ 读高低字节→换算成℃→返回
① 复位函数
ds18b20_reset ()****------ 发 "呼叫信号"(严格匹配复位时序)
这是单片机(主机)给 DS18B20(传感器)发的 "大声招呼"------ 把摸鱼的传感器喊醒,告诉它 "我要和你说话了,准备好!"
用「便利店买东西」的生活化场景拆解开,一看就懂:
先定角色
- 单片机(主机)= 你(顾客)
- DS18B20(传感器)= 便利店店员(平时可能低头玩手机 / 摸鱼,没注意你)
- DQ 引脚 = 你和店员之间的 "交流通道"(比如喊人的声音)
cpp
//复位DS18B20
//复位时序"------ 主机拉低≥480μs→释放→等待传感器应答;
void ds18b20_reset()
{
DS18B20_PORT = 0; //拉低DQ引脚(主机发起复位)
Delay10us(75); //拉低750 μs(满足"≥480μs"的最低要求,留余量)
DS18B20_PORT = 1; //释放DQ引脚(让上拉电阻拉到高电平)
Delay10us(2); //等待20 us(给传感器回应的时间)
}
对应到 "喊店员" 的动作:
DS18B20_PORT=0(拉低 DQ)→ 你提高嗓门喊:"店员!"主动发起 "呼叫",用低电平这个 "信号" 告诉传感器:"我要和你通讯了,别摸鱼了!"Delay10us(75)(拉低 750μs)→ 你持续喊够 1 秒(留足够时间)不是随口喊一声就停,而是喊够传感器能识别的时间(DS18B20 要求至少喊 480μs),确保它能听到,而不是把你的 "招呼" 当成干扰。DS18B20_PORT=1(释放 DQ)→ 你喊完,停下来不说话释放引脚(让上拉电阻把 DQ 拉回高电平),相当于告诉传感器:"我喊完了,该你回应了!"Delay10us(2)(等 20μs)→ 你稍等片刻,给店员反应时间传感器听到招呼后,需要一点时间从 "摸鱼状态" 切换到 "应答状态",这 20μs 就是留给它的 "反应缓冲",避免你刚喊完就急着等回应,没给它准备时间。
此外,
- 拉低 750μs:DS18B20 要求主机拉低 DQ 至少 480μs 才能识别 "复位信号",750μs 是工程上的常用值(留余量);
- 释放引脚后等 20μs:传感器需要 15~60μs 的时间反应,这里等 20μs 是 "提前就位",准备检测应答
- 释放 DQ 后 ,DQ 电平由上拉电阻(4.7KΩ)拉到高电平,传感器若在线,会主动拉低 DQ 来回应。
通过拉低再释放 DQ 引脚的动作,给传感器发一个 "我要和你说话了" 的明确信号,并且留够时间让传感器听到、准备好 ------ 这是和传感器通讯的 "第一步",不管是初始化、启动测温还是读数据,都得先做这一步,就像和人说话前,得先喊一声对方名字一样。
② 应答检测函数
ds18b20_check () ------ 听传感器 "回应"
ds18b20_check 本身不发起复位 ,也不执行任何业务指令 ,是纯工具函数
唯一作用是:验证 DS18B20 对主机 "复位信号(reset)" 的应答是否符合时序规则,以此判断传感器是否在线 / 通讯正常 ------ 它不发起任何指令,只做 "应答有效性检测"。
通俗的说,check( ) 的作用是
**每次和传感器通讯前(包括初始化、启动转换、读取温度),检测传感器和主机的通讯链路是否正常,**就像每次做饭打鸡蛋放进去前,都要单独打到碗里进行"检测",避免一个臭鸡蛋坏了一锅饭
cpp
//检测DS18B20是否存在--复位后等待 15~60μs 检测低电平
u8 ds18b20_check()
{
u8 time_temp = 0;
//第一步:等待 DS18B20 拉低DQ(应答信号),超时则不存在
/*传感器正常的话,会在主机释放DQ后15~60μs内拉低DQ,
所以循环最多等 200μs(留余量),如果 DQ 一直是高电平,
说明传感器没回应;*/
while(DS18B20_PORT && time_temp < 20)
{
time_temp++;
Delay10us(1);//每次延时10μs,最多等200μs
}
if(time_temp >= 20) return 1;//超时(200us),传感器不存在
else time_temp=0;
//第二步:等待 DS18B20 释放 DQ(应答结束),超时则异常
/*传感器的应答低电平会持续 60~240μs,循环等它释放
DQ(拉回高电平),超时则说明传感器异常*/
while((!DS18B20_PORT) && time_temp < 20)
{
time_temp++;
Delay10us(1);
}
if(time_temp>=20) return 1;//超时,传感器异常
return 0;//检测通过,传感器存在
}
这是第一步**,传感器正常的话,会在主机释放 DQ 后 15~60μs 内拉低 DQ**,所以循环最多等 200μs(留余量),如果 DQ 一直是高电平,说明传感器没回应
如果 DQ一直没有被拉低,将继续执行 while 循环,200us 后自然跳出循环,返回 1
若中途 DQ 被拉低,直接跳出 while 循环,同时 time_temp 清零,进入第二步
cpp
u8 time_temp = 0;
//第一步:等待 DS18B20 拉低DQ(应答信号),超时则不存在
while(DS18B20_PORT==1 && time_temp < 20)//若中途DS18B20_PORT==0,进入第二步
time_temp++;
Delay10us(1);//每次延时10μs,最多等200μs
}
if(time_temp >= 20) return 1;//超时(200us),传感器不存在
else time_temp=0;
第二步:传感器的应答低电平会持续 60~240μs,循环等它释放 DQ(拉回高电平),超时则说明传感器异常
① 低电平时间超过 200us,跳出循环,传感器异常
② 中途变为高电平,跳出循环,传感器正常
cpp
//第二步:等待 DS18B20 释放 DQ(应答结束),超时则异常
/*传感器的应答低电平会持续 60~240μs,循环等它释放
DQ(拉回高电平),超时则说明传感器异常*/
while(DS18B20_PORT==0 && time_temp<20)
{
time_temp++;
Delay10us(1);
}
if(time_temp>=20) return 1;//超时,传感器异常
return 0;//检测通过,传感器存在
这样写更加通俗易懂:
cpp
u8 ds18b20_check()
{
u8 time_temp = 0;
//第一步:等待 DS18B20 拉低DQ(应答信号),超时则不存在
while(DS18B20_PORT==1)//若中途DS18B20_PORT==0,进入第二步
{
time_temp++;
Delay10us(1);//每次延时10μs,最多等200μs
if(time_temp >= 20)//超时(200us),传感器不存在
{
return 1;
}
}
time_temp = 0;//以待下次使用
//第二步:等待 DS18B20 释放 DQ(应答结束),超时则异常
/*传感器的应答低电平会持续 60~240μs,循环等它释放
DQ(拉回高电平),超时则说明传感器异常*/
while(DS18B20_PORT==0)
{
time_temp++;
Delay10us(1);
if(time_temp>=20)
return 1;//超时,传感器异常
}
return 0;//检测通过,传感器存在
}
③ 初始化函数
ds18b20_init(初始化函数)就是程序上电后对传感器做的「首次全面检测」,核心就是验证传感器物理层面在线 + 通讯层面正常
- "物理层面正常"------ 传感器没坏、接线对
init函数返回 0 的前提是:
传感器硬件没损坏(比如芯片没烧、引脚没断);
接线正确(DQ 引脚接对、上拉电阻(4.7KΩ)接好、电源 / 地没接反) ;如果传感器物理层面有问题,check函数会检测不到应答,init返回 1,相当于 "这台设备根本没法用"。
- "通讯层面正常"------ 单总线时序匹配、能握手
init里调用reset(主机发复位信号)+check(检测传感器应答),本质是验证:
- 主机的 IO 口时序(拉低 / 释放 DQ 的延时)和传感器的应答时序匹配;
- 传感器能识别主机的复位信号,且能正常发回应答信号;这一步确认 "主机和传感器能'说上话'",是后续发测温 / 读数据指令的前提。
init 的 "一次性全面检测"vscheck 的 "重复性链路检测"
用 "新手机激活" 类比更易理解
ds18b20_init:新买的手机,首次开机激活 ------ 既要确认手机硬件没坏(能开机),又要确认能和运营商基站通讯(能打电话),这是 "一次性全面检测";
ds18b20_check:后续每次打电话前,看一下手机信号格(确认当前能通讯)------ 不验证手机硬件是否坏了,只验证 "此刻能和基站说话",是 "重复性快速检测"。

以下是详细代码
cpp
//功能:初始化 DQ 口,同时检测 DS18B20 是否在线
//逻辑:先复位 DS18B20(发"呼叫信号"),再检测传感器的"应答信号"
u8 ds18b20_init()
{
ds18b20_reset();
return ds18b20_check();//1:不存在;0:存在
}
核心目标:确认传感器 "物理上接好、能通讯"(比如接线错、传感器坏、没接电源,这一步会直接返回 1,程序可以提前报错)
调用时机:程序上电后只执行 1 次(比如 main 函数开头),后续不会再用 ------ 它是 "一次性的硬件检测",不是 "业务指令"
返回值:0 = 传感器在线(通讯正常),1 = 传感器离线(最常见原因:DQ 引脚接线错、上拉电阻没接、传感器损坏)。
进行测温
④ 启动温度转换函数
ds18b20_start () ------ 让传感器 "去测温"
start只是 "让传感器开始测温度",相当于 "下单让店员干活";
读温度是ds18b20_read_temperture函数的事,相当于 "去拿店员测好的结果"。
cpp
//实现温度转换
void ds18b20_start()
{
ds18b20_reset(); //复位传感器(先握手)
ds18b20_check(); //检测应答(确认在线)
ds18b20_write_byte(0xcc); //"请听接下来的指令"(单传感器跳过地址)
ds18b20_write_byte(0x44); //发送启动温度转换命令
void Delay10us(7500); //【语法错误!】给时间进行温度转换
}
0xcc(SKIP ROM):单传感器场景下,不用匹配 64 位 ROM 地址,直接发功能指令(多传感器必须用 0x55 + 地址)
0x44(Convert T):传感器收到后,启动内部温度转换(把物理温度转成 16 位数字补码,存到高速缓存)
DS18B20 12 位分辨率下,温度转换需要 750ms,所以正确的延时是DelayMs(750);(毫秒级延时),若没这个延时,传感器还没转换完就读数据,会读到错误值。
ds18b20_start() = 唤醒传感器 → 确认它在线 → 简化沟通 → 下达 "开始测温" 指令 → 等测温完成,全程只为让传感器产出 "最新的、有效的温度数据",是后续读取温度的必经前提 ------ 就像你要吃炒饭,得先让厨师 "开始炒",等他炒完,才能端过来吃。
测温后
⑤ 温度读取函数
ds18b20_read_temperture () ------ 拿测温结果并解析
这是单片机 "让传感器先测温度→再要测温结果→最后把结果换算成能看懂的℃数" 的完整操作------ 就像你让便利店店员帮你测体温,然后拿过温度计、换算成实际体温的全过程。
函数的完整流程:
你(单片机)→ 喊店员(reset)→ 让店员测体温(start)→ 等店员测完(延时)
→ 再喊店员(reset)→ 确认店员在(check)→ 说"直接给我读数"(0xcc)+"把读数给我"(0xbe)
→ 拿店员给的两半读数(datl/dath)→ 拼起来(value)→ 换算成℃数(正/负)
→ 把℃数交给数码管显示
cpp
//温度读取函数:从 DS18B20 读取温度
float ds18b20_read_temperture()
{
float temp; //最终返回的浮点型温度
u8 dath = 0; //温度高字节
u8 datl = 0; //温度低字节
u16 value = 0; //合并后的16位温度值
ds18b20_start(); //第一步:启动温度转换
ds18b20_reset(); //第二步:重新复位,建立通讯
ds18b20_check(); //检测应答
ds18b20_write_byte(0xcc); //SKIP ROM命令
ds18b20_write_byte(0xbe); //读存储器命令(0xBE)
datl = ds18b20_read_byte();//读低字节(缓存第0字节)
dath = ds18b20_read_byte();//读高字节(缓存第1字节)
value = (dath<<8)+datl;//合并为16位补码(高字节左移8位+低字节)
// 第三步:解析正负温度
if((value&0xf800)==0xf800)//判断符号位:0xf800=11111000 00000000
{ //高5位全1,说明是负温度
value = (~value)+1; //补码还原:取反+1得到绝对值
temp = value*(-0.0625);//乘以精度(0.0625℃),加负号
}
else
{
temp = value * 0.0625;//正温度,直接计算
}
return temp;
}
这是整个代码的 "最终目标",拆成 3 个核心步骤:
第一步:先看变量定义(准备 "装数据的容器")
cpp
float temp; //最终返回的浮点型温度(如25.5,-0.5)
u8 dath = 0; //温度高字节(温度计的"十位/百位纸条")
u8 datl = 0; //温度低字节(温度计的"个位/小数位纸条")
u16 value = 0; //合并后的16位温度值(把两张纸条拼起来的完整数)
准备了3 个容器:
temp:最终要写 "25.5℃" 的纸条;
dath/datl:分别装店员温度计拆成的 "两半读数";
value:把两半读数拼起来的 "完整编码数"。
第二步:核心操作拆解(和店员的互动流程)
- 让店员先去测温度(启动转换)
对应场景:你对店员说 "帮我测个体温"→店员拿起专用温度计,开始测温(需要等 750ms 才能测完)。
👉 关键:这一步是 "让传感器干活",不是上来就拿结果 ------ 如果跳过这步,直接要数据,拿到的是上一次的旧温度,甚至错数据。
cpp
ds18b20_start(); //第一步:启动温度转换
- 重新喊店员、确认它在(复位 + 检测应答)
对应场景:店员测完体温后,你怕它低头摸鱼没听见,再喊一声 "店员!"(reset),确认它回应 "在呢!"(check)------ 确保它能听你接下来的 "要读数" 指令。
👉 为什么要重新复位?启动转换(ds18b20_start())是 "让店员干活",干活后需要重新建立通讯,才能要结果,就像店员干完活可能走神,得再喊一声。
cpp
ds18b20_reset(); //第二步:重新复位,建立通讯
ds18b20_check(); //检测应答
- 告诉店员 "不用报名字,直接给我读数"(发命令)
0xcc(SKIP ROM):店里只有这一个店员,不用喊名字(跳过 "点名"),直接说指令;如果有多个店员(多传感器),就得喊名字(0x55 + 地址);
0xbe(读存储器):你对店员说 "把刚测的体温数给我!"→店员听到后,准备把温度计的读数拆成两半告诉你。
cpp
ds18b20_write_byte(0xcc); //SKIP ROM命令
ds18b20_write_byte(0xbe); //读存储器命令(0xBE)
回顾:
0xCC:单传感器时用,跳过地址;多传感器时不能用(会冲突);
0x44:启动测温,发完必须延时(12 位分辨率等 750ms)
0xBE:读温度数据,发完后先读低字节(第 0 字节),再读高字节(第 1 字节)
- 拿店员给的 "两半读数"(读高低字节)
对应场景:店员的温度计读数是 "拆成两半写的"(比如 25.5℃拆成 "5"(低字节)和 "2"(高字节)),你先拿低字节(个位 / 小数位),再拿高字节(十位 / 百位)------ 必须按 "先低后高" 的顺序,拿反了就拼错数。
cpp
datl = ds18b20_read_byte();//读低字节(缓存第0字节)
dath = ds18b20_read_byte();//读高字节(缓存第1字节)
- 把两半读数拼起来(合并 16 位数据)
ath<<8:把高字节 "左移 8 位"→相当于把 "十位纸条" 挪到十位的位置(比如高字节是 0x02,左移 8 位变成 0x0200);
加低字节:把个位拼上去(比如低字节是 0x30,0x0200+0x30=0x0230,对应十进制 560);
👉 例子:25.5℃的高字节是 0x01、低字节是 0x90,合并后是 0x0190(十进制 400),后续换算就是 400×0.0625=25.5℃。
cpp
value = (dath<<8)+datl;//合并为16位补码(高字节左移8位+低字节)
- 把 "编码数" 换算成能看懂的℃数(解析正负温度)
拆成通俗的话讲:
(1)先判断温度是正还是负
value & 0xf800(1111 1000 0000 0000) == 0xf800:相当于看温度计编码的 "最高位标记"------ 如果标记是 "1",就是负温度;是 "0" 就是正温度。
👉 比如 - 5℃ 的编码是 0xffb0,和 0xf800 做 "与运算" 结果是 0xf800,判定为负温度。
(2)负温度换算(负数用"补码"表示)
① 取反加1:value = (~value)+1:把负温度的 "特殊编码" 还原成绝对值(比如 0xffb0→取反是 0x004f→加 1 是 0x0050,对应十进制 80)【负数的二进制取反加一即为其绝对值】;
② 乘以精度:temp = value*(-0.0625)(0.0625=1/16,传感器默认的最小精度)
③ 再加负号→80×(-0.0625)=-5℃。
(3)正温度换算
直接用合并后的数 ×0.0625→比如 400×0.0625=25.5℃。
cpp
// 第三步:解析正负温度
if((value&0xf800)==0xf800)//判断符号位:0xf800=11111000 00000000
{ //高5位全1,说明是负温度
value = (~value)+1; //补码还原:取反+1得到绝对值
temp = value*(-0.0625);//乘以精度(0.0625℃),加负号
}
else
{
temp = value * 0.0625;//正温度,直接计算
}
- 把换算好的温度返回(给数码管显示)
cpp
return temp;
注意
以上两个函数(ds18b20_read_temperture()、 ds18b20_start())的关系如下:
① 两者是 "先干活→再取结果" 的先后关系
start是 "让传感器开始测温度",测温函数是 "去拿传感器测好的温度结果",缺一不可;
必须先执行start,再执行测温函数 :就像不能没让厨师炒饭,就直接去端饭(要么端到空盘子,要么端到上一桌的剩饭);传感器没被start触发测温,就读数据会拿到旧温度 / 错误数据。
start负责 "让传感器干活",测温函数负责 "拿干活的结果" :start里的0x44是 "炒菜指令",测温函数里的0xbe是 "端饭指令",前者让传感器产生数据,后者读取数据,是完整流程的两个阶段。
start里的延时(750ms)是 "等炒饭熟":传感器测温需要时间(12 位分辨率下必须 750ms),start的延时就是等温度数据 "炒好",测温函数只能拿 "炒好的成品"。
② 指令看似重复(0xcc 出现两次),实则是 "不同通讯场景的必要复用"
0xcc是 "简化沟通的前置指令",0x44和0xbe是完全不同的核心指令(一个 "启动测温",一个 "读取结果"),毫无重复。

0xcc是 "每次和传感器说话前的'开场白'"------ 因为:
start是 "第一次通讯":你和传感器说 "要测温",先讲开场白(0xcc),再下达测温指令(0x44);- 测温函数里是 "第二次通讯":你和传感器说 "要数据",需要重新建立通讯(复位 + check),再讲开场白(0xcc),再下达读数据指令(0xbe);
这就像:
- 第一次找厨师:"不用报工号(0xcc),现在炒炒饭(0x44)";
- 第二次找厨师:"不用报工号(0xcc),把炒饭端过来(0xbe)";两次的 "不用报工号"(0xcc)是同一个开场白,但针对的是不同的核心指令,不是重复,而是单传感器场景下的 "通用简化操作"。
③ 关于重复
- 0xcc 出现两次:
0xcc 是 "通讯前置指令",两次通讯(启动测温、读数据)是独立的,每次都要发,就像每次找厨师说话都要先讲 "不用报工号",不是重复,是规则。
- start 和测温函数都有" 复位 + check "
复位 + check 是 "每次和传感器说话前的握手"------ 传感器可能在 start 后掉线(比如接线松),读数据前必须再握手,确认它还在线,避免发 0xbe 指令白发。
- "0x44 和 0xbe 功能不重复"
0x44 是 "让传感器生产数据",0xbe 是 "让传感器交出数据",一个是 "生产指令",一个是 "取货指令",是流程的两个环节,完全不重复。
主函数
主函数本质在于把 "传感器输出的浮点温度(如 25.5℃)",通过 "放大 10 倍转整数→拆分各位→转数码管显示密码" 的步骤,转换成数码管能显示的格式,并且通过定时读取避免频繁操作,最终实现温度的持续、稳定显示。
整个过程就像:你有一个 "专业温度计(DS18B20)",只能输出带小数点的数字(25.5),但家里的 "数码屏(数码管)" 只能显示整数和小数点,还得手动拆数字。你做的事:
- 先检查温度计能不能用(初始化);
- 每 50 秒读一次温度计(定时读取);
- 把 25.5 写成 255(放大 10 倍),方便拆成 0、2、5、5;
- 在数码屏上:负号位空着,百位显示 0,十位显示 2,个位显示 5.,小数位显示 5;
- 一直重复做这件事,数码屏就持续显示 25.5℃。
先初始化传感器确保能用,
再通过死循环持续做三件事:「定时读取温度(避免频繁操作)→ 把温度处理成数码管能 "看懂" 的格式 → 让数码管显示温度」
- 全局数组
gsmg_code
比如gsmg_code[5]=0x6D,数码管收到这个值,就知道该亮哪些段,显示出 "5"。
cpp
u8 gsmg_code[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
- main 函数里的变量(装数据的容器)

第一步:初始化传感器(程序上电先检查)
👉 对应场景:你拿出电子温度计,先按开机键,检查温度计能不能正常开机、传感器有没有反应 ------ 只有初始化通过(传感器在线),后续才能测温度。
cpp
ds18b20_init();//初始化 DS18B20
第二步:死循环(持续测温 + 显示,永不停止)
👉 对应场景:电子温度计开机后,一直显示温度,不会自动关机 ------ 死循环就是让单片机一直做 "读温度→处理→显示" 的动作。
cpp
i++;
if(i%50==0) //定时读取温度(避免频繁读,降低CPU占用)
{
temp_value = ds18b20_read_temperture()*10;
}
子步骤 1:定时读取温度(避免频繁操作)
👉 通俗拆解:
i++:每次循环,小本本记的次数 + 1(比如从 1→2→3...→50→51...);i%50==0:每记满 50 次,才读一次温度 ------ 就像你不用每 1 秒看一次温度计,每50 秒看一次就够,既省电又避免温度计反应不过来;temp_value = 温度×10:把浮点温度转成整数(比如 25.5→255、-5.0→-50)------51 单片机处理浮点数(带小数点的数)容易出错,转成整数后,拆分 "百位 / 十位 / 个位 / 小数位" 更简单,就像把 "25.5℃" 记成 "255 个 0.1℃",只用算整数就行。
cpp
i++;
if(i%50==0) //定时读取温度(避免频繁读,降低CPU占用)
{
temp_value = ds18b20_read_temperture()*10;
}
子步骤 2:处理负温度(显示负号)
👉 对应场景:
- 如果温度计显示 - 5.0℃:先把数字变成 50(绝对值),然后在数码管的 "负号位" 显示 "-";
- 如果是 25.5℃:负号位啥也不显示(全灭)。👉 关键:
0x40是负号的 "显示密码"------ 数码管只有 g 段亮,刚好是 "-" 的形状;0x00是所有段灭,相当于 "这个位置空着"。
cpp
if(temp_value<0)
{
temp_value =- temp_value; // 取绝对值(方便拆分数字)
temp_buf[0] = 0x40;// 负号的段选码(只有g段亮,显示"-")
}
else
temp_buf[0] = 0x00; // 正数:负号位全灭,不显示符号
子步骤 3:拆分温度各位,生成数码管显示指令
👉 通俗拆解(以 255 为例):
- 拆分逻辑:把 255 拆成 "0(百位)、2(十位)、5(个位)、5(小数位)";
- 加小数点:
|0x80是给个位加小数点 ------ 数码管的小数点对应 8 段的最高位(bit7),0x80 = 10000000B,和 "5 的显示密码" 做 "或运算",就像给 "5" 加个小数点,变成 "5."; - 最终
temp_buf数组:[0x00, 0x3F(0), 0x5B(2), 0xED(5.), 0x6D(5)]。
cpp
// 百位:255/1000=0 → 显示0
temp_buf[1] = gsmg_code[temp_value/1000];
// 十位:255%1000/100=2 → 显示2
temp_buf[2] = gsmg_code[temp_value%1000/100];
// 个位:255%1000/10=5 → 显示5,|0x80点亮小数点(变成"5.")
temp_buf[3] = gsmg_code[temp_value%1000/10]|0x80;
// 小数位:255%10=5 → 显示5
temp_buf[4] = gsmg_code[temp_value%1000%100%10];
子步骤 4:让数码管显示
👉 对应场景:把 "显示指令数组" 传给数码管,告诉它 "从第 4 个位置开始,依次显示:空、0、2、5.、5"------ 最终数码管显示 "025.5";如果是 - 5.0℃(temp_value=50),则显示 "-05.0"。
cpp
smg_display(temp_buf,4);
注意
- **为什么要 i%50==0?**不是固定 50,是为了 "软件定时"------51 单片机死循环执行极快(微秒级),如果每次都读温度,会频繁触发传感器的转换指令(转换需要 750ms),导致数据错误,50 次循环是工程上的 "折中值",可根据需求调整;
- **为什么要 ×10 放大?**避免浮点运算的精度误差 ------ 比如 25.5℃用浮点数存储可能变成 25.499999,×10 后变成 255(整数),拆分后完全准确;
- 为什么 | 0x80? 数码管的小数点是独立的段(DP 位),
0x80只点亮小数点,不影响数字显示,是 "给个位加小数点" 的标准操作; - **temp_buf [5] 的 5 个位置必须对应吗?**是的 ------ 数组位置和数码管的物理位置一一对应,顺序错了,数码管显示的数字就会乱(比如 25.5 变成 52.5)。