【STM32单片机】#11.5 I2C通信(硬件读写)
主要参考学习资料:
B站@江协科技
STM32入门教程-2023版 细致讲解 中文字幕
开发资料下载链接:https://pan.baidu.com/s/1h_UjuQKDX9IpP-U1Effbsw?pwd=dspb
单片机套装:STM32F103C8T6开发板单片机C6T6核心板 实验板最小系统板套件科协
目录
- I2C外设简介
- I2C框图
- I2C基本结构
- 主机发送
- 主机接收
- 函数详解
- I2C_DeInit函数
- I2C_Init函数
- I2C_InitTypeDef结构体
- I2C_StructInit函数
- I2C_Cmd函数
- I2C_GenerateSTART函数
- I2C_GenerateSTOP函数
- I2C_AcknowledgeConfig函数
- I2C_SendData函数
- I2C_ReceiveData函数
- I2C_Send7bitAddress函数
- I2C_CheckEvent函数
- 中断标志位函数
- 实验25 硬件I2C读写MPU6050
- MPU6050驱动
I2C外设简介
- STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担。
- 支持多主机模型
- 支持7位/10位地址模式
- 支持不同的通讯速度:标准速度(高达100KHz),快速(高达400KHz)
- 支持DMA
- 兼容SMBus协议
- STM32F103C8T6硬件I2C资源:I2C1、I2C2
I2C框图
I2C外设框图左侧SDA、SCL为通信引脚,具体复用的GPIO口见引脚定义表,SMBALERT为SMBus所用引脚。数据控制部分中,收发数据的核心部分为数据寄存器(DR)和数据移位寄存器,流程与串口通信相似。发送数据时,可以将一个字节的数据写入DR,DR在移位寄存器空闲时直接将数据转移进去,并将标志位TXE置一,随后移位寄存器进行数据移位,DR可接收下一个字节的数据;接收数据时,输入数据从引脚移位到移位寄存器,当一个字节的数据移位完成后,数据从移位寄存器转移到DR,并将标志位RXNE置一,此时可以读取数据。起始条件、终止条件和应答位均由控制电路完成。
比较器、自身地址寄存器和双地址寄存器用于STM32作从机时的通信,自身地址寄存器存储STM32的从机地址。当STM32作为从机被寻址时,如果收到的寻址通过比较器判断和自身地址相同,则STM32做出响应。STM32还支持同时响应两个从机地址,因此拥有双地址寄存器。帧错误校验寄存器和帧错误校验计算自动执行CRC校验计算,进行数据有效性验证。本实验不涉及这些功能。下方SCL引脚部分,时钟控制和控制逻辑电路均可视为黑盒,暂时无需掌握原理细节。
I2C基本结构
本实验所需I2C结构简化如下:
主机发送
上图为主机发送流程图,只需关注7位发送。其中EVx为通信的不同阶段触发的事件,库函数中有对应的检查EVx是否发生的函数,可以将其理解为大标志位。
发送时,需通过在控制寄存器I2C_CR1的START位写1使STM32自动产生起始条件。检测到起始条件发送后,即可将从机地址写入DR中由硬件电路自动发送,随后硬件自动接收应答并判断,若无应答,硬件置应答失败标志位,可申请中断。寻址完成后相继触发EV6和EV8_1,此时需要我们写入DR进行数据发送。移位寄存器发送数据时产生EV8事件,EV8事件期间即可继续向DR写入数据并清除事件,因此图中的EV8结束在了下一次数据发送前的任意时刻。如果移位寄存器已经发送完数据,DR仍没有新数据,则表示字节发送结束,并产生EV8_2事件,可以通过在I2C_CR1的STOP位写1自动产生停止条件。
主机接收
接收时,将START位写1产生起始条件,EV5事件可写入地址,EV6事件表示寻址完成,EV6_1在接收数据第一个字节时产生。接收完每个字节的数据之前,需要在I2C_CR1的ACK配置好应答使能,写0无应答,写1应答。EV7在移位寄存器接收完数据转移到DR后产生,此时可读取数据并清除事件。若不需要再接收数据,需要在最后一个时序单元发生提前向ACK写0并设置STOP请求,即EV7_1事件。
函数详解
I2C_DeInit函数
简介:恢复缺省配置。
参数:I2C名称
I2C1/2
I2C_Init函数
简介:初始化I2C。
参数一:I2C名称
参数二:I2C_InitTypeDef结构体指针
I2C_InitTypeDef结构体
成员I2C_Mode:模式
I2C_Mode_I2C(I2C)
I2C_Mode_SMBusDevice(SMBus设备)
I2C_Mode_SMBusHost(SMBus主机)
成员I2C_ClockSpeed:时钟频率,标准速度0~100KHz,快速100~400KHz。
成员I2C_DutyCycle:时钟占空比,标准速度固定1:1,快速可选下列参数(低电平:高电平)。
I2C_DutyCycle_16_9(16:9)
I2C_DutyCycle_2(2:1)
由于I2C数据线在通信过程中为强下拉、弱上拉,因此SDA电平由低到高需要一定的翻转时间,在快速模式中需适当延长SCL低电平时间。
成员I2C_Ack:应答位配置
I2C_Ack_Enable/Disable
成员I2C_AcknowledgedAddress:STM32作为从机响应的地址位数
I2C_AcknowledgedAddress_10bit/7bit
成员I2C_OwnAddress1:STM32作为从机的自身地址
I2C_StructInit函数
简介:初始化I2C_InitTypeDef结构体。
参数:I2C_InitTypeDef结构体指针
I2C_Cmd函数
简介:I2C使能。
参数一:I2C名称
参数二:使能/失能
I2C_GenerateSTART函数
简介:生成起始条件。
参数一:I2C名称
参数二:使能/失能
I2C_GenerateSTOP函数
简介:生成起始条件。
参数一:I2C名称
参数二:使能/失能
I2C_AcknowledgeConfig函数
简介:配置ACK位。
参数一:I2C名称
参数二:使能/失能
I2C_SendData函数
简介:向DR写入数据。
参数一:I2C名称
参数二:数据
I2C_ReceiveData函数
简介:读取DR数据。
参数:I2C名称
I2C_Send7bitAddress函数
简介:发送7位地址。
参数一:I2C名称
参数二:地址
参数三:读写模式
I2C_Direction_Transmitter(写入)
I2C_Direction_Receiver(读取)
I2C_CheckEvent函数
简介:检测事件。
参数一:I2C名称
参数二:事件
返回值
SUCCESS, ERROR
中断标志位函数
I2C_GetFlagStatus函数
I2C_ClearFlag函数
参数一:I2C名称
参数二:I2C标志位
I2C_FLAG_TXE
I2C_FLAG_RXNE
其余暂时不用
I2C_GetITStatus函数
I2C_ClearITPendingBit函数
参数一:I2C名称
参数二:I2C中断源
实验25 硬件I2C读写MPU6050
接线图同实验24,引脚PB10/11对应I2C2。
将实验24MyI2C驱动删除,在MPU6050驱动中使用内置库函数。
MPU6050驱动
头文件不变,源文件将MPU6050中MyI2C函数全部替换为库函数,为方便参考改前代码均留作注释。
MPU6050.c
#include "stm32f10x.h"
#include "MPU6050_Reg.h"#define MPU6050_ADDRESS 0xD0void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
// MyI2C_Start();
// MyI2C_SendByte(MPU6050_ADDRESS);
// MyI2C_ReceiveAck();
// MyI2C_SendByte(RegAddress);
// MyI2C_ReceiveAck();
// MyI2C_SendByte(Data);
// MyI2C_ReceiveAck();
// MyI2C_Stop();I2C_GenerateSTART(I2C2, ENABLE);//I2C函数仅配置寄存器,其执行完毕不代表波形发送完毕,需检测对应事件//等待EV5while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//函数根据传输方向自动调整地址所在字节的最低位I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);//应答位由硬件自动完成,第一次发送地址等待EV6(发送模式)while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);I2C_SendData(I2C2, RegAddress);//中间连续发送等待EV8while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);I2C_SendData(I2C2, Data);//最后一次发送等待EV8_2while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);I2C_GenerateSTOP(I2C2, ENABLE);
}uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t Data;
// MyI2C_Start();
// MyI2C_SendByte(MPU6050_ADDRESS);
// MyI2C_ReceiveAck();
// MyI2C_SendByte(RegAddress);
// MyI2C_ReceiveAck();
//
// MyI2C_Start();
// MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
// MyI2C_ReceiveAck();
// Data = MyI2C_ReceiveByte();
// MyI2C_SendAck(1);
// MyI2C_Stop();//发送地址部分移用自WriteReg函数I2C_GenerateSTART(I2C2, ENABLE);while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);I2C_SendData(I2C2, RegAddress);//由于在从机地址之后只发送寄存器地址一个数据,此处EV8改为EV8_2while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);//重复起始条件I2C_GenerateSTART(I2C2, ENABLE);//等待EV5while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);//方向变为接收I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);//等待EV6(接收模式)while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);//单字节(最后一个字节)接收,需在EV6后立刻配置好寄存器的非应答和停止条件,否则收到数据时应答已发送I2C_AcknowledgeConfig(I2C2, DISABLE);I2C_GenerateSTOP(I2C2, ENABLE);//等待EV7while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);//读取DRData = I2C_ReceiveData(I2C2);//寄存器应答恢复默认配置以便接收多字节I2C_AcknowledgeConfig(I2C2, ENABLE);return Data;
}void MPU6050_Init(void)
{
// MyI2C_Init();RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;//复用开漏输出,外设使用复用,I2C协议要求开漏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);I2C_InitTypeDef I2C_InitStructure;//初始化默认给应答I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;I2C_InitStructure.I2C_ClockSpeed = 50000;//时钟占空比在标准速度下无效I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;//非从机模式地址任意I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;I2C_InitStructure.I2C_OwnAddress1 = 0x00;I2C_Init(I2C2, &I2C_InitStructure);I2C_Cmd(I2C2, ENABLE);//I2C初始化完毕MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);MPU6050_WriteReg(MPU6050_CONFIG, 0x06);MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{uint8_t DataH, DataL;DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);*AccX = (DataH << 8) | DataL;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_L);*GyroX = (DataH << 8) | DataL;DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);*GyroY = (DataH << 8) | DataL;DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);*GyroZ = (DataH << 8) | DataL;
}
该代码为了检测事件使用了大量while死循环等待,一旦一个事件未发生,则整个程序都会卡死。可通过封装超时退出的while循环等待函数解决该问题:
//参数复制自I2C_CheckEvent函数
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;}
}
随后将I2C_CheckEvent函数均替换为封装函数即可。
主程序不变。