第六周 DS18B20学习笔记

第六周 DS18B20 温度传感器模块

1. DS18B20 简介

DS18B20 是单总线数字温度传感器,只需一个数据引脚即可完成通信。
在蓝桥杯 CT107D 开发板上,数据引脚通常接在 P1^4,定义为 DQ。


2. 暂存器结构

  • 温度值低 8 位存放在暂存器第 0 字节。
  • 温度值高 8 位存放在暂存器第 1 字节。
  • 温度数据为 16 位有符号数,默认分辨率 12 位,最低位代表 0.0625℃。

3. 工作步骤

DS18B20 的典型操作流程分为三步:

  1. 初始化 DS18B20
    主机发送复位脉冲,检测 DS18B20 的存在脉冲。
  2. 执行 ROM 指令
    如 0xCC(跳过 ROM),用于单设备场景。
  3. 执行功能指令
    如 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. 初始化时序

初始化流程:

  1. 主机将 DQ 拉高,延时一段时间。
  2. 主机将 DQ 拉低,保持 480μs 以上,发出复位脉冲。
  3. 主机释放 DQ(拉高),等待 15~60μs。
  4. DS18B20 检测到复位脉冲后,会在 60~240μs 内拉低 DQ,表示存在。
  5. 主机读取 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. 注意事项

  1. 头文件包含问题
    若在 .h 文件中定义了 sbit DQ = P1^4;,请将其移到 .c 文件中。
    .h 文件中只保留 #include "reg52.h" 或其他必要的包含,否则可能引起编译错误。

  2. 上电后第一次读取温度可能不准确
    DS18B20 上电后第一次转换结果可能不稳定。
    建议在主函数初始化后先读取一次温度并延时 750ms,再进入正常显示循环。

    float t;
    t = rd_temperature();   // 第一次读取(用于跳过不稳定阶段)
    Delay750ms();           // 延时 750ms,等待传感器稳定
    
  3. 时序严格性
    DS18B20 对时序敏感,请尽量关闭中断或使用短暂中断屏蔽,避免打断时序。
    在蓝桥杯竞赛中,若使用定时器中断动态扫描数码管,需注意在读写 DS18B20 时可能被中断打断,必要时可在读写期间临时关闭总中断(EA = 0;),完成后恢复(EA = 1;)。

  4. 多设备场景
    若总线上挂载多个 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;
    }
}

原因分析

  1. DS18B20 单总线时序严格
    rd_temperature() 内部包含微秒级延时,时序不能被中断打断。

  2. 定时器中断频繁
    定时器 0 每 1ms 进入一次中断,中断里执行 Seg_Disp() 和 Led_Disp(),占用了 CPU 时间。
    当主循环执行 rd_temperature() 时,中断仍然会打断时序,导致通信失败或程序卡死。

  3. 主循环被阻塞
    温度读取函数执行时间较长,且被打断后无法恢复正常,导致 Key_Proc()、Led_Proc() 等任务无法执行,系统看起来“无反应”。