STM32入门教程(I2C通信篇)

参考教程:[10-1] I2C通信协议_哔哩哔哩_bilibili

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

2、I2C协议硬件电路:

(1)所有I2C设备的SCL连在一起,SDA连在一起

(2)设备的SCL和SDA均要配置成开漏输出模式

(3)SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。

弱上拉输入模式下,通信线被完全释放,也就是没有被任何设备控制时会处于高电平(设备要想释放通信线,不将通信线接地即可,在软件中体现为将通信线置为高电平)。

弱上拉输入模式下,只要有一个设备给通信线置为低电平,也就是接地,那么即使其它设备想置通信线为高电平也无济于事(因为所有设备的通信线都连在了一起,实现了“线与”)。

(4)左图是一个一主多从的结构,任何时候都是主机完全掌控SCL线(从机只有读取SCL线的权力),在空闲状态下主机可以主动发起对SDA的控制只有在主机给从机发送读取命令后(从机需要借助SDA向主机发送数据)或者从机应答的时候主机才会将SDA的控制权转交给从机

3、I2C时序基本单元:

(1)起始条件和终止条件:

①起始条件:SCL高电平期间,SDA从高电平切换到低电平。(左图)

②终止条件:SCL高电平期间,SDA从低电平切换到高电平。(右图)

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

(3)接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后主机释放SCL,在SCL高电平期间主机读取读取SDA线数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA,这样才能保证SDA的数据位来源于从机)。

(4)发送应答和接收应答:

①发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。(左图)

②接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答主机在接收之前,需要释放SDA,这样才能保证SDA的数据位来源于从机)。(右图)

4、I2C时序:

(1)指定地址写:对于指定的从机设备地址(Slave Address),在设备内部的指定地址(Reg Address)中写入指定数据(Data)

①首先在SCL高电平期间主机拉低SDA,这就产生了起始条件

②在起始条件后紧跟着的必须是发送一个字节的时序,字节的内容是从机地址(7位)+读写选择位(最后发送的1位,写操作为0)。

③主机发送一个字节的过程:SCL低电平期间SDA被主机写入一个数据位,接着主机释放SCL,SCL高电平期间从机读取SDA上的数据位,接着主机拉低SCL,往SDA写入下一个数据位,以此循环8次

④发送了一个字节后,主机需要接收从机的应答位(RA)首先主机释放SDA,然后置SCL为高电平,在SCL为高电平时读取SDA的数据,如果为0,代表从机应答,主机可以继续下面的数据发送操作。

主机收到从机应答后拉低SCL,从机也随之释放SDA,将SDA的控制权交回给主机,主机开始给从机发送第二个字节数据,一般第二个字节数据可以是寄存器地址或指令控制字等(第二个字节以及后面字节的用途由从机设备定义),下图所示的第二个字节数据内容为寄存器地址,代表主机要操作从机中地址为0x19的寄存器。

主机发送完第二个字节数据,需要再一次接收从机的应答位,如果从机应答,主机发送第三个字节数据,流程和前面类似

如果主机完成了所有数据的发送,这时主机会拉低SDA,接着主机释放SCL,再释放SDA,SCL处于高电平期间SDA出现上升沿,这就产生了终止条件

(2)当前地址读:对于指定的从机设备地址(Slave Address),在当前地址指针指示的地址,读取从机数据(Data)

①首先在SCL高电平期间主机拉低SDA,这就产生了起始条件

②在起始条件后紧跟着的必须是发送一个字节的时序,字节的内容是从机地址(7位)+读写选择位(最后发送的1位,读操作为1)。

③主机发送一个字节的过程:SCL低电平期间SDA被主机写入一个数据位,接着主机释放SCL,SCL高电平期间从机读取SDA上的数据位,接着主机拉低SCL,往SDA写入下一个数据位,以此循环8次

④发送了一个字节后,主机需要接收从机的应答位(RA)首先主机释放SDA,然后置SCL为高电平,在SCL为高电平时读取SDA的数据,如果为0,代表从机应答,主机可以开始下面的数据接收操作。

主机释放SDA,将SDA的控制权移交给从机,SCL低电平期间SDA被从机写入一个数据位,接着主机释放SCL,SCL高电平期间主机读取SDA上的数据位,然后主机拉低SCL,从机往SDA写入下一个数据位,以此循环8次,主机接收从机的一个字节数据

主机每接收一个字节的数据,从机就需要接收一次主机的应答位(SA),主机会拉低SDA,接着释放SCL,这时从机读取SDA上的数据位为0,代表主机应答,从机开始下一个字节数据的发送。

如果主机完成了所有数据的接收,主机会释放SDA,接着释放SCL,这时从机读取SDA上的数据位为1,代表主机非应答,从机暂停往SDA写数据接着主机拉低SDA,再释放SCL,再释放SDA,SCL处于高电平期间SDA出现上升沿,这就产生了终止条件

⑧“当前地址读”没有指定从机设备的内部地址,从机会将“当前地址指针”指向地址的字节数据发送给主机。主机对从机写操作时,比如主机指定往从机的0x19地址写数据,那么写一字节数据的操作结束后,“当前地址指针”会从0x19开始自增,指向地址0x1A,这时主机如果对从机进行读一字节数据的操作,那么从机会将0x1A地址中的值发送给主机,同时“当前地址指针”会自增为0x1B。(可见这个时序使用起来非常麻烦,在实际运用中并不会单独使用

(3)指定地址读:前两种时序的结合版,对于指定设备(Slave Address),读出指定地址(Reg Address)下数据(Data)

①首先在SCL高电平期间主机拉低SDA,这就产生了起始条件

②在起始条件后紧跟着的必须是发送一个字节的时序,字节的内容是从机地址(7位)+读写选择位指定地址读”需要“指定地址写”的时序使“当前地址指针”指向指定的从机内部地址,所以该位为0)。

③主机发送一个字节的过程:SCL低电平期间SDA被主机写入一个数据位,接着主机释放SCL,SCL高电平期间从机读取SDA上的数据位,接着主机拉低SCL,往SDA写入下一个数据位,以此循环8次

④起始条件后相当于发送了一个命令字节,接着主机需要接收从机的应答位(RA)首先主机释放SDA,然后置SCL为高电平,在SCL为高电平时读取SDA的数据,如果为0,代表从机应答,可以继续下面的数据发送操作。

主机收到从机应答后拉低SCL,从机也随之释放SDA,将SDA的控制权交回给主机,主机开始给从机发送第二个字节数据,下图所示的第二个字节数据内容为寄存器地址0x19,该段时序会将“当前地址指针”指向指定的从机内部地址0x19

主机发送完第二个字节数据,需要再一次接收从机的应答位,首先主机释放SDA,然后置SCL为高电平,在SCL为高电平时读取SDA的数据,如果为0,代表从机应答,说明从机的“当前地址指针”成功被修改,这时可以马上接上“当前地址读”时序(从起始条件开始),主机读取从机中地址为0x19的寄存器

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

(1)3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度。

(2)3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度。

6、MPU6050参数:

(1)16位ADC采集传感器的模拟信号,量化范围:-32768~32767

(2)加速度计满量程选择:±2、±4、±8、±16(g)

(3)陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)

(4)可配置的数字低通滤波器

(5)可配置的时钟源

(6)可配置的采样分频

(7)I2C从机地址:1101000(AD0=0)/1101001(AD0=1)(AD0是MPU6050的一个引脚)

7、MPU6050硬件电路:

(1)AD0引脚接低电平,那么I2C中MPU6050的地址为1101000

(2)XDA和XCL用于扩展芯片功能,它们可以外接磁力计或者气压计,MPU6050的主机接口可以直接访问这些扩展芯片,并把这些数据读取到MPU6050中,MPU6050中的DMP单元会进行数据融合和姿态解算。

(3)SDA和SCL已经接了上拉电阻,接线时直接将两个引脚接在GPIO口即可,不需要另接上拉电阻

(4)可以配置芯片内部的一些事件触发引脚INT输出中断信号。

引脚

功能

VCC、GND

电源

SCL、SDA

I2C通信引脚

XCL、XDA

主机I2C通信引脚

AD0

从机地址最低位

INT

中断信号输出

8、MPU6050框图:

(1)CLOCK是时钟系统,有输入脚和输出脚,不过MPU6050一般使用内部时钟,CLKIN接地、CLKOUT悬空即可。

(2)时钟系统下面是芯片内部的7个传感器(3个加速度计、3个陀螺仪和一个温度传感器),传感器的本质是可变电阻,通过与定值电阻分压后会输出模拟电压,模拟电压通过ADC进行转换,转换结果统一放到数据寄存器(Sensor Registers)中,用户读取数据寄存器就能得到传感器测量的结果。(每个ADC输出都对应16位的数据寄存器,不存在数据覆盖的问题)

(3)加速度计和陀螺仪都配有自测单元(Self test)用于验证芯片好坏,启动自测后芯片内部会模拟一个外力施加在传感器上,这时数据寄存器中存放模拟而得的数据,接着关闭自测,数据寄存器的值更新,两组数据相减,得到“自测响应”,芯片手册中对于自测响应给出了参考范围,如果自测响应在参考范围中,说明芯片能够正常工作。

(4)Interrupt Status Register(中断状态寄存器)控制内部事件触发引脚INT输出中断信号,FIFO(先入先出寄存器)可以对数据流进行缓存,Config Registers(配置寄存器)可以对内部的各个电路进行配置。

9、软件I2C读写MPU6050:

(1)按照下图所示接好电路,并将OLED显示屏的项目文件夹复制一份作为模板使用。

(2)在项目的Hardware组中添加MyI2C.h文件和MyI2C.c文件用于封装I2C模块的代码。

①MyI2C.h文件:

#ifndef __MyI2C_H
#define __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);

#endif

②MyI2C.c文件:

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

void MyI2C_W_SCL(uint8_t BitValue)  //更改SCL的电平
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	Delay_us(10);  //延时10us,防止时序频率超过要求
}

void MyI2C_W_SDA(uint8_t BitValue)  //更改SDA的电平/往SDA写一位数据
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);  //延时10us,防止时序频率超过要求
}

uint8_t MyI2C_R_SCL(void)  //读取SCL的电平
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10);
	Delay_us(10);  //延时10us,防止时序频率超过要求
	return BitValue;
}

uint8_t MyI2C_R_SDA(void)  //读取SDA的电平/读取SDA上的一位数据
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);  //延时10us,防止时序频率超过要求
	return BitValue;
}

void MyI2C_Init(void)
{
	//将SCL和SDA初始化为开漏输出模式(开漏是I2C协议的设计要求)
	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_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}

void MyI2C_Start(void)  //产生起始条件
{
	MyI2C_W_SDA(1);  //释放SDA
	MyI2C_W_SCL(1);  //释放SCL
	MyI2C_W_SDA(0);  //SCL处于高电平时SDA出现下降沿,起始条件完成
	MyI2C_W_SCL(0);  //为发送字节数据做准备
}

void MyI2C_Stop(void)   //产生终止条件
{
	MyI2C_W_SDA(0);  //拉低SDA
	MyI2C_W_SCL(1);  //释放SCL
	MyI2C_W_SDA(1);  //SCL处于高电平时SDA出现上升沿,起始条件完成
}

void MyI2C_SendByte(uint8_t Byte)  //主机发送一字节数据
{
	uint8_t i = 0;
	for(i = 0; i < 8; i++)
	{
		MyI2C_W_SDA(Byte & (0x80) >> i);   //主机将位数据写到SDA
		MyI2C_W_SCL(1);   //主机将位数据写到SDA上后释放SCL,从机读取SDA的位数据
		MyI2C_W_SCL(0);   //从机在SCL上升沿一刻读取位数据,紧接着主机可以拉低SCL,准备发送下一位数据
	}
}

uint8_t MyI2C_ReceiveByte(void)    //主机接收一字节数据
{
	uint8_t i = 0, Byte = 0x00;
	MyI2C_W_SDA(1);      //主机在接收数据前需要释放SDA
	for(i = 0; i < 8; i++)
	{
		MyI2C_W_SCL(1);  //主机释放SCL
		if(MyI2C_R_SDA())
		{
			Byte |= (0x80 >> i);  //主机读取SDA上的位数据,存入局部变量Byte中
		}
		MyI2C_W_SCL(0);  //主机读取位数据后拉低SCL,SCL低电平期间从机往SDA上写位数据
	}
	return Byte;
}

void MyI2C_SendAck(uint8_t AckBit)  //主机发送应答
{
	MyI2C_W_SDA(AckBit);   //主机将应答位写到SDA上
	MyI2C_W_SCL(1);   //主机释放SCL,从机读取SDA的应答信号
	MyI2C_W_SCL(0);   //紧接着主机可以拉低SCL
}

uint8_t MyI2C_ReceiveAck(void)    //主机接收应答
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);      //主机在接收应答前需要释放SDA
	MyI2C_W_SCL(1);      //主机释放SCL,从机往SDA上写应答信号
	AckBit = MyI2C_R_SDA();  //主机读取SDA上的应答信号
	MyI2C_W_SCL(0);   //主机读取应答信号后拉低SCL
	
	return AckBit;
}

(3)在项目的Hardware组中添加MPU6050_Reg.h文件用于存放MPU6050寄存器的宏定义,添加MPU6050.h文件和MPU6050.c文件用于封装MPU6050模块的代码。

①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.h文件:

#ifndef __MPU6050_H
#define __MPU6050_H

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
void MPU6050_Init(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);
uint8_t MPU6050_GetID(void);

#endif

③MPU6050.c文件:

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

#define MPU6050_ADDRESS 0xD0

/**
  * 函    数:MPU6050写寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 参    数:Data 要写入寄存器的数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();						//I2C起始
	MyI2C_SendByte(MPU6050_ADDRESS);	//发送从机地址,读写位为0,表示即将写入
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_SendByte(RegAddress);			//发送寄存器地址
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_SendByte(Data);				//发送要写入寄存器的数据
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_Stop();						//I2C终止
}

/**
  * 函    数:MPU6050读寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 返 回 值:读取寄存器的数据,范围:0x00~0xFF
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	MyI2C_Start();						//I2C起始
	MyI2C_SendByte(MPU6050_ADDRESS);	//发送从机地址,读写位为0,表示即将写入
	MyI2C_ReceiveAck();					//接收应答
	MyI2C_SendByte(RegAddress);			//发送寄存器地址
	MyI2C_ReceiveAck();					//接收应答
	
	MyI2C_Start();						//I2C重复起始
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);	//发送从机地址,读写位为1,表示即将读取
	MyI2C_ReceiveAck();					//接收应答
	Data = MyI2C_ReceiveByte();			//接收指定寄存器的数据
	MyI2C_SendAck(1);					//发送应答,给从机非应答,终止从机的数据输出
	MyI2C_Stop();						//I2C终止
	
	return Data;
}

void MPU6050_Init(void)
{
	MyI2C_Init();
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);		//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);		//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);		//采样率分频寄存器,配置采样率
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);			//配置寄存器,配置DLPF
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);	//陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);	//加速度计配置寄存器,选择满量程为±16g
}

uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);		//返回WHO_AM_I寄存器的值
}

void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint8_t DataH, DataL;								//定义数据高8位和低8位的变量
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);		//读取加速度计X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);		//读取加速度计X轴的低8位数据
	*AccX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);		//读取加速度计Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);		//读取加速度计Y轴的低8位数据
	*AccY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);		//读取加速度计Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);		//读取加速度计Z轴的低8位数据
	*AccZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);		//读取陀螺仪X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);		//读取陀螺仪X轴的低8位数据
	*GyroX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);		//读取陀螺仪Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);		//读取陀螺仪Y轴的低8位数据
	*GyroY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);		//读取陀螺仪Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);		//读取陀螺仪Z轴的低8位数据
	*GyroZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
}

(4)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,不断改变开发板的姿态,观察OLED屏显示。

#include "stm32f10x.h"                  // Device headerCmd
#include "OLED.h"
#include "MPU6050.h"

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

int main()
{
	OLED_Init();
	MPU6050_Init();
	
	OLED_ShowString(1,1,"ID:");
	uint8_t 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);
	}
}

10、I2C外设简介:

(1)STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担。

(2)支持多主机模型,支持7位/10位地址模式,支持不同的通讯速度(标准速度(高达100 kHz),快速(高达400 kHz)),支持DMA,兼容SMBus协议。

(3)STM32F103C8T6的硬件I2C资源:I2C1、I2C2。

11、I2C框图:

(1)STM32的I2C外设有两个通信引脚SDA和SCL,两个引脚都是借助GPIO口的复用模式与外界相连,STM32F103C8T6中I2C1_SCL复用在PB6上、I2C2_SDA复用在PB7上、I2C2_SCL复用在PB10上、I2C2_SDA复用在PB11上,SMBALERT引脚与SMBus有关,目前可以不予理会。

(2)需要发送数据时,把一个字节数据写到数据寄存器(DATA REGISTER),数据寄存器会将字节数据写进数据移位寄存器并自动置状态寄存器的TxE位为1,数据移位寄存器一位一位地将数据写到SDA上,在这个过程中用户可以把下一个字节数据写进数据寄存器,TxE位会自动被置为0,当数据移位寄存器完成上一个字节数据的发送后,数据寄存器就会把新写入的数据写进数据移位寄存器并自动置状态寄存器的TxE位为1,以此往复。

(3)需要接收数据时,数据移位寄存器会将SDA传来的数据一位一位读入,当读满一个字节时数据移位寄存器将字节数据一次性写入数据寄存器,同时自动置标志位RxNE为0,用户可以读取数据寄存器的内容,RxNE会自动被置为1,以此往复。

(4)在时钟控制寄存器写对应的位,时钟控制电路执行对应的功能;在控制寄存器写对应的位,控制逻辑电路执行对应的功能;读取状态寄存器,可以得知电路的工作状态。

12、主机发送和接收:

13、硬件I2C读写MPU6050:

(1)按照下图所示接好电路,并将上例的项目文件夹复制一份作为模板使用。(I2C2_SCL复用在PB10上、I2C2_SDA复用在PB11上)

(2)在stm32f10x_i2c.h文件中有I2C相关的函数。

[1]I2C_DeInit函数:恢复I2C缺省配置。

[2]I2C_Init函数:使用结构体中的参数初始化I2C。

[3]I2C_StructInit函数:给结构体中的参数赋一个初始值。

[4]I2C_Cmd函数:使能I2C。

[5]I2C_GenerateSTART函数:生成起始条件。

[6]I2C_GenerateSTOP函数:生成终止条件。

[7]I2C_AcknowledgeConfig函数:主机给从机发送应答信号。

[8]I2C_SendData函数:发送模式下,主机把Data写进数据寄存器。

[9]I2C_ReceiveData函数:接受模式下,主机读取数据寄存器接收到的Data数据。

[10]I2C_Send7bitAddress函数:主机发送7位地址和1个读写选择位。

[11]I2C_CheckEvent函数:判断事件是否发生。

(3)将MyI2C.h文件和MyI2C.c文件移出工程,修改MPU6050.c文件。

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

#define MPU6050_ADDRESS 0xD0

void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)  //等待事件发生,如果超时则跳过等待,防止程序卡死
{
	uint32_t Timeout = 10000;
	while(I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)  //等待事件发生
	{
		Timeout --;
		if(Timeout == 0)
		{
			break;   //如果事件迟迟不发生,强制结束循环
		}
	}
}

/**
  * 函    数:MPU6050写寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 参    数:Data 要写入寄存器的数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	I2C_GenerateSTART(I2C2, ENABLE);	//I2C起始
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);  //等待EV5事件发生
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //发送从机地址并说明是写从机模式
	//发送过程结束后自动处理从机的应答位,如果应答有问题,硬件会置相应标志位并产生中断
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);  //等待主机发送模式的EV6事件发生
	
	I2C_SendData(I2C2, RegAddress);     //发送寄存器地址
	//发送过程结束后自动处理从机的应答位,如果应答有问题,硬件会置相应标志位并产生中断
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);  //等待EV8事件发生

	I2C_SendData(I2C2, Data);           //发送Data数据
	//发送过程结束后自动处理从机的应答位,如果应答有问题,硬件会置相应标志位并产生中断
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);   //等待EV8_2事件发生
	
	I2C_GenerateSTOP(I2C2, ENABLE);     //I2C终止
}

/**
  * 函    数:MPU6050读寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 返 回 值:读取寄存器的数据,范围:0x00~0xFF
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);	//I2C起始
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);  //等待EV5事件发生
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //发送从机地址并说明是写从机模式
	//发送过程结束后自动处理从机的应答位,如果应答有问题,硬件会置相应标志位并产生中断
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);  //等待主机发送模式的EV6事件发生
	
	I2C_SendData(I2C2, RegAddress);     //发送寄存器地址
	//发送过程结束后自动处理从机的应答位,如果应答有问题,硬件会置相应标志位并产生中断
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);  //等待EV8_2事件发生

	I2C_GenerateSTART(I2C2, ENABLE);	//I2C起始
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);  //等待EV5事件发生
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver); //发送从机地址并说明是读从机模式
	//发送过程结束后自动处理从机的应答位,如果应答有问题,硬件会置相应标志位并产生中断
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);  //等待主机接受模式的EV6事件发生(EV6结束后数据移位寄存器开始接收数据)
	
	I2C_AcknowledgeConfig(I2C2,DISABLE);
	//只传输1个字节,那么在接收数据完成前就要清除响应位,这样数据寄存器接收所有数据后,主机就能马上发送非应答
	I2C_GenerateSTOP(I2C2, ENABLE);
	//在接收数据前就要申请I2C终止,主机将非应答位发送后就会马上产生停止条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);  //等待EV7事件发生(数据寄存器接收到1字节数据)
	Data = I2C_ReceiveData(I2C2);  //读取数据寄存器中的内容
	
	I2C_AcknowledgeConfig(I2C2,ENABLE);   //置主机发送应答位为1,方便下次主机发送应答

	return Data;
}

void MPU6050_Init(void)
{
	//使能I2C和GPIO的时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	//配置PB10和PB11为复用开漏输出(开漏是I2C协议的设计要求)
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	//配置I2C2
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;	//I2C模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;   //通讯速度为50kHz
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //配置为默认接收一个数据位后发送应答
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;  //7位地址(STM32作为从机时相关的参数)
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;   //STM32作为从机时的自身地址
	I2C_Init(I2C2, &I2C_InitStructure);
	
	//开启I2C2
	I2C_Cmd(I2C2, ENABLE);
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);		//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);		//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);		//采样率分频寄存器,配置采样率
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);			//配置寄存器,配置DLPF
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);	//陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);	//加速度计配置寄存器,选择满量程为±16g
}

uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);		//返回WHO_AM_I寄存器的值
}

void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint8_t DataH, DataL;								//定义数据高8位和低8位的变量
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);		//读取加速度计X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);		//读取加速度计X轴的低8位数据
	*AccX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);		//读取加速度计Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);		//读取加速度计Y轴的低8位数据
	*AccY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);		//读取加速度计Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);		//读取加速度计Z轴的低8位数据
	*AccZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);		//读取陀螺仪X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);		//读取陀螺仪X轴的低8位数据
	*GyroX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);		//读取陀螺仪Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);		//读取陀螺仪Y轴的低8位数据
	*GyroY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);		//读取陀螺仪Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);		//读取陀螺仪Z轴的低8位数据
	*GyroZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
}

(4)无需改动main.c文件,直接编译,将程序下载到开发板中,不断改变开发板的姿态,观察OLED屏显示。

猜你喜欢

转载自blog.csdn.net/Zevalin/article/details/134764916