(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)

目录

(1)功能

(2)软件I2C与硬件I2C的优缺点

(3)I2C的功能框图

(4)发送数据 & 接收数据的流程

(5)I2C基本结构

(6)主机发送 & 主机接收序列图

(7) 使用STM32的I2C外设实现I2C通信

(8)代码实现


(1)功能

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

外设:

控制寄存器CR、数据寄存器DR、状态寄存器SR

(2)软件I2C与硬件I2C的优缺点

软件I2C的优缺点:

优点:资源没有限制,只要代码存得下,开多少个I2C总线都可以,且引脚没有任意指定。

缺点:占用CPU资源,效率不高。

硬件I2C的优缺点:

优点:硬件电路实现I2C通信,效率更高,更专注,节省CPU资源。

缺点:硬件I2C的资源有限,只有I2C1和I2C2两个硬件资源,引脚是固定的。

(3)I2C的功能框图

(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)_第1张图片

像这种外设模块引出来的引脚,一般都是借助GPIO口的复用模式与外部世界相连的,

具体复用在哪个GPIO口,可以查看“引脚定义表”。I2C2两个引脚复用在了PB10和PB11这两个端口,如下图1-2所示。I2C1两个引脚复用在PB6和PB7,而且还可以重映射到PB8和PB9两个引脚上,如下图1-3所示。

(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)_第2张图片

                                                             图1-2

(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)_第3张图片

                                                             图1-3

所以,硬件I2C的引脚就是固定的,不能任意更改,不像软件I2C那样可以随意。

------------------------------------------

(4)发送数据 & 接收数据的流程

SDA,数据控制部分

(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)_第4张图片

                                                                       图1-4

发送数据的流程:

数据的收发的核心部分是数据寄存器(DATA REGISTER)数据移位寄存器

当我们需要发送数据时,可以把一个字节数据写到数据寄存器DR,当移位寄存器可以数据移位时,数据寄存器的值就会转到移位寄存器里,在移位的过程中,下一个数据就会被放到数据寄存器中等着了,一旦前一个数据移位成功,下一个数据就可以无缝衔接,继续发送。

当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的TXE位为1,表示发送寄存器为空,这是发送数据的流程。

接收数据的流程:

输入的数据,一位一位地从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位RXNE,表示数据寄存器非空,此时就可以把数据从数据寄存器里读出来。

流程如图所示:

(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)_第5张图片

                                                                       图1-5

(5)I2C基本结构

(这是简化的结构,只用上部分的寄存器)

(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)_第6张图片

发送数据时:

移位寄存器数据寄存器通信的核心部分,由于I2C是高位先行,所以这个移位寄存器是向左移位的。在发送的时候,最高位先移出去,然后是次高位,以此类推。一位SCL时钟移位一次,移位8次,由高位到低位,依次放到SDA线上。

接收数据时:

数据通过GPIO口,从右往左的方向依次移入移位寄存器,总共移8次,一个字节就接收完成了。在使用硬件I2C的时候,这两个GPIO口都要配置成复用开漏输出的模式。复用就是GPIO的状态交由片上外设来控制;开漏输出就是I2C协议要求的端口配置。

(6)主机发送 & 主机接收序列图

主机发送:

(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)_第7张图片

起始条件-(S),检测起始条件已经发送时-(EV5),就可以发送一个字节的从机地址-(地址),从机地址需要写到数据寄存器DR中,写入DR之后,硬件电路就会自动把这个字节转到移位寄存器里,再把这个字节发送到I2C总线上,之后硬件会自动接收应答并判断(A)。如果没有应答,硬件就会置应答失败的标志位,这个标志位可以申请中断来提醒我们。

当寻址完成之后,会发生EV6事件,标志位ADDR=1,在主模式状态下代表发送结束。-(EV6)               

EV6事件结束后,接下来是EV8_1事件,EV8_1事件就是TxE标志位=1,移位寄存器和数据寄存器都为空,需要写入数据寄存器DR进行数据发送。一旦写入DR寄存器之后,由于移位寄存器也是空,所以DR会立刻转到移位寄存器进行发送。

接下来就是EV8事件,移位寄存器非空,数据寄存器空,此时正是移位寄存器正在发送数据的状态,因此数据1的时序就产生了。

在EV8事件结束之前,数据2就写入到数据寄存器等候着了。接收应答(A)之后,数据2就转入移位寄存器进行发送,EV8事件消失,以此类推。

最后,当我们不需要继续发送数据的时候,就可以通过EV8_2事件和置TxE=1 标志位结束。

BTF(Byte Transfer Finished),字节发送结束标志位。

主机接收:

(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)_第8张图片

(STM32学习笔记)I2C通信协议-外设实现I2C通信(二)_第9张图片

7位主接收时序流程:

起始,从机地址+读,接收应答,接收数据,发送应答,接收数据,发送应答…,非应答,终止。

S:产生起始条件,然后等待EV5事件(代表起始条件已发送),之后是寻址,接收应答,结束后产生EV6事件(代表寻址已完成),

数据1:表示数据正在通过移位寄存器进行输入,EV6_1:数据正在移入移位寄存器,数据还没收到,所以这个事件没有标志位,最后硬件会自动根据配置,把应答位发送出去。如果应答位写1,表示在接收到一个字节后就返回一个应答;如果应答位写0,表示无应答返回。

经过这波操作,移位寄存器已经成功移入一个字节的数据1了,此时移入的一个字节就整体转移到数据寄存器,同时置RxNE标志位,表示数据寄存器非空,也就是收到数据1了,当前状态就是EV7事件。

在EV7事件消失之前,数据2就可以直接移入移位寄存器等候了,然后就是数据1被读取了。之后是收到数据2,产生EV7事件,读走数据2,EV7事件消失,下一个数据正在移入移位寄存器等候。按照以上流程就可以一直接收数据了。

最后,如果不需要继续接收时,在最后一个时序单元发生时,提前把应答位控制寄存器ACK置0,并设置终止条件请求,也就是EV7_1事件(ACK=0 + STOP请求)

ps : 主机发送 --> 数据寄存器 --> 移位寄存器 --> 从机接收

         主机接收 <-- 数据寄存器 <-- 移位寄存器 <-- 从机发送

相关介绍可以查看STM32F10xx参考手册(中文).pdf

(7) 使用STM32的I2C外设实现I2C通信

       常用库函数:

// I2C外设缺省配置
void I2C_DeInit(I2C_TypeDef* I2Cx);

// I2C外设初始化函数
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);

// 初始化结构体的缺省初始化
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);

// I2C外设的开关控制函数
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);

// 主机发送数据
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
// 主机接收数据
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
// 7位地址模式发送函数(该函数功能也可由数据发送函数实现)
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);

  下面是和状态检测相关的库函数:

// 获取当前事件是否发生
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
// 获取当前状态寄存器的值(两个16位寄存器拼接而成的数据)
uint32_t I2C_GetLastEvent(I2C_TypeDef* I2Cx);

// 获取当前的状态标志位
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);

(8)代码实现

软件和硬件I2C通信在本次实验中实验数据和现象完全相同,仅在通信层有区别。在算法实现时,MPU6050.c模块不在需要继承软件通信协议MyI2C.h,所以在工程文件中可以直接删除MyI2C.cMyI2C.h文件。
新的MPU6050.c的代码如下所示:

  1.   ​​​​ MPU6050.c
#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS			0xD0	// 0x68 + 读写位,注意这里的定义和软件模拟I2C有些许区别

// 用宏定义在一定程度上实现解耦
// 重定义GPIO端口,需要注意使用的GPIO如果不是APB2的外设需要更改MPU6050_Init函数
#define GPIO_Periph_CLK			RCC_APB2Periph_GPIOB
#define GPIO_Periph				GPIOB
#define GPIO_Pin_SCL			GPIO_Pin_6
#define GPIO_Pin_SDA			GPIO_Pin_7
// 重定义I2C外设端口,这里同样需要检查使用的I2C外设是否是APB1的外设
#define I2C_Periph_CLK			RCC_APB1Periph_I2C1
#define I2C_Periph				I2C1

/**
  * @brief  I2C硬件读写的事件等待发生函数,即等待某事件发生
  * @param  I2Cx		操作的I2Cx外设
  * @param  I2C_EVENT	要等待的事件,该参数的可取值在stm32f10x_i2c.c文件中
  * @retval 无
  */
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;
		}
	}
}

/**
  * @brief  MPU6050 向芯片内部的指定地址写
  * @param  RegAddress	芯片内部的寄存器地址
  * @param  Data		要写入的数据
  * @retval 无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	I2C_GenerateSTART(I2C_Periph, ENABLE);
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_MODE_SELECT);		// EV5, 等待起始条件已发送,事件发生(主模式已选择)
	
	I2C_Send7bitAddress(I2C_Periph, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	/* 在库函数中,发送函数都自带接收应答的过程,接收函数都自带发送应答的过程 */
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	// EV6, I2C地址和写命令已发送
	
	I2C_SendData(I2C_Periph, RegAddress);		// 发送寄存器地址
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_BYTE_TRANSMITTING);	// EV8, 数据正在发送(DR非空)
	I2C_SendData(I2C_Periph, Data);			// 发送寄存器数据
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_BYTE_TRANSMITTED);		// EV8_2, 数据发送已完成
	
	I2C_GenerateSTOP(I2C_Periph, ENABLE);
}

/**
  * @brief MPU6050 向芯片内部的指定地址读
  * @param  RegAddress	要读取的寄存器在芯片内部的地址
  * @retval 读取的数据
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C_Periph, ENABLE);
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_MODE_SELECT);	// EV5, 起始条件已发送(主模式已选择)
	
	I2C_Send7bitAddress(I2C_Periph, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	// EV6, I2C地址和写命令已发送
	
	I2C_SendData(I2C_Periph, RegAddress);
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_BYTE_TRANSMITTED);		// EV8_2, 等待数据(寄存器地址)发送已完成
	
	I2C_GenerateSTART(I2C_Periph, ENABLE);		// 重复起始条件
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_MODE_SELECT);	// EV5, 起始条件已发送(主模式已选择)
	
	I2C_Send7bitAddress(I2C_Periph, MPU6050_ADDRESS, I2C_Direction_Receiver);
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);	// EV6, I2C地址和读命令已发送
	
	// 如果要读取的数据使最后一个数据, 则在执行读命令之前, 就将ACK置0(不响应), STOP置1(结束条件)
	I2C_AcknowledgeConfig(I2C_Periph, DISABLE);
	I2C_GenerateSTOP(I2C_Periph, ENABLE);
	
	MPU6050_WaitEvent(I2C_Periph, I2C_EVENT_MASTER_BYTE_RECEIVED);	// EV7, RxNE = 1, 已收到一个字节
	Data = I2C_ReceiveData(I2C_Periph);	// 取走DR的值, 并存放在Data变量中

	return Data;
}

/**
  * @brief  MPU6050初始化函数
  * @param  无
  * @retval 无
  */
void MPU6050_Init(void)
{
	RCC_APB1PeriphClockCmd(I2C_Periph_CLK, ENABLE);
	RCC_APB2PeriphClockCmd(GPIO_Periph_CLK, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;		// 复用开漏模式, 将GPIO端口的控制权交给片上外设
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_SCL | GPIO_Pin_SDA;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIO_Periph, &GPIO_InitStructure);
	
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;	
	I2C_InitStructure.I2C_ClockSpeed = 100000;			// 标准模式,时钟频率为100kHz(该参数最大不能超过400kHz)
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;	// 快速模式下的时钟占空比, 在标准模式下该参数没有作用
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;			// 主机接收一个数据后是否响应从机, 该参数也可以由独立的函数进行配置
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;		// 地址的位数
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;			// STM32从模式下的自身地址, 主模式下没有作用
	I2C_Init(I2C_Periph, &I2C_InitStructure);
	
	I2C_Cmd(I2C_Periph, ENABLE);
	
	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);
}

本节知识点:

(1)介绍了硬件I2C的功能,以及软件I2C和硬件I2C的优缺点。

(2)硬件I2C实现通信,采用I2C1或I2C2两个硬件资源,且两个资源的引脚都是固定的。

(3)硬件I2C实现通信的核心部分是数据寄存器和移位寄存器。

当主机发送数据的时候,数据从数据寄存器转到移位寄存器,移位寄存器再将数据发送出去,在移位寄存器发送数据的时候,下一个数据就已经在数据寄存器里排队准备就绪了。在整个发送数据的时序流程中,会产生事件,用于判断数据的发送/接收情况。比如EV5表示检测起始条件是否发送,检测已发送,就可以进行下一步的寻址操作,然后是应答位,如果没有应答,就会置应答失败的标志位,并且会申请中断来提示我们。寻址完成之后,就会产生EV6事件,在主模式状态下代表发送结束。EV6结束之后,接下来会产生EV8_1事件,在EV8_1事件时,数据寄存器和移位寄存器都是空,此时会有数据写入到数据寄存器,由于移位寄存器是空的,所以数据寄存器会将数据转到移位寄存器中,等到EV8_1结束了,数据寄存器是空的,移位寄存器非空,然后来到了EV8事件,此时移位寄存器正在发送数据,下一个数据也来到数据寄存器中排队了,接收应答(A)之后,数据2就转入移位寄存器进行发送,EV8事件消失,以此类推。

最后,当我们不需要继续发送数据的时候,就可以通过EV8_2事件和置TxE=1 标志位结束。

当主机接收数据的时候,移位寄存器接收到数据,再将数据转到数据寄存器。

核心知识点:

(1)硬件资源是 I2C1、I2C2,引脚是固定的;

(2)I2C硬件通信的核心部分:数据寄存器、移位寄存器。

学习心得:理解I2C硬件通信的流程,不用死记硬背,但懂得根据时序图去写代码,在项目中实践,夯实理论知识并熟悉应用。

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