江协科技STM32学习笔记(第09章 I2C通信)

第09章 I2C通信

9.1 I2C通信协议

9.1.1 I2C通信

串口通信没有时钟线的异步全双工的协议。

案例:通信协议设计:

某个公司开发了一款芯片,可以干很多事情,比如AD转换、温湿度测量、姿态测量等等。这个芯片里的众多外设也是通过读写寄存器来控制运行的,寄存器本身也是存储器的一种,这个芯片所有的寄存器也都被分配到了一个线性的存储空间,如果我们想要读写寄存器来控制硬件电路,就至少需要定义两个字节数据,一个字节是我们要读写哪个寄存器,也就是指定寄存器的地址,另一个字节就是这个地址下存储器存的内容,写入内容就是控制电路,读出内容就是获取电路状态。这整个流程和单片机操作外设的原理是一样的。那么问题是,单片机读取自己的寄存器,可以直接通过内部的数据总线来实现,直接用指针操作就行,不需要我们操心,但是现在这个模块的寄存器在单片机的外面,要是直接把单片机内部的数据总线拽出来,把2个芯片合为一体是不现实的。所以,设计一个通信协议,在单片机外部连接少量几根线,实现单片机读写外部模块寄存器的功能。

举个例子,如果用串口实现,假设我们的数据包一共三位,前两位代表寄存器地址,后1为为数据内容进行发送。那么,新的需求又来了。

需求:

1.目前串口这个设计,是一个需要两根通信线的全双工协议,但是可以吗明显的发现,我们的这个操作流程是一种基于对话的形式来进行的,我们在整个过程中,并不需要同时进行发送或接收,发送的时候就不需要接收,接收的时候就不需要发送。这样就会导致始终存在一条信号线处于空闲状态,这就是资源的浪费。所以要求1是删掉一根通信线,只能在同一根线上进行发送和接收,也就是把全双工变成半双工。

.我们的串口通信并没有一个应答机制,也就是单片机发送了一个数据,对方有没有收到,单片 机完全不了解的,所以为了安全起见,要求增加应答机制,要求每发送一个字节,对方都要给我一个应答,每接收一个字节,我也要给对方一个应答。

3.一根线上能够同时接多个模块,单片机可以指定和任意一个模块通信,同时单片机在跟某个模块通信时,其它模块不能对正常的通信产生干扰。

4.这个串口是异步的时序,也就是发送方和接收方约定的传输速率是非常严格的,时钟不能有过大的偏差。也不能说,在传输过程中,单片机有点事,进中断了,这个时序能不能中断一下,对异步时序来说,这是不行的,单片机一个字节暂停了,接收方可是不知道的,它仍然会按照原来约定的那个速率读取,这就会导致传输出错。所以异步时序的缺点就是非常依赖硬件外设的支持,必须要有USART电路才能方便地使用,如果没有USART硬件电路地支持,那么串口是很难用软件来模拟的,虽然说软件模拟串口通信也是行得通的,但是由于异步时序,对时间要求很严格,一般我们很少用软件来模拟串口通信。所以要求是,把这个串口通信的协议改成同步的协议,另外加一条时钟线来指导对方读写,由于存在时钟线,对传输要求就不高了,单片机也可以随时暂停传输,去处理其它事情,因为暂停传输的同时,时钟线也暂停了,所以传输双方都能定格在暂停的时刻,等过一段时间再来继续,不会对传输造成影响。这就是同步时序的好处,使用同步时序就可以极大地降低单片机对硬件电路的依赖。即使没有硬件电路的支持,也可以很方便地使用软件手动翻转电平来实现通信。比如51单片机里,那个单片机就没有I2C的硬件外设,但是同样不影响51单片机进行软件模拟的I2C通信。异步时序的好处就是省一根时钟线,节省资源;缺点就是对时间要求严格,对硬件电路的依赖比较严重。同步时序的好处就是反过来,对时间要求不严格,对硬件电路不怎么依赖。在一些低端单片机,没有硬件资源的情况下,也很容易使用软件来模拟时序,缺点就是多一根时钟线。考虑到这个协议要主打下沉市场,所以需要一个同步的协议。

I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线;

两根通信线:SCLSerial Clock)、SDASerial Data);

SCL时钟线满足了使用同步的时序、降低对硬件的依赖,同时同步的时序稳定性也比异步时序更高;然后只有一根SDA数据线,就满足了要求1,变全双工为半双工,一根兼具发送和接收、最大化利用资源。

同步,半双工;

满足要求1。

带数据应答;

满足要求2。

支持总线挂载多设备(一主多从、多主多从)。

满足要求3。

一主多从的意思就是单片机作为主机,主导I2C总线的运行,挂载在I2C总线的所有外部模块都是从机,从机只有被主机点名之后才能控制I2C总线,不能在未经允许的情况下去碰I2C总线,防止冲突。我们使用I2C的场景绝大多数都是一主多从的场景,一个单片机作为主机,挂载一个或多个模块作为从机。

多主多从:也就是多个主机,多主多从的模型,在总线上任何一个模块都可以主动跳出来,说接下来我就是主机,你们都得听我的。就像是在教室里,老师正在讲话,突然有个同学站起来说,老师,打断一下,接下来让我来说,所有同学听我指挥。但是同一个时间只能有一个人讲话,这时候就发生了总线冲突。在总线冲突时,I2C协议会进行仲裁,仲裁胜利的一方取得总线控制权,失败的一方自动变回从机。当然由于时钟线也是由主机控制的,所以在多主机的模型下,还要进行时钟同步,多主机的情况下,协议是比较复杂的。

第1个图片是套件里的MPU6050模块, 可以进行姿态测量,使用了I2C通信协议;

第2个图片是套件里的OLED模块, 可以显示字符、图片等下信息,使用了I2C通信协议;

第3个图片是AT24C02、存储器模块,51单片机教程里学习I2C的模块;

第4个图片是DS3231、实时时钟模块,也是使用I2C通信。

作为一个通信协议,I2C必须要在硬件和软件上,都作出规定,硬件上的规定,就是你的电路应该如何连接,端口的输入输出模式都是啥样的。软件上的规定,就是你的时序是怎么定义的,字节如何传输,高位先行还是低位先行,一个完整的秩序由哪些部分构成。硬件的规定和软件的规定配合起来,就是一个完整的通信协议。

9.1.2 硬件电路

所有I2C设备的SCL连在一起,SDA连在一起;

假设我们就向下图这样连接电路,那如何规定每个设备SCL和SDA地输入输出模式呢?

SCL应该好归档,因为是一主多从,主机拥有SCL地绝对控制权,所以主机的SCL可以配置成推挽输出,所有从机的SCL都配置成浮空输入或者上拉输入。数据流向是主机发送,所有从机接收。但是到SDA线这里就比较麻烦了,因为这是半双工的协议,所以主机的SDA在发送的时候是输出,在接收的时候是输入,同样,从机的SDA也会在输入和输出之间反复切换。如果能协调好输入输出的切换时机,那其实也没问题,但是这样做,如果总线时序没协调好,极有可能发生两个引脚同时处于输出的状态。

设备的SCLSDA均要配置成开漏输出模式;

SCLSDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。

左边这个图就是I2C的一个典型电路模型,这是一个一主多从的模型,左边CPU就是我们的单片机,作为总线的主机,主机的权力很大,包括对SCL线的完全控制,任何时候,都是主机完全掌控SCL线,另外在空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机,这就是主机的权力。下面都是一系列被控IC,也就是挂载在I2C总线上的从机,这些从机可以是姿态传感器、OLED、存储器、时钟模块等等,从机的权利比较小。对于SCL时钟线,在任何时刻都只能被动的读取,从机不允许控制SCL线,对于SDA数据线,从机不允许主动发起对SDA的控制,只有主机发送读取从机的命令后,或者从机应答的时候,从机才能短暂地取得SDA地控制权,这就是一主多从模型中协议的规定。

右边这一块是SDA的结构,这里DATA就是SDA的意思,首先引脚的信号进来,都可以通过一个数据缓冲器或者是施密特触发器,进行输入,因为输入对电路没有任何影响,所以任何设备在任何时刻都是可以输入的,但是在输出的这部分,采用的是开漏输出的配置。输出低电平,这个开关管导通,引脚直接接地,是强下拉;输出高电平,这个开关管断开,引脚什么都不接,处于浮空状态,这样的话,所有的设备都只能输出低电平而不能输出高电平。为了避免高电平造成的引脚浮空,这时候就需要在总线外面,SCL和SDA各外置一个上拉电阻,这是通过一个电阻拉到高电平的,所以这是一个弱上拉。用之前二点弹簧和杆子模型来解释就是,SCL或SDA就是一根杆子,为了防止有人向上推杆子,有人向下拉杆子,造成冲突,我们就规定,所有的人不允许向上推杆子,只能选择向下拉或者放手。然后我们再外置一根弹簧向上拉,你要输出低电平,就往下拽,这跟弹簧肯定拽不赢你,所以弹簧被拉伸,杆子处于低电平状态,你要输出高电平,就放手,杆子在弹簧的拉力下,回弹到高电平。这就是一个弱上拉的高电平,但是完全不影响数据传输。这样做有什么好处呢?第一,完全杜绝了电源短路现象,保证电路的安全,所有人无论怎么拉杆子或者放手,杆子都不会处于一个被同时强拉和强推的状态,即使有多个人同时向下拉杆子,也没问题。第二,避免了引脚模式的频繁切换,开漏加弱上拉的模式,同时兼具了输入和输出的功能,你要是想输出,就去拉杆子或放手,操作杆子变化就行了,要是想输入,就直接放手,然后观察杆子高低就行了。因为开漏模式下,输出高电平就相当于断开引脚,所以在输入之前可以直接输出高电平,不需要再切换成输入模式了。第三,这个模式会有一个“线与”的现象,就是只要有一个或多个设备输出了低电平,总线就处于低电平,只有所有设备都输出高电平,总线才处于高电平,I2C可以利用这个电路特性,执行多主机模式下的时钟同步和总线仲裁,所以这里SCL虽然在一主多从模式下可以使用推挽输出,但是它仍然采用了开漏加上拉输出的模式,因为在多主机模式下会利用到这个特征。

9.1.3 I2C时序基本单元

起始条件:SCL高电平期间,SDA从高电平切换到低电平;

在I2C总线处于空闲状态时,SCL和SDA都处于高电平状态,也就是没有任何一个设备去碰SCL和SDA,SCL和SDA由外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态,当主机需要进行数据收发时,首先就要打破总线的宁静,产生一个起始条件,这个其实条件就是,SCL处于高电平不去动它,然后把SDA拽下来,产生一个下降沿,当从机捕获到这个SCL高电平,SDA下降沿信号时,就会进行自身的复位,等待主机的召唤。然后在SDA下降沿之后,主机要再把SCL拽下来,拽下SCL,一方面是占用这个总线,另一方面也是为了方便我们这些基本单元的拼接,就是我们之后会保证,处理起始和终止条件,每个时序单元的SCL都是以低电平开始,低电平结束。这样这些单元拼接起来,SCL才能续得上。

终止条件:SCL高电平期间,SDA从低电平切换到高电平。

SCL先放手,回弹到高电平,SDA再放手,回弹高电平,产生一个上升沿,这个上升沿触发终止条件,同时终止条件之后,SCL和SDA都是高电平,回归到最初的平静状态。

这个起始条件和终止条件就类似串口时序里的起始位和停止位。一个完整的数据帧,总是以起始条件开始,终止条件结束。另外起始和终止都是由主机产生的,从机不允许产生起始和终止,所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来,去碰总线。如果允许的话,那就是多主机模型了。

发送一个字节: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位数据,也就是1个字节。

另外要注意的是,这里是高位先行,所以第一位是一个字节的最高位B7,依次是次高位...最后发送最低为B0。这个和串口是不一样的,串口时序是低位先行,这里I2C是高位先行。

另外,由于这里有时钟线去同步,所以如果主机一个字节发送一半,突然进中断了,不操作SCL和SDA了,那时序就会在中断的位置不断拉长,SCL和SDA电平都暂停变化,传输也完全暂停,等中断结束后,主机回来继续操作,传输仍然不会出问题,这就是同步时序的好处,最后就是,由于这整个时序是主机发送一个字节,所以在这个单元里,SCL和SDA全程都由主机掌控,从机只能被动读取,这就是发送一个字节的时序。

接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA 。

释放SDA其实就相当于切换成输入模式,或者这样来理解,所有设备包括主机都始终处于输入模式,当主机需要发送的时候,就可以主动去拉低SDA,而主机在被动接收的时候,就必须先释放SDA,不要去动它,以免影响别人发送。因为总线是“线与”的特征,任何一个设备拉低了,总线就是低电平,如果你接收的时候,还拽着SDA不放手,那别人无论发什么数据,总线始终都是低电平,你自己给它拽着不放,还让别人怎么发送呢。所以主机在接收之前,需要先释放SDA。

从流程上来看,接收一个字节和发送一个字节是非常相似的,区别就是发送一个字节是,低电平主机发数据,高电平从机读数据;而接收一个字节是,低电平从机放数据,高电平主机读数据。

主机在接收之前要先释放SDA,然后这时从机就取得了SDA的控制权,从机需要发送0,就把SDA拉低,从机需要发送1,就放手,SDA回弹高电平;然后同样的,低电平变换数据,高电平读取数据。下图SDA时序实线部分表示主机控制的电平,虚线部分表示从机控制的电平,SCL全程由主机控制,SDA主机在接受前要释放,交由从机控制。之后还是一样,因为SCL始终是由主机控制的,所以从机的数据变换基本都是贴着SCL下降沿进行的;而主机可以在SCL高电平的任意时刻读取,这就是接收一个字节的时序。

应答机制分为发送应答和接收应答,它们的时序分别和发送一个字节,接收一个字节的其中一位是相同的。可以理解位发送给一位和接收一位,这一位就用来做应答。 

发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答;

接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。

右图:我们在调用发送一个字节之后,就要紧跟着接收应答的时序,用来判断从机有没有收到刚才给它的数据,如果从机收到了,那在应答位这里,主机释放SDA的时候,从机就应该立刻把SDA拉下来,然后在SCL高电平期间,主机读取应答位,如果应答位为0,就说明从机确实收到了。这个场景就是,主机刚发送一个字节,然后说有没有人收到啊,我现在要把SDA放手了哈,如果有人收到的话,就把SDA拽下来,然后主机高电平读取数据,发现,确实有人给它拽下来了,那就说明有人收到了;如果主机发现,我松手了,结果这个SDA就跟着回弹到高电平了,那就说明没有人回应我,刚发的一个字节可能没人收到,或者它收到了但是没给我回应,这就是发送一个字节,接收应答的流程。

左图:同理,在接收一个字节之后,我们也要给从机发送一个应答位,发送应答位的目的是告诉从机,你是不是还要继续发;如果从机发送一个数据后,得到了主机的应答,那从机就还会继续发送,如果从机没得到主机的应答,那从机就会认为,我发送了一个数据,但是主机不理我,可能主机不想要了吧,这时从机就会乖乖地释放SDA,交出SDA的控制权,防止干扰主机之的操作。

9.1.4 I2C时序

我们的I2C是一主多从的模型,主机可以访问从线上的任何一个设备,那如何发出指令,来确定要访问的是哪个设备呢,这就需要首先把每个设备都确定一个唯一的设备地址,从机设备地址就相当于每个设备的名字,主机在起始条件之后,要先发送一个字节叫一下从机名字,所有从机都会收到第一个字节,和自己的名字进行比较,如果不一样,则认为主机没有叫我,之后的时序我就不管了,如果一样,就说明,主机现在在叫我,那我就响应之后主机的读写操作。在同一条I2C总线里,挂载的每个设备地址必须不一样,否则主机叫一个地址,有多个设备响应,就乱套了。

从机设备地址,在I2C协议标准里分为7位地址和10位地址,目前只讲7位地址的模式,因为7位地址比较简单而且应用范围最广。在每个I2C设备出厂时,厂商都会为它分配一个7位的地址,这个地址具体是什么可以在芯片手册里找到。

比如MPU6050这个芯片的地址是1101 000;AT24C02的7位地址是1010 000。一般不同型号的芯片地址都是不同的,相同型号的芯片地址都是一样的。那如果有相同的芯片挂载在同一条总线上怎么办呢?这就需要用到地址中的可变部分了,一般器件地址的最后几位是可以在电路中改变的,比如MPU6050的最后一位,就可以由板子上的AD0引脚确定,这个引脚接低电平,那它的地址就是1101 000,这个引脚接高电平,那它的地址就是1101 001;比如AT24C02的最后三位,都可以分别由这个板子上的A0、A1、A2引脚确定,比如A0引脚接低电平,地址对应的位就是0,接高电平对应的位就是1,A17、A2也是同理。一般I2C的从机设备地址,高位都是由厂商确定的,低位可以由引脚来灵活切换,这样即使相同型号的芯片,挂载在同一个总线上,也可通过切换地址低位的方式,保证每个设备的地址都不一样,这就是I2C设备的从机地址。

指定地址写;

对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)。

Slave Address:从机地址;

Reg Address:某个设备内部的寄存器地址。

空闲状态,它两都是高电平, 然后主机需要给从机写入数据的时候;首先SCL高电平期间,拉低SDA,产生起始条件(Start,s),在起始条件之后,紧跟着的时序,必须是发送一个字节的时序,字节的内容,必须是从机地址+读写位,正好从机地址是7位,读写位是1位,加起来是一个字节,8位,发送从机地址,就是确定通信的对象,发送读写位,就是确定我们接下来是要写入还是要读出。具体发送的时候,在这里,低电平期间,SDA变换数据,高电平期间,从机读取SDA,绿色的线标明了从机读到的数据,比如上如所示波形,从机收到的第一位就是高电平1,然后SCL低电平,主机继续变换数据,因为第二位还是1,所以这里SDA电平没有变换,然后SCL高电平,从机读到第二位是1,之后继续,低电平变换数据,高电平读取数据,第三位就是0,这样持续8次,就发送了一个字节数据。其中这个数据的定义是,高7位表示从机地址,比如这个波形下,主机寻找的地址就是1101 000,这个就是MPU6050的地址;然后最低为表示读写位,0表示,之后的时序,主机要进行写入操作;1表示之后的时序主机要进行读出操作,这里是0说明之后我们要进行写入操作。目前,主机发送了一个字节,字节的内容转换为16进制,高位先行,就是0xD0,然后根据协议规定,紧跟着的单元,就得是接收从机的应答位(Receive Ack,RA),在这个时刻,主机要释放SDA,由于SDA被从机拽住了,所以主机松手后,SDA并没有回弹高电平,这个过程就代表从机产生了应答,最终高电平期间,主机读取SDA,发现是0,就说明我进行寻址,有人给我应答了,传输没问题;如果主机读取SDA,发现是1,就说明我进行寻址,应答位期间,我松手了,但是没拽住它,没人给我应答,那就直接产生停止条件,并提示一些信息,这就是应答位。然后从机产生上升沿,从机释放SDA,交出SDA的控制权,因为从机要在低电平尽快变换数据,所以这个上升沿和SCL的下降沿几乎是同时发生的。继续往后,之前我们读写位给了0,所以我们继续发送一个字节,同样的时序,再来一遍,第二个字节,就可以送到指定设备的内部了。从机设备可以自己定义第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址或者是指令控制字等,比如MPU6050定义的第二个字节就是寄存器地址;AD转换器,第二个字节可能就是指令控制字;存储器第二个字节可能就是存储器地址。接着是从机应答,主机释放SDA,从机拽住SDA,SDA表现为低电平,主机收到应答位为0,表示收到了从机的应答。然后继续,同样的流程再来一遍,主机再发送一个字节,这个字节就是主机想要写入到0x19地址寄存器的内容了,最后是接收应答位。如果主机不需要继续传输了,就可以产生停止条件(Stop,p),再停止条件之前,先拉低SDA,为后续SDA的上升沿做准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间,SDA的上升沿,这样一个完整的数据帧就拼接完了。

这个数据帧的作用就是,对于指定从机地址位1101 000的设备,在其内部0x19地址的寄存器中,写入0xAA这个数据。

当前地址读;

对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)。

最开始,还是SCL高电平期间,拉低SDA,产生起始条件, 起始条件开始后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位,比如图示的波形,表示本次寻址的目标是1101 000的设备,同时,最后一位读写标志为1,表示主机接下来需要读取数据,紧跟着发送一个字节之后,接收一下从机应答位,从机应答0,代表从机收到了第一个字节,在从机应答之后,从这里开始,数据的传输方向就要反过来了,因为主机刚刚发了读的命令,所以主机就不能再继续发送了,要把SDA的控制权交给从机,主机调用接收一个字节的时序,进行接收操作。从机得到主机的允许后,可以再SCL低电平期间,写入SDA,然后主机再SCL高电平期间读取SDA,那最终,主机在SCL高电平期间,依次读取8位,就接收到了从机发送的一个字节数据0000 1111也就是0x0F,那这个0x0F是从机哪个寄存器的数据呢?在读的过程中,I2C协议的规定是,主机进行寻址时,一旦读写标志位给1了,下一个字节就要立马转为读的时序,所以主机还来不及指定,我想要读哪个寄存器,就得开始接收了,所以这里没有指定地址的这个环节,那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据呢?就需要用到上面说的,当前地址指针了,在从机中,所有的寄存器被分配到了一个线性区域中,并且会有一个单独的指针变量,指示着其中一个寄存器,这个指针上电默认一般指向0地址,并且每写入一个字节和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置,那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值。

那假设,刚刚调用了这个指针地址写的时序,在0x19的位置写入了0xAA,那么指针就会+1,移动到0x1A的位置,再调用当前地址读的时序,返回的就是0x1A地址下的值,如果在调用一次返回的就是0x1B地址下的值,依次类推。这就是当前地址读时序的操作逻辑,由于当前地址读并不能指定读的地址,所以这个时序用的不是很多。

指定地址读;

对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)。

最开始,仍然是启动条件, 然后发送一个字节进行寻址,这里指定从机地址是1101000,读写标志位是0,代表我要进行写的操作,经过从机应答之后,再发送一个字节;第二字节用来指定地址,这个数据就写入到了从机的地址指针里了,也就说说从机接收到这个数据之后,它的寄存器指针就指向了0x19这个位置,之后我们要写入的数据,不给它发,而是直接再来个起始条件,这个Sr(Start Repeat)的意思就是重复起始条件,相当于另起一个时序,因为指定读写标志位只能是跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件,然后起始条件后,重新寻址并且指定读写标志位,此时读写标志位是1,代表我要开始读了。接着主机接收一个字节,这个字节就是0x19地址下的数据。这就是指定地址读。在写入时序后面也可以再加一个停止条件,这样也行,这样的话,这就是两个完整的时序了。先起始,写入地址,停止,因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失,我们就可以再起始,读当前位置,停止。这样两条时序,也可以完成任务。但是I2C官方规定的复合格式是一整个数据帧,就是先起始,再重复起始,再停止,相当于把两条时序拼接成1条了。

 如果指向读一个字节就停止的话,一定要给从机发个非应答(Send Ack,SA),非应答就是该主机应答的时候,主机不把SDA拉低,从机读到SDA为1,就代表主机没有应答,从机收到非应答之后,就知道主机不想要再继续了,从机就会释放总线,把SDA的控制权交还给主机。如果主机读完仍然给从机应答了,从机就会认为主机还想要数据,就会继续发送下一个数据,而这时,主机如果想要产生停止条件,SDA可能就会因为被从机拽住了,而不能正常弹回高电平。如果主机想连续读取多个字节,就需要再最后一个字节给非应答,而之前的所有字节都要给应答。简单来说就是主机如果给应答了,从机就会继续发,主机给非应答了,从机就不会再发了,交出SDA的控制权。从机控制SDA发送一个字节的权利,开始于读写标志位1,结束于主机给应答为1,这就是主机给从机发送应答位的作用。

9.2  MPU6050简介

9.2.1 MPU6050简介

MPU6050是一个6轴姿态传感器,可以测量芯片自身XYZ轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角(欧拉角),常应用于平衡车、飞行器等需要检测自身姿态的场景;

欧拉角是什么:以飞机为例,欧拉角就是飞机机身相对于初始3个轴的夹角:飞机机头下倾或者上仰,这个轴的夹角叫做俯仰,Pitch;飞机机身左翻滚或者右翻滚,这个轴的夹角叫作滚转,Roll;飞机机身保持水平,机头向左转或者向右转向,这个轴的转向叫做偏航,Yaw。

为了保持飞机姿态平稳,那么得到一个精确且稳定的欧拉角就至关重要;但是可惜的是,加速度计、陀螺仪、磁力计任何一种传感器都不能获得精确且稳定的欧拉角,要想获得精确且稳定的欧拉角,就必须进行数据融合,把这几种传感器的数据结合起来,综合多种传感器的数据,取长补短,这样才能获得精确且稳定的欧拉角。常见的数据融合算法,一般有互补滤波、卡尔曼滤波等等,这就涉及到惯性导航领域里,姿态结算的知识点了。本节课的侧重点是I2C通信,最终的程序现象就是把这些传感器的原始数据读出来,显示在OLED上,就结束了。所以有关姿态解算的内容暂时不会涉及。

3轴加速度计(Accelerometer):测量XYZ轴的加速度;

在XYZ轴,这个芯片内部都分别布置了一个加速度计。如下图所示:水平的这根线是感应轴线,中间是一个具有一定质量、可以左右滑动的小滑块,左右各有一个弹簧顶着它。如果把这个东西拿在手上,来回晃,中间这个小滑块就会左右移动去压缩或者拉伸两边的弹簧,当滑块移动时,就会带动上面的电位器滑动,这个电位器其实就是一个分压电阻,然后我们测量电位器输出的电压,就能得到小滑块所受到的加速度值了。这个加速度计,实际上就是一个弹簧测力计,根据就牛顿第二定律,F=ma,我们想测量加速度a,就可以找一个单位质量的物体,测量它所受的力F就行了。

加速度计具有静态稳定性,不具有动态稳定性。

3轴陀螺仪传感器(Gyroscope):测量XYZ轴的角速度。

当中间的旋转轮高速旋转时,根据角动量守恒的原理,这个旋转轮具有保持它原有角动量的趋势,这个趋势可以保持旋转轴方向不变,当外部物体的方向转动时,内部的旋转轴方向并不会转动,这就会子啊平衡环连接处产生角度偏差,如果我们在连接处放一个旋转的电位器,测量电位器的电压,就能得到旋转的角度了。从这里分析,陀螺仪是可以直接得到角度的,但是我们这个MPU6050的陀螺仪,并不能直接测量角度,可能是结构的差异或者是工艺的限制。我们这个芯片测量的实际上是角速度,而不是角度。

如果想通过角速度得到角度,只需要对角速度进行积分即可,角速度积分,就是角度,和加速度计测角度也是一样。这个角速度积分得到的角度也有局限性,就是当物体静止时,角速度值会因为噪声无法完全归零,然后经过积分的不断累积,这个小噪声就会导致计算出来的角度产生缓慢的漂移。也就是角速度积分得到的角度经不起时间的考验。不过这个角度呢,无论是运动还是静止都是没问题的,不会受物体运动的影响。

所以总结下来就是,陀螺仪具有动态稳定性、不具有静态稳定性。

加速度计具有静态稳定性,不具有动态稳定性;陀螺仪具有动态稳定性、不具有静态稳定性。这两种传感器的特性正好互补,所以我们取长补短,进行一下互补滤波。就能融合得到静态和动态都稳定的姿态角了。

如果芯片里再集成一个3轴的磁场传感器,测量XYZ轴的磁场强度,那就叫做9轴传感器;如果再集成一个气压传感器,测量气压大小,那就叫做10轴传感器。一般气压值反应的是高度信息,海拔越高,气压越低,所以气压计是单独测量垂直地面的高度信息的。

9.2.2 MPU6050参数

16位ADC采集传感器的模拟信号,量化范围:-32768~32767

加速度计满量程选择:±2±4±8±16g

-32768~32767对应±2、±4±8、±16等模拟量。

陀螺仪满量程选择: ±250±500±1000±2000°/sec

-32768~32767对应±250、±500±1000±2000°/sec)等模拟量。

可配置的数字低通滤波器

可以配置寄存器来选择对输出数据进行低通滤波,如果觉得输出数据抖动太厉害,就可以加一点低通滤波。这样输出数据就会平缓一些。

可配置的时钟源

可配置的采样分频

上面两个参数是配合使用的,时钟源经过分频器的分频,可以为AD转换和内部其它电路提供时钟。控制分频系数就是,就可以控制AD转换的快慢了。

I2C从机地址:1101000AD0=0

                        1101001(AD0=1

在程序中,如果用16进制表示上述7位地址,一般有两种表示方式,以1101000为例:

第一种,单纯地把这7位二进制数转换为16进制,这里110 1000低4位和高3位切开,转换16进制就是0x68,所以有的地方就说MPU6050的从机地址是0x68;如果认为0x68是从机地址的话,在发第一个字节时,要先把0x68左移1位,在按位或上读写位,读1写0。

第二种,把0x68左移一位后的数据当作从机地址,0x68左移一位之后是0xD0,那种这样MPU6050的从机地址就是0xD0,这时,在实际发送第一个字节时,如果要写,就把0xD0当作第一个字节,如果要读,就把0xD0或上0x01,即0xD1当作第一个字节。这种表示方式就不需要进行左移的操作了。或者说这种表示方式是把读写位也融入到从机地址里了。

9.2.3 硬件电路

LDO:低压差线性稳压器。 

右边MPU6050芯片:芯片本身的引脚是非常多的,包括时钟、I2C通信引脚、供电、帧同步等等,不过这里很多引脚我们都用不到,还有一些引脚,是这个芯片的最小系统里的固定连接,这个最小系统电路,一般手册里都会有。

左下角:引出来的引脚有VCC和GND,这两个引脚是电源供电,然后SCL和SDA,SCL和SDA,在模块内部已经内置了两个4.7K的上拉电阻了,所以在接线的时候,直接把SCL和SDA接在GPIO口就行了。不需要再在外面另外接上拉电阻了。XCL和XDA,这两个是芯片里面的主机I2C通信引脚,设计这两个引脚是为了扩展芯片功能,MPU6050是一个6轴姿态传感器,但是只有加速度计和陀螺仪的6个轴,融合出来的姿态角是有缺陷的,这个缺陷就是绕Z轴的角度,也就是偏航角,它的漂移无法通过加速度计进行纠正。就相当于坐在车里,不看任何窗户,然后辨别当前车子的行驶方向,短时间内可以通过陀螺仪得知方向的变化,从而确定变化后的行驶方向,但是时间一长,车子到处转弯,没有稳定的参考了,就会迷失方向,所以这时候就要带个指南针在上边,提供长时间的稳定偏航角进行参考。XCL和XDA,通常就是用于外界磁力计或者气压计,当接上磁力计或气压计之后,MPU6050的主机接口可以直接访问这些扩展芯片的数据。把这些扩展芯片的数据读取到MPU6050里面,在MPU6050里面,会有DMP单元进行数据融合和姿态解算。当然如果不需要MPU6050的姿态解算功能的话,也可以把这个磁力计或者气压计直接挂载在SCL和SDA这条总线上,因为I2C本来就可以挂载很多设备,所以把多个设备挂载在一起也是没问题的。这是XCL和XDA的用途。

        AD0:接低电平的话,7位从机地址就是1101 000;接高电平的话,7位从机地址就是1101 001。电路中可以看到,有一个电阻,默认弱下拉到低电平了。所以引脚悬空的话,就是低电平。如果想接高电平,就可以把AD0直接引到VCC,强上拉至高电平。

        INT:终端输出引脚,可以配置芯片内部的一些事件,来触发中断引脚的输出,比如数据准备好了,I2C主机错误等等。另外芯片内部还内置了一些实用的小功能,比如自由落体检测、运动检测、零运动检测等。这些信号都可以触发INT引脚产生电平跳变,需要的话可以进行中断信号的配置,如果不需要的话,就可以不配置。

LDO:供电的逻辑,从手册里可以查到,这个MPU6050芯片的VDD供电是2.375~3.46V,属于3.3V供电的设备,不能直接接5V,所以为了扩大供电范围,这个模块的设计者就加了个3.3V的稳压器,输入端电压VCC_5V可以在3.3V到5V之间,然后经过3.3V的稳压器,给芯片端供电。右边的电源指示灯,只要3.3V端有电,电源指示灯就会亮,所以这一块需不需要可以根据项目要求来。如果已经有了稳定的3.3V电源了,就不再需要这一部分了。

引脚

功能

VCCGND

电源

SCLSDA

I2C通信引脚

XCLXDA

主机I2C通信引脚

AD0

从机地址最低位

INT

中断信号输出

9.2.4 MPU6050框图 

左上角是时钟系统,有时钟输入脚和输出脚,不过我们一般使用内部时钟。硬件电路那里,CLKIN直接接了GND,CLKOUT没有引出,所以这部分不需要过多关心。

下面灰色的部分就是芯片内部的传感器,这个芯片还内置了一个温度传感器,用它来测量温度也是没问题的。这么多传感器本质上也都相当于可变电阻,通过分压后,输出模拟电压,然后通过ADC,进行模数转换,转换完成之后,这些传感器的数据同一都放到数据寄存器中,我们读取数据寄存器就能得到传感器测量的值了。这个芯片内部的转换,都是全自动进行的,就类似我们之前学的AD连续转换+DAM转运,每个ADC输出,对应16位的数据寄存器,不存在数据覆盖的问题,我们配置好转换频率之后,每个数据就自动以我们设置的频率刷新到数据寄存器,我们需要数据的时候,直接来读就行了。每个传感器都有个自测单元(Self test),这部分是用来验证芯片好坏的,当启动自测后,芯片内部就会模拟一个外力施加在传感器上,这个外力导致传感器数据会比平时大一些,那如何进行自测呢?我们可以线使能自测,读取数据;再失能自测,读取数据,两个数据一相减,得到的数据叫自测响应,这个自测响应,芯片手册里给出了一个范围,如果自测响应在这个范围内,就说明芯片没有问题,如果不在,就说明芯片可能坏了,使用的话就要小心点。

Charge Pump:电荷泵,或者叫充电泵,CPOUT引脚需要外接一个电容,什么样的电容在手册里有说明,电荷泵是一种升压电路,在其它地方也有出现过,比如我们用的OLED屏幕,里面就有电荷泵进行升。

电荷泵的升压原理:首先电池和电容并联,电池给电容充电,充满之后,电容也相当于一个5V的电池了,然后修改电路的接法,让电源与电容串联起来,这样就相当于输出10V电压了,不过由于电容电荷较少,用一下就不行了,所以这个并联、串联的切换速度要快,乘电容还没放电完,就要及时并联充电,这样一直持续,并联充电、串联放电、并联充电、串联放电,然后后续在加个电源滤波,就能进行平稳地升压了。这就是电荷泵地升压原理。

由于陀螺仪内部是需要一个高电压支持的, 所以这里设计了一个电荷泵进行升压,这个升压过程是自动的,不需要我们管。

右边这一大块就是寄存器和通信接口的部分了。

中断状态寄存器:可以控制内部的哪些事件到中断引脚的输出;

FIFO:先入先出寄存器,可以对数据流进行缓存;

Config Registers(配置寄存器):可以对内部的各个电路进行配置;

传感器寄存器:也就是数据寄存器,存储了各个传感器的数据;

工厂校准:这个意思就是内部的传感器都进行了校准;

Digital Motion Processor(DMP):数字运动处理器,是芯片内部自带一个姿态解算的硬件算法,配合官方的DMP库,可以进行姿态解算;

FSYNC:帧同步;

上面部分用于和STM32通信,下面这一部分是主机的I2C通信接口,用于和MPU6050扩展的设备进行通信;

Serial Interface Bypass Mux:接口旁路选择器,就是一个开关,如果拨到上面,辅助的I2C引脚就和正常的I2C引脚接到一起,这样两路总线就合在一起了,STM32可以控制所有设备;如果拨到下面,辅助的I2C引脚就由MPU6050控制,两条I2C总线独立分开。

Bias&LDO:供电部分。

9.2.5 MPU6050器件说明书

9.2.6   MPU6050寄存器映像手册

寄存器总表:

第1列是16进制表示的寄存器地址;第2列是十进制表示的寄存器地址;第3列是寄存器的名称;第4列是读写权限,RW代表可读可写,R代表只读;后面是寄存器内的每一位的名字。

寄存器英文名称中文名称
SMPLRT_DIV

采样分频器

CONFIG配置寄存器
GYRO_CONFIG陀螺仪配置寄存器
ACCEL_CONFIG加速度计配置寄存器
ACCEL_XOUT_H数据寄存器

加速度计XYZ轴

(_H表示高8位,_L表示低8位)

ACCEL_XOUT_L
ACCEL_YOUT_H
ACCEL_YOUT_L
ACCEL_ZOUT_H
ACCEL_ZOUT_L
TEMP_OUT_H

温度传感器

(_H表示高8位,_L表示低8位)

TEMP_OUT_L
GYRO_XOUT_H

陀螺仪XYZ轴

(_H表示高8位,_L表示低8位)

GYRO_XOUT_L
GYRO_YOUT_H
GYRO_YOUT_L
GYRO_ZOUT_H
GYRO_ZOUT_L
PWR_MGMT_1电源管理寄存器1
PWR_MGMT_2电源管理寄存器2
WHO_AM_I器件ID号

(1)SMPLRT_DIV(采样分频器) :里面的8位作为一个整体,作为分频值,这个寄存器可以配置采样频率的分频系数,简单来说就是分频越小,内部的AD转换越快,数据寄存器刷新就越快;反之越慢

采样频率(数据刷新率) = 陀螺仪输出时钟频率/(1+分频值)

这个时钟就是我们刚才说的那个时钟源, 内部晶振、陀螺仪晶振和外部时钟引脚的方波,这里直接就是以外陀螺仪晶振作为例子了,陀螺仪时钟/这个寄存器指定的分频系数,就是采样频率。不使用低通滤波器时,陀螺仪时钟为8KHz,使用滤波器了,时钟就是1KHz了。

(2)CONFIG(配置寄存器):配置寄存器,内部有两部分,外部同步设置和低通滤波器配置。

低通滤波器,可以选择上表所示各个参数,这个低通滤波器可以让输出数据更加平滑,配置滤波器参数越大,输出数据抖动就越小,0是不使用低通滤波器,陀螺仪时钟为8KHz,之后使用了滤波器,陀螺仪时钟就是1KHz, 最大的参数是保留位,没有用到。

(3)GYRO_CONFIG(陀螺仪配置寄存器):高3位是XYZ轴的自测使能位,中间两位是满量程选择位,后面三位没用到

自测的用法:

自测响应 = 自测使能时的数据-自测失能时的数据 

上电后,先使能自测,读取数据,再失能自测,读取数据,两者相减得到自测响应。如果在下图所示范围里,芯片就通过了自测。

满量程选择:量程越大,范围越广,量程越小,分辨率越高。

(4)ACCEL_CONFIG(加速度计配置寄存器):

高三位是自测使能位,中间两位是满量程选择,最后三位是配置高通滤波器的,这个高通滤波器就是内置小功能,运动检测用到的,对数据输出没有影响。

(5) 加速度计数据寄存器

我们想读取数据的话,直接读取数据寄存器就行了。

这是一个16进制的有符号数,以二进制补码的方式存储,我们读出高8位和低8位,高位左移8次,或上低位数据,最后再存在一个int16_t的变量里, 这样就可以得到数据了。

(6)温度传感器数据寄存器

(7)陀螺仪数据寄存器 

(8)电源管理寄存器1

第1位、设备复位, 这一位写1,所有寄存器都恢复到默认值;

第2位、睡眠模式,这一位写1,芯片睡眠,芯片不工作,进入低功耗;

第3位、循环模式,这一位写1,设备进入低功耗,过一段时间,启动一次,并且唤醒的频率,由下一个寄存器的高2位确定

第5位、温度传感器失能,写1之后,禁用那日不的温度传感器;

最后3位、用来选择系统时钟来源。

上表: 内部晶振、XYZ轴陀螺仪晶振、外部引脚的两个方波,一般我们选择内部晶振或者陀螺仪晶振。

(9)电源管理寄存器2

后面6位可以分别控制6个轴进入待机模式,如果只需要部分轴的数据,可以让其它轴待机,这样比较省电。 

(10)器件ID号 

这个寄存器是只读的,ID号不可以修改,ID号中间这6位固定为110100,实际上这个ID号就是这个芯片的I2C地址,它的最高位和最低为其实都是0,那读出这个寄存器,值就固定位0x68。AD0引脚的值并不反映在这个寄存器上,意思就是,之前我们说过这个I2C地址可以通过AD0引脚进行配置, 但是这里的ID号的最低位是不随AD0引脚变化而变化的,读出ID号,始终都是0x68,当然这个ID号也不是非要和I2C地址一样。

所有的寄存器上电默认值都是0x00,除了下面这两个。

117是器件ID号,107是电源管理寄存器1,默认是0x40,也就是 次高位为1,这里次高位是SLEEP,所以这个芯片上电默认就是睡眠模式,我们在操作它之前,要先记得接触睡眠,否则操作其它寄存器是无效的。

9.3 软件I2C读写MPU6050

9.3.1 硬件电路

9.3.2 软件部分

(1)复制《OLED显示屏》工程并改名为《软件I2C读写MPU6050》

(2)添加驱动文件

(3) MyI2C.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/*封装SCL电平翻转函数*/
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction) BitValue);
	Delay_us(10);
}
/*封装SDA电平翻转函数*/
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction) BitValue);
	Delay_us(10);
}
/*封装读取SDA函数*/
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

/*初始化函数*/
/*我们使用的是软件I2C,所以库函数I2C不用看了,自己实现*/
/*
第1个任务:把SCL和SDA都初始化为开漏输出模式
第2个任务:把SCL和SDA置高电平
*/
void MyI2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);        
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;         //把GPIO端口配置成开漏输出模式
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStruct);
	GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);         //释放总线,让SCL和SDA处于高电平
}

/*I2C起始条件函数*/
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);	    //释放SCL和SDA,让SCL和SDA处于空闲状态,为了能够兼容重复起始条件,将释放SDA放在前面
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);     //先拉低SDA
	MyI2C_W_SCL(0);     //再拉低SCL
}
/*I2C终止条件函数*/
/*stop开始时,SCL和SDA都已经是低电平了,但是在释放时序单元开始时,SDA不一定是低电平,为了确保之后释放SDA能产生上升沿,
所以再时序单元开始时先拉低SDA,然后再释放SCL、释放SDA*/
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);	    
	MyI2C_W_SCL(1);     
	MyI2C_W_SDA(1);       
}
/*I2C发送字节函数*/
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for(i=0;i<8;i++)    //循环发送8位数
	{
		MyI2C_W_SDA(Byte & (0x80>>i));   //在SCL处于低电平状态,将数据发送出去
		MyI2C_W_SCL(1);                  //释放SCL,从机立刻把刚才放在SDA的数据读走
		MyI2C_W_SCL(0);                  //再拉低SCL,驱动时钟走一个脉冲,可以继续放下一位数据	
	}
}
/*接收字节函数*/
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i,Byte = 0x00;
	MyI2C_W_SDA(1);	       //主机释放SDA,把权限交给从机 
	for(i=0;i<8;i++)
	{
		MyI2C_W_SCL(1);        //主机释放SCL,这时就能读取数据了
		if(MyI2C_R_SDA() == 1){ Byte |= (0x80 >> i); }      //读取数据 
		MyI2C_W_SCL(0);        //拉低SCL,开始读取下一位数据
	}
	return Byte;
}
/*I2C发送应答函数*/
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);             //函数进来时,SCL处于低电平状态,主机把数据放到SDA上。
	MyI2C_W_SCL(1);                  //释放SCL,从机立刻把刚才放在SDA的数据读走
	MyI2C_W_SCL(0);                  //再拉低SCL,驱动时钟走一个脉冲,可以继续放下一位数据	
}
/*I2C接收应答函数*/
/*函数进来时,SCL低电平,主机释放SDA,防止干扰从机*/
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);	           //主机释放SDA,然后从机把应答位放在SDA上
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();    //主机读取应答位
	MyI2C_W_SCL(0);            //SCL低电平,进入下一个时序单元
	return AckBit;
}

(4) MyI2C.h

#ifndef __MYI2C_
#define __MYI2C_
void MyI2C_W_SCL(uint8_t BitValue);
void MyI2C_W_SDA(uint8_t BitValue);
uint8_t MyI2C_R_SDA(void);
void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);
#endif

(5) MPU6050_Reg.h

/*定义我们要用到的MPU6050内部寄存器的名称*/
#ifndef __MPU6050_
#define __MPU6050_

#define	MPU6050_SMPLRT_DIV		0x19
#define	MPU6050_CONFIG			0x1A
#define	MPU6050_GYRO_CONFIG		0x1B
#define	MPU6050_ACCEL_CONFIG	0x1C

#define	MPU6050_ACCEL_XOUT_H	0x3B
#define	MPU6050_ACCEL_XOUT_L	0x3C
#define	MPU6050_ACCEL_YOUT_H	0x3D
#define	MPU6050_ACCEL_YOUT_L	0x3E
#define	MPU6050_ACCEL_ZOUT_H	0x3F
#define	MPU6050_ACCEL_ZOUT_L	0x40
#define	MPU6050_TEMP_OUT_H		0x41
#define	MPU6050_TEMP_OUT_L		0x42
#define	MPU6050_GYRO_XOUT_H		0x43
#define	MPU6050_GYRO_XOUT_L		0x44
#define	MPU6050_GYRO_YOUT_H		0x45
#define	MPU6050_GYRO_YOUT_L		0x46
#define	MPU6050_GYRO_ZOUT_H		0x47
#define	MPU6050_GYRO_ZOUT_L		0x48

#define	MPU6050_PWR_MGMT_1		0x6B
#define	MPU6050_PWR_MGMT_2		0x6C
#define	MPU6050_WHO_AM_I		0x75

#endif

(6) MPU6050.c

#include "stm32f10x.h"                  //Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS 0xD0            //MPU6050的从机地址

/*使用I2C向MPU6050发送数据*/
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(Data);
	MyI2C_ReceiveAck();
	MyI2C_Stop();
}
/*使用I2C向MPU6050读取数据*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	
	MyI2C_Start();        //重新指定起始位
	MyI2C_SendByte(MPU6050_ADDRESS|0xD1);      //现在要让MPU6050写地址,最后一位通过或操作变为1
	MyI2C_ReceiveAck();
	Data = MyI2C_ReceiveByte();
	MyI2C_Stop();
	return Data;
}

/*MPU6050初始化通信函数*/
void MPU6050_Init(void)
{
	MyI2C_Init();
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);    //配置电源管理寄存器1
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);    //配置电源管理寄存器2
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);    //配置分频寄存器,10分频
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);        //配置配置寄存器
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);   //配置陀螺仪配置寄存器
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);   //配置陀螺仪配置寄存器
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);  //配置加速度计配置寄存器
}
/*获取MPU6050ID号的函数*/
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}	

/*获取MPU6050数据寄存器的函数*/
void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ,
					int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ)
{
	uint8_t DataH,DataL;
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8)| DataL;
}

(7) MPU6050.h

#ifndef __MPU6050_
#define __MPU6050_
void MPU6050_Init(void);
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ,
					int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ);
#endif

(8) mian.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"
#include "MPU6050.h"

int16_t AX,AY,AZ,GX,GY,GZ;
uint8_t ID;

int main(void)
{
	OLED_Init();                           // 初始化OLED屏幕
	MPU6050_Init();
//	MPU6050_WriteReg(0x6B,0x00);           //向MPU6050写入数据前,要先解除电源管理寄存器2的睡眠模式
//	MPU6050_WriteReg(0x19,0x66);           //向采样分频器写入0x66      
//	uint8_t ID = MPU6050_ReadReg(0x75);    //读取ID寄存器
//	uint8_t Data = MPU6050_ReadReg(0x19);    //读取ID寄存器
//	OLED_ShowHexNum(1,1,Data,2);
	OLED_ShowString(1,1,"ID:");
	ID = MPU6050_GetID();
	OLED_ShowHexNum(1,4,ID,2);
	while(1)
	{	
        MPU6050_GetData(&AX,&AY,&AZ,&GX,&GY,&GZ);  
		OLED_ShowSignedNum(2,1,AX,5);
		OLED_ShowSignedNum(3,1,AY,5);
		OLED_ShowSignedNum(4,1,AZ,5);
		OLED_ShowSignedNum(2,8,GX,5);
		OLED_ShowSignedNum(3,8,GY,5);
		OLED_ShowSignedNum(4,8,GZ,5);
	}
}

9.4 I2C通信外设

9.4.1 I2C外设简介

STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担;

支持多主机模型;

固定多主机:一个教室里多位老师发言,学生不允许发言;

可变多主机:教室里的一个或多个学生均可发言。STM32是按照可变多主机设计的。

支持7/10位地址模式;

支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz);

这个标准速度是协议规定的标准速度,也就是说如果某个设备声称支持快速的I2C,那它就最大支持400KHz的时钟频率,当然,作为一个同步协议,这个时钟并不严格,所以只要不超过这个最大频率,多少都可以,所以这个频率的具体值,我们一般关注不多。

支持DMA;

在多字节传输的时候,可以提高传输效率,比如指定地址读多字节或写多字节的时序,如果我想要连续读或写非常多的字节,那用一下DMA自动帮我们转运数据,这个过程效率就会大大提升。如果只有几个字节,那就没必要用DMA了。

兼容SMBus协议;

SMBus(System Management Bus):系统管理总线,SMBus是基于I2C总线改进而来的,主要用于电源管理系统中,SMBus和I2C非常像,所以STM32的I2C外设计就顺便兼容了一下SMBus。

STM32F103C8T6 硬件I2C资源:I2C1I2C2。

使用软件I2C,想开几路开几路。

9.4.2 STM32的I2C外设框图

左边是这个外设的通信引脚SDA和SCL ,这就是I2C通信的两个引脚,SMBALERT是SMBus用的,I2C用不到。像这种外设模块引出来的引脚,一般都是借用GPIO口的复用模式与外部世界相连的,具体是复用在了哪个GPIO口呢?

上面这一步分,是SDA,也就是数据控制部分, 数据收发的核心部分是这里的数据寄存器和数据移位寄存器,当我们需要发送数据时,可以把一个字节数据写到数据寄存器DR,当移位寄存器没有数据移位时,这个数据寄存器的值就会进一步,转移到移位寄存器里,在移位的过程中,我们就可以直接把下一个数据放到数据寄存器里等着了。一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送,当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的TXE位为1,表示发送寄存器为空,这是发送的流程。那在接收时,也是这一路,输入的数据一位一位的,从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位RXNE,表示接收寄存器非空,这时我们就可以把数据寄存器读出来了。这个流程和串口那里一样,串口的数据收发也是由数据寄存器和移位寄存器两级实现的,只不过串口是全双工,这里数据收和发是分开的;在I2C这里,是半双工,所以数据收发是同一组数据寄存器和移位寄存器,但是这个数据寄存器和移位寄存器的配合,设计思路都是异曲同工,有了这一块,SDA的数据收发就可以完成了。至于什么时候收,什么时候发,需要我们写入控制寄存器的对应位进行操作。对于起始条件、终止条件、应答位什么的,这里也都有控制电路可以完成。

比较器和地址寄存器是从机模式用的,STM32的I2C是基于可变多主机模型设计的,STM32不进行通信的时候,就是从机,既然作为从机,它就可以被别人召唤,想被别人召唤,就应该有从机地址,从机地址是多少就可以由这个自身地址寄存器指定,我们可以自定义一个从机地址,写到这个寄存器,当STM32作为从机,在被寻址时,如果收到的寻址通过比较器判断,和自身地址相同,那STM32就作为从机,响应外部主机的召唤。并且这个STM32支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器,这一块我们需要在多主机的模型下来理解,把角色转换一下,STM32作为从机,才需要有这一部分。我们只需要一主多从的模型,STM32就不会作为从机,这一块就不需要使用。

帧错误校验(PEC)计算:这是STM32设计的一个数据校验模块,当我们发送一个多字节的数据帧时,在这里硬件可以自动执行CRC校验计算,CRC是一种很常见的数据校验算法,它会根据前面这些数据,进行各种数据运算,然后会得到一个字节的校验位,附加在这个数据帧后面,在接收到这一帧数据后,STM32的硬件也可以自动执行校验的判定,如果数据在传输的过程中出错了,CRC校验算法就通不过,硬件就会置校验错误标志位,告诉你数据错了,使用的时候注意点。这个数据校验过程就跟串口的奇偶校验差不多,也是用于进行数据有效性验证的。

SCL部分,时钟控制是用来控制SCL线的,在这个时钟控制寄存器写对应的位,电路就会执行对应的功能。控制逻辑电路也是黑盒子,写入控制寄存器,可以对整个电路进行控制,读取状态寄存器,可以得知电路的工作状态。之后是中断,当内部有一些标志位置1之后,可能事件比较紧急,就可以申请中断,如果我们开启了这个中断,那当这个事件发生之后,程序就可以跳到中断函数来处理这个事件了。

最后是DMA请求与响应,在进行很多字节的收发时,可以配合DMA来提高效率。

9.4.3 STM32的I2C基本结构

首先,移位寄存器和数据寄存器DR的配合是通信的核心部分, 这里因为I2C是高位先行,所以这个移位寄存器是向左移位,在发送的时候,最高位先移出去,然后是次高位等等,一个SCL时钟,移位一次,移位8次,这样就能把一个字节,由高位到低位,依次放到SDA线上了,那在接收的时候,数据通过GPIO口从右边依次移进来,最终移8次,一个字节就接收完成了。之后GPIO口这里,使用硬件I2C的时候,这两个对应的GPIO口,都要配置成复用开漏输出的模式,复用就是GPIO的状态是交由片上外设来控制的,开漏输出,这是I2C协议要求的端口配置,之前也说过,这里即使是开漏输出模式,GPIO口也是可以进行输入的,然后SCL这里,时钟控制器通过GPIO去控制时钟线,这里简化成一主多从的模型了,所以时钟这里只画了输出的方向。

SDA的部分,输出数据通过GPIO输出到端口,输入数据也是通过GPIO输入到移位寄存器。

最后,有个开关控制,也就是I2C_Cmd,配置好了,就使能外设,外设就能正常工作了。 

实际上,如果是多主机的模型,时钟线也是会进行输入的,这个时钟的输入可以先不管。

9.4.4 硬件I2C的操作流程

9.4.4.1 主机发送

9.4.4.2 主机接收

9.4.5 软件/硬件I2C波形对比

9.5 硬件I2C写MPU6050

9.5.1 硬件电路

9.5.2 软件部分

(1)复制《软件I2C读写MPU6050》并更改名为《硬件I2C读写MPU6050》

(2)I2C库函数

void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_DMACmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_DMALastTransferCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);         //生成起始条件
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);          //生成终止条件
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);     //配置应答使能,在收到一个字节之后,是否给从机应答
void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);
void I2C_DualAddressCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GeneralCallCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_ITConfig(I2C_TypeDef* I2Cx, uint16_t I2C_IT, FunctionalState NewState);
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);                          //发送数据,写数据到数据寄存器DR
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);                                  //读取DR的数据,作为返回值
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);   //发送7位地址的专用函数
uint16_t I2C_ReadRegister(I2C_TypeDef* I2Cx, uint8_t I2C_Register);
void I2C_SoftwareResetCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_NACKPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_NACKPosition);
void I2C_SMBusAlertConfig(I2C_TypeDef* I2Cx, uint16_t I2C_SMBusAlert);
void I2C_TransmitPEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_PECPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_PECPosition);
void I2C_CalculatePEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
uint8_t I2C_GetPEC(I2C_TypeDef* I2Cx);
void I2C_ARPCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_StretchClockCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_FastModeDutyCycleConfig(I2C_TypeDef* I2Cx, uint16_t I2C_DutyCycle);
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);        //基本状态监控,监控
uint32_t I2C_GetLastEvent(I2C_TypeDef* I2Cx);
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT);

(3)删除“ MyI2C.c”和“ MyI2C.h”,更改“MPU6050.c” 和“MPU6050.h”

(4)MPU6050.c

#include "stm32f10x.h"                  //Device header
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS 0xD0            //MPU6050的从机地址

/*带超时退出的状态监测函数*/
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	uint32_t TimeOut;
	TimeOut = 10000;
	while(I2C_CheckEvent(I2Cx,I2C_EVENT)!=SUCCESS)   
	{
		TimeOut--;
		if(TimeOut == 0)
		{
			break;              //超时跳出循环,防止程序卡死,更严谨应该增加错误处理函数
		}	 
	}

}

/*使用I2C向MPU6050发送数据*/
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
	
	I2C_GenerateSTART(I2C2,ENABLE);            //起始条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);       //等待EV5状态监测完成
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);  //配置发送从机地址以及是要发送还是接收,发送数据自带接收应带功能
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);   //等待EV6事件完成
	I2C_SendData(I2C2,RegAddress);             //发送要写到的外设寄存器的地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);   //等待EV8事件完成
	I2C_SendData(I2C2,Data);                   //发送数据
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);    //等待EV8_2事件完成
	I2C_GenerateSTOP(I2C2,ENABLE);             //终止条件
	
}
/*使用I2C向MPU6050读取数据*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	I2C_GenerateSTART(I2C2,ENABLE);            //起始条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);   //等待EV5状态监测完成
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);  //配置发送从机地址以及是要发送还是接收,发送数据自带接收应带功能
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);   //等待EV6事件完成
	I2C_SendData(I2C2,RegAddress);             //发送要写到的外设寄存器的地址
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);   //等待EV8事件完成
	I2C_GenerateSTART(I2C2,ENABLE);            //生成重复起始条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);   //等待EV5状态监测完成
	I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);     
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //等待EV6事件完成
	I2C_AcknowledgeConfig(I2C2, DISABLE);           //设置ACK=0,不给应答
	I2C_GenerateSTOP(I2C2,ENABLE);                  //申请产生终止条件
	MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED); //等待EV7事件完成
	Data = I2C_ReceiveData(I2C2);
	I2C_AcknowledgeConfig(I2C2, ENABLE);           //设置默认状态下ACK就是1,给从机应答
	return Data;
}

/*MPU6050初始化通信函数
第1步:配置I2C外设,对I2C2外设进行初始化;
第2步:控制外设电路,实现指定地址写的时序;
第3步:控制外设电路,实现指定地址读的时序;
*/
void MPU6050_Init(void)
{
	/*第1步:初始化I2C外设*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;   //复用开漏模式
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStruct);
     
	I2C_InitTypeDef I2C_InitStruct;
	I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
	I2C_InitStruct.I2C_ClockSpeed = 50000;
	I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
	I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
	I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	I2C_InitStruct.I2C_OwnAddress1 = 0x00;
	I2C_Init(I2C2,&I2C_InitStruct);
	I2C_Cmd(I2C2,ENABLE);
	
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);    //配置电源管理寄存器1
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);    //配置电源管理寄存器2
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);    //配置分频寄存器,10分频
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);        //配置配置寄存器
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);   //配置陀螺仪配置寄存器
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);   //配置陀螺仪配置寄存器
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);  //配置加速度计配置寄存器
}
/*获取MPU6050ID号的函数*/
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}	

/*获取MPU6050数据寄存器的函数*/
void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ,
					int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ)
{
	uint8_t DataH,DataL;
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8)| DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8)| DataL;
}

(5)MPU6050.h

#ifndef __MPU6050_
#define __MPU6050_
void MPU6050_Init(void);
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX,int16_t *AccY,int16_t *AccZ,
					int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ);
#endif

(6)main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"
#include "MPU6050.h"

int16_t AX,AY,AZ,GX,GY,GZ;
uint8_t ID;

int main(void)
{
	OLED_Init();                           // 初始化OLED屏幕
	MPU6050_Init();
//	MPU6050_WriteReg(0x6B,0x00);           //向MPU6050写入数据前,要先解除电源管理寄存器2的睡眠模式
//	MPU6050_WriteReg(0x19,0x66);           //向采样分频器写入0x66      
//	uint8_t ID = MPU6050_ReadReg(0x75);    //读取ID寄存器
//	uint8_t Data = MPU6050_ReadReg(0x19);    //读取ID寄存器
//	OLED_ShowHexNum(1,1,Data,2);
	OLED_ShowString(1,1,"ID:");
	ID = MPU6050_GetID();
	OLED_ShowHexNum(1,4,ID,2);
	while(1)
	{	
        MPU6050_GetData(&AX,&AY,&AZ,&GX,&GY,&GZ);  
		OLED_ShowSignedNum(2,1,AX,5);
		OLED_ShowSignedNum(3,1,AY,5);
		OLED_ShowSignedNum(4,1,AZ,5);
		OLED_ShowSignedNum(2,8,GX,5);
		OLED_ShowSignedNum(3,8,GY,5);
		OLED_ShowSignedNum(4,8,GZ,5);
	}
}

  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值