19:I2C一:程序模拟I2C通信时序

I2C

1、什么是I2C

I2C通信协议和串口通信USART不同,USART是异步,全双工的通信协议。而I2C通信协议是一种同步,半双工,带数据应答,支持总线挂载多设备的通信协议。USART传输数据的时候是先传输数据帧的低位,在传高位。而I2C传输数据的时候是高位先行。

由上图所示:有2条总线,数据总线SDA和时钟总线SCL。其中时钟线SCL只能由主机控制,而SDA主机和从机都可以控制。

当主机向从机发送一个字节数据时:①主机先发送一个起始信号,②然后在发送一个字节的数据(从机地址(7位)+W),然后接收从机的应答信号,③然后在发送一个字节的数据(从机寄存器的地址),然后接收一个应答信号,④然后开始发送需要发送给从机的数据,然后接收应答信号,⑤发送一个停止信号。 如下图所示:

当主机读取从机一个字节的数据时:①主机先发送一个起始信号,②然后在发送一个字节的数据(从机地址(7位)+W),然后接收从机的应答信号,③然后在发送一个字节的数据(从机寄存器的地址)④然后主机在发送一个起始信号,⑤然后在发送一个字节的数据(从机地址(7位)+R),然后接收从机的应答信号,⑥然后开始读取SDA上面的数据,读取到一个字节后,给从机发送一个应答信号,如下图所示:

2、I2C的通信时序

2.1:起始信号

起始信号和停止信号都是由主机进行发送,主机需要发送起始信号时:SCL高电平期间,SDA从高电平切换到低电平。

使用程序模拟此时序代码如下:

c 复制代码
void I2C_W_SCL(uint8_t BitValue)//拉低SCL或者释放SCL(拉高)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
	Delay_us(10);
}

void I2C_W_SDA(uint8_t BitValue)//给SDA写数据(拉低SDA/释放SDA(拉高))
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}

void MyI2C_Start(void)//起始信号
{
//	SCL(1);//释放SCL
	I2C_W_SDA(1);//释放SDA
	I2C_W_SCL(1);//释放SCL
	I2C_W_SDA(0);//拉低SDA
	I2C_W_SCL(0);//拉低SCL
}

2.2:停止信号

主机需要发送停止信号时:SCL高电平期间,SDA从低电平切换到高电平。

使用程序模拟此时序代码如下:

c 复制代码
void MyI2C_Stop(void)
{
	I2C_W_SCL(0);
	I2C_W_SDA(0);
	I2C_W_SCL(1);
	I2C_W_SDA(1);
}

2.3:主机向从机发送一个字节数据

SCL低电平期间,主机将数据位依次给SDA写入数据(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节,从机读取到一个字节,会有一个应答信号。

使用程序模拟此时序代码如下:

c 复制代码
/*
	发送数据前,主机会发送一个起始信号,由上面的程序可知:起始信号发送完后SCL和SDA都被拉低。
	所以函数里面不用在拉低,直接向SDA写入数据即可。
*/
void MyI2C_SendByte(uint8_t Byte)//主机向从机发送一个字节,高位先行
{
	I2C_W_SDA(Byte & 0x80);//给SDA写入数据,只要Byte不是0,那么写入的就是1。因为BitAction
	I2C_W_SCL(1);//释放SCL,从机读取数据
	I2C_W_SCL(0);//拉低SCL,主机准备给SDA写入字节的次高位数据
	
	I2C_W_SDA(Byte & 0x40);//给SDA写入数据,
	I2C_W_SCL(1);//释放SCL,从机读取数据
	I2C_W_SCL(0);//拉低SCL,主机准备给SDA写入字节的次次高位数据
	.
	.
	.
}

代码优化如下:

c 复制代码
/*
	发送数据前,主机会发送一个起始信号,由上面的程序可知:起始信号发送完后SCL和SDA都被拉低。
	所以函数里面不用在拉低,直接向SDA写入数据即可。
*/
void MyI2C_SendByte(uint8_t Byte)//主机向从机发送一个字节,高位先行
{
	for(uint8_t i = 0;i<8;i++)
	{
		I2C_W_SDA(Byte & (0x80 >> i));//给SDA写入数据,只要Byte不是0,那么写入的就是1。因为BitAction
		I2C_W_SCL(1);//释放SCL,从机读取数据
		I2C_W_SCL(0);//拉低SCL,主机准备给SDA写入字节的次高位数据
	}
}

2.4:主机向从机读取一个字节数据

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

使用程序模拟此时序代码如下:

c 复制代码
uint8_t I2C_R_SDA(void)//主机读SDA数据,即判断引脚的电平
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

/*
	主机接收数据之前,主机会给从机发送一个字节,由上面的代码得发送完数据后此时SCL为低电平。之后释放SDA总线让从机拥有SDA总线的控制权。
*/
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t Byte = 0x00;//接收数据的变量
	I2C_W_SDA(1);//主机释放SDA,让控制权给从机,而SCL也是低电平,释放的一瞬间,从机就给SDA写入了数据
	
	I2C_W_SCL(1);
	if(I2C_R_SDA() == 1)//读取第一位数据
	{
		Byte |= 0x80;
	}
	I2C_W_SCL(0);//主机拉低SCL,让从机给SDA写入数据	
	
	I2C_W_SCL(1);//主机拉高SCL,准备开始读取SDA上面的数据
	if(I2C_R_SDA() == 1)
	{
		Byte |= 0x40;
	}
	I2C_W_SCL(0);//主机拉低SCL,让从机给SDA写入数据	
	.
	.
	.
	return Byte;
}

代码优化如下:

c 复制代码
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t Byte = 0x00;//接收数据的变量
	I2C_W_SDA(1);//主机释放SDA,让控制权给从机
	
	for(uint8_t i =0; i<8;i++)
	{
		I2C_W_SCL(1);//主机拉高SCL,准备开始读取SDA上面的数据
		if(I2C_R_SDA() == 1)
		{
			Byte |= (0x80 >> i);
		}	
		I2C_W_SCL(0);//主机拉低SCL,准备让从机给SDA写入数据	
	}
	return Byte;
}

2.5:主机接收应答

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

使用程序模拟此时序代码如下:

c 复制代码
uint8_t I2C_R_SDA(void)//主机读SDA数据,即判断引脚的电平
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

uint8_t MyI2C_ReceiveACK(void)//主机接收应答
{
/*
	主机发送完一个字节后,SCL为低电平,
	等待从机给SDA上面写入数据,如果拉低则代表接收成功
*/
	uint8_t ACKBit;
	I2C_W_SDA(1);//主机释放SDA,让控制权给从机
	I2C_W_SCL(1);//主机释放SCL,准备开始读取SDA上面的数据
	ACKBit = I2C_R_SDA();
	I2C_W_SCL(0);//主机拉低SCL,进入下一个时序单元,主机准备给SDA写入字节的数据
	return ACKBit;
}

2.6:主机发送应答

主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。

c 复制代码
void MyI2C_SendACK(uint8_t ACKBit)//主机发送应答
{
/*
	从机发送完一个字节后,SCL为低电平,
	等待主机给SDA上面写数据,如果拉低代表接收成功。
*/
	
	I2C_W_SDA(ACKBit);//主机给SDA写入应答信号,0为应答,1为非应答
	I2C_W_SCL(1);//主机拉高SCL,让从机读取应答信号
	I2C_W_SCL(0);//主机拉低SCL,准备写入数据
}

3、程序模拟I2C的通信时序

3.1:指定地址写一个字节的数据

用程序模拟I2C时序实现如上图的波形:

①I2C.c文件使用程序模拟I2C时序程序如下:

c 复制代码
#include "stm32f10x.h"                 
#include "Delay.h"                 

//#define SCL(x)          GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)(x))
//#define SDA(x)          GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)(x))

void I2C_W_SCL(uint8_t BitValue)//拉低SCL/释放SCL(拉高)
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue);
	Delay_us(10);
}

void I2C_W_SDA(uint8_t BitValue)//给SDA写数据(拉低SDA/释放SDA(拉高))
{
	GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}

uint8_t I2C_R_SDA(void)//主机读SDA数据,即判断引脚的电平
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}


void MyI2C_Init(void)
{
	//1. 使能挂载在APB2总线上面的片上外设时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	//2. 对GPIO_PA10/PA11进行配置
	GPIO_InitTypeDef GPIOInitStruct;
	GPIOInitStruct.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_OD;//输出开漏模式,0才有驱动能力,开漏模式没有输出低电平时,也可以作为输入
	GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;//最大输出速度
	GPIO_Init(GPIOB,&GPIOInitStruct);
	
	GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);//先给输出高电平,释放总线
																								//PB10连接SCL。PB11连接SDA
}

void MyI2C_Start(void)//起始信号
{
	I2C_W_SDA(1);//释放SDA
	I2C_W_SCL(1);//释放SCL
	I2C_W_SDA(0);//拉低SDA
	I2C_W_SCL(0);//拉低SDA
}

void MyI2C_Stop(void)
{
	I2C_W_SCL(0);
	I2C_W_SDA(0);
	I2C_W_SCL(1);
	I2C_W_SDA(1);
}

void MyI2C_SendByte(uint8_t Byte)//主机向从机发送一个字节,高位先行
{
	for(uint8_t i = 0;i<8;i++)
	{
		I2C_W_SDA(Byte & (0x80 >> i));//给SDA写入数据,只要Byte不是0,那么写入的就是1。因为BitAction
		I2C_W_SCL(1);//释放SCL,从机读取数据
		I2C_W_SCL(0);//拉低SCL,主机准备给SDA写入字节的次高位数据
	}
}

uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t Byte = 0x00;//接收数据的变量
	
	I2C_W_SDA(1);//主机释放SDA,让控制权给从机
	for(uint8_t i =0; i<8;i++)
	{
		I2C_W_SCL(1);//主机释放SCL,准备开始读取SDA上面的数据
		if(I2C_R_SDA() == 1)
		{
			Byte |= (0x80 >> i);
		}	
	I2C_W_SCL(0);//主机拉低SCL,让从机给SDA写入数据	
	}
	return Byte;
}


void MyI2C_SendACK(uint8_t ACKBit)//主机发送应答
{
/*
	从机发送完一个字节后,SCL为低电平,
	等待主机给SDA上面写数据,如果拉低代表接收成功。
*/
	
	I2C_W_SDA(ACKBit);
	I2C_W_SCL(1);
	I2C_W_SCL(0);
}

uint8_t MyI2C_ReceiveACK(void)//主机接收应答
{
/*
	主机发送完一个字节后,SCL为低电平,
	等待从机给SDA上面写入数据,如果拉低则代表接收成功
*/
	uint8_t ACKBit;
	I2C_W_SDA(1);//主机释放SDA,让控制权给从机
	I2C_W_SCL(1);//主机释放SCL,准备开始读取SDA上面的数据
	ACKBit = I2C_R_SDA();
	I2C_W_SCL(0);//主机拉低SCL,进入下一个时序单元,主机准备给SDA写入字节的数据
	return ACKBit;
}

②主函数文件模拟波形的程序如下:

c 复制代码
#include "stm32f10x.h"   
#include "MyI2C.h"
int main(void)
{
	MyI2C_Init();
	MyI2C_Start();//发送一个起始信号
	
	MyI2C_SendByte(0xD0);//发送一个字节的数据(从机地址7位+W),0表示写,1表示读
	MyI2C_ReceiveACK();//接收从机应答信号
	
	MyI2C_SendByte(0x19);//发送一个字节的数据(从机中的寄存器地址)
	MyI2C_ReceiveACK();//接收从机应答信号
	
	MyI2C_SendByte(0xAA);//发送一个字节的数据(需要发送给从机的数据)
	MyI2C_ReceiveACK();//接收从机应答信号
	
	MyI2C_Stop();//发送一个停止信号
}

3.2:指定地址读一个字节的数据

c 复制代码
#include "stm32f10x.h"   
#include "MyI2C.h"
int main(void)
{
	uint8_t Data;
	
	MyI2C_Init();
	MyI2C_Start();//发送一个起始信号
	
	MyI2C_SendByte(0xD0);//发送一个字节的数据(从机地址7位+W),0表示写,1表示读
	MyI2C_ReceiveACK();//接收从机应答信号
	
	MyI2C_SendByte(0x19);//发送一个字节的数据(从机中的寄存器地址)
	MyI2C_ReceiveACK();//接收从机应答信号
	
	MyI2C_Start();//发送一个起始信号
	
	MyI2C_SendByte(0xD1);//发送一个字节的数据(从机地址7位+R),0表示写,1表示读
	MyI2C_ReceiveACK();//接收从机应答信号
	
	Data = MyI2C_ReceiveByte();//主机接收一个字节的数据
	MyI2C_SendACK(1);//主机发送非应答信号
	
	MyI2C_Stop();//发送一个停止信号
}
相关推荐
PegasusYu43 分钟前
STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组
stm32·教程·rtos·stm32cubeide·free-rtos·eventgroup·时间标志组
lantiandianzi5 小时前
基于单片机的多功能跑步机控制系统
单片机·嵌入式硬件
文弱书生6565 小时前
输出比较简介
stm32
哔哥哔特商务网5 小时前
高集成的MCU方案已成电机应用趋势?
单片机·嵌入式硬件
跟着杰哥学嵌入式5 小时前
单片机进阶硬件部分_day2_项目实践
单片机·嵌入式硬件
电子科技圈6 小时前
IAR与鸿轩科技共同推进汽车未来
科技·嵌入式硬件·mcu·汽车
东芝、铠侠总代136100683937 小时前
浅谈TLP184小型平面光耦
单片机·嵌入式硬件·物联网·平面
lantiandianzi7 小时前
基于单片机中医药柜管理系统的设计
单片机·嵌入式硬件
嵌入式知识大讲堂7 小时前
HDMI数据传输三种使用场景
单片机
黑客呀8 小时前
[系统安全]Rootkit基础
stm32·单片机·系统安全