目录
串口通信协议和I2C通信协议的联系
同步和异步的区别
I2C通信
硬件电路
指定地址写的流程
当前地址读的流程
指定地址读的流程
声明:本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记,我之所以记录下来是为了方便自己日后复习。如果你也是跟着江科大的视频学习的,可以配套本专栏食用,如有问题可以QQ交流群:963138186
本节学习I2C通信。
事先说明:在看本节之前最好要有51的基础,因为本节很多内容都会省略不写,没有51基础的学习者可以先去看看我之前在51单片机专栏介绍的I2C协议。
单片机学习笔记---AT24C02(I2C总线)_i2c发送应答和接收应答区别-CSDN博客
单片机学习笔记---AT24C02数据存储_at24c02c-CSDN博客
有51基础的学习者直接往下看。
串口通信协议和I2C通信协议的联系
在学I2C之前,我们已经学习了串口通信。串口通信就是从TX引脚向RX引脚发送数据流。数据流以字节为单位,我们可以组合多个字节,变成多字节的数据包传输。
另外,串口通信的设计是一条发送线,一条接收线,没有时钟线的异步全双工的协议,这是我们学习串口通信的时候了解到的。
现在假如有个大公司找到了,出资一千万,要求给他设计一个通讯协议,这个通讯协议是用来干什么的?
大概是这样:这个公司开发出来一款芯片可以干很多事情,比如AD转换温湿度测量,姿态测量等等。
像我们单片机一样,这个芯片里的众多外设也都是通过读写寄存器来控制运行的。寄存器本身也是存储器的一种,这个芯片所有的寄存器也都是被分配到了一个线性的存储空间。如果我们想要读写寄存器来控制硬件电路,我们就至少需要定义两个字节数据。一个字节是我们要读写哪个寄存器,也就是指定寄存器的地址。另一个字节就是这个地址下存储器存的内容。写入内容就是控制电路,读出内容就是获取电路状态。这整个流程和我们单片机CPU操作外设的原理是一样的。
现在问题来了,单片机读写自己的寄存器,可以直接通过内部的数据总线来实现,直接用指针操作就行,不需要我们操心。但是现在这个模块的寄存器在单片机的外面,要是直接把单片机内部的数据总线拽出来,把两个芯片合为一体,可能不太现实。
所以现在这个大公司就找到了你,要求给他设计一种通讯协议,在单片机和外部模块连接少量的几根线,实现单片机读写外部模块寄存器的功能。这时你可能会想,我们就用这个串口的数据包通信就可以完成任务。比如我们就用Hex数据包,定义一个三个字节的数据包,从单片机向外挂模块发过去。第一个字节表示读写,发送0表示这是一个写数据包,发送1表示这是一个读数据包。第二个字节表示读写的地址。第三个字节表示写入的数据。
比如发送数据包为0x00,0x06, 0xAA。这就表示在0x06地址下写入0xAA,模块收到之后就执行这个写入操作。如果发送数据包为0x01,0x06,0x00,这就表示要读取0x06地址下的数据,注意这个读的数据包,第三个字节无效。模块收到之后就要再给我发送一个字节,返回0x06地址下的数据,这样就行了,是不是完成任务了?
你认为这一千万已经到手了,但是这个大公司对这个通讯协议的要求非常多。
第一个要求:目前串口这个设计是一个需要两根通信线的全双工协议,但是可以明显发现,我们这个操作流程是一种基于对话的形式来进行的。我们在整个过程中并不需要同时进行发送和接收,发送的时候,就不需要接收,接收的时候就不需要发送,这样就会导致始终存在一根信号线,处于空闲状态,这就是资源的浪费。所以要求就是删掉一根通讯线,只能在同一根线上进行发送和接收,也就是把全双工变成半双工。
第二个要求:我们这个协议并没有一个应答机制,也就是单片机发送了一个数据,对方有没有收到,单片机是完全不了解的。所以为了安全起见,大公司要求增加应答机制,要求每发送一个字节,对方都要给个应答,每接收一个字节,也要给对方一个应答。
第三个要求:大公司说这一根线只能接一个模块不太行,他要求这一根线上能同时接多个模块。单片机可以指定和任意一个模块通信,同时单片机在跟某个模块进行通信时,其他模块不能对正常的通信产生干扰。
第四个要求:这个串口是异步的时序。也就是发送方和接收方约定的传输速率是非常严格的,时钟不能有过大的偏差,也不能说是在传输过程中,单片机有点事儿进中断了,这个时序能不能暂停一下,对于异步时序来说,这是不行的。单片机一个字节发一半暂停了,接收方不知道,他仍然会按照原来个约定的速率读取,这就会导致传输出错。所以异步时序的缺点就是非常依赖硬件外设的支持。必须要有USART电路才能方便地使用。如果没有USART硬件电路的支持,串口是很难用软件来模拟的。虽然说软件模拟串口通信也是行得通的,但是由于异步时序对时间要求很严格,一般我们很少用软件来模拟串口通信。所以这个大公司的要求是,要把这个协议改成同步的协议,另外加一条时钟线来指导对方读写。由于存在时钟线,对传输的时间要求就不高了。单片机也可以随时暂停传输去处理其他事情。因为暂停传输的同时,时钟线也暂停了,所以传输双方都能定格在暂停的时刻,可以过一段时间再来继续,不会对传输造成影响。这就是同步时序的好处,使用同步时序就可以极大的降低单片机对硬件电路的依赖。即使没有硬件电路的支持,也可以很方便的用软件手动翻转电平来实现通信。比如我们之前51单片机里就没有I2C的硬件外设。但是同样不影响51单片机进行软件模拟的I2C通信。
同步和异步的区别
异步时序的好处就是省一根时钟线,节省资源,缺点就是对时间要求严格,对硬件电路的依赖比较严重。
同步时序的好处就是反过来,对时间要求不严格,对硬件电路不怎么依赖。在一些低端单片积没有硬件资源的情况下,也很容易使用软件来模拟时序。缺点就是多一根时钟线。
本节我们最基本的任务是通过通信线实现单片机读写外挂模块寄存器的功能。其中至少要实现在指定的位置写寄存器和在指定的位置读寄存器这两个功能。实现了读写寄存器就实现了对这个外挂模块的完全控制。
并且不要忘了还要实现刚刚说的那个大公司提出的4点要求。
I2C通信
我之前在51单片机专栏写过I2C总线的介绍,没有基础的学习者可以去看看
单片机学习笔记---AT24C02(I2C总线)_i2c发送应答和接收应答区别-CSDN博客
单片机学习笔记---AT24C02数据存储_at24c02c-CSDN博客
I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
两根通信线:SCL(Serial Clock)串行时钟线(满足设计要求四)、SDA(Serial Data)串行数据线(满足设计要求一)
同步,半双工(满足设计要求一)
带数据应答(满足设计要求二)
支持总线挂载多设备(一主多从、多主多从)(满足设计要求三)
挂载在I2C总线的所有外部模块都是从机,从机只有被主机点名之后才能控制I2C总线,不能在未经允许的情况下去碰I2C总线,防止冲突。
多主多从的模型,即在总线上任何一个模块都可以主动跳出来说接下来我就是主机,你们都得听我的,就像是在教室里,老师正在讲课,突然一个学生站出来说,老师打断一下,接下来让我来说,所有同学听我指挥。但是同一个时间只能有一个人说话,这时就相当于发生了总线冲突。在总线冲突时,I2C协议会进行仲裁,仲裁胜利的一方取得总线控制权,失败的一方自动变回从机。当然由于时钟线也是由主机控制的,所以在多主机的模型下还要进行时钟同步。多主机的情况下,协议是比较复杂的,大家感兴趣的,可以自己去了解。我们本课程仅使用一主多从的模型,多主多从的部分不做要求。
之前学习51单片机时我们用的是AT24C02存储器模块来学习I2C,现在STM32我们是用MPU6050模块来学习I2C协议的。
上图第一个图片就是我们套件里的MPU6050模块,可以进行姿态测量,使用了I2C通信协议。
第二个图片是我们套件里的OLED模块,可以显示字符,图片等信息,也是I2C协议。
第三个图片是AT24C02存储器模块。
第四个图片是DS3231,实时时钟模块,也是使用I2C通信。
还有很多模块都支持I2C通信,使用了这个通信协议对于开发者来说非常方便。同样的协议,在不同的硬件上,操作方法都是极为相似的。学会了其中一个硬件,再学其他的硬件就很容易了。
有关I2C是怎么实现功能的解释,我之前在51单片机专栏都详细写过,不懂的去翻翻。在这里就不再赘述了。
单片机学习笔记---AT24C02(I2C总线)_i2c发送应答和接收应答区别-CSDN博客
单片机学习笔记---AT24C02数据存储_at24c02c-CSDN博客
硬件电路
关于硬件电路51单片机专栏中也已经写过,但是这里适当再补充一下
所有I2C设备的SCL连在一起,SDA连在一起
设备的SCL和SDA均要配置成开漏输出模式
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
如何规定每个设备SCL和SDA的输入输出模式?
SCL应该好规定,因为现在是一主多从,主机拥有SCL的绝对控制权,所以主机的SCL可以配置成推挽输出,所有从机的SCL都配置成浮空输入或者上拉输入。数据流向是主机发送,所有从机接收,这没问题,但是到SDA线这里就比较麻烦了,因为这是半双工的协议,所以主机的SDA在发送的时候是输出,在接收的时候是输入。同样从机的SDA也会在输入和输出之间反复切换。如果能协调好输入输出的切换时机,其实也没问题。但这样做,如果总线时序没协调好,极有可能发生两个引脚同时处于输出的状态。如果这时又正好是一个输出高电平,一个输出低电平,这个状态就是电源短路板。这个状态是要极力避免的。所以为了避免总线没协调好,导致电源短路这个问题。I2C的设计是禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构。
CPU和所有的被控设备IC它引脚的内部结构都是下图这样的:
左边这一块是SCL的结构,这里SCLK就是SCL的意思。右边这一块是SDA的结构,这里Data就是SDA的意思。
首先,引脚的信号进来,都可以通过一个数据缓冲器或者是斯密特触发器进行输入。因为输入对电路没有任何影响,所以任何设备在任何时刻都是可以输入的。
正常的推挽输出是这样的:上面一个开关管接到正极,下面一个开关管接到负极。上面导通输出高电平,下面导通输出低电平。因为这是通过开关管直接接到正极和负极的,所以这个是强上拉和强下拉的模式。
而开漏输出就是去掉这个强上拉的开关管,输出低电平时下管导通是强下拉。输出高电平时下管断开,但是没有上管了,此时引脚处于浮空的状态,这就是开漏输出。
所以这个图在输出的这部分采用的是开漏输出的配置。
和上图示是一样的,输出低电平这个开关管导通引脚,直接接地是强下拉。
输出高电平,这个开关管断开,引脚什么都不接,处于浮空状态。
这样的话,所有的设备都只能输出低电平,而不能输出高电平。为了避免高电平造成的引脚浮空,这时就需要在总线外面,SCL和SDA各外置一个上拉电阻,这是通过一个电阻拉到高电平的,所以这是一个弱上拉。
用我们之前的弹簧和杆子的模型来解释,就是SCL或SDA就是一根杆子。为了防止有人向上推杆子,有人向下拉杆子造成冲突,我们就规定所有的人不准向上推杆子,只能选择向下拉或者放手。然后我们再外置一根弹簧向上拉,要输出低电平就往下拽,这根弹簧肯定拽不赢你,所以弹簧被拉伸,杆子处于低电平状态。要输出高电平就放手杆子,在弹簧的拉力下回弹到高电平,这就是一个弱上拉的高电平,但是完全不影响数据传输。
这样做有什么好处?
第一,完全杜绝了电源短路现象,保证电路的安全。所有人无论怎么拉杆子或者放手杆子,都不会处于一个被同时强拉和强推的状态。即使有多个人同时向下拉杆子也没问题。
第二,避免了引脚模式的频繁切换。开漏加弱上拉的模式,同时兼具了输入和输出的功能。要是想输出,就去拉杆子或者放手,操作杆子变化就行了。要是想输入,就直接放手,然后观察杆子高低就行了。因为开漏模式下,输出高电平就相当于断开引脚,所以在输入之前可以直接输出高电平,不需要再切换成输入模式。
- 就是这个模式会有个“线与”的现象,就是只要有任意一个或多个设备输出了低电平总线就处于低电平,只有所有的设备都输出高电平,总线才处于高电平。I2C可以利用这个电路特征,执行多主机模式下的时钟同步和总线仲裁。所以这里SCL虽然在一组多从模式下可以用推挽输出,但是它仍然采用了开漏加上拉输出的模式,因为在多主机模式下会利用到这个特征。
关于I2C时序的讲解可以去翻一翻我的51单片机专栏中写的,这里不再赘述
单片机学习笔记---AT24C02(I2C总线)_i2c发送应答和接收应答区别-CSDN博客
单片机学习笔记---AT24C02数据存储_at24c02c-CSDN博客
这里特别说明一些重点:
- SCL全程都是有主机控制的。
- SCL低电平时,主机或者从机SDA变换数据,SCL高电平时,从机或者主机读取数据。
- 起始和终止都是有主机产生的,从机不允许产生起始和终止。所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来去碰总线。如果允许的话,就是多主机模型了,我们暂时不讲多主机模型。
- 在发送一个字节的时候,之前学的串口的时序是低位先行,I2C是高位先行。
- 所有设备包括主机都始终处于输入模式,当主机需要发送的时候,就可以主动去拉低SDA。而主机在被动接收的时候,就必须先释放SDA,不要去动它,以免影响别人发送。因为总线是线与的特征,任何一个设备拉低的总线就是低电平,如果接收的时候还拽着SDA不放手,别人无论发什么数据,总线都始终是低电平。
- 从流程上来看,接收一个字节和发送一个字节是非常相似的。区别就是发送一个字节是低电平主机放数据,高电平从机读数据,而接收一个字节是低电平从机放数据,高电平主机读数据。
- 从机设备地址在I2C协议标准里分为七位地址和十位地址,我们目前只讲七位地址的模式,因为七位地址比较简单,而且应用范围最广。
- 在每个I2C设备出厂时,厂商都会为它分配一个七位的地址。这个地址具体是什么?可以在芯片手册里找到。比如我们MPU6050这个芯片的七位地址是1101000。之前我们学习AT24C02的七位地址是1010000。一般不同型号的芯片地址都是不同的,相同型号的芯片地址都是一样的。如果有相同的芯片挂载在同一条总线怎么办?这需要用到地址中的可变部分。一般器件地址的最后几位是可以在电路中改变的,比如MPU6050地址的最后一位就可以由这个板子上的AD0引脚确定,这个引脚接低电平,那它的地址就是1101 000,这个引脚接高电平,它的地址就是1101 001。
- 再比如AT24C02地址的最后三位都可以分别由这个板子上的A0、A1、A2引脚确定。比如A0引脚接低电平,地址对应的位就是0,接高电平,地址对应的位就是1。A1、A2也同理。
- 一般I2C的从机设备地址的高位都是由厂商确定的,低位可以由引脚来灵活切换。这样即使相同型号的芯片挂载在同一个总线上,也可以通过切换地址低位的方式,保证每个设备的地址都不一样,这就是I2C设备的从机地址。
- 从机地址是七位,读写位是一位,加起来是一个字节八位。
- 发送从机地址,就是确定通信的对象;
- 发送读写位就是确认接下来是要写入还是要读出。
指定地址写的流程
对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
当前地址读的流程
对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
现在问题就来了,这个0x0F是从机哪个寄存器的数据?
我们看到在读的时序中,I2C协议的规定是主机进行寻址时,一旦读写标志位给1了,那下一个字节就要立马转为读的时序,所以主机还来不及指定想要读哪个寄存器就得开始接收了。所以这里就没有指定地址这个环节。主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据?
这需要用到我们上面说的当前地址指针。在从机中,所有的寄存器被分配到了一个线性区域中。并且会有个单独的指针,变量指示着其中一个寄存器。这个指针上电默认一般指向零地址。并且每写入一个字节和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置。在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值。
比如,假设刚刚调用了这个指定地址写的时序,在0x19的位置写入了0xAA,那么指针就会加1移动到0x1A的位置,接下来再调用这个当前地址读的时序,返回的就是0x1A地址下的值。如果再调用一次,返回的就是0x1B地址下的值,以此类推。这就是当前地址读时序的操作逻辑,由于当前地址读并不能指定读的地址,所以这个时序用的不是很多。
指定地址读的流程
对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
指定地址读这个时序的目的就是对于指定设备,在指定地址下读取从机数据。
这个时序为什么能指定读的地址?
我们看一下指定就是写,在这前面一部分就是指定地址的时序。我们把最后面的写数据的这一部分给去掉。
然后把前面这一段设置地址还没有指定写什么数据的时序给它追加到这个当前地址读时序的前面
就得到了指定地址读的时序,一般我们也把它称作复合格式。
这个时序在这里分隔一下,前面的部分是指定地址写,但是只指定的地址还没来得及写。
后面的部分是当前地址读,因为我们刚指定的地址,所以再调用当前地址读两者加在一起,就是指定地址读了。
所以指定地址读的时序会复杂一些,我们来详细分析一下:
首先最开始仍然是启动条件,然后发送一个字节进行寻址。这里指定从机地址是1101000,读写标志位是0,代表要进行写的操作。经过从机应答之后,再发送一个字节,第二个字节用来指定地址,这个数据就写入到了从机的地址指针里了。也就是说,从机接收到这个数据之后,它的寄存器指针就指向了ox19这个位置。之后我们要写入的数据,不给它发,而是直接再来个起始条件。SR的意思就是重复起始条件(Start Repeat),相当于另起一个时序。
因为指定读写标志位只能是跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件,然后起始条件后重新寻址,并且指定读写标志位,此时读写标志位是1代表要开始读了。接着主机接收一个字节,这个字节就是0x19地址下的数据,这就是指定地址读。
也可以再加一个停止条件,这样的话就是两个完整的时序了。
因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失,我们就可以再起始读当前位置,停止,这样两条时序也可以完成任务,但是I2C协议官方规定的复合格式是一整个数据帧,相当于把两条时序拼接成一条。
除了以上三个时序,I2C还有这些时序的进阶版本。目前指定地址写只是写一个字节,当前地址读和指定地址读也都是读一个字节。进阶版本就是指定地址写多个字节,当前地址读多个字节和指定地址读多个字节,时序上和以上三个时序都非常相似,只需要增加一些小细节就行。
比如在这里指定地址,然后写入一个字节,如果只想写一个字节,就停止就行了。
如果想写多个字节,就可以把这最后一部分多重复几次。
比如重复三遍,发送一个字节和接收应答。这样第一个数据就写入到了指定地址0x19的位置,写入一个数据后,地址指针会自动加1,变成0x1A,所以这第二个数据就写入到了,0x1A的位置。同理,第三个数据就写入的是0x1B的地址。以此类推,这样这个时序就进阶为在指定的位置开始按顺序连续写入多个字节。比如需要连续写入多个寄存器,就可以考虑这样来操作。这样在一条数据帧就可以同时写入多个字节,执行效率就会比较高。然后同理,当前位置读和指定位置读,也可以多次执行这最后一部分时序,由于地址指针在读后也会自帧,所以这样就可以连续读出一片区域的寄存器,效率也会非常高。
注意:如果只想读一个字节就停止的话,在读完一个字节之后,一定要给从机发个非应答。非应答就是该主机应答的时候,主机不把SDA拉低,从机读到SDA为1,就代表主机没有应答。从机收到非应答之后,就知道主机不想要继续了,从机就会释放总线,把SDA的控制权交还给主机。如果主机读完,仍然给从机应答了,从机就会认为主机还想要数据,就会继续发送下一个数据。而这时主机如果想产生停止条件,SDA可能就会因为被从机拽住了,而不能正常弹回高电平。
如果主机想连续读取多个字节,就需要在最后一个字节给非应答,而之前的所有字节都要给应答。
我们可以按照硬件规定来连接线路,用软件规定来操作总线,依次实现指定位置写寄存器和指定位置读寄存器。有了这两个功能主机就可以完全掌控外挂模块的运行了,也就实现了我们设计这个协议的目的。
下节我们来了解MPU6050这个芯片,看看它是怎样工作的,有哪些寄存器,以及如何利用寄存器控制硬件电路的运行。
QQ交流群:963138186
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓