IIC总线的硬件部分的两个关键点:开漏输出+上拉电阻
大家好,我是良许。
在嵌入式开发中,IIC(I2C)总线可以说是最常用的通信协议之一了。
无论是读取传感器数据、控制EEPROM存储器,还是与各种外设进行通信,IIC总线都扮演着重要角色。
但很多初学者在使用IIC时,往往只关注软件层面的时序和协议,却忽略了硬件层面的关键设计。
今天我就来聊聊IIC总线硬件部分的两个核心要点:开漏输出和上拉电阻。
理解了这两点,你才能真正掌握IIC总线的精髓。
1. IIC总线的基本结构
在深入讲解之前,我们先简单回顾一下IIC总线的基本构成。
IIC总线只需要两根信号线就能实现多主机、多从机之间的通信,这两根线分别是:
- SCL(Serial Clock):时钟线,由主机产生时钟信号
- SDA(Serial Data):数据线,用于主从设备之间的数据传输
一条IIC总线上可以挂载多个设备,每个设备都有唯一的地址。
这种简洁的设计让IIC总线在嵌入式系统中广受欢迎,特别是在PCB布线空间有限的场景下。
但问题来了:多个设备共用同一根数据线和时钟线,它们是如何避免冲突的呢?这就要说到IIC总线硬件设计的核心机制了。
2. 开漏输出:IIC总线的灵魂
2.1 什么是开漏输出
开漏输出(Open-Drain)是IIC总线最核心的硬件特性。
要理解开漏输出,我们先来看看常见的GPIO输出模式。
在普通的推挽输出(Push-Pull)模式下,GPIO引脚可以主动输出高电平(通过上管导通)或低电平(通过下管导通)。
这种模式下,引脚能够提供较强的驱动能力,但有个致命问题:如果两个推挽输出的引脚连接在一起,一个输出高电平,另一个输出低电平,就会造成短路,可能烧毁芯片。
而开漏输出则不同,它的内部结构只有一个下拉的NMOS管,没有上拉的PMOS管。这意味着:
- 当GPIO输出低电平时,NMOS管导通,引脚被拉到地(GND),呈现低电平
- 当GPIO输出高电平时,NMOS管截止,引脚呈现高阻态(既不输出高也不输出低)
这种"只能拉低,不能拉高"的特性,正是开漏输出的精髓所在。
2.2 开漏输出的优势
你可能会问:只能拉低不能拉高,这不是很鸡肋吗?恰恰相反,这正是IIC总线能够实现多设备共享总线的关键。
第一个优势:线与逻辑
多个开漏输出连接在同一根线上时,会形成"线与"(Wired-AND)逻辑。
只要有任何一个设备输出低电平,整条总线就是低电平;只有当所有设备都输出高阻态时,总线才能被上拉电阻拉到高电平。
这种特性在IIC总线中至关重要。
比如在多主机系统中,如果两个主机同时发送数据产生冲突,通过检测总线电平,主机可以发现冲突并进行仲裁。
发送"1"的主机如果检测到总线为"0",就知道有其他主机在发送数据,会主动放弃总线控制权。
第二个优势:电平转换
开漏输出配合上拉电阻,可以轻松实现不同电压域之间的电平转换。
比如一个3.3V的MCU和一个5V的传感器通信,只需要将上拉电阻接到5V电源,就能实现电平匹配。
3.3V的MCU输出低电平时可以将总线拉低,输出高阻态时总线被上拉到5V,这个5V电平不会损坏MCU(因为MCU引脚是高阻态,没有电流流入)。
第三个优势:避免总线冲突
在推挽输出模式下,如果两个设备同时驱动总线,一个输出高一个输出低,就会造成短路。
而开漏输出永远不会主动输出高电平,最多只是高阻态,因此不会产生短路风险。
2.3 STM32中的开漏配置
在STM32中配置IIC引脚为开漏输出非常简单。
使用HAL库的话,代码如下:
void MX_I2C1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* 使能GPIOB时钟 */
__HAL_RCC_GPIOB_CLK_ENABLE();
/* 配置IIC引脚:PB6(SCL), PB7(SDA) */
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 复用开漏输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用内部上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* 配置IIC外设 */
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 100kHz标准速率
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
HAL_I2C_Init(&hi2c1);
}
注意代码中的 GPIO_MODE_AF_OD,这就是配置为复用功能的开漏输出模式。
同时 GPIO_NOPULL 表示不使用芯片内部的上下拉电阻,因为我们需要外部上拉电阻。
3. 上拉电阻:开漏输出的最佳拍档
3.1 为什么需要上拉电阻
前面提到,开漏输出只能拉低电平,不能主动输出高电平。
那么高电平从哪里来呢?答案就是上拉电阻。
上拉电阻一端连接到电源(通常是VCC),另一端连接到IIC总线。
当所有设备的开漏输出都处于高阻态时,上拉电阻会将总线"拉"到高电平。
当任何一个设备输出低电平时,由于低电平的驱动能力远强于上拉电阻,总线会被拉到低电平。
可以把上拉电阻想象成一根弹簧,总是试图把总线拉到高电平。
而开漏输出就像一只手,需要的时候可以把总线按下去(拉低),松开手(高阻态)时弹簧就会把总线弹回高电平。
3.2 上拉电阻的阻值选择
上拉电阻的阻值选择是个技术活,选大了选小了都不行。
阻值太小的问题:
如果上拉电阻太小(比如1kΩ),虽然可以提供很强的上拉能力,但会带来两个问题:
- 功耗增加。当总线被拉低时,会有较大的电流从VCC经过上拉电阻流向GND,计算公式为I=VCC/Rpullup。以3.3V系统为例,1kΩ电阻会产生3.3mA的电流,在低功耗应用中这是不可接受的。
- 增加驱动负担。开漏输出需要吸收更大的电流才能将总线拉低,可能超出芯片的驱动能力。
阻值太大的问题:
如果上拉电阻太大(比如100kΩ),上拉能力会变弱,带来的问题是:
- 上升沿变慢。总线电容(包括走线电容、引脚电容等)需要通过上拉电阻充电才能从低电平变为高电平。阻值越大,充电时间越长,上升沿越慢。时间常数可以用 τ=R×C 计算。
- 抗干扰能力下降。较弱的上拉能力使得总线更容易受到外部干扰的影响。
合适的阻值范围:
一般来说,IIC总线的上拉电阻推荐范围是:
- 标准速率(100kHz):4.7kΩ ~ 10kΩ
- 快速模式(400kHz):2.2kΩ ~ 4.7kΩ
- 高速模式(3.4MHz):需要更精确的计算,通常在1kΩ左右
最常用的值是4.7kΩ,这是一个经过实践检验的经验值,在大多数应用场景下都能良好工作。
3.3 上拉电阻的计算方法
如果你想精确计算上拉电阻的阻值,可以使用以下公式。首先需要确定总线电容 Cbus,它包括:
- 走线电容(约10pF/cm)
- 每个设备的引脚电容(数据手册会标明,通常5~10pF)
- 其他寄生电容
假设IIC总线时钟频率为 fSCL,上升时间要求为tr
(标准模式下最大1000ns,快速模式下最大300ns),则上拉电阻的最大值为:

同时,为了保证足够的驱动能力,上拉电阻的最小值需要满足:

其中 VOL(max) 是输出低电平的最大值(通常0.4V),IOL是开漏输出的最大吸收电流(查阅芯片手册)。
举个实际例子,假设:
- 总线电容 Cbus=100pF
- 上升时间要求 tr=1000ns(标准模式)
- 电源电压 VCC=3.3V
- 最大吸收电流 IOL=3mA
则:

因此上拉电阻应该选择在1kΩ到11.8kΩ之间,选择4.7kΩ是非常合适的。
3.4 多个上拉电阻并联的情况
在实际应用中,有时候会遇到多个模块都带有上拉电阻的情况。
比如你的主板上有上拉电阻,外接的传感器模块上也有上拉电阻。
这时候多个电阻会并联,等效电阻会变小。
两个电阻并联的等效电阻计算公式为:

比如两个4.7kΩ的电阻并联,等效电阻为:

这个值仍然在合理范围内,但如果并联的电阻太多,等效电阻可能会过小,导致功耗增加。
因此在设计时,建议只在主板上放置上拉电阻,外接模块上不要再加上拉电阻。
如果模块已经有上拉电阻,可以考虑用0欧电阻或跳线帽来选择性地启用。
4. 实际应用中的注意事项
4.1 上拉电阻的位置
上拉电阻应该尽量靠近主控芯片放置,而不是分散在各个从设备附近。
这样可以减少总线的寄生电容,提高信号质量。
在多层PCB中,建议将IIC走线放在内层,并在下方铺设完整的地平面,以减少干扰。
4.2 长距离传输的考虑
IIC总线本来是为板级通信设计的,传输距离通常在几厘米到几十厘米之间。
如果需要长距离传输(超过1米),需要特别注意:
- 降低通信速率,比如从400kHz降到100kHz甚至更低
- 使用更小的上拉电阻(但不要小于最小值)
- 考虑使用IIC总线扩展芯片或差分信号方案
- 增加滤波电容,提高抗干扰能力
4.3 调试技巧
在调试IIC通信问题时,可以用示波器观察SCL和SDA信号。正常情况下应该看到:
- 高电平接近VCC,低电平接近0V
- 上升沿呈指数曲线(RC充电曲线),下降沿陡峭
- 没有明显的振铃或过冲
如果上升沿太慢,说明上拉电阻太大或总线电容太大;如果有振铃,可能需要增加串联电阻或并联电容进行阻尼。
4.4 软件模拟IIC的配置
有时候我们需要用GPIO模拟IIC(比如硬件IIC引脚被占用了),这时候也要配置为开漏输出。
示例代码如下:
/* 初始化模拟IIC的GPIO */
void Soft_I2C_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
/* 配置SCL和SDA为开漏输出 */
GPIO_InitStruct.Pin = I2C_SCL_PIN | I2C_SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct);
/* 初始状态设为高电平(实际是高阻态) */
HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET);
}
/* 读取SDA电平 */
uint8_t I2C_SDA_Read(void)
{
return HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN);
}
/* 设置SDA为低电平 */
void I2C_SDA_Low(void)
{
HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET);
}
/* 设置SDA为高电平(高阻态) */
void I2C_SDA_High(void)
{
HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET);
}
注意在读取SDA电平时,要先将SDA设为高阻态(输出高电平),然后再读取引脚状态。
这样才能正确读取从设备发送的应答信号。
5. 总结
IIC总线的硬件设计看似简单,实则蕴含着精妙的设计思想。
开漏输出和上拉电阻这两个关键点,共同构成了IIC总线多设备共享、双向通信的基础。
开漏输出提供了"线与"逻辑,使得多个设备可以安全地共享同一根总线,避免了总线冲突的风险。
而上拉电阻则为开漏输出提供了高电平,同时还能实现电平转换、限制电流等功能。
两者配合,才能让IIC总线稳定可靠地工作。
在实际应用中,正确选择上拉电阻的阻值、合理布局PCB、注意信号完整性,都是保证IIC通信质量的关键。
希望通过今天的讲解,能让大家对IIC总线有更深入的理解,在以后的项目中少走弯路。
如果你在使用IIC总线时遇到通信不稳定、速率上不去等问题,不妨从硬件层面入手,检查一下是不是开漏输出配置不对,或者上拉电阻选择不合适。
很多时候,硬件问题比软件问题更隐蔽,但一旦找到根源,解决起来反而更简单。
更多编程学习资源
浙公网安备 33010602011771号