tags: [source-summary] type: source source: "STM32学习笔记(三教程综合) — GPIO详解" author: "铁头山羊 / 江协科技 / STM32 HAL库入门" date: 2026-07-09 url: ""
基于三个教程系列(铁头山羊、江协科技标准库、STM32 HAL库入门)综合整理。本章覆盖:GPIO 基本概念、8 种工作模式(含 P-MOS/N-MOS 原理)、推挽输出 vs 开漏输出、4 种输入模式(浮空/上拉/下拉/模拟)、输出速度选择(2/10/50MHz 与 EMI)、LED 驱动代码、Key 模块(含消抖)、HAL 库等效实现。
GPIO(General Purpose Input Output),通用输入输出口。STM32 的 GPIO 引脚在内部通过一组寄存器控制,软件写入寄存器即可改变引脚电平,或读取引脚上的电平状态。
STM32F103 系列 GPIO 特性:
| 特性 | 参数 |
|---|---|
| GPIO 组数 | GPIOA ~ GPIOE(F103C8T6 有 PA~PB,部分 PC) |
| 每组引脚数 | 16 个(Pin0 ~ Pin15) |
| 最大翻转频率 | 50MHz(配置为 50MHz 输出模式时) |
| 工作电压 | 2.0V ~ 3.6V(大部分型号容忍 5V 输入) |
| 每个引脚可独立配置 | 模式、速度、上下拉 |
GPIO 内部由以下主要部件组成(铁头山羊课程重点讲解):
上图:STM32 GPIO 引脚位结构示意图(来源:STM32入门教程 PPT 第20页)
上图:GPIO 输入/输出/复用功能模式信号路径(来源:STM32入门教程 PPT 第22页)
STM32F103 的 GPIO 有 8 种工作模式,通过 CRL/CRH 寄存器的 CNF[1:0] 和 MODE[1:0] 配置。
上图:STM32 GPIO 推挽输出与开漏输出电路结构(来源:STM32入门教程 PPT 第24页)
| 引脚输出电平 | P-MOS | N-MOS |
|---|---|---|
| 高电平 (1) | 导通 | 截止 |
| 低电平 (0) | 截止 | 导通 |
VDD
│
┌──▼──┐ P-MOS 导通时,引脚直接连 VDD(≈3.3V)
│P-MOS│
└──┬──┘
├──────────→ 引脚(强驱动,高电平 = VDD)
┌──▼──┐
│N-MOS│
└──┬──┘
│
GND N-MOS 导通时,引脚直接连 GND(≈0V)
特点:
┌───────────────── 需要外部上拉电阻到 VDD
│
R (外部上拉电阻,典型 4.7kΩ ~ 10kΩ)
│
├──────────→ 引脚
┌──▼──┐
│N-MOS│
└──┬──┘
│
GND
| 特性 | 推挽输出 | 开漏输出 |
|---|---|---|
| 高电平驱动 | P-MOS 直推,强 | 外部上拉电阻,弱 |
| 低电平驱动 | N-MOS 下拉,强 | N-MOS 下拉,强 |
| 电平兼容 | 固定 VDD(3.3V) | 可外接不同上拉电压(如 5V) |
| 线与能力 | ❌ 不可(P-MOS 同时导通会短路) | ✅ 多个 OD 输出可并接(线与) |
| 典型用途 | 普通 GPIO、LED 推流 | I2C SDA/SCL、5V 电平转换 |
由片上外设(定时器 PWM、USART TX、SPI SCK 等)控制 P-MOS/N-MOS,而非 ODR 寄存器。
| 使用场景 | 对应外设 |
|---|---|
| 定时器 PWM 输出 | TIM_CHx |
| USART 发送 | TX 引脚 |
| SPI 时钟/数据 | SCK/MOSI |
外设控制开漏输出,典型用途是 I2C 的 SCL/SDA 引脚。
引脚 ──┬──→ 施密特触发器 ──→ 输入数据寄存器(IDR)
│
(断开) ← 无上下拉电阻
VDD
│
R (约 40kΩ 内部上拉电阻)
│
引脚 ──┼──→ 施密特触发器 ──→ IDR
引脚 ──┬──→ 施密特触发器 ──→ IDR
│
R (约 40kΩ 内部下拉电阻)
│
GND
引脚 ──→ 模拟通道 ──→ ADC 转换器
(施密特触发器和上下拉全部断开)
每种模式对应STM32内部不同的电路连接方式:
推挽输出 (Push-Pull)
开漏输出 (Open-Drain)
复用推挽/开漏 (Alternate Function)
输入模式详解
STM32 GPIO 的输出速度不是"信号跑多快",而是 IO 口的响应速度 — 即驱动电路的上升/下降时间。
| 模式 | 代码宏 | 上升/下降时间 | 适用主频 | 典型用途 |
|---|---|---|---|---|
| 2MHz | GPIO_Speed_2MHz | > 125ns | ≤ 2MHz | 低频控制、按键输入 |
| 10MHz | GPIO_Speed_10MHz | > 25ns | ≤ 10MHz | UART、I2C |
| 50MHz | GPIO_Speed_50MHz | > 5ns | ≤ 50MHz | SPI、PWM、高速信号 |
核心规则:设置的输出速度是 IO 口驱动电路能支持的最大频率。实际信号频率由程序控制,但 IO 驱动电路的响应时间决定了信号质量。
铁头山羊在教程中用了很大篇幅解释这个问题:
实际选型建议:
| 实际信号频率 | 推荐设置 | 原因 |
|---|---|---|
| LED 闪烁(1Hz) | 2MHz | 完全够用,EMI 最低 |
| UART 115200 bps | 2MHz 或 10MHz | 信号频率极低 |
| I2C 400kHz | 10MHz | 标准快速模式 |
| SPI 18MHz | 50MHz | 需要陡峭边沿保证时序 |
| PWM 20kHz | 2MHz 或 10MHz | 频率不高 |
CNF[1:0] 配置模式类别,MODE[1:0] 配置速度:
| MODE[1:0] | 输出模式下的速度 |
|---|---|
| 00 | 输入模式(MODE=00 时引脚为输入) |
| 01 | 输出 10MHz |
| 10 | 输出 2MHz |
| 11 | 输出 50MHz |
GPIOx_CRL/CRH (端口配置低/高寄存器)
每组GPIO有CRL(控制0-7号引脚)和CRH(控制8-15号引脚)。每个引脚占用4位:
| 位域 | 名称 | 说明 |
|---|---|---|
| MODEy[1:0] | 输出模式/速度 | 00=输入, 01=10MHz输出, 10=2MHz输出, 11=50MHz输出 |
| CNFy[1:0] | 配置 | 根据MODE不同含义不同 |
输出模式下(MODE != 00):
输入模式下(MODE = 00):
GPIOx_IDR (输入数据寄存器)
uint8_t val = GPIOA->IDR & GPIO_IDR_IDR0;GPIOx_ODR (输出数据寄存器)
GPIOx_BSRR (位设置/清除寄存器)
GPIOA->BSRR = GPIO_Pin_0 | (GPIO_Pin_1 << 16); // PA0高, PA1低BSRR vs ODR 位操作对比(尚硅谷核心讲解):
ODR 位操作是"读-改-写"三步:
GPIOA->ODR |= GPIO_Pin_0;
// ① 读ODR → ② ODR | Pin0 → ③ 写回ODR
// 问题: 如果①和③之间发生中断, 中断也改了ODR
// ③写回时会覆盖中断的修改 → 竞态条件
BSRR 写操作是"原子写",一步完成:
GPIOA->BSRR = GPIO_Pin_0;
// 直接写BSRR, 硬件自动将对应引脚置高
// 不存在读-改-写过程, 不会被中断干扰
BSRR 寄存器位分布:
低16位(BSx): Bit[i]=1 → 引脚i输出高电平, Bit[i]=0 → 不变
高16位(BRx): Bit[i]=1 → 引脚i输出低电平, Bit[i]=0 → 不变
注意: BSRR 只允许置位,不允许清零某个位 — 如果需要保持某位不变,写 0 即可(不影响)。如果要单独清零,用 BRR 寄存器(GPIOx_BRR)或 BSRR 高 16 位。
尚硅谷教程采用"三步进化法"从底层寄存器逐步过渡到可读性强的宏定义写法:
// === 寄存器风格:LED流水灯驱动 ===
// 【关键函数】GPIO初始化流程:
// 1. 使能时钟 → 2. 配置模式(CRL/CRH) → 3. 输出电平(ODR/BSRR)
// |= 按位或: 保持APB2ENR其他位不变, 只将IOPAEN位置1
// RCC->APB2ENR 地址 0x40021018, Bit[2]=IOPAEN
// 写1前GPIOA时钟关闭 → 所有寄存器读取为0
// 写1后APB2总线开启GPIOA时钟, 寄存器才可读写
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // (1) 使能GPIOA时钟
// &= ~ 按位与+取反: 清除CRL寄存器的CNF0位域
// CNF0占CRL[7:6]即Bit[7:6], GPIO_CRL_CNF0是这两位掩码
// 先&取反: 将Bit[7:6]清零, 其他位保持不变
GPIOA->CRL &= ~GPIO_CRL_CNF0; // (2) 清除CNF0位
// |= 按位或: 设置MODE0=11(速度50MHz)
// MODE0占CRL[1:0]=Bit[1:0], GPIO_CRL_MODE0=0x03
// 配合上一步: CNF0=00(通用推挽) + MODE0=11(50MHz)
// CRL[7:6]=00 + CRL[1:0]=11 → 0x03+0x00 → PA0=推挽输出50MHz
GPIOA->CRL |= GPIO_CRL_MODE0; // 设置MODE0=11(50MHz推挽输出)
// &= ~ 按位与+取反: 将ODR寄存器的Bit[0]清零
// ODR[0]=0 → N-MOS导通, P-MOS截止 → PA0=GND(0V)
// LED电路: VCC→限流电阻→LED→PA0, PA0=0V时LED亮
// 只改Bit[0], 其他15位不变, 不影响其他引脚电平
GPIOA->ODR &= ~GPIO_ODR_ODR0; // (3) 设置PA0输出低电平 → LED点亮
// SysTick 延时
void Delay_us(uint16_t us) {
// = 写入LOAD寄存器: 72MHz时钟下, 1μs=72个时钟周期
// LOAD = 72×us 个周期后计数器归零
SysTick->LOAD = 72 * us;
// |= 按位或: 将CTRL的Bit[0]和Bit[2]置1
// CTRL[2]=1 → CLKSOURCE=HCLK(72MHz)而非HCLK/8
// CTRL[0]=1 → ENABLE=1, 使能定时器开始递减
// CTRL[1]=0 → TICKINT保持0, 不触发中断
SysTick->CTRL |= 0x05;
// & 按位与: 提取CTRL的Bit[16]即COUNTFLAG
// 计数器从LOAD减到0时硬件置1, 读取后自动清0
// ==0则循环等待, !=0退出
while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG) == 0) {}
// &= ~ 按位与+取反: 清除CTRL的Bit[0]=ENABLE
// 关闭定时器, 降低功耗, 防止后台空跑
SysTick->CTRL &= ~SysTick_CTRL_ENABLE;
}
完整 LED 流水灯寄存器代码:
#include "stm32f10x.h"
void Delay_us(uint16_t us) {
// = 写入LOAD(24位重装值): 72×us个时钟周期后归零
SysTick->LOAD = 72 * us;
// |= 按位或: CTRL[2]=1(时钟源HCLK), CTRL[0]=1(使能)
SysTick->CTRL |= 0x05;
// & 按位与: 轮询Bit[16]=COUNTFLAG, 减到0时硬件置位
while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG) == 0) {}
// &= ~ 按位与+取反: 清除Bit[0]=ENABLE, 关闭定时器
SysTick->CTRL &= ~SysTick_CTRL_ENABLE;
}
void Delay_ms(uint16_t ms) {
while (ms--) Delay_us(1000);
}
int main(void)
{
// |= 按位或: RCC->APB2ENR Bit[2]置1 → 使能GPIOA时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// = 直接赋值CRL寄存器: 0x33333333
// 二进制: 0011 0011 0011 0011 0011 0011 0011 0011
// 每4位对应1个引脚: 从Bit[0]开始Pin0~Pin7
// 每个引脚4位 = MODE[1:0]=11(50MHz) + CNF[1:0]=00(通用推挽)
// 例: Pin0对应CRL[3:0] = 0011 → MODE=11, CNF=00
// 结论: PA0~PA7全部配置为推挽输出50MHz
GPIOA->CRL = 0x33333333;
// 流水灯 — 每次只亮一个灯, 轮流点亮
while (1) {
// ~ 按位取反: 0x01 = 0b0000 0001
// ~0x01 = 0xFFFFFFFE = 0b...1111 1110
// ODR Bit[0]=0 → PA0低电平(LED亮)
// ODR Bit[1~15]=1 → PA1~PA15高电平(LED灭)
// 注意: 这里是直接赋值ODR, 会一次影响所有引脚
// 但流水灯中我们只关心低8位, 高位置1不影响
GPIOA->ODR = ~0x01; Delay_ms(100);
// ~0x02 = 0xFFFFFFFD → ODR Bit[1]=0, PA1亮
GPIOA->ODR = ~0x02; Delay_ms(100);
// ~0x04 = 0xFFFFFFFB → ODR Bit[2]=0, PA2亮
GPIOA->ODR = ~0x04; Delay_ms(100);
// ~0x08 = 0xFFFFFFF7 → ODR Bit[3]=0, PA3亮
GPIOA->ODR = ~0x08; Delay_ms(100);
// ~0x10 = 0xFFFFFFEF → ODR Bit[4]=0, PA4亮
GPIOA->ODR = ~0x10; Delay_ms(100);
// ~0x20 = 0xFFFFFFDF → ODR Bit[5]=0, PA5亮
GPIOA->ODR = ~0x20; Delay_ms(100);
// ~0x40 = 0xFFFFFFBF → ODR Bit[6]=0, PA6亮
GPIOA->ODR = ~0x40; Delay_ms(100);
// ~0x80 = 0xFFFFFF7F → ODR Bit[7]=0, PA7亮
GPIOA->ODR = ~0x80; Delay_ms(100);
}
}
带按键中断的完整寄存器代码:
#include "stm32f10x.h"
void Key_Init(void) {
// |= 按位或: 置位RCC->APB2ENR的Bit[7]=IOPFEN, 使能GPIOF时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPFEN;
// |= 按位或: 置位RCC->APB2ENR的Bit[0]=AFIOEN, 使能AFIO时钟
// AFIO(复用IO)用于EXTI引脚映射和功能重映射, 必须开
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
// &= ~ 按位与+取反: 清除GPIOF->CRH中Pin10的MODE位域
// PF10属于CRH(控制Pin8~15), 每个引脚4位
// PF10在CRH[11:8] = CRH的3号4位组, MODE占后2位
// CRH[9:8]原本可能是01/10/11, 现在清为00
// GPIO_CRH_MODE10 = 0x00000300 (Bit[9:8]掩码)
GPIOF->CRH &= ~GPIO_CRH_MODE10;
// |= 按位或: 设置CNF10_1=1, CNF10_0保持=0
// CNF[1:0] = 10 → 上拉/下拉输入模式
// MODE=00(输入模式) + CNF=10(上拉/下拉) = 输入上拉
GPIOF->CRH |= GPIO_CRH_CNF10_1;
// &= ~ 按位与+取反: 确保CNF10_0=0
// 完成CNF=10配置, 不设置此行的话CNF10_0可能=1 → CNF=11
GPIOF->ODR &= ~GPIO_ODR_ODR10;
// EXTI 配置
// |= 按位或: 设置AFIO->EXTICR[2]中EXTI10的映射源
// EXTICR[2]控制EXTI10~EXTI13的引脚映射
// AFIO_EXTICR3_EXTI10_PF = 将EXTI10映射到PF10
// 写入EXTICR[2]的Bit[7:6] = 0b10(PF)
AFIO->EXTICR[2] |= AFIO_EXTICR3_EXTI10_PF;
// |= 按位或: 设置EXTI->RTSR(上升沿触发选择寄存器)
// RTSR Bit[10]=1 → PF10上升沿(低→高)时触发中断
// EXTI_RTSR_TR10 = 0x00000400 (Bit[10]掩码)
EXTI->RTSR |= EXTI_RTSR_TR10;
// |= 按位或: 设置EXTI->IMR(中断屏蔽寄存器)
// IMR Bit[10]=1 → 放行EXTI线路10的中断请求
// IMR=0则会屏蔽中断, RTSR配了也没用
EXTI->IMR |= EXTI_IMR_MR10;
// NVIC 配置
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(EXTI15_10_IRQn, 3);
NVIC_EnableIRQ(EXTI15_10_IRQn);
}
void EXTI15_10_IRQHandler(void) {
// |= 按位或: 写1到EXTI->PR(挂起寄存器)Bit[10]
// 注意: 这里不是"置位中断标志", 而是"写1清除"
// STM32规定: 向PR写1清零, 写0无效
// 必须清除标志, 否则中断会反复触发
EXTI->PR |= EXTI_PR_PR10;
Delay_ms(10);
// & 按位与: 测试IDR Bit[10]是否为1
// IDR是输入数据寄存器, 只读
// !=0说明PF10当前为高电平 → 按键已松开(上拉输入)
if ((GPIOF->IDR & GPIO_IDR_IDR10) != 0) {
// ^= 按位异或: 翻转ODR Bit[1]
// 异或特性: 1^1=0, 0^1=1 → 一次翻转
// 等效于: if(当前高)置低 else 置高
GPIOA->ODR ^= GPIO_ODR_ODR1;
}
}
STM32 标准开发板的 LED 连接方式(低电平点亮):
VCC (3.3V)
│
R (限流电阻, 典型 1kΩ)
│
┌──────┴──────┐
│ │
LED STM32 引脚
│ (GPIO)
│
GND
// LED.h
#ifndef __LED_H
#define __LED_H
void LED_Init(void);
void LED1_ON(void);
void LED1_OFF(void);
void LED1_Turn(void);
void LED2_ON(void);
void LED2_OFF(void);
void LED2_Turn(void);
#endif
// LED.c — 标准库方案
#include "stm32f10x.h"
void LED_Init(void)
{
// 开启 GPIOA 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置 PA1、PA2 为推挽输出
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 初始化为高电平(LED 灭)
GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2);
}
// 开启 LED1:输出低电平
void LED1_ON(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_1); }
// 关闭 LED1:输出高电平
void LED1_OFF(void) { GPIO_SetBits(GPIOA, GPIO_Pin_1); }
// 翻转 LED1:读取当前输出状态后取反
void LED1_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1) == 0)
GPIO_SetBits(GPIOA, GPIO_Pin_1); // 当前低 → 置高
else
GPIO_ResetBits(GPIOA, GPIO_Pin_1); // 当前高 → 置低
}
关键点解析:
GPIO_ReadOutputDataBit() 读取的是输出数据寄存器(ODR)而非引脚状态#include "stm32f10x.h"
#include "Delay.h"
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
while (1)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_0); // PA0 = 0, LED 亮
Delay_ms(500);
GPIO_SetBits(GPIOA, GPIO_Pin_0); // PA0 = 1, LED 灭
Delay_ms(500);
}
}
#include "stm32f10x.h"
#include "Delay.h"
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; // 一次性配置 PA0~PA7
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
while (1)
{
// GPIO_Write() 直接写ODR寄存器, 16位同时输出
// ~ 按位取反: 0x0001 = 0b0000 0000 0000 0001
// ~0x0001 = 0xFFFF FFFE = 低16位: 1111 1111 1111 1110
// → ODR Bit[0]=0(PA0低→LED亮), Bit[1~15]=1(其他高→灭)
GPIO_Write(GPIOA, ~0x0001); // PA0=0, 其他=1
Delay_ms(100);
// ~0x0002 = 0xFFFF FFFD → ODR Bit[1]=0, PA1=低
GPIO_Write(GPIOA, ~0x0002); // PA1=0, 其他=1
Delay_ms(100);
// ~0x0004 = 0xFFFF FFFB → ODR Bit[2]=0, PA2=低
GPIO_Write(GPIOA, ~0x0004); // PA2=0, 其他=1
Delay_ms(100);
// ...依次类推到 PA7
}
}
GPIO_Write() 直接写入 ODR(Output Data Register),16 位掩码同时控制 16 个引脚。用 ~ 取反是因为低电平点亮。核心技巧:~0x0001 只让 ODR Bit[0]=0(亮灯),其他位=1(灭灯),巧妙利用取反操作一次设置所有引脚状态。
// HAL 库方案 — CubeMX 生成的用户代码区域
// 使用 HAL_GPIO_WritePin 替代标准库的 SetBits/ResetBits
// 初始化(CubeMX 在 MX_GPIO_Init() 中自动生成)
// 只需在 main 中调用 MX_GPIO_Init()
// LED 点亮
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET); // 低电平
// LED 熄灭
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET); // 高电平
// LED 翻转
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1); // HAL 库内置翻转函数
// 批量操作
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1 | GPIO_PIN_2, GPIO_PIN_RESET);
标准库 vs HAL 库 API 对照:
| 操作 | 标准库 | HAL 库 |
|---|---|---|
| 置高电平 | GPIO_SetBits(GPIOx, Pin) |
HAL_GPIO_WritePin(GPIOx, Pin, GPIO_PIN_SET) |
| 置低电平 | GPIO_ResetBits(GPIOx, Pin) |
HAL_GPIO_WritePin(GPIOx, Pin, GPIO_PIN_RESET) |
| 读取输入 | GPIO_ReadInputDataBit(GPIOx, Pin) |
HAL_GPIO_ReadPin(GPIOx, Pin) |
| 读取输出 | GPIO_ReadOutputDataBit(GPIOx, Pin) |
(无对应函数,直接读 ODR) |
| 翻转 | 手动读输出后取反 | HAL_GPIO_TogglePin(GPIOx, Pin) |
HAL 库的 GPIO_InitTypeDef 包含 4 个成员:Pin、Mode、Pull、Speed,通过 HAL_GPIO_Init() 一次性配置。
// HAL 库 GPIO 初始化结构体
GPIO_InitTypeDef GPIO_InitStruct = {0};
// ===== 推挽输出(对应标准库 GPIO_Mode_Out_PP)=====
GPIO_InitStruct.Pin = GPIO_PIN_1 | GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无需上拉/下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速(约 50MHz)
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// ===== 开漏输出(对应标准库 GPIO_Mode_Out_OD)=====
GPIO_InitStruct.Pin = GPIO_PIN_6; // I2C SCL
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM; // 中速(约 10MHz)
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// ===== 浮空输入(对应标准库 GPIO_Mode_IN_FLOATING)=====
GPIO_InitStruct.Pin = GPIO_PIN_6; // SPI MISO
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式
GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉 → 浮空
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// ===== 上拉输入(对应标准库 GPIO_Mode_IPU)=====
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// ===== 下拉输入(对应标准库 GPIO_Mode_IPD)=====
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLDOWN; // 内部下拉
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// ===== 复用推挽输出(对应标准库 GPIO_Mode_AF_PP)=====
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7; // SPI SCK + MOSI
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// ===== 模拟输入(对应标准库 GPIO_Mode_AIN)=====
GPIO_InitStruct.Pin = GPIO_PIN_0; // ADC 通道
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; // 模拟模式
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
速度选项(HAL 库):
| HAL 宏 | 对应标准库 | 说明 |
|---|---|---|
GPIO_SPEED_FREQ_LOW |
GPIO_Speed_2MHz | 低速 |
GPIO_SPEED_FREQ_MEDIUM |
GPIO_Speed_10MHz | 中速 |
GPIO_SPEED_FREQ_HIGH |
GPIO_Speed_50MHz | 高速 |
AFIO / EXTI 引脚映射(HAL 库):
HAL 库中 AFIO 和 EXTI 的配置通过 HAL_GPIO_Init() 的 Mode 参数完成,无需单独调用 AFIO 函数:
// GPIO 外部中断(上升沿触发,使用内部上拉)
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; // 上升沿外部中断
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 使能 AFIO 时钟(HAL 库需手动开启以支持 EXTI 和 remap)
__HAL_RCC_AFIO_CLK_ENABLE();
// 复用功能重映射示例:USART1 TX/RX 从 PA9/PA10 重映射到 PB6/PB7
__HAL_RCC_AFIO_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
| HAL 库 Mode | 标准库 Mode | 说明 |
|---|---|---|
GPIO_MODE_INPUT |
GPIO_Mode_IN_FLOATING |
浮空输入 |
GPIO_MODE_INPUT + PULLUP |
GPIO_Mode_IPU |
上拉输入 |
GPIO_MODE_INPUT + PULLDOWN |
GPIO_Mode_IPD |
下拉输入 |
GPIO_MODE_OUTPUT_PP |
GPIO_Mode_Out_PP |
推挽输出 |
GPIO_MODE_OUTPUT_OD |
GPIO_Mode_Out_OD |
开漏输出 |
GPIO_MODE_AF_PP |
GPIO_Mode_AF_PP |
复用推挽输出 |
GPIO_MODE_AF_OD |
GPIO_Mode_AF_OD |
复用开漏输出 |
GPIO_MODE_ANALOG |
GPIO_Mode_AIN |
模拟输入 |
GPIO_MODE_IT_RISING |
EXTI + GPIO_Mode_IPU |
上升沿外部中断 |
GPIO_MODE_IT_FALLING |
EXTI + GPIO_Mode_IPU |
下降沿外部中断 |
GPIO_MODE_IT_RISING_FALLING |
EXTI + GPIO_Mode_IPU |
双边沿外部中断 |
关键区别:HAL 库将输入方向与上下拉分离为 Mode 和 Pull 两个字段;标准库则合并为单一 GPIO_Mode 枚举。速度配置在 HAL 库中为独立字段 Speed,标准库在输出模式下也通过 GPIO_Speed 设置,但输入模式下速度可选任意值。
// main.c — 蜂鸣器(无源蜂鸣器,需 PWM 驱动,有源蜂鸣器可直接高低电平)
#include "stm32f10x.h"
#include "Delay.h"
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
while (1)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_12); // PB12 = 0, 蜂鸣器叫
Delay_ms(100);
GPIO_SetBits(GPIOB, GPIO_Pin_12); // PB12 = 1, 蜂鸣器停
Delay_ms(100);
GPIO_ResetBits(GPIOB, GPIO_Pin_12); // 再叫
Delay_ms(100);
GPIO_SetBits(GPIOB, GPIO_Pin_12); // 再停
Delay_ms(700);
}
}
VDD (3.3V)
│
┌───┤ ← 内部上拉电阻 ~40kΩ
│ │
按键 ├──────→ STM32 GPIO(上拉输入模式)
│ │
└───┤
│
GND
消抖策略:检测到电平变化后,延时 20ms 再次确认,等待稳定后再判定。
// Key.c — 标准库方案
#include "stm32f10x.h"
#include "Delay.h"
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
// 返回 1/2 表示按键按下,返回 0 表示无按键
// 阻塞式:按住按键时函数会卡住直到松手
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
// 检测按键 1(PB1)
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) // 按下
{
Delay_ms(20); // 消抖
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0); // 等待松手
Delay_ms(20); // 松手消抖
KeyNum = 1;
}
// 检测按键 2(PB11)
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0);
Delay_ms(20);
KeyNum = 2;
}
return KeyNum;
}
#include "stm32f10x.h"
#include "Delay.h"
#include "LED.h"
#include "Key.h"
uint8_t KeyNum;
int main(void)
{
LED_Init(); // 初始化 LED
Key_Init(); // 初始化按键
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1) LED1_Turn(); // 按一次翻转一次
if (KeyNum == 2) LED2_Turn();
}
}
// HAL 库方案
// CubeMX 配置:PB1 → GPIO_Input, GPIO_PULLUP
// 读取按键
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) // PB1 检测到低电平
{
HAL_Delay(20); // 消抖
while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET);
HAL_Delay(20);
// 按键处理
}
// LightSensor.c — 上拉输入读取光敏传感器模块输出
#include "stm32f10x.h"
void LightSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
uint8_t LightSensor_Get(void)
{
return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13);
}
// main.c — 光敏控制蜂鸣器
#include "stm32f10x.h"
#include "Buzzer.h"
#include "LightSensor.h"
int main(void)
{
Buzzer_Init();
LightSensor_Init();
while (1)
{
if (LightSensor_Get() == 1) // 光强变化→输出高电平
Buzzer_ON(); // 蜂鸣器响
else
Buzzer_OFF(); // 蜂鸣器停
}
}
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 按键(一端接 GND) | 上拉输入 | 内部上拉省外部电阻 |
| 按键(一端接 VDD) | 下拉输入 | 内部下拉省外部电阻 |
| 外部信号(有外部驱动) | 浮空输入 | 外部已确定电平 |
| I2C SCL/SDA | 开漏输出 + 浮空输入 | I2C 协议要求 |
| 外部中断(上升沿触发) | 上拉/下拉输入 | 确保空闲电平稳定 |
| ADC 采样 | 模拟输入 | 必须断开数字电路 |
| USART RX | 浮空输入 / 上拉输入 | 根据外部驱动能力 |
| 误区 | 正确理解 |
|---|---|
| "输出速度越大越好" | ❌ 速度越大 → EMI 越强 → 功耗越高 → 应"够用就好" |
| "上拉和下拉可以同时开" | ❌ 不可能同时开,CRL/CRH 的配置位互斥 |
| "开漏输出就是弱" | ❌ 下拉(N-MOS 导通)时驱动能力一样强,只有上拉是弱 |
| "浮空输入最常用" | ❌ 浮空输入最不稳定,实际浮空输入反而用得少 |
| "GPIO_SetBits 速度最快" | ❌GPIO_WriteBit 和寄存器直接写 ODR 都更快 |
| "所有引脚都 5V 容忍" | ❌ 仅部分引脚有 FT(Five-Volt Tolerant)标识 |
| 模式 | 函数组合 | 应用场景 |
|---|---|---|
| 输出推挽 | GPIO_Init(Mode_Out_PP) + SetBits/ResetBits |
LED、蜂鸣器、普通控制 |
| 输出开漏 | GPIO_Init(Mode_Out_OD) + 外部上拉 |
I2C、5V 电平转换 |
| 输入上拉 | GPIO_Init(Mode_IPU) + ReadInputDataBit |
按键(GND 侧) |
| 输入下拉 | GPIO_Init(Mode_IPD) + ReadInputDataBit |
按键(VDD 侧) |
| 输入浮空 | GPIO_Init(Mode_IN_FLOATING) + ReadInputDataBit |
外部驱动信号 |
| 模拟输入 | GPIO_Init(Mode_AIN) |
ADC |
| 复用推挽 | GPIO_Init(Mode_AF_PP) |
USART TX、PWM、SPI |
| 复用开漏 | GPIO_Init(Mode_AF_OD) |
I2C |
寄存器方式:
RCC->APB2ENR |= RCC_APB2ENR_xxxEN;
// |= 按位或: 将对应外设时钟使能位置1, 保持其他位不变
// GPIO操作前必须开时钟, 否则总线无法响应GPIOx->CRL = ... / GPIOx->CRH = ...
// = 直接赋值: 配置Pin0~7(CRL)或Pin8~15(CRH)的模式与速度
// 每引脚4位: MODE[1:0]决定速度, CNF[1:0]决定模式GPIOx->ODR |= GPIO_Pin_x;
// |= 按位或: 将ODR对应位置1 → 引脚输出高电平
// 不影响其他引脚电平(区别于直接赋值ODR)GPIOx->ODR &= ~GPIO_Pin_x;
// &= ~ 按位与+取反: 将ODR对应位清零 → 引脚输出低电平
// GPIO_Pin_x的二进制只有一位为1, ~后只有该位为0GPIOx->BSRR = GPIO_Pin_x;
// = 写入BSRR低16位: 对应位置1 → 引脚输出高电平
// BSRR写0不影响, 比ODR位操作更安全(无需读-改-写)GPIOx->BSRR = GPIO_Pin_x << 16;
// << 左移16位: 将引脚掩码移到BSRR高16位位置
// BSRR高16位置1 → 对应引脚输出低电平(BR位)
// 例: GPIO_Pin_0<<16 = 0x00010000, 写BSRR后PA0=0GPIOx->IDR & GPIO_Pin_x
// & 按位与: 提取IDR中对应引脚的电平值
// IDR是只读寄存器, 引脚高→位=1, 低→位=0
// 结果非0则引脚为高电平, =0则为低电平标准库方式:
GPIO_Init() // 初始化结构体 + 调用GPIO_SetBits() // 置高GPIO_ResetBits() // 置低GPIO_ReadInputDataBit() // 读输入位HAL库方式:
HAL_GPIO_Init() // 初始化HAL_GPIO_WritePin() // 写电平HAL_GPIO_TogglePin() // 翻转HAL_GPIO_ReadPin() // 读输入