I2C 接收与发送数据的流程
·
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屏幕等)。
更多推荐

所有评论(0)