1. 项目背景与核心需求
最近在做一个需要掉电保存数据的小项目,手头正好有块搭载极海APM32F003F6P6的开发板。这个MCU性价比很高,但资源有限,没有硬件I2C外设。而项目中需要用到的AT24C02C EEPROM芯片又必须通过I2C接口通信。于是决定用GPIO模拟I2C时序来实现数据存储功能。
AT24C02C是Microchip推出的256字节容量EEPROM,采用I2C接口,工作电压1.7V-5.5V,支持100kHz和400kHz两种通信速率。在嵌入式系统中常用于存储配置参数、校准数据等需要长期保存的信息。
2. 硬件设计与引脚配置
2.1 硬件连接原理
开发板上APM32F003F6P6与AT24C02C的连接非常简单:
- PB4作为SCL时钟线
- PB5作为SDA数据线
- EEPROM的A0-A2地址引脚接地,设备地址为0xA0
注意:I2C总线上需要加上拉电阻,通常选择4.7kΩ。如果开发板已经内置上拉,则无需额外添加。
2.2 GPIO模式配置关键点
模拟I2C时必须将GPIO配置为开漏输出模式(GPIO_MODE_OUT_OD),这是本项目的第一个技术要点:
c复制static void i2c_gpio_init(void)
{
GPIO_Config_T gpioConfig;
gpioConfig.mode = GPIO_MODE_OUT_OD; // 开漏输出
gpioConfig.speed = GPIO_SPEED_10MHz;
gpioConfig.intEn = GPIO_EINT_DISABLE;
gpioConfig.pin = I2C_CLK_PIN | I2C_DATA_PIN;
GPIO_Config(I2C_GPIO, &gpioConfig);
I2C_CLK_SET; // 初始化为高电平
I2C_DATA_SET;
}
开漏输出的重要性在于:
- 避免总线冲突:当多个设备同时驱动总线时,推挽输出可能导致短路
- 实现线与逻辑:任何设备拉低总线都会使整个总线变低
- 兼容不同电压设备:上拉电阻可以接到不同电压
3. 模拟I2C协议实现
3.1 基础时序函数
3.1.1 起始信号时序
起始信号是I2C通信的开始标志,时序要求严格:
- SDA先由高变低
- 保持t_HD_STA时间后SCL变低
c复制void I2c_Start(){
I2C_DATA_MODE_OUT;
I2C_CLK_SET;
I2C_DATA_SET;
Delay_us(2); // t_SU_STA
I2C_DATA_RESET;
Delay_us(2); // t_HD_STA
I2C_CLK_RESET;
}
3.1.2 停止信号时序
停止信号结束通信:
- SCL先拉高
- 保持t_SU_STO时间后SDA拉高
c复制void I2c_Stop(){
I2C_DATA_MODE_OUT;
Delay_us(2);
I2C_DATA_RESET;
Delay_us(3);
I2C_CLK_SET;
Delay_us(2);
I2C_DATA_SET;
Delay_us(5);
}
3.2 数据收发实现
3.2.1 发送单字节
每个bit的发送需要遵循:
- SCL低电平时准备数据
- SCL高电平时数据稳定
- 重复8次完成1字节
c复制void I2c_Send_Byte(uint8_t data){
uint8_t temp;
I2C_DATA_MODE_OUT;
for(temp=0;temp<8;temp++){
Delay_us(1);
if((data & 0x80) == 0x80){
I2C_DATA_SET;
}else{
I2C_DATA_RESET;
}
data <<= 1;
Delay_us(2);
I2C_CLK_SET;
Delay_us(5);
I2C_CLK_RESET;
}
}
3.2.2 接收单字节
接收时需要切换SDA为输入模式:
c复制uint8_t I2c_read_Byte(){
uint8_t temp,data;
I2C_DATA_MODE_IN;
for(temp=0;temp<8;temp++){
data <<= 1;
Delay_us(5);
I2C_CLK_SET;
Delay_us(2);
if(READ_I2C_DATA()) {
data += 1;
}
Delay_us(3);
I2C_CLK_RESET;
}
return(data);
}
4. AT24C02C驱动实现
4.1 单字节读写函数
4.1.1 写入单字节
c复制void write_24c02_byte(uint8_t addr,uint8_t data){
uint8_t temp;
I2c_Start();
I2c_Send_Byte(0xa0); // 设备地址+写
temp = i2c_checkack();
I2c_Send_Byte(addr); // 存储地址
temp = i2c_checkack();
I2c_Send_Byte(data); // 写入数据
temp = i2c_checkack();
I2c_Stop();
Delay_us(5000); // 等待写入完成
}
重要:AT24C02C页写入需要5ms最大时间,必须添加延时
4.1.2 读取单字节
c复制uint8_t read_24c02_byte(uint8_t addr){
uint8_t temp,data;
I2c_Start();
I2c_Send_Byte(0xa0); // 设备地址+写
temp = i2c_checkack();
I2c_Send_Byte(addr); // 读取地址
temp = i2c_checkack();
Delay_us(5);
I2c_Start();
I2c_Send_Byte(0xa1); // 设备地址+读
temp = i2c_checkack();
data = I2c_read_Byte();
I2c_Nack();
I2c_Stop();
return data;
}
4.2 多字节读写实现
4.2.1 多字节写入
AT24C02C支持页写入(8字节/页):
c复制void write_24c02(uint8_t addr,uint8_t len,u32 data){
uint8_t temp,data_len;
u32 temp1 = data;
I2c_Start();
I2c_Send_Byte(0xa0);
temp = i2c_checkack();
I2c_Send_Byte(addr);
temp = i2c_checkack();
for(data_len=0;data_len<len;data_len++){
I2c_Send_Byte((u8)temp1);
temp = i2c_checkack();
temp1 >>= 8;
}
I2c_Stop();
Delay_us(5000); // 页写入等待
}
4.2.2 多字节读取
c复制u32 read_24c02(uint8_t addr,uint8_t len){
uint8_t temp,data_len;
u8 trmp2[4];
u32 temp1=0;
I2c_Start();
I2c_Send_Byte(0xa0);
temp = i2c_checkack();
I2c_Send_Byte(addr);
temp = i2c_checkack();
Delay_us(5);
I2c_Start();
I2c_Send_Byte(0xa1);
temp = i2c_checkack();
for(data_len=0;data_len<len;data_len++){
trmp2[data_len] = I2c_read_Byte();
if((len-data_len) != 1){
I2c_sendAck();
}else{
I2c_Nack();
}
}
I2c_Stop();
// 组合多字节数据
temp1 = trmp2[3];
temp1 <<= 8;
temp1 += trmp2[2];
temp1 <<= 8;
temp1 += trmp2[1];
temp1 <<= 8;
temp1 += trmp2[0];
return (temp1);
}
5. 测试与验证
5.1 基础功能测试
写入并读取10字节数据测试:
c复制#define EEPROM_DATA_LEN (10)
uint8_t i2c_data[EEPROM_DATA_LEN];
// 写入测试
for(uint8_t i=0; i< EEPROM_DATA_LEN; i++) {
write_24c02_byte(i+1,i+0x55);
}
// 读取验证
for(uint8_t i=0; i< EEPROM_DATA_LEN; i++) {
i2c_data[i]= read_24c02_byte(i+1);
}
5.2 稳定性测试方案
建议增加以下测试:
- 边界测试:读写地址0和255
- 连续写入测试:循环写入不同模式数据
- 长时间稳定性测试:持续运行24小时
- 电源波动测试:在写入过程中突然断电
测试代码框架:
c复制void eeprom_test(void)
{
uint8_t write_buf[256];
uint8_t read_buf[256];
uint8_t error_count = 0;
// 填充测试数据
for(int i=0; i<256; i++){
write_buf[i] = i;
}
// 写入全部空间
for(int i=0; i<256; i++){
write_24c02_byte(i, write_buf[i]);
}
// 验证数据
for(int i=0; i<256; i++){
read_buf[i] = read_24c02_byte(i);
if(read_buf[i] != write_buf[i]){
error_count++;
printf("Error at addr %d: W=0x%02X R=0x%02X\n",
i, write_buf[i], read_buf[i]);
}
}
printf("Test complete, errors: %d\n", error_count);
}
6. 常见问题与解决方案
6.1 通信失败排查步骤
-
检查硬件连接
- 确认SCL/SDA线连接正确
- 测量上拉电阻是否正常
- 检查电源电压是否稳定
-
示波器观察波形
- 起始/停止信号是否符合时序
- 数据线变化是否在时钟低电平期间
- 信号上升时间是否过快(应加合适上拉)
-
软件调试技巧
- 在关键位置添加调试输出
- 逐步简化测试用例
- 检查延时时间是否足够
6.2 典型问题与解决
问题1:读取总是返回0xFF
- 可能原因:ACK应答失败
- 解决方案:检查从机地址是否正确,确认EEPROM上电完成
问题2:偶尔写入失败
- 可能原因:未等待写入完成
- 解决方案:在每次写入后添加足够延时(5ms)
问题3:高字节数据错误
- 可能原因:多字节读写时移位错误
- 解决方案:检查字节顺序处理逻辑
7. 性能优化建议
- 延时优化:根据实际测试减少不必要的延时
- 批量写入:利用页写入功能提高效率
- 错误重试:添加自动重试机制提高可靠性
- 数据校验:添加CRC校验确保数据完整性
优化后的页写入示例:
c复制void eeprom_page_write(uint8_t addr, uint8_t *data, uint8_t len)
{
uint8_t i;
I2c_Start();
I2c_Send_Byte(0xA0);
i2c_checkack();
I2c_Send_Byte(addr);
i2c_checkack();
for(i=0; i<len; i++){
I2c_Send_Byte(data[i]);
if(i2c_checkack()) break;
}
I2c_Stop();
Delay_us(5000); // 等待写入完成
}
在实际项目中,我将这个驱动应用到了一个需要保存用户配置的物联网设备上。经过3个月的现场测试,累计写入超过10万次,没有出现数据错误。关键经验是:每次写入前检查总线状态,重要数据采用"写入-验证-重试"的三重保障机制。