MPU6050是一个 6 轴运动传感器,包括 3 轴加速度传感器和 3 轴陀螺仪,具有 2 组 I2C 接口,1 组用于与 MCU 通信,另外 1 组可以外接另外一个传感器(比如磁力计),组成 9 轴传感器系统。
为了方便,这里我用的 MPU6050 模块,在优信那里购买(模块居然比单芯片便宜,就离谱)。
引脚说明
总共有 8 个引脚,各引脚说明如下:
编号 | 引脚名称 | 引脚说明 |
---|---|---|
1 | VCC | 电源输入,3.3V 或 5V |
2 | GND | 接地 |
3 | SCL | I2C 从时钟线 |
4 | SDA | I2C 从数据线 |
5 | XDA | I2C 主数据线,可外接其他传感器 |
6 | XCL | I2C 主时钟线,可外接其他传感器 |
7 | AD0 | 从机地址引脚 接地或悬空时,地址为 0x68 接 VCC 时,地址为 0x69 |
8 | INT | 中断输出 芯片使用 DMP 库时,每当其内部处理完一次数据,就产生一次中断,提醒 MCU 接收数据 |
接线说明
由于网上不少人说使用 STM32 的硬件 I2C 与 MPU6050 通信时会出问题,所以这里选择软件模拟I2C通信时序。并且读取原始数据用不到 DMP 库,因此这里只需要将 SCL 和 SDA 接入 STM32 任意引脚,VCC 接 3.3V 并接好地,其他引脚悬空即可。
软件模拟 I2C
直接用杜邦线连的 MPU6050,没有加上拉电阻,所以模拟 I2C 时不能用开漏输入模式读取到电平变化,而 I2C 总线上也只有一个 MPU6050 从设备,因此可以采用推挽输出+浮空输入的方式,必要时通过寄存器切换 I/O 方向即可。如何切换请看这里
2023-12-07更正:MPU6050 模块内部有上拉电阻,因此 GPIO 可以使用开漏输出模式。下面的代码是推挽模式的,只需要去掉 I2C 时序中读写模式切换的相关代码、把 I2C 对应 GPIO 改为初始化为开漏输出(GPIO_MODE_OUTPUT_OD)即可,其余部分不用修改。
- 如果只有一个从设备,那么推挽和开漏都可以,此时不会出现短路以及线与的需求;
- 如果有多个从设备,那么就只能用开漏输出。因为推挽模式下,有多个从设备挂载在一个总线上时,一个设备输出高电平(推挽),另一个输出低电平,此时就会发生短路,并且推挽模式无法实现线与功能
头文件定义
/**********引脚定义***********/
#define SCL_PIN GPIO_PIN_10
#define SDA_PIN GPIO_PIN_11
#define SCL_GPIO_PORT GPIOB
#define SDA_GPIO_PORT GPIOB
#define SCL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define SDA_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
/**********高低电平***********/
#define SCL_HIGH() HAL_GPIO_WritePin(SCL_GPIO_PORT,SCL_PIN,GPIO_PIN_SET)
#define SCL_LOW() HAL_GPIO_WritePin(SCL_GPIO_PORT,SCL_PIN,GPIO_PIN_RESET)
#define SDA_HIGH() HAL_GPIO_WritePin(SDA_GPIO_PORT,SDA_PIN,GPIO_PIN_SET)
#define SDA_LOW() HAL_GPIO_WritePin(SDA_GPIO_PORT,SDA_PIN,GPIO_PIN_RESET)
/**********SDA读写模式切换****/
#define SDA_IN() {SDA_GPIO_PORT->CRH &= 0xFFFF0FFF;SDA_GPIO_PORT->CRH |= 4<<12;}//配置SDA引脚为浮空输入
#define SDA_OUT() {SDA_GPIO_PORT->CRH &= 0xFFFF0FFF;SDA_GPIO_PORT->CRH |= 3<<12;}//配置SDA引脚为高速推挽输出
/**********读取SDA状态*******/
#define SDA_READ() HAL_GPIO_ReadPin(SDA_GPIO_PORT,SDA_PIN)
/**********I2C应答位*********/
#define I2C_NOACK 1
#define I2C_ACK 0
/**********I2C读写方向位*/
#define I2C_READ 1
#define I2C_WRITE 0
这里我略去了函数声明。
函数实现
I2C 初始化
void I2C_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
//初始化引脚时钟
SCL_GPIO_CLK_ENABLE();
SDA_GPIO_CLK_ENABLE();
//初始化SCL
GPIO_InitStruct.Pin = SCL_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;//推挽输出
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(SCL_GPIO_PORT,&GPIO_InitStruct);
//初始化SDA
GPIO_InitStruct.Pin = SDA_PIN;
HAL_GPIO_Init(SDA_GPIO_PORT,&GPIO_InitStruct);
SDA_HIGH();
SCL_HIGH();
}
将 SCL、SDA 对应引脚初始化为高速推挽输出。最后将两条线的电平都拉高,因为 I2C 空闲时二者皆为高电平。
原因是输出模式下,引脚电平取决于输出数据寄存器,而输出数据寄存器初始为 0,所以即使初始化为上拉输出后,引脚的电平仍然为低电平,上拉只能小幅提高输出电流能力,不会影响引脚的默认状态
起始和停止信号
void I2C_Start(void)
{
SDA_OUT();//SDA设置为推挽输出,MCU向MPU6050发送数据
SCL_HIGH();
SDA_HIGH();
delay_5us();
SDA_LOW();
delay_5us();
SCL_LOW();
delay_5us();
}
在发出起始信号之前,需要先把 I/O 方向调整为输出。
SDA 保持不少于 4.7us 的高电平后拉低,SCL 继续保持不少于 4us 的高电平后再拉低
void I2C_Stop(void)
{
SDA_OUT();
SCL_LOW();
SDA_LOW();
SCL_HIGH();
delay_5us();
SDA_HIGH();
delay_5us();
}
与起始信号类似,就不赘述了。
写应答和读应答
void I2C_WriteACK(uint8_t ack)
{
SDA_OUT();
SCL_LOW();
if(ack)//非应答
SDA_HIGH();
else//应答
SDA_LOW();
SCL_HIGH();
delay_5us();
SCL_LOW();
SDA_HIGH();//释放数据线
}
uint8_t I2C_ReadACK(void)
{
uint8_t ack;
SDA_IN();//切换为输入模式
SCL_LOW();
SCL_HIGH();
delay_5us();
if(SDA_READ())
ack = I2C_NOACK;
else
ack = I2C_ACK;
SCL_LOW();
delay_5us();
SDA_OUT();
return ack;
}
严格按照 I2C 的通信时序就行了。值得注意的是,在读取应答之前,需要先把 SDA 切换为输入模式。
读写字节
void I2C_WriteByte(uint8_t byte)
{
uint8_t i;
SDA_OUT();
for(i = 0;i < 8;i++)
{
SCL_LOW();
if(byte & 0x80)//取出最高位,1则拉高SDA
SDA_HIGH();
else
SDA_LOW();
SCL_HIGH();
byte <<= 1;
}
SCL_LOW();
SDA_HIGH();
}
uint8_t I2C_ReadByte(void)
{
uint8_t byte,i;
SDA_IN();
for(i = 0;i < 8;i++)
{
byte <<= 1;
SCL_LOW();
SCL_HIGH();
if(SDA_READ())
byte |= 0x01;
}
return byte;
}
一样的,注意 I/O 方向的切换即可。
读取 MPU6050 模块
终于到主题了 2333
头文件定义
#define MPU6050_ADDR 0x68 //mpu6050 i2c地址(AD0悬空或接地)
#define MPU6050_WHO_AM_I 0x75 //检验I2C通信是否正常的寄存器,如正常,应读取到的值为0x68
/***********配置寄存器地址**********/
#define PWR_MGMT_1 0x6B //电源管理1,典型值0x80(上电复位,关闭睡眠、循环模式,使能温度传感器,使用内部晶振)
#define GYRO_CONFIG 0x1B //陀螺仪配置寄存器(不自检,±2000/0x18)
#define ACCEL_CONFIG 0x1C //加速度配置寄存器(不自检 ±16g/0x18 ±8g/0x10 ±4/0x08 ±2g/0x00)
#define CONFIG 0x1A //配置寄存器,典型值为0x06(5Hz低通滤波)
#define SMPRT_DIV 0x19 //陀螺仪输出速率的分频器,典型值0x07(对应采样频率125Hz)
/***********加速度输出寄存器********/
#define ACCEL_XOUT_H 0x3B
#define ACCEL_XOUT_L 0x3C
#define ACCEL_YOUT_H 0x3D
#define ACCEL_YOUT_L 0x3E
#define ACCEL_ZOUT_H 0x3F
#define ACCEL_ZOUT_L 0x40
/***********温度输出寄存器**********/
#define TEMP_OUT_H 0x41
#define TEMP_OUT_L 0x42
/***********陀螺仪输出寄存器********/
#define GYRO_XOUT_H 0x43
#define GYRO_XOUT_L 0x44
#define GYRO_YOUT_H 0x45
#define GYRO_YOUT_L 0x46
#define GYRO_ZOUT_H 0x47
#define GYRO_ZOUT_L 0x48
/***********量程***************/
#define GYRO_RANGE (float)2000 //陀螺仪量程
#define ACCEL_RANGE (float)2 //加速度计量程
#define COUNT_UPPER_LIMIT (float)32767 //ADC计数上限
/*****加速度、陀螺仪数据********/
extern float ACCEL_X;
extern float ACCEL_Y;
extern float ACCEL_Z;
extern float GYRO_X;
extern float GYRO_Y;
extern float GYRO_Z;
如前面所言,MPU6050 模块在 AD0 引脚悬空或接地时,其 I2C 地址为 0x68(110 1000);接电源时,为 0x69(110 1000)。该地址会作为 I2C 起始信号之后的第一个数据帧的前 7 位传输给 MPU6050,而该数据帧的最后一位(数据帧有 8 位)表示读写方向。
剩下的寄存器地址定义和配置可以看这篇文章:关于MPU6050学习的一些总结之一MPU6050芯片手册的整理-CSDN
函数实现
初始化 MPU6050
void MPU6050_Init(void)
{
I2C_Init();
if(MPU6050_ReadReg(MPU6050_WHO_AM_I) == MPU6050_ADDR)
{
MPU6050_WriteReg(PWR_MGMT_1,0x00); //解除休眠
MPU6050_WriteReg(SMPRT_DIV,0x07); //配置陀螺仪输出速率分频器,生成采样率
MPU6050_WriteReg(CONFIG,0x06); //配置低通滤波
MPU6050_WriteReg(GYRO_CONFIG,0x18); //配置陀螺仪量程
MPU6050_WriteReg(ACCEL_CONFIG,0x00); //配置加速度计量程
}else{
printf("MPU6050 Init Failed\n");
}
}
在配置寄存器之前,需要先确定 I2C 通信正常,这就需要读取寄存器 WHO_AM_I 的值,与设备地址做对比。
读写寄存器
void MPU6050_WriteReg(uint8_t regAddr,uint8_t regData)
{
//发送起始信号
I2C_Start();
//发送从机地址,并表示写入方向
I2C_WriteByte((MPU6050_ADDR << 1)+I2C_WRITE);
if(I2C_ReadACK())
{
I2C_Stop();
return;
}
//发送寄存器地址
I2C_WriteByte(regAddr);
if(I2C_ReadACK())
{
I2C_Stop();
return;
}
//发送写入数据到寄存器
I2C_WriteByte(regData);
if(I2C_ReadACK())
{
I2C_Stop();
return;
}
}
就是标准的 I2C 写流程:起始信号-->发送地址+写方向-->发送寄存器地址-->写入数据
uint8_t MPU6050_ReadReg(uint8_t regAddr)
{
uint8_t regData;
//发送起始信号
I2C_Start();
//发送从机地址,并表示写入方向
I2C_WriteByte((MPU6050_ADDR << 1)+I2C_WRITE);
if(I2C_ReadACK())
{
I2C_Stop();
return 0;
}
//发送寄存器地址
I2C_WriteByte(regAddr);
if(I2C_ReadACK())
{
I2C_Stop();
return 0;
}
//重新发送起始信号
I2C_Start();
//发送从机地址,并表示读方向
I2C_WriteByte((MPU6050_ADDR << 1)+I2C_READ);
if(I2C_ReadACK())
{
I2C_Stop();
return 0;
}
regData = I2C_ReadByte(); //读取寄存器数据
I2C_WriteACK(1); //发送非应答
I2C_Stop(); //发送停止信号
return regData;
}
与写寄存器不同点在于,写入寄存器之后,需要重新发出起始信号并转换为读方向。
读取测量数据
int16_t MPU6050_GetData(uint8_t regAddr)
{
uint16_t data_L,data_H;
int16_t data;
data_H = MPU6050_ReadReg(regAddr);
data_L = MPU6050_ReadReg(regAddr + 1);
data = (data_H << 8) | data_L;//合成数据
return data;
}
MPU6050 的 ADC 是 16 位的,因此每个轴测量得到的数据会由两个 8 位寄存器保存,因此读取数据时需要合成数据。
MPU6050 输出的数据是补码格式,不能用 uint
类型存储,必须用 int
类型存储才可以读出符号。
数据转换
void MPU6050_Update(void)
{
/* 更新 x, y, z 轴加速度 */
ACCEL_X = (float)(MPU6050_GetData(ACCEL_XOUT_H) - ACCEL_X_Calibrate) * ACCEL_RANGE / COUNT_UPPER_LIMIT;
ACCEL_Y = (float)(MPU6050_GetData(ACCEL_YOUT_H) - ACCEL_Y_Calibrate) * ACCEL_RANGE / COUNT_UPPER_LIMIT;
ACCEL_Z = (float)(MPU6050_GetData(ACCEL_ZOUT_H) - ACCEL_Z_Calibrate) * ACCEL_RANGE / COUNT_UPPER_LIMIT;
/* 更新 x, y, z 轴角速度 */
GYRO_X = (float)(MPU6050_GetData(GYRO_XOUT_H) - GYRO_X_Calibrate) * GYRO_RANGE / COUNT_UPPER_LIMIT;
GYRO_Y = (float)(MPU6050_GetData(GYRO_YOUT_H) - GYRO_Y_Calibrate) * GYRO_RANGE / COUNT_UPPER_LIMIT;
GYRO_Z = (float)(MPU6050_GetData(GYRO_ZOUT_H) - GYRO_Z_Calibrate) * GYRO_RANGE / COUNT_UPPER_LIMIT;
}
MPU6050 直接输出的数据是 ADC 的计数值,需要经过转换才能得到加速度值/角速度值。
转换之前需要知道 ADC 计数一次等于多少对应的物理量,即用量程除以计数上限,然后再用这个值乘以计数值就能完成数据转换。
以加速度为例。这里我设置加速度计量程为±2g,而 MPU6050 的 ADC 为 16 位,计数范围为 -32768~+32767,因此转换公式如下:
$$
加速度 = ADC计数值\times 2/32767
$$
陀螺仪同理。
上电校准
void MPU6050_Calibrate(void)
{
uint8_t i;
for(i = 0;i < 30;i++)
{
ACCEL_X_Calibrate += MPU6050_GetData(ACCEL_XOUT_H);
ACCEL_Y_Calibrate += MPU6050_GetData(ACCEL_YOUT_H);
ACCEL_Z_Calibrate += MPU6050_GetData(ACCEL_ZOUT_H);
GYRO_Y_Calibrate += MPU6050_GetData(GYRO_YOUT_H);
GYRO_Z_Calibrate += MPU6050_GetData(GYRO_ZOUT_H);
GYRO_X_Calibrate += MPU6050_GetData(GYRO_XOUT_H);
}
ACCEL_X_Calibrate /= 30;
ACCEL_Y_Calibrate /= 30;
ACCEL_Z_Calibrate /= 30;
GYRO_X_Calibrate /= 30;
GYRO_Y_Calibrate /= 30;
GYRO_Z_Calibrate /= 30;
}
MPU6050 在未校准的情况下会存在一定的误差,且不能忽略,因此最简单的办法就是在上电时读取数据、取平均,之后每次读取数据时都减掉这个平均值。
- 该函数应当在上电一段时间、等待 MPU6050 读数稳定后调用一次
- 读取次数可自行修改并测试
- 读取角速度的代码的顺序不能是 XYZ,否则 X 轴测量得到的角速度值在校准之后仍然会有较大偏差,原因未知
需要指出的是,这种方法只能解决上电时的零漂(零点漂移),而零漂在工作一段时间后会发生接近线性的变化。除此之外还有温漂等会对测量结果造成误差的因素。