蓝桥杯单片机学习笔记(五):DS18B20 深度解析与工程规范

蓝桥杯单片机通关秘籍:DS18B20 深度解析与工程规范

前言:在蓝桥杯单片机比赛中,DS18B20 温度传感器是绝对的“常客”。它看起来只是一个黑色的小豆子,但想要驯服它,不仅需要读懂它的“心”(时序),还要懂得如何优雅地在代码中安顿它(工程规范)。本篇笔记将带你从底层驱动到上层代码架构,彻底攻克这个模块。


第一章:避坑指南——大模板中的“隐形杀手”

在开始攻克 DS18B20 之前,先回顾一下数码管显示(Seg.c)中一个极易被忽视的错误。很多同学代码逻辑是对的,但数码管就是显示乱码,原因往往出在函数参数的顺序上。

1.1 参数顺序的重要性

在编写数码管显示函数时,我们通常会定义三个参数:

  • wela (位选):决定哪一位数码管亮。

  • dula (段选):决定显示什么数字。

  • point (小数点):决定是否显示小数点。

:warning: 警告:在 Seg.c 的底层驱动函数里,入口参数 weladulapoint 的定义顺序与调用顺序必须严格一致!

比喻时刻

这就好比你去银行柜台办事,柜员要求你依次递交“身份证、银行卡、密码”。

  • 如果你按照“银行卡、身份证、密码”的顺序递进去,柜员(编译器/单片机)就会处理错误,导致业务办理失败(数码管乱码)。

  • 切记:怎么定义的,就怎么传参,顺序绝对不能乱!


第二章:DS18B20 —— 单总线上的“独行侠”

2.1 硬件初相识

DS18B20 是一个非常有个性的传感器,它只需要 1根数据线 就能完成双向通信,我们称之为“单总线”(One-Wire)通信。

  • 接线法则

    • GND:接地(地基)。

    • VCC:接电源(能量源)。

    • DQ (Data):数据线(唯一的传声筒)。

2.2 它的“内心世界”(寄存器)

当你给它上电复位时,它会处于一种“刚睡醒”的状态,此时温度寄存器里的默认值是 +85°C。如果你上电瞬间读取到了85°C,别慌,那是因为它还没来得及转换第一次温度。

温度数据的存储格式(16位二进制):

想象一个由16个小格子组成的抽屉:

  • Bit 0-3 (小数位):负责存储精细的温度值(分辨率高达0.0625°C)。

  • Bit 4-10 (整数位):负责存储温度的整数部分。

  • Bit 11-15 (符号位):负责告诉你现在是零上还是零下。

    • 全为 0:正温度(暖和)。

    • 全为 1:负温度(冷)。

    • 注:DS18B20 只会出现这两种符号位情况,不会有0和1混杂。

2.3 关键功能指令(核心考点)

想要让 DS18B20 干活,单片机(主机)必须发送特定的“暗号”。我们将这些指令按操作流程进行了编号和拆解:

指令 1:跳过 ROM 指令 [0xCC]

解释

这是“点名”环节的简化版。

正常情况下总线上有多个设备,主机需要叫名字(序列号)才能指定谁回答。但蓝桥杯板子上只有一个 DS18B20,它是“独生子”。发送 0xCC 就是对它说:“别管名字了,我知道只有你,听我指挥!”

指令 2:温度转换指令 [0x44]

解释

这是启动测量的“开关”。

发送此指令后,DS18B20 开始采集环境温度并进行计算。

  • 注意细节:转换需要时间(毫秒级)。如果是“寄生电源”(偷电模式),主机必须在转换期间拉高总线供电。但在蓝桥杯板子上是外部供电,发送完这个指令后,我们可以通过读取总线状态来判断进度:读到 0 表示正在忙(转换中),读到 1 表示忙完了(转换完成)。转换好的数据会暂存在内部寄存器中。

指令 3:读取暂存寄存器指令 [0xBE]

解释

这是“交作业”的命令。

该指令告诉 DS18B20:“把你刚刚测好、放在暂存器里的数据吐出来。”

  • 读取流程:它会像流水线一样,从第0字节(温度低8位)一直吐到第8字节(CRC校验)。

  • 实战技巧:在比赛中,我们通常只关心温度数据(前两个字节)。所以,读完前两个字节(LSB和MSB)后,主机可以直接发复位信号打断它,不需要把后面没用的数据都读完。

指令 4:写入暂存寄存器指令 [0x4E]

解释

这是“设定规则”的命令。

用于向 DS18B20 写入配置信息,比如设置报警温度的上限(TH)和下限(TL),或者设置分辨率(9-12位)。

  • 写入顺序:必须严格遵循 3 个字节的顺序:TH → TL → 配置寄存器。且数据传输遵循 LSB First(低位先出)原则。写入前通常建议先复位。

第三章:实战代码 —— 编写驱动与转换公式

在比赛资源包中,官方会提供 onewire.c(底层驱动),我们需要把它添加到工程中,并自己编写 onewire.h 以及上层读取函数。

3.1 读取温度的核心代码

 // 记得包含必要的头文件
 #include "onewire.h"
 ​
 // 读取温度函数
 float rd_Temperature()
 {
     unsigned char low, high; // 用于暂存读回来的高低八位数据
   
     // --- 第一阶段:命令它干活(转换温度) ---
     init_ds18b20();      // 1. 握手:复位初始化
     Write_DS18B20(0xcc); // 2. 指令1:跳过ROM
     Write_DS18B20(0x44); // 3. 指令2:开始温度转换!
  
     // --- 第二阶段:把数据读回来 ---
     // 注意:转换需要时间,为防止程序卡死,通常连续调用时
     // 读取到的可能是上一次的转换结果,这在比赛中通常是允许的。
     
     init_ds18b20();      // 1. 再次握手:准备读取
     Write_DS18B20(0xcc); // 2. 指令1:再次跳过ROM
     Write_DS18B20(0xbe); // 3. 指令3:读取暂存器数据
  
     low = Read_DS18B20();  // 4. 接收低8位 (LSB)
     high = Read_DS18B20(); // 5. 接收高8位 (MSB)
  
     // --- 第三阶段:数据合成与换算 ---
     // high << 8 : 把高8位移到它该在的位置(左移8位)
     // | low     : 把低8位拼上去
     // / 16.0    : 核心算法!
     return ((high << 8) | low) / 16.0;
 }

3.2 为什么要除以 16.0?

这涉及到二进制小数的原理。DS18B20 的低 4 位是小数位。

  • Bit 0 代表 2^{-4} = 0.0625

  • Bit 1 代表 2^{-3} = 0.125

  • Bit 2 代表 2^{-2} = 0.25

  • Bit 3 代表 2^{-1} = 0.5

将一个 16 位的整数右移 4 位(相当于除以 2^4 = 16),就能把小数点对齐到正确的位置。例如,若读出的原始 16 位二进制数代表整数 2000,那么实际温度就是 2000 / 16 = 125.0^\circ C。


第四章:工程规范 —— C语言中的“防重复包含”机制

准备工作完毕后,我们需要在 main.c 中引用 onewire.h。这里涉及到两个至关重要的 C 语言工程概念:Include Guard(头文件卫士)引用路径

4.1 什么是“防重复包含机制”?

简单来说,它的作用是:保证头文件里的内容,在一个 .c 文件(编译单元)里只被“抄写”一次。

核心原理:#include 的本质是“复制粘贴”

首先你要知道,编译器是“傻瓜式”的。当它看到 #include "onewire.h" 时,它不会思考,只会把 onewire.h 里的所有文字,原封不动地复制过来。

如果没有卫士(悲剧发生的场景):

假设你有一个 config.h,里面定义了 sbit LED = P1^0;

  1. onewire.h 引用了 config.h

  2. main.c 引用了 onewire.h,又手滑直接引用了 config.h

编译器在处理 main.c 时,会先把 config.h 的内容复制一次,然后处理 onewire.h 时,又把 config.h 的内容复制了一次。

结果:编译器看到两行 sbit LED = P1^0;,它直接崩溃报错:“Redefinition(重定义)!你到底要定义几个 LED?”

解决方案:游乐园的“隐形印章”

为了解决这个问题,我们利用 C 语言预处理指令制作一个“卫士”。请看下面的代码模板:

 #ifndef __ONEWIRE_H_   // 1. 门卫检查:我们要进门了吗?
 #define __ONEWIRE_H_   // 2. 盖章确认:进来了,先盖个章!
 ​
 // ... 头文件内容(函数声明、变量定义) ...
 ​
 #endif                 // 3. 后门:结束

通俗比喻:游乐园检票逻辑

我们将编译器比作一个只认死理的保安,将头文件比作游乐园

  1. #ifndef __ONEWIRE_H_ (If Not Defined)

    • 场景:你走到门口。

    • 保安问:“你手上盖章了吗?(系统里记录过 __ONEWIRE_H_ 这个名字了吗?)”

    • 如果没盖章:保安放你进去。

    • 如果盖了章:保安直接把你拦在门外,指着旁边的快速通道说:“你已经进过去一次了,直接跳到出口(#endif 后面)去吧,别再进去占位置了。”

  2. #define __ONEWIRE_H_

    • 场景:进门后的第一件事。

    • 动作:保安在你手上盖个章(在编译器内存里定义这个标记)。

    • 作用:标记“这个文件已经被读取过了”,防止下次再被读取。

  3. #endif

    • 场景:游乐园的出口。

总结:当你写任何一个 .h 文件时,请形成肌肉记忆,永远加上这三行代码。这就叫“防重复包含机制”。


第五章:尖括号 < > 与双引号 " " 的爱恨情仇

在引用头文件时,很多小白分不清 #include <...>#include "..."。其实,它们的区别在于编译器“找书”(搜索文件)的路线不同

5.1 尖括号 < >:去“公立图书馆”找

  • 含义:引用 标准库编译器自带的库

  • 搜索路线:编译器会 直接 去它的安装目录下的 include 文件夹里找。

  • 它不看哪里:它通常不会去你的当前工程文件夹里找。

  • 适用场景

    • C 标准库:<stdio.h>, <math.h>

    • 单片机寄存器定义:<STC15F2K60S2.H>, <reg52.h>

  • 例子#include <STC15F2K60S2.H>

    • 这告诉编译器:“别在我桌子上(工程目录)乱翻,直接去系统库(Keil 安装目录)把这个芯片定义拿来。”

5.2 双引号 " ":先在“家里”找,找不到再去“图书馆”

  • 含义:引用 你自己编写的项目私有的 头文件。

  • 搜索路线

    1. 第一步:编译器先在 当前源文件(.c)所在的目录 查找。

    2. 第二步:如果家里没找到,它才会勉为其难地去 系统标准库目录 查找。

  • 适用场景

    • 你自己写的驱动:"onewire.h", "key.h"

    • 项目配置文件:"config.h"

  • 例子#include "onewire.h"

    • 这告诉编译器:“这个文件就在我现在的工程文件夹里,先在这里找。如果实在找不到,你再去系统目录看看。”

5.3 避坑总结表

符号 搜索顺序 推荐用途 你的代码示例
< > 仅系统目录 官方库、IDE自带库 #include <STC15F2K60S2.H>
" " 当前目录 \to 系统目录 自写 .h 文件、项目模块 #include "onewire.h"

5.4:完整底层代码

完整的底层代码如下:

 //onewire.c文件
 /*  #   单总线代码片段说明
     1.  本文件夹中提供的驱动代码供参赛选手完成程序设计参考。
     2.  参赛选手可以自行编写相关代码或以该代码为基础,根据所选单片机类型、运行速度和试题
         中对单片机时钟频率的要求,进行代码调试和修改。
 */
 ​
 //
 ​
 #include "onewire.h" 
  
 ​
 sbit DQ = P1^4;
 ​
 ​
 void Delay_OneWire(unsigned int t)  
 {
     unsigned char i;
     while(t--){
         for(i=0;i<12;i++);
     }
 }
 ​
 //
 void Write_DS18B20(unsigned char dat)
 {
     unsigned char i;
     for(i=0;i<8;i++)
     {
         DQ = 0;
         DQ = dat&0x01;
         Delay_OneWire(5);
         DQ = 1;
         dat >>= 1;
     }
     Delay_OneWire(5);
 }
 ​
 //
 unsigned char Read_DS18B20(void)
 {
     unsigned char i;
     unsigned char dat;
   
     for(i=0;i<8;i++)
     {
         DQ = 0;
         dat >>= 1;
         DQ = 1;
         if(DQ)
         {
             dat |= 0x80;
         }       
         Delay_OneWire(5);
     }
     return dat;
 }
 ​
 //
 bit init_ds18b20(void)
 {
     bit initflag = 0;
     
     DQ = 1;
     Delay_OneWire(12);
     DQ = 0;
     Delay_OneWire(80);
     DQ = 1;
     Delay_OneWire(10); 
     initflag = DQ;     
     Delay_OneWire(5);
   
     return initflag;
 }
 ​
 //读取温度函数
 float rd_Temperature()
 {
     unsigned char low,high;//返回温度数据的高低八位
         
     init_ds18b20();//初始化
     Write_DS18B20(0xcc);//跳过ROM
     Write_DS18B20(0x44);//进行温度转换
     
     init_ds18b20();//初始化
     Write_DS18B20(0xcc);//跳过ROM
     Write_DS18B20(0xbe);//读取温度
     
     low = Read_DS18B20();//读取低位
     high = Read_DS18B20();//读取高位
     
     return ((high << 8) | low) / 16.0;
     
 }
 //onewire.h文件
 #ifndef __ONEWIRE_H_
 #define __ONEWIRE_H_
 ​
 #include <STC15F2K60S2.H>
 ​
 // 1. 函数声明 (只写名字,不写身体)
 void Delay_OneWire(unsigned int t);
 void Write_DS18B20(unsigned char dat);
 unsigned char Read_DS18B20(void);
 bit init_ds18b20(void);
 ​
 // 2. 补上你之前代码里有的温度读取函数,否则主函数无法调用
 float rd_Temperature(void); 
 ​
 #endif

第六章:最终调用

准备工作都做完了,我们该如何在主程序中调用这个芯片呢?

main.c 文件里:

  1. 引入头文件:#include "onewire.h"

  2. 定义变量:定义一个 float 类型的变量,比如 Temperature

  3. 循环调用:

 void main()
 {
     float Temperature; // 定义变量存储温度
     
     // 初始化系统...
     
     while(1)
     {
         // 实时获取温度
         Temperature = rd_Temperature();
         
         // 此处可以添加显示代码,将 Temperature 显示在数码管上
         // ...
     }
 }

这条语句会让温度值实时更新在 Temperature 这个变量里,接下来你想怎么显示它,就是数码管的事情啦!