江科大STM32入门:I2C通信协议详解(软件模拟篇)| 从时序到数据帧一次讲透 我的STM32学习专栏[点击直达 | 持续更新中]适用平台STM32F103C8T6标准库 / 寄存器版本均适用本文先讲通用协议时序 前置文章江科大STM32入门GPIO的8种工作模式详解前言如果你也跟我一样第一次翻开《STM32F10xxx参考手册》看到 I2C 章节时被满屏的时序图、寄存器位、仲裁逻辑直接劝退那这篇文章就是为你写的。这篇文章我会完全抛开代码和库函数只从协议时序的角度带你把 I2C 的硬件电路、基本时序单元、三种完整数据帧从头到尾捋一遍。读完之后你不仅能看懂逻辑分析仪抓出来的波形还能自己在纸上画出指定地址读的完整时序图——这才是真正理解 I2C 的标志。⚠️阅读提示本文聚焦 I2C 通用协议与软件模拟时序逻辑不涉及 STM32 硬件 I2C 外设I2C1/I2C2的寄存器配置。如果你正在学江科大 STM32 入门教程的软件模拟 I2C章节那么这篇文章就是为你量身定制的笔记。一、I2C到底是什么从两根线说起刚接触通信协议的时候我总是把USART、I2C、SPI搞混。江科大老师在视频里打了个比方我觉得特别形象I2C就像老师在课堂上点名只有被点到名的学生才能说话老师掌控着整个课堂的节奏。1.1 I2C总线的基本概念I2CInter-IC Bus集成电路总线是一种同步、半双工、带数据应答机制的通用数据总线。它最大的特点就是只用两根通信线就能让单片机和多个外设聊天引脚全称作用我的理解SCLSerial Clock串行时钟线老师手中的节拍器统一全班的节奏SDASerial Data串行数据线传递消息的话筒老师和学生共用同步所有设备共享同一根时钟线SCL收发双方靠时钟边沿对齐不用担心波特率误差累积。半双工SDA只有一根线同一时刻只能单向传输要么主机说从机听要么从机说主机听。带数据应答每传完一个字节接收方必须回复一个ACK应答位告诉发送方我收到了这比USART的盲发靠谱多了。1.2 一主多从 vs 多主多从I2C支持总线挂载多个设备分两种模式一主多从单片机固定当老师主机其他模块MPU6050、AT24C02、DS3231等全是学生从机。从机不允许主动发言只有被老师点名后才能控制SDA线。这是STM32入门中最常用的模式。多主多从总线上任何模块都可以跳出来当主机I2C协议会通过仲裁机制决定谁当老大。这个比较复杂入门阶段先不用深究。【I2C常见模块】图注左侧为MPU60507位地址1101000、中间为AT24C02存储器模块7位地址1010000、右侧为DS3231实时时钟模块。所有从机都有唯一的7位设备地址主机通过地址来点名。二、硬件电路为什么必须接上拉电阻这是我当时最困惑的地方SCL和SDA明明接单片机的GPIO为啥还要外接两个电阻拉到VCC2.1 开漏输出与线与现象I2C协议规定所有设备的SCL和SDA引脚必须配置为开漏输出模式Open-Drain。我的理解开漏输出就像一个个只能往下拉的手。当设备输出低电平时内部的MOS管导通把总线强拉到GND当设备输出高电平时MOS管断开引脚处于浮空状态总线电平不确定。如果所有设备都同时松手输出高电平总线就飘在空中电平是悬空的这不行。所以必须外接上拉电阻一般4.7kΩ左右把总线默认拉到高电平。这就产生了一个关键特性——线与Wired-AND只要总线上有一个设备输出低电平拉低总线就被钳位在低电平只有当所有设备都输出高电平松手时总线才被上拉电阻拉到高电平。这个特性很重要它让I2C总线天然支持多设备共享你想发0就直接拉低想发1就松手让电阻拉高不用担心两个设备同时输出高/低电平短路。2.2 主机对总线的控制权SCL时钟线任何时候都是主机完全掌控。从机没有资格碰SCL这是为了保证全班的节拍统一。SDA数据线空闲时刻由主机控制只有在从机发送数据和从机应答这两个特殊时段主机才会把SDA控制权交还给从机。【硬件电路图与开漏输出结构】图注I2C一主多从硬件连接示意图。所有设备的SCL、SDA分别并联各通过一个4.7kΩ上拉电阻接VCC。右侧为开漏输出结构输出低电平时开关闭合输出高电平时开关断开呈浮空输入状态。三、I2C时序基本单元协议的字母表如果把I2C通信比作说话那么起始条件、终止条件、发送字节、接收字节、应答位就是组成语言的字母。下面逐个拆解。3.1 起始条件START和终止条件STOP这是每次通信的发令枪和结束哨。起始条件SCL高电平期间SDA从高电平切换为低电平一个下降沿。终止条件SCL高电平期间SDA从低电平切换为高电平一个上升沿。我的理解老师要上课了先敲一下铃起始全班安静听令下课了再敲一下铃终止大家解散。⚠️关键细节起始和终止条件只能由主机产生从机没有资格发起这是主机的特权。【起始条件与终止条件时序图】图注左侧为起始条件时序SCL保持高电平SDA产生下降沿。右侧为终止条件时序SCL保持高电平SDA产生上升沿。虚线框标注了保证基本单元拼接时的电平稳定时间。3.2 发送一个字节时序流程如下高位先行MSB FirstSCL低电平期间主机把数据位放到SDA上。想发0就拉低SDA想发1就释放SDA由上拉电阻拉高。释放SCL拉高从机在SCL高电平期间读取SDA上的数据位。SCL高电平期间SDA上的数据必须保持稳定不允许变化这是为了保证从机有足够时间采样。重复上述过程8次就发送完一个字节。我的理解老师主机在节拍器响之前SCL低电平把答案写在黑板上放数据节拍器响的时候SCL高电平学生从机抬头看黑板读数据。节拍器响的时候老师不能擦黑板改答案否则学生就抄错了。【发送一个字节时序图】图注展示了发送一个字节的完整时序。SCL低电平期间主机设置SDAB7~B0SCL高电平期间从机读取。注意在上升沿时刻从机即完成采样。3.3 接收一个字节接收是发送的逆过程此时SDA控制权交给从机主机释放SDA拉高相当于把话筒递给从机。SCL低电平期间从机把数据位放到SDA上。SCL高电平期间主机读取SDA上的数据位。重复8次接收一个字节。发送 vs 接收的区别这个我当时记了很久阶段SCL低电平SCL高电平SDA控制权发送字节主机放数据从机读数据主机接收字节从机放数据主机读数据从机【接收一个字节时序图】图注上方为接收一个字节时序。虚线代表从机控制的电平实线代表主机控制的电平。主机接收前必须释放SDA将数据线控制权交给从机。3.4 发送应答与接收应答每传完一个字节必须跟一个应答位ACK/NACK这是I2C可靠性的核心。发送应答主机接收完一个字节后在下一个时钟发送1位数据给从机。SDA0ACK表示我收到了继续发SDA1NACK表示我没收到/不要发了接收应答主机发送完一个字节后释放SDA在下一个时钟读取从机的应答。SDA0从机ACK表示我收到了SDA1从机NACK表示我没收到/忙我的理解这就像老师讲完一句话会问学生听懂了吗。学生点点头ACK0就继续讲学生摇摇头NACK1就停下来查原因。【发送应答与接收应答时序图】图注下方为应答位时序。左侧为发送应答主机发给从机查看是否继续发右侧为接收应答从机发给主机查看是否接收成功。应答位发生在第9个时钟周期紧跟在8位数据之后。四、I2C完整数据帧把字母拼成单词掌握了基本单元接下来就要把它们组合成完整的句子——数据帧。I2C通信中最常用的有三种帧格式。4.1 指定地址写向从机的指定寄存器写入数据这是最常见的操作比如配置MPU6050的某个寄存器。时序流程如下起始条件主机发起发送从机地址 写标志位07位地址左移1位最低位补0。例如MPU6050地址1101000发送字节为110100000xD0。接收从机应答RA从机回复ACK表示我在线。发送寄存器地址Reg Address告诉从机我要操作你内部的哪个寄存器。接收从机应答。发送要写入的数据。接收从机应答。终止条件。我的理解这就像是老师点名张三从机地址我要在你的笔记本第19页0x19寄存器地址写个AA数据0xAA。【指定地址写时序图】图注指定地址写完整时序。示例向地址为11010000xD0写方向的MPU6050内部0x19寄存器写入数据0xAA。时序依次为S起始、从机地址写、RA应答、寄存器地址0x19、RA应答、数据0xAA、RA应答、P停止。4.2 当前地址读读取从机指针指向的数据这个稍微抽象一点。在从机内部所有寄存器被分配到了一个线性区域里从机维护着一个地址指针变量默认指向某个寄存器。每次写入操作后这个指针会自动自增指向下一个寄存器。当前地址读就是不指定寄存器地址直接读取从机当前指针指向位置的数据。时序流程起始条件发送从机地址 读标志位1最低位补1如0xD1。接收从机应答接收一个字节数据此时SDA控制权交给从机主机发送非应答NACK1或应答ACK0如果还要读下一个就发ACK不读就发NACK。终止条件【当前地址读时序图】图注当前地址读时序。最后1位为1代表读操作。从机收到读命令后将当前地址指针指向的寄存器内容放到SDA上。注意传输方向反转时SDA控制权从主机交给从机。4.3 指定地址读先指位置再读数据重点这是最复杂也最常用的帧格式比如读取MPU6050的X轴加速度高8位。问题I2C协议没有直接指定地址读的单帧命令怎么办解决方案把指定地址写和当前地址读拼接起来中间用重复起始条件Sr连接时序流程起始条件S发送从机地址 写0先假装要写入目的是设置从机的内部地址指针。接收应答发送要读取的寄存器地址如0x19此时从机的地址指针就指向了0x19。接收应答重复起始条件Sr不发送停止位直接重新发一个起始条件。相当于告诉总线我还没说完换个话题继续发送从机地址 读1这次是真的要读。接收应答接收数据字节从机把0x19寄存器的内容发给主机主机发送NACK表示我读完了终止条件P我的理解这就像你跟朋友说你笔记本翻到第19页指定地址写只翻页不写字然后读给我听重复起始当前地址读。 翻页和朗读是两个动作但中间不需要挂电话不发停止位直接说等等再读一下Sr。【指定地址读时序图】图注指定地址读完整时序。前半段为指定地址写发送0xD0、寄存器地址0x19中间用Sr重复起始条件衔接后半段为当前地址读发送0xD1、读取数据。这是I2C协议中最核心的复合帧格式。五、个人理解 / 学习心得关于地址指针的理解一开始我不明白为什么当前地址读能连续读多个寄存器。后来想通了从机内部就是一个数组指针每次自增。就像你读一本书读完第1页书签自动夹到第2页下次直接读就是第2页的内容。所以连续读取多个字节时只需要发一次地址然后循环接收发ACK最后发NACKStop即可。关于重复起始条件Sr我一开始疑惑为什么指定地址读中间不能发Stop发了Stop再Start不行吗答案是发了Stop总线就空闲了其他设备可能抢占总线或者从机复位了内部指针你就白设置了。Sr的作用就是在不释放总线所有权的前提下切换通信方向。六、踩坑记录踩坑现象可能原因解决办法从机始终不回复ACK从机地址写错注意是7位地址左移1位后的8位值核对数据手册如MPU6050地址0x68发送时应为0xD0/0xD1读取数据始终为0xFFSDA线被主机一直拉高从机无法拉低主机未释放SDA接收前务必释放SDA配置为输入模式时序偶尔错乱软件延时不够从机来不及响应适当增大延时时间或降低SCL频率多字节读取时最后一个字节错误最后一个字节后主机发了ACK从机继续发送下一字节最后一个字节前发送NACK1告诉从机停止发送七、总结表格为了让大家对I2C的三种数据帧一目了然我整理了这个表格操作类型方向关键时序地址指针变化典型应用场景指定地址写主机→从机S 地址写 ACK 寄存器地址 ACK 数据 ACK P指针指向写入寄存器并自增配置MPU6050采样率、设置AT24C02存储地址当前地址读从机→主机S 地址读 ACK 数据 NACK P指针自增连续读取传感器数据配合指定地址写使用指定地址读从机→主机S 地址写 ACK 寄存器地址 ACK Sr 地址读 ACK 数据 NACK P先设置指针再读取指针自增读取MPU6050指定轴加速度、读取EEPROM指定地址数据核心口诀想写就发地址0想读就发地址1读之前先指位置中间用Sr连起来。写这篇文章的时候我把江科大的视频又回顾了一遍重新梳理了I2C的时序逻辑。说实话I2C协议刚学的时候确实有点绕尤其是接收应答和发送应答容易搞混指定地址读的Sr重复起始条件也需要多画几遍时序图才能理解。但只要你把基本单元起始、终止、发送、接收、应答这五个字母记牢再复杂的通信也只不过是它们的排列组合而已。开发环境Keil5 STM32F103C8T6最小系统板 ST-Link参考资料江科大STM32入门教程、《STM32F10xxx参考手册》如果本文对你有帮助欢迎点赞 收藏 ⭐ 有任何问题欢迎在评论区交流一起学习进步