3. GPIO基础与LED控制:从寄存器到呼吸灯

GPIO,说白了就是芯片的“手脚”。你要让STM32跟外界打交道,第一步就是学会怎么用这些引脚。我记得刚入行那会儿,总觉得GPIO太简单,不就是输出高电平、低电平嘛。直到有一次项目里LED死活不亮,查了两天才发现是模式配错了……嗯,从那以后我再也不敢小看GPIO配置了。

3.1 GPIO模式:不只是输入输出

STM32的GPIO比你想的复杂。它一共有8种工作模式,我整理了一张表,你一看就明白:

模式 用途 典型场景
推挽输出 强驱动高低电平 LED、蜂鸣器
开漏输出 需要外部上拉 I2C总线
浮空输入 电平不确定 按键检测(需外接上拉)
上拉输入 默认高电平 按键检测(省外部电阻)
下拉输入 默认低电平 特定传感器
模拟输入 ADC采样 光敏电阻、电位器
复用推挽 外设驱动引脚 USART TX、SPI MOSI
复用开漏 外设开漏驱动 I2C SCL/SDA
我的习惯:控制LED我永远用推挽输出。开漏输出虽然也能亮,但亮度不够,而且还得加外部上拉电阻,麻烦。

3.2 寄存器操作:直接跟硬件对话

为什么还要学寄存器?HAL库不是封装好了吗?说实话,HAL库确实方便,但出了问题你还是要看寄存器。我在项目里遇到过HAL_GPIO_WritePin莫名其妙不生效,最后发现是时钟没开——这种问题不看寄存器根本查不出来。

每个GPIO端口有7个关键寄存器。我挑最常用的几个说:

  • GPIOx_MODER(模式寄存器):每2位控制一个引脚,00=输入,01=输出,10=复用,11=模拟
  • GPIOx_OTYPER(输出类型):0=推挽,1=开漏
  • GPIOx_OSPEEDR(输出速度):00=低速,01=中速,10=高速,11=超高速
  • GPIOx_PUPDR(上拉/下拉):00=无,01=上拉,10=下拉
  • GPIOx_ODR(输出数据寄存器):写1输出高,写0输出低
  • GPIOx_BSRR(位设置/复位寄存器):低16位置位,高16位复位,原子操作

核心技巧:操作ODR和BSRR的区别很大。ODR是“读-改-写”,多线程或中断里容易出问题。BSRR是原子操作,写1就置位,写1就复位,不会被打断。我建议你养成用BSRR的习惯。

来个实际例子。配置PA5为推挽输出,速度50MHz:

// 开启GPIOA时钟
RCC->AHB1ENR |= (1 << 0);

// 配置PA5为输出模式(MODER[11:10] = 01)
GPIOA->MODER &= ~(0x3 << 10);  // 先清零
GPIOA->MODER |= (0x1 << 10);   // 设置为输出

// 推挽输出(默认就是推挽,这步可省略)
GPIOA->OTYPER &= ~(1 << 5);

// 速度50MHz(OSPEEDR[11:10] = 10)
GPIOA->OSPEEDR &= ~(0x3 << 10);
GPIOA->OSPEEDR |= (0x2 << 10);

// 无上拉下拉
GPIOA->PUPDR &= ~(0x3 << 10);

// 输出高电平(用BSRR)
GPIOA->BSRR = (1 << 5);    // 置位
// 输出低电平
GPIOA->BSRR = (1 << (5+16)); // 复位
我曾经踩过的坑:配置MODER时一定要先清零再置位。直接赋值会把其他引脚的模式也改了。别问我怎么知道的……那次调试了整整一个下午。

3.3 HAL库函数:封装好的便利

寄存器操作虽然直接,但项目里我还是推荐用HAL库。原因很简单:可读性好,移植方便。你想想看,如果团队里每个人写寄存器的方式都不一样,代码维护起来得多痛苦。

HAL库GPIO的核心函数就这几个:

// 初始化GPIO
HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

// 写引脚电平
HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

// 切换引脚电平
HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

// 读取引脚电平
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

用HAL库配置PA5点亮LED,代码长这样:

GPIO_InitTypeDef GPIO_InitStruct = {0};

// 开启时钟
__HAL_RCC_GPIOA_CLK_ENABLE();

// 配置结构体
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;     // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;             // 无上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;   // 高速

// 初始化
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 点亮LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);

// 熄灭LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);

// 切换状态
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
我的建议:项目初期用HAL库快速验证功能,后期优化性能时再考虑寄存器。别一上来就寄存器,那样开发效率太低了。

3.4 LED闪烁:第一个实战

闪烁的原理很简单:延时-亮-延时-灭,循环。但这里有个关键——延时函数怎么写?

最简单的办法是用HAL_Delay():

while (1)
{
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);   // 亮
    HAL_Delay(500);                                        // 延时500ms
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 灭
    HAL_Delay(500);                                        // 延时500ms
}

但HAL_Delay有个问题——它是阻塞的。延时期间CPU啥也干不了。如果你要做多任务,就得用定时器或者SysTick。这个后面章节会细讲,这里先留个印象。

避坑指南:HAL_Delay依赖SysTick中断。如果你在中断服务函数里调HAL_Delay,系统会死锁。我曾经在外部中断里加了个延时做消抖,结果程序直接卡死……后来改用定时器才解决。

3.5 呼吸灯:PWM的入门

呼吸灯比闪烁高级一点。它不是简单的亮灭,而是亮度渐变。怎么实现?靠PWM(脉冲宽度调制)。

PWM的原理说白了就是:快速切换高低电平,通过调整高电平占的比例(占空比)来控制亮度。人眼有视觉暂留效应,只要频率够高(一般100Hz以上),看到的就是平均亮度。

用软件模拟PWM,代码也不复杂:

// 软件PWM实现呼吸灯
// 周期20ms(50Hz),分100级亮度

#define PWM_PERIOD 100   // 一个周期分100份
#define PWM_FREQ    5    // 每级延时5ms

void Breath_LED(void)
{
    static uint8_t brightness = 0;
    static int8_t direction = 1;
    
    // 输出当前占空比
    for (uint8_t i = 0; i < PWM_PERIOD; i++)
    {
        if (i < brightness)
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
        else
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
        
        HAL_Delay(1);  // 每级1ms,周期100ms
    }
    
    // 更新亮度
    brightness += direction;
    if (brightness >= PWM_PERIOD || brightness == 0)
        direction = -direction;
}

这个代码有个问题——CPU占用率太高。实际项目中我推荐用硬件PWM(定时器),效率高得多。但软件PWM作为理解原理的手段,还是很有价值的。

我的经验:软件PWM适合1-2路LED,多了CPU就扛不住了。我之前做过一个项目要控制8路呼吸灯,软件PWM直接让主循环跑不动,最后全改成硬件PWM才搞定。

3.6 知识体系总览

这一章的内容比较多,我画了张图帮你理清思路:

GPIO基础与LED控制知识体系 GPIO核心概念 8种工作模式 7个关键寄存器 HAL库封装 推挽/开漏输出 上拉/下拉/浮空 MODER BSRR/ODR Init/WritePin TogglePin LED闪烁(延时控制) 呼吸灯(PWM控制) HAL_Delay阻塞 定时器非阻塞 软件PWM 硬件PWM 核心原则:先理解寄存器原理,再用HAL库提高效率 实战中优先使用硬件PWM,软件PWM仅用于原理验证

3.7 本章小结

这一章我们走了从寄存器到HAL库、从闪烁到呼吸灯的完整路径。你可能会觉得内容有点多,但这些都是基本功。GPIO用好了,后面I2C、SPI、UART这些协议栈配置才能顺手。

最后送你一句话:别怕寄存器,也别死磕寄存器。理解原理用HAL,出了问题看寄存器——这才是嵌入式工程师该有的姿态。