欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 资讯 > I2C通信时序

I2C通信时序

2024/10/23 15:24:37 来源:https://blog.csdn.net/weixin_69902486/article/details/142962318  浏览:    关键词: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

#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;		//引脚8GPIO_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;		//引脚8GPIO_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:

#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:

#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)
{//判断接受标志位是否置1if(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~3NVIC_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有更深刻的理解。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com