51单片机实现对24C02进行页写、顺序读取并显示验证

转自:http://blog.sina.com.cn/s/blog_4e6ca7bb0100g876.html


看看自己的博客,实在不忍心眼睁睁看着它成为图片传播站。但自己对将别人的技术文章或程序转来转去也没感觉有多大意思,干脆把上个月写的个小程序放上来吧。参看过类似程序,自己编写了这个程序。I2C虽是较简单的通信协议,但要完全理解透彻可也并非那么轻而易举,自己也是在不断加深对它的理解。这个程序已通过实验验证,并加上个人非常详细的注释(复制过来显示有点乱,没时间一一调整了),但如果使用它还是请慎重。

 

//*************************************************************************************
//**程序名称:51单片机实现对24C02进行页写、顺序读取并显示验证
//**编写人:海右愚叟 
                修改人:海右愚叟    
//**程序目的:熟悉I2C总线协议,实现51模拟I2C时序和24C02通信
//**功能描述:51单片机将8个字节数据写入24C02的一页中,然后顺序读出,每隔1秒送P0口LED显示
//**其他说明:本程序是采用某51开发板,若在其他地方验证可更改相关端口及延时程序等。
//** 
        程序编写前曾参考过多个教程,最终自己编程通过,并详加注释。
//** 
        可供初学者参考,并不对程序的可靠性等作保证。
//**开发工具:keil 7.50(C51) 
     日期:2009-11-21
//*************************************************************************************

 

#include <REGX51.H>
#include<intrins.h> 
//因为用到_nop_();
typedef unsigned char uchar;
sbit SCL =P3^3; 
    //注意P1、P2、P3口有内部上拉电阻,可直接连SDA和SCL,若想用P0需外接上拉电阻,否则连上无法输出高电平!
sbit SDA = P3^4;
ucharj; 
   //用于计数50ms的个数的全局变量
uchar code ToSDAdataBuffer[8] = {
0xfe,0xfc,0xf8,0xf0,
0xe0,0xc0,0x80,0x00
}; 
     //写入24C02的一组数据,8个字节对应24C02的一页(共32页),这里把这些要验证的常数放到程序存储区
uchar ReceivedData[8];//用于存储接收的8个字节数据(1页)的数组

//本例51为单主机,24C02为从机,不需要总线裁决

//延时5us子程序
void delay5us(void)
{
 
_nop_(); //时序图要求开始建立时间tSU.STA大于4.7us,开始保持时间tHD.STA大于4us。51中每个_nop_();延时1个CPU cycle,即1us。
 
_nop_(); //如考虑不同CPU频率不同,可用带参数的延时,参数在前面宏定义。
 
_nop_();
 
_nop_();
 
_nop_();
}

//50ms定时器0中断函数
void timer0() interrupt1 
   //j是个全局变量,不是返回值,所以这里还是void。
{
 
TH0 = (65536-46080)/256; //11.0592MHz时每50ms一次定时器中断
 
TL0 = (65536-46080)%256;
 
j++;                     //也可以把判断j到20,并给P0口送显示数据的程序放在中断里处理
}

//延时1秒的子程序,用于将读取的数据每隔一秒显示在LED上
void delay1s(void)
{
 
  = 0;
 
TMOD =0x01;   //方式1的16位计数器
 
TH0  = (65536-46080)/256;
 
TL0  = (65536-46080)%256;
 
EA   = 1;
 
ET0  = 1;
 
TR0  =1;    //启动定时器0工作

 while(j < 20) //j达到20之前空操作,达到20时说明已到1s,下面关中断和定时器0
 
 ;
 
EA   = 0;
 
ET0  = 0;
 
TR0  = 0;        
}

//约2ms的延时
void delay(uchar t)
{
 
uchar x,y;
 
for(x=0;x<t;x++)
 
 for(y=0;y<250;y++)
 
  ;
}

//I2C初始化
void InitI2C(void)
{
 
SDA = 1;  //总线空闲时,因各设备都是集电极或漏极开路,上拉电阻使SDA和SCL线都保持高电平。
 
SCL = 1; 
 
delay5us();
}

//产生I2C开始信号
void StartI2C(void)
{
 
SDA = 1;  //SDA在SCL为高期间由高变低表示开始,所以先要高
 
SCL = 1;
 
delay5us();  //时序图要求tSU.STA(Start Set-up Time)大于4.7us
 
SDA = 0;  //注意SDA拉低前后都要维持5us以上!
 
delay5us();  //tHD.STA(Start Hold Time)大于4us

 SCL = 0;  //拉低SCL,准备发送或接收数据(这两句也可在写或读字节的程序中先将SCL置0,延时)
 
delay5us();
}

//产生I2C结束信号
void StopI2C(void)
{
 
SDA = 0;  //SDA在SCL为高期间由低变高,说明结束
 
SCL = 1;
 
delay5us();
 
SDA = 1;
 
delay5us();
}

//发送方在发完一个字节后检测接收方有没有应答。返回应答成功否。
bit ChkAck(void)
 
 
 bit SDAtemp;
 
SDA     1; //释放SDA(置1),然后等待接收方应答将它拉低。确切的说,应是24C02发送字节最后一位的第8个时钟周期下降沿后经tAA
 
//(SCL变低到SDAOUT有效的时间)约0.1-4.5us后拉低SDA,并随第9个时钟后结束。所以24C02正常时,SDA为1并不体现
 
//(第8脉冲后马上被拉低了),但若器件坏了,就需要靠这个置1后不变来判断!(若不置1而上次发的数据最后一位为0就不好判断了)
 
//从24C02的BlockDiagram看,它只能在SDA为1时通过控制内部的Dout来把SDA拉低,但不能在SDA为0时将其置高!故主机要常将SDA置1,而SCl置0。
 
SCL     1; //WriteI2CByte中写完一字节后又将SCL拉低,这里拉高产生第9个时钟上升沿,然后在SCL为高期间对SDA进行检测
 
delay5us();
 
SDAtemp = SDA; //如果不用暂存变量,直接returnSDA,就不会执行后面的SCL = 0,检测期间的第9个时钟就不完整了
 
SCL     0;
 
delay5us();
 
return SDAtemp;

}

//51作为主机时,如果接收数据,模拟产生应答时序。形参Ack为0,则应答0,为1不应答。
void AckAsMaster(bit Ack)
{
 
if(!Ack)
 
 SDA = 0;
 
else
 
 SDA = 1;

 delay5us();
 
SCL =1;  //主机控制SCL时序。关键是保证在SCL脉冲上升沿之前SDA数据已稳定即可。
 
delay5us();
 
SCL = 0;
 
delay5us();
}

void WriteI2CByte(uchar);
uchar ReadI2CByte();

//页写。输入两参数,一个为首字地址,另一个是指向待写入数据数组的指针(括号内第二个参数也可写作ucharToSDAdataBuffer[],即数组名代表首地址)。
bit PageWrite(uchar WordAddress,uchar *ToSDAdataBuffer)
{
 
  //下面的程序我用的if嵌套,网上有些程序是顺序结构,但因为遇到return就返回主程序不再往下执行,所以效果是一样的。
 
uchar i;
 
StartI2C();
 
WriteI2CByte(0xa0);//之所以没设DeviceAddress这个参数,是因为最后一位不属于地址。E2PROM一般前四位为1010,这里A2~A0接地,为0,最后一位0表示写
 
if(!ChkAck()){   //检查应答函数返回0说明从机应答0成功。
 
 WriteI2CByte(WordAddress); //写8-bitdata wordaddress,即写到哪个存储单元(24C02有2kbits,所以数据字有2048/8=256个,故地址线有8位)
 
 if(!ChkAck()){
 
  for(i = 0; i< 8; i++){
 
   WriteI2CByte(ToSDAdataBuffer[i]);
 
   if(ChkAck()){
 
               //这里可添加错误处理代码。如用几个LED的亮灭组合表示此I2C器件有问题,类似主板错误提示。
 
    return1;//一般返回1表示异常,且遇到return就退出整个子程序。 
 
   }
 
  }
 
  StopI2C();     //写完发送结束信号。
 
  return0;       //一般返回0表示程序正常
 
 }
 
 else{   
 
  return1;       //之前可添加错误处理代码。
 
 
 
}
 
else{   
 
 return1; 
 

}

//不能用Current AddressRead,因为那是24C02数据字地址计数器上次操作后加1的值;而SEQUENTIAL_READ如果不给一个要读取的开始地址,会从头输出,
//所以需要Random Read的开始部分,但不要停止信号。
bit SequentialRead(uchar WordAddress)
{
 
uchar i;
 
StartI2C();
 
WriteI2CByte(0xa0);
 
if (!ChkAck()){
 
   WriteI2CByte(WordAddress);
 
 if (!ChkAck()){
 
  StartI2C(); //the microcontroller must generate another start condition
 
  WriteI2CByte(0xa1);//DeviceAddress后紧跟的那一位R/W^是1说明是读,24C02内部就是根据最后这位来判断是从SDA上读数,还是往SDA上送数
 
  //之所以设为1是读,是因为根据WriteI2CByte子程序,最后给SDA赋1,P3^4就维持1,这样24C02内部Dout为高就将SDA拉低;如果最后一位是0,24C02没能力拉高!
 
  if(!ChkAck()){
 
   for(i= 0;i < 8;i++){
 
    ReceivedData[i]= ReadI2CByte();
 
    AckAsMaster(0);//51此时接收数据,调用应答的函数(置SDA为0)
 
   }

    AckAsMaster(1);    //NO ACK.The microcontroller does not respond with a zero but doesgenerate a following stop condition.
 
   StopI2C();
 
   return0;
 
  }
 
  else{
 
   return1;          //之前可添加错误处理代码。
 
  
 
 }
 
 else{
 
  return1;
 
 }
 
}
 
else{
 
 return 1;
 
}
}

int main(void)
{
 
uchar i;
 
P0 = 0xff;
 
InitI2C();
 
//注意在24C02中用到的页写和顺序读的地址是同一个,且必须是8的整数倍,即每页的首地址才行,如0x08,0x20等。因为24C02页写时后三位地址自动加1,
 
//When the word address,internally generated,reaches the page boundary, the following byte is placed at thebeginning of the same page.
 
//而顺序读时只有在达到整个存储区边界时才会rollover。所以,如读写都用0x32这个地址,由于不是8的整数倍,只有前6个数显示是正确的,最后两个数
 
//虽然又从头写在了该页的前面,但SequentialRead确读到了该页之外的两个存储单元,造成错误。
 
if (PageWrite(0x08,ToSDAdataBuffer) == 0) {//先执行页写操作,设从地址00开始,没问题就延迟一下再从同一地址读回来。
 
 delay(100);                //等待24C02页写操作完毕
 
 if(SequentialRead(0x08) ==0){   //如果顺序读操作成功,则每隔1秒送P0口显示一个字节
 
  for(i = 0; i< 8; i++){
 
   P0= ReceivedData[i];
 
   delay1s();
 
  }
 
 
 
}
 
while(1)
 
 ;
 
return 0;
}

//往I2C总线写一个字节的数据(即将一个字节的数据发送到SDA上)
void 
WriteI2CByte(uchar ByteData)
{
 
uchar i,temp;
 
temp = ByteData;

//  (StartI2C()最后已经先将SCL变0了):
 
for(i=0;i<8;i++){
 
 temp<<= 1; //左移一位,I2C要求由MSB最高位开始,移出的CY即要发送到SDA上的数据。下面考虑时序:
 
 SDA   CY; //此时SCL已为低,每次移一位送出去(下次进循环后SDA还保持着上次发出去的数据)
 
 delay5us();  //SDAIN数据变化中点SCL上升沿中点的一段时间是tSU.DAT,即数据建立时间Data In Set-upTime,需大于200ns,多延无所谓

  SCL    1; 
 
 delay5us();  //tHIGH即ClockPulse Width High,最小4us
 
 SCL    0; 
 
 delay5us();  //tLOW即ClockPulse Width Low,最小4.7us
 
  

}

//读取I2C总线一个字节的数据
uchar 
ReadI2CByte()   //串行总线,51一位位接收从机发送到SDA上的数据,这里只考虑数据已在SDA上时如何存下来这几位,组成一个字节
{
 
uchar i,ByteData;
 
SDA =1;           //SCL在ChkAck中已经置0了。注意SCL时序仍然由主机控制!24C02只能将SDA由高拉低,象橡皮筋松手又恢复高,而下面只是读SDA,没赋值
 
     //其实程序中多处给SDA置1都可省,因为检查应答时为0就正常,无所谓,写字节时也无所谓,就是在读之前要保证SDA为1!
 
     //因之前有WriteI2CByte(0xa1);其实这句也可省略。
 
delay5us();   //24C02作为发送方在第9个时钟的negativeedge clocks data out of each device,所以现在SDA上为新数据
 
for(i=0;i<8;i++){
 
 SCL =1;  //置时钟线为高使数据线上数据有效
 
 delay5us();
 
 ByteData =(ByteData<<1)|SDA;//SDA上已是新数据了,读之。data不管以前多少,左移后最右边为0,和SDA“按位或”后MLB就是SDA
 
 SCL = 0;
 
 delay5us();
 
}
 
return ByteData; 
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值