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

先认识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;

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

写数据时:

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

读数据时:

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

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

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

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

//写入///
//边沿触发
SDA = 1;
SCL = 0;
SCL = 1;

//电平触发
SDA = 1;
SCL = 0;
SCL = 1;
Delay();
//或者
SCL = 0;
SCL = 1;
SDA = 1;
Delay();

//读数据///
//边沿触发
SCL = 0;
SCL = 1;
DAT = SDA;

//电平触发
SCL = 0;
SCL = 1;
DAT = SDA;
Delay();
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

/************************************************************
*日期:2022年7月27日
*作者:星辰
*文件内容:eeprom功能程序入口
**************************************************************/

#include "uart.h"
#include "eeprom.h"
 
/*************************************************************
*
*函数入口
*
**************************************************************/
void main(void)
{
    uchar flag1 = 0, flag2 = 0;
    uchar uartArr[1] = {0};
    
    I2cStart();
    flag1 = I2cWrite(0xA0) && I2cWrite(0x22) && I2cWrite('A'); //写入数据
    I2cStop();

    if(flag1)
    {
        I2cStart();
        flag2 = I2cWrite(0xA0) && I2cWrite(0x22);
        if(flag2)
        {
            I2cStart();
            I2cWrite(0xA1);
            uartArr[0] = I2cRead();  //读出的数据
        }
        I2cStop();
    }
    
    UartInit();
    SendSomeChar(uartArr, 1);    
}

eeprom.c

/************************************************************
*日期:2022年7月27日
*作者:星辰
*文件内容:eeprom读写
**************************************************************/

#include "eeprom.h"
#include "somedelay.h"

sbit SDA = P2^0;
sbit SCL = P2^1;

/************************************************************
*
*开始标志
*
**************************************************************/
void I2cStart()
{
    SDA = 1;
    Delay10us();
    SCL = 1;
    Delay10us();
    SDA = 0;
    Delay10us();
    SCL = 0;
    Delay10us();
}

/************************************************************
*
*结束标志
*
**************************************************************/
void I2cStop()
{ 
    SDA = 0;
    Delay10us();
    SCL = 1;
    Delay10us();
    SDA = 1;
    Delay10us();
}   

/************************************************************
*
*按字节写入
*
**************************************************************/
uchar I2cWrite(uchar dataToWrite)
{
    uchar i = 0;
    SCL = 0;

    for(i; i < 8; i++)
    {
        SDA = dataToWrite >> 7;    //从最高位开始传输
        dataToWrite <<= 1;
        Delay10us();
        SCL = 1;
        Delay10us();
        SCL = 0;
        Delay10us();
    }
    
    SDA = 1;    //释放总线
    Delay10us();
    SCL = 1;
    Delay10us();
    
    if(SDA == 0)
    {
        SCL = 0;
        Delay10us(); 
        return 1;   //返回1表示写入成功
    }
    
    SCL = 0;
    Delay10us();   
    return 0;   //返回0表示失败		
}

/************************************************************
*
*按字节读取
*读之前也要先写入从设备地址和寻址地址
**************************************************************/
uchar I2cRead()
{
    uchar charGeted = 0;
    uchar i = 0;
    SCL = 0;

    for(i; i < 8; i++)
    {
        SCL = 1;
        Delay10us();
        charGeted |= SDA;
        if(i != 7)
        {
            charGeted  <<= 1;
        }  
        SCL = 0;
        Delay10us();
    }
    
    //读的时候不用发送ACK吧?我只要看有没有读出数据就可以了呀。
    return charGeted;
}

uart.c

/************************************************************
*日期:2022年7月27日
*作者:星辰
*文件内容:串口调试
**************************************************************/

#include "uart.h"
#include "somedelay.h"

/*************************************************************
*
*初始化串口
*
**************************************************************/
void UartInit()
{
    SCON = 0x50;        //设置使用模式1,波特率可变的8位UART,接收模式可用
    TMOD = 0x20;        //配置定时器1处于模式3,8位自动重载,用作波特率发生器
    PCON = 0x80;        //使用波特率加倍
    TH1 = TL1 = 243;    //设置波特率为4800Hz
    TR1 = 1;            //打开定时器1
}

/*************************************************************
*
*串口发送字符串
*
**************************************************************/
void SendSomeChar(uchar charArr[], int len)
{    
    while(1)
    {
        int i = 0;
        
        for(i; i < len; i++)
        {
            SBUF = charArr[i];      //直接把数据扔给硬件即可,之后的由硬件完成
            while(!TI);             //等待上一个数据发完再发下一轮
            TI = 0;                 //软件复位
            Delay100us();           //必须要延时,速度太快,会出错
        }
        
        SBUF = '\r';        //直接把数据扔给硬件即可,之后的由硬件完成
        while(!TI);         //等待上一个数据发完再发下一轮
        TI = 0;             //软件复位标志位
        Delay1s();          //目标对象之间的延时
    }
}

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

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

代码的分层设计

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

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

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

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

### 回答1: 单片机断电记忆恢复是通过保存数据到非易失性存储器(NVM)中,以便在断电后能够恢复上一次的运行状态和数据。在C程序中,我们可以使用以下方法实现单片机断电记忆恢复: 1. 声明并定义需要保存的变量。这些变量包括当前运行状态和需要持久化的数据。例如,可以声明一个全局结构体变量,用于保存程序状态和数据。 ```c struct ProgramState { int state; float data; }; struct ProgramState program; ``` 2. 在程序初始化时,读取NVM中保存的上一次的状态和数据。如果有保存的记录,则将其加载到变量中。如果没有保存的记录,则初始化变量为默认值。 ```c void init_program() { // 从NVM中加载保存的状态和数据 // 如果没有保存的记录,则使用默认值 ... } ``` 3. 在程序运行过程中,当有状态或数据发生更改时,即时更新NVM中的保存记录。 ```c void save_program_state() { // 更新NVM中的保存记录 ... } void update_program_data(float new_data) { // 更新数据 program.data = new_data; // 保存状态和数据 save_program_state(); } ``` 4. 在程序结束或断电前的关键节点(例如循环迭代结束或按下复位按钮)时,将程序的状态和数据保存到NVM中。 ```c void save_program_data() { // 保存状态和数据 save_program_state(); } void main() { // 初始化程序 init_program(); // 主循环 while (1) { // 程序运行代码 // 当需要保存数据时,调用save_program_data() if (need_to_save_data()) { save_program_data(); } } } ``` 通过以上步骤,我们可以在单片机断电后,重新上电时从NVM中恢复上一次的运行状态和数据。这样可以有效避免数据丢失或重新初始化的问题,提高单片机系统的可靠性和稳定性。 ### 回答2: 单片机断电记忆恢复程序是一种保护单片机数据的机制,可以在单片机断电后恢复到之前的状态,保证数据的安全性和稳定性。下面是一个简单的C程序实现。 首先,在程序开始处定义一个全局变量volatile int memory,用于保存单片机的状态。 随后,在主函数中,首先将该变量保存到非易失性存储器(如EEPROM)中,以确保即使断电也能恢复数据。可以使用write_memory()函数将memory的值写入EEPROM。 接着,在程序的入口处,检查是否有保存在EEPROM中的上一次的状态值。可以使用read_memory()函数读取EEPROM中的值,并赋值给memory。 最后,执行其他程序逻辑,对memory进行操作和修改,直到结束。 当单片机断电后再次上电时,程序会先读取EEPROM中的值,将之前的状态值赋给memory变量。这样,即使断电,也能保证程序能从上一次的位置继续执行,达到断电记忆恢复的目的。 需要注意,为了保证数据的安全性,这里用到了volatile关键字,用来告诉编译器该变量可能在任何时刻被修改,不进行优化。同时,也需要确保EEPROM的写入和读取功能正常,以及适当的错误处理机制。 总结来说,单片机断电记忆恢复的C程序通过将程序状态保存在EEPROM中,在重新上电时读取该值,使程序能从上一次的位置继续执行,保证数据的安全性和稳定性。 ### 回答3: 单片机断电记忆恢复是指在单片机意外断电后能够恢复之前的程序执行状态。实现单片机断电记忆恢复的一种方法是通过在程序中保存关键数据,比如变量值、程序计数器(PC)等信息。这样,当单片机重新上电时,可以根据保存的数据恢复之前的执行状态。 在C程序中实现单片机断电记忆恢复有以下几个步骤: 1. 定义需要保存的关键数据:在程序中找出需要保存的变量值和PC的位置,并将它们保存到非易失性存储器中,如EEPROM。 2. 断电前保存数据:在程序中的合适位置,将需要保存的数据写入EEPROM中。可以使用EEPROM写入函数将数据存储到指定的地址。 3. 上电后读取数据:在程序初始化阶段,读取之前保存的数据。可以使用EEPROM读取函数将数据从EEPROM中读取出来,并存储到相应的变量中。 4. 恢复执行状态:根据读取到的数据,将变量值恢复到之前保存的状态,并将PC设置为之前的值,从断点位置继续程序的执行。 需要注意的是,在实际应用中,可能会存在多个需要保存的变量和数据,需要根据具体情况进行相应的保存和恢复操作。 单片机断电记忆恢复可以确保程序在断电后能够继续执行,减少数据丢失,提高系统的可靠性。但需要注意的是,存储器的寿命和存储容量是有限的,因此需要谨慎使用,避免频繁写入和读取数据,以延长存储器的使用寿命。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值