STM32软件I2C驱动MPU6050

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYCIMOYz' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片