I2C的硬件实现

因为I2C是同步的,所以相对来说I2C更好用软件来实现,硬件却相对来说没这么好,但是硬件I2C通信也是有其优点的

我们是通过软件写入控制寄存器CR和数据寄存器DR,读取状态寄存器SR来了解外设电路当前处于什么状态,来实现I2C通信的,而我们通过STM32的库函数来实现配置这些寄存器,这些操作就变得更简单了;有了I2C硬件外设的存在,硬件自动实现时序,就可以减轻CPU的负担,节省软件资源,由硬件来做这件事,可以更加专注,时序生成的性能、效率也会更高,这就是I2C外设存在的意义。

I2C外设简介

STM32 内部集成了硬件 I2C 收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻 CPU 的负担
支持多主机模型
支持 7 /10 位地址模式
支持不同的通讯速度,标准速度 ( 高达 100 kHz) ,快速 ( 高达 400 kHz)
支持 DMA
兼容 SMBus 协议
STM32F103C8T6 硬件 I2C 资源: I2C1 I2C2
(硬件I2C的资源是有限的,这也是硬件和软件的区别)

多主机模型:

1、固定多主机:固定多台机器为主机,只有其中一台主机才可以通过主线控制所有从机,当多个主机控制主线时,会出现主线冲突,这个时候就会进行总线总裁,仲裁失败的一方会让出总线控制权;

2、可变多主机:默认情况下,全部机器都是从机,当某台机器需要控制权时,就会跳出来变成主机,控制主线,多个从机跳出来时,就会进行总线仲裁,仲裁成功的获得总线控制权。STM32的I2C就是可变多主机的模型

I2C框图

I2C的硬件实现_第1张图片

引脚定义

 

I2C基本结构

I2C的硬件实现_第2张图片

发送:移位寄存器中的数据由高位到低位先移出去,移8次就可以移一个字节,有高位到低位依次放到SDA线上;

接收:数据从GPIO口从右边依次移进来,最终移8次,一个字节就接收完成了。

配置问题:两个GPIO口都需要配置成复用开漏输出模式,复用——GPIO的状态是交由片上外设来控制的,开漏输出——I2C协议要求的端口配置(在这个模式下依然可以通过GPIO口进行输入)

主机发送

EV-标志位(多个事件发生的标志位)

I2C的硬件实现_第3张图片

对应数据手册24.6.7

EV5:(不需要手动清除的) 

I2C的硬件实现_第4张图片 主机接收

I2C的硬件实现_第5张图片

 代码实操

I2C的硬件实现_第6张图片

 硬件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的硬件实现_第7张图片

想要更深入了解I2C还是得需要阅读数据手册 

I2C的硬件实现_第8张图片

可以看到这是配置ACK应答使能的函数 

//接收应答
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);

I2C的硬件实现_第9张图片 

 接收数据

void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);

查看其函数定义可以发现是配置DR数据寄存器的

I2C的硬件实现_第10张图片 I2C的硬件实现_第11张图片

//接收数据——读取DR寄存器
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
//发送七位地址
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);

这个函数还可以通过参数判断是接收还是发送模式来写入最低位(R/W)当然也可以自己通过SendData来操作 

I2C的硬件实现_第12张图片

 I2C状态监控函数

这是库函数给我们设计的多种监控状态方案

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的代码删去

第一步:配置IIC外设

I2C的硬件实现_第13张图片

A、开启IIC外设的时钟和对应GPIO口的时钟
B、配置GPIO口为复用开漏模式
    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);
C、通过结构体配置相关参数

特别说明:为什么要设置一个控制占空比的结构体成员呢?

占空比的设置是为了快速传输而设置的

50kHz的情况下

I2C的硬件实现_第14张图片

400kHz的情况下

I2C的硬件实现_第15张图片

因为时钟信号是强下拉,弱上拉,这就会导致在产生下降沿的时候速度非常快,而产生上升沿的时候电平却变化得比较慢,是慢慢回弹回去的(类似于弹簧模型,强下拉就像强行把弹簧按下去,而弱上拉却是弹簧自己慢慢弹上去)

数据变化需要一定时间来产生波形,由其是上升沿,变化比较慢,而数据的采集却会很快(在上升沿时采集SDA上的电平)所以我们需要给产生波形多一点时间,即让SCL时钟信号的低电平时间(写入时间)变得更长一点,所以采取设置其占空比来实现该目的

D、开关使能

整体:

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,所以要确保数据完整,就必须在函数后加上判断相应标志位的函数,如下图所示

I2C的硬件实现_第16张图片

举个栗子:在发送完起始位后需要判断EV5事件,这样我们就需要用上之前的监控函数了

ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);

第二个参数: 

I2C的硬件实现_第17张图片

返回值 

所以要判断是否完成就需要套用一个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);
}

第三步:实现指定地址读的时序

I2C的硬件实现_第18张图片

 注意:在接收最后一个字节前,必须要提前把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循环改为此函数即可

你可能感兴趣的:(单片机,嵌入式硬件)