STM32 GPIO 输出 — 使用 HAL 库点亮 LED 灯
开发板:野火 EBF STM32F103 Mini
固件库:HAL 库(STM32CubeF1)
对应例程:9-GPIO输出—使用固件库点亮LED灯
参考源码:User/main.c、User/led/bsp_led.c、User/led/bsp_led.h
GPIO(General Purpose Input/Output,通用输入输出)是 MCU 上最基础、最常用的外设,几乎所有实验都从点亮一颗 LED 开始。输出模式让我们能用代码控制引脚输出高/低电平,从而驱动 LED、蜂鸣器、继电器等。本文是整套外设教程的起点,务必吃透。
一、功能简介
本实验把开发板上的两颗 LED(LED1、LED2)对应的 GPIO 引脚配置为推挽输出模式,在主循环中轮流点亮、熄灭,实现简单的"流水灯"效果。
- LED1:周期性亮 1 秒、灭 1 秒
- LED2:在 LED1 熄灭后再亮 1 秒、灭 1 秒
通过这个例子,你会掌握:
- HAL 工程的入口
main() 与系统时钟初始化套路;
- 如何用
GPIO_InitTypeDef 结构体配置一个引脚;
- 用
HAL_GPIO_Init() 把配置写入寄存器;
- 用
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 秒:LED1 亮,LED2 灭
- 第 2 秒:LED1 灭,LED2 亮
- 如此循环,呈现交替闪烁的流水灯效果。
- 若 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;亮灭电平方向由硬件电路决定,务必结合原理图。
八、拓展应用
- 改造成宏定义的翻转宏:用
HAL_GPIO_TogglePin() 实现 500ms 翻转一次,比"亮-延时-灭"更简洁。
- 控制蜂鸣器:把 LED 引脚换成蜂鸣器引脚(参考工程
GPIO输出—蜂鸣器),注意加三极管驱动。
- 非阻塞闪烁:用后面学的基本定时器产生 1ms 时基,在定时器回调里累计时间翻转 LED,主循环就能腾出手做别的事。
- PWM 调光:结合 TIM 的 PWM 输出,让 LED 实现渐亮渐暗的呼吸灯效果。