Chapter 02 · ARM 嵌入式 C 开发
GPIO 与寄存器
深入理解 GPIO 四种工作模式的硬件原理,掌握寄存器操作与 HAL 库双路径,实现按键消抖与呼吸灯。
深入理解 GPIO 四种工作模式的硬件原理,掌握寄存器操作与 HAL 库双路径,实现按键消抖与呼吸灯。
每个 GPIO 引脚背后都是一个相对复杂的电路,理解这个结构才能明白各模式的区别:
| 模式 | MODER | OTYPER | PUPDR | 典型应用 |
|---|---|---|---|---|
| 浮空输入 | 00 | — | 00 | 外部已有上下拉的信号输入 |
| 上拉输入 | 00 | — | 01 | 按键(按下接GND),I/O检测 |
| 下拉输入 | 00 | — | 10 | 悬空时需要默认低电平的场景 |
| 推挽输出 | 01 | 0 | 00 | 驱动LED、控制继电器 |
| 开漏输出 | 01 | 1 | — | I2C总线、多设备线与逻辑 |
| 复用推挽 | 10 | 0 | — | UART TX、SPI MOSI/CLK |
| 复用开漏 | 10 | 1 | — | I2C SDA/SCL |
| 模拟模式 | 11 | — | — | ADC输入、DAC输出 |
| 偏移 | 寄存器 | 读写 | 功能 |
|---|---|---|---|
| +0x00 | MODER | RW | 每引脚2bit:输入/输出/复用/模拟 |
| +0x04 | OTYPER | RW | 每引脚1bit:推挽/开漏 |
| +0x08 | OSPEEDR | RW | 每引脚2bit:速度等级 |
| +0x0C | PUPDR | RW | 每引脚2bit:无/上拉/下拉 |
| +0x10 | IDR | RO | 低16bit:当前引脚输入电平 |
| +0x14 | ODR | RW | 低16bit:输出数据(读写均可) |
| +0x18 | BSRR | WO | 高16bit清零,低16bit置位(原子) |
| +0x24 | AFR[0] | RW | PIN0-7的复用功能选择(AF0-AF15) |
| +0x28 | AFR[1] | RW | PIN8-15的复用功能选择 |
为什么用 BSRR 而不是直接读-改-写 ODR?考虑这种情况:
/* 危险:读-改-写 ODR 不是原子操作 */ /* 步骤1:读 ODR */ /* 步骤2(中断打入!改变了其他引脚)*/ /* 步骤3:写 ODR,中断的修改被覆盖!*/ GPIOD->ODR |= (1 << 12); /* ← 存在竞态条件 */ /* 安全:BSRR 是单次写操作,无需关中断 */ /* 低16位:对应位写1 → 置高 */ GPIOD->BSRR = (1 << 12); /* 置 PD12 高,其他引脚不受影响 */ /* 高16位:对应位写1 → 置低 */ GPIOD->BSRR = (1 << (12 + 16)); /* 清 PD12,拉低 */ /* HAL 库内部就是这样实现的:*/ /* HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET) → BSRR = (1<<12) */ /* HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET) → BSRR = (1<<12)<<16 */
#include "stm32f4xx.h" /** * @brief 初始化 LED(PD12-PD15)和按键(PA0,用户按钮) */ void GPIO_Init_All(void) { /* ===== 开启时钟 ===== */ RCC->AHB1ENR |= (1 << 0); /* GPIOA 时钟 */ RCC->AHB1ENR |= (1 << 3); /* GPIOD 时钟 */ /* ===== 配置 PD12-PD15 为推挽输出(4个LED)===== */ /* MODER: PD12→bit25:24=01, PD13→bit27:26=01 ... */ GPIOD->MODER &= ~(0xFF << 24); /* 清零 pin12-15 的4×2 bits */ GPIOD->MODER |= (0x55 << 24); /* 0101_0101 = 全部为输出模式 */ GPIOD->OTYPER &= ~(0xF << 12); /* 推挽输出 */ GPIOD->OSPEEDR &= ~(0xFF << 24); /* 低速 */ GPIOD->PUPDR &= ~(0xFF << 24); /* 无上下拉 */ GPIOD->ODR &= ~(0xF << 12); /* 初始熄灭 */ /* ===== 配置 PA0 为上拉输入(用户按钮) ===== */ GPIOA->MODER &= ~(0x3 << 0); /* 输入模式 (00) */ GPIOA->PUPDR &= ~(0x3 << 0); /* 清零 */ GPIOA->PUPDR |= (0x1 << 0); /* 上拉 (01) */ } /** * @brief 读取按键(PA0),高有效(按下为高) * STM32F407-Discovery 的 User Button 按下时 PA0 = 高 */ uint8_t Button_Read(void) { return (GPIOA->IDR & (1 << 0)) ? 1 : 0; }
#include "stm32f4xx.h" #include "stm32f4xx_hal.h" /* 使用 HAL_GetTick() 和 HAL_Delay() */ #define BTN_PIN GPIO_PIN_0 #define BTN_PORT GPIOA #define DEBOUNCE_MS 20 /* 消抖时间窗口 20ms */ /** * @brief 软件消抖:检测按键是否被有效按下(高有效) * 本函数是阻塞式,仅适用于简单场景 * 复杂场景应使用状态机方式(见下) */ uint8_t Button_Debounce_Blocking(void) { if (HAL_GPIO_ReadPin(BTN_PORT, BTN_PIN) == GPIO_PIN_SET) { HAL_Delay(DEBOUNCE_MS); /* 等待抖动平息 */ if (HAL_GPIO_ReadPin(BTN_PORT, BTN_PIN) == GPIO_PIN_SET) { while (HAL_GPIO_ReadPin(BTN_PORT, BTN_PIN) == GPIO_PIN_SET); /* 等待松手 */ HAL_Delay(DEBOUNCE_MS); /* 松手消抖 */ return 1; /* 确认一次有效按下 */ } } return 0; } /* ────────────────────────────────────────── 状态机消抖(非阻塞,推荐在主循环中调用) ────────────────────────────────────────── */ typedef enum { BTN_IDLE, /* 等待按下 */ BTN_PRESSING, /* 检测到下降沿,等待消抖 */ BTN_PRESSED, /* 确认按下 */ BTN_RELEASING /* 检测到上升沿,等待消抖 */ } BtnState_t; static BtnState_t btn_state = BTN_IDLE; static uint32_t btn_tick = 0; /** * @brief 非阻塞按键状态机,在主循环每次调用 * @return 1=检测到一次完整按下并松手,0=否 */ uint8_t Button_StateMachine(void) { uint8_t pin = (HAL_GPIO_ReadPin(BTN_PORT, BTN_PIN) == GPIO_PIN_SET); uint32_t now = HAL_GetTick(); switch (btn_state) { case BTN_IDLE: if (pin) { btn_state = BTN_PRESSING; btn_tick = now; } break; case BTN_PRESSING: if (!pin) { btn_state = BTN_IDLE; } /* 抖动,忽略 */ else if (now - btn_tick >= DEBOUNCE_MS) { btn_state = BTN_PRESSED; } break; case BTN_PRESSED: if (!pin) { btn_state = BTN_RELEASING; btn_tick = now; } break; case BTN_RELEASING: if (pin) { btn_state = BTN_PRESSED; } /* 抖动,忽略 */ else if (now - btn_tick >= DEBOUNCE_MS) { btn_state = BTN_IDLE; return 1; /* 一次完整的按下+松手 */ } break; } return 0; }
硬件 PWM 由定时器生成(第3章讲),这里用延时模拟 PWM 占空比变化,实现呼吸灯效果:
/** * @brief 软件 PWM 呼吸灯 * 通过改变高低电平时间比(占空比),让 LED 亮度渐变 * 注意:软件 PWM 占用 CPU,频率低,有闪烁感;实际应用用定时器 PWM */ #define LED_PIN GPIO_PIN_12 #define LED_PORT GPIOD #define PWM_PERIOD_US 1000 /* PWM 周期 1000μs → 1kHz */ /* 简单微秒延时(基于 SysTick,非精确,仅演示)*/ void delay_us(uint32_t us) { uint32_t start = DWT_GetCycles(); /* 需要先使能 DWT */ uint32_t cycles = us * (SystemCoreClock / 1000000); while (DWT_GetCycles() - start < cycles); } void LED_Breathe_Loop(void) { int duty; /* 占空比 0~100 */ for (int dir = 0; dir < 2; dir++) { /* dir=0 渐亮,dir=1 渐暗 */ for (duty = (dir ? 100 : 0); dir ? duty >= 0 : duty <= 100; dir ? duty-- : duty++) { /* 连续输出 20 个 PWM 周期让人眼感受到亮度 */ for (int i = 0; i < 20; i++) { uint32_t on_time = (PWM_PERIOD_US * duty) / 100; uint32_t off_time = PWM_PERIOD_US - on_time; if (on_time > 0) { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET); delay_us(on_time); } if (off_time > 0) { HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET); delay_us(off_time); } } } } }
HAL_GPIO_Init(GPIOx, &Init) — 初始化引脚
HAL_GPIO_WritePin(GPIOx, Pin, State) — 写引脚(底层用 BSRR)
HAL_GPIO_ReadPin(GPIOx, Pin) — 读引脚(读 IDR)
HAL_GPIO_TogglePin(GPIOx, Pin) — 翻转引脚(读-改-写 ODR,注意非原子)
HAL_GPIO_DeInit(GPIOx, Pin) — 复位引脚到默认状态(浮空输入)
STM32 引脚绝对最大电压为 VDD+0.3V(约3.6V)。若外部电路有5V信号直接连接,必须使用电平转换电路(3.3V↔5V)或串联限流电阻(至少1kΩ)。直接连接5V信号到 GPIO 引脚可能永久损坏芯片。I/O 引脚最大持续电流:单引脚25mA,所有 I/O 总电流不超过 120mA。