第六周 DS18B20 温度传感器模块
1. DS18B20 简介
DS18B20 是单总线数字温度传感器,只需一个数据引脚即可完成通信。
在蓝桥杯 CT107D 开发板上,数据引脚通常接在 P1^4,定义为 DQ。
2. 暂存器结构
- 温度值低 8 位存放在暂存器第 0 字节。
- 温度值高 8 位存放在暂存器第 1 字节。
- 温度数据为 16 位有符号数,默认分辨率 12 位,最低位代表 0.0625℃。
3. 工作步骤
DS18B20 的典型操作流程分为三步:
- 初始化 DS18B20
主机发送复位脉冲,检测 DS18B20 的存在脉冲。 - 执行 ROM 指令
如0xCC(跳过 ROM),用于单设备场景。 - 执行功能指令
如0x44(启动温度转换)、0xBE(读取暂存器)。
4. 单总线延时函数
由于 DS18B20 对时序要求严格,需要微秒级延时。
当单片机使用 12T 模式、主频 12MHz 时,一个指令周期为 1μs,因此可以通过循环实现延时。
/**
* @brief 单总线微秒级延时函数
* @param t 延时的微秒数
* @note 适用于 12T 模式、12MHz 晶振
*/
void Delay_OneWire(unsigned int t)
{
t *= 12; // 12T 模式下,每个循环约 1μs
while (t--);
}
说明:若使用其他晶振频率或 1T 模式,需要重新调整延时系数。
5. 初始化时序
初始化流程:
- 主机将
DQ拉高,延时一段时间。 - 主机将
DQ拉低,保持 480μs 以上,发出复位脉冲。 - 主机释放
DQ(拉高),等待 15~60μs。 - DS18B20 检测到复位脉冲后,会在 60~240μs 内拉低
DQ,表示存在。 - 主机读取
DQ电平,若为低则初始化成功。
/**
* @brief DS18B20 初始化函数
* @return 0:初始化成功;1:初始化失败
*/
bit init_ds18b20(void)
{
bit initflag = 0;
DQ = 1; // 拉高总线
Delay_OneWire(12); // 延时 12μs
DQ = 0; // 拉低总线,发出复位脉冲
Delay_OneWire(80); // 保持低电平 80μs(符合 480μs 要求,此处使用 80μs 即可)
DQ = 1; // 释放总线,拉高
Delay_OneWire(10); // 延时 10μs,等待 DS18B20 响应
initflag = DQ; // 读取存在脉冲
Delay_OneWire(5); // 再延时 5μs
return initflag; // 返回状态:0 表示检测到 DS18B20
}
6. 写时序与读时序
6.1 写时序
写时序特点:
- 写“0”:拉低总线并保持 60μs 以上,然后释放。
- 写“1”:拉低总线约 1~15μs,然后释放,保持高电平 60μs 以上。
- 每个写时隙至少持续 60μs,且有至少 1μs 的恢复时间。
/**
* @brief DS18B20 单字节写函数
* @param dat 要写入的字节
*/
void Write_DS18B20(unsigned char dat)
{
unsigned char i;
for (i = 0; i < 8; i++)
{
DQ = 0; // 拉低总线,启动写时隙
DQ = dat & 0x01; // 写入最低位
Delay_OneWire(5); // 保持 5μs
DQ = 1; // 释放总线
dat >>= 1; // 准备下一位
}
Delay_OneWire(5); // 写完后延时
}
6.2 读时序
读时序特点:
- 主机拉低总线 1μs 以上,然后释放。
- DS18B20 在检测到下降沿后 15μs 内将数据送到总线上。
- 主机在拉低后约 1~15μs 内读取总线电平。
- 读“0”:总线为低电平;读“1”:总线为高电平。
/**
* @brief DS18B20 单字节读函数
* @return 读取到的字节
*/
unsigned char Read_DS18B20(void)
{
unsigned char i;
unsigned char dat = 0;
for (i = 0; i < 8; i++)
{
DQ = 0; // 拉低总线,启动读时隙
dat >>= 1; // 先右移,为最高位腾出空间
DQ = 1; // 释放总线
if (DQ) // 读取总线电平
{
dat |= 0x80; // 若为高电平,最高位置 1
}
Delay_OneWire(5); // 延时 5μs
}
return dat;
}
7. 常用指令
7.1 ROM 指令
| 指令 | 代码 | 说明 |
|---|---|---|
| 搜索 ROM | 0xF0 | 用于多设备识别 |
| 读 ROM | 0x33 | 读取单个 DS18B20 的 64 位 ROM 编码 |
| 匹配 ROM | 0x55 | 指定某个 DS18B20 进行操作 |
| 跳过 ROM | 0xCC | 单设备时跳过 ROM 匹配 |
| 报警搜索 | 0xEC | 搜索温度超限设备 |
蓝桥杯竞赛中,通常只有一个 DS18B20,所以最常用的是 跳过 ROM(0xCC)。
7.2 功能指令
| 指令 | 代码 | 说明 |
|---|---|---|
| 温度转换 | 0x44 | 启动一次温度转换 |
| 读暂存器 | 0xBE | 读取暂存器所有字节,前两个字节为温度 |
| 写暂存器 | 0x4E | 写暂存器(如配置分辨率) |
| 复制暂存器 | 0x48 | 将暂存器数据复制到 EEPROM |
| 读 EEPROM | 0xB8 | 从 EEPROM 读数据 |
| 读取电源模式 | 0xB4 | 判断是寄生供电还是外部供电 |
8. 温度读取完整代码
/**
* @brief 读取温度函数
* @return 浮点型温度值(单位:℃)
* @note DS18B20 默认 12 位分辨率,最低位代表 0.0625℃
*/
float rd_temperature(void)
{
unsigned char low, high;
// 启动温度转换
init_ds18b20(); // 初始化 DS18B20
Write_DS18B20(0xcc); // 跳过 ROM
Write_DS18B20(0x44); // 启动温度转换
// 读取温度暂存器
init_ds18b20(); // 再次初始化
Write_DS18B20(0xcc); // 跳过 ROM
Write_DS18B20(0xbe); // 读暂存器命令
low = Read_DS18B20(); // 先读低 8 位
high = Read_DS18B20(); // 再读高 8 位
// 合并 16 位温度值并乘以精度 0.0625
return ((high << 8) | low) / 16.0;
}
也可以直接使用
return ((high << 8) | low) * 0.0625;表示精度。
9. 注意事项
-
头文件包含问题
若在.h文件中定义了sbit DQ = P1^4;,请将其移到.c文件中。
.h文件中只保留#include "reg52.h"或其他必要的包含,否则可能引起编译错误。 -
上电后第一次读取温度可能不准确
DS18B20 上电后第一次转换结果可能不稳定。
建议在主函数初始化后先读取一次温度并延时 750ms,再进入正常显示循环。float t; t = rd_temperature(); // 第一次读取(用于跳过不稳定阶段) Delay750ms(); // 延时 750ms,等待传感器稳定 -
时序严格性
DS18B20 对时序敏感,请尽量关闭中断或使用短暂中断屏蔽,避免打断时序。
在蓝桥杯竞赛中,若使用定时器中断动态扫描数码管,需注意在读写 DS18B20 时可能被中断打断,必要时可在读写期间临时关闭总中断(EA = 0;),完成后恢复(EA = 1;)。 -
多设备场景
若总线上挂载多个 DS18B20,需要使用 ROM 匹配指令(0x55)指定设备,不能直接使用0xCC。
第六周 温度报警装置程序设计
主程序
/*头文件*/
#include <STC15F2K60S2.H>
#include "Init.h"
#include "Led.h"
#include "Key.h"
#include "Seg.h"
#include "onewire.h"
/*变量声明区*/
unsigned char Key_Slow_Down;//按键减速专用变量10ms
unsigned int Seg_Slow_Down;//数码管减速专用变量500ms
unsigned char Key_Val,Key_Up,Key_Down,Key_Old;//按键读取变量
unsigned char Seg_Buf[8] = {10,10,10,10,10,10,10,10};//数码管显示数据存放数组
unsigned char Seg_Point[8] = {0,0,0,0,0,0,0,0};//数码管小数点数据存放数组
unsigned char Seg_Pos;//数码管显示数据扫描变量
unsigned char ucLed[8] = {0,0,0,0,0,0,0,0};//Led数据存放数组
unsigned char Seg_Mode;//数码管显示界面变量
unsigned char Temp_Disp[2] = {30,20};//参数数据显示数组
unsigned char Temp_Disp_Index;//参数数据显示数组索引
unsigned char Temp_Control[2] = {30,20};//参数数据实际数组
unsigned int Timer_500Ms;//500ms标志位
unsigned char Key_Tick;//按键计时变量
unsigned char Led_Num;//亮度等级变量
unsigned char Led_Pwm;
float temp;//温度读取数据变量
bit Key_Flag;//按键计时标志位
bit Seg_Flag;//数码管闪烁标志位
bit Error_Flag;//错误操作标志位
/*按键处理函数*/
void Key_Proc()
{
if(Key_Slow_Down) return;
Key_Slow_Down = 1;//按键减速程序
Key_Val = Key_Read();//读取键码值
Key_Down = Key_Val & (Key_Old ^ Key_Val);//检测下降沿
Key_Up = ~Key_Val & (Key_Old ^ Key_Val);//检测上升沿
Key_Old = Key_Val;//辅助扫描变量
/*按键+-*/
if(Seg_Mode == 1)
{
if(Key_Down == 14 || Key_Down == 15)//检测到下降沿
Key_Flag = 1;//开始计时
}
if(Key_Tick < 500)//短按
{
if(Key_Up == 14)//+
{
Key_Flag = Key_Tick = 0;//复位
if(++Temp_Disp[Temp_Disp_Index] == 71)
Temp_Disp[Temp_Disp_Index] = 10;
}
if(Key_Up == 15)//-
{
Key_Flag = Key_Tick = 0;//复位
if(--Temp_Disp[Temp_Disp_Index] == 9)
Temp_Disp[Temp_Disp_Index] = 70;
}
}
else
{
if(Key_Old == 14)//+
{
if(++Temp_Disp[Temp_Disp_Index] == 71)
Temp_Disp[Temp_Disp_Index] = 10;
}
if(Key_Up == 15)//-
{
if(--Temp_Disp[Temp_Disp_Index] == 9)
Temp_Disp[Temp_Disp_Index] = 70;
}
if(Key_Up == 14 || Key_Up == 15)
{
Key_Flag = Key_Tick = 0;//复位
}
}
switch(Key_Down)
{
case 12://“界面切换”按键
if(Seg_Mode == 0)
{
Temp_Disp[0] = Temp_Control[0];
Temp_Disp[1] = Temp_Control[1];
Seg_Mode = 1;
Temp_Disp_Index = 0;//复位
}
if(Seg_Mode == 1)//处于参数设置界面
{
Seg_Mode = 0;
if(Temp_Disp[0] >= Temp_Disp[1])//参数合法
{
Error_Flag = 0;
Temp_Control[0] = Temp_Disp[0];
Temp_Control[1] = Temp_Disp[1];
}
else//参数不合法
{
Error_Flag = 1;
Temp_Disp[0] = Temp_Control[0];
Temp_Disp[1] = Temp_Control[1];
}
}
break;
case 13://“参数切换”按键
if(++Temp_Disp_Index == 2) Temp_Disp_Index = 0;
break;
case 16://“重置”按键
Temp_Control[0] = 30;
Temp_Control[1] = 20;
break;
}
}
/*信息处理函数*/
void Seg_Proc()
{
if(Seg_Slow_Down) return;
Seg_Slow_Down = 1;//数码管减速程序
temp = rd_temperature();//实时读取温度
switch(Seg_Mode)
{
case 0://温度显示界面
Seg_Buf[0] = Seg_Buf[7] = 11;//C
Seg_Buf[3] = 10;//固定熄灭
Seg_Buf[4] = (unsigned char)temp / 10 % 10;
Seg_Buf[5] = (unsigned char)temp % 10;
Seg_Buf[6] = (unsigned int)(temp*10) % 10;
Seg_Point[5] = 1;
break;
case 1://参数设置界面
Seg_Point[5] = 0;
Seg_Buf[0] = Seg_Buf[7] = 12;//P
Seg_Buf[3] = Temp_Disp[0] / 10 % 10;//Tmax
Seg_Buf[4] = Temp_Disp[0] % 10;
Seg_Buf[5] = 13;//-
Seg_Buf[6] = Temp_Disp[1] / 10 % 10;//Tmin
Seg_Buf[7] = Temp_Disp[1] % 10;
Seg_Buf[3+Temp_Disp_Index*3] = Seg_Flag?Temp_Disp[Temp_Disp_Index] / 10 % 10:10;
Seg_Buf[4+Temp_Disp_Index*3] = Seg_Flag?Temp_Disp[Temp_Disp_Index] % 10:10;
break;
}
}
/*其他显示函数*/
void Led_Proc()
{
ucLed[0] = (temp > Temp_Control[0]);
ucLed[1] = (temp < Temp_Control[0] && temp > Temp_Control[1]);
ucLed[2] = (temp < Temp_Control[1]);
ucLed[3] = Error_Flag;
if(temp > Temp_Control[0])
Led_Num = 3;
else if(temp > Temp_Control[1])
Led_Num = 6;
else
Led_Num = 9;
}
/*延时函数*/
void Delay750ms(void) //@12.000MHz
{
unsigned char data i, j, k;
i = 35;
j = 51;
k = 182;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
/*定时器0初始化函数*/
void Timer0_Init(void) //1毫秒@12.000MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //定时器0中断打开
EA = 1; //总中断打开
}
/*定时器0中断服务函数*/
void Timer0Server() interrupt 1
{
if(++Key_Slow_Down == 10) Key_Slow_Down = 0;
if(++Seg_Slow_Down == 500) Seg_Slow_Down = 0;
if(++Seg_Pos == 8) Seg_Pos = 0;
Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos],Seg_Point[Seg_Pos]);
if(++Timer_500Ms == 500)//闪烁
{
Timer_500Ms = 0;
Seg_Flag ^= 1;
}
if(Key_Flag == 1)//按键计时
{
if(++Key_Tick == 600)
Key_Tick = 600;
}
if(++Led_Pwm == 10)
Led_Pwm = 0;
if(Led_Num < Led_Pwm)
Led_Disp(Seg_Pos,ucLed[Seg_Pos]);
else
Led_Disp(Seg_Pos,0);
}
/*主函数Main*/
void main()
{
rd_temperature();
Delay750ms();
System_Init();
Timer0_Init();
while(1)
{
Key_Proc();
Seg_Proc();
Led_Proc();
}
}
错误点
-
上电后,LED 和数码管短暂全亮,然后系统没有任何反应。
-
调试发现程序卡在温度读取函数
rd_temperature()附近。
错误代码位置
void Seg_Proc()
{
unsigned char i;
// 错误点:温度读取放在减速判断之后
if(Seg_Slow_Down) return;
Seg_Slow_Down = 1;
temp = rd_temperature(); // 这里执行时会被定时器中断打断
switch(Seg_Mode)
{
case 0:
// 显示温度
break;
}
}
原因分析
-
DS18B20 单总线时序严格
rd_temperature()内部包含微秒级延时,时序不能被中断打断。 -
定时器中断频繁
定时器 0 每 1ms 进入一次中断,中断里执行Seg_Disp()和Led_Disp(),占用了 CPU 时间。
当主循环执行rd_temperature()时,中断仍然会打断时序,导致通信失败或程序卡死。 -
主循环被阻塞
温度读取函数执行时间较长,且被打断后无法恢复正常,导致Key_Proc()、Led_Proc()等任务无法执行,系统看起来“无反应”。



