GPIO(General Purpose Input/Output)配置的目标,是让一个实际引脚按照电路要求采样、驱动或交给外设控制。应先确定芯片型号、引脚连接与有效极性,再选择寄存器和初始化顺序。电源、复位及专用引脚并不都能改作普通 GPIO。
本页以 STM32F1 为配置主线,比较部分 F4、F7、H7 的差异。关于下拉输入停在 1.x V、内部电流路径和原图分压计算,见 STM32F1 GPIO 电平与排查。
推挽与开漏:名称和电流路径
跳转到“推挽与开漏:名称和电流路径”典型 CMOS 推挽输出级可抽象为:
VDD ── P-MOS 的 S(源极) D(漏极)──┐ ├── 引脚 D(漏极)──┘VSS ── N-MOS 的 S(源极)两个栅极受内部驱动逻辑控制,使上管和下管按要求工作。上管向负载提供电流、拉高引脚,下管吸收电流、拉低引脚,这有助于理解“推挽”。这里的源漏位置限于这个输出级模型,不是“凡电源端都是源极”的通用 MOS 管辨识法;也不应把载流子方向和常规电流方向混为一谈。
开漏把下管的漏极作为输出端,未用内部上管主动拉高。漏极仍连接引脚,并非这个端子永远悬空;下管导通时吸收电流,关断时才释放节点。外部上拉、其他器件和负载共同决定释放后的电压。
| 项目 | 推挽 | 开漏 |
|---|---|---|
| 输出 0 | 主动拉低 | 主动拉低 |
| 输出 1 | 主动拉高 | 释放,由外部电路决定高电平 |
| 多个输出接同一节点 | 不能把互相冲突的高、低输出直接相连 | 电压、漏电、灌电流和协议条件满足时可共享 |
| 典型用途 | 单源数字输出、外设时钟或数据、有限电流的指示灯 | 共享低有效中断、常见 I²C、适当设计的线逻辑 |
| 边沿 | 取决于驱动等级、负载与走线 | 上升沿通常还受上拉与电容限制 |
多个开漏输出构成共享节点时,任何一个拉低都会使该节点为低。在正逻辑中,这称为“线与”;对于低有效请求,也可理解为任一请求都能使信号有效。它不意味着多个设备可以不遵守时序和仲裁就任意发送数据。
开漏也不是自动电平转换器。把上拉电源从 3.3 V 改到 5 V 之前,必须核对所有连接引脚的耐压、输入阈值、供电状态、注入电流和保护结构;不满足时应采用适当的电平转换电路。LED 要有限流措施,继电器、电机等负载需要合适的驱动级,不能用“推挽驱动强”替代电流与功耗设计。
第一步:确认所属总线并打开端口时钟
跳转到“第一步:确认所属总线并打开端口时钟”GPIO 寄存器访问需要对应端口时钟。默认状态应查具体芯片,而不是概括成“所有外设时钟都关闭”。STM32 系列也不是共用同一条 GPIO 总线。
| 本页核对的器件范围 | GPIO 时钟使能寄存器 |
|---|---|
| RM0008 对应 STM32F1 | RCC_APB2ENR,如 IOPAEN |
| RM0090 对应 STM32F4 | RCC_AHB1ENR,如 GPIOAEN |
| ST STM32F7 LL 驱动所列端口 | RCC_AHB1ENR |
| RM0433 对应 H742/H743/H753/H750 | RCC_AHB4ENR,如 GPIOAEN |
原笔记把“F7/H7”统一列为 AHB1,不能用于 H743 等本页列出的 H7。依据分别为 RM0008、RM0090、F7 官方 LL 总线定义和 RM0433。
F1 打开 GPIOA 的三种表达属于不同代码层次,工程中选择所用库的方式即可:
/* CMSIS 设备寄存器:F1 */RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;(void)RCC->APB2ENR; /* 读回,随后才访问 GPIO */
/* F1 标准外设库(SPL) */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* F1 HAL */__HAL_RCC_GPIOA_CLK_ENABLE();F1 的 RCC_APB2ENR 位于 RCC 基址加 0x18,即 RM0008 映射中的 0x40021018;bit 0 为 AFIOEN,bit 2~6 对应 GPIOA~GPIOE 的使能。端口是否实际存在、哪些引脚引出,仍受型号和封装限制。
第二步:用正确寄存器表达模式
跳转到“第二步:用正确寄存器表达模式”F1:一个引脚四个位
跳转到“F1:一个引脚四个位”CRL 管理 pin 0~7,CRH 管理 pin 8~15。对引脚 ,选中寄存器内的移位为 ,其四位字段按 CNF[1:0] MODE[1:0] 排列。
| MODE | 含义 |
|---|---|
| 00 | 输入配置 |
| 01 | 输出,10 MHz 速度档 |
| 10 | 输出,2 MHz 速度档 |
| 11 | 输出,50 MHz 速度档 |
| CNF | MODE=00 时 | MODE≠00 时 |
|---|---|---|
| 00 | 模拟输入 | 普通推挽输出 |
| 01 | 浮空输入 | 普通开漏输出 |
| 10 | 上下拉输入,由 ODR 选择方向 | 复用推挽输出 |
| 11 | 保留 | 复用开漏输出 |
这对应旧 SPL 的八类常用名称:AIN、IN_FLOATING、IPU、IPD、Out_PP、Out_OD、AF_PP、AF_OD。浮空输入是“不用内部偏置”,不表示外部已驱动的引脚电压必然不确定。模拟模式关闭相应数字输入缓冲与输出路径,不宜夸大为“整个引脚周边所有数字电路关闭”。
PA5 位于 CRL 的 bit 20~23。例如 50 MHz 普通推挽为 0x3,同档普通开漏为 0x7;上下拉输入为 0x8,还需设置 ODR 为 1(上拉)或 0(下拉)。F1 输出模式没有可独立启用的内部上下拉。
F4 示例:分离的字段
跳转到“F4 示例:分离的字段”RM0090 的配置由多个寄存器共同表达:
| 寄存器 | 每引脚位数 | 编码要点 |
|---|---|---|
| MODER | 2 | 00 输入、01 普通输出、10 复用、11 模拟 |
| OTYPER | 1 | 0 推挽、1 开漏 |
| OSPEEDR | 2 | 00 Low、01 Medium、10 High、11 Very high |
| PUPDR | 2 | 00 无上下拉、01 上拉、10 下拉、11 保留 |
| AFRL/AFRH | 4 | 选择复用功能编号,按实际引脚复用表查询 |
这不是“F4 以后所有系列都完全一样”的版本规则。这里仅核对上述器件范围,迁移到其他 MCU 时仍应重新检查模式、时钟、速度编码及复位值。
例如把一个两位字段从旧值改为新值,应先清除掩码再写入新值:
/* F4:PA5 设置为普通输出;其余配置应先完成 */GPIOA->MODER = (GPIOA->MODER & ~(3u << 10u)) | (1u << 10u);单独 |= 不能可靠地把任意旧值改成目标值。上述读改写也不具备跨线程或中断的配置事务原子性;初始化阶段应由一个明确的所有者配置该端口。
第三步:先准备初始值,再启用输出级
跳转到“第三步:先准备初始值,再启用输出级”下面假设 F1 的 PA5 接一个低有效 LED:VDD → 限流电阻 → LED → PA5,高电平为不点亮。示例不适用于引脚已被其他外设占用的板子。
#include "stm32f1xx.h" /* 工程还需选择实际器件宏 */
void Led_Init(void){ RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; (void)RCC->APB2ENR;
GPIOA->BSRR = 1u << 5u; /* 先预装 ODR=1 */ GPIOA->CRL = (GPIOA->CRL & ~(0xFu << 20u)) | (0x2u << 20u); /* 普通推挽,2 MHz 档 */}
void Led_On(void){ GPIOA->BSRR = 1u << (5u + 16u);}
void Led_Off(void){ GPIOA->BSRR = 1u << 5u;}BSRR 低 16 位用于置位,高 16 位用于清零;F1 的 BRR 也可清零相应位。直接写 BSRR 可避免 ODR |= ... / ODR &= ... 的读改写被中断打断、丢失其他位更新的问题,但不会自动解决多个执行者对同一输出含义的争用。
若沿用原来的 SPL 工程,应先 GPIO_SetBits() 预装不动作电平,再 GPIO_Init() 配置 GPIO_Mode_Out_PP 与所需速度。HAL 中对应先 HAL_GPIO_WritePin(),再 HAL_GPIO_Init()。这些顺序减少模式切换时的脉冲,不能保证软件运行之前的复位窗口;该阶段还需电路偏置和上电波形验证。
第四步:速度按电气时序选择
跳转到“第四步:速度按电气时序选择”F1 的 2/10/50 MHz 是输出速度档,并不是写入后引脚就会按该频率振荡。CPU 访问、外设时钟、负载电容、供电及走线共同约束实际波形。F4 的速度名称也不能简单换算成所有电压、负载下都可达到“100 MHz”。
选择能满足接收端建立保持、上升下降时间要求的档位,再检查过冲、振铃及干扰。没有测量条件时,不应机械规定“所有 SPI 用 50 MHz、所有 UART 用 10 MHz”。按键等输入不使用输出速度字段;旧 SPL 结构体中填该成员不等于它改变输入采样速度。
原来的“速度越高,功耗一定越高”也过于简单。负载的动态功耗还与电容、电压和实际翻转频率相关,驱动配置不能替代完整功耗分析。低速指示灯通常不需要最快边沿;精确方波宜用定时器等外设,循环 HAL_Delay(1) 翻转只能得到受时基与调度影响的近似波形。
第五步:配置按键输入并处理抖动
跳转到“第五步:配置按键输入并处理抖动”上拉输入配合按键接地,按下读低;下拉输入配合按键接 VDD,按下读高。这建立了电气极性,还不等于已得到一次可靠的“按下事件”。
F1 的 PA0 上拉输入示例:
void Key_Init(void){ RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; (void)RCC->APB2ENR; GPIOA->BSRR = 1u; /* ODR0=1,选择上拉 */ GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x8u;}若采用下拉,预装 ODR0=0,CRL 字段仍是 0x8。外部已有明确偏置或主动驱动时,是否加内部偏置需要结合电路判断;未使用引脚也不是一律接下拉,应按器件低功耗建议和板上连接处理。
原文“延时 10 ms,再一直等待松开”的实现会阻塞主循环,按键卡住时还可能永远不返回。以下纯 C 状态机每次调用立即结束,在候选电平持续 20 ms 后接受变化,只在接受按下时返回一次 true:
#include <stdbool.h>#include <stdint.h>
typedef struct { bool stable_high; bool candidate_high; uint32_t since_ms;} KeyDebounce;
bool Key_Update(KeyDebounce *key, bool raw_high, uint32_t now_ms){ if (raw_high != key->candidate_high) { key->candidate_high = raw_high; key->since_ms = now_ms; } if (key->candidate_high != key->stable_high && (uint32_t)(now_ms - key->since_ms) >= 20u) { key->stable_high = key->candidate_high; return !key->stable_high; } return false;}每隔约 1 ms 调用下面的轮询函数,传入同一递增时基的毫秒数。必须先取出 PA0 这一位,不能把整个 GPIOA->IDR 转成 bool:其他引脚为高时,整口数值仍非零。状态要跨轮询保留,不能每次调用都重新初始化。
bool Key_Poll(uint32_t now_ms){ static KeyDebounce key = {true, true, 0u}; bool raw_high = (GPIOA->IDR & (1u << 0u)) != 0u; return Key_Update(&key, raw_high, now_ms);}20 ms 是示例消抖时间,不是所有机械开关的固有值;两次采样之间发生的抖动不可见。时间使用同一递增的 32 位毫秒计数,采用无符号差值可跨一次回绕,但不能让调用间隔长达整个计数周期。每个按键使用独立状态,调用方需保证单一所有者。
本例把上电时持续按住也视为一次按下;若产品要求先释放再接收,应另设上电抑制状态。需要区分单击、长按和连发时,在稳定状态之上增加事件逻辑。
PB6/PB7:分清软件 I²C 和硬件 I²C
跳转到“PB6/PB7:分清软件 I²C 和硬件 I²C”原笔记将 GPIO_Mode_Out_OD 的初始化直接称为“I²C 配置”,实际上它只准备了普通开漏 GPIO,适合作为软件模拟 I²C 的引脚部分;并未提供 START、STOP、数据采样、ACK、超时等协议实现。
对于 F1 默认引脚映射的硬件 I2C1,PB6/PB7 通常对应 SCL/SDA,应按器件手册使用复用开漏,并配置外设时钟与 I²C 控制器;重映射或不同型号需另查。两种方式都应使用满足电气和时序要求的外部上拉。软件模拟时应预先释放两根线,并读取真实引脚状态,不能把 ODR=1 当作总线必定为高。
常见双向 I²C 使用开漏/开集电极以支持共享线、仲裁和时钟延展。规范也允许特定无时钟延展的单控制器系统把 SCL 设计为推挽;这不是将任意共享 SDA/SCL 改为推挽的许可,其他速度模式也有各自要求。这里的示例保持常见开漏接法。NXP UM10204,3.1.1
F1 常用寄存器速查与验证
跳转到“F1 常用寄存器速查与验证”| 寄存器 | 相对 GPIO 基址偏移 | 用途 |
|---|---|---|
| CRL | 0x00 | pin 0~7 配置 |
| CRH | 0x04 | pin 8~15 配置 |
| IDR | 0x08 | 读取数字输入 |
| ODR | 0x0C | 输出数据;上下拉输入时选择方向 |
| BSRR | 0x10 | 按位设置/清零输出数据 |
| BRR | 0x14 | 按位清零输出数据 |
完成配置后,读取寄存器确认没有被其他初始化覆盖,测量初始电平和输出波形,检查电流、电压和外设反馈是否满足要求。共享引脚配置应有单一所有者;输入速度字段、内部偏置、复用选择和输出命令不能相互替代。
本页合并了原来的名称解释和配置详解,保留寄存器位表、三类库接口、LED、按键及 I²C 场景。已核对列出的官方寄存器资料,消抖状态机已在主机 C 环境验证抖动、长按、释放、重复按下与计数回绕;寄存器片段尚未在具体 STM32 板上编译、下载或测量。