STM32软件I2C驱动MPU6050
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