51单片机学习笔记14-DS18B20(单总线)

目录

[14.1 DS18B20基础知识](#14.1 DS18B20基础知识)

[14.1.1 器件特点](#14.1.1 器件特点)

[14.1.2 引脚定义](#14.1.2 引脚定义)

[14.1.3 关键硬件要求](#14.1.3 关键硬件要求)

[14.2 单总线(One-Wire)](#14.2 单总线(One-Wire))

[14.2.1 单总线介绍](#14.2.1 单总线介绍)

[14.2.2 单总线时序结构](#14.2.2 单总线时序结构)

[⭐初始化(复位 + 应答)时序](#⭐初始化(复位 + 应答)时序)

⭐发送一位

⭐接收一位

⭐发送一个字节

⭐接收一个字节​编辑

✅OneWire.c

✅OneWire.h

[14.3 DS18B20操作流程](#14.3 DS18B20操作流程)

✅DS18B20.c

✅DS18B20.h

[14.4 项目示例1 :DS18B20温度传感器](#14.4 项目示例1 :DS18B20温度传感器)

✅项目功能:

✅项目架构:

✅main.c主函数

[14.4 项目示例2 :DS18B20温度报警器](#14.4 项目示例2 :DS18B20温度报警器)

✅项目功能:

✅项目架构:

✅main.c主函数


14.1 DS18B20基础知识

14.1.1 器件特点

  1. 单总线通信 :只需要一根 IO 口数据线(DQ) 即可完成与 51 单片机的双向通信,无需 I2C/SPI 等总线,接线极简;
  2. 数字输出 :直接输出数字温度值,无需 AD 转换,单片机直接读取,精度高、无误差;
  3. 无需外部元件:内置振荡器和温度传感器,只需要接电源、地、数据线即可工作;
  4. 测温范围 :-55℃ ~ +125℃,满足绝大多数场景使用;
  5. 精度可选:默认 12 位精度(测温精度 0.0625℃),也可配置 9/10/11 位精度;
  6. 寄生供电 / 外部供电 :两种供电方式,51 单片机中优先用外部供电,稳定不易出错。

14.1.2 引脚定义

引脚1 (GND) :接地引脚
引脚2 (DQ) :单总线数据引脚,接51单片机任意一个IO口(如P3_7)
引脚3 (VCC) :电源引脚,接5V/3.3V(51单片机直接接5V即可)

14.1.3 关键硬件要求

DS18B20 的 DQ 引脚(数据线)必须串联一个 4.7KΩ 的上拉电阻,一端接 DQ 引脚,一端接 5V 电源;

❗ 不接 4.7K 上拉电阻 → 通信失败,永远读不到温度值!这是新手 99% 的踩坑点。

14.2 单总线(One-Wire)

14.2.1 单总线介绍


DS18B20 是 单总线协议 的代表器件,所谓单总线: 一根数据线,既传输时钟信号,又传输数据信号,双向通信
单总线没有时钟线,所有的通信时序、读写操作,都由 单片机严格的延时时序来控制,这是驱动 DS18B20 的核心!
✅ 单总线的核心特点

  1. 所有操作基于 精准的微秒级延时,时序错一点就通信失败;
  2. 通信的发起者是单片机(主机),DS18B20 是从机,完全被动响应;
  3. DS18B20 的所有操作,都基于 3 个最基础的时序函数,所有功能都是这 3 个函数的组合:
  • ① 复位 + 应答时序(主机复位 DS18B20,从机给出应答)
  • ② 写一个字节时序(主机给 DS18B20 发送指令)
  • ③ 读一个字节时序(主机从 DS18B20 读取温度数据)

14.2.2 单总线时序结构

⭐初始化 (复位 + 应答)时序


标准时序步骤(官方规范)

关键判定与易错点

  • 应答位AckBit=0 → 通信成功(从机存在);AckBit=1 → 失败(无器件 / 上拉电阻缺失 / 时序错);
  • 易错点:步骤 2 拉低时长不足 480us → 从机无法复位;步骤 3 等待时间过长(>120us)→ 错过应答窗口,误判为无应答。
cpp 复制代码
/**
@brief  单总线初始化函数(核心:复位+应答时序)
@param  无
@retval 应答位:0=有应答(从机存在,通信成功),1=无应答(从机未响应,通信失败)
@note   所有单总线通信的前置操作,必须先初始化成功才能收发数据;
    延时基于11.0592MHz晶振,循环次数对应精准微秒级延时,不可随意修改
*/
unsigned char OneWire_Init(void)
{
    unsigned char i;          // 循环变量,用于微秒级延时计数
    unsigned char AckBit;     // 存储从机应答位,0=应答成功,1=无应答
    OneWire_DQ = 1;           // 第一步:释放总线(先拉至高电平,保证复位前总线状态稳定)
    OneWire_DQ = 0;           // 第二步:主机拉低总线,发送复位信号(要求拉低≥480us)
    i = 247;while (--i);      // 延时约500us(满足复位拉低时长要求,核心时序)
    OneWire_DQ = 1;           // 第三步:主机释放总线,等待从机应答(总线由上拉电阻拉至高电平)
    i = 32;while (--i);       // 延时约70us(等待从机拉低总线产生应答,核心时序)
    AckBit = OneWire_DQ;      // 第四步:读取总线电平,获取应答位(0=从机拉低应答,1=无应答)
    i = 247;while (--i);      // 第五步:延时约500us,等待从机释放总线(完成复位+应答流程)
    return AckBit;            // 返回应答状态,供上层函数判断通信是否成功

}

⭐发送一位


功能目的
主机向从机发送 1 位数据(0/1),从机在主机定义的「位窗口」内读取电平。
标准时序步骤


关键规则

  • 发送位0:步骤 2 全程保持 DQ=0;发送位1:步骤 2 保持 DQ=1;
  • 从机读取窗口是「步骤 2 的 45us」,主机必须保证此窗口内电平稳定,否则数据读错。
cpp 复制代码
/**
  * @brief  单总线发送单个位(单总线最基础的写操作)
  * @param  Bit 要发送的位,范围:0/1(0=低电平,1=高电平)
  * @retval 无
  * @note   单总线无时钟线,靠精准延时定义「位写入窗口」,时序错则数据传输错误
  */
void OneWire_SendBit(unsigned char Bit)
{ 
    unsigned char i;          // 延时计数变量
    
    OneWire_DQ = 0;           // 第一步:主机拉低总线,启动位发送时序(拉低≥1us)
    i = 4;while (--i);        // 延时约10us(满足位写入的最小拉低时长)
    OneWire_DQ = Bit;         // 第二步:输出要发送的位(0/1),从机在此窗口读取电平
    i = 24;while (--i);       // 延时约50us(维持位电平,保证从机稳定读取)
    OneWire_DQ = 1;           // 第三步:释放总线,完成一位发送
}

⭐接收一位


功能目的
主机触发从机输出 1 位数据,主机在「读取窗口」内捕获从机的电平。
标准时序步骤


关键规则

  • 主机拉低总线是「读位触发信号」,从机仅在主机拉低后才会输出数据;
  • 读取窗口是「释放总线后 5~15us」,提前 / 延后读都会捕获到错误电平。
cpp 复制代码
/**
  * @brief  单总线接收单个位(单总线最基础的读操作)
  * @param  无
  * @retval 读取到的位,范围:0/1(0=低电平,1=高电平)
  * @note   主机主动拉低总线触发读时序,从机在窗口期输出电平,主机读取
  */
unsigned char OneWire_ReceiveBit(void)
{
    unsigned char Bit;        // 存储读取到的位
    unsigned char i;          // 延时计数变量
    
    OneWire_DQ = 0;           // 第一步:主机拉低总线,启动位读取时序(拉低≥1us)
    i = 2;while (--i);        // 延时约5us(触发从机输出数据位)
    OneWire_DQ = 1;           // 第二步:主机释放总线,准备读取从机输出的电平
    i = 2;while (--i);        // 延时约5us(等待从机电平稳定,核心窗口期)
    Bit = OneWire_DQ;         // 第三步:读取总线电平,获取从机发送的位
    i = 24;while (--i);       // 延时约50us,完成一位读取时序
    return Bit;               // 返回读取到的位
}

⭐发送一个字节


功能目的
单次传输 8 位数据(0x00~0xFF),是单总线与从机交互的实际数据单元(如发送 DS18B20 指令、读取温度数据)。
发送字节时序(主机→从机)

  • 核心规则:低位在前(LSB),即先发送 bit0,再发送 bit1... 最后发送 bit7;
  • 实现逻辑:循环 8 次调用「发送位」函数,每次提取字节的第 i 位(Byte&(0x01<);
  • 代码对应:for(i=0;i<8;i++){OneWire_SendBit(Byte&(0x01<<i));}。
cpp 复制代码
/**
  * @brief  单总线发送一个字节(8位)
  * @param  Byte 要发送的字节,范围:0x00~0xFF
  * @retval 无
  * @note   基于「发送单个位」函数实现,按**低位在前(LSB)** 顺序发送8个位
  */
void OneWire_SendByte(unsigned char Byte)
{ 
    unsigned char i;          // 循环变量,遍历8个位
    for(i=0;i<8;i++)          // 逐位发送,共8次
    {
        // 按位发送:Byte&(0x01<<i) 提取第i位(0~7位),低位先发送
        OneWire_SendBit(Byte&(0x01<<i));
    }
}

⭐接收一个字节

接收字节时序(从机→主机)

  • 核心规则:低位在前(LSB),即先接收 bit0,再接收 bit1... 最后接收 bit7;
  • 实现逻辑:循环 8 次调用「接收位」函数,若读取到 1 则将其写入字节的第 i 位(Byte|=(0x01<);
  • 代码对应:if(OneWire_ReceiveBit()){Byte|=((0x01<<i));}。
cpp 复制代码
/**
  * @brief  单总线接收一个字节(8位)
  * @param  无
  * @retval 读取到的字节,范围:0x00~0xFF
  * @note   基于「接收单个位」函数实现,按**低位在前(LSB)** 顺序接收8个位
  */
unsigned char OneWire_ReceiveByte(void)
{
    unsigned char Byte=0x00;  // 存储最终读取的字节,初始化为0
    unsigned char i;          // 循环变量,遍历8个位
    for(i=0;i<8;i++)          // 逐位接收,共8次
    {
        // 若读取到的位为1,将其写入Byte的第i位;为0则不操作(初始为0)
        if(OneWire_ReceiveBit())
        {
            Byte |= ((0x01<<i));
        }
    }
    return Byte;              // 返回拼接后的完整字节
}

✅OneWire.c

cpp 复制代码
#include <REGX52.H>    // 51单片机寄存器定义头文件,必须包含

// 单总线数据引脚定义:DQ引脚接P3_7,可修改为任意IO口(需同步调整硬件接线)
sbit OneWire_DQ = P3^7;

/**
  * @brief  单总线初始化函数(核心:复位+应答时序)
  * @param  无
  * @retval 应答位:0=有应答(从机存在,通信成功),1=无应答(从机未响应,通信失败)
  * @note   所有单总线通信的前置操作,必须先初始化成功才能收发数据;
  *         延时基于11.0592MHz晶振,循环次数对应精准微秒级延时,不可随意修改
  */
unsigned char OneWire_Init(void)
{
    unsigned char i;          // 循环变量,用于微秒级延时计数
    unsigned char AckBit;     // 存储从机应答位,0=应答成功,1=无应答
    
    OneWire_DQ = 1;           // 第一步:释放总线(先拉至高电平,保证复位前总线状态稳定)
    OneWire_DQ = 0;           // 第二步:主机拉低总线,发送复位信号(要求拉低≥480us)
    i = 247;while (--i);      // 延时约500us(满足复位拉低时长要求,核心时序)
    OneWire_DQ = 1;           // 第三步:主机释放总线,等待从机应答(总线由上拉电阻拉至高电平)
    i = 32;while (--i);       // 延时约70us(等待从机拉低总线产生应答,核心时序)
    AckBit = OneWire_DQ;      // 第四步:读取总线电平,获取应答位(0=从机拉低应答,1=无应答)
    i = 247;while (--i);      // 第五步:延时约500us,等待从机释放总线(完成复位+应答流程)
    return AckBit;            // 返回应答状态,供上层函数判断通信是否成功
}

/**
  * @brief  单总线发送单个位(单总线最基础的写操作)
  * @param  Bit 要发送的位,范围:0/1(0=低电平,1=高电平)
  * @retval 无
  * @note   单总线无时钟线,靠精准延时定义「位写入窗口」,时序错则数据传输错误
  */
void OneWire_SendBit(unsigned char Bit)
{ 
    unsigned char i;          // 延时计数变量
    
    OneWire_DQ = 0;           // 第一步:主机拉低总线,启动位发送时序(拉低≥1us)
    i = 4;while (--i);        // 延时约10us(满足位写入的最小拉低时长)
    OneWire_DQ = Bit;         // 第二步:输出要发送的位(0/1),从机在此窗口读取电平
    i = 24;while (--i);       // 延时约50us(维持位电平,保证从机稳定读取)
    OneWire_DQ = 1;           // 第三步:释放总线,完成一位发送
}

/**
  * @brief  单总线接收单个位(单总线最基础的读操作)
  * @param  无
  * @retval 读取到的位,范围:0/1(0=低电平,1=高电平)
  * @note   主机主动拉低总线触发读时序,从机在窗口期输出电平,主机读取
  */
unsigned char OneWire_ReceiveBit(void)
{
    unsigned char Bit;        // 存储读取到的位
    unsigned char i;          // 延时计数变量
    
    OneWire_DQ = 0;           // 第一步:主机拉低总线,启动位读取时序(拉低≥1us)
    i = 2;while (--i);        // 延时约5us(触发从机输出数据位)
    OneWire_DQ = 1;           // 第二步:主机释放总线,准备读取从机输出的电平
    i = 2;while (--i);        // 延时约5us(等待从机电平稳定,核心窗口期)
    Bit = OneWire_DQ;         // 第三步:读取总线电平,获取从机发送的位
    i = 24;while (--i);       // 延时约50us,完成一位读取时序
    return Bit;               // 返回读取到的位
}

/**
  * @brief  单总线发送一个字节(8位)
  * @param  Byte 要发送的字节,范围:0x00~0xFF
  * @retval 无
  * @note   基于「发送单个位」函数实现,按**低位在前(LSB)** 顺序发送8个位
  */
void OneWire_SendByte(unsigned char Byte)
{ 
    unsigned char i;          // 循环变量,遍历8个位
    for(i=0;i<8;i++)          // 逐位发送,共8次
    {
        // 按位发送:Byte&(0x01<<i) 提取第i位(0~7位),低位先发送
        OneWire_SendBit(Byte&(0x01<<i));
    }
}

/**
  * @brief  单总线接收一个字节(8位)
  * @param  无
  * @retval 读取到的字节,范围:0x00~0xFF
  * @note   基于「接收单个位」函数实现,按**低位在前(LSB)** 顺序接收8个位
  */
unsigned char OneWire_ReceiveByte(void)
{
    unsigned char Byte=0x00;  // 存储最终读取的字节,初始化为0
    unsigned char i;          // 循环变量,遍历8个位
    for(i=0;i<8;i++)          // 逐位接收,共8次
    {
        // 若读取到的位为1,将其写入Byte的第i位;为0则不操作(初始为0)
        if(OneWire_ReceiveBit())
        {
            Byte |= ((0x01<<i));
        }
    }
    return Byte;              // 返回拼接后的完整字节
}

✅OneWire.h

cpp 复制代码
#ifndef _ONEWIRE_H_
#define _ONEWIRE_H_

unsigned char OneWire_Init(void);
void OneWire_SendBit(unsigned char Bit);
unsigned char OneWire_ReceiveBit(void);
void OneWire_SendByte(unsigned char Byte);
unsigned char OneWire_ReceiveByte(void);

#endif

14.3 DS18B20操作流程


✅DS18B20.c

cpp 复制代码
#include <REGX52.H>        // 51单片机寄存器定义头文件,提供IO口、寄存器等底层定义
#include "OneWire.h"       // 引入单总线底层驱动头文件,包含OneWire_Init、OneWire_SendByte、OneWire_ReceiveByte等核心函数

// DS18B20单总线指令宏定义(固定指令码,提高代码可读性,避免硬编码)
#define  DS18B20_SKIP_ROM         0xCC    // 跳过ROM指令:单总线仅接1个DS18B20时使用,跳过ROM匹配流程
#define  DS18B20_CONVERT_T        0x44    // 温度转换指令:命令DS18B20启动温度采集并转换
#define  DS18B20_READ_SCRATCHPAD  0xBE    // 读暂存器指令:读取DS18B20内部暂存器中的温度数据

/**
  * @brief  启动DS18B20温度转换
  * @param  无
  * @retval 无
  * @note   调用此函数后,DS18B20进入温度转换状态,12位精度下需等待约100ms转换完成;
  *         此函数仅发送转换指令,不等待转换完成,需在读取温度前确保转换完成
  */
void DS18B20_ConvertT(void)
{
	OneWire_Init();                          // 单总线初始化(复位+应答):唤醒DS18B20,确认通信链路正常
	OneWire_SendByte(DS18B20_SKIP_ROM);      // 发送跳过ROM指令:单机场景无需匹配ROM地址,节省通信时间
	OneWire_SendByte(DS18B20_CONVERT_T);     // 发送温度转换指令:触发DS18B20开始采集并转换温度
}

/**
  * @brief  读取DS18B20转换后的温度值
  * @param  无
  * @retval float - 实际温度值(如25.0℃、-5.5℃),12位精度分辨率0.0625℃
  * @note   调用此函数前,必须先调用DS18B20_ConvertT()并等待至少100ms,否则读取到的是旧数据;
  *         温度数据为16位补码,低字节(TLSB)先读,高字节(TMSB)后读,顺序不可反
  */
float DS18B20_ReadT(void)
{
    unsigned char TLSB,TMSB;  // 定义变量存储温度字节:TLSB=温度低字节,TMSB=温度高字节
    int Temp;                 // 存储拼接后的16位原始温度值(补码形式)
    float T;                  // 存储最终换算后的实际温度值(浮点型)
	
    OneWire_Init();                          // 单总线初始化:重新建立通信,准备读取数据
    OneWire_SendByte(DS18B20_SKIP_ROM);      // 发送跳过ROM指令:单机场景跳过ROM匹配
    OneWire_SendByte(DS18B20_READ_SCRATCHPAD); // 发送读暂存器指令:命令DS18B20输出暂存器中的温度数据
	
	TLSB=OneWire_ReceiveByte();              // 读取温度低字节(包含温度整数低位+小数部分)
	TMSB=OneWire_ReceiveByte();              // 读取温度高字节(包含温度符号位+整数高位)
	
	Temp=(TMSB<<8)|TLSB;                     // 拼接高低字节为16位整数:高字节左移8位 + 低字节
	T=Temp/16.0;                             // 换算为实际温度:12位精度下,原始值×0.0625 = 原始值/16.0
    return T;                                // 返回最终的浮点型温度值
}

✅DS18B20.h

cpp 复制代码
#ifndef _DS18B20_H_
#define _DS18B20_H_

void DS18B20_ConvertT(void);
float DS18B20_ReadT(void);

#endif

14.4 项目示例1 :DS18B20温度传感器

项目功能:

51 单片机驱动 DS18B20 温度传感器,并通过 LCD1602 实时显示温度值

✅项目架构:

main.c主函数

cpp 复制代码
#include <REGX52.H>        // 51单片机寄存器定义头文件,提供IO口、寄存器等底层硬件定义
#include "LCD1602.h"       // 引入LCD1602液晶显示屏驱动头文件,包含LCD_Init、LCD_ShowString等显示函数
#include "OneWire.h"       // 引入单总线底层驱动头文件,为DS18B20提供通信时序支持
#include "DS18B20.h"       // 引入DS18B20温度传感器驱动头文件,包含DS18B20_ConvertT、DS18B20_ReadT等函数

float T;                   // 全局变量:存储DS18B20读取到的实际温度值(浮点型,支持正负和小数)

void main()
{
	LCD_Init();                          // 初始化LCD1602显示屏,配置显示模式、端口等,为后续显示做准备
	LCD_ShowString(1,1,"Temperature:");  // 在LCD1602第1行第1列显示字符串"Temperature:",作为温度显示的标题
	
	while(1)                             // 死循环:持续采集并显示温度
	{
		DS18B20_ConvertT();              // 触发DS18B20温度转换:发送指令让传感器开始采集并转换温度(12位精度需约100ms完成)
		T=DS18B20_ReadT();               // 读取转换后的温度值:从DS18B20暂存器读取数据并换算为浮点型温度
		
		// 处理温度正负号显示
		if(T<0)                          // 若温度为负数
		{
			LCD_ShowChar(2,1,'-');       // 在LCD第2行第1列显示负号"-"
			T=-T;                        // 将温度取绝对值,方便后续拆分整数和小数部分显示
		}
		else                             // 若温度为正数/零
		{
			LCD_ShowChar(2,1,'+');       // 在LCD第2行第1列显示正号"+"
		}
		
		LCD_ShowNum(2,2,T,3);            // 在LCD第2行第2列显示温度整数部分,占3位(如25℃显示"025",125℃显示"125")
		LCD_ShowChar(2,5,'.');           // 在LCD第2行第5列显示小数点".",分隔整数和小数部分
		// 显示温度小数部分(4位):
		// 1. (unsigned long)T*10000:将浮点温度放大10000倍并转为无符号长整型(避免浮点运算误差)
		// 2. %10000:取后4位,得到四位小数部分(如25.125℃ → 251250 → 1250)
		// 3. LCD_ShowNum:在第2行第6列显示这4位小数
		LCD_ShowNum(2,6,(unsigned long)T*10000%10000,4); 
	}
}
  • 温度显示格式为「符号位 + 3 位整数 +.+4 位小数」,覆盖 DS18B20 的测温范围(-55℃~+125℃)和 12 位精度;
  • 浮点温度通过 "放大取整 + 取模" 拆分小数部分,避免直接浮点运算的显示误差,保证小数位显示准确。

14.4 项目示例2 :DS18B20温度报警器

项目功能:

  • 温度采集:通过 DS18B20 单总线温度传感器采集环境温度,支持 - 55℃~+125℃测温范围,LCD 显示「符号 + 3 位整数 + 2 位小数」格式的实时温度;
  • 人机交互:通过 4 个按键实现温度上限 (THigh)/ 下限 (TLow) 的加减调节,LCD 实时更新阈值;
  • 掉电保存:将温度阈值写入 AT24C02 EEPROM,上电自动读取,避免掉电丢失设置;
  • 非阻塞设计:定时器 0 每 20ms 中断扫描按键,主循环无延时阻塞,保证温度采集和显示的流畅性;
  • 超限提示:实时判断当前温度是否超出上下限,LCD 显示 "OV:H"(超上限)/"OV:L"(超下限),正常则清空提示。

✅项目架构:

✅main.c主函数

cpp 复制代码
#include <REGX52.H>        // 51单片机寄存器定义头文件,提供IO口、中断等底层硬件定义
#include "LCD1602.h"       // LCD1602液晶驱动头文件,包含初始化、字符/数字显示等函数
#include "DS18B20.h"       // DS18B20温度传感器驱动头文件,包含温度转换、读取函数
#include "Key.h"           // 按键驱动头文件,包含按键读取(Key())和扫描(Key_Loop())函数
#include "Timer0.h"        // 定时器0驱动头文件,包含定时器初始化函数
#include "AT24C02.h"       // AT24C02 EEPROM驱动头文件,包含字节读写函数(掉电保存阈值)
#include "Delay.h"         // 延时函数头文件,用于硬件操作的短延时(如AT24C02写后延时)

float T, TShow;            // 全局温度变量:T=DS18B20读取的原始温度(含正负);TShow=显示用温度(绝对值)
char TLow, THigh;          // 温度阈值变量:TLow=温度下限,THigh=温度上限(char覆盖-55~125℃范围)
unsigned char KeyNum;      // 按键编号变量:存储读取到的按键值(1/2/3/4分别对应上下限加减)

void main()
{
	DS18B20_ConvertT();    // 上电后先触发一次DS18B20温度转换,避免首次读取到无效值
	Delay(1000);           // 延时1秒,确保温度转换完成(12位精度需约100ms,此处冗余延时更稳定)
	
	LCD_Init();            // 初始化LCD1602显示屏,配置显示模式和IO口
	Timer0Init();          // 初始化定时器0,用于非阻塞式按键扫描(避免按键扫描阻塞主循环)
	
	// 从AT24C02读取掉电保存的温度阈值:地址0存上限(THigh),地址1存下限(TLow)
	THigh = AT24C02_ReadByte(0);
	TLow = AT24C02_ReadByte(1);
	
	// 阈值合法性检查:防止非法值(如上限>125/下限<-55/上限≤下限),重置为默认值
	if(THigh>125 || TLow<-55 || THigh<=TLow)
	{
		THigh=25;  // 默认温度上限:25℃
		TLow=15;   // 默认温度下限:15℃
	}
	
	// LCD固定字符串显示:搭建界面框架
	LCD_ShowString(1,1,"T:");        // 第1行第1列显示"T:",标识当前温度
	LCD_ShowString(2,1,"TH:");       // 第2行第1列显示"TH:",标识温度上限
	LCD_ShowString(2,9,"TL:");       // 第2行第9列显示"TL:",标识温度下限
	
	while(1)                         // 死循环:持续采集温度、处理按键、更新显示
	{
		KeyNum = Key();              // 读取按键值(0=无按键,1/2/3/4=对应按键按下)
		
		/* ========== 温度读取及显示模块 ========== */
		DS18B20_ConvertT();          // 触发DS18B20温度转换(每次读取前需触发)
		T = DS18B20_ReadT();         // 读取转换后的实际温度值(浮点型,含正负)
		
		// 温度正负号处理:显示时区分正负,TShow存绝对值便于数字显示
		if(T<0)                      // 温度为负数
		{
			LCD_ShowChar(1,3,'-');   // 第1行第3列显示负号"-"
			TShow = -T;              // 取绝对值,避免负数显示异常
		}
		else                         // 温度为正数/零
		{
			LCD_ShowChar(1,3,'+');   // 第1行第3列显示正号"+"
			TShow = T;               // 直接赋值
		}
		
		// 显示温度整数部分:第1行第4列,3位数字(如25℃显示"025",125℃显示"125")
		LCD_ShowNum(1,4,TShow,3);
		LCD_ShowChar(1,7,'.');       // 第1行第7列显示小数点"."
		// 显示温度小数部分:第1行第8列,2位数字(放大100倍取模,如25.125℃显示"12")
		LCD_ShowNum(1,8,(unsigned long)TShow*100%100,2); 	

		/* ========== 阈值设置及掉电保存模块 ========== */	
		if(KeyNum)                   // 检测到有按键按下(KeyNum≠0)
		{
			// 按键1:温度上限+1,边界限制(不超过DS18B20最大量程125℃)
			if(KeyNum==1)
			{
				THigh++;
				if(THigh>125) {THigh=125;}
			}
			// 按键2:温度上限-1,边界限制(不低于下限TLow)
			if(KeyNum==2)
			{
				THigh--;
				if(THigh<=TLow) {THigh++;}
			}
			// 按键3:温度下限+1,边界限制(不高于上限THigh)
			if(KeyNum==3)
			{
				TLow++;
				if(TLow>=THigh) {TLow--;}
			}
			// 按键4:温度下限-1,边界限制(不低于DS18B20最小量程-55℃)
			if(KeyNum==4)
			{
				TLow--;
				if(TLow<-55) {TLow=-55;} // 原代码笔误:THigh改为TLow,修正下限边界
			}
			
			// 更新LCD显示新的阈值:带符号显示(支持负数),3位数字
			LCD_ShowSignedNum(2,4,THigh,3);   // 第2行第4列显示温度上限
			LCD_ShowSignedNum(2,12,TLow,3);   // 第2行第12列显示温度下限
			
			// 将新阈值写入AT24C02 EEPROM,掉电不丢失
			AT24C02_WriteByte(0,THigh);      // 地址0存储上限
			Delay(5);                        // AT24C02写操作后延时,确保写入完成
			AT24C02_WriteByte(1,TLow);       // 地址1存储下限
			Delay(5);                        // 同上,保证写入稳定
		}
		
		/* ========== 阈值超限判断及提示模块 ========== */
		if(T>THigh)                          // 当前温度高于上限
		{
			LCD_ShowString(1,13,"OV:H");     // 第1行第13列显示"OV:H"(Over High:超上限)
		}
		else if(T<TLow)                      // 当前温度低于下限
		{
			LCD_ShowString(1,13,"OV:L");     // 第1行第13列显示"OV:L"(Over Low:超下限)
		}
		else                                 // 当前温度在正常范围
		{
			LCD_ShowString(1,13,"    ");     // 清空超限提示,显示4个空格
		}
	}
}

/**
  * @brief  定时器0中断服务函数(1ms中断一次)
  * @note   用于非阻塞式按键扫描,避免主循环被按键扫描阻塞,保证温度采集/显示流畅
  */
void Timer0_Rountine() interrupt 1
{
	static unsigned int T0Count;  // 静态计数变量:函数退出后值不丢失,用于累计中断次数
	TL0 = 0x18;	                  // 重置定时器0初值(11.0592MHz晶振,1ms定时)
	TH0 = 0xFC;	                  // 同上,保证每次中断间隔1ms
	T0Count++;                     // 中断次数累计
	
	// 每20ms调用一次按键扫描函数(20ms是按键消抖的标准时长)
	if(T0Count>=20)
	{
		T0Count=0;                 // 计数清零,循环累计
		Key_Loop();                // 非阻塞按键扫描:更新按键状态,供Key()函数读取
	}
}
相关推荐
麦托团子12 小时前
51单片机学习笔记12-蜂鸣器
51单片机
麦托团子12 小时前
51单片机学习笔记15-PWM脉冲编码调制
51单片机
麦托团子1 天前
51单片机学习笔记10-点阵屏
51单片机
恶魔泡泡糖1 天前
51单片机外部中断
c语言·单片机·嵌入式硬件·51单片机
项目題供诗2 天前
51单片机入门(五)
单片机·嵌入式硬件·51单片机
A-code2 天前
嵌入式UI刷新:观察者模式实战
stm32·单片机·mcu·物联网·51单片机
项目題供诗2 天前
51单片机入门(四)
单片机·嵌入式硬件·51单片机
项目題供诗3 天前
51单片机入门(三)
单片机·嵌入式硬件·51单片机
电子工程师成长日记-C513 天前
51单片机16路抢答器
单片机·嵌入式硬件·51单片机