I2C通信时序

前言:

I2C作为众多通信时序中脱颖而出的最常见的通信时序,但是学习起来也是非常难解,并且时不时要回去复习,发这篇博客,我想把I2C给讲明白了,并且也是记录自己对于I2C的学习。

I2C介绍:

I2C(IIC,Inter-Integrated Circuit),两线式串行总线,由PHILIPS公司开发用于连接微控制器及其外围设备。

它是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbps。

IIC是同步半双工通信方式,有应答机制,可实现一对多,多对多通信。

SDA:数据线,用于传输数据,可从主机到从机,也可以从机到主机,但是同一时刻只能有一个方向传输,所以I2C是半双工通信,

SCL:时钟线,只能由主机发送,用于数据同步,一个脉冲下发送/接收一位数据。

硬件电路:

线的连接:I2C通信要求主机从机的SCL和SDA数据线都连在一起。

电路分析:

假设我们去掉SCL和SDA外接的上拉电阻,先对SCL配置,因为我们是一主多从模式,不需要多主机,所以SCL单独由主机控制,就可以配置成推挽输出模式,可输出高低电平。

SDA呢,因为是半双工,SDA可以主机到从机,也可以从机到主机,假如当主从机同时输出电平时,并且还是输出不一样的电平时,SDA就造成了短路,这是我们不愿意见到的,所以,I2C就把

SDA和SCL都配置成了开漏输出并且外接了两个上拉电阻到SDA和SCL。

继续对右边的电路进行分析,这里SCL和SDA的电路都是一样的,我们就取一个分析。

SCL:当SCL输入时,会经过一个施密特触发器或者数据缓冲器,这个输入没问题,输出的时候,因为电路是开漏输出,输出低电平时,电路导通,直接接地,强下拉,引脚直接输出低电平,输出高电平时,电路不导通,直接断开,引脚为浮空状态,这时候因为SCL和SDA都外接了一个弱上拉电阻,所以会输出高电平。

硬件电路设计好处:

SCL和SDA这样配置就好像一根杆子,外接了一个弱弹簧,有人往下拉就输出低电平,没人往下拉就输出高电平,并且当多个设备拽杆子时,也只会输出低电平,杜绝了短路的情况,并且只有当所有设备当输出高电平时,才会输出高电平,(因为输出高电平时引脚是浮空状态,高电平是弱上拉电阻提供,电平并不会太高)

I2C通信时序基本单元:

空闲状态:

I2C总线总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。

起始、终止信号:

起始条件是:当SCL处于高电平状态时,SDA由高电平切换到低电平,即拉一下杆子。

终止条件时:当SCL处于高电平状态时,SDA由低电平到高电平变化,即放手杆子。

这样子,所有从机就都会收到主机发出的起始信号。

发送一个字节:

当SCL低电平期间,主机会把要发送的数据放到SDA上,并且是高位先行,然后就会释放SCL,给到从机一个上升沿信号,从机在收到上升沿信号后,会在高电平期间把SDA上的电平信号进行读取,并且在SCL高电平期间,不允许SDA由数据变化,不然就是会变成起始终止信号了。

接受一个字节:

当SCL低电平期间,从机把数据的每一个位依次放到SDA线上,并且是高位先发,然后主机会释放SCL,SCL回到高电平,主机会在SCL高电平期间把SDA上的数据进行读取,并且SCL高电平期间,不允许SDA有数据变化,因为如果有数据变化,就变成起始终止信号了,主机在接收之前需要先释放SDA,不然从机无法用SDA发送数据。

发送接收字节总结:

SCL为低电平期间,SDA进行数据变更,即发出数据位,不过当主机发送数据时,由主机操控SDA发送数据位,当从机发送数据时,由从机操控SDA发送数据位,当SCL位高电平期间,主机或者从机进行SDA数据位的读取。

应答信号ACK:

发送应答和接收应答都是相对于主机而言,所以当主机是在接收数据时,主机接收完一个字节数据时,即八位数据后,要在下一个SCL时钟信号,发送一个应答位给从机表示应答。

在SCL为低电平时,拉低SDA,表示应答,从机在SCL为高电平时,读取应答位,SDA为低电平表示主机应答,相反则为不应答。

当主机发送数据时,发送完一个字节数据后,即八位数据后,要在下一个SCL时钟信号,接收一个应答信号,判断从机是否应答,主机拉低SCL,从机在SCL低电平期间,拉低SDA,表示应答,主机拉高SCL,主机在SCL高电平期间读取SDA应答位数据,低电平表示从机应答,相反为非应答。

总结:

(1)每发送(主机与从机都适用)一个字节,则会(主机与从机都适用)接受一个应答信号(信号其实就是一位数据);每接收(主机与从机都适用)一个字节,则会(主机与从机都适用)发送一位应答信号(信号其实就是一位数据);

(2)规定低电平为有效应答,表示成功接受字节;规定高电平为无效应答,表示没有成功接受字节。

(3)假设主机接受数据,如果不想再接受,则发送一个无效应答(非应答)来通知从机不要再进行传输数据。

I2C通信时序:

设备地址:

在I2C中,因为是一主多从模式,所以如何区别不同从机并指定从机进行读写就成了一个问题。

I2C中,设备地址就相当于从机的名字,每个设备地址都是唯一的,主机会在通信前先叫一下指定的从机名字,即指定一下从机的设备地址,并且点名每个从机都会收到,从机会判断自己的设备地址是否被主机,如果被点名,就响应主机之后的读写操作,如果没被点名,主机之后的读写时序,就不关该从机事情了。

所以,在I2C中,每个从机设备在出厂时都会自带一个设备地址,用于来区分挂载在同一I2C总线上时,设备地址可以在设备手册中找到,I2C有7位地址和12位地址模式,这里我们用7位地址模式,因为7位地址模式最为常见。

那还有一个问题,如果是同样的设备芯片接到I2C总线呢,在设备中会有另外的引脚来改变设备地址,一般设备高位数据地址是出厂固定的,低位数据引出引脚外接来决定设备地址,如图:

指定地址写:

指定地址写 对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

当SCL为高电平时,SDA由高到低,产生一个下降沿信号,表示起始信号。

主机发送一个字节数据,前七位代表点名的从机设备地址,最后一位代表主机要进行读还是写操作,0代表主机写数据到从机操作,1代表主机读从机数据操作。

接着主机接收一个应答信号,来表示从机的应答

接着第二个字节发送数据信息具体看设备信息,一般第二个字节是寄存器地址或者控制字指令,如MPU6050第二个字节就是寄存器地址,AD转换器第二个字节就是指令控制字,这里我们以从机寄存器地址来表示。

接着主机接受一个应答信号,来表示从机的应答。

第三个字节表示要写入从机设备寄存器地址的字节数据,发送八位后,主机再接收一个应答信号。

最后,如果主机不想再发送数据了,就发送一个停止信号,终止指定地址写。

如果需要写入多个字节数据,同理,连续发八位数据后,接受一位应答信号,最后不想写了就发送一个停止信号。

指定地址读:

指定地址读 对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

指定地址读为复合格式数据帧:起始信号+前两位字节为(指定设备地址7位+写位1)+(设备寄存器地址)+重复起始信号+(指定从机设备地址7位+读位0)

软件模拟I2C:

这里,我们用一个STM32F407和一个AT24C02来模拟实现I2C:

I2C.c

cpp 复制代码
#include "iic.h"

/****************************************
引脚说明
SCL -- PB8
SDA -- PB9

*****************************************/

void Iic_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStruct;
	
	//打开GPIOB组时钟 
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);	

	
	GPIO_InitStruct.GPIO_Pin	= GPIO_Pin_9|GPIO_Pin_8;		//引脚8
	GPIO_InitStruct.GPIO_Mode	= GPIO_Mode_OUT;				//输出模式
	GPIO_InitStruct.GPIO_OType	= GPIO_OType_OD;				//开漏输出
	GPIO_InitStruct.GPIO_PuPd	= GPIO_PuPd_UP;					//上拉
	GPIO_InitStruct.GPIO_Speed	= GPIO_Speed_50MHz;				//速度
	
	GPIO_Init(GPIOB, &GPIO_InitStruct);	
	
	//空闲状态
	SCL = 1;
	SDA_OUT = 1;
}

//数据引脚模式
void Iic_Sda_Mode(GPIOMode_TypeDef mode)
{

	GPIO_InitTypeDef  GPIO_InitStruct;
	
	GPIO_InitStruct.GPIO_Pin	= GPIO_Pin_9;		//引脚8
	GPIO_InitStruct.GPIO_Mode	= mode;							//输出模式
	GPIO_InitStruct.GPIO_OType	= GPIO_OType_OD;				//开漏输出
	GPIO_InitStruct.GPIO_PuPd	= GPIO_PuPd_UP;					//上拉
	GPIO_InitStruct.GPIO_Speed	= GPIO_Speed_50MHz;				//速度
	
	GPIO_Init(GPIOB, &GPIO_InitStruct);	
}

//启动信号
void Iic_Start(void)
{
	//数据线设置为输出
	Iic_Sda_Mode(GPIO_Mode_OUT);
	
	//空闲状态
	SCL = 1;
	SDA_OUT = 1;
	
	delay_us(5);
	SDA_OUT = 0;
	delay_us(5);
	SCL = 0;

}

//停止信号
void Iic_Stop(void)
{
	//数据线设置为输出
	Iic_Sda_Mode(GPIO_Mode_OUT);
	
	SCL = 0;
	SDA_OUT = 0;

	delay_us(5);
	SCL = 1;
	delay_us(5);
	SDA_OUT = 1;
}
//发送一位数据
void Iic_Send_Ack(u8 ack)
{
	//数据线设置为输出
	Iic_Sda_Mode(GPIO_Mode_OUT);
	
	//SCL为低电平区间,允许电平变化
	SCL = 0;
	
	if(ack == 1)
	{
		SDA_OUT = 1; //引脚为高电平
	}
	
	if(ack == 0)
	{
		SDA_OUT = 0;//引脚为低电平
	}	
	
	delay_us(5);
	

	SCL = 1;
	delay_us(5);
	SCL = 0;

}
void Iic_Send_Byte(u8 tx_data)
{
	u8 i;
	
	//假设数据rx_data:1 0 1 0 0 1 1 0
	//数据线设置为输出
	Iic_Sda_Mode(GPIO_Mode_OUT);
	
	SCL = 0;
	for(i=0; i<8; i++)
	{
		//数据准备,一位一位发送 先发高位
		if(tx_data & (0x01<<(7-i)))
		{
			SDA_OUT = 1;
		}
		else
		{
			SDA_OUT = 0;
		}
		
		
		delay_us(5);
		
		SCL = 1;
		delay_us(5);
		SCL = 0;	
	}


}
//接收一位数据
u8 Iic_Recv_Ack(void)
{
	u8 ack = 0;
	
	Iic_Sda_Mode(GPIO_Mode_IN);
	
	SCL = 0;
	
	delay_us(5);
	//高电平区间获取数据
	SCL = 1;
	
	delay_us(5);
	//收一位数据
	if(SDA_IN == 1)
	{

		ack = 1;
	}
	if(SDA_IN == 0)
	{

		ack = 0;
	}	
	SCL = 0;

	return ack;
}
//接收一个字节
u8 Iic_Recv_Byte(void)
{
	u8 i, rx_data = 0x00;
	
	Iic_Sda_Mode(GPIO_Mode_IN);

	SCL = 0;
	for(i=0; i<8; i++)
	{
		delay_us(5);
		SCL = 1;
		delay_us(5);
		//数据合成 
		if(SDA_IN == 1)
		{
			rx_data |= (0x01<<(7-i));
			
		}
		SCL = 0;	
	}
	
	return rx_data;
}

//u8 addr:写数据的起始地址
void At24c02_Write_Page(u8 addr, u8 *write_buff, u8 len)
{
	u8 ack = 0;
	
	//发送启动信号
	Iic_Start();

	//发送设备地址,执行写操作
	Iic_Send_Byte(0xA0);
	//判断应答信号
	ack = Iic_Recv_Ack();
	if(ack == 1)
	{
		printf("ack fail 1\r\n");
		return;
	}
	
	//发送写数据起始地址
	Iic_Send_Byte(addr);
	//判断应答信号
	ack = Iic_Recv_Ack();
	if(ack == 1)
	{
		printf("ack fail 2\r\n");
		return;
	}	

	while(len--)
	{
		//写数据
		Iic_Send_Byte(*write_buff);
		//判断应答信号
		ack = Iic_Recv_Ack();
		if(ack == 1)
		{
			printf("ack fail 3\r\n");
			return;
		}			
	
		write_buff++;
	}

	//停止信号
	Iic_Stop();
	
	return;
}

//写数据从页头开始写
void At24c02_Write_Data(u8 addr, u8 *write_buff, u8 len)
{
	u8 page, len_byte;
	
	page = len/8;
	len_byte = len%8;
	
	while(page--)
	{
		At24c02_Write_Page(addr, write_buff, 8); //写一页
		addr = addr+0x8;  //移动到下一页
		write_buff = write_buff+8;
		delay_ms(5);
	}
	//加延时,跨页写数据无延时会失败
	delay_ms(5);

	//写入不满一页的数据
	if(len_byte > 0)
		At24c02_Write_Page(addr, write_buff, len_byte); 


	
}

void At24c02_Read_Data(u8 addr, u8 *read_buff, u8 len)
{
	u8 ack = 0;
	
	//发送启动信号
	Iic_Start();
	//发送设备地址,执行写操作
	Iic_Send_Byte(0xA0);
	//判断应答信号
	ack = Iic_Recv_Ack();
	if(ack == 1)
	{
		printf("ack fail 1\r\n");
		return;
	}
	//发送读数据起始地址
	Iic_Send_Byte(addr);
	//判断应答信号
	ack = Iic_Recv_Ack();
	if(ack == 1)
	{
		printf("ack fail 2\r\n");
		return;
	}		
	
	//发送启动信号
	Iic_Start();	
	//发送设备地址,执行读操作
	Iic_Send_Byte(0xA1);
	//判断应答信号
	ack = Iic_Recv_Ack();
	if(ack == 1)
	{
		printf("ack fail 1\r\n");
		return;
	}	
	
	while(len--) //
	{
		*read_buff = Iic_Recv_Byte();
		
		if(len > 0)
			Iic_Send_Ack(0);
		
		read_buff++;
	}
	
	//发送非应答信号,结果接收数据
	Iic_Send_Ack(1);
	
	
	//停止信号
	Iic_Stop();	
	
}

I2C.h:

cpp 复制代码
#ifndef __IIC_H
#define __IIC_H

#include "stm32f4xx.h"
#include "sys.h"
#include "delay.h"
/*******************************
引脚说明
SCL -- PB8
SDA -- PB9
********************************/


#define SCL  	PBout(8)
#define SDA_IN	PBin(9)
#define SDA_OUT	PBout(9)

void Iic_Init(void);
void At24c02_Write_Page(u8 addr, u8 *write_buff, u8 len);
void At24c02_Write_Data(u8 addr, u8 *write_buff, u8 len);
void At24c02_Read_Data(u8 addr, u8 *read_buff, u8 len);

#endif

main.c:

cpp 复制代码
#include "stm32f4xx.h"
#include "led.h"
#include "key.h"
#include "exti.h"
#include "delay.h"
#include "tim.h"
#include "pwm.h"
#include "usart.h"
#include "sr04.h"
#include "iwdg.h"
#include "rtc.h"
#include "adc.h"
#include "flash.h"
#include "iic.h"


u8 g_flag = 0;  //g_flag = 1表示接受到数据

u8 g_data = 0;

void delay(int n)
{
	int i, j;
	
	for(i=0; i<n; i++)
		for(j=0; j<30000; j++);
}



void USART1_IRQHandler(void)
{
	//判断接受标志位是否置1
	if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
		
		//接受数据
		g_data = USART_ReceiveData(USART1);
	
		g_flag = 1;  //表示接受到数据

		
	}

}



int main(void)
{
	u8 write_buff[12] = "helloworld";
	u8 read_buff[12] = {0};
	//设置NVIC分组(一个项目只能配置一次)
	//抢占优先级取值范围:0~3  响应优先级取值范围:0~3
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	Delay_Init();
	Led_Init();
	Usart1_Init(115200);
	Iic_Init();
	//At24c02_Write_Page(0x00, write_buff, 10);
	At24c02_Write_Data(0x00, write_buff, 10);
	
	
	delay_ms(50);
	At24c02_Read_Data(0x00, read_buff, 10);
	
	printf("read_buff:%s\r\n", read_buff);
	
	while(1)
	{

		delay_s(1);
	}
	
	
    return 0;
}

结语:

在这篇博客中,我们深入探讨了I2C协议的基本原理、工作方式和应用场景。作为一种广泛使用的串行通信协议,I2C以其简单的两线制连接、灵活的主从架构以及较低的成本,广泛应用于各类电子设备和嵌入式系统中。

写到这里,I2C差不多也是写完了吧,希望读者读完这篇博客,能对I2C有更深刻的理解。

相关推荐
飞凌嵌入式39 分钟前
飞凌嵌入式FET527N-C核心板已适配OpenHarmony4.1
arm开发·嵌入式硬件·飞凌嵌入式
每天一杯冰美式oh1 小时前
51单片机的智能水温控制系统【proteus仿真+程序+报告+原理图+演示视频】
嵌入式硬件·51单片机·proteus
追梦少年时2 小时前
STM32-USART串口协议
stm32·单片机·嵌入式硬件
cykaw25902 小时前
ESP-01S WIFI模块指南
c语言·单片机·嵌入式硬件·51单片机
梦境虽美,却不长4 小时前
51单片机快速入门之PWM控制 灯的亮度 2024年10/15
单片机·嵌入式硬件·51单片机
Abaaba+4 小时前
【树莓派 5B】重启后vnc无法使用失效
python·stm32·单片机·嵌入式硬件
weixin_446260856 小时前
嵌入式硬件设计
嵌入式硬件
银科院-计算机与人工智能7 小时前
单片机原理及应用笔记:单片机的结构原理与项目实践
笔记·单片机·mongodb
极客小张8 小时前
基于STM32的工厂安防巡检机器人设计流程实现自主识别检测、机器人自主行驶、环境监控和数据采集
c语言·stm32·单片机·opencv·物联网·机器人·视觉检测