第四讲:GPIO和LED模块
基础概念
GPIO 基础知识
GPIO(General Purpose Input/ Output,通用输入/输出口)是决定引脚的基本工作方式,是STM32CubeMX中最基础的配置项
| 模式 | 说明 | 适用场景 | 备注 |
|---|---|---|---|
| 输入模式 | 将引脚配置为输入,用于读取外部电平 | 读取按键、开关、传感器信号等 | 通常需配合内部/外部上拉或下拉电阻,防止悬空 |
| 输出模式 | 将引脚配置为输出,用于驱动外部电路 | LED 控制、继电器、蜂鸣器、电机使能信号等 | 可选择推挽或开漏输出类型 |
| 模拟模式 | 引脚用于模拟信号输入或输出 | ADC 采集(如温度、电压)、DAC 输出音频等 | 此时数字功能被禁用 |
| 备用功能 | 引脚配置为外设复用功能(Alternate Function) | UART、SPI、I2C、PWM、定时器等通信或控制接口 | 具体功能由芯片手册及引脚复用表确定 |
推挽 vs 开漏对比
当GPIO配置为输出模式时,可以选择不同的输出类型
| 特性 | 推挽输出 | 开漏输出 |
|---|---|---|
| 高电平驱动 | ||
| 低电平驱动 | ||
| 多设备共享总线 | ||
| 电流驱动能力 | ||
| 电平转换兼容性 | ||
| LED 直接驱动 | ||
| 总线冲突保护 |
输出速度配置
决定GPIO引脚状态切换的速率和驱动能力,更高的速度设置意味着更快的电平切换速率和更强的电流驱动能力
| 速度等级 | 典型频率范围 | 特点与影响 | 适用场景 |
|---|---|---|---|
| 低速(Low Speed) | 2 – 10 MHz | 功耗低、EMI 小、边沿较缓 | 按键检测、LED 控制、低频开关信号 |
| 中速(Medium Speed) | 10 – 50 MHz | 平衡功耗与速度 | 普通 UART、SPI、显示驱动等 |
| 高速(High Speed) | 50 – 100 MHz | 边沿陡、噪声与功耗增加 | 高速 SPI、SDIO、USB 等 |
| 超高速(Very High Speed) | 100 MHz 以上 | 信号完整性与 EMI 要求高 | 高速存储器接口、摄像头并行数据等 |
速度设置的影响
-
更高的速度带来更快的信号边沿,但也会导致:
-
功耗增加 -
电磁干扰(EMI)增强 -
信号完整性设计难度提高
-
-
经验建议:在满足时序要求的前提下,尽量选用较低速度,以降低系统噪声与功耗。
例如 LED 控制一般选用低速即可,而 SPI 通信则需根据时钟频率选择中/高速。
上拉/下拉电阻
决定GPIO引脚在未被驱动时的默认状态
| 配置模式 | 工作原理 | 关键特性 |
|---|---|---|
| 无上拉/下拉 | 不启用内部电阻,引脚直接浮空 | • 电平完全由外部电路决定 • 功耗最低(无电阻电流) • 灵敏度最高 |
| 上拉电阻 | 内部电阻连接至电源(VCC),引脚悬空时为高电平 | • 默认输出高电平(1) • 按键按下时接地变低(0) • 常见阻值:4.7KΩ~10KΩ |
| 下拉电阻 | 内部电阻连接至地(GND),引脚悬空时为低电平 | • 默认输出低电平(0) • 按键按下时接电源变高(1) • 有助于降低静态功耗 |
STM32 HAL库GPIO配置
GPIO Mode (GPIO模式):对于LED控制,通常选择Output Mode。如果需要通过PWM控制LED亮度,则选择Alternate Function并连接到定时器。
GPIO Output Type (输出类型):对于一般的LED控制,选择Push-Pull输出类型。对于需要共享总线的场景(如I2C),选择Open-Drain输出类型。
GPIO Speed (输出速度):对于普通LED控制,选择Low速度即可。更高的速度会增加功耗和电磁干扰,除非应用需要,否则不要选择过高的速度设置。
GPIO Pull-up/Pull-down (上拉/下拉电阻):对于输出模式下的LED控制,通常选择No Pull-up/Pull-down。对于输入模式下的按键检测,建议选择Pull-up以防止引脚浮空。
GPIO Label (引脚标签):为GPIO引脚指定一个有意义的名称,方便在代码中引用。
在CubeMX图形化工具配置LED引脚,LED1-6依次配成PB12,PB13,PB14,PB15,PD8,PD9
新建led的.c和.h文件,并移植配置,在mydefine.h文件引用led的文件
在main.h中引用调度器头文件,并调用调度器初始化函数和调度器运行函数
烧录程序,第2/4/6个LED灯亮
DEBUG调试①
在DEBUG界面中点击全速运行再停止,将ucLed添加至调试窗口,观察ucLed的值是否正常。若值显示正常但灯不正常,则排除软件问题,转向解决硬件
单个LED呼吸灯
需额外引用头文件:
#include <math.h> // 需要包含数学库以使用 sinf 函数
#include "gpio.h" // 确保包含了HAL库的GPIO头文件
#include <stdint.h> // 引入标准整数类型
应用层代码如下:
// 呼吸灯相关变量 (使用 static 确保它们在函数调用之间保持值)
static uint32_t breathCounter = 0; // 呼吸效果的内部计时器,模拟时间流逝
static uint8_t pwmCounter = 0; // 软件PWM的内部计数器,用于生成PWM波形
static uint8_t brightness = 0; // 当前计算出的LED亮度值 (0-pwmMax)
static const uint16_t breathPeriod = 2000; // 定义一个完整的呼吸周期时长 (单位:毫秒或调用次数,取决于调用频率)
static const uint8_t pwmMax = 10; // 软件PWM周期的最大计数值 (决定PWM精度和频率)
breathCounter = (breathCounter + 1) % breathPeriod;
pwmCounter = (pwmCounter + 1) % pwmMax;
brightness = (uint8_t)((sinf((2.0f * 3.14159f * breathCounter) / breathPeriod) + 1.0f) * pwmMax / 2.0f);
ucLed[0] = (pwmCounter < brightness) ? 1 : 0;
breathCounter = (breathCounter + 1) % breathPeriod;: 更新呼吸计时器:自动加加,每次调用函数时加1,达到周期后归零这个计数器相当于呼吸效果的时间轴
pwmCounter = (pwmCounter + 1) % pwmMax;:自动加加,更新软件PWM计数器:每次调用函数时加1,达到 pwmMax 后归零这个计数器用于在 pwmMax 的周期内比较亮度,决定当前时刻LED是亮还是灭
brightness = (uint8_t)((sinf((2.0f * 3.14159f * breathCounter) / breathPeriod) + 1.0f) * pwmMax / 2.0f);: (2.0f * 3.14159f * breathCounter) / breathPeriod 将 breathCounter 映射到 0 到 2π 的弧度范围,sinf(…) 的结果在 -1.0 到 1.0 之间,(sinf(…) + 1.0f) 将范围变为 0.0 到 2.0, pwmMax / 2.0f 将范围缩放到 0 到 pwmMax,即我们期望的亮度范围
ucLed[0] = (pwmCounter < brightness) ? 1 : 0;:控制第一个LED (ucLed[0])
多个LED呼吸流水灯
需额外引用头文件:
#include <math.h> // 需要包含数学库以使用 sinf 函数
#include "gpio.h" // 确保包含了HAL库的GPIO头文件
#include <stdint.h> // 引入标准整数类型
应用层代码如下:
static uint32_t breathCounter = 0; // 全局呼吸计时器
static uint8_t pwmCounter = 0; // 软件PWM计数器 (所有LED共用)
static uint8_t brightness = 0; // 当前计算出的LED亮度值 (0-pwmMax)
static const uint16_t breathPeriod = 4000; // 整个流水呼吸效果的周期 (调慢一点,看得清楚)
static const uint8_t pwmMax = 10; // PWM精度 (同上)
static const float phaseShift = 3.14159f / 3.0f;
breathCounter = (breathCounter + 1) % breathPeriod;
pwmCounter = (pwmCounter + 1) % pwmMax;
for(uint8_t i = 0; i < 6; i++) // 遍历所有6个LED
{
float angle = (2.0f * 3.14159f * breathCounter) / breathPeriod - i * phaseShift;
float sinValue = sinf(angle);
float enhancedValue = sinValue > 0 ? powf(sinValue, 0.5f) : -powf(-sinValue, 0.5f);
enhancedValue = enhancedValue > 0.7f ? enhancedValue : enhancedValue * 0.6f;
brightness = (uint8_t)((enhancedValue + 1.0f) * pwmMax / 2.0f);
ucLed[i] = (pwmCounter < brightness) ? 1 : 0;
}
static const float phaseShift = 3.14159f / 3.0f;:每个LED比前一个晚 π/3 的相位开始呼吸,6个LED × 60度 = 360度,正好一个完整周期
float angle = (2.0f * 3.14159f * breathCounter) / breathPeriod - i * phaseShift;:
想象6个人在跑道上赛跑 ![]()
![]()
![]()
![]()
![]()
:
LED0:立即起跑
LED1:晚1秒起跑
LED2:晚2秒起跑
LED3:晚3秒起跑
LED4:晚4秒起跑
LED5:晚5秒起跑
这样他们就会形成"波浪"效果!
float sinValue = sinf(angle);:计算原始的正弦值 (-1.0 到 1.0)
float enhancedValue = sinValue > 0 ? powf(sinValue, 0.5f) : -powf(-sinValue, 0.5f);:端点不变,中间段放大
| sinValue | 原始值 | 增强计算 | enhancedValue | 效果说明 |
|---|---|---|---|---|
| 1.0 | 1.0 | √1.0 | 1.0 | 峰值不变 |
| 0.9 | 0.9 | √0.9 ≈ 0.949 | 0.949 | 变大! |
| 0.7 | 0.7 | √0.7 ≈ 0.837 | 0.837 | 明显变大! |
| 0.5 | 0.5 | √0.5 ≈ 0.707 | 0.707 | 变大了41%! |
| 0.3 | 0.3 | √0.3 ≈ 0.548 | 0.548 | 变大了83%! |
| 0.1 | 0.1 | √0.1 ≈ 0.316 | 0.316 | 变大了216%! |
| 0.0 | 0.0 | √0.0 | 0.0 | 零点不变 |
| -0.1 | -0.1 | -√0.1 ≈ -0.316 | -0.316 | 变小了(绝对值变大) |
| -0.3 | -0.3 | -√0.3 ≈ -0.548 | -0.548 | 更小! |
| -0.5 | -0.5 | -√0.5 ≈ -0.707 | -0.707 | 更小! |
| -0.7 | -0.7 | -√0.7 ≈ -0.837 | -0.837 | 更小! |
| -0.9 | -0.9 | -√0.9 ≈ -0.949 | -0.949 | 更小! |
| -1.0 | -1.0 | -√1.0 = -1.0 | -1.0 | 谷值不变 |
enhancedValue = enhancedValue > 0.7f ? enhancedValue : enhancedValue * 0.6f;:暗部抑制:enhancedValue > 0.7,已经足够亮了,保持原样;否则,不够亮,再压暗
uint8_t brightness = (uint8_t)((enhancedValue + 1.0f) * pwmMax / 2.0f);:将增强后的浮点数值[-1.0, 1.0]映射到LED的PWM亮度值[0, pwmMax]








