在單片機與器件的有線通信中,並行最簡單了,比方說1602液晶屏。但是代價就是連線多,占用單片機較多的IO口。一般的話,都是使用串行通信方式,其中有UART、SPI、IIC,還有一個我一直以來都比較頭疼的達拉斯公司(現被美信收購)的單總線。單總線,故名思意,就是用1條連線進行通信,比起兩條線的IIC(時鍾線、數據線),單總線在操作上比較困難,對於時序要求比較高。沒有了時鍾線的幫助,它依靠電平持續時間判斷信號,操作難度可想而知。
在這之前,個人曾經購買過兩個價格不菲(主要是價格貴,傳感器又是小得不起眼)的DS18B20。這兩塊都在我手上經過了相同的經歷:起初用用還行,勉強能夠用單片機驅動,可以讀出溫度值來。一段時間沒有用過后,重新拿出來后就出現各色各樣的問題(要不就原來的程序壓根驅動不了,要不就是有溫度,當是讀出來的溫度是芯片內部默認的85度)。就這樣子,毀掉了。所以,對於單總線,我有一種莫名的恐懼。
不過現在,被我花了大半天時間,從閱讀手冊開始,重新開始掌握單總線的編程。當然,也花了幾個鍾頭調試(在protues上面仿真),結果,是看到了成功。現在將其中的一點點心得記載下來。
DS18B20的硬件連接就不多說了,分為外部電源供電與寄生電源供電方式。
其內部方框圖如下圖,64位的ROM(只讀),可以理解為DS18B20的ID。一個暫存器(掉電丟失數據)有8byte:從低字節開始是溫度傳感器的低字節、高字節、上限觸發(用於設置溫度報警上限)、下限觸發、配置寄存器(用於設置溫度傳感器轉化位數,默認12位)、(3字節保留位)、CRC。另外有一個EEPROM(掉電不丟失數據),用來保存暫存器相應的上限觸發、下限觸發、配置寄存器、CRC。
現在,我們需要跟傳感器通信,通信的話一般先要先要有一個初始化,所謂初始化,說白了就是問一下DS18B20在不在,然后,如果DS18B20還“活“着的話,它會給你回話,當然這里的回話很簡單,把原來高電平拉至低電平。(跟IIC差不多,平常狀態是高電平,低電平的話就是非常態)
初始化的時序如下圖:
仔細看這張圖就知道,主控器先將電平拉低,最少拉低480us,然后將總線釋放掉(回到高電平)。然后,DS18B20為了表示存在,會在60-240us這段時間之間把總線拉低。這樣,你可以在將總線釋放掉后大約70us時候,讀取一下總線電平狀態,就可以知道了。
打完了招呼之后,就需要聊天。沒錯,就是向其發送命令或者數據。但你得知道怎么在一條總線上發一位數據。
上圖就是單片機向總線發送1位數據的時序圖。先拉低總線(持續時間要大於1us),然后在15us之內,你要發送你的電平(如果是高電平,釋放掉總線;如果是低電平,就持續拉低電平),在接下來的45us之內,DS18B20會采樣總線電平值。最后你還要把總線釋放掉,便會常態。通常需要一次發個8bit,在每比特之間,要記得至少有1us的延時,當然,延時沒上限。
學會了發送數據,也要學會接受數據。
從總線上面接受數據也跟發送數據一樣,仔細看時序圖,就可以解決了。
通常,我們會寫成發一個字節的函數(而不是一比特)。有了通信的手段,再去看看通信的內容。就像人類可以看懂a,b,c一樣,DS18B20可以看懂的就比較少,而且你得按照規則發送,不然它也看不懂。
在數據手冊上,分為ROM指令:包括MATCH ROM[0x55]、SKIP ROM[0xCC]等。當然SKIP ROM這幾個單詞是給人類看的,真正發送給DS18B20的是0xcc(十六進制)。還有功能指令:包括CONVERT T[0x44]等。
總結來看,你得這樣跟DS18B20交流:
步驟1:初始化(打招呼)
步驟2:ROM操作指令(就是發送指令)
步驟3:DS18B20功能指令(還是發送指令)
當然,可能需要讀取溫度數據,就跟在發送完讀取指令之后。需要提醒一下:各個步驟之間不必沒有空隙,而是需要有個延時(總線一直處於高電平),這個延時很長也不要緊,就是不要太短,不然的話,DS18B20就工作不正常。
下面貼出我寫的代碼:
頭文件部分:(通常頭文件寫常數、變量、宏定義、函數原型,C文件寫函數實體)
#ifndef __hal_ds18b20_h__
#define __hal_ds18b20_h__
#include<reg52.h>
#include"datatype.h"
#include"hal.h"
#include"delay.h"
sbit dq=P1^2;
#define DQ dq
//DS18B20 ROM指令
#define SEARCH_ROM 0xf0
#define READ_ROM 0x33
#define MATH_ROM 0x55
#define SKIP_ROM 0xcc//使用該指令跳過ROM指令
#define ALARM_SEARCH 0xec
//DS18B20 功能指令
#define CONVERT_T 0x44//使用該指令開始轉換溫度
#define WRITE_SCRATCHPAD 0x4e
#define READ_SCRATCHPAD 0xbe//使用該指令讀取溫度值
#define COPY_SCRATCHPAD 0x48
#define RECALL_E2 0xb8
#define READ_POWER_SUPPLY 0xb4
bit hal_ds18b20_init();
void hal_ds18b20_bit_write(bit val);
bit hal_ds18b20_bit_read();
void hal_ds18b20_byte_write(uchar val);
uchar hal_ds18b20_byte_read();
uint hal_ds18b20_get_temp(bit length,uchar * flag);
#endif
C文件部分:
#include"hal_ds18b20.h"
//返回1:表示初始化成功
bit hal_ds18b20_init()
{
//DQ高電平
DQ=0;//先拉低
delay_480us();//等待480us
DQ=1;//再釋放總線,進入等待狀態
delay_70us();//等待70us
return !DQ;
//延時一段時間后,DQ高電平
}
void hal_ds18b20_bit_write(bit val)
{
//DQ高電平
DQ=0;//拉低
delay_8us();//延時8us
if(val)
{
DQ=1;//寫1的話,需要在15us之內拉高
}
//如果寫0,則DQ依舊是低電平
delay_52us();
DQ=1;//經過60us,然后釋放總線
//DQ高電平
}
bit hal_ds18b20_bit_read()
{
bit tmp=0;
//DQ高電平
DQ=0;
_nop_();_nop_();//拉低電平2us
DQ=1;//釋放總線
delay_10us();//時間到此有12us
tmp=DQ;//對總線進行采樣
delay_48us();//時間到此有60us
return tmp;
//此時,DQ被ds18b20釋放
//DQ高電平
}
//單總線要求從最低有效位開始傳送
void hal_ds18b20_byte_write(uchar val)
{
uchar i;
for(i=0;i<8;i++)
{
_nop_();_nop_();//每傳送一位,期間至少間隔1us
if(val&(0x01<<i))
hal_ds18b20_bit_write(1);
else
hal_ds18b20_bit_write(0);
}
}
uchar hal_ds18b20_byte_read()
{
uchar tmp=0,i;
for(i=0;i<8;i++)
{
_nop_();_nop_();//每傳送一位,期間至少間隔1us
if(hal_ds18b20_bit_read())
tmp=tmp|(0x01<<i);
}
return tmp;
}
//參數length=0:返回精確度為1位小數,此時返回值擴大了10倍
//參數length=1:返回精確度為2位小數,此時返回值擴大了100倍
//參數*flag,只是作為溫度正負值標志傳出用。0:正;1:負
//注意接收返回值變量需要是int型的
uint hal_ds18b20_get_temp(bit length,uchar * flag)
{
uint val=0;
uchar tmp1,tmp2;
while(!hal_ds18b20_init())
DELAY_500MS();
delay_ms(1);//發指令期間延時比較重要
hal_ds18b20_byte_write(SKIP_ROM);
delay_ms(1);
hal_ds18b20_byte_write(CONVERT_T);
DELAY_1S();//12bit精度情況下,需要750ms轉換溫度時間
while(!hal_ds18b20_init())
DELAY_500MS();
delay_ms(1);
hal_ds18b20_byte_write(SKIP_ROM);
delay_ms(1);
hal_ds18b20_byte_write(READ_SCRATCHPAD);
delay_ms(1);
tmp1=hal_ds18b20_byte_read();//一共可以讀取9字節
tmp2=hal_ds18b20_byte_read();//這里來只讀取前兩個字節,溫度值
//tmp1低字節 tmp2高字節
*flag=(tmp2&0x80)?1:0;//傳回溫度正負
val=tmp1|tmp2<<8;//組成16位
if(*flag)//溫度值是負數,需要取反加一
val=(~val)+1;
//按照12bit測溫精度來算,擴大了100倍
return ((uint)(val*6.25+(length?0.5:50)))/(length?1:10);
}