STM32学习:深入理解I2C通信协议

目录

前言

一、I2C通信协议

1.简介

2.硬件电路设计

3.I2C时序(软件)

二、MPU6050姿态传感器

1.MPU6050简介

 2.MPU6050参数与电路

三、软件I2C读取MPU6050

总结


提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

前言

声明:学习笔记来自江科大自化协B站STM32教程,仅供学习交流!!

I2C通信学习这里分两大块,第一,学习协议规则,用软件模拟的形式实现协议;第二,学习STM32的I2C外设,然后用硬件实现协议。因为I2C为同步协议,软件模拟协议非常方便,目前也存在很多这样的代码!软件与硬件各有优劣势。

本次的程序:软件I2C读写MPU6050、硬件I2C读写MPU6050。51教程里有关于AT24C02存储模块的I2C可参考对比。


提示:以下是本篇文章正文内容,下面案例可供参考

一、I2C通信协议

1.简介

I2C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线 ,有两根通信线:SCL(Serial Clock)、SDA(Serial Data)。常见使用I2C的例子,MPU6050姿态传感器、OLED、AT24C02存储器模块、DS3231实时时钟模块:

同步,半双工 。异步通信(如串口)如果发送方发送一半突然进中断停止了,接收方是不知道的,这是不允许的,异步对硬件外设USART电路依赖性很强,虽然也可软件实现,但因为对时间要求严格所以一般不用软件。同步协议由于存在时钟线SCL,对时间的要求就不高,发送方可随时停止去处理其他事情(因为暂停传输的同时,时钟线也暂停了)。半双工也可大大节约硬件资源。

带数据应答 支持总线挂载多设备(一主多从、多主多从)。一主多从,单片机作为主机主导I2C总线的运行,挂载在I2C总线的所有外设模块都是从机,从机只有被主机点名后才可控制I2C总线。多主多从,就是具有多个主机,总线上任何一个模块都可以主动跳出来成为主机,但是同一时间只能有一个主机,这时就相当于发生了总线冲突,I2C协议会进行仲裁,仲裁胜利的一方取得控制权,协议比较复杂可自行了解。

2.硬件电路设计

设计要点:

①所有I2C设备的SCL连在一起,SDA连在一起。

②设备的SCL和SDA均要配置成开漏输出模式。

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

 主从机权力:

图上的CPU就代表了单片机,作为总线的主机:①完全控制时钟线SCL。②空闲状态下,可主动发起对SDA的控制,只有在从机发送数据和从机应答时才会转交SDA的控制权给从机。

4个被控IC从机:①对SCL时钟线在任何时候只能被动读取。②不允许主动发起对SDA的控制,只有在主机发送读取从机的命令后或从机应答时才可短暂获得控制权。

电源短路问题:

主机和从机的SDA会在输入和输出之间反复切换,这个要协调好时机,为了避免没协调好导致电源短路的问题:I2C设计禁止所有设备输出强上拉的高电平,所以采用外置弱上拉电阻加开漏输出的电路结构。

右图SCLK和DATA就分别是主/从机SCL引脚和SDA引脚的内部电路:引脚的信号进来通过一个数据缓冲器或者施密特触发器(三角形)进行输入,因为输入对电路没影响,所以任何设备在任何时刻都可以输入。但是输出就采用开漏输出的配置,xxxxN1 OUT输出低电平,开关管导通,引脚直接接地,是强下拉;输出高电平,开关管断开,引脚什么都没接,处于浮空状态,这样所有的设备只能输出低电平而不能输出高电平。为了避免高电平造成的引脚浮空,这时就需要在外面(左图)SCL和SDA各外置一个上拉电阻,通过一个电阻拉到高电平属于弱上拉。好处:①杜绝电源短路现象,保证安全;②避免了引脚模式的频繁切换。

3.I2C时序(软件)

I2C时序基本单元:

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

在I2C总线处于空闲状态时,SCL和DA都处于高电平状态。然后再SDA下降后,主机要再把SCL”拽下来“,一方面是占用这个总线,另一方面也是为了方便时序基本单元的拼接,即之后会保证除了起始和终止条件,每个时序单元的SCL都是以低电平开始,低电平结束,这样各个单元拼接起来SCL才可连续

终止条件:SCL高电平期间,SDA从低电平切换到高电平。起始和终止条件类似串口数据帧里面的起始位和终止位。起始和终止都是有主机产生,从机不可产生,所以空闲状态下从机必不可碰总线。(当然多主机模式可能可以)

 ③主机发送一个字节:起始之后第一个字节必须是由主机发送的,SCL低电平期间,主机将数据位依次放到SDA线上(高位先行,串口是低位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。

SCL处于高电平之后,从机需要尽快地读取SDA,一般是再SCL上升沿时刻从机就已完成读取,因为时钟线是主机控制的,从机并不知道什么时候就会出现下降沿了。主机也需要在SCL下降沿之后尽快把数据放在SDA上,但是它不用那么着急。

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

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

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

 完整时序(完整数据帧)

I2C的完整时序,主要有指定地址写当前地址读指定地址读3种。每个从机设备地址都不同,在I2C协议标准里分为7位地址和10位地址,目前只讲7位地址的,它简单且应用广,这个地址由芯片厂商规定可在手册查找。一般相同型号芯片的地址一样,当挂载在同一总线时,地址低位可通过特定引脚切换。

指定地址写:主机对于指定设备(从机地址Slave Address),在指定地址(内部寄存器地址Reg Address)下,写入指定数据(Data)。

下图Slave Address前7位是选定的从机地址,第8位为0表示写,1表示读,称为读写标志位。

如果就写一个字节,那最后就P停止;如果想写多个字节,就可把Send Byte :0xAA(Data)、RA这部分多重复几次!

 ②当前地址读:对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)。不常用。

在I2C协议规定中,主机进行寻址时一旦读写标志位给”1“,下一个字节就必须立马进入读的时序,主机来不及指示寄存器地址,没有指定寄存器地址那么读取哪个单元数据呢?这就需要当前地址指针,在从机中所有寄存器被分配到了一个线性区域,且有一个单独的指针变量指示着其中一个寄存器,这个指针上电默认一般指向0地址,并且每写入一个字节和读出一个字节后,此指针就会自动自增一次移动到下一个位置。比如假设开始调用了上面的指定地址写的时序,在0x19的位置写入了0xAA,那么指针就会+1移动到0x1A的位置,再调用这个当前地址读的时序,返回的就是0x1A地址下的值,如果再调用一次返回的就是0x1B地址下的值,以此类推。

指定地址读:对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)。RA为Read Ack(发送字节后出现)、SA为Send Ack(读到字节后出现),”0“为应答、”1“为非应答。

这个时序为什么可以指定读的寄存器地址呢?把指定地址写的时序前面指定地址的部分,加到当前地址读的前面,就得到了指定地址读的时序,也叫做复合格式。前面写入的寄存器地址会存在地址指针里面,不会随着时序的变化而消失,应答位RA后面再起始,再发送一个字节(设备还是刚才那个但读写标志位为1),后续开始读字节。

 如果就读一个字节,最后就写SA=”1“(非应答)和P停止,非应答就是该主机应答的时候主机不把SDA拉低,从机读到SDA为”1“就代表主机未应答,就知道主机不想继续了,从机就会释放总线,把SDA控制权交还给主机;如果想读多个字节,就需要读一个字节,给从机一个应答SA=”0“,依次类推,最后一个字节给非应答SA=”1“,停止P。

二、MPU6050姿态传感器

1.MPU6050简介

具体可参考芯片手册!

①MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景。如果芯片里再集成一个3轴的磁场传感器测XYZ轴的磁场强度就为9轴,再集成一个气压传感器测气压大小就称为10轴姿态传感器,一般气压值反映了高度信息。

姿态角也叫欧拉角,以飞机为例,欧拉角就是飞机机身相对于初始XYZ3个轴的夹角。飞机机头下倾/上仰,与y轴的夹角叫俯仰角Pitch;机身左/右翻滚,与x轴的夹角叫滚转Roll;飞机机身保持水平,机头左转/右转向,与z轴的夹角叫偏航Yaw。数据融合是综合多个传感器得出精确的欧拉角的意思,常见的数据融合算法有互补滤波卡尔曼滤波等,涉及到惯性导航领域中姿态解算的知识了。

②3轴加速度计(Accelerometer):简称为Accel或者Acc或A,测量X、Y、Z轴的加速度。它其实也可以得出物体静止时的角度,如车停在斜坡上。加速度计具有静态稳定性,不具有动态稳定性

③3轴陀螺仪传感器(Gyroscope):简称Gyro或G,测量X、Y、Z轴的角速度。中间黄色部分是一个有一定质量的旋转轮,外面灰色的是3个轴的平衡环,当中间的旋转轮高速旋转时,根据角动量守恒,它具有保持它原有角动量的趋势,这个趋势可保持旋转轴保持不变。当外部物体的方向转动时,内部的旋转轴并不会转动,此时就会在平衡环连接处产生角度偏差。如果在连接处放一个旋转的电位器,测量电位器电压就可得到旋转的角度。

但是MPU6050芯片里的陀螺仪测出的是角速度,如果想通过角速度得到角度,只需要对角速度积分,但这样积分也有局限性:当物体静止时,角速度值会因为噪声无法完全归零,经过积分累积就会使得到的角度产生漂移。陀螺仪具有动态稳定性,不具有静态稳定性

当然芯片里可不是直接这种机械结构式传感器,它可能是用电实现(厂商秘密)!

应用场景:图像稳定器、位置服务、游戏手柄、3D遥控器、玩具、平衡车、无人机等

 2.MPU6050参数与电路

部分常用参数

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

②加速度计满量程选择:±2、±4、±8、±16(g)。1个g=9.8m/s2

③陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)。度/秒为角速度单位,如果被测物体运动非常剧烈,两个量程就可选大的,满量程越小测量越细粒。

④可配置的数字低通滤波器。如果输出数据抖动剧烈,可配置寄存器来选择对输出数据进行低通滤波。

⑤可配置的时钟源和采样分频。时钟源经过采样分频可以为AD转换和内部其他电路提供时钟。

⑥I2C从机地址:110 1000(AD0=0),110 1001(AD0=1)。

写的时候转换为十六进制,比如110 1000直接转换为0x68H,但这样直接转换与“时序上第一个字节高7位作为从机地址,最低位是读写位”表示不符,所以要把(0x68<<1)|读写位,读1写0。也可直接把从机地址1101 0000当作从机地址0xD0,再根据需要或上读写位。

硬件电路

 右边为MPU6050芯片,左下角是一个8针排针,左上角是一个低压差线性稳压器(LDO)。芯片本身引脚很多,包括一些在内部固定连接的最小系统,引出来使用的是左下角的8针,且SDA和ACL模块内部已经内置两个4.7K的上拉电阻,接线时直接把SCL和SDA接在GPIO口就可。

设计XCL和XDA是为了扩展芯片功能,加上3轴的磁力计传感器弥补偏航角不准的缺陷,无人机则还要加上气压计实现定高飞行,这时扩展时就要用到XCL和XDA了,可外接磁力计或气压计,将其数据读取到MPU6050中进行姿态解算,如果用不到解算功能,也可以把它们直接挂载在SCL和SDA上。

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

LDO这部分是供电逻辑,手册中MPU6050芯片的VDD供电为2.375~3.46V,可接3.3V,不可接5V。输入端VCC_5V可以在3.3~5V之间,经过3.3VLDO稳压器给芯片端供电,蓝色二极管为电源指示灯。如果自己已经有了稳定的3.3V电源,就不要再设计这一部分电路。

框图

左上角为时钟输入和输出脚,不过一般使用内部时钟,参照刚才的硬件电路CLKIN直接接地,CLKOUT未引出,这部分不需要过于担心。

灰色部分是芯片内部传感器,最下面有个温度传感器,传感器数据AD转换完后统一放在数据寄存器里Sensor Register,转换都是自动进行的,我们配置好转换频率后,每个数据就会自动以设置好的频率刷新到数据寄存器,需要的时候直接读取数据寄存器即可。

Self test自测单元用来验证芯片好坏,当启动自测后芯片内部会模拟一个外力施加在传感器上,导致传感器数据会比平时大些。自测流程:使能自测、读取数据、失能自测、读取数据、两数据相减得到自测响应。如果自测响应在规定范围就说明芯片没问题,反之有问题。

Charge Pump是电荷泵/充电泵,CPOUT引脚需要外接一个电容,电荷泵是一种升压电路,比如OLED里就有。升压原理:比如一个5V电池,并联一个电容,电池给电容充电,充满后电容也相当于一个5V电池,然后修改接法改为串联,输出就为10V。不过电容电荷较少,使用时持续时间不长,所以并联到串联的切换速度应该很快,后面再加个电源滤波。

右边绿圈为寄存器通信接口部分,中断状态寄存器(Interr Status Register)控制内部哪些事件到中断引脚的输出;先入先出寄存器(FIFO)对数据流进行缓存(本节暂时不用);配置寄存器(Config Register)对内部各个电路进行配置;工厂校准(Factory Calibration)意思就是内部传感器都进行了校准(不用了解)。数字运动处理器(DMP)是芯片内部自带的姿态解算的硬件算法,配合官方的DMP库可以进行姿态解算。FSYNC是帧同步,暂时不用。

红圈部分就是通信接口部分,8、9、13、14是从机的I2C和SPI通信接口,用于和STM32通信(MPU6050作为从机);6、7是主机的I2C通信接口,用于和扩展的设备进行通信(MPU6050作为主机)。接口旁路选择器(Serial Interface Bypass Mux)就是个开关,如果拨到上面,辅助的引脚(6、7)就会和通常引脚(8、9、13、14)接在一起,这时STM32是“大哥”,可控制所有设备;反之两路总线分开。

三、软件I2C读取MPU6050

接线图

 软件I2C是指用普通GPIO口,手动(软件)翻转电平实现协议,并不需要STM32内部的外设资源支持,所以GPIO端口可以任意指定,这里SCL是PB10,SDA是PB11。只需要用到GPIO的函数,用不到配置I2C的库函数。硬件电路不需要上拉电阻了,因为芯片内部已经自带了。

注意程序中涉及的“读R”和“写W”都是以主机即STM32为对象说的!

程序思路

1、建立I2C通信层的.c和.h模块,写好I2C底层的GPIO初始化和6个时序基本单元。

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

3、在main.c里调用MPU6050模块,初始化、拿到数据、显示数据。

//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);//防止MPU6050跟不上响应
}
void MyI2C_W_SDA(uint8_t BitValue){//主机写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;//返回读到SDA线的电平
}

//初始化任务: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 ;//开漏输出
	//开漏输出但不代表只可输出,也可输入,输入时先输出1,再直接读取输入数据寄存器就行
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10| GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);//配置好默认就是输出低电平
	GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);//置为高电平
}

//6个时序单元
void MyI2C_Start(void){
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	//SCL高时可对SDA进行读操作,为防止误判(Sr重复起始时易发生),所以先释放SDA(置1)再释放SCL
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
	//除了Stop,其他单元一般都会保证SCL以低电平0结束,便于各单元拼接
}
void MyI2C_Stop(void){
	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);//取位发送到SDA。此时SCL为低,可向SDA线发一位数据(高位先行)
	//第一次按位与结果0x80或0x00,因为参数类型为BitAction,只能非1即0,所以结果就变为1或0
	MyI2C_W_SCL(1);//释放SCL,一般在上升沿时,数据就被从机读走
	MyI2C_W_SCL(0);//拉低SCL,驱动时钟走一个脉冲
	}
}
uint8_t MyI2C_ReceiveByte(void){
	uint8_t i,Byte = 0x00;
	MyI2C_W_SDA(1);//主机在接收前要先释放SDA,即置SDA为1。这时从机就会往SDA发数据
	for(i=0;i<=8;i++){
		MyI2C_W_SCL(1);//置高SCL,开始读了,此时SDA数据禁止变化
		if (MyI2C_R_SDA()==1){
			Byte |= (0x80 >> i);//如果读到SDA是1就转存到Byte对应位写1。置1用或,清0用与
		}
		MyI2C_W_SCL(0);
	}
	return Byte;
}
//发送应答和接收应答其实就是上面两个函数1位的简化版
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);//读之前先释放SDA。这时从机向主机发送Ack到SDA
	//I2C引脚是开漏输出弱上拉,主机输出1并不是强制SDA为高电平,而是释放SDA
	//从机有义务将SDA再拉低的(弹簧杆子),当然它想发送1另说。
	//所以即使上面SDA置1了,下面读到的值也可能为0。
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();//读SDA线上的Ack
	return AckBit;
}

//MPU6050.c
#include "stm32f10x.h"                  
#include "MyI2C.h"
#include "MPU6050_Reg.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();
	//若发送多字节可把这两句用for循环括起来把一个数组的数据依次发出去
	
	MyI2C_Stop();
}

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写0
	MyI2C_ReceiveAck();
	Data = MyI2C_ReceiveByte();
	MyI2C_SendAck(1);//非应答。如果接收多个字节同理给应答,循环接收,最后再给非应答
	MyI2C_Stop();
	
	return Data;
}

/*************************************************************************************/


void MPU6050_Init(void){
	MyI2C_Init();
	//普通存储器里的数据没什么实际意义,而寄存器中的每一位对应了电路的某种状态,可与外设硬件电路互动
	//根据芯片手册配置MPU6050相应寄存器,就像回到了51时期用寄存器进行开发
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);//解除睡眠,选择陀螺仪时钟(更精确)
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);//6个轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);//采样分频为10
	MPU6050_WriteReg(MPU6050_CONFIG	,0x06);//滤波参数给最大
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);//陀螺仪最大量程
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);//加速度计最大量程
	//配置完这些之后,内部就在连续不断地进行数据转换,输出的数据放在数据寄存器里
	//想获取数据的话,下面再写个函数:
}

//这个函数需要返回6个int16_t数据,但函数返回值只能1个。
//多返回值函数设计,最简单一种方法:
//在函数外面定义6个全局变量,子函数读到的数据直接写到全局变量,在主函数里进行共享。但不利于封装
//第二种:用指针进行变量的地址传递实现多返回值。
//第三种:用结构体对多个变量打包,然后再统一进行传递。类似于STM32库函数利用结构体输入参数,只不过这里是输出
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_ReadReg(MPU6050_ACCEL_XOUT_H);//加速度X轴高8位
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*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);//陀螺仪X轴高8位
	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;
	
	//因为这些寄存器地址是连续的,可优化连续读取。上面的方法太长
}

uint8_t MPU6050_GetID(void){//获取芯片ID号
	return MPU6050_ReadReg(MPU6050_WHO_AM_I	);
}
//main.c这里只是读取MPU6050数据简单显示下
#include "stm32f10x.h"   // Device header
#include "Delay.h"   
#include "OLED.h"
#include "MPU6050.h"  //包含了“MyI2C.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);//参数是6个指针变量,“指了六条路”这样就可拿到6个数据
		OLED_ShowSignedNum(2,1,AX,5);
		OLED_ShowSignedNum(3,1,AY,5);
		OLED_ShowSignedNum(4,1,AZ,5);
		OLED_ShowSignedNum(5,1,GX,5);
		OLED_ShowSignedNum(6,1,GY,5);
		OLED_ShowSignedNum(7,1,GZ,5);
	}
}

上面的程序用了多层的模块架构。最底层I2C协议层,主要关注点是引脚是哪几个,什么配置,时序什么时候高电平,什么时候低电平?这些与协议相关的内容。I2C协议层之上,是MPU6050的驱动层,主要关注点是如何读写寄存器,怎么配置寄存器,怎么读取数据?这些驱动相关的内容。最后是主函数应用层,只需要MPU6050_GetData函数得到数据就行,然后就是用这些数据进行再一步功能设计。

拓:

MPU6050_GetData函数需要返回6个int16_t数据,但函数返回值只能1个。多返回值函数设计,常见有一下三种:

最简单的一种:在函数外面定义6个全局变量,子函数读到的数据直接写到全局变量,在主函数里进行共享。但不利于封装。
第二种:用指针进行变量的地址传递实现多返回值。
第三种:用结构体对多个变量打包,然后再统一进行传递。类似于STM32库函数利用结构体输入参数,只不过这里是输出。


总结

I2C学习分两大块,第一,学习协议规则,用软件模拟的形式实现协议;第二,学习STM32的I2C外设,然后用硬件实现协议。以上是第一块,下次再进一步学习STM32的I2C外设,和协议的硬件实现方式!
下次再见!!

往期精彩:
STM32定时器输入捕获(IC)
STM32定时器输出比较(PWM波)
STM32定时中断
STM32外部中断
STM32GPIO精讲

物联沃分享整理
物联沃-IOTWORD物联网 » STM32学习:深入理解I2C通信协议

发表评论