STM32 GPIO 输出 — 使用 HAL 库点亮 LED 灯

野火 EBF STM32F103 Mini HAL 库入门:从零理解 GPIO 推挽输出、HAL 初始化流程,用代码让 LED 流水闪烁。

STM32 GPIO 输出 — 使用 HAL 库点亮 LED 灯

开发板:野火 EBF STM32F103 Mini
固件库:HAL 库(STM32CubeF1)
对应例程9-GPIO输出—使用固件库点亮LED灯
参考源码User/main.cUser/led/bsp_led.cUser/led/bsp_led.h

GPIO(General Purpose Input/Output,通用输入输出)是 MCU 上最基础、最常用的外设,几乎所有实验都从点亮一颗 LED 开始。输出模式让我们能用代码控制引脚输出高/低电平,从而驱动 LED、蜂鸣器、继电器等。本文是整套外设教程的起点,务必吃透。


一、功能简介

本实验把开发板上的两颗 LED(LED1、LED2)对应的 GPIO 引脚配置为推挽输出模式,在主循环中轮流点亮、熄灭,实现简单的"流水灯"效果。

  • LED1:周期性亮 1 秒、灭 1 秒
  • LED2:在 LED1 熄灭后再亮 1 秒、灭 1 秒

通过这个例子,你会掌握:

  1. HAL 工程的入口 main() 与系统时钟初始化套路;
  2. 如何用 GPIO_InitTypeDef 结构体配置一个引脚;
  3. HAL_GPIO_Init() 把配置写入寄存器;
  4. HAL_GPIO_WritePin() 或宏控制引脚电平。

二、硬件准备

项目 说明
开发板 野火 EBF STM32F103 Mini(主控 STM32F103xC,最高 72MHz)
LED 引脚 LED1 → PC2,LED2 → PC3(低电平点亮,即"灌电流"接法)
调试器 ST-Link V2(或兼容下载器)
软件 STM32CubeIDE / Keil MDK + 野火提供的 HAL 工程

小提示:本板 LED 为低电平点亮(共阳接法)。所以在代码里用 GPIO_PIN_RESET(0)表示亮、GPIO_PIN_SET(1)表示灭,与直观的"高电平亮"相反。


三、完整代码(带中文注释)

1. main.c(主程序)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "stm32f1xx.h"
#include "./led/bsp_led.h"

/**
  * @brief  主函数
  * @param  无
  * @retval 无
  */
int main(void)
{
    /* 系统时钟初始化成 72 MHz(HAL 工程的标配第一步) */
    SystemClock_Config();

    /* LED 端口初始化:把 PC2、PC3 配置成推挽输出 */
    LED_GPIO_Config();

    /* 控制 LED 灯:循环里依次翻转电平 */
    while (1)
    {
        LED1( ON );             // LED1 亮(宏 ON = GPIO_PIN_RESET = 低电平)
        HAL_Delay(1000);        // 阻塞延时 1000ms(基于 SysTick)
        LED1( OFF );            // LED1 灭

        LED2( ON );             // LED2 亮
        HAL_Delay(1000);
        LED2( OFF );            // LED2 灭
    }
}

/**
  * @brief  System Clock Configuration
  *         系统时钟配成:HSE(8M) -> PLL×9 -> 72MHz SYSCLK
  */
void SystemClock_Config(void)
{
  RCC_ClkInitTypeDef clkinitstruct = {0};
  RCC_OscInitTypeDef oscinitstruct = {0};

  /* 使能 HSE 晶振,并以 HSE 作为 PLL 的时钟源 */
  oscinitstruct.OscillatorType  = RCC_OSCILLATORTYPE_HSE;
  oscinitstruct.HSEState        = RCC_HSE_ON;
  oscinitstruct.HSEPredivValue  = RCC_HSE_PREDIV_DIV1;
  oscinitstruct.PLL.PLLState    = RCC_PLL_ON;
  oscinitstruct.PLL.PLLSource   = RCC_PLLSOURCE_HSE;
  oscinitstruct.PLL.PLLMUL      = RCC_PLL_MUL9;   // 8MHz × 9 = 72MHz
  if (HAL_RCC_OscConfig(&oscinitstruct) != HAL_OK)
  {
    while(1);                  // 时钟配置失败,卡死在此
  }

  /* 选择 PLL 作为系统时钟,并配置 AHB/APB 分频 */
  clkinitstruct.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
                             RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
  clkinitstruct.SYSCLKSource   = RCC_SYSCLKSOURCE_PLLCLK;
  clkinitstruct.AHBCLKDivider  = RCC_SYSCLK_DIV1;   // HCLK = 72MHz
  clkinitstruct.APB2CLKDivider = RCC_HCLK_DIV1;     // PCLK2 = 72MHz
  clkinitstruct.APB1CLKDivider = RCC_HCLK_DIV2;     // PCLK1 = 36MHz
  if (HAL_RCC_ClockConfig(&clkinitstruct, FLASH_LATENCY_2) != HAL_OK)
  {
    while(1);
  }
}

2. bsp_led.h(LED 引脚宏定义)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef __LED_H
#define	__LED_H

#include "stm32f1xx.h"

/* 引脚定义:LED1 -> PC2,LED2 -> PC3 */
#define LED1_PIN                  GPIO_PIN_2
#define LED1_GPIO_PORT            GPIOC
#define LED1_GPIO_CLK_ENABLE()   __HAL_RCC_GPIOC_CLK_ENABLE()

#define LED2_PIN                  GPIO_PIN_3
#define LED2_GPIO_PORT            GPIOC
#define LED2_GPIO_CLK_ENABLE()   __HAL_RCC_GPIOC_CLK_ENABLE()

/* LED 亮灭宏:本板低电平点亮,故 ON=0、OFF=1 */
#define ON  GPIO_PIN_RESET
#define OFF GPIO_PIN_SET

/* 带参宏,像内联函数一样使用,底层调用 HAL 写引脚 */
#define LED1(a)  HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_PIN, a)
#define LED2(a)  HAL_GPIO_WritePin(LED2_GPIO_PORT, LED2_PIN, a)

void LED_GPIO_Config(void);

#endif /* __LED_H */

3. bsp_led.c(GPIO 初始化)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "./led/bsp_led.h"

/**
  * @brief  初始化控制 LED 的 IO
  */
void LED_GPIO_Config(void)
{
    /* 定义一个 GPIO_InitTypeDef 类型的结构体 */
    GPIO_InitTypeDef  GPIO_InitStruct;

    /* 开启 LED 相关的 GPIO 外设时钟(任何外设使用前必须先开时钟!) */
    LED1_GPIO_CLK_ENABLE();
    LED2_GPIO_CLK_ENABLE();

    /* 选择要控制的 GPIO 引脚 */
    GPIO_InitStruct.Pin   = LED1_PIN;

    /* 设置引脚为推挽输出 */
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;

    /* 设置引脚为上拉(输出模式下上拉可让空闲电平更稳定) */
    GPIO_InitStruct.Pull  = GPIO_PULLUP;

    /* 设置引脚速率为高速 */
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;

    /* 调用库函数,用上面的配置初始化 GPIO */
    HAL_GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStruct);

    /* LED2 用同样的配置初始化 */
    GPIO_InitStruct.Pin = LED2_PIN;
    HAL_GPIO_Init(LED2_GPIO_PORT, &GPIO_InitStruct);
}

四、代码分段讲解

1. 为什么先开时钟?

STM32 为了省电,每个外设的时钟默认都是关闭的。使用任何 GPIO 端口前,必须先用 __HAL_RCC_GPIOC_CLK_ENABLE() 打开它的时钟,否则写寄存器不会生效。这是 STM32 初学者最容易踩的坑。

2. GPIO_InitTypeDef 四个关键字段

字段 作用 本例取值
Pin 要配置的引脚号 GPIO_PIN_2 / GPIO_PIN_3
Mode 工作模式 GPIO_MODE_OUTPUT_PP(推挽输出)
Pull 上下拉 GPIO_PULLUP
Speed 翻转速率 GPIO_SPEED_FREQ_HIGH

调用 HAL_GPIO_Init() 后,HAL 会把结构体里的参数写进对应端口的 CRL/CRH 配置寄存器。

3. 推挽输出 vs 开漏输出

  • 推挽输出(PP):引脚可主动输出高、低电平,驱动能力强,适合直接驱动 LED。
  • 开漏输出(OD):只能拉低或"高阻",需要外部上拉电阻才能输出高电平,常用于 I2C 等需要"线与"的总线(后文 I2C 章节会用到)。

4. HAL_Delay() 从哪来?

HAL_Delay() 依赖 SysTick 定时器(HAL 在 HAL_Init() 里已默认配置好)。它本质是阻塞延时,会占用 CPU。后续章节我们会用定时器中断实现"非阻塞"延时。

5. 宏的妙用

LED1(ON) 实际展开为 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_2, GPIO_PIN_RESET)。把"硬件引脚"和"业务逻辑"解耦,换板子时只改头文件宏即可,这就是**板级支持包(BSP)**的思想。


五、实验现象

  1. 编译下载程序,开发板复位后:
    • 第 1 秒:LED1 亮,LED2 灭
    • 第 2 秒:LED1 灭,LED2 亮
    • 如此循环,呈现交替闪烁的流水灯效果。
  2. 若 LED 不亮,先确认下载成功、开发板供电正常,再检查引脚定义是否与你的硬件一致。

六、常见问题

Q1:程序下载了但 LED 完全不亮?

  • 检查是否调用了 LED_GPIO_Config()
  • 确认 GPIO 时钟已开启;
  • 确认 LED 是低电平点亮还是高电平点亮(本板为低电平点亮,若你用的是高电平点亮板,需把 ON/OFF 宏反过来)。

Q2:LED 一直亮着不闪烁?

  • 多半是 HAL_Delay() 没生效——确认系统时钟和 HAL_Init() 已正确初始化(本例在 SystemClock_Config 已配好 72MHz);
  • 也可能是编译器把死循环优化掉了(一般不会,可尝试加 volatile)。

Q3:想同时亮两颗 LED 怎么办?

  • 直接连续调用 LED1(ON); LED2(ON); 即可;想要"呼吸灯"效果则需 PWM(见后续 TIM 章节)。

七、小结

  • GPIO 输出模式是 STM32 编程的"Hello World";
  • “开时钟 → 配置结构体 → HAL_GPIO_Init → 控制电平” 是标准四步法,后面所有外设都遵循这个套路;
  • 推挽输出适合直接驱动 LED;亮灭电平方向由硬件电路决定,务必结合原理图。

八、拓展应用

  1. 改造成宏定义的翻转宏:用 HAL_GPIO_TogglePin() 实现 500ms 翻转一次,比"亮-延时-灭"更简洁。
  2. 控制蜂鸣器:把 LED 引脚换成蜂鸣器引脚(参考工程 GPIO输出—蜂鸣器),注意加三极管驱动。
  3. 非阻塞闪烁:用后面学的基本定时器产生 1ms 时基,在定时器回调里累计时间翻转 LED,主循环就能腾出手做别的事。
  4. PWM 调光:结合 TIM 的 PWM 输出,让 LED 实现渐亮渐暗的呼吸灯效果。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计