4、GPIO与基本外设:GPIO模式配置、按键输入、PWM输出基础

各位同学,欢迎来到第四讲。今天咱们聊聊最基础、也最容易被忽视的东西——GPIO。你可能会想,GPIO谁不会啊?不就是点个灯、读个按键吗?嗯,话是这么说,但我在实际项目中见过太多因为GPIO配置不当导致的“灵异现象”。比如按键误触发、PWM输出波形不对、甚至芯片莫名其妙发热。说白了,GPIO用好了,你的系统就稳了一半。

4.1 GPIO模式配置:不只是输入输出那么简单

STM32G4的GPIO,比你想的要复杂一些。每个引脚都有多种工作模式。我个人习惯,在开始任何项目前,先把所有用到的引脚模式列个表,写死在代码注释里。这习惯帮我省了不少排查时间。

GPIO模式主要有这么几种:

  • 输入模式:浮空输入、上拉输入、下拉输入、模拟输入
  • 输出模式:推挽输出、开漏输出
  • 复用功能:比如把引脚配置成TIM的PWM输出通道
  • 特殊模式:比如用于ADC的模拟输入

这里有个坑,我刚开始用G4时踩过。G4的GPIO速度等级有四种:低速、中速、快速、高速。如果你把PWM输出引脚配成了低速,波形上升沿会变得很“圆润”,高频下根本没法用。所以,做数字电源时,PWM引脚我一般配成“高速”。

核心要点:GPIO模式不是随便选的。输入要看外部电路有没有上拉/下拉;输出要看负载类型;复用功能要看外设需求。选错了,轻则功能异常,重则烧引脚。

来看一段配置代码。这是把PA0配置成推挽输出,PA1配置成上拉输入:

// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitTypeDef GPIO_InitStruct = {0};

// PA0: 推挽输出,高速
GPIO_InitStruct.Pin = GPIO_PIN_0;
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);

// PA1: 上拉输入
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

你看,代码本身不复杂。但要注意,GPIO_NOPULLGPIO_PULLUP的区别。如果外部电路已经有上拉电阻了,你内部再配上拉,相当于两个电阻并联,电平可能被拉高到不期望的值。我曾经在一个按键电路上犯过这错,按键没按下时电平就不对,查了半天才发现是内部上拉和外部上拉冲突了。

4.2 按键输入:消抖是个技术活

按键输入,听起来简单吧?读引脚电平就行了。但实际项目中,按键按下和释放的瞬间,会有机械抖动。这个抖动时间大概5-20ms。如果不做消抖,一次按键可能被识别成多次。

消抖有两种常见方法:

  • 硬件消抖:在按键引脚上加RC滤波电路。简单粗暴,但会增加BOM成本。
  • 软件消抖:检测到电平变化后,延时10-20ms再读一次。确认电平稳定了,才认为是有效按键。

我个人更倾向软件消抖,因为灵活。而且G4的主频高,延时10ms对系统影响微乎其微。来看一个典型的按键扫描函数:

#define KEY_PIN GPIO_PIN_1
#define KEY_PORT GPIOA
#define DEBOUNCE_TIME 20  // 消抖时间20ms

uint8_t Key_Scan(void)
{
    static uint8_t key_state = 0;  // 0: 未按下, 1: 按下
    static uint32_t last_time = 0;
    uint8_t current_level = HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN);
    
    // 检测电平变化
    if (current_level == GPIO_PIN_RESET)  // 假设低电平有效
    {
        if (key_state == 0)
        {
            // 第一次检测到按下,记录时间
            last_time = HAL_GetTick();
            key_state = 1;
        }
        else
        {
            // 检查是否超过消抖时间
            if ((HAL_GetTick() - last_time) > DEBOUNCE_TIME)
            {
                return 1;  // 确认按键按下
            }
        }
    }
    else
    {
        key_state = 0;  // 按键释放,重置状态
    }
    
    return 0;
}

这里用了HAL_GetTick()来做非阻塞延时。为什么不用HAL_Delay()?因为HAL_Delay()会阻塞CPU,如果你在中断里调用它,系统直接卡死。嗯,这个坑我也踩过。

小技巧:如果你的按键需要支持长按、短按、双击,建议用状态机来实现。把按键状态拆成“空闲”、“按下”、“确认按下”、“释放”等几个状态,每个状态处理不同的逻辑。这样代码可读性强,也容易扩展。

4.3 PWM输出基础:从点灯到调压

PWM,脉冲宽度调制。说白了,就是通过调节高电平占一个周期的时间比例,来控制输出功率。在数字电源里,PWM是核心中的核心。不管是Buck、Boost还是LLC,最终都是通过PWM来控制开关管的导通时间。

STM32G4的定时器非常强大。高级定时器TIM1和TIM8支持互补输出、死区插入、刹车功能,这些在电源应用中必不可少。咱们先从最基础的PWM输出说起。

配置PWM的步骤大致如下:

  1. 使能定时器时钟
  2. 配置定时器时基(预分频、自动重装值)
  3. 配置PWM通道(模式、极性、占空比)
  4. 使能定时器
  5. 使能PWM输出

来看一个用TIM1通道1输出PWM的例子,频率设为100kHz,占空比50%:

// 使能TIM1时钟
__HAL_RCC_TIM1_CLK_ENABLE();

// 配置时基
TIM_HandleTypeDef htim1 = {0};
htim1.Instance = TIM1;
htim1.Init.Prescaler = 16 - 1;      // 预分频,假设主频160MHz
htim1.Init.Period = 100 - 1;        // 自动重装值,PWM频率 = 160MHz / 16 / 100 = 100kHz
htim1.Init.CounterMode = TIM_COUNTERMODE_CENTERALIGNED1;  // 中心对齐模式
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim1);

// 配置PWM通道
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;       // PWM模式1
sConfigOC.Pulse = 50;                      // 占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);

// 启动PWM输出
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

这里我用了中心对齐模式,而不是边沿对齐。为什么?在数字电源里,中心对齐模式能减少EMI,而且适合做移相控制。当然,边沿对齐也有它的优势,比如响应速度快。具体用哪种,看你的拓扑和控制策略。

注意:启动PWM输出前,一定要确保GPIO已经配置成复用功能模式。否则,定时器输出的信号到不了引脚。我见过有人配了半天定时器,结果忘了配GPIO,波形死活出不来。

GPIO的复用功能配置也很简单,比如把PA8配成TIM1_CH1:

GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;       // 复用推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM1;     // 复用为TIM1
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

你看,GPIO_AF1_TIM1这个参数,不同引脚对应的复用号不一样。查数据手册的“Alternate function mapping”表格,就能找到对应关系。我习惯把常用引脚的复用号记在笔记本上,省得每次翻手册。

4.4 实战中的避坑指南

最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路:

  • GPIO电平不匹配:G4的GPIO电平是3.3V,如果你的外部设备是5V,需要加电平转换或者用开漏输出加外部上拉。直接硬接,可能会烧引脚。
  • PWM死区时间:做半桥或全桥拓扑时,上下管的PWM一定要加死区。否则,上下管同时导通,直接短路。G4的高级定时器支持硬件死区插入,务必用起来。
  • 按键中断优先级:如果用外部中断检测按键,中断优先级不要设得太高。否则,频繁的按键中断会干扰PWM的实时性。数字电源里,PWM中断的优先级通常是最高的。
  • 调试时用示波器:别只靠逻辑分析仪。PWM的上升沿、下降沿时间,死区时间,这些必须用示波器看。逻辑分析仪只能看逻辑电平,看不到模拟特性。

好了,这一讲的内容就到这里。GPIO和PWM是数字电源的“手脚”,配置好了,后面的控制算法才能跑得稳。下一讲,咱们会深入定时器的中断和DMA,这些东西才是G4的精髓所在。到时候见。