当前位置:首页 > 随笔 > 正文内容

STM32F407软件模拟IIC驱动AT24C02:时序拆解与工程实践

访客 随笔 2026年7月1日 1

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)。建议在关键节点添加测试点,用示波器测量实际信号质量,检查上升沿是否有过冲或振铃现象。

标签: STM32F4AT24C02

相关文章

可以按小时收费的VPS

很多 VPS 提供商都支持 按小时计费(hourly billing),想短期试用 / 临时搭建节点、测试网络、短期项目等场景非常合适。下面是当前最主流且靠谱的按小时 VPS 选项,分别按不同需求场景整理: 1. Vultr(全球节点,包括日本) 按小时计费 可选机房:东京 / 大阪 / 洛杉矶 / 法兰克福 / 伦敦 … 支持 PayPal(部分情况),但更常用信用卡/PayPal+卡价格参考$...

在 iPhone 上下载国外App

地区/国家限制App Store 会根据 Apple ID 的国家或地区限制应用下载。如果你的 Apple ID 绑定的是中国大陆,就可能无法下载 OpenAI 官方的 ChatGPT 应用,因为它在大陆 App Store 不上架。解决办法:换成美国、加拿大、香港等地区的 Apple ID。或者在现有 Apple ID 上更改地区。注册一个国外 Apple ID(推荐)比如注册 美国区 Appl...

Node.js 中的异步编程:回调与 Promise

Node.js 是一个基于 JavaScript 构建的单线程、非阻塞运行环境,它通过异步编程机制来高效处理多个操作。在执行如文件读取、API 请求或数据库查询等任务时,Node.js 不会等待这些操作完成,而是使用回调函数和 Promise 来避免阻塞主线程。 回调方式实现异步 那么当异步操作完成后,Node.js 如何知道接下来要做什么呢?这就要用到 回调函数(callback)。 回调本质上...

Selenium自动化测试入门指南

Selenium自动化测试入门指南

什么是自动化测试? 自动化测试是指利用软件工具自动执行测试用例,模拟用户操作,如打开网页、点击链接、输入文本等,并验证结果是否符合预期。 其主要优点包括: 大幅减少人工成本 测试速度快 可以在非工作时间运行 支持持续集成和交付 然而,它也存在一些局限性,例如开发成本较高、不适合快速变化的项目、依赖稳定的UI界面等。 自动化测试的应用条件 适合引入自动化测试的情况包括: 手动测试耗时且需要大量...

MariaDB Galera集群故障快速恢复指南

OpenStack控制节点采用三节点MariaDB Galera集群架构。当数据库集群因故障重启时,有时会出现Galera集群无法正常启动的问题。虽然有多种方法可以恢复数据库服务,但如何实现快速启动同时确保数据完整性呢? 通过分析日志发现,MariaDB Galera集群节点宕机时会在日志中输出以下信息: [Note] WSREP: 新集群视图:全局状态: 874d8e7e-5980-11e8-8...

Android 中 EventBus 的通信机制与实现原理深度解析

EventBus 核心设计思想 EventBus 是一个基于观察者模式的事件总线框架,广泛应用于 Android 平台以实现组件解耦。它通过中心化的消息分发机制,使不同层级、不同线程的对象能够以"发布-订阅"方式通信,避免了传统接口回调或广播带来的强依赖问题。 核心角色说明 事件(Event):任意 Java 对象,作为数据载体,如网络状态变更通知、用户登录信息等。 发布者(Publi...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。