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();//发送一个停止信号
}
相关推荐
HQB20261 天前
EG1198|屹晶微电子 120V 内置 MOS 异步降压 DC‑DC(高性价比)
嵌入式硬件·硬件工程
果壳science1 天前
张祥前实验如何获取正电荷
笔记·科技·嵌入式硬件·硬件工程·制造
南山电子nscn1 天前
中科芯CETC汽车电池管理系统BMS进口芯片国产型号替代指南
单片机·汽车·bms
沉淀的.晴天1 天前
can总线
stm32
桀人1 天前
认识AT24C02
stm32·单片机·嵌入式硬件
国科安芯1 天前
星间激光通信终端CANFD控制网络的时延确定性与辐射可靠性
网络·嵌入式硬件·信息与通信·canfd·低轨卫星·商业航天·抗辐射
一直C1 天前
嵌入式 Linux 驱动开发学习笔记:内核定时器、platform 总线与 I²C 驱动
linux·驱动开发·嵌入式硬件·学习·imx6ull
传感器与混合集成电路1 天前
150MHz主频、8路DAC与6路PWM:JLH221360伺服控制SIP解析
嵌入式硬件
comyccccc1 天前
STM32PID控制板设计全解析学习笔记
单片机·嵌入式硬件
碧海银沙音频科技研究院1 天前
实现炬芯ATS308X的usb cdc 串口IAP 固件升级功能实现
人工智能·嵌入式硬件·深度学习·算法