欢迎光临
我们一直在努力

【ARM裸机学习第八天】I2C总线全拆解:从硬件开漏/线与 → 协议时序 → 硬件寄存器 → 裸机代码验证

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、硬件底层:为什么I2C必须用开漏输出?
    • 1.1 推挽输出 vs 开漏输出
    • 1.2 线与特性(I2C多设备挂载的核心)
    • 1.3 天然支持电平转换
  • 二、I2C协议时序:每一段波形都有意义
    • 2.1 总线空闲状态
    • 2.2 Start 起始信号
    • 2.3 数据传输核心规则(必考)
    • 2.4 ACK应答位(SDA上升沿疑问解答)
    • 2.5 Stop 停止信号
    • 2.6 完整写操作数据帧结构
  • 三、I2C主机写完整流程(流程图+代码对应)
    • 3.1 初始化:i2c_init()
    • 3.2 传输前置准备
    • 3.3 产生Start起始信号
    • 3.4 发送从机地址+读写位
    • 3.5 等待传输完成与应答:wait_i2c_iif()
    • 3.6 发送从机内部寄存器地址
    • 3.7 循环发送用户数据
    • 3.8 产生Stop信号,等待总线空闲
  • 四、i.MX6ULL硬件I2C核心寄存器汇总
  • 五、高频疑问整理(今天踩过的坑)
    • 1. 为什么判断IIF不能直接写 base->I2SR == 0 ?
    • 2. I2DR到底是发地址还是发数据?
    • 3. 从机为什么还有内部寄存器地址?
    • 4. ipg_clk和I2C是什么关系?
  • 六、I2C的典型应用场景
  • 学习总结

前言

打卡ARM裸机学习第八天,今天核心攻克I2C通信协议。从最底层的GPIO输出模式、线与特性,到I2C标准时序规则,再到i.MX6ULL硬件I2C寄存器配置,最后结合裸机代码和逻辑分析仪波形验证。


一、硬件底层:为什么I2C必须用开漏输出?

这张图从上到下完整讲透了I2C的物理基础:线与特性 → 上拉电阻电平转换 → GPIO内部推挽/开漏结构,是理解I2C的前提。

1.1 推挽输出 vs 开漏输出

GPIO引脚内部由两个MOS管控制输出:上管mos1接3.3V电源,下管mos2接GND地。

  • 推挽输出模式 两个MOS管交替导通,一个导通另一个必然截止:
    • 输出1:mos1导通、mos2截止 → 主动输出高电平3.3V
    • 输出0:mos1截止、mos2导通 → 主动拉低到GND ❌ 致命问题:如果两个芯片的推挽引脚直接相连,一个输出高、一个输出低,会形成电源到地的直接短路,烧毁芯片。因此I2C总线绝对不能使用推挽模式。
  • 开漏输出模式(I2C引脚必须配置) 上管mos1永久截止,只控制下管mos2,只有两种合法状态:
    • 输出0:mos2导通 → 主动把引脚拉到GND低电平
    • 输出1:mos2截止 → 引脚进入高阻悬空状态,芯片自己不输出任何电平 ✅ 必要条件:总线外部必须接 4.7k~10k 上拉电阻,高阻态时由上拉电阻把总线拉成高电平。

1.2 线与特性(I2C多设备挂载的核心)

图中的真值表就是开漏总线的线与逻辑:

总线上任意一个设备把引脚拉低,实际总线电平就是低;只有所有设备都释放引脚(高阻),总线才会被上拉电阻拉成高电平。

这个特性带来两个核心价值:

  • 支持多个设备并联在同一根SDA/SCL总线上,不会出现驱动冲突;
  • 支持双向通信:应答位时从机可以拉低SDA,向主机回复应答信号。
  • 1.3 天然支持电平转换

    图中3.3V芯片与5V芯片互联的结构,就是开漏的另一大优势: 开漏引脚只能拉低,高电平由外部上拉电阻的供电决定。因此3.3V的主控可以直接和5V的I2C器件通信,只需要把上拉电阻接到5V即可,不需要额外的电平转换芯片。


    二、I2C协议时序:每一段波形都有意义

    I2C只有两根信号线:SCL(时钟线,主机提供通信节拍)、SDA(数据线,半双工双向传输)。所有通信都严格遵循时钟规则。

    2.1 总线空闲状态

    SCL和SDA都保持高电平,总线上所有设备都释放引脚,由上拉电阻拉高。

    2.2 Start 起始信号

    • 规则:SCL为高电平时,SDA产生下降沿(高电平→低电平)。
    • 代码对应:配置I2CR寄存器的MSTA位从0→1,硬件控制器自动生成Start信号。
    • 意义:宣告一次通信开始,总线上所有从机开始监听数据。

    2.3 数据传输核心规则(必考)

  • MSB优先:一个字节的8位数据,最高位先发送,最低位最后发送。
  • SCL低电平期间:发送方可以修改SDA的电平,接收方禁止采样读取(数据不稳定)。
  • SCL高电平期间:SDA必须保持稳定,接收方在此时刻采样读取SDA电平。

    一句话记忆:时钟低,改数据;时钟高,读数据。

  • 2.4 ACK应答位(SDA上升沿疑问解答)

    每发送完8位数据,会紧跟第9个时钟周期,就是应答位,用于接收方向发送方反馈接收状态。

    • ACK(应答):接收方把SDA拉低(低电平),表示成功收到数据。
    • NACK(非应答):SDA保持高电平,表示接收失败或拒绝接收。
    今天的核心疑问:为什么ACK结束后SDA会有一个上升沿?

    这是今天最容易困惑的点,本质是总线控制权的交接: 应答位期间,SDA的控制权在从机手里,从机拉低SDA回复ACK。当应答的时钟结束、SCL回到低电平时,从机必须立刻释放SDA(自身下管截止)。此时总线上没有任何设备拉低,外部上拉电阻会把SDA自动拉回高电平——这就是那个上升沿的来源。

    ✅ 关键结论:这个上升沿是总线释放的自然回弹,和下一位数据是0还是1没有关系。如果下一位要发0,主机会在SCL低电平期间,再把SDA从高电平拉下来。

    两种SDA上升沿的本质区别
    上升沿类型发生时机本质意义
    ACK后回弹 SCL低电平期间 从机释放总线,上拉电阻自然回弹 总线控制权交接,无控制含义
    Stop停止信号 SCL高电平期间 主机主动拉高SDA 控制信号,宣告通信结束

    2.5 Stop 停止信号

    • 规则:SCL为高电平时,SDA产生上升沿(低电平→高电平)。
    • 代码对应:MSTA位从1→0,硬件自动生成Stop信号。
    • 意义:宣告本次通信结束,总线回到空闲状态。

    2.6 完整写操作数据帧结构

    【此处插入第二张图左侧的黄色帧结构图】 标准的I2C主机写操作帧结构: Start → 从机地址(7bit) + 写位(0) → ACK → 从机内部寄存器地址 → ACK → 数据字节1 → ACK → 数据字节2 → ACK → … → Stop


    三、I2C主机写完整流程(流程图+代码对应)

    这张流程图和我们的裸机i2c_write函数完全一一对应,每一步都对应硬件寄存器的操作。

    3.1 初始化:i2c_init()

  • 引脚复用:把GPIO引脚切换为I2C功能(SDA/SCL);
  • 电气配置:配置为开漏输出+上拉电阻,匹配I2C物理要求;
  • 关闭I2C模块,配置IFDR分频寄存器(对ipg_clk分频,得到SCL总线波特率);
  • 置位IEN位,使能I2C硬件控制器。
  • ⚠️ 重要前提:clock_init中必须已经打开I2C对应的IPG时钟(CCGR寄存器),否则写所有寄存器都无效。

    3.2 传输前置准备

    对应流程图第一步:

  • 清除仲裁丢失标志IAL、传输完成中断标志IIF;
  • 置位MTX位,设置为主机发送模式。
  • 3.3 产生Start起始信号

    置位MSTA位,硬件自动在总线上输出Start信号。

    3.4 发送从机地址+读写位

    向I2DR数据寄存器写入:(dev_addr << 1) | 0

    • 7位从机地址左移1位,挪到高7位;
    • 最低位填0,代表本次是主机写操作。

    3.5 等待传输完成与应答:wait_i2c_iif()

  • 循环等待IIF标志置1,代表1字节数据+第9位应答全部传输完成;
  • 软件手动向IIF位写0清零(硬件不会自动清除该标志);
  • 判断RXAK位:0=收到从机ACK,通信正常;1=NACK,通信失败。
  • 3.6 发送从机内部寄存器地址

    向I2DR写入reg_addr,告诉从机要操作它内部的哪一个存储单元/寄存器,随后等待应答。

    3.7 循环发送用户数据

    for循环依次把data[i]写入I2DR,每发送一个字节,就调用一次等待应答函数。

    3.8 产生Stop信号,等待总线空闲

  • 清零MSTA位,硬件自动输出Stop停止信号;
  • 循环等待IBB位清零,确认总线真正回到空闲状态。

  • 四、i.MX6ULL硬件I2C核心寄存器汇总

    寄存器关键位功能作用
    I2CR 控制寄存器 IEN(bit7) I2C模块总使能开关
    MSTA(bit5) 0→1产生Start信号;1→0产生Stop信号
    MTX(bit4) 收发模式:1=主机发送,0=主机接收
    TXAK(bit3) 主机接收时,控制回复ACK/NACK
    RSTA(bit2) 产生重复起始信号(读操作用)
    I2SR 状态寄存器 IBB(bit5) 总线忙标志:1=忙碌,0=空闲
    IAL(bit4) 仲裁丢失标志,多主机场景使用
    IIF(bit1) 字节传输完成标志,必须软件手动清零
    RXAK(bit0) 接收应答:0=ACK,1=NACK
    IFDR 分频寄存器 对ipg_clk分频,设置SCL总线波特率
    I2DR 数据寄存器 写入=发送数据;读取=接收数据

    五、高频疑问整理(今天踩过的坑)

    1. 为什么判断IIF不能直接写 base->I2SR == 0?

    I2SR是8位状态寄存器,里面同时有IBB、IAL、IIF、RXAK等多个标志位。我们只关心IIF这1位,必须用& I2SR_IIF做掩码,把其他位全部清0再判断。如果直接判断整个寄存器等于0,其他任意一位为1都会导致判断错误。

    2. I2DR到底是发地址还是发数据?

    I2DR就是单纯的数据寄存器,你写入什么内容,硬件就发送什么内容。第一次写入是设备地址,第二次写入是寄存器地址,后面循环写入是用户数据——功能由写入的内容决定,寄存器本身不区分类型。

    3. 从机为什么还有内部寄存器地址?

    I2C从设备不是单一功能的黑盒。比如AT24C02 EEPROM有256个存储单元,MPU6050陀螺仪有配置寄存器、加速度寄存器、陀螺仪寄存器。主机必须先告诉从机“操作内部第几个单元”,才能正确读写对应的数据。

    4. ipg_clk和I2C是什么关系?

    ipg_clk是外设总线时钟,给I2C控制器的寄存器读写提供时钟;I2C总线的SCL通信时钟,是ipg_clk经过IFDR寄存器分频之后得到的。


    六、I2C的典型应用场景

    I2C适合板内短距离、低速、多外设的场景,优势是只占用2个引脚,非常节省IO资源:

    • 传感器类:MPU6050六轴、SHT30温湿度、BH1750光照传感器
    • 存储类:AT24C系列EEPROM,掉电保存小容量参数
    • 显示类:0.96寸OLED液晶屏
    • 外设扩展:PCA9685等IO扩展芯片
    • 实时时钟:DS3231等RTC时钟芯片

    学习总结

    今天从物理层的开漏、线与特性,到协议层的时序规则、应答机制,再到硬件寄存器配置和裸机代码实现,把I2C的完整体系串了起来。最容易混淆的ACK后SDA上升沿,本质是总线控制权交接的上拉回弹,和数据位无关。结合逻辑分析仪的波形和代码一步步对应,就能彻底理解I2C的工作流程。 明天继续攻克I2C主机读操作和重复起始信号。

    赞(0)
    未经允许不得转载:171主机测评 » 【ARM裸机学习第八天】I2C总线全拆解:从硬件开漏/线与 → 协议时序 → 硬件寄存器 → 裸机代码验证
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址