本节摘要:通用输入输出端口(GPIO)是单片机数量最多、用法最广的外设:输出能驱动 LED 与继电器,输入能读取按键与电平信号,复用后还能变成各种总线的引脚。本节讲清八种工作模式的差异、推挽与开漏的本质、灌电流与拉电流的接法,以及输入端悬浮导致"按键时灵时不灵"的经典故障。GPIO 是本章所有外设的公共地基,必须一次打牢。
一块刚焊好的实验板,按键读数奇怪:手指不碰它时读数还算稳定,手指一靠近按键,串口就开始疯狂打印"按下松开按下松开"。用万用表测引脚,不按键时电压在 1V 上下漂移。原因很快就找到了:输入引脚配置成了浮空模式,按键又没配上拉电阻——引脚悬在半空,电平全靠环境电磁场的施舍,人体靠近改变了电场分布,读数自然随风摇摆。这个小故障里藏着本节一半的知识点:输入引脚必须给电平一个确定的出处。另一半知识点则在输出侧:怎么让引脚安全地驱动负载。
GPIO 的工作模式可以按"输入还是输出、普通还是复用"来组织。配置前先回答两个问题:这个引脚读世界还是写世界?由 GPIO 自己驱动还是由外设借用?
| 模式 | 适用场景 | 要点 |
|---|---|---|
| 输入浮空 | 引脚外部确定有驱动源 | 内部无上拉下拉,裸露易受扰 |
| 输入上拉 | 按键接地接法 | 默认读 1,按下读 0 |
| 输入下拉 | 按键接电源接法 | 默认读 0,按下读 1 |
| 模拟输入 | ADC 采集 | 旁路施密特触发器,数字读无效 |
| 开漏输出 | 总线、电平转换、线与 | 只能主动拉低,高电平靠外部上拉 |
| 推挽输出 | 常规 LED、驱动信号 | 双管推拉,强弱驱均可靠 |
| 复用开漏 | I2C 等总线功能 | 外设接管输出级,保留开漏特性 |
| 复用推挽 | SPI、UART、PWM 引脚 | 外设接管,引脚不再受 GPIO 寄存器控制 |
两种输入的对称设计值得体会:上拉与下拉是把电平"拴"在确定值的锚——按键一端接地,就配上拉锚在 1;一端接电源,就配下拉锚在 0。模拟输入则是为 ADC 让路:数字输入端的施密特触发器会干扰缓慢变化的模拟电压,配置成模拟模式即把它旁路。
输出级的差异是 GPIO 最深的一层理解。推挽输出级有一对互补晶体管:输出 1 时上管导通把引脚顶到电源,输出 0 时下管导通把引脚按到地——推与挽配合,驱动能力双向都强,是 LED、使能信号这类常规驱动的默认选择。
开漏输出级只有下管:输出 0 时把引脚按到地,输出 1 时下管关闭、引脚悬空,高电平全靠外部上拉电阻拉起来。这个看似残缺的设计有三个妙用。其一,线与逻辑:多个开漏输出直接并联到一根总线,任何一个拉低总线就为低——I2C 总线正是靠这个特性实现多机仲裁。其二,电平转换:上拉电阻接到 5V,开漏引脚就能输出 5V 电平,轻松对接 5V 老外设。其三,防冲突:两个推挽输出若同时驱动一根线,一个顶高一个按低,瞬间形成电源到地的短路;开漏天生没有这个风险。
推挽输出 开漏输出(需外部上拉) VCC VCC VCC │ │ │ ┌┴┐ 上管 │ [R 上拉] │ │ 导通则输出1 │ │ └┬┘ ├─────┼──► 引脚电平 ├──► 引脚 │ ┌┴┐ 下管 │ │ │ 导通则输出0 │ └┬┘ [外设] 只能拉低 │ GND
LED 接在引脚与电源之间(另一端经限流电阻接地),引脚输出 0 时电流从电源流经 LED 灌入引脚,叫灌电流;LED 接在引脚与地之间,引脚输出 1 时电流从引脚拉出流向 LED,叫拉电流。多数 MCU 的灌电流能力略强于拉电流,且单引脚总电流通常限在 25mA 以内——够点亮一颗指示 LED(2 到 10mA),远远不够直接驱动继电器或电机。驱动大负载的正路是加驱动级:三极管、MOS 管,或者专用驱动芯片。
/* GPIO 三部曲:开时钟、配模式、读写电平 */ RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; /* 1. 打开 GPIOC 时钟门闩 */ GPIOC->CRH &= ~(0xF << 20); /* 2. 清空 PC13 配置位 */ GPIOC->CRH |= (0x2 << 20); /* 配成 2MHz 推挽输出 */ GPIOC->ODR |= (1 << 13); /* 3. 写 1:输出高电平 */ GPIOC->ODR &= ~(1 << 13); /* 写 0:输出低电平 */ uint8_t key = (GPIOA->IDR & (1 << 0)) ? 1 : 0; /* 读输入:PA0 电平 */
⚠️ 初学者第一大坑就在第一行:忘了开外设时钟。STM32 的每个外设默认断电省电,时钟门闩不开,寄存器写进去毫无反应——现象是"配置全对就是不工作"。遇到配置不生效,第一反应查时钟使能。
机械按键按下与松开的瞬间,金属触点会弹跳几毫秒,电平在 0 与 1 之间抖动多次。不消抖的话,一次按键会被读成七八次——你按一下,菜单翻了三页。消抖有两条路。软件路是检测到电平变化后延时 10 到 20 毫秒再读一次确认,简单可靠但占 CPU(配合定时器或状态机则不占);硬件路是按键并一个小电容 RC 滤波,一劳永逸但多了元件。产品代码多用"定时器周期采样加连续计数确认"的状态机:
/* 状态机消抖:每 5ms 采样一次,连续 4 次同值才认定状态变化 */ uint8_t sample = read_key(); if (sample == key_filter) { key_cnt = 0; /* 电平未变,计数归零 */ } else { if (++key_cnt >= 4) { /* 连续 4 次(20ms)不同 */ key_filter = sample; /* 接受新状态 */ key_cnt = 0; key_event = 1; /* 通知主程序:按键状态变了 */ } }
任务一:把一个 LED 配成推挽输出实现闪烁,把另一个引脚配成开漏输出、外接 10k 上拉到 5V,用示波器观察其高电平确实是 5V——亲手验证电平转换妙用。任务二:配置输入上拉读按键,先用无消抖版本观察一次按键触发的事件数,再上状态机消抖版本对比——用数据体会抖动。任务三:把按键次数通过串口打印(串口配置不会做可先跳到 4.2 再回来)。三个任务做完,GPIO 的配置、读改写、消抖三板斧就真正长在了手上。
画板或做面包板实验时还有个不起眼的决定:用不到的引脚怎么办。悬空的输入引脚是漂浮的天线——电平随环境电磁场漂移,内部施密特触发器反复翻转,白耗电流(低功耗设计里这叫"隐形成本"),还可能被误读成信号。惯例有三选一:配置成模拟输入(最省电,触发器不工作)、使能内部下拉并保持低、或直接配置为输出低。量产设计会在硬件上统一处理(接地或按功能预留焊盘),实验阶段养成"配置完顺手清点引脚"的习惯即可。这条纪律在 2.4 节的低功耗语境下还会再值一次回票:休眠电流对不上手册标称值,八成先查悬空引脚。