简介
I2C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线
两根通信线:SCL(Serial Clock)、SDA(Serial Data)
同步,半双工
带数据应答
支持总线挂载多设备(一主多从、多主多从)
I2C 通信
基本定义:I2C 总线(Inter IC BUS )是飞利浦(Philips)开发的通用数据总线 ,用于设备间(比如单片机与传感器、存储芯片等)短距离、低速的数据传输,让不同 IC(集成电路)能简单通信。
通信线:
SCL(Serial Clock):串行时钟线,主设备通过它输出时钟信号,同步总线上数据传输节奏,类似 “指挥家” 定节拍。
SDA(Serial Data):串行数据线,用于传输实际数据(如传感器采集值、指令等 ),是设备间 “对话内容” 通道。
通信特性:
同步:数据收发靠 SCL 时钟信号同步,收发双方按统一节奏(时钟节拍)操作,避免数据混乱。
半双工:同一时刻,总线只能单向传数据(主发从收,或从发主收 ),不能同时双向,像 “单行道”,但可切换方向。
带数据应答:数据传输时,接收方收到数据后会回传应答信号,告诉发送方 “收到了 / 没收到”,保障通信可靠,类似 “收到请回复”。
硬件电路
所有I2C设备的SCL连在一起,SDA连在一起
设备的SCL和SDA均要配置成开漏输出模式
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右

一主多从 SCL SDA
外部信号输入引脚可通过数据缓冲器或施密特触发器进行电平采集。由于输入仅对信号进行采样,不会对外电路产生驱动作用,因此设备在任意时刻均可执行输入操作。
而在输出端,I²C 总线采用开漏输出模式。常规推挽输出结构包含上、下两个 MOS 管:上管导通时输出强高电平,下管导通时输出强低电平,可实现强上拉与强下拉的全驱动能力。
开漏输出则移除了上侧 PMOS 管,仅保留下拉 NMOS 管:
- 输出低电平时,下拉管导通,引脚呈现强下拉特性;
- 输出高电平时,下拉管关断,引脚无内部驱动,处于高阻浮空状态。
因此,开漏结构的引脚只能主动输出低电平,无法主动输出高电平。为使总线在无器件驱动时保持稳定高电平,需在 SCL 与 SDA 总线上外部配置上拉电阻,以实现弱上拉,从而完成高电平状态的维持。
采用开漏输出外加外部上拉电阻的结构,主要具有以下优势:
-
彻底避免总线短路,提高硬件可靠性传统推挽输出若多个设备同时驱动高低电平,极易出现电源与地直接导通的短路情况。而开漏结构仅能主动拉低电平,无法主动输出高电平,从硬件机制上杜绝了短路风险,保证总线安全稳定。
-
无需频繁切换引脚输入输出模式设备在准备接收数据前,只需输出高电平(即关闭输出管,使引脚呈现高阻态),即可等效为输入模式,不必额外配置引脚方向,简化了软件时序设计。
-
天然具备 “线与” 逻辑特性总线上只要任意一个设备输出低电平,总线电平就会被拉低;只有当所有设备均释放总线(呈高阻态),总线才会被上拉电阻拉高为高电平。这种线与特性是 I²C 实现多主机通信的基础,可用于多主机时钟同步与总线仲裁。
因此,即使在单主机系统中 SCL 可以使用推挽输出,I²C 仍统一采用开漏 + 上拉的结构,以兼容多主机场景下的时钟同步与仲裁机制。

I²C 协议规定:
- 当 SCL = 高 时,SDA 上的数据必须保持稳定。
- 唯一例外是 起始条件(Start) 和 停止条件(Stop)。
- 在数据传输和应答期间,SCL 高时不能改变 SDA!
- 从机在 SCL 上升沿或高电平期间采样 SDA 上的数据
- I²C 数据接收规则:主机在 SCL 低电平时,从机可以准备数据;在 SCL 高电平时,主机读取数据。
注:
|
MPU6050_IIC_IO_Init() |
初始化引脚电平 |
❌ 不在通信 |
❌ 不受时序限制 |
未在通信时规定无效
I2C软件
在 I²C 通信中,核心在于严格的时序设计。我们首先学习 I²C 协议定义的基本时序单元。
一、起始条件(Start Condition)
起始条件定义为:SCL 保持高电平时,SDA 由高电平变为低电平,即产生一个下降沿。
总线空闲状态下,SCL 与 SDA 均由外部上拉电阻置为高电平,总线处于稳定的空闲状态。当主机需要发起通信时,需主动打破总线空闲状态,产生起始条件:在 SCL 为高期间,主机将 SDA 拉低,形成下降沿。从机检测到该时序后,会识别为通信开始,并准备接收主机指令。
为便于后续时序拼接,协议约定:除起始与终止条件外,其余时序单元均以 SCL 低电平开始、以 SCL 低电平结束,保证时序可以连续衔接。
二、终止条件(Stop Condition)
终止条件定义为:SCL 保持高电平时,SDA 由低电平变为高电平,即产生一个上升沿。
操作顺序为:先释放 SCL 使其回到高电平,再释放 SDA 使其回到高电平。该上升沿标志一次通信结束,时序完成后 SCL 与 SDA 均恢复高电平,总线回到空闲状态。
起始条件与终止条件类似于 UART 中的起始位与停止位,一帧完整的 I²C 通信以起始条件开始,以终止条件结束,且二者均由主机产生。在单主机模式下,通信过程中从机不得主动控制总线,否则会引发总线冲突;多主机系统则需额外的仲裁机制。
三、字节发送时序
起始条件产生后,即可进入发送一个字节的时序。
时序规则如下:
- SCL 低电平期间,主机将数据位按高位先行的方式依次放到 SDA 线上;
- 随后释放 SCL,使其变为高电平;
- 从机在 SCL 高电平期间进行数据采样;
- 因此在 SCL 高电平期间,SDA 必须保持稳定,不允许发生电平变化。
重复以上过程 8 次,即可完成一个字节的数据传输。
补充:
接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
应答机制:
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答 接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答, 数据1表示非应答(主机在接收之前,需要释放SDA)
相当于上面的发送一位或接收一位
8位地址:
7+1:7个地址位+一个读写位
发送与读入顺序

核心规则
位移方向与传输顺序的匹配
在 I2C 通信中,无论传输的是地址还是数据字节,位传输顺序始终是 MSB 优先(即最高位 D7 先发送,最低位 D0 最后发送)。要实现这一点,位操作必须满足:
- 发送最高位(D7):需通过 (data >> 7) & 1 获取 D7 的值。
- 发送次高位(D6):需通过 (data >> 6) & 1 获取 D6 的值。
- 以此类推,直到发送最低位(D0):(data >> 0) & 1。
关键逻辑:通过 右移操作(>>)+ 掩码(& 1) 逐位提取数据,且右移次数与位序号(D7~D0)一一对应。
I2C 读操作
核心场景:主机(如单片机)要从从机(如 MPU6050)的寄存器读取数据。
流程拆解:
起始信号 → 发送从机地址(写模式,读写位 = 0) → 从机应答 → 发送寄存器地址 → 从机应答 → 重复起始 → 发送从机地址(读模式,读写位 = 1) → 从机应答 → 读取数据 → 主机发送非应答 → 停止信号
关键逻辑:
- 先通过 “写模式” 告诉从机 “要读哪个寄存器”(发送寄存器地址),再通过 “重复起始 + 读模式” 切换为 “从该寄存器读数据”。
- 重复起始(Restart)是为了不释放总线,直接切换读写方向,避免其他设备干扰。
I2C 写操作
核心场景:主机要往从机的寄存器写入数据(如配置 MPU6050 的采样率、使能陀螺仪等)。
流程简化:
起始信号 → 发送从机地址(写模式,读写位 = 0) → 从机应答 → 发送寄存器地址 → 从机应答 → 发送要写入的数据 → 从机应答 → 停止信号
区别点:
- 无需 “重复起始” 和 “读模式切换”,因为全程是 “写操作”:先写寄存器地址,再写数据。
- 最后直接发停止信号结束通信,不需要 “非应答”(因为主机是写数据方,从机只需确认收到即可)。
3. 总结:流程的通用性
- 读操作:必须用「先写寄存器地址 → 重复起始 → 读数据」的流程(你贴的就是读流程)。
- 写操作:流程更简单,全程写模式,无需重复起始。
IIC时序:

这张图展示了 I2C 总线的“指定地址读”时序。(基本单元在上面)
这里以MPU6050 读寄存器流程解释
/**
* @brief MPU6050 读取一个字节的数据
* @param RegAddress: 想要读取的寄存器地址 (对应步骤 ④)
* @retval 读取到的 8 位数据 (对应步骤 ⑨)
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
// --- 第一阶段:伪写操作(告诉从机要读哪个寄存器) ---
MyI2C_Start(); // ① 起始信号 (S)
// ② 发送从机地址 + 写位 (0x68 << 1 | 0 = 0xD0)
MyI2C_SendByte(MPU6050_ADDRESS);
MyI2C_ReceiveAck(); // ③ 从机应答 (RA:0)
// ④ 发送寄存器地址
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck(); // ⑤ 从机应答 (RA:0)
// --- 第二阶段:正式读操作 ---
MyI2C_Start(); // ⑥ 重复起始信号 (Sr)
// ⑦ 发送从机地址 + 读位 (0x68 << 1 | 1 = 0xD1)
MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
MyI2C_ReceiveAck(); // ⑧ 从机应答 (RA:0)
// ⑨ 从总线读取数据
Data = MyI2C_ReceiveByte();
// ⑩ 主机发送非应答 (NACK),告诉从机停止发送
MyI2C_SendAck(1); // SA:1 (1表示NACK)
// ⑪ 停止信号 (P)
MyI2C_Stop();
return Data;
}
结合波形图和标注,逐段拆解如下:
1. 关键标注与波形对应
图中用 `SCL`(时钟线,黄色)和 `SDA`(数据线,蓝色)的电平变化,描述了一次完整的 **“从指定从机(Slave Address)的指定寄存器(Reg Address)读数据(Data)”** 过程。
标注的核心阶段:
- `S`:起始信号(Start)
- `Sr`:重复起始信号(Restart)
- `P`:停止信号(Stop)
- `RA:0`:从机应答(Acknowledge,低电平表示应答)
2. 时序流程拆解(对应代码步骤)
① 起始信号(`S`)
-波形:`SCL` 高电平时,`SDA` 从高→低跳变。
作用:告诉从机“开始通信”,对应代码 `MyI2C_Start()`。
② 发送从机地址(写模式)
波形标注:`Send Byte: 0x10 (Slave Address + R/W)`
实际是 7 位从机地址 + 1 位读写位**(I2C 地址共 8 位)。
这里 `R/W = 0`(最后一位是 0),表示“写操作”(先告诉从机:我要写寄存器地址)。
对应代码:`MyI2C_SendByte(MPU6050_ADDRESS)`(`MPU6050_ADDRESS` 是 7 位地址,代码里会自动带读写位 0)。
③ 从机应答(`RA:0`)
波形:`SCL` 第 9 个脉冲时,`SDA` 被从机拉低(低电平 = 应答)。
作用:从机告诉主机“地址收到了”,对应代码 `MyI2C_ReceiveAck()`。
④ 发送寄存器地址(`Reg Address`)
波形标注:`Send Byte: 0x19 (Reg Address)`
主机给从机发 “要读取的寄存器地址”(比如 MPU6050 的加速度数据寄存器、ID 寄存器等)。
对应代码:`MyI2C_SendByte(RegAddress)`。
⑤ 从机应答(`RA:0`)
波形:`SCL` 第 17 个脉冲时,`SDA` 再次被从机拉低(应答)。
作用:从机确认“寄存器地址收到”,准备后续读数据,对应代码 `MyI2C_ReceiveAck()`。
⑥ 重复起始信号(`Sr`)
波形:`SCL` 高电平时,`SDA` 再次从高→低跳变(但不释放总线)。
作用:不中断通信,直接切换为“读模式”,对应代码 `MyI2C_Start()`(重复起始)。
⑦ 发送从机地址(读模式)
波形标注:`Send Byte: 0x11 (Slave Address + R/W)`
同样是 7 位从机地址,但最后一位 `R/W = 1`(读模式),告诉从机“现在要读数据”。(例如图中的0xD0)
对应代码:`MyI2C_SendByte(MPU6050_ADDRESS | 0x01)`(`| 0x01` 就是把读写位设为 1)。
⑧ 从机应答(`RA:0`)
波形:`SCL` 第 25 个脉冲时,`SDA` 被从机拉低(应答)。
作用:从机确认“读模式地址收到”,准备发数据,对应代码 `MyI2C_ReceiveAck()`。
⑨ 读取数据(`Data`)
波形标注:`Receive Byte: 0xAA (Data)`
从机把“指定寄存器”的数据通过 `SDA` 发回主机,主机读取这一字节。
对应代码:`Data = MyI2C_ReceiveByte()`。
⑩ 主机发送“非应答”(`SA:1`)
波形:`SCL` 第 33 个脉冲时,`SDA` 保持高电平(非应答)。
作用:主机告诉从机“数据读完了,别再发了”,对应代码 `MyI2C_SendAck(1)`(`1` 表示非应答)。
⑪ 停止信号(`P`)
波形:`SCL` 高电平时,`SDA` 从低→高跳变。
作用:结束通信,对应代码 `MyI2C_Stop()`。
简单说:
先通过写模式告诉从机“要读哪个寄存器”,再通过重复起始 + 读模式读取该寄存器的数据。
-波形里的 `S`/`Sr`/`P`、应答/非应答,都是 I2C 协议的标准交互,确保主机和从机“收发数据不出错”。
理解这张图后,你就能更清晰地对应代码逻辑和实际总线时序,调试 I2C 通信问题(比如时序不对、应答失败)时也会更顺手 。
硬件IIC
I2C外设简介
硬件I2C,也就是stm32内部的I2C外设(可实现串口)
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担 - 支持多主机模型 - 支持7位/10位地址模式 - 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz) - 支持DMA - 兼容SMBus协议 - STM32F103C8T6 硬件I2C资源:I2C1、I2C2 软件呢,只需要写入控制寄存器CR和数据寄存器DR
硬件I2C的“心脏”:架构细节
在硬件 I2C 内部,只有控制注册(CR)和数据注册(DR),其核心是**“双注册”机制**:
-
数据注册 (DR):这是你代码直接操作的对象。
-
移位寄存器(Shift Register):这是真正连接到
SDA线上的。-
发送数据时:数据从DR搬到移位寄存器,然后一侧地“挪”出去。
-
接收数据时:升降架一位地收,攒够8位后,整体“搬运”到DR供你读取。
-
-
这种设计的意义是:当升降机仓库还在苦哈哈地发数据时,CPU就可以提前把下一个字节写进DR候着,实现了连续传输,提高了效率。
状态监控:状态注册 (SR1 & SR2)
一起是硬件自动执行,你就得知道它“进行到哪一步了”。STM32提供了两个状态注册:
-
SR1:主要记录通讯事件(比如:SB 起始位发送、ADDR 地址已发送、TXE 发送注册空、RXNE 接收已注册非空)。
-
SR2:记录主要总线状态(如:MSL 主机模式、BUSY 总线忙、TRA 发送/接收状态)。
-
代码逻辑:你会看到很多
while(I2C_CheckEvent(...))语句,这就是在不断查询硬件跑到了第一个中断。
通讯速度的“幕后推手”:时钟控制寄存器(CCR)
在软件 I2C 中,我们Delay用于控制频率。在硬件 I2C 中,你需要配置CCR 注册。
-
它会根据你输入的PCLK1频率,自动计算产生100kHz或400kHz所需的高电平持续时间。
-
快速模式(Fast Mode)下,它还支持配置占空比(Duty Cycle),让时钟信号在高速下依然稳定。
补充:I2C的“优点”对比
| 特性 | 软件模拟 I2C (Bit-Banging) | 硬件 I2C 外部设置 |
| CPU占用 | 高(跳跃引脚、延迟等待) | 低(硬件自动处理,支持DMA) |
| 移植性 | 极强(只要有GPIO就可以跑) | 光源(不同芯片输入不同) |
| 灵活 | 高(引脚便捷选,相互随心控) | 限制多(必须使用特定的AFIO引脚) |
| 稳定性 | 一般(易受中断影响范围) | 高(硬件逻辑电路严格控制) |
开发建议(避坑指南)
虽然I2C硬件看起来很漂亮,但是STM32F1系列的I2C硬件有一个著名的“槽点”:
死锁风险:在某些极限干扰下,硬件 I2C 可能会陷入休眠等待(BUSY 位置 1 后不复位)。
建议方案:
-
初学者:建议先用软件模拟I2C跑通逻辑。因为软件模拟看得见摸得着,哪里卡住了debug一下就知道了。
-
工程实践:如果数据量巨大(比如读取高频传感器),请务必打开DMA(直接存储器访问)。这样CPU要发一个“开始”指令,几十字节的数据就会自动排队进内存,CPU甚至可以去睡觉。
补充说明:
本文是学习笔记,素材来源于B站江协科技
1432

被折叠的 条评论
为什么被折叠?



