GD32VW553开发板I2C驱动AT24C02 EEPROM:从原理到字节/页读写实战

GD32VW553开发板I2C驱动AT24C02 EEPROM:从原理到字节/页读写实战 GD32VW553开发板I2C驱动AT24C02 EEPROM从原理到字节/页读写实战最近在做一个基于GD32VW553的项目需要保存一些配置参数掉电后还不能丢失这时候EEPROM就派上用场了。AT24C02是市面上非常常见的一款I2C接口EEPROM价格便宜使用简单。今天我就结合手头的GD32VW553开发板带大家从原理到代码一步步实现AT24C02的驱动。这篇文章适合刚开始接触嵌入式存储或者GD32VW553开发的朋友。我会先讲清楚AT24C02的基本原理和I2C通信时序然后手把手教大家用GPIO模拟I2C的方式编写驱动代码最后通过一个完整的读写示例来验证。跟着做一遍你就能掌握在项目中添加非易失性存储的基本技能了。1. AT24C02 EEPROM基础它是什么怎么工作1.1 EEPROM是什么EEPROM全称是“电可擦除可编程只读存储器”。这个名字听起来有点绕咱们拆开来看只读存储器数据掉电后不会丢失这是它和RAM内存最大的区别。可编程我们可以通过程序往里面写数据。电可擦除不需要像老式的EPROM那样用紫外线照射来擦除直接用电信号就能擦除重写。简单说EEPROM就像一块可以反复擦写的“电子笔记本”断电后笔记还在下次上电能接着看。AT24C02就是这样一个“笔记本”容量是2K位也就是256个字节1字节8位。虽然容量不大但存一些设备参数、校准数据、运行状态记录是绰绰有余的。1.2 AT24C02的关键特性根据原始资料AT24C02有几个关键参数需要记住特性参数值工作电压1.8V - 5.5V (很宽兼容3.3V和5V系统)工作电流最大3mA (非常省电)通信接口I2C (也叫IIC两线制)存储容量2048位 / 256字节页大小16字节 (一次最多能连续写16个字节)最大时钟速度5V时1000KHz其他电压400KHz这里有个坑要注意AT24C02内部有一个16字节的“页写缓冲器”。这意味着你写数据时如果一次写入超过16个字节或者跨页写入比如从地址14开始写10个字节芯片内部会自动把地址“翻卷”覆盖掉本页开头的数据。这个细节后面写代码时会特别讲到。1.3 I2C通信与AT24C02的“门牌号”AT24C02通过I2C总线和我们的主控芯片GD32VW553通信。I2C总线只有两根线SCL时钟线由主机MCU控制节奏。SDA数据线主从设备都通过它收发数据。总线上可以挂很多设备怎么区分呢靠“设备地址”。AT24C02的7位设备地址是1010xxx。高4位1010是厂家固定的所有AT24CXX系列都一样。低3位xxx由芯片的A0, A1, A2这三个硬件引脚的电平决定接VCC就是1接GND就是0。这意味着通过设置这三个引脚一条I2C总线上最多可以挂8个AT24C022^38。我们用的模块通常这三个引脚都接地所以地址就是1010000。在实际通信时我们发送的是一个8位的“寻址字节”。它由7位设备地址加上1位“读写方向位”组成方向位 0表示主机要写数据到从机AT24C02。方向位 1表示主机要从从机AT24C02读数据。所以对于地址引脚全接地的AT24C02写操作的寻址字节是1010000 0即0xA0。读操作的寻址字节是1010000 1即0xA1。这个地址会在我们后面的代码里直接用到。2. 硬件连接与工程准备2.1 接线很简单把AT24C02模块和GD32VW553开发板连起来只需要4根线AT24C02模块引脚GD32VW553开发板引脚说明VCC3V3接3.3V电源模块兼容3.3VGNDGND共地SCLPA2I2C时钟线SDAPA3I2C数据线注意I2C总线要求SDA和SCL线都需要上拉电阻。幸运的是GD32VW553的GPIO内部可以配置上拉我们代码里会打开这个功能。如果你的模块上已经有外部上拉电阻通常4.7K或10K那也没问题内部上拉可以不开或者两者并存。2.2 在工程中添加驱动文件咱们不直接用硬件I2C外设而是用GPIO模拟也叫“软件I2C”。这样做的好处是引脚可以任意选代码移植起来也方便。我们在工程里新建两个文件bsp_at24c02.c– 驱动源文件bsp_at24c02.h– 驱动头文件记得把包含头文件的路径添加到你的编译器中。这个操作在不同IDE比如Keil、IAR、VS CodeGCC里不太一样但原理都是告诉编译器去哪里找我们写的.h文件。3. 手把手编写I2C底层驱动I2C的时序有严格的规定咱们用GPIO模拟就得自己用代码把这些高低电平的时序“画”出来。别担心我带你一步步写。3.1 引脚宏定义与初始化首先在头文件bsp_at24c02.h里我们把用到的引脚和基本操作定义好#ifndef BSP_CODE_BSP_AT24C02_H_ #define BSP_CODE_BSP_AT24C02_H_ #include gd32vw55x.h #include systick.h // 为了方便定义一些简化的类型和延时宏 #ifndef u8 #define u8 uint8_t #endif #ifndef delay_ms #define delay_ms(x) delay_1ms(x) #endif #ifndef delay_us #define delay_us(x) delay_1us(x) #endif // 引脚和时钟定义 #define Module_SCL_RCU RCU_GPIOA #define Module_SCL_PORT GPIOA #define Module_SCL_PIN GPIO_PIN_2 //SCL #define Module_SDA_RCU RCU_GPIOA #define Module_SDA_PORT GPIOA #define Module_SDA_PIN GPIO_PIN_3 //SDA // 使能GPIO时钟的宏 #define Module_RCU_ENABLE() \ rcu_periph_clock_enable(Module_SCL_RCU); \ rcu_periph_clock_enable(Module_SDA_RCU); // SDA引脚模式快速切换宏输入/输出 #define SDA_IN() \ gpio_mode_set(Module_SDA_PORT, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, Module_SDA_PIN); #define SDA_OUT() \ gpio_mode_set(Module_SDA_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, Module_SDA_PIN); \ gpio_output_options_set(Module_SDA_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_25MHZ, Module_SDA_PIN); // 控制SCL和SDA电平的宏 #define SCL(BIT) gpio_bit_write(Module_SCL_PORT, Module_SCL_PIN, BIT) #define SDA(BIT) gpio_bit_write(Module_SDA_PORT, Module_SDA_PIN, BIT) // 读取SDA引脚电平 #define SDA_GET() gpio_input_bit_get(Module_SDA_PORT, Module_SDA_PIN) // AT24C02的读写地址假设A2,A1,A0引脚都接地 #define AT24C02_ADDRESS_WRITE 0xA0 // 写地址 #define AT24C02_ADDRESS_READ 0xA1 // 读地址 // 函数声明 void AT24C02_Init(void); void AT24C02_WriteByte(unsigned char WordAddress, unsigned char Data); unsigned char AT24C02_ReadByte(unsigned char WordAddress); #endif接下来是初始化函数AT24C02_Init()放在.c文件里。它的任务就是打开GPIO时钟把PA2和PA3配置成开漏输出模式这是I2C总线要求的并且初始化为高电平。void AT24C02_Init(void) { // 1. 使能GPIOA的时钟 Module_RCU_ENABLE(); // 2. 初始化SCL引脚PA2为开漏输出上拉速度25MHz gpio_mode_set(Module_SCL_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, Module_SCL_PIN); gpio_output_options_set(Module_SCL_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_25MHZ, Module_SCL_PIN); // 3. 初始化SDA引脚PA3为开漏输出上拉速度25MHz gpio_mode_set(Module_SDA_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, Module_SDA_PIN); gpio_output_options_set(Module_SDA_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_25MHZ, Module_SDA_PIN); // 4. 将总线拉高进入空闲状态 SCL(1); SDA(1); // 5. 稍作延时等待器件稳定 delay_ms(100); }关键点GPIO模式一定要选GPIO_OTYPE_OD开漏输出。开漏输出的特点是当输出0时引脚被强拉到低电平当输出1时引脚实际上是高阻态靠外部或内部的上拉电阻拉到高电平。这样多个设备挂在总线上才不会冲突。3.2 I2C时序的“基本笔画”起始、停止、应答I2C通信就像两个人用一套摩斯密码电报机对话起始信号是“开始通话”停止信号是“通话结束”应答信号是“收到请回复”。起始信号StartSCL为高电平时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); // 拉低SCL为后续发送数据做准备 delay_us(5); }停止信号StopSCL为高电平时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主机在接收完一个字节后需要告诉从机“我收到了”。做法是在第9个时钟脉冲期间主机将SDA拉低。void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); if(!ack) { SDA(0); // 发送应答信号低电平 } else { SDA(1); // 发送非应答信号高电平常用于读取结束 } delay_us(5); SCL(1); delay_us(5); SCL(0); SDA(1); // 释放SDA线 }等待应答Wait ACK主机发送完地址或数据后需要等待从机回复“收到”。主机在第9个时钟脉冲期间去检测SDA是否为低电平。unsigned char I2C_WaitAck(void) { unsigned char ack_flag 10; // 超时计数 SCL(0); SDA(1); SDA_IN(); // 先把SDA设置为输入模式准备读取 delay_us(5); SCL(1); // 第9个时钟脉冲 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(); // 切换回输出模式为后续发送做准备 } return 0; // 收到应答返回0 }提示delay_us(5)的延时是为了满足I2C的时序要求。AT24C02在400KHz速率下一个时钟周期是2.5us这里留了足够的余量。如果你的系统主频很高可能需要调整这个延时。3.3 字节的发送与接收有了基本信号我们就可以发送和接收数据了。I2C规定数据在SCL为低电平时变化在SCL为高电平时保持稳定并被读取。发送一个字节从最高位MSB开始依次放到SDA线上。void Send_Byte(uint8_t dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟开始数据传输 for( i 0; i 8; i ) { // 取出最高位右移7位得到0或1然后写入SDA线 SDA( (dat 0x80) 7 ); delay_us(1); SCL(1); // 拉高时钟从机在此刻采样数据位 delay_us(5); SCL(0); // 拉低时钟准备发送下一位 delay_us(5); dat 1; // 左移准备发送下一位 } }接收一个字节主机控制SCL产生时钟并在SCL高电平时读取SDA线上的数据。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); // 拉高时钟此时数据稳定 delay_us(5); receive 1; // 左移为接收新数据位腾出空间 if( SDA_GET() ) { receive | 1; // 如果SDA为高该位置1 } delay_us(5); } SCL(0); return receive; }4. AT24C02的读写操作实战底层时序函数准备好了现在我们来封装针对AT24C02的具体操作。4.1 字节写入Byte Write字节写入是最基本的操作一次写一个字节到指定地址。流程如下发送起始信号。发送写地址0xA0并等待应答。发送要写入的存储单元地址0x00-0xFF并等待应答。发送要写入的数据字节并等待应答。发送停止信号。void AT24C02_WriteByte(unsigned char WordAddress, unsigned char Data) { IIC_Start(); Send_Byte(AT24C02_ADDRESS_WRITE); // 发送写地址 I2C_WaitAck(); Send_Byte(WordAddress); // 发送要写的内存地址 I2C_WaitAck(); Send_Byte(Data); // 发送要写的数据 I2C_WaitAck(); IIC_Stop(); // 停止信号触发内部写周期 delay_ms(10); // 重要必须等待内部写周期完成AT24C02需要约5-10ms }这里有个大坑发送停止信号后AT24C02才开始把数据从缓冲区真正写入存储单元这个内部写过程需要时间典型值5ms。在此期间芯片不会响应任何I2C命令。所以写操作后必须加一个足够长的延时delay_ms(10)否则紧接着的读写操作会失败。4.2 随机读取Random Read随机读取可以从任意地址读出一个字节。它的流程有点特殊需要先“假装”写一下告诉芯片我要读哪个地址然后再发起读操作。发送起始信号。发送写地址0xA0并等待应答。伪写操作开始发送要读取的存储单元地址并等待应答。伪写操作结束再次发送起始信号Repeated Start。发送读地址0xA1并等待应答。读取一个字节的数据。主机发送非应答信号NACK表示读取结束。发送停止信号。unsigned char AT24C02_ReadByte(unsigned char WordAddress) { unsigned char Data; // 第一步伪写操作设定要读的地址 IIC_Start(); Send_Byte(AT24C02_ADDRESS_WRITE); // 发送写地址 I2C_WaitAck(); Send_Byte(WordAddress); // 发送要读的内存地址 I2C_WaitAck(); // 第二步重新发起起始条件开始读操作 IIC_Start(); Send_Byte(AT24C02_ADDRESS_READ); // 发送读地址 I2C_WaitAck(); Data Read_Byte(); // 读取数据字节 IIC_Send_Ack(1); // 发送非应答(NACK)表示只读一个字节 IIC_Stop(); return Data; }4.3 页写入Page Write与连续读取原始资料提到AT24C02支持页写入一次最多可以连续写入16个字节。这比分16次写一个字节快得多。流程和字节写类似只是在发送停止信号前可以连续发送最多16个数据字节。芯片内部地址计数器会在收到每个字节后自动加1。页写入的注意事项写入不能跨页。比如从地址0开始最多写到地址15共16字节。如果你试图写第17个字节地址会“翻卷”回到0覆盖掉开头的数据。同样写完后需要等待内部写周期完成。连续读取则更简单。在随机读操作中读完第一个字节后如果主机回复的是应答信号ACK而不是非应答NACKAT24C02就会继续输出下一个地址的数据。这样就可以连续读取多个字节直到主机发出NACK和停止信号。5. 上机测试让代码跑起来理论讲完了是骡子是马拉出来溜溜。我们在main.c里写个简单的测试程序。#include gd32vw55x.h #include systick.h #include stdio.h #include bsp_at24c02.h int main(void) { // 系统初始化时钟、延时、串口等 systick_config(); gd_eval_com_init(EVAL_COM0); // 初始化串口用于打印 printf(GD32VW553 AT24C02 Test Start...\r\n); // 初始化AT24C02 AT24C02_Init(); printf(AT24C02 EEPROM initialized.\r\n); uint8_t write_data 48; uint8_t read_back_data 0; // 测试1字节写入与读取 printf(\r\n--- Test 1: Single Byte Write/Read ---\r\n); AT24C02_WriteByte(0, write_data); // 向地址0写入48 printf(Write data %d to address 0.\r\n, write_data); delay_ms(10); // 等待写入完成 read_back_data AT24C02_ReadByte(0); // 从地址0读出 printf(Read data %d from address 0.\r\n, read_back_data); if(read_back_data write_data) { printf(Test 1 PASS!\r\n); } else { printf(Test 1 FAIL! Data mismatch.\r\n); } // 测试2另一个地址的写入读取 printf(\r\n--- Test 2: Another Address ---\r\n); AT24C02_WriteByte(8, 66); // 向地址8写入66 delay_ms(10); read_back_data AT24C02_ReadByte(8); printf(Read data %d from address 8.\r\n, read_back_data); // 测试3验证数据持久性可先注释掉写入直接读取 printf(\r\n--- Test 3: Data Persistence ---\r\n); printf(Now, please reset or power off/on the board, then check if data is still there.\r\n); // 复位或重新上电后再次运行程序注释掉上面的Write操作直接Read // read_back_data AT24C02_ReadByte(0); // printf(After reset, data at address 0 is: %d\r\n, read_back_data); while(1) { // 主循环 } }编译、下载到GD32VW553开发板打开串口助手波特率要和代码里初始化的一致。你应该能看到类似下面的输出GD32VW553 AT24C02 Test Start... AT24C02 EEPROM initialized. --- Test 1: Single Byte Write/Read --- Write data 48 to address 0. Read data 48 from address 0. Test 1 PASS! --- Test 2: Another Address --- Read data 66 from address 8.最关键的测试是数据持久性先运行一次完整的程序执行了写入然后按一下开发板的复位键或者重新上电。再次运行程序时把main函数里两个AT24C02_WriteByte的调用注释掉只保留读取和打印。如果串口还能正确打印出48和66恭喜你EEPROM驱动成功了6. 实际项目中的几点心得写周期等待是必须的这是我踩过的坑。连续写操作如果不加延时第二次写肯定会失败。稳妥起见每次写操作后都delay_ms(10)。地址对齐做页写入时一定要计算好起始地址别跨页。一个简单的对齐方法是起始地址 % 16 0。上拉电阻如果通信不稳定读写出错首先检查SDA和SCL线上是否有上拉电阻通常4.7KΩ-10KΩ。GD32VW553内部上拉电阻约40KΩ在总线较长或速度较高时可能不够强建议外接。调试技巧用逻辑分析仪或者示波器抓一下I2C波形是最直接的调试方法。可以清楚看到起始、停止、地址、数据、应答位是否正确。没有仪器的话就多用printf打印中间状态比如检查I2C_WaitAck()的返回值。好了关于GD32VW553驱动AT24C02 EEPROM的核心内容就是这些。代码我已经尽量写得清晰并加上了详细的注释。你可以直接拿去用也可以根据自己项目的需求封装页写和连续读的函数。希望这篇教程能帮你顺利在项目里加上可靠的掉电存储功能。