I2C(Inter-Integrated Circuit,即串行外设接口)是一种用于在电路板内连接微控制器和外部设备的串行通信协议。它的作用包括:
-
连接外部设备:I2C协议能够连接多种外部设备,如传感器、存储器、显示屏等,使它们能够与主控制器(如微处理器或微控制器)进行通信和交换数据。
-
简化线路设计:由于是串行通信协议,I2C相比并行接口需要更少的引脚,有助于简化电路板设计和布线。
-
数据传输效率:尽管是串行通信,I2C支持高达400 kHz的速度,且还有更高速率的变体,例如Fast Mode(最高1 MHz)和High-Speed Mode(最高3.4 MHz),能够满足不同设备的通信需求。
-
主从设备结构:I2C使用主从设备结构,主设备负责发起通信并控制总线访问,而从设备则响应主设备的命令并提供数据或执行操作。
-
广泛应用:由于其灵活性和广泛支持,I2C被广泛应用于各种电子设备和嵌入式系统中,特别是需要连接多个外设并进行数据交换的场景,如传感器网络、显示屏控制、电源管理等。
/********************************************************************************** * I2C 延时函数 * 这是一个宏定义,用于产生微小的延时。它初始化一个volatile类型的变量i,并在一个while循环中递减,直到i变为0 * * @param 无 * @return 无 **********************************************************************************/ #define I2c_delay() { volatile unsigned char i = 1; while (i) i--; }
/********************************************************************************** * I2C GPIO初始化 * 初始化I2C的GPIO端口。首先启用GPIOB时钟,设置SCL和SDA的管脚模式为开漏输出,速度为50MHz * * @param 无 * @return 无 **********************************************************************************/ void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); // 启用GPIOB端口的时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE); // 启用I2C1端口的时钟 GPIO_InitStructure.GPIO_Pin = SCL_PIN | SDA_PIN; // 设置SCL和SDA引脚 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置GPIO速度为50MHz GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 设置GPIO模式为开漏输出 GPIO_Init(IIC_GPIO, &GPIO_InitStructure); // 初始化GPIO }
* 选择开漏输出
* 在 I2C 通信中,选择开漏输出(Open-Drain Output)模式主要是为了支持总线上多个设备之间的数据和时钟信号共享,以及确保信号的完整性和稳定性。
*
* 多主控支持:I2C协议支持多主控配置,即多个设备可以控制总线。使用开漏输出,任何一个设备都可以拉低共享线路(如SDA或SCL),而不影响其他设备
* 防止短路冲突:如果两个设备尝试同时发送不同的逻辑级别(一个设备发送逻辑高,另一个发送逻辑低),开漏配置可以防止电源与地之间的直接短路。如果输出被配置为推挽(Push-Pull),则可能导致短路
* 信号电平控制:通过使用外部上拉电阻,可以确保当线路上没有设备主动拉低时,线路能够安全地回到高电平状态。这允许任何设备能够读取到稳定的高电平状态,除非另一个设备明确将其拉低
* 灵活的电压匹配:由于I2C设备可能工作在不同的电压水平,开漏输出允许这些设备在不直接连接电源输出的情况下共享同一总线。上拉电阻可以连接到适合所有设备的适中电压水平,从而确保总线的兼容性和可靠性
*
* 对于I2C通信,GPIO口通常配置为开漏输出模式(GPIO_Mode_Out_OD),这是因为I2C总线内部使用的是漏极开路输出驱动器。
* 开漏输出模式允许SDA和SCL引脚被外部设备拉低为低电平,同时当不需要输出时,引脚处于高阻态,通过上拉电阻保持高电平。/********************************************************************************** * 发送I2C起始信号 * 首先将SDA和SCL拉高,检查SDA是否正确读取为高电平,然后拉低SDA,再次检查是否读取为低,最后拉低SCL * * @param 无 * @return 成功返回SUCCESS,失败返回FAILED **********************************************************************************/ static uint8_t I2c_Start(void) { SDA_H; // SDA线拉高 SCL_H; // SCL线拉高 I2c_delay(); if (!SDA_read) // 如果SDA读取不到高电平,则返回失败 return FAILED; SDA_L; // SDA线拉低 I2c_delay(); if (SDA_read) // 如果SDA读取到高电平,则返回失败 return FAILED; SCL_L; // SCL线拉低 I2c_delay(); if (SCL_read) // 如果SCL读取到高电平,则返回失败 return FAILED; return SUCCESS; // 返回成功 } // 发送I2C起始信号,通常在开始一个新的通信之前调用 /********************************************************************************** * 发送I2C停止信号 * 先将SCL拉低,SDA拉低,然后将SCL拉高,最后将SDA拉高 * * @param 无 * @return 无 **********************************************************************************/ static void I2c_Stop(void) { SCL_L; // SCL线拉低 I2c_delay(); SDA_L; // SDA线拉低 I2c_delay(); I2c_delay(); SCL_H; // SCL线拉高 I2c_delay(); SDA_H; // SDA线拉高 I2c_delay(); } // 发送I2C停止信号,通常在通信结束后调用 /********************************************************************************** * 产生I2C应答信号 * 将SCL和SDA拉低,然后将SCL拉高,保持一段时间后拉低 * * @param 无 * @return 无 **********************************************************************************/ /********************************************************************************** * 延时多次是为了满足以下要求 * * 确保信号稳定性:当你更改 SDA(数据线)或 SCL(时钟线)的状态时,需要一定的时间让这些变化在物理线路上稳定下来。在电路中,由于存在电容效应,信号变化需要一段时间才能从一个电平稳定到另一个电平。延时确保在读取或设置信号之前,线路上的电压级别已经稳定 * 遵循设备的时序要求:I2C 设备通常有明确的时序要求,比如时钟线高电平和低电平的最小持续时间。这些时序要求是为了确保设备能够正确地检测到每一个信号的边缘和状态。多次延时有助于确保这些时序要求得到满足,尤其是在处理速度较快或电路响应较慢的系统中 * 协调主从设备之间的通信:延时可以帮助协调主设备和从设备之间的通信节奏,确保从设备有足够的时间来响应主设备的请求。在发送应答信号时,从设备需要将数据线拉低,主设备则通过时钟线来读取这一状态。适当的延时确保从设备准备就绪,能够在主设备读取之前稳定输出信号 * 防止信号碰撞:在多主模式下,延时有助于防止信号碰撞。当多个主设备尝试同时访问总线时,适当的延时可以避免数据冲突和总线错误 **********************************************************************************/ static void I2c_Ack(void) { SCL_L; // SCL线拉低 I2c_delay(); SDA_L; // SDA线拉低 I2c_delay(); SCL_H; // SCL线拉高 I2c_delay(); I2c_delay(); I2c_delay(); I2c_delay(); SCL_L; // SCL线拉低 I2c_delay(); } // 产生一个I2C应答信号,通常在接收数据后调用以通知发送方数据已接收 /********************************************************************************** * 产生I2C非应答信号 * 与应答信号类似,但在应答阶段SDA保持高电平 * * @param 无 * @return 无 **********************************************************************************/ static void I2c_NoAck(void) { SCL_L; // SCL线拉低 I2c_delay(); SDA_H; // SDA线拉高 I2c_delay(); SCL_H; // SCL线拉高 I2c_delay(); I2c_delay(); I2c_delay(); I2c_delay(); SCL_L; // SCL线拉低 I2c_delay(); } // 产生一个I2C非应答信号,通常在最后一个数据接收后调用以通知发送方停止发送数据 /********************************************************************************** * 等待应答信号到来 * 等待从设备的应答。检查从设备在SCL为高时是否拉低SDA * * @param 无 * @return 成功返回SUCCESS,失败返回FAILED **********************************************************************************/ static uint8_t I2c_WaitAck(void) { SCL_L; // SCL线拉低 I2c_delay(); SDA_H; // 释放SDA线 I2c_delay(); SCL_H; // SCL线拉高 I2c_delay(); I2c_delay(); I2c_delay(); if (SDA_read) { // 如果SDA线为高,则应答失败 SCL_L; return FAILED; } SCL_L; // SCL线拉低 return SUCCESS; // 应答成功 } // 等待从设备的应答信号。如果从设备已正确接收到数据,它将拉低SDA线
这些是I2C设备收发信号必要的基础应答条件
/********************************************************************************** * 发送一个字节数据 * 发送一个字节的数据到I2C总线上。这个函数将数据从最高位到最低位逐位发送 * * @param byte: 发送的数据 * @return 无 **********************************************************************************/ /********************************************************************************** * i为什么等于8 * 在 I2C 通信协议中,数据是按字节(8 位)进行传输的。因此,为了发送一个字节的数据,需要发送 8 位。为了实现这个过程,计数器 i 初始化为 8,这样在循环中每次处理一位数据,同时将 i 减 1,直到所有 8 位数据都发送完毕 * * 这里的 byte & 0x80 用来检查数据的最高位(第 7 位)是否为 1。0x80 的二进制表示是 10000000,它的作用是通过按位与操作筛选出 byte 中的最高位。如果最高位是 1,那么 byte & 0x80 的结果就不为 0;如果最高位是 0,那么结果就是 0 * * 在 I2C 协议中,数据是通过 SDA 线进行传输的,具体如下: * 数据位为 1 时:SDA 线要被拉高,表示传输的这一位是 1 * 数据位为 0 时:SDA 线要被拉低,表示传输的这一位是 0 * 这种方式是标准的 I2C 通信协议,通过 SDA 线的电平变化来传输数据位 **********************************************************************************/ static void I2c_SendByte(uint8_t byte) { uint8_t i = 8; while (i--) { SCL_L; // 拉低时钟线SCL I2c_delay(); // 延时等待数据稳定 if (byte & 0x80) // 检查数据最高位 SDA_H; // 如果是1,则SDA线高电平 else SDA_L; // 如果是0,则SDA线低电平 byte <<= 1; // 数据左移一位,准备发送下一位 I2c_delay(); // 再次延时 SCL_H; // 拉高时钟线,表示可以读取数据位 I2c_delay(); // 延时确保数据被读取 I2c_delay(); I2c_delay(); } SCL_L; // 一字节发送完毕,拉低时钟线准备停止或发送应答 } // 发送一个字节的数据到I2C总线上。这个函数将数据从最高位到最低位逐位发送 /********************************************************************************** * 读取一个字节数据 * 读取一个字节的数据从I2C总线上。这个函数从最高位到最低位逐位读取数据 * * @param 无 * @return 读取到的数据 **********************************************************************************/ /********************************************************************************** * byte |= 0x01 有啥用 * 这个代码段用于读取 I2C 总线上的数据,并将其存储到变量 byte 中 * 这一行代码用于设置 byte 变量的最低位。通过使用按位或操作符 |=, 如果 SDA 线的状态为高(1),则设置 byte 变量的最低位为 1。如果 SDA 线的状态为低(0),则不改变 byte 变量的最低位。 * * 具体操作如下: * byte |= 0x01 等价于 byte = byte | 0x01 * 0x01 的二进制表示是 00000001 * 通过按位或操作,如果 byte 原本最低位为 0,则将其设置为 1。如果最低位原本为 1,则保持为 1 **********************************************************************************/ static uint8_t I2c_ReadByte(void) { uint8_t i = 8; uint8_t byte = 0; SDA_H; // 释放数据线,准备接收数据 while (i--) { byte <<= 1; // 已接收的数据左移,为新位腾出空间 SCL_L; // 拉低时钟线开始新一位的数据传输 I2c_delay(); I2c_delay(); SCL_H; // 拉高时钟线,读取数据 I2c_delay(); I2c_delay(); I2c_delay(); if (SDA_read) { // 读取SDA线状态 byte |= 0x01; // 如果SDA为高,设置当前位为1 } } SCL_L; // 读取完毕,拉低时钟线 return byte; // 返回接收到的字节 } // 读取一个字节的数据从I2C总线上。这个函数从最高位到最低位逐位读取数据 /********************************************************************************** * 从指定I2C设备的某个寄存器读取多个字节 * * @param addr: 目标设备的I2C地址 * @param reg: 目标寄存器的地址 * @param data: 用于接收数据的缓冲区指针 * @param len: 要读取的数据长度 * @return 成功返回SUCCESS,失败返回FAILED **********************************************************************************/ int8_t IIC_read_Bytes(uint8_t addr,uint8_t reg,uint8_t *data,uint8_t len) { if (I2c_Start() == FAILED) // 发送起始信号 return FAILED; I2c_SendByte(addr); // 发送设备地址 if (I2c_WaitAck() == FAILED) { I2c_Stop(); return FAILED; } I2c_SendByte(reg); // 发送寄存器地址 I2c_WaitAck(); I2c_Stop(); // 发送停止信号,结束写入过程 I2c_Start(); // 重新启动I2C准备读取数据 I2c_SendByte(addr+1); // 发送设备地址,进入读取模式 if (I2c_WaitAck() == FAILED) { I2c_Stop(); return FAILED; } while (len) { *data = I2c_ReadByte(); // 读取数据 if (len == 1) I2c_NoAck(); // 最后一个字节发送NACK else I2c_Ack(); // 其他字节发送ACK data++; len--; } I2c_Stop(); // 发送停止信号 return SUCCESS; } /********************************************************************************** * 向设备的某一个地址写入固定长度的数据 * * @param addr: 目标设备的I2C地址 * @param reg: 目标寄存器的地址 * @param data: 指向要发送数据的指针 * @param len: 要写入的数据长度 * @return 成功返回SUCCESS,失败返回FAILED **********************************************************************************/ int8_t IIC_Write_Bytes(uint8_t addr,uint8_t reg,uint8_t *data,uint8_t len) { int i; if (I2c_Start() == FAILED) // 发送起始信号,检测是否成功 return FAILED; I2c_SendByte(addr); // 发送设备地址 if (I2c_WaitAck() == FAILED) { // 等待应答 I2c_Stop(); return FAILED; } I2c_SendByte(reg); // 发送寄存器地址 I2c_WaitAck(); for (i = 0; i < len; i++) { // 循环发送数据 I2c_SendByte(data[i]); if (I2c_WaitAck() == FAILED) { I2c_Stop(); return FAILED; } } I2c_Stop(); // 发送完毕,发送停止信号 return SUCCESS; } /********************************************************************************** * 从指定I2C设备的某个寄存器读取单个字节 * * @param addr: 目标设备的I2C地址 * @param reg: 目标寄存器的地址 * @return 读取到的数据 **********************************************************************************/ int8_t IIC_Read_One_Byte(uint8_t addr,uint8_t reg) { uint8_t recive = 0; if (I2c_Start() == FAILED) // 发送起始信号 return FAILED; I2c_SendByte(addr); // 发送设备地址 if (I2c_WaitAck() == FAILED) { I2c_Stop(); return FAILED; } I2c_SendByte(reg); // 发送寄存器地址 I2c_WaitAck(); I2c_Stop(); // 发送停止信号,结束写入过程 I2c_Start(); // 重新启动I2C准备读取数据 I2c_SendByte(addr+1); // 发送设备地址,进入读取模式 if (I2c_WaitAck() == FAILED) { I2c_Stop(); return FAILED; } recive = I2c_ReadByte(); // 读取一个字节 I2c_NoAck(); I2c_Stop(); return recive; } /********************************************************************************** * 向指定设备的指定寄存器写入一个字节 * * @param addr: 目标设备的I2C地址 * @param reg: 目标寄存器的地址 * @param data: 要写入的单个字节数据 * @return 成功返回SUCCESS,失败返回FAILED **********************************************************************************/ int8_t IIC_Write_One_Byte(uint8_t addr,uint8_t reg,uint8_t data) { if (I2c_Start() == FAILED) // 发送起始信号 return FAILED; I2c_SendByte(addr); // 发送设备地址 if (I2c_WaitAck() == FAILED) { I2c_Stop(); return FAILED; } I2c_SendByte(reg); // 发送寄存器地址 I2c_WaitAck(); I2c_SendByte(data); // 发送数据 I2c_WaitAck(); I2c_Stop(); // 发送停止信号 return SUCCESS; }