2. GPIO输出控制:HAL_GPIO_WritePin与LL_GPIO_SetOutputPin,点亮LED实战与代码对比
点亮LED,几乎是每个嵌入式工程师的“Hello World”。
我记得刚入行那会儿,拿到一块开发板,第一件事就是找原理图,看哪个GPIO连着板载LED。然后翻手册、配寄存器,折腾半天才让灯闪起来。现在用STM32的HAL库或LL库,这事儿简单多了。但简单归简单,背后GPIO的工作模式、输出逻辑、库函数的差异,还是得搞清楚。
这一章,我们就拿LED实战开刀。一边写代码,一边对比HAL和LL两种库的写法。你想想看,同样是点灯,HAL库一行HAL_GPIO_WritePin,LL库一行LL_GPIO_SetOutputPin,到底有什么区别?哪个更快?哪个更适合你的项目?
2.1 GPIO输出模式的核心概念
在动手写代码之前,得先明白GPIO输出到底在干什么。说白了,就是让芯片的一个引脚输出高电平(3.3V或5V)或低电平(0V),去驱动外部电路。
STM32的GPIO输出模式,有几个关键配置项:
| 配置项 | 可选值 | 说明 |
|---|---|---|
| 输出类型 | 推挽输出 / 开漏输出 | 推挽:能输出高低电平;开漏:只能输出低电平或高阻 |
| 上拉/下拉 | 上拉 / 下拉 / 无 | 引脚空闲时的默认电平状态 |
| 输出速度 | 低速 / 中速 / 高速 / 超高速 | 影响上升沿和下降沿的陡峭程度 |
2.2 硬件连接与原理图分析
实战之前,先看硬件。大部分STM32开发板上,LED都是这样接的:
- LED正极 → 串联一个限流电阻(通常330Ω~1kΩ) → STM32的GPIO引脚
- LED负极 → GND
这种接法叫低电平驱动。GPIO输出低电平时,LED亮;输出高电平时,LED灭。
也有板子是反着接的:LED正极接VCC,负极通过电阻接GPIO。那就是高电平驱动,GPIO输出高电平时LED亮。
2.3 HAL库点亮LED:HAL_GPIO_WritePin
HAL库是ST官方主推的库,封装层次高,移植性好。用HAL库点灯,代码非常直观。
2.3.1 初始化GPIO
首先,得把GPIO初始化成输出模式。通常在MX_GPIO_Init()函数里完成:
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIO时钟
__HAL_RCC_GPIOB_CLK_ENABLE();
// 配置PB0为推挽输出,速度低,无上拉下拉
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
2.3.2 控制LED亮灭
初始化完成后,控制LED就一行代码的事:
// 点亮LED(低电平驱动)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
// 熄灭LED
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
// 翻转LED状态
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
你看,HAL_GPIO_WritePin第三个参数传GPIO_PIN_RESET就是低电平,GPIO_PIN_SET就是高电平。语义清晰,新手一看就懂。
2.4 LL库点亮LED:LL_GPIO_SetOutputPin
LL库是轻量级库,直接操作寄存器,效率高。如果你对代码体积和执行速度有要求,LL库是更好的选择。
2.4.1 初始化GPIO
LL库的初始化,同样需要配置GPIO模式:
void MX_GPIO_Init(void)
{
// 使能GPIO时钟
LL_AHB2_GRP1_EnableClock(LL_AHB2_GRP1_PERIPH_GPIOB);
// 配置PB0为推挽输出,速度低,无上拉下拉
LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_0, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType(GPIOB, LL_GPIO_PIN_0, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed(GPIOB, LL_GPIO_PIN_0, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinPull(GPIOB, LL_GPIO_PIN_0, LL_GPIO_PULL_NO);
}
嗯,这里要注意:LL库的初始化函数是分开的,每个配置项一个函数。不像HAL库那样用一个结构体一次性搞定。代码行数多了,但每一步都清晰,没有隐藏逻辑。
2.4.2 控制LED亮灭
LL库控制GPIO输出,也有对应的函数:
// 点亮LED(低电平驱动)
LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_0);
// 熄灭LED
LL_GPIO_SetOutputPin(GPIOB, LL_GPIO_PIN_0);
// 翻转LED状态
LL_GPIO_TogglePin(GPIOB, LL_GPIO_PIN_0);
注意函数名:SetOutputPin是置高,ResetOutputPin是置低。和HAL库的命名习惯正好相反。刚开始用LL库时,我经常搞混,后来养成了习惯:看到“Set”就想“高电平”,看到“Reset”就想“低电平”。
2.5 HAL库 vs LL库:核心差异对比
两种库都能点灯,但背后的设计哲学完全不同。我整理了一张对比表,方便你快速把握:
| 对比维度 | HAL库 | LL库 |
|---|---|---|
| 封装层次 | 高,函数内部有多层调用 | 低,直接操作寄存器 |
| 代码体积 | 较大,包含错误处理、超时机制 | 小,只做最核心的操作 |
| 执行效率 | 相对较低 | 高,接近寄存器操作 |
| 可读性 | 好,函数名语义清晰 | 中等,需要熟悉寄存器命名 |
| 移植性 | 强,换芯片只需改底层 | 弱,不同系列LL库有差异 |
| 适用场景 | 快速原型、复杂外设、初学者 | 性能敏感、资源受限、老手 |
说白了,HAL库是“开箱即用”,LL库是“精准控制”。没有绝对的好坏,只有合不合适。
2.6 实战:LED闪烁(完整代码对比)
下面给出两个完整的LED闪烁示例。硬件假设:PB0接LED,低电平驱动,系统时钟已配置好。
2.6.1 HAL库版本
#include "main.h"
int main(void)
{
HAL_Init();
MX_GPIO_Init();
while (1)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 亮
HAL_Delay(500); // 延时500ms
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 灭
HAL_Delay(500);
}
}
2.6.2 LL库版本
#include "main.h"
int main(void)
{
MX_GPIO_Init();
while (1)
{
LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_0); // 亮
LL_mDelay(500); // 延时500ms
LL_GPIO_SetOutputPin(GPIOB, LL_GPIO_PIN_0); // 灭
LL_mDelay(500);
}
}
你看,主循环的逻辑几乎一样。区别在于:
- HAL库需要先调用
HAL_Init()初始化内部机制 - LL库没有
HAL_Delay,需要自己实现延时(或使用LL_mDelay,前提是配置了SysTick) - LL库的GPIO控制函数名更短,但需要区分Set和Reset
2.7 知识结构图:GPIO输出控制全景
下面这张图,帮你把本章的知识点串起来。从硬件连接到库函数选择,再到代码实现,一目了然。
2.8 本章小结
这一章,我们从硬件连接讲到库函数选择,再到完整的代码实现。核心就三件事:
- 搞清硬件逻辑:LED是低电平驱动还是高电平驱动?看原理图!
- 选对库函数:HAL库适合快速开发,LL库适合性能优化。别混用。
- 写对代码:初始化 → 控制输出 → 延时,循环往复。
点亮LED只是第一步。下一章,我们会深入GPIO的输入模式,看看如何用HAL库和LL库读取按键状态。到时候,你会发现输入和输出的套路完全不同。
嗯,先把这个LED闪烁跑通再说。动手试试吧!