STM32软件I2C驱动MPU6050

发布时间 2023-07-13 14:44:47作者: Sightseer观光客

STM32软件I2C驱动MPU6050

STM32F103C8T6基于Keil MDK标准库

硬件接线

这里没有什么复杂的地方,采用MPU6050的现成模块.模块的SCL接B10,SDA接B11,这里连接了一个OLED显示屏,用于显示获取到的数据.
注意:这里使用的模块自带上拉电阻

Alt text

软件实现

首先在工程目录里创建:

"MyI2C.h"和"MyI2C.c"文件,用于软件驱动I2C.

"MPU6050.h","MPU6050.c"和"MPU6050Reg.h"文件,用于MPU6050的驱动.

  • 在MyI2C.h文件中设置软件I2C的GPIO号,这里采用宏定义的方式:
//设置I2C引脚端口,注意如端口号修改,时钟使能也要修改
#define SCL_PORT    GPIOB
#define SCL_LINE    GPIO_Pin_10
#define SDA_PORT    GPIOB
#define SDA_LINE    GPIO_Pin_11
  • 软件I2C的延迟宏定义
//设置I2C操作延迟(速度)
#define I2C_DELAY    do{Delay_us(10);}while(0);
  • 由于使用库函数的GPIO_WriteBit()函数操作GPIO口电平不够优雅简洁,这里使用了带参宏和函数来对GPIO进行操作,注意:这里的操作使用了延时宏.
//I2C引脚电平写
#define SCL_SET(x)    do{GPIO_WriteBit(SCL_PORT,SCL_LINE,(BitAction)(x)); I2C_DELAY;} \
                      while(0);
#define SDA_SET(x)    do{GPIO_WriteBit(SDA_PORT,SDA_LINE,(BitAction)(x)); I2C_DELAY;} \
                      while(0);
//I2C引脚电平读
uint8_t READ_SDA(void){
    uint8_t val;
    val = GPIO_ReadInputDataBit(SDA_PORT,SDA_LINE);
    I2C_DELAY;
    return val;
}
  • 接下来写软件I2C的初始化代码,这里就是配置GPIO,不像硬件I2C那样麻烦,注意这里GPIO模式要配置成开漏输出(I2C的定义).
void MyI2C_Init(){
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
    GPIO_InitStructure.GPIO_Pin  = SCL_LINE | SDA_LINE;
    GPIO_InitStructure.GPIO_Speed= GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&GPIO_InitStructure);
}
  • 然后就是I2C的六块拼图:起始,终止,接收一个字节,发送一个字节,接收应答,发送应答

起始和终止信号

Alt text

注意:SDA_SET(1)不是设置SDA或SCL为高电平,而是释放总线(开漏输出特性),然后利用上拉电阻把总线拉高

void MyI2C_Start(){
    //为保证兼容重复开始条件,先释放SDA再释放SCL
    SDA_SET(1);
    SCL_SET(1);
    SDA_SET(0);
    SCL_SET(0);
}
void MyI2C_Stop(){
    SDA_SET(0);
    SCL_SET(1);
    SDA_SET(1);
}

发送一个字节和接收一个字节

Alt text

Alt text

void MyI2C_SendByte(uint8_t byte){
    for(uint8_t i = 0;i < 8;i++){
        SDA_SET(byte & (0x80 >> i));    //SDA写数据,I2C是高位先行
        SCL_SET(1);    SCL_SET(0);            //给SCL一个脉冲,让从机把SDA的数据读走
    }
}

uint8_t MyI2C_ReceiveByte(){
    uint8_t byte = 0x00;
    SDA_SET(1);            //先释放SDA
    for(uint8_t i = 0; i < 8;i++){
        SCL_SET(1);        //设置SCL为高,此时从机把数据放在SDA上
        if(READ_SDA() == 1){byte |= (0x80 >> i);}    //由高到低位读SDA
        SCL_SET(0);        //设置SCL为低,一个时钟结束
    }
    return byte;
}

发送应答和接收应答

Alt text

void MyI2C_SendACK(uint8_t ackbit){
    SDA_SET(ackbit);        //把应答位放在SDA上
    SCL_SET(1);    SCL_SET(0);    //给SCL一个脉冲,让从机读取应答位
}

uint8_t MyI2C_ReceiveACK(){
    uint8_t ackbit;
    SDA_SET(1);        //释放SDA
    SCL_SET(1);        //给SCL一个脉冲,让从机把应答位写到SDA上
    ackbit = READ_SDA();
    SCL_SET(0);
    return ackbit;
}
  • 不要忘记在MyI2C.h头文件中声明
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);
  • 至此,I2C六块拼图全部完成,可以开始写应用层代码了

  • 首先在MPU6050_Reg.h中添加如下宏定义,这样就不用查寄存器表了

#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H

#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

  • 首先在MPU6050.c文件中,添加芯片的I2C的地址号,同样采用宏定义方式.0x68为MPU6050的固有I2C地址,实际发送时,要把I2C的地址左移1位,再在最低位写0表示写时序,写1表示读时序.
#define MPU6050_I2C_ADDR    (0x68)
#define MPU6050_WRITE_ADDR    (((MPU6050_I2C_ADDR) << 1) | 0x00)
#define MPU6050_READ_ADDR    (((MPU6050_I2C_ADDR) << 1) | 0x01)

这是芯片的读寄存器函数

  • 我们要通过这个函数对芯片的寄存器进行读出,具体为什么要这样做,可以参考芯片的读写时序图

Alt text

  • 上图部分解释:
  1. Master/Slave 主机/从机
  2. S开始 ,AD+W地址+写 ,ACK应答
  3. RA寄存器地址 ,AD+R地址+读 ,DATA数据 ,NACK非应答 ,停止P .
  • 我们使用的是单字节读时序,参考上图和下表,时序结构就很清晰了.
uint8_t MPU6050_ReadReg(uint8_t reg_addr){
    uint8_t data;
    MyI2C_Start();  //1
    MyI2C_SendByte(MPU6050_WRITE_ADDR);  //2
    MyI2C_ReceiveACK();  //3
    MyI2C_SendByte(reg_addr);  //4
    MyI2C_ReceiveACK();  //5
    
    MyI2C_Start();  //6
    MyI2C_SendByte(MPU6050_READ_ADDR);  //7
    MyI2C_ReceiveACK();  //8
    data = MyI2C_ReceiveByte();  //9
    MyI2C_SendACK(1);        //NACK 10
    MyI2C_Stop();  //11
    
    return data;
}

这里是芯片的写寄存器函数

Alt text

void MPU6050_WriteReg(uint8_t reg_addr,uint8_t data){
    MyI2C_Start();  //1
    MyI2C_SendByte(MPU6050_WRITE_ADDR);  //2
    MyI2C_ReceiveACK();  //3
    MyI2C_SendByte(reg_addr);  //4
    MyI2C_ReceiveACK();  //5
    MyI2C_SendByte(data);  //6
    MyI2C_ReceiveACK();  //7
    MyI2C_Stop();  //8
}
  • 好了,我们现在有芯片的读/写函数了,众所周知,如果我们能把一个屏幕的任意一个像素点点亮,那么,我们就能对这块屏幕胡作非为了,哈哈.屏幕如此,芯片亦如此.

下面是MPU6050的初始化函数

首先初始化I2C(GPIO初始化),然后配置必要的寄存器,最后动态分配一块内存区域,用于下面的函数返回结构体变量,注意使用malloc函数要在.h文件内包含#include <stdlib.h>这个库文件

void MPU6050_Init(){
    MyI2C_Init();
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);        //关闭睡眠模式
    MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
    MPU6050_WriteReg(MPU6050_CONFIG,0x06);
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x00);
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x00);
    //动态分配一个内存区域
    p = (MPU6050_DATA*)malloc(sizeof(MPU6050_DATA));
}
  • 对上述初始化函数配置的寄存器的解释:

  • MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);

Alt text

我们配置为0b0000 0001,即设备不复位,不睡眠,关闭循环模式,温度传感器使能,时钟选择为内部X轴陀螺仪晶振.

  • 以下为CLKSEL位对应的时钟源选择:

Alt text

  • MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);

Alt text

这里配置为0b0000 0000,唤醒频率我们不配置(默认1.25Hz),6轴的传感器也不待机.

  • 以下为LP_WAKE_CTRL位对应的唤醒频率:

Alt text

  • MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);

Alt text

采样率分频配置为0x09,低通滤波配置为最大,此时陀螺仪输出速率为1kHz(下文会提及),根据手册公式可得采样率100Hz.

  • 手册给出的采样率计算公式

Alt text

  • MPU6050_WriteReg(MPU6050_CONFIG,0x06);

Alt text

此处配置为低通滤波最大(0b0000 0110),对应输出速率为5Hz

  • 下图为低通滤波器配置表,可见当没有启用DLPF时,陀螺仪输出频率为8kHz,启用DLPF时,输出频率为1kHz.

Alt text

  • MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x00); MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x00);

Alt text

  • 陀螺仪量程配置位对应的量程

Alt text

Alt text

  • 加速度计量程配置为对应的量程

Alt text

我们这里没有配置自检和加速度计高通滤波,我们配置了陀螺仪的量程为最低:±250°/s,加速度计量程也为最低:±2g,以获得最大的测量精度.

  • 至此,MPU6050的初始化完成.

下面是陀螺仪获取数据的函数

这里在MPU6050.h文件里声明了一个结构体变量,用于储存MPU6050的数据信息

typedef struct{
    int16_t AccX,AccY,AccZ;
    int16_t GyroX,GyroY,GyroZ;
    int16_t Temp;
}MPU6050_DATA;

这里获取数据的函数采用返回结构体指针的方式返回多个变量,这样做可以减少模块之间的耦合性,提高程序可移植性.

这里还使用了移位操作把高8位和低8位的数据结合,应该很好理解,这里不再阐述.

MPU6050_DATA* MPU6050_GetData(){
    
    uint16_t dataH,dataL;
    //AccX
    dataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
    dataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
    p->AccX = (dataH << 8)    | dataL;
    //AccY
    dataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
    dataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
    p->AccY = (dataH << 8)    | dataL;
    //AccZ
    dataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
    dataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
    p->AccZ = (dataH << 8)    | dataL;
    //GyroX
    dataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
    dataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
    p->GyroX = (dataH << 8)    | dataL;
    //GyroY
    dataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
    dataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
    p->GyroY = (dataH << 8)    | dataL;
    //GyroZ
    dataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
    dataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
    p->GyroZ = (dataH << 8)    | dataL;
    //TEMP
    dataH = MPU6050_ReadReg(MPU6050_TEMP_OUT_H);
    dataL = MPU6050_ReadReg(MPU6050_TEMP_OUT_L);
    p->Temp = (dataH << 8)    | dataL;
    
    return p;
}

最后在main.c文件内调用功能函数

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

int main(void){
    OLED_Init();
    //MPU6050初始化
    MPU6050_Init();
    //创建一个MPU6050_DATA类型的指针变量ptr
    MPU6050_DATA* ptr = NULL;
    //用于保存加速度计的中间值
    float AccX,AccY,AccZ;
    
    while (1){
        //获取MPU6050的数据,返回一个结构体指针给ptr
        ptr = MPU6050_GetData();
        //OLED显示陀螺仪数据,这里没有把原始数据换算成°/s的单位了
        OLED_ShowSignedNum(1,1,ptr->GyroX,5);
        OLED_ShowSignedNum(2,1,ptr->GyroY,5);
        OLED_ShowSignedNum(3,1,ptr->GyroZ,5);
        //把加速度计的数据转换成m/s^2的单位
        AccX = (float)(ptr->AccX) * (float)((float)2 / (float)32767);
        AccY = (float)(ptr->AccY) * (float)((float)2 / (float)32767);
        AccZ = (float)(ptr->AccZ) * (float)((float)2 / (float)32767);
        //在OLED上显示数据,这里的单位是cm/s^2了
        OLED_ShowSignedNum(1,8,(int16_t)(AccX * 9.8 * 100),5);
        OLED_ShowSignedNum(2,8,(int16_t)(AccY * 9.8 * 100),5);
        OLED_ShowSignedNum(3,8,(int16_t)(AccZ * 9.8 * 100),5);
        //显示读取到的温度数据,单位℃
        OLED_ShowSignedNum(4,8,ptr->Temp,5);
    }
}

使用逻辑分析仪抓取的部分波形如图:

Alt text

试验现象如图:

Alt text

此文章是一篇学习笔记,由笔者在学习B站江协科技UP主的STM32入门教程时写下,部分资料和代码来自江协科技.感谢前辈制作这门教程,致敬!

至此,软件I2C读取MPU6050的例程结束,感谢阅读.如果帮助到了你,还请动动手指点个赞,笔者将十分感谢!

如有错误,欢迎指正! 有些地方不太明白,欢迎与我讨论.共同学习,一起进步.

QQ:1583031618

By Sightseer 2023/07/13