DS18B20 是美信公司的一款溫度傳感器,單片機(jī)可以通過(guò) 1-Wire 協(xié)議與 DS18B20 進(jìn)行通信,最終將溫度讀出。1-Wire 總線的硬件接口很簡(jiǎn)單,只需要把 DS18B20 的數(shù)據(jù)引腳和單片機(jī)的一個(gè) IO 口接上就可以了。硬件的簡(jiǎn)單,隨之而來(lái)的,就是軟件時(shí)序的復(fù)雜。1-Wire總線的時(shí)序比較復(fù)雜,很多同學(xué)在這里獨(dú)立看時(shí)序圖都看不明白,所以這里還要帶著大家來(lái)研究 DS18B20 的時(shí)序圖。我們先來(lái)看一下 DS18B20 的硬件原理圖,如圖16-12所示。
http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/35.png" alt="" />
圖16-12 DS18B20 電路原理圖
DS18B20 通過(guò)編程,可以實(shí)現(xiàn)最高12位的溫度存儲(chǔ)值,在寄存器中,以補(bǔ)碼的格式存儲(chǔ),如圖16-13所示。
http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/36.png" alt="" />
圖16-13 DS18B20 溫度數(shù)據(jù)格式
一共2個(gè)字節(jié),LSB 是低字節(jié),MSB 是高字節(jié),其中 MSb 是字節(jié)的高位,LSb 是字節(jié)的低位。大家可以看出來(lái),二進(jìn)制數(shù)字,每一位代表的溫度的含義,都表示出來(lái)了。其中 S表示的是符號(hào)位,低11位都是2的冪,用來(lái)表示最終的溫度。DS18B20 的溫度測(cè)量范圍是從-55度到+125度,而溫度數(shù)據(jù)的表現(xiàn)形式,有正負(fù)溫度,寄存器中每個(gè)數(shù)字如同卡尺的刻度一樣分布,如圖16-14所示。
http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/37.png" alt="" />
圖16-14 DS18B20 溫度值
二進(jìn)制數(shù)字最低位變化1,代表溫度變化0.0625度的映射關(guān)系。當(dāng)0度的時(shí)候,那就是 0x0000,當(dāng)溫度125度的時(shí)候,對(duì)應(yīng)十六進(jìn)制是 0x07D0,當(dāng)溫度是零下55度的時(shí)候,對(duì)應(yīng)的數(shù)字是 0xFC90。反過(guò)來(lái)說(shuō),當(dāng)數(shù)字是 0x0001 的時(shí)候,那溫度就是0.0625度了。
首先,我先根據(jù)手冊(cè)上 DS18B20 工作協(xié)議過(guò)程大概講解一下。
1)初始化 和 I2C 的尋址類似,1-Wire 總線開始也需要檢測(cè)這條總線上是否存在 DS18B20 這個(gè)器件。如果這條總線上存在 DS18B20,總線會(huì)根據(jù)時(shí)序要求返回一個(gè)低電平脈沖,如果不存在的話,也就不會(huì)返回脈沖,即總線保持為高電平,所以習(xí)慣上稱之為檢測(cè)存在脈沖。此外,獲取存在脈沖不僅僅是檢測(cè)是否存在 DS18B20,還要通過(guò)這個(gè)脈沖過(guò)程通知 DS18B20 準(zhǔn)備好,單片機(jī)要對(duì)它進(jìn)行操作了,如圖16-15所示。
http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/38.png" alt="" />
圖16-15 檢測(cè)存在脈沖
大家注意看圖,實(shí)粗線是我們的單片機(jī) IO 口拉低這個(gè)引腳,虛粗線是 DS18B20 拉低這個(gè)引腳,細(xì)線是單片機(jī)和 DS18B20 釋放總線后,依靠上拉電阻的作用把 IO 口引腳拉上去。這個(gè)我們前邊提到過(guò)了,51單片機(jī)釋放總線就是給高電平。
存在脈沖檢測(cè)過(guò)程,首先單片機(jī)要拉低這個(gè)引腳,持續(xù)大概 480 us 到 960 us 之間的時(shí)間即可,我們的程序中持續(xù)了 500 us。然后,單片機(jī)釋放總線,就是給高電平,DS18B20 等待大概15到 60 us 后,會(huì)主動(dòng)拉低這個(gè)引腳大概是60到 240 us,而后 DS18B20 會(huì)主動(dòng)釋放總線,這樣 IO 口會(huì)被上拉電阻自動(dòng)拉高。
有的同學(xué)還是不能夠徹底理解,程序列出來(lái)逐句解釋。首先,由于 DS18B20 時(shí)序要求非常嚴(yán)格,所以在操作時(shí)序的時(shí)候,為了防止中斷干擾總線時(shí)序,先關(guān)閉總中斷。然后第一步,拉低 DS18B20 這個(gè)引腳,持續(xù) 500 us;第二步,延時(shí) 60 us;第三步,讀取存在脈沖,并且等待存在脈沖結(jié)束。
bit Get18B20Ack(){
bit ack;
EA = 0; //禁止總中斷
IO_18B20 = 0; //產(chǎn)生 500us 復(fù)位脈沖
DelayX10us(50);
IO_18B20 = 1;
DelayX10us(6); //延時(shí) 60us
ack = IO_18B20; //讀取存在脈沖
while(!IO_18B20); //等待存在脈沖結(jié)束
EA = 1; //重新使能總中斷
return ack;
}
很多同學(xué)對(duì)第二步不理解,時(shí)序圖上明明是 DS18B20 等待 15 us 到 60 us,為什么要延時(shí)60 us 呢?舉個(gè)例子,媽媽在做飯,告訴你大概5分鐘到10分鐘飯就可以吃了,那么我們什么時(shí)候去吃,能夠絕對(duì)保證吃上飯呢?很明顯,10分鐘以后去吃肯定可以吃上飯。同樣的道理,DS18B20 等待大概是 15 us 到 60 us,我們要保證讀到這個(gè)存在脈沖,那么 60 us 以后去讀肯定可以讀到。當(dāng)然,不能延時(shí)太久,太久,超過(guò) 75 us,就可能讀不到了,為什么是 75 us,大家自己思考一下。
2)ROM 操作指令 我們學(xué) I2C 總線的時(shí)候就了解到,總線上可以掛多個(gè)器件,通過(guò)不同的器件地址來(lái)訪問(wèn)不同的器件。同樣,1-Wire 總線也可以掛多個(gè)器件,但是它只有一條線,如何區(qū)分不同的器件呢?
在每個(gè) DS18B20 內(nèi)部都有一個(gè)唯一的64位長(zhǎng)的序列號(hào),這個(gè)序列號(hào)值就存在 DS18B20 內(nèi)部的 ROM 中。開始的8位是產(chǎn)品類型編碼(DS18B20 是 0x10),接著的48位是每個(gè)器件唯一的序號(hào),最后的8位是 CRC 校驗(yàn)碼。DS18B20 可以引出去很長(zhǎng)的線,最長(zhǎng)可以到幾十米,測(cè)不同位置的溫度。單片機(jī)可以通過(guò)和 DS18B20 之間的通信,獲取每個(gè)傳感器所采集到的溫度信息,也可以同時(shí)給所有的 DS18B20 發(fā)送一些指令。這些指令相對(duì)來(lái)說(shuō)比較復(fù)雜,而且應(yīng)用很少,所以這里大家有興趣的話就自己去查手冊(cè)完成吧,我們這里只講一條總線上只接一個(gè)器件的指令和程序。
Skip ROM(跳過(guò) ROM):0xCC。當(dāng)總線上只有一個(gè)器件的時(shí)候,可以跳過(guò) ROM,不進(jìn)行 ROM 檢測(cè)。
3)RAM 存儲(chǔ)器操作指令 RAM 讀取指令,只講2條,其它的大家有需要可以隨時(shí)去查資料。 Read Scratchpad(讀暫存寄存器):0xBE
這里要注意的是,DS18B20 的溫度數(shù)據(jù)是2個(gè)字節(jié),我們讀取數(shù)據(jù)的時(shí)候,先讀取到的是低字節(jié)的低位,讀完了第一個(gè)字節(jié)后,再讀高字節(jié)的低位,直到兩個(gè)字節(jié)全部讀取完畢。
Convert Temperature(啟動(dòng)溫度轉(zhuǎn)換):0x44
當(dāng)我們發(fā)送一個(gè)啟動(dòng)溫度轉(zhuǎn)換的指令后,DS18B20 開始進(jìn)行轉(zhuǎn)換。從轉(zhuǎn)換開始到獲取溫度,DS18B20 是需要時(shí)間的,而這個(gè)時(shí)間長(zhǎng)短取決于 DS18B20 的精度。前邊說(shuō) DS18B20 最高可以用12位來(lái)存儲(chǔ)溫度,但是也可以用11位,10位和9位一共四種格式。位數(shù)越高,精度越高,9位模式最低位變化1個(gè)數(shù)字溫度變化0.5度,同時(shí)轉(zhuǎn)換速度也要快一些,如圖16-16所示。
http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/39.png" alt="" />
圖16-16 DS18B20 溫度轉(zhuǎn)換時(shí)間
其中寄存器 R1 和 R0 決定了轉(zhuǎn)換的位數(shù),出廠默認(rèn)值就11,也就是12位表示溫度,最大的轉(zhuǎn)換時(shí)間是 750 ms。當(dāng)啟動(dòng)轉(zhuǎn)換后,至少要再等 750 ms 之后才能讀取溫度,否則讀到的溫度有可能是錯(cuò)誤的值。這就是為什么很多同學(xué)讀 DS18B20 的時(shí)候,第一次讀出來(lái)的是85度,這個(gè)值要么是沒有啟動(dòng)轉(zhuǎn)換,要么是啟動(dòng)轉(zhuǎn)換了,但還沒有等待一次轉(zhuǎn)換徹底完成,讀到的是一個(gè)錯(cuò)誤的數(shù)據(jù)。
4)DS18B20 的位讀寫時(shí)序 DS18B20 的時(shí)序圖不是很好理解,大家對(duì)照時(shí)序圖,結(jié)合我的解釋,一定要把它學(xué)明白。寫時(shí)序圖如圖16-17所示。
http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/40.png" alt="" />
圖16-17 DS18B20 位寫入時(shí)序
當(dāng)要給 DS18B20 寫入0的時(shí)候,單片機(jī)直接將引腳拉低,持續(xù)時(shí)間大于 60 us 小于 120 us就可以了。圖上顯示的意思是,單片機(jī)先拉低 15 us 之后,DS18B20 會(huì)在從 15 us 到 60 us 之間的時(shí)間來(lái)讀取這一位,DS18B20 最早會(huì)在 15 us 的時(shí)刻讀取,典型值是在 30 us 的時(shí)刻讀取,最多不會(huì)超過(guò) 60us,DS18B20 必然讀取完畢,所以持續(xù)時(shí)間超過(guò) 60 us 即可。
當(dāng)要給 DS18B20 寫入1的時(shí)候,單片機(jī)先將這個(gè)引腳拉低,拉低時(shí)間大于 1 us,然后馬上釋放總線,即拉高引腳,并且持續(xù)時(shí)間也要大于 60 us。和寫0類似的是,DS18B20 會(huì)在 15 us 到 60 us 之間來(lái)讀取這個(gè)1。
可以看出來(lái),DS18B20 的時(shí)序比較嚴(yán)格,寫的過(guò)程中最好不要有中斷打斷,但是在兩個(gè)“位”之間的間隔,是大于1小于無(wú)窮的,那在這個(gè)時(shí)間段,我們是可以開中斷來(lái)處理其它程序的。發(fā)送即寫入一個(gè)字節(jié)的數(shù)據(jù)程序如下。
void Write18B20(unsigned char dat){
unsigned char mask;
EA = 0; //禁止總中斷
for (mask=0x01; mask!=0; mask<<=1){ //低位在先,依次移出 8 個(gè) bit
IO_18B20 = 0; //產(chǎn)生 2us 低電平脈沖
_nop_();
_nop_();
if ((mask&dat) == 0){ //輸出該 bit 值
IO_18B20 = 0;
}else{
IO_18B20 = 1;
}
DelayX10us(6); //延時(shí) 60us
IO_18B20 = 1; //拉高通信引腳
}
EA = 1; //重新使能總中斷
}
讀時(shí)序圖如圖16-18所示。
http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/41.png" alt="" />
圖16-18 DS18B20 位讀取時(shí)序
當(dāng)要讀取 DS18B20 的數(shù)據(jù)的時(shí)候,我們的單片機(jī)首先要拉低這個(gè)引腳,并且至少保持 1 us 的時(shí)間,然后釋放引腳,釋放完畢后要盡快讀取。從拉低這個(gè)引腳到讀取引腳狀態(tài),不能超過(guò) 15 us。大家從圖16-18可以看出來(lái),主機(jī)采樣時(shí)間,也就是 MASTER SAMPLES,是在 15 us 之內(nèi)必須完成的,讀取一個(gè)字節(jié)數(shù)據(jù)的程序如下。
unsigned char Read18B20({
unsigned char dat;
unsigned char mask;
EA = 0; //禁止總中斷
for (mask=0x01; mask!=0; mask<<=1){ //低位在先,依次采集 8 個(gè) bit
IO_18B20 = 0; //產(chǎn)生 2us 低電平脈沖
_nop_();
_nop_();
IO_18B20 = 1; //結(jié)束低電平脈沖,等待 18B20 輸出數(shù)據(jù)
_nop_(); //延時(shí) 2us
_nop_();
if (!IO_18B20){ //讀取通信引腳上的值
dat &= ~mask;
}else{
dat |= mask;
}
DelayX10us(6); //再延時(shí) 60us
}
EA = 1; //重新使能總中斷
return dat;
}
DS18B20 所表示的溫度值中,有小數(shù)和整數(shù)兩部分。常用的帶小數(shù)的數(shù)據(jù)處理方法有兩種,一種是定義成浮點(diǎn)型直接處理,第二種是定義成整型,然后把小數(shù)和整數(shù)部分分離出來(lái),在合適的位置點(diǎn)上小數(shù)點(diǎn)即可。我們?cè)诔绦蛑惺褂玫氖堑诙N方法,下面我們就寫一個(gè)程序,將讀到的溫度值顯示在 1602 液晶上,并且保留一位小數(shù)位。
/***************************DS18B20.c 文件程序源代碼****************************/
#include <reg52.h>
#include <intrins.h>
sbit IO_18B20 = P3^2; //DS18B20 通信引腳
/* 軟件延時(shí)函數(shù),延時(shí)時(shí)間(t*10)us */
void DelayX10us(unsigned char t){
do {
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
} while (--t);
}
/* 復(fù)位總線,獲取存在脈沖,以啟動(dòng)一次讀寫操作 */
bit Get18B20Ack(){
bit ack;
EA = 0; //禁止總中斷
IO_18B20 = 0; //產(chǎn)生 500us 復(fù)位脈沖
DelayX10us(50);
IO_18B20 = 1;
DelayX10us(6); //延時(shí) 60us
ack = IO_18B20; //讀取存在脈沖
while(!IO_18B20); //等待存在脈沖結(jié)束
EA = 1; //重新使能總中斷
return ack;
}
/* 向 DS18B20 寫入一個(gè)字節(jié),dat-待寫入字節(jié) */
void Write18B20(unsigned char dat){
unsigned char mask;
EA = 0; //禁止總中斷
for (mask=0x01; mask!=0; mask<<=1){ //低位在先,依次移出 8 個(gè) bit
IO_18B20 = 0; //產(chǎn)生 2us 低電平脈沖
_nop_();
_nop_();
if ((mask&dat) == 0){ //輸出該 bit 值
IO_18B20 = 0;
}else{
IO_18B20 = 1;
}
}
DelayX10us(6); //延時(shí) 60us
IO_18B20 = 1; //拉高通信引腳
EA = 1; //重新使能總中斷
}
/* 從 DS18B20 讀取一個(gè)字節(jié),返回值-讀到的字節(jié) */
unsigned char Read18B20(){
unsigned char dat;
unsigned char mask;
EA = 0; //禁止總中斷
for (mask=0x01; mask!=0; mask<<=1){ //低位在先,依次采集 8 個(gè) bit
IO_18B20 = 0; //產(chǎn)生 2us 低電平脈沖
_nop_();
_nop_();
IO_18B20 = 1; //結(jié)束低電平脈沖,等待 18B20 輸出數(shù)據(jù)
_nop_(); //延時(shí) 2us
_nop_();
if (!IO_18B20){ //讀取通信引腳上的值
dat &= ~mask;
}else{
dat |= mask;
}
DelayX10us(6); //再延時(shí) 60us
}
EA = 1; //重新使能總中斷
return dat;
}
/* 啟動(dòng)一次 18B20 溫度轉(zhuǎn)換,返回值-表示是否啟動(dòng)成功 */
bit Start18B20(){
bit ack;
ack = Get18B20Ack(); //執(zhí)行總線復(fù)位,并獲取 18B20 應(yīng)答
if (ack == 0){ //如 18B20 正確應(yīng)答,則啟動(dòng)一次轉(zhuǎn)換
Write18B20(0xCC); //跳過(guò) ROM 操作
Write18B20(0x44); //啟動(dòng)一次溫度轉(zhuǎn)換
}
return ~ack; //ack==0 表示操作成功,所以返回值對(duì)其取反
}
/* 讀取 DS18B20 轉(zhuǎn)換的溫度值,返回值-表示是否讀取成功 */
bit Get18B20Temp(int *temp){
bit ack;
unsigned char LSB, MSB; //16bit 溫度值的低字節(jié)和高字節(jié)
ack = Get18B20Ack(); //執(zhí)行總線復(fù)位,并獲取 18B20 應(yīng)答
if (ack == 0){ //如 18B20 正確應(yīng)答,則讀取溫度值
Write18B20(0xCC); //跳過(guò) ROM 操作
Write18B20(0xBE); //發(fā)送讀命令
LSB = Read18B20(); //讀溫度值的低字節(jié)
MSB = Read18B20(); //讀溫度值的高字節(jié)
*temp = ((int)MSB << 8) + LSB; //合成為 16bit 整型數(shù)
}
return ~ack; //ack==0 表示操作應(yīng)答,所以返回值為其取反值
}
/*Lcd1602.c 文件程序源代碼***/ (此處省略,可參考之前章節(jié)的代碼)
/*****************************main.c 文件程序源代碼******************************/
#include <reg52.h>
bit flag1s = 0; //1s 定時(shí)標(biāo)志
unsigned char T0RH = 0; //T0 重載值的高字節(jié)
unsigned char T0RL = 0; //T0 重載值的低字節(jié)
void ConfigTimer0(unsigned int ms);
unsigned char IntToString(unsigned char *str, int dat);
extern bit Start18B20();
extern bit Get18B20Temp(int *temp);
extern void InitLcd1602();
extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);
void main(){
bit res;
int temp; //讀取到的當(dāng)前溫度值
int intT, decT; //溫度值的整數(shù)和小數(shù)部分
unsigned char len;
unsigned char str[12];
EA = 1; //開總中斷
ConfigTimer0(10); //T0 定時(shí) 10ms
Start18B20(); //啟動(dòng) DS18B20
InitLcd1602(); //初始化液晶
while (1){
if (flag1s){ //每秒更新一次溫度
flag1s = 0;
res = Get18B20Temp(&temp); //讀取當(dāng)前溫度
if (res){ //讀取成功時(shí),刷新當(dāng)前溫度顯示
intT = temp >> 4; //分離出溫度值整數(shù)部分
decT = temp & 0xF; //分離出溫度值小數(shù)部分
len = IntToString(str, intT); //整數(shù)部分轉(zhuǎn)換為字符串
str[len++] = '.'; //添加小數(shù)點(diǎn)
decT = (decT*10) / 16; //二進(jìn)制的小數(shù)部分轉(zhuǎn)換為 1 位十進(jìn)制位
str[len++] = decT + '0'; //十進(jìn)制小數(shù)位再轉(zhuǎn)換為 ASCII 字符
while (len < 6){ //用空格補(bǔ)齊到 6 個(gè)字符長(zhǎng)度
str[len++] = ' ';
}
str[len] = '\0'; //添加字符串結(jié)束符
LcdShowStr(0, 0, str); //顯示到液晶屏上
}else{ //讀取失敗時(shí),提示錯(cuò)誤信息
LcdShowStr(0, 0, "error!");
}
Start18B20(); //重新啟動(dòng)下一次轉(zhuǎn)換
}
}
}
/* 整型數(shù)轉(zhuǎn)換為字符串,str-字符串指針,dat-待轉(zhuǎn)換數(shù),返回值-字符串長(zhǎng)度 */
unsigned char IntToString(unsigned char *str, int dat){
signed char i = 0;
unsigned char len = 0;
unsigned char buf[6];
if (dat < 0){ //如果為負(fù)數(shù),首先取絕對(duì)值,并在指針上添加負(fù)號(hào)
dat = -dat;
*str++ = '-';
len++;
}
do { //先轉(zhuǎn)換為低位在前的十進(jìn)制數(shù)組
buf[i++] = dat % 10;
dat /= 10;
} while (dat > 0);
len += i; //i 最后的值就是有效字符的個(gè)數(shù)
while (i-- > 0){ //將數(shù)組值轉(zhuǎn)換為 ASCII 碼反向拷貝到接收指針上
*str++ = buf[i] + '0';
}
*str = '\0'; //添加字符串結(jié)束符
return len; //返回字符串長(zhǎng)度
}
/* 配置并啟動(dòng) T0,ms-T0 定時(shí)時(shí)間 */
void ConfigTimer0(unsigned int ms){
unsigned long tmp; //臨時(shí)變量
tmp = 11059200 / 12; //定時(shí)器計(jì)數(shù)頻率
tmp = (tmp * ms) / 1000; //計(jì)算所需的計(jì)數(shù)值
tmp = 65536 - tmp; //計(jì)算定時(shí)器重載值
tmp = tmp + 12; //補(bǔ)償中斷響應(yīng)延時(shí)造成的誤差
T0RH = (unsigned char)(tmp>>8); //定時(shí)器重載值拆分為高低字節(jié)
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零 T0 的控制位
TMOD |= 0x01; //配置 T0 為模式 1
TH0 = T0RH; //加載 T0 重載值
TL0 = T0RL;
ET0 = 1; //使能 T0 中斷
TR0 = 1; //啟動(dòng) T0
}
/* T0 中斷服務(wù)函數(shù),完成 1 秒定時(shí) */
void InterruptTimer0() interrupt 1{
static unsigned char tmr1s = 0;
TH0 = T0RH; //重新加載重載值
TL0 = T0RL;
tmr1s++;
if (tmr1s >= 100){ //定時(shí) 1s
tmr1s = 0;
flag1s = 1;
}
}