单片机学习!
文章目录
目录
前言
串口通信就是从TX引脚向RX引脚发送数据流,数据流以字节为单位,可以组合多个字节变成多字节的数据包传输。另外串口通信的设计是,一条发送线,一条接收线,没有时钟线的异步全双工的协议。那么如何根据串口通信协议来设计一种特殊要求的通信协议呢?
新的通信协议需求:
- 在单片机和外部模块连接少量的几根线,实现单片机读写外部模块寄存器的功能。至少实现在指定的位置写寄存器和在指定的位置读寄存器这两个功能。
- 只能在同一根通信线上进行发送和接收,也就是半双工通信。
- 需要有应答机制,在发送方每次发送完一个字节数据后需要接收方给一个应答。
- 在一根通信线上接多个模块,单片机可以指定和任意一个模块通信。同时单片机在和其中一个模块通信时,其他模块不能对正常的通信进行干扰。
- 通信协议为同步通信,需要有一条时钟线来指导对方读写。
一、I2C通信
- I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线。
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)。
- 同步,半双工、带数据应答。
- 支持总线挂载多设备(一主多从、多主多从)。
1.1 I2C总线
I2C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线。
I2C就是一种通信协议,就像串口一样,串口通信也是要遵循串口的通信协议。I2C的实现的功能也和串口一样,就是实现多个设备之间互相通信。
I2C目前应用还是非常广泛的,已经有很多模块都使用了I2C的协议标准。使用通用的协议对于开发者来说就非常方便了,同样的协议在不同的硬件上操作方法都是极为相似的,学会了其中一个硬件再学其他的硬件就很容易了。
1.2 I2C通信线
两根通信线:SCL(Serial Clock)、SDA(Serial Data)。
I2C的标志性引脚就是两根通信线,SCL全称Serial Clock串行时钟线;SDA全称Serial Data串行数据线。使用I2C通信的器件都有SCL和SDA这两个引脚。
SCL时钟线就满足了前言里的最后一个需求4,使用同步的时序,降低对硬件的依赖,同时同步的时序稳定性也比异步时序更高。
同时,只有一根SDA数据线,也满足了大公司提出的需求1,变全双工为半双工,一根线兼具发送和接收,最大化利用资源。
1.3 同步半双工且数据应答
同步、半双工、带数据应答
I2C协议是同步、半双工的协议,也是带数据应答的协议。这里满足需求2。
1.4 一主多从
支持总线挂载多设备(一主多从,多从多主)
I2C支持总线挂载多设备,满足设计要求3,并且挂载多设备支持两种模型,一主多从和多主多从。
一主多从的意思就是,单片机作为主机,主导I2C总线的运行,挂载在I2C总线的所有外部模块都是从机,从机只有被主机点名之后才能控制I2C总线,不能在未经允许的情况下去碰I2C总线,防止冲突。
这就像是在教室里,老师是主机,主导课程的进行,所有学生都是从机,所有从机可以同时被动地听老师讲课。但是从机只有在被老师点名之后才能说话,不可以在未经允许的情况下说话,这样课堂才能有条不紊的进行。
以上就是一主多从的模型,使用I2C的绝大多数场景都是一主多从的形式,一个单片机作为主机,挂载一个或多个模块作为从机。
另外I2C其实还支持多主多从的模型,也就是多个主机。多主多从的模型就是在总线上的任何一个模块都可以主动跳出来说接下来我就是主机,你们都得听我的。
这就像是在教室里,老师正在讲课,突然有个学生站起来说:“老师打断一下,接下来让我来说,所有同学听我指挥。”
但是,同一个时间只能有一个人说话,这时就相当于发生了总线冲突。在总线冲突时,I2C协议会进行仲裁,仲裁胜利的一方取得总线控制权,仲裁失败的一方自动变回从机。当然由于时钟线也是由主机控制的,所以在多主机的模型下还要进行时钟同步。多主机的情况下,协议是比较复杂的本文不做过多讲解,仅展示一主多从的模型使用。
作为一个通信协议,它必须要在硬件和软件上都做出规定。
- 硬件上的规定就是电路应该如何连接,端口的输入输出模式都是啥样的这类内容。
- 软件上的规定就是时序是如何定义的,字节如何传输,高位先行还是低位先行,一个完整的时序由哪些部分构成这些东西。
硬件的规定和软件的规定配合起来就是一个完整的通信协议。
二、硬件电路
I2C的硬件规定也就是硬件电路部分的内容如下。
- 所有I2C设备的SCL连在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏输出模式
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
2.1 I2C电路模型
上图就是一个I2C的典型电路模型,这是一个一主多从的模型。
左边CPU就是单片机,作为总线的主机,主机的权利很大,包括对SCL线的完全控制,任何时候都是主机完全掌控SCL线,另外在空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答的时候主机才会转交SDA的控制权给从机。这就是主机的权利。
图中下方四个模块都是一系列的被控IC,也就是挂载在I2C总线上的从机,这些从机可以是姿态传感器、OLED、存储器、时钟模块等等。
从机的权利比较小:
- 对于SCL时钟线,在任何时刻都只能被动的读取。从机不允许控制SCL线。
- 对于SDA数据线,从机不允许主动发起对SDA的控制,只有在主机发送读取从机的命令后,或者从机应答的时候,从机才能短暂地取得SDA的控制权。
以上就是一主多从模型中协议的规定。
2.2 I2C接线要求
观察图中接线可以发现,所有I2C设备的SCL连在一起,SDA连在一起。
这就是I2C的接线要求,在图中也可以看到,主机SCL线一条拽出来,所有从机的SCL都接在这上面。SDA线也是一样的,主机SDA线一条拽出来,所有从机的SDA都接在这上面。这就是SCL和SDA的接线方式。
2.3 I2C上拉电阻作用
图中右上方还有两个电阻,那先忽略这两个电阻,会有什么情况呢?
假设不要这两个电阻,来看一下如何规定每个设备SCL和SDA的输入输出模式。
首先SCL应该好规定,因为现在是一主多从,主机拥有SCL的绝对控制权,所以主机的SCL可以配置成推挽输出,所有从机的SCL都配置成浮空输入或者上拉输入。数据流向是,主机发送,所有从机接收。
SCL线这边规定没问题,但是SDA线这里就比较麻烦了,因为这是半双工的协议,所以主机的SDA在发送的时候是输出,在接收的时候是输入。同样,从机的SDA也会在输入和输出之间反复切换。如果可以协调好输入和输出的切换时机,那其实这样设计也没问题。但是这样做,如果总线时序没协调好,极有可能发生两个引脚同时处于输出的状态。如果这时又正好是一个输出高电平,一个输出低电平,那这个状态就是电源短路,这种状态是要极力避免的。
所以为了避免总线没协调好导致电源短路这个问题,I2C的设计是禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构。这两点规定就是硬件电路的这两条内容,设备的SCL和SDA均要配置成开漏输出模式。
SCL和SDA各添加一个上拉电阻,阻值一般为4.7K欧左右。对应下面这个图就是:
所有的设备包括CPU和被控IC,它引脚的内部结构都是图中这样的,图中:
- 左边这一块是SCL的结构,这里SCLK就是SCL的意思。
- 右边这一块是SDA的结构,这里DATA就是SDA的意思。
首先引脚的信号进来,都可以通过一个数据缓冲器或者是施密特触发器进行输入。因为输入对电路没有任何影响,所以任何设备在任何时刻都是可以输入的。但是在输出的这部分采用的是开漏输出的配置。
正常的推挽输出是:
- 上方一个开关管接到正极,
- 下方一个开关管接到负极。
- 上方导通输出高电平,
- 下方导通输出低电平。
因为这是通过开关管直接接到正极和负极的,所以这个是强上拉和强下拉的模式,而开漏输出就是去掉这个强上拉的开关管:
- 输出低电平时,下管道通,是强下拉;
- 输出高电平时,下关断开,但是没有上管了,此时引脚处于浮空的状态,
这就是开漏输出,和图示一样
- 输出低电平,开关管导通,引脚直接接地,是强下拉;
- 输出高电平,这个开关管断开,引脚什么都不接,处于浮空状态。
这样的话,所有的设备都只能输出低电平而不能输出高电平。为了避免高电平造成的引脚浮空,就需要在总线外面SCL和SDA各外置一个上拉电阻,这是通过一个电阻拉到高电平的,所以这是一个弱上拉。
用弹簧和杆子的模型来解释就是,SCL或SDA就是一根杆子,为了防止有人向上推杆子,有人向下拉杆子,造成冲突。就规定所有人不准向上推杆子,只能选择往下拉或者放手。然后再外置一根弹簧向上拉,要输出低电平就往下拽,弹簧肯定会随着下拽而拉伸,杆子处于低电平状态,要输出高电平就放手,杆子在弹簧的拉力下回弹到高电平。这就是一个弱上拉的高电平,但是完全不影响数据传输。
这样做的好处:
第一,完全杜绝了电源短路现象,保证电路的安全。所有人无论怎么拉杆子或者放手,杆子都不会处于一个被同时强拉和强推的状态。即使有多个人同时往下拉杆子,也没问题。
第二,避免了引脚模式的频繁切换。开漏加弱上拉的模式同时兼具了输入和输出的功能。要是想输出,就去拉杆子或放手,操作杆子变化就行了;要是想输入,就直接放手,观察杆子高低就行了。因为开漏模式下,输出高电平就相当于断开引脚,所以在输入之前,可以直接输出高电平。不需要再切换成输入模式了。
第三,就是这个模式会有一个“线与”的现象,就是只要有任意一个或多个设备输出了低电平,总线就处于低电平;只有所有的设备都输出高电平,总线才处于高电平。I2C可以利用这个电路特性执行多主机模式下的时钟同步和总线仲裁。所以这里SCL虽然在一主多从模式下可以使用推挽输出,但是它任然采用了开漏加上拉输出的模式。因为在多主机模式下会利用到这个特征。以上就是I2C的硬件电路设计。
三、I2C时序基本单元
接下来的内容就是软件部分,也就是时序的设计了。首先来学习一下I2C规定的一些时序基本单元。
3.1 起始终止条件
3.1.1 起始条件
首先就是起始条件,它是指SCL高电平期间,SDA从高电平切换到低电平。
看上图,在I2C总线处于空闲状态时,SCL和SDA都处于高电平状态。也就是没有任何一个设备去碰SCL和SDA,SCL和SDA由外挂的上拉电阻拉高至高电平。总线处于平静的高电平状态。
当主机需要进行数据收发时,首先就要打破总线的宁静,产生一个起始条件。这个起始条件就是,SCL处于高电平不去动它,然后把SDA拽下来,产生一个下降沿。当从机捕获到SCL高电平,SDA下降沿信号时,就会进行自身的复位,等待主机的召唤。然后在SDA下降沿之后,主机要再把SCL拽下来。拽下SCL一方面是占用这个总线,另一方面也是为了方便基本单元的拼接,就是之后设计保证,除了起始和终止条件,每个时序单元的SCL都是以低电平开始,低电平结束。这样这些单元拼接起来,SCL才能连续得上。
3.1.2 终止条件
终止条件是,SCL高电平期间,SDA从低电平切换到高电平。
如上图所示SCL先放手,回弹到高电平,SDA再放手,回弹到高电平,产生一个上升沿,这个上升沿触发终止条件。同时终止条件之后,SCL和SDA都是高电平,回归到最初的平静状态。
起始条件和终止条件就类似串口时序里的起始位和停止位。一个完整的数据帧,总是以起始条件开始,终止条件结束。另外,起始和终止,都是由主机产生的,从机不允许产生起始和终止。所以在总线空闲转态时,从机必须始终双手放开,不允许主动跳出来去碰总线。如果允许的话,那就是多主机模型了。以上就是起始和终止条件。
3.2 发送一个字节
在起始条件之后,这时就可以紧跟着一个发送一个字节的时序单元。发送一个字节就是SCL低电平期间,主机将数据依次放到SDA线上,高位先行。然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
如上图所示,起始条件之后,第一个字节也必须是主机发送的。
主机如何发送呢,就是最开始SCL低电平,
- 主机如果想发送0,就拉低SDA到低电平;
- 主机如果想发送1,就放手,SDA回弹到高电平。
在SCL低电平期间,允许改变SDA的电平。
当第一个字节这一位放好之后,主机就松手时钟线,SCL回弹到高电平。在高电平期间,是从机读取SDA的时候。所以高电平期间,SDA不允许变化。SCL处于高电平之后,从机需要尽快地读取SDA,一般都是在上升沿这个时刻,从机就已经读取完成了。因为时钟是主机控制的,从机并不知道什么时候就会产生下降沿。从机要是磨磨唧唧的,主机可不会等从机。所以从机在上升沿时,就会立刻把数据读走。
主机在放手SCL一段时间后就可以继续拉低SCL,传输下一位了。主机也需要在SCL下降沿之后尽快把数据放在SDA上。但是主机有时钟的主导权,所以主机并不需要那么着急,只需要在低电平的任意时刻把数据放在SDA上就行了,晚点也没关系。数据放完之后,主机再松手SCL,SCL高电平,从机再读取这一位。
重复这样的流程,主机拉低SCL,把数据放在SDA上,主机松开SCL,从机读取SDA的数据。在SCL的同步下,依次进行主机发送和从机接收。循环8次就发送了8位数据,也就是一个字节。
另外注意,这里是高位先行,所以第一位是一个字节的最高位B7,然后依次是次高位B6、B5......最后发送最低位B0。这个和串口是不一样的,串口时序是低危先行,I2C是高位先行,这个需要注意!
另外,由于I2C有时钟线进行同步,所以如果主机一个字节发送一半,突然进中断了,不操作SCL和SDA了,那时序就会在中断的位置不断拉长。SCL和SDA电平都暂停变化,传输也完全暂停,等中断结束后,主机回来继续操作。传输仍然不会出问题,这就是同步时序的好处。
最后就是,由于这整个时序是主机发送一个字节。所以在这个单元里,SCL和SDA全程都由主机掌控,从机只能被动读取,这就是发送一个字节的时序。
3.3 接收一个字节
接收一个字节,基本流程是SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据位变化,依次循环上述过程8次,即可接收一个字节。
注意,主机在接收之前,需要释放SDA。
前文讲述释放SDA其实就相当于切换成输入模式,或者这样来理解,所有设备包括主机都始终处于输入模式,
- 当主机需要发送的时候,就可以主动去拉低SDA;
- 而主机在被动接收的时候就必须先释放SDA,不去动SDA以免影响别人发送。
因为总线是线与的特征,任何一个设备拉低电平了,总线就是低电平。如果总线在接受的时候,还拽着SDA不放手,那其他设备无论发什么数据,总线始终是低电平。
从流程上来看,接收一个字节和发送一个字节是非常相似的。
区别就是:
- 发送一个字节是低电平主机放数据,高电平从机读数据;
- 接收一个字节是低电平从机放数据,高电平主机读数据。
看一下接收一个字节的时序,和上面发送一个字节的时序基本一样。
区别就是SDA线,主机在接收之前要释放SDA,然后这时从机就取得了SDA的控制权,
- 从机需要发送0,就把SDA拉低;
- 从机需要发送1,就放手,SDA回弹高电平。
然后同样的,低电平变换数据,高电平读取数据。图中的
- 实线部分表示主机控制的电平。
- 虚线部分表示从机控制的电平。
SCL全程由主机控制,SDA主机在接收前要释放,交由从机控制。之后还是一样,因为SCL时钟是由主机控制的,所以从机的数据变换基本上都是贴着SCL下降沿进行的。而主机可以在SCL高电平的任意时刻读取。
以上就是接受一个字节的时序。
3.4 应答机制
应答机制分为发送应答和接收应答,它们的时序分别和发送一个字节、接收一个字节的其中一位是相同的。可以理解成发送一位和接收一位,这一位就用来做应答。
3.4.1 接收应答
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
在调用发送一个字节之后,就要紧跟着调用接受应答的时序,用来判断从机有没有收到刚才给它的数据。
如果从机收到了,那在应答位这里,主机释放SDA的时候,从机就应该立刻把SDA拉下来,然后在SCL高电平期间,主机读取应答位。如果应答位为0,就说明从机确实收到了。
这个场景就是,主机刚发送一个字节,然后说,有没有人收到啊,我现在把SDA放手了哈。如果有人收到的话,就把SDA拽下来。然后主机高电平读取数据,发现确实有人给拽下来了,那就说明确实有人收到了;如果主机发现把SDA松手了,结果这个SDA就跟着回弹到高电平了,那就说明没有人回应主机,刚发的一个字节可能没人收到,或者有人收到了字节数据但是没给主机回应。
以上就是发送一个字节接收应答的流程。
3.4.2 发送应答
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。
同理在接收一个字节之后,主机也需要给从机发送一个应答位,发送应答位的目的是告诉从机,是不是还要继续发。
如果从机发送一个数据后,得到了主机的应答,那从机就还会继续发送;
如果从机没得到主机的应答,那从机就会认为,我发送了一个数据但是主机不理我,可能主机不想要了吧。这时从机就会乖乖地释放SDA,交出SDA的控制权,防止干扰主机之后的操作。以上就是应答位的执行逻辑。
到这里I2C的六块拼图就已经介绍完了,分别是,起始条件、终止条件、发送一个字节、接收一个字节、发送应答和接受应答。接下来就来拼接这些基本单元,组成一个完整的数据帧。
四、I2C时序
I2C的完整时序主要有指定地址写、当前地址读和指定地址读这三种。
4.1 I2C设备的从机地址
上文介绍的I2C是一主多从的模型,主机可以访问总线上的任何一个设备。那如何发出指令,来确定要访问的是哪个设备呢?这就需要首先把每个从设备都确定一个唯一的设备地址。从机设备地址就相当于每个设备的名字。
主机在起始条件之后,要先发送一个字节叫一下从机名字。所有从机都会收到第一个字节和自己的名字进行比较。
- 如果不一样,则认为主机没有叫我之后的时序此从机就不用管了;
- 如果一样,就说明主机现在在叫这个从机,那此从机就响应之后主机的读写操作。
在同一条I2C设备里,挂载的每个设备地址必须不一样,否则,主机叫一个地址,有多个设备都响应,那不就乱套了。
从机设备地址在I2C协议标准里分为了7位地址和10位地址,本文只介绍7位地址的模型。因为7位地址比较简单而且应用范围最广,那在每个I2C设备出厂时,厂商都会为它分配一个7位的地址。这个地址具体是什么,可以在芯片手册里找到,
比如:
- MPU6050这个芯片的7位地址是1101000;
- AT24C02芯片的7位地址是1010000,
芯片地址:
- 一般不同型号的芯片地址都是不同的;
- 相同型号的芯片地址都是一样的,
那如果有相同的芯片挂载在同一条总线上怎么办呢,这就需要用到地址中的可变部分了。一般器件地址的最后几位是可以在电路中改变的。
比如MPU6050地址的最后一位就可以由这个板子上的AD0引脚确定,
- AD0引脚接低电平,那它的地址就是1101000;
- AD0引脚接高电平,那它的地址就是1101001。
比如AT24C02芯片地址的最后三位,都可以分别由这个板子上的A0、A1、A2引脚确定。
- 比如A0引脚接低电平,地址对应的位就是0,地址就是1010000;
- A0引脚接高电平,地址对应的位就是1,地址就是1010001;
A1和A2引脚也是同理。
一般I2C的从机设备地址,高位都是厂商确定的,低位可以由引脚来灵活切换。这样,即使相同型号的芯片挂载在同一个总线上也可以通过切换地址低位的方式保证每个设备的地址都不一样。
4.2 指定地址写
指定地址写:对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)。
指定地址写的时序,它完成的任务是对于指定设备,指定设备通过Slave Address从机地址来确定。在指定地址下,这个指定地址就是某个设备内部的Reg Address寄存器地址。写入指定数据就是要在这个寄存器中写入的Data数据。
看上图由示波器抓取的时序波形。
图中:
- 上面黄色的线是SCL,
- 下面蓝色的线是SDA。
空闲状态SCL和SDA都是高电平。
当主机需要给从机写入数据时。在SCL高电平期间,拉低SDA,产生起始条件(Start,S)。
在起始条件之后,紧跟着的时序必须是发送一个字节的时序,字节的内容必须是从机地址+读写位。正好从机地址是7位,读写位是1位,加起来是1个字节8位。
- 发送从机地址就是确定通信的对象;
- 发送读写位就是确认接下来是要写入还是要读出。
数据发送逻辑:
- 在SCL低电平期间,SDA变换数据;
- 在SCL高电平期间,从机读取SDA。
图中绿色的线标明了从机读到的SDA数据,比如图中最开始接收到的数据波形就显示:
- 从机收到的第一位数据就是高电平1。然后SCL低电平期间,主机继续变换数据。
- 因为第二位数据还是1,所以SDA电平没有变换。然后SCL高电平,从机读到的第二位是1。
- 之后继续,SCL低电平变换数据,高电平读取数据,第三位数据就是0.
- ......
一直持续8次,就发送了一个字节数据。
其中这个数据的定义是:
- 高7位表示从机地址,图示波形中主机寻找的从机地址就是1101000,也就是MPU6050的地址。
- 最后一位表示读写位,读写位为0表示之后的时序主机要进行写入操作;
读写位意义:
- 读写位为1表示之后的时序主机要进行读出操作。
- 读写位为0表示之后的时序主机要进行写入操作。
波形图中读写位这里是0,说明之后要进行写入操作。
图示主机是发送了一个字节,字节的内容转换为16进制,高位先行,就是0xD0。
根据协议规定紧跟着的单元就得是接收从机的应答位(Receive Ack,RA),在这个时刻,主机要释放SDA。所以如果单看主机的波形,应该是释放SDA之后,SDA引脚电平回弹到高电平。但是根据协议规定,从机要在这个位拉低SDA,所以单看从机的波形应该就是该应答的时候,从机立刻拽住SDA,应答结束之后,从机再放开SDA。
图示波形是综合两者的波形,结合线与的特性来分析,在主机释放SDA之后,由于SDA也被从机拽住了,所以主机松手后,SDA并没有回弹到高电平。这个过程就代表从机产生了应答。
最终高电平期间,主机读取SDA,发现是0,就说明主机发送数据进行寻址,有从机给应答了,传输没问题。
如果主机读取SDA发现是1,就说明在主机发送数据进行寻址,应答位期间,主机松手了,但是没有从机拽住它,没人给主机应答。那就直接产生停止条件吧,并提示一些信息。
这就是应答位。后面这个上升沿就是应答结束后,从机释放SDA产生的,从机交出了SDA的控制权,因为从机要在低电平尽快变换数据,所以这个上升沿和SCL的下降沿几乎是同时发生的。
继续看后面波形,由于之前读写位给了0,所以应答结束后,要继续发送一个字节,同样的时序再来一遍。第二个字节就可以送到指定设备的内部了,从机设备可以自己定义第二个字节和后续字节的用途。
一般第二个字节可以是寄存器地址或者是指令控制字等,
- 比如MPU6050定义的第二个字节就是寄存器地址;
- 比如AD转换器,第二个字节可能就是指令控制字;
- 比如存储器,第二个字节可能就是存储器地址。
图示里,主机发送这样一个波形,一一判定数据为0001 1001,即主机向从机发送了0x19这个数据。在MPU6050里就表示要操作0x19地址下的寄存器了。
接着同样是从机应答,主机释放SDA,从机拽住SDA,SDA表现为低电平,主机收到应答位为0,表示收到了从机的应答。
然后继续,同样的流程再来一遍,主机再发送一个字节,这个字节就是主机想要写入到0x19地址下寄存器的内容了。
如图示中就是发送了0xAA的波形,就表示,主机要在0x19地址下写入0xAA。
最后是接收应答位,如果主机不需要继续传输了,就可以产生停止条件(Stop,P),在停止条件之前,先拉低SDA,为后续SDA的上升沿作准备,然后释放SCL,再释放SDA。这样就产生了SCL高电平期间,SDA的上升沿。
这样一个完整的数据帧就拼接完成了。这个数据帧的目的就是,对于指定从机地址为1101000的设备在其内部0x19地址的寄存器中,写入0xAA这个数据。这就是指定地址写的时序。
4.3 当前地址读时序
当前地址读:对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)。
当前地址读时序,完成的任务是对于指定设备在当前地址指针指示的地址下读取从机数据。看上方的时序图,图示波形就是当前地址读的时序。如果主机想要读取从机的数据,就可以执行这个当前地址读时序。
波形最开始还是SCL高电平期间,拉低SDA,产生起始条件。起始条件开始后,主机必须首先调用发送一个字节来进行从机的寻址和指定读写标志位。比如图示的波形,表示本次寻址的目标是1101000的设备。同时,最后一位读写标志位为1,表示主机接下来想要读取数据。
紧跟着,发送一个字节之后,接收一下从机应答位。从机应答0,代表从机收到了第一个字节。
在从机应答之后,从应答波形结束位开始,数据的传输方向就要反过来了,因为刚才主机发出了读的指令,所以这之后,主机就不能继续发送了,要把SDA的控制权交给从机。
- 主机调用接收一个字节的时序,进行接收操作。
- 然后在第二个字节位开始,从机就得到了主机的允许,可以在SCL低电平期间写入SDA。
- 然后主机在SCL高电平期间读取SDA。
那最终,主机在SCL高电平期间依次读取8位,就接收到了从机发送的一个字节数据0000 1111,也就是0x0F。那这个0x0F是从机哪个寄存器的数据呢 ?可以观察到在读的时序中I2C协议的规定是,主机进行寻址时,一旦读写标志位给1了,下一个字节就要立马转为读的时序。所以主机还来不及指定想要读哪个寄存器,就得开始接收了,所以这里就没有指定地址这个环节。
那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据呢,这就需要用到之前说的当前地址指针了。在从机中,所有的寄存器被分配到了一个线性区域中,并且会有一个单独的指针变量,指示着其中一个寄存器,这个指针上电默认一般指向0地址。并且每写入一个字节和读出一个字节后这个指针就会自动自增一次,移动到下一个位置。那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值。
那假设,刚调用了指定地址写的时序,在0x19的位置写入了0xAA,那么指针就会+1,移动到0x1A的位置,再调用当前地址读的时序,返回的就是0x1A地址下的值。如果再调用一次,返回的就是0x1B地址下的值,以此类推。以上就是当前地址读时序的操作逻辑,由于当前地址读并不能指定读的地址,所以这个时序用的不是很多。
4.4 指定地址读
指定地址读:对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)。
指定地址读,这个时序的目的就是,对于指定设备,在指定地址下,读取从机数据。
先来看一下指定地址写的时序:
在第二个字节数据接收应答位和它之前的部分:
就是指定地址的时序,把最后面也就是第三个字节数据,写数据的这一部分给去掉。然后把前面一段第一和第二个字节数据,也就是设置地址,还没有指定写什么数据的时序,给追加到当前地址读时序的前面:
就得到了指定地址读的时序,一般也把它称作复合格式。
上面的时序在第二个字节数据应答位之后分隔一下,
- 前面的部分是指定地址写,但是只指定了地址,还没来得及写。
- 后面的部分是当前地址读,因为刚指定了地址,所以再调用当前地址读。
两者加在一起,就是指定地址读了。
所以,指定地址读的时序会复杂一些,来详细分析一下。
首先最开始,仍然是启动条件。然后发送一个字节,进行寻址,这里指定从机地址是1101000。读写标志位是0,代表主机要进行写的操作。
经过从机应答之后,再发送一个字节,第二个字节用来指定地址,这个数据就写入到了从机的地址指针里了。也就是说,从机接收到这个数据之后,它的寄存器指针就指向了0x19这个位置。
之后要写入的数据,不马上给0x19这个位置发,而是直接再来个起始条件,这个Sr(Start Repeat)的意思就是重复起始条件,相当于另起一个时序。因为指定读写标志位只能是跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件。
然后起始条件后,重新寻址并且指定读写标志位。此时读写标志位是1,代表主机要开始读了,接着主机接收一个字节,这个字节就是0x19地址下的数据。以上就是指定地址读。
另外在Sr这一位之前也可以再加一个停止条件,这样的话就是两个完整的时序了,先起始,写入地址,停止,因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失。就可以再起始,读当前位置,停止。这样两条时序也可以完成任务。但是I2C协议官方规定的复合格式是一整数据帧,就是先起始,再重复起始,再停止。相当于把两条时序拼接成一条了。
以上就是对三个I2C完整的时序介绍了,其中第一个指定地址写和第三个指定地址读用的比较多,也会是后续博文代码中使用的时序。
除了这三个时序,I2C还有这些时序的进阶版本,
- 图示时序指定地址写,只是写一个字节,当前地址读和指定地址读,也都是读一个字节,
- 进阶版本就是指定地址写多个字节,当前地址读多个字节和指定地址读多个字节。
进阶版本在时序上和图示示例的这些都非常相似,只需要增加一些小细节就行。
从指定地址写的波形图来看,在指定地址数据后写入一个字节,
- 如果只需要写一个字节,那就停止,就可以了。
- 如果想写多个字节,就可以把最后写字节数据这部分多重复几次。
比如在指定地址数据后,重复三遍发送一个字节和接收应答,这样第一个数据就写到了指定地址0x19的位置,因为写入一次数据后地址指针会自动+1,变成0x1A,所以第二个数据就写到了0x1A的位置,同理第三个数据就写到了0x1B的位置,后面如果还有写入数据就依次类推。这样这个时序就进阶为在指定的位置开始,按顺序写入多个字节。比如需要连续写入多个寄存器,就可以考虑这样来操作,这样在一条数据帧里就可以同时写入多个字节,执行效率就会比较高。
同理当前位置读和指定位置读也可以多次执行最后一部分字节数据的时序,由于地址指针在读后也会自增,所以这样就可以连续读出一片区域的寄存器,效率也会非常高。
如果只想读一个字节就停止的话,在读完一个字节之后,一定要给从机发个非应答(Send Ack,SA),非应答就是该主机应答的时候,主机不把SDA拉低,从机读到SDA位1,就代表主机没有应答。从机收到非应答之后,就知道主机不想要继续了,从机就会释放总线,把SDA控制权交还给主机。
如果主机读完任然给从机应答了,从机就会认为主机还想要数据,就会继续发送下一个数据,而这时,主机如果想产生停止,SDA可能就会因为被从机拽住了,而不能正常弹回高电平。如果主机想连续读取多个字节,就需要在最后一个字节给非应答,而之前的所有字节都要给应答。简单来说就是:
- 主机给应答了,从机就会继续发;
- 主机给非应答了,从机就不会再发了,交出SDA的控制权。
从机控制SDA发送一个字节的权利:
- 开始于读写标志位为1,
- 结束于主机给应答位为1。
这就是主机给从机发送应答位的作用。
总结
以上就是I2C总线的硬件规定和软件规定了,有了这些规定就可以按照硬件规定来连接线路;用软件规定来操作总线,以此实现指定位置写寄存器和指定位置读寄存器。有了这两个功能主机就可以完全掌控外挂模块的运行了,也就实现了设计这个协议的目的。