目录
(1)功能
(2)软件I2C与硬件I2C的优缺点
(3)I2C的功能框图
(4)发送数据 & 接收数据的流程
(5)I2C基本结构
(6)主机发送 & 主机接收序列图
(7) 使用STM32的I2C外设实现I2C通信
(8)代码实现
外设:
控制寄存器CR、数据寄存器DR、状态寄存器SR
软件I2C的优缺点:
优点:资源没有限制,只要代码存得下,开多少个I2C总线都可以,且引脚没有任意指定。
缺点:占用CPU资源,效率不高。
硬件I2C的优缺点:
优点:硬件电路实现I2C通信,效率更高,更专注,节省CPU资源。
缺点:硬件I2C的资源有限,只有I2C1和I2C2两个硬件资源,引脚是固定的。
像这种外设模块引出来的引脚,一般都是借助GPIO口的复用模式与外部世界相连的,
具体复用在哪个GPIO口,可以查看“引脚定义表”。I2C2两个引脚复用在了PB10和PB11这两个端口,如下图1-2所示。I2C1两个引脚复用在PB6和PB7,而且还可以重映射到PB8和PB9两个引脚上,如下图1-3所示。
图1-2
图1-3
所以,硬件I2C的引脚就是固定的,不能任意更改,不像软件I2C那样可以随意。
------------------------------------------
SDA,数据控制部分
图1-4
发送数据的流程:
数据的收发的核心部分是数据寄存器(DATA REGISTER)和数据移位寄存器。
当我们需要发送数据时,可以把一个字节数据写到数据寄存器DR,当移位寄存器可以数据移位时,数据寄存器的值就会转到移位寄存器里,在移位的过程中,下一个数据就会被放到数据寄存器中等着了,一旦前一个数据移位成功,下一个数据就可以无缝衔接,继续发送。
当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的TXE位为1,表示发送寄存器为空,这是发送数据的流程。
接收数据的流程:
输入的数据,一位一位地从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位RXNE,表示数据寄存器非空,此时就可以把数据从数据寄存器里读出来。
流程如图所示:
图1-5
(这是简化的结构,只用上部分的寄存器)
发送数据时:
移位寄存器和数据寄存器是通信的核心部分,由于I2C是高位先行,所以这个移位寄存器是向左移位的。在发送的时候,最高位先移出去,然后是次高位,以此类推。一位SCL时钟移位一次,移位8次,由高位到低位,依次放到SDA线上。
接收数据时:
数据通过GPIO口,从右往左的方向依次移入移位寄存器,总共移8次,一个字节就接收完成了。在使用硬件I2C的时候,这两个GPIO口都要配置成复用开漏输出的模式。复用就是GPIO的状态交由片上外设来控制;开漏输出就是I2C协议要求的端口配置。
主机发送:
起始条件-(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),字节发送结束标志位。
主机接收:
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
常用库函数:
// 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);
软件和硬件I2C通信在本次实验中实验数据和现象完全相同,仅在通信层有区别。在算法实现时,MPU6050.c
模块不在需要继承软件通信协议MyI2C.h
,所以在工程文件中可以直接删除MyI2C.c
和MyI2C.h
文件。
新的MPU6050.c
的代码如下所示:
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硬件通信的流程,不用死记硬背,但懂得根据时序图去写代码,在项目中实践,夯实理论知识并熟悉应用。