I2C 接收与发送数据的流程

I2C(Inter-Integrated Circuit)是一种同步串行通信协议,由主设备(Master)控制通信节奏,从设备(Slave)被动响应。以下分别详细说明主设备接收数据主设备发送数据的完整流程。

一、通用前提与基础信号

无论接收还是发送数据,通信前需满足以下条件,且涉及相同的基础控制信号:

  • 总线空闲状态:SCL(时钟线)和SDA(数据线)均为高电平(由上拉电阻维持)。
  • 起始信号(Start Condition):主设备拉低SDA(此时SCL为高),随后拉低SCL,标志通信开始。
  • 终止信号(Stop Condition):主设备拉高SCL(此时SDA为低),随后拉高SDA,标志通信结束,释放总线。
  • 应答信号(ACK/NACK):每传输1字节(8位)后,接收方需在第9个时钟周期回应:
    • ACK:接收方拉低SDA,表示数据接收成功,允许继续传输。
    • NACK:接收方让SDA保持高电平,表示数据接收完成或失败,终止传输。

二、主设备发送数据流程(主→从)

主设备向从设备发送数据时,核心是“主设备主动输出数据,从设备被动接收并应答”。

1. 发送起始信号

  • 主设备发送Start信号,唤醒总线上的所有从设备。

2. 发送从设备地址及写指令

  • 主设备通过SDA发送7位从设备地址(高位在前)。
  • 地址后紧跟1位读写位:由于是“发送数据”,此位为0(写指令)。
  • 每发送1位,主设备通过SCL产生时钟脉冲(高电平期间数据有效)。
  • 从设备响应
    • 地址匹配的从设备在第9个时钟周期拉低SDA(发送ACK),表示准备接收数据。
    • 地址不匹配的从设备不响应,SDA保持高电平。

3. 向从设备发送数据

  • 主设备通过SDA逐字节发送数据(高位在前)。
  • 每发送1位,主设备产生1个时钟脉冲(SCL高电平期间数据稳定)。
  • 每发送完1字节,主设备等待从设备应答:
    • 从设备发送ACK(拉低SDA):主设备继续发送下一字节。
    • 从设备发送NACK(SDA保持高):主设备停止发送(通常表示从设备接收缓冲区满)。

4. 发送终止信号

  • 主设备完成所有数据发送后,发送Stop信号,释放总线。

三、主设备接收数据流程(从→主)

主设备从从设备接收数据时,核心是“主设备控制时钟,从设备主动输出数据,主设备应答”。

1. 发送起始信号

  • 主设备发送Start信号,唤醒总线上的所有从设备。

2. 发送从设备地址及读指令

  • 主设备通过SDA发送7位从设备地址(高位在前)。
  • 地址后紧跟1位读写位:由于是“接收数据”,此位为1(读指令)。
  • 每发送1位,主设备产生时钟脉冲(SCL高电平期间数据有效)。
  • 从设备响应
    • 地址匹配的从设备在第9个时钟周期拉低SDA(发送ACK),表示准备发送数据。
    • 地址不匹配的从设备不响应,SDA保持高电平。

3. 接收从设备发送的数据

  • 从设备通过SDA逐字节发送数据(高位在前)。
  • 每发送1位,主设备产生1个时钟脉冲(SCL高电平期间主设备读取数据)。
  • 每接收完1字节,主设备发送应答信号:
    • 主设备发送ACK(拉低SDA):从设备继续发送下一字节。
    • 主设备发送NACK(SDA保持高):从设备停止发送(主设备已接收完所有数据)。

4. 发送终止信号

  • 主设备接收完数据并发送NACK后,发送Stop信号,释放总线。
    在这里插入图片描述

在这里插入图片描述

四、接收与发送流程对比表

环节 主设备发送数据(写操作) 主设备接收数据(读操作)
读写位 0(写指令) 1(读指令)
数据传输方向 主设备→从设备 从设备→主设备
数据发送方 主设备主动输出数据 从设备主动输出数据
ACK/NACK发送方 从设备(确认接收数据) 主设备(确认接收数据或终止传输)
时钟控制方 始终由主设备控制(SCL由主设备驱动) 始终由主设备控制(SCL由主设备驱动)

STC8 的 I2C 功能

STC8 系列单片机通常内置硬件 I2C 模块,支持主从模式,时钟频率可配置(通常 100kHz 或 400kHz)。主要寄存器包括:

  • I2CCFG:配置 I2C 模式(主/从)、时钟等。
  • I2CMSST:主模式状态寄存器。
  • I2CSLST:从模式状态寄存器。
  • I2CTXD:发送数据寄存器。
  • I2CRXD:接收数据寄存器。

硬件连接

  • SCL:时钟线,连接上拉电阻(通常 4.7kΩ)。
  • SDA:数据线,连接上拉电阻(通常 4.7kΩ)。
  • 多个设备时,SDA 和 SCL 并联。

初始化 I2C(主模式)

/****************  I2C初始化函数 *****************/
void	I2C_config(void)
{
	I2C_InitTypeDef		I2C_InitStructure;

	I2C_InitStructure.I2C_Mode      = I2C_Mode_Master;	//主从选择   I2C_Mode_Master, I2C_Mode_Slave
	I2C_InitStructure.I2C_Enable    = ENABLE;			//I2C功能使能,   ENABLE, DISABLE
	I2C_InitStructure.I2C_MS_WDTA   = DISABLE;			//主机使能自动发送,  ENABLE, DISABLE
	I2C_InitStructure.I2C_Speed     = 13;				//总线速度=Fosc/2/(Speed*2+4),      0~63
                                                        // 400k, 24M => 13
	I2C_Init(&I2C_InitStructure);
	NVIC_I2C_Init(I2C_Mode_Master,DISABLE,Priority_0);	//主从模式, I2C_Mode_Master, I2C_Mode_Slave; 中断使能, ENABLE/DISABLE; 优先级(低到高) Priority_0,Priority_1,Priority_2,Priority_3

	I2C_SW(I2C_P33_P32);					//I2C_P14_P15,I2C_P24_P25,I2C_P33_P32
}

发送的写操作 I2C_WriteNbyte

void I2C_WriteNbyte(u8 dev_addr, u8 mem_addr, u8 *p, u8 number)  /*  DeviceAddress,WordAddress,First Data Address,Byte lenth   */
//dev_addr为设备地址
//mem_addr为寄存器地址
//*p是要传送的数据
//number是要发送的个数

案例:写入的设定的时间

// 设备地址
#define		PCF8563_DEV_ADDR	0xa2
// 存储地址(寄存器地址): 时间(秒)存储地址
#define		PCF8563_REG_TD		0x02
// 设置时间
void PCF8563_set_clock(Clock_t temp) {
    u8 p[7] = {0};
    u8 C;
    // 秒的寄存器地址为: 0x02
    // 秒p[0]:  第0~3位记录个位,第4~6位记录十位
    p[0] = ((temp.second/10) << 4) + (temp.second % 10);
    // printf("p[0] = %d\n", (int)p[0]);
    // 分p[1]: 第0~3位,保存个数,第4到6位,保存十位
    p[1] = ((temp.minute/10) << 4) + (temp.minute % 10);
    // 时p[2]:第0~3位,保存个数,第4到5位,保存十位
    p[2] = ((temp.hour/10) << 4) + (temp.hour % 10);
    // 日p[3]:第0~3位,保存个数,第4到5位,保存十位
    p[3] = ((temp.day/10) << 4) + (temp.day % 10);
    // 周p[4]:第0~2位,保存个数
    p[4] =  temp.weekday;
    // 月_世纪p[5]:  第0~3位记录个位,第4位记录十位,第7位为0,世纪数为20xx,为1,世纪数为21xx
    C = temp.year > 2100 ? 1 : 0;
    p[5] = ((temp.month/10) << 4) + (temp.month % 10) + (C << 7);
    // 年p[6]:第0~3位,保存个数,第4到7位,保存十位
    // 2023   要23   有得分   2 和 3     year % 100 / 10  得到2      year % 10 得到 3
    p[6] = ((temp.year % 100 / 10) << 4) + (temp.year % 10);

    // 写数据
    I2C_WriteNbyte(PCF8563_DEV_ADDR, PCF8563_REG_TD, p, 7);
}

读取设备地址(写模式)

void I2C_ReadNbyte(u8 dev_addr, u8 mem_addr, u8 *p, u8 number)   
/*  DeviceAddress,WordAddress,First Data Address,Byte lenth   */
    //dev_addr为设备地址
	//mem_addr为寄存器地址
	//*p是要传送的数据
	//number是要发送的个数

案例:获取写入的数据

// 获取时间
void PCF8563_get_clock(Clock_t *temp) {
    u8 p[7] = {0};
    u8 C;

    //    // 读数据
    I2C_ReadNbyte(PCF8563_DEV_ADDR, PCF8563_REG_TD, p, 7);
    //              8  4  2  1  8421码
    // 7  6  5  4   3  2  1  0  位置,从右往左算
    // y  y  y  y   x  x  x  x  内容占位
    //              y  y  y  y
    //              7  6  5  4

    // 秒的寄存器地址为: 0x02
    // 秒p[0]:  第0~3位记录个位,第4~6位记录十位
    temp->second = ((p[0] >> 4) & 0x07) * 10 + (p[0] & 0x0f);
    // 分p[1]: 第0~3位,保存个数,第4到6位,保存十位
    temp->minute = ((p[1] >> 4) & 0x07) * 10 + (p[1] & 0x0f);
    // 时p[2]:第0~3位,保存个数,第4到5位,保存十位
    temp->hour = ((p[2] >> 4) & 0x03) * 10 + (p[2] & 0x0f);
    // 日p[3]:第0~3位,保存个数,第4到5位,保存十位
    temp->day = ((p[3] >> 4) & 0x03) * 10 + (p[3] & 0x0f);
    // 周p[4]:第0~2位,保存个数
    temp->weekday = p[4];
    // 月_世纪p[5]:  第0~3位记录个位,第4位记录十位,第7位为0,世纪数为20xx,为1,世纪数为21xx
    temp->month = ((p[5] >> 4) & 0x01) * 10 + (p[5] & 0x0f);
    C = p[5] >> 7; //取出最高位
    // 年p[6]:第0~3位,保存个数,第4到7位,保存十位
    temp->year = ((p[6] >> 4) & 0x0f) * 10 + (p[6] & 0x0f) + (C == 0 ? 2000 : 2100);

}

示例:向RTC时钟发送一个时间,并接收时间

#include    "GPIO.h"
#include	"Delay.h"
#include 	"UART.h"	// 串口配置 UART_Configuration
#include 	"NVIC.h"	// 中断初始化NVIC_UART1_Init
#include 	"Switch.h"  // 引脚切换 UART1_SW_P30_P31
#include    "I2C.h"
void GPIO_config() { 
    GPIO_InitTypeDef info;
	// ===== UART1  P30  P31  准双向
    info.Mode = GPIO_PullUp; 				// 准双向
    info.Pin = GPIO_Pin_0 | GPIO_Pin_1;   	// 引脚
    GPIO_Inilize(GPIO_P3, &info);

    // P32  P33  开漏
    P3_MODE_OUT_OD(GPIO_Pin_2 | GPIO_Pin_3);
}
// 串口配置函数的定义
void UART_config(void) {
	// >>> 记得添加 NVIC.c, UART.c, UART_Isr.c <<<
    COMx_InitDefine		COMx_InitStructure;					//结构定义
    COMx_InitStructure.UART_Mode      = UART_8bit_BRTx;	//模式, UART_ShiftRight,UART_8bit_BRTx,UART_9bit,UART_9bit_BRTx
    COMx_InitStructure.UART_BRT_Use   = BRT_Timer1;			//选择波特率发生器, BRT_Timer1, BRT_Timer2 (注意: 串口2固定使用BRT_Timer2)
    COMx_InitStructure.UART_BaudRate  = 115200ul;			//波特率, 一般 110 ~ 115200
    COMx_InitStructure.UART_RxEnable  = ENABLE;				//接收允许,   ENABLE或DISABLE
    COMx_InitStructure.BaudRateDouble = DISABLE;			//波特率加倍, ENABLE或DISABLE
    UART_Configuration(UART1, &COMx_InitStructure);		//初始化串口1 UART1,UART2,UART3,UART4

  	NVIC_UART1_Init(ENABLE,Priority_1);		//中断使能, ENABLE/DISABLE; 优先级(低到高) Priority_0,Priority_1,Priority_2,Priority_3
    UART1_SW(UART1_SW_P30_P31);		// 引脚选择, UART1_SW_P30_P31,UART1_SW_P36_P37,UART1_SW_P16_P17,UART1_SW_P43_P44
}
// I2C 配置函数定义
/****************  I2C初始化函数 *****************/
void	I2C_config(void)
{
	I2C_InitTypeDef		I2C_InitStructure;

	I2C_InitStructure.I2C_Mode      = I2C_Mode_Master;	//主从选择   I2C_Mode_Master, I2C_Mode_Slave
	I2C_InitStructure.I2C_Enable    = ENABLE;			//I2C功能使能,   ENABLE, DISABLE
	I2C_InitStructure.I2C_MS_WDTA   = DISABLE;			//主机使能自动发送,  ENABLE, DISABLE
	I2C_InitStructure.I2C_Speed     = 13;				//总线速度=Fosc/2/(Speed*2+4),      0~63
                                                       // 400k, 24M => 13
	I2C_Init(&I2C_InitStructure);
	NVIC_I2C_Init(I2C_Mode_Master,DISABLE,Priority_0);	//主从模式, I2C_Mode_Master, I2C_Mode_Slave; 中断使能, ENABLE/DISABLE; 优先级(低到高) Priority_0,Priority_1,Priority_2,Priority_3
	I2C_SW(I2C_P33_P32);					//I2C_P14_P15,I2C_P24_P25,I2C_P33_P32
}
/*
                        设备地址  寄存器地址     数组首元素   数量
void I2C_WriteNbyte(u8 dev_addr, u8 mem_addr, u8 *p, u8 number);
void I2C_ReadNbyte(u8 dev_addr, u8 mem_addr, u8 *p, u8 number);
读操作,也是用写设备地址一样,因为I2C_ReadNbyte内部,会把写地址,转换为读地址
*/
void main() {
    u8 dev_addr = 0xa2;  // 设备地址,不要乱写,查手册
    u8 mem_addr = 0x02;  // 寄存器,写第一个就好,因为等会用数组操作,数组是连续的,后面地址会自动处理     
    u8 p[7] = {0};  // 写完整  年月日 星期几  时分秒
    u16 year; // 年,不是u8,是u16
    u8 month, day, weekday, hour, minute, second;
    u8 flag;  // 月的第七位

	EA = 1;         // 使能中断总开关
    EAXSFR();		/* 扩展寄存器访问使能 */

    GPIO_config(); // GPIO配置
	UART_config(); // 串口配置
    I2C_config(); // I2C 配置

    // ==============================写时间
    year = 2025, month = 8, day = 11;
    weekday = 1;  // 如果是星期日,是0
    hour = 23, minute = 59, second = 54;

    // 10进制  用 16进制表示    低4位放个位     高4位放10位
    //              8  4  2  1  8421码
    // 7  6  5  4   3  2  1  0  位置,从右往左算
    // y  y  y  y   x  x  x  x  内容占位
    
    // 秒的寄存器地址为: 0x02
    // 秒:  第0~3位记录个位,第4~6位记录十位
    //     十位                  个位
    p[0] = ((second / 10) << 4) +   (second%10);
    // printf("p[0] = %#x\n", (int)p[0]);
    // 分: 第0~3位,保存个数,第4到6位,保存十位
    p[1] = ((minute / 10) << 4) +   (minute%10);
    // 时:第0~3位,保存个数,第4到5位,保存十位
    p[2] = ((hour / 10) << 4) +   (hour%10);
    // 日:第0~3位,保存个数,第4到5位,保存十位
    p[3] = ((day / 10) << 4) +   (day%10);
    // 周:第0~2位,保存个数
    p[4] = weekday;
    // 月_世纪:  第0~3位记录个位,第4位记录十位,第7位为0,世纪数为20xx,为1,世纪数为21xx
    p[5] = ((month / 10) << 4) +   (month%10);
    // 月的第7位
    if (year >= 2100) { // 第7位置1
        p[5] |= (1 << 7);
    }  // 第7位置0,不处理就是0

    // 年:第0~3位,保存个数,第4到7位,保存十位
    // 2025  ===> 25 ==> 2 和 5
    //               2                      5
    // p[6] = ((year / 1000) << 4) +   (year % 10);
    p[6] = ((year / 10 % 10) << 4) +   (year % 10);
    // printf("%#x\n", (int)p[6]);
    I2C_WriteNbyte(dev_addr, mem_addr, p, 7);
	
    while (1){
        // 读时间
        I2C_ReadNbyte(dev_addr, mem_addr, p, 7);
        //              8  4  2  1  8421码
        // 7  6  5  4   3  2  1  0  位置,从右往左算
        // y  y  y  y   x  x  x  x  内容占位
        //      5           4
        // 0  0  0  0   1  1  1  1   &  ===> 得到低4位

        // 10进制  用 16进制表示    低4位放个位     高4位放10位
        // 秒: 第0~3位记录个位,第4~6位记录十位
        //           十位                 个位
        second = (p[0] >> 4) * 10 + (p[0] & 0x0f);
        // 分: 第0~3位,保存个数,第4到6位,保存十位
        minute = (p[1] >> 4) * 10 + (p[1] & 0x0f);
        // 时:第0~3位,保存个数,第4到5位,保存十位
        hour = (p[2] >> 4) * 10 + (p[2] & 0x0f);
        // 日:第0~3位,保存个数,第4到5位,保存十位
        day = (p[3] >> 4) * 10 + (p[3] & 0x0f);
        // 周:第0~2位,保存个数
        weekday = p[4]; // 如果是星期日,是0
        // 月_世纪:  第0~3位记录个位,第4位记录十位,第7位为0,世纪数为20xx,为1,世纪数为21xx
        // 处理第7位
        // 取出第7位
        flag = p[5] >> 7;
        // 第7位置0, 月的第7位,是年的标志位,不是月的有效数据
        p[5] &= ~(1 << 7);
        month = (p[5] >> 4) * 10 + (p[5] & 0x0f);
        // 年:第0~3位,保存个数,第4到7位,保存十位
        year = (p[6] >> 4) * 10 + (p[6] & 0x0f);
        if (flag == 1) year += 2100;
        else year += 2000;

        printf("%02d-%02d-%02d\n", (int)year, (int)month, (int)day);
        printf("weekday: %02d\n", (int)weekday);
        printf("%02d:%02d:%02d\n", (int)hour, (int)minute, (int)second);
		
        delay_ms(250); // 每隔1s 发送1次
		delay_ms(250);
		delay_ms(250);
		delay_ms(250);
    }
    
}

五、补充说明

  • 数据有效性规则:SDA线上的数据仅在SCL为高电平时有效,SCL为低电平时SDA可切换状态。
  • 多从设备场景:总线上可连接多个从设备,主设备通过地址区分目标设备,未被寻址的设备忽略总线数据。
  • 重复起始信号(Repeated Start):主设备可在一次通信中发送多个Start信号(无需先发送Stop),用于切换读写模式或寻址其他从设备,避免总线被其他设备抢占。
  • 错误处理:若从设备无应答(如未就绪),主设备可超时后重新发送Start信号重试,或发送Stop信号终止通信。

通过以上流程,I2C协议实现了主从设备间的双向数据传输,适用于短距离、低速率的外设通信(如传感器、EEPROM、OLED屏幕等)。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐