一、STM32 的 I2C 特性及架构:

1、STM32 的 I2C 外设简介:

       STM32 的 I2C 外设可用作通讯的主机及从机,支持 100Kbit/s 和 400Kbit/s 的速率,支持 7 位、 10 位设备地址支持 DMA 数据传输,并具有数据校验功能。它的 I2C 外设还支持 SMBus2.0 协议, SMBus 协议与 I2C 类似,主要应用于笔记本电脑的电池管理中,本教程不展开,感兴趣的读者可参考《SMBus20》文档了解。

2、STM32 的 I2C 通讯引脚:

3、通讯过程:

       使用 I2C 外设通讯时,在通讯的不同阶段它会对“状态寄存器(SR1 及 SR2)”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。

① 主发送器
作为 I2C 通讯的主机端时,向外发送数据时的过程。

② 主接收器:
作为 I2C 通讯的主机端时,从外部接收数据的过程,见下图:

二、I2C 初始化结构体详解:

       STM32 标准库提供了 I2C 初始化结构体初始化函数来配置 I2C 外设。初始化结构体及函数定义在库文件“ stm32f4xx_i2c.h”及“ stm32f4xx_i2c.c”中。

typedef struct {
uint32_t I2C_ClockSpeed; /*!< 设置 SCL 时钟频率,此值要低于 40 0000*/
uint16_t I2C_Mode; /*!< 指定工作模式,可选 I2C 模式及 SMBUS 模式 */
uint16_t I2C_DutyCycle; /*指定时钟占空比,可选 low/high = 2:1 及 16:9 模式*/
uint16_t I2C_OwnAddress1; /*!< 指定自身的 I2C 设备地址 */
uint16_t I2C_Ack; /*!< 使能或关闭响应(一般都要使能) */
uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的长度,可为 7 位及 10 位 */
} I2C_InitTypeDef;

(1) I2C_ClockSpeed

       本成员设置的是 I2C 的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到 I2C 的时钟控制寄存器 CCR。而我们写入的这个参数值不得高于 400KHz。 
(2) I2C_Mode
       本成员是选择 I2C 的使用方式,有 I2C 模式(I2C_Mode_I2C )和 SMBus 主、从模式(I2C_Mode_SMBusHost、I2C_Mode_SMBusDevice ) 。 I2C 不需要在此处区分主从模式,直接设置 I2C_Mode_I2C 即可。
(3) I2C_DutyCycle
       本成员设置的是 I2C 的 SCL 线时钟的占空比。 该配置有两个选择,分别为低电平时间比高电平时间为 2: 1 ( I2C_DutyCycle_2)和 16: 9 (I2C_DutyCycle_16_9)。其实这两个模式的比例差别并不大,一般要求都不会如此严格,这里随便选就可以了。
(4) I2C_OwnAddress1
       本成员配置的是 STM32 的 I2C 设备自己的地址。 地址可设置为 7 位或 10 位(受下面I2C_AcknowledgeAddress 成员决定),只要该地址是 I2C 总线上唯一的即可。

       STM32 的 I2C 外设可同时使用两个地址,即同时对两个地址作出响应,这个结构成员I2C_OwnAddress1 配置的是默认的、 OAR1 寄存器存储的地址,若需要设置第二个地址寄存器 OAR2,可使用 I2C_OwnAddress2Config 函数来配置, OAR2 不支持 10 位地址。

(5) I2C_Ack_Enable
       本成员是关于 I2C 应答设置,设置为使能则可以发送响应信号。 该成员值一般配置为允许应答(I2C_Ack_Enable),这是绝大多数遵循 I2C 标准的设备的通讯要求,改为禁止应答(I2C_Ack_Disable)往往会导致通讯错误。
(6) I2C_AcknowledgeAddress
       本成员选择 I2C 的寻址模式是 7 位还是 10 位地址。这需要根据实际连接到 I2C 总线上设备的地址进行选择,这个成员的配置也影响到 I2C_OwnAddress1 成员,只有这里设置成10 位模式时, I2C_OwnAddress1 才支持 10 位地址。
 

三、硬件I2C—读写 EEPROM 实验

       EEPROM 是一种掉电后数据不丢失的存储器,常用来存储一些配置信息,以便系统重新上电的时候加载之。 EEPOM 芯片最常用的通讯方式就是 I2C 协议。实验中 STM32 的 I2C 外设采用主模式,分别用作主发送器和主接收器, 通过查询事件的方式来确保正常通讯。

1、硬件设计

       本实验板中的 EEPROM 芯片(型号: AT24C02)的 SCL 及 SDA 引脚连接到了 STM32 对应的 I2C 引脚中,结合上拉电阻,构成了 I2C 通讯总线,它们通过 I2C 总线交互。EEPROM 芯片的设备地址一共有 7 位,其中高 4 位固定为: 1010 b,低 3 位则由 A0/A1/A2信号线的电平决定,见下图,图中的 R/W 是读写方向位,与地址无关。

       按照我们此处的连接, A0/A1/A2 均为 0,所以 EEPROM 的 7 位设备地址是: 1010000b ,即 0x50。由于 I2C 通讯时常常是地址跟读写方向连在一起构成一个 8 位数,且当R/W 位为 0 时,表示写方向,所以加上 7 位地址,其值为“ 0xA0”,常称该值为 I2C 设备的“写地址”;当 R/W 位为 1 时,表示读方向,加上 7 位地址,其值为“0xA1”,常称该值为“读地址”。
       EEPROM 芯片中还有一个 WP 引脚,具有写保护功能,当该引脚电平为高时,禁止写入数据,当引脚为低电平时,可写入数据,我们直接接地,不使用写保护功能。

2、软件设计

(1)编程要点

  1. 配置通讯使用的目标引脚为开漏模式;
  2. 使能 I2C 外设的时钟;
  3. 配置 I2C 外设的模式、地址、速率等参数并使能 I2C 外设;
  4. 编写基本 I2C 按字节收发的函数;
  5. 编写读写 EEPROM 存储内容的函数;
  6. 编写测试程序,对读写数据进行校验。

(2)代码分析
       ① I2C 硬件相关宏定义

/* STM32 I2C 速率 */
#define I2C_Speed 400000
/* STM32 自身的 I2C 地址, 这个地址只要与 STM32 外挂的 I2C 器件地址不一样即可 */
#define I2C_OWN_ADDRESS7 0X0A
/*I2C 接口*/
#define EEPROM_I2C I2C1
#define EEPROM_I2C_CLK RCC_APB1Periph_I2C1
#define EEPROM_I2C_SCL_PIN GPIO_Pin_6
#define EEPROM_I2C_SCL_GPIO_PORT GPIOB
#define EEPROM_I2C_SCL_GPIO_CLK RCC_AHB1Periph_GPIOB
#define EEPROM_I2C_SCL_SOURCE GPIO_PinSource6
#define EEPROM_I2C_SCL_AF GPIO_AF_I2C1
#define EEPROM_I2C_SDA_PIN GPIO_Pin_7
#define EEPROM_I2C_SDA_GPIO_PORT GPIOB
#define EEPROM_I2C_SDA_GPIO_CLK RCC_AHB1Periph_GPIOB
#define EEPROM_I2C_SDA_SOURCE GPIO_PinSource7
#define EEPROM_I2C_SDA_AF GPIO_AF_I2C1

      以上代码根据硬件连接, 把与 EEPROM 通讯使用的 I2C 号 、引脚号、引脚源以及复用功能映射都以宏封装起来,并且定义了自身的 I2C 地址及通讯速率,以便配置模式的时候使用。

       ② 初始化 I2C 的 GPIO

/**
* @brief I2C1 I/O 配置
* @param 无
* @retval 无
*/
static void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*使能 I2C 外设时钟 */
RCC_APB1PeriphClockCmd(EEPROM_I2C_CLK, ENABLE);
/*使能 I2C 引脚的 GPIO 时钟*/
RCC_AHB1PeriphClockCmd(EEPROM_I2C_SCL_GPIO_CLK |
EEPROM_I2C_SDA_GPIO_CLK, ENABLE);
/* 连接引脚源 PXx 到 I2C_SCL*/
GPIO_PinAFConfig(EEPROM_I2C_SCL_GPIO_PORT, EEPROM_I2C_SCL_SOURCE,
EEPROM_I2C_SCL_AF);
/* 连接引脚源 PXx 到 to I2C_SDA*/
GPIO_PinAFConfig(EEPROM_I2C_SDA_GPIO_PORT, EEPROM_I2C_SDA_SOURCE,
EEPROM_I2C_SDA_AF);
/*配置 SCL 引脚 */
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(EEPROM_I2C_SCL_GPIO_PORT, &GPIO_InitStructure);
/*配置 SDA 引脚 */
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;
GPIO_Init(EEPROM_I2C_SDA_GPIO_PORT, &GPIO_InitStructure);
}

       ③ 配置 I2C 的模式

/**
* @brief I2C 工作模式配置
* @param 无
* @retval 无
*/
static void I2C_Mode_Config(void)
{
I2C_InitTypeDef I2C_InitStructure;
/* I2C 配置 */
/*I2C 模式*/
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
/*占空比*/
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
/*I2C 自身地址*/
I2C_InitStructure.I2C_OwnAddress1 =I2C_OWN_ADDRESS7;
/*使能响应*/
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
/* I2C 的寻址模式 */
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
/* 通信速率 */
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
/*写入配置*/
I2C_Init(EEPROM_I2C, &I2C_InitStructure);
/* 使能 I2C */
I2C_Cmd(EEPROM_I2C, ENABLE);
}
/**
* @brief I2C 外设初始化
* @param 无
* @retval 无
*/
void I2C_EE_Init(void)
{
I2C_GPIO_Config();
I2C_Mode_Config();
}

       ④ 向 EEPROM 写入一个字节的数据,见代码清单。

/***************************************************************/
/*通讯等待超时时间*/
#define I2CT_FLAG_TIMEOUT ((uint32_t)0x1000)
#define I2CT_LONG_TIMEOUT ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
/**
* @brief I2C 等待事件超时的情况下会调用这个函数来处理
* @param errorCode:错误代码,可以用来定位是哪个环节出错.
* @retval 返回 0,表示 IIC 读取失败.
*/
static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* 使用串口 printf 输出错误信息,方便调试 */
EEPROM_ERROR("I2C 等待超时!errorCode = %d",errorCode);
return 0;
}
/*** @brief 写一个字节到 I2C EEPROM 中
* @param pBuffer:缓冲区指针
* @param WriteAddr:写地址
* @retval 正常返回 1,异常返回 0
*/
uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr)
{
/* 产生 I2C 起始信号 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
/*设置超时等待时间*/
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV5 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
}
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS,
I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV6 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C,
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
}
/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */
I2C_SendData(EEPROM_I2C, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C,
I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
}
/* 发送一字节要写入的数据 */
I2C_SendData(EEPROM_I2C, *pBuffer);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C,
I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
}
/* 发送停止信号 */
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
return 1;
}

      ⑤ 多字节写入及状态等待

/**
* @brief 将缓冲区中的数据写到 I2C EEPROM 中,采用单字节写入的方式,
速度比页写入慢
* @param pBuffer:缓冲区指针
* @param WriteAddr:写地址
* @param NumByteToWrite:写的字节数
* @retval 无
*/
uint8_t I2C_EE_ByetsWrite(uint8_t* pBuffer,uint8_t WriteAddr,
uint16_t NumByteToWrite)
{
uint16_t i;
uint8_t res;
/*每写一个字节调用一次 I2C_EE_ByteWrite 函数*/
for (i=0; i<NumByteToWrite; i++)
{
/*等待 EEPROM 准备完毕*/
I2C_EE_WaitEepromStandbyState();
/*按字节写入数据*/
res = I2C_EE_ByteWrite(pBuffer++,WriteAddr++);
}
return res;
}

        这段代码比较简单,直接使用 for 循环调用前面定义的 I2C_EE_ByteWrite 函数一个字节 一 个 字 节 地 向 EEPROM 发 送 要 写 入 的 数 据 。 在 每 次 数 据 写 入 通 讯 前 调 用 了I2C_EE_WaitEepromStandbyState 函数等待 EEPROM 内部擦写完毕。

//等待 Standby 状态的最大次数
#define MAX_TRIAL_NUMBER 300
/**
* @brief 等待 EEPROM 到准备状态
* @param 无
* @retval 正常返回 1,异常返回 0
*/
static uint8_t I2C_EE_WaitEepromStandbyState(void)
{
__IO uint16_t tmpSR1 = 0;
__IO uint32_t EETrials = 0;
/*总线忙时等待 */
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(20);
}
/* 等待从机应答,最多等待 300 次 */
while (1)
{
/*开始信号 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
/* 检测 EV5 事件并清除标志*/
I2CTimeout = I2CT_FLAG_TIMEOUT;
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(21);
}
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);
/* 等待 ADDR 标志 */
I2CTimeout = I2CT_LONG_TIMEOUT;
do
{
/* 获取 SR1 寄存器状态 */
tmpSR1 = EEPROM_I2C->SR1;
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(22);
}
/* 一直等待直到 addr 及 af 标志为 1 */
while ((tmpSR1 & (I2C_SR1_ADDR | I2C_SR1_AF)) == 0);
/*检查 addr 标志是否为 1 */
if (tmpSR1 & I2C_SR1_ADDR)
{
/* 清除 addr 标志该标志通过读 SR1 及 SR2 清除 */
(void)EEPROM_I2C->SR2;
/*产生停止信号 */
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
/* 退出函数 */
return 1;
}
else
{
/*清除 af 标志 */
I2C_ClearFlag(EEPROM_I2C, I2C_FLAG_AF);
}
/*检查等待次数*/
if (EETrials++ == MAX_TRIAL_NUMBER)
{
/* 等待 MAX_TRIAL_NUMBER 次都还没准备好,退出等待 */
return I2C_TIMEOUT_UserCallback(23);
}
}
}

       这个函数主要实现是向 EEPROM 发送它设备地址,检测 EEPROM 的响应,若EEPROM 接收到地址后返回应答信号,则表示 EEPROM 已经准备好,可以开始下一次通讯。函数中检测响应是通过读取 STM32 的 SR1寄存器的 ADDR 位及 AF 位来实现的,当I2C 设备响应了地址的时候, ADDR 会置 1,若应答失败, AF 位会置 1。

       ⑥ EEPROM 的页写入
        根据页写入时序,第一个数据被解释为要写入的内存地址 address1,后续可连续发送 n 个数据,这些数据会依次写入到内存中。其中 AT24C02 型号的芯片页写入时序最多可以一次发送 8 个数据(即 n = 8 ),该值也称为页大小,某些型号的芯片每个页写入时序最多可传输16 个数据。 

/**
* @brief 在 EEPROM 的一个写循环中可以写多个字节,但一次写入的字节数
* 不能超过 EEPROM 页的大小, AT24C02 每页有 8 个字节
* @param
* @param pBuffer:缓冲区指针
* @param WriteAddr:写地址
* @param NumByteToWrite:要写的字节数要求 NumByToWrite 小于页大小
* @retval 正常返回 1,异常返回 0
*/
uint8_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr,
uint8_t NumByteToWrite)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);
}
/* 产生 I2C 起始信号 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV5 事件并清除标志 */
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);
}
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV6 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C,
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);
}
/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */
I2C_SendData(EEPROM_I2C, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (! I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);
}
/* 循环发送 NumByteToWrite 个数据 */
while (NumByteToWrite--)
{
/* 发送缓冲区中的数据 */
I2C_SendData(EEPROM_I2C, *pBuffer);
/* 指向缓冲区中的下一个数据 */
pBuffer++;
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);
}
}
/* 发送停止信号 */
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
return 1;
}

     这段页写入函数主体跟单字节写入函数是一样的,只是它在发送数据的时候,使用 for循环控制发送多个数据,发送完多个数据后才产生 I2C 停止信号,只要每次传输的数据小于等于 EEPROM 时序规定的页大小,就能正常传输。

       ⑦ 快速写入多字节
       利用 EEPROM 的页写入方式,可以改进前面的“ 多字节写入”函数,加快传输速度。

/* AT24C01/02 每页有 8 个字节 */
#define I2C_PageSize 8
/**
* @brief 将缓冲区中的数据写到 I2C EEPROM 中,采用页写入的方式,加快写入速度
* @param pBuffer:缓冲区指针
* @param WriteAddr:写地址
* @param NumByteToWrite:写的字节数
* @retval 无
*/
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr,
u16 NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
/*mod 运算求余,若 writeAddr 是 I2C_PageSize 整数倍,运算结果 Addr 值为 0*/
Addr = WriteAddr % I2C_PageSize;
/*差 count 个数据,刚好可以对齐到页地址*/
count = I2C_PageSize - Addr;
/*计算出要写多少整数页*/
NumOfPage = NumByteToWrite / I2C_PageSize;
/*mod 运算求余,计算出剩余不满一页的字节数*/
NumOfSingle = NumByteToWrite % I2C_PageSize;
/* Addr=0,则 WriteAddr 刚好按页对齐 aligned */
if (Addr == 0)
{
/* 如果 NumByteToWrite < I2C_PageSize */
if (NumOfPage == 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
/* 如果 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();
}
}
}
/* 如果 WriteAddr 不是按 I2C_PageSize 对齐 */
else
{
/* 如果 NumByteToWrite < I2C_PageSize */
if (NumOfPage== 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
/* 如果 NumByteToWrite > I2C_PageSize */
else
{
/*地址不对齐多出的 count 分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;
/*先把 WriteAddr 所在页的剩余字节写了*/
if (count != 0)
{
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
I2C_EE_WaitEepromStandbyState();
/*WriteAddr 加上 count 后,地址就对齐到页了*/
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();
}
}
}
}

       很多读者觉得这段代码的运算很复杂,看不懂,其实它的主旨就是对输入的数据进行分页(本型号芯片每页 8 个字节),见表 23-2。通过“整除”计算要写入的数据NumByteToWrite 能写满多少“完整的页”,计算得的值存储在 NumOfPage 中,但有时数据不是刚好能写满完整页的,会多一点出来,通过“求余”计算得出“不满一页的数据个数”就存储在 NumOfSingle 中。计算后通过按页传输 NumOfPage 次整页数据及最后的NumOfSing 个数据,使用页传输,比之前的单个字节数据传输要快很多。

        除了基本的分页传输,还要考虑首地址的问题,见下下表。若首地址不是刚好对齐到页的首地址,会需要一个 count 值,用于存储从该首地址开始写满该地址所在的页,还能写多少个数据。实际传输时,先把这部分 count 个数据先写入,填满该页,然后把剩余的数据(NumByteToWrite-count),再重复上述求出 NumOPage 及 NumOfSingle 的过程,按页传输到 EEPROM。

1. 若 writeAddress=16,计算得 Addr=16%8= 0 , count=8-0= 8;
2. 同时,若 NumOfPage=22,计算得 NumOfPage=22/8= 2, NumOfSingle=22%8= 6。
3. 数据传输情况如表 23-2

4. 若 writeAddress=17,计算得 Addr=17%8= 1, count=8-1= 7;
5. 同时,若 NumOfPage=22,
6. 先把 count 去掉,特殊处理,计算得新的 NumOfPage=22-7= 15
7. 计算得 NumOfPage=15/8= 1, NumOfSingle=15%8= 7。
8. 数据传输情况如下表

       最后,强调一下, EEPROM 支持的页写入只是一种加速的 I2C 的传输时序,实际上并不要求每次都以页为单位进行读写, EEPROM 是支持随机访问的(直接读写任意一个地址), EEPROM 数据写入和擦除的最小单位是“字节”而不是“页”,数据写入前不需要擦除整页。
       ⑧ 从 EEPROM 读取数据
       从 EEPROM 读取数据是一个复合的 I2C 时序,它实际上包含一个写过程和一个读过程,见下图。

/**
* @brief 从 EEPROM 里面读取一块数据
* @param pBuffer:存放从 EEPROM 读取的数据的缓冲区指针
* @param ReadAddr:接收数据的 EEPROM 的地址
* @param NumByteToRead:要从 EEPROM 读取的字节数
* @retval 正常返回 1,异常返回 0
*/
uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr,
u16 NumByteToRead)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
}
/* 产生 I2C 起始信号 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV5 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
}
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV6 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C,
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);
}
/*通过重新设置 PE 位清除 EV6 事件 */
I2C_Cmd(EEPROM_I2C, ENABLE);
/* 发送要读取的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */
I2C_SendData(EEPROM_I2C, ReadAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);
}
/* 产生第二次 I2C 起始信号 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV5 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);
}
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS, I2C_Direction_Receiver);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV6 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2C,
I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);
}
/* 读取 NumByteToRead 个数据*/
while (NumByteToRead)
{
/*若 NumByteToRead=1,表示已经接收到最后一个数据了,
发送非应答信号,结束传输*/
if (NumByteToRead == 1)
{
/* 发送非应答信号 */
I2C_AcknowledgeConfig(EEPROM_I2C, DISABLE);
/* 发送停止信号 */
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
}
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
}
{
/*通过 I2C,从设备中读取一个字节的数据 */
*pBuffer = I2C_ReceiveData(EEPROM_I2C);
/* 存储数据的指针指向下一个地址 */
pBuffer++;
/* 接收数据自减 */
NumByteToRead--;
}
}
/* 使能应答,方便下一次 I2C 传输 */
I2C_AcknowledgeConfig(EEPROM_I2C, ENABLE);
return 1;
}

(3) main 文件:EEPROM 读写测试函数

/**
* @brief I2C(AT24C02)读写测试
* @param 无
* @retval 正常返回 1 ,不正常返回 0
*/
uint8_t I2C_Test(void)
{
u16 i;
EEPROM_INFO("写入的数据");
for ( i=0; i<=255; i++ ) //填充缓冲
{
I2c_Buf_Write[i] = i;
printf("0x%02X ", I2c_Buf_Write[i]);
if (i%16 == 15)
printf("\n\r");
}
//将 I2c_Buf_Write 中顺序递增的数据写入 EERPOM 中
//页写入方式
// I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, 256);
//字节写入方式
I2C_EE_ByetsWrite( I2c_Buf_Write, EEP_Firstpage, 256);
EEPROM_INFO("写结束");
EEPROM_INFO("读出的数据");
//将 EEPROM 读出数据顺序保持到 I2c_Buf_Read 中
I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256);
//将 I2c_Buf_Read 中的数据通过串口打印
for (i=0; i<256; i++)
{
if (I2c_Buf_Read[i] != I2c_Buf_Write[i])
{
printf("0x%02X ", I2c_Buf_Read[i]);
EEPROM_ERROR("错误:I2C EEPROM 写入与读出的数据不一致");
return 0;
}
printf("0x%02X ", I2c_Buf_Read[i]);
if (i%16 == 15)
printf("\n\r");
}
EEPROM_INFO("I2C(AT24C02)读写测试成功");
return 1;
}

main 函数

/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
LED_GPIO_Config();
LED_BLUE;
/*初始化 USART1*/
Debug_USART_Config();
printf("\r\n 这是一个 I2C 外设(AT24C02)读写测试例程 \r\n");
/* I2C 外设(AT24C02)初始化 */
I2C_EE_Init();
if (I2C_Test() ==1)
{
LED_GREEN;
}
else
{
LED_RED;
}
while (1)
{
}
}