STM32F407软件模拟IIC驱动AT24C02:时序拆解与工程实践
IIC总线物理层与协议机制
IIC通信依赖两根信号线完成双向数据传输:SCL时钟线由主设备驱动,SDA数据线采用双向开漏结构。总线通过上拉电阻维持高电平,设备以开漏方式下拉实现"线与"逻辑。这种设计允许多主设备仲裁,但要求所有参与者严格遵循时序规范。
AT24C02的器件地址由固定前缀与可配置位组成。A0-A2引脚电平决定地址低三位,读写方向由最低位区分:0xA0为写操作,0xA1为读操作。
四个关键时序参数决定通信可靠性:
- 起始条件:SCL保持高电平时SDA产生下降沿,建立时间≥4.7μs
- 停止条件:SCL保持高电平时SDA产生上升沿,建立时间≥4.0μs
- 数据有效:SCL高电平期间SDA必须稳定,数据建立时间≥250ns
- 应答周期:第9个SCL时钟期间从设备拉低SDA,保持时间≥4.7μs
GPIO配置与硬件连接
STM32F407的PB8/PB9引脚配置为开漏输出模式,配合外部4.7kΩ上拉电阻。内部上拉电阻(约40kΩ)不足以驱动总线,必须外接物理上拉。
void SoftI2C_InitGPIO(void)
{
GPIO_InitTypeDef cfg;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
cfg.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
cfg.GPIO_Mode = GPIO_Mode_OUT;
cfg.GPIO_OType = GPIO_OType_OD; // 开漏输出关键配置
cfg.GPIO_PuPd = GPIO_PuPd_UP; // 使能内部上拉辅助
cfg.GPIO_Speed = GPIO_Speed_2MHz; // 降低边沿速率减少EMI
GPIO_Init(GPIOB, &cfg);
GPIO_SetBits(GPIOB, GPIO_Pin_8 | GPIO_Pin_9);
}
时序控制核心实现
延时精度直接影响通信稳定性。基于168MHz系统时钟,以下实现经逻辑分析仪验证满足100kHz标准模式要求:
static void Timing_Delay(void)
{
volatile uint32_t cnt = 30; // 实测调整值
while(cnt--);
}
#define SCL_H() GPIO_SetBits(GPIOB, GPIO_Pin_8)
#define SCL_L() GPIO_ResetBits(GPIOB, GPIO_Pin_8)
#define SDA_H() GPIO_SetBits(GPIOB, GPIO_Pin_9)
#define SDA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_9)
#define SDA_READ() GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9)
起始信号生成需确保SDA从高到低的跳变发生在SCL高电平期间:
void SoftI2C_Start(void)
{
SDA_H();
SCL_H();
Timing_Delay();
SDA_L(); // 起始条件:SCL高时SDA下降
Timing_Delay();
SCL_L(); // 钳住总线准备传输
Timing_Delay();
}
字节发送采用MSB优先方式,每个位在SCL低电平期间放置,高电平期间锁存:
void SoftI2C_SendByte(uint8_t tx_data)
{
for(int8_t bit = 7; bit >= 0; bit--) {
if(tx_data & (1 << bit)) {
SDA_H();
} else {
SDA_L();
}
Timing_Delay();
SCL_H(); // 时钟上升沿锁存数据
Timing_Delay();
SCL_L();
Timing_Delay();
}
SDA_H(); // 释放SDA准备接收ACK
}
应答检测需切换SDA为输入模式,实际实现中通过开漏特性直接读取电平:
uint8_t SoftI2C_CheckAck(void)
{
uint8_t ack_status = 0;
SDA_H(); // 释放总线
Timing_Delay();
SCL_H(); // 第9个时钟高电平期间采样
Timing_Delay();
if(!SDA_READ()) { // 低电平表示应答
ack_status = 1;
}
SCL_L();
Timing_Delay();
return ack_status;
}
AT24C02页写入与跨页处理
该器件存储阵列按8字节分页,页内地址连续写入可在一个通信周期完成,跨页操作必须拆分。写入周期最大5ms,期间器件不响应总线操作。
uint8_t AT24C02_PageWrite(uint8_t mem_addr, uint8_t *src, uint8_t len)
{
uint8_t page_offset = mem_addr & 0x07;
if(page_offset + len > 8) { // 超出当前页边界检查
len = 8 - page_offset;
}
SoftI2C_Start();
SoftI2C_SendByte(0xA0);
if(!SoftI2C_CheckAck()) return 0;
SoftI2C_SendByte(mem_addr);
if(!SoftI2C_CheckAck()) return 0;
for(uint8_t idx = 0; idx < len; idx++) {
SoftI2C_SendByte(src[idx]);
if(!SoftI2C_CheckAck()) return 0;
}
SoftI2C_Stop();
for(uint16_t wait = 0; wait < 5000; wait++) {
Timing_Delay(); // 软件延时等待写入完成
}
return len;
}
随机读取的伪写入机制
读取指定地址需先执行"虚写"操作定位存储指针,随后重新启动总线进入读模式:
uint8_t AT24C02_RandomRead(uint8_t mem_addr)
{
uint8_t rx_data;
SoftI2C_Start();
SoftI2C_SendByte(0xA0); // 写模式定位地址
SoftI2C_CheckAck();
SoftI2C_SendByte(mem_addr);
SoftI2C_CheckAck();
SoftI2C_Start(); // 重复起始条件
SoftI2C_SendByte(0xA1); // 读模式
SoftI2C_CheckAck();
rx_data = SoftI2C_RecvByte();
SoftI2C_SendNack(); // 单字节读取发送非应答
SoftI2C_Stop();
return rx_data;
}
工程化可靠性设计
实际部署需考虑总线冲突、器件忙状态等异常场景:
uint8_t AT24C02_WriteWithRetry(uint8_t addr, uint8_t *buf, uint8_t len)
{
for(uint8_t attempt = 0; attempt < 3; attempt++) {
if(AT24C02_PageWrite(addr, buf, len) == len) {
return 1; // 成功
}
SoftI2C_Stop(); // 强制复位总线
Timing_Delay();
Timing_Delay();
}
return 0; // 最终失败
}
总线恢复机制处理SCL/SDA均被外部设备拉低的死锁状态:
void SoftI2C_BusRecovery(void)
{
SDA_H();
for(uint8_t clk = 0; clk < 9; clk++) {
SCL_H(); Timing_Delay();
SCL_L(); Timing_Delay();
}
SoftI2C_Start(); // 发送起始条件测试
SoftI2C_Stop();
}
移植注意事项
更换外部晶振后需重新验证延时参数。系统时钟变更影响空循环执行时间,建议采用SysTick或定时器实现与主频无关的精确延时。不同优化等级(-O0至-O3)显著改变代码执行速度,关键时序代码应添加volatile修饰或禁用优化。
调试手段与问题定位
逻辑分析仪捕获波形对照时序图是最有效的调试方法。常见故障模式包括:ACK检测失效(SDA方向未切换)、数据错位(位序错误)、写入失败(未等待器件Ready)。建议在关键节点添加测试点,用示波器测量实际信号质量,检查上升沿是否有过冲或振铃现象。
