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 EEPROM_ADDRESS   0x50

/**
 * @brief EEPROM写一个字节函数
 * 
 * @param address 待写入数据的内存地址
 * @param data 待写入的数据
 */
void EEPROM_WriteOneByte(uint16_t address, uint8_t data)
{
    I2C_Simulate_Start();                                                       // 1、产生起始信号
    I2C_Simulate_SendOneByte(EEPROM_ADDRESS << 1);                              // 2、发送写操作地址
    I2C_Simulate_WaitAck();                                                     // 3、等待应答信号
    I2C_Simulate_SendOneByte(address);                                          // 4、发送内存地址
    I2C_Simulate_WaitAck();                                                     // 5、等待应答信号
    I2C_Simulate_SendOneByte(data);                                             // 6、发送数据
    I2C_Simulate_WaitAck();                                                     // 7、等待应答信号
    I2C_Simulate_Stop();                                                        // 8、产生停止信号

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

2.2、页写模式

AT24C02页写模式时序图

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

/**
 * @brief EEPROM在一页中写入多个字节
 * 
 * @param address 待写入数据的内存地址
 * @param pBuffer 待写入的数据
 * @param length 待写入数据的个数
 * 
 * @note EEPROM的页写模式存在页的限制,超过一页(8字节)时,超出数据覆盖原先写入数据
 */
static void EEPROM_WriteOnePage(uint16_t address, uint8_t *pBuffer, uint8_t length)
{
    I2C_Simulate_Start();                                                       // 1、产生起始信号
    I2C_Simulate_SendOneByte(EEPROM_ADDRESS << 1);                              // 2、发送写操作地址
    I2C_Simulate_WaitAck();                                                     // 3、等待应答信号
    I2C_Simulate_SendOneByte(address);                                          // 4、发送内存地址
    I2C_Simulate_WaitAck();                                                     // 5、等待应答信号
    for (uint8_t i = 0; i < length; i++)
    {   
        I2C_Simulate_SendOneByte(pBuffer[i]);                                   // 6、发送数据
        I2C_Simulate_WaitAck();                                                 // 7、等待应答信号
    }
    I2C_Simulate_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 EEPROM读一个字节函数
 * 
 * @param address 待读取数据的内存地址
 * @return uint8_t 读取的数据
 */
uint8_t EEPROM_ReadOneByte(uint16_t address)
{
    uint8_t receive = 0;

    I2C_Simulate_Start();                                                       // 1、产生起始信号
    I2C_Simulate_SendOneByte(EEPROM_ADDRESS << 1);                              // 2、发送写操作地址
    I2C_Simulate_WaitAck();                                                     // 3、等待应答信号
    I2C_Simulate_SendOneByte(address);                                          // 4、发送内存地址
    I2C_Simulate_WaitAck();                                                     // 5、等待应答信号
    I2C_Simulate_Start();                                                       // 6、再次产生起始信号
    I2C_Simulate_SendOneByte((EEPROM_ADDRESS << 1) | 1);                                             // 7、发送读操作地址
    I2C_Simulate_WaitAck();                                                     // 8、等待应答信号
    receive = I2C_Simulate_ReadOneByte(1);                                      // 9、读取数据,并发送非应答信号
    I2C_Simulate_Stop();                                                        // 10、产生停止信号

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

3.3、顺序读模式

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

AT24C02顺序读模式

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

四、原理图

AT24C02原理图

I2C1接口引脚

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

五、程序源码

/**  根据不同的 24CXX 型号, 发送高位地址
 * 1、24C16以上的型号,分 2 个字节发送地址
 * 2、24C16及以下的型号,,分 1 个低字节地址 + 占用器件地址的 bit1~bit3 位用于表示高位地址,最多 11 位地址
 *    对于 24C01/02,其器件地址格式(8bit)为: 1  0  1  0  A2  A1  A0  R/W
 *    对于 24C04,   其器件地址格式(8bit)为: 1  0  1  0  A2  A1  a8  R/W
 *    对于 24C08,   其器件地址格式(8bit)为: 1  0  1  0  A2  a9  a8  R/W
 *    对于 24C16,   其器件地址格式(8bit)为: 1  0  1  0  a10 a9  a8  R/W
 *    R/W      : 读/写控制位 0: 表示写; 1: 表示读;
 *    A0/A1/A2 : 对应器件的 1、2、3引脚(只有 24C01/02/04/8 有这些脚)
 *    a8/a9/a10: 对应存储整列的高位地址,11bit 地址最多可以表示 2048 个位置, 可以寻址 24C16 及以内的型号
 */  
AT24Cxx 容量(bit) 页数 页内字节数 数据地址(占用bit数) 设备地址 寄存器高地址 寄存器低地址
AT24C01 1 K bit(128 B) 16 8 Byte 7 bit 1 0 1 0 A2 A1 A0 R/W a6 a5 a4 a3 a2 a1 a0
AT24C02 2 K bit(256 B) 32 8 Byte 8 bit 1 0 1 0 A2 A1 A0 R/W a7 a6 a5 a4 a3 a2 a1 a0
AT24C04 4 K bit(512 B) 32 16 Byte 9 bit 1 0 1 0 A2 A1 a8 R/W a7 a6 a5 a4 a3 a2 a1 a0
AT24C08 8 K bit(1024B) 64 16 Byte 10 bit 1 0 1 0 A2 a9 a8 R/W a7 a6 a5 a4 a3 a2 a1 a0
AT24C16 16 K bit(2048 B) 128 16 Byte 11 bit 1 0 1 0 a10 a9 a8 R/W a7 a6 a5 a4 a3 a2 a1 a0
AT24C32 32 K bit(4096 B) 128 32 Byte 12 bit 1 0 1 0 A2 A1 A0 R/W a11 a10 a9 a8 a7 a6 a5 a4 a3 a2 a1 a0
AT24C64 64 K bit(8192 B) 256 32 Byte 13 bit 1 0 1 0 A2 A1 A0 R/W a12 a11 a10 a9 a8 a7 a6 a5 a4 a3 a2 a1 a0
AT24C128 128 K bit(16384 B) 256 64 Byte 14 bit 1 0 1 0 A2 A1 A0 R/W a13 a12 a11 a10 a9 a8 a7 a6 a5 a4 a3 a2 a1 a0
AT24C256 256 K bit(32768 B) 512 64 Byte 15 bit 1 0 1 0 A2 A1 A0 R/W a14 a13 a12 a11 a10 a9 a8 a7 a6 a5 a4 a3 a2 a1 a0
AT24C512 512 K bit(65535 B) 512 128 Byte 16 bit 1 0 1 0 A2 A1 A0 R/W a15 a14 a13 a12 a11 a10 a9 a8 a7 a6 a5 a4 a3 a2 a1 a0

  EEPROM 写数据函数:

/**
 * @brief EEPROM写数据函数
 * 
 * @param address 待写入数据的内存地址
 * @param pBuffer 待写入的数据
 * @param length 待写入数据的个数
 */
void EEPROM_WriteBytes(uint16_t address, uint8_t *pBuffer, uint16_t length)
{
    uint16_t offset = address % 8;                                              // 在第一页中的偏移地址
    uint16_t remain = 8 - offset;                                               // 一页剩余空间大小
    uint16_t pageNum = (length - remain) / 8;                                   // 去除第一页后还需要写入的整页数
    uint8_t lastPageByteNum = (length - remain) % 8;                            // 最后一页需要写入的字节数

    // 当写入的数据小一页剩余空间大小,将写入的字节数赋值给页中剩余空间大小
    remain = (length <= remain ? length : remain);

    EEPROM_WriteOnePage(address, pBuffer, remain);                              // 写入第一页数据
    address += remain;
    pBuffer += remain;
  
    while (pageNum--)
    {
        EEPROM_WriteOnePage(address, pBuffer, 8);                               // 写入整页数据
        address += 8;
        pBuffer += 8;
    }
  
    if (lastPageByteNum)
    {
        EEPROM_WriteOnePage(address, pBuffer, lastPageByteNum);                // 写入最后一页数据
    }
}

  main() 函数内容如下:

int main(void)
{
    uint8_t data[] = "hello world!";
    uint8_t length = sizeof(data) / sizeof(data[0]);
    uint8_t address = 5;

    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);

    UART_Init(&g_usart1_handle, USART1, 115200);
    I2C_Simulate_Init();

    EEPROM_WriteBytes(address, data, length);
    EEPROM_ReadBytes(address, data, length);
    printf("%s\r\n", data);

    while (1)
    {

    }
  
    return 0;
}
posted @ 2023-11-16 19:50  星光樱梦  阅读(143)  评论(0编辑  收藏  举报