一、概述
DS18B20數字溫度傳感器提供9bit到12bit的攝氏溫度測量精度和一個使用者可程式設計的非易失性且具有過溫和低溫觸發報警的報警功能。DS18B20采用的1-Wire即單總線通信方式,即僅采用一個資料線與微控制器進行通信。該傳感器的溫度監測範圍為-55℃至+125℃,并且在溫度超過-10℃至85℃之外時還具有+-0.5℃的精度。此外,DS18B20可以直接由資料線供電而不需要外部電源供電。(本篇文章重在以簡單例子講清楚該型傳感器最難的部分即工作時序,同時向大家分享例程及自己遇到的程式設計中的“坑”,幫助大家少走彎路盡快上手該型傳感器,而不追求功能上的盡善盡美,是以本文仿真隻能實作正整數溫度值的顯示,對于小數則進行四舍五入後再顯示)
二、重要特性
- 獨特的1-wire總線接口僅需要一個管腳來通信
- 每個裝置的内部ROM上都燒寫了一個獨一無二的64位序列号
- 多路采集能力使得分布式溫度采集應用更加簡單
- 無需外圍元件
- 能夠采用資料線供電;供電範圍為3.0V至5.5V
- 溫度可測量範圍為-55℃至+125℃(-67℉至+257℉)
- 溫度超過-10℃至85℃之外時還具有+-0.5℃的精度
- 内部溫度采集精度可由使用者自定義為9bit至12bit(上電預設12bit)
- 溫度轉換時間在12bit時達到最大值750ms
- 使用者自定義非易失性的報警設定
三、工作指令
- 溫度轉換指令:0x44(即44H),啟動Ds18b20啟動轉換溫度
- 讀暫存器指令:0xBE(即BEH),讀取暫存器中的九位元組資料
- 寫暫存器置零:0x4E(即4EH),把資料寫入暫存器的TH、TL
- 指派暫存器:0x48(即48H),把暫存器中的TH、TL寫入EEPROM中
- 讀電源供電方式:0xB4(即B4H):啟動Ds18b20,發送電源供電方式
- 重調EEPROM:0xB8(即B8H):把EEPROM中的TH、TL讀至暫存器
四·、通過單總線通路DS18B20的順序
- 初始化
- ROM操作指令
- 存儲器操作指令
- 執行/資料
五、工作時序
(一)初始化(複位操作)
在初始化序列期間,總線上的主裝置通過拉低1-wire總線超過480us來發送(TX)複位脈沖。之後主裝置釋放總線而進入接收模式(RX)。當總線釋放後,5KΩ左右的上拉電阻将1-wire總線拉至高電平。當DS18B20檢測到該上升沿後,其等待15us至60us後通過1-wire總線拉低60us至240us來是實作發送一個存在脈沖。
圖5.1 “複位”操作時序圖
根據上述描述及時序圖,可以寫出“複位”操作的子函數:
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。
圖5.2 “寫”操作時序圖
根據上述描述及時序圖,可以寫出“寫”操作的子函數:
/********************************向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需要用括号括起來作為一個整體,否則會出錯。
七、完整例程(例程均為自己編寫且驗證成功)
#include<reg52.h>
#include<intrins.h>
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;i<t;i++)
for(b=199;b>0;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仿真圖
圖8.1 仿真圖