这一讲来和大家介绍另一款非常有用的模块---MPU6050。这是一款集成度比较高的模块,它的内部集成了多种传感器。根据集成传感器种类的多少,MPU6050分高端,中端,低端几种不同版本,高端的版本可以输出更多的传感信息,低端的版本输出的信息会相对较少。具体的区别如下:
高端版:可以输出X-Y-Z三个方向的加速度,X-Y-Z三个方向的角速度,X-Y-Z三个方向的偏转角,温度,海拔,气压,经纬度等等。。。除了集成了这么多传感信息以外,高端版还自带卡尔曼滤波,这是非常好的,因为如果没有卡尔曼滤波,你获得的信号数据会有很多噪音,没有办法直接拿来使用。比如,你要测量某个方向上的偏转角,如果没有卡尔曼滤波,那么你得到的初始数据波动会很大,导致你根本无法确定当前的状态值,所以你需要先对这些初始数据进行滤波,然后才能得到一串比较稳定的数据。不过由于高端版的MPU6050本身植入了卡尔曼滤波功能,所以它输出的所有信号数据都是稳定且可直接使用的。
中端版:只输出X-Y-Z三个方向的加速度,X-Y-Z三个方向的角速度,X-Y-Z三个方向的偏转角。同时可喜的是,中端版也带有卡尔曼滤波,所以中端版的数据也是非常稳定的,可以直接拿来使用。
低端版:低端版一般只输出X-Y-Z方向的加速度信号和角速度信号,并且它不带有卡尔曼滤波。所以使用起来比较麻烦也很考验专业功底。假如你不懂卡尔曼滤波的话(其实我也不懂,只知道是一种滤波算法),那么光是自己编制滤波程序可能就够你花上一段时间的。
在版本选择上我本人用的是中端版,因为考虑到高端版价格比较高。在淘宝上面一般高端的MPU6050要100块左右,中端的60块左右,低端版只需要10多块钱。不过我个人用下来还是觉得一分钱一分货,不差钱的话还是推荐直接买高端版本的省时省力,不过假如你是那种喜欢捣鼓底层算法并且想成为一个深度专业人士的话可以用用低端版的。
了解了MPU6050的功能后,我们其实可以隐约感觉到 MPU6050的广泛应用了:手机,四轴飞行器,飞机,平衡车等等产品内部都有它的身影。比如手机里面的指南针,气压,加速度什么的,全都是运用这款模块采集而来的,还有手机横屏竖屏之间的转换也都仰仗MPU6050,平衡车之所以不倒也是仰仗MPU6050。既然MPU6050这么腻害,那是不是很复杂呢?是的。不过你别慌,复杂指的是它的结构原理,对于我们消费者使用来讲,一点都不难。当然前提是你买的高端版或者中端版啦,低端版玩不转别找我。。。下面就来介绍下MPU怎么使用。
本篇以本人用的一款中端版为例,介绍两大部分内容:1.MPU6050的结构和简单的作用原理;2.MPU6050的软件部分,即驱动程序和内部的数据结构。
先看硬件部分,图1是MPU6050的外形结构图。可见MPU6050的体积是非常小的,也就是指尖那么大。图2是MPU6050的引脚说明。我们可以看到,MPU6050左右两侧各有4个接口,在图2中这4个接口没有焊引脚上去,那么这就需要我们在买来MPU6050模块后自己焊接金属引脚上去,这都没什么可说的。需要说明一下的是左右两侧的引脚区别,我使用的这款MPU6050模块它支持两种串行通信方式:I2C和USART(串口通信,注意是串口通信,不是串行通信)(关于串行通信模式虽然之前已经有两篇文章进行了讲解,不过在下一片我会进行一个总结,因为串行通信花样繁多,SPI,I2C,USB,USART等等着实让我自己也犯迷糊了,在最近整合不同模块来控制四轴飞行器的时候这种模糊让我走了不少弯路,所以我觉得在下一片有必要将串行通信做一个归纳总结),左侧VCC,TX,RX,GND是USART模式用的引脚,右侧VCC,SCL,SDA,GND是I2C模式使用的引脚。那么在选择上我选择了USART模式,为什么?因为根据说明书上所讲,I2C模式下, MPU6050不会输出X-Y-Z三个方向的偏转角信息,而我恰恰要用这个信息,所以我就采用了USART模式。下面我也就以USART模式为例来介绍了。这个USART模式采用引脚TX,RX来发送和接收数据;大家一定记得,对!就是在第4篇中我们讲到过,其实USART就是第四篇中我们讲Arduino和电脑之间的串口通信模式。强调一句话:要区分串口通信和串行通信,串口通信在Arduino 上就是指USART模式,USART是串行通信的一种;而我们通常说到串行通信,是和并行通信区别的,它们俩都是一个大类的总称,串行通信有好几种:SPI,I2C,USART,USB,UART等。他们之间是有一些区别的,具体的区别在下一篇会讲解。
介绍完MPU6050的引脚,下面看看MPU6050和Arduino怎么连接。正如前面所讲,我们采用了USART模式来让MPU6050和Arduino通信,所以我们采用左侧的四个引脚。如图3所示,VCC接Arduino的3.5V或者5V引脚,因为MPU6050的供电电压位3~6V,所以Arduino上面3.3V或者5V两个引脚都可以用来给MPU6050供电;GND当然和Arduino的 GND相连了;MPU6050的TX要和Arduino的RX相连,因为TX是输出信号端,RX是输入信号端,由于MPU6050要将它测量到的数据传送给Arduino使用,所以要将MPU6050的输出端和Arduino的输入端连接;同理,Arduino的TX端要和 MPU6050的RX端链接,这是因为有时候Arduino需要回传一些信息给MPU6050,不过在图3中可以看到我没有把Arduino的TX端和MPU6050的RX端相连,这个我要解释一下。是这样:由于在我的四轴无人机上,我不需要Arduino回传信息给MPU6050,相反,我需要Arduino随时随地将MPU6050传送给它的数据反馈到电脑上的串口监控窗口,以便于我随时监测数据是否正确,但是Arduino UNO只有一个硬串口(TX,RX为一组硬串口),假如全被MPU6050占用了就不能再和电脑连接,所以我不将Arduino的TX接到MPU6050的RX上。相反,我会通过USB线,将Arduino上的TX接口和电脑连接,那么这样一来我就可以通过Arduino上的TX接口把数据传到电脑上了,这样我就可以在电脑上监视MPU6050的测量数据。连接原理见图4,图4是我使用的正式接法,MPU6050的数据通过TX--RXD传给Arduino,Arduino通过TXD将数据传给USB接口,USB接口连接到电脑上把数据发送到串口监控窗口上。
上面讲解的就是MPU6050和Arduino的连接方式,下面简单介绍一下MPU6050的一些作用原理。MPU6050最常用的数据信息就是加速度和角速度,通过这两者又能解算出X-Y-Z的偏转角。图4中右上角可以看到有一个小坐标系,根据这个小坐标系可知,将MPU6050按照图4所示摆放好以后,水平向右为X轴正方向,向上为Y轴正方向,沿纸面向外为Z轴正方向。下面分别介绍下角速度和加速度的相关知识。
角速度---角速度符号定义规则:从原点往某一轴正向看去,顺时针为正,逆时针为负。偏转角的符号定义同角速度是一致的。角速度的单位是度/秒,这里的度是指角度不是指弧度。
加速度---加速度的单位很简单,正负号就按照X-Y-Z轴正方向而定。对于加速度需要讲解的是它的测量原理。如图5所示,我们可以假想有一个球在MPU6050里面,它可以在X,Y,Z三个轴的方向上自由滑动;当MPU6050往X轴正向加速时,假想球会向X轴负向偏移,事实上MPU6050的加速度数值就是按照这个假想球的偏移量坐标取反得到的。也就是说当假想球往X轴负向偏移10个单位时,MPU6050测出来的加速度就应该是X正方向10个单位。来了解到这一个原理后,我们可以想象一下这么一个场景:当MPU6050处于图4中那样的水平静止状态时,这个假想球会由于重力的作用向下偏移,也就是向Z轴负向偏移,那么此时MPU6050将会有一个Z轴正向的读数,且这个读数就是9.8米/ s^2。所以我们要知道这样一个事实,在如图4所示的水平静止状态下,MPU6050并不是读数为0,而是有一个Z向且数值为9.8米/ s^2的读数,代表了此时MPU6050所受到的重力加速度。更进一步可以知道,当MPU6050处于倾斜静止状态时,假想球将不沿着Z轴偏移,此时MPU6050所测出来的Z方向加速度将不再是9.8米/ s^2,如图6所示,此时的Z向加速度将是9.8 x cos(Z轴倾斜角),当Z轴水平时即倾斜角达到90度时,Z向加速度读数将变为0。
好了,讲到这里关于MPU6050的硬件部分需要了解的知识基本讲完了。接下来就是软件部分了,软件部分又分为两部分:数据结构和驱动程序。
首先要讲数据结构,因为只有在了解了MPU6050内部数据结构以后,才能够编写驱动程序来读取数据。什么是数据结构呢?数据结构这个词我们经常看到,这里我就用我自己的个人语言来解释一下,可能不严谨不过差不多应该能让你明白个大概。所谓数据结构,就是这些数据是以什么样的方式存储和组织在一起的。举个通俗的例子,比如我们现在要统计一个班级里小朋友的健康程度,那么通常我们会搜集小朋友们的身高,体重,年纪,姓名,性别等等,那么这些数据在搜集完毕以后肯定需要整理出来吧?比如我们要给当地教育局领导进行汇报什么的,那么我们不可能拿着一堆写满数据的草稿甩给领导,说您看着得嘞!这领导绝对立刻马上炒你鱿鱼。那么该怎么做呢?是个人都懂,要制作一张表格,把数据清清楚楚统计到表格里,为啥要这样做呢?为的是能够让领导轻松地从表格中获取到他们想要的信息。那么好,其实制作表格这个过程也就是我们把数据结构化的过程,而结构化的目的也是想让使用者能够读取这些数据并获得信息,试想一堆乱七八糟的草稿怎么能够让使用者提取出有效信息呢?所以这个表格就叫数据结构,按照表格排布的数据就是结构化的数据。另外表格形式可以各种各样,所以数据结构也有很多种,但是一个不变的宗旨就是:只有结构化的数据,才具有可读性,才能从中挖掘出信息和价值。就像我们工作中一样,只有整理成表格或者报告的数据,才让人读得懂。这也是为什么目前很火的“大数据”,它有大量的数据清洗工作需要做,所谓数据清洗,无非也就是把有用的数据挑出来,并结构化的过程。
好,接下来我们看一下MPU6050的数据结构是怎么样。见图7,看过第4篇文章的朋友应该会对图7非常熟悉,图中所示就是MPU6050一帧数据的数据结构。MPU6050传输的一帧数据有11个字节,这11个字节组成了一个数组 Re_buff,Re_buff是数组名;有的朋友可能会迷糊,怎么又是帧又是数组的,其实这些都只是对于数据结构的称呼而已,换句话说图7中一帧数据是由一个名叫Re_buff的数组构成的,这个数组可存放11个字节,然后MPU6050一次发送这样的一帧数据。下面具体讲讲其中细节,数组中的第0位数据为包头,其实就是第4篇文章中讲过的帧头,它的作用是用来识别一帧数据开始位置的,也就是说一旦Arduino读到0x55这个信息时,它就知道接下来是一帧新的数据发过来了;数组中的第1位数据是一个识别位,通过这个数据Arduino可以获知这一帧数据是加速度,角速度还是偏转角,图7中0x51代表着一帧数据是加速度数据,如果是0x52则代表是角速度数据,如果是0x53则代表是偏转角数据;数组中的第2到第9位数据依次是X轴加速度,Y轴加速度,Z轴加速度,温度数据,这几位是我们真正关心的数据,也是我们真正要从MPU6050中取出来的数据;数组的最后一位即第10位是“校验和”,它的作用是检查数据发送是否完整,同时也作为这一帧数据的帧尾,当Arduino读到“校验和”这一位后,它就知道这一帧数据传输结束了。这就是MPU6050的数据结构;角速度,偏转角和加速度的数据结构是一致的。
通过上面的讲解,大家应该对MPU6050的数据结构有了一定了解,下面就可以介绍驱动程序了,简单地讲就是怎么让Arduino和MPU6050通信,进而读取出MPU6050的数据。见图8,整个程序分为四大部分:定义变量,setup主程序,loop主程序,serialEvent程序。这里有两点要说明:
1.为什么图8中程序里没有头文件?看过上一篇NRF24L01的朋友可能会问这个问题。在NRF24L01一篇中,我们讲到过各种模块都可以在网上找到别人写好的库文件,在下载这些库文件以后,我们可以很方便地操控这些模块;这些库文件通常会通过#include ******这个命令作为头文件加入到程序里,NRF24L01一篇中就是如此。那么本篇中图8的程序里怎么没有调用相关的头文件呢?是这样,前面我们说过MPU6050由两套通信模式:USART和I2C,这两套模式你可以自己任意选择,那么在这里我选择了USART模式。对于USART模式是不需要上网寻找别人的库来用的,可以直接进行数据的读取;而如果你选用了I2C模式的话,那么你就需要上网寻找别人的库文件,来帮助你操控MPU6050模块了。
2.为什么MPU6050多出来一个serialEvent()函数?serialEvent()函数在Arduino里面是一个伪中断函数,关于中断和伪中断我在后续会专门开一篇来讲解,这里我就先简单介绍一下serialEvent()在这里的作用,我们知道loop()函数是一个不断循环的函数,而serialEvent()函数一般是在两个loop()函数之间被调用的,也就是说当loop()函数运行完一次后,在下一次循环运行前serialEvent()函数会被调用;但是还有一个条件,那就是串口寄存器中要有信息输入。总结一下来说,serialEvent()函数被调用需要同时满足两个条件:1.在上一个loop()函数运行完毕,下一个loop()函数还没开始前;2.串口寄存器中要有信息传入。好像还是有点绕是吧?好的,那我用更通俗的说法来讲一下到底serialEvent()函数是怎么运行的:首先,假设我们正在运行loop()函数,当loop()函数运行完毕一次以后,这时候Arduino会去查看一下它的串口寄存器(关于串口寄存器在第4篇中有讲解),假如串口寄存器中没有数据传入的话,那么Arduino就会直接开始下一个loop()函数的运行,但是如果Arduino发现串口寄存器中有数据传入的话,它就会暂时不开始下一次loop()函数运行而去运行serialEvent()函数,在serialEvent()函数运行完毕后再开始下一次的loop()函数运行。所以在图8的程序中,serialEvent()函数的作用就是监视串口寄存器是否有数据传入,这里讲的数据其实就是MPU6050传过来的数据,一旦发现有数据传入,那么Arduino就会在两次loop()函数的运行中间间隔期间,通过调用serialEvent()函数来读取MPU6050传入的数据。这就是serialEvent()在图8程序中的作用。此外,正由于serialEvent()函数是在两次loop()函数运行间隔期间才运行的,而不是随时随地可以运行的,所以serialEvent()被称为伪中断,而真正的中断时随时可跳转运行的。
下面针对图8中的4大部分分别讲解一下其功能:
定义变量---这部分很好理解,就是定义一些后面需要用到的变量;
setup()函数---这部分也是老样子,定义Arduino的一些设置,不过在这里我们只需要定义一个波特率,由于MPU6050默认的波特率是115200,所以我们也需要将Arduino的波特率定义为115200。这样Arduino才能和MPU6050正常通话。多说几句话,同步串行通信一般不需要设定波特率,因为它们有时钟线来协调发送端和接收端,异步串行通信没有时钟线所以一般需要设定波特率来协调收发,且发送端和接收端的波特率要一致,MPU6050的USART就是一种异步串行通信,而SPI和I2C时同步串行通信,所以MPU6050一定要设定波特率并且要按照MPU6050模块的设定来决定Arduino程序中的波特率,不能随意选择。细心的朋友可能会问:“你说SPI是同步串行通信,不需要设定波特率,那么上一篇的NRF24L01就是SPI,为什么上一篇中依然设定了波特率呢?”是这样:“在我的这几篇文章中,大家会发现不论是什么模块,我都会用Arduino的串口监控窗口来监控数据传输,所以上一篇中NRF24L01中的波特率是我设定来将NRF24L01的数据通过Arduino上传给电脑时用的,由于Arduino和电脑的通信是USART模式,所以需要设定波特率,更进一步说在上一篇中如果我不打算通过串口监控窗口监视数据而仅仅让发送和接收两片NRF24L01之间相互通信的话就不需要设定波特率。MPU6050不一样,不管我是否想通过电脑来监控,Arduino和MPU6050之间的通信都必须要设定波特率,并且必须是115200(MPU6050的默认波特率,也可以切换成9600),因为我用的也是USART模式,当然假如你在使用MPU6050的时候采用了I2C模式来通信,那么此时就和NRF24L01一样不需要设定波特率了。
loop()函数---其作用就是不断地将Re_buff[]数组中的信息换算成加速度,角速度或者偏转角。其中分为两大步骤:首先判断帧头,即if (Re_buf[0] == 0x55)这句话的作用,它的意思是如果Re_buff[0]这一位的数据是0x55的话,那么说明接下来是一帧新的数据,这就是帧头的作用前面我们讲过的;其次是按条件进行数据计算,即switch(Re_buff[1])...case(0x51)...case(0x52)...case(0x53)...这一组判断语句,它的意思是对于Re_buff[1] 这一位的数据进行分类讨论,假如Re_buff[1]的数据是0x51的话,说明后面的这一帧数据是加速度那么就运行case(0x51)后面的语句,假如是0x52则是角速度那么就运行case(0x52)后面的语句,假如是0x53则是偏转角那么就运行(0x53)后面的语句;这些也在本文最开头涉及过。以加速度数据为例,case(0x51)语句后面的计算方式是:
a[0] = (short(Re_buf [3] << 8 | Re_buf [2])) / 32768.0 * 16;
a[1] = (short(Re_buf [5] << 8 | Re_buf [4])) / 32768.0 * 16;
a[2] = (short(Re_buf [7] << 8 | Re_buf [6])) / 32768.0 * 16;
T = (short(Re_buf [9] << 8 | Re_buf [8])) / 340.0 + 36.25;
break;
这几句话表达的是如何计算出a[0],a[1],a[2]和T这四个数据,a[0]代表的就是X方向加速度,a[1]代表的就是Y方向加速度,a[2]代表的就是Z方向的加速度,它们的计算公式也就是等号后面写的那些式子。这些式子怎么来的呢?其实都是MPU6050的说明手册上提供的,只要你从网上买了一款模块,一般卖家都会提供相关说明资料的下载,所以你大可不必担心。最后的那句break意思是跳出当前switch判断语句,继续运行后面的程序。你可以看到每一个case分支的最后都有一个break语句,所以switch...case1...case2...case3只会运行一个case,一旦运行完就会跳出switch继续运行后面的Serial.print()函数。后面那一大堆的Serial.print()都是用来显示数据用的,它会将加速度,角速度,偏转角数据都传递到Arduino的串口监控窗口上,让你能够实时监控数据的正确性。
serialEvent()---这个函数按照前面讲的,在同时满足两个条件时会被调用:1.两次loop()函数循环的中间间隔时期;2.串口寄存器内有数据传入。所以其实它实现的是这样一个功能:一旦MPU6050将数据传入Arduino的串口寄存器中时,Arduino就会通过调用serialEvent()来读取这些数据,并将这些数据存入Re_buff数组中,然后在接下来的loop循环中由switch...case...case...case对Re_buff数据中的数据进行处理。
好了,这一讲到此为止,其中有一些地方如果展开太多则会跑题所以可能不够细致,在下一讲和下下一讲我会专门讲解这些不清晰的地方,下一讲打算对串行通信的概念进行总结,因为在写文章的过程中我注意到这其中有很多容易混淆的概念,如:串行通信,串口通信,USART,SPI,USB,I2C;他们之间的关系,区别等等等。下下一讲打算讲一下中断和伪中断,主要也是对本篇中serialEvent()函数的补充讲解,因为对于忽然冒出来的serialEvent()函数,可能不少朋友会比较疑惑。