一、配置GPIO
配置推挽,开漏,复用……
二、点亮LED
1.LED加限流电阻
2.代码
三、呼吸灯实现
单个LED以正弦函数呼吸
/*单个LED以正弦函数呼吸*/
#include <math.h> // 需要包含数学库以使用 sinf 函数
extern uint8_t ucLed[6];
extern void led_disp(uint8_t *ucLed);
/**
* @brief LED 显示处理函数 - 呼吸灯效果 (在主循环中周期性调用)
*/
void led_proc(void)
{
// 呼吸灯相关变量 (使用 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精度和频率)
// 更新呼吸计时器:每次调用函数时加1,达到周期后归零
// 这个计数器相当于呼吸效果的时间轴
breathCounter = (breathCounter + 1) % breathPeriod;
// 核心:计算当前时刻的亮度值
// 使用正弦函数 (sinf) 来模拟平滑的亮度变化
// (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,即我们期望的亮度范围
brightness = (uint8_t)((sinf((2.0f * 3.14159f * breathCounter) / breathPeriod) + 1.0f) * pwmMax / 2.0f);
// 更新软件PWM计数器:每次调用函数时加1,达到 pwmMax 后归零
// 这个计数器用于在 pwmMax 的周期内比较亮度,决定当前时刻LED是亮还是灭
pwmCounter = (pwmCounter + 1) % pwmMax;
// 软件PWM逻辑:
// 如果 pwmCounter 小于当前的亮度值 brightness,则LED应该亮 (ucLed[0] = 1)
// 否则,LED应该灭 (ucLed[0] = 0)
// 效果:brightness 越大,LED在一个PWM周期内亮的时间越长,看起来就越亮
// 当 brightness 为 0 时,pwmCounter 永远不小于 0,LED 始终灭
// 当 brightness 为 pwmMax 时,pwmCounter 始终小于 pwmMax,LED 始终亮
ucLed[0] = (pwmCounter < brightness) ? 1 : 0; // 控制第一个LED (ucLed[0])
// 调用之前定义的 led_disp 函数,将计算好的 ucLed 状态更新到实际的GPIO引脚
led_disp(ucLed); // 注意:led_disp内部最好也有优化,避免状态不变时重复写GPIO
}
graph TD
A[函数调用 led_proc] --> B{更新呼吸计时器 breathCounter};
B --> C[计算正弦值 -> 得到目标亮度 brightness];
C --> D{更新 PWM 计数器 pwmCounter};
D --> E{比较: pwmCounter < brightness?};
E -- 是 --> F[LED 亮 ucLed=1];
E -- 否 --> G[LED 灭 ucLed=0];
F --> H[刷新 GPIO 输出 led_disp];
G --> H;
如果增大pwmMax(以100hz为单位频率)
| 影响维度 | pwmMax = 10 |
pwmMax = 100 |
结果分析 |
|---|---|---|---|
| PWM频率 | 100Hz / 10 = 10Hz | 100Hz / 100 = 1Hz | 频率急剧下降。1Hz的PWM意味着LED每秒会完整地亮灭一次,人眼会看到非常明显的、严重的闪烁,而不是稳定的亮度。 |
| 亮度平滑度 | 亮度分为 11 个等级 (0-10) | 亮度分为 101 个等级 (0-100) | 平滑度提升。亮度变化的阶梯感会减弱,从“一级一级跳”变得更接近“平滑过渡”。 |
六个LED以正弦波动
#include <math.h> // 同样需要数学库
#include <stdint.h> // 引入标准整数类型
// ... (假设 ucLed 数组 和 led_disp 函数已在别处定义)
extern uint8_t ucLed[6]; // 假设有6个LED
extern void led_disp(uint8_t *ucLed);
/**
* @brief LED 显示处理函数 - 呼吸流水灯效果 (在主循环中周期性调用)
*/
void led_proc(void)
{
// 呼吸流水灯相关变量
static uint32_t breathCounter = 0; // 全局呼吸计时器
static uint8_t pwmCounter = 0; // 软件PWM计数器 (所有LED共用)
static const uint16_t breathPeriod = 4000; // 整个流水呼吸效果的周期 (调慢一点,看得清楚)
static const uint8_t pwmMax = 10; // PWM精度 (同上)
// 关键:相位差!决定了相邻LED呼吸节奏的错开程度
// π / 3 意味着一个完整周期(2π)内能容纳 6 个LED (2π / (π/3) = 6)
// 每个LED比前一个晚 π/3 的相位开始呼吸
static const float phaseShift = 3.14159f / 3.0f;
// 更新全局呼吸计时器
breathCounter = (breathCounter + 1) % breathPeriod;
// 更新PWM计数器
pwmCounter = (pwmCounter + 1) % pwmMax;
// 循环为每个LED计算独立的亮度并设置状态
for(uint8_t i = 0; i < 6; i++) // 遍历所有6个LED
{
// 计算当前LED的相位角 (angle)
// (2.0f * 3.14159f * breathCounter) / breathPeriod 是基础角度,随时间变化
// - i * phaseShift 是为第 i 个LED引入的相位延迟
float angle = (2.0f * 3.14159f * breathCounter) / breathPeriod - i * phaseShift;
// 计算原始的正弦值 (-1.0 到 1.0)
float sinValue = sinf(angle);
// 增强对比度并调整曲线 (让亮灭更分明,全亮时间更短)
// powf(x, 0.5f) 相当于 sqrt(x),对于正数,它会把小的数拉高,大的数相对压低一点 (但这里主要用在 >0 的部分)
// 对于负数,我们取绝对值再开根号,然后加回负号,保持形状对称
// 这使得亮度从暗到亮的过程变快,从亮到暗的过程也变快,中间亮的时间缩短
float enhancedValue = sinValue > 0 ? powf(sinValue, 0.5f) : -powf(-sinValue, 0.5f);
// 进一步压缩亮度曲线,使得只有在接近峰值时才真正达到高亮度
// 如果增强后的值大于0.7 (接近峰值),则保持不变;否则,乘以0.6,让它更暗
// 目的是让“光波“显得更窄,流动感更强
enhancedValue = enhancedValue > 0.7f ? enhancedValue : enhancedValue * 0.6f;
// 将处理后的 enhancedValue (-1 到 1 之间) 映射到 0 到 pwmMax 的亮度值
uint8_t brightness = (uint8_t)((enhancedValue + 1.0f) * pwmMax / 2.0f);
// 根据计算出的该LED的亮度,使用PWM逻辑设置其状态
ucLed[i] = (pwmCounter < brightness) ? 1 : 0;
}
// 所有LED的状态都计算完毕后,统一调用 led_disp 更新GPIO
led_disp(ucLed);
}





