为什么需要理解 GPIO 模式

使用 STM32 时,CubeMX 或 HAL 库给 GPIO 提供了 8 种模式:

1
2
3
4
5
6
7
8
GPIO_MODE_INPUT          输入浮空
GPIO_MODE_INPUT_PULLUP 输入上拉
GPIO_MODE_INPUT_PULLDOWN 输入下拉
GPIO_MODE_ANALOG 模拟输入
GPIO_MODE_OUTPUT_PP 推挽输出
GPIO_MODE_OUTPUT_OD 开漏输出
GPIO_MODE_AF_PP 复用推挽
GPIO_MODE_AF_OD 复用开漏

选错模式轻则不工作,重则烧引脚。理解每种模式的内部电路,才能正确选型。

一、输出模式

1.1 推挽输出(Push-Pull)

1
2
3
4
5
6
7
8
9
     VDD

┌─── P-MOS ───┐ 输出高电平:P-MOS 导通, N-MOS 截止 → VDD
│ │
│ ┌──────┤─── GPIO
│ │ │
└─── N-MOS ───┘ 输出低电平:N-MOS 导通, P-MOS 截止 → GND

GND

工作原理:两个 MOS 管交替导通,”推”(P-MOS 送 VDD)和”挽”(N-MOS 拉 GND),输出端始终是确定的电平。

1
2
3
4
5
6
HAL 配置:
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;

寄存器操作:
GPIOx->CRL |= (0x2 << pin*4); // 输出模式, 推挽
GPIOx->ODR |= (1 << pin); // 输出高

特点

特性 说明
驱动能力 强,通常 ±25mA/引脚
电平 明确的高/低,无中间态
速度 快,可达 50MHz
线与 ❌ 不支持,两个推挽输出不能短接

典型应用

  • LED 驱动(LED_GPIO_Port → LED_Pin
  • 控制继电器/蜂鸣器
  • 普通数字信号输出

1.2 开漏输出(Open-Drain)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
         VDD                 VDD
│ │
│ 外接上拉电阻 │
│ 4.7KΩ │
┌───────────┤ │
│ │ │
│ ┌────┴────── GPIO │
│ │ │
└─── N-MOS ───┐ │
│ │ │
GND │

输出高:N-MOS 截止 → 上拉电阻拉到 VDD
输出低:N-MOS 导通 → 拉到 GND

工作原理:只有 N-MOS 管,输出高电平时靠外部上拉电阻拉到 VDD。

1
2
3
4
5
6
HAL 配置:
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
// 注意:必须外接上拉电阻或在内部使能上拉

寄存器操作:
GPIOx->CRL |= (0x6 << pin*4); // 输出模式, 开漏

特点

特性 说明
驱动能力 高电平由上拉电阻决定(弱),低电平由 N-MOS 决定(强)
电平转换 上拉电阻接 5V 即可驱动 5V 设备(3.3V STM32 → 5V 外设)
线与 ✅ 多个开漏输出可直接并接,任一拉低即低
速度 受上拉电阻影响,RC 充放电限制频率

典型应用

  • I2C 总线(SDA/SCL 必须开漏,靠外部上拉)
  • 1-Wire(DS18B20 单总线)
  • 多设备共享信号线(多个芯片的 INT 中断引脚并接)
  • 5V 电平转换(上拉电阻接 5V)
1
2
3
4
5
6
7
8
9
10
为什么 I2C 必须用开漏:

SDA ──┬────┬────┬──── VDD
│ │ │ │
4.7K DEV1 DEV2 DEV3
│ │ │ │
GND OD OD OD

所有设备都是开漏 → 任一设备可以拉低 SDA → 实现多主仲裁
如果推挽,两个设备同时输出不同电平 → 短路烧毁

二、输入模式

2.1 浮空输入

1
2
3
4
5
6
7
8
9
10
11
12
             GPIO

┌─────────┤
│ │
ESD 输入缓冲器 ──→ 内部数据总线
│ │
└─────────┤

GND

无上拉/下拉,引脚电平完全由外部信号决定。
悬空时电平不确定(受电磁干扰会乱跳)。
1
2
3
HAL 配置:
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 无上下拉
GPIO_InitStruct.Pull = GPIO_NOPULL;

2.2 上拉输入

1
2
3
4
5
6
7
8
9
10
11
12
         VDD

内部上拉 (~40KΩ)

├────── GPIO ──── 输入缓冲器

外部按键

GND

默认状态(按键未按下):上拉电阻 → GPIO 读到高电平
按键按下:GPIO 直接接地 → 读到低电平
1
2
3
HAL 配置:
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;

典型应用

  • 按键检测(按键接地,默认高电平,按下变低)
  • I2C 空闲保持高电平
  • UART RX 防悬空乱收数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
按键接线(上拉输入):

VDD ──── 内部上拉 ~40KΩ

├──── GPIO ──── MCU

┌──┤
│ │
│按键
│ │
└──┤

GND

按下 → 低电平 松开 → 高电平(上拉)

2.3 下拉输入

1
2
3
4
5
6
7
8
9
10
11
12
         外部按键

VDD

├────── GPIO ──── 输入缓冲器

内部下拉 (~40KΩ)

GND

默认状态(按键未按下):下拉电阻 → GPIO 读到低电平
按键按下:GPIO 直接接 VDD → 读到高电平
1
2
3
HAL 配置:
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;

典型应用

  • 按键检测(按键接 VDD,默认低电平,按下变高)
  • 使能引脚(默认禁用,外接高电平使能)
1
2
3
4
5
6
7
8
9
10
11
按键接线(下拉输入):

VDD

├──── 按键 ──── GPIO ──── MCU
│ │
│ 内部下拉 ~40KΩ
│ │
│ GND

按下 → 高电平 松开 → 低电平(下拉)

三、上拉 vs 下拉:按键怎么选

上拉输入 下拉输入
默认电平
按键接法 按键 → GND 按键 → VDD
抗干扰 较好(高电平驱动强) 一般
省电 无按键时上拉电阻耗电 无按键时下拉电阻耗电
业界惯例 更常用 较少

经验法则:按键优先用上拉输入。几乎所有 MCU 的 GPIO 默认状态都是上拉或浮空,下拉支持较弱。STM32 每个 GPIO 内部上拉约 40KΩ。

四、推挽 vs 开漏:输出怎么选

场景 选推挽 选开漏
LED 也能用
普通 IO 控制 ❌(需外加电阻)
I2C ✅ 必须
多设备共享线 ✅ 线与
电平转换 3.3→5V ✅ 上拉接 5V
高速信号 ✅ 50MHz 慢(RC 限制)

五、HAL 库配置速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
GPIO_InitTypeDef GPIO_InitStruct = {0};

/* === 推挽输出(LED, 蜂鸣器, 普通 IO) === */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

/* === 开漏输出(I2C, 电平转换) === */
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 可选内部上拉,但通常外加 4.7K
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

/* === 上拉输入(按键接 GND) === */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// 读取:HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_0) == GPIO_PIN_RESET → 按下

/* === 下拉输入(按键接 VDD) === */
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// 读取:HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_1) == GPIO_PIN_SET → 按下

六、常见错误

现象 原因 解决
I2C 通信失败 配置成推挽输出 改为 GPIO_MODE_AF_OD
按键读到乱跳 浮空输入,未加上拉 改为 GPIO_PULLUP
LED 亮度很暗 开漏输出未加上拉电阻 改为推挽,或外接上拉
引脚发热 两个推挽输出短接 检查电路,改用开漏
悬空引脚电平乱跳 未用引脚配置为浮空输入 设为模拟模式 GPIO_MODE_ANALOG