14. I2C读取EEPROM 的副本

一、AT24C02简介

  AT24C02 是一个 2K bit 的串行 EEPROM 存储器,内部含有 256 个字节。在 24C02 里面还有一个 8 字节的页写缓冲器。该设备的通信方式 I2C,通过其 SCL 和 SDA 与其他设备通信,芯片的引脚图如下图所示。

AT24C02引脚图

  上图中有一个 WP,这个是写保护引脚,接高电平只读,接地允许读和写。每一个设备都有自己的设备地址,AT24C02 也不例外,但是 AT24C02 的设备地址是包括不可编程部分和可编程部分,可编程部分是根据上图的硬件引脚 A0、A1 和 A2 所决定。设备地址最后一位用于设置数据的传输方向,即读操作/写操作,0 是写操作,1 是读操作,具体格式如下图 所示:

AT24C02设备地址格式图

二、AT24C02写时序

  AT24C02 支持 字节写模式页写模式字节写模式 就是一个地址一个数据写入。页写模式 就是连续写入数据。只需要写一个地址,连续写入数据时地址会自增,但存在页的限制,超过一页时,超出数据覆盖原先写入数据。

2.1、字节写模式

AT24C02字节写模式时序图

  主机在 I2C 总线发送第 1 个字节的数据为 AT24C02的设备地址 0xA0,用于寻找总线上找到 AT24C02,在获得 AT24C02 的应答信号之后,继续发送第 2 个字节数据,该字节数据是 AT24C02 的内存地址,再等到 AT24C02 的应答信号,主机继续发送第 3 字节数据,这里的数据即是写入在第 2 字节内存地址的数据。主机完成写操作后,可以发出停止信号,终止数据传输。

#define AT24C02_ADDRESS   0xA0

/**
 * @brief AT24C02写一个字节函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 待写入的数据
 */
void AT24C02_WriteOneByte(uint8_t address, uint8_t data)
{
    I2C_Start();                                                                // 1、产生起始信号
    I2C_SendOneByte(AT24C02_ADDRESS);                                           // 2、发送写操作地址
    I2C_WaitAck();                                                              // 3、等待应答信号
    I2C_SendOneByte(address);                                                   // 4、发送内存地址
    I2C_WaitAck();                                                              // 5、等待应答信号
    I2C_SendOneByte(data);                                                      // 6、发送数据
    I2C_WaitAck();                                                              // 7、等待应答信号
    I2C_Stop();                                                                 // 8、产生停止信号

    Delay_ms(10);                                                               // 9、等待AT24C02写入完成
}

2.2、页写模式

AT24C02页写模式时序图

  在单字节写时序时,每次写入数据时都需要先写入设备的内存地址才能实现,在页写时序中,只需要告诉 AT24C02 第一个内存地址 1,后面数据会按照顺序写入到内存地址 2,内存地址 3等,大大节省了通信时间,提高了时效性。因为 AT24C02 每次只能 8bit 数据,所以它的页大小也就是 1 字节。页写时序的操作方式跟上面的单字节写时序差不多。

/**
 * @brief AT24C02在一页中写入多个字节
 * 
 * @param address 待写入数据的内存地址
 * @param pBuffer 待写入的数据
 * @param length 待写入数据的个数
 * 
 * @note AT24C02的页写模式存在页的限制,超过一页(8字节)时,超出数据覆盖原先写入数据
 */
void AT24C02_WriteOnePage(uint8_t address, uint8_t *pBuffer, uint8_t length)
{
    I2C_Start();                                                                // 1、产生起始信号
    I2C_SendOneByte(AT24C02_ADDRESS);                                           // 2、发送写操作地址
    I2C_WaitAck();                                                              // 3、等待应答信号
    I2C_SendOneByte(address);                                                   // 4、发送内存地址
    I2C_WaitAck();                                                              // 5、等待应答信号
    for (uint8_t i = 0; i < length; i++)
    {   
        I2C_SendOneByte(pBuffer[i]);                                            // 6、发送数据
        I2C_WaitAck();                                                          // 7、等待应答信号
    }
    I2C_Stop();                                                                 // 8、产生停止信号

    Delay_ms(10);                                                               // 9、等待AT24C02写入完成
}

三、AT24C02读时序

  AT24C02 支持 当前地址读模式随机地址读模式顺序读模式。AT24C02 的读操作会自动翻页。当前地址读模式 是基于上一次读/写操作的最后位置继续读出数据。随机地址读模式 是指定地址读出数据。顺序读模式 是连续读出数据。

3.1、当前地址读模式

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

AT24C02当前地址读模式

3.2、随机地址读模式

AT24C02随机地址读模式

  AT24C02 读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产生后,主机发送 AT24C02 设备地址 0xA0,获取从机应答信号后,接着发送需要读取的内存地址;在读时序中,起始信号产生后,主机发送 AT24C02 设备地址 0xA1,获取从机应答信号后,接着从机返回刚刚在写时序中内存地址的数据,以字节为单位传输在总线上,假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,当主机发出的是非应答信号并以停止信号发出为结束,从机就会结束传输。

/**
 * @brief AT24C02读一个字节函数
 * 
 * @param address 待读取数据的内存地址
 * @return uint8_t 读取的数据
 */
uint8_t AT24C02_ReadOneByte(uint8_t address)
{
    uint8_t receive = 0;

    I2C_Start();                                                                // 1、产生起始信号
    I2C_SendOneByte(AT24C02_ADDRESS);                                           // 2、发送写操作地址
    I2C_WaitAck();                                                              // 3、等待应答信号
    I2C_SendOneByte(address);                                                   // 4、发送内存地址
    I2C_WaitAck();                                                              // 5、等待应答信号
    I2C_Start();                                                                // 6、再次产生起始信号
    I2C_SendOneByte(AT24C02_ADDRESS | 1);                                       // 7、发送读操作地址
    I2C_WaitAck();                                                              // 8、等待应答信号
    receive = I2C_ReadOneByte(1);                                               // 9、读取数据,并发送非应答信号
    I2C_Stop();                                                                 // 10、产生停止信号

    return receive;                                                             // 11、返回读取的数据
}

3.3、顺序读模式

  顺序读可以通过 “当前地址读” 或 “随机读” 启动。主器件接收到一个数据后,应答 ACK。只要 EEPROM 接收到 ACK,将自动增加字地址并继续随时钟发送后面的数据。若达到存储器地址末尾,地址自动回转到 0,仍可继续顺序读取数据。主器件发送非应答信号,即可结束顺序读操。

AT24C02顺序读模式

/**
 * @brief AT24C02顺序读取多个字节数据函数
 * 
 * @param address 起始地址
 * @param pBuffer 保存读取的数据的缓冲区
 * @param length 读取字节的个数
 */
void AT24C02_ReadBytes(uint8_t address, uint8_t *pBuffer, uint16_t length)
{
    I2C_Start();                                                                // 1、产生起始信号
    I2C_SendOneByte(AT24C02_ADDRESS);                                           // 2、发送写操作地址
    I2C_WaitAck();                                                              // 3、等待应答信号
    I2C_SendOneByte(address);                                                   // 4、发送内存地址
    I2C_WaitAck();                                                              // 5、等待应答信号
    I2C_Start();                                                                // 6、再次产生起始信号
    I2C_SendOneByte(AT24C02_ADDRESS | 1);                                       // 7、发送读操作地址
    I2C_WaitAck();                                                              // 8、等待应答信号
    for (uint8_t i = 0; i < length; i++)
    {
        if (i < length - 1)
        {
            pBuffer[i] = I2C_ReadOneByte(0);                                    // 9、读取数据,并发送应答信号
        }
        else
        {
            pBuffer[i] = I2C_ReadOneByte(1);                                    // 10、读取最后一个数据,并发送非应答信号
        }
    }
  
    I2C_Stop();                                                                 // 11、产生停止信号
}

四、源码实现

4.1、原理图

AT24C02原理图

AT24C02接线图

  从原理图上,我们可以看出 AT24C02 的 A0、A1 和 A2 引脚都接地,因此 AT24C02 的设备地址为 1010000

4.2、程序源码

  I2C1 初始化函数内容如下:

I2C_HandleTypeDef g_i2c1_handle;

/**
 * @brief I2C1初始化
 * 
 * @param speed SCL的时钟频率,此值要低于400000
 */
void I2C1_Init(uint32_t speed)
{
    g_i2c1_handle.Instance = I2C1;                                                  // 使用的I2C
    g_i2c1_handle.Init.ClockSpeed = speed;                                      // SCL时钟频率
    g_i2c1_handle.Init.DutyCycle = I2C_DUTYCYCLE_2;                             // 时钟占空比
    g_i2c1_handle.Init.OwnAddress1 = 0;                                         // STM32自身设备地址1
    g_i2c1_handle.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;                // 地址模式
    g_i2c1_handle.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;               // 双地址模式
    g_i2c1_handle.Init.OwnAddress2 = 0;                                         // STM32自身设备地址2
    g_i2c1_handle.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;               // 通用广播地址
    g_i2c1_handle.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;                   // 禁止时钟延长模式
   
    HAL_I2C_Init(&g_i2c1_handle);
}

  I2C 底层初始化函数内容如下:

/**
 * @brief I2C底层初始化函数
 * 
 * @param hi2c I2C句柄
 */
void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    if(hi2c->Instance == I2C1)
    {
	__HAL_RCC_I2C1_CLK_ENABLE();                                            // 使能I2C1时钟
        __HAL_RCC_GPIOB_CLK_ENABLE();                                           // 使能I2C1对应的GPIO时钟

        GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;                          // SCL引脚和SDA引脚
        GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;                                 // 复用开漏输出
        GPIO_InitStruct.Pull = GPIO_PULLUP;                                     // 使用上拉电阻
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;                           // 输出速度
        GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;                              // 复用功能
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    }
}

  AT24C02 一次写入多个字节函数内容如下:

/**
 * @brief AT24C02在一页中写入多个字节
 * 
 * @param hi2c I2C句柄
 * @param address 待写入数据的内存地址
 * @param pData 待写入的数据
 * @param length 待写入数据的个数
 */
void AT24C02_WriteData(I2C_HandleTypeDef *hi2c, uint16_t address, uint8_t *pData, uint16_t length)
{
    HAL_I2C_Mem_Write(hi2c, AT24C02_ADDRESS, address, I2C_MEMADD_SIZE_8BIT, pData, length, 1000);
    HAL_Delay(5);
}

  AT24C02 一次读取多个字节函数内容如下:

/**
 * @brief AT24C02顺序读取多个字节数据函数
 * 
 * @param hi2c I2C句柄
 * @param address 起始地址
 * @param pData 保存读取的数据的缓冲区
 * @param length 读取字节的个数
 */
void AT24C02_ReadData(I2C_HandleTypeDef *hi2c, uint16_t address, uint8_t *pData, uint16_t length)
{
    HAL_I2C_Mem_Read(hi2c, AT24C02_ADDRESS, address, I2C_MEMADD_SIZE_8BIT, pData, length, 1000);
}

  有关时钟配置函数请在 STM32 的时钟系统 篇章查看。

  有关 USART1 的配置请在 串口通信 篇章查看。

  main() 函数内容如下:

int main(void)
{
    uint8_t data[10] = {0};

    HAL_Init();

    System_Clock_Init(8, 336, 2, 7);
    HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);                         // 设置中断优先级分组

    USART1_Init(115200);
    I2C1_Init(100000);

    AT24C02_WriteData(&g_i2c1_handle, 8, (uint8_t *)"123456789", 10);
    AT24C02_ReadData(&g_i2c1_handle, 8, data, 10);
    printf("%s\r\n", data);

    while (1)
    {

    }
  
    return 0;
}
posted @ 2024-03-15 21:40  星光樱梦  阅读(108)  评论(0编辑  收藏  举报