Skip to content

GPIO 与高低电平

GPIO 是 MCU 与外部世界交互的最基本方式。本章从物理层的"高低电平"出发,系统讲解 GPIO 的输入输出模式及其在 HAL 库中的操作方式。

上一篇:时钟树与总线架构


1. 概念——数字世界的"开关"

1.1 高低电平的物理含义

在数字电路中,一切信息最终都归结为电压

电压
 │  3.3V ────────────────  高电平 (High / 1 / SET)
 │
 │  0V   ────────────────  低电平 (Low  / 0 / RESET)
 └────────────────────────────→ 时间
  • 高电平:电压接近供电电压(STM32 通常为 3.3V),逻辑上记为 1
  • 低电平:电压接近 0V(GND),逻辑上记为 0
  • 阈值:实际判断并非严格的 3.3V/0V,而是有一个区间。STM32 的输入阈值大约在 1.0V~2.0V 之间,中间区域是"不确定区"。

1.2 GPIO 引脚的本质

每个 GPIO 引脚内部有一个推挽电路(两个 MOSFET 开关),可以将其切换到 VDD 或 GND:

输出高电平:              输出低电平:
    VDD(3.3V)               VDD(3.3V)
       │                        │
    ┌──┴──┐                 ┌──┴──┐
    │ PMOS│ ON              │ PMOS│ OFF
    └──┬──┘                 └──┬──┘
       │                        │
       ├──────→ 引脚 3.3V       ├──────→ 引脚 0V
       │                        │
    ┌──┴──┐                 ┌──┴──┐
    │ NMOS│ OFF             │ NMOS│ ON
    └──┬──┘                 └──┬──┘
       │                        │
      GND                      GND

2. GPIO 的工作模式

2.1 四大模式速览

模式 方向 用途 内部上/下拉
推挽输出 (Push-Pull) 输出 驱动 LED、控制信号 不需要
开漏输出 (Open-Drain) 输出 I2C 总线、电平转换 需外部上拉
浮空输入 (Floating Input) 输入 读取按键(需外部上/下拉) 无,引脚悬空时电平不确定
上拉/下拉输入 (Pull-Up/Down) 输入 读取按键、外部中断触发 内置电阻上/下拉

2.2 推挽输出——"自己出力"

推挽输出可以主动输出高电平和低电平,驱动能力强,适合直接驱动 LED 等负载。

推挽输出是日常使用中最常见的输出模式,原因很直接:两个 MOSFET 一推一拉,无论输出高还是低,引脚都有低阻抗通路连接到 VDD 或 GND,信号切换速度极快,驱动能力也强(典型值 8~25mA)。

但推挽输出有一个致命缺陷:设想两个推挽输出引脚直接连在一起,一个输出高电平(PMOS 导通到 VDD),另一个输出低电平(NMOS 导通到 GND)——VDD 和 GND 之间形成了一条近乎零电阻的通路:

致命短路路径:
    VDD
     │
  ┌──┴──┐  ON  ← GPIO_A 输出高
  └──┬──┘
     │
     ├────────── 短路!大电流直接流入
     │
  ┌──┴──┐  ON  ← GPIO_B 输出低
  └──┬──┘
     │
    GND

这一瞬间会有数百毫安甚至更大的电流流过两个引脚,足以烧毁 MCU 的 IO 驱动管。这正是在总线上必须使用开漏输出的根本原因——在多设备共享的信号线上,如果每个设备都用推挽输出,只要有任意两个设备输出状态不同,就会发生短路。

// 推挽输出:直接点亮 LED
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);   // LED 亮
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); // LED 灭

2.3 开漏输出——"只拉低,不拉高"

开漏输出只能主动输出低电平(拉低),不能主动输出高电平。要输出高电平,需要外部上拉电阻将引脚拉到 VDD。

开漏输出结构:
    VDD ──[上拉电阻]──┬── 引脚
                      │
                  ┌───┴───┐
                  │ NMOS   │  ← 只有一个下拉管
                  └───┬───┘
                      │
                     GND

NMOS 导通 → 引脚被拉到 GND → 低电平
NMOS 截止 → 上拉电阻把引脚拉到 VDD → 高电平

开漏输出的核心优势在于安全性:多个开漏输出可以直接并接在同一根线上,不会产生短路。

工作方式是"线与"(Wired-AND):任意一个设备拉低,整条线就是低电平;只有所有设备都释放,上拉电阻才能把线拉回高电平。这正是 I2C、One-Wire 等总线能够支持多设备共享的根本原理。

开漏输出还天生支持电平转换:上拉电阻接到 5V,引脚就能输出 5V 逻辑;上拉接到 3.3V,引脚就输出 3.3V 逻辑。这比使用专用电平转换芯片灵活得多,成本几乎为零。

但天下没有免费的午餐,开漏输出也有代价:

  • 上升沿慢:高电平时,全靠上拉电阻对引脚和走线的寄生电容充电。RC 时间常数决定了信号从低到高的速度。上拉电阻越大功耗越小,但上升沿越慢。典型 I2C 标准模式(100kHz)可以正常工作,但要跑到快速模式(400kHz 甚至 1MHz),就必须减小上拉电阻,精细权衡速度与功耗。
  • 高电平驱动能力弱:输出高电平时,电流只能从上拉电阻过来,通常仅几百微安,远不如推挽输出的数十毫安。换言之,开漏输出的高电平只能"传递信号",不能"驱动负载"。
  • 低电平时持续耗电:当引脚输出低电平时,VDD → 上拉电阻 → NMOS → GND 这条通路一直在走电流,功耗无法避免。

这就解释了为什么不同协议选择不同的输出方式:

协议 输出方式 原因
SPI 推挽输出 点对点通信,只有一主一从,不存在冲突风险;需要高速(MHz 级别),推挽的强驱动能力正合适
I2C 开漏输出 多设备共享一条总线,必须避免短路;速度要求相对低(100kHz~1MHz),开漏的上升沿问题在可接受范围内
UART 推挽输出 点对点,不需要多主,推挽最简单可靠

开漏输出的关键应用:

  1. I2C 总线:多设备共享同一根线,必须用开漏避免冲突
  2. 电平转换:上拉到 5V 即可输出 5V 信号,实现 3.3V MCU 驱动 5V 设备

2.4 浮空输入的隐患——"悬空的引脚不是0,是未知数"

初学者常有一个误区:引脚什么都不接,读到的不就是低电平吗?

实际上,CMOS 输入端的输入阻抗极高(通常在兆欧级别),几乎不从外部汲取电流。这听起来是好事,但意味着引脚对外界电场极度敏感——它像一根天线,电磁干扰、旁边的信号线切换、甚至手指靠近,都能在引脚上感应出微小的电压变化。

后果可能比你想象的更严重:

  • 随机跳变:悬空引脚的电压在高低阈值之间飘忽不定,读取的值一会儿是 0 一会儿是 1。如果这个引脚接到了外部中断线,就会产生大量虚假中断,CPU 被无意义地打断。
  • 振荡与功耗:当引脚电压停留在 CMOS 输入缓冲器的中间区域(既不是高也不是低)时,缓冲器内部的 PMOS 和 NMOS 可能同时部分导通,形成从 VDD 到 GND 的漏电流。此类"crowbar current"虽然单个引脚不大,但多个悬空引脚加起来会使 MCU 的静态功耗显著增加。
  • 按键的典型翻车:把按键一端接引脚、另一端接 GND,但没有配置上拉电阻。按下时引脚被拉到 GND 读到 0,逻辑正确;但松开后引脚悬空,可能读到 0 也可能读到 1——反复横跳,按键"鬼触"。

解决方案就是上下拉电阻,用一个确定的电阻路径把悬空的引脚"锚定"在某个电平上。

2.5 上拉/下拉输入

当引脚配置为输入时,如果外部没有明确的电平驱动,引脚处于"悬空"状态,电平不确定(容易受电磁干扰影响)。STM32 内部集成了可配置的上拉/下拉电阻(约 40kΩ):

上拉输入:                下拉输入:
    VDD                       │
     │                        │
  [~40kΩ]                     │
     │                     [~40kΩ]
     ├─── 引脚                 ├─── 引脚
     │                        │
    默认高电平              GND 默认低电平

典型应用——按键检测:

// 按键一端接 GND,另一端接引脚(配置为上拉输入)
// 未按下 → 上拉电阻使其保持高电平
// 按下   → 引脚被直接接到 GND → 低电平
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {
    // 按键被按下(低电平有效)
}

上拉还是下拉,怎么选?

实践中上拉输入远比下拉输入常见,背后有清晰的工程逻辑:

  • 大量控制信号默认使用低电平有效(Active-Low):典型的如片选(CS)、中断(INT)、复位(RST)。芯片内部的逻辑是:正常工作时这些引脚被上拉电阻保持为高;需要触发时,任意设备将其拉低。低电平有效的设计还有一个安全考量——上电瞬间引脚自然被上拉电阻拉到高,保证芯片不会在上电过程中被误触发。
  • 开漏输出天然只能拉低:在开漏总线(如 I2C)的另一端,接收设备也通常配置为上拉输入。开漏输出只能产生"低电平"和"释放(高阻)"两种状态,上拉输入恰好与之完美配合。
  • 下拉输入的存在价值:有些信号约定为高电平有效(如某些使能引脚 EN),此时用下拉输入保证默认关闭状态,避免上电时意外使能。下拉输入还在一些需要"线与高"逻辑的场景中有用。

归根结底,选择上拉还是下拉,取决于默认状态应该是什么。不按按键时读到高(上拉),还是读到低(下拉)——让默认值符合系统的安全状态。


3. HAL 库 GPIO API

3.1 基本操作

// 写引脚(输出高/低电平)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);    // PA5 = 高电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);  // PA5 = 低电平

// 翻转引脚电平
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);

// 读引脚(返回当前电平状态)
GPIO_PinState state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5);

3.2 CubeMX 中的 GPIO 配置

在 CubeMX 中,每个引脚可以配置以下属性:

配置项 可选值 含义
GPIO mode Input / Output 输入或输出模式
GPIO Pull-up/Pull-down No pull / Pull-up / Pull-down 内部上下拉
GPIO output level Low / High 初始输出电平
Maximum output speed Low / Medium / High / Very High 输出翻转速度
User Label 自定义名称 给引脚起名,生成宏定义

CubeMX 生成的代码(gpio.c):

void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    __HAL_RCC_GPIOA_CLK_ENABLE();   // 使能 GPIOA 时钟

    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);  // 初始低电平

    GPIO_InitStruct.Pin = GPIO_PIN_5;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;       // 推挽输出
    GPIO_InitStruct.Pull = GPIO_NOPULL;               // 无上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;      // 低速
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

4. GSRL 中的 GPIO 使用场景

4.1 SPI 的片选信号(CS)

SPI 通信中,主设备通过 CS 引脚(低电平有效)选择从设备。GSRL 中 BMI088 IMU 的加速度计和陀螺仪分别使用不同的 CS 引脚:

// dvc_imu.hpp 中 BMI088 的 SPI 配置
struct SPIConfig {
    SPI_HandleTypeDef *hspi;      // SPI 句柄
    GPIO_TypeDef *csGPIOGroup;    // CS 引脚 GPIO 组
    uint16_t csGPIOPin;           // CS 引脚编号
};

实际操作时:

// 拉低 CS → 选中从设备
HAL_GPIO_WritePin(csGPIOGroup, csGPIOPin, GPIO_PIN_RESET);
// ... SPI 传输数据 ...
// 拉高 CS → 释放从设备
HAL_GPIO_WritePin(csGPIOGroup, csGPIOPin, GPIO_PIN_SET);

4.2 外部中断引脚

IMU 的 INT1 引脚连接到 STM32 的外部中断线,当 IMU 数据准备好时触发中断

// stm32f4xx_it.c 中的外部中断处理
void EXTI4_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(INT1_ACCEL_Pin);  // HAL 处理,最终调用回调
}

配置此类引脚时,CubeMX 中需将 GPIO mode 设为 External Interrupt Mode with Rising/Falling edge


5. 实践要点

场景 做法 原因
驱动 LED 推挽输出 + 串联限流电阻 LED 需要主动驱动电流
按键检测 上拉输入(按键接 GND) 避免悬空,无按键时稳定为高
I2C 通信 开漏输出 + 外部上拉 4.7kΩ 多设备共享,避免总线冲突
读取传感器就绪信号 上拉输入 + 外部中断 悬空时电平不确定,中断误触发
控制大电流负载 GPIO → 三极管/MOSFET → 负载 MCU 引脚驱动能力有限(约 8mA)

5.1 MCU 引脚驱动能力为什么有限?

STM32 的单个 IO 引脚通常只能提供约 8~25mA 的驱动电流(不同型号略有差异),总量也有上限(比如整颗芯片所有 IO 的总电流不能超过 150mA)。这不是厂商偷工减料,而是三方面的物理限制:

  1. 芯片散热:几十个引脚同时输出数十毫安,焦耳热(I²R)会让芯片温度急剧上升。封装的热传导能力有限,没有强力散热手段的话,芯片可能过热损坏。
  2. 键合线(Bond Wire)的瓶颈:芯片内部的硅片(Die)通过极细的金线或铝线(直径约 25μm)连接到外部引脚。这些"键合线"的载流能力有限,一根细细的金属丝承载不了大电流,会像保险丝一样熔断。
  3. 意外短路的自我保护:调试时用示波器探针不小心把输出和 GND 短接、杜邦线误插——这些是嵌入式开发的日常。有限的驱动电流意味着短路时不会立即烧毁引脚,给了排查和纠正的窗口。

正因为如此,凡是需要控制电机、继电器、电磁阀、大功率 LED、加热丝等大电流负载,绝对不能用 GPIO 直接驱动。必须通过三极管、MOSFET 或专用驱动芯片来"放大"电流,GPIO 只负责输出微弱的控制信号。


6. 总结

要点 说明
高电平 接近 3.3V,逻辑 1
低电平 接近 0V,逻辑 0
推挽输出 可主动输出高/低电平,驱动能力强
开漏输出 只能拉低,需外部上拉电阻实现高电平
上拉输入 内部电阻拉到 VDD,默认为高电平
下拉输入 内部电阻拉到 GND,默认为低电平
HAL API WritePin / ReadPin / TogglePin

下一篇:中断与 NVIC

作者: Qing | 修改日期: 2026-07-18