0 前言
??作為慣性傳感器中入門級(jí)別的器件,MPU6050憑借它出色的性價(jià)比成為一款非常常用的角度姿態(tài)傳感器,在很多科創(chuàng)項(xiàng)目中被使用。我之前也接觸過很多次這個(gè)器件,也收集了不少資料,趁此機(jī)會(huì)總結(jié)一下學(xué)習(xí)筆記。
1 MPU6050概述
1.1 基本概述
??MPU6050包含3軸陀螺儀和3軸加速度計(jì),其中陀螺儀的主要作用是測(cè)量物體繞芯片的三個(gè)坐標(biāo)軸的角速度,其原理是高速旋轉(zhuǎn)的轉(zhuǎn)子指向的方向會(huì)保持不變,即所謂陀螺效應(yīng),詳細(xì)介紹建議自行搜索“陀螺儀工作原理”;加速度計(jì)則是測(cè)量三個(gè)軸向的加速度,同樣也是指芯片的三個(gè)坐標(biāo)軸。其原理可以想象成一個(gè)立方體箱子里面有一個(gè)失重懸空的小球,當(dāng)受到外界壓力時(shí),小球會(huì)朝著某個(gè)方向運(yùn)動(dòng),箱子內(nèi)壁就會(huì)受到對(duì)應(yīng)大小的壓力,從而可以計(jì)算出各個(gè)方向的加速度大小。
??當(dāng)整體受到向左的加速度時(shí),小球會(huì)有一個(gè)相對(duì)箱子向右的加速度,從而右側(cè)“箱壁”會(huì)受到對(duì)應(yīng)加速度大小的力,這樣就能計(jì)算出加速度的大小。
??MPU6050是InvenSense公司推出的全球首款整合性6軸運(yùn)動(dòng)處理組件。目前InvenSense已被日本的TDK公司收購(gòu),在他官網(wǎng)(https://invensense.tdk.com/),可能是年代久遠(yuǎn),MPU6050已經(jīng)是該公司的邊緣產(chǎn)品了,6軸芯片當(dāng)中,6050和6500兩款芯片被排在最后,還都是NOT RECOMMENDED的狀態(tài),而且資料支持也不是很完善,找遍了網(wǎng)站,也只找到了一個(gè)放數(shù)據(jù)手冊(cè)的網(wǎng)頁(yè),開發(fā)相關(guān)的寄存器手冊(cè)并未找到。因此,建議還是在網(wǎng)上去搜索資料吧,如下圖所示,都是網(wǎng)上流傳的經(jīng)典資料。
??MPU6050的核心就是它內(nèi)部的寄存器。它內(nèi)部有118個(gè)寄存器(編號(hào)從0到117)。其中需要注意,雖然寄存器手冊(cè)上寄存器編號(hào)是從13開始,但實(shí)際上13之前的寄存器也是可以使用的(看后面的代碼就知道了)。
??這些寄存器有一些是用來(lái)設(shè)置參數(shù)的,可讀可寫;也有一些是存放一些數(shù)據(jù)供外部讀取的,只可讀。它是基于IIC進(jìn)行通信,因此,在使用時(shí),先找到傳輸器件地址,然后再傳輸寄存器地址,最后傳輸相應(yīng)的數(shù)據(jù)或者指令,這也是IIC協(xié)議和器件寄存器交互的常用設(shè)定。
??既然有這么多寄存器,那用起來(lái)豈不是很困難?并不是,雖然寄存器多,但實(shí)際使用時(shí)也不需要使用如此多的寄存器。而且即使要使用的話,也可以利用C語(yǔ)言中的宏定義,這樣也不麻煩。
1.2 引腳和常用原理圖
??為了將這個(gè)芯片集成到我們需要的系統(tǒng)當(dāng)中,就需要了解這款芯片的引腳和它常用的原理圖,如下圖所示,這個(gè)是市面上賣的MPU6050模塊的原理圖。
可以看到,這里引出了8個(gè)引腳,分別是電源引腳5V和GND,IIC通信引腳SCL和SDA,一般來(lái)說(shuō),大部分的應(yīng)用只需要接這四個(gè)引腳即可。其中,XCL和XDA是額外的IIC通信引腳,主要用于連接外部的磁力傳感器,并利用自帶的運(yùn)動(dòng)處理器DMP硬件加速引擎,通過主IIC接口,向應(yīng)用輸出完整的9軸融合演算數(shù)據(jù)。
??而AD0引腳是用來(lái)設(shè)置IIC通信中的從機(jī)地址,如果接地(不接),則從機(jī)地址為0x68, 如果接高電平,則從機(jī)地址為0x69。而INT引腳主要用于中斷,如果要使用中斷需要設(shè)置相關(guān)的寄存器。
2 代碼
??了解了MPU6050的基礎(chǔ)知識(shí),接下來(lái)就是寫代碼來(lái)使用了。如果還沒確定使用的微處理器,我推薦先使用Arduino,因?yàn)樗闪撕芏嗟牡谌綆?kù),這樣在使用一些器件時(shí)不用自己再重復(fù)造輪子,只需要會(huì)調(diào)用即可。
??在Arduino中也有MPU6050的庫(kù),如下圖所示。
安裝好庫(kù)之后,接下來(lái)就找到給出的例子來(lái)學(xué)習(xí)它內(nèi)部的代碼了。
這里提供了6個(gè)例子,基本包含了大部分的使用。
??當(dāng)然,使用Arduino IDE也存在一個(gè)問題,那就是代碼不能定位過去,查看庫(kù)的源碼不太方便,因此建議自己基于VS Code配一個(gè)Arduino的環(huán)境,或者直接下載插件Platform IO這個(gè)插件,具體的教程建議自行搜索。
??具體可以看一下這個(gè)庫(kù)的源碼,主要是以下幾個(gè)文件,各自的作用已標(biāo)注清楚。
因此,如果不需要使用DMP時(shí),只需要包含"MPU6050.h"
即可。
??那如果是其他的微處理器呢?比如51或者STM32等。這個(gè)可以考慮在網(wǎng)上找一些現(xiàn)成的,也可以考慮自己根據(jù)這個(gè)庫(kù)文件的源碼自己寫一個(gè)適配某個(gè)處理器的庫(kù)。本質(zhì)就是IIC通信和寄存器的讀取。這里放一個(gè)基于51的網(wǎng)上流傳甚廣的代碼。
//****************************************
// Update to MPU6050 by shinetop
// MCU: STC89C52
// 2012.3.1
// 功能: 顯示加速度計(jì)和陀螺儀的10位原始數(shù)據(jù)
//****************************************
// 使用單片機(jī)STC89C52
// 晶振:11.0592M
// 顯示:串口
// 編譯環(huán)境 Keil uVision2
//****************************************
#include <REG52.H>
#include <math.h> //Keil library
#include <stdio.h> //Keil library
#include <INTRINS.H> //Keil library
typedef unsigned char uchar;
typedef unsigned short ushort;
typedef unsigned int uint;
//****************************************
// 定義51單片機(jī)端口
//****************************************
sbit SCL = P1 ^ 5; //IIC時(shí)鐘引腳定義
sbit SDA = P1 ^ 4; //IIC數(shù)據(jù)引腳定義
//****************************************
// 定義MPU6050內(nèi)部地址
//****************************************
#define SMPLRT_DIV 0x19 //陀螺儀采樣率,典型值:0x07(125Hz)
#define CONFIG 0x1A //低通濾波頻率,典型值:0x06(5Hz)
#define GYRO_CONFIG 0x1B //陀螺儀自檢及測(cè)量范圍,典型值:0x18(不自檢,2000deg/s)
#define ACCEL_CONFIG 0x1C //加速計(jì)自檢、測(cè)量范圍及高通濾波頻率,典型值:0x01(不自檢,2G,5Hz)
#define ACCEL_XOUT_H 0x3B
#define ACCEL_XOUT_L 0x3C
#define ACCEL_YOUT_H 0x3D
#define ACCEL_YOUT_L 0x3E
#define ACCEL_ZOUT_H 0x3F
#define ACCEL_ZOUT_L 0x40
#define TEMP_OUT_H 0x41
#define TEMP_OUT_L 0x42
#define GYRO_XOUT_H 0x43
#define GYRO_XOUT_L 0x44
#define GYRO_YOUT_H 0x45
#define GYRO_YOUT_L 0x46
#define GYRO_ZOUT_H 0x47
#define GYRO_ZOUT_L 0x48
#define PWR_MGMT_1 0x6B //電源管理,典型值:0x00(正常啟用)
#define WHO_AM_I 0x75 //IIC地址寄存器(默認(rèn)數(shù)值0x68,只讀)
#define SlaveAddress 0xD0 //IIC寫入時(shí)的地址字節(jié)數(shù)據(jù),+1為讀取
//**************************************************************************************************
//定義類型及變量
//**************************************************************************************************
uchar dis[6]; //顯示數(shù)字(-511至512)的字符數(shù)組
int dis_data; //變量
//**************************************************************************************************
//函數(shù)聲明
//**************************************************************************************************
void Delay5us();
void delay(unsigned int k); //延時(shí)
void lcd_printf(uchar* s, int temp_data);
//********************************MPU6050操作函數(shù)***************************************************
void InitMPU6050(); //初始化MPU6050
void I2C_Start();
void I2C_Stop();
void I2C_SendACK(bit ack);
bit I2C_RecvACK();
void I2C_SendByte(uchar dat);
uchar I2C_RecvByte();
void I2C_ReadPage();
void I2C_WritePage();
void display_ACCEL_x();
void display_ACCEL_y();
void display_ACCEL_z();
uchar Single_ReadI2C(uchar REG_Address); //讀取I2C數(shù)據(jù)
void Single_WriteI2C(uchar REG_Address, uchar REG_data); //向I2C寫入數(shù)據(jù)
//********************************************************************************
//整數(shù)轉(zhuǎn)字符串
//********************************************************************************
void lcd_printf(uchar* s, int temp_data)
{
if(temp_data < 0)
{
temp_data = -temp_data;
*s = '-';
}
else *s = ' ';
*++s = temp_data / 10000 + 0x30;
temp_data = temp_data % 10000; //取余運(yùn)算
*++s = temp_data / 1000 + 0x30;
temp_data = temp_data % 1000; //取余運(yùn)算
*++s = temp_data / 100 + 0x30;
temp_data = temp_data % 100; //取余運(yùn)算
*++s = temp_data / 10 + 0x30;
temp_data = temp_data % 10; //取余運(yùn)算
*++s = temp_data + 0x30;
}
//******************************************************************************************************
//串口初始化
//*******************************************************************************************************
void init_uart()
{
TMOD = 0x21;
TH1 = 0xfd; //實(shí)現(xiàn)波特率9600(系統(tǒng)時(shí)鐘11.0592MHZ)
TL1 = 0xfd;
SCON = 0x50;
PS = 1; //串口中斷設(shè)為高優(yōu)先級(jí)別
TR0 = 1; //啟動(dòng)定時(shí)器
TR1 = 1;
ET0 = 1; //打開定時(shí)器0中斷
ES = 1;
EA = 1;
}
//*************************************************************************************************
//串口發(fā)送函數(shù)
//*************************************************************************************************
void SeriPushSend(uchar send_data)
{
SBUF = send_data;
while(!TI);
TI = 0;
}
//*************************************************************************************************
//************************************延時(shí)*********************************************************
//*************************************************************************************************
void delay(unsigned int k)
{
unsigned int i, j;
for(i = 0; i < k; i++)
{
for(j = 0; j < 121; j++);
}
}
//************************************************************************************************
//延時(shí)5微秒(STC90C52RC@12M)
//不同的工作環(huán)境,需要調(diào)整此函數(shù)
//注意當(dāng)改用1T的MCU時(shí),請(qǐng)調(diào)整此延時(shí)函數(shù)
//************************************************************************************************
void Delay5us()
{
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
}
//*************************************************************************************************
//I2C起始信號(hào)
//*************************************************************************************************
void I2C_Start()
{
SDA = 1; //拉高數(shù)據(jù)線
SCL = 1; //拉高時(shí)鐘線
Delay5us(); //延時(shí)
SDA = 0; //產(chǎn)生下降沿
Delay5us(); //延時(shí)
SCL = 0; //拉低時(shí)鐘線
}
//*************************************************************************************************
//I2C停止信號(hào)
//*************************************************************************************************
void I2C_Stop()
{
SDA = 0; //拉低數(shù)據(jù)線
SCL = 1; //拉高時(shí)鐘線
Delay5us(); //延時(shí)
SDA = 1; //產(chǎn)生上升沿
Delay5us(); //延時(shí)
}
//**************************************************************************************************
//I2C發(fā)送應(yīng)答信號(hào)
//入口參數(shù):ack (0:ACK 1:NAK)
//**************************************************************************************************
void I2C_SendACK(bit ack)
{
SDA = ack; //寫應(yīng)答信號(hào)
SCL = 1; //拉高時(shí)鐘線
Delay5us(); //延時(shí)
SCL = 0; //拉低時(shí)鐘線
Delay5us(); //延時(shí)
}
//****************************************************************************************************
//I2C接收應(yīng)答信號(hào)
//****************************************************************************************************
bit I2C_RecvACK()
{
SCL = 1; //拉高時(shí)鐘線
Delay5us(); //延時(shí)
CY = SDA; //讀應(yīng)答信號(hào)
SCL = 0; //拉低時(shí)鐘線
Delay5us(); //延時(shí)
return CY;
}
//*****************************************************************************************************
//向I2C總線發(fā)送一個(gè)字節(jié)數(shù)據(jù)
//*****************************************************************************************************
void I2C_SendByte(uchar dat)
{
uchar i;
for(i = 0; i < 8; i++) //8位計(jì)數(shù)器
{
dat <<= 1; //移出數(shù)據(jù)的最高位
SDA = CY; //送數(shù)據(jù)口
SCL = 1; //拉高時(shí)鐘線
Delay5us(); //延時(shí)
SCL = 0; //拉低時(shí)鐘線
Delay5us(); //延時(shí)
}
I2C_RecvACK();
}
//*****************************************************************************************************
//從I2C總線接收一個(gè)字節(jié)數(shù)據(jù)
//******************************************************************************************************
uchar I2C_RecvByte()
{
uchar i;
uchar dat = 0;
SDA = 1; //使能內(nèi)部上拉,準(zhǔn)備讀取數(shù)據(jù),
for(i = 0; i < 8; i++) //8位計(jì)數(shù)器
{
dat <<= 1;
SCL = 1; //拉高時(shí)鐘線
Delay5us(); //延時(shí)
dat |= SDA; //讀數(shù)據(jù)
SCL = 0; //拉低時(shí)鐘線
Delay5us(); //延時(shí)
}
return dat;
}
//*****************************************************************************************************
//向I2C設(shè)備寫入一個(gè)字節(jié)數(shù)據(jù)
//*****************************************************************************************************
void Single_WriteI2C(uchar REG_Address, uchar REG_data)
{
I2C_Start(); //起始信號(hào)
I2C_SendByte(SlaveAddress); //發(fā)送設(shè)備地址+寫信號(hào)
I2C_SendByte(REG_Address); //內(nèi)部寄存器地址,
I2C_SendByte(REG_data); //內(nèi)部寄存器數(shù)據(jù),
I2C_Stop(); //發(fā)送停止信號(hào)
}
//*******************************************************************************************************
//從I2C設(shè)備讀取一個(gè)字節(jié)數(shù)據(jù)
//*******************************************************************************************************
uchar Single_ReadI2C(uchar REG_Address)
{
uchar REG_data;
I2C_Start(); //起始信號(hào)
I2C_SendByte(SlaveAddress); //發(fā)送設(shè)備地址+寫信號(hào)
I2C_SendByte(REG_Address); //發(fā)送存儲(chǔ)單元地址,從0開始
I2C_Start(); //起始信號(hào)
I2C_SendByte(SlaveAddress + 1);//發(fā)送設(shè)備地址+讀信號(hào)
REG_data = I2C_RecvByte(); //讀出寄存器數(shù)據(jù)
I2C_SendACK(1); //接收應(yīng)答信號(hào)
I2C_Stop(); //停止信號(hào)
return REG_data;
}
//******************************************************************************************************
//初始化MPU6050
//******************************************************************************************************
void InitMPU6050()
{
Single_WriteI2C(PWR_MGMT_1, 0x00); //解除休眠狀態(tài)
Single_WriteI2C(SMPLRT_DIV, 0x07);
Single_WriteI2C(CONFIG, 0x06);
Single_WriteI2C(GYRO_CONFIG, 0x18);
Single_WriteI2C(ACCEL_CONFIG, 0x01);
}
//******************************************************************************************************
//合成數(shù)據(jù)
//******************************************************************************************************
int GetData(uchar REG_Address)
{
uchar H, L;
H = Single_ReadI2C(REG_Address);
L = Single_ReadI2C(REG_Address + 1);
return ((H << 8) + L); //合成數(shù)據(jù)
}
//******************************************************************************************************
//超級(jí)終端(串口調(diào)試助手)上顯示10位數(shù)據(jù)
//******************************************************************************************************
void Display10BitData(int value)
{
uchar i;
// value/=64; //轉(zhuǎn)換為10位數(shù)據(jù)
lcd_printf(dis, value); //轉(zhuǎn)換數(shù)據(jù)顯示
for(i = 0; i < 6; i++)
{
SeriPushSend(dis[i]);
}
// DisplayListChar(x,y,dis,4); //啟始列,行,顯示數(shù)組,顯示長(zhǎng)度
}
//*******************************************************************************************************
//主程序
//*******************************************************************************************************
void main()
{
delay(500); //上電延時(shí)
init_uart();
InitMPU6050(); //初始化MPU6050
delay(150);
while(1)
{
Display10BitData(GetData(ACCEL_XOUT_H)); //顯示X軸加速度
Display10BitData(GetData(ACCEL_YOUT_H)); //顯示Y軸加速度
Display10BitData(GetData(ACCEL_ZOUT_H)); //顯示Z軸加速度
Display10BitData(GetData(GYRO_XOUT_H)); //顯示X軸角速度
Display10BitData(GetData(GYRO_YOUT_H)); //顯示Y軸角速度
Display10BitData(GetData(GYRO_ZOUT_H)); //顯示Z軸角速度
SeriPushSend(0x0d);
SeriPushSend(0x0a);//換行,回車
delay(2000);
}
}
3 姿態(tài)解算
??前面提到MPU6050有很多的寄存器,但其實(shí)最核心的就是陀螺儀和加速度讀取的數(shù)值,即姿態(tài)數(shù)據(jù)。但是,需要注意的是,前面也強(qiáng)調(diào)過,這個(gè)讀取到的姿態(tài)數(shù)據(jù)是基于元器件坐標(biāo)系的,而在實(shí)際應(yīng)用中需要的更多是相對(duì)于大地坐標(biāo)系的數(shù)據(jù)。這樣得到的才是真正意義上的姿態(tài)。
??首先要搞清楚芯片坐標(biāo)系的樣式,這直接關(guān)系到所測(cè)數(shù)據(jù)的正方向。如下圖所示。
芯片水平放置,絲印朝正上方,此時(shí),前方是+Y方向,右側(cè)是+X方向,正上方是+Z方向,三軸符合右手坐標(biāo)系。至于旋轉(zhuǎn)的角速度的正方向,也是按照右手定則,大拇指指向軸的正方向,四指所指的方向?yàn)樾D(zhuǎn)的正方向。
??關(guān)于MPU6050的姿態(tài)解算,主要有兩種方式,分別是基于歐拉角和旋轉(zhuǎn)矩陣推導(dǎo) 和直接調(diào)用芯片中的DMP模塊,這里簡(jiǎn)要介紹一下。
3.1 歐拉角&旋轉(zhuǎn)矩陣
??關(guān)于歐拉角和旋轉(zhuǎn)矩陣,如果不理解基礎(chǔ)知識(shí)的建議翻閱我之前的一篇博客:
【學(xué)習(xí)筆記】空間坐標(biāo)系旋轉(zhuǎn)與四元數(shù)
了解基本的旋轉(zhuǎn)矩陣的知識(shí)后,接下來(lái)就是利用旋轉(zhuǎn)矩陣來(lái)計(jì)算芯片的姿態(tài)角了。這里建議參考這篇文章,寫得比較詳細(xì)。
有一點(diǎn)存在一點(diǎn)疑問,那就是文章中提到的是Z-Y-X歐拉角,但我認(rèn)為應(yīng)該叫Z-Y-X固定角更合理,這也符合教材上的矩陣相乘的順序。
此外,這篇文章當(dāng)中陀螺儀和加速度計(jì)解算是分開的,加速度主要負(fù)責(zé)靜態(tài)的姿態(tài)角,陀螺儀反應(yīng)動(dòng)態(tài)的姿態(tài)角變化,然后設(shè)定不同的權(quán)值,加權(quán)求和得到。其實(shí)數(shù)據(jù)融合更加合理的應(yīng)該是使用卡爾曼濾波。這個(gè)后續(xù)也會(huì)跟進(jìn)補(bǔ)充。
3.2 DMP
??DMP(Digital Motion Processor),即數(shù)字運(yùn)動(dòng)處理器,是MPU6050芯片內(nèi)置的一個(gè)重要模塊。它的作用就是根據(jù)測(cè)得的陀螺儀和加速度數(shù)據(jù)計(jì)算得到芯片的歐拉角yaw,pitch,roll的值。使用者可以不用關(guān)心它內(nèi)部是怎么實(shí)現(xiàn)的。如果想了解的話可以去找找DMP相關(guān)的資料。
3 校正
??IMU有一個(gè)重要的特性,那就是它的誤差是會(huì)隨時(shí)間累積的,最好是隔一段時(shí)間校正一次。而所謂校正,就一定要有一個(gè)參考對(duì)象。對(duì)于MPU6050來(lái)說(shuō),一般參考對(duì)象就是地面。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-824579.html
??校正時(shí),首先將MPU6050放置水平,保持靜止。然后運(yùn)行代碼,讀取陀螺儀和加速度數(shù)據(jù)。理論上來(lái)說(shuō),水平靜止放置,陀螺儀數(shù)值應(yīng)該為零,不為零則是誤差,將誤差寫入到芯片內(nèi)部“偏差寄存器”中,再次讀取數(shù)值,計(jì)算誤差,如此反復(fù),直到誤差值在允許范圍之類,記下誤差值,寫入到偏差寄存器中。
??至于加速度數(shù)值,要考慮重力的影響,因?yàn)槭撬椒胖?,所以重力只在Z軸有分量。即Z軸分量為g,而默認(rèn)加速度的單位是2g,而加速度數(shù)值用16位寄存器表示,且第一位為符號(hào)位(有正負(fù)之分),因此實(shí)際數(shù)值為
(
2
15
?
1
)
2
=
16383.5
≈
16384
\frac{\left( 2^{15}-1 \right)}{2}=16383.5\approx 16384
2(215?1)?=16383.5≈16384,但由于這個(gè)重力加速度與Z軸正方向相反,故值為負(fù)的,即-16384.
??當(dāng)然,有時(shí)候因?yàn)榘惭b等原因,IMU校正時(shí)不能Z軸保持豎直,比如X軸保持豎直,則將實(shí)際數(shù)值代入到校正函數(shù)中,沒啥差別。這里有一篇博客給出了一個(gè)測(cè)試代碼,建議仔細(xì)閱讀。
??關(guān)于校正,實(shí)際上也可以看作是一個(gè)自動(dòng)控制系統(tǒng),因此也可以采用如PID或者是機(jī)器學(xué)習(xí)等控制方法來(lái)進(jìn)行校正。
??關(guān)于PID的例子,其實(shí)上面提到的Arduino的庫(kù)中就有使用,即它內(nèi)部的calibration()
函數(shù),具體原理可以去找源碼查看。
??關(guān)于機(jī)器學(xué)習(xí)的例子,是我找到的一篇博客,用的是梯度下降的方式來(lái)取值,思路很清奇,但個(gè)人感覺本質(zhì)上還是迭代,是否使用梯度下降差別不是很大。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-824579.html
到了這里,關(guān)于【嵌入式模塊】MPU6050的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!