蓝桥杯单片机通关秘籍: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"
// 读取温度函数
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;。
-
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>
-
-
例子:
#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;
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 文件里:
-
引入头文件:
#include "onewire.h" -
定义变量:定义一个
float类型的变量,比如Temperature。 -
循环调用:
void main()
{
float Temperature; // 定义变量存储温度
// 初始化系统...
while(1)
{
// 实时获取温度
Temperature = rd_Temperature();
// 此处可以添加显示代码,将 Temperature 显示在数码管上
// ...
}
}
这条语句会让温度值实时更新在 Temperature 这个变量里,接下来你想怎么显示它,就是数码管的事情啦!