I2C(Inter-Integrated Circuit)是一种通用的总线协议。它是由Philips(飞利浦)公司,现NXP(恩智浦)半导体开发的一种简单的双向两线制总线协议标准。

I2C有两根双向的信号线,一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步。

支持同步,半双工,带数据应答,支持总线挂载多设备(一主多从、多主多从)协议)。

多主多从模式下,总线上任何一个模块都可以主动申请成为主机,若同时多个模块申请冲突时,总线就会进行仲裁,失败的一方自动 变为从机。

硬件电路

所有I2C设备的SCL连接在一起,SDA连接在一起

设备的SCL和SDA均要设置为开漏输出模式,开漏输出高电平没有驱动能力。

SCL和SDA各添加一个上拉电阻,阻值为4.7Ω左右。

从机对于SCL时钟线任何时刻只能被动读取。对于SDA数据线,从机不允许主动发起对SDA的控制,只有在主机发起读取从机或者从机发送应答的时候,从机才能获得短暂的控制权。

线与特性:只要有一个低电平,总线就处于低电平,所有设备都输出高电平总线才处于高电平。

软件I2C

软件I2C时序基本单元

设置软件I2C时,为保证时序拼接,尽量让起始条件和终止条件外的时序电源的SCL都是以低电平开始,低电平结束。

起始条件

SCL为高电平,SDA从高电平切换到低电平

终止条件

SCL为高电平,SDA从低电平切换到高电平

发送一个字节

SCL低电平器件,主机将数据位依次放到SDA线上,高位先行,主机发送0则拉低SDA,若发生发送1则放手SDA会被上拉电阻自动拉为高电平。放完一位数据后,释放SCL锁存住SDA线上的数据(此时SDA数据不可变),从机在SCL高电平期间读取数据位,依次循环8次即可发送一个字节。

接收一个字节

SCL低电平期间,从机将数据位依次放在SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,循环8次即可接收一个字节(主机在接收之前需要释放SDA给从机权限)

发送应答位

发送应答:主机在接收完一个字节后,在下个一时钟发送一位数据,数据0(拉低SDA线)表示应答,数据1表示非应答(什么也不做)。

接收应答:主机发送完一个字节后,在下一个时钟接收一个数据判断从机是否应答,0表示应答,1表示非应答。(接收之前主机需要释放SDA)

I2C通讯地址

大多数I2C器件支持7位地址模式,有一些器件还支持10位地址,而且两种类型的器件可以连接在同一个I2C总线上,目前10位地址的器件还没有被广泛使用

7位地址

在硬件手册中可查看到地址为多少,一般地址的最后一位是可以改变的,防止出现地址冲突的情况。一般来说I2C的从机地址高位都是由厂商设置的,低位可以灵活切换,具体可参考对应配置手册。一般主机地址需要向左移动一位,低位0或1为读写位。

10位地址

起始条件后的两个字节都是寻址

第一个字节:帧头:5位的标志位+2位地址+1位读写位

第二个字节:8位地址

第一个字节的2位地址和第二个字节的8位字节构成了10位地址

I2C通讯时序

I2C指定地址写数据

I2C指定地址读数据

I2C规定在主机寻址一旦读写标志位给1,立刻进行读的时序。

I2C在读的时候并不能指定特定寄存器进行读取,只会读取当前指针所在的位置的内容。若在0x09地址下写一个数据,接这转换为读,则会读出0x0a地址的内容。

所以为了指定读到特定及储存器的内容,可以在读之前,进行写数据的操作来进行指定地址。

软件I2C协议代码

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/*引脚配置层*/

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);		//根据BitValue,设置SCL引脚的电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

//I2C写SDA引脚电平
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);		
    //根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
	Delay_us(10);		//延时10us,防止时序频率超过要求
}

//I2C读SDA引脚电平
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);	//读取SDA电平
	Delay_us(10);											//延时10us,防止时序频率超过要求
	return BitValue;										//返回SDA电平
}


//I2C初始化
void MyI2C_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);				//将PB10和PB11引脚初始化为开漏输出
	
	
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);			
    //设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}



//I2C起始
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);				//释放SDA,确保SDA为高电平
	MyI2C_W_SCL(1);				//释放SCL,确保SCL为高电平
	MyI2C_W_SDA(0);				//在SCL高电平期间,拉低SDA,产生起始信号
	MyI2C_W_SCL(0);				//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}

//I2C终止
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);							//拉低SDA,确保SDA为低电平
	MyI2C_W_SCL(1);							//释放SCL,使SCL呈现高电平
	MyI2C_W_SDA(1);							//在SCL高电平期间,释放SDA,产生终止信号
}

//I2C发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)			//循环8次,主机依次发送数据的每一位
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
		MyI2C_W_SCL(1);					//释放SCL,从机在SCL高电平期间读取SDA
		MyI2C_W_SCL(0);					//拉低SCL,主机开始发送下一位数据
	}
}

//I2C接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;	    //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	MyI2C_W_SDA(1);				//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	for (i = 0; i < 8; i ++)	//循环8次,主机依次接收数据的每一位
	{
		MyI2C_W_SCL(1);			//释放SCL,主机机在SCL高电平期间读取SDA
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量
		//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0

		MyI2C_W_SCL(0);						//拉低SCL,从机在SCL低电平期间写入SDA
	}
	return Byte;							//返回接收到的一个字节数据
}

// I2C发送应答位
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);					//主机把应答位数据放到SDA线
	MyI2C_W_SCL(1);							//释放SCL,从机在SCL高电平期间,读取应答位
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
}

//I2C接收应答位
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;							//定义应答位变量
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	MyI2C_W_SCL(1);							//释放SCL,主机机在SCL高电平期间读取SDA
	AckBit = MyI2C_R_SDA();					//将应答位存储到变量里
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
	return AckBit;							//返回定义应答位变量
}

硬件IIC外设

STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能

支持多主机模型

支持7位/10位地址模式

支持不同的通讯速度,标准速度(100kHZ),快速(400kHZ)

支持DMA转运

兼容SMBus协议

I2C框图

 I2C硬件时序

 硬件I2C配置

#define MPU6050_ADDRESS		0xD0		//MPU6050的I2C从机地址

//等待事件
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)								//自减到0后,等待超时
		{
			/*超时的错误处理代码,可以添加到此处*/
			break;										//跳出等待
		}
	}
}

//写寄存器:RegAddress 寄存器地址
void WriteReg(uint8_t RegAddress, uint8_t Data)
{
	I2C_GenerateSTART(I2C2, ENABLE);								//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);			//等待EV5
	
    //硬件I2C发送从机地址,方向为发送
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	


	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	I2C_SendData(I2C2, RegAddress);									//硬件I2C发送寄存器地址
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);	//等待EV8
	
	I2C_SendData(I2C2, Data);										//硬件I2C发送数据
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);		//等待EV8_2
	
	I2C_GenerateSTOP(I2C2, ENABLE);									//硬件I2C生成终止条件
}

//读寄存器
uint8_t ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);								//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);			//等待EV5
	
    //硬件I2C发送从机地址,方向为发送
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	

	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	I2C_SendData(I2C2, RegAddress);									//硬件I2C发送寄存器地址
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);		//等待EV8_2
	
	I2C_GenerateSTART(I2C2, ENABLE);							//硬件I2C生成重复起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);		//等待EV5
	
    //硬件I2C发送从机地址,方向为接收
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, 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
	Data = I2C_ReceiveData(I2C2);									//接收数据寄存器
	
    //将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
	I2C_AcknowledgeConfig(I2C2, ENABLE);

	return Data;
}

//硬件I2C初始化
void I2C_Init(void)
{
	//开启时钟,需要根据硬件手册确定I2C对应GPIO口
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);		//开启I2C2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	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和PB11引脚初始化为复用开漏输出
	
	/*I2C初始化*/
	I2C_InitTypeDef I2C_InitStructure;						//定义结构体变量
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;				//模式,选择为I2C模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;				//时钟速度,选择为50KHz
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;	//时钟占空比,选择Tlow/Thigh = 2
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;			//应答,选择使能
	
    //应答地址,选择7位,从机模式下才有效
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	

	I2C_InitStructure.I2C_OwnAddress1 = 0x00;				//自身地址,从机模式下才有效
	I2C_Init(I2C2, &I2C_InitStructure);			//将结构体变量交给I2C_Init,配置I2C2
	
	/*I2C使能*/
	I2C_Cmd(I2C2, ENABLE);									//使能I2C2,开始运行
	
}

作者:小陈会发光

物联沃分享整理
物联沃-IOTWORD物联网 » STM32 I2C通信教程

发表评论