蓝桥杯单片机学习笔记(五):DS18B20 深度解析与工程规范
蓝桥杯单片机通关秘籍:DS18B20 深度解析与工程规范
温馨提示:内容源自米醋电子工作室培训课,由本人总结
前言:在蓝桥杯单片机比赛中,DS18B20 温度传感器是绝对的“常客”。它看起来只是一个黑色的小豆子,但想要驯服它,不仅需要读懂它的“心”(时序),还要懂得如何优雅地在代码中安顿它(工程规范)。本篇笔记将带你从底层驱动到上层代码架构,彻底攻克这个模块。
第一章:避坑指南——大模板中的“隐形杀手”
在开始攻克 DS18B20 之前,先回顾一下数码管显示(Seg.c)中一个极易被忽视的错误。很多同学代码逻辑是对的,但数码管就是显示乱码,原因往往出在函数参数的顺序上。
1.1 参数顺序的重要性
在编写数码管显示函数时,我们通常会定义三个参数:
- wela(位选):决定哪一位数码管亮。
- dula(段选):决定显示什么数字。
- point(小数点):决定是否显示小数点。
⚠️ 警告:在Seg.c的底层驱动函数里,入口参数wela、dula、point的定义顺序与调用顺序必须严格一致!
比喻时刻:
这就好比你去银行柜台办事,柜员要求你依次递交“身份证、银行卡、密码”。
- 如果你按照“银行卡、身份证、密码”的顺序递进去,柜员(编译器/单片机)就会处理错误,导致业务办理失败(数码管乱码)。
- 切记:怎么定义的,就怎么传参,顺序绝对不能乱!
第二章: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"// 读取温度函数floatrd_Temperature(){unsignedcharlow,high;// 用于暂存读回来的高低八位数据// --- 第一阶段:命令它干活(转换温度) ---init_ds18b20();// 1. 握手:复位初始化Write_DS18B20(0xcc);// 2. 指令1:跳过ROMWrite_DS18B20(0x44);// 3. 指令2:开始温度转换!// --- 第二阶段:把数据读回来 ---// 注意:转换需要时间,为防止程序卡死,通常连续调用时// 读取到的可能是上一次的转换结果,这在比赛中通常是允许的。init_ds18b20();// 1. 再次握手:准备读取Write_DS18B20(0xcc);// 2. 指令1:再次跳过ROMWrite_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.06252^{-4} = 0.06252−4=0.0625
- Bit 1 代表2−3=0.1252^{-3} = 0.1252−3=0.125
- Bit 2 代表2−2=0.252^{-2} = 0.252−2=0.25
- Bit 3 代表2−1=0.52^{-1} = 0.52−1=0.5
将一个 16 位的整数右移 4 位(相当于除以24=162^4 = 1624=16),就能把小数点对齐到正确的位置。例如,若读出的原始 16 位二进制数代表整数 2000,那么实际温度就是2000/16=125.0∘C2000 / 16 = 125.0^\circ C2000/16=125.0∘C。
第四章:工程规范 —— C语言中的“防重复包含”机制
准备工作完毕后,我们需要在main.c中引用onewire.h。这里涉及到两个至关重要的 C 语言工程概念:Include Guard(头文件卫士)和引用路径。
4.1 什么是“防重复包含机制”?
简单来说,它的作用是:保证头文件里的内容,在一个.c文件(编译单元)里只被“抄写”一次。
核心原理:#include的本质是“复制粘贴”
首先你要知道,编译器是“傻瓜式”的。当它看到#include "onewire.h"时,它不会思考,只会把onewire.h里的所有文字,原封不动地复制过来。
如果没有卫士(悲剧发生的场景):
假设你有一个config.h,里面定义了sbit LED = P1^0;。
onewire.h引用了config.h。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. 后门:结束通俗比喻:游乐园检票逻辑
我们将编译器比作一个只认死理的保安,将头文件比作游乐园。
#ifndef __ONEWIRE_H_(If Not Defined)- 场景:你走到门口。
- 保安问:“你手上盖章了吗?(系统里记录过
__ONEWIRE_H_这个名字了吗?)” - 如果没盖章:保安放你进去。
- 如果盖了章:保安直接把你拦在门外,指着旁边的快速通道说:“你已经进过去一次了,直接跳到出口(
#endif后面)去吧,别再进去占位置了。”
#define __ONEWIRE_H_- 场景:进门后的第一件事。
- 动作:保安在你手上盖个章(在编译器内存里定义这个标记)。
- 作用:标记“这个文件已经被读取过了”,防止下次再被读取。
#endif- 场景:游乐园的出口。
总结:当你写任何一个.h文件时,请形成肌肉记忆,永远加上这三行代码。这就叫“防重复包含机制”。
第五章:尖括号< >与双引号" "的爱恨情仇
在引用头文件时,很多小白分不清#include <...>和#include "..."。其实,它们的区别在于编译器“找书”(搜索文件)的路线不同。
5.1 尖括号< >:去“公立图书馆”找
- 含义:引用标准库或编译器自带的库。
- 搜索路线:编译器会直接去它的安装目录下的
include文件夹里找。 - 它不看哪里:它通常不会去你的当前工程文件夹里找。
- 适用场景:
- C 标准库:
<stdio.h>,<math.h> - 单片机寄存器定义:
<STC15F2K60S2.H>,<reg52.h>
- C 标准库:
- 例子:
#include <STC15F2K60S2.H>- 这告诉编译器:“别在我桌子上(工程目录)乱翻,直接去系统库(Keil 安装目录)把这个芯片定义拿来。”
5.2 双引号" ":先在“家里”找,找不到再去“图书馆”
- 含义:引用你自己编写的或项目私有的头文件。
- 搜索路线:
- 第一步:编译器先在当前源文件(.c)所在的目录查找。
- 第二步:如果家里没找到,它才会勉为其难地去系统标准库目录查找。
- 适用场景:
- 你自己写的驱动:
"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;voidDelay_OneWire(unsignedintt){unsignedchari;while(t--){for(i=0;i<12;i++);}}//voidWrite_DS18B20(unsignedchardat){unsignedchari;for(i=0;i<8;i++){DQ=0;DQ=dat&0x01;Delay_OneWire(5);DQ=1;dat>>=1;}Delay_OneWire(5);}//unsignedcharRead_DS18B20(void){unsignedchari;unsignedchardat;for(i=0;i<8;i++){DQ=0;dat>>=1;DQ=1;if(DQ){dat|=0x80;}Delay_OneWire(5);}returndat;}//bitinit_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);returninitflag;}//读取温度函数floatrd_Temperature(){unsignedcharlow,high;//返回温度数据的高低八位init_ds18b20();//初始化Write_DS18B20(0xcc);//跳过ROMWrite_DS18B20(0x44);//进行温度转换init_ds18b20();//初始化Write_DS18B20(0xcc);//跳过ROMWrite_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. 函数声明 (只写名字,不写身体)voidDelay_OneWire(unsignedintt);voidWrite_DS18B20(unsignedchardat);unsignedcharRead_DS18B20(void);bitinit_ds18b20(void);// 2. 补上你之前代码里有的温度读取函数,否则主函数无法调用floatrd_Temperature(void);#endif第六章:最终调用
准备工作都做完了,我们该如何在主程序中调用这个芯片呢?
在main.c文件里:
- 引入头文件:
#include "onewire.h" - 定义变量:定义一个
float类型的变量,比如Temperature。 - 循环调用:
voidmain(){floatTemperature;// 定义变量存储温度// 初始化系统...while(1){// 实时获取温度Temperature=rd_Temperature();// 此处可以添加显示代码,将 Temperature 显示在数码管上// ...}}这条语句会让温度值实时更新在Temperature这个变量里,接下来你想怎么显示它,就是数码管的事情啦!
