回头想了想在工作中调过的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结构体初始化
首先先介绍一下编程思想:
- 初始化IIC相关的GPIO
- 配置IIC外设的工作模式
- 编写IIC写入EEPROM的Byte函数
- 编写IIC读取EEPROM的RANDOM READ函数
- 使用read函数及write函数对读写进行校验
- 编写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。)
- 只要芯片有电,该地址就一直保存,当读到最后页的最后字节,地址会回转到0:
- 当写到某页尾的最后一个字节,地址会回转到该页的首字节。
- 接收器件地址(读/写选择位为"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)过程。以下是这个过程的简要解释:
-
内部定时写周期: 这指的是EEPROM用于写入数据的内部过程。一旦启动了这个过程,EEPROM就会忙于写周期,无法立即响应读取或写入请求。
-
禁用EEPROM输入: 在内部写周期期间,EEPROM的输入被禁用。这意味着外部信号或数据输入在写周期完成之前可能不会被接受。
-
确认轮询: 为了检查内部写周期是否已完成,执行确认轮询。这包括发送起始条件,然后是设备地址字。设备地址字中的读/写位表示所需的操作(读取或写入)。
-
读/写位: 设备地址字中的读/写位非常重要。如果内部写周期尚未完成,EEPROM将不会以确认方式响应。在这种情况下,读取或写入序列不应继续。
-
响应: 如果内部写周期已完成,EEPROM将以“0”作为确认响应。这个响应表示EEPROM已准备好进行后续的读取或写入操作。
-
继续序列: 一旦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通讯协议,一看这篇博客的却写的太长了,后面我争取加上~
摘自季羡林老先生的《清华园日记》