02-GPIO详解.md 38 KB


tags: [source-summary] type: source source: "STM32学习笔记(三教程综合) — GPIO详解" author: "铁头山羊 / 江协科技 / STM32 HAL库入门" date: 2026-07-09 url: ""

created: 2026-07-09

STM32 GPIO 详解

基于三个教程系列(铁头山羊、江协科技标准库、STM32 HAL库入门)综合整理。本章覆盖:GPIO 基本概念、8 种工作模式(含 P-MOS/N-MOS 原理)、推挽输出 vs 开漏输出、4 种输入模式(浮空/上拉/下拉/模拟)、输出速度选择(2/10/50MHz 与 EMI)、LED 驱动代码、Key 模块(含消抖)、HAL 库等效实现。


1. GPIO 基本概念

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 内部结构框图

GPIO 内部由以下主要部件组成(铁头山羊课程重点讲解): GPIO 位结构图 上图:STM32 GPIO 引脚位结构示意图(来源:STM32入门教程 PPT 第20页)

GPIO 输入/输出/复用功能信号路径 上图:GPIO 输入/输出/复用功能模式信号路径(来源:STM32入门教程 PPT 第22页)

2. GPIO 的 8 种工作模式

STM32F103 的 GPIO 有 8 种工作模式,通过 CRL/CRH 寄存器的 CNF[1:0] 和 MODE[1:0] 配置。

2.1 输出模式(4 种)

① 推挽输出(GPIO_Mode_Out_PP)

GPIO 推挽与开漏输出模式 上图: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)

特点

  • 输出高低电平均有强驱动能力
  • GPIO_SetBits → P-MOS 导通,引脚输出 VDD(3.3V)
  • GPIO_ResetBits → N-MOS 导通,引脚输出 GND(0V)
  • 两管交替导通,不存在高阻态(Z)

② 开漏输出(GPIO_Mode_Out_OD)

          ┌───────────────── 需要外部上拉电阻到 VDD
          │
          R (外部上拉电阻,典型 4.7kΩ ~ 10kΩ)
          │
           ├──────────→ 引脚
        ┌──▼──┐
        │N-MOS│
        └──┬──┘
           │
          GND
  • P-MOS 被禁用(始终截止)
  • 输出高电平:N-MOS 截止 → 引脚通过外部上拉电阻被拉到高电平
  • 输出低电平:N-MOS 导通 → 引脚直接连 GND
  • 推挽 vs 开漏对比
特性 推挽输出 开漏输出
高电平驱动 P-MOS 直推,强 外部上拉电阻,弱
低电平驱动 N-MOS 下拉,强 N-MOS 下拉,强
电平兼容 固定 VDD(3.3V) 可外接不同上拉电压(如 5V)
线与能力 ❌ 不可(P-MOS 同时导通会短路) ✅ 多个 OD 输出可并接(线与)
典型用途 普通 GPIO、LED 推流 I2C SDA/SCL、5V 电平转换

③ 复用推挽输出(GPIO_Mode_AF_PP)

由片上外设(定时器 PWM、USART TX、SPI SCK 等)控制 P-MOS/N-MOS,而非 ODR 寄存器。

使用场景 对应外设
定时器 PWM 输出 TIM_CHx
USART 发送 TX 引脚
SPI 时钟/数据 SCK/MOSI

④ 复用开漏输出(GPIO_Mode_AF_OD)

外设控制开漏输出,典型用途是 I2C 的 SCL/SDA 引脚。


2.2 输入模式(4 种)

⑤ 浮空输入(GPIO_Mode_IN_FLOATING)

    引脚 ──┬──→ 施密特触发器 ──→ 输入数据寄存器(IDR)
           │
         (断开)   ← 无上下拉电阻
  • 输入阻抗极高
  • 电平由外部电路唯一决定
  • 引脚悬空时电平不确定(容易引入噪声)

⑥ 上拉输入(GPIO_Mode_IPU)

       VDD
        │
        R (约 40kΩ 内部上拉电阻)
        │
    引脚 ──┼──→ 施密特触发器 ──→ IDR
  • 内部上拉电阻(约 40kΩ)将引脚默认拉到 VDD
  • 按下按键 → 引脚接地(读 0),松开 → 上拉至高电平(读 1)
  • 江协科技的按键实验全部使用上拉输入模式

⑦ 下拉输入(GPIO_Mode_IPD)

    引脚 ──┬──→ 施密特触发器 ──→ IDR
           │
           R (约 40kΩ 内部下拉电阻)
           │
          GND
  • 内部下拉电阻(约 40kΩ)将引脚默认拉到 GND
  • 使用场景较少,通常上拉输入更常用

⑧ 模拟输入(GPIO_Mode_AIN)

    引脚 ──→ 模拟通道 ──→ ADC 转换器
           (施密特触发器和上下拉全部断开)
  • 完全断开数字电路(施密特触发器和上下拉电阻)
  • 仅用于 ADC 采样
  • 降低数字电路对模拟信号的噪声干扰

GPIO 8种工作模式详解(尚硅谷风格)

每种模式对应STM32内部不同的电路连接方式:

推挽输出 (Push-Pull)

  • 内部由P-MOS和N-MOS组成推挽结构
  • 输出1时:P-MOS导通,N-MOS截止 → 引脚输出高电平
  • 输出0时:P-MOS截止,N-MOS导通 → 引脚输出低电平
  • 特点:输出能力强,不需要上拉电阻
  • 适用:驱动LED、蜂鸣器等普通数字输出

开漏输出 (Open-Drain)

  • P-MOS始终关闭,只有N-MOS工作
  • 输出1时:N-MOS截止 → 引脚高阻态(需外部上拉电阻拉高)
  • 输出0时:N-MOS导通 → 引脚输出低电平
  • 特点:可以实现"线与"逻辑、电平转换
  • 适用:I2C总线、需要5V兼容的场合

复用推挽/开漏 (Alternate Function)

  • 与普通推挽/开漏的区别:输出数据来自片上外设(如USART的TX、TIM的PWM)而非ODR寄存器
  • 配置为复用功能时,GPIO模块的其余部分自动断开

输入模式详解

  • 浮空输入:无上下拉,引脚电平由外部决定
  • 上拉输入:内部上拉电阻(约40kΩ)接通,无外部信号时为高电平
  • 下拉输入:内部下拉电阻(约40kΩ)接通,无外部信号时为低电平
  • 模拟输入:施密特触发器关闭,信号直通到ADC等模拟外设

3. 输出速度详解(铁头山羊课程重点)

STM32 GPIO 的输出速度不是"信号跑多快",而是 IO 口的响应速度 — 即驱动电路的上升/下降时间

3.1 三种速度档位

模式 代码宏 上升/下降时间 适用主频 典型用途
2MHz GPIO_Speed_2MHz > 125ns ≤ 2MHz 低频控制、按键输入
10MHz GPIO_Speed_10MHz > 25ns ≤ 10MHz UART、I2C
50MHz GPIO_Speed_50MHz > 5ns ≤ 50MHz SPI、PWM、高速信号

核心规则:设置的输出速度是 IO 口驱动电路能支持的最大频率。实际信号频率由程序控制,但 IO 驱动电路的响应时间决定了信号质量。

3.2 速度与 EMI 的关系

铁头山羊在教程中用了很大篇幅解释这个问题:

  • 设置速度越快 → 上升/下降沿越陡 → 高频谐波越多 → EMI 越强
  • 高速信号会在 PCB 上产生反射、串扰和辐射
  • 原则:在满足功能需要的前提下,选择尽可能低的速度等级

实际选型建议:

实际信号频率 推荐设置 原因
LED 闪烁(1Hz) 2MHz 完全够用,EMI 最低
UART 115200 bps 2MHz 或 10MHz 信号频率极低
I2C 400kHz 10MHz 标准快速模式
SPI 18MHz 50MHz 需要陡峭边沿保证时序
PWM 20kHz 2MHz 或 10MHz 频率不高

3.3 速度设置对功耗的影响

  • 速度越高 → 每次翻转的瞬态电流越大 → 动态功耗越高
  • 对于电池供电产品,不用的 GPIO 应设置为低速或输入模式以降低功耗

3.4 CRL/CRH 寄存器中的 MODE 位

CNF[1:0] 配置模式类别,MODE[1:0] 配置速度:

MODE[1:0] 输出模式下的速度
00 输入模式(MODE=00 时引脚为输入)
01 输出 10MHz
10 输出 2MHz
11 输出 50MHz

GPIO寄存器功能详解(尚硅谷风格)

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):

  • CNF=00: 通用推挽
  • CNF=01: 通用开漏
  • CNF=10: 复用推挽
  • CNF=11: 复用开漏

输入模式下(MODE = 00):

  • CNF=00: 模拟输入
  • CNF=01: 浮空输入
  • CNF=10: 上拉/下拉输入

GPIOx_IDR (输入数据寄存器)

  • 只读,读取引脚当前电平
  • 每1位对应1个引脚:0=低电平, 1=高电平
  • 使用示例:uint8_t val = GPIOA->IDR & GPIO_IDR_IDR0;

GPIOx_ODR (输出数据寄存器)

  • 读写,控制引脚输出电平
  • 注意:应使用BSRR或位操作修改某一位,避免影响其他引脚

GPIOx_BSRR (位设置/清除寄存器)

  • 写入操作:低16位置1=对应引脚输出高电平
  • 高16位置1=对应引脚输出低电平
  • 优点:一次写入可同时设置多个引脚,避免中断干扰
  • 示例: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;
    }
}

4. LED 驱动详解

4.1 硬件连接

STM32 标准开发板的 LED 连接方式(低电平点亮):

                VCC (3.3V)
                  │
                  R (限流电阻, 典型 1kΩ)
                  │
           ┌──────┴──────┐
           │             │
          LED    STM32 引脚
           │      (GPIO)
           │
          GND
  • GPIO 输出低电平 → 电流从 VCC → 电阻 → LED → 引脚 → GND → LED 亮
  • GPIO 输出高电平 → LED 两端电位相等 → LED 灭

4.2 LED 模块化代码(标准库)

// 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)而非引脚状态
  • 翻转函数在中断判断时使用输出寄存器,因为输出状态是软件可控的
  • 低电平点亮是因为 LED 电路是 灌电流 方式(电流流入引脚)

4.3 LED 闪烁(main.c)

#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);
    }
}

4.4 LED 流水灯

#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(灭灯),巧妙利用取反操作一次设置所有引脚状态。

4.5 HAL 库等效代码

// 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)

4.6 GPIO 模式初始化与速度配置(HAL 库)

HAL 库的 GPIO_InitTypeDef 包含 4 个成员:PinModePullSpeed,通过 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 库 vs 标准库模式对照表

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 库将输入方向与上下拉分离为 ModePull 两个字段;标准库则合并为单一 GPIO_Mode 枚举。速度配置在 HAL 库中为独立字段 Speed,标准库在输出模式下也通过 GPIO_Speed 设置,但输入模式下速度可选任意值。


5. 蜂鸣器模块

// 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);
    }
}

6. Key 按键模块(含消抖)

6.1 硬件连接与消抖原理

         VDD (3.3V)
          │
      ┌───┤      ← 内部上拉电阻 ~40kΩ
      │   │
   按键    ├──────→ STM32 GPIO(上拉输入模式)
      │   │
      └───┤
          │
         GND
  • 未按下:上拉电阻 → 引脚读到高电平(1)
  • 按下:引脚直接连 GND → 引脚读到低电平(0)
  • 按键抖动:机械触点在按下/松开的瞬间会多次弹跳(持续约 5~20ms)

消抖策略:检测到电平变化后,延时 20ms 再次确认,等待稳定后再判定。

6.2 Key 模块代码

// 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;
}

6.3 按键控制 LED 主逻辑

#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();
    }
}

6.4 HAL 库按键等效

// 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);
    // 按键处理
}

7. 光敏传感器控制蜂鸣器

// 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();               // 蜂鸣器停
    }
}

8. 输入模式选择指南

场景 推荐模式 原因
按键(一端接 GND) 上拉输入 内部上拉省外部电阻
按键(一端接 VDD) 下拉输入 内部下拉省外部电阻
外部信号(有外部驱动) 浮空输入 外部已确定电平
I2C SCL/SDA 开漏输出 + 浮空输入 I2C 协议要求
外部中断(上升沿触发) 上拉/下拉输入 确保空闲电平稳定
ADC 采样 模拟输入 必须断开数字电路
USART RX 浮空输入 / 上拉输入 根据外部驱动能力

9. GPIO 常见误区

误区 正确理解
"输出速度越大越好" ❌ 速度越大 → EMI 越强 → 功耗越高 → 应"够用就好"
"上拉和下拉可以同时开" ❌ 不可能同时开,CRL/CRH 的配置位互斥
"开漏输出就是弱" ❌ 下拉(N-MOS 导通)时驱动能力一样强,只有上拉是弱
"浮空输入最常用" ❌ 浮空输入最不稳定,实际浮空输入反而用得少
"GPIO_SetBits 速度最快" GPIO_WriteBit 和寄存器直接写 ODR 都更快
"所有引脚都 5V 容忍" ❌ 仅部分引脚有 FT(Five-Volt Tolerant)标识

10. 应用模式总结

模式 函数组合 应用场景
输出推挽 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

11. 参考资料

  • 本文基于:铁头山羊 HAL 库教程、江协科技标准库教程、STM32 HAL库入门教程
  • 相关 wiki:[[STM32-GPIO]]、[[STM32-环境搭建]]、[[STM32-环境搭建与工程模板]]、[[STM32-HAL库笔记]]、[[STM32-标准库笔记]]
  • 相关教程来源:[[source-jiangxie-stm32]]、[[source-hal-library-best]]、[[source-tietoushanyang]]

GPIO核心函数速查(标注版)

寄存器方式:

  • 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, ~后只有该位为0
  • GPIOx->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=0
  • GPIOx->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() // 读输入