2. STM32基础与GPIO控制

各位同学好,欢迎来到我们的第二节课。今天我们要聊的是STM32最基础、也是最核心的内容——GPIO控制。说实话,不管你是做传感器采集、电机控制,还是搞什么复杂的通信协议,最终都离不开GPIO。我当年刚入行时,第一个项目就是点灯,哈哈,现在看来虽然简单,但那是理解整个芯片工作方式的起点。

2.1 STM32系列介绍

STM32是意法半导体推出的32位ARM Cortex-M内核微控制器。市面上型号多得让人眼花缭乱,但说白了,你可以把它们分成几个大类:

系列 内核 典型应用场景
STM32F0 Cortex-M0 低成本、低功耗,替代8位机
STM32F1 Cortex-M3 通用型,入门首选,我第一个项目用的就是F103
STM32F4 Cortex-M4 带DSP和FPU,适合做音频、算法处理
STM32H7 Cortex-M7 高性能,跑图形界面、复杂控制

我个人习惯是:做简单控制用F1,做复杂运算用F4。你想想看,如果只是点个灯、读个按键,用H7是不是有点大材小用?选型这事,够用就好。

2.2 GPIO工作原理

GPIO,全称General Purpose Input Output,通用输入输出口。每个GPIO引脚内部其实是一堆电路:输入驱动器、输出驱动器、上下拉电阻、施密特触发器等等。

这里我画了一张图,帮你理清GPIO的内部结构:

STM32 GPIO内部结构框图 GPIO引脚 输出数据寄存器 输出驱动器 输入数据寄存器 施密特触发器 上拉/下拉 模式选择:推挽输出 / 开漏输出 / 浮空输入 / 上拉输入 / 下拉输入

嗯,这里要注意:GPIO的输入和输出路径是分开的。你写数据时走输出路径,读引脚状态时走输入路径。所以即使你配置成输出模式,也能读回引脚电平——这个特性我在调试时经常用到。

2.3 寄存器与库函数概念

操作STM32的GPIO,有两种方式:直接操作寄存器和调用库函数。

直接操作寄存器:说白了就是直接往芯片的某个地址写值。比如GPIOA的ODR寄存器地址是0x40020014,你写个0x01进去,PA0就输出高电平了。这种方式效率高,但代码可读性差。我记得刚学的时候,对着数据手册一个个查寄存器地址,头都大了。

库函数:ST官方帮你封装好了,你调用HAL_GPIO_WritePin()就行,不用管底层地址。我个人建议新手先用库函数,等你对芯片熟悉了,再考虑寄存器操作。

核心观点:库函数是封装好的寄存器操作。你调用HAL_GPIO_WritePin,底层还是在操作ODR寄存器。理解这一点,你就能明白为什么库函数效率略低——因为它多了一层函数调用开销。

2.4 使用HAL库配置GPIO输出

好,我们直接上代码。用HAL库配置一个GPIO输出,其实就三步:

// 第一步:定义GPIO初始化结构体
GPIO_InitTypeDef GPIO_InitStruct = {0};

// 第二步:使能时钟
__HAL_RCC_GPIOA_CLK_ENABLE();

// 第三步:配置引脚
GPIO_InitStruct.Pin = GPIO_PIN_0;          // PA0
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;         // 无上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;// 低速
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 然后就可以控制输出了
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);   // 输出高电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 输出低电平
// 或者用翻转函数
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);

这里有个坑,我曾经踩过:时钟使能必须在配置之前。如果你先调用HAL_GPIO_Init再使能时钟,配置会失败,因为寄存器根本写不进去。嗯,这个顺序问题,很多人都会忽略。

小技巧:调试时可以用HAL_GPIO_TogglePin配合示波器,看代码执行时间。我在做Modbus协议时,就用这个方法来测量帧间隔时间,非常方便。

2.5 按键输入检测与消抖

按键输入比输出稍微复杂一点。为什么?因为机械按键有抖动。你按下去的一瞬间,电平会跳变好几次,如果不处理,一次按键可能被误判成十几次。

看这个波形你就明白了:

按键按下时的电平抖动 未按下 抖动 按下稳定 按下时刻

消抖的方法有两种:硬件消抖和软件消抖。硬件消抖就是加个RC滤波电路,软件消抖就是延时再采样。我一般用软件消抖,因为省成本嘛。

软件消抖的标准写法:

// 按键消抖函数
uint8_t Key_Scan(void)
{
    if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET) // 检测到按下
    {
        HAL_Delay(20);  // 延时20ms,避开抖动区间
        if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET) // 再次确认
        {
            // 等待按键释放(可选)
            while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET);
            return 1;  // 有效按键
        }
    }
    return 0;  // 无效
}

注意:HAL_Delay(20)会阻塞CPU。如果你在中断或实时性要求高的场合,建议改用定时器计时的方式做消抖。我曾经在一个电机控制项目里用HAL_Delay做按键消抖,结果电机响应慢了半拍,查了半天才发现是这里的问题。

另外,按键的上下拉配置也很关键。如果按键一端接地,另一端接GPIO,那GPIO内部要配置成上拉输入。这样按键没按下时读到高电平,按下时读到低电平。反过来,如果按键接VCC,就配置成下拉输入。

好了,这一节的内容就这些。GPIO看似简单,但它是你理解整个嵌入式系统的基础。把GPIO玩熟了,后面的串口、I2C、SPI其实都是类似的套路——配置寄存器、读写数据。你想想看,是不是这个道理?