第四讲:GPIO和LED模块

第四讲:GPIO和LED模块

基础概念

:brain: GPIO 基础知识

GPIO(General Purpose Input/ Output,通用输入/输出口)是决定引脚的基本工作方式,是STM32CubeMX中最基础的配置项

模式 说明 适用场景 备注
输入模式 将引脚配置为输入,用于读取外部电平 读取按键、开关、传感器信号等 通常需配合内部/外部上拉或下拉电阻,防止悬空
输出模式 将引脚配置为输出,用于驱动外部电路 LED 控制、继电器、蜂鸣器、电机使能信号等 可选择推挽或开漏输出类型
模拟模式 引脚用于模拟信号输入或输出 ADC 采集(如温度、电压)、DAC 输出音频等 此时数字功能被禁用
备用功能 引脚配置为外设复用功能(Alternate Function) UART、SPI、I2C、PWM、定时器等通信或控制接口 具体功能由芯片手册及引脚复用表确定:high_voltage: 输出类型

:vs_button: 推挽 vs 开漏对比

当GPIO配置为输出模式时,可以选择不同的输出类型

特性 推挽输出 开漏输出
高电平驱动 :white_check_mark: 芯片内部提供 :cross_mark: 需外部上拉电阻
低电平驱动 :white_check_mark: 芯片内部提供 :white_check_mark: 芯片内部提供
多设备共享总线 :cross_mark: 不适合(可能短路) :white_check_mark: 适合(支持线与)
电流驱动能力 :white_check_mark: 较强(由芯片决定) :warning: 取决于上拉电阻
电平转换兼容性 :cross_mark: 一般不能直接转换 :white_check_mark: 容易实现不同电压匹配
LED 直接驱动 :white_check_mark: 非常适合 :warning: 需要外加上拉电路
总线冲突保护 :cross_mark: 较差 :white_check_mark: 较好(靠外部电阻限流)

:rocket: 输出速度配置

决定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 要求高 高速存储器接口、摄像头并行数据等

:chart_increasing: 速度设置的影响

  • 更高的速度带来更快的信号边沿,但也会导致:

    • :battery: 功耗增加

    • :satellite_antenna: 电磁干扰(EMI)增强

    • :electric_plug: 信号完整性设计难度提高

  • 经验建议:在满足时序要求的前提下,尽量选用较低速度,以降低系统噪声与功耗。
    例如 LED 控制一般选用低速即可,而 SPI 通信则需根据时钟频率选择中/高速。

:police_car_light:上拉/下拉电阻

决定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个人在跑道上赛跑 :man_running::woman_running::man_running::woman_running::man_running::woman_running:
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 变大!:chart_increasing:
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]