上拉/下拉电阻取值实战:同一颗 I2C,为什么别人 4.7kΩ 我得换 2.2kΩ 才稳
起因:一份”照抄官方 EVB”的原理图,翻车了
上个月接手一块 ToF 测距模组的板子,主控 STM32F103,I2C 跑标准模式 100kHz,挂 3 个从机(ToF、EEPROM、一颗 IO 扩展),中间过了 20cm 排线。
原理图基本是照着厂商 EVB 抄的:SCL/SDA 各一个 4.7kΩ 上拉到 3.3V。EVB 上跑得稳稳的,我这块板子读寄存器会随机返回 0xFF,大概每几十次读碰一次。
第一反应当然是软件。于是我在中断里加延时、把速率降到 50kHz、换了一套 I2C 库、把重试次数从 1 加到 5……两天没解决。后来借了台示波器,两个通道分别挂 SCL 和 SDA,问题一秒现形:
上升沿是弯的——从 0.3VDD 爬到 0.7VDD 用了差不多 1.4μs,而 SDA 已经在这之前被从机当成高电平采走了。
把上拉从 4.7kΩ 换成 2.2kΩ,同一块板子、同一套代码,波形立刻变陡,误码消失。
问题从来不是”4.7kΩ 到底好不好”,而是我这块板子的总线电容比 EVB 大一倍还多,而 4.7kΩ 是给 EVB 那套电容配的值。这就是我写这篇的原因:上拉电阻不是一个”抄个典型值”的事,它是一道有确定解法的三角账。
一句话结论:上拉电阻是三道约束的交集——上升沿时间(t_r ≈ 0.8473·R_p·C_b)、低电平灌电流(R_min = (VDD − VOL_max)/IOL)、静态功耗。”典型值”只是别人那套总线电容下的解,换板子就得重算。
先把上拉电阻的角色拆成两件事
I2C 是开漏(开集)总线,器件只能把线拉低,拉高这件事全交给上拉电阻。所以这个电阻同时干着两份工作:
- 它是一只手,负责把线拽回高电平。 手劲(阻值)不够,线就爬得慢,上升沿就软。这是”电流源”角色。
- 它和总线电容组成一个 RC 定时器,决定上升沿的形状。 下降沿由器件的开漏管决定,跟它没关系。这是”时间常数”角色。
很多人只记住第 1 点,于是觉得”上拉电阻大一点省电、小一点驱动强”,然后凭感觉选。真正决定误码的是第 2 点,因为 I2C 协议对上升沿时间是有硬性上限的,而这个上限换算回阻值,就成了 R 的上限。
算式一:上升沿时间,决定 R 的上限
I2C 规范把上升沿定义在 0.3VDD → 0.7VDD 这个窗口内。RC 充电的解析解就是那条 0.8473 的来历:
t_r ≈ 0.8473 · R_p · C_b
(R_p 单位 Ω,C_b 单位 F,t_r 单位 s)
反解一下,给定允许的上升沿时间和总线电容,R 的上限是:
R_max = t_r_max / (0.8473 · C_b)
各速率的 t_r 上限(来自 I2C-bus specification):
- 标准模式 100kHz:t_r ≤ 1000 ns
- 快速模式 400kHz:t_r ≤ 300 ns
- 快速模式+ 1MHz:t_r ≤ 120 ns
拿我那天的实测倒推:t_r = 1.4μs,R = 4.7kΩ → C_b = 1.4e-6 / (0.8473 × 4700) ≈ 350pF。
而 350pF 在 100kHz 下允许的最大上拉是 1000ns / (0.8473 × 350pF) ≈ 3.4kΩ。4.7kΩ 已经越界了,2.2kΩ 给出 0.8473 × 2200 × 350e-12 ≈ 652ns,留了三分之一余量——这就是那天换完立刻好的原因,一点都不玄学。
算式二:低电平灌电流,决定 R 的下限
上拉不是越大越省事,它还有个下限,来自 I2C 对 VOL 的要求:从机把线拉到低电平时,这个低电平必须足够低,才能被所有人识别成 0。而低电平的高低取决于 上拉电流有没有把开漏管灌到饱和。
R_min = (VDD − VOL_max) / IOL_max
规范给的判据(不同版本数字略有差异,按你这颗器件的数据手册对齐):
| 模式 | VOL_max | IOL | 折算 R_min @3.3V |
|---|---|---|---|
| 标准模式 100kHz | 0.4 V(固定值) | 3 mA | ≈ 967Ω |
| 快速模式 400kHz | 0.6 V(固定值) | 6 mA | ≈ 450Ω |
| 低压 1.8V | 0.2·VDD = 0.36V | 2 mA | ≈ 720Ω |
注意一个细节:R_min 和总线电容无关,只和电压、灌电流能力有关。所以它是一条水平线,而 R_max 是一条随电容急剧下降的斜线,可行区间就是两条线之间。
把两个算式放到一张表里:可行区间在哪
下表按 R_max = t_r_max/(0.8473·C_b) 计算,保留一个档位的标称电阻(E24 系列),并和 R_min ≈ 967Ω(3.3V 标准模式)取交集:
| VDD | 总线电容 C_b | 100kHz 的 R 上限 | 400kHz 的 R 上限 | 推荐标称(100k / 400k) |
|---|---|---|---|---|
| 3.3V | 100 pF | 11.8 kΩ | 3.5 kΩ | 4.7k / 2.2k |
| 3.3V | 200 pF | 5.9 kΩ | 1.8 kΩ | 4.7k / 1.5k |
| 3.3V | 300 pF | 3.9 kΩ | 1.2 kΩ | 3.3k / 1.0k |
| 3.3V | 400 pF | 2.9 kΩ | 0.88 kΩ(低于下限,无解) | 2.2k / 必须削电容 |
最后一行是这张表最值得看的地方:400kHz + 400pF 这个组合是数学上无解的——要满足上升沿就得把 R 压到 880Ω,而 880Ω 又满足不了 0.6V/6mA 的灌电流判据。这时候唯一的出路是把 C_b 砍下去(缩短走线、换低容器件、把排线改短),而不是继续试电阻。
为什么”别人的 4.7kΩ”到我这儿不够:C_b 是怎么悄悄变大的
C_b 从来不是一个能”看一眼原理图就知道”的数。它由下面这些东西加起来,而且每一项都在你抄原理图时被忽略:
| 来源 | 典型值 | 我这块板子的账 |
|---|---|---|
| 从机引脚电容 | 5–15 pF/个 | 3 个器件 ≈ 40 pF |
| PCB 走线 | 约 1–3 pF/cm | 约 15cm ≈ 30 pF |
| 连接器/排针 | 1–5 pF/脚 | 2 对 ≈ 8 pF |
| 20cm 排线 | 约 1 pF/cm | ≈ 20 pF |
| ESD/TVS 阵列(低容型) | 10–50 pF/路 | 2 路 ≈ 60 pF |
| 有源缓冲器/多路开关(若在总线上) | 5–10 pF | 0 |
| 示波器探头(测量时才加入) | 10–15 pF | +13 pF |
| 合计 | ≈ 170 pF(不含探头) |
等等,上面算出来才 170pF,可我实测反推是 350pF,差了整整一倍。差在哪?后来用 LCR 表一量才明白:排线那 20pF 是”相邻线之间的互容”,而排线到地、到屏蔽层的电容远不止这个量级,加上线长之后分布电容实际在 120pF 以上;TVS 阵列也不是低容型,实测每路 90pF 上下。两项一加,200pF 就出来了。
这件事给我的教训是:C_b 不要算,要测。算出来的只能当数量级参考,尤其涉及排线、连接器、TVS 的时候。
固件侧怎么量:一个二十行的小工具
没有示波器的时候,可以用固件测一个能用于横向比较的 t_r,再反推 C_b。做法是:把 SDA 脚从 I2C 复用切到 GPIO,用开漏拉低一段时间,然后释放,同时用定时器输入捕获量出”从释放到输入翻转”的时间。
/* bus_probe.c —— 用已知上拉 R 反推总线电容 C_b
* 全程整数运算:不用 %f/%g。
* 原因:newlib-nano 的浮点格式化会走 _dtoa_r(内部有堆分配),
* 而且 CubeIDE 的 Release 配置默认关掉 printf 浮点支持,
* snprintf("%f") 会编译零告警、运行期输出空串 —— 很难查。
*/
#define R_PULL_OHM 4700u /* 总线外部上拉,单位:欧姆 */
#define TIM_LSB_NS 1000u /* 定时器 1MHz 计数,1 LSB = 1000ns */
static TIM_HandleTypeDef htim2;
static volatile uint32_t g_tr_ns;
void bus_release_and_measure(void)
{
/* 0. 调用前把该脚 AFR 切回 0(脱离 I2C 复用),否则拉低无效 */
/* 1. 开漏拉低 SDA */
LL_GPIO_SetPinMode(SDA_PORT, SDA_PIN, LL_GPIO_MODE_OUTPUT);
LL_GPIO_ResetOutputPin(SDA_PORT, SDA_PIN);
for (volatile uint32_t i = 0; i < 200u; i++) { } /* >5us,确保拉到底 */
/* 2. 清计数,切浮空输入 = 释放总线,等输入翻转 */
__HAL_TIM_SET_COUNTER(&htim2, 0);
LL_GPIO_SetPinMode(SDA_PORT, SDA_PIN, LL_GPIO_MODE_INPUT);
while (LL_GPIO_IsInputPinSet(SDA_PORT, SDA_PIN) == 0u) { }
/* 3. 读计数换算成 ns */
g_tr_ns = (__HAL_TIM_GET_COUNTER(&htim2)) * TIM_LSB_NS;
}
/* C_b[pF] = t_r[ns] * 1e7 / (0.8473 * R[ohm])
* 0.8473 = 8473/10000,所以分子乘 10000 把小数消掉
* 例:t_r = 1400ns,R = 4700Ω → 1.4e10 / 3.98231e7 ≈ 351 pF */
uint32_t bus_cap_pf(uint32_t tr_ns, uint32_t r_ohm)
{
return (uint32_t)((uint64_t)tr_ns * 10000000ULL / (8473ULL * r_ohm));
}
必须说清楚的局限:MCU 的数字输入阈值大约是 0.5·VDD,不是 I2C 定义的 0.7·VDD,所以这样量出来的 t_r 偏小、反推的 C_b 偏小。它的正确用法是横向比较——换线缆、换批次、换从机型号、改走线之后各测一次,看趋势和跳变。要拿绝对值,还是得示波器按 0.3–0.7·VDD 口径量。
顺便,同一条公式反过来用,可以直接放进自检代码里,把”我该用多大电阻”算出来:
#define TR_MAX_NS_100K 1000u
#define TR_MAX_NS_400K 300u
/* R_max[ohm] = t_r[ns] * 1e7 / (0.8473 * C_b[pF]) */
#define R_MAX_OHM(tr_ns, cb_pf) \
((uint32_t)((uint64_t)(tr_ns) * 10000000ULL / (8473ULL * (cb_pf))))
/* R_min[ohm] = (VDD[mV] - VOL[mV]) / IOL[mA] → mV/mA = Ω */
#define R_MIN_OHM(vdd_mv, vol_mv, iol_ma) \
((uint32_t)((vdd_mv) - (vol_mv)) / (iol_ma))
/* 100kHz、C_b = 320pF、3.3V/0.4V/3mA:
* R_max = 1000 * 1e7 / (8473 * 320) = 3689 Ω → 标称取 3.3k
* R_min = (3300 - 400) / 3 = 967 Ω
* 可行区间 967 Ω ~ 3689 Ω;实测余量优先,选 2.2k */
顺带纠正一个误解:t_r 的公式里没有 VDD
很多人以为”电压低了上升沿就慢”,其实不是。t_r ≈ 0.8473·R·C_b 里没有 VDD——因为 I2C 定义的是 0.3VDD→0.7VDD 的比例窗口,阈值随 VDD 等比缩放,同样的 RC 在 1.8V 和 3.3V 上的上升沿时间是一样的。
把 3.3V 换成 1.8V,真正变的是另外两件事:
- 灌电流下限的绝对值变小了。 1.8V 时 VOL 判据约 0.2·VDD = 0.36V,IOL 只要 2mA,R_min 掉到 720Ω 出头,允许的上拉比 3.3V 更小。
- 漏电流的相对权重变大了。 同样 1μA 的引脚漏电,等效到 1.8V 总线上相当于一个 1.8MΩ 的下拉,对”这只 4.7kΩ 的手”的削弱比例更大;加上噪声容限的绝对幅度只剩 3.3V 系统的 55%。
所以我的取舍是:1.8V 系统里反而把上拉取小一档(4.7k → 2.2k),用更强的驱动换更硬的上升沿和更大的抗扰余量。这和”低压要省电所以加大电阻”的直觉是反的,但实测更稳。
别忽略的四个工程细节
第一,内部上拉不是上拉。 STM32 的内部上拉典型值 40kΩ 上下。40kΩ 并联 4.7kΩ 等于 4.24kΩ,几乎没影响;但如果你指望内部上拉单独带一条 I2C,那就是在拿 40kΩ 给 300pF 充电,t_r 会飙到 10μs 量级,100Hz 都跑不动。I2C 一律用外部电阻。
/* I2C 引脚配置:开漏是必须的,内部上拉不要开 */
GPIO_InitTypeDef g = {0};
g.Pin = SCL_PIN | SDA_PIN;
g.Mode = GPIO_MODE_AF_OD; /* 复用开漏,I2C 的命根子 */
g.Pull = GPIO_NOPULL; /* 外部已有上拉,内部不再叠加 */
g.Speed = GPIO_SPEED_FREQ_HIGH; /* 下降沿由它决定,别用 LOW */
HAL_GPIO_Init(GPIOB, &g);
第二,容差和批次。 5% 的 4.7kΩ 上沿能到 4.94kΩ,直接吃掉 5% 的 t_r 余量;再叠上温漂和 VDD 偏差,原本算得很紧的设计就会在高温批次上翻车。总线电容这种事,本来就没法算准,所以标准做法是留 30% 以上的 t_r 余量,别按理论值卡着选。
第三,上拉不等于端接。 上拉电阻解决的是”把线拽高”,不解决”边沿反射”。线一长(>20cm)上升沿又会因为反射出现回沟和振铃,这时候要靠串阻(源端 22–33Ω)或改拓扑,光调上拉是调不出来的。
第四,ESD 器件的结电容要算进 C_b。 我这次就是栽在这——为了过 ESD 在 SCL/SDA 上加了 TVS 阵列,每路 90pF,两条线 180pF 直接砸进总线电容。低容型 TVS(结电容 1–5pF)贵一点,但 I2C 上用普通型往往得不偿失。这一点在ESD/浪涌防护那篇里也提过,选型时一定要把结电容当硬指标看。
下拉电阻:同一个道理,另一套坑
下拉的逻辑完全一样——只不过换成”默认把线拽到低”,所以下拉的取值由下降沿时间和被驱动为高时的灌电流共同决定。落到具体场景:
BOOT / 配置引脚。 STM32 的 BOOT0 是上电瞬间采样,只在复位期间需要确定的电平,所以 10kΩ 下拉足够,采样完成后完全不耗电。这类”只在某一时刻需要确定状态”的脚,阻值可以放得很宽。
SPI 的 CS。 从机片选是低有效,上电时主控引脚还没初始化(高阻),如果 CS 悬空,从机可能被噪声误选中执行垃圾指令。所以 CS 要加 10kΩ 上拉——它只在上电那几十毫秒起作用,正常工作期间主控把 CS 拉低,此时流过 10k 的电流是设计内的(3.3V/10k = 330μA,短暂且正常)。
复位脚。 一般 10kΩ 上拉 + 100nF 到地。要注意的是下拉能力:如果板上还有复位监控芯片或调试器,它拉低时要能把这个 10k 上拉压下去(3.3V/10k = 330μA,大多数复位芯片没问题),但如果上拉用 1kΩ 就会顶不住。同时 100nF 和 10kΩ 组成 τ = 1ms 的 RC,上电释放会慢一点,有些严格的从机等不起,需要相应缩小电容。
推挽对推挽就是设计事故。 两个都是推挽输出的引脚被外部的上拉/下拉连在一起,或者一个推挽高、一个推挽低直接短接,不是”电流大一点”的问题,是发热和可靠性问题。开漏 + 上拉才是安全的”线与”。
低功耗设备的隐藏账单:上拉在”被拉低期间”一直烧电
上拉电阻在下拉电阻这件事上有一个容易误解的地方:总线空闲(线为高)时,上拉不耗电。所以 I2C 的 4.7kΩ 上拉在待机时是零成本,这是它可以放心用小的原因。
真正让人亏电的是下面这几种”常态被拉低”的布置:
| 场景 | 阻值 | 常态/有效期间电流 | 说明 |
|---|---|---|---|
| I2C 空闲(线为高) | 4.7kΩ | ≈ 0 | 上拉不耗电,放心 |
| 开漏中断常态有效(低有效信号长期拉低) | 10kΩ | 330 μA | 一天 7.9mAh,纯白扔 |
| 分压式电平转换 5V→3.3V | 10k + 20k | 167 μA | 一天 4mAh,换专用转换器 |
| 外部上拉 10k + MCU 内部下拉 40k(配置失误) | 等效 50k | 66 μA | 两边都没想清楚就会撞上 |
| 上电误导通,两个外部电阻串成分压 | 10k + 10k | 165 μA | 加串阻或改单端驱动 |
那个”外部上拉 + 内部下拉”的组合是我在别人板子上真的见过的:I2C 引脚配成了普通输入,还顺手开了内部下拉,结果外部 4.7k 上拉和内部 40k 下拉常态分压,总线电平被拉到 2.9V 左右——既有几微安到几十微安的常态漏电,又把噪声容限削掉了一截,两个问题一起来。查的时候很容易只看外部电阻,不去翻 GPIO 配置。
低功耗场合的替代路线:
- 低有效中断:改成”常态有效”的反向逻辑,或者用 100kΩ 上拉 + 固件边沿中断 + 消抖,把常态电流压到 μA 级(代价是边沿变软,长线慎用)。
- 电平转换:分压是最省钱的方案,也是最费电的方案。静态电流 nA 级的专用转换器贵不到一块钱,电池设备上基本必选。
- 按键:把外部下拉改成”内部上拉 + 按键到地”,只在按下时耗电,闲时零成本;同时按下期间 3.3V/40k ≈ 83μA,比 10k 外部上拉的 330μA 省得多。
这一段的账和低功耗 Tickless 那篇是连着的:休眠做到 9μA 之后,这种”设计里没人算过”的 66μA 就是最大的漏点。
一张落地速查表
| 场景 | 取值建议 | 理由 |
|---|---|---|
| I2C 100kHz,C_b ≤ 200pF | 4.7kΩ | t_r ≈ 800ns,余量充足 |
| I2C 400kHz,C_b ≤ 200pF | 1.5k–2.2kΩ | t_r ≈ 250–370ns,且高于 R_min |
| I2C 挂在排线上(C_b ≥ 300pF) | 2.2kΩ 并把速率降到 100kHz | 先削电容,再谈阻值 |
| SPI 的 CS(低有效) | 10kΩ 上拉 | 只在上电瞬间起作用 |
| BOOT0 / 配置脚 | 10kΩ 下拉(按手册) | 只在复位期间采样 |
| 复位脚 | 10kΩ 上拉 + 100nF | 注意下拉能力和 RC 释放时间 |
| 开漏中断(低有效) | 10kΩ 长线 / 100kΩ 短距 | 功耗与抗扰的取舍 |
| 1.8V 电压域 I2C | 比 3.3V 再小一档 | 漏电权重更大,R_min 更低 |
| 开漏 + 上拉做线与 | 按上面 I2C 同法算 | 永远不要两个推挽对接 |
12 项上拉/下拉自查清单
- ☐ 每个开漏总线的 t_r 都按 0.8473·R·C_b 算过,且留了 30% 以上余量
- ☐ C_b 是”测过”的,不是只算了器件引脚电容(排线、连接器、TVS 必须计入)
- ☐ 上拉取值同时满足 R_min = (VDD − VOL_max)/IOL_max
- ☐ 检查过”400kHz + 400pF”这类无解组合,确认没有硬凑
- ☐ ESD/TVS 器件用的是低容型(I2C 上用普通型是常见坑)
- ☐ I2C 引脚配的是 GPIO_MODE_AF_OD + NOPULL,没有叠内部上拉/下拉
- ☐ 所有”默认需要确定状态”的脚(CS/BOOT/复位/使能)都有上拉或下拉
- ☐ 复位脚的 RC 释放时间与从机的上电时序要求核对过
- ☐ 上电瞬间(引脚高阻期)的电平逐个过过一遍,没有会导致误动作的悬空脚
- ☐ 低功耗场景的常态电流算过:开漏中断、分压转换、内外部电阻打架
- ☐ 上拉电阻的容差(建议 1%)和温漂纳入了 t_r 余量
- ☐ 用示波器按 0.3–0.7·VDD 口径实测过 t_r,而不是只看理论值
写在最后
上拉电阻是我见过的”最不起眼、最容易抄、也最容易翻车”的元件。它的取值其实一点都不玄学——两个公式、一条水平线、一条斜线,交集就是答案。真正花时间的全在前面那步:把 C_b 测出来,而不是算出来。
那次翻车之后我改了个习惯:任何开漏总线在画板之前,先把 C_b 估一遍,把上限和下限算出来写进原理图备注。图上写一个 4.7kΩ,和图旁写一行”100kHz,C_b≈350pF,上限 3.4k,取 2.2k”,是两种完全不同水平的硬件设计。
下一篇预告:《EFT/群脉冲测试与整改实战:EMC 静态测试全过,一上群脉冲就复位——这次我把干扰的”注入口”和”逃逸口”分别找了出来的过程记了下来》——把电快速瞬变脉冲群(IEC 61000-4-4)从耦合路径、共模/差模注入、到固件级看门狗与状态机恢复,一条线捋清楚。
上期文章:FreeRTOS 低功耗 Tickless 实战。相关:I2C 总线避坑实战。