3. GPIO输出驱动——点亮LED:LL库函数详解与实战
各位同学,今天我们来聊聊GPIO输出。这是嵌入式开发里最基础、也是最直观的一环——让一个LED亮起来。你想想看,一个芯片能不能正常工作,很多时候就是看这个灯能不能按你的想法亮灭。我刚开始学STM32那会儿,第一个程序就是点灯,虽然简单,但背后涉及的概念其实不少。
3.1 GPIO模块LL库函数详解
LL库,全称Low Layer,是ST官方推出的轻量级库。跟HAL库比起来,LL库更接近寄存器操作,代码执行效率高,体积也小。我个人习惯在资源受限或者对实时性要求高的项目里用LL库。
GPIO相关的LL库函数,主要分布在 stm32f1xx_ll_gpio.h 这个头文件里。常用的就那么几个:
| 函数名 | 功能描述 | 我常用的场景 |
|---|---|---|
LL_GPIO_SetOutputPin |
设置指定引脚输出高电平 | 点亮LED、使能外设 |
LL_GPIO_ResetOutputPin |
设置指定引脚输出低电平 | 熄灭LED、关闭外设 |
LL_GPIO_TogglePin |
翻转指定引脚的电平 | 实现LED闪烁,非常方便 |
LL_GPIO_WriteOutputPort |
一次性写入整个端口的电平 | 控制8位或16位并行数据总线 |
LL_GPIO_ReadOutputPort |
读取当前输出寄存器的值 | 调试时查看引脚状态 |
LL_GPIO_TogglePin 做闪烁效果,比先Set再Reset要简洁得多,而且不容易出错。我在做状态指示灯的时候特别喜欢用这个函数。
3.2 GPIO初始化结构体配置
在使用GPIO之前,必须先初始化。LL库的初始化是通过一个结构体 LL_GPIO_InitTypeDef 来配置的。这个结构体长这样:
LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = LL_GPIO_PIN_13; // 选择引脚
GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT; // 输出模式
GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH; // 输出速度
GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL; // 推挽输出
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO; // 无上下拉
LL_GPIO_Init(GPIOB, &GPIO_InitStruct);
这里有几个关键点,我挨个说一下:
- Pin: 可以是单个引脚,也可以用
|组合多个引脚,比如LL_GPIO_PIN_13 | LL_GPIO_PIN_14。 - Mode: 输出模式选
LL_GPIO_MODE_OUTPUT,输入模式选LL_GPIO_MODE_INPUT,还有复用功能模式。 - Speed: 输出速度有低速、中速、高速。对于LED这种低频应用,低速就够了。但如果是SPI或I2C,建议用高速。
- OutputType: 推挽输出还是开漏输出,这个下面会细讲。
- Pull: 上下拉电阻配置。输出模式一般用
LL_GPIO_PULL_NO,输入模式根据外部电路选择上拉或下拉。
= {0} 或者 memset 清零。我曾经有一次忘了清零,结果结构体里残留了随机值,导致GPIO配置异常,查了半天才发现是这个问题。
3.3 推挽输出与开漏输出
这两个概念,很多初学者容易搞混。我简单解释一下:
- 推挽输出(Push-Pull): 内部有两个MOS管,一个负责推(输出高电平),一个负责挽(输出低电平)。输出高电平时,直接连接到VCC;输出低电平时,直接连接到GND。驱动能力强,速度快。
- 开漏输出(Open-Drain): 只有下拉的MOS管,没有上拉的。输出低电平时,直接接地;输出高电平时,引脚处于高阻态,需要外部上拉电阻才能拉到高电平。
什么时候用开漏?最常见的就是I2C总线。因为I2C是多主机通信,需要多个设备都能拉低总线,但谁都不能主动拉高,所以必须用开漏输出加上拉电阻。如果用了推挽,两个设备同时输出不同电平,那就短路了。
| 特性 | 推挽输出 | 开漏输出 |
|---|---|---|
| 输出高电平 | 直接VCC | 需要外部上拉电阻 |
| 输出低电平 | 直接GND | 直接GND |
| 驱动能力 | 强 | 弱(取决于上拉电阻) |
| 适用场景 | 普通GPIO、LED、蜂鸣器 | I2C、电平转换、线与逻辑 |
3.4 实现LED闪烁与呼吸灯效果
好了,理论说完了,咱们直接上代码。先来个最简单的闪烁:
while (1)
{
LL_GPIO_TogglePin(GPIOB, LL_GPIO_PIN_13);
for (int i = 0; i < 500000; i++); // 简单延时
}
这段代码会让LED以大约1Hz的频率闪烁。但注意,这种空转延时很浪费CPU资源。在实际项目中,我一般用定时器或者系统滴答定时器来实现精确延时。
接下来是呼吸灯效果。呼吸灯的本质是PWM(脉冲宽度调制),通过改变占空比来调节LED的亮度。STM32的定时器可以输出PWM,但这里我们先用软件模拟的方式,让你理解原理:
void breath_led(void)
{
for (int duty = 0; duty < 100; duty++)
{
LL_GPIO_SetOutputPin(GPIOB, LL_GPIO_PIN_13);
delay_us(duty * 10); // 高电平时间逐渐变长
LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_13);
delay_us((100 - duty) * 10); // 低电平时间逐渐变短
}
for (int duty = 100; duty > 0; duty--)
{
LL_GPIO_SetOutputPin(GPIOB, LL_GPIO_PIN_13);
delay_us(duty * 10);
LL_GPIO_ResetOutputPin(GPIOB, LL_GPIO_PIN_13);
delay_us((100 - duty) * 10);
}
}
这段代码通过不断调整高电平和低电平的持续时间,模拟出PWM的效果。虽然精度不如硬件PWM,但用来理解呼吸灯的原理足够了。
3.5 本章知识体系
为了让你更直观地理解本章的内容结构,我画了一张图:
这张图把本章的知识点串联起来了。从LL库函数入手,到初始化结构体的配置,再到推挽和开漏的选择,最后落到LED闪烁和呼吸灯的实际应用。每一步都是环环相扣的。
3.6 避坑指南
最后,分享几个我踩过的坑:
- 时钟没使能: 操作GPIO之前,一定要先使能对应端口的时钟。比如
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_GPIOB)。我刚开始学的时候经常忘,结果引脚死活没反应。 - 引脚复用冲突: 有些引脚默认被JTAG/SWD占用,比如PA13、PA14、PB3、PB4。如果你要用这些引脚做普通GPIO,需要先禁用JTAG功能。
- 输出速度过高: 对于LED这种低频应用,输出速度设成
LL_GPIO_SPEED_FREQ_LOW就够了。设成高速反而可能引入电磁干扰。
好了,这一章的内容就到这里。GPIO输出看似简单,但它是所有外设驱动的基础。把这一章吃透了,后面的I2C、SPI、UART等通信协议,你上手会快很多。