I2C總線驅(qū)動(dòng)程序的實(shí)現(xiàn)
I2C 驅(qū)動(dòng)程序的簡介
本驅(qū)動(dòng)程序?yàn)闃?biāo)準(zhǔn)的51 系列CPU 編寫,讓CPU 模擬成一個(gè)I2C 總線主器件,并部分
支持多個(gè)主器件同時(shí)存在。當(dāng)CPU 晶振為12MHz 時(shí),I2C 總線頻率為不超過100KHz。
如果I2C 總線上有多個(gè)I2C 總線主器件,用戶程序需要進(jìn)行一些額外處理。本書配套光盤
也包含一個(gè)模擬400KHz 的I2C 總線規(guī)范主器件的驅(qū)動(dòng)程序。而DP-51PROC 上的I2C 器
件并不是全都為400KHz 的,如ZLG7290 LED 鍵盤控制芯片的速度就比較慢,是32KHz,
所以該驅(qū)動(dòng)程序不適合ZLG7290。
驅(qū)動(dòng)程序的使用
本驅(qū)動(dòng)程序可以在沒有Small RTOS51 的情況下使用。此時(shí),要使用本驅(qū)動(dòng)程序只需
要配置I2C 總線使用的IO 口。在驅(qū)動(dòng)程序的主文件iic_master.c 僅包含一個(gè)文件
config.h。用戶需要的是在這個(gè)文件中設(shè)置I2C 總線使用的IO 口SDA 和SCL。定義SDA
和SCL 的例子見程序清單4.13。如果用戶單獨(dú)使用iic_master.c,還要在config.h 包含
iic_master.h 文件和其它必須的文件如reg51 等;并定義宏TRUE、FALSE 和與編譯器無
關(guān)的數(shù)據(jù)類型。在使用Small RTOS51 的情況下,如果用戶只有一個(gè)任務(wù)使用I2C 總線,
用戶只要在config.h 定義SDA 和SCL 和包含iic_master.h 就可以了。 如果用戶不止一
個(gè)任務(wù)使用I2C 總線,則驅(qū)動(dòng)程序需要使用信號(hào)量保證各個(gè)任務(wù)對(duì)I2C 總線的互斥操作。
這時(shí),需要將宏IICSem 定義為分配給I2C 總線驅(qū)動(dòng)程序的信號(hào)量的索引,并在使用驅(qū)動(dòng)
程序前建立這個(gè)信號(hào)量。
在使用I2C 總線驅(qū)動(dòng)程序前應(yīng)該調(diào)用函數(shù)IICInit()初始化I2C 總線。單獨(dú)使用或單任
務(wù)使用本驅(qū)動(dòng)程序時(shí),使用函數(shù)IICRead()對(duì)I2C 總線進(jìn)行讀操作,使用IICWrite()對(duì)I2C
總線進(jìn)行寫操作。如果有多個(gè)任務(wù)需要對(duì)I2C 總線進(jìn)行操作,則分別調(diào)用宏OSIICRead()
和OSIICWrite()對(duì)其進(jìn)行讀寫。
程序清單4. 13 定義I2C 使用的IO 口例
sbit SDA = P1^7; //I2C 總線驅(qū)動(dòng)使用的數(shù)據(jù)線
sbit SCL = P1^6; //I2C 總線驅(qū)動(dòng)使用的時(shí)鐘線
基本I2C 總線信號(hào)的產(chǎn)生
I2C 總線有很多基本總線信號(hào),每一個(gè)基本總線信號(hào)由一個(gè)函數(shù)產(chǎn)生。這些函數(shù)都比較
簡單,讀者對(duì)比I2C 總線規(guī)范應(yīng)該很容易讀懂。
I2C 總線啟動(dòng)信號(hào)由函數(shù)IICStart 產(chǎn)生,代碼見程序清單4.14。當(dāng)操作成功,函數(shù)返
回 TRUE。當(dāng)函數(shù)返回FALSE 時(shí)可能有別的總線主器件正在使用總線或總線故障。
程序清單4.14 產(chǎn)生I2C 總線啟動(dòng)信號(hào)
uint8 IICStart(void)
{
SDA = 1;
SCL = 1;
if (SDA == 1)
{
SDA = 0;
_nop_();
SCL = 0;
SDA = 1;
return TRUE;
}
else
{
return FALSE;
}
}
I2C 總線中止信號(hào)由函數(shù)IICStop 產(chǎn)生,代碼見程序清單4.15。函數(shù)沒有返回值。
程序清單4.15 產(chǎn)生I2C總線中止信號(hào)
void IICStop(void)
{
SDA = 0;
_nop_();
_nop_();
SCL = 1;
SDA = 1;
_nop_();
_nop_();
_nop_();
SCL = 0;
}
I2C 總線應(yīng)答信號(hào)(ACK)由函數(shù)IIC_ACK 產(chǎn)生,代碼見程序清單4.16。函數(shù)沒有
返回值。
程序清單4.16 產(chǎn)生I2C 總線應(yīng)答(ACK)信號(hào)
void IIC_ACK(void)
{
SDA = 0;
_nop_();
_nop_();
SCL = 1;
_nop_();
_nop_();
_nop_();
_nop_();
SCL = 0;
}
I2C 總線非應(yīng)答信號(hào)(NO ACK)由函數(shù)IIC_NO_ACK 產(chǎn)生,代碼見程序清單4.17。
函數(shù)沒有返回值。
程序清單4.17 產(chǎn)生I2C 總線非應(yīng)答(NO ACK)信號(hào)
void IIC_NO_ACK(void)
{
SDA = 1;
_nop_();
_nop_();
SCL = 1;
_nop_();
_nop_();
_nop_();
_nop_();
SCL = 0;
return;
}
I2C 總線初始化
在使用I2C 總線前必須初始化I2C 總線,使I2C 總線處于空閑狀態(tài)。這是通過發(fā)送總
線停止信號(hào)來實(shí)現(xiàn)的。代碼見程序清單4.18。
程序清單4.18 I2C 總線初始化
void IICInit(void)
{
SCL = 0;
IICStop();
}
發(fā)送和接收一個(gè)字節(jié)
發(fā)送一個(gè)字節(jié)和接收一個(gè)字節(jié)也是I2C 總線的基本操作。對(duì)I2C 的寫操作需要用到這
兩個(gè)操作。發(fā)送一個(gè)字節(jié)使用函數(shù)IICSend()實(shí)現(xiàn)。函數(shù)IICSend()的代碼見程序清單
4.19。流程圖見圖4.3。函數(shù)同時(shí)處理了應(yīng)答位。
程序清單4.19 向I2C 發(fā)送一個(gè)字節(jié)
uint8 IICSend(uint8 IIC_data)
{
uint8 i;
for (i = 0; i < 8; i++) (1)
{
IIC_data = IIC_data << 1; (2)
F0 = SDA = CY; (3)
SCL = 1; (4)
if (F0 != SDA) (5)
{
SCL = 0; (6)
return FALSE; (7)
}
_nop_(); (8)
_nop_(); (9)
SCL = 0; (10)
}
SDA = 1; (11)
_nop_(); (12)
_nop_(); (13)
SCL = 1; (14)
_nop_(); (15)
_nop_(); (16)
if (SDA == 1) (17)
{
SCL = 0; (18)
return FALSE; (19)
}
else
{
SCL = 0; (20)
return TRUE; (21)
}
}
I2C 發(fā)送一個(gè)字節(jié)
有了流程圖,程序應(yīng)該比較容易看懂。唯一要注意的是程序清單4.19(2)、(3)句和
F0 的使用。在Keil C51 中,左移(<<)操作會(huì)把最高位移到CY 標(biāo)志中。同理,右移移(>>)
操作會(huì)把最低位移到CY 標(biāo)志中。這是不可移植的,但卻是效率最高的方式??梢浦驳姆绞?br>見程序清單4.20。同理,F(xiàn)0 是51 單片機(jī)中用戶可使用的標(biāo)志,在PSW 中。雖然使用F0
隱含不可移植性,但沒有程序清單4.19(2)、(3)句那么嚴(yán)重,移植時(shí)只要定義一個(gè)全局
(或局部)變量F0 就可以了。程序清單4.20 可移植代碼
if ((IIC_data & 0x80) != 0)
{
F0 = SDA = 1;
}
else
{
F0 = SDA = 0;
}
IIC_data = IIC_data << 1;
接收一個(gè)字節(jié)使用函數(shù)IICReceive()實(shí)現(xiàn),代碼見程序清單4.21。由于接收一個(gè)字
節(jié)后發(fā)送的應(yīng)答信號(hào)不盡相同,函數(shù)沒有處理應(yīng)答信號(hào)。程序比較簡單,讀者對(duì)照I2C 總線
規(guī)范應(yīng)該可以讀懂,這里不再說明。
程序清單4.21 從I2C 從器件接收一個(gè)字節(jié)
uint8 IICReceive(void)
{
uint8 i,r;
r = 0; (1)
SDA = 1; (2)
for (i = 0; i < 8; i++) (3)
{
r = r * 2; (4)
SCL = 1; (5)
_nop_(); (6)
_nop_(); (7)
if (SDA == 1) (8)
{
r++; (9)
}
SCL = 0; (10)
}
return r; (11)
}
對(duì)I2C 進(jìn)行讀操作
如果有多個(gè)任務(wù)需要訪問I2C 總線,則使用OSIICRead()對(duì)I2C 總線進(jìn)行讀操作。如
果僅有一個(gè)任務(wù)需要I2C 總線,則使用IICRead()對(duì)I2C 總線進(jìn)行讀操作。OSIICRead ()
是一個(gè)宏,代碼見程序清單4.22。
程序清單4.22 多任務(wù)從I2C 讀數(shù)據(jù)
#define OSIICRead(a,b,c)
if (OSSemPend(IICSem,10) == OS_SEM_OK) (1)
{
IICRead(a,b,c); (2)
OSSemPost(IICSem); (3)
}
程序通過在對(duì)器件讀之前等待信號(hào)量(程序清單4.22 (1))和在對(duì)器件讀之后發(fā)送信
號(hào)量(程序清單4.22 (3))來實(shí)現(xiàn)對(duì)I2C 總線的互斥操作。這樣做的原因可以參見本章的
4.1.1 節(jié)。在宏中調(diào)用函數(shù)IICRead()對(duì)器件進(jìn)行讀操作(程序清單4.22 (2))。而
IICRead()就是單任務(wù)情況下對(duì)I2C 總線進(jìn)行讀操作的函數(shù),所以兩者的參數(shù)相同。
函數(shù)IICRead()的代碼見程序清單4.23。函數(shù)IICRead()的流程圖見圖4.4。函數(shù)
IICRead()的第一個(gè)參數(shù)是一個(gè)指針,讀出的數(shù)據(jù)將從這里開始存放。函數(shù)IICRead()的
第二個(gè)參數(shù)是將要訪問的從器件的器件地址。函數(shù)IICRead()的第三個(gè)參數(shù)是將要讀取的
字節(jié)數(shù)目。當(dāng)函數(shù)IICRead()成功讀取數(shù)據(jù)時(shí),返回TRUE。函數(shù)IICRead()返回FALSE
時(shí)可能有別的I2C 總線主器件正在訪問I2C 總線,或是總線故障,或是從器件故障。
程序清單4.23 單任務(wù)從I2C 讀數(shù)據(jù)
uint8 IICRead(uint8 OS_SEM_MEM_SEL *Ret,uint8 Addr,uint8 NByte)
{
uint8 i;
Addr = Addr | 0x01; (1)
if (IICStart() == FALSE) (2)
{
return FALSE; (3)
}
if(IICSend(Addr) == FALSE) (4)
{
return FALSE; (5)
}
i = NByte - 1; (6)
if (i != 0) (7)
{
do
{
*Ret++ = IICReceive(); (8)
IIC_ACK(); (9)
} while (--i != 0); (10)
}
*Ret = IICReceive(); (11)
IIC_NO_ACK(); (12)
IICStop(); (13)
return TRUE; (14)
}
圖4.4 從I2C 讀取數(shù)據(jù)流程圖
函數(shù)IICRead()首先將地址的最低位設(shè)置為1(程序清單4.23(1))告訴從器件此次
操作為讀。然后啟動(dòng)總線(程序清單4.23(2))。如果啟動(dòng)不成功,函數(shù)返回FALSE(程序
清單4.23(3))。否則發(fā)送從器件地址(程序清單4.23(3))。如果發(fā)送不成功,函數(shù)返回
FALSE(程序清單4.23(5))。否則就讀取比指定讀取的字節(jié)數(shù)少1 的字節(jié)數(shù),在每次讀取
一個(gè)字節(jié)后發(fā)送應(yīng)答(ACK)信號(hào)(程序清單4.23(6)~(10))。然后,接收最后一個(gè)字節(jié)
(程序清單4.23(11)),并發(fā)送非應(yīng)答(NO ACK)信號(hào)通知從器件讀取結(jié)束(程序清單
4.23(12))。最后,函數(shù)停止總線(程序清單4.23(13)),函數(shù)調(diào)用成功(程序清單4.23(14))。
4.3.7 對(duì)I2C 進(jìn)行寫操作
如果有多個(gè)任務(wù)需要訪問I2C 總線,則使用OSIICWrite()對(duì)I2C 總線進(jìn)行寫操作。如
果僅一個(gè)任務(wù)需要I2C 總線,則使用IICWrite()對(duì)I2C 總線進(jìn)行寫操作。OSIICWrite()
是一個(gè)宏,代碼見程序清單4.24。
程序清單4.24 多任務(wù)給I2C 從器件寫數(shù)據(jù)
#define OSIICWrite(a,b,c)
if (OSSemPend(IICSem,10) == OS_SEM_OK) (1)
{
IICWrite(a,b,c); (2)
OSSemPost(IICSem); (3)
}
程序通過在對(duì)器件寫之前等待信號(hào)量(程序清單4.24(1))和在對(duì)器件寫之后發(fā)送信
號(hào)量(程序清單4.24(3))來實(shí)現(xiàn)對(duì)I2C 總線的互斥操作。這樣做的原因可以參見本章的
4.1.1 節(jié)。在宏中調(diào)用函數(shù)IICWrite()對(duì)器件進(jìn)行寫操作(程序清單4.24(2))。而
IICWrite()就是單任務(wù)情況下對(duì)I2C 總線進(jìn)行寫操作的函數(shù),所以兩者的參數(shù)相同。
函數(shù)IICWrite()的代碼見程序清單4.25。函數(shù)IICWrite()的第一個(gè)參數(shù)是一個(gè)指針,
將要寫入的數(shù)據(jù)將從這里開始存放。函數(shù)IICWrite()的第二個(gè)參數(shù)是將要訪問的從器件的
器件地址。函數(shù)IICWrite()的第三個(gè)參數(shù)是將要寫入的字節(jié)數(shù)。當(dāng)函數(shù)IICWrite()成功寫
入數(shù)據(jù)時(shí),返回TRUE。函數(shù)IICWrite()返回FALSE 時(shí)可能有別的I2C 總線主器件正在訪
問I2C 總線,或是總線故障,或是從器件故障。
程序清單4.25 單任務(wù)給I2C 從器件寫數(shù)據(jù)
uint8 IICWrite(uint8 Addr,uint8 OS_SEM_MEM_SEL *Data,uint8 NByte)
{
uint8 i;
Addr = Addr & 0xfe; (1)
if (IICStart() == FALSE) (2)
{
return FALSE; (3)
}
if (IICSend(Addr) == FALSE) (4)
{
return FALSE; (5)
}
i = NByte; (6)
do (7)
{
if (IICSend(*Data++) == FALSE) (8)
{
return FALSE; (9)
}
} while (--i !=0 );
IICStop(); (10)
return TRUE; (11)
}
圖4.5 把數(shù)據(jù)寫道I2C流程圖
函數(shù)IICWrite()首先將地址的最低位設(shè)置為0(程序清單4.25(1))告訴從器件此次
操作為寫。然后啟動(dòng)總線(程序清單4.25(2))。如果啟動(dòng)不成功,函數(shù)返回FALSE(程序
清單4.25(3))。否則發(fā)送從器件地址(程序清單4.25(4))。如果發(fā)送不成功,函數(shù)返回
FALSE(程序清單4.25(5))。否則就寫入指定字節(jié)(程序清單4.25(6)~(9))。在每次寫
入一個(gè)字節(jié)后讀取發(fā)送應(yīng)答判斷發(fā)送一個(gè)字節(jié)的函數(shù)是否調(diào)用正確(程序清單4.25(8)),正確才繼續(xù)執(zhí)行,否則函數(shù)返回FALSE(程序清單4.25(9))。)最后,函數(shù)停止總線(程
序清單4.25(10)),函數(shù)調(diào)用成功(程序清單4.25(11))。
評(píng)論