IIC总线协议及具体编程(读取MPU6050数据)

目录

同步时序与异步时序

异步时序简介

同步时序简介

小结

GPIO易遗漏知识点

GPIO框图​编辑

各种输出模式输出时能否输入

GPIO的默认上拉/下拉的作用(弱上拉/弱下拉)

位带区域

GPIO小结

IIC总线协议

IIC简介

IIC总线时序初步了解

SCL使用推挽输出/开漏输出,SDA使用开漏输出的原因

IIC总线外部电路连接

1、SCL

2、SDA

3、构建IIC总线开漏输出外加上拉电阻模型

4、SDA使用开漏输出外加上拉电阻的好处

IIC总线协议重要的时序单元

主机发送一个字节时序

主机接收一个字节时序

主机接收/发送应答位时序

以下为基本单元拼接整体时序

IIC指定地址发送一个字节时序

IIC当前地址读时序

IIC指定地址写时序

IIC通信协议具体编程

HAL库程序

myiic.h程序

myiic.c程序

相关注意点

MPU6050.h程序

MPU6050_Reg.h程序

MPU6050.c程序

main.c

标准库程序

myiic.h程序

myiic.c程序


为了理解IIC总线协议的硬件和软件设计原理,首先讲一下同步时序和异步时序的差异与GPIO易遗漏的知识点。

同步时序与异步时序

异步时序简介

像之前的串行通讯的异步通信(如串口通信),通信线有TX、RX、GND(一方的TX连另一方的RX,一方的RX连另一方的TX,GND连在一起(有相同的参考电平))。 两方按照之前约定好的波特率进行传输每位,如果一方发生了中断,接收的数据就会出错(接收方并不知道发生了中断,会继续按照之前约定好的速率接收,很依赖硬件电路(在固定的时间收发数据的每一位))。特点:全双工(大多数情况下同一时间,只有发送或者接收),没有应答机制。

同步时序简介

像IIC通信就是一种同步时序通信,通信线有SDA、SCL,设备的SDA和SCL都连在总线上。时钟SCL完全由主机控制,既使在发送一个字节的时序中间发生了中断,只要再继续发送字节的时序,接收方也可以收到正确的发送数据,接收方在何时接收/发送什么位完全由主机发出的时钟SCL决定。特点:半双工(节省资源),有应答机制,总线可接多个模块,接线简单。

小结

异步时序的优缺点:优点就是省一根时钟线,节省资源。缺点是对时间要求严格,对硬件电路依赖比较严重。

同步时序的优缺点:跟上面的优缺点相反。

GPIO易遗漏知识点

GPIO框图IIC总线协议及具体编程(读取MPU6050数据)_第1张图片

推挽输出:可以输出高电平和低电平。输出的同时可以输入。

开漏输出:如果不外接上拉电阻的话,只能输出低电平。输出的同时可以输入。

复用推挽/开漏输出:如一些外设在STM32上有指定的一些引脚,如STM32串口1的TX和RX要用到PA9和PA10(或者其他的复用引脚),这时初始化GPIO引脚就要使用复用模式。

IIC总线协议及具体编程(读取MPU6050数据)_第2张图片

各种输出模式输出时能否输入

GPIO在输出模式可以输入,向外界输出时,内部可以接收输出的高低电平,但一般不允许一个GPIO口输出的时候外界同时输入引脚(对于GPIO的一般使用情况来说)。实际向外界输出时,如果外界同时输入,输入的结果受自身输出值和外界输入值影响,详见下面小结第4条。

GPIO的默认上拉/下拉的作用(弱上拉/弱下拉)

上下拉只对输入模式有作用,输出模式是被禁止的。

给输入提供一个默认的输入电平,对应一个数字的端口,输入不是高电平就是低电平,如果输入引脚什么都不接,那到底算高电平还是低电平?如果输入什么都不接,输入就会处于一种浮空的状态,引脚的输入电平极易受外界干扰而改变,为了避免引脚悬空导致的输入数据不确定,接入上拉电阻,当引脚悬空时,就有上拉电阻来保证引脚的高电平,上拉输入也可以称为默认为高电平的输入模式,下拉输入就是默认为低电平的输入模式,上拉电阻和下拉电阻都是比较大的,是一种弱上拉和弱下拉,目的是不影响正常的输入操作。

位带区域

在STM32中,专门分配的有一段地址区域。这段地址映射了RAM和外设寄存器所有的位(位带区域),读写这段地址中的数据就相当于读写映射的某一位。

GPIO小结

1、只有模拟输入模式(直接通向ADC,GPIO无效)会关闭GPIO的输入功能。

2、所有输入模式都不允许输出,输出通道关闭。

3、开漏输出模式只能输出低电平,需要输出高电平时要外接上拉电阻。开漏输出可用于输出5V电压(外接5V+上拉电阻)

4、开漏输出模式+上拉电阻(SDA)。引脚输出时输入不一定等于输出的值(由外部电路决定)。(输出的同时也可以输入,该模式兼顾输入和输出。该模式下引脚一直在输入,以IIC总线为例,当主机发送寻址字节时,主机SDA在输出的同时也在输入,且这时输入=主机SDA输出,当主机发送寻址结束,该由从机应答前,主机主动输出1释放SDA线(SDA(1))交出SDA总线控制权,从机产生应答,将SDA总线拉低(此时即使主机输出1但由于线与的特性,主机的SDA输入也被拉低为0),之后应答时钟脉冲下降沿,从机主动主动输出1释放SDA线(SDA(1))交出SDA总线控制权给主机。可见IIC所有设备一直在输入,输入状态只与IIC总线状态保持一致)。

5、在GPIO输出模式下,输入的上下拉配置被禁止。

IIC总线协议

IIC简介

    相互集成的     电路

Inter-Integrated Circuit  总线

IIC(Inter-Integrated-Circuit)其实是IICBus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从(一主多从、多主多从)架构,适用于芯片和芯片之间的通讯。IC芯片(Integrated Circuit  Chip:集成电路芯片)是将大量的微电子元器件(晶体管、电阻、电容等)形成的集成电路放在一块塑基上,做成一块芯片。

IIC总线时序初步了解

总线上的时序就是IIC实际传输时总线的各个时间的状态,例如主机发送一个数据,发送的同时从机就会实时收到主机发送的时序,并按照其时序对发送信息进行解读(由于之前已经约定好了IIC通信),解读信息之后再传给主机(应答位或者数据)。同一时刻只有一方在发送数据。由于SCL是由主机发送的,从机发送给主机数据时,按照其SCL的高低电平发送SDA线数据(从机只有被主机点名之后,才能控制IIC总线)。

IIC设计出来的最基本功能是通过通信线,实现单片机读写外挂模块寄存器的功能。(在指定位置读写寄存器,读写寄存器就实现了对这个模块的完全控制)

SCL使用推挽输出/开漏输出,SDA使用开漏输出的原因

IIC总线外部电路连接

IIC总线协议及具体编程(读取MPU6050数据)_第3张图片 

本图片选自B站江科大STM32入门教程

1、SCL

主机端SCL的GPIO模式既可以设置为推挽输出也可以是开漏输出外加上拉电阻的模式,因为SCL完全由主机输出,从机在任何时刻只能被动接收,不允许控制SCL线。

①当主机为推挽输出时,从机可设置为浮空输入/上拉输入模式(但一般从机模式不需要我们设置)。

②当主机为开漏输出外加上拉电阻时,从机可设置为同样的模式(开漏输出模式同样可以输入)。但一般主机SCL也要设置为开漏输出外加上拉电阻,当多个主机需要仲裁时,就需要这种模式。

2、SDA

SDA线既要输出也要兼顾输入。

SDA如果是推挽输出模式,假如没有控制好SDA输出,一方SDA输出0,另一方SDA输出1,一方面会造出总线的冲突,另一方面很容易造成线路的短路(一方输出低电平,另一方输出高电平,相当于短路,正极连负极,从而烧毁芯片)。(若要使用推挽输出模式,一方发送,其他接收方要呈现高阻态,避免总线状态冲突)所以主从机SDA线只能是开漏输出外加上拉电阻的模式,这种模式能够很好的杜绝上述电源短路现象,实现线与的功能,避免引脚频繁切换。

线与:总线上只要有一方SDA输出为0就将总线拉低,总线呈现低电平状态,总线上所有设备SDA输出1(开漏模式下输出1相当于与总线断开联系),SDA总线由总线上的外接上拉电阻拉高,总线呈现高电平状态。

3、构建IIC总线开漏输出外加上拉电阻模型

开漏输出外加上拉电阻是一种弱上拉作用,相当于一根弹簧拉着一根杆子的模型,为了避免同时有设备推杆子(强上拉)和拉杆子(强下拉)造成的冲突,规定所有设备不能向上推杆子(强上拉),当主机或从机需要输出低电平时就拉下杆子(将总线拉低),需要输出高电平就相当于松手(断开引脚/断开与总线的联系),总线在弹簧(上拉电阻)的作用下自动回到高位,各设备输入时相当于观察杆子的高低状态。

弱上拉/强上拉:没有像推挽输出那样强上拉/强下拉(强上/下拉:相当于没有接电阻直接通过开关接电源正极/通过开关接地/负极),弱上拉往往是电源正极+阻值较大的电阻,上升沿(低电平转变高电平)的时间较长。

4、SDA使用开漏输出外加上拉电阻的好处

1.完全杜绝电源短路现象,保证电路安全。(所有设备无论怎么拉杆子还是松手,都不会使杆子处于被强拉和强推的状态,即使有多个设备同时拉杆子也没问题)

2.避免引脚模式的频繁切换(相对于切换输入输出模式),开漏+弱上拉的模式,同时兼顾了输入和输出的功能。

3.线与的功能。只要有一个设备输出低电平,总线就处于低电平。只有所有设备输出1,总线才处于高电平。

IIC总线空闲状态下SDASCL都为高电平。在空闲状态下,主机可以主动发起对SDA的控制,只有在从机需要发送数据/应答时,主机才会转交SDA的控制权给从机。同一时间,只有一方(主机/从机)掌握总线的控制权。

IIC总线协议重要的时序单元

IIC开始和结束时序均由主机产生,分别是SCL高电平期间产生下降沿和上升沿,这里就不具体讲了。

主机发送一个字节时序

IIC总线协议及具体编程(读取MPU6050数据)_第4张图片

从机读取SDA总线数据的每一位往往在SCL上升沿期间就完成了,主机也要在下一个上升沿来临之前,将要写的数据写在SDA上,但时钟是主机主导的并没有从机那么着急。可以看出一个SCL脉冲前,依次执行主机写入、从机读出的操作。写一个字节一个SCL脉冲就要执行8次,这里要注意的是IIC发送一个字节是先发送最高位,从高位到低位发送。

IIC同步时序的优越性:如果主机一个字节发送一半,突然执行中断了,不操作SCL和SDA了,那么时序就会在中断的位置不断拉长,SCL和SDA电平暂停变化,传输也完全暂停,等中断结束,主机再继续操作,传输不会受到中断的影响,这就是同步时序的好处。

主机接收一个字节时序

IIC总线协议及具体编程(读取MPU6050数据)_第5张图片

从机的数据每一位数据变换基本上是贴着SCL下降沿的(第7位在第一个SCL前夕变换),而主机接收数据可在高电平任意时间读取。

SDA输入模式两种理解方式:

1.释放SDA(SDA(1))就相当于切换至输入模式。

2.所有设备包括从机始终处于输入模式,当主机需要发送时,就可以主动拉低SDA,而主机被动接收时,就必须释放SDA(SDA(1))。主机不释放总线,从机就没法输入(总线SDA同一时刻只有一方在控制总线)。

主机接收/发送应答位时序

IIC总线协议及具体编程(读取MPU6050数据)_第6张图片

发送/接收应答位相当于发送一位/接收一位,需要注意的是主机接收从机发送的数据时,当主机发出非应答信号时,从机会交出总线的控制权。

以下为基本单元拼接整体时序

IIC指定地址写时序

IIC总线协议及具体编程(读取MPU6050数据)_第7张图片

主机发送的第一个地址是从机地址(7位/10位)+读写位(0:写入1:读取)

从机应答,总线控制权的切换过程:

1、主机发送完字节的最后一位时,释放SDA总线(SDA(1)),总线控制权交给从机,从机此刻立即产生应答(拉低SDA总线),综合两者的波形,结合线与的特性,总线一直呈现低电平,如下图。

IIC总线协议及具体编程(读取MPU6050数据)_第8张图片

2、从机在应答CLK脉冲的下降沿,要将总线控制权交给主机。

IIC总线协议及具体编程(读取MPU6050数据)_第9张图片

IIC当前地址读时序

对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)。如果之前已经写入了地址,指针变量(不会因为结束时序而清零)就存着写入的地址,如果没有写就默认为0(设备上电后,地址指针默认指向0地址)。

在写入/读出一个字节后,地址指针会自动+1。由于该时序的所读地址不能指定,故用的较少,下面指定地址读的时序用的较多。

IIC总线协议及具体编程(读取MPU6050数据)_第10张图片

IIC指定地址读时序

由于指定读写标志位只能是跟着起始条件的第一个字节,故IIC指定地址写时序是先用指定地址写,将要读的设备地址写到设备的地址指针,后用当前地址读时序将地址指针中的地址内容读出来,IIC指定地址写时序是由以上两种时序拼接而来的。

IIC总线协议及具体编程(读取MPU6050数据)_第11张图片

IIC总线协议及具体编程(读取MPU6050数据)_第12张图片

主机接收从机数据时,主机给应答了,从机就会继续发,主机给非应答了,从机就会交出SDA的控制权。如果主机没有发送非应答,即使发送停止时序也可能停不下来,因为SDA可能被从机下拉,就不能在SCL高电平期间产生上升沿。

从机控制SDA发送一个字节的权利开始于读写标志位为1,结束于主机给应答位为1。

IIC通信协议具体编程

HAL库程序

myiic.h程序

#ifndef _MYIIC_H
#define _MYIIC_H
#include "./SYSTEM/sys/sys.h"     //sys.h中包含#include "stm32f1xx.h"                                                           
#include "./SYSTEM/delay/delay.h" //要使用延时函数
//引脚重定义
#define SCL_GPIO_PORT GPIOB
#define SCL_GPIO_PIN GPIO_PIN_10
#define SCL_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOB_CLK_ENABLE();}while(0)

#define SDA_GPIO_PORT GPIOB
#define SDA_GPIO_PIN GPIO_PIN_11
#define SDA_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOB_CLK_ENABLE();}while(0)
						
//函数声明					
void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t data);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);
uint8_t MyI2C_WaitAck(void);
							
#endif

myiic.c程序

#include "./BSP/MYIIC/myiic.h"
#include "./SYSTEM/delay/delay.h"  

void MyI2C_W_SCL(uint8_t BitValue)
{
	HAL_GPIO_WritePin(SCL_GPIO_PORT,SCL_GPIO_PIN,(GPIO_PinState)BitValue);
	delay_us(10);//对于STMF1系列,即使不加延时MPU6050也完全跟的上,保险起见,加上了延时
}             //IIC可以慢一些,多慢都行,但是快的话,要参考被控器件手册中对IIC时序的要求修改

void MyI2C_W_SDA(uint8_t BitValue)
{
	HAL_GPIO_WritePin(SDA_GPIO_PORT,SDA_GPIO_PIN,(GPIO_PinState)BitValue);
	delay_us(10);
}

uint8_t MyI2C_R_SCL(void)
{
	uint8_t BitValue;     //也可以延时后直接返回
	BitValue=HAL_GPIO_ReadPin(SCL_GPIO_PORT,SCL_GPIO_PIN);//该函数返回值类型为GPIO_PinState
	delay_us(10);
	return BitValue;
}

uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;     //也可以延时后直接返回
	BitValue=HAL_GPIO_ReadPin(SDA_GPIO_PORT,SDA_GPIO_PIN);//该函数返回值类型为GPIO_PinState
	delay_us(10);
	return BitValue;
}


void MyI2C_Init(void)
{
	GPIO_InitTypeDef iic_gpio_init_struct;
	SCL_GPIO_CLK_ENABLE();       //开启时钟
	SDA_GPIO_CLK_ENABLE();       
	
	iic_gpio_init_struct.Mode= GPIO_MODE_OUTPUT_OD; //SCL使用开漏输出  上下拉不配置默认是无上下拉
	iic_gpio_init_struct.Pin=SCL_GPIO_PIN;          //配置为输出模式时上下拉没有作用
    iic_gpio_init_struct.Speed=GPIO_SPEED_FREQ_HIGH;//高速
	HAL_GPIO_Init(SCL_GPIO_PORT,&iic_gpio_init_struct);
  
	iic_gpio_init_struct.Pin=SDA_GPIO_PIN;          //SDA使用开漏输出
	HAL_GPIO_Init(SDA_GPIO_PORT,&iic_gpio_init_struct);
	
	MyI2C_W_SCL(1);//空闲状态
	MyI2C_W_SDA(1);
  //MyI2C_stop();//将SDA/SCL总线拉高或直接使用IIC停止时序
}

void MyI2C_Start(void)
{ 
	MyI2C_W_SDA(1);//由SR(再次起始时序)可知,传输完寄存器地址后SCL被拉低,趁SCL还是低电平,SDA状态不确定
	MyI2C_W_SCL(1);//故先将SDA拉高,为下降沿做准备
	MyI2C_W_SDA(0);
	
	MyI2C_W_SCL(0);//钳住总线1.占用总线 2.写/读一个字节起始SCL都需要是低电平,为了更好的拼接时序
}

void MyI2C_Stop(void)
{ 
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
	                //只有IIC结束时序,SCL是高电平结尾
}

void MyI2C_SendByte(uint8_t data)
{
  uint8_t i;
	for(i = 0; i < 8; i ++)
	{                             
		MyI2C_W_SDA(data &(0x80 >> i));//此时SCL为低电平
		MyI2C_W_SCL(1);                //此时从机读取数据
		MyI2C_W_SCL(0);            
	}
}

uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i,byte=0x00;
	MyI2C_W_SDA(1);  //主机释放SDA总线,从机在此时就传输要传输字节的最高位
	for(i = 0; i < 8; i ++)
	{     
		MyI2C_W_SCL(1);//产生上升沿主机读取数据		
	    if(MyI2C_R_SDA()){byte |= (0x80 >> i);}
	    MyI2C_W_SCL(0);//此时从机要发送的位信息传过去
	}
	return byte;
}

void MyI2C_SendAck(uint8_t AckBit)//发送应答位
{
                             
	MyI2C_W_SDA(AckBit);//此时接收完了从机的一个数据,SCL为低电平
	MyI2C_W_SCL(1);     //此时发送给从机
	MyI2C_W_SCL(0);            
	
}

//此接收应答只是简单的接收应答位,如果要判断从机是否应答还要对返回值进行判断
//最好在函数中就加入判断从机应答机制,如果从机应答就继续,否则就要MyI2C_Stop,主机停止发送
uint8_t MyI2C_ReceiveAck(void)//接收一位
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);//主机释放SDA总线,从机在此时将应答位放在SDA上
	MyI2C_W_SCL(1);//产生上升沿主机读取数据		
	AckBit=MyI2C_R_SDA();
	MyI2C_W_SCL(0);//此时从机要释放SDA线,交出SDA控制权
	return AckBit;
}

//此函数可代替MyI2C_ReceiveAck()函数,该函数返回值为从机的应答,应答为0,非应答为1
uint8_t MyI2C_WaitAck(void)
{
	uint8_t AckBit=0;
	
	MyI2C_W_SDA(1);//主机释放SDA总线,从机在此时将应答位放在SDA上
	MyI2C_W_SCL(1);//产生上升沿主机读取数据	
	if(MyI2C_R_SDA())//从机正常收到主机的信息,应该发送应答0,否则从机没有应答,主机应该主动停止IIC通信
      {
		MyI2C_Stop(); 
	    AckBit=1;
	  }
    else
      {
	    MyI2C_W_SCL(0);
	  }//此时从机要释放SDA线,交出SDA控制权
	return AckBit;
}

相关注意点

IIC总线协议及具体编程(读取MPU6050数据)_第13张图片

(GPIO_PinState)uint8_t是将u8强转为GPIO_PinState,只要u8不是0,都会被强转为GPIO_PIN_SET。

MPU6050.h程序

#ifndef _MUP6050_H
#define _MUP6050_H
#include "./BSP/MYIIC/myiic.h"                                                                 


//初始化函数声明
void MPU6050_WriteReg(uint8_t Regadress,uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t Regadress);
void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(uint16_t *AccX,uint16_t *AccY,uint16_t *AccZ,
	            uint16_t *GyroX,uint16_t *GyroY,uint16_t *GyroZ);
#endif

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.c程序

#include "./BSP/MUP6050/mup6050.h"
#include "./BSP/MUP6050/MPU6050_Reg.h"


#define MPU6050_ADRESS  0xD0
void MPU6050_WriteReg(uint8_t Regadress,uint8_t Data)
{
    MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADRESS);
	MyI2C_ReceiveAck();//接收从机应答,如果想处理应答位可以在这里处理一下应答位。HAL库中直接是等待应答的到来,没有应答Stop
	MyI2C_SendByte(Regadress);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(Data);//如果想指定地址写多个字节就可以多执行几次
	MyI2C_ReceiveAck();  //这两行代码,可以用for循环,把一个数据的多个数据发送过去
	MyI2C_Stop();
}
uint8_t MPU6050_ReadReg(uint8_t Regadress)
{
	
	uint8_t Data;
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(Regadress);//将MPU6050的地址指针写为Regadress
	MyI2C_ReceiveAck();
	
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADRESS|0x01);//改为读寄存器
	MyI2C_ReceiveAck();                 //此时SDA控制权交给从机
	Data=MyI2C_ReceiveByte();           //如果想读多个字节,就可以用for循环多执行几次这两行代码,注意就要
	MyI2C_SendAck(1);                   //在最后一个字节给非应答(1),之前的字节都给应答,表示主机想继续接收.
	MyI2C_Stop(); 
	
	return Data;
}
void MPU6050_Init(void)
{
	MyI2C_Init();
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);//不复位,不睡眠,不循环开启,不使能温度传感器,选择陀螺仪时钟(0x00是内部时钟)
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);//循环模式唤醒频率不设置,六个轴均不待机
	 
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);//采样频率分频,10分频
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);    //外部同步全为0,低通滤波给110
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);//陀螺仪配置寄存器 自测使能为0,选择最大量程
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);//加速度计配置寄存器  自测使能为0,选择最大量程  高通滤波暂时不需要
	
}

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

//该函数需要返回6个uint16_t的数据,分别表示X,Y,Z的加速度值和陀螺仪值
//多返回值函数的设计
//1.定义6个全局变量,子函数读到的数据直接写在全局变量里,6个全局变量在主函数中进行共享,不利于封装
//2.使用指针,进行变量的地址传递,来实现多返回值
//3.用结构体,对多个变量进行打包,再统一进行传递


void MPU6050_GetData(uint16_t *AccX,uint16_t *AccY,uint16_t *AccZ,
	            uint16_t *GyroX,uint16_t *GyroY,uint16_t *GyroZ)
{
	uint8_t DataH, DataL;
	  
	DataH=MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);  
	DataL=MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //DataH << 8左移不会变为0,因为最终赋值是16位的,
    *AccX=(DataH << 8)| DataL;                   //8位数据左移之后会自动进行类型转换 也可以在定义时就定义为uint16_t
		
	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_H);
    *GyroX=(DataH << 8)| DataL;
		
	DataH=MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);  
	DataL=MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
    *GyroY=(DataH << 8)| DataL;
		
	DataH=MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);  
	DataL=MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
    *GyroZ=(DataH << 8)| DataL;

}

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"  
#include "./BSP/led/led.h"
#include "./BSP/beep/beep.h" 
#include "./BSP/key/key.h" 
#include "./BSP/IIC/iic.h"
#include "./BSP/MYIIC/myiic.h"
#include "./BSP/MUP6050/mup6050.h"
#include "./BSP/OLED/OLED.h"

uint8_t Ack=0x01;
uint8_t ID;								          //定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ;			//定义用于存放各个数据的变量
int main(void)
{ 
	
	HAL_Init();                        //初始化HAL库
	sys_stm32_clock_init(RCC_PLL_MUL9);//9倍频,72MHz
	delay_init(72);                    //延时初始化
	
	
	//点名时序1
//	iic_init();
//	iic_start();
//	iic_send_byte(0xD0);
//	Ack=iic_wait_ack();
//	iic_stop();
	
//	//点名时序2
//	MyI2C_Init();
//	MyI2C_Start();
//	MyI2C_SendByte(0xD0);
//	//Ack=MyI2C_ReceiveAck();
//	Ack=MyI2C_WaitAck();
//	MyI2C_Stop();
		/*模块初始化*/
	OLED_Init();		//OLED初始化
	MPU6050_Init();		//MPU6050初始化
	
	/*显示ID号*/
	OLED_ShowString(1, 1, "ID:");		//显示静态字符串
	ID = MPU6050_GetID();				//获取MPU6050的ID号
	OLED_ShowHexNum(1, 4, ID, 2);		//OLED显示ID号
	
	while (1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);		//获取MPU6050的数据
		OLED_ShowSignedNum(2, 1, AX, 5);					//OLED显示数据
		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);
	}
		while(1)
	{
	
	}
	
}

标准库程序

标准库程序与HAL库的差异在于SDA和SCL的GPIO初始化和写引脚和读引脚函数的不同,基本框架都相同,这里列出不同的程序,其他程序与上相同。

myiic.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

myiic.c程序

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

/*引脚配置层*/

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);		//根据BitValue,设置SCL引脚的电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C写SDA引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);		//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);		//读取SDA电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
	return BitValue;											//返回SDA电平
}

/**
  * 函    数:I2C初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
  */
void MyI2C_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启GPIOB的时钟
	
	/*GPIO初始化*/
	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);					//将PB10和PB11引脚初始化为开漏输出
	
	/*设置默认电平*/
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);			//设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
/*协议层*/

/**
  * 函    数:I2C起始
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);							//释放SDA,确保SDA为高电平
	MyI2C_W_SCL(1);							//释放SCL,确保SCL为高电平
	MyI2C_W_SDA(0);							//在SCL高电平期间,拉低SDA,产生起始信号
	MyI2C_W_SCL(0);							//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}

/**
  * 函    数:I2C终止
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);							//拉低SDA,确保SDA为低电平
	MyI2C_W_SCL(1);							//释放SCL,使SCL呈现高电平
	MyI2C_W_SDA(1);							//在SCL高电平期间,释放SDA,产生终止信号
}

/**
  * 函    数:I2C发送一个字节
  * 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)				//循环8次,主机依次发送数据的每一位
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));	//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
		MyI2C_W_SCL(1);						//释放SCL,从机在SCL高电平期间读取SDA
		MyI2C_W_SCL(0);						//拉低SCL,主机开始发送下一位数据
	}
}

/**
  * 函    数:I2C接收一个字节
  * 参    数:无
  * 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
  */
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	for (i = 0; i < 8; i ++)				//循环8次,主机依次接收数据的每一位
	{
		MyI2C_W_SCL(1);						//释放SCL,主机机在SCL高电平期间读取SDA
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量
														//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
		MyI2C_W_SCL(0);						//拉低SCL,从机在SCL低电平期间写入SDA
	}
	return Byte;							//返回接收到的一个字节数据
}

/**
  * 函    数:I2C发送应答位
  * 参    数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
  * 返 回 值:无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);					//主机把应答位数据放到SDA线
	MyI2C_W_SCL(1);							//释放SCL,从机在SCL高电平期间,读取应答位
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
}

/**
  * 函    数:I2C接收应答位
  * 参    数:无
  * 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
  */
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;							//定义应答位变量
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	MyI2C_W_SCL(1);							//释放SCL,主机机在SCL高电平期间读取SDA
	AckBit = MyI2C_R_SDA();					//将应答位存储到变量里
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
	return AckBit;							//返回定义应答位变量
}

你可能感兴趣的:(通讯协议,单片机,嵌入式硬件,stm32)