I2C通信协议

简介

I2C Inter IC Bus )是由 Philips 公司开发的一种通用数据总线
两根通信线: SCL Serial Clock )、 SDA Serial Data
同步,半双工
带数据应答
支持总线挂载多设备(一主多从、多主多从)

 

硬件电路 

所有 I2C 设备的 SCL 连在一起, SDA 连在一起
设备的 SCL SDA 均要配置成开漏输出模式
SCL SDA 各添加一个上拉电阻,阻值一般为 4.7KΩ 左右

 

I2C时序基本单元

起始条件: SCL 高电平期间, SDA 从高电平切换到低电平
终止条件: SCL 高电平期间, SDA 从低电平切换到高电平
起始和终止都是主机产生的,从机不允许产生起始和中止,所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来去碰主线

 

        •发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节

         接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)(虚线是从机控制)

发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA

I2C从机地址 

 一主多从模型下,如何发出指令来确定要访问的是哪一个设备呢?这就需要首先把每一个设备都确定一个唯一的设备地址,从机设备地址就相当于每个设备的名字,主机在起始条件之后,要先发送一个字节选择从机名字,所以从机都会收到这个字节(名字),然后和自己的名字做比较,如果不一样,则该设备将不会再接收如何数据,如果一样,就说明主机选择了该设备,则响应接下来主机的读写操作。(这就要求在同一条I2C总线中挂载的每个设备的地址都必须得不一样)

从机设备地址在I2C协议标准中分为7位地址和10位地址,7位地址最常用(可在相关芯片手册中找到),如果有相同的芯片挂载在同一条总线中该如何解决呢?一般器件地址的最后几位都是可以在电路中改变的。一般高位都是由厂商确定的,低位可以依靠接入的引脚进行灵活切换。(比如MPU6050的地址的最后一位就可以由其引脚AD0确定,接低电平,地址为1101 000,接高电平,地址为1101 001)

I2C时序

指定地址写
对于指定设备( Slave Address ),在指定地址(寄存器地址)( Reg Address )下,写入指定数据( Data

当前地址读
对于指定设备( Slave Address ),在当前地址指针指示的地址下,读取从机数据( Data
因为主机没有指定地址,从机就会返回当前指针指向的寄存器的地址(假如我们之前指定地址0x19写了数据,然后我们再调用当前地址读就会读0x19这个地址的数据,然后下一次读的就是地址+1的数据了)

指定地址读
对于指定设备( Slave Address ),在指定地址( Reg Address )下,读取从机数据( Data
因为不可以指定地址读,所以我们是先指定地址写,选择完地址之后还没开始写就开始当前地址读,接着之前的地址就可以完成指定地址读的操作了

 MPU6050

简介

MPU6050 是一个 6 轴姿态传感器,可以测量芯片自身 X Y Z 轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
3 轴加速度计( Accelerometer ):测量 X Y Z 轴的加速度
3 轴陀螺仪传感器( Gyroscope ):测量 X Y Z 轴的角速度

 

参数 

16 ADC 采集传感器的模拟信号,量化范围: -32768~32767
加速度计满量程选择: ±2 ±4 ±8 ±16 g
陀螺仪满量程选择: ±250 ±500 ±1000 ±2000 °/sec
可配置的数字低通滤波器
可配置的时钟源
可配置的采样分频
I2C 从机地址: 1101000 AD0=0 )(0x68)

                                    1101001(AD0=1)(0x69)

我们在I2C时序中输入I2C从机的地址时,输入的第一个字节前七位位从机地址,最后一位是决定读或者写。当我们输入时就需要将我们的从机的地址左移一位,然后或上读1写0.

当然我们可以直接把左移一位的地址看做I2C从机地址,把读写位也融入I2C从机地址中,即1101 0000(AD0=0)(0xD0)和1101 0010(AD0=1)(0xD2) ,然后再按照读1写0或上对应数字。

硬件电路

SDA和SCL是I2C通信脚,可以看到已经挂载了两个上拉电阻,可以不用外接电阻再接到GPIO口上了。                                                                        XCL、XDA:用于拓展芯片功能,MPU6050是6轴姿态传感器,融合出来的姿态角是有缺陷的,可以外接磁力计和气压计,拓展为十轴姿态传感器,MPU6050的主机接口就可以直接访问这些拓展芯片的数据,把这些拓展芯片的数据读取到MPU6050中,通过DMP单元进行数据融合和姿态解算,融合出更准确的姿态角。

INT:可以配置芯片内部的一些事件来触发中断引脚的输出,例如数据准备好了、I2C主机错误等,芯片中还内置了一些实用的小功能,比如自由落体检测、运动检测、零运动检测等这些信号都可以触发INT引脚产生电平跳变。

框图

XYZ....:都是各个方向的有关传感器

Temp Sensor:温度传感器;

各个传感器传出来的数据都会由芯片自动进行AD数模转换发送到数据寄存器中,且不会相互覆盖。

自测系统:我们先使能自测系统,得到传感器的数据,再失能自测系统,得到另一组传感器的数据,两组数据之差在芯片数据手册中有标明一个正常的范围,如果超了这个范围,则代表传感器失灵。 

Interrupt Status Register:中断状态寄存器,控制内部哪些事件到中断引脚的输出;

FIFO:先入先出寄存器,可以对数据流进行缓存

Config Register:配置寄存器,可以对内部的各个电路进行配置

Sensor Register:传感器数据寄存器

数组运动处理器(DMP):芯片内部自带的姿态解算的硬件算法,配合官方的DMP库,可以进行姿态解算

寄存器

采样分频寄存器:配置采样分频率的分频系数,简单来说,分频越小,内部的AD转换就越快,数据寄存器刷新越快

 配置寄存器

分为两部分:外部同步设置和低通滤波器配置

外部我们不用,先不看

低通滤波器配置——让输出数据更加平滑,配置滤波器参数越大,输出数据抖动越小

 

陀螺仪配置寄存器

 高三位:XYZ轴的自测使能位,中间两位:满量程选择位

自测响应的计算公式

 

自测响应的范围

 满量程选择:量程越大,范围越广,量程越小,分辨率越高

 加速度计配置寄存器

 高三位:XYZ轴的自测使能位,中间两位:满量程选择位,第三位:配置高通滤波器

 加速度计的数据寄存器

得到的数据时16位的有符号数,以二进制补码的方式存储

其他数据寄存器也是相同操作

电源管理寄存器1

第一位:设备复位,写1,所有寄存器恢复默认值;

2:睡眠模式,写1芯片睡眠,不工作,进入低功耗;

3:循环模式,设备进入低功耗,过一段时间启动一次

4:温度传感器失能,写1,禁用内部的温度传感器

最后三位:选择系统的时钟来源(建议选择陀螺仪的晶振,比较准确)

电源管理寄存器2:

前两位:控制电源管理寄存器1中的循环模式的频率;

 后面6位:可以分别控制6个轴进入待机模式

ID号:只读(即这个芯片的I2C地址)

除了ID号和电源管理寄存器1,其他寄存器默认值为0x00

软件实现I2C通信 

 首先建立I2C通信层的.c和.h模块,在通信层里写好I2C底层的GPIO初始化和6个时序基本单元(起始终止,发送一个字节,接收一个字节,发送应答和接收应答)。

                                                                        |

再建立MPU6050的.c和.h模块,再这一层基于I2C通信的模块来实现指定地址读、指定地址写,再实现寄存器对芯片进行配置,读寄存器得到传感器数据。

                                                                        |

最后再在主函数中调用MPU6050模块,初始化,拿到数据,显示数据。

因为是用软件实现I2C,所以我们可以不用库中的I2C外设的函数,而是自己通过配置GPIO口的函数来实现就行了。

软件I2C初始化

1、把SCL和SDA都初始化为开漏输出模式,把SCL和SDA置高电平

void MyI2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	//开漏输出模式下也可以输入
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	//再把SCL和SDA置高电平,释放总线,此时I2C总线处于空闲状态
	GPIO_WriteBit(GPIOA, GPIO_Pin_10 | GPIO_Pin_11, Bit_SET);

}

2、时序基本单元的配置

配置起始条件等时序基本单元时需要多次反转SCL和SDA的电平,如果我们仅仅使用Reset和Set来反转电平,并不是不可以,只是这样会导致代码意义不明确,而且在修改时需要更改太多,况且也不好移植到其他开发板上应用,所以我们就需要对反转电平的操作进行简化,目标是使其易更改,易移植且意义明确。

我们就可以使用宏定义来完成

想法1是把SET和RESET语句宏定义(有参宏)(OLED模块中有示例)

#define MyI2C_W_SCL(x)    GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
//BitAction是枚举类型

但是这种方法移植到其他单片机时,不好修改,而且如果把这种代码移植到一个主频很高的单片机中,需要对软件时序进行延时操作时,不方便进一步修改

所以不如直接用函数封装起来

/*用于翻转电平的函数*/
//释放或拉低SCL
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	//以防单片机主频太快
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);
}

//读取SDA的值
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

这样我们在移植代码时就可以只修改这部分和初始化部分的代码了 

a、起始条件

起始条件:SCL高电平期间,SDA从高电平切换到低电平

//起始条件:SCL高电平期间,SDA从高电平切换到低电平
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}
b、终止条件

终止条件:SCL高电平期间,SDA从低电平切换到高电平

//终止条件:SCL高电平期间,SDA从低电平切换到高电平
void MyI2C_Stop(void)
{
	//因为在终止之前SDA可能是高电平,则无法实现从低切换到高
	//所以需要创造能从低到高的条件,就必须先拉低SDA
	MyI2C_W_SDA(0);
	
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}
c、发送一个字节

除了终止条件,我们都会保证SCL以低电平结束,方便各个单元的拼接

//发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
	//下面函数的参数非0即1
	//通过与上一个值把每一位数字都取出来
	MyI2C_W_SDA(Byte & 0x80);
	
	//高电平读取SCL的数据
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
	
	MyI2C_W_SDA(Byte & 0x40);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
	
	MyI2C_W_SDA(Byte & 0x20);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
	
	MyI2C_W_SDA(Byte & 0x10);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
	
	MyI2C_W_SDA(Byte & 0x08);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
	
	MyI2C_W_SDA(Byte & 0x04);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
	
	MyI2C_W_SDA(Byte & 0x02);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
	
	MyI2C_W_SDA(Byte & 0x01);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
}

 用for循环简化

//发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));
	    MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}
d、接收一个字节

接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)(虚线是从机控制)

在接收字节前,主机必须放手SDA(SDA置高电平),交由从机控制SDA,这样就可以实现在SCL高电平期间读取从机SDA上的电平而实现读取从机的数据。要读取从机的数据,主机就不可以“乱动”SDA,如果这期间主机SDA变化,那么就是起始位和终止位的条件了。

//接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	//主机先放手SDA
	MyI2C_W_SDA(1);
	for (i = 0; i < 8; i ++)
	{
		//在SCL为1时接收数据(从高位开始)
		MyI2C_W_SCL(1);
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
		MyI2C_W_SCL(0);
	}
	return Byte;
}
e、发送应答和接收应答

其实就是发送一个字节和接收一个字节的简化版,只接收一位数据版

//发送应答
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
}

//接收应答
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	return AckBit;
}
总体 

这样就把六个时序基本单元配置好了

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

/*用于翻转电平的函数*/
//释放或拉低SCL
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	//以防单片机主频太快
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);
}

//读取SDA的值
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

void MyI2C_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	//开漏输出模式下也可以输入
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	//再把SCL和SDA置高电平,释放总线,此时I2C总线处于空闲状态
	GPIO_WriteBit(GPIOA, GPIO_Pin_10 | GPIO_Pin_11, Bit_SET);

}	

//时序基本单元
//起始条件:SCL高电平期间,SDA从高电平切换到低电平
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}

//终止条件:SCL高电平期间,SDA从低电平切换到高电平
void MyI2C_Stop(void)
{
	//因为在终止之前SDA可能是高电平,则无法实现从低切换到高
	//所以需要创造能从低到高的条件,就必须先拉低SDA
	MyI2C_W_SDA(0);
	
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

//发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));
	    MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
//	//下面函数的参数非0即1
//	//通过与上一个值把每一位数字都取出来
//	MyI2C_W_SDA(Byte & 0x80);
//	//高电平读取SCL的数据
//	MyI2C_W_SCL(1);
//	MyI2C_W_SCL(0);
}

//接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	//主机先放手SDA
	//I2C的引脚都是开漏输出+弱上拉的配置
	//主机输出1,并不是强制SDA为高电平,而是释放SDA
	MyI2C_W_SDA(1);
	for (i = 0; i < 8; i ++)
	{
		//在SCL为1时接收数据(从高位开始)
		MyI2C_W_SCL(1);
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
		MyI2C_W_SCL(0);
	}
	return Byte;
}

//发送应答
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
}

//接收应答
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	return AckBit;
}
验证

目前还未写好MPU的代码,可以先验证起始条件、终止条件、发送一个字节和接收应答

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

int main(void)
{
	OLED_Init();
	
	MyI2C_Init();
	
	MyI2C_Start();
	MyI2C_SendByte(0xD0); //1101 000 0
	uint8_t Ack = MyI2C_ReceiveAck();
	MyI2C_Stop();
	
	OLED_ShowNum(1, 1, Ack, 3);
	while(1)
	{
		
	}
}

如果OLED上显示0,则表示接收到了数据,我们再修改发送的值为其他,如果OLED上显示1,则代表没接收到数据,也就代表代码编写正确。

我们还可以通过连接MPU的AD0引脚到高电平修改从机的地址来验证代码,此时输入的字节必须为0xD2(1101 010 0)

接下来编写MPU6050模块

即读写MPU6050的寄存器

#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"

#define MPU6050_ADDRESS		0xD0
//指定地址写
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	//可以验证是否收到数据,具体怎么处理就不加上了
	MyI2C_ReceiveAck();
	//发送寄存器的地址
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	//接收一个字节
	MyI2C_SendByte(Data);
	MyI2C_ReceiveAck();
	MyI2C_Stop();
}

//指定地址读
uint8_t MPU6050_ReadtheReg(uint8_t RegAddress)
{
	uint8_t Data;
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
	MyI2C_ReceiveAck();
	Data = MyI2C_ReceiveByte();
	//发送主机应答,已经接收到数据了
	MyI2C_SendAck(1);
	MyI2C_Stop();
	
	return Data;
}

void MPU6050_Init(void)
{
	MyI2C_Init();
}

然后再在主函数中验证

先读MPU的ID号

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


int main(void)
{
	OLED_Init();
	
	MPU6050_Init();
	
	uint8_t Data = MPU6050_ReadtheReg(0x75);
	OLED_ShowHexNum(1, 1, Data, 2);
	while(1)
	{
		
	}
}

结果是OLED上显示68,即ID号为0x68,则代表读寄存器的代码编写正确

那我们再验证一下写寄存器的代码

首先需要关闭芯片的睡眠模式,下图Bit7即为我们需要更改的寄存器

先关闭睡眠模式,然后再在地址为0x19的寄存器上写入0x66 

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


int main(void)
{
	OLED_Init();
	
	MPU6050_Init();
	
	MPU6050_WriteReg(0x6B, 0x00);
	
	MPU6050_WriteReg(0x19, 0x66);
	
	uint8_t Data = MPU6050_ReadtheReg(0x19);
	OLED_ShowHexNum(1, 1, Data, 2);
	while(1)
	{
		
	}
}

 结果是OLED上显示为66,即0x66,代表没问题

我们这里的验证思路是把MPU6050当做一个存储器来验证的,MPU6050里的每个寄存器都对应着芯片的一种状态,寄存器和外设的硬件电路是可以进行互动的,意思是我们可以通过配置寄存器来和MPU6050的硬件电路进行互动。

课后任务:实现发送一个数组到多个地址,读取多个地址的数据 

配置寄存器控制电路

我们初始化MPU6050芯片还需要做到配置其电源管理寄存器、采样分频寄存器等等,所以我们可以在Init函数中调用写入寄存器函数来配置这些寄存器

为了方便移植,我们可以用宏定义来定义各个寄存器,因为寄存器数量多,我们决定使用头文件模块化这部分宏定义

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

然后配置各个寄存器

void MPU6050_Init(void)
{
	MyI2C_Init();
	//电源管理寄存器1
	//设备复位 	睡眠模式	    循环模式	    无关位	温度传感器失能	选择时钟(后三位)
	//0(不复位) 0(解除睡眠)	0(不需要)	0		0(不失能)		000(内部时钟)(或者001-陀螺仪时钟)
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
	
	//电源管理寄存器2
	//循环模式唤醒频率(不需要-00)
	//后6位每个轴的待机位-(000000-不待机)
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
	
	//采样率分频(值越小,数据输出越快)(采样分频为10)
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
	
	//配置寄存器
	//外部同步-000000 数字低通滤波-110
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
	
	//陀螺仪配置寄存器
	//自测使能-000	满量程选择-11	无关位-000
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
	
	//加速度计配置寄存器
	//自测使能-000	满量程选择-11	高通滤波器-000
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}

 读取特定寄存器的值

根据任务需求,我们这个函数需要返回6个数据,但是c语言函数不可以同时返回这么多值,所以我们就会用到以下方法

1、定义全局变量,调用函数时修改这些变量的值——不推荐在大项目中使用

2、使用指针

3、使用结构体,将这些数据打包起来

这里我们使用指针来完成

//读取寄存器
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint16_t DataH, DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}

//读取寄存器
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint16_t DataH, DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}

uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadtheReg(MPU6050_WHO_AM_I);
}

这样是连续调用多次读取指定寄存器的函数,再把两个8位的数据拼接成16位的数据的方法

但其实有更方便的方法,就是使用课后作业中实现读取连续多个字节的代码,从一个基地址开始,连续读取一片的寄存器——因为这些寄存器的地址是连续在一起的,这样在时序上,读取效率就会大大提升。

在主函数中调用

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

uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;

int main(void)
{
	OLED_Init();
	
	MPU6050_Init();
	OLED_ShowString(1, 1, "ID:");
	ID = MPU6050_GetID();
	OLED_ShowHexNum(1, 4, ID, 2);
	while(1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
		OLED_ShowSignedNum(2, 1, AX, 5);
		OLED_ShowSignedNum(3, 1, AY, 5);
		OLED_ShowSignedNum(4, 1, AZ, 5);
		OLED_ShowSignedNum(2, 8, GX, 5);
		OLED_ShowSignedNum(3, 8, GY, 5);
		OLED_ShowSignedNum(4, 8, GZ, 5);
	}
}

求出各项具体值的方法:显示值/32768=x/满量程,x即为所求

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值