AT24C02 EEPROM I2C驱动移植与读写实战:基于立创开发板的存储模块应用
AT24C02 EEPROM I2C驱动移植与读写实战基于立创开发板的存储模块应用最近在做一个需要保存配置参数的小项目发现单片机内部的Flash写起来比较麻烦而且有寿命限制。这时候就想到了外挂一颗EEPROM芯片AT24C02就是非常经典的选择。它通过I2C接口通信容量是2Kb256字节对于保存一些校准数据、设备参数或者运行记录来说完全够用。今天咱们就手把手地把AT24C02模块接到立创开发板R7FA6E2BB3CNE上从硬件连接到软件驱动再到读写测试走一遍完整的移植流程。就算你之前没怎么接触过I2C或者EEPROM跟着做下来也能完全掌握。1. 认识一下AT24C02它是什么能干什么在动手接线写代码之前咱们先花几分钟了解一下要用的芯片。EEPROM的全称是“带电可擦可编程只读存储器”。这个名字听起来有点绕其实理解起来很简单只读存储器ROM数据存进去一般不改比如电脑的BIOS。可擦可编程EPROM能擦掉重写但通常需要紫外线照射或者高压很麻烦。带电可擦EEPROM最大的优点就是可以在电路板上直接用电路信号来擦除和重新写入数据而且掉电后数据不会丢失。AT24C02就是一颗EEPROM芯片它的核心特点有容量2K位bit也就是256字节Byte。别看字节数不多存几十个配置项绰绰有余。接口I2C总线。只需要两根线SCL时钟线和SDA数据线就能通信非常节省单片机引脚。页写缓冲内部有一个16字节的页写缓冲区。这意味着你可以一次连续写入最多16个字节效率比单个字节写入高很多。写保护有一个专门的写保护引脚WP接高电平时芯片就“锁住”不能写了防止数据被意外修改。注意I2C总线是一个“多主多从”的通信协议。你可以把它想象成几个人打电话SCL是大家统一遵循的语速节奏SDA是说话的内容。主机我们的单片机发起通话起始信号然后呼叫某个从机AT24C02的“电话号码”设备地址双方确认后开始传输数据。1.1 关键参数与设备寻址根据芯片资料有几个关键参数需要记住参数值说明工作电压1.8V - 5.5V兼容性很广我们的开发板提供3.3V直接接上就行通信接口I2C两线制串行总线容量2048位 (256字节)地址范围是 0x00 ~ 0xFF页大小16字节一次连续写入的最大长度时钟速度5V时最大1MHz其他电压400kHz我们用3.3V所以通信速度别超过400kHz怎么找到芯片——设备地址I2C总线上可以挂很多设备每个设备都有一个唯一的7位地址。AT24C02的地址高4位固定是1010这是厂家预设的。低3位A2, A1, A0则由芯片上对应的三个引脚电平决定接GND是0接VCC是1。对于我们常用的模块这三个引脚通常都接地0所以7位设备地址就是1010000二进制即0x50十六进制。但是注意I2C通信时主机发送的第一个字节是8位的7位地址 1位读写方向位0写1读。因此写操作时发送的地址字节是0x50 1 | 0 0xA0读操作时发送的地址字节是0x50 1 | 1 0xA1这个0xA0和0xA1就是我们等会儿写代码时要用的从机地址。2. 硬件连接与图形化配置理论清楚了咱们开始动手。第一步是把模块和开发板连起来。2.1 引脚连接AT24C02模块一般有4个引脚VCC, GND, SCL, SDA。连接非常简单AT24C02模块引脚立创开发板引脚说明VCC3V3电源正极接3.3VGNDGND电源地SCLP408I2C时钟线我们用它作为SCLSDAP409I2C数据线我们用它作为SDA提示如果你用的模块有WP写保护引脚想启用写保护功能就接高电平VCC想随时能写就接低电平GND。我们演示时可以先接GND。2.2 图形化配置引脚功能立创开发板配套的IDE支持图形化配置引脚非常方便。这里我们虽然用软件模拟I2C时序即“GPIO模拟I2C”不需要配置专门的I2C外设但仍然需要把P408和P409配置为普通的GPIO引脚。打开你的项目找到图形化引脚配置界面。找到引脚P408和P409。将它们的功能都设置为“GPIO”模式。按Ctrl S保存配置。点击右上角的Generate Project Content按钮生成代码。这一步完成后IDE就会自动生成初始化这两个GPIO的代码我们后续直接调用API控制它们的高低电平即可。3. 驱动代码编写手把手实现I2C时序硬件准备好了接下来是重头戏写驱动。我们采用GPIO模拟I2C的方式好处是代码透明、易于理解而且不占用芯片的硬件I2C资源。我们会创建两个文件bsp_at24c02.c和bsp_at24c02.h。3.1 头文件定义 (bsp_at24c02.h)头文件里主要做三件事定义引脚、定义数据类型和延时函数、声明对外提供的函数。#ifndef BSP_CODE_BSP_AT24C02_H_ #define BSP_CODE_BSP_AT24C02_H_ #include hal_data.h #include stdio.h // 为了方便定义一些简化的类型别名 #ifndef u8 #define u8 uint8_t #endif // 定义我们使用的引脚 #define Module_SCL_PIN BSP_IO_PORT_04_PIN_08 // SCL 接 P408 #define Module_SDA_PIN BSP_IO_PORT_04_PIN_09 // SDA 接 P409 // SDA引脚设置为输入模式读取数据时用 #define SDA_IN() { \ fsp_err_t err R_IOPORT_PinCfg(g_ioport_ctrl, Module_SDA_PIN, \ (uint32_t) IOPORT_CFG_PORT_DIRECTION_INPUT); \ if(err ! FSP_SUCCESS) { \ printf(GPIO Mode Set INPUT Failed!!\r\n); \ } \ } // SDA引脚设置为输出模式发送数据时用 #define SDA_OUT() { \ fsp_err_t err R_IOPORT_PinCfg(g_ioport_ctrl, Module_SDA_PIN, \ ((uint32_t) IOPORT_CFG_DRIVE_HIGH \ | (uint32_t) IOPORT_CFG_NMOS_ENABLE \ | (uint32_t) IOPORT_CFG_PORT_DIRECTION_OUTPUT \ | (uint32_t) IOPORT_CFG_PORT_OUTPUT_HIGH)); \ if(err ! FSP_SUCCESS) { \ printf(GPIO Mode Set OUTPUT Failed!!\r\n); \ } \ } // 控制SCL和SDA引脚输出高/低电平的宏 #define SCL(BIT) R_IOPORT_PinWrite(g_ioport_ctrl, Module_SCL_PIN, BIT) #define SDA(BIT) R_IOPORT_PinWrite(g_ioport_ctrl, Module_SDA_PIN, BIT) // 读取SDA引脚电平状态的函数 static inline bsp_io_level_t SDA_GET(void) { bsp_io_level_t p_pin_value; fsp_err_t err R_IOPORT_PinRead(g_ioport_ctrl, Module_SDA_PIN, p_pin_value); if(err ! FSP_SUCCESS) { printf(GPIO Input Read Failed!!\r\n); } return p_pin_value; } // 对外提供的函数声明 void AT24C02_Init(void); void AT24C02_WriteByte(unsigned char WordAddress,unsigned char Data); unsigned char AT24C02_ReadByte(unsigned char WordAddress); #endif /* BSP_CODE_BSP_AT24C02_H_ */3.2 核心驱动实现 (bsp_at24c02.c)这个文件是实现所有I2C底层时序和AT24C02读写命令的地方。咱们一段段来看。首先是一些全局定义和I2C基本时序函数#include bsp_at24c02.h #include stdio.h // AT24C02的读写地址根据前面计算的来 #define AT24C02_ADDRESS_READ 0xA0 // 写操作地址 #define AT24C02_ADDRESS_WRITE 0xA1 // 读操作地址 // IIC起始信号SCL高电平时SDA产生一个下降沿 void IIC_Start(void) { SDA_OUT(); // 确保SDA是输出模式 SDA(1); delay_us(5); SCL(1); delay_us(5); SDA(0); // 在SCL高电平期间SDA从高变低这就是起始信号 delay_us(5); SCL(0); // 钳住总线准备发送数据 delay_us(5); } // IIC停止信号SCL高电平时SDA产生一个上升沿 void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); delay_us(5); SCL(1); delay_us(5); SDA(1); // 在SCL高电平期间SDA从低变高这就是停止信号 delay_us(5); } // 主机发送应答(ACK)或非应答(NACK)信号 // ack0: 发送应答(低电平) // ack1: 发送非应答(高电平) void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); SDA(0); // 先拉低准备根据ack值决定 delay_us(5); if(!ack) SDA(0); // 发送应答 else SDA(1); // 发送非应答 SCL(1); delay_us(5); SCL(0); SDA(1); // 释放SDA线 } // 主机等待从机AT24C02的应答信号 // 返回0从机正确应答 // 返回1从机无应答可能设备不存在或通信失败 unsigned char I2C_WaitAck(void) { unsigned char ack_flag 10; // 超时计数 SCL(0); SDA(1); // 主机先释放SDA线 SDA_IN(); // 把SDA设置为输入准备读取从机应答 delay_us(5); SCL(1); // 主机拉高SCL此时从机可以控制SDA delay_us(5); // 等待SDA被从机拉低应答信号 while( (SDA_GET()1) ( ack_flag ) ) { ack_flag--; delay_us(5); } if( ack_flag 0 ) // 超时没有等到应答 { IIC_Stop(); // 发生错误发送停止信号 return 1; } else // 等到了应答 { SCL(0); SDA_OUT(); // 重新把SDA控制权拿回来设置为输出 } return 0; }接下来是发送和接收一个字节数据的函数这是I2C通信的基础// 主机向从机发送一个字节8位数据 void Send_Byte(uint8_t dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟开始数据传输 for( i 0; i 8; i ) // 从最高位(MSB)开始发送 { // 取出当前要发送的最高位放到SDA线上 SDA( (dat 0x80) 7 ); delay_us(1); SCL(1); // 拉高SCL从机在此时采样SDA数据 delay_us(5); SCL(0); // 拉低SCL为发送下一位做准备 delay_us(5); dat 1; // 数据左移准备发送下一位 } } // 主机从从机读取一个字节8位数据 unsigned char Read_Byte(void) { unsigned char i, receive 0; SDA_IN(); // 设置SDA为输入准备读取从机数据 for(i 0; i 8; i ) { SCL(0); delay_us(5); SCL(1); // 拉高SCL从机将数据放到SDA上 delay_us(5); receive 1; // 左移为接收新数据位腾出空间 if( SDA_GET() ) // 读取SDA线上的电平 { receive | 1; // 如果为高电平则该位为1 } delay_us(5); } SCL(0); return receive; }最后就是封装好的、针对AT24C02的读写函数了// AT24C02初始化实际上就是让I2C总线处于空闲状态SCL和SDA都拉高 void AT24C02_Init(void) { SCL(1); SDA(1); delay_ms(100); // 上电后等待一小段时间让芯片稳定 } // 向AT24C02的指定地址写入一个字节数据 // WordAddress: 要写入的地址 (0x00 ~ 0xFF) // Data: 要写入的数据 void AT24C02_WriteByte(unsigned char WordAddress, unsigned char Data) { IIC_Start(); // 1. 发送起始信号 Send_Byte(AT24C02_ADDRESS_READ); // 2. 发送写操作地址 (0xA0) I2C_WaitAck(); // 3. 等待从机应答 Send_Byte(WordAddress); // 4. 发送要写入的内部存储地址 I2C_WaitAck(); // 5. 等待从机应答 Send_Byte(Data); // 6. 发送要写入的数据 I2C_WaitAck(); // 7. 等待从机应答 IIC_Stop(); // 8. 发送停止信号触发芯片内部写周期 delay_ms(10); // 非常重要等待内部写操作完成AT24C02需要几毫秒 } // 从AT24C02的指定地址读取一个字节数据 // WordAddress: 要读取的地址 (0x00 ~ 0xFF) // 返回值: 读到的数据 unsigned char AT24C02_ReadByte(unsigned char WordAddress) { unsigned char Data; // 先执行一个“伪写”操作把地址指针设定到我们要读的位置 IIC_Start(); Send_Byte(AT24C02_ADDRESS_READ); // 发送写地址 I2C_WaitAck(); Send_Byte(WordAddress); // 发送要读的地址 I2C_WaitAck(); // 然后重新发起一个读操作 IIC_Start(); // 发送重复起始信号 Send_Byte(AT24C02_ADDRESS_WRITE); // 发送读地址 (0xA1) I2C_WaitAck(); Data Read_Byte(); // 读取一个字节数据 IIC_Send_Ack(1); // 主机发送非应答(NACK)表示读取结束 IIC_Stop(); // 发送停止信号 return Data; }关键点提醒写操作后的延时AT24C02_WriteByte函数最后的delay_ms(10)绝对不能少AT24C02在接收到停止信号后需要几毫秒的时间在内部把数据真正写入存储单元。在这期间它不会响应任何I2C命令。如果马上进行下一次操作会导致失败。读操作的“伪写”随机读从指定地址读需要两步。第一步是“写”模式发送目标地址把芯片内部的地址指针定位过去。第二步才是“读”模式把数据读出来。应答与非应答读最后一个字节时主机要发送非应答(NACK)然后马上发停止信号这是I2C协议规定的告诉从机“我读完了”。4. 实战验证编写测试程序驱动写好了怎么知道它能不能用呢我们写个简单的测试程序往EEPROM里写两个数再读出来看看对不对。在你的主应用程序文件例如app.c的Run函数里添加以下测试代码#include bsp_uart.h // 用于串口打印 #include bsp_at24c02.h // 我们刚写的驱动头文件 void Run(void) { // 初始化调试串口方便在电脑端查看结果 UART0_Debug_Init(); printf(\r\n AT24C02 EEPROM Test Start \r\n); // 初始化AT24C02 AT24C02_Init(); printf(AT24C02 EEPROM initialized.\r\n); uint8_t write_data_1 48; // 准备写入的数据1 uint8_t write_data_2 66; // 准备写入的数据2 uint8_t read_back_1, read_back_2; // 用于存放读回的数据 printf(\r\nWriting data to EEPROM...\r\n); // 向地址0写入数据48 AT24C02_WriteByte(0, write_data_1); delay_ms(10); // 等待写入完成 // 向地址8写入数据66 AT24C02_WriteByte(8, write_data_2); delay_ms(10); // 等待写入完成 printf(Reading data back from EEPROM...\r\n); // 从地址0读取数据 read_back_1 AT24C02_ReadByte(0); delay_ms(10); // 从地址8读取数据 read_back_2 AT24C02_ReadByte(8); delay_ms(10); // 打印结果 printf(\r\n--- Test Result ---\r\n); printf(Write to Address 0: %d\r\n, write_data_1); printf(Read from Address 0: %d\r\n, read_back_1); printf(Write to Address 8: %d\r\n, write_data_2); printf(Read from Address 8: %d\r\n, read_back_2); if((read_back_1 write_data_1) (read_back_2 write_data_2)) { printf(\r\nSUCCESS! EEPROM read/write test passed!\r\n); } else { printf(\r\nFAILED! Data mismatch.\r\n); } while(1) { // 主循环可以添加其他任务 // 例如让一个LED闪烁表示系统在运行 // led_blink(); delay_ms(1000); } }编译代码并下载到开发板。打开串口调试助手波特率根据你的UART0_Debug_Init设置通常是115200你应该能看到类似的输出 AT24C02 EEPROM Test Start AT24C02 EEPROM initialized. Writing data to EEPROM... Reading data back from EEPROM... --- Test Result --- Write to Address 0: 48 Read from Address 0: 48 Write to Address 8: 66 Read from Address 8: 66 SUCCESS! EEPROM read/write test passed!如果看到“SUCCESS”恭喜你AT24C02的驱动已经成功移植并可以正常工作了。你可以尝试修改写入的地址和数据或者断电后再上电看看数据是否真的保存住了。这个小模块以后就可以在你的项目里担任“记忆官”的角色了用来存储那些需要掉电保存的关键信息。