51单片机——DS18B02(三)

目录

实验设计

[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_tempdat的第 0、1、2... 位,对应显示到数码管的pos_temppos_temp+1...位。举例:pos=2pos_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 字节数据(模拟单总线读时序)。

①--③ 部分容易混淆,这里做个区分

分成测温前准备 进行测温 测温后 三个方面进行叙述

测温前准备

可以把主机(单片机)比作 "老师",传感器比作 "学生":

  1. 第一步:等学生 "举手回应"(传感器拉低 DQ)

    • 老师喊 "上课"(主机发 reset 复位)后,会盯着学生(检测 DQ 引脚);
    • 正常学生(传感器)会在 15~60μs 内举手(主动拉低 DQ 引脚);
    • check函数做的事:最多等 200μs(留余量),如果学生一直没举手(DQ 始终高电平),判定 "学生不在教室(传感器不存在)",返回 1;如果举手了,进入第二步。
  2. 第二步:等学生 "放下手"(传感器释放 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(给传感器回应的时间)
}

对应到 "喊店员" 的动作:

  1. DS18B20_PORT=0(拉低 DQ)→ 你提高嗓门喊:"店员!"主动发起 "呼叫",用低电平这个 "信号" 告诉传感器:"我要和你通讯了,别摸鱼了!"
  2. Delay10us(75)(拉低 750μs)→ 你持续喊够 1 秒(留足够时间)不是随口喊一声就停,而是喊够传感器能识别的时间(DS18B20 要求至少喊 480μs),确保它能听到,而不是把你的 "招呼" 当成干扰。
  3. DS18B20_PORT=1(释放 DQ)→ 你喊完,停下来不说话释放引脚(让上拉电阻把 DQ 拉回高电平),相当于告诉传感器:"我喊完了,该你回应了!"
  4. Delay10us(2)(等 20μs)→ 你稍等片刻,给店员反应时间传感器听到招呼后,需要一点时间从 "摸鱼状态" 切换到 "应答状态",这 20μs 就是留给它的 "反应缓冲",避免你刚喊完就急着等回应,没给它准备时间。

此外,

  1. 拉低 750μs:DS18B20 要求主机拉低 DQ 至少 480μs 才能识别 "复位信号",750μs 是工程上的常用值(留余量);
  2. 释放引脚后等 20μs:传感器需要 15~60μs 的时间反应,这里等 20μs 是 "提前就位",准备检测应答
  3. 释放 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(初始化函数)就是程序上电后对传感器做的「首次全面检测」,核心就是验证传感器物理层面在线 + 通讯层面正常

  1. "物理层面正常"------ 传感器没坏、接线对

init函数返回 0 的前提是:

传感器硬件没损坏(比如芯片没烧、引脚没断);

接线正确(DQ 引脚接对、上拉电阻(4.7KΩ)接好、电源 / 地没接反) ;如果传感器物理层面有问题,check函数会检测不到应答,init返回 1,相当于 "这台设备根本没法用"。

  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:把两半读数拼起来的 "完整编码数"。

第二步:核心操作拆解(和店员的互动流程)

  1. 让店员先去测温度(启动转换)

对应场景:你对店员说 "帮我测个体温"→店员拿起专用温度计,开始测温(需要等 750ms 才能测完)。

👉 关键:这一步是 "让传感器干活",不是上来就拿结果 ------ 如果跳过这步,直接要数据,拿到的是上一次的旧温度,甚至错数据。

cpp 复制代码
ds18b20_start();		  //第一步:启动温度转换
  1. 重新喊店员、确认它在(复位 + 检测应答)

对应场景:店员测完体温后,你怕它低头摸鱼没听见,再喊一声 "店员!"(reset),确认它回应 "在呢!"(check)------ 确保它能听你接下来的 "要读数" 指令。

👉 为什么要重新复位?启动转换(ds18b20_start())是 "让店员干活",干活后需要重新建立通讯,才能要结果,就像店员干完活可能走神,得再喊一声。

cpp 复制代码
ds18b20_reset();		  //第二步:重新复位,建立通讯
ds18b20_check();		  //检测应答
  1. 告诉店员 "不用报名字,直接给我读数"(发命令)

0xcc(SKIP ROM):店里只有这一个店员,不用喊名字(跳过 "点名"),直接说指令;如果有多个店员(多传感器),就得喊名字(0x55 + 地址);

0xbe(读存储器):你对店员说 "把刚测的体温数给我!"→店员听到后,准备把温度计的读数拆成两半告诉你。

cpp 复制代码
ds18b20_write_byte(0xcc); //SKIP ROM命令
ds18b20_write_byte(0xbe); //读存储器命令(0xBE)

回顾:

0xCC:单传感器时用,跳过地址;多传感器时不能用(会冲突);

0x44:启动测温,发完必须延时(12 位分辨率等 750ms)

0xBE:读温度数据,发完后先读低字节(第 0 字节),再读高字节(第 1 字节)

  1. 拿店员给的 "两半读数"(读高低字节)

对应场景:店员的温度计读数是 "拆成两半写的"(比如 25.5℃拆成 "5"(低字节)和 "2"(高字节)),你先拿低字节(个位 / 小数位),再拿高字节(十位 / 百位)------ 必须按 "先低后高" 的顺序,拿反了就拼错数。

cpp 复制代码
datl = ds18b20_read_byte();//读低字节(缓存第0字节)
dath = ds18b20_read_byte();//读高字节(缓存第1字节)
  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. 把 "编码数" 换算成能看懂的℃数(解析正负温度)

拆成通俗的话讲:

(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;//正温度,直接计算
}
  1. 把换算好的温度返回(给数码管显示)
cpp 复制代码
return temp;
注意

以上两个函数(ds18b20_read_temperture()、 ds18b20_start())的关系如下:

① 两者是 "先干活→再取结果" 的先后关系

start是 "让传感器开始测温度",测温函数是 "去拿传感器测好的温度结果",缺一不可;

必须先执行start,再执行测温函数 :就像不能没让厨师炒饭,就直接去端饭(要么端到空盘子,要么端到上一桌的剩饭);传感器没被start触发测温,就读数据会拿到旧温度 / 错误数据。

start负责 "让传感器干活",测温函数负责 "拿干活的结果"start里的0x44是 "炒菜指令",测温函数里的0xbe是 "端饭指令",前者让传感器产生数据,后者读取数据,是完整流程的两个阶段。

start里的延时(750ms)是 "等炒饭熟":传感器测温需要时间(12 位分辨率下必须 750ms),start的延时就是等温度数据 "炒好",测温函数只能拿 "炒好的成品"。

② 指令看似重复(0xcc 出现两次),实则是 "不同通讯场景的必要复用"

0xcc是 "简化沟通的前置指令",0x440xbe是完全不同的核心指令(一个 "启动测温",一个 "读取结果"),毫无重复。

0xcc是 "每次和传感器说话前的'开场白'"------ 因为:

  1. start是 "第一次通讯":你和传感器说 "要测温",先讲开场白(0xcc),再下达测温指令(0x44);
  2. 测温函数里是 "第二次通讯":你和传感器说 "要数据",需要重新建立通讯(复位 + check),再讲开场白(0xcc),再下达读数据指令(0xbe);

这就像:

  • 第一次找厨师:"不用报工号(0xcc),现在炒炒饭(0x44)";
  • 第二次找厨师:"不用报工号(0xcc),把炒饭端过来(0xbe)";两次的 "不用报工号"(0xcc)是同一个开场白,但针对的是不同的核心指令,不是重复,而是单传感器场景下的 "通用简化操作"。

③ 关于重复

  1. 0xcc 出现两次:

0xcc 是 "通讯前置指令",两次通讯(启动测温、读数据)是独立的,每次都要发,就像每次找厨师说话都要先讲 "不用报工号",不是重复,是规则。

  1. start 和测温函数都有" 复位 + check "

复位 + check 是 "每次和传感器说话前的握手"------ 传感器可能在 start 后掉线(比如接线松),读数据前必须再握手,确认它还在线,避免发 0xbe 指令白发。

  1. "0x44 和 0xbe 功能不重复"

0x44 是 "让传感器生产数据",0xbe 是 "让传感器交出数据",一个是 "生产指令",一个是 "取货指令",是流程的两个环节,完全不重复。

主函数

主函数本质在于把 "传感器输出的浮点温度(如 25.5℃)",通过 "放大 10 倍转整数→拆分各位→转数码管显示密码" 的步骤,转换成数码管能显示的格式,并且通过定时读取避免频繁操作,最终实现温度的持续、稳定显示

整个过程就像:你有一个 "专业温度计(DS18B20)",只能输出带小数点的数字(25.5),但家里的 "数码屏(数码管)" 只能显示整数和小数点,还得手动拆数字。你做的事:

  1. 先检查温度计能不能用(初始化);
  2. 每 50 秒读一次温度计(定时读取);
  3. 把 25.5 写成 255(放大 10 倍),方便拆成 0、2、5、5;
  4. 在数码屏上:负号位空着,百位显示 0,十位显示 2,个位显示 5.,小数位显示 5;
  5. 一直重复做这件事,数码屏就持续显示 25.5℃。

先初始化传感器确保能用,

再通过死循环持续做三件事:「定时读取温度(避免频繁操作)→ 把温度处理成数码管能 "看懂" 的格式 → 让数码管显示温度」

  1. 全局数组 gsmg_code

比如gsmg_code[5]=0x6D,数码管收到这个值,就知道该亮哪些段,显示出 "5"。

cpp 复制代码
u8 gsmg_code[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
  1. main 函数里的变量(装数据的容器)

第一步:初始化传感器(程序上电先检查)

👉 对应场景:你拿出电子温度计,先按开机键,检查温度计能不能正常开机、传感器有没有反应 ------ 只有初始化通过(传感器在线),后续才能测温度。

cpp 复制代码
ds18b20_init();//初始化 DS18B20

第二步:死循环(持续测温 + 显示,永不停止)

👉 对应场景:电子温度计开机后,一直显示温度,不会自动关机 ------ 死循环就是让单片机一直做 "读温度→处理→显示" 的动作。

cpp 复制代码
i++;
if(i%50==0)		//定时读取温度(避免频繁读,降低CPU占用)
{
    temp_value = ds18b20_read_temperture()*10;
}

子步骤 1:定时读取温度(避免频繁操作)

👉 通俗拆解:

  1. i++:每次循环,小本本记的次数 + 1(比如从 1→2→3...→50→51...);
  2. i%50==0:每记满 50 次,才读一次温度 ------ 就像你不用每 1 秒看一次温度计,每50 秒看一次就够,既省电又避免温度计反应不过来
  3. 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);
注意
  1. **为什么要 i%50==0?**不是固定 50,是为了 "软件定时"------51 单片机死循环执行极快(微秒级),如果每次都读温度,会频繁触发传感器的转换指令(转换需要 750ms),导致数据错误,50 次循环是工程上的 "折中值",可根据需求调整;
  2. **为什么要 ×10 放大?**避免浮点运算的精度误差 ------ 比如 25.5℃用浮点数存储可能变成 25.499999,×10 后变成 255(整数),拆分后完全准确;
  3. 为什么 | 0x80? 数码管的小数点是独立的段(DP 位),0x80只点亮小数点,不影响数字显示,是 "给个位加小数点" 的标准操作;
  4. **temp_buf [5] 的 5 个位置必须对应吗?**是的 ------ 数组位置和数码管的物理位置一一对应,顺序错了,数码管显示的数字就会乱(比如 25.5 变成 52.5)。
相关推荐
BMS小旭2 分钟前
CubeMx-GPIO学习
单片机·学习
清风6666662 小时前
基于单片机的PID调节脉动真空灭菌器上位机远程监控设计
数据库·单片机·毕业设计·nosql·课程设计·期末大作业
polarislove02143 小时前
9.6 [定时器]超声波测距实验-嵌入式铁头山羊STM32笔记
笔记·stm32·嵌入式硬件
一路往蓝-Anbo4 小时前
C语言从句柄到对象 (六) —— 继承与 HAL:父类指针访问子类数据
c语言·开发语言·stm32·嵌入式硬件·物联网
一路往蓝-Anbo6 小时前
C语言从句柄到对象 (七) —— 给对象加把锁:RTOS 环境下的并发安全
java·c语言·开发语言·stm32·单片机·嵌入式硬件·算法
一路往蓝-Anbo7 小时前
C语言从句柄到对象 (八) —— 当对象会说话:观察者模式与事件链表
c语言·开发语言·数据结构·stm32·单片机·观察者模式·链表
polarislove02147 小时前
9.5 [定时器]输入捕获-嵌入式铁头山羊STM32笔记
笔记·stm32·嵌入式硬件
国科安芯7 小时前
核工业检测系统通信链路的国产化元器件安全等级评估
运维·网络·人工智能·单片机·嵌入式硬件·安全·安全性测试
IT阳晨。9 小时前
【STM32】采集温湿度数据上传至OneNET平台项目
stm32·单片机·嵌入式硬件
FL16238631299 小时前
flash_attn windows whl下载安装教程
windows·stm32·单片机