从零开始学习嵌入式LED控制 — 从基本概念到实际应用的全面指南
本页以你提供的《GPIO 与 LED 模块》课件为模板,保留原作者署名、讲解结构和进阶代码。当前开发板的 LED 接线尚未核实:原课中 PB12~PB15、PD8/PD9 是特定参考板示例,不能直接当作你的板载 LED 引脚。请先理解概念,结合 CubeMX 配置指南 与实际板卡原理图核对配置和引脚。
课程图中 LED 开关是网页模拟,不读取真实开发板。实际电路以开发板原理图和 STM32F427 数据手册为准。
LED(发光二极管)是嵌入式系统中最简单也是最常用的输出设备之一。通过控制GPIO引脚输出,我们可以点亮或熄灭LED,从而实现各种指示功能。
在深入学习LED控制之前,我们需要先了解GPIO的基本概念和配置方式,这些都是控制LED的基础知识。在本教程中,我们将从最基础的概念开始,逐步深入到STM32单片机上的GPIO配置和LED控制实现。
GPIO(通用输入/输出口)是微控制器上可以由用户自定义控制的引脚。对于嵌入式开发初学者来说,理解GPIO的几个关键概念至关重要:
将引脚配置为输入模式,用于读取外部信号。
适用场景:读取按钮状态、传感器信号等
将引脚配置为输出模式,用于驱动外部设备或信号。
适用场景:LED控制、电机驱动等
将引脚配置为模拟模式,用于模拟信号输入或输出。
适用场景:ADC读取、DAC输出等
将引脚配置为某个外设的备用功能,如UART、SPI或I2C等。
适用场景:通信接口、定时器输出等
| 特性 | 推挽输出 | 开漏输出 |
|---|---|---|
| 高电平驱动 | ✓ 内部提供 | ✗ 需要外部上拉 |
| 低电平驱动 | ✓ 内部提供 | ✓ 内部提供 |
| 多设备共享总线 | ✗ 不适合 | ✓ 适合 |
| 电流驱动能力 | ✓ 较强 | ⚠ 取决于上拉电阻 |
| LED控制适用性 | ✓ 非常适合 | ⚠ 需要外部上拉 |
F427 的 Speed 控制输出边沿驱动档位,并非闪烁频率、CPU 主频或固定的允许 GPIO 频率。下面是原课的档位比较,具体电气表现需依据 F427 数据手册及负载条件;本课 LED 选 Low。
设置引脚的驱动速度为低,适用于低频率的信号。
适用场景:按键、LED控制等低频应用
设置引脚的驱动速度为中等,适用于中等频率的信号。
适用场景:一般通信接口、显示驱动等
设置引脚的驱动速度为高,适用于高速信号。
适用场景:高速总线、快速数据传输等
设置引脚的驱动速度为超高,适用于非常高频率的信号。
适用场景:高速存储器接口、高速数据采集等
更高的速度会导致:
对于LED控制这类应用,通常选择低速即可,这能降低功耗并减少EMI。
STM32 HAL(Hardware Abstraction Layer,硬件抽象层)库提供了一套完整的API来配置和控制GPIO引脚。相比直接操作寄存器,HAL库更易用且可移植性更好。
HAL库使用GPIO_InitTypeDef结构体来配置GPIO引脚:
STM32微控制器的GPIO引脚可以分配给不同的外设功能,这称为引脚复用(Alternate Function):
GPIO_MODE_AF_PP或GPIO_MODE_AF_OD模式并设置相应的AF值在STM32中,使用任何外设前都必须使能其时钟:
__HAL_RCC_GPIOx_CLK_ENABLE()宏来使能GPIO端口时钟HAL库提供了多种操作GPIO的函数:
HAL_GPIO_WritePin(): 设置引脚输出高/低电平HAL_GPIO_ReadPin(): 读取引脚输入状态HAL_GPIO_TogglePin(): 翻转引脚状态HAL_GPIO_LockPin(): 锁定引脚配置HAL_GPIO_EXTI_IRQHandler(): 中断处理这些函数屏蔽了底层寄存器操作的复杂性,使得代码更清晰易读。
为LED控制配置GPIO的基本流程:
上拉电阻和下拉电阻是GPIO配置中非常重要的概念。它们用于确保GPIO引脚在未连接外部设备时的状态,以及在连接外部设备时的状态。
引脚不连接内部上下拉电阻,适用于浮空或外部电路自带上下拉电阻的情况。
⚠️ 注意:引脚可能处于浮空状态,容易受到外部干扰。
内部连接一个电阻到VCC,当引脚未被驱动时拉高到逻辑高电平。
内部连接一个电阻到地(GND),当引脚未被驱动时拉低到逻辑低电平。
想象一个水龙头(GPIO引脚)连接到一个小水管(上拉电阻),这个小水管始终有一小股水(电流)流向水龙头。当你不特意关闭水龙头时,这小股水会确保水龙头有水流出(高电平)。当你刻意关闭水龙头(连接到GND)时,水流会被阻断(低电平)。
在STM32微控制器中,每个GPIO引脚都有内置的上拉和下拉电阻,可以通过软件配置:
注意:内部上下拉电阻值较大,驱动能力有限。对于需要更强驱动能力的场合,建议使用外部上下拉电阻。
LED(发光二极管)是最常见的输出设备之一,我们通常通过GPIO控制LED的亮灭状态。
对于普通的LED控制,我们通常使用GPIO的输出模式,并且选择推挽输出类型。这种配置可以提供足够的驱动能力来点亮LED。
将LED的阴极通过限流电阻连接到地(GND),阳极连接到GPIO引脚。当GPIO输出高电平时LED点亮。
将LED的阳极连接到电源(VCC),阴极通过限流电阻连接到GPIO引脚。当GPIO输出低电平时LED点亮。
使用HAL库控制LED的常用函数:
HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
以下是一个使用HAL库控制LED的底层代码实现:
#include "led_app.h"
#include "gpio.h" // 确保包含了HAL库的GPIO头文件
uint8_t ucLed[6] = {1,0,1,0,1,1}; // LED 状态数组 (6个LED)
/**
* @brief 根据ucLed数组状态更新6个LED的显示
* @param ucLed Led数据储存数组 (大小为6)
*/
void led_disp(uint8_t *ucLed)
{
uint8_t temp = 0x00; // 用于记录当前 LED 状态的临时变量 (最低6位有效)
static uint8_t temp_old = 0xff; // 记录之前 LED 状态的变量, 用于判断是否需要更新显示
for (int i = 0; i < 6; i++) // 遍历6个LED的状态
{
// 将LED状态整合到temp变量中,方便后续比较
if (ucLed[i]) temp |= (1 << i); // 如果ucLed[i]为1, 则将temp的第i位置1
}
// 仅当当前状态与之前状态不同的时候,才更新显示
if (temp != temp_old)
{
// 使用HAL库函数根据temp的值设置对应引脚状态 (假设高电平点亮)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (temp & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); // LED 0 (PB12)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, (temp & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); // LED 1 (PB13)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_14, (temp & 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); // LED 2 (PB14)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_15, (temp & 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); // LED 3 (PB15)
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_8, (temp & 0x10) ? GPIO_PIN_SET : GPIO_PIN_RESET); // LED 4 (PD8)
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_9, (temp & 0x20) ? GPIO_PIN_SET : GPIO_PIN_RESET); // LED 5 (PD9)
temp_old = temp; // 更新记录的旧状态
}
}
/**
* @brief LED 显示处理函数 (主循环调用)
*/
void led_proc(void)
{
led_disp(ucLed); // 调用led_disp函数更新LED状态
}
代码包含了 "led_app.h"(函数声明)和 "gpio.h"。后者通常由STM32CubeMX生成,包含了HAL库GPIO相关的定义和函数声明,如 HAL_GPIO_WritePin。
uint8_t ucLed[6] = {1,0,1,0,1,1}; 定义了一个包含6个元素的数组,用于存储6个LED的开关状态(1为亮,0为灭),并初始化了状态。
这种方式使得从程序的其他部分可以方便地修改各个LED的状态。
在 led_disp 函数中,for循环遍历 ucLed 数组。
if (ucLed[i]) temp |= (1 << i); 这行代码是核心:如果第i个LED状态为1,则通过按位或操作 |= 将 temp 变量的第i位置为1。最终,temp 的低6位组合了所有6个LED的状态。
代码使用 static uint8_t temp_old = 0xff; 和 if (temp != temp_old) 来优化性能:
temp 与上次记录的状态 temp_old 不同时,才执行实际的GPIO写操作。HAL_GPIO_WritePin,减少了CPU负担和潜在的总线访问。与之前的寄存器操作不同,新代码使用了HAL库函数 HAL_GPIO_WritePin 来控制GPIO引脚电平。
例如 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, (temp & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET);:
GPIOB: 目标GPIO端口。GPIO_PIN_12: 目标引脚。(temp & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET: 使用按位与 & 检查 temp 的最低位(对应LED 0)。如果为1,则设置引脚为高电平 (GPIO_PIN_SET),否则为低电平 (GPIO_PIN_RESET)。假设高电平点亮LED。temp的不同位。这种方法提高了代码的可读性和可移植性。
代码明确了6个LED对应的GPIO引脚:
在实际应用中,需要确保这些引脚在STM32CubeMX中已正确配置为推挽输出模式。
| 函数名 | 参数 | 返回值 | 功能描述 |
|---|---|---|---|
| led_disp | uint8_t *ucLed - 指向6元素LED状态数组的指针 | void | 根据传入的LED状态数组更新6个LED的GPIO输出。仅当状态变化时执行更新。 |
| led_proc | 无 | void | LED处理函数,供主程序周期性调用,内部调用led_disp更新全局ucLed数组的状态。 |
将此代码用于不同项目时,需要注意:
GPIOB, GPIOD, GPIO_PIN_12 到 GPIO_PIN_9 的定义与你的硬件原理图和CubeMX配置一致。ucLed 数组大小、for循环范围 (i < 6) 以及 HAL_GPIO_WritePin 的调用。GPIO_PIN_SET)。如果你的电路是低电平点亮,需要将 GPIO_PIN_SET 和 GPIO_PIN_RESET 对调,或者在检查 ucLed[i] 时反转逻辑。直接连接LED到GPIO引脚时,必须使用限流电阻,否则可能烧毁LED或损坏MCU。根据公式 R = (Vcc - Vf) / If 计算合适的电阻值,其中Vf是LED正向压降,If是期望的电流。
STM32的GPIO引脚驱动能力通常在20mA左右。如果需要驱动大功率LED,请使用三极管或MOSFET进行放大。例如,使用2N2222或BC547等小信号三极管可以轻松控制高达100mA的电流。
注意LED的接法:高电平驱动(阳极接GPIO)或低电平驱动(阴极接GPIO)。这会影响你的代码逻辑,高电平驱动时写1点亮,低电平驱动时写0点亮。HAL库中可以使用GPIO_PIN_SET或GPIO_PIN_RESET来明确表示。
确保用于LED控制的GPIO引脚没有被其他功能占用(如UART、SPI等)。在STM32 CubeMX中配置时,应确认引脚功能设置为GPIO_Output而非其他复用功能。如果必须使用已分配的引脚,可以通过合理设计切换机制。
实现LED呼吸效果需要使用PWM(脉宽调制)技术,有以下几种方法:
实现呼吸效果通常使用正弦或指数函数计算PWM占空比,使亮度渐变更自然。
两种方式各有优缺点:
建议:初学者或注重开发效率时使用HAL库;对性能要求极高或资源极其受限的场景使用寄存器操作。实际项目中常采用混合策略,关键路径使用寄存器操作,其他部分使用HAL库。
控制多个LED实现复杂显示效果的方法:
复杂效果通常需要结合定时器中断或RTOS任务调度来实现动画、呼吸灯、流水灯等效果。
理论学习后,动手实践是最好的巩固方式。请尝试实现以下两个效果,挑战你的GPIO控制和定时器思维!
目标:让单个LED实现明暗渐变的呼吸效果。
#include <math.h> // 需要包含数学库以使用 sinf 函数
// ... (可能需要包含HAL库GPIO头文件,如 "gpio.h")
// ... (假设 ucLed 数组 和 led_disp 函数已在别处定义)
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
}
因为我们用了 sinf 函数来计算亮度,这个函数是标准C数学库里的,所以你需要在文件开头加上 #include <math.h>。别忘了,有些编译器可能需要在链接时额外指定链接数学库(比如GCC用`-lm`)。
函数里的 static 变量(像 breathCounter, pwmCounter, brightness)只会在程序第一次运行到这里时初始化一次。之后每次调用 led_proc,它们都会记住上次的值。这对于需要跨时间计数的呼吸灯和PWM来说是必须的。
它就像一个节拍器,每次调用 led_proc 就“滴答“一下(加1)。当数到 breathPeriod 那么多次时,就重新从0开始数。这个计数器的值决定了当前呼吸进行到了哪个阶段(是吸气、呼气,还是中间状态)。
pwmMax 决定了我们用软件模拟PWM波的“精细度“。这里设为10,意味着一个PWM周期被分成了10个小时间片。亮度 brightness 的值也是在0到10之间变化。如果 pwmMax 越大,理论上亮度调节能更平滑,但对CPU的要求也更高。
这一长串计算 (uint8_t)((sinf(...) + 1.0f) * pwmMax / 2.0f) 是核心。它把 breathCounter 的线性增长,通过正弦函数 sinf 变成了一个平滑的、像波浪一样的变化曲线。结果就是 brightness 的值会在0到 pwmMax 之间平滑地来回变化,完美模拟呼吸的明暗感觉。
pwmCounter 也是个计数器,但它只数到 pwmMax 就归零。它的作用是配合 brightness 实现PWM。想象一下,在一个 pwmMax 的时间窗口内,pwmCounter 从0数到9。如果当前 brightness 是5,那么当 pwmCounter 小于5时(0, 1, 2, 3, 4),LED就亮;当 pwmCounter 大于等于5时(5, 6, 7, 8, 9),LED就灭。这样,在一个窗口内,LED亮了5/10的时间,看起来就是半亮度。
这行就是根据上面PWM的比较结果,决定第一个LED(数组索引为0)的状态是亮(1)还是灭(0)。这个状态会被存到 ucLed 数组里。
最后,调用我们之前写的 led_disp 函数,它会读取 ucLed 数组里最新的状态,并真正地去操作GPIO引脚,让LED亮或灭。
思考以下问题,能让你更深入地理解:
led_proc 函数应该放在哪里调用?如果在主循环 `while(1)` 里直接调用,并且CPU很快,那么 breathCounter 和 pwmCounter 会增加得非常快。breathPeriod = 2000 可能代表2000次循环而不是2000毫秒。如何才能让它按毫秒计时?(提示:定时器中断、HAL_Delay()? 哪个更好?)
pwmMax 改成100,亮度变化会更平滑吗?PWM频率会变吗?(提示:PWM频率 = led_proc 调用频率 / pwmMax)。如果PWM频率太低(比如低于50Hz),你会看到什么现象?(提示:闪烁感)
breathPeriod 改成4000会怎么样?改成500呢?(提示:呼吸快慢)
led_proc 在微秒(us)级别被调用,breathPeriod 和 pwmMax 的值需要如何调整才能保持相同的呼吸速度和PWM频率?(提示:数值会变得非常大)。这样做有什么潜在好处或坏处?(提示:PWM频率可以很高,闪烁感完全消失;但CPU负担可能极大,uint32_t 可能不够用)。
目标:让6个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);
}
这个流水灯的代码结构和单个呼吸灯很像,也有 breathCounter, pwmCounter, breathPeriod, pwmMax 这些老朋友,它们的作用基本一样。最大的不同在于如何为 每个 LED 计算亮度。
这是实现“流水“效果的关键!phaseShift (相位差) 就像是给每个LED安排了一个不同的“起跑时间“。这里设为 π / 3.0f,意味着第1个LED按正常节奏呼吸,第2个LED比第1个晚 π/3 的相位开始呼吸,第3个又比第2个晚 π/3 ……以此类推。这样它们的呼吸节奏就错开了,看起来就像光在流动。
用一个 for(uint8_t i = 0; i < 6; i++) 循环来遍历6个LED。在循环里面,为当前这个 i 号LED单独计算它的亮度。
计算角度时,除了基础的时间变化部分 (2.0f * ... * breathCounter) / breathPeriod,还减去了 i * phaseShift。这就给每个LED引入了它自己的相位延迟。所以,即使 breathCounter 相同,不同 i 的LED算出来的 angle 也不同,导致它们的亮度也不同。
单纯用 sinf(angle) 也能实现流水,但可能效果比较"糊",亮灭过渡太缓和。代码里用了 powf(sinValue, 0.5f) (类似开根号) 和后面的 enhancedValue = enhancedValue > 0.7f ? ... 来调整亮度曲线。目的是让LED亮起来和暗下去的过程更快,并且真正在“全亮“状态的时间缩短,这样“光波“看起来更窄、更清晰,流动感更强。
你可以试着注释掉这两行调整代码,看看只用 sinValue 的效果有什么不同。
拿到调整后的 enhancedValue 后,同样用 (enhancedValue + 1.0f) * pwmMax / 2.0f 把它映射到 0 到 pwmMax 的亮度值 brightness。然后,再用 ucLed[i] = (pwmCounter < brightness) ? 1 : 0; 这个PWM逻辑来决定当前 i 号LED是亮还是灭。
注意,led_disp(ucLed) 是在 `for` 循环 外面 调用的。这意味着我们先在循环里把所有6个LED的状态都计算好,存到 ucLed 数组里,然后才一次性把这个最终状态更新到GPIO上。这样效率更高。
基于流水灯,我们可以思考更多玩法:
phaseShift 改成 PI / 6.0f 会怎么样?改成 PI / 1.0f 呢?(提示:改变流水的"波长"或同时亮的LED数量)
breathPeriod 会影响单个LED的呼吸快慢,还是整个流水的速度?(提示:两者都会影响)
powf 的指数(比如改成2.0f,即平方)或者调整 enhancedValue > 0.7f 这个阈值,观察流水效果的变化。有没有办法让光波更窄或者更宽?
i 或 phaseShift 的符号?)
sinf, powf)。如果CPU性能不足,或者 led_proc 调用频率很高,可能会有效率问题。有什么优化方法吗?(提示:查找表(Look-up Table, LUT)预计算正弦值?定点数运算代替浮点运算?)
led_proc 在us级被调用,浮点运算会成为瓶颈吗?breathPeriod = 4000 可能需要变成多少才能维持几秒的周期?(提示:4,000,000)。使用查找表或硬件PWM会是更好的选择吗?