STM32学习笔记08-I2C通信

目录

I2C通信

硬件电路

I2C时序基本单元

I2C时序

指定地址写

当前地址读

指定地址读

MPU6050简介

MPU6050参数

硬件电路

MPU6050框图

重要寄存器简介

I2C外设简介

I2C框图

I2C基本结构

硬件I2C操作流程

主机发送

主机接收

软件/硬件波形对比

I2C读写MPU6050实例

软件I2C读写MPU6050

硬件I2C读写MPU6050 


I2C通信

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

硬件电路

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

 

如图为一主多从模型,左边CPU就是我们的单片机,作为总线的主机。主机的权力很大,包括对SCL线的完全控制,任何时候都是主机完成掌控SCL线,在空闲状态下主机可以主动发起对SDA线的控制,只有在从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机。从机的权力比较小,从机对于SCL时钟线,在任何时候都只能被动的读取,从机不允许控制SCL线,对于SDA数据线,从机不允许主动发起对SDA线的控制,只有在主机发送读取从机命令后或者从机应答时从机才能短暂地取得SDA的控制权。

I2C时序基本单元

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

I2C总线处于空闲状态时,SCL和SDA都处于高电平状态,SCL和SDA由外挂的上拉电阻拉高至高电平,当主机需要进行数据收发时,首先就要打破总线的宁静,产生一个起始条件(SCL高电平期间,SDA从高电平切换到低电平), 当从机捕获到这个SCL高电平SDA下降沿信号时就会进行自身的复位等待主机的召唤,然后在SDA下降沿之后,主机要把SCL拽下来,拉低SCL一方面是占用这个总线,另一方面也是为了方便我们这些基本单元的拼接。就是我们之后会保证除了起始条件和终止条件,每个时序单元的SCL都是以低电平开始,低电平结束。这样拼接起来SCL才能续上。

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

 

SCL处于高电平之后从机需要尽快读取SDA,一般都是在上升沿这个时刻从机就已经读取完成了,因为时钟是主机控制的从机并不知道什么时候就会产生下降沿了,从机要是磨磨唧唧的主机可不会等你。主机在放手SCL一段时间后,就可以继续拉低SCL,传输下一位了,主机也需要在SCL下降沿之后尽快把数据放在SDA上,但是主机有时钟控制主导权,所以主机并不需要那么着急,只需要在低电平任意时刻把数据放在SDA上就行了,数据放完之后主机在松手SCL,SCL高电平从机读取下一位。

流程:主机拉低SCL把数据放在SDA上,主机松开SCL从机读取SDA数据,在SCL的同步下一次进行主机发送和从机接收,循环8次就发送了8位数据也就是一个字节 。

  • 接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA,释放SDA相当于切换输入模式。或者这样理解:所有设备包括主机都始终处于输入模式,当主机需要发送的时候,就可以主动拉低SDA,而主机在被动接收时必须先释放SDA,不去动他以免影响别人发送,因为总线是线与的特征,任何一个设备拉低了总线就是低电平,如果在接收时还拽着SDA不放手,那别人无论发什么数据总线都始终是低电平

同样是低电平变换数据,高电平读取数据,图中实线部分表示主机控制的电平,虚线部分表示从机控制的电平,SCL全程由主机控制,主机在接收之前要释放SDA,交由从机控制。

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

在调用发送一个字节之后就要紧跟着调用接收应答的时序,用来判断从机有没有接收到刚才给它的数据,如果从机收到了那在应答位这里主机释放SDA的时候,从机就应该立刻把SDA拉下来,然后在SCL高电平期间主机读取应答位,如果应答位为0就说明从机确实收到了。这个场景就是:主机刚发送一个字节,然后说有没有人收到啊我现在把SDA放手了,如果有人收到你就把SDA拽下来,然后主机高电平读取数据,发现确实有人给它拽下来了那就说明有人收到了,如果主机发现松手了结果这个SDA就跟着回弹到高电平了,那就说明没有人收到,或者收到了没给回应。

同理在接收一个字节之后,我们也要给从机发送一个应答位,目的是告诉从机你是不是还要继续发,如果从机发送一个数据之后得到了主机的应答,那从机还会继续发送,如果没有得到就会认为发送了一个数据可能主机不想要了,这时从机就会释放SDA交出SDA控制权。

I2C时序

I2C一主多从模型主机可以访问总线上任何一个设备,那如何发送指令来确定要访问的是哪个设备呢,这就需要首先每个设备都确定一个唯一的设备地址相当于名字,主机在起始条件之后要先发送一个字节叫一下从机的名字,所有从机都会收到第一个字节和自己的名字进行比较,如果不一样则认为主机没有叫我,之后的时序就不管了,如果一样就说明主机在叫我,那我就响应之后主机的读写操作,在同一条I2C总线里挂载的某个设备的地址必须不一样。

从机设备地址在I2C协议标准里分为7位地址和10位地址,目前只讲7位地址模式。那在每个I2C设备出厂时厂商都会为它分配一个7位的地址,这个地址具体是什么可以在芯片手册里找到,比如MPU6050的7位地址是1101 000,AT24C02的7位地址是1010 000。一般不同型号芯片地址不同,相同型号芯片地址都是一样的,那如果相同芯片挂载在同一条总线就需要用到地址中可变部分了,一般器件地址的最后几位可以在电路中改变。比如MPU6050的最后一位就可以由板子的AD0引脚确定,这个引脚接低电平那它的地址就是1101 000,接高电平那地址就是1101 001。AT24C02地址的最后3位可以由A0、A1、A2引脚确定。

指定地址写

  • 对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data

当前地址读

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

 

有一个问题,这个0x0F是从机哪个寄存器的数据呢?我没看到在读的时序中,I2C协议的规定是主机进行寻址时,一旦读写标志位给1了,下一个字节就要立马变成读的时序,所以主机还来不及指定我想要读哪个寄存器就得开始接收了,所以这里没有指定地址这个环节,那主机并没有指定寄存器地址从机到底该发哪个寄存器的数据呢?这就需要上面提到的当前地址指针了,在从机中所有的寄存器分配到了一个线性区域中,并且会有一个单独的指针变量指示其中一个寄存器,指针上电默认一般指向0地址,并且每写入一个字节和读出一个字节后这个指针就会自动自增一次,移动到下一个位置,那么在调用当前地址读的时序时,主机没有指定要读哪个地址从机就会返回当前指针指向的寄存器的值。那假设刚刚调用了指定地址写的时序, 在0x19的位置写入0xAA,那么指针就会加1,移动到0x1A的位置,再调用这个当前地址读的时序返回的就是0x1A地址下的值,再调用一次返回的就是0x1B地址下的值。这个时序用得不多。

指定地址读

  • 对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data

如果只想读一个字节就停止,在读完一个字节后一定要给从机发个非应答,就是该主机应答的时候主机不把SDA拉低,从机读到SDA为1就代表主机没有应答,从机收到非应答之后就知道主机不想要继续读了,从机就会释放总线把SDA控制权交还给主机,如果读完主机仍然给应答从机就会认为主机还想要数据就会继续发送下一个数据,而这时主机如果想产生停止条件SDA可能就会因为被从机拽住了而不能正常弹回高电平。 

除了这三个时序还有进阶版本,就是指定地址写多个字节和指定地址读多个字节,当前地址写多个字节当前地址读多个字节。比如指定地址写多个字节:

 

MPU6050简介

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

如果芯片再集成3轴的磁场传感器测量xyz轴的磁场强度那就叫9轴姿态传感器,如果再集成一个气压传感器测量气压大小那就叫10轴姿态传感器。

MPU6050参数

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

                               1101001(AD0=1

硬件电路

XCL和XDA是主机I2C通信引脚,为了拓展芯片功能,通常外接磁力计或者气压计。 

MPU6050框图

重要寄存器简介

 

  • SMPLRT_DIV:采样频率分频器,配置采样频率的分频系数,分频越小内部AD转换就越快,数据寄存器刷新就越快。

  • CONFIG:配置寄存器,内部有两部分,外部同步设置和低通滤波配置,配置低通滤波参数越大输出数据抖动越小。0是不使用低通滤波器,陀螺仪时钟为8KHz,使用了滤波器陀螺仪时钟就是1KHz。

  • GYRO_CONFIG:陀螺仪配置寄存器,高3位是xyz轴的自测使能位,中间两位是满量程选择位,后面3位没用到。

  • ACCEL_CONFIG:加速度计配置寄存器,与上面差不多,高3位是xyz轴的自测使能位,中间两位是满量程选择位,后面3位是配置高通滤波器的是内部小功能运动检测用的对数据输出没用影响。

  • 数据寄存器,包括加速度计xyz轴、温度传感器、陀螺仪xyz轴

ACCEL_XOUT_H
ACCEL_XOUT_L 
ACCEL_YOUT_H
ACCEL_YOUT_L
ACCEL_ZOUT_H
ACCEL_ZOUT_L 


TEMP_OUT_H
TEMP_OUT_L


GYRO_XOUT_H 
GYRO_XOUT_L
GYRO_YOUT_H
GYRO_YOUT_L
GYRO_ZOUT_H
GYRO_ZOUT_L

PWR_MGMT_1:电源管理寄存器1,Bit7设备复位,这一位写1所有寄存器都恢复默认值;Bit6睡眠模式,这一位写1芯片睡眠芯片不工作进入低功耗;Bit5循环模式,这一位写1设备进入低功耗过一段时间启动一次,并且唤醒的频率由下一个寄存器的高两位决定;Bit3温度传感器失能,写1之后禁用内部温度传感器,最后三位用来选择系统时钟来源。


PWR_MGMT_2:电源管理寄存器2,Bit7和Bit6刚刚提到过决定唤醒的频率;后面6位分别控制6个轴进入待机模式,如果你只需要部分轴的数据可以选择让其他轴待机这样比较省电。


WHO_AM_I:器件ID号,中间6位固定为110100,实际这个ID号就是这个芯片的I2C地址,最高位最低位都是0,读出这个寄存器值就是0x68,加上读写位0xD0或者0xD1。

所有寄存器上电默认值0x00,除了107号寄存器(电源管理寄存器1)上电默认0x40,117号寄存器(ID号)上电默认0x68, 所以芯片上电默认就是睡眠模式,我们在操作它之前要记得解除睡眠模式,否则操作其他寄存器都是无效的。

I2C外设简介

  • STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
  • 支持多主机模型
  • 支持7/10位地址模式
  • 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
  • 支持DMA
  • 兼容SMBus协议
  • STM32F103C8T6 硬件I2C资源:I2C1I2C2

I2C框图

I2C基本结构

移位寄存器和数据寄存器配合是通信的核心部分,I2C是高位先行,所以移位寄存器向左移位,发送的时候最高位先移出去,一个SCL时钟移位一次,移位8次就能把一个字节由高位到低位依次放到SDA线上了。那在接收的时候,数据通过GPIO口从右边一次移位进来,移8次一个字节就接收完成了。

GPIO口这里使用硬件I2C时, 对应的GPIO口都要配置成复用开漏输出模式,复用就是GPIO的状态是交由片上外设控制,开漏输出这是I2C协议要求的端口配置。

SCL这里时钟控制器通过GPIO去控制时钟线。SDA的部分,输出数据通过GPIO口输出到端口,输入数据也是通过GPIO输入到移位寄存器。

硬件I2C操作流程

主机发送

 当STM32想要执行指定地址写的时候,就要按照这个主发送器传送序列图来进行,这里有7位地址的主发送和10位地址的主发送,他们的区别就是7位地址起始条件后的一个字节是寻址,10位地址起始条件后的两个字节是寻址。我们关注7位地址就行了。

I2C协议只规定起始之后必须是寻址,至于后面数据的用途并没有明确规定,这些数据可以由各个芯片厂商自己决定。比如MPU6050规定就是寻址之后数据1为指定寄存器地址,数据2为指定寄存器地址下的数据,之后的数据n就是从指定寄存器地址开始依次往后写。

我们从头看:首先初始化之后,总线默认空闲状态,STM32默认是从模式,为了产生一个起始条件,STM32需要写入控制寄存器。看一下手册寄存器描述:

这一位写1STM32就自动产生起始条件了。

之后STM32由从模式转为主模式,也就是多主机模型下STM32有数据要发就要跳出来这个意思。然后控制完硬件电路之后,我们就要检查标志位,来看看硬件有没有达到我没想要的状态,起始条件之后,会发生EV5事件,这个EV5事件可以把它当成标志位,这个手册都用EVx这个事件代替标志位,因为有的状态同时产生多个标志位所以这个EVx事件就是组合了多个标志位的大标志位,在库函数中也有对应检查EVx事件是否发生的函数 。EV5的SB位在手册中可以找到:

当我们检测起始条件已发送时,就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器DR中,写入DR之后硬件电路就会自动把这一字节转到移位寄存器里,再把这个字节发送到I2C总线上,之后硬件会自动接收应答并判断,如果没有应答硬件会置应答失败标志位,然后这个标志位可以申请中断来提醒我们,在寻址完成后会发生EV6事件,看看ADDR标志位描述:

EV6事件结束后,是EV8_1事件,这时需要我们写入数据寄存器DR进行数据发送了,一旦写入DR后,因为移位寄存器也是空,所以DR会立刻转到移位寄存器进行数据发送,这时就是EV8事件:移位寄存器非空数据寄存器空。这时就是移位寄存器正在发送数据的状态,那数据1的时序就产生了。 

EV8事件结束时,对应下面解释是写入DR将清除该事件,所以按理说这个位置应该是写入了下一个数据,也就是后面这个数据2,在这个时刻就被写入到数据寄存器里等着了。

然后接收应答位之后,数据2就转入移位寄存器进行发送,此时的状态是移位寄存器非空数据寄存器空,所以这时EV8事件就又发了。这个EV8事件结束时数据2还正在移位发送,但此时下一个数据已经写到数据寄存器等着了,所以EV8事件消失。

也就是说一旦我们检测到EV8事件就可以写入下一个数据了。最后当我们想要发送的数据写完之后,这时就没有新的数据可以写入到数据寄存器了,当移位寄存器当前的数据移位完成时,此时计数移位寄存器空数据寄存器也空的状态,这个事件就是EV8_2。

当检测到EV8_2事件就可以产生终止条件了,显然在控制寄存器里有相应位进行控制。手册里找到控制寄存器CR1中的STOP位写1就会在当前字节传输或在当前起始条件发出后产生停止条件。

到这里一个完整时序就发送完成了,简单来说就是写入控制寄存器CR或者数据寄存器DR就可以控制时序单元的发生,比如产生起始条件,发送一个字节数据,时序单元发送后检查相应的EVx事件,其实就是检查状态寄存器SR,来对待时序单元发送完成 。我们有库函数不需要实际配置寄存器。

主机接收

首先写入控制寄存器的START位产生起始条件,然后等待EV5事件, EV5事件前文提过就是代表起始条件已发送,之后是寻址接收应答,结束后产生EV6事件,EV6事件代表寻址已完成。之后数据1这块代表数据正在通过移位寄存器进行输入,EV6_1可以看到数据1其实还正在移位,还没收到呢所以这个事件就没有标志位,之后这个时序单元完成时,硬件会自动根据我们的配置把应答位发送出去,任何配置是否要给应答呢,也是看手册:

如果这一位写1在接收到一个字节后就会返回一个应答,写0就是不给应答。

之后这个时序单元结束后,就说明移位寄存器就已经成功移入一个字节的数据1了,这时移入的一个字节就整体转移到数据寄存器,同时置RXNE标志位表示数据寄存器非空,也就是收到了一个字节数据,这个状态就是EV7事件。最后我们不需要接收时需要在最后一个时序单元发生时提前把刚才说的应答位控制寄存器ACK置0,并且设置终止条件请求,这就是EV7_1事件。之后在这个时序完成后,用于设置了ACK=0,所以这里就会给出非应答,最后由于设置STOP位,所以产生终止条件,这样接收一个字节的时序就完成了。

软件/硬件波形对比

硬件I2C波形更加规整,硬件I2C每个时钟的周期占空比都非常一致,软件I2C由于操作引脚之后都加了延时有时候多有时候少,不规整。不过I2C是同步时序,这些不规整也没有影响。 

I2C读写MPU6050实例

 

软件I2C读写MPU6050

软件I2C就是用普通IO口手动翻转电平实现的协议,它并不需要STM32内部外设资源支持,所以这里的端口可以任意指定。SCL和SDA都需要上拉电阻,前文提到芯片内部自带了上拉电阻这里我们不需要接了。目前这里STM32是主机,MPU6050是从机,是一主一从模型。

整体架构如下:

在MyI2C模块写MyI2C_Init函数软件I2C初始化,我们要做两个任务,第一个任务把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);
	
	GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);//初始化为高电平,释放总线,I2C总线处于空闲状态
}

接下来完成I2C的6个基本时序单元,先定义函数对操作端口的库函数进行封装:

void MyI2C_W_SCL(uint8_t BitValue)//调用这个函数,参数给1或者0,就可以释放或拉低SCL
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue)//调用这个函数,参数给1或者0,就可以释放或拉低SDA
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);
}

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

有了这三个函数的封装,我们就实现了函数名称、端口号的替换,同时可以很方便地修改时序的延时,当我需要替换端口或者把这个程序移植到其他单片机时就只需要对前4个函数对应修改,后面的函数都调用这里封装的函数名称进行操作。

/*起始条件
*SCL高电平期间,SDA由高电平到低电平
*/
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);//释放SDA
	MyI2C_W_SCL(1);//释放SCL
	MyI2C_W_SDA(0);//拉低SDA
	MyI2C_W_SCL(0);//拉低SCL
}

/*终止条件
*SCL高电平期间,SDA由低电平到高电平
*/
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);//拉低SDA
	MyI2C_W_SCL(1);//释放SCL
	MyI2C_W_SDA(1);//释放SDA
}

/*发送一个字节
*以SCL低电平开始,SCL低电平变换数据,SCL高电平保存数据稳定,高位先行把每一位放在SDA线上
*每放完一位执行释放SCL、拉低SCL驱动时钟运转
*/
void MyI2C_SendByte(uint8_t Byte)
{
	//除了终止条件,其余都保证SCL以低电平结束
	uint8_t i;
	for(i = 0;i < 8;i++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));//取出Byte,从最高位开始(高位先行)
		MyI2C_W_SCL(1);//释放SCL,从机立刻把刚才放在SDA上的数据读走
		MyI2C_W_SCL(0);//拉低SCL,驱动时钟走一个脉冲,继续放下一个数据
	}
}

/*接收一个字节
*以SCL低电平开始,此时从机把数据放到SDA,主机释放SCL
*SCL高电平期间读取SDA,再拉低SCL,低电平期间从机把下一位数据放到SDA
*/
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t Byte = 0x00,i;
	MyI2C_W_SDA(1);//防止主机干扰从机写数据,主机先释放SDA,从机把数据放到SDA,相当于切换为输入模式
	for(i = 0;i < 8;i++)
	{
		MyI2C_W_SCL(1);//主机释放SCL,主机读取数据
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
		MyI2C_W_SCL(0);//主机拉低SCL,从机把下一位数据放在SDA上
	}
	return Byte;
}

/*发送应答
*函数进来时SCL低电平,主机把AckBit放到SDA,SCL高电平从机读取应答
*/
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);//主机把AckBit放到SDA
	MyI2C_W_SCL(1);//释放SCL,从机读取应答
	MyI2C_W_SCL(0);//拉低SCL,进入下一个时序单元
}

/*接收应答
*函数进来时SCL低电平,主机释放SDA防止干扰从机,从机把应答位放在SDA,SCL高电平主机读取应答位
*/
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);//防止主机干扰从机写数据,主机先释放SDA同时从机把应答位放在SDA
	MyI2C_W_SCL(1);//主机释放SCL,SCL高电平主机读取应答位
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);//拉低SCL,进入下一个时序单元
	return AckBit;
}

接下来我们写建立在MyI2C模块上的MPU6050模块。 先封装指定地址写和指定地址读的函数:

/*指定地址写寄存器
* 参数1 RegAddr:8位寄存器地址
* 参数2 Data:8位数据
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();//起始
	MyI2C_SendByte(MPU6050_ADDRESS);//发送一个字节,从机地址+读写位
	MyI2C_ReceiveAck();//接收应答
	MyI2C_SendByte(RegAddress);//发送下一个字节,指定寄存器地址,存在MPU6050地址指针用于指定具体读写哪个寄存器
	MyI2C_ReceiveAck();//接收应答
	MyI2C_SendByte(Data);//发送第三个字节,指定要写入指定寄存器地址下的数据
	MyI2C_ReceiveAck();//接收应答
	MyI2C_Stop();//终止
}

/*指定地址读寄存器
* 参数1 RegAddr:指定读的8位寄存器地址
*/
uint8_t MPU6050_ReadReg(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);//发送下一个字节,指定从机地址+读写位,读地址最低位为1
	MyI2C_ReceiveAck();//接收应答
	Data = MyI2C_ReceiveByte();//返回接收到的数据
	MyI2C_SendAck(1);//不继续读给非应答
	MyI2C_Stop();//终止
	
	return Data;
}

 首先在MPU6050_Init函数里调用MyI2C初始化之后,还要再写入一些寄存器对MPU6050硬件电路进行初始化配置。寄存器一般用宏定义先把寄存器地址都用一个字符串表示,要不然每次查手册比较麻烦,而且光写一个数据的地址也不容易理解。寄存器比较多所以我们新建一个单独的头文件存放,叫MPU6050_Reg:

#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

接下来就一步步配置:

第一步,配置电源管理寄存器1、2。 电源管理寄存器1的Bit7是设备复位,给0不复位,睡眠模式给0解除睡眠,循环模式给0不需要循环,无关位给0,温度传感器失能给0不失能,最后3位选择时钟给000选择内部时钟,都是它非常建议我们选择陀螺仪时钟,所以我们可以给001选择x轴陀螺仪时钟,所以这个寄存器写入的就是0x01。 电源管理寄存器2的Bit7和Bit6是循环模式唤醒频率,给00不需要,后6位每一个轴的待机位,全给0不需要待机,所以这个寄存器写入的值就是0x00

//第一步,配置电源管理寄存器1、2
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);//解除睡眠,选择陀螺仪时钟
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);//6轴均不待机

第二步,配置采样率分频。这8位决定数据输出的快慢,值越小越快,根据实际需求,我们给0x09,也就是10分频。 

//第二步,配置采样率分频
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);//十分频

第三步,配置配置寄存器。外部同步全给0不需要,数字低通滤波器也根据实际需求,我们给个110也就是最平滑的滤波。所以这个寄存器的值就是0x06。

//第三步,配置配置寄存器
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);//滤波参数最大

第四步,配置陀螺仪配置寄存器。Bit7-Bit5是自测使能,不自测都给0,Bit4和Bit3是满量程选择,根据实际需求,我们就给11选择最大量程。后面三位无关位给0。所以这个寄存器值就是0x18。

//第四步,配置陀螺仪配置寄存器
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);//陀螺仪最大量程

 第五步,配置加速度计配置寄存器。自测都给0,选择最大量程,最后高通滤波器不需要给00。这个寄存器值也是0x18。

//第五步,配置加速度计配置寄存器
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);//加速度计最大量程

配置完之后陀螺仪内部就在不断进行数据转换了,输出的数据就存在数据寄存器里,接下来我们想获取数据的话只需要再写一个获取数据寄存器值的函数即可。 

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;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//读取加速度寄存器X轴高8位
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//读取加速度寄存器X轴低8位
	*AccX = (DataH << 8) | DataL;//得到加速度计x轴的16位数据
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
	
}

完整代码:

MyI2C.c:

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

void MyI2C_W_SCL(uint8_t BitValue)//调用这个函数,参数给1或者0,就可以释放或拉低SCL
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue)//调用这个函数,参数给1或者0,就可以释放或拉低SDA
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);
}

uint8_t MyI2C_R_SDA(void)//读SDA
{
	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);
	
	GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);//初始化为高电平,释放总线,I2C总线处于空闲状态
}

/*起始条件
*SCL高电平期间,SDA由高电平到低电平
*/
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);//释放SDA
	MyI2C_W_SCL(1);//释放SCL
	MyI2C_W_SDA(0);//拉低SDA
	MyI2C_W_SCL(0);//拉低SCL
}

/*终止条件
*SCL高电平期间,SDA由低电平到高电平
*/
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);//拉低SDA
	MyI2C_W_SCL(1);//释放SCL
	MyI2C_W_SDA(1);//释放SDA
}

/*发送一个字节
*以SCL低电平开始,SCL低电平变换数据,SCL高电平保存数据稳定,高位先行把每一位放在SDA线上
*每放完一位执行释放SCL、拉低SCL驱动时钟运转
*/
void MyI2C_SendByte(uint8_t Byte)
{
	//除了终止条件,其余都保证SCL以低电平结束
	uint8_t i;
	for(i = 0;i < 8;i++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));//取出Byte,从最高位开始(高位先行)
		MyI2C_W_SCL(1);//释放SCL,从机立刻把刚才放在SDA上的数据读走
		MyI2C_W_SCL(0);//拉低SCL,驱动时钟走一个脉冲,继续放下一个数据
	}
}

/*接收一个字节
*以SCL低电平开始,此时从机把数据放到SDA,主机释放SCL
*SCL高电平期间读取SDA,再拉低SCL,低电平期间从机把下一位数据放到SDA
*/
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t Byte = 0x00,i;
	MyI2C_W_SDA(1);//防止主机干扰从机写数据,主机先释放SDA,从机把数据放到SDA,相当于切换为输入模式
	for(i = 0;i < 8;i++)
	{
		MyI2C_W_SCL(1);//主机释放SCL,主机读取数据
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
		MyI2C_W_SCL(0);//主机拉低SCL,从机把下一位数据放在SDA上
	}
	return Byte;
}

/*发送应答
*函数进来时SCL低电平,主机把AckBit放到SDA,SCL高电平从机读取应答
*/
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);//主机把AckBit放到SDA
	MyI2C_W_SCL(1);//释放SCL,从机读取应答
	MyI2C_W_SCL(0);//拉低SCL,进入下一个时序单元
}

/*接收应答
*函数进来时SCL低电平,主机释放SDA防止干扰从机,从机把应答位放在SDA,SCL高电平主机读取应答位
*/
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);//防止主机干扰从机写数据,主机先释放SDA同时从机把应答位放在SDA
	MyI2C_W_SCL(1);//主机释放SCL,SCL高电平主机读取应答位
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);//拉低SCL,进入下一个时序单元
	return AckBit;
}

MPU6050.c:

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

#define MPU6050_ADDRESS		0xD0


/*指定地址写寄存器
* 参数1 RegAddr:8位寄存器地址
* 参数2 Data:8位数据
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();//起始
	MyI2C_SendByte(MPU6050_ADDRESS);//发送一个字节,从机地址+读写位
	MyI2C_ReceiveAck();//接收应答
	MyI2C_SendByte(RegAddress);//发送下一个字节,指定寄存器地址,存在MPU6050地址指针用于指定具体读写哪个寄存器
	MyI2C_ReceiveAck();//接收应答
	MyI2C_SendByte(Data);//发送第三个字节,指定要写入指定寄存器地址下的数据
	MyI2C_ReceiveAck();//接收应答
	MyI2C_Stop();//终止
}

/*指定地址读寄存器
* 参数1 RegAddr:指定读的8位寄存器地址
*/
uint8_t MPU6050_ReadReg(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);//发送下一个字节,指定从机地址+读写位,读地址最低位为1
	MyI2C_ReceiveAck();//接收应答
	Data = MyI2C_ReceiveByte();//返回接收到的数据
	MyI2C_SendAck(1);//不继续读给非应答
	MyI2C_Stop();//终止
	
	return Data;
}

void MPU6050_Init(void)
{
	MyI2C_Init();//调用I2C底层
	
	//第一步,配置电源管理寄存器1、2
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);//解除睡眠,选择陀螺仪时钟
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);//6轴均不待机
	
	//第二步,配置采样率分频
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);//十分频
	
	//第三步,配置配置寄存器
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);//滤波参数最大
	
	//第四步,配置陀螺仪配置寄存器
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);//陀螺仪最大量程
	
	//第五步,配置加速度计配置寄存器
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);//加速度计最大量程
}

uint8_t MPU6050_GetID(void)//获取ID号
{
	return MPU6050_ReadReg(MPU6050_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;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//读取加速度寄存器X轴高8位
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//读取加速度寄存器X轴低8位
	*AccX = (DataH << 8) | DataL;//得到加速度计x轴的16位数据
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
	
}

main.c:

#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, GX, 5);
		OLED_ShowSignedNum(4, 8, GX, 5);
	}
}

硬件I2C读写MPU6050 

软件I2C两个通信引脚是可以任意更改的,但是硬件I2C引脚是不可以任意指定的,我们需要查询引脚定义表:

如果使用硬件I2C1就需要接在PB6和PB7, I2C2就需要接在PB10和PB11,SCL对应PB10,SDA对应PB11。

 ​​​

接下来看几个库函数:

void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);//生成起始条件
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);//生成终止条件
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);//配置CR1的ACK位,在收到一个字节之后是否给从机应答
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);//发送数据,把Data数据直接写入DR寄存器。
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);//读取DR的数据
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);//发送7位地址的专用函数

状态监控:

I2C_CheckEvent();//同时判断一个或多个标志位,来确定EVx这个状态是否发生
I2C_GetFlagStatus();//判断某一个标志位是否置1了

FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);//读取标志位
void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);//清除标志位
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);//读取中断标志位
void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT);//清除中断标志位

 接下来写硬件I2C代码,首先对硬件I2C初始化:

第一步开启I2C和GPIO时钟。

/* 第一步,开启I2C外设和GPIO时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

第二步,把I2C外设对应的GPIO口初始化为复用开漏模式。初始化PB10和PB11为复用开漏模式。 

/* 第二步,把I2C外设对应的GPIO口初始化为复用开漏模式 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;	//复用开漏
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);

第三步,初始化I2C2外设。

/* 第三步,使用结构体配置整个I2C */
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;//I2C模式
I2C_InitStructure.I2C_ClockSpeed = 50000;//时钟速度(0-100KHz标准,100KHz-400KHz快速),配置SCL的时钟频率
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//时钟占空比(进入快速状态才能用)
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//应答位配置,ENABLE默认给应答
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//指定STM32作为从机可以响应几位地址
I2C_InitStructure.I2C_OwnAddress1 = 0x00;//自身地址1,指定STM32自身地址,STM32作为从机时使用,这里随便给一个
I2C_Init(I2C2, &I2C_InitStructure);

 第四步,使能I2C。

/* 第四步,I2C_Cmd使能I2C */
I2C_Cmd(I2C2, ENABLE);

这四步就替代了软件I2C的MyI2C_Init()。 

之后的MPU6050寄存器配置与软件I2C相同:

//第一步,配置电源管理寄存器1、2
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);//解除睡眠,选择陀螺仪时钟
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);//6轴均不待机
//第二步,配置采样率分频
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);//十分频
//第三步,配置配置寄存器
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);//滤波参数最大
//第四步,配置陀螺仪配置寄存器
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);//陀螺仪最大量程
//第五步,配置加速度计配置寄存器
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);//加速度计最大量程

接下来写指定地址写和指定地址读:

库函数给EVx事件起了名字:

void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)//带有超时退出
{
	uint32_t Timeout;
	Timeout = 10000;
	while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)//等待EV5事件
	{
		Timeout --;
		if (Timeout == 0)
		{
			break;
		}
	}
}

/*指定地址写寄存器
* 参数1 RegAddr:8位寄存器地址
* 参数2 Data:8位数据
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	
	I2C_GenerateSTART(I2C2, ENABLE);//生成起始条件
	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);//不用等待EV8_1事件,EV8_1事件是告诉你该写入DR发送数据了,所以直接写入DR
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);//等待EV8事件
	
	I2C_SendData(I2C2, Data);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);//由于Data是最后一个字节,发送完Data就要终止了,所以等待EV8_2事件
	
	I2C_GenerateSTOP(I2C2, ENABLE);//终止条件
}

/*指定地址读寄存器
* 参数1 RegAddr:指定读的8位寄存器地址
*注意最后一个字节,在接收之前要提前设置ACK和STOP,如果不提前在数据还没收到的时候给ACK置0
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);//生成起始条件
	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事件
	
	I2C_GenerateSTART(I2C2, ENABLE);//重复起始条件
	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事件
	
	I2C_AcknowledgeConfig(I2C2, DISABLE);//提前ACK置0
	I2C_GenerateSTOP(I2C2, ENABLE);//提前设置终止条件
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);//等待EV7事件
	Data = I2C_ReceiveData(I2C2);//读取DR
	
	I2C_AcknowledgeConfig(I2C2, ENABLE);//恢复默认ACK为1,方便之后改进代码
	
	return Data;
}

完整代码:

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;
	Timeout = 10000;
	while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)//等待EV5事件
	{
		Timeout --;
		if (Timeout == 0)
		{
			break;
		}
	}
}

/*指定地址写寄存器
* 参数1 RegAddr:8位寄存器地址
* 参数2 Data:8位数据
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	
	I2C_GenerateSTART(I2C2, ENABLE);//生成起始条件
	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);//不用等待EV8_1事件,EV8_1事件是告诉你该写入DR发送数据了,所以直接写入DR
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);//等待EV8事件
	
	I2C_SendData(I2C2, Data);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);//由于Data是最后一个字节,发送完Data就要终止了,所以等待EV8_2事件
	
	I2C_GenerateSTOP(I2C2, ENABLE);//终止条件
}

/*指定地址读寄存器
* 参数1 RegAddr:指定读的8位寄存器地址
*注意最后一个字节,在接收之前要提前设置ACK和STOP,如果不提前在数据还没收到的时候给ACK置0
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);//生成起始条件
	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事件
	
	I2C_GenerateSTART(I2C2, ENABLE);//重复起始条件
	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事件
	
	I2C_AcknowledgeConfig(I2C2, DISABLE);//提前ACK置0
	I2C_GenerateSTOP(I2C2, ENABLE);//提前设置终止条件
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);//等待EV7事件
	Data = I2C_ReceiveData(I2C2);//读取DR
	
	I2C_AcknowledgeConfig(I2C2, ENABLE);//恢复默认ACK为1,方便之后改进代码
	
	return Data;
}

void MPU6050_Init(void)
{
	/* 第一步,开启I2C外设和GPIO时钟 */
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	/* 第二步,把I2C外设对应的GPIO口初始化为复用开漏模式 */
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;	//复用开漏
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	/* 第三步,使用结构体配置整个I2C */
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;//I2C模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;//时钟速度(0-100KHz标准,100KHz-400KHz快速),配置SCL的时钟频率
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//时钟占空比(进入快速状态才能用)
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//应答位配置,ENABLE默认给应答
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//指定STM32作为从机可以响应几位地址
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;//自身地址1,指定STM32自身地址,STM32作为从机时使用,这里随便给一个
	I2C_Init(I2C2, &I2C_InitStructure);
	
	/* 第四步,I2C_Cmd使能I2C */
	I2C_Cmd(I2C2, ENABLE);
	
	
	//第一步,配置电源管理寄存器1、2
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);//解除睡眠,选择陀螺仪时钟
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);//6轴均不待机
	//第二步,配置采样率分频
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);//十分频
	//第三步,配置配置寄存器
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);//滤波参数最大
	//第四步,配置陀螺仪配置寄存器
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);//陀螺仪最大量程
	//第五步,配置加速度计配置寄存器
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);//加速度计最大量程
	
}

uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_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;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
	
}

main.c:

#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, GX, 5);
		OLED_ShowSignedNum(4, 8, GX, 5);
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值