stm32之I2C通信协议
系列文章目录
1. stm32之I2C通信外设
2. stm32之软件I2C读写MPU6050陀螺仪、加速度传感器应用案例
3. stm32之硬件I2C读写MPU6050陀螺仪、加速度传感器应用案例
文章目录
-
系列文章目录
-
前言
-
一、I2C通信协议
-
二、I2C硬件电路
-
三、I2C时序基本单元
-
- 3.1 起始与终止信号
- 3.2 发送与接收一个字节
- 3.3 发送与接收应答
-
四、I2C时序分析
-
- 4.1 指定地址写
- 4.2 当前地址读
- 4.3 指定地址读
前言
提示:本文主要用作在学习江科大自化协STM32入门教程后做的归纳总结笔记,旨在学习记录,如有侵权请联系作者
本文主要探讨I2C通信协议。关于I2C通信的内容我主要会分为两大块来讲,第一块,就是介绍协议规则,然后用软件模拟的形式来实现协议。第二块,就是介绍stm32的I2C外设,然后用硬件来实现协议。因为I2C是同步时序,软件模拟协议也是非常方便,目前也存在很多软件模拟I2C的代码,所以我们先学习软件I2C,再学习硬件I2C。
一、I2C通信协议
I2C(Inter-Integrated Circuit)是一种广泛用于短距离通信的串行通信协议,主要用于连接低速外围设备,如传感器、EEPROM、ADC、DAC等,常见于嵌入式系统中。STM32微控制器系列中通常包含I2C接口,使得与各种外围设备进行通信变得非常方便。像下图中的外设也是支持I2C通信协议的,比如左图1的MPU6050模块,可以进行姿态测量。再比如说左图2OLED模块,可以显示字符之类的信息,也是采用I2C的通信协议。还有,左图3,AT24C02存储器模块,左图4,DS3231实时时钟模块等等。

二、I2C硬件电路
I2C是一种多主从(multi-master, multi-slave)同步半双工的通讯协议,这意味着可以有多个主设备和多个从设备连接在同一条总线上。I2C总线主要由SCL(Serial Clock Line)和SDA(Serial Data Line)线组成。SCL是时钟信号线,由主设备控制。SDA是数据线,用于在主设备和从设备之间传输数据。
接下来我们就来详细分析一下,看看I2C的实现原理是怎样的吧!如下左图所示就是I2C典型的电路模型了,这是一个一主多从的模型。

CPU就是我们的单片机,作为总线的主机,主机的权力很大,包括对SCL线的完全控制,任何时候都是主机完全掌控SCL线的。另外,在空闲状态下,主机可以主动发起对SDA线的控制,只有在从机发送数据以及从机应答的时候主机才会转交SDA线的控制权给从机。
下面这四个都是被控的IC,也就是挂载在I2C总线上的从机。这些从机可以是姿态传感器、OLED、存储器、时钟模块等等。从机的权力比较小,对于SCL时钟线,在任何时刻都只能被动地读取,从机不允许控制SCL线。对于SDA数据线,从机不允许主动发起对SDA线的控制,只有在主机发送读取从机的命令后或者从机应答的时候,从机才能短暂地获取SDA线的控制权。
我们再来看一下I2C的电路接线要求,主要有以下三点要求:
1. 所有I2C设备的SCL连在一起,SDA连在一起
2. 设备的SCL和SDA均要配置成开漏输出模式
3. SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
首先我们来看一下第一点要求,所有I2C设备的SCL连在一起,SDA连在一起。
I2C是一种多主多从的串行通信协议,所有的设备包括主设备和从设备需要共享同一条数据线(SDA)和时钟线(SCL),这使得所有设备都可以通过同一条总线进行通信,主设备根据需要选择相应的从设备进行数据传输。这种设计使得电路结构更加简单,无需为每个从设备分配独立的数据和时钟线,节省了引脚和布线资源,特别适用于多设备系统。
接着我们再来看一下剩下的两点要求。
在开漏输出模式下,所有的设备只能通过给引脚输出0低电平而不能输出1高电平,引脚的高电平只能由外部的上拉电阻提供,当总线上没有设备主动拉低时,总线会处于高阻态。这样做有什么好处呢?
避免信号冲突: 当多个设备同时连接到总线时,如果使用的是推挽输出模式(能够主动驱动高低电平),可能会发生冲突(例如一个设备驱动高电平,另一个设备驱动低电平,导致短路)。而开漏输出只允许设备将线拉低或保持高阻态,这样多个设备不会在总线上发生电平冲突。
支持线与逻辑 : I2C总线利用开漏输出实现了“线与逻辑”,即任何一个设备将线拉低,整个总线就会处于低电平状态,这对于实现I²C的总线仲裁机制至关重要。
关于开漏输出模式,你可以把SCL和SDA线想象成一条杆子,杆子上拴着一根弹簧。当你想要输出低电平时就向下拉杆子,当你想要输出高电平时就松手,由于弹簧的作用下杆子就会自动被拉高至一个水平。
所有的设备包括CPU和被控IC,它们引脚的内部结构都是如上右图所示。左边这块是SCL的结构,这里SCLK就是SCL的意思。右边这一块是SDA的结构,这里DATA就是SDA的意思。当给引脚输出低电平0时,开关管导通,引脚直接接地。当你给引脚输出高电平1时开关管断开,引脚在上拉电阻的作用下保持高阻态,也就是高电平。
三、I2C时序基本单元
3.1 起始与终止信号

起始信号:SCL高电平期间,SDA从高电平切换到低电平
终止信号:SCL高电平期间,SDA从低电平切换到高电平
起始信号产生:
一开始,I2C总线处于空闲状态,SCL和SDA线没有被任何设备占据,此时它们由于外挂的上拉电阻作用下引脚电平被拉至高电平(高阻态),总线处于平静的高电平状态。另一方面,从机时刻监控着总线的电平状态等待主机的召唤。
当主机需要进行数据收发时首先就要打破总线的宁静产生一个起始信号,这个起始信号就是在SCL高电平期间把SDA电平拉低产生一个下降沿。然后在SDA产生下降沿后的一小段时间以后主机也把SCL拉低,拉低SCL一方面是为了占用这个总线,另一方面是为了方便基本单元的拼接。就是之后我们会保证除了起始和终止信号,每个时序单元的SCL都是以低电平开始,低电平结束,这样这些单元拼接起来SCL才能续得上。
终止信号产生:
终止条件是SCL高电平期间,SDA从低电平切换到高电平。也就是SCL先放手回弹到高电平,SDA再放手回弹到高电平,同时产生一个上升沿信号,这个上升沿信号触发终止条件。同时终止条件之后SCL和SDA都是高电平,回归到最初的状态。
这个起始条件和终止条件就类似串口时序里的起始位和停止位。一个完整的数据帧总是以起始条件开始、终止条件结束的。另外,起始和终止都是由主机产生的,从机不允许产生起始和终止。所以在总线状态空闲时,从机必须始终双手放开不允许主动跳出来去碰总线。
3.2 发送与接收一个字节
发送一个字节:

SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
注意:SDA这里有两根实线 表示的是主机控制的两根高低电平线,如果主机想要让这一位的数据置1就松手回弹至高电平,置0就拉低。
另外,由于这里有时钟线进行同步,所以如果主机的一个字节发送到一半突然进中断不操作SCL和SDA了,那时序就会在中断的位置不断拉长(此时SCL处于低电平状态,从机等待SCL切换到高电平才能读取SDA的数据),SCL和SDA电平都暂停变化,传输也完全停止了。等中断结束后主机回来继续操作传输仍然不会出问题,这就是同步时序的好处。
最后就是,由于这整个时序是主机发送一个字节,所以在这个单元里SCL和SDA全程都由主机掌控,从机只能被动读取。
接收一个字节:

SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
注意:SDA这里的实线部分表示主机控制的电平,虚线部分表示从机控制的电平。SCL全程由主机控制,主机在接收数据前要释放SDA交由从机控制。
3.3 发送与接收应答
发送应答:

主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
如果应答位为0则从机认为主机还需要数据就会继续发送,如果应答位为1则从机认为主机不想要数据了就不再发送数据。
接收应答:

主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
那到这里,I2C的6块拼图就已经集齐了。分别是起始信号、终止信号、发送一个字节、接收一个字节、发送应答和接收应答。接下来我们就来拼接这些基本单元,组成一个完整的数据帧吧。
四、I2C时序分析
以下时序为在示波器下实际抓取的时序波形 ,其中黄色线表示为SCL,蓝色线表示为SDA,绿色线表示为读取数据的数据线,红色线为每个阶段时序的分隔线。 (注意:不知道是什么原因部分图片有点失真,SCL和SDA均是连续的实线,大家知道一下就行了)
4.1 指定地址写

上图所示时序为对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
具体就是对于指定从机地址为1101 000的设备,在指定地址为0x19下的寄存器中,写入数据0xAA。
一开始,空闲状态下SCL和SDA都处于高电平状态,没有任何设备占据总线。
当主机需要给从机写入数据时。首先,SCL高电平期间主机拉低SDA产生一个起始信号S(Start) 。在起始信号之后紧跟着的时序必须是发送一个字节的时序,字节的内容必须是从机地址(7位)+读写位(1位) 。紧跟着,在起始信号之后主机把SCL拉低,然后主机往SDA写入一位数据,然后主机再把SCL拉高,在SCL高电平期间从机读取SDA上的一位数据,然后主机再把SCL拉低,再往SDA写入第二位数据。如此循环写入一帧数据(8位)。最后,根据协议规定紧跟着的时序就是接收从机的应答位RA (Receive Ack) 。此时主机再次拉低SCL,然后释放SDA控制权,从机得到SDA的控制权后如果将SDA拉低表示为应答,如果什么都不做则表示为非应答(由于外挂上拉电阻作用下SDA被拉至高电平)。最后主机再把SCL拉至高电平然后再读取SDA上的数据来获取从机的应答状态。
注意:从机设备地址,在I2C协议标准里分为7位地址和10位地址,我们目前只讲7位地址的模式,因为7位地址比较简单而且应用范围最广。

紧接着,主机继续发送一个字节数据。跟上面发送一个字节数据的时序一样,同样的时序再来一遍,第二个字节的数据就可以送到指定设备的内部了。从机设备可以自己定义第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址或者是指令控制字等。比如MPU6050定义的第二个字节就是寄存器的地址,AD转换器第二个字节可能定义的就是指令控制字,存储器第二个字节可能定义的就是存储器地址。在我们这里,第二个字节表示为寄存器地址,意思就是我要操作你0x19地址下的寄存器了。最后紧跟着的时序同样是从机的应答位。

同样的流程再来一遍,主机再发送一个字节数据,这个字节就是主机写入到0x19地址下寄存器的内容了,然后紧跟着的时序同样还是从机的应答位,最后,如果主机不再需要继续传输数据了就可以产生一个终止信号 P(Stop) 。在产生终止信号之前先拉低SDA为后续SDA的上升沿作准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间SDA的上升沿了,也就是终止信号了。

到这里一个完整的指定地址写数据帧就完成了,这个数据帧的目的就是对于指定从机地址为1101 000的设备,在指定地址为0x19下的寄存器中,写入数据0xAA。
4.2 当前地址读

上图所示时序为对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
具体来说就是对于指定从机地址为1101 000的设备,在当前地址指针指示的地址下,读取到的从机数据为0x0F
一开始,空闲状态下SCL和SDA都处于高电平状态,没有任何设备占据总线,当主机需要读取从机数据时。
首先,SCL高电平期间主机拉低SDA产生一个起始信号S(Start) 。然后主机发送一个字节数据来进行从机寻址和指定读写标志位(上图所示波形为主机寻址从机地址为1101 000的设备,同时最后一位读写标志为1表示主机接下来想要读取数据),主机收到从机应答位之后拉低SCL,释放SDA,从机获取SDA控制权后往SDA进行写入一个字节数据时序,主机接收完一个字节数据以后回复应答位SDA写1表示结束接收(若主机不需要继续接收数据就回复非应答也就是SDA写1,若主机还想继续接收数据就回复0也就是SDA写0),最后主机先拉低SDA为后续SDA的上升沿作准备,然后释放SCL,再释放SDA产生终止信号结束通信。
到这里一个完整的当前地址读数据帧就完成了,这个数据帧的目的就是对于指定从机地址为1101 000的设备,在当前地址指针指示的地址下,读取到的从机数据为0x0F。
那现在问题就来了,这个0x0F的数据是从机哪个寄存器的数据呢?
在读的时序中I2C规定的是,主机进行寻址时一旦读写标志位给1,下一个字节就要立即转为读的时序,所以主机还来不及指定要读取的寄存器地址就得开始接收了,所以这里就没有指定地址这个环节。那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据呢?这就需要用到上面我们说的当前地址指针了。
在从机中,所有的寄存器都被分配到了一个线性区域中并且会有一个单独的指针变量指示着其中一个寄存器,这个指针上电默认一般指向0地址,并且每写入和读出一个字节后这个指针就会自动自增一次移动到下一个位置。
那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值。那按照这个特性,结合上面我们讲过的写的时序我们可以想一下,假设我刚刚在调用了指定地址写的时序,在0x19这个位置写入了0xAA这个数据,完了之后指针就会加1移动到了0x1A这个位置,此时我再调用这个当前地址读的时序,从机返回的就是0x1A这个当前地址,那我是不是就能读取到0x1A这个位置的数据了?
那我这样操作,假如我指定从机地址(1101 000)+写(0),收到从机应答后,再发送第二个字节寄存器地址(0x1A),完了之后不写数据了直接重新发起信号Sr(Start Repeat) ,然后再调用当前地址读的时序,这样是不是就能读到从机地址(1101 000)0x1A这个位置的数据了?
这个时序也就是接下来我们要讲到的指定地址读的时序了。
4.3 指定地址读

上图所示时序为对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
具体就是对于指定从机地址为1101 000的设备,在指定地址为0x19下的寄存器中,读取到的数据为0xAA。
上面我们已经讲了该时序的基本思路了,现在就来简单地分析一下。
首先最开始依然是主机先产生一个起始信号S(Start) ,然后发送第一个字节数据进行从机寻址(1101 000)+读写标志位(0表示为写),收到从机应答之后再发送第二个字节数据指定寄存器地址0x19,此时从机接收到这个时序后指针就指向了这个0x19地址的寄存器了,然后主机继续收取从机的应答信号。

在收到从机应答位之后我们不写入数据而是直接重新发起起始信号Sr(Start Repeat) ,然后主机再重新进行从机寻址(1101 000)+读写标志位(1表示为读),接着主机发起接收一个字节的时序,这个接收的字节数据就是地址为0x19的寄存器数据了,然后主机回复应答位 SA(Send Ack) 为非应答,从机收到主机的非应答信号之后停止发送数据,最后主机先把SDA拉低为产生上升沿作准备,然后主机先释放SCL回弹到高电平,再释放SDA回弹到高电平产生一个上升沿信号产生终止束信号。
注意:如果主机只想读一个字节就停止的话,那么在读完一个字节之后一定要给从机发个非应答SA(Send Ack) 1。如果主机想连续读取多个字节就需要在最后一个字节给非应答SA(Send Ack) 1,而在这之前的所有字节都要给应答SA(Send Ack) 0。

