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有更深刻的理解。

相关推荐
cjy_Somnr3 小时前
keil5报错显示stm32的SWDIO未连接不能烧录
stm32·单片机·嵌入式硬件
Lay_鑫辰4 小时前
西门子诊断-状态和错误位(“轴”工艺对象 V1...3)
服务器·网络·单片机·嵌入式硬件·自动化
无垠的广袤6 小时前
【工业树莓派 CM0 NANO 单板计算机】本地部署 EMQX
linux·python·嵌入式硬件·物联网·树莓派·emqx·工业物联网
雲烟8 小时前
嵌入式设备EMC安规检测参考
网络·单片机·嵌入式硬件
泽虞8 小时前
《STM32单片机开发》p7
笔记·stm32·单片机·嵌入式硬件
田甲9 小时前
【STM32】 数码管驱动
stm32·单片机·嵌入式硬件
up向上up9 小时前
基于51单片机垃圾箱自动分类加料机快递物流分拣器系统设计
单片机·嵌入式硬件·51单片机
纳祥科技18 小时前
Switch快充方案,内置GaN,集成了多个独立芯片
单片机
单片机日志20 小时前
【单片机毕业设计】【mcugc-mcu826】基于单片机的智能风扇系统设计
stm32·单片机·嵌入式硬件·毕业设计·智能家居·课程设计·电子信息
松涛和鸣20 小时前
从零开始理解 C 语言函数指针与回调机制
linux·c语言·开发语言·嵌入式硬件·排序算法