一文看懂i2c通信详解+实践

adminadmin 欧易资讯 2026-07-17 19 0

I2C 通讯规则与物理结构

I2C 协议的精髓在于其极简的物理连接:

一文看懂i2c通信详解+实践

SCL (Serial Clock Line):串行时钟线。由主设备产生,用于同步所有设备的数据传输节奏

SDA (Serial Data Line):串行数据线。用于在主设备和从设备之间双向传输实际的数据

这两条线都是开漏输出结构,需要通过上拉电阻连接到正电源。这意味着任何一个设备都可以通过将线拉低来输出‘0’,而释放线路(输出高阻态)则靠上拉电阻将电平拉回‘1’。这种设计实现了线与功能,并支持多主设备仲裁。

总线上可以挂载多个设备,每个设备都有一个唯一的地址。主设备可以通过广播地址来选择要与哪个从设备进行通信。

I2C 通讯流程 写操作流程(主设备向从设备写入数据)

当一个主设备(如单片机)想要控制一个从设备(如传感器)时,通常采用写操作。

一文看懂i2c通信详解+实践

开始 (Start):主设备发出一个“开始”信号

发送设备地址(写):主设备发送一个7位或10位的从设备地址,并跟随着一位读写位(0表示写操作)。

等待应答 (Wait for ACK):所有从设备都会聆听这个地址。地址匹配的从设备会拉低SDA线作为应答(ACK),表示:地址正确

发送寄存器地址:主设备接着发送一个或多个字节的数据(通常是告诉从设备要操作哪个内部寄存器)。

等待应答:从设备每收到一个字节,都会回复一个ACK。

发送数据:主设备开始正式发送要写入的数据字节。每发送一个字节,都会等待从设备的ACK确认。

停止 (Stop):数据发送完毕后,主设备产生一个“停止”信号,结束本次通信

读操作流程(主设备从从设备读取数据)

当主设备需要从从设备获取数据(如读取传感器数值)时,采用读操作

一文看懂i2c通信详解+实践

开始 (Start):主设备发出“开始”信号。

发送设备地址(写):主设备先以写模式(读写位为0) 发送从设备地址。

等待应答:从设备应答。

发送寄存器地址:主设备发送它想要读取的起始寄存器地址。

等待应答:从设备应答。

重复开始 (Repeated Start):主设备再次发送一个“开始”信号(称为“重复开始”)。这个信号既不结束上一个通信,也能开始一个新的通信。

发送设备地址(读):主设备再次发送从设备地址,但这次读写位为1,表示读操作。

等待应答:从设备应答。

接收数据:主设备开始接收从设备发来的数据字节。每接收一个字节后,主设备需要向从设备发送一个应答信号(ACK)。

如果主设备还想继续读取下一个字节,就发送ACK(应答)。

如果主设备读取完最后一个字节,需要发送NACK(非应答),以此告知从设备:“数据已收齐,不用再发了。”

停止 (Stop):主设备产生“停止”信号,结束通信。

通讯信号与软件模拟实现

在无硬件I2C控制器的情况下,可以通过GPIO引脚模拟其时序,即“软件I2C”。关键在于精确控制SCL和SDA的电平变化时序。

起始信号 (Start) 和停止信号 (Stop)

起始信号 (S):在SCL为高电平期间,SDA线产生一个从高到低的下降沿。这个组合信号是唯一的,标志着一次传输的开始。

一文看懂i2c通信详解+实践

void I2C_Start(void) {
    SDA_HIGH();  // SDA = 1
    SCL_HIGH();  // SCL = 1
    delay();
    SDA_LOW();   // 在SCL高时,SDA由高变低 -> 产生起始条件
    delay();
    SCL_LOW();   // 拉低SCL,为后续发送数据做准备
    delay();
}

停止信号 §:在SCL为高电平期间,SDA线产生一个从低到高的上升沿。这个组合信号标志着一次传输的结束。

一文看懂i2c通信详解+实践

void I2C_Stop(void) {
    SDA_LOW();   // SDA = 0
    SCL_LOW();   // SCL = 0
    delay();
    SCL_HIGH();  // SCL = 1
    delay();
    SDA_HIGH();  // 在SCL高时,SDA由低变高 -> 产生停止条件
    delay();
}

所以,这类的意思是,在scl高电平期间,sda电平是不应该变化的,否则就触发了开始或者停止信号

发送一个字节的数据(MSB先行):

数据有效性 & 发送一个字节 (Byte)

数据在时钟线SCL为低电平期间可以变化,在SCL的高电平期间必须保持稳定。这样,接收方就可以在SCL高电平时对SDA进行采样,读取稳定的数据位。

一文看懂i2c通信详解+实践

void I2C_SendByte(uint8_t byte) {
    for (int i = 0; i < 8; i++) {
        SCL_LOW();             // 拉低时钟,允许SDA变化
        delay();
        if (byte & 0x80) {     // 取出最高位
            SDA_HIGH();
        } else {
            SDA_LOW();
        }
        byte <<= 1;            // 左移,准备发送下一位
        delay();
        SCL_HIGH();            // 拉高时钟,此时SDA必须稳定
        delay();               // 接收方在此高电平期间采样
    }
    SCL_LOW(); // 最后拉低SCL,为后续应答位做准备
}

一文看懂i2c通信详解+实践

这里msb格式发送数据

应答 (ACK) 与非应答 (NACK)

每传输一个字节后,接收方必须向发送方发送一位应答信号。

应答 (ACK):接收方在第9个时钟脉冲期间将SDA线拉低,表示成功接收字节,并请求下一个字节。

非应答 (NACK):接收方在第9个时钟脉冲期间保持SDA线为高,表示接收失败或不再请求更多数据。

一文看懂i2c通信详解+实践

主机等待从机应答的代码:

uint8_t I2C_WaitAck(void) {
    uint8_t ack;
    SCL_LOW();
    SDA_HIGH();   // 主机释放SDA线(设为输入模式)
    set_SDA_Input();
    delay();
    SCL_HIGH();   // 产生第9个时钟脉冲
    delay();
    ack = READ_SDA(); // 读取SDA电平(0为ACK,1为NACK)
    SCL_LOW();    // 拉低SCL,结束ACK时钟脉冲
    set_SDA_Output(); // 主机重新拿回SDA控制权
    delay();
    return ack;
}

主机发送应答/非应答的代码(在读取数据后):

一文看懂i2c通信详解+实践

// 主机发送ACK(请求下一字节)
void I2C_SendAck(void) {
    SCL_LOW();
    set_SDA_Output();
    SDA_LOW();   // 拉低SDA,表示ACK
    delay();
    SCL_HIGH();  // 产生一个完整的时钟脉冲
    delay();
    SCL_LOW();
}
// 主机发送NACK(告知从机停止发送)
void I2C_SendNack(void) {
    SCL_LOW();
    set_SDA_Output();
    SDA_HIGH();  // SDA为高,表示NACK
    delay();
    SCL_HIGH();
    delay();
    SCL_LOW();
}

接收一个字节

接收数据同样由SCL同步。主机在SCL高电平时读取SDA线上的数据。

一文看懂i2c通信详解+实践

uint8_t I2C_ReceiveByte(void) {
    uint8_t byte = 0;
    set_SDA_Input(); // 主机释放SDA,准备接收
    for (int i = 0; i < 8; i++) {
        SCL_LOW();
        delay();
        SCL_HIGH();   // 产生一个时钟脉冲
        delay();
        byte <<= 1;    // 左移,为下一位腾出空间
        if (READ_SDA()) {
            byte |= 0x01; // 读取SDA电平并存入最低位
        }
        delay();
    }
    SCL_LOW();
    set_SDA_Output(); // 接收完毕,主机拿回SDA控制权以备发送ACK/NACK
    return byte;
}

硬件 I2C

软件模拟I2C虽然灵活,但会占用CPU资源,且时序容易受中断干扰。因此,现代MCU都集成了硬件I2C控制器。

硬件I2C的工作可以概括为:配置好参数后,硬件自动处理底层时序。

初始化配置:通过配置寄存器设置I2C的工作模式(主/从)、通信速率(时钟频率)、自身地址等参数。

自动时序处理:一旦启动,硬件模块会自动产生起始信号、停止信号、时钟SCL,并按照设定的速率生成时钟脉冲。

数据移位:硬件自动将待发送的数据字节并行->串行移位输出到SDA线上,或者将接收到的串行数据移位->并行存入接收数据寄存器。

自动应答管理:硬件可以自动在发送完一个字节后检测ACK位,或在接收完一个字节后发送ACK/NACK。

中断/DMA支持:数据准备好或发送完成等事件可以触发中断,或者直接通过DMA与内存交换数据,极大减轻CPU负担。

状态监控:提供状态寄存器,可以查询当前状态(如总线忙、数据已发送完毕、是否收到ACK等),从而决定下一步操作。

错误处理:硬件能检测一些错误,如仲裁丢失、总线错误等。

核心优势:使用硬件I2C时,只需关注高层逻辑(如“向地址0x50的EEPROM的0x0000地址写入数据‘0xAB’”),而具体的比特位传输、时序、ACK处理等底层细节全部由硬件模块自动完成,效率高且可靠。

GD32F4寄存器标志

一文看懂i2c通信详解+实践

实战pcf8563 rtc时钟

一文看懂i2c通信详解+实践

#include "gd32f4xx.h"
#include "systick.h"
#include 
#include "LIB_USART.h"
/*********************
任务目标:
模板工程
**********************/
#define RCU_I2C_SCL RCU_GPIOB
#define RCU_I2C_SDA RCU_GPIOB
#define SCL_PORT GPIOB
#define SDA_PORT GPIOB
#define SCL_PIN GPIO_PIN_6
#define SDA_PIN GPIO_PIN_7
#define SCL(x) gpio_bit_write(SCL_PORT,SCL_PIN,x)
#define SDA(x) gpio_bit_write(SDA_PORT,SDA_PIN,x)
#define SDA_GET() gpio_input_bit_get(SDA_PORT,SDA_PIN)
#define DELAY(x) delay_1us(x)
#define SDA_IN() gpio_mode_set(SDA_PORT,GPIO_MODE_INPUT,GPIO_PUPD_NONE,SDA_PIN)
#define SDA_OUT() gpio_mode_set(SDA_PORT,GPIO_MODE_OUTPUT,GPIO_PUPD_NONE,SDA_PIN)
#define RECV_DEBUG_ACK(x)  do{if(recv_ack()){return x;}}while(0)
void I2C_Soft_Init(){
	rcu_periph_clock_enable(RCU_I2C_SCL);
	rcu_periph_clock_enable(RCU_I2C_SDA);
	gpio_mode_set(SCL_PORT ,GPIO_MODE_OUTPUT,GPIO_PUPD_NONE,SCL_PIN);
	gpio_output_options_set(SCL_PORT ,GPIO_OTYPE_OD,GPIO_OSPEED_2MHZ,SCL_PIN);
	gpio_mode_set(SDA_PORT,GPIO_MODE_OUTPUT,GPIO_PUPD_NONE,SDA_PIN);
	gpio_output_options_set(SDA_PORT,GPIO_OTYPE_OD,GPIO_OSPEED_2MHZ,SDA_PIN);
}
void start(){
	SDA_OUT();
	SDA(1);
	SCL(1);
	DELAY(5);
	SDA(0);
	DELAY(5);
	SCL(0);
}
void send_byte(uint8_t data){
	SDA_OUT();
	for(uint8_t i=0;i<8;i++){
		SCL(0);
		SDA(data&0x80);
		DELAY(5);
		SCL(1);
		DELAY(5);
		SCL(0);
		data<<=1;
	}
}
uint8_t recv_byte(){
	SDA_IN();
	uint8_t data=0;
	for(int i=7;i>=0;i--){
		SCL(0);
		DELAY(5);
		SCL(1);
		if(SDA_GET()){
			data|=1<<i;
		}
		DELAY(5);
	}
SCL(0);
	return data;
}
void send_ack(){
	SDA_OUT();
	SDA(0);
	DELAY(5);
	SCL(1);
	DELAY(5);
	SCL(0);
	DELAY(5);
	
}
void send_nack(){
	SDA_OUT();
	SDA(1);
	DELAY(5);
	SCL(1);
	DELAY(5);
	SCL(0);
	DELAY(5);
}
uint8_t recv_ack(){
	SDA_OUT();
	SDA(1);
	DELAY(5);
	SDA_IN();
	SCL(1);
	DELAY(5);
	if(SDA_GET()==RESET){
		SCL(0);
		DELAY(5);
		return 0;
	}
	SCL(0);
	DELAY(5);
	return 1;
	
}
void stop(){
	SDA_OUT();
	SDA(0);
	SCL(1);
	DELAY(5);
	SDA(1);
	
}
uint8_t I2C_Soft_WriteNByte(uint8_t dev_addr,uint8_t mem_addr,uint8_t *data,uint8_t len){
	// 开始
	start();
	// 发送设备地址
	send_byte(dev_addr);
	// 等待从响应
	// recv_ack();
	RECV_DEBUG_ACK(11);
// 	发送寄存器地址
	send_byte(mem_addr);
	// 等待从响应
	// recv_ack();
	RECV_DEBUG_ACK(22);
// 	发送数据
	for(uint8_t i=0;i<len;i++){
		send_byte(data[i]);
		// 等待从响应
		recv_ack();
		// DEBUG_ACK(33);
		// send_ack(); 
	}
// 	停止
	stop();
	return 0;
}
uint8_t I2C_Soft_ReadNByte(uint8_t dev_addr,uint8_t mem_addr,uint8_t *data,uint8_t len){
	// 发送开始信号
	start();
	// 发送对方设备地址
	send_byte(dev_addr);
	// 等待从响应
	// recv_ack();
	RECV_DEBUG_ACK(1);
	// 发送要操作的对方的寄存器地址
	send_byte(mem_addr);
	// 等待从响应
	// recv_ack();
	RECV_DEBUG_ACK(2);
	// 发送开始信号
	start();
	// 发送对方设备地址+1,表明要进行读数据
	send_byte(dev_addr+1);
	// 等待从响应
	// recv_ack();
	RECV_DEBUG_ACK(3);
	//循环读数据
	for(uint8_t i=0;i<len;i++){
		//接收数据
		*data=recv_byte();
		data++;
		// //发送应答信号
		// send_ack();
		//最后一条数据响应不进行发送
		if(i!=len-1){
			send_ack();
		}
	}
	//改为发送nack,非应答信号
	send_nack();
	//停止接收数据
	stop();
	return 0;
}
// #define test_arr
#define test_value
int main(void) {
	// 系统滴答定时器初始化
	systick_config();
	I2C_Soft_Init();
	USART0_INIT();
	// uint8_t testdat=0x08;
	#ifdef test_arr
	uint8_t testdat[]={0x55,0x29,0x15};
	I2C_Soft_WriteNByte(0xA2,0x02,testdat,3);
	while(1) {
	//   gpio_bit_set(GPIOB,GPIO_PIN_2);
	//   delay_1ms(10);
	//   gpio_bit_reset(GPIOB,GPIO_PIN_2);
		uint8_t flag= I2C_Soft_ReadNByte(0xA2,0x02,testdat,3);
		printf("%x:%x:%x,flag=%d\n",testdat[2],testdat[1],testdat[0],flag);
	  delay_1ms(1000);
	}
	#endif
	#ifdef test_value
	uint8_t testdat=0x09;
	I2C_Soft_WriteNByte(0xA2,0x02,&testdat,1);
	while(1) {
	//   gpio_bit_set(GPIOB,GPIO_PIN_2);
	//   delay_1ms(10);
	//   gpio_bit_reset(GPIOB,GPIO_PIN_2);
	// uint8_t flag= I2C_Soft_WriteNByte(0xA2,0x02,&testdat,1);
	// printf("flag=%d\n",flag);
		uint8_t flag= I2C_Soft_ReadNByte(0xA2,0x02,&testdat,1);
		printf("testdat=%x,flag=%d\n",testdat,flag);
	  delay_1ms(1000);
	}
	#endif 
}

版权声明

本文仅代表作者观点,不代表xx立场。
本文系作者授权xx发表,未经许可,不得转载。

喜欢0评论已闭