当前位置:   article > 正文

51单片机存储篇:EEPROM(I2C)_单片机eeprom

单片机eeprom

先认识I2C通信

基本概述 

IICInter-Integrated Circuit)其实是IICBus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I²C的正确读法为“I平方C”("I-squared-C"),而“I二C”("I-two-C")则是另一种错误但被广泛使用的读法。自2006年10月1日起,使用I²C协议已经不需要支付专利费,但制造商仍然需要付费以获取I²C从属设备地址。

I2C总线是一种同步、半双工,带数据应答的二线制串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。

主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件。

在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。

如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件,然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下.主机负责产生定时时钟和终止数据传送。

IIC是为了与低速设备通信而发明的。

比如:本来高速设备一个周期发送1位,但是低速设备5个周期才能接收1位。两者速率无法同步,现在通过主设备控制1个时钟频率,使得主从设备能够在同一个时钟频率下工作。

IIC的传输速率比不上SPI。

通信速率一般(kbps级别),不适合语音、视频等信息类型。

主要用途:SoC和周边外设之间的通信(典型的如EEPROM、电容触摸IC、各种sensor等)

物理接口:SCL + SDA
SCL(serial clock):时钟线,传输CLK信号,一般是I2C主设备向从设备提供时钟的通道。
SDA(serial data):数据线,通信数据都通过SDA线传输。

通信特征:串行、同步、非差分、低速率

  • I2C属于串行通信,所有的数据以位为单位在SDA线上串行传输。
  • 同步通信就是通信双方工作在同一个时钟下,一般是通信的A方通过一根CLK信号线传输A自己的时钟给B,B工作在A传输的时钟下。所以同步通信的显著特征就是:通信线中有CLK。
  • 非差分。因为I2C通信速率不高,而且通信双方距离很近,所以使用电平信号通信。
  • 低速率。I2C一般是用在同一个板子上的2个IC之间的通信,而且用来传输的数据量不大,所以本身通信速率很低(一般几百Kbps,不同的I2C芯片的通信速率可能不同,具体在编程的时候要看自己所使用的设备允许的I2C通信最高速率,不能超过这个速率)

突出特征1:主设备+从设备
I2C通信的时候,通信双方地位是不对等的,而是分主设备和从设备。通信由主设备发起,由主设备主导,从设备只是按照I2C协议被动的接受主设备的通信,并及时响应。
谁是主设备、谁是从设备是由通信双方来定的(I2C协议并无规定),一般来说一个芯片可以只做主设备、也可以只做从设备、也可以既当主设备又当从设备(软件配置)。

突出特征2:可以多个设备挂在一条总线上(从设备地址)
I2C通信可以一对一(1个主设备对1个从设备),也可以一对多(1个主设备对多个从设备)。

在这里插入图片描述
主设备来负责调度总线,决定某一时间和哪个从设备通信。

注意:同一时间内,I2C的总线上只能传输一对设备的通信信息,所以同一时间只能有一个从设备和主设备通信,其他从设备处于“休眠”状态,不能出来捣乱,否则通信就乱套了(广播然后匹配的思路)。
每一个I2C从设备在通信中都有一个I2C从设备地址,这个设备地址是从设备本身固有的属性,然后通信时主设备需要知道自己将要通信的那个从设备的地址,然后在通信中通过地址来甄别是不是自己要找的那个从设备。(这个地址是一个电路板上唯一的,不是全球唯一的)

关于I2C的上拉电阻:I2C协议规定,总线空闲时两根线都必须为高。

小对比:

SPI是通过片选来一对多的(需要多根片选线),I2C是通过地址识别来一对多的(用SDA来实现即可,无需额外的地址线)。

时序:起始和结束

I2C总线上有2种状态;空闲态(所有从设备都未和主设备通信,此时总线空闲)和忙态(其中一个从设备在和主设备通信,此时总线被这一对占用,其他从设备必须歇着)。


整个通信分为一个周期一个周期的,两个相邻的通信周期是空闲态。每一个通信周期由一个起始位开始,一个结束位结束,中间是本周期的通信数据。


起始位并不是一个时间点,起始位是一个时间段,在这段时间内总线状态变化情况是:SCL线维持高电平,同时SDA线发生一个从高到低的下降沿。
与起始位相似,结束位也是一个时间段。在这段时间内总线状态变化情况是:SCL线维持高电平,同时SDA线发生一个从低到高的上升沿。

时序:数据传输

每一个通信周期的发起和结束都是由主设备来做的,从设备只有被动的响应主设备,没法自己自发的去做任何事情。
主设备在每个通信周期会先发8位的数据(其中7位是从设备地址,还有1位表示主设备下面要写入还是读出,所以最多能连接2^7即128个从设备)到总线。然后总线上的每个从设备都能收到这个地址,并且收到地址后和自己的设备地址比较看是否相等。如果相等说明主设备本次通信就是给我说话,如果不相等说明这次通信与我无关,不用听了不管了。

时序:ACK应答

ACK,Acknowledge character,确认字符


发送方发送一段数据后,接收方需要回应一个ACK。这个响应本身只有1个bit位,不能携带有效信息,只能表示2个意思(要么表示收到数据,即有效响应;要么表示未收到数据,无效响应)
在某一个通信时刻,主设备和从设备只能有一个在发(占用总线,也就是向总线写),另一个在收(从总线读)。

应答信号是可以被配置有或者没有的。

数据格式如下:


I2C通信时的基本数据单位也是以字节为单位的,每次传输的有效数据都是1个字节(8位)。


起始位及其后的8个clk都是主设备在发送(主设备掌控总线),此时从设备只能读取总线来得知主设备发给它的信息;然后到了第9周期,按照协议规定从设备需要发送ACK给主设备,所以此时主设备必须释放总线(也就是主设备把SDA总线置为高电平然后不要动),同时从设备试图拉低总线发出ACK。

如果从设备拉低总线失败,或者从设备根本就没有拉低总线,则主设备看到的现象就是总线在第9周期仍然一直保持高,这对主设备来说,意味着我没收到ACK,主设备就认为刚才给从设备发送的8字节不对(接收失败)

写数据和读数据

发送一个字节:

SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。

接收应答:

在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答。

接收一个字节:

SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节。

发送应答:

在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

如果是读取数据,我们就会知道是否读到数据,所以一般无需发送应答。

数据传输顺序

SPI可以选择从最低位或者最高位开始发送,之前学DS1302的时候是从最低位开始发的,可是在学习SPI的时候又说是从最高位开始发送的。不知道哪个是对的。后面查资料,又说其实是可以配置的,具体情况具体对待的,看手册就知道了,不必纠结。


虽然很多器件都是用了某种接口协议,比如SPI I2C等等,但是对这些接口协议的封装会根据具体功能有所差异,不是完全一致的。另一方面,有些协议是要收费的,所以制造商只能借鉴,不能完全使用。只是说,用了那些接口协议的思想。

I2C一般是从最高位开始传输的。

关于释放总线

某个引脚如果接了地,那么无法将其拉高;如果引脚是高电平,那么可以控制其拉高或者拉低。因为接地的控制力比高电平高。

释放总线,就是将总线置1,为什么,因为只有置1,其他设备才有可能改变其状态。如果是置0,那么其它设备就无法将其从接地中改变状态。

关于边沿触发和电平触发

在之前学习SPI时,是边沿触发。这里的EEPROM,或者说I2C,是高电平触发。

关于触发方式,在硬件上来说,涉及到触发器和锁存器。如果器件处于锁存状态,那么输出就不受输入影响。如果想要某时刻的输入作用后能影响到输出,就要触发,那么,什么时候触发,就是个问题。

有些电路设计让电平改变的瞬间触发,过了这个点,就会进入锁存状态。这就是边沿触发,分为上升沿触发、下降沿触发以及双边触发。

有些电路设计,只要电平处于某种状态,比如高电平或者低电平,就可以一直触发。比如当处于高电平时,某电路只要输入一变,输出就会随之改变。

更多可以参考:下降沿触发和低电平触发的区别 - 知乎

我们进一步来考虑编程中的处理。

比如,上升沿触发,和高电平触发,在编程时有何区别?

这里很容易搞错,比如上升沿触发是SCL = 0;SCL = 1;高电平触发也是SCL = 0; SCL = 1;

这里,一个是关注瞬间,一个是关注阶段。

写数据时:

先将数据放到数据线上,然后边沿触发就会在改变的瞬间写入;高电平触发在高电平期间就会起作用。按照原理来看,高电平触发可以先放数据然后再将电平拉高,也可以先将电平拉高然后再放数据。有的设计中,要求高电平至少持续一段时间才能将数据写入。

读数据时:

主设备先拉高电平,在电平拉高的瞬间,从设备的数据就会被发送到总线上,然后主设备去获取该数据。如果是电平触发,那么,主设备要先拉高电平,在高电平期间从设备就可以将数据放到总线上,然后主设备就可以去读取。读取完就可以将电平拉低。有的设计中,要求高电平至少持续一段时间从设备才能将数据放到总线上。

在两种方式时,到达高电平之后,都会有一段延时。虽然都是延时,但是作用不同。

边沿触发后的延时是为了构建正常的时钟信号,对延时的时长没有具体要求。

电平触发的延时是为了构建起作用的条件,有时,还会对延时时间有要求。

  1. //写入///
  2. //边沿触发
  3. SDA = 1;
  4. SCL = 0;
  5. SCL = 1;
  6. //电平触发
  7. SDA = 1;
  8. SCL = 0;
  9. SCL = 1;
  10. Delay();
  11. //或者
  12. SCL = 0;
  13. SCL = 1;
  14. SDA = 1;
  15. Delay();
  16. //读数据///
  17. //边沿触发
  18. SCL = 0;
  19. SCL = 1;
  20. DAT = SDA;
  21. //电平触发
  22. SCL = 0;
  23. SCL = 1;
  24. DAT = SDA;
  25. Delay();
  26. SCL = 0;

具体用什么方式来触发,一般取决于从设备所使用的协议要求。

另外,是否需要延时,延时多久,都需要取决于从设备的协议要求,如果不延时,频率过快,从设备可能承受不了这么高的频率,从而导致数据获取错误。

EEPROM

ROM:Read-Only Memory,只读存储器,断电后也能保存数据。

EEPROM:Electrically Erasable Programmable Read-Only Memory,电可擦除可编程只读存储器,最小读写单位为字节,读写速度很慢。

EEPROM存在系统中的2种形式:内置在单片机内部,外部扩展。


EEPROM如何编程?

  • I2C接口底层时序
  • 器件定义的寄存器读写时序

24C02

相关内容查看数据手册,这里提供一篇参考文章:

24C02是一个2Kbit的串行EEPROM存储芯片,可存储256个字节数据。工作电压范围为1.8V到6.0V,具有低功耗CMOS技术,自定时擦写周期,1000000次编程/擦除周期,可保存数据100年。24C02有一个16字节的页写缓冲器和一个写保护功能。通过I2C总线通讯读写芯片数据,通讯时钟频率可达400KHz。

可以通过存储IC的型号来计算芯片的存储容量是多大,比如24C02后面的02表示的是可存储2Kbit的数据,转换为字节的存储量为2*1024/8 = 256Byte;又比如24C04后面的04表示的是可存储4Kbit的数据,转换为字节的储存量为4*1024/8 = 512Byte;以此来类推其它型号的存储空间。
24C02的管脚图如下:

在这里插入图片描述

VCC和VSS是芯片的电源和地,电压的工作范围为:+1.8V~+6.0V。
A0、A1、A2是IC的地址选择脚。
WP是写保护使能脚。
SCL是I2C通讯时钟引脚。
SDA是I2C通讯数据引脚。


下图为芯片从地址:

在这里插入图片描述
以看出对于不同大小的24Cxx,具有不同的从器件地址。由于24C02为2k容量,也就是说只需要参考图中第一行的内容。

芯片的寻址:
AT24C设备地址为如下:前四位固定为1010,A2~A0为由管脚电平。AT24CXX EEPROM Board模块中默认为接地。A2-A0=000,最后一位表示读写操作。所以AT24Cxx的读地址为0xA1,写地址为0xA0。

也就是说:
写24C02的时候,从器件地址为10100000(0xA0);
读24C02的时候,从器件地址为10100001(0xA1)。

片内地址寻址(注意从设备地址和内部寻址的区别):

芯片寻址可对内部256B中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单位。
具体解释:
由于24C02只有256个字节的存储空间,所以只需要1个字节就可以寻址完24C02的存储空间,但是无法寻址完更大容量的存储IC,比如24C04的存储容量是512字节,需要9个bit的地址位才能寻址完。那怎么办呢?该芯片的解决方法是将A0作为其中一个地址位,由上图可以看到,24C04的设备地址内是没有A0参数的,也就是说24C04的A0引脚是不起作用的,这样也就造成了在I2C总线上只能同时挂载4个24C04芯片。其它存储器如24C08、24C16也可以这么类推。

24C02的WP引脚是写保护引脚,当WP引脚接高电平时,24C02只能进行读取操作,不能进行写操作。只有当WP引脚悬空或接低电平时,24C02才能进行写操作。

读写EEPROM

在写EEPROM的时候,要连续写入三个字节,第一个字节为从设备地址,第二个字节为要寻址的内存地址,第三个字节为要写入的数据;

同理,读的时候也要先写入从设备地址,然后就是寻址地址,然后开始读数据。

至于是读还是写,上面说了,用从设备地址的最后一位来表示,1表示读,0表示写。

页写:只发一次从设备地址和寻址首地址,之后直接写入多个数据,就会从首地址开始,按连续地址依次写入。

读写代码实现

注意,截至至2022年7月28日凌晨0点46分,此代码是有问题的,没法跑通,编译没问题,就是从设备怎么都没法应答成功,相对应的应该是数据没有写入到ROM中,不知道哪里出了问题,先放这,等查查资料再看看。

eeprommain.c

  1. /************************************************************
  2. *日期:2022727
  3. *作者:星辰
  4. *文件内容:eeprom功能程序入口
  5. **************************************************************/
  6. #include "uart.h"
  7. #include "eeprom.h"
  8. /*************************************************************
  9. *
  10. *函数入口
  11. *
  12. **************************************************************/
  13. void main(void)
  14. {
  15. uchar flag1 = 0, flag2 = 0;
  16. uchar uartArr[1] = {0};
  17. I2cStart();
  18. flag1 = I2cWrite(0xA0) && I2cWrite(0x22) && I2cWrite('A'); //写入数据
  19. I2cStop();
  20. if(flag1)
  21. {
  22. I2cStart();
  23. flag2 = I2cWrite(0xA0) && I2cWrite(0x22);
  24. if(flag2)
  25. {
  26. I2cStart();
  27. I2cWrite(0xA1);
  28. uartArr[0] = I2cRead(); //读出的数据
  29. }
  30. I2cStop();
  31. }
  32. UartInit();
  33. SendSomeChar(uartArr, 1);
  34. }

eeprom.c

  1. /************************************************************
  2. *日期:2022727
  3. *作者:星辰
  4. *文件内容:eeprom读写
  5. **************************************************************/
  6. #include "eeprom.h"
  7. #include "somedelay.h"
  8. sbit SDA = P2^0;
  9. sbit SCL = P2^1;
  10. /************************************************************
  11. *
  12. *开始标志
  13. *
  14. **************************************************************/
  15. void I2cStart()
  16. {
  17. SDA = 1;
  18. Delay10us();
  19. SCL = 1;
  20. Delay10us();
  21. SDA = 0;
  22. Delay10us();
  23. SCL = 0;
  24. Delay10us();
  25. }
  26. /************************************************************
  27. *
  28. *结束标志
  29. *
  30. **************************************************************/
  31. void I2cStop()
  32. {
  33. SDA = 0;
  34. Delay10us();
  35. SCL = 1;
  36. Delay10us();
  37. SDA = 1;
  38. Delay10us();
  39. }
  40. /************************************************************
  41. *
  42. *按字节写入
  43. *
  44. **************************************************************/
  45. uchar I2cWrite(uchar dataToWrite)
  46. {
  47. uchar i = 0;
  48. SCL = 0;
  49. for(i; i < 8; i++)
  50. {
  51. SDA = dataToWrite >> 7; //从最高位开始传输
  52. dataToWrite <<= 1;
  53. Delay10us();
  54. SCL = 1;
  55. Delay10us();
  56. SCL = 0;
  57. Delay10us();
  58. }
  59. SDA = 1; //释放总线
  60. Delay10us();
  61. SCL = 1;
  62. Delay10us();
  63. if(SDA == 0)
  64. {
  65. SCL = 0;
  66. Delay10us();
  67. return 1; //返回1表示写入成功
  68. }
  69. SCL = 0;
  70. Delay10us();
  71. return 0; //返回0表示失败
  72. }
  73. /************************************************************
  74. *
  75. *按字节读取
  76. *读之前也要先写入从设备地址和寻址地址
  77. **************************************************************/
  78. uchar I2cRead()
  79. {
  80. uchar charGeted = 0;
  81. uchar i = 0;
  82. SCL = 0;
  83. for(i; i < 8; i++)
  84. {
  85. SCL = 1;
  86. Delay10us();
  87. charGeted |= SDA;
  88. if(i != 7)
  89. {
  90. charGeted <<= 1;
  91. }
  92. SCL = 0;
  93. Delay10us();
  94. }
  95. //读的时候不用发送ACK吧?我只要看有没有读出数据就可以了呀。
  96. return charGeted;
  97. }

uart.c

  1. /************************************************************
  2. *日期:2022727
  3. *作者:星辰
  4. *文件内容:串口调试
  5. **************************************************************/
  6. #include "uart.h"
  7. #include "somedelay.h"
  8. /*************************************************************
  9. *
  10. *初始化串口
  11. *
  12. **************************************************************/
  13. void UartInit()
  14. {
  15. SCON = 0x50; //设置使用模式1,波特率可变的8位UART,接收模式可用
  16. TMOD = 0x20; //配置定时器1处于模式38位自动重载,用作波特率发生器
  17. PCON = 0x80; //使用波特率加倍
  18. TH1 = TL1 = 243; //设置波特率为4800Hz
  19. TR1 = 1; //打开定时器1
  20. }
  21. /*************************************************************
  22. *
  23. *串口发送字符串
  24. *
  25. **************************************************************/
  26. void SendSomeChar(uchar charArr[], int len)
  27. {
  28. while(1)
  29. {
  30. int i = 0;
  31. for(i; i < len; i++)
  32. {
  33. SBUF = charArr[i]; //直接把数据扔给硬件即可,之后的由硬件完成
  34. while(!TI); //等待上一个数据发完再发下一轮
  35. TI = 0; //软件复位
  36. Delay100us(); //必须要延时,速度太快,会出错
  37. }
  38. SBUF = '\r'; //直接把数据扔给硬件即可,之后的由硬件完成
  39. while(!TI); //等待上一个数据发完再发下一轮
  40. TI = 0; //软件复位标志位
  41. Delay1s(); //目标对象之间的延时
  42. }
  43. }

其他的一些延时代码就不放了~~~~~~~~~~~

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

代码的分层设计

经过SPI和I2C等协议的学习,可以看出,有些外设使用了这些协议来实现功能,那么,这里就涉及到了两个层次。

底层用的就是这些协议,涉及到这些协议的基本读写。

高层,也就是具体的外设,也有一些具体的协议,通过读写特定的数据,实现具体的功能。

所以,在编程时,通常底层时序单独一个文件,高层时序单独一个文件,然后在main函数中,使用这些封装好的高层函数,而不直接使用底层函数。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号