一.硬件I2C通信
相对于软件I2C而言,硬件I2C有其独有的优势,比如执行效率比较高,可以节省软件资源,功能强大,可以实现完整的多主机通信模型,时序波形规整,通信速率快等等,所以如果对性能指标要求比较高那就可以考虑硬件I2C
1.I2C外设简介
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,硬件自动生成时序,减轻CPU的负担
支持多主机模型,多主机模型分为固定多主机和可变多主机,固定多主机表示在主线上固定有多个主机,多个主机同时操控主线时就需要总线仲裁;可变多主机指的是主线上没有固定的主机,都是从机,每个从机都可以在总线空闲时跳出作为主机,指定其他设备进行通信,通信结束后退为从机,STM32是按照可变多主机的模型进行设计的。
支持7位/10位地址模式(7位地址只能支持128个设备,地址数量不够时可以通过开辟多条I2C总线或者改用10位地址来解决,十位地址的情况下就需要规定起始位后的两个字节都作为寻址为使用,如果想将第二个字节作为寻址使用那就要将第一个字节前五位写入11110来作为标志),支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz),支持DMA,兼容SMBus协议(此协议是基于I2C总线改进而来的,主要应用于电源管理系统中)
STM32F103C8T6 硬件I2C资源:I2C1、I2C2
I2C框图:
数据移位寄存器和DATA REGISTER的配合与串口相似,也是通过改变状态位实现发送和读取,区别在于串口是全双工接收发送时分开的,而I2C合并到一个寄存器中
I2C基本结构图:
GPIO口配置为复用开漏输出,此模式下可以输出也可以输入
一图流主机收发:
EV8事件就是正在发送的状态
二.硬件I2C通信实验
根据结构图我们可以使用库函数对MPU6050进行初始化,初始化步骤为:
1.开启I2C外设和GPIO口的时钟
2.把I2C外设和对应GPIO口初始化为复用开漏模式
3.使用结构体对整个I2C进行配置
4.使能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);
//发送数据
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);
配置代码部分:
#include "stm32f10x.h" // Device header
#include "MPU6050_Reg.h"
#define MPU6050_Address 0xD0 //宏定义MPU地址
//为防止卡死将检验封装
void I2C_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t timeout=10000;
while(I2C_CheckEvent(I2Cx,I2C_EVENT)!=SUCCESS)
{
timeout--;
if(timeout==0)
{
//此处还可以进行更进一步的错误处理部分,比如返回错误日志或者紧急停机等等
break;
}
}
}
//封装按地址写时序
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data){
//软件触发时的函数都是阻塞式的,每一行执行后才会执行下一行,硬件触发时非阻塞式,所以要在每一行后检测标志位
I2C_GenerateSTART(I2C2,ENABLE);
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//事件检测函数,根据序列图进行事件检验,EV5事件
I2C_Send7bitAddress(I2C2,MPU6050_Address,0);//发送地址确认字节,最后一位为读写位,不需要单独接收应答因为内置了已经
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//EV6两种可选,此处为发送模式
I2C_SendData(I2C2,RegAddress);
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING);
I2C_SendData(I2C2,Data);
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);
I2C_GenerateSTOP(I2C2,ENABLE);
}
//封装按地址读时序
uint8_t MPU6050_ReadReg(uint8_t RegAddress){
uint8_t Data;
I2C_GenerateSTART(I2C2,ENABLE);
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//事件检测函数,根据序列图进行事件检验,EV5事件
I2C_Send7bitAddress(I2C2,MPU6050_Address,I2C_Direction_Transmitter);//发送地址确认字节,最后一位为读写位,不需要单独接收应答因为内置了已经
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//EV6两种可选,此处为发送模式
I2C_SendData(I2C2,RegAddress);//发送一字节数据
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);//因为要重新起始所以判断是否传送完毕
I2C_GenerateSTART(I2C2,ENABLE);
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2,MPU6050_Address,I2C_Direction_Receiver);//发送地址确认字节,最后一位为读写位,不需要单独接收应答因为内置了已经
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);//EV6确认接收
//以下是读取多字节的循环,函数要求的数据多加一个uint8_t total
// for(i=0;i<total;i++){
// if(i==tatal-1){
// I2C_AcknowledgeConfig(I2C2,DISABLE);//清除响应位
// I2C_GenerateSTOP(I2C2,ENABLE);//生成停止条件
// }
//
//
//
//
// }
//此处遵从EV6_1的建议清楚响应位生成停止条件
I2C_AcknowledgeConfig(I2C2,DISABLE);//清除响应位
I2C_GenerateSTOP(I2C2,ENABLE);//生成停止条件
I2C_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);//EV7
Data=I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2,ENABLE);//再将响应位置1,默认正常状态为1,在接收最后一个数据前临时置0
return Data;
}
void MPU6050_Init(void){
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_N;
GPIO_N.GPIO_Mode=GPIO_Mode_AF_OD;//复用开漏,复用就是将控制权交给了外设使用
GPIO_N.GPIO_Pin=GPIO_Pin_11 | GPIO_Pin_10;
GPIO_N.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_N);
I2C_InitTypeDef I2C_I;
I2C_I.I2C_Mode=I2C_Mode_I2C;//模式选择,此处为I2C模式
I2C_I.I2C_ClockSpeed=50000;//时钟频率
I2C_I.I2C_DutyCycle=I2C_DutyCycle_2;//时钟占空比(低:高),只有时钟频率大于100k时才有效果,100k以下默认1:1
I2C_I.I2C_Ack=I2C_Ack_Enable;//配置Ack位,用于确认在接收一个字节后是否给从机应答
I2C_I.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;//配置作为从机可以响应7/10位的地址,此处配置为7位
I2C_I.I2C_OwnAddress1=0x00;//配置STM32作为从机的地址,根据上一行配置的位数进行配置
I2C_Init(I2C2,&I2C_I);//配置I2C
I2C_Cmd(I2C2,ENABLE);
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);
}
//获取数据寄存器的函数,此处使用指针地址传递,还可以使用结构体传递的方法
//指针法的原理是在主函数中定义变量将地址传入函数,在函数中直接对地址进行改写
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);//读取加速度计x轴高八位数据
DataL=MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//读取加速度计x轴低八位数据
*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);//读取陀螺仪x轴高八位数据
DataL=MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);//读取陀螺仪x轴低八位数据
*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);
}
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
硬件I2C部分的代码是将软件I2C底层时序部分代码删除后更改配置完成的所以主函数部分无变化。