標題: 第13章 DS18B20溫度傳感器和Flash存儲器13.1 [打印本頁]
作者: 卓然塵世間 時間: 2026-4-30 13:12
標題: 第13章 DS18B20溫度傳感器和Flash存儲器13.1
DS18B20是一種常用的溫度傳感器,提供了感知周圍溫度的手段。Flash存儲器是一款常用的數據存儲器件,相比較于EEPROM,FLASH的存儲容量更大、單位成本更低。
本章除了學習這兩個器件外,還要學習控制這兩種器件的兩種通信協議--1-wire總線協議(一般常用)和SPI總線協議(重要且常用)。
13.1 溫度傳感器DS18B20DS18B20是美信公司的一款溫度傳感器,單片機可以通過1-Wire協議與DS18B20進行通信,最終將溫度讀出。1-Wire總線的硬件接口很簡單,只需要將DS18B20的數據引腳和單片機的一個I/O口接上就可以了。先來看一下DS18B20的硬件原理圖,如圖13-1所示。
13-1.png (20.15 KB, 下載次數: 0)
下載附件
2026-4-30 13:10 上傳
圖13-1 DS18B20電路原理圖
DS18B20通過編程,可以實現最高12位的溫度存儲值,在寄存器中,以補碼的格式存儲(補碼的相關內容請自學了解),如圖13-2所示。
13-2.png (9.99 KB, 下載次數: 0)
下載附件
2026-4-30 13:10 上傳
圖13-2 DS18B20溫度數據格式
共兩個字節,LSB是低字節,MSB是高字節,其中MSb是字節的高位,LSb是字節的低位。每一位代表的溫度的含義,都表示了出來。其中S表示的是符號位,低11位都是2的冪,用來表示最終的溫度。DS18B20的溫度測量范圍是從-55度到+125度,而溫度數據的表現形式,有正負溫度,寄存器中每個數字如同卡尺的刻度一樣分布,如圖13-3所示。
13-3.png (39.03 KB, 下載次數: 0)
下載附件
2026-4-30 13:10 上傳
圖13-3 DS18B20溫度值
二進制數字最低位變化1,代表溫度變化0.0625度的映射關系。當0度的時候,就是0x0000,當溫度125度的時候,對應十六進制是0x07D0,當溫度是零下55度的時候,對應的數字是0xFC90。當數字是0x0001的時候,那溫度就是0.0625度了。
首先根據手冊上DS18B20工作協議過程簡單介紹。
(1)初始化。和I2C的尋址類似,1-Wire總線開始也需要檢測這條總線上是否存在DS18B20這個器件。如果這條總線上存在DS18B20,總線會根據時序要求返回一個低電平脈沖,如果不存在的話,也就不會返回脈沖,即總線保持為高電平,所以習慣上稱之為檢測存在脈沖。獲取存在脈沖不僅僅是檢測是否存在DS18B20,還要通過這個脈沖過程通知DS18B20準備好,單片機要對它進行操作了,如圖13-4所示。
13-4.png (150.46 KB, 下載次數: 0)
下載附件
2026-4-30 13:11 上傳
圖13-4 檢測存在脈沖
注意時序圖,實粗線是單片機I/O口拉低這個引腳,虛粗線是DS18B20拉低這個引腳,細線是單片機和DS18B20釋放總線后,依靠上拉電阻的作用把I/O口引腳拉高。前邊介紹過,51單片機釋放總線需要給高電平。
存在脈沖檢測過程,首先單片機要拉低這個引腳,持續大概480us到960us之間的時間,程序中持續了大概500us。然后,單片機釋放總線,就是給高電平,DS18B20等待大概15到60us后,會主動拉低這個引腳大概是60到240us,而后DS18B20會主動釋放總線,這樣I/O口會被上拉電阻自動拉高。
由于DS18B20時序要求非常嚴格,所以在操作時序的時候,為了防止中斷干擾總線時序,先關閉總中斷。第一步,拉低DS18B20這個引腳,持續500us;第二步,延時60us;第三步,讀取存在脈沖,并且等待存在脈沖結束。
bit Get18B20Ack()
{
bit ack;
EA = 0; //禁止總中斷
IO_18B20 = 0; //產生500us復位脈沖
DelayX10us(50);
IO_18B20 = 1;
DelayX10us(6); //延時60us
ack = IO_18B20; //讀取存在脈沖
while(!IO_18B20); //等待存在脈沖結束
EA = 1; //重新使能總中斷
return ack;
}
時序圖上明明是DS18B20等待15us到60us,為什么要延時60us呢?舉個例子,媽媽在做飯,告訴你大概5分鐘到10分鐘飯就可以吃了,那么什么時候去吃,能夠絕對保證吃上飯呢?很明顯,10分鐘以后去吃肯定可以吃上飯。同樣的道理,DS18B20等待大概是15us到60us,要保證讀到這個存在脈沖,那么60us以后去讀肯定可以讀到。當然,不能延時太久,太久,超過75us,就有可能讀不到,為什么是75us,請自己思考一下。
(2)ROM操作指令。學I2C總線的時候就了解到,總線上可以掛多個器件,通過不同的器件地址來訪問不同的器件。同樣,1-Wire總線也可以掛多個器件,但是它只有一條線,如何區分不同的器件呢?
在每個DS18B20內部都有一個唯一的64位長的序列號,這個序列號值就存在DS18B20內部的ROM中。開始的8位是產品類型編碼(DS18B20是0x10),接著的48位是每個器件唯一的序號,如同人的身份證號,最后的8位是CRC校驗碼。DS18B20可以引出去很長的線,最長可以到幾十米,測不同位置的溫度。單片機可以通過和DS18B20之間的通信,獲取每個傳感器所采集到的溫度信息,也可以同時給所有的DS18B20發送一些指令。這些指令相對來說比較復雜,而且應用較少,這里不再贅述。
Skip ROM(跳過ROM):0xCC。當總線上只有一個器件的時候,可以跳過ROM,不進行ROM檢測。
(3)RAM存儲器操作指令。
RAM讀取指令,只講2條,其它的有需要可以查手冊。
Read Scratchpad(讀暫存寄存器):0xBE
這里要注意的是,DS18B20的溫度數據是2個字節,讀取數據的時候,先讀取到的是低字節的低位,讀完了第一個字節后,再讀高字節的低位,直到兩個字節全部讀取完畢。
Convert Temperature(啟動溫度轉換):0x44
當發送啟動溫度轉換的指令后,DS18B20開始轉換。從轉換開始到獲取溫度,DS18B20是需要時間的,而這個時間長短取決于DS18B20的精度。前邊說DS18B20最高可以用12位存儲溫度,但是也可以用11位,10位和9位共四種格式。位數越高,精度越高,9位模式最低位變化1個數字溫度變化0.5度,同時轉換速度也要快一些,如圖13-5所示。
13-5.png (21.59 KB, 下載次數: 0)
下載附件
2026-4-30 13:11 上傳
圖13-5 DS18B20溫度轉換時間
其中寄存器R1和R0決定了轉換的位數,出廠默認值就11,也就是12位表示溫度,最大的轉換時間是750ms。當啟動轉換后,至少要再等750ms之后才能讀取溫度,否則讀到的溫度有可能是錯誤的值。
(4)DS18B20的位讀寫時序比較復雜,結合圖文理解清楚。寫時序圖如圖13-6所示。
13-6.png (166.67 KB, 下載次數: 0)
下載附件
2026-4-30 13:11 上傳
圖13-6 DS18B20位寫入時序
當要給DS18B20寫入0的時候,單片機將引腳拉低,持續時間大于60us小于120us就可以了。圖13-6顯示的意思是,單片機先拉低15us之后,DS18B20會在從第15us到第60us之間的時間來讀取這一位,DS18B20最早會在15us的時刻讀取,典型值是在30us的時刻讀取,最多不會超過60us,DS18B20必然讀取完畢,所以持續時間超過60us,但不超過120us。
當要給DS18B20寫入1的時候,單片機先將這個引腳拉低,拉低時間大于1us,然后釋放總線,即拉高引腳,并且持續時間也要大于60us。和寫0類似的是,DS18B20會在15us到60us之間來讀取這個1。
可以看出來,DS18B20的時序比較嚴格,寫的過程中最好不要有中斷打斷,但是在兩個“位”之間的間隔,是大于1us小于無窮的,那在這個時間段,是可以開中斷來處理其它程序的。發送即寫入一個字節的數據程序如下。
void Write18B20(unsigned char dat)
{
unsigned char mask;
EA = 0; //禁止總中斷
for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次移出8個bit
{
IO_18B20 = 0; //產生2us低電平脈沖
_nop_();
_nop_();
if ((mask&dat) == 0) //輸出該bit值
IO_18B20 = 0;
else
IO_18B20 = 1;
DelayX10us(6); //延時60us
IO_18B20 = 1; //拉高通信引腳
}
EA = 1; //重新使能總中斷
}
讀時序圖如圖13-7所示。
13-7.png (137.07 KB, 下載次數: 0)
下載附件
2026-4-30 13:11 上傳
圖13-7 DS18B20位讀取時序
當要讀取DS18B20的數據的時候,單片機首先要拉低這個引腳,并且至少保持1us的時間,然后釋放引腳,釋放完畢后要盡快讀取。從拉低這個引腳到讀取引腳狀態,不能超過15us。大家從圖13-7可以看出來,主機采樣時間,也就是MASTER SAMPLES,是在15us之內必須完成的,讀取一個字節數據的程序如下。
unsigned char Read18B20()
{
unsigned char dat;
unsigned char mask;
EA = 0; //禁止總中斷
for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次采集8個bit
{
IO_18B20 = 0; //產生2us低電平脈沖
_nop_();
_nop_();
IO_18B20 = 1; //結束低電平脈沖,等待18B20輸出數據
_nop_(); //延時2us
_nop_();
if (!IO_18B20) //讀取通信引腳上的值
dat &= ~mask;
else
dat |= mask;
DelayX10us(6); //再延時60us
}
EA = 1; //重新使能總中斷
return dat;
}
DS18B20所表示的溫度值中,有小數和整數兩部分。常用的帶小數的數據處理方法有兩種,一種是定義成浮點型直接處理,第二種是定義成整型,然后把小數和整數部分分離出來,在合適的位置點上小數點即可,Kingst51程序中使用的是第二種方法。下面就寫一個程序,將讀到的溫度值通過數碼管顯示出來,并且保留一位小數位。
/***************************DS18B20.c文件程序源代碼****************************/
#include <reg52.h>
#include <intrins.h>
sbit IO_18B20 = P3^2; //DS18B20通信引腳
/* 軟件延時函數,延時時間(t*10)us */
void DelayX10us(unsigned char t)
{
do {
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
} while (--t);
}
/* 復位總線,獲取存在脈沖,以啟動一次讀寫操作 */
bit Get18B20Ack()
{
bit ack;
EA = 0; //禁止總中斷
IO_18B20 = 0; //產生500us復位脈沖
DelayX10us(50);
IO_18B20 = 1;
DelayX10us(6); //延時60us
ack = IO_18B20; //讀取存在脈沖
while(!IO_18B20); //等待存在脈沖結束
EA = 1; //重新使能總中斷
return ack;
}
/* 向DS18B20寫入一個字節,dat-待寫入字節 */
void Write18B20(unsigned char dat)
{
unsigned char mask;
EA = 0; //禁止總中斷
for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次移出8個bit
{
IO_18B20 = 0; //產生2us低電平脈沖
_nop_();
_nop_();
if ((mask&dat) == 0) //輸出該bit值
IO_18B20 = 0;
else
IO_18B20 = 1;
DelayX10us(6); //延時60us
IO_18B20 = 1; //拉高通信引腳
}
EA = 1; //重新使能總中斷
}
/* 從DS18B20讀取一個字節,返回值-讀到的字節 */
unsigned char Read18B20()
{
unsigned char dat;
unsigned char mask;
EA = 0; //禁止總中斷
for (mask=0x01; mask!=0; mask<<=1) //低位在先,依次采集8個bit
{
IO_18B20 = 0; //產生2us低電平脈沖
_nop_();
_nop_();
IO_18B20 = 1; //結束低電平脈沖,等待18B20輸出數據
_nop_(); //延時2us
_nop_();
if (!IO_18B20) //讀取通信引腳上的值
dat &= ~mask;
else
dat |= mask;
DelayX10us(6); //再延時60us
}
EA = 1; //重新使能總中斷
return dat;
}
/* 啟動一次18B20溫度轉換,返回值-表示是否啟動成功 */
bit Start18B20()
{
bit ack;
ack = Get18B20Ack(); //執行總線復位,并獲取18B20應答
if (ack == 0) //如18B20正確應答,則啟動一次轉換
{
Write18B20(0xCC); //跳過ROM操作
Write18B20(0x44); //啟動一次溫度轉換
}
return ~ack; //ack==0表示操作成功,所以返回值對其取反
}
/* 讀取DS18B20轉換的溫度值,返回值-表示是否讀取成功 */
bit Get18B20Temp(int *temp)
{
bit ack;
unsigned char LSB, MSB; //16bit溫度值的低字節和高字節
ack = Get18B20Ack(); //執行總線復位,并獲取18B20應答
if (ack == 0) //如18B20正確應答,則讀取溫度值
{
Write18B20(0xCC); //跳過ROM操作
Write18B20(0xBE); //發送讀命令
LSB = Read18B20(); //讀溫度值的低字節
MSB = Read18B20(); //讀溫度值的高字節
*temp = ((int)MSB << 8) + LSB; //合成為16bit整型數
}
return ~ack; //ack==0表示操作應答,所以返回值為其取反值
}
/*****************************main.c文件程序源代碼******************************/
#include <reg52.h>
bit flag1s = 0; //1s定時標志
unsigned char T0RH = 0; //T0重載值的高字節
unsigned char T0RL = 0; //T0重載值的低字節
void ConfigTimer0(unsigned int ms);
extern bit Start18B20();
extern bit Get18B20Temp(int *temp);
void InitLed();
void LedScan();
void LedNumber(unsigned char index, unsigned char num, unsigned char point);
void main()
{
bit res;
int temp; //讀取到的當前溫度值
int intT, decT; //溫度值的整數和小數部分
EA = 1; //開總中斷
InitLed(); //初始化數碼管IO
Start18B20(); //啟動DS18B20
ConfigTimer0(1); //T0定時1ms
while (1)
{
if (flag1s) //每秒更新一次溫度
{
flag1s = 0;
res = Get18B20Temp(&temp); //讀取當前溫度
if (res) //讀取成功時,刷新當前溫度顯示
{
intT = temp >> 4; //分離出溫度值整數部分
decT = temp & 0xF; //分離出溫度值小數部分
decT = (decT*10) / 16; //二進制的小數部分轉換為1位十進制位
LedNumber(0, decT, 0); //顯示小數位
LedNumber(1, intT%10, 1); //顯示整數個位+小數點
LedNumber(2, intT/10%10, 0); //顯示整數十位
}
Start18B20(); //重新啟動下一次轉換
}
}
}
/* 配置并啟動T0,ms-T0定時時間 */
void ConfigTimer0(unsigned int ms)
{
unsigned long tmp; //臨時變量
tmp = 11059200 / 12; //定時器計數頻率
tmp = (tmp * ms) / 1000; //計算所需的計數值
tmp = 65536 - tmp; //計算定時器重載值
tmp = tmp + 33; //補償中斷響應延時造成的誤差
T0RH = (unsigned char)(tmp>>8); //定時器重載值拆分為高低字節
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x01; //配置T0為模式1
TH0 = T0RH; //加載T0重載值
TL0 = T0RL;
ET0 = 1; //使能T0中斷
TR0 = 1; //啟動T0
}
/* T0中斷服務函數,完成1秒定時 */
void InterruptTimer0() interrupt 1
{
static unsigned int tmr1s = 0;
TH0 = T0RH; //重新加載重載值
TL0 = T0RL;
LedScan();
tmr1s++;
if (tmr1s >= 1000) //定時1s
{
tmr1s = 0;
flag1s = 1;
}
}
/*****************************Led.c文件程序源代碼*******************************/
#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
unsigned char code LedChar[] = { //數碼管顯示字符轉換表
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[6] = { //數碼管顯示緩沖區,初值0xFF確保啟動時都不亮
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
/* LED初始化函數 */
void InitLed()
{
P0 = 0xFF;
ENLED = 0;
ADDR3 = 1;
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 1;
}
/* LED動態掃描函數,在定時中斷中調用 */
void LedScan()
{
static unsigned char i = 0; //動態掃描的索引,定義為局部靜態變量
P0 = 0xFF; //顯示消隱
switch (i)
{
case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=LedBuff[0]; break;
case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=LedBuff[1]; break;
case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=LedBuff[2]; break;
case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=LedBuff[3]; break;
case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=LedBuff[4]; break;
case 5: ADDR2=1; ADDR1=0; ADDR0=1; i=0; P0=LedBuff[5]; break;
default: break;
}
}
/* 數碼管上顯示一位數字,index-數碼管位索引(從右到左對應0~5),
num-待顯示的數字,point-代表是否顯示該位上的小數點 */
void LedNumber(unsigned char index, unsigned char num, unsigned char point)
{
LedBuff[index] = LedChar[num]; //輸入數字轉換為數碼管字符0~F
if (point != 0)
{
LedBuff[index] &= 0x7F; //point不為0時點亮當前位的小數點
}
}
作者: hqboy 時間: 2026-4-30 15:16
感謝老師分享,學習了
作者: hurbocn 時間: 2026-5-5 10:06
講的很詳細,感謝你的分享,謝謝
| 歡迎光臨 (http://www.denmoz.com/bbs/) |
Powered by Discuz! X3.1 |