因为I2C是同步的,所以相对来说I2C更好用软件来实现,硬件却相对来说没这么好,但是硬件I2C通信也是有其优点的
我们是通过软件写入控制寄存器CR和数据寄存器DR,读取状态寄存器SR来了解外设电路当前处于什么状态,来实现I2C通信的,而我们通过STM32的库函数来实现配置这些寄存器,这些操作就变得更简单了;有了I2C硬件外设的存在,硬件自动实现时序,就可以减轻CPU的负担,节省软件资源,由硬件来做这件事,可以更加专注,时序生成的性能、效率也会更高,这就是I2C外设存在的意义。
多主机模型:
1、固定多主机:固定多台机器为主机,只有其中一台主机才可以通过主线控制所有从机,当多个主机控制主线时,会出现主线冲突,这个时候就会进行总线总裁,仲裁失败的一方会让出总线控制权;
2、可变多主机:默认情况下,全部机器都是从机,当某台机器需要控制权时,就会跳出来变成主机,控制主线,多个从机跳出来时,就会进行总线仲裁,仲裁成功的获得总线控制权。STM32的I2C就是可变多主机的模型
引脚定义
发送:移位寄存器中的数据由高位到低位先移出去,移8次就可以移一个字节,有高位到低位依次放到SDA线上;
接收:数据从GPIO口从右边依次移进来,最终移8次,一个字节就接收完成了。
配置问题:两个GPIO口都需要配置成复用开漏输出模式,复用——GPIO的状态是交由片上外设来控制的,开漏输出——I2C协议要求的端口配置(在这个模式下依然可以通过GPIO口进行输入)
EV-标志位(多个事件发生的标志位)
对应数据手册24.6.7
EV5:(不需要手动清除的)
硬件IIC的缺点就是引脚固定
//老朋友
void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
//生成起始条件
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
//生成结束条件
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
生成起始条件的函数——通过配置IIC中的CR1寄存器的值来决定是否生成起始条件
想要更深入了解I2C还是得需要阅读数据手册
可以看到这是配置ACK应答使能的函数
//接收应答
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
查看其函数定义可以发现是配置DR数据寄存器的
//接收数据——读取DR寄存器
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
//发送七位地址
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
这个函数还可以通过参数判断是接收还是发送模式来写入最低位(R/W)当然也可以自己通过SendData来操作
这是库函数给我们设计的多种监控状态方案
1、基本状态监控
同时判断一个或者多个标志位,来确定正处于哪一个状态
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
2、高级状态监控(不需要掌握)
实际上并不高级
3、基于状态标志位的监控
可以判断某一个标志位是否置1
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);
我们这节内容和上一节的软件读取IIC类似,所以复制上一个过程文件,把工程中有关IIC的代码删去
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
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);
特别说明:为什么要设置一个控制占空比的结构体成员呢?
占空比的设置是为了快速传输而设置的
50kHz的情况下
400kHz的情况下
因为时钟信号是强下拉,弱上拉,这就会导致在产生下降沿的时候速度非常快,而产生上升沿的时候电平却变化得比较慢,是慢慢回弹回去的(类似于弹簧模型,强下拉就像强行把弹簧按下去,而弱上拉却是弹簧自己慢慢弹上去)
数据变化需要一定时间来产生波形,由其是上升沿,变化比较慢,而数据的采集却会很快(在上升沿时采集SDA上的电平)所以我们需要给产生波形多一点时间,即让SCL时钟信号的低电平时间(写入时间)变得更长一点,所以采取设置其占空比来实现该目的
整体:
void HI2C_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
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);
I2C_InitTypeDef I2C_InitStructure;
//I2C模式
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
//I2C时钟速度-0~100KHZ(标准速度) 100KHz~400KHz(快速)
I2C_InitStructure.I2C_ClockSpeed = 50000;
//时钟占空比-标准速度(只能为1:1) 快速模式下可以为16:9或者2:1
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
//应答位(默认为Enable)
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
//Stm32作为从机的地址位数
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
//给STM32指定一个从机地址
I2C_InitStructure.I2C_OwnAddress1 = 0x00;
I2C_Init(I2C2, &I2C_InitStructure);
I2C_Cmd(I2C2, ENABLE);
}
对照之前写的I2C的软件实现即可
需要注意的是软件I2C的这些函数内部都添加了Delay函数来等待数据的接收完成,是一种阻塞式的流程,也就是上一个函数完成后,数据肯定已经完成接收或者发送了,但是硬件I2C函数不一样,都不是阻塞式的,函数结束后是将对应的标志位置1,所以要确保数据完整,就必须在函数后加上判断相应标志位的函数,如下图所示
举个栗子:在发送完起始位后需要判断EV5事件,这样我们就需要用上之前的监控函数了
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
第二个参数:
返回值
所以要判断是否完成就需要套用一个while循环
//对照上面的软件I2C协议写
I2C_GenerateSTART(I2C2, ENABLE);
//EV5事件的检测
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
总体
void 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协议写
I2C_GenerateSTART(I2C2, ENABLE);
//EV5事件的检测
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);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
I2C_SendData(I2C2, Data);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
I2C_GenerateSTOP(I2C2, ENABLE);
}
注意:在接收最后一个字节前,必须要提前把ACK置0和设置STOP请求(STOP不会打断字节的接收),如果只接收一个字节,就可以直接在接收数据1之前就把ACK置0和发送STOP请求。
//指定地址读
uint8_t MPU6050_ReadtheReg(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();
//与发送一个字节的前半部分一样
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);
//注意这里不是硬抄,检测的标志位改为了ED结尾(其实ing还是ED结尾都没有区别
//都会等到发送数据完全结束后才会进行下一步操作
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
//开始接收字节
I2C_GenerateSTART(I2C2, ENABLE);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
//发送寄存器地址
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
//提前ACK置0和发送STOP请求
I2C_AcknowledgeConfig(I2C2, DISABLE);
I2C_GenerateSTOP(I2C2, ENABLE);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
Data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2, ENABLE);
return Data;
}
整体:记得在初始化函数中引用HI2C的初始化函数
#include "stm32f10x.h" // Device header
#include "MPU6050_Reg.h"
#include "Hard-I2C.h"
#define MPU6050_ADDRESS 0xD0
//指定地址写
void 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协议写
I2C_GenerateSTART(I2C2, ENABLE);
//EV5事件的检测
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);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
I2C_SendData(I2C2, Data);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
I2C_GenerateSTOP(I2C2, ENABLE);
}
//指定地址读
uint8_t MPU6050_ReadtheReg(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();
//与发送一个字节的前半部分一样
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);
//注意这里不是硬抄,检测的标志位改为了ED结尾(其实ing还是ED结尾都没有区别
//都会等到发送数据完全结束后才会进行下一步操作
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
//开始接收字节
I2C_GenerateSTART(I2C2, ENABLE);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
//发送寄存器地址
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
//提前ACK置0和发送STOP请求
I2C_AcknowledgeConfig(I2C2, DISABLE);
I2C_GenerateSTOP(I2C2, ENABLE);
while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
Data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2, ENABLE);
return Data;
}
void MPU6050_Init(void)
{
HI2C_Init();
//电源管理寄存器1
//设备复位 睡眠模式 循环模式 无关位 温度传感器失能 选择时钟(后三位)
//0(不复位) 0(解除睡眠) 0(不需要) 0 0(不失能) 000(内部时钟)(或者001-陀螺仪时钟)
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
//电源管理寄存器2
//循环模式唤醒频率(不需要-00)
//后6位每个轴的待机位-(000000-不待机)
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
//采样率分频(值越小,数据输出越快)(采样分频为10)
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
//配置寄存器
//外部同步-000000 数字低通滤波-110
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
//陀螺仪配置寄存器
//自测使能-000 满量程选择-11 无关位-000
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
//加速度计配置寄存器
//自测使能-000 满量程选择-11 高通滤波器-000
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)
{
int16_t DataH, DataL;
DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_XOUT_L);
*AccX = (DataH << 8) | DataL;
DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH << 8) | DataL;
DataH = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadtheReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH << 8) | DataL;
DataH = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadtheReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH << 8) | DataL;
DataH = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadtheReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH << 8) | DataL;
DataH = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadtheReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH << 8) | DataL;
}
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadtheReg(MPU6050_WHO_AM_I);
}
其他的没改变
主函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;
int main(void)
{
OLED_Init();
MPU6050_Init();
OLED_ShowString(1, 1, "ID:");
ID = MPU6050_GetID();
OLED_ShowHexNum(1, 4, ID, 2);
while(1)
{
MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
OLED_ShowSignedNum(2, 1, AX, 5);
OLED_ShowSignedNum(3, 1, AY, 5);
OLED_ShowSignedNum(4, 1, AZ, 5);
OLED_ShowSignedNum(2, 8, GX, 5);
OLED_ShowSignedNum(3, 8, GY, 5);
OLED_ShowSignedNum(4, 8, GZ, 5);
}
}
这样就可以实现相关功能了
我们是用死循环来等待标志位结束的,但凡有一个没结束都会导致整个函数停止
我们可以封装一个函数来实现计时功能,计数指定时间后退出函数
可以防止卡死错误
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;
}
}
}
把函数中的每个while循环改为此函数即可