(STM32学习笔记)I2C通信协议-软件模拟通信 (一)

目录

1、I2C通信(分为软件I2C通信、硬件I2C通信)

1.1 I2C通信的特点

1.2  时钟线和数据线

1.3 硬件电路

2、时序设计

2.1 起始条件 & 终止条件

2.2 发送一个字节 & 接收一个字节

2.3 应答机制:发送应答 & 接收应答

2.4 指定地址写 & 当前地址读 & 指定地址读

2.4.1 指定地址写

2.4.2 当前地址读

2.4.3 指定地址读

3、代码实现

3.1 软件模拟的I2C通信

3.1.1 I2C软件模拟通信(协议)层

3.1.2 MPU6050设备操作层

3.1.3 主函数逻辑层


1、I2C通信(分为软件I2C通信、硬件I2C通信)

I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线。

案例程序目的:通过软件I2C通信,对MPU6050芯片内部的寄存器进行读写,

写入到配置寄存器,就可以对外挂的模块进行配置,读出数据寄存器,就可以获取外挂模块的数据,这就是I2C通信的目的。

1.1 I2C通信的特点

1、同步、半双工

2、带数据应答

3、支持总线挂载多设备(一主多从、多主多从)

4、可以是软件IC和硬件IC

5、两根通信线:SCL(Serial Clock)、SDA(Serial Data)

1.2  时钟线和数据线

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第1张图片

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第2张图片

1.3 硬件电路

  • 所有I2C设备的SCL连在一起,SDA连在一起
  • 设备的SCL(时钟线)和SDA(数据线)均要配置成开漏输出模式
  • SCL和SDA各添加一个上拉电阻,阻值一般为4.7KQ左右

下面图是I2C典型电路模型(一主多从的模型)

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第3张图片

CPU是单片机,作为总线的主机,权力很大,对SCL线的完全控制,任何时候都是主机完全掌握SCL总线;在空闲状态下,主机还可以主动发起对SDA的控制,只有从机在发送数据和从机应答的时候,主机才会转交SDA的控制权给从机,这是主机的权利。

下面是一系列“被控IC”(挂载在I2C总线上的从机)

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第4张图片

“被控IC”这些从机可以是姿态传感器、OLED、存储器、时钟模块等。

从机的权利比较小,对于SCL时钟线,在任何时刻都只能被动的读取,

从机不允许控制SCL线,对于SDA线数据线,从机不允许主动发起对SDA的控制。

只有在主机发送读取从机的命令后,或者从机应答的时候,从机才能暂短的取得SDA的控制权。这就是一主多从的规定。

下面图是“被控IC”的内部电路

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第5张图片

左边绿色部分是SCL,右边是SDA。

2、时序设计

2.1 起始条件 & 终止条件

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

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第6张图片

起始条件:

在I2C总线处于空闲状态时,SCL和SDA都处于高电平状态,也就是没有任何一个设备区碰SCL和SDA,SCL和SDA由外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态。

当主机需要收发数据时,首先就要打破总线的宁静,产生一个起始条件,这个起始条件就是SCL处于高电平不去动它,然后把SDA拉下来,会产生一个下降沿,当从机捕获到这个SCL高电平,SDA下降沿信号时,就会进行自身的复位,等待主机的召唤,接下来在SDA下降沿之后,主机要再把SCL拉下来。

 模拟起始信号:

/**
  * @brief  软件I2C的起始信号
  * @param  无
  * @retval 无
  */
void MyI2C_Start(void)
{
	// 这里先释放SDA,再释放SCL的原因是为了使Start信号兼容重复起始信号RS
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}

终止条件:

SCL先放手,回弹到高电平,SDA再放手,回弹到高电平,产生一个上升沿,

这个上升沿触发终止条件,同时终止条件后,SCL和SDA都是高电平,回归平静状态。

起始和终止都是主机产生的,从机是没有这个权利的,所以在总线空闲状态时,从机必须始终双手放开,不允许主动去接触总线,如果允许,那就是多主机模型了。

模拟终止信号:

/**
  * @brief  软件I2C的结束信号
  * @param  无
  * @retval 无
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

2.2 发送一个字节 & 接收一个字节

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

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第7张图片

通俗理解:SCL低电平的时候,主机把数据放到SDA线上,过了一会SCL回到高电平,

此时从机趁SCL高电平的时候,立马读取SDA线上的数据。

举个例子,玩123木头人的游戏,数数字的人是主机,其他人是从机。

当主机不动的时候,相当于SCL处于高电平状态,此时其他人趁数数字人不动的期间(SCL高电平期间)就快速动起来,这些人的行为就相当于从机在读取SDA数据,直到数数字的人转头喊123木头人的时候,其他人就得保持静止状态,此时SCL又是低电平状态,主机也是在这个时候把数据放到SDA线上,等到转身不动时(SCL高电平期间),其他人又开始动起来(从机读取数据)。依次类推,循环8次,一个字节就发送成功了。

省流:主机拉低SCL,把数据放到SDA上,主机松开SCL回到高电平,从机读取SDA的数据。

模拟发送一个字节:

/**
  * @brief  发送一个字节
  * @param  Byte	发送的字节数据
  * @retval 无
  */
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));
		// SCL产生一个正脉冲,让从机读取数据
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}

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

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第8张图片

主机在接收数据之前,需要释放SDA。释放SDA相当于切换成输入模式,

当主机需要发送的时候,就可以主动去拉低SDA,而主机在被动接收的时候,就必须先释放SDA,此时从机取得了SDA的控制权,

从机发送0,就把SDA拉低;从机需要发送1,就放手,SDA回弹高电平。

实线部分表示主机控制的电平,虚线表示从机控制的电平,SCL全程由主机控制。

模拟接收一个字节: 

/**
  * @brief  接收一个字节
  * @param  无
  * @retval 接收的字节数据
  */
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	MyI2C_W_SDA(1);		// 主机释放SDA,交出SDA控制权
	for (i = 0; i < 8; i ++)
	{
		// 在SCL高电平期间读取SDA,如果SDA为1,则将Byte对应位置1(高位先行)
		MyI2C_W_SCL(1);
		if (MyI2C_R_SDA() == 1)
		{
			Byte |= (0x80 >> i);
		}
		MyI2C_W_SCL(0);
	}
	return Byte;
}

2.3 应答机制:发送应答 & 接收应答

  • 发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。
  • 接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。
  • (STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第9张图片

模拟发送应答:

/**
  * @brief  发送应答,以通知从机数据是是否发送结束
  * @param  AckBit		0为发送未结束,1为发送已结束
  * @retval 无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);
	// SCL产生一个正脉冲,让从机读取数据
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
}

模拟接收应答: 

**
  * @brief  接受应答,主机读取该信号以确认从机是否接受到数据
  * @param  无
  * @retval 应答信号,0为已收到(从机受到后下拉SDA),1为未收到
  */
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);		// 主机释放SDA,交出SDA控制权
	// 在SCL高电平期间读取SDA,如果SDA为1,则将AckBit置1
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	
	return AckBit;
}

2.4 指定地址写 & 当前地址读 & 指定地址读

2.4.1 指定地址写

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

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第10张图片

(1)读写数据位:读数据置1,写数据置0

(2)第一个应答信号:信号时由从机发送给主机,如果从机收到之前的信息,回复0,没有收到或者(主机)读取接收完成回复1

(3)第二个应答信号:单片机需要存储器返回一个应答信号

(4)第三个应答信号:发送完数据后,需要再给主机发送应答信号0,告诉主机写入成功

(5)最后写入停止位:SCL为高电平,SDA为上升沿

案例:对于指定从机地址为1101000的设备,在其内部0x19地址的寄存器中,写入数据0xAA,下图是指定地址写的时序。

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第11张图片

首先,SCL高电平期间,拉低SDA,产生起始条件(Start,S),在起始条件之后,紧跟着的时序,必须是发送一个字节的时序(Send Byte:0xD0),字节的内容必须是从机地址和读写位(Slave Address +R/W),刚好从机地址是7位,读写位是1位,总共8位,刚好一个字节。发送从机地址(Slave Address),就是确定通信的对象,发送读写位,就是确认接下来要写入还是要读出。

如下图所示:

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第12张图片

根据上图波形,如何理解应答非应答

应答:如果主机释放SDA之后,回弹到高电平,此时没有从机拉下SDA,主机依旧处于高电平,就代表从机没作出应答,即非应答;非应答的波形如下图所示:

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第13张图片

非应答:如果主机释放SDA之后,从机立马拉下SDA,此时又处于低电平状态,就代表从机产生了应答。应答的波形如下图所示:

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第14张图片

2.4.2 当前地址读

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

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第15张图片

如果主机要读取从机的数据,就可以执行这个时序。

首先,SCL高电平期间,拉低SDA,产生起始条件,然后主机调用发送一个字节,

来进行从机的寻址和指定读写标志位。

比如图示的波形,表示本次寻址的目标是1101000的设备,最后一位读写标志为1, 表示主机接下来想要读取从机的数据,

紧跟着,发送一个字节之后,接收一下从机应答位,从机应答0,代表从机收到了第一个字节。

在从机应答之后,数据的传输方向得放过来了,因为主机发出了读的命令,所以主机就不能继续发送了,同时主机要把SDA的控制权交给从机,主机调用接收一个字节的时序,进行接收操作。

从机在SCL低电平期间写入SDA,主机在SCL高电平期间读取数据,最后,主机在SCL高电平期间依次读取8位,就接收到了从机发送的一个字节数据,0000 1111,也就是0x0F。

那问题来了,这个0x0F是从机的哪个寄存器的数据呢?

此时就用“当前地址指针”来说明这个情况了。在从机中,所有的寄存器被分配到了一个线性区域中,且会有一个单独的指针变量,指示着其中一个寄存器,指针上默认一般指向0地址,并且每写入一个字节和读出一个字节之后,这个指针就会自动自增一次,移动到下一个位置,在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值,假设主机当前接收的数据是来自从机地址0x1A的,那下一次接收的地址就是0x1B,以此类推。

不过当前地址读这个时序不常用。

2.4.3 指定地址读

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

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第16张图片

(STM32学习笔记)I2C通信协议-软件模拟通信 (一)_第17张图片

指定地址读,其实就是指定地址写和当前地址读的复合格式。

 前半部分是指定地址写(但没来得及写),后半部分是当前地址读,两者加在一起就是指定地址读

        首先写入设备地址,然后发送一个字节,进行寻址,指定从机地址是1101 0000,读写标志位是0,代表主机要写的操作,经过从机应答之后,

        再发送一个字节,用来指定地址,这个数据就写入到了从机的地址指针里了。它的寄存器指针指向0x19,

        再发送一遍设备地址,主机接收数据,该数据就是寄存器地址0x19下的数据。

        最后一部分的数据可以多来几个,就可以写多个数据,地址指针在读后会自增,就可以连续读出一片区域的寄存器,效率也会变高。

        主机给应答:从机就会继续发,主机给非应答,从机不会再法发,交出SDA的控制权,从机控制SDA发送一个字节的权力,开始于读写标志位1,结束于主机给应答位为1

  

异步时序和同步时序的优缺点

异步时序:

1、好处:省一根时钟线,节省资源;

2、坏处:对时钟要求严格,发送方和接收方时钟不能由过大的偏差;

传输过程中,单片机进中断,发送方时序暂停,接受方仍会按照约定的速率读取,传输出错;

故异步时序的缺点:非常依赖硬件外设的支持,必须有USART电路才能方便的使用,否则很难用软件模拟。

同步时序(时钟要求不严格,对电路依赖度低)

1、设计时钟线,则对传输的时间要求变低;

2、在单方面暂停传输时,时钟线也暂停,传输双方都能定格在暂停的时刻,可过段时间再来继续;

3、极大的降低单片机对硬件电路的依赖,没有硬件电路的支持,也可以很方便的用软件手动翻转电平来实现通信。

3、代码实现

3.1 软件模拟的I2C通信

3.1.1 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

// 更改GPIO和引脚时,只需要更改以下的宏定义即可
// 需要注意:请检查使用的GPIO是否是APB2总线的外设,如果不是,则需要更改MyI2C_Init函数中的RCC_APB2PeriphClockCmd函数名称
#define SCL_GPIO_Port_CLK	RCC_APB2Periph_GPIOA
#define SDA_GPIO_Port_CLK	RCC_APB2Periph_GPIOA
#define SCL_GPIO_Port		GPIOA
#define SDA_GPIO_Port		GPIOA
#define SCL_Pin				GPIO_Pin_6
#define SDA_Pin				GPIO_Pin_7

/**
  * @brief  软件I2C的GPIO端口初始化函数
  * @param  无
  * @retval 无
  */
void MyI2C_Init(void)
{
	// 开启SCL和SDA对应GPIO的时钟
	RCC_APB2PeriphClockCmd(SCL_GPIO_Port_CLK, ENABLE);
	RCC_APB2PeriphClockCmd(SDA_GPIO_Port_CLK, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = SCL_Pin;
 	GPIO_Init(SCL_GPIO_Port, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = SDA_Pin;
 	GPIO_Init(SDA_GPIO_Port, &GPIO_InitStructure);
	
	GPIO_SetBits(SCL_GPIO_Port, SCL_Pin);
	GPIO_SetBits(SDA_GPIO_Port, SDA_Pin);
}

/**
  * @brief  控制SCL线的下拉与释放
  * @param  BitValue	其值可以是0或1,0为下拉,1为释放
  * @retval 
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(SCL_GPIO_Port, SCL_Pin, (BitAction)BitValue);
	// Delay_us(10);
}

/**
  * @brief  控制SDA线的下拉与释放
  * @param  BitValue	其值可以是0或1,0为下拉,1为释放
  * @retval 无
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(SDA_GPIO_Port, SDA_Pin, (BitAction)BitValue);
	// Delay_us(10);
}

/**
  * @brief  读取SDA
  * @param  无
  * @retval 读取到SDA的高低电平值
  */
uint8_t MyI2C_R_SDA(void)
{
	return GPIO_ReadInputDataBit(SDA_GPIO_Port, SDA_Pin);
	// Delay_us(10);
}

/**
  * @brief  软件I2C的起始信号
  * @param  无
  * @retval 无
  */
void MyI2C_Start(void)
{
	// 这里先释放SDA,再释放SCL的原因是为了使Start信号兼容重复起始信号RS
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}

/**
  * @brief  软件I2C的结束信号
  * @param  无
  * @retval 无
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

/**
  * @brief  发送一个字节
  * @param  Byte	发送的字节数据
  * @retval 无
  */
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));
		// SCL产生一个正脉冲,让从机读取数据
		MyI2C_W_SCL(1);
		MyI2C_W_SCL(0);
	}
}

/**
  * @brief  接收一个字节
  * @param  无
  * @retval 接收的字节数据
  */
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	MyI2C_W_SDA(1);		// 主机释放SDA,交出SDA控制权
	for (i = 0; i < 8; i ++)
	{
		// 在SCL高电平期间读取SDA,如果SDA为1,则将Byte对应位置1(高位先行)
		MyI2C_W_SCL(1);
		if (MyI2C_R_SDA() == 1)
		{
			Byte |= (0x80 >> i);
		}
		MyI2C_W_SCL(0);
	}
	return Byte;
}

/**
  * @brief  发送应答,以通知从机数据是是否发送结束
  * @param  AckBit		0为发送未结束,1为发送已结束
  * @retval 无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);
	// SCL产生一个正脉冲,让从机读取数据
	MyI2C_W_SCL(1);
	MyI2C_W_SCL(0);
}

/**
  * @brief  接受应答,主机读取该信号以确认从机是否接受到数据
  * @param  无
  * @retval 应答信号,0为已收到(从机受到后下拉SDA),1为未收到
  */
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);		// 主机释放SDA,交出SDA控制权
	// 在SCL高电平期间读取SDA,如果SDA为1,则将AckBit置1
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	MyI2C_W_SCL(0);
	
	return AckBit;
}

// I2C地址应答测试程序,在main函数中可以循环以下过程来遍历I2C地址,以检查从机是否能正常应答
	uint8_t ACK;
	MyI2C_Start();
	MyI2C_SendByte(0xD0);	// 0xD0为MPU6050的I2C地址和读写操作复合而成的地址
	ACK = MyI2C_ReceiveAck();
	MyI2C_Stop();

3.1.2 MPU6050设备操作层

  • MPU6050.h
#ifndef __MPU6050_H_
#define __MPU6050_H_

void MPU6050_Init(void);

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
uint8_t MPU6050_ReadID(void);
void MPU6050_ReadData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
					  int16_t *GyroX, int16_t *GyroY, int16_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	//电源管理1
#define	MPU6050_PWR_MGMT_2		0x6C	//电源管理2
#define	MPU6050_WHO_AM_I		0x75	//ID寄存器(默认数值0x68,只读)

#endif

  • MPU6050.c
#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS_W			0xD0
#define MPU6050_ADDRESS_R			0xD1

/**
  * @brief  MPU6050 向芯片内部的指定地址写
  * @param  RegAddress	芯片内部的寄存器地址
  * @param  Data		要写入的数据
  * @retval 无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS_W);
	MyI2C_ReceiveAck();		// 这里可以对获取到的回应信号进行处理
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(Data);
	MyI2C_ReceiveAck();
	MyI2C_Stop();
}

/**
  * @brief MPU6050 向芯片内部的指定地址读
  * @param  RegAddress	要读取的寄存器在芯片内部的地址
  * @retval 读取的数据
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	// 向MPU6050发送将要读的地址
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS_W);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	
	MyI2C_Start();		// 重复启动信号
	MyI2C_SendByte(MPU6050_ADDRESS_R);		// 发送读地址,让出SDA控制权
	MyI2C_ReceiveAck();
	Data = MyI2C_ReceiveByte();
	MyI2C_SendAck(1);	// 向从机发送应答信号(不响应),从机终止数据发送
	MyI2C_Stop();
	
	return Data;
}

/**
  * @brief  MPU6050初始化函数
  * @param  无
  * @retval 无
  */
void MPU6050_Init(void)
{
	MyI2C_Init();
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);		// 不复位,关闭睡眠模式,不循环,使能温度传感器,选择X轴陀螺仪的内部震荡电路作为系统时钟
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);		// 不需要设置循环模式的唤醒频率, 六个轴都不需要待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);		// 设置采样分频, 这里选择10分频
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);			// 不需要外部同步, 数字低通滤波设置为最高(最平滑)
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);	// 角速度计配置:不自测(高三位为自测使能, 手册有遗漏), 设计为最大量程
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);	// 加速度计配置:不自测,选择为最大量程,不使用高通滤波器
}

/**
  * @brief  获取MPU6050的ID值,可根据ID值检查STM32和MPU6050之间是否正常通信
  * @param  无
  * @retval ID值
  */
uint8_t MPU6050_ReadID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

/**
  * @brief  获取并返回MPU6050六轴传感器的返回值
  * @param  无
  * @retval 通过参数指针操作(返回)6个返回值
  */
void MPU6050_ReadData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
					  int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	*AccX = (MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H) << 8)
			| MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	
	*AccY = (MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H) << 8)
			| MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	
	*AccZ = (MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H) << 8)
			| MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	
	*GyroX = (MPU6050_ReadReg(MPU6050_GYRO_XOUT_H) << 8)
			| MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	
	*GyroY = (MPU6050_ReadReg(MPU6050_GYRO_YOUT_H) << 8)
			| MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	
	*GyroZ = (MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H) << 8)
			| MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
}

3.1.3 主函数逻辑层

  • main.c
#include "stm32f10x.h"                  // Device header
#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:0x");
	ID = MPU6050_ReadID();
	OLED_ShowHexNum(1, 6, ID, 2);
	while (1)
	{
		MPU6050_ReadData(&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);
	}
}

你可能感兴趣的:(Stm32笔记,stm32,笔记,嵌入式硬件)