主函数
#include "public.h"
void main()
{
u8 i=0;
int temp_value;
u8 temp_buf[5];
ds18b20_init();//初始化DS18B20
while(1)
{
i++;
if(i%50==0)//间隔一段时间读取温度值,间隔时间要大于温度传感器转换温度时间
temp_value=ds18b20_read_temperture()*10;//保留温度值小数后一位
if(temp_value<0)//负温度
{
temp_value=-temp_value;
temp_buf[0]=0x40;//显示负号
}
else
temp_buf[0]=0x00;//不显示
temp_buf[1]=gsmg_code[temp_value/1000];//百位
temp_buf[2]=gsmg_code[temp_value%1000/100];//十位
temp_buf[3]=gsmg_code[temp_value%1000%100/10]|0x80;//个位+小数点
temp_buf[4]=gsmg_code[temp_value%1000%100%10];//小数点后一位
smg_display(temp_buf,4);
}
}
数码管函数
#define SMG_A_DP_PORTP0//使用宏定义数码管段码口
//定义数码管位选信号控制脚
sbit LSA=P2^2;
sbit LSB=P2^3;
sbit LSC=P2^4;
//共阴极数码管显示0~F的段码数据
u8 gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
void smg_display(u8 dat[],u8 pos)
{
u8 i=0;
u8 pos_temp=pos-1;
for(i=pos_temp;i<8;i++)
{
switch(i)//位选
{
case 0: LSC=1;LSB=1;LSA=1;break;
case 1: LSC=1;LSB=1;LSA=0;break;
case 2: LSC=1;LSB=0;LSA=1;break;
case 3: LSC=1;LSB=0;LSA=0;break;
case 4: LSC=0;LSB=1;LSA=1;break;
case 5: LSC=0;LSB=1;LSA=0;break;
case 6: LSC=0;LSB=0;LSA=1;break;
case 7: LSC=0;LSB=0;LSA=0;break;
}
SMG_A_DP_PORT=dat[i-pos_temp];//传送段选数据
delay_10us(100);//延时一段时间,等待显示稳定
SMG_A_DP_PORT=0x00;//消音
}
}
DS18B20函数
sbit DS18B20_PORT=P3^7;//DS18B20数据口定义
void ds18b20_reset(void)
{
DS18B20_PORT=0;//拉低DQ
delay_10us(75);//拉低750us,
DS18B20_PORT=1;//DQ=1
delay_10us(2);//20US
}
u8 ds18b20_check(void)
{
u8 time_temp=0;
while(DS18B20_PORT&&time_temp<10)
{
time_temp++;
delay_10us(1);
}
if(time_temp>=10)return 1;
else time_temp=0;
while((!DS18B20_PORT)&&time_temp<20)
{
time_temp++;
delay_10us(1);
}
if(time_temp>=20)return 1;
return 0;
}
u8 ds18b20_read_bit(void)
{
u8 dat=0;
DS18B20_PORT=0;
_nop_();_nop_();
DS18B20_PORT=1;
_nop_();_nop_();
if(DS18B20_PORT)dat=1;
else dat=0;
delay_10us(5);
return dat;
}
u8 ds18b20_read_byte(void)
{
u8 i=0;
u8 dat=0;
u8 temp=0;
for(i=0;i<8;i++)
{
temp=ds18b20_read_bit();
dat=(temp<<7)|(dat>>1);
}
return dat;
}
void ds18b20_write_byte(u8 dat)
{
u8 i=0;
u8 temp=0;
for(i=0;i<8;i++)
{
temp=dat&0x01;
dat>>=1;
if(temp)
{
DS18B20_PORT=0;
_nop_();_nop_();
DS18B20_PORT=1;
delay_10us(6);
}
else
{
DS18B20_PORT=0;
delay_10us(6);
DS18B20_PORT=1;
_nop_();_nop_();
}
}
}
void ds18b20_start(void)
{
ds18b20_reset();
ds18b20_check();
ds18b20_write_byte(0xcc);
ds18b20_write_byte(0x44);
}
u8 ds18b20_init(void)
{
ds18b20_reset();
return ds18b20_check();
}
float ds18b20_read_temperture(void)
{
float temp;
u8 dath=0;
u8 datl=0;
u16 value=0;
ds18b20_start();
ds18b20_reset();
ds18b20_check();
ds18b20_write_byte(0xcc);
ds18b20_write_byte(0xbe);
datl=ds18b20_read_byte();
dath=ds18b20_read_byte();
value=(dath<<8)+datl;
if((value&0xf800)==0xf800)
{
value=(~value)+1;
temp=value*(-0.0625);
}
else
{
temp=value*0.0625;
}
return temp;
}
以上代码实现的功能是,在动态数码管上显示4位温度值,包括3位整数和1位小数。
直接看代码可能有些繁杂,如果我们把它们按功能模块划分,就像摩托车包括核心部分——发动机、控制部分——龙头、驱动部分——轮胎,这样我们就可以集中一点。
由于DS18B20在温度检测起到十分重要的作用,本文几乎80%的内容都是介绍它,然后花费较少的笔墨介绍数码管按显示个十百以及小数点位的原理,最后会整合前面两者,再分析一下主函数的含义。前言就讲这么多,接下来我们来看看这篇文章的主角——DS18B20
一、DS18B20部分
什么是DS18B20?你的直觉一定是“温度传感器”,实际上它还有很多你不知道的本事。
首先,DS18B20并不全是我们熟知的三引脚,它还有其他两种形态

我们使用的DS18B20采用的TO-92封装,只有三个引脚。
其次,这块小小的温度传感器并不只负责测温,其内部还嵌入了一个AD转换器,也就是说,DS18B20在自己测得温度模拟量后,又自己将其转换成数字量输出,一气呵成。
最后,别看DS18B20引脚很少,实际上,可以更少。在寄生电源模式下,VDD引脚都可以不需要,只靠DQ引脚在接收数据的同时,顺便给DS18B20供电。
1.概述
DS18B20数字温度计,可提供9~12位分辨率的摄氏温度测量,可配置上下限报警功能(断电后不擦除),可通过DQ引脚与中央MCU连接。此外,每一块DS18B20都有一个独一无二的“身份证”——存储于ROM的8位家族码,理论上单片机可以同时控制2的8次方个DS18B20。
接下来我们把目光转向那个传奇引脚——DQ,它都能传输哪些数据?这取决于DS18B20里面放了什么数据

忽略左边的电路,右边就是DS18B20的三个存储器——64位的ROM、Scratchpad(暂存器)、EEPROM(非易失性存储器),我们看看都放了些什么在里面
64位光刻ROM
ROM只可读,不可写入。64位ROM分成三部分,分别是
| Byte 0 | 8位家族码(family code) | 固定为0x28(DS18B20的标识) |
| Byte 1~6 | 48位唯一序列号 | 全球唯一的设备ID,由厂家光刻写入 |
| Byte 7 | 8位CRC校验码 | 循环冗余校验冗余 |
单片机通过Read Rom[0x33]或Search Rom[0xf0]命令读取ROM中的64位数据
暂存器(Scatchpad)
这里是温度测量和通信的核心缓冲区,断电后数据丢失,所有温度转换结果和配置数据首先写入此处(注意这个“首先”)。暂存器共9个字节,72位,可分成五部分
| Byte 0 | 温度LSB | 温度值低四位(包含小数部分) | 否(运行时数据) |
| Byte 1 | 温度MSB | 温度值高 8 位(包含符号位) | 否(运行时数据) |
| Byte 2 | TH寄存器 | 高温报警阈值 | 是(字节 2 的 EEPROM 备份) |
| Byte 3 | TL寄存器 | 低温报警阈值 | 是(字节 3 的 EEPROM 备份) |
| Byte 4 | 配置寄存器 | 分辨率设置 | 是(字节 4 的 EEPROM 备份) |
| Byte 5 | 保留 | 否 | |
| Byte 6 | 保留 | 否 | |
| Byte 7 | 保留 | 否 | |
| Byte 8 | CRC字节 | 对Byte0~7的循环冗余校验码 | 否 |
注意到,上表中增加了一列“是否可映射到EEPROM”,可以把这个EEPROM理解成保险箱,我们把比较重要的数据存进去,断电后其他数据被清除,保险箱的数据依然保留,这样下次上电时这些数据就可以直接取出来用了。这个过程用DS自己的语言表述如下:
Write Scratchpad[0x4e]:单片机通过 向暂存器中写入TH、TL和配置数据;(努力挣钱)
Read Scratchpad[0xbe]:单片机通过 向暂存器中读取TH、TL和配置数据,但可以随时通过复位信号终止读取;(合理花钱)
Copy Scratchpad[0x48] :把暂存器中的TH、TL和配置数据复制到EEPROM;(往保险箱存钱)
Recall EE[0xb8] :(再次上电后)把EEPROM中的TH、TL和配置数据复制到暂存器(从保险箱取钱)
EEPROM(非易失性存储器)
这个存储器相当于保险箱,用于保护报警阈值和分辨率配置,掉电也不丢失。这个保险箱很小,只存了三样东西
| Address 0 | Byte2 (TH) | 高温报警器 |
| Address 1 | Byte 3(TL) | 低温报警器 |
| Address 2 | Byte 4(Configuration) | 分辨率配置 |
EEPROM的主要功能在介绍暂存器中就提过了,分别是存钱和取钱
Copy Scratchpad[0x48] :把暂存器中的TH、TL和配置数据复制到EEPROM;(存钱)
Recall EE[0xb8] :(再次上电后)把EEPROM中的TH、TL和配置数据复制到暂存器(取钱)
现在,我们清楚了DS18B20三个存储器中,ROM存放“身份证号”,暂存器存放温度数据、报警阈值和配置数据,EEPROM“存钱”,如此多功能,如此多数据,我们来看看势单力薄的DQ引脚是如何连接两者的。

2.代码分析
复位函数
void ds18b20_reset(void)
{
DS18B20_PORT=0;//拉低DQ
delay_10us(75);//拉低750us,
DS18B20_PORT=1;//DQ=1
delay_10us(2);//20US
}
DS18B20的通信流程始于初始化序列,该序列由主设备发送的复位脉冲和DS18B20发出的应答脉冲组成,上面代码体现的是复位脉冲部分。为什么要先拉低DQ一段时间再拉高呢?我们看一下DS手册里的初始化时序图


左边部分是复位脉冲,先拉低总线(DQ)最少480微秒,代码中拉低了750微秒;之后再缓慢拉高
检测函数
u8 ds18b20_check(void)
{
u8 time_temp=0;
while(DS18B20_PORT&&time_temp<10)//等待DQ为低电平,循环检测DQ引脚(DS_PORT)是否为低电平
{
time_temp++;
delay_10us(1);
}
if(time_temp>=10)return 1;//如果超时则强制返回1
else time_temp=0;//如果没超时,就把time_temp复位
while((!DS18B20_PORT)&&time_temp<20)//等待DQ为高电平
{
time_temp++;
delay_10us(1);
}
if(time_temp>=20)return 1;//如果超时则强制返回1
return 0;
}
在复位完成之后,DS还需要产生一个60~240微秒的低电平,以此向单片机表明 “我活着呢”。上面代码的目的就是产生这个60~240微秒的低平。如何产生呢?
while(DS18B20_PORT&&time_temp<10)//等待DQ为低电平,循环检测DQ引脚(DS_PORT)是否为低电平
{
time_temp++;
delay_10us(1);
}
DS18B20_PORT就是DQ,循环检测10次DQ是否是低电平,即是否有100微秒的低电平状态
if(time_temp>=10)return 1;//如果超时则强制返回1
else time_temp=0;//如果没超时,就把time_temp复位
如果检测10次之后第11次DQ还是低电平,一定是这孩子睡着了,就强制返回1;
如果检测十次之内DQ能自己拉高,就把计数器time_temp重启,以备下一次检测。
while((!DS18B20_PORT)&&time_temp<20)//等待DQ为高电平
{
time_temp++;
delay_10us(1);
}
if(time_temp>=20)return 1;//如果超时则强制返回1
DQ自己拉高后,还要检测它拉高了多少微秒,因为上升沿要保持至少200微秒

按位读取
u8 ds18b20_read_bit(void)
{
u8 dat=0;
DS18B20_PORT=0;
_nop_();_nop_();
DS18B20_PORT=1;
_nop_();_nop_();
if(DS18B20_PORT)dat=1;
else dat=0;
delay_10us(5);//确保整个时隙长度
return dat;
}
DS18初始化完成后,就要进行数据传输,以上代码中,单片机通过DQ从DS18中读一个位。我们注意到,这里出现了和初始化的复位很像的拉低延时再拉高的操作,我们看看写数据的时序图

单片机要从DS18中读取数据,需要自己产生一个60微秒的“读取时隙”,且时隙之间需预留至少 1µs 的恢复时间。主设备通过将单线总线拉低至少 1µs 来触发读取时隙(即上图的“L”黑线)
DS18B20_PORT=0;
_nop_();_nop_();//_nop_()通常产生1~2微秒延时,两个nop满足大于1微秒的条件
DS18B20_PORT=1;
_nop_();_nop_();
随后单片机从DS18中读取位数据,若读取低位,就如左图;若读取高位,就如右图。无论读取到什么数据,读取所花的时间必须保持在15微秒之内
if(DS18B20_PORT)dat=1;
else dat=0;
最后再补上一段延时(50微秒),确保整个读取时隙的时长。
字节读取
u8 ds18b20_read_byte(void)
{
u8 i=0;
u8 dat=0;
u8 temp=0;
for(i=0;i<8;i++)//循环8次,每次读取一位,且先读低位再读高位,读取八次位就形成一个字节
{
temp=ds18b20_read_bit();
dat=(temp<<7)|(dat>>1);//dat右移1位,并将temp的第0位填入dat的最高位
}
return dat;
}
这部分就与DS18内部特性没什么关系了,原理是用8次循环读取8个位,然后把串行输入并行输出成8位的dat
字节写入
void ds18b20_write_byte(u8 dat)
{
u8 i=0;
u8 temp=0;
for(i=0;i<8;i++)//循环8次,每次写一位,且先写低位再写高位
{
temp=dat&0x01;//选择低位准备写入,这部分属于采样窗口
dat>>=1;//将次高位移到低位
if(temp)
{
DS18B20_PORT=0;
_nop_();_nop_();
DS18B20_PORT=1;
delay_10us(6);//进行一次读取时隙
}
else
{
DS18B20_PORT=0;
delay_10us(6);
DS18B20_PORT=1;
_nop_();_nop_();
}
}
}
这部分原理是,从最低位开始读取dat(dat是8位数据),若读取到高位,则向DS18中写入1;若读取到低位,则向DS18中写入0。一共循环8次(刚好一个字节),每次读取后,dat就右移一位,原来的次低位就变成低位继续被读取。
开启温转
void ds18b20_start(void)
{
ds18b20_reset();//复位
ds18b20_check();//检查DS18B20
ds18b20_write_byte(0xcc);//同时控制总线所有设备
ds18b20_write_byte(0x44);//转换命令
}
文章开头提过,DS18是数字温度计,因为它能自己进行模数转换,上面代码就是启动模数转换
首先初始化(复位和检测);然后跳过Read ROM指令,也就是不看身份证,直接控制DS18;最后向DS18输入AD转换命令,这样DS18内部就会自动把采集的模拟量温度转换成数字量。
初始化
u8 ds18b20_init(void)
{
ds18b20_reset();
return ds18b20_check();
}
为什么DS18也需要初始化?因为这是1-Wire协议的标准流程
每次新的通信会话都必须以复位和存在检测开始
温度读取
float ds18b20_read_temperture(void)
{
float temp;
u8 dath=0;
u8 datl=0;
u16 value=0;
ds18b20_start();
ds18b20_reset();
ds18b20_check();
ds18b20_write_byte(0xcc);
ds18b20_write_byte(0xbe);
datl=ds18b20_read_byte();
dath=ds18b20_read_byte();
value=(dath<<8)+datl;
if((value&0xf800)==0xf800)
{
value=(~value)+1;
temp=value*(-0.0625);
}
else
{
temp=value*0.0625;
}
return temp;
}
这部分就是从DS18获取数字温度值的函数,我们来看看它是如何运作的
首先定义温度值为浮点数(因为温度有零有整,有正有负);
然后依次开启AD转换、初始化、跳过ROM(不看身份证)。完成准备工作后,可以进行读取了
ds18b20_write_byte(0xbe);
以上命令是Read Scratchpad[0xbe],通过DQ从暂存器字节0的最低有效位开始,直到字节9(即CRC字节)
| Byte 0 | 温度LSB | 温度值低四位(包含小数部分) | 否(运行时数据) |
| Byte 1 | 温度MSB | 温度值高 8 位(包含符号位) | 否(运行时数据) |
| Byte 2 | TH寄存器 | 高温报警阈值 | 是(字节 2 的 EEPROM 备份) |
| Byte 3 | TL寄存器 | 低温报警阈值 | 是(字节 3 的 EEPROM 备份) |
| Byte 4 | 配置寄存器 | 分辨率设置 | 是(字节 4 的 EEPROM 备份) |
| Byte 5 | 保留 | 否 | |
| Byte 6 | 保留 | 否 | |
| Byte 7 | 保留 | 否 | |
| Byte 8 | CRC字节 | 对Byte0~7的循环冗余校验码 | 否 |
从上表也可以看出,温度数据是先传低位再传高位的,所以可以借助时序特点把高位和低位分别存储起来:
datl=ds18b20_read_byte();
dath=ds18b20_read_byte();
然后就是索然无味的移位、合并流程。最后通过(value&0xf800)==0xf800确定温度值是正值还是负值,把转码得到的温度值(DS18的温度值是以16位二进制补码形式存储,所以需要转码)乘以精度(0.0625)得到对应分辨率下的温度值。
至此,我们已经分析完成8个DS18B20函数的功能,这部分实际上已经算是模块化了,也就是说我们已经完成了温度检测部分,剩下的显示,无论是数码管、LCD1602,又或者是LCD12864,都可以完成,但考虑到数码管原理比较简单,所以这里我们选择数码管显示温度。
二、数码管部分
//共阴极数码管显示0~F的段码数据
u8 gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
void smg_display(u8 dat[],u8 pos)
{
u8 i=0;
u8 pos_temp=pos-1;
for(i=pos_temp;i<8;i++)
{
switch(i)//位选
{
case 0: LSC=1;LSB=1;LSA=1;break;
case 1: LSC=1;LSB=1;LSA=0;break;
case 2: LSC=1;LSB=0;LSA=1;break;
case 3: LSC=1;LSB=0;LSA=0;break;
case 4: LSC=0;LSB=1;LSA=1;break;
case 5: LSC=0;LSB=1;LSA=0;break;
case 6: LSC=0;LSB=0;LSA=1;break;
case 7: LSC=0;LSB=0;LSA=0;break;
}
SMG_A_DP_PORT=dat[i-pos_temp];//传送段选数据
delay_10us(100);//延时一段时间,等待显示稳定
SMG_A_DP_PORT=0x00;//消音
}
}
开头的gsmg_code数组实际上存储了共阴极数码管0~F形状的段选数据。然后输入起始位置(pos),8位数码管就会从这一位开始,依次向右移动,移动的同时会填入相应的段选数据
三、主函数部分
#include "public.h"
#include "smg.h"
#include "ds18b20.h"
void main()
{
u8 i=0;
int temp_value;
u8 temp_buf[5];
ds18b20_init();//初始化DS18B20
while(1)
{
i++;
if(i%50==0)//间隔一段时间读取温度值,间隔时间要大于温度传感器转换温度时间
temp_value=ds18b20_read_temperture()*10;//保留温度值小数后一位
if(temp_value<0)//负温度
{
temp_value=-temp_value;
temp_buf[0]=0x40;//显示负号
}
else temp_buf[0]=0x00;//不显示
temp_buf[1]=gsmg_code[temp_value/1000];//百位
temp_buf[2]=gsmg_code[temp_value%1000/100];//十位
temp_buf[3]=gsmg_code[temp_value%1000%100/10]|0x80;//个位+小数点
temp_buf[4]=gsmg_code[temp_value%1000%100%10];//小数点后一位
smg_display(temp_buf,4);
}
}
相较于DS18,主函数部分就很好解释了,可以按作用分成几个部分:显示位数定义、读取温度值、进位显示
显示位数定义
u8 temp_buf[5];
定义一个5位数组,包括符号位、百位、十位、个位(小数点与个位共用一位)、小数点后一位。
读取温度值
if(i%50==0)//间隔一段时间读取温度值,间隔时间要大于温度传感器转换温度时间
temp_value=ds18b20_read_temperture()*10;//保留温度值小数后一位
if(temp_value<0)//负温度
{
temp_value=-temp_value;
temp_buf[0]=0x40;//显示负号
}
else temp_buf[0]=0x00;//不显示
读取温度间隙不能过短,所以设置i作为计时器、每50次循环读取一次温度;然后检测温度正负值:若为正,直接显示;若为负,翻转后加个负号再显示。
进位显示
temp_buf[1]=gsmg_code[temp_value/1000];//百位
temp_buf[2]=gsmg_code[temp_value%1000/100];//十位
temp_buf[3]=gsmg_code[temp_value%1000%100/10]|0x80;//个位+小数点
temp_buf[4]=gsmg_code[temp_value%1000%100%10];//小数点后一位
smg_display(temp_buf,4);
因为前面在读取温度时乘了10,相当于只保留一位小数,所以这里的百位就从原来的value/100变成value/1000,后面同理。在将温度值四位数据都写进数组tem_buf[]后,数码管显示函数从8位数码管的第四位开始依次显示。

网硕互联帮助中心







评论前必须登录!
注册