一、概述
DS18B20数字温度传感器提供9bit到12bit的摄氏温度测量精度和一个用户可编程的非易失性且具有过温和低温触发报警的报警功能。DS18B20采用的1-Wire即单总线通信方式,即仅采用一个数据线与微控制器进行通信。该传感器的温度监测范围为-55℃至+125℃,并且在温度超过-10℃至85℃之外时还具有+-0.5℃的精度。此外,DS18B20可以直接由数据线供电而不需要外部电源供电。(本篇文章重在以简单例子讲清楚该型传感器最难的部分即工作时序,同时向大家分享例程及自己遇到的编程中的“坑”,帮助大家少走弯路尽快上手该型传感器,而不追求功能上的尽善尽美,因此本文仿真只能实现正整数温度值的显示,对于小数则进行四舍五入后再显示)
二、重要特性
三、工作指令
四·、通过单总线访问DS18B20的顺序
五、工作时序
(一)初始化(复位操作)
在初始化序列期间,总线上的主设备通过拉低1-wire总线超过480us来发送(TX)复位脉冲。之后主设备释放总线而进入接收模式(RX)。当总线释放后,5KΩ左右的上拉电阻将1-wire总线拉至高电平。当DS18B20检测到该上升沿后,其等待15us至60us后通过1-wire总线拉低60us至240us来是实现发送一个存在脉冲。
根据上述描述及时序图,可以写出“复位”操作的子函数:
void Init_Ds(void)//DS18B20初始化
{
Bus=0;//主动拉低480-960us(此处选择600us)
Delay600us();
Bus=1;//释放总线,传感器15-60us后拉低总线
while(Bus);//等待传感器拉低;
while(!Bus);//度过传感器被拉低的时间(60-240us)后主动拉高
Bus=1;//主动拉高
}
(二)控制器的“写”操作(先写低位后写高位)
“写”时段有两种情况:写“1”时段和写“0”时段。控制器通过写1时段来向DS18B20中写入逻辑1以及通过写0时段来向DS18B20中写入逻辑0。每个写时段最小必须有60us的持续时间且堵路的写时段之间至少要有1us的恢复时间。两个写时段都是由控制器通过将1-wire中先拉低来进行初始化(详见图5.2)。
为了形成写1时段,在将1-wire总线拉低后,主设备必须在15us之内释放总线。当总线释放后,5KΩ的上拉电阻将总线拉高;为了形成写0时段,在将1-wire总线拉低后,在整个时段期间控制器必须一直拉低总线(至少60us)。
在控制器初始化写时段后,DS18B20将会在15us至60us的时间窗口对1-wire总线进行采样。如果总线在采样窗口期间是高电平,则逻辑1被写入DS18B20;若总线是低电平,则逻辑0被写入DS18B20。
根据上述描述及时序图,可以写出“写”操作的子函数:
/********************************向DS18B20写入一字节***********************/
void Write_Ds(uchar com)//从低位开始写入
{
uchar mask;
for(mask=0x01;mask!=0;mask<<=1)
{
//该位为0,先拉低,15us后在拉高,并通过延时使整个周期为60us
//该位为1,先拉低并在15us内(此处选择5us)拉高,并通过延时使整个周期为60us
Bus=0;
_nop_();_nop_();_nop_();_nop_();_nop_();//先拉低5us
if((com&mask)==0)//该位是0
{
Bus=0;
}
else//该位是1
{
Bus=1;
}
Delay10us();Delay10us();Delay10us();Delay10us();Delay10us();;//延时60us
_nop_();_nop_();_nop_();_nop_();_nop_();
Bus=1;//拉高
_nop_();_nop_();//写两个位之间至少有1us的间隔(此处选择2us)
}
}
(三)控制器的“读”操作(先读低位后读高位)
仅在读时段期间DS18B20才能向主设备传动数据。因此,主设备在执行完读暂存寄存器[BEh]或读取供电模式[B4h]后,必须及时的生成读时段,这样DS18B20才能提供所需的数据。此外,主设备可以在执行完温度转换[44h]或拷贝EEPROM[B8h]命令后生成读时段,以便获得在“DS18B20功能命令”章节中提到的操作信息。
每个读时段最小必须有60us的持续时间且独立的写时段之间至少间隔1us。读时段通过控制器将总线拉低超过1us再释放总线来实现初始化(详见图5.3)。当控制器初始化完读时段后,DS18B20将会向总线发送0或1。DS18B20将通过拉高总线发送逻辑1,拉低总线发送逻辑0.发送完逻辑0后,DS18B20将会释放总线,在通过上拉电阻将该总线拉至高电平的闲置状态。从DS18B20中输出的数据在初始化读时序后仅有15us的有效时间。因此。控制器再开始改读时段后的15us之内必须释放总线,并且对总线进行采样。
图5.3 “读”操作时序图根据上述描述及时序图,可以写出“读”操作的子函数
/********************************从DS18B20读出一字节***********************/
uchar Read_Ds(void)//先读的是低位,整个读周期至少为60us,但控制器采样要在15us内完成,相邻“位”之间至少间隔1us
{
uchar value=0,mask;
for(mask=0x01;mask!=0;mask<<=1)
{
Bus=0;//先把总线拉低超过1us(此处选择2us)后释放
_nop_();_nop_();
Bus=1;
_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();//再延时6us后读总线数据
if(Bus==0)//如果该位是0
{
value&=(~mask);
}
else
{
value|=mask;
}
Delay10us();Delay10us();Delay10us();Delay10us();Delay10us();//再延时52us,凑够至少60us的采样周期
_nop_();_nop_();
Bus=1;
_nop_();_nop_();//写两个位之间至少有1us的间隔(此处选择2us)
}
return value;
}
六、注意事项(我踩过的坑)
1. 关于延时问题
DS18B20最大的优势之一就是单总线通信,我们通过一根数据线就可以完成诸多操作,但作为代价的是,DS18B20的工作时序十分复杂,因此对定时精度要求极高。平时大家操作定时精度要求不高的传感器可能会养成一个习惯,比如我们已经有了一个1ms且0误差的延时函数,当我们遇到一个20ms的延时需求时,可能会通过for/while循环将延时为1ms的延时函数执行20次。实际上,这样的方式所达到的延时时间的远大于20ms的,但对于定时精度要求不高的传感器,毫秒级的误差不会带来影响,但对于该传感器则不可。所以,在这款传感器的操作中,即使已经有一个10us的延时函数而需要一个20us的延时时,也要重新写一个20us的延时函数,不可将10us的延时函数循环执行两次。
2. 关于总时序问题
该传感器中的所有操作都要遵循“初始化-ROM命令-DS18B20功能命令”的总时序。比如,测量温度的操作要先后经过“初始化-跳过ROM命令-转换温度命令”与“初始化-跳过ROM命令-读取温度命令”这两大步。常犯的错误为“初始化-跳过ROM命令-转换温度命令-读取温度命令”,也就是说认为初始化与ROM命令在操作传感器的最初执行一次即可,这种想法是错误的。
3. 关于编程细节
在自己编程的过程中,遭遇了一个细节性的bug,即将命令值com与掩码mask相与是否为0作为进入if语句内部的判断条件的过程中,判断条件是这么写的if(com&mask==0),而实际应该写为if((com&mask)==0),即com&mask需要用括号括起来作为一个整体,否则会出错。
七、完整例程(例程均为自己编写且验证成功)
/*所用单片机型号为AT89C52,晶振为12MHz,显示模块采用LCD1602液晶屏*/
#include
#include
typedef unsigned char uchar;
typedef unsigned int uint;
sbit Bus=P3^0;//数据单总线
sbit RS=P3^3;
sbit RW=P3^4;
sbit E=P3^5;
void Delay10us(void);//10us延时函数
void Delay600us(void);//600us延时子函数
void Delay(uint n);//LCD1602中延时子函数
void Delay1ms(uint t);//t毫秒延时子函数
void Init_Ds(void);//DS18B20初始化
void Write_Ds(uchar com);//向DS18B20写入一字节
uchar Read_Ds(void);//从DS18B20读出一字节
uint Get_Tem(void);//获取温度值
void Change(uint x);//把整型数值x转换为字符串
void Write_com(uchar com);//写命令子函数
void Write_dat(uchar dat);//写数据子函数
void Init_1602(void);//LCD1602初始化子函数
void Show(uchar x,uchar y,uchar *str);//LCD1602显示子函数
uchar str[4];//储存转换值对应的字符串
void main()
{
unsigned int temp;
Init_1602();
temp=Get_Tem();
Change(temp);
Show(1,1,"T:");
Show(1,3,str);
while(1);
}
/***************************************延时函数体**************************/
void Delay10us(void)//10us延时函数
{
unsigned char a,b;
for(b=1;b>0;b--)
for(a=1;a>0;a--);
}
void Delay600us(void)//600us延时函数
{
unsigned char a,b;
for(b=119;b>0;b--)
for(a=1;a>0;a--);
}
void Delay(uint n)//LCD1602中延时函数
{
uint x,y;
for(x=n;x>0;x--)
for(y=110;y>0;y--);
}
void Delay1ms(uint t)//t毫秒延时函数
{
unsigned char a,b;
uint i;
for(i=0;i0;b--)
for(a=1;a>0;a--);
}
/********************************DS18B20初始化函数*************************/
void Init_Ds(void)//DS18B20初始化
{
Bus=0;//主动拉低480-960us(此处选择600us)
Delay600us();
Bus=1;//释放总线,传感器15-60us后拉低总线
while(Bus);//等待传感器拉低;
while(!Bus);//度过传感器被拉低的时间(60-240us)后主动拉高
Bus=1;//主动拉高
}
/********************************向DS18B20写入一字节***********************/
void Write_Ds(uchar com)//从低位开始写入
{
uchar mask;
for(mask=0x01;mask!=0;mask<<=1)
{
//该位为0,先拉低,15us后在拉高,并通过延时使整个周期为60us
//该位为1,先拉低并在15us内(此处选择5us)拉高,并通过延时使整个周期为60us
Bus=0;
_nop_();_nop_();_nop_();_nop_();_nop_();//先拉低5us
if((com&mask)==0)//该位是0
{
Bus=0;
}
else//该位是1
{
Bus=1;
}
Delay10us();Delay10us();Delay10us();Delay10us();Delay10us();;//延时60us
_nop_();_nop_();_nop_();_nop_();_nop_();
Bus=1;//拉高
_nop_();_nop_();//写两个位之间至少有1us的间隔(此处选择2us)
}
}
/********************************从DS18B20读出一字节***********************/
uchar Read_Ds(void)//先读的是低位,整个读周期至少为60us,但控制器采样要在15us内完成,相邻“位”之间至少间隔1us
{
uchar value=0,mask;
for(mask=0x01;mask!=0;mask<<=1)
{
Bus=0;//先把总线拉低超过1us(此处选择2us)后释放
_nop_();_nop_();
Bus=1;
_nop_();_nop_();_nop_();_nop_();_nop_();_nop_();//再延时6us后读总线数据
if(Bus==0)//如果该位是0
{
value&=(~mask);
}
else
{
value|=mask;
}
Delay10us();Delay10us();Delay10us();Delay10us();Delay10us();//再延时52us,凑够至少60us的采样周期
_nop_();_nop_();
Bus=1;
_nop_();_nop_();//写两个位之间至少有1us的间隔(此处选择2us)
}
return value;
}
/**********************************获取温度值函数***************************/
uint Get_Tem(void)
{
uint temp=0;
float tp;
uchar LSB=0,MSB=0;
Delay1ms(10);//延时10ms度过不稳定期
Init_Ds();//Ds18b20初始化
Delay1ms(1);
Write_Ds(0xcc);//跳过ROM寻址
Write_Ds(0x44);//启动一次温度转换
Delay1ms(1000);//延时1s等待转化
Init_Ds();//Ds18b20初始化
Delay1ms(1);
Write_Ds(0xcc);//跳过ROM寻址
Write_Ds(0xbe);//发送读值命令·
LSB=Read_Ds();
MSB=Read_Ds();
temp=MSB;
temp<<=8;
temp|=LSB;
tp=temp*0.0625;
temp=tp;
if(tp-temp>=0.5)
{
temp+=1;
}
return temp;
}
/******************************把整型数据转换为字符串**********************/
void Change(uint x)
{
str[0]=x/100+48;
str[1]=(x/10)%10+48;
str[2]=x%10+48;
str[3]='\0';
}
/********************************写命令函数体****************************/
void Write_com(uchar com)
{
RS=0;
P2=com;
Delay(5);
E=1;
Delay(5);
E=0;
}
/********************************写数据函数体****************************/
void Write_dat(uchar dat)
{
RS=1;
P2=dat;
Delay(5);
E=1;
Delay(5);
E=0;
}
/*****************************LCD1602初始化函数体*************************/
void Init_1602()
{
uchar i=0;
RW=0;
Write_com(0x38);//屏幕初始化
Write_com(0x0c);//打开显示 无光标 无光标闪烁
Write_com(0x06);//当读或写一个字符是指针后一一位
Write_com(0x01);//清屏
Write_com(0x80);//设置位置
}
/*******************************显示内容函数体**************************/
void Show(uchar x,uchar y,uchar *str)
{
unsigned char addr;
if (x==1)
{
addr=0x00+y-1; //从第一行、第y列开始显示
}
else
{
addr=0x40+y-1; //第二行、第y列开始显示
}
Write_com(addr+0x80);
while (*str!='\0')
{
Write_dat(*str++);
}
}
八、Proteus仿真图