1. STM32F103 I2C-EEPROM 实验详解
最近在调试普中STM32F103开发板的I2C通信功能,通过IO口模拟I2C时序与AT24C02 EEPROM芯片进行数据交互。这个实验看似简单,但实际调试过程中遇到了不少坑,今天就把完整的实现过程和经验总结分享给大家。
2. I2C总线基础解析
2.1 I2C物理层特性
I2C总线最吸引我的就是它的简洁性——仅需两根线(SDA数据线和SCL时钟线)就能实现多设备通信。在实际电路设计中,有几点需要特别注意:
-
上拉电阻选择:通常使用4.7kΩ电阻,但具体值需要根据总线电容计算。我实测发现,当总线长度超过30cm时,可能需要减小电阻值到2.2kΩ以保证信号质量。
-
地址冲突问题:每个I2C设备都有唯一地址,AT24C02的地址由A0-A2引脚决定。在同一个总线上挂载多个相同器件时,必须确保它们的地址不同。我曾经因为忽略这点导致数据读写异常。
2.2 I2C协议层关键点
协议层有几个容易出错的细节:
-
起始条件:SCL高电平时SDA由高变低。调试时我用逻辑分析仪抓取信号,发现如果SDA下降沿与SCL上升沿太接近会导致识别失败。
-
数据有效性:必须在SCL低电平期间改变SDA数据,高电平时保持稳定。初期我编写的代码在这个时序上没处理好,导致数据出错。
-
应答机制:每个字节传输后必须跟一个ACK/NACK。常见错误是忘记检查ACK信号,当从设备无响应时主设备会一直等待。
3. AT24C02芯片深度剖析
3.1 芯片特性与硬件设计
AT24C02是2Kbit(256字节)的EEPROM,支持页写入(16字节/页)。硬件设计时要注意:
-
地址引脚处理:开发板将A0-A2接地,所以器件地址为0x50(7位地址)。若需要多个EEPROM,必须分开配置这些引脚。
-
写保护引脚:WP接高电平时禁止写入,开发板接地表示允许写操作。在产品设计中可以通过MCU控制这个引脚来防止误写入。
-
电源滤波:虽然手册没明确要求,但实际测试发现VCC引脚加0.1μF电容可以显著提高抗干扰能力。
3.2 读写时序要点
AT24C02的读写时序有几个关键参数:
-
写周期时间(tWR):典型值5ms,这段时间内芯片不会响应新的命令。初期我没加延时导致连续写入失败。
-
地址翻转:跨页写入时地址会自动回到页首。这意味着如果要写入20字节数据,从地址15开始,后5字节会覆盖该页开头。
4. 软件模拟I2C实现
4.1 GPIO初始化配置
使用PB6(SCL)和PB7(SDA)模拟I2C,配置为开漏输出模式更符合I2C标准:
c复制void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
IIC_SCL_HIGH();
IIC_SDA_HIGH();
}
注意:虽然推挽输出也能工作,但在多主机场景下可能引发总线冲突。开漏输出才是标准做法。
4.2 关键时序函数实现
起始信号和停止信号的实现需要严格时序控制:
c复制void IIC_Start(void)
{
SDA_OUT();
IIC_SDA_HIGH();
IIC_SCL_HIGH();
delay_us(4); // 保持时间tSU;STA
IIC_SDA_LOW(); // START条件
delay_us(4);
IIC_SCL_LOW(); // 钳住总线
}
void IIC_Stop(void)
{
SDA_OUT();
IIC_SCL_LOW();
IIC_SDA_LOW();
delay_us(4);
IIC_SCL_HIGH();
delay_us(4); // 保持时间tSU;STO
IIC_SDA_HIGH();
}
调试中发现,延时时间不足会导致从设备无法正确识别信号。建议用逻辑分析仪验证时序是否符合规格书要求。
5. EEPROM驱动开发
5.1 单字节读写函数
读函数实现时要注意随机读和当前地址读的区别:
c复制u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
u8 temp = 0;
IIC_Start();
// 发送器件地址+写命令
if(EE_TYPE > AT24C16) {
IIC_Send_Byte(0xA0);
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr>>8); // 高地址
} else {
IIC_Send_Byte(0xA0 + ((ReadAddr>>7)<<1));
}
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr&0xFF); // 低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0xA1); // 发送器件地址+读命令
IIC_Wait_Ack();
temp = IIC_Read_Byte(0); // 读数据,发送NACK
IIC_Stop();
return temp;
}
写函数需要处理写周期等待:
c复制void AT24CXX_WriteOneByte(u16 WriteAddr, u8 DataToWrite)
{
IIC_Start();
// 地址发送同上
...
IIC_Send_Byte(DataToWrite);
IIC_Wait_Ack();
IIC_Stop();
delay_ms(10); // 等待写周期完成
}
5.2 多字节读写优化
页写入可以显著提高写入效率,但要注意页边界处理:
c复制void AT24CXX_WritePage(u16 WriteAddr, u8 *pBuffer, u8 NumToWrite)
{
while(NumToWrite--) {
// 检查是否到达页边界
if((WriteAddr & (EE_PAGE_SIZE-1)) == 0 && NumToWrite) {
AT24CXX_WriteOneByte(WriteAddr, *pBuffer);
pBuffer++;
WriteAddr++;
continue;
}
IIC_Start();
// 发送地址
...
IIC_Send_Byte(*pBuffer);
IIC_Wait_Ack();
pBuffer++;
WriteAddr++;
}
IIC_Stop();
delay_ms(10);
}
6. 主程序设计与调试技巧
6.1 硬件检测实现
可靠的硬件检测可以避免后续操作失败:
c复制u8 AT24CXX_Check(void)
{
u8 temp;
temp = AT24CXX_ReadOneByte(0x7F); // 读取一个非易失地址
AT24CXX_WriteOneByte(0x7F, 0x55); // 写入测试值
if(AT24CXX_ReadOneByte(0x7F) == 0x55) {
AT24CXX_WriteOneByte(0x7F, temp); // 恢复原值
return 0; // 检测正常
}
return 1; // 检测失败
}
6.2 按键控制逻辑
通过按键实现交互式测试:
c复制while(1) {
key = KEY_Scan(0);
if(key == KEY_UP_PRESS) {
// 写入字符串
char str[] = "www.prechin.cn";
AT24CXX_Write(0, (u8*)str, sizeof(str));
printf("已写入字符串: %s\r\n", str);
}
if(key == KEY1_PRESS) {
// 读取字符串
char buf[20] = {0};
AT24CXX_Read(0, (u8*)buf, sizeof(buf));
printf("读取到数据: %s\r\n", buf);
}
// LED闪烁指示系统运行
if(i++ % 20 == 0) LED1_TOGGLE();
delay_ms(10);
}
7. 常见问题与解决方案
-
检测不到EEPROM芯片
- 检查硬件连接:SCL、SDA是否接反,上拉电阻是否正常
- 测量电源电压:确保在4.5-5.5V范围内
- 用逻辑分析仪抓取I2C波形,验证时序
-
写入后读取数据不正确
- 确认写周期延时足够(至少5ms)
- 检查地址是否越界(AT24C02只有256字节)
- 验证页写入时是否跨页边界
-
随机读写失败
- 确保每次操作后都产生停止条件
- 检查从设备地址是否正确(0xA0写,0xA1读)
- 确认ACK信号被正确处理
-
长距离通信不稳定
- 减小上拉电阻值(可尝试2.2kΩ)
- 降低通信速率(改用标准模式100kHz)
- 增加电源去耦电容
通过这个项目,我深刻体会到硬件协议实现中时序控制的重要性。虽然STM32有硬件I2C外设,但软件模拟方式在跨平台移植和调试方面更有优势。建议初学者先用软件模拟理解I2C本质,再尝试使用硬件外设。