ws2812b效果研究之一 cylon
名字来源于经典科幻系列《太空堡垒卡拉狄加》中机器人的眼部扫描效果。这个效果通常表现为灯光在LED灯带上来回移动,像一只眼睛在扫描一样。其实感觉就是流水灯的效果
平台是atmega 2560,三个引脚分别是vcc,gnd和信号引脚
对应于arduino中的fastled库中的cylon例子
简单的版本(跑马灯)
#include <FastLED.h>
#define NUM_LEDS 100 // LED灯数量
#define DATA_PIN 2 // 连接到LED数据引脚的Arduino引脚
CRGB leds[NUM_LEDS]; // 定义LED数组
void setup() {
//FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS); // 初始化LED灯带
FastLED.addLeds<WS2812,DATA_PIN,RGB>(leds,NUM_LEDS);
}
void loop() {
cylonEffect(255, 0, 0, 10); // 红色的Cylon效果,亮度255,延时50ms
}
void cylonEffect(uint8_t r, uint8_t g, uint8_t b, uint8_t wait) {
// 从左到右逐个点亮LED
for (int i = 2; i < NUM_LEDS-2; i++) {
leds[i-2] = CRGB(r/3, g, b);
leds[i-1] = CRGB(r/2, g, b);
leds[i] = CRGB(r, g, b); // 设置当前LED的颜色
leds[i+1] = CRGB(r/2, g, b);
leds[i+2] = CRGB(r/3, g, b);
FastLED.show(); // 更新显示
delay(wait); // 延时以控制移动速度
leds[i-2] = CRGB(0, 0, 0);
leds[i-1] = CRGB(0, 0, 0);
leds[i] = CRGB(0, 0, 0); // 关闭当前LED以实现“流动”效果
leds[i+1] = CRGB(0, 0, 0);
leds[i+2] = CRGB(0, 0, 0);
}
#if 1
// 从右到左逐个点亮LED
for (int i = NUM_LEDS - 1; i >= 0; i--) {
leds[i] = CRGB(r, g, b);
FastLED.show();
delay(wait);
leds[i] = CRGB(0, 0, 0);
}
#endif
}
上述其实可以实现两个效果,如果单程那就是“发射”或者“流水”,如果加上反向那就是“循环”或者“震动”
好看的版本(梦幻)
/// @file Cylon.ino
/// @brief 实现一个单个LED来回移动的动画效果(Larson扫描器效果)
/// @example Cylon.ino
#include <FastLED.h> // 包含FastLED库,用于控制LED灯带
// 定义灯带上的LED数量
#define NUM_LEDS 108
// 定义数据引脚(DATA_PIN)和时钟引脚(CLOCK_PIN)
// 对于像WS2812这样的LED芯片,只需要定义DATA_PIN;
// 如果使用SPI协议的LED芯片(如LPD8806),还需要定义CLOCK_PIN。
#define DATA_PIN 2
#define CLOCK_PIN 13 // 这里CLOCK_PIN不适用于WS2812,但定义在此方便解释
// 定义LED数组,用于存储灯带中每个LED的颜色状态
CRGB leds[NUM_LEDS];
void setup() {
// 初始化LED灯带
// 使用FastLED库中的addLeds函数来设置灯带类型(WS2812)、数据引脚(DATA_PIN)和颜色顺序(RGB)
FastLED.addLeds<WS2812, DATA_PIN, RGB>(leds, NUM_LEDS);
// 设置灯带的亮度,范围为0到255,这里设置为最大亮度255
FastLED.setBrightness(255);
}
// 定义一个函数fadeall,用于逐渐减弱所有LED的亮度,产生淡出效果
void fadeall() {
for(int i = 0; i < NUM_LEDS; i++) {
leds[i].nscale8(250); // 使用nscale8函数将LED亮度缩放到原亮度的250/255
}
}
void loop() {
static uint8_t hue = 0; // 定义一个静态变量hue,表示色调值(0到255),用于控制颜色变化
// 第一部分:LED从左到右逐个点亮
for(int i = 0; i < NUM_LEDS; i++) {
// 设置第i个LED的颜色为当前色调hue(使用HSV颜色空间),并将色调值hue递增
// CHSV函数的参数依次为色调(hue)、饱和度(255)、亮度(255)
leds[i] = CHSV(hue++, 255, 255);
// 更新LED显示,将颜色数据发送到灯带
FastLED.show();
// 将所有LED的亮度逐渐减弱,形成淡出效果
fadeall();
// 延时10毫秒,用于控制LED移动的速度
delay(10);
}
// 第二部分:LED从右到左逐个点亮
for(int i = NUM_LEDS - 1; i >= 0; i--) {
// 设置第i个LED的颜色为当前色调hue(使用HSV颜色空间),并将色调值hue递增
leds[i] = CHSV(hue++, 255, 255);(此处通过算数符号重载,将CHSV类转换成了RGB然后给到了leds[i],注意leds[i]的数据类型为CRGB类)
// 更新LED显示,将颜色数据发送到灯带
FastLED.show();
// 将所有LED的亮度逐渐减弱,形成淡出效果
fadeall();
// 延时10毫秒,用于控制LED移动的速度
delay(10);
}
}
这个效果算法的核心是:每次通过hsv色彩空间点亮下一个灯,注意灯的hue值增加,这样的话,可以使得每个灯的颜色变化没那么突然,然后把所有的灯的亮度都降低(其实只处理当前点亮的灯可以节省一些处理时间,算法才是最优的)
算法逻辑再次解释:
处理第i个灯数据最高亮度,饱和度增加,也就是CHSV(hue++, 255, 255);
发送亮灯命令,使其亮灯。
所有灯的亮度均降低一些(缩放到原亮度的250/255)此处并不会再次发送命令让其亮,待到下次循环处理下一个灯的饱和度时候才会再次亮灯。
这里提供一个hsv转rgb的参考函数
#include <stdint.h>
void hsv_to_rgb(uint8_t hue, uint8_t saturation, uint8_t value, uint8_t* red, uint8_t* green, uint8_t* blue) {
uint8_t region, remainder, p, q, t;
if (saturation == 0) {
// 如果饱和度为0,颜色为灰色(无色),即 R = G = B = V
*red = value;
*green = value;
*blue = value;
return;
}
// 计算色调所在的区域(色环被分为6个区域)
region = hue / 43;
remainder = (hue - (region * 43)) * 6;
// 计算中间值
p = (value * (255 - saturation)) >> 8;
q = (value * (255 - ((saturation * remainder) >> 8))) >> 8;
t = (value * (255 - ((saturation * (255 - remainder)) >> 8))) >> 8;
// 根据当前的色调区域,计算最终的 RGB 值
switch (region) {
case 0:
*red = value;
*green = t;
*blue = p;
break;
case 1:
*red = q;
*green = value;
*blue = p;
break;
case 2:
*red = p;
*green = value;
*blue = t;
break;
case 3:
*red = p;
*green = q;
*blue = value;
break;
case 4:
*red = t;
*green = p;
*blue = value;
break;
default:
*red = value;
*green = p;
*blue = q;
break;
}
}
后续考虑到这个函数使用极其频繁, 于是乎考虑到使用内嵌汇编和查找表进行函数优化
#include <stdint.h>
// 函数原型
void hsv_to_rgb(uint8_t hue, uint8_t saturation, uint8_t value, uint8_t* red, uint8_t* green, uint8_t* blue);
// 查找表定义:每个条目包含三个字段,分别是 RGB 通道的分配顺序
const uint8_t lookup_table[6][3] = {
{0, 1, 2}, // case 0: red = value, green = t, blue = p
{1, 0, 2}, // case 1: red = q, green = value, blue = p
{2, 0, 1}, // case 2: red = p, green = value, blue = t
{2, 1, 0}, // case 3: red = p, green = q, blue = value
{1, 2, 0}, // case 4: red = t, green = p, blue = value
{0, 2, 1} // case default: red = value, green = p, blue = q
};
void hsv_to_rgb(uint8_t hue, uint8_t saturation, uint8_t value, uint8_t* red, uint8_t* green, uint8_t* blue) {
uint8_t region, remainder, p, q, t;
// 优化:直接用位移操作代替除法
region = hue >> 5; // hue / 32 (接近于原始代码中的 hue / 43)
remainder = (hue & 31) << 3; // hue % 32 * 8,原始 remainder 计算优化
// 计算中间值
p = (value * (255 - saturation)) >> 8;
// 使用内嵌汇编优化 q 和 t 的计算
__asm__ volatile (
"mul %[temp1], %[sat], %[rem]\n\t" // temp1 = saturation * remainder
"mul %[temp2], %[sat], %[comp_rem]\n\t" // temp2 = saturation * (255 - remainder)
"rsb %[temp3], %[value], #255\n\t" // temp3 = 255 - value
"mla %[q], %[temp1], %[temp3], %[value]\n\t" // q = value - (temp1 * temp3) / 256
"mla %[t], %[temp2], %[temp3], %[value]\n\t" // t = value - (temp2 * temp3) / 256
: [q] "=r" (q), [t] "=r" (t)
: [value] "r" (value), [sat] "r" (saturation), [rem] "r" (remainder), [comp_rem] "r" (255 - remainder)
: "cc", "memory"
);
// 从查找表中获取 RGB 分量的分配顺序
const uint8_t* lookup = lookup_table[region];
uint8_t rgb[3];
rgb[lookup[0]] = value;
rgb[lookup[1]] = t;
rgb[lookup[2]] = p;
*red = rgb[0];
*green = rgb[1];
*blue = rgb[2];
}
那么大体上这个效果相关的技术细节就都处理完成了。后续将其移植到stm32上面去。