STM32软件I2C驱动MPU6050
STM32F103C8T6基于Keil MDK标准库
硬件接线
这里没有什么复杂的地方,采用MPU6050的现成模块.模块的SCL接B10,SDA接B11,这里连接了一个OLED显示屏,用于显示获取到的数据.
注意:这里使用的模块自带上拉电阻
软件实现
首先在工程目录里创建:
“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的六块拼图:起始,终止,接收一个字节,发送一个字节,接收应答,发送应答
起始和终止信号
注意: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);}
发送一个字节和接收一个字节
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;}
发送应答和接收应答
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)
这是芯片的读寄存器函数
- 我们要通过这个函数对芯片的寄存器进行读出,具体为什么要这样做,可以参考芯片的读写时序图
- 上图部分解释:
- Master/Slave 主机/从机
- S开始 ,AD+W地址+写 ,ACK应答
- 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;}
这里是芯片的写寄存器函数
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);
我们配置为0b0000 0001,即设备不复位,不睡眠,关闭循环模式,温度传感器使能,时钟选择为内部X轴陀螺仪晶振.
- 以下为CLKSEL位对应的时钟源选择:
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);
这里配置为0b0000 0000,唤醒频率我们不配置(默认1.25Hz),6轴的传感器也不待机.
- 以下为LP_WAKE_CTRL位对应的唤醒频率:
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
采样率分频配置为0x09,低通滤波配置为最大,此时陀螺仪输出速率为1kHz(下文会提及),根据手册公式可得采样率100Hz.
- 手册给出的采样率计算公式
MPU6050_WriteReg(MPU6050_CONFIG,0x06);
此处配置为低通滤波最大(0b0000 0110),对应输出速率为5Hz
- 下图为低通滤波器配置表,可见当没有启用DLPF时,陀螺仪输出频率为8kHz,启用DLPF时,输出频率为1kHz.
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x00); MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x00);
- 陀螺仪量程配置位对应的量程
- 加速度计量程配置为对应的量程
我们这里没有配置自检和加速度计高通滤波,我们配置了陀螺仪的量程为最低:±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); }}
使用逻辑分析仪抓取的部分波形如图:
试验现象如图:
此文章是一篇学习笔记,由笔者在学习B站江协科技UP主的STM32入门教程时写下,部分资料和代码来自江协科技.感谢前辈制作这门教程,致敬!
至此,软件I2C读取MPU6050的例程结束,感谢阅读.如果帮助到了你,还请动动手指点个赞,笔者将十分感谢!
如有错误,欢迎指正! 有些地方不太明白,欢迎与我讨论.共同学习,一起进步.
QQ:1583031618
By Sightseer 2023/07/13