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))。
评论
查看更多