欣欣学习网,老工程师带你学习单片机技术,欢迎来坐坐。
首  页 | 学习NIOSII | 学习C51 | 学习CPLD | 51+CPLD实验板 | | | MY-RTOS

 实验板首页
 安装QuartusII软件
 安装USB-Blaster驱动
 安装CH340C驱动
 安装Keil uVision软件
 运行Hello World例程
 新建Keil uVision工程
 在Flash中运行程序
 恢复CPLD中的出厂逻辑
 安装调试代理程序
 硬件架构分析
 实现LCD驱动接口
 实现数码管驱动接口
 实现4x2键盘接口
 使用ADC采集拟量
 使用EEPROM保存数据
 使用UART通信
 红外遥控信号解码
 用作USB转232/485
 相关软件与文档


使用EEPROM保存数据


EEPROM是"可电擦除只读存贮器"的简写。虽然名字叫只读存贮器,但它是可以写入的,只不过写入的速度比不上随机存贮器那么快。这种存贮器所存贮的数据可以在没有电源供电的情况下予以保存,所以在工业控制领域得到广泛的应用。其中,以AT24CXX系列芯片是最为常见。

AT24CXX系列芯片又叫串行EEPROM,因为它采用了I2C串行接口以减小封装。常见的封装形有:SOIC、TSSOP等形式,整个芯片只有8个引脚。

图(1)

管脚功能
A0A2、A1、A0 为总线地址设置管脚,它们共同决定了芯片的总线地址:1 0 1 0 A2 A1 A0
A1见 A0 引脚。
A2见 A0 引脚。
GND
SDAI2C总线接口的数据信号,与SCL共同构成I2C总线接口。
SCLI2C总线接口的时钟信号,与SDA共同构成I2C总线接口。
WP写保护引脚,高有效。当WP置"1"时,芯片处于只读状态。
VCC电源

表(1)

AT24CXX的通信接口是标准的I2C接口。具有I2C接口的芯片可以挂接在I2C总线上,通过I2C总线与其它芯片交换数据。

I2C总线是双线总线,由两路信号线构成:时钟线SCL和数据线SDA。时钟线SCL用来传递同步时钟信号,数据线SDA用来以同步时钟信号的节拍传递数据信号。同时挂在I2C总线上的I2C接口可以有很多,这些接口以I2C总线地址加以区分。根据不同版本的协议,I2C总线地址有7位地址和10位地址之分。其中7位地址是在基本的I2C总线协议中定义的,10位地址则是在7位地址的基础上扩展而来。这里我们的目标是对EEPROM进行读写,所以内容仅限于7位地址的基本I2C协议范围。

图(2)

I2C接口对总线的高、低电平的驱动是有区别的。对低电平的驱动是强驱动,用MOS管来驱动;对高电平的驱动是弱驱动,用上拉电阻来驱动。于是,所有挂在I2C总线上的接口形成了"线与"的关系。这样,就从电路属性上保证了各个接口可以同时对总线加以驱动,而不用担心高、低电平之间发生短路的问题。总线上实际表现出的逻辑值,是所有接口输出的逻辑值的"与"运算。

I2C接口通过I2C总线交换数据的过程中,需要参与通信的接口分别扮演各自的角色。要完成一次通信,总线上需有1个接口作为Master端出现,其余的接口则是Slave端。当然,挂在总线上的I2C接口,由于物理设计原因,有些只能做Master端,有些只能做Slave端,有些则是可以在Master端和Slave端自动切换。每次通信总是由总线上的1个Master端接口发起,并由其对总线进行管理,Slave端则是被动的收发数据。当有多个Master端试图接管总线时,由仲裁机制决定哪个端口胜出,失败者自动切换回Slave模式,等待下一次机会。这里不对仲裁机制展开介绍,只考虑单一Master的总线通信情况。

当一个Master端口接管总线之后,总线的时钟信号由该端口发出。数据信号则根据传输方向而定,可以是Master端驱动,也可以是Slave端驱动。

每次数据交换,总是以总线的起始条件开始,以总线的停止条件结束。总线的起始条件和停止条件匀由Master端接口产生。起始条件是SDA信号在SCL信号的高电平期间发生一个下降沿跳变,停止条件则是SDA信号在SCL高电平期间发生一个升沿跳变。

图(3)

I2C总线上的数据传输是以8个数据位组成的字节为基本单位的。字节的各个数据位在总线上的传递次序是MSB在前,也就是先传高位,后传低位。每个数据位要在SCL信号的高电平期间保持稳定,数据位之间的切换发生在SCL信号的低电平期间。总线上每传送1个字节的数据,接收方要给出1个数据的应答,ACK或NACK。ACK代表要求继续传递下一个字节的数据,NACK则是表示不再需要下一个字节的数据。

图(4)

在Master端发出起始条件后,新的一次数据交换开始。紧随起始条件之后的第1个字是由Master端发出的。这个字节的高7位是Slave端的I2C总线地址,用于选择和哪个Slave接口进行通信。最低位是数据传输方向控制位,该位低电平代表随后的数据由Master端发送,由Slave端接收;高电平则代表随后的数据由Slave端发送,由Master端接收。

图(5)

每个Slave端I2C接口都会分配1个或多个I2C总线地址,Master端则根据总线地址选则与哪个Slave端接口通信。I2C协议中还规定了一些特殊的地址,如下:

地址RW描述
00000000广播地址
00000001起始字节
0000001XCBUS地址
0000010X保留
0000011X保留
00001XXXHS主机模式
11111XXX保留
11110XXX10位从机寻址

AT24CXX有3个管脚用于设置该芯片的I2C总线地址的最低3位,而其地址的高4位固定为1010。在与单片机进行通信时,AT24CXX作为Slave端出现,而单片机则是Master端。

单片机向AT24CXX写入数据的总线数据流如图(6)所示。

图(6)

起始条件之后,第1个字节是I2C总线地址和传输方向控制位。控制位RW为0表示随后的数据是从单片机传送给AT24CXX的。再后面的两个字节是AT24CXX的存贮单元地址,告诉AT24CXX随后的数据存贮在哪里。最后一个字节是写入的数据。每当1个字节的数据传送完,AT24CXX送出1个ACK应答。全部数据传送完成之后,单片机发送1个停止条件,结束本次通信。

对于AT24CXX来说,也可以一次写入多个数据。在一次写入多个数据时,AT24CXX内部的存贮单元地址寄存器会在每写入一个字节数据后自动加1,新接收的数据则写到新的地址中去。需要注意的是,AT24CXX的存贮单元是按页来组织的,每页64个字节。当地址寄存器遇到了页的边缘的时候,新地址并不会指向下一页的起始,而是回到本页的起始。所以,如果一次写入的数据多于64个字节时,就会发生数据覆盖现象。最先写入的若干字节被覆盖掉,只保留了最后写入的64个字节。另外,AT24CXX并不会自动的从一个页的起始地址开始写入数据。所以,如果发给AT24CXX的地址不是一个页的起始地址,则数据会从给定的地址开始写入,在遇到本页边缘时再回到本页的起始地址。这样,最后写入的数据会保存在本页的前部。

图(7)

从AT24CXX中读回1个字节的数据的总线数据流如图(8)所示。所读回的数据的是当前AT24CXX内部地址寄存器所指向的存贮器单元的内容,读出后地址寄存器自动加1。

图(8)

如果想从指定的存贮单元的地址读数据,可按图(9)的时序操作。先发起一个写入操作,在传送完存贮器地址后重新以一个起始条件开始读出操作。这样,读出的数据就是所传入地址单元上的数据。

图(9)

当然,也可以一次读出多个数据。每读出一个字节的数据,AT24CXX内部的存贮单元地址寄存器会自动加1。这样,下一个读出的数据就是下一个地址单元的内容。与写入不同的是,在读出数据时,存贮单元地址寄存器的值会跨跃页的边缘,一直增大到整个存贮器的最高地址,然后回到0地址。所以,在读出操作时,可以一次读出多于64个字节的数据。

AT24CXX直接由89C52管理,其通信管脚与89C52的连接方式如下:

P1.4 ----> WP

P1.3 ----> SCL

P1.2 <---> SDA

注:89C52的P1.4管脚也兼作TLC0832的CS信号,所以AT24CXX和TLC0832不能在同一时间访问

启动读操作的示例代码如下:

char EEStartRead(void)
{
  char Ack;
  
  SDA = 1;          // 将数据线SDA置高
  SCL = 1;          // 将时钟线SCL置高
  SCL = 1;          // 重复3次是为了起到延时的作用
  SCL = 1;          // 以匹配I2C总线的速度
  SDA = 0;          // 在数据线SDA上产生1个下降沿
  SDA = 0;          // 实现起始条件
  SCL = 0;          // 将时钟线SCL拉低以便更新数据线上的数据位
                    // 数据线上的数据位只能在SCL低电平期间更新
                    
  SDA = 1;          // 在数据线SDA上送出I2C地址的最高位 A6 = 1
  EESclPulse();     // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;          // 在数据线SDA上送出I2C地址的次高位 A5 = 0
  EESclPulse();     // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 1;          // 在数据线SDA上送出I2C地址的 A4 = 1
  EESclPulse();     // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;          // 在数据线SDA上送出I2C地址的 A3 = 0
  EESclPulse();     // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;          // 在数据线SDA上送出I2C地址的 A2 = 0
  EESclPulse();     // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;          // 在数据线SDA上送出I2C地址的 A1 = 0
  EESclPulse();     // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;          // 在数据线SDA上送出I2C地址的 A0 = 0
  EESclPulse();     // 在时钟线SCL上产生1个时钟脉冲
    
                    // 由于AT24CXX的A2、A1、A0管脚在硬件上都接地
                    // 所以对应的I2C地址的A2、A1、A0都是0
    
  SDA = 1;          // 在数据线SDA上送出"1",表明是读操作
  EESclPulse();     // 在时钟线SCL上产生1个时钟脉冲
  
  SDA = 1;          // 将SDA置"1",以便接收数据,"1"是弱驱动
  Ack = SDA;        // 读回AT24CXX送出的应答位
                    // 
  EESclPulse();     // 在时钟线SCL上产生1个时钟脉冲
                    // AT24CXX送出1位数据或释放数据线
  
  return Ack;       // 返加应答值,以便决定是否断续读操作
}
          

读一个数据页的示例代码如下:

void EEReadPage(unsigned char xdata * pBuf)
{
  unsigned char x, y, z;
  
  for(x = 0; x < 64; x ++)        // AT24CXX的数据页长64个字节
  {
    SDA = 1;                      // 读数据前先把数据线置高,不对外强驱动
    z = 0;                        // 
    for(y = 0; y < 8; y++)        // 读8个数据位构成1个字节
    {
      z <<= 1;                    // 先左移1位,以便在最低位保存新有数据位
      if(SDA != 0) z ++;          // 读回的数位为"1",则把接收数据的最低位置"1"
      EESclPulse();               // 产生1个时钟脉冲,让AT24CXX送出下1个数据位
    }
    pBuf[x] = z;                  // 接收完1个字节,保存到缓冲区
    if(x != 63) SDA = 0;          // 不够64个字节,送出ACK继续接收
    else SDA = 1;                 // 否则送出NACK不再接收
    EESclPulse();                 // 产生1个时钟脉冲
    SDA = 1;
  }
}
          

结束对AT24CXX访问的示例代码如下:

void EEStop(void)         // 产生1个停止条件,结束当前的访问
{                         // 在这之前要确保AT24CXX已收到NACK从而释放SDA
                          // 否则停止条件无法产生
  SDA = 0;                // 将SDA置"0",以便产生上升沿
  SCL = 1;                // 将SCL置"1",多次重复做延时
  SCL = 1;
  SCL = 1;
  SCL = 1;
  SDA = 1;                // 在SDA上产生1个上升沿
  SDA = 1;                // 构成停止条件,结束本次总线访问
}
          

启动写操作的示例代码如下:

char EEStartWrite(unsigned int Addr)
{
  unsigned char n;
  
  char Ack;  
  unsigned char AH = Addr>>8;
  unsigned char AL = Addr&0x00ff;
  
  SDA = 1;                      // 将数据线SDA置高
  SCL = 1;                      // 将时钟线SCL置高
  SCL = 1;                      // 重复3次是为了起到延时的作用
  SCL = 1;                      // 以匹配I2C总线的速度
  SDA = 0;                      // 在数据线SDA上产生1个下降沿
  SDA = 0;                      // 实现起始条件
  SCL = 0;                      // 将时钟线SCL拉低以便更新数据线上的数据位
                                // 数据线上的数据位只能在SCL低电平期间更新
                    
  SDA = 1;                      // 在数据线SDA上送出I2C地址的最高位 A6 = 1
  EESclPulse();                 // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;                      // 在数据线SDA上送出I2C地址的次高位 A5 = 0
  EESclPulse();                 // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 1;                      // 在数据线SDA上送出I2C地址的 A4 = 1
  EESclPulse();                 // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;                      // 在数据线SDA上送出I2C地址的 A3 = 0
  EESclPulse();                 // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;                      // 在数据线SDA上送出I2C地址的 A2 = 0
  EESclPulse();                 // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;                      // 在数据线SDA上送出I2C地址的 A1 = 0
  EESclPulse();                 // 在时钟线SCL上产生1个时钟脉冲
    
  SDA = 0;                      // 在数据线SDA上送出I2C地址的 A0 = 0
  EESclPulse();                 // 在时钟线SCL上产生1个时钟脉冲
    
                                // 由于AT24CXX的A2、A1、A0管脚在硬件上都接地
                                // 所以对应的I2C地址的A2、A1、A0都是0
    
  SDA = 0;                      // 在数据线SDA上送出"0",表明是写操作
  EESclPulse();                 // 在时钟线SCL上产生1个时钟脉冲
  
  SDA = 1;                      // 将SDA置"1",以便接收数据,"1"是弱驱动
  Ack = SDA;                    // 读回AT24CXX送出的应答位
  
  EESclPulse();                 // 在时钟线SCL上产生1个时钟脉冲
  
  if(Ack == 0)                  // 如果ACK应答,可以继续下一步
  {
    for(n = 0; n < 8; n ++)
    {                           // 送出存贮器地址高8位,MSB在前
      if(AH&0x80) SDA = 1;
      else SDA = 0;  
      EESclPulse();
      AH <<= 1;
    }
    
    SDA = 1;
    Ack = SDA;                  // 读回应答位
    EESclPulse();
  }
  
  if(Ack == 0)                  // 如果是ACK应答,可以继续
  {
    for(n = 0; n < 8; n ++)
    {                           // 送出存贮器地址低8位,MSB在前
      if(AL&0x80) SDA = 1;
      else SDA = 0;      
      EESclPulse();
      AL <<= 1;
    }
      
    SDA = 1;
    Ack = SDA;                  // 读回应答位
    EESclPulse();  
  }
  
  return Ack;                   // 返回应答位,以便决定是否继续操作
}
          

写一个数据页的示例代码如下:

void EEWritePage(unsigned char xdata * pBuf)
{
  unsigned char x, y;
  unsigned char ch;
  
  for(x = 0; x < 64; x ++)        // AT24CXX的数据页长64个字节
  {
    ch = pBuf[x];                 // 从取缓冲区取出1个数据
    for(y = 0; y < 8; y ++)       // 以MSB在前的方式依次传送8个数据位
    {
      if(ch&0x80) SDA = 1;
      else SDA = 0;          
      EESclPulse();
      ch <<= 1;
    }
    
    SDA = 1;                      // 发1个时钟脉冲,令AT24CXX送出应答位
    EESclPulse();   
  }                               // 64个字节全部写完,返回
}
          

完整的范例程序可点击 这里 下载,范例程序的使用方法可参见:Hello World 程序




管理员信箱: stonewayqi@hotmail.com

欣 欣 学 习 网

粤ICP备2023138008号