2. MPU6050模块

2.1. MPU6050 简介

YH-MPU6050 是野火科技推出的六轴传感器模块,见图2-1,它采用 InvenSense 公司的 MPU6050 作为主芯片, 能同时检测三轴加速度、三轴陀螺仪(三轴角速度)的运动数据以及温度数据。利用 MPU6050 芯片内部的 DMP 模块(Digital Motion Processor 数字运动处理器), 可对传感器数据进行滤波、融合处理,直接通过 IIC 接口向主控器输出姿态解算后的数据,降低主控器的运算量。其姿态解算频率最高可达 200Hz, 非常适合用于对姿态控制实时要求较高的领域。常见应用于手机、智能手环、四轴飞行器、计步器等的姿态检测。

mpu6050_1

图2 - 1 MPU6050模块外观

2.2. MPU6050模块的引脚功能说明

该模块引出的8个引脚功能说明见下表,

mpu6050模块引脚说明

序号

引脚名称

说明

1

VCC

3.3/5V电源输入

2

GND

地线

3

SCL

I2C从时钟信号线SCL (模块上已接上拉电阻)

4

SDA

I2C从数据信号线SDA (模块上已接上拉电阻)

5

XDA

I2C主串行数据信号线,用于外接传感器(模块上已接上拉电阻)

6

XCL

I2C主串行时钟信号线,用于外接传感器(模块上已接上拉电阻)

7

AD0

从机地址设置引脚 :1.接地或悬空时, 地址为0x68;2.接VCC时,地址为0x69

8

INT

中断输出引脚

其中的SDA/SCL、XDA/XCL通讯引脚分别为两组I2C信号线。当模块与外部主机通讯时,使用SDA/SCL,如与STM32芯片通讯; 而XDA/XCL则用于MPU6050芯片与其它I2C传感器通讯时使用,例如使用它与磁场传感器连接,MPU6050模块可以把从主机SDA/SCL接收的数据或命令通过XDA/XCL引脚转发到磁场传感器中。 但实际上这种功能比较鸡肋,控制麻烦且效率低,一般会直接把磁场传感器之类的I2C传感器直接与MPU6050挂载在同一条总线上(即都连接到SDA/SCL),使用主机直接控制。

2.3. 硬件原理图说明

MPU6050模块的硬件原理图见下图2 - 2,

mpu6050_2

图2 - 2 MPU6050模块原理图

它的硬件非常简单,SDA与SCL被引出方便与外部I2C主机连接,看图中的右上角,可知该模块的I2C通讯引脚SDA及SCL已经连接了上拉电阻, 因此它与外部I2C通讯主机通讯时直接使用导线连接起来即可;而MPU6050模块与其它传感器通讯使用的XDA、XCL引脚没有接上拉电阻,要使用时需要注意。 模块自身的I2C设备地址可通过AD0引脚的电平控制,当AD0接地时,设备地址为0x68(七位地址),当AD0接电源时,设备地址为0x69(七位地址)。 另外,当传感器有新数据的时候会通过INT引脚通知STM32。

由于MPU6050检测时是基于自身中心坐标系的,见图2 - 3,它表示的坐标系及旋转符号标出了MPU6050传感器的XYZ轴的加速度有角速度的正方向。所以在安装模块时,您需要考虑它与所在设备的坐标系统的关系。

mpu6050_3

图2 - 3 MPU6050传感器的坐标及方向

2.4. 连接方式

本模块配套STM32驱动程序,可直接使用野火F103霸道、F103指南者及F429挑战者开发板进行测试。按要求使用杜邦线把模块连接到开发板,并下载程序即可, 其中野火F429挑战者与霸道开发板板载了MPU6050芯片,不需要外接模块,此处直接列出其引脚关系供使用STM32F1的用户参考。

mpu6050模块引脚说明

序号

引脚名称

与F103霸道、F103指南者、F429开发板连接

F103-MINI开发板

1

VCC

接3.3V或5V

接3.3V或5V

2

GND

GND

GND

3

SCL

PB6

PA2 (软件IIC)

4

SDA

PB7

PA3(软件IIC)

5

AD0

悬空或接地

悬空或接地

6

INT

悬空或接地

悬空或接地

2.5. 硬件STM32-MPU6050例程介绍及实验现象

硬件I2C,MPU6050基本驱动程序,不包含DMP功能,没有移植官方驱动程序。本程序通过串口输出简单测量数据,没有驱动液晶显示。(不支持匿名上位机可视数据)。

(使用硬件I2C时不能与液晶屏同时使用,因为FSMC的NADV与I2C1的SDA 是同一个引脚,互相影响了)

2.5.1. 实验现象

按照上面的连接方式将模块与开发板连接之后,通过USB线将开发板与电脑连接并上电,下载程序后打开电脑上的野火多功能调试助手, 打开与开发板连接的串口后会有MPU6050不断传来的数据信息,见下图2 - 4。 同时板子上的 LED 灯也会缓慢闪烁,每1秒钟翻转一次状态。

mpu6050_4

图2 - 4 MPU6050传感器的坐标及方向

2.5.2. 硬件STM32-MPU6050例程介绍

初始化硬件I2C

本实验中的I2C驱动与MPU6050驱动分开主要是考虑到扩展其它传感器时的通用性,如使用磁场传感器、气压传感器都可以使用同样一个I2C驱动, 这个驱动只要给出针对不同传感器时的不同读写接口即可。关于STM32的I2C驱动原理请参考野火STM32教程《零死角玩转STM32》中读写EEPROM的章节, 本章讲解的I2C驱动主要针对接口封装讲解,细节不再赘述。本实验中的I2C硬件定义在 bsp_i2c.h 中,具体如下,这些宏根据传感器使用的I2C硬件封装起来了。

bsp_i2c.h(霸道开发板例程)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 /**************************I2C参数定义,I2C1或I2C2********************************/
 #define             SENSORS_I2Cx                                I2C1
 #define             SENSORS_I2C_APBxClock_FUN                   RCC_APB1PeriphClockCmd
 #define             SENSORS_I2C_CLK                             RCC_APB1Periph_I2C1
 #define             SENSORS_I2C_GPIO_APBxClock_FUN              RCC_APB2PeriphClockCmd
 #define             SENSORS_I2C_GPIO_CLK                        RCC_APB2Periph_GPIOB
 #define             SENSORS_I2C_SCL_PORT                        GPIOB
 #define             SENSORS_I2C_SCL_PIN                         GPIO_Pin_6
 #define             SENSORS_I2C_SDA_PORT                        GPIOB
 #define             SENSORS_I2C_SDA_PIN                         GPIO_Pin_7

接下来利用这些宏对I2C进行初始化,初始化过程与I2C读写EEPROM中的无异,见如下,

bsp_i2c.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 static void I2C_GPIO_Config(void)
 {
   GPIO_InitTypeDef  GPIO_InitStructure;

   /* 使能与 I2C1 有关的时钟 */
   SENSORS_I2C_APBxClock_FUN ( SENSORS_I2C_CLK, ENABLE );
   SENSORS_I2C_GPIO_APBxClock_FUN ( SENSORS_I2C_GPIO_CLK, ENABLE );


   /* PB6-I2C1_SCL、PB7-I2C1_SDA*/
   GPIO_InitStructure.GPIO_Pin = SENSORS_I2C_SCL_PIN;
   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;          // 开漏输出
   GPIO_Init(SENSORS_I2C_SCL_PORT, &GPIO_InitStructure);

   GPIO_InitStructure.GPIO_Pin = SENSORS_I2C_SDA_PIN;
   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;          // 开漏输出
   GPIO_Init(SENSORS_I2C_SDA_PORT, &GPIO_InitStructure);

 }


 /**
 * @brief  I2C 工作模式配置
 * @param  无
 * @retval 无
 */
 static void I2C_Mode_Configu(void)
 {
   I2C_InitTypeDef  I2C_InitStructure;

   /* I2C 配置 */
   I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;

       /* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
   I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;

   I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7;
   I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;

       /* I2C的寻址模式 */
   I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

       /* 通信速率 */
   I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;

       /* I2C1 初始化 */
   I2C_Init(SENSORS_I2Cx, &I2C_InitStructure);

       /* 使能 I2C1 */
   I2C_Cmd(SENSORS_I2Cx, ENABLE);
 }

MPU6050的寄存器定义

MPU6050有各种各样的寄存器用于控制工作模式,我们把这些寄存器的地址、寄存器位使用宏定义到了 mpu6050.h 文件中了,代码如下。

mpu6050.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 //模块的A0引脚接GND,IIC的7位地址为0x68,若接到VCC,需要改为0x69
 #define MPU6050_SLAVE_ADDRESS  (0x68<<1)      //MPU6050器件读地址

 #define MPU6050_WHO_AM_I        0x75
 #define MPU6050_SMPLRT_DIV      0  //8000Hz
 #define MPU6050_DLPF_CFG        0
 #define MPU6050_GYRO_OUT        0x43     //MPU6050陀螺仪数据寄存器地址
 #define MPU6050_ACC_OUT         0x3B     //MPU6050加速度数据寄存器地址

 // ... 这里省略了很多MPU6050的寄存器宏定义
 // ... ...

初始化MPU6050

根据MPU6050的寄存器功能定义,我们使用I2C往寄存器写入特定的控制参数,代码如下,

mpu6050.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 /**
   * @brief   写数据到MPU6050寄存器
   * @param
   * @retval
   */
 void MPU6050_WriteReg(u8 reg_add,u8 reg_dat)
 {
   I2C_ByteWrite(reg_dat,reg_add);
 }

 /**
   * @brief   从MPU6050寄存器读取数据
   * @param
   * @retval
   */
 void MPU6050_ReadData(u8 reg_add,unsigned char* Read,u8 num)
 {
   I2C_BufferRead(Read,reg_add,num);
 }


 /**
   * @brief   初始化MPU6050芯片
   * @param
   * @retval
   */
 void MPU6050_Init(void)
 {
   int i=0,j=0;
   //在初始化之前要延时一段时间,若没有延时,则断电后再上电数据可能会出错
   for(i=0;i<1000;i++)
   {
     for(j=0;j<1000;j++)
     {
       ;
     }
   }
   MPU6050_WriteReg(MPU6050_RA_PWR_MGMT_1, 0x00);      //解除休眠状态
   MPU6050_WriteReg(MPU6050_RA_SMPLRT_DIV , 0x07);     //陀螺仪采样率
   MPU6050_WriteReg(MPU6050_RA_CONFIG , 0x06);
   MPU6050_WriteReg(MPU6050_RA_ACCEL_CONFIG , 0x01);   //配置加速度传感器工作在2G模式
   MPU6050_WriteReg(MPU6050_RA_GYRO_CONFIG, 0x18);     //陀螺仪自检及测量范围,典型值:0x18(不自检,2000deg/s)
 }

这段代码首先使用 MPU6050_ReadData 及MPU6050_WriteReg 函数封装了I2C的底层读写驱动,接下来用它们在 MPU6050_Init 函数中向MPU6050寄存器写入控制参数,设置了MPU6050的采样率、量程(分辨率)等。

读传感器ID

初始化后,可通过读取它的“WHO AM I”寄存器内容来检测硬件是否正常,该寄存器存储了ID号0x68,见代码如下。

mpu6050.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 /**
   * @brief   读取MPU6050的ID
   * @param
   * @retval  正常返回1,异常返回0
   */
 uint8_t MPU6050ReadID(void)
 {
     unsigned char Re = 0;
      MPU6050_ReadData(MPU6050_RA_WHO_AM_I,&Re,1);    //读器件地址
      if (Re != 0x68) {
          MPU_ERROR("检测不到MPU6050模块,请检查模块与开发板的接线");
          return 0;
      } else {
          MPU_INFO("MPU6050 ID = %d\r\n",Re);
          return 1;
      }

  }

读取原始数据

若传感器检测正常,就可以读取它数据寄存器获取采样数据了,见代码如下。

mpu6050.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  /**
    * @brief   读取MPU6050的加速度数据
    * @param
    * @retval
    */
  void MPU6050ReadAcc(short *accData)
  {
      u8 buf[6];
      MPU6050_ReadData(MPU6050_ACC_OUT, buf, 6);
      accData[0] = (buf[0] << 8) | buf[1];
      accData[1] = (buf[2] << 8) | buf[3];
      accData[2] = (buf[4] << 8) | buf[5];
  }

  /**
    * @brief   读取MPU6050的角加速度数据
    * @param
    * @retval
    */
  void MPU6050ReadGyro(short *gyroData)
  {
      u8 buf[6];
      MPU6050_ReadData(MPU6050_GYRO_OUT,buf,6);
      gyroData[0] = (buf[0] << 8) | buf[1];
      gyroData[1] = (buf[2] << 8) | buf[3];
      gyroData[2] = (buf[4] << 8) | buf[5];
  }

  /**
    * @brief   读取MPU6050的原始温度数据
    * @param
    * @retval
    */
  void MPU6050ReadTemp(short *tempData)
  {
      u8 buf[2];
      MPU6050_ReadData(MPU6050_RA_TEMP_OUT_H,buf,2);     //读取温度值
      *tempData = (buf[0] << 8) | buf[1];
  }

  /**
    * @brief   读取MPU6050的温度数据,转化成摄氏度
    * @param
    * @retval
    */
  void MPU6050_ReturnTemp(float*Temperature)
  {
      short temp3;
      u8 buf[2];

      MPU6050_ReadData(MPU6050_RA_TEMP_OUT_H,buf,2);     //读取温度值
      temp3= (buf[0] << 8) | buf[1];
      *Temperature=((double) (temp3 /340.0))+36.53;
  }

其中以上前三个函数:MPU6050ReadAcc、MPU6050ReadGyro 和 MPU6050ReadTemp 分别用于读取三轴加速度、角速度及温度值,这些都是原始的ADC数值(16位长), 对于加速度和角速度,把读取得的ADC值除以分辨率,即可求得实际物理量数值。 最后一个函数 MPU6050_ReturnTemp 展示了温度ADC值与实际温度值间的转换,它是根据MPU6050的说明给出的转换公式进行换算的, 注意陀螺仪检测的温度会受自身芯片发热的影响,严格来说它测量的是自身芯片的温度,所以用它来测量气温是不太准确的。 对于加速度和角速度值我们没有进行转换,在下一小节中我们直接利用这些数据交给 DMP 单元,求解出姿态角。

main函数

最后我们来看看本实验的main函数:

main.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 /**
   * @brief  主函数
   * @param  无
   * @retval 无
   */
 int main(void)
 {
   /* LED 端口初始化 */
   LED_GPIO_Config();
   /* 串口通信初始化 */
   USART_Config();

   //I2C初始化
   I2C_Bus_Init();
   //MPU6050初始化
   MPU6050_Init();
   //检测MPU6050
   if( MPU6050ReadID() == 0 )
   {
     printf("\r\n没有检测到MPU6050传感器!\r\n");
     LED_RED;
     while(1);       //检测不到MPU6050 会红灯亮然后卡死
   }

   /* 配置SysTick定时器和中断 */
   SysTick_Init(); //配置 SysTick 为 1ms 中断一次,在中断里读取传感器数据
   SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; //启动定时器


   while(1)
   {
     if( task_readdata_finish ) //task_readdata_finish = 1 表示读取MPU6050数据完成
     {

       printf("加速度:%8d%8d%8d",Acel[0],Acel[1],Acel[2]);

       printf("    陀螺仪%8d%8d%8d",Gyro[0],Gyro[1],Gyro[2]);

       printf("    温度%8.2f\r\n",Temp);

       task_readdata_finish = 0; // 清零标志位

     }
   }

 }

在main函数里,调用 I2C_Bus_Init、MPU6050_Init 及 MPU6050ReadID 函数后,在 while 主循环中使用串口打印加速度、角速度及温度值到电脑端。

本实验中控制MPU6050并没有使用中断引脚进行检测,我们是利用 SysTick 定时器进行计时,每隔一段时间就会读取 MPU6050 的数据寄存器获取采样数据,并且将 task_readdata_finish 标志位置 1, 该标志位置 1 后就可以在主循环中将MPU6050的加速度、角速度及温度值打印出来,再将 task_readdata_finish 重新置0,等待它再一次被置 1、然后再一次打印数据。

那么标志位 task_readdata_finish 是在哪里被置位的呢?答案是在 SysTick 定时器中断服务函数当中。 我们在进入主循环之前就已经配置SysTick定时器和中断、并且启动了它,使得它每 1ms 它都会产生一次中断,去执行中断服务函数。该中断服务函数如下。

stm32f10x_it.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 #define TASK_DELAY_NUM  2       //总任务个数,可以自己根据实际情况修改
 #define TASK_DELAY_0    1000    //任务0延时 1000*1 毫秒后执行:翻转LED
 #define TASK_DELAY_1    500     //任务1延时 500*1 毫秒后执行:MPU6050任务

 uint32_t Task_Delay_Group[TASK_DELAY_NUM];  //任务数组,用来计时、并判断是否执行对应任务


 /* 执行任务标志:读取MPU6050数据 */
 // - 标志置 1表示读取MPU6050数据完成,需要在主循环处理MPU6050数据
 // - 标志置 0表示未完成读取MPU6050数据,需要在中断中读取MPU6050数据
 int task_readdata_finish;


 void SysTick_Handler(void)
 {
   int i;

   for(i=0; i<TASK_DELAY_NUM; i++)
   {
     Task_Delay_Group[i] ++;                   //任务计时,时间到后执行
   }

   /* 处理任务0 */
   if(Task_Delay_Group[0] >= TASK_DELAY_0)     //判断是否执行任务0
   {
     Task_Delay_Group[0] = 0;                  //置0重新计时

     /* 任务0:翻转LED */
     LED2_TOGGLE;
   }

   /* 处理任务1 */
   if(Task_Delay_Group[1] >= TASK_DELAY_1)     //判断是否执行任务1
   {
     Task_Delay_Group[1] = 0;                  //置0重新计时

     /* 任务1:MPU6050任务 */
     if( ! task_readdata_finish )
     {
       MPU6050ReadAcc(Acel);
       MPU6050ReadGyro(Gyro);
       MPU6050_ReturnTemp(&Temp);

       task_readdata_finish = 1; //标志位置1,表示需要在主循环处理MPU6050数据
     }
   }

   /* 处理任务2 */
   //添加任务需要修改任务总数的宏定义 TASK_DELAY_NUM
   //并且添加定义任务的执行周期宏定义 TASK_DELAY_x(x就是一个编号),比如 TASK_DELAY_2
 }

SysTick中断服务函数的代码中,我们用for语句将变量 Task_Delay_Group[1] 自加1,使用 TASK_DELAY_1 变量来控制定时时间, 当 Task_Delay_Group[1] >= TASK_DELAY_1 时说明定时时间到了,就将 Task_Delay_Group[1] 重新置0,让它重新计时, 并且读取 MPU6050 的加速度、角速度及温度数据后将标志位 task_readdata_finish 置 1,表示数据读取完成, 然后在执行完中断服务函数之后,我们就回到主循环把读取到的数据打印出来。