项目一:IIC读写EEPROM AT24C02

回头想了想在工作中调过的EEPROM还挺多的,有M24M02 、M28010 、AT24C02等,今天介绍一下AT24C02吧,包括初始化,读写,以及看用户手册应该注意哪些地方,硬件II2,软件IIC等,硬件IIC简单,但是硬件IIC可能会有一些乱七八糟的bug有时候会比较难受~

下面给大家一一介绍下吧

一、AT24C02简介

1.1 特点

文档已经上传了,需要的同学可以自行下载哈,下面为下载链接。AT24C02

我大概照着文档翻译了一下:

  • 存储器内部按组织256字节 × 8位 (2K)组织
  • 双线串行接口(IIC)
  • 兼容400kHz通信速率
  • 具有硬件数据保护的写保护引脚
  • 8字节/页写模式
  • 允许部分页写入
  • 高可靠性:100万次写周期,数据保留:100年

1.2 引脚定义

串行时钟(SCL)、串行数据(SDA)不再赘述。A2,A1和A0引脚用于AT24C02的设备地址输入。WP为写保护引脚,提供硬件数据保护。

写保护引脚在连接到地(GND)时允许正常的读写操作。当写保护引脚接在VCC上时,写保护功能开启,操作如上表所示。

在板子的硬件原理图上可以看到,设备地址输入A2、A1、A0都为0(接地了),WP已经接在GND上关闭了写保护,我们可以正常读写。

需要的话,可以将WP接到一个GPIO引脚,使用推挽输出,防止数据误操作,可任意保护关键数据。

1.3 存储空间

AT24C02,2K,串行EEPROM内部组织为32页,每页8字节,2K需要一个8位的字地址进行随机字寻址。

2K EEPROM设备都需要一个8位设备地址字包含一个启动条件,以使芯片能够进行读或写操作。

设备地址字前4位最高有效位为1010。这对所有串行EEPROM设备都是通用的。接下来的3位是1K/2K EEPROM的A2、A1和A0设备地址位。设备地址的第8位是读写操作选择位。如果该位高,则进行读操作;如果该位低,则进行写操作。

综上,如果对AT24C02进行读操作,则设备地址为10100001B=A1H

如果对AT24C02进行写操作,则设备地址为10100000B=A0H.

二、 AT24C01编程 

 2.1 I2C结构体初始化

首先先介绍一下编程思想:

  1. 初始化IIC相关的GPIO
  2. 配置IIC外设的工作模式
  3. 编写IIC写入EEPROM的Byte函数
  4. 编写IIC读取EEPROM的RANDOM READ函数
  5. 使用read函数及write函数对读写进行校验
  6. 编写page write 以及seq read函数并进行校验

初始化一般就放在bsp_iic.c里面

/**
  * @brief  I2C_EEPROM GPIO 配置,工作参数配置
  * @param  无
  * @retval 无
  */
void I2C_EEPROM_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	I2C_InitTypeDef I2C_InitStructure;

	// 打开I2C GPIO的时钟
	EEPROM_I2C_GPIO_APBxClkCmd(EEPROM_SCL_GPIO_CLK|EEPROM_SDA_GPIO_CLK, ENABLE);//加个或相当于把它们都初始化
	
	// 打开I2C 外设的时钟
	EEPROM_I2C_APBxClkCmd(EEPROM_I2C_CLK, ENABLE);

	// 将I2C 的SCL配置为开漏复用模式
	GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(EEPROM_I2C_SCL_GPIO_PORT, &GPIO_InitStructure);

  // 将I2C 的SDA配置为开漏复用模式
	GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_GPIO_PIN; 
	GPIO_Init(EEPROM_I2C_SDA_GPIO_PORT, &GPIO_InitStructure);
	
	//配置I2C的工作参数
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//使能应答
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//使用7位设备地址
	I2C_InitStructure.I2C_ClockSpeed = EEPROM_I2C_BAUDRATE;//配置SCL时钟频率
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_16_9;//选16:9 与2:1没有任何影响
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	I2C_InitStructure.I2C_OwnAddress1 = STM32_I2C_OWN_ADDR;//这是STM32 I2C自身设备地址,只要是总线上唯一即可
	
	I2C_Init(EEPROM_I2C,&I2C_InitStructure);
	
	// 使能I2C
	I2C_Cmd (EEPROM_I2C, ENABLE);	    
}

相关的宏配置如下 bsp_iic.h

/** 
  * IIC E2PROM的引脚定义
  */

/*等待超时时间*/
#define I2CT_FLAG_TIMEOUT         ((uint32_t)0x1000)
#define I2CT_LONG_TIMEOUT         ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))

//EEPROM的总线地址(8位)	
#define	 EEPROM_ADDR				   0xA0
// IC
//只要不与总线上从设备地址一样就可以了
#define  STM32_I2C_OWN_ADDR			   0x5f	

/* AT24C01/02每页有8个字节 */
#define I2C_PageSize           8

#define  EEPROM_I2C                    I2C1
#define  EEPROM_I2C_CLK                RCC_APB1Periph_I2C1
#define  EEPROM_I2C_APBxClkCmd         RCC_APB1PeriphClockCmd
#define  EEPROM_I2C_BAUDRATE           400000

// IIC GPIO 引脚宏定义
#define  EEPROM_SCL_GPIO_CLK           (RCC_APB2Periph_GPIOB)
#define  EEPROM_SDA_GPIO_CLK           (RCC_APB2Periph_GPIOB)

#define  EEPROM_I2C_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
    
#define  EEPROM_I2C_SCL_GPIO_PORT       GPIOB   
#define  EEPROM_I2C_SCL_GPIO_PIN        GPIO_Pin_6

#define  EEPROM_I2C_SDA_GPIO_PORT       GPIOB
#define  EEPROM_I2C_SDA_GPIO_PIN        GPIO_Pin_7

2.2 硬件IIC

2.2.1 写操作

从手册里可以看出写数据有字写页写两种方式:(几乎所有的EEPROM都是这的)

  • 字写:一次写入一个字节。
  • 页写:1K/2K EEPROM能够一次写入一个8字节的页。

如果准备写入数据,则需要知道数据的存储地址。因为AT24C02的存储空间为2K(2^11),故寻址空间为0~2^11-1,即000H~7FFH。每页8字节,故第1页地址000H,第2页地址008H,第3页地址010H,……,第256页地址7F8H。

2.2.1 .1  Byte write 写一个字节

下图为字写的过程

 解读时序的时候,对于新手同学还是最好参考一下我之前的博客2.2 STM32的I2C通讯过程章节,对于写一个字节为主发,参考主发送器通讯过程, 可使用STM32标准库函数来直接检测这些事件的复合标志,降低编程难度。

 结合主发送器通讯过程图,那么写字节的时序图可以得到下图:

则根据时序图可以得到如下:

/**
  * @brief 向EEPROM写入一个字节
  * @param   
  *		@arg pBuffer:缓冲区指针
  *		@arg WriteAddr:写地址 
  * @retval  无
  */
uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr) 
{
  /*  产生起始信号 */
  I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);

  I2CTimeout = I2CT_FLAG_TIMEOUT;  
  /* 检测EV5事件并清除标志 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))  
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);	/* 加上超时判断 不然就卡死在哪个地方都不知道 */
  } 
  
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /* EV5 事件被检测到 发送设备地址 */
  I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
  
  /* 检测EV6事件 并清除事件标志 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
  }  
  /* EV6 事件被检测到 发送内存要操作的存储单元地址 */
  I2C_SendData(EEPROM_I2Cx, WriteAddr);
  
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /*检测EV8事件 并清除 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
  } 
  
  /* EV8 事件被检测到 发送要存储的数据 */
  I2C_SendData(EEPROM_I2Cx, *pBuffer); 
  
  I2CTimeout = I2CT_FLAG_TIMEOUT;  
  /* 检测EV8_2事件 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
  } 
  
  /* 数据传输完成,产生停止信号 */
  I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
  
  return 1;
}
2.2.1 .2  Page write 页写

下图描述了页编程写数据的过程,根据图中的分析和前文的讲述,可以写出也写函数:

/**
  * @brief   在EEPROM的一个写循环中可以写多个字节,但一次写入的字节数
  *          不能超过EEPROM页的大小,AT24C02每页有8个字节 
  *			 页写入有一个要求首地址必须跟8地址对齐  WriteAddr % 8 == 0  即为对齐
  * @param   
  *		@arg pBuffer:缓冲区指针
  *		@arg WriteAddr:写地址
  *     @arg NumByteToWrite:写的字节数
  * @retval  无
  */
uint32_t I2C_EE_PageWrite(u8* pBuffer, u8 WriteAddr, u8 NumByteToWrite)
{
  I2CTimeout = I2CT_LONG_TIMEOUT;

  while(I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))   
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);
  } 
  
  /* 产生起始信号 */
  I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
  
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /* 检测EV5事件 并清除 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))  
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);
  } 
  
  /* EV5 事件被检测到 发送设备地址  这个0xA0是读地址或者写地址都可以/
  该函数可以根据第三个输入参数来给总线发送读或者写地址 它会自己来处理*/
  I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
  
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /* 检测EV6事件 并清除*/
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))  
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);
  } 
  
  /* EV6 事件被检测到 发送内存要操作的存储单元地址 */    
  I2C_SendData(EEPROM_I2Cx, WriteAddr);  

  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /* 检测EV8事件  并清除*/
  while(! I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);
  } 

  /* While there is data to be written */
  while(NumByteToWrite--)  
  {
    /* EV8 事件被检测到 发送要存储的数据 */
    I2C_SendData(EEPROM_I2Cx, *pBuffer); 

    /* Point to the next byte to be written */
    pBuffer++; 
  
    I2CTimeout = I2CT_FLAG_TIMEOUT;

    /* 检测EV8_2事件  */
    while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    {
      if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);
    } 
  }

  /* 数据传输完成,产生停止信号 */
  I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
  
  return 1;
}

到这之后手册上关于写的部分就结束了,但是直接操作这两个接口很难直接使用啊,假如用户要写5个字节,难道要调用五次写字节函数吗?或者说用户写十多个字节,难道要分别调用吗?

2.2.1.3 跨页写

这样使用起来非常不便。很多时候,我们希望的是可以通过某个函数,传入数据,地址,写字节的个数,这样去实现写操作,那么我们可以在字写 以及 页写的基础上封装一个写buffer的函数如下:

/**
  * @brief   将缓冲区中的数据写到I2C EEPROM中
  * @param   
  *		@arg pBuffer:缓冲区指针
  *		@arg WriteAddr:写地址
  *     @arg NumByteToWrite:写的字节数
  * @retval  无
  */
void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr, u16 NumByteToWrite)
{
  u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0,temp = 0;

  Addr = WriteAddr % I2C_PageSize;
  count = I2C_PageSize - Addr;
  NumOfPage =  NumByteToWrite / I2C_PageSize;
  NumOfSingle = NumByteToWrite % I2C_PageSize;
 
  /* If WriteAddr is I2C_PageSize aligned  */
  if(Addr == 0) 
  {
    /* If NumByteToWrite < I2C_PageSize */
    if(NumOfPage == 0) 
    {
      I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
      I2C_EE_WaitEepromStandbyState();
    }
    /* If NumByteToWrite > I2C_PageSize */
    else  
    {
      while(NumOfPage--)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize); 
				I2C_EE_WaitEepromStandbyState();
        WriteAddr +=  I2C_PageSize;
        pBuffer += I2C_PageSize;
      }

      if(NumOfSingle!=0)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
        I2C_EE_WaitEepromStandbyState();
      }
    }
  }
  /* If WriteAddr is not I2C_PageSize aligned  */
  else 
  {
    /* If NumByteToWrite < I2C_PageSize */
    if(NumOfPage== 0) 
    {
			/* 当前页剩余的count 个数比NumofSingle小,一页写不完*/
			if(NumOfSingle > count)
			{
				temp = NumOfSingle - count;
				/* 先写满当前页*/
				I2C_EE_PageWrite(pBuffer, WriteAddr, count);
				I2C_EE_WaitEepromStandbyState();
				WriteAddr += count;
				pBuffer += count;
				/* 再写剩余的数据 */
				I2C_EE_PageWrite(pBuffer, WriteAddr, temp);
				I2C_EE_WaitEepromStandbyState();
			}
			else{/* 当前页剩下的count个数 能写完 numofSingle个数据*/
				
				I2C_EE_PageWrite(pBuffer,WriteAddr,NumOfSingle);
			}
			
    }
    /* If NumByteToWrite > I2C_PageSize */
    else
    {
      NumByteToWrite -= count;
      NumOfPage =  NumByteToWrite / I2C_PageSize;
      NumOfSingle = NumByteToWrite % I2C_PageSize;	
      
      if(count != 0)
      {  
        I2C_EE_PageWrite(pBuffer, WriteAddr, count);
        I2C_EE_WaitEepromStandbyState();
        WriteAddr += count;
        pBuffer += count;
      } 
      
      while(NumOfPage--)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
        I2C_EE_WaitEepromStandbyState();
        WriteAddr +=  I2C_PageSize;
        pBuffer += I2C_PageSize;  
      }
      if(NumOfSingle != 0)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); 
        I2C_EE_WaitEepromStandbyState();
      }
    }
  }  
}

 2.2.2 读操作

2.2.2.1 读当前地址Current Address Read

  1. 当前地址读:内部地址计数器保存着上次访问时最后一个地址加1的值。(使用”当前地址读“读出的数据,其地址为上次访问的最后一个地址加1。)
  2. 只要芯片有电,该地址就一直保存,当读到最后页的最后字节,地址会回转到0:
  3. 当写到某页尾的最后一个字节,地址会回转到该页的首字节。
  4. 接收器件地址(读/写选择位为"1")、EEPROM应答ACK后,当前地址的数据就随时钟送出。主器件无需应答"0",但需发送停止条件。

但其实这个函数没啥用~因为所谓的当前地址,我们很难知道啊,假设我们刚刚操作过0x10地址,调用该函数就会读出0x10地址存的数,所以我们此处仅做介绍,不实现哈,需要的同学可以根据时序自行实现~

2.2.2.2 读随机地址RANDOM READ

 随机读需先写一个目标字地址,一旦EEPROM接收器设备地址(读/写选择位为"0")和字地址并应答了ACK,主机就产生一个重复的起始条件。然后,主器件发送设备地址(读/写选择位为"1"),EEPROM应答ACK,并随时钟送出数据。主器件无需应答"0",但需发送停止条件。

 

这个和连续读是一样的,只不过连续读在它的基础上判断了长度,所以此处也不进行分析了~

2.2.2.3 连续读地址Sequential Read

AT24C02还有一种读操作,就是连续读取。连续读取由当前地址读取或随机地址读取启动。主机接收到一个数据字后,它以确认响应。只要 EEPROM 接收到应答,将自动增加字地址并继续随时钟发送后面的数据。当达到内存地址限制时,地址自动回转到0,认可继续连续读取数据。主机不应答“0”,而发送停止条件,即可结束连续读操作。

我们对时序进行解读可以得出连续读的函数如下:

/**
  * @brief   从EEPROM里面读取一块数据 
  * @param   
  *		@arg pBuffer:存放从EEPROM读取的数据的缓冲区指针
  *		@arg ReadAddr:接收数据的EEPROM的地址
  *     @arg NumByteToWrite:要从EEPROM读取的字节数
  * @retval  无
  */
uint32_t I2C_EE_BufferRead(u8* pBuffer, u8 ReadAddr, u16 NumByteToRead)
{  
  
  I2CTimeout = I2CT_LONG_TIMEOUT;
  
  //*((u8 *)0x4001080c) |=0x80; 
  while(I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
   }
  
  /* 产生起始信号 */
  I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
  //*((u8 *)0x4001080c) &=~0x80;
  
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /* 检测EV5事件并对事件进行清除 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
   }
  
  /*EV5 事件被检测到 发送设备地址 */
  I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);

  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /* 检测EV6事件 并对事件进行清除 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);
   }
    
  /* Clear EV6 by setting again the PE bit */
  I2C_Cmd(EEPROM_I2Cx, ENABLE);

  /* EV6 事件被检测到 发送内存要操作的存储单元地址 */
  I2C_SendData(EEPROM_I2Cx, ReadAddr);  

   
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /* 检测EV8事件 并对事件进行清除 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);
   }
    
  /* 第二次起始信号(把上面的直接复制下来 然后方向需要改  */  
  I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
  
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /* 检测EV5事件 并对事件进行清除 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);
   }
    
  /* EV5 事件被检测到 发送设备地址 */
  I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Receiver);
  
  I2CTimeout = I2CT_FLAG_TIMEOUT;
  /* 检测EV6事件 并对事件进行清除 */
  while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
  {
    if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);
   }
  
  /* While there is data to be read */
  while(NumByteToRead)  
  {
		/* 如果是最后一个字节 就产生一个NACK	*/
    if(NumByteToRead == 1)
    {
      /* Disable Acknowledgement */
      I2C_AcknowledgeConfig(EEPROM_I2Cx, DISABLE);
      
      /* 数据传输完成,产生停止信号 */
      I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
    }
    
    I2CTimeout = I2CT_LONG_TIMEOUT;
		
		/* 检测EV7事件 并对事件进行清除 */
	while(I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)  
	{
		if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
	} 
    {      
      /*EV7事件被检测到,即数据寄存器有新的数据 */
      *pBuffer = I2C_ReceiveData(EEPROM_I2Cx);

      /* 指针++ 指向下一个内存单元 */
      pBuffer++; 
      
      /* Decrement the read bytes counter */
      NumByteToRead--;        
    }   
  }

  /* 重新配置ACK使能,让它默认为响应,以便下次通讯 */
  I2C_AcknowledgeConfig(EEPROM_I2Cx, ENABLE);
  
    return 1;
}

一定要注意手册中的

ACKNOWLEDGE POLLING: Once the internally timed write cycle has started and the EEPROM inputs are disabled, acknowledge polling can be initiated. This involves sending a start condition followed by the device address word. The read/write bit is representative of the operation desired. Only if the internal write cycle has completed will the EEPROM respond with a “0”, allowing the read or write sequence to continue.

这段话的意思是:

"确认轮询:一旦内部定时写周期开始并且EEPROM输入被禁用,就可以启动确认轮询。这涉及发送起始条件,然后是设备地址字。读/写位代表所需的操作。只有在内部写周期完成后,EEPROM才会以“0”回应,允许读取或写入序列继续。"

这段话涉及到EEPROM(可擦可编程只读存储器)中的确认轮询(acknowledge polling)过程。以下是这个过程的简要解释:

  1. 内部定时写周期: 这指的是EEPROM用于写入数据的内部过程。一旦启动了这个过程,EEPROM就会忙于写周期,无法立即响应读取或写入请求。

  2. 禁用EEPROM输入: 在内部写周期期间,EEPROM的输入被禁用。这意味着外部信号或数据输入在写周期完成之前可能不会被接受。

  3. 确认轮询: 为了检查内部写周期是否已完成,执行确认轮询。这包括发送起始条件,然后是设备地址字。设备地址字中的读/写位表示所需的操作(读取或写入)。

  4. 读/写位: 设备地址字中的读/写位非常重要。如果内部写周期尚未完成,EEPROM将不会以确认方式响应。在这种情况下,读取或写入序列不应继续。

  5. 响应: 如果内部写周期已完成,EEPROM将以“0”作为确认响应。这个响应表示EEPROM已准备好进行后续的读取或写入操作。

  6. 继续序列: 一旦EEPROM以“0”响应,读取或写入序列就可以继续进行,因为这表明内部写周期已经结束,EEPROM准备好进行外部操作。

这个过程有助于确保数据的完整性,并防止内部写周期与外部读取或写入操作发生冲突。确认轮询是在I2C通信中与EEPROM设备一起常用的技术。

Byte_Write函数,实际上它只是通过I2C协议将数据传给E2PROM而已,我们写完这个数据后,直接又读取了,这个时候会卡死,因为EEPROM正在产生一个内部时序(比如正在把0x5A写到0x01地址),内部时序是不会响应我们的读的起始信号的。因此需要优化。

优化:加一个函数去保证内部时序已经走完了。不然写的数据多了不知道是否写完了,死等也可以,但是最好判断内部时序,void EEPROM_WaitForWriteEnd(void)。

通过检测EEPROM的响应来判断它的内部写时序是否完成

/**
  * @brief  Wait for EEPROM Standby state 
  * @param  ÎÞ
  * @retval ÎÞ
  */
//通过检测IIC的响应,来判断它的读写时序是否完成
void I2C_EE_WaitEepromStandbyState(void)      
{
	vu16 SR1_Tmp = 0;
	I2CTimeout = I2CT_LONG_TIMEOUT;

  do
  {
	I2CTimeout--;
    /* Send START condition */
    I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
    /* Read I2C1 SR1 register */
    SR1_Tmp = I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1);
    /* Send EEPROM address for write */
    I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
  }while(!(I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1) & 0x0002) && (I2CTimeout > 0));//I2C_Register_SR1 的第二位就是 I2C_FLAG_ADDR
  
  /* Clear AF flag */
  I2C_ClearFlag(EEPROM_I2Cx, I2C_FLAG_AF);
    /* STOP condition */    
    I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE); 
}

对硬件IIC介绍完毕接下来是软件IIC的介绍。

2.3 软件IIC

1.“软件模拟协议”:直接控制GPIO引脚电平产生通讯时序时,需要由CPU控制每个时刻的引脚状态
 
2.“硬件协议”:STM32的I2C片上外设专门负责实现I2C通讯协议, 只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来, CPU只要检测该外设的状态和访问数据寄存器,就能完成数据收发。 这种由硬件外设处理I2C协议的方式减轻了CPU的工作,且使软件设计更加简单。

那为什么硬件I2C可以减轻CPU的工作,且软件设计更加简单,那我们为什么还要用软件I2C呢?

主要因为STM32的硬件I2C经常会有很多错误,经常使用的时候会发现I2C会卡死在某一些环节里面,或者使用了液晶屏的时候,FSMC使用了某些公用的引脚,比如PB7

当我们初始化FSMC的时候,虽然我们没有初始化这个引脚,但是我们使能了FSMC,一初始化FSMC,就会影响到硬件I2C了,没有办法,后面只能使用软件I2C来规避这个问题,所以在业界基本上很多都对STM32的硬件I2C很抱怨了。所以这才有了软件模拟I2C它跟

它跟硬件I2C主要的区别:就是直接控制GPIO引脚电平产生通讯时序时,需要由CPU控制每个时刻的引脚状态。

下面我提供一下bsp_i2c_ee.c,大家只要理解编程思想就可以了

 ******************************************************************************
  */ 

#include "bsp_i2c_ee.h"
#include "bsp_i2c_gpio.h"
#include "bsp_usart.h" 

/*
*********************************************************************************************************
*	函 数 名: ee_CheckOk
*	功能说明: 判断串行EERPOM是否正常
*	形    参:无
*	返 回 值: 1 表示正常, 0 表示不正常
*********************************************************************************************************
*/
uint8_t ee_CheckOk(void)
{
	if (i2c_CheckDevice(EEPROM_DEV_ADDR) == 0)
	{
		return 1;
	}
	else
	{
		/* 失败后,切记发送I2C总线停止信号 */
		i2c_Stop();		
		return 0;
	}
}

/*
*********************************************************************************************************
*	函 数 名: ee_ReadBytes
*	功能说明: 从串行EEPROM指定地址处开始读取若干数据
*	形    参:_usAddress : 起始地址
*			 _usSize : 数据长度,单位为字节
*			 _pReadBuf : 存放读到的数据的缓冲区指针
*	返 回 值: 0 表示失败,1表示成功
*********************************************************************************************************
*/
uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
{
	uint16_t i;
	
	/* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */
	
	/* 第1步:发起I2C总线启动信号 */
	i2c_Start();
	
	/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
	i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR);	/* 此处是写指令 */
	 
	/* 第3步:等待ACK */
	if (i2c_WaitAck() != 0)
	{
		goto cmd_fail;	/* EEPROM器件无应答 */
	}

	/* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
	i2c_SendByte((uint8_t)_usAddress);
	
	/* 第5步:等待ACK */
	if (i2c_WaitAck() != 0)
	{
		goto cmd_fail;	/* EEPROM器件无应答 */
	}
	
	/* 第6步:重新启动I2C总线。前面的代码的目的向EEPROM传送地址,下面开始读取数据 */
	i2c_Start();
	
	/* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
	i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_RD);	/* 此处是读指令 */
	
	/* 第8步:发送ACK */
	if (i2c_WaitAck() != 0)
	{
		goto cmd_fail;	/* EEPROM器件无应答 */
	}	
	
	/* 第9步:循环读取数据 */
	for (i = 0; i < _usSize; i++)
	{
		_pReadBuf[i] = i2c_ReadByte();	/* 读1个字节 */
		
		/* 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack */
		if (i != _usSize - 1)
		{
			i2c_Ack();	/* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */
		}
		else
		{
			i2c_NAck();	/* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
		}
	}
	/* 发送I2C总线停止信号 */
	i2c_Stop();
	return 1;	/* 执行成功 */

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
	/* 发送I2C总线停止信号 */
	i2c_Stop();
	return 0;
}

/*
*********************************************************************************************************
*	函 数 名: ee_WriteBytes
*	功能说明: 向串行EEPROM指定地址写入若干数据,采用页写操作提高写入效率
*	形    参:_usAddress : 起始地址
*			 _usSize : 数据长度,单位为字节
*			 _pWriteBuf : 存放读到的数据的缓冲区指针
*	返 回 值: 0 表示失败,1表示成功
*********************************************************************************************************
*/
uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
{
	uint16_t i,m;
	uint16_t usAddr;
	
	/* 
		写串行EEPROM不像读操作可以连续读取很多字节,每次写操作只能在同一个page。
		对于24xx02,page size = 8
		简单的处理方法为:按字节写操作模式,每写1个字节,都发送地址
		为了提高连续写的效率: 本函数采用page wirte操作。
	*/

	usAddr = _usAddress;	
	for (i = 0; i < _usSize; i++)
	{
		/* 当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址 */
		if ((i == 0) || (usAddr & (EEPROM_PAGE_SIZE - 1)) == 0)
		{
			/* 第0步:发停止信号,启动内部写操作 */
			i2c_Stop();
			
			/* 通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms 			
				CLK频率为200KHz时,查询次数为30次左右
			*/
			for (m = 0; m < 1000; m++)
			{				
				/* 第1步:发起I2C总线启动信号 */
				i2c_Start();
				
				/* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
				i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR);	/* 此处是写指令 */
				
				/* 第3步:发送一个时钟,判断器件是否正确应答 */
				if (i2c_WaitAck() == 0)
				{
					break;
				}
			}
			if (m  == 1000)
			{
				goto cmd_fail;	/* EEPROM器件写超时 */
			}
		
			/* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
			i2c_SendByte((uint8_t)usAddr);
			
			/* 第5步:等待ACK */
			if (i2c_WaitAck() != 0)
			{
				goto cmd_fail;	/* EEPROM器件无应答 */
			}
		}
	
		/* 第6步:开始写入数据 */
		i2c_SendByte(_pWriteBuf[i]);
	
		/* 第7步:发送ACK */
		if (i2c_WaitAck() != 0)
		{
			goto cmd_fail;	/* EEPROM器件无应答 */
		}

		usAddr++;	/* 地址增1 */		
	}
	
	/* 命令执行成功,发送I2C总线停止信号 */
	i2c_Stop();
	return 1;

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
	/* 发送I2C总线停止信号 */
	i2c_Stop();
	return 0;
}


/* 这一步起始没有必要 擦除一般是在flash中有需要 */
void ee_Erase(void)
{
	uint16_t i;
	uint8_t buf[EEPROM_SIZE];
	
	/* 填充缓冲区 */
	for (i = 0; i < EEPROM_SIZE; i++)
	{
		buf[i] = 0xFF;
	}
	
	/* 写EEPROM, 起始地址 = 0,数据长度为 256 */
	if (ee_WriteBytes(buf, 0, EEPROM_SIZE) == 0)
	{
		printf("擦除eeprom出错!\r\n");
		return;
	}
	else
	{
		printf("擦除eeprom成功!\r\n");
	}
}


/*--------------------------------------------------------------------------------------------------*/
static void ee_Delay(__IO uint32_t nCount)	 //简单的延时函数
{
	for(; nCount != 0; nCount--);
}


/*
 * eeprom AT24C02 读写测试
 * 正常返回1,异常返回0
 */
uint8_t ee_Test(void) 
{
  uint16_t i;
	uint8_t write_buf[EEPROM_SIZE];
  uint8_t read_buf[EEPROM_SIZE];
  
/*-----------------------------------------------------------------------------------*/  
  if (ee_CheckOk() == 0)
	{
		/* 没有检测到EEPROM */
		printf("没有检测到串行EEPROM!\r\n");
				
		return 0;
	}
/*------------------------------------------------------------------------------------*/  
  /* 填充测试缓冲区 */
	for (i = 0; i < EEPROM_SIZE; i++)
	{		
		write_buf[i] = i;
	}
/*------------------------------------------------------------------------------------*/  
  if (ee_WriteBytes(write_buf, 0, EEPROM_SIZE) == 0)
	{
		printf("写eeprom出错!\r\n");
		return 0;
	}
	else
	{		
		printf("写eeprom成功!\r\n");
	}
  
  /*写完之后需要适当的延时再去读,不然会出错*/
  ee_Delay(0x0FFFFF);
/*-----------------------------------------------------------------------------------*/
  if (ee_ReadBytes(read_buf, 0, EEPROM_SIZE) == 0)
	{
		printf("读eeprom出错!\r\n");
		return 0;
	}
	else
	{		
		printf("读eeprom成功,数据如下:\r\n");
	}
/*-----------------------------------------------------------------------------------*/  
  for (i = 0; i < EEPROM_SIZE; i++)
	{
		if(read_buf[i] != write_buf[i])
		{
			printf("0x%02X ", read_buf[i]);
			printf("错误:EEPROM读出与写入的数据不一致");
			return 0;
		}
    printf(" %02X", read_buf[i]);
		
		if ((i & 15) == 15)
		{
			printf("\r\n");	
		}		
	}
  printf("eeprom读写测试成功\r\n");
  return 1;
}
/*********************************************END OF FILE**********************/

bsp_i2c_ee.h

#ifndef __I2C_EE_H
#define	__I2C_EE_H


#include "stm32f10x.h"


/* 
 * AT24C02 2kb = 2048bit = 2048/8 B = 256 B
 * 32 pages of 8 bytes each
 *
 * Device Address
 * 1 0 1 0 A2 A1 A0 R/W
 * 1 0 1 0 0  0  0  0 = 0XA0
 * 1 0 1 0 0  0  0  1 = 0XA1 
 */

/* AT24C01/02每页有8个字节 
 * AT24C04/08A/16A每页有16个字节 
 */
	

#define EEPROM_DEV_ADDR			0xA0		/* 24xx02的设备地址 */
#define EEPROM_PAGE_SIZE		  8			  /* 24xx02的页面大小 */
#define EEPROM_SIZE				  256			  /* 24xx02总容量 */


uint8_t ee_CheckOk(void);
uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize);
uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize);
void ee_Erase(void);
uint8_t ee_Test(void);


#endif /* __I2C_EE_H */

​​​​​​​​​​​​​​一个型号的芯片讲着还蛮累的,本来还想介绍另一个外设是一个温湿度传感器GXHTX3X-DIS,用的也是I2C通讯协议,一看这篇博客的却写的太长了,后面我争取加上~ 

摘自季羡林老先生的《清华园日记》 

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

I am Supreme

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值