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