目录
8.[扩展]为什么STM32硬件I2C为什么在速度快时会调节占空比;为什么有最大速度限制
I2C通信介绍部分可以看这一篇博客:STM32软件I2C通信详解-CSDN博客
STM32软件读取MPU6050可以看这一篇博客:STM32通过I2C硬件读写MPU6050-CSDN博客
STM32通过I2C硬件读写MPU6050
1. STM32的I2C外设简介
硬件I2C具有高速传输、低占用率和稳定性高的优点,适用于对传输速度和稳定性要求较高的场景; 而软件I2C具有灵活性高和可移植性强的特点,适用于没有硬件I2C支持或需要扩展硬件I2C功能的场景
- STM32内部集成了硬件12C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
- 支持多主机模型
- 支持7位/10位地址模式
- 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
- 支持DMA
- 兼容SMBus协议(System Management Bus 系统管理总线) 基于I2C改进而来的,主要用于电源管理系统
- STM32F103C8T6 硬件I2C资源:12C1、12C2
2. STM32的I2C基本框图
3. STIM32硬件I2C主机发送流程
10位地址与7位地址的区别
- 7位地址
- 起始条件后的第一个字节是寻址(地址+读写位)
- 10位地址
- 起始条件后的两个字节都是寻址(11110+2位地址+读写位+8位地址)
7位主机发送的时序流程
- 起始、从机地址+读写位、应答、数据1、应答、数据2、应答、….数据N、应答、停止
-
起始 STM32默认为从模式,所以需要下在START寄存器下写1就产生起始条件,由硬件自动清除。之后STM32由从模式转为主模式
-
发生EV5事件 可以把他当做大标志位。因为有的状态会产生多个标志位。所以EVx事件就是组合了很多标志位的一个大标志位 EV5事件,就是SB标志为1 。SB是状态寄存器的一个位。表示了硬件的状态(在状态寄存器SR1中可以找到这一位,置1代表起始条件已发送。由硬件自动清除)
-
发送一个字节 需要写入DR寄存器。硬件电路会自动把DR寄存器的值转入移位寄存器。再把这一个字节发送到I2C总线上。
-
应答 电路会自动判断。如果没有应答,则会置应答失败标志位。这个标志位可以申请中断。在寻址完成之后
-
会发生EV6事件,也就是ADDR为1。手册中查看到的意思是: 在主模式下,代表地址发送结束
-
发生EV8_1事件 此时 TXE = 1,移位寄存器为空、数据寄存器为空 这时需要我们写入数据寄存器DR进行数据发送了。
-
发生EV8事件 DR在填写数据之后,会立刻把数据转移到移位寄存器进行发送。 (也就是填写了数据1) 这时就是EV8事件:移位寄存器非空、数据寄存器空。
-
此时再写入DR 把数据放到数据寄存器中。同时因写入DR,导致EV8事件清除。 (也就是说已经写入了下一个数据:数据2)(在没有应答之前写入)
-
接收应答位 当从机接收到应答位之后,数据2就转入移位寄存器进行发送。
-
发送后又产生了EV8事件 移位寄存器非空、数据寄存器空。
-
此时再写入DR……等待应答….转入移位寄存器发送…产生EV8事件….写入…. (在没有应答之前写入)
-
直到数据发完,触发EV8_2 当移位寄存器空、数据寄存器也空时。就会触发EV8_2 EV8_2 是 EXT = 1 (数据寄存器为空 )、 BTF = 1(字节发送结束标志位 )(也就是移位寄存器移位完成后找数据寄存器要下一个数据时发现数据寄存器没数据时的标志位)
-
停止 (控制寄存器CR1中) STOP、这一位写1的时候,就会在当前字节或在起始条件产生停止条件
7位主机接收的时序流程
7位地址主机接收流程:
- 起始、从机地址+读、接收、接收数据、发送应答、接收数据、发送应答…接收数据、非应答、停止
10位地址主机接收流程
- 起始、发送帧头(11110 + 两位地址 + 0(写))、发送第二个字节的8位地址、重复起始条件、发送枕头(11110 + 两位地址 + 1(写))、直接接收数据、发送应答、接收…发送…接收、非应答、停止(没有发送第二次的字节)
7位详解:
- 写入控制位的Start位、产生起始条件
- 等待EV5事件 表示已发送
- 发送地址(地址+读)
- 接收应答(ACK,写1 在接受一个字节后就返回一个应答)
- 产生EV6事件, 代表寻址已完成
- 产生EV6_1时间, 没有对应的标志事件。
- 接收数据1 代表数据正在通过移位寄存器输入
- 发送应答 硬件自动执行,需要配置是否应答。CR1寄存器中,ACK写1 应答, 否则不应答
- 产生EV7事件 因为此时就代表已经接收到数据了。 数据会通过移位寄存器转移到数据寄存器。 产生RxNE标志位,表示数据寄存器非空 (读DR寄存器来清除这个Ev7事件)
- 接收数据2 在我们正在读DR寄存器是 数据2就已经移位到移位寄存器中。等待移位到DR数据寄存器中。
- 发送应答
- 这事数据又被传入DR数据寄存器。EV7事件又来了。需要我们读取、
- 如此循环……直到停止 也就是需要设置ACK = 0 不应答, 和STOP请求。
其实同上,这里是在发送一个字节之后,在对面没有应答前,就填充好了下一次要发送的数据。产生的标志位也是一样的。
4. STM32硬件与软件的波形对比
5. STM32配置硬件I2C外设流程
- 开启外设和对应GPIO口的时钟
- 把I2C外设对应的GPIO口配置为复用开漏模式
- 使用结构体,对I2C进行配置
- 使能I2C
6. STM32的I2C.h标准库函数介绍
1.I2C配置和使用函数
void I2C_DeInit(I2C_TypeDef* I2Cx);
- 恢复缺省配置
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
- 初始化I2C
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
- 初始化结构体
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 使能I2C
void I2C_DMACmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
- I2C MDA使能
void I2C_DMALastTransferCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 用于控制 I2C 传输中的最后一次传输的相关操作。
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 调用,生成起始条件
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 调用,生成终止条件
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 配置 I2C 的应答机制。1为应答
void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);
- 设置 I2C 设备自身的地址。
void I2C_DualAddressCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 启用或禁用双地址模式,以适应特定的通信需求。
void I2C_GeneralCallCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 控制是否响应通用呼叫。
void I2C_ITConfig(I2C_TypeDef* I2Cx, uint16_t I2C_IT, FunctionalState NewState);
- 配置 I2C 的中断功能,根据不同的中断类型进行使能或禁用。
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
- 发送一个字节的数据。
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
- 接收一个字节的数据。
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
- 发送 7 位地址并指定通信方向(读或写)。
uint16_t I2C_ReadRegister(I2C_TypeDef* I2Cx, uint8_t I2C_Register);
- 读取 I2C 设备的特定寄存器的值。
void I2C_SoftwareResetCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 通过软件方式对 I2C 进行复位操作。
void I2C_NACKPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_NACKPosition);
- 配置非应答信号(NACK)的位置。
void I2C_SMBusAlertConfig(I2C_TypeDef* I2Cx, uint16_t I2C_SMBusAlert);
- 针对 SMBus 警报进行相关配置。
void I2C_TransmitPEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 控制是否传输 PEC(Packet Error Checking,数据包错误检查)信息。
void I2C_PECPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_PECPosition);
- 配置 PEC 的位置。
void I2C_CalculatePEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 决定是否计算 PEC 值。
uint8_t I2C_GetPEC(I2C_TypeDef* I2Cx);
- 获取计算得到的 PEC 值。
void I2C_ARPCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 控制自动重传功能的启用或禁用。
void I2C_StretchClockCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
- 配置时钟拉伸功能。
void I2C_FastModeDutyCycleConfig(I2C_TypeDef* I2Cx, uint16_t I2C_DutyCycle);
- 设置快速模式下的占空比,以优化通信性能。
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);
- 清除中断标志位
2.I2C状态监控功能
I2C的标志位,往往是由多个标志位组合起来的一个大标志位。
一个个去判断往往比较麻烦。
所以I2C的库在.h头文件的最后,
为我们提供了两种检测方案
- 基础状态监测
- 使用
I2C_CheckEvent()
函数 可以同时检测多个标志位来进行比较。 一般使用这个。
- 使用
- 高级状态监测
- 使用
I2C_GetLastEvent()
函数 (实际上是把SR1和SR2两个状态寄存器拼接成一个16位的数据扔给你,随便你处理。所以一般不用)
- 使用
- 基于标志位的状态监控
- 使用
I2C_GetFlagStatus()
函数 可以判断某一个标志位是否置一
- 使用
7. STM32编写代码实现I2C硬件读写MPU6050
7.1编写时需要注意的点
-
发送字节时
- 对于发送一个字节,发送地址和读写的一个字节之后,在移位寄存器还未发送完成时,就要填充下一次要发送的字节到DR数据寄存器中。否则将发生EV8_2标志(移位寄存器空,数据寄存器空)。导致读写停止。 正常应为EV8事件发生(字节正在发送,移位寄存器非空,数据寄存器空)
- 对于发送多个字节,只需要重复发送、等待EV8事件即可。不发送时给予不应答、停止即可。
-
接收字节时,
- 对于接收一个字节,在指定完地址,重复起始之后,接收第第一个字节中时,就要提前配置Ack非应答和停止条件。之后等数据全部移位到数据寄存器中,才能保证只接收了一个字节。读取DR寄存器即可。
- 对于接收多个字节,可以重复应答、然后等待EV7事件到来,读取寄存器。直到不想接收时,在数据进行移位时(还未到达DR寄存器时)提前设置非应答与停止操作。
7.2程序文件简要说明:
- MPU6050.c:初始化MPU6050的寄存器。使用STM32 自带的I2C外设,编写对指定地址发送指定值、读取指定地址的函数。以及等待标志位且超时退出的函数
- MPU6050.h:函数声明、数据结构体声明
- main.c:测试I2C通信结果。
MPU650.c
#include "stm32f10x.h" // Device header
#include "MPU6050.h"
#include "MPU6050_Reg.h"
#define MPU6050ADDRESS 0xD0
/**
* 函 数:超时退出、检测标志位
* 参 数:I2C_TypeDef* I2Cx, 定时器x
uint32_t I2C_EVENT 标志位名称
* 返 回 值:无
* 注意事项:无
*/
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)//超时退出、检测标志位
{
uint32_t Timeout;
Timeout = 10000;
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
{
Timeout --;
if (Timeout == 0)
{
break;
}
}
}
/**
* 函 数:指定地址写一个字节
* 参 数:RegAddress 指定的寄存器地址
Data 指定写入的数据
* 返 回 值:无
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2,ENABLE);//非阻塞。所以要等待事件发送完成。
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件发生
I2C_Send7bitAddress(I2C2,MPU6050ADDRESS,I2C_Direction_Transmitter);//发送地址和读写操作(也能用发送一个字节来完成)(自动应答)
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//等待发送EV6事件发生
I2C_SendData(I2C2,RegAddress);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);//等待发送EV8事件发生(字节正在发送)
I2C_SendData(I2C2,Data);//直接写入下一个要发的数据
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED );//等待EV8_2事件发生(发送完成,并且数据寄存器无)
I2C_GenerateSTOP(I2C2,ENABLE);
}
/**
* 函 数:指定地址读一个字节
* 参 数:RegAddress 指定要读的寄存器地址
* 返 回 值:无
*/
uint8_t MPU6050_ReadeReg(uint8_t RegAddress)
{
uint8_t Data = 0x00;
I2C_GenerateSTART(I2C2,ENABLE);//起始位
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件发生
I2C_Send7bitAddress(I2C2,MPU6050ADDRESS,I2C_Direction_Transmitter);//发送地址和读写操作(也能用发送一个字节来完成)(自动应答)
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//等待发送EV6事件发生
I2C_SendData(I2C2,RegAddress);
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);//等待发送EVEV8_2事件发生(字节发送完毕)
I2C_GenerateSTART(I2C2,ENABLE);//重复起始
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件发生
I2C_Send7bitAddress(I2C2,MPU6050ADDRESS,I2C_Direction_Receiver);//发送地址和读写操作(也能用发送一个字节来完成)(自动应答)
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);//等待发送EV6事件发生
I2C_AcknowledgeConfig(I2C2,DISABLE);//在字节来之前,设置非应答。
I2C_GenerateSTOP(I2C2,ENABLE);//直接停止。但是会接收字节完毕之后才停
MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);//等待EV7事件到达。代表一个数据的字节已经在DR里了
Data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2,ENABLE);//恢复默认状态,给从机应答
return Data;//返回读取到的值
}
/**
* 函 数:初始化MPU6050
* 参 数:无
* 返 回 值:无
*/
void MPU6050_Init(void)
{
/*初始化I2C*/
// MyI2C_Init();
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//开启GPIO和I2C外设时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化PB10、11为复用开漏
/*初始化I2C外设*/
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//确定是否应答
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//指定STM32作为从机。可以响应几位的地址
I2C_InitStructure.I2C_ClockSpeed = 50000;//最大400KHZ(快速)、标准为(100KHZ)(MPU6050最快也是400KHZ)
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//配置占空比。进入快速模式后才有用(>100KHZ后) (默认为1:1)
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;//I2C模式
I2C_InitStructure.I2C_OwnAddress1 = 0x00;//指定STM32作为从机。STM本身的地址
I2C_Init(I2C2,&I2C_InitStructure);
/*使能I2C外设*/
I2C_Cmd(I2C2,ENABLE);
/*初始化MPU6050*/
MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); //电源管理1:不复位、解除睡眠、不循环、温度传感器不失能、选择X轴陀螺仪时钟
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); //电源管理2:不需要循环模式唤醒频率、每个轴都不需要待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09); //采样率分频:数据输出的快慢,越小输出越快.这里给10分频
MPU6050_WriteReg(MPU6050_CONFIG,0x06); //配置寄存器:外部同步不需要、数字低通滤波器设置为110
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18); //陀螺仪配置寄存器:不自测、满量程选择:11最大量程
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18); //加速度计配置寄存器:不自测、满量程选择:11最大量程、不用高通滤波器
}
/**
* 函 数:得到六轴传感器中的数据
* 参 数:Str MPU6050_Data的地址
* 返 回 值:无
*/
SensorData MPU6050_Data;
void MPU6050_GetData(SensorData *Str)//存放这种结构体类型的地址
{
Str->AccX = ( MPU6050_ReadeReg(MPU6050_ACCEL_XOUT_H) <<8|
MPU6050_ReadeReg(MPU6050_ACCEL_XOUT_L));
Str->AccY = ( MPU6050_ReadeReg(MPU6050_ACCEL_YOUT_H) <<8|
MPU6050_ReadeReg(MPU6050_ACCEL_YOUT_L));
Str->AccZ = ( MPU6050_ReadeReg(MPU6050_ACCEL_ZOUT_H) <<8|
MPU6050_ReadeReg(MPU6050_ACCEL_ZOUT_L));
Str->Temp = ( MPU6050_ReadeReg(MPU6050_TEMP_OUT_H) <<8|
MPU6050_ReadeReg(MPU6050_TEMP_OUT_L));
Str->GyroX = ( MPU6050_ReadeReg(MPU6050_GYRO_XOUT_H) <<8 |
MPU6050_ReadeReg(MPU6050_GYRO_XOUT_L));
Str->GyroY = ( MPU6050_ReadeReg(MPU6050_GYRO_YOUT_H) <<8 |
MPU6050_ReadeReg(MPU6050_GYRO_YOUT_L));
Str->GyroZ = ( MPU6050_ReadeReg(MPU6050_GYRO_ZOUT_H) <<8 |
MPU6050_ReadeReg(MPU6050_GYRO_ZOUT_L));
}
//MPU6050_ACCEL_XOUT_H 0x3B
//MPU6050_ACCEL_XOUT_L 0x3C
//MPU6050_ACCEL_YOUT_H 0x3D
//MPU6050_ACCEL_YOUT_L 0x3E
//MPU6050_ACCEL_ZOUT_H 0x3F
//MPU6050_ACCEL_ZOUT_L 0x40
//MPU6050_TEMP_OUT_H 0x41
//MPU6050_TEMP_OUT_L 0x42
//MPU6050_GYRO_XOUT_H 0x43
//MPU6050_GYRO_XOUT_L 0x44
//MPU6050_GYRO_YOUT_H 0x45
//MPU6050_GYRO_YOUT_L 0x46
//MPU6050_GYRO_ZOUT_H 0x47
//MPU6050_GYRO_ZOUT_L 0x48
MPU650.h
#ifndef __MPU6050_H
#define __MPU6050_H
/**
* 函 数:初始化MPU6050
* 参 数:无
* 返 回 值:无
*/
void MPU6050_Init(void);
/**
* 函 数:指定地址写一个字节
* 参 数:RegAddress 指定的寄存器地址
Data 指定写入的数据
* 返 回 值:无
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
/**
* 函 数:指定地址读一个字节
* 参 数:RegAddress 指定要读的寄存器地址
* 返 回 值:无
*/
uint8_t MPU6050_ReadeReg(uint8_t RegAddress);
//传感器数据
typedef struct Data
{
int16_t AccX;
int16_t AccY;
int16_t AccZ;
int16_t Temp;
int16_t GyroX;
int16_t GyroY;
int16_t GyroZ;
}SensorData;
extern SensorData MPU6050_Data;
/**
* 函 数:得到六轴传感器中的数据
* 参 数:Str MPU6050_Data的地址
* 返 回 值:无
*/
void MPU6050_GetData(SensorData *Str);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "oled.h"
#include "Delay.h"
#include "key.h"
#include "MPU6050.h"
/*
硬件读写I2C
*/
int main()
{
OLED_Init();//初始化OLED;
MPU6050_Init();//初始化MPU6050
while(1)
{
MPU6050_GetData(&MPU6050_Data);
//显示加速度
OLED_ShowSignedNum(1,1,(int16_t)MPU6050_Data.AccX,5);
OLED_ShowSignedNum(2,1,(int16_t)MPU6050_Data.AccY,5);
OLED_ShowSignedNum(3,1,(int16_t)MPU6050_Data.AccZ,5);
//显示陀螺仪
OLED_ShowSignedNum(1,8,MPU6050_Data.GyroX,5);
OLED_ShowSignedNum(2,8,MPU6050_Data.GyroY,5);
OLED_ShowSignedNum(3,8,MPU6050_Data.GyroZ,5);
//显示温度
OLED_ShowSignedNum(4,4,MPU6050_Data.Temp,5);
}
}
8.[扩展]为什么STM32硬件I2C为什么在速度快时会调节占空比;为什么有最大速度限制
由之前的知识我们可以了解到。
对于I2C而言,在硬件电路上采用了开漏输出+上拉电阻的形式
这就导致了下拉时为强下拉,上拉时为弱上拉。
这就导致了信号在由高电平往低电平时是十分迅速的,而从低电平回到高电平时是需要一定时间的。
在波形上的体现就是:通信速率越快,上升沿呈现出圆弧形越强。而下降沿几乎没有太大影响。
所以,在速度最快时,需要使得低电平的时间更长一些。
在图片中的反馈就是这样子的