文章目录
前言
提示:本文主要用作在学习江科大自化协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):数据线,用于在主设备和从设备之间传输数据。
接下来我们就来详细分析一下,看看I2C的实现原理是怎样的吧!
如上左图所示就是I2C典型的电路模型了,这是一个一主多从的模型,CPU就是我们的单片机,作为总线的主机,主机的权力很大,包括对SCL线的完全控制,任何时候都是主机完全掌控SCL线的。另外,在空闲状态下,主机可以主动发起对SDA线的控制。只有在从机发送数据以及从机应答的时候,主机才会转交SDA线的控制权给从机,这就是主机的权力。
下面这四个都是被控的IC,也就是挂载在I2C总线上的从机。这些从机可以是姿态传感器、OLED、存储器、时钟模块等等。从机的权力比较小,对于SCL时钟线,在任何时刻都只能被动地读取,从机不允许控制SCL线。对于SDA数据线,从机不允许主动发起对SDA线的控制,只有在主机发送读取从机的命令后或者从机应答的时候,从机才能短暂地获取SDA线的控制权。
以上就是一主多从模型中协议的规定了。
我们先来看一下I2C的电路接线。如下图所示,I2C的接线要求是所有I2C设备的SCL连在一起,SDA连在一起。假设我们先忽略这两个电阻,那按照如下所示的接线方式,我们要如何规定每个设备的SCL和SDA的输入输出模式呢?
SCL应该好规定,因为现在是一主多从模式,主机拥有SCL的绝对控制权,所以主机的SCL可以配置为推挽输出模式,所有从机的SCL都配置成浮空输入或上拉输入。数据流向是主机发送,所有从机接收,这没问题,但是到SDA这里就比较麻烦了。
因为这是半双工的协议,所以主机的SDA在发送的时候是输出,在接收的时候是输入。同样,从机的SDA也会在输入和输出之间反复切换。如果你能协调好输入输出的切换时机那其实也没问题,但是这样做的话,如果总线时序没协调好,极有可能两个引脚同时处于输出的状态,如果这时又正好是一个输出高电平,一个输出低电平,那这个状态就是电源短路,这个状态是要极力避免的。
所以为了避免总线状态没协调好导致电源短路这个问题,I2C的设计是禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构。 具体来说就是设备的SCL和SDA均要配置成开漏输出模式,SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。
如下图所示,所有的设备,包括CPU和被控IC,它引脚的内部结构都是如下图所示。左边这块是SCL的结构,这里SCLK就是SCL的意思。右边这一块是SDA的结构,这里DATA就是SDA的意思。
简而言之就是:
- 所有I2C设备的SCL连在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏输出模式
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
三、I2C时序基本单元
3.1 起始与终止信号
起始信号:SCL高电平期间,SDA从高电平切换到低电平
终止信号:SCL高电平期间,SDA从低电平切换到高电平
起始信号产生:
一开始,I2C总线处于空闲状态,SCL和SDA都被拉至高电平(SCL和SDA由外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态)
当主机需要进行数据收发时首先就要打破总线的宁静产生一个起始信号。这个起始信号就是先把SDA电平拉低产生一个下降沿,此时,当从机捕获到SCL高电平时就会进行自身的复位,等待主机的召唤。然后在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表示非应答
意思大概就是主机在接收完一个字节之后(此时SCL处于高电平)把SCL拉低,然后再将SDA拉低表示应答,最后再把SCL拉高,从机检测到SCL产生上升沿后(SCL高电平)立刻读取SDA的数据来获取主机的应答状态。
接收应答:
主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
意思大概就是主机在发送完一个字节之后(此时SCL处于高电平)再把SCL拉低,把SDA的控制权交给从机,当从机收到SCL的下降沿后,若将SDA至低电平则表示应答,若什么也不做也就默认被外挂的上拉电阻拉高至高电平。最后主机再把SCL拉至高电平去读取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拉高,从机读取SDA上的一位数据,如此循环8次为写入一帧数据。然后根据协议规定,紧跟着的时序就是接收从机的应答位RA (Receive Ack),此时主机拉低SCL释放SDA,在主机释放SDA的瞬间从机将SDA拉低表示为应答,然后主机再把SCL拉高读取SDA获取从机应答的状态,若从机无应答则读取的SDA为高电平。
应答结束后,主机继续发送一个字节数据。跟上面发送一个字节数据的时序一样,同样的时序再来一遍,第二个字节就可以送到指定设备的内部了。从机设备可以自己定义第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址或者是指令控制字等。比如MPU6050定义的第二个字节就是寄存器地址,比如AD转换器第二个字节可能定义的就是指令控制字,再比如存储器第二个字节可能定义的就是存储器地址。在我们这里第二个字节表示为寄存器地址,意思就是我要操作你0x19地址下的寄存器了,最后同样是从机应答把SDA拉低表示ok。
同样的流程再来一遍,主机再发送一个字节数据,这个字节就是主机写入到0x19地址下寄存器的内容了,然后就是从机接收应答。
如果主机不再需要继续传输数据了就可以产生一个停止信号 P(Stop) 。在产生停止信号之前先拉低SDA为后续SDA的上升沿作准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间SDA的上升沿了。
到这里一个完整的写数据帧就完成了,这个数据帧的目的就是对于指定从机地址为1101 000的设备,在指定地址为0x19下的寄存器中,写入数据为0xAA。
4.2 当前地址读
上图所示时序为对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
具体来说就是对于指定从机地址为1101 000的设备,在当前地址指针指示的地址下,读取到的从机数据为0x0F
一开始,主机在SCL高电平期间拉低SDA产生一个起始信号,然后主机发送一个字节数据来进行从机寻址和指定读写标志位。上图所示波形为主机寻址从机地址为1101 000的设备,同时最后一位读写标志为1,表示主机接下来想要读取数据。紧跟着,在发送一个字节后接收一个从机应答位。从机应答后主机释放SDA,从机往SDA写入一位数据,主机在高电平期间读取一位SDA数据,如此循环8次为读取一个字节数据,最后的结果就是主机读取到的数据为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。
上面我们已经讲了该时序的基本思路了,现在就来简单地分析一下。
首先最开始依然是主机先产生一个起始信号,然后发送第一个字节数据进行从机寻址(1101 000)+读写标志位(0表示为写),收到从机应答之后再发送第二个字节数据指定寄存器地址0x19,此时从机接收到这个时序后指针就指向了这个0x19地址的寄存器了,然后主机继续收取从机的应答信号。
然后我们不写入数据而是直接重新发起起始信号Sr(Start Repeat),然后主机再重新进行从机寻址(1101 000)+读写标志位(1表示为读),接着主机发起接收一个字节的时序,这个接收的字节数据就是地址为0x19的寄存器数据了。最后主机回复了一个非应答 SA(Send Ack) 也就是SDA为1表示停止接收数据了,从机收到主机的非应答信号之后释放SDA控制权,主机拿到SDA控制权后先把SDA拉低为产生上升沿作准备,最后主机先释放SCL回弹到高电平,然后再释放SDA回弹到高电平,同时产生一个上升沿信号触发结束信号。
注意:如果主机只想读一个字节就停止的话,那么在读完一个字节之后一定要给从机发个非应答SA(Send Ack)。非应答就是该主机应答的时候主机不把SDA拉低(在外挂的上拉电阻作用下拉高至高电平),SCL高电平期间从机读到SDA为1代表主机没有应答,从机收到非应答之后知道主机不想再继续了,从机就会释放SDA把控制权交还给主机。如果主机想连续读取多个字节就需要在最后一个字节给非应答,而在这之前的所有字节都要给应答。
还有另外一种方法,就是你可以不重新发起起始信号而是直接发停止信号P(Stop) ,然后再发起起始信号,这样也是可以的,只不过这样是两条时序而已。但是I2C协议官方规定的复合格式是一整个数据帧,就是先起始,再重复起始,再停止,相当于把两条时序拼接成一条。