I2C(Inter Intergrated Circuit)总线是Philips公司推出的一种用于IC器件之间连接的二线制串行扩展总线,它通过两根信号线(SDA-串行数据线;SCL-串行时钟线)在连接到总线上的器件之间传送数据,并根据地址来识别每个器件。
51单片机一般并没有在硬件中集成这种新的接口,所以要用软件来进行模拟。
1 硬件设计
24CXX系列串行E2PROM是常用的I2C串行E2PROM,正被广泛地用在各种智能仪器仪表当中。本例就是将一组数据写入24C02C中,然后读出并在LCD上显示,其电路如下图所示。
在桌面上双击图标,打开ISIS 7 Professional窗口(本人使用的是v7.4 SP3中文版)。单击菜单命令“文件”→“新建设计”,选择DEFAULT模板,保存文件名为“IIC.DSN”。在器件选择按钮中单击“P”按钮,或执行菜单命令“库”→“拾取元件/符号”,添加如下表所示的元件。
51单片机AT89C51 一片 | 晶体CRYSTAL 12MHz 一只 |
瓷片电容CAP 22pF 二只 | 电解电容CAP-ELEC 10uF 一只 |
电阻RES 10K 一只 | 排阻 RESPAC-8 10K 一只 |
1602液晶显示器 LM016L 一只 | I2C存储器芯片 24C02C 一片 |
若用Proteus软件进行仿真,则上图中的晶体、U1的复位电路和U1的31脚可以不画,它们大都是默认的。
在ISIS原理图编辑窗口中放置元件,再单击工具箱中元件终端图标,在对象选择器中单击POWER或GROUND放置电源或地。放置好元件后,布好线。左键双击各元件,设置相应元件参数,完成电路图的设计。
2 软件设计
将若干个数据写入24C02C中,然后读出并在LCD上显示,其流程图如下所示。
将32个数据写入24C02C中,然后再读出并用1602LCD显示。本例主要目的是如何用软件模拟I2C总线对24C02C进行读、写,其详细的C51程序如下所示。
#include<reg51.h> //包含单片机寄存器的头文件
#include<intrins.h> //包含_nop_()函数定义的头文件
sbit RS="P2"^0; //(LCD)寄存器选择位,将RS位定义为P2.0引脚
sbit RW="P2"^1; //(LCD)读写选择位,将RW位定义为P2.1引脚
sbit E="P2"^2; //(LCD)使能信号位,将E位定义为P2.2引脚
sbit BF="P0"^7; //(LCD)忙碌标志位,将BF位定义为P0.7引脚
#define OP_READ 0xa1 // (IIC)器件地址以及读取操作,0xa1即为1010 0001B
#define OP_WRITE 0xa0// (IIC)器件地址以及写入操作,0xa0即为1010 0000B
sbit SDA="P1"^1; //(IIC)将串行数据总线SDA位定义在为P1.1引脚
sbit SCL="P1"^0; //(IIC)将串行时钟总线SDA位定义在为P1.0引脚
unsigned char code string[ ]={"0123456789ABCDEFGHIJKLMNOPQRSTUV"};
//定义字符数组显示数字和字母
/*****************************************************
函数功能:延时1ms
(3j+2)*i=(3×33+2)×10=1010(微秒),可以认为是1毫秒
***************************************************/
void delay1ms()
{
unsigned char i,j;
for(i=0;i<10;i++)
for(j=0;j<33;j++)
;
}
/*****************************************************
函数功能:延时若干毫秒
入口参数:n
***************************************************/
void delaynms(unsigned char n)
{
unsigned char i;
for(i=0;i<n;i++)
delay1ms();
}
/******************************
以下是对液晶模块的操作程序
*******************************/
/************************************************
函数功能:判断液晶模块的忙碌状态
返回值:result。result=1,忙碌;result=0,不忙
*************************************************/
bit BusyTest(void)
{
bit result;
RS=0; //根据规定,RS为低电平,RW为高电平时,可以读状态
RW=1;
E=1; //E=1,才允许读写
_nop_(); //空操作
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
result=BF; //将忙碌标志电平赋给result
E=0; //将E恢复低电平
return result;
}
/*****************************************************
函数功能:将模式设置指令或显示地址写入液晶模块
入口参数:dictate
***************************************************/
void WriteInstruction (unsigned char dictate)
{
while(BusyTest()==1); //如果忙就等待
RS=0; //根据规定,RS和R/W同时为低电平时,可以写入指令
RW=0;
E=0; //E置低电平(根据表8-6,写指令时,E为高脉冲,
// 就是让E从0到1发生正跳变,所以应先置"0"
_nop_();
_nop_(); //空操作两个机器周期,给硬件反应时间
P0=dictate; //将数据送入P0口,即写入指令或地址
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=1; //E置高电平
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=0; //当E由高电平跳变成低电平时,液晶模块开始执行命令
}
/*****************************************************
函数功能:将数据(字符的标准ASCII码)写入液晶模块
入口参数:y(为字符常量)
***************************************************/
void WriteData(unsigned char y)
{
while(BusyTest()==1);
RS=1; //RS为高电平,RW为低电平时,可以写入数据
RW=0;
E=0; //E置低电平(根据表8-6,写指令时,E为高脉冲,
// 就是让E从0到1发生正跳变,所以应先置"0"
P0=y; //将数据送入P0口,即将数据写入液晶模块
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=1; //E置高电平
_nop_();
_nop_();
_nop_();
_nop_(); //空操作四个机器周期,给硬件反应时间
E=0; //当E由高电平跳变成低电平时,液晶模块开始执行命令
}
/*****************************************************
函数功能:对LCD的显示模式进行初始化设置
***************************************************/
void LcdInitiate(void)
{
delaynms(15); //首次写指令时应给LCD一段较长的反应时间
WriteInstruction(0x38);
//显示模式设置:16×2显示,5×7点阵,8位数据接口
delaynms(5); //给硬件一点反应时间
WriteInstruction(0x38);
delaynms(5);
WriteInstruction(0x38); //连续三次,确保初始化成功
delaynms(5);
WriteInstruction(0x0c);
//显示模式设置:显示开,无光标,光标不闪烁
delaynms(5);
WriteInstruction(0x06); //显示模式设置:光标右移,字符不移
delaynms(5);
WriteInstruction(0x01); //清屏幕指令,将以前的显示内容清除
delaynms(5);
}
/********************************
以下是对24C02的读写操作程序
*********************************/
/*******************************
函数功能:开始数据传送
********************************/
void start()
// 开始位
{
SDA = 1; //SDA初始化为高电平“1”
SCL = 1; //开始数据传送时,要求SCL为高电平“1”
_nop_();
_nop_(); //等待二个机器周期
SDA = 0; //SDA的下降沿被认为是开始信号
_nop_();
_nop_();
_nop_();
_nop_(); //等待四个机器周期
SCL = 0;
//SCL为低电平时,SDA上数据才允许变化(即允许以后的数据传递)
}
/************************
函数功能:结束数据传送
*************************/
void stop()
// 停止位
{
SDA = 0; //SDA初始化为低电平“0”
_nop_();
_nop_(); //等待二个机器周期
SCL = 1; //结束数据传送时,要求SCL为高电平“1”
_nop_();
_nop_();
_nop_();
_nop_(); //等待四个机器周期
SDA = 1; //SDA的上升沿被认为是结束信号
}
/***************************
函数功能:从24Cxx读取数据
出口参数:x
****************************/
unsigned char ReadData()
// 从24Cxx移入数据到MCU
{
unsigned char i;
unsigned char x; //储存从24Cxx中读出的数据
for(i = 0; i < 8; i++)
{
SCL = 1; //SCL置为高电平
x<<=1; //将x中的各二进位向左移一位
x|=(unsigned char)SDA;
//将SDA上的数据通过按位“或“运算存入x中
SCL = 0; //在SCL的下降沿读出数据
}
return(x); //将读取的数据返回
}
/************************************
函数功能:向24Cxx的当前地址写入数据
入口参数:y (储存待写入的数据)
************************************/
//在调用此数据写入函数前需首先调用开始函数start(),所以SCL=0
bit WriteCurrent(unsigned char y)
{
unsigned char i;
bit ack_bit; //储存应答位
for(i = 0; i < 8; i++) // 循环移入8个位
{
SDA = (bit)(y&0x80); //通过按位“与”运算将最高位数据送到S
//因为传送时高位在前,低位在后
_nop_(); //等待一个机器周期
SCL = 1; //在SCL的上升沿将数据写入AT24Cxx
_nop_();
_nop_(); //等待二个机器周期
SCL = 0;
//将SCL重新置为低电平,以在SCL线形成传送数据所需的8个脉冲
y <<= 1; //将y中的各二进位向左移一位
}
SDA = 1;
// 发送设备(主机)应在时钟脉冲的高电平期间(SCL=1)释放SDA线,
//以让SDA线转由接收设备(AT24Cxx)控制
_nop_();
_nop_(); //等待二个机器周期
SCL = 1; //根据上述规定,SCL应为高电平
_nop_();
_nop_();
_nop_();
_nop_(); //等待四个机器周期
ack_bit = SDA; //接受设备(24Cxx)向SDA送低电平,表示已经接收到一
//个字节;若送高电平,表示没有接收到,传送异常
SCL = 0;
//SCL为低电平时,SDA上数据才允许变化(即允许以后的数据传递)
return ack_bit; // 返回AT24Cxx应答位
}
/****************************************
函数功能:从24Cxx中的当前地址读取数据
出口参数:x (储存读出的数据)
*****************************************/
unsigned char ReadCurrent()
{
unsigned char x;
start(); //开始数据传递
WriteCurrent(OP_READ); //选择要操作的24Cxx芯片,并告知要读其数据
x=ReadData(); //将读取的数据存入x
stop(); //停止数据传递
return x; //返回读取的数据
}
/***********************
函数功能:主函数
************************/
void main(void)
{
unsigned char i;
unsigned char x; //储存从24C02读出的值
LcdInitiate(); //调用LCD初始化函数
for(i=0;i<32;i++)
{ start(); //开始数据传递
WriteCurrent(OP_WRITE);
//选择要操作的24Cxx芯片,并告知要对其写入数据
WriteCurrent(i); //写入指定地址
WriteCurrent(string[i]); //向当前地址写入数据
stop(); //停止数据传递
delaynms(4);
}
start(); //开始数据传递
WriteInstruction(0x80); //第一行显示地址
WriteCurrent(OP_WRITE);
//选择要操作的24Cxx芯片,并告知要对其写入数据
WriteCurrent(0x00); //写入指定地址
for(i=0;i<16;i++)
{
x=ReadCurrent(); //从24C02中读出
WriteData(x); //将值用1602LCD显示
}
WriteInstruction(0xc0); //第二行显示地址
for(i=0;i<16;i++)
{
x=ReadCurrent(); //从24C02中读出
WriteData(x); //将值用1602LCD显示
}
stop(); //停止数据传递
delaynms(4);
while(1) //无限循环
{
}
}
打开Keil程序(本人使用的是Keil8.05中文版),执行菜单命令“工程”→“新建工程”创建“IIC”项目,并选择单片机型号为AT89C51。执行菜单命令“文件”→“新建”创建文件,输入C语言源程序,保存为“IIC.C”。在Project Workspace窗口中右击源代码组1,选择“添加文件到组‘源代码组 l’”将源程序“IIC.C”添加到项目中。
在Keil中执行执行菜单命令“工程”→“创建目标”(或点击“创建目标”快捷按钮),编译源程序。如果编译成功,则在“Output Window”的“创建”窗口中显示没有错误,并创建了“IIC.HEX”文件。
3 仿真与调试
关于Proteus与Keil的联合仿真调试,可参见我以前所写的博文或其它参考资料。
启动Proteus的ISIS,并将其放在屏幕的右上角(可将原理图放大到合适大小);再启动Keil的μVision3,并将其放在屏幕的左下角。
在Keil中执行菜单命令“调试”→“启动/停止调试”,或直接单击图标,进入Keil调试环境。同时,在Proteus ISIS的窗口中可看出Proteus也进入了程序调试状态。
在Keil代码编辑窗口中设置相应断点,断点的设置方法:在需要设置断点语句前双击鼠标左键,可设置断点;再次双击,可取消该断点。
在Keil中按F5键(或点击“运行”快捷按钮)运行程序。1602LCD将显示0、1、2……9、A、B……V共32个字符,如下图所示。
或可以点击单步、运行到光标处、全速运行等快捷按钮,以及同时观察工程窗口寄存器页面、存储器窗口等,来进行仿真调试。