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