记MPU6050模块的学习过程
ZheWana / 2021-04-05 / 嵌入式 / 阅读量 407

wallhaven-q26k9q_1920x1080.png学习过程中用到的资料整理了下存了起来:

https://wwe.lanzouv.com/iROIKnjuo6h
密码:gzai

环境:STM32F401CCU6+CubeMX+HAL库


模块组成:

MPU6050+HMC5883L+MS5611

MPU6050:加速度计+陀螺仪

HMC5883L: 3轴数字罗盘

MS5611 :气压传感器


模块引脚定义:

以下引脚用途以及配置为个人对与数据手册内容的理解,可能有误。

VCC_IN:5V电源输入

3.3V:3.3V电源输入

GND:接地

SCL:IIC总线时钟

SDA:IIC总线数据输入

FSYNC:(MPU6050)

​ Frame synchronization digital input. Connect to GND if unused

​ 帧同步数字输入。 如果未使用,则连接到GND

(GND)

INTA:(MPU6050)

​ Interrupt digital output (totem pole or open-drain)

​ 中断数字输出(图腾柱或漏极开路)

​ Interrupt functionality is configured via the Interrupt Configuration register. Items that are configurable include the INT pin configuration, the interrupt latching and clearing method, and triggers for the interrupt. Items that can trigger an interrupt are

(1) Clock generator locked to new reference oscillator (used when switching clock sources);

(2) new data is available to be read (from the FIFO and Data registers);

(3) accelerometer event interrupts;

(4) the MPU-60X0 did not receive an knowledge from an auxiliary sensor on the secondary I2C bus. The interrupt status can be read from the Interrupt Status register.

(配置为外部中断)

DRDY:(HMC5883L)

​ Data Ready, Interrupt Pin. Internally pulled high. Optional connection. Low for 250 µsec when data is placed in the data output registers.

​ 数据准备,中断引脚。内部被拉高。可选连接,当数据位于输出寄存器 上时会在低电位上停250μs。(理解为数据传输完成标志引脚)

(配置为外部中断)


模块使用:

伪前言:

使用分为两部分,首先是为了熟悉IIC使用以及对数据手册的阅读而存在的原始数据打印部分,接下来是为了真正应用而存在的DMP库移植部分。

第一次进行库的移植的确会出现很多奇怪的问题,请细心细心加耐心。

官方的手册是好东西,请多阅读。

如果只是需要使用欧拉角数据,则不需要参考第一部分内容,直接进行第二部分即可

如果只是需要使用欧拉角数据,则不需要参考第一部分内容,直接进行第二部分即可

如果只是需要使用欧拉角数据,则不需要参考第一部分内容,直接进行第二部分即可


原始数据的打印:

首先新建一个F401工程,在新工程里面完成IIC和UART的配置。

其中IIC选择IIC1,下拉栏选择IIC,配置默认即可。

生成工程并打开。

在工程文件夹中新建一个mpu6050.h文件。

(PS:如果是自己添加的文件需要添加包含路径,方法参考u8g2屏幕的配置文档)

为了方便使用,首先把要用到的寄存器在mpu6050.h中进行宏定义:

/***********************************************************************************
 *设备地址  
 *PS:在数据手册里面的七位地址是0x68,但是HAL库中函数前面的注释有说明如下:
 *DevAddress Target device address: The device 7 bits address value
 *in datasheet must be shifted to the left before calling the interface
 *也就是说我们在使用时要将设备地址左移一位,而0x68左移一位是0xD0。
 **********************************************************************************/
#define MPU6050_ADDR 0xD0

//采样频率分频器寄存器
#define SMPRT_DIV_REG 0x19

//陀螺仪配置寄存器
#define GYRO_CONFIG_REG 0x1B
//角速度量程
#define FSR1_GYRO 0 //+-250°/s
#define FSR2_GYRO 1 //+-500°/s
#define FSR3_GYRO 2 //+-1000°/s
#define FSR4_GYRO 3 //+-2000°/s
//角速度最低分辨率
#define LSB1_GYRO 131.0 //+-250°/s
#define LSB2_GYRO 65.5 //+-500°/s
#define LSB3_GYRO 32.8 //+-1000°/s
#define LSB4_GYRO 16.4 //+-2000°/s

//加速度配置寄存器
#define ACCEL_CONFIG_REG 0x1c
//加速度量程
#define FSR1_ACCEL 0 //+-2g
#define FSR2_ACCEL 1 //+-4g
#define FSR3_ACCEL 2 //+-8g
#define FSR4_ACCEL 3 //+-16g
//加速度最低分辨率
#define LSB1_ACCEL 16384.0 //+-2g
#define LSB2_ACCEL 8192.0 //+-4g
#define LSB3_ACCEL 4096.0 //+-8g
#define LSB4_ACCEL 2048.0 //+-16g

//加速度测量值
#define ACCEL_XOUT_H_REG 0x3B

//温度测量值
//温度计算公式:摄氏度=(int16_t)REG/340+36.53
#define TEMP_OUT_H_REG 0x41

//陀螺仪测量值
#define GYRO_XOUT_H_REG 0x43

//电源管理寄存器
#define PWR_MGMT_1_REG 0x6B

//设备身份标识寄存器
#define WHO_AM_I_REG 0x75

定义几个变量:

int16_t Axo = 0;
int16_t Ayo = 0;
int16_t Azo = 0;

int16_t Gxo = 0;
int16_t Gyo = 0;
int16_t Gzo = 0;

int16_t Temp_RAW = 0;

float Ax,Ay,Az,Gx,Gy,Gz,Temp;

首先对模块进行初始化设置:

void MPU6050_Init(void){
    uint8_t check,Data;
    //检查设备身份标识(WHO_AM_I)
    HAL_I2C_Mem_Read (&hi2c1 ,MPU6050_ADDR,WHO_AM_I_REG,1,&check ,1,1000);
    if(check == 104)        //验证设备地址
    {
        //在电源管理寄存器中写入0来使能传感器
        Data = 0;
        HAL_I2C_Mem_Write (&hi2c1 ,MPU6050_ADDR ,PWR_MGMT_1_REG ,1,&Data ,1,1000);
        
        //设置采样频率
        Data = 0x07;
        HAL_I2C_Mem_Write (&hi2c1 ,MPU6050_ADDR ,SMPRT_DIV_REG ,1 ,&Data,1,1000);
        
        //设置加速度量程
        Data = FSR1_ACCEL;
        HAL_I2C_Mem_Write (&hi2c1 ,MPU6050_ADDR, ACCEL_CONFIG_REG, 1, &Data, 1, 1000);
        
        //设置陀螺仪量程
        Data = FSR1_GYRO;
        HAL_I2C_Mem_Write (&hi2c1 ,MPU6050_ADDR, GYRO_CONFIG_REG, 1, &Data, 1, 1000);
    }
}

这里对几个重要寄存器做一些说明:

1.采样频率分频器:

1617031200192-1620477659491.png

采样频率的计算公式为:

采样频率=陀螺仪输出频率/( 1+SMPLRT_DIV )

而陀螺仪的输出频率是受到DLPF(数字低通滤波器)的配置影响的,具体影响如下图:

1617031379242-1620477659492.png

而上述滤波器在配置寄存器中进行设置:

1617031435930-1620477659492.png

2.陀螺仪配置寄存器

1617031496511-1620477659492.png

主要功能为触发陀螺仪自检以及陀螺仪最大量程配置,我们主要用到第二个功能。

自检功能:

1617031878416-1620477659492.png

具体量程配置参考下表:

1617031692618-1620477659492.png

3.加速度配置寄存器

1617031935975-1620477659492.png

主要功能和陀螺仪配置寄存器一致,我们还是用第二个功能。

自检功能:

1617032006471-1620477659492.png

具体量程配置参考下表:

1617032031176-1620477659492.png

4.测量值寄存器

要用到的测量值寄存器有三个:加速度、陀螺仪和温度。

首先是加速度计:

1617032142421-1620477659492.png

不难看出,数据的存储方式是用了六个字节来存储三个坐标轴的数据,其中相邻两个数据分别表示同一坐标轴数据的高八位和低八位,因此我们可以对数据如此处理:

//定义六位数组来存放寄存器值
int8_t Mesurment[6];
//将寄存器值读取出来存入数组中方便后续操作
HAL_I2C_Mem_Read (&hi2c1 ,MPU6050_ADDR ,ACCEL_XOUT_H_REG ,1,Mesurment ,6,1000);
//以x轴数据为例,应做如下处理:
Ax=(int16_t)(Mesurment[0]<<8)|Mesurment[1];
//处理后的数据除以LSB(灵敏度最低分辨率)即可得到加速度值
Accel=Ax*1.0/16384;

最小分辨率取值参考下表:

1617032758704-1620477659492.png

接下来是陀螺仪:

1617280360231-1620477659492.png

处理方式与加速度相同,最小分辨率参考下表:

1617280418367-1620477659493.png

最后是温度:

1617280458545-1620477659493.png

读出数据需要按照下述公式进行处理:

1617349496815-1620477659493.png

5.电源管理寄存器

1617280616125-1620477659493.png

具体各位功能参考数据手册,这里我们想让他正常工作,需要将所有位置0.

6.设备身份标识寄存器

1617280733301-1620477659493.png

说明设备地址的寄存器,若AD0接地,则最低位位0,设备地址为0x68;

若AD0置高,则最低位为1,设备地址为0x69.

配置完模块后,只要自己写出函数将数据读出打印出来即可。

以下读取函数仅作参考:

//陀螺仪数据读取
void GYRO_READ(void){
  uint8_t Mesurment[6];

  HAL_I2C_Mem_Read(&hi2c1,MPU6050_ADDR,GYRO_XOUT_H_REG,1,Mesurment,6,1000);
      Gxo = (int16_t )(Mesurment [0] << 8 | Mesurment [1]);
    Gyo = (int16_t )(Mesurment [2] << 8 | Mesurment [3]);
    Gzo = (int16_t )(Mesurment [4] << 8 | Mesurment [5]);

      Gx = (double)Gxo/LSB1_GYRO;
    Gy = (double)Gyo/LSB1_GYRO;
    Gz = (double)Gzo/LSB1_GYRO;
}
//加速度数据读取
void ACCEL_READ(void){
  uint8_t Mesurment[6];
    
    HAL_I2C_Mem_Read (&hi2c1 ,MPU6050_ADDR ,ACCEL_XOUT_H_REG ,1,Mesurment ,6,1000);
    
    Axo = (int16_t )(Mesurment [0] <<8 | Mesurment [1]);
    Ayo = (int16_t )(Mesurment [2] <<8 | Mesurment [3]);
    Azo = (int16_t )(Mesurment [4] <<8 | Mesurment [5]);
    
    Ax = (double) Axo / LSB1_ACCEL;
    Ay = (double) Ayo / LSB1_ACCEL;
    Az = (double) Azo / LSB1_ACCEL;
}
//温度数据读取
void TEMP_READ(void){
  uint8_t Mesurment[2];
    
    HAL_I2C_Mem_Read (&hi2c1 ,MPU6050_ADDR ,TEMP_OUT_H_REG ,1 ,Mesurment  ,2 ,1000);
    
    Tempo = (int16_t )(Mesurment [0]<<8)|Mesurment [1];
    Temp = 36.53 + Tempo * 1.0 / 340;
}

至此,第一部分结束。


DMP库移植

首先新建一个F401工程,在新工程里面完成IIC和UART的配置。

其中IIC选择IIC1,下拉栏选择IIC,配置默认即可。

生成工程并打开。

先把我们要移植的库文件复制到工程文件夹中:

下载并解压“MPU6050资料”这一文件,文件内容以及具体文件路径在README中有说明。

解压官方库文件“motion_driver_6.12.7z”

复制库文件和包含文件所在的两个文件夹,粘贴到工程文件夹。

1617352471139-1620477659493.png

回到keil中,新建文件夹命名为DMP

1617352556822-1620477659493.png

双击文件夹为其中添加库文件:

记得将文件类型的筛选改成所有文件

1617352641220-1620477659493.png

全选eMPL中的6个文件点击添加:

1617352677192-1620477659493.png

接下来添加所需的包含路径:

1617352990063-1620477659493.png

添加这两个文件:

1617353030848-1620477659493.png

接下来添加我们要用到的包含文件:

#include <stdio.h>
#include <math.h>
#include "inv_mpu.h"
#include "inv_mpu_dmp_motion_driver.h"

这时编译一下应该是0 errors

咳咳 应该是

1617353398948-1620477659493.png

那么接下来我们来修改库文件。

打开inv_mpu.c

可以看到这里官方已经为我们写好了F4的接口,只需要我们定义一个宏即可。复制一下宏的内容,打开选项界面选择C/C++,在上面的define中添加预编译的内容,即刚刚复制的宏,顺手把优化改到最低(方便后面出问题调试):

1617353709603-1620477659493.png

编译:

1617353777911-1620477659493.png

我们来看看这两个错误:

1617353828850-1620477659493.png

都是"board-st_discovery.h"没有找到导致的,这个文件是官方的测试工程中用到的文件,所以直接删除对于我们没有影响。

双击对应的错误传送带对应位置删除宏定义代码,

编译:0 errors!!!

1617354000906-1620477659493.png

缓缓的打出一堆问号??????????????????????????????

别急,我们慢慢解决。

首先让我们好好读一下官方的说明:

1617354101759-1620477659493.png

明确的说明了我们需要定义这四个函数来使库正常工作。而下面的宏定义就是留给我们的函数接口,于是我们开始写一下这几个函数。

首先根据网上资料,get_ms函数没什么用,所以下一个空函数防止报错;

delay_ms函数直接调用HAL库的延时函数;

而i2c_read和i2c_write我们通过调用HAL库的IIC函数来实现,需要注意的是HAL库函数与需要的函数参数个数和顺序都不一样。

参考代码如下:

//空函数防止报错
void mget_ms(unsigned long *time){}

//调用HAL库函数
uint8_t i2c_write(unsigned char slave_addr, unsigned char reg_addr,unsigned char length, unsigned char const *data){
    int res;
    res=HAL_I2C_Mem_Write(&hi2c1,slave_addr,reg_addr,1,(uint8_t *)data,length,1000);
    return res;
}
uint8_t i2c_read(unsigned char slave_addr, unsigned char reg_addr,unsigned char length, unsigned char *data){
    int res;
    res=HAL_I2C_Mem_Read(&hi2c1,slave_addr,reg_addr,1,(uint8_t *)data,length,1000);
    return res;
}

由于之前提到的HAL库IIC地址偏移特性,我们需要把库里面的设备地址改为0xD0:

在inv_mpu.c中搜索0x68:

1617357292231-1620477659493.png

改为0xD0:

1617357328178-1620477659493.png

把函数接口改成我们的函数名:

1617354938152-1620477659493.png

记得inv_mpu_dmp_motion_driver.c中的接口也要改:

1617355462081-1620477659493.png

编译,会看到一堆错误中有这样一个画风清奇:

1617355116084-1620477659493.png

这是在提醒我们要定义我们的模块型号,于是在预编译中添加MPU6050:

1617355183949-1620477659493.png

编译:2 errors

我们来看看是什么错误:

1617355311309-1620477659493.png

到inv_mpu_dmp_motion_driver.c中搜索operation看到

__no_operation();

看函数名就知道啥都没做,注释掉。

更改inv_mpu.c中log的宏,用空语句替换:

#define log_i(...)     do {} while (0)
#define log_e(...)     do {} while (0)

编译,0 errors!!!

接下来我们来调用库里面的函数进行模块的初始化以及数据的输出。

添加我们需要的宏定义:

/* USER CODE BEGIN PD */
#define q30  1073741824.0f
/* USER CODE END PD */

定义我们需要的变量:

/* USER CODE BEGIN PV */
float pitch=0,roll=0,yaw=0;
static signed char gyro_orientation[9] = { 1, 0, 0,
                                           0, 1, 0,
                                           0, 0, 1};
/* USER CODE END PV */

定义我们需要的函数(网上抄的):

/* USER CODE BEGIN PFP */
//得到dmp处理后的数据(注意,本函数需要比较多堆栈,局部变量有点多)
//pitch:俯仰角 精度:0.1°   范围:-90.0° <---> +90.0°
//roll:横滚角  精度:0.1°   范围:-180.0°<---> +180.0°
//yaw:航向角   精度:0.1°   范围:-180.0°<---> +180.0°
//返回值:0,正常
//    其他,失败
uint8_t mpu_dmp_get_data(float *pitch,float *roll,float *yaw)
{
    float q0=1.0f,q1=0.0f,q2=0.0f,q3=0.0f;
    unsigned long sensor_timestamp;
    short gyro[3], accel[3], sensors;
    unsigned char more;
    long quat[4]; 
    if(dmp_read_fifo(gyro, accel, quat, &sensor_timestamp, &sensors,&more))return 1;     
    /* Gyro and accel data are written to the FIFO by the DMP in chip frame and hardware units.
     * This behavior is convenient because it keeps the gyro and accel outputs of dmp_read_fifo and mpu_read_fifo consistent.
    **/
    /*if (sensors & INV_XYZ_GYRO )
    send_packet(PACKET_TYPE_GYRO, gyro);
    if (sensors & INV_XYZ_ACCEL)
    send_packet(PACKET_TYPE_ACCEL, accel); */
    /* Unlike gyro and accel, quaternions are written to the FIFO in the body frame, q30.
     * The orientation is set by the scalar passed to dmp_set_orientation during initialization. 
    **/
    if(sensors&INV_WXYZ_QUAT) 
    {
        q0 = quat[0] / q30;    //q30格式转换为浮点数
        q1 = quat[1] / q30;
        q2 = quat[2] / q30;
        q3 = quat[3] / q30; 
        //计算得到俯仰角/横滚角/航向角
        *pitch = asin(-2 * q1 * q3 + 2 * q0* q2)* 57.3;    // pitch
        *roll  = atan2(2 * q2 * q3 + 2 * q0 * q1, -2 * q1 * q1 - 2 * q2* q2 + 1)* 57.3;    // roll
        *yaw   = atan2(2*(q1*q2 + q0*q3),q0*q0+q1*q1-q2*q2-q3*q3) * 57.3;    //yaw
    }else return 2;
    return 0;
}


int run_self_test(void)
{
    int result;
    char test_packet[4] = {0};
    long gyro[3], accel[3]; 
    result = mpu_run_self_test(gyro, accel);
    if (result == 0x7) 
    {
        /* Test passed. We can trust the gyro data here, so let's push it down
        * to the DMP.
        */
        float sens;
        unsigned short accel_sens;
        mpu_get_gyro_sens(&sens);
        gyro[0] = (long)(gyro[0] * sens);
        gyro[1] = (long)(gyro[1] * sens);
        gyro[2] = (long)(gyro[2] * sens);
        dmp_set_gyro_bias(gyro);
        mpu_get_accel_sens(&accel_sens);
        accel[0] *= accel_sens;
        accel[1] *= accel_sens;
        accel[2] *= accel_sens;
        dmp_set_accel_bias(accel);
        return 0;
    }else return 1;
}


//DMP初始化函数
uint8_t mpu_dmp_init(void)
{
    uint8_t res=0;
    struct int_param_s int_param;//这个没什么用,就是为了能给他实参调用起来
    if(mpu_init(&int_param)==0)    //初始化MPU6050
    {     
        res=mpu_set_sensors(INV_XYZ_GYRO|INV_XYZ_ACCEL);//设置所需要的传感器
        if(res)return 1; 
        res=mpu_configure_fifo(INV_XYZ_GYRO | INV_XYZ_ACCEL);//设置FIFO
        if(res)return 2; 
        res=mpu_set_sample_rate(100);    //设置采样率
        if(res)return 3; 
        res=dmp_load_motion_driver_firmware();        //加载dmp固件
        if(res)return 4; 
        //res=dmp_set_orientation(inv_orientation_matrix_to_scalar(gyro_orientation));//设置陀螺仪方向
        if(res)return 5; 
        res=dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT|DMP_FEATURE_TAP|    //设置dmp功能
            DMP_FEATURE_ANDROID_ORIENT|DMP_FEATURE_SEND_RAW_ACCEL|DMP_FEATURE_SEND_CAL_GYRO|
            DMP_FEATURE_GYRO_CAL);
        if(res)return 6; 
        res=dmp_set_fifo_rate(100);    //设置DMP输出速率(最大不超过200Hz)
        if(res)return 7;   
        res=run_self_test();        //自检
        if(res)return 8;    
        res=mpu_set_dmp_state(1);    //使能DMP
        if(res)return 9;
    }
    else return 10;
    return 0;
}

在循环前面调用初始化函数,在循环中调用数据处理函数,并打印出欧拉角的值:

  /* USER CODE BEGIN WHILE */
    
    uint8_t res;
    res = mpu_dmp_init();
    
  while (1)
  {
        res=mpu_dmp_get_data(&pitch,&roll,&yaw);
        printf("%.2f,%.2f,%.2f\n",pitch,roll,yaw);
    /* USER CODE END WHILE */

串口重定向:

/* USER CODE BEGIN 4 */
int fputc(int ch,FILE *f)
{
    HAL_UART_Transmit (&huart1 ,(uint8_t *)&ch,1,HAL_MAX_DELAY );
    return ch;
}
/* USER CODE END 4 */

编译烧录,就可以在串口调试助手上看到欧拉角了:

1617357509432-1620477659493.png

如果出现了三个角度全为0的情况,必然是某一环出现的问题,我会将我遇到的问题在之后列举一下,可以自己使用打印对应返回值以及单步调试的方法逐步排查。

问题汇总

PS:“/”表示不易出现问题,如出现问题可能是硬件问题,检查是否是未供电以及供电电压是否正确,或者可能你的模块坏掉了。

函数返回值mpu_dmp_init函数mpu_dmp_get_data函数
1/dmp_read_fifo函数出现问题
2//
3//
4//
5//
6//
7//
8说明run_self_test函数出现问题/
9//
10说明mpu_init函数出现问题/
run_self_test问题

进入run_self_test函数内部有一个

mpu_run_self_test函数,将它的result打印出来。

resultmpu_run_self_test内部
0x4加速度计和陀螺仪自检均未通过,考虑为模块损坏
0x5加速度计自检未通过,未知原因
0x6陀螺仪自检未通过,未知原因

如果重新连线、改变防止方向(芯片平放)无法解决你的问题,可以试试将后面的条件判断修改为你的值,看看是否可以出数据,但是如果出了数据,对数据的准确性无法做以保证。

mpu_init问题

这里的问题一般是由于IIC配置错误导致的。

进入函数内不出意外的话程序会在i2c_write返回-1,此时进入函数内部查看返回值:

HAL_ERROR:请检查之前的0x68改为0xD0是否有更改。如果更改了,则可能是其他的地方地址出现问题。(检查AD0引脚是否被拉高,若拉高则地址应为0xD1)。

HAL_BUSY:硬件IIC会出现的busy锁死问题,这是ST硬件上的一点小问题,可能已经在后续的产品中修复。临时解决办法是把设备完全断电,在不上IIC的情况下上电,清除BUSY位后断电重新插线即可解决问题。

dmp_read_fifo问题

进入函数内部,不出意外是mpu_read_fifo_stream这个函数处返回了-1,这说明我们读取速度太慢了导致了FIFO溢出,只需要在读取语句之后加上一行代码:

while(mpu_dmp_get_data(&pitch,&roll,&yaw));

在FIFO下次溢出前再次读取即可。

支付宝捐赠
请使用支付宝扫一扫进行捐赠
微信捐赠
请使用微信扫一扫进行赞赏
1 + 1 =
快来做第一个评论的人吧~