Language语言

Hardware Effect System硬件灯效系统

NollieOS_2.1 — LED Animation EngineNollieOS_2.1 — LED 动画引擎

Overview — How It Works概述 — 工作原理

The hardware effect system drives addressable LED strips through a canvas-based architecture. Each canvas controls an independent group of LEDs with its own effect mode, colors, speed, and timing.硬件灯效系统通过画布(canvas)架构驱动可寻址 LED 灯带。每个画布独立控制一组 LED,并拥有各自的灯效模式、颜色、速度与定时。

Flash ConfigFlash 配置 FX_init_config() Runtime Arrays运行时数组 SysTickCallback Mode[n](canvas) NeoPixel_show()

Every frame, the system checks timing, then calls each canvas's effect function. Four key parameters control the behavior:每一帧系统会检查时序,再调用各画布对应的灯效函数。以下四个核心参数控制行为:

Parameter参数Scope作用域Role作用
FX_FPSGlobal (1 value)全局(单一值)Master frame interval — how often the engine ticks主帧间隔 — 引擎节拍频率
FX_step[16]Per canvas每画布Movement speed — how far the effect advances each frame移动速度 — 每帧推进多少
FX_delay[16]Per canvas每画布Frame skip — how many frames to skip between updates跳帧 — 两次更新之间跳过多少帧
FX_size[16]Per canvas每画布Light width — how many LEDs the effect shape spans光带宽度 — 灯效形状跨越多少颗 LED

Main loop pseudocode (Python):主循环伪代码(Python):

Python# Main engine loop — called by SysTick interrupt
def FX_SysTickCallback():
    current_time = get_tick()

    # Gate: only proceed if enough time has passed
    if current_time - fps_tick < FX_FPS:
        return
    fps_tick = current_time

    # Process each canvas independently
    for canvas in range(0, canvas_count):
        FX_delay_count[canvas] += 1

        if FX_delay_count[canvas] >= FX_delay[canvas]:
            FX_delay_count[canvas] = 0
            # Run the effect function for this canvas
            mode_functions[FX_mode[canvas]](canvas)

    NeoPixel_show()  # push RGB data to physical LEDs

FX_FPS Master Frame Interval主帧间隔

Type:类型: uint32_t  |  Scope:作用域: Global (shared by all canvases)全局(所有画布共用)

FX_FPS defines the minimum time (in milliseconds) between consecutive engine ticks. The engine only processes a new frame when enough time has elapsed:FX_FPS 定义两次引擎节拍之间的最短时间(毫秒)。仅当间隔足够长时才会处理新一帧:

Python# Frame rate gating
if get_tick() - fps_tick >= FX_FPS:
    fps_tick = get_tick()
    # ... process all canvases
FX_FPS valueFX_FPS 取值Frame Rate帧率Feel观感
16 ms~60 fpsVery smooth, high CPU load on the MCU running the effect engine — not your PC/phone viewing this page很流畅,运行灯效引擎的微控制器 CPU 占用高(非浏览本页的电脑/手机)
33 ms (default)(默认)~30 fpsBalanced — smooth enough for most effects均衡 — 多数灯效已足够顺滑
50 ms~20 fpsNoticeable stepping; lower CPU load on the MCU — not your PC/phone viewing this page可见阶梯感,微控制器 CPU 占用较低(指板载 MCU,非浏览设备)
100 ms~10 fpsSlow, choppy — only for dramatic effects慢且顿挫 — 适合强调性效果
Tip:提示: FX_FPS acts as a global speed baseline. To slow down only one canvas, use FX_delay on that canvas instead of raising FX_FPS (which would affect everything).FX_FPS 是全局速度基准。若只想让某一画布变慢,请对该画布使用 FX_delay,而不要提高 FX_FPS(否则会拖累全部画布)。

Live demo — Scanning dot at different FPS:实时演示 — 不同 FPS 下的扫描点:

FX_FPS = 33ms (~30 fps) — FastFX_FPS = 33ms(约 30 fps)— 快 LIVE实时
FX_FPS = 100ms (~10 fps) — SlowFX_FPS = 100ms(约 10 fps)— 慢 LIVE实时

The diagram below shows how FX_FPS gates the engine tick cycle:下图说明 FX_FPS 如何门控引擎节拍:

time →时间 → 0ms 33ms 66ms 99ms 132ms 165ms TICK 节拍 TICK节拍 TICK节拍 TICK节拍 TICK节拍 TICK节拍 (FX_FPS = 33) TICK 节拍 TICK节拍 TICK节拍 (FX_FPS = 66)

FX_step Movement Speed移动速度

Type:类型: uint8_t[16]  |  Scope:作用域: Per canvas每画布

FX_step controls how far the effect advances each frame. Its meaning varies slightly by effect mode, but the core idea is always "amount of change per tick".FX_step 控制每帧灯效推进多少。具体含义因模式略有不同,核心都是「每次节拍的变化量」。

Effect Type灯效类型What FX_step ControlsFX_step 控制什么Example示例
Scanning / Wipe扫描 / 擦除Pixels moved per frame每帧移动的像素数step=1 → 1 LED/frame, step=3 → 3 LEDs/framestep=1 → 每帧 1 颗 LED;step=3 → 每帧 3 颗
Rainbow彩虹Hue increment per frame每帧色相增量step=1 → slow color shift, step=10 → rapid cyclingstep=1 → 变色慢;step=10 → 快速循环
Breath呼吸Brightness change per frame每帧亮度变化step=1 → slow fade, step=5 → fast pulsestep=1 → 缓起缓落;step=5 → 快脉冲
Meteor / Bullets流星 / 子弹Travel distance per frame每帧移动距离step=2 → medium speed particlestep=2 → 中等速度粒子
Python# Scanning effect — position advances by FX_step each frame
def FX_scanning(canvas):
    local[canvas][LED_STEP] += FX_step[canvas]   # move forward
    if local[canvas][LED_STEP] >= led_count:
        local[canvas][LED_STEP] = 0              # wrap around

# Rainbow effect — hue rotates by FX_step each frame
def FX_rainbow(canvas):
    hue_offset[canvas] += FX_step[canvas]       # rotate hue
    for i in range(led_count):
        leds[i] = hsv_to_rgb(hue_offset[canvas], 255, 255)

Live demo — Adjust FX_step with the slider:实时演示 — 拖动滑块调节 FX_step:

Scanning effect — drag slider to change step扫描灯效 — 拖动滑块改变 step INTERACTIVE可交互

FX_delay Frame Skip Counter跳帧计数

Type:类型: uint8_t[16]  |  Scope:作用域: Per canvas每画布

FX_delay determines how many engine ticks to skip before the canvas actually updates. It acts as a per-canvas speed divider on top of FX_FPS.FX_delay 决定在画布真正更新前要跳过多少次引擎节拍。它是在 FX_FPS 之上、按画布划分的速度分频器。

JavaScript// Frame-skip logic for each canvas
function processCanvas(canvas) {
    FX_delay_count[canvas]++;

    if (FX_delay_count[canvas] >= FX_delay[canvas]) {
        FX_delay_count[canvas] = 0;
        modeFunctions[FX_mode[canvas]](canvas);  // execute!
    }
    // otherwise: skip this frame, do nothing
}

Effective update rate:等效更新周期: actual_interval = FX_FPS × (FX_delay + 1)

FX_delayExecutes every执行间隔With FX_FPS=33ms当 FX_FPS=33ms
0Every frame (no skip)每帧执行(不跳)33ms → ~30 fps
1Every 2nd frame每 2 帧一次66ms → ~15 fps
2Every 3rd frame每 3 帧一次99ms → ~10 fps
5Every 6th frame每 6 帧一次198ms → ~5 fps

Animated timeline动画时间轴 (FX_delay = 2 — watch the highlight move):(FX_delay = 2,观察高亮移动):

RUN运行
skip跳过
skip跳过
RUN运行
skip跳过
skip跳过
RUN运行
skip跳过
skip跳过
RUN运行

Frame:帧:   0       1       2       3       4       5       6       7       8       9

Live demo — Two strips, same step, different delay:实时演示 — 两条灯带,step 相同、delay 不同:

FX_delay = 0 (every frame)FX_delay = 0(每帧) LIVE实时
FX_delay = 3 (every 4th frame)FX_delay = 3(每第 4 帧) LIVE实时
Tip:提示: Use FX_delay to run different canvases at different speeds while sharing the same FX_FPS clock. For example, canvas 0 with delay=0 runs at 30fps, while canvas 1 with delay=2 runs at 10fps.在共用同一 FX_FPS 时钟的前提下,用 FX_delay 让不同画布以不同速度运行。例如画布 0 的 delay=0 约 30fps,画布 1 的 delay=2 约 10fps。

FX_size Light Width / Shape Size光带宽度 / 形状尺寸

Type:类型: uint8_t[16]  |  Scope:作用域: Per canvas每画布

FX_size controls how wide the lit portion of the effect is, measured in LEDs. The actual rendered width is:FX_size 控制灯效点亮区域有多宽,单位为 LED 颗数。实际渲染宽度为:

SHOW_SIZE = FX_size + 1

JavaScript// Rendering a scanning bar with FX_size width
function renderScanningBar(canvas) {
    const showSize = FX_size[canvas] + 1;   // actual width
    const pos = local[canvas].LED_STEP;

    for (let i = 0; i < ledCount; i++) {
        if (i >= pos && i < pos + showSize) {
            leds[i] = color_1[canvas];          // lit
        } else {
            leds[i] = { r: 0, g: 0, b: 0 };   // off
        }
    }
}

// Stacking effect uses doubled width
const lumpWidth = FX_size[canvas] * 2 + 2;  // wider lumps

Some effects use FX_size in specialized ways:部分灯效会以特殊方式使用 FX_size:

Effect灯效How size is usedsize 的用法
Scanning / Wipe扫描 / 擦除Width of the moving bar (SHOW_SIZE pixels)移动光条宽度(SHOW_SIZE 颗)
Stacking堆叠Width of each "lump" = size * 2 + 2 pixels每段「块」宽度 = size * 2 + 2
Meteor流星Length of the meteor head + tail流星头部 + 尾迹长度
Bullets子弹Length of each bullet projectile每发子弹长度
Wave波浪Width of the wave peak波峰宽度

Live demo — Adjust FX_size with the slider:实时演示 — 拖动滑块调节 FX_size:

Scanning bar — drag slider to change size扫描光条 — 拖动滑块改变宽度 INTERACTIVE可交互
SHOW_SIZE = 3
Note:注意: Setting FX_size too large relative to your LED strip length may cause the effect to fill the entire strip or behave unexpectedly. Keep size < total_LEDs / 2 for best results.相对灯带总长,FX_size 过大可能导致整条被填满或表现异常。建议保持 size < total_LEDs / 2

Putting It All Together综合理解

Here's the complete frame engine in Python pseudocode:完整帧引擎的 Python 伪代码如下:

Python# Complete frame engine pseudocode
def FX_SysTickCallback():
    # Step 1: Check master timing gate
    if get_tick() - fps_tick < FX_FPS:          # e.g. 33ms
        return
    fps_tick = get_tick()

    # Step 2: Process each canvas
    for canvas in range(canvas_count):
        # Step 2a: Frame skip check
        FX_delay_count[canvas] += 1
        if FX_delay_count[canvas] < FX_delay[canvas]:  # e.g. 2
            continue                                  # skip
        FX_delay_count[canvas] = 0

        # Step 2b: Execute the effect
        # Inside each effect, FX_step controls speed
        # and FX_size controls the light width
        mode_functions[FX_mode[canvas]](canvas)

    # Step 3: Output to LEDs
    NeoPixel_show()

And here it is in JavaScript if you prefer:若更习惯 JavaScript,等价逻辑如下:

JavaScript// Complete frame engine pseudocode
function FX_SysTickCallback() {
    // Step 1: Master timing gate
    if (getTick() - fpsTick < FX_FPS) return;  // e.g. 33ms
    fpsTick = getTick();

    // Step 2: Process each canvas
    for (let c = 0; c < canvasCount; c++) {
        // Step 2a: Frame skip
        delayCount[c]++;
        if (delayCount[c] < FX_delay[c]) continue;  // skip
        delayCount[c] = 0;

        // Step 2b: Run effect (uses FX_step + FX_size internally)
        modeFunctions[FX_mode[c]](c);
    }

    // Step 3: Push to hardware
    NeoPixel_show();
}

Example configuration:配置示例:

Parameter参数Value取值Effect效果
FX_FPS33Engine ticks every 33ms (~30 fps)引擎每 33ms 节拍一次(约 30 fps)
FX_delay[0]2Canvas 0 updates every 3rd tick = 99ms per update画布 0 每第 3 次节拍更新 = 每次间隔 99ms
FX_step[0]3Moves 3 pixels per update每次更新移动 3 颗
FX_size[0]5Renders a 6-pixel wide bar渲染 6 颗宽的光条

Result: A 6-pixel wide bar moves 3 pixels every 99ms across the LED strip.结果:6 颗宽的光条每隔 99ms 沿灯带移动 3 颗

Live demo — All parameters combined (interactive):实时演示 — 全部参数联动(可交互):

Combined effect — tweak all parameters组合灯效 — 调节全部参数 INTERACTIVE可交互

Perceived speed体感速度 = FX_step / (FX_FPS × (FX_delay + 1))

Mode 23 FX_ColorFromPalette

This mode maps a 16-color palette across the entire LED strip, with automatic scrolling each frame. It supports two rendering modes: direct 16-color cycling and 128-level smooth interpolation.该模式将16 色调色板映射到整条灯带,并每帧自动滚动。支持两种渲染:16 色硬切换128 级平滑插值

Parameters参数

Parameter参数Role in this mode在本模式中的作用Range范围
FX_dynamic_1Palette group index — selects which of the 8 palettes to use调色板组索引 — 选用 8 组中的哪一组0 – 7
FX_dynamic_2Blend mode — 0 = direct 16-color cycling, 1 = 128-level lerp smooth混合模式 — 0 = 16 色直接循环,1 = 128 级平滑插值0 or 1
FX_stepScroll speed — how fast the palette scrolls across the strip each frame滚动速度 — 每帧调色板沿灯带滚多快1 – 255
FX_sizeColor density — the index gap between adjacent LEDs (SHOW_SIZE = size + 1)颜色密度 — 相邻 LED 的索引间隔(SHOW_SIZE = size + 1)0 – 255
FX_brightnessGlobal brightness scaling (0–100, converted to 0–255 internally)全局亮度(0–100,内部换算为 0–255)0 – 100

How It Works工作原理

Python# FX_ColorFromPalette — Mode 23
def FX_ColorFromPalette(canvas):
    pal_idx = FX_dynamic_1[canvas] & 0x07   # which palette (0-7)
    blend   = FX_dynamic_2[canvas]           # 0=direct, 1=smooth
    bright  = int(FX_brightness[canvas] * 2.55)  # 0-100 → 0-255

    # Advance the start index each frame (scroll effect)
    color_index = FX_local_u8[canvas] += FX_step[canvas]

    for each_led in canvas_leds:
        if blend:
            # Smooth mode: 128-level interpolation
            color = ColorFromPalette(palette[pal_idx],
                                     color_index & 0x7F, bright)
        else:
            # Direct mode: hard 16-color cycling
            color = palette[pal_idx][color_index & 0x0F]
            color = scale_brightness(color, bright)

        leds[led_pos] = color
        color_index += SHOW_SIZE   # density gap between LEDs

The Two Blend Modes两种混合模式

Mode 0 — Direct (16 colors):模式 0 — 直接(16 色): Each LED picks a color straight from the 16-entry palette using color_index & 0x0F. Adjacent LEDs jump between palette entries. Fast, but you can see hard color boundaries.每颗 LED 用 color_index & 0x0F 从 16 项调色板直接取色。相邻 LED 会在条目间跳变。速度快,但可见明显色带边界。

Mode 1 — Smooth (128 levels):模式 1 — 平滑(128 级): Uses ColorFromPalette() to interpolate between palette entries. The 7-bit index (0–127) is split:使用 ColorFromPalette() 在调色条目间插值。7 位索引(0–127)拆分为:

Python# ColorFromPalette — 128-level palette interpolation
def lerp8(a, b, frac):
    # frac = 0..7, produces 0/8 to 7/8 blend
    return a + (b - a) * frac // 8

def ColorFromPalette(palette, index, brightness):
    hi  = (index >> 3) & 0x0F   # upper 4 bits → palette[0..15]
    lo  =  index       & 0x07   # lower 3 bits → blend weight 0..7

    color_a = palette[hi]
    color_b = palette[(hi + 1) & 0x0F]  # next color (wraps 15→0)

    # Linear interpolate each channel
    r = lerp8(color_a.r, color_b.r, lo)
    g = lerp8(color_a.g, color_b.g, lo)
    b = lerp8(color_a.b, color_b.b, lo)

    # Apply brightness
    return CRGB(r * brightness // 255,
                g * brightness // 255,
                b * brightness // 255)
Index 0..127 breakdown:索引 0..127 分解: index = 0b HHHHLLL ↑↑↑↑ ↑↑↑ hi=0..15 lo=0..7 select选取 blend混合 palette调色板 fraction比例 entry条目 between介于 hi & hi+1 Example: index = 44 = 0b 00101 100例:index = 44 = 0b 00101 100 hi = 5, lo = 4 → 50% blend between palette[5] and palette[6] hi = 5,lo = 4 → palette[5] 与 palette[6] 之间约 50% 混合

How FX_size Affects DensityFX_size 如何影响密度

SHOW_SIZE = FX_size + 1 is added to color_index after each LED. This controls how quickly you move through the palette across the strip:每颗 LED 之后将 SHOW_SIZE = FX_size + 1 加到 color_index,从而控制沿灯带穿过调色板的速度:

FX_sizeSHOW_SIZEEffect on strip对灯带的影响
01Palette spread across many LEDs — gentle gradient, takes 128 (or 16) LEDs to complete one full palette cycle调色板摊在很多 LED 上 — 渐变柔和,完整走一圈约需 128(平滑)或 16(直接)颗
344× denser — palette repeats every ~32 LEDs (smooth) or ~4 LEDs (direct)4 倍更密 — 约每 32 颗(平滑)或 4 颗(直接)重复一轮
788× denser — tight, compressed color bands8 倍更密 — 色带紧凑
1516One full palette per 8 LEDs (smooth) or every single LED shows a different color (direct)平滑模式下约每 8 颗一轮完整调色板;直接模式下每颗 LED 可对应不同颜色

All 8 Palette Groups全部 8 组调色板

0 Rainbow0 彩虹
1 Ocean1 海洋
2 Lava2 熔岩
3 Forest3 森林
4 Sunset4 日落
5 Aurora5 极光
6 Party6 派对
7 Ice7 冰霜

Live demo — Select palette group, adjust step & density:实时演示 — 选择调色板组并调节 step 与密度:

ColorFromPalette — Smooth (blend = 1)ColorFromPalette — 平滑(blend = 1) INTERACTIVE可交互
ColorFromPalette — Direct 16-color (blend = 0)ColorFromPalette — 直接 16 色(blend = 0)
Tip:提示: For the smoothest gradients, use blend mode 1 with a small FX_size (0–3). For sharp, retro-style color bands, use blend mode 0 with larger FX_size.要最顺滑的渐变,用混合模式 1 并取较小 FX_size(0–3)。要锐利、复古色带,用模式 0 并增大 FX_size。

Palette Editor调色板编辑器

Click any color swatch to edit it. Changes update the preview strip in real time.点击任意色块即可编辑,预览灯带会实时更新。

Click a color to edit — selected entry highlighted点击颜色编辑 — 当前选中项高亮
Preview — 128-level smooth interpolation预览 — 128 级平滑插值

Effect Mode Demos (0 – 22)灯效模式演示(0 – 22)

All effect modes animated live. Mode 23 (ColorFromPalette) is demonstrated in its own section above.以下模式均为实时动画。模式 23(ColorFromPalette)见上文专节。

Mode 0: Off模式 0:关闭
Mode 1: Solid — all LEDs fixed to color_1模式 1:常亮 — 全部 LED 固定为 color_1
Mode 2: Breath — linear brightness fade in/out模式 2:呼吸 — 亮度线性渐亮渐暗
Mode 3: Scanning — alternating bright/dark bands scrolling模式 3:扫描 — 明暗条带交替滚动
Mode 4: Rainbow — all LEDs rotate through hues together模式 4:彩虹 — 全体 LED 同步色相旋转
Mode 5: Rainbow Cycle — hue gradient spread by position模式 5:彩虹环 — 按位置展开色相渐变
Mode 6: Meteor — multiple particles with dim tails streaming模式 6:流星 — 多粒子暗尾迹流动
Mode 7: Shake — bar bounces back and forth between edges模式 7:晃动 — 光条在两端间来回弹跳
Mode 8: Slowly Wipe — fill forward, then slow brightness fade erase模式 8:慢擦除 — 正向填满后慢速亮度渐隐擦除
Mode 9: Color Wipe — fill forward, then erase forward模式 9:色擦除 — 正向填满再正向擦除
Mode 10: Rainbow Strip — scanning bands with rainbow hues (fast)模式 10:彩虹条 — 扫描条带 + 彩虹色(快)
Mode 11: Rainbow Strip L — scanning bands with rainbow hues (slow gradient)模式 11:彩虹条 L — 扫描条带 + 彩虹色(慢渐变)
Mode 12: Wave — single-direction color sweep, head to tail模式 12:波浪 — 单向色带从首到尾扫过
Mode 13: Sunny — forward sweep then reverse sweep模式 13:阳光 — 正向扫过后反向扫回
Mode 14: Pull Flower — forward sweep, blackout reverse, then repeat模式 14:拉花 — 正向扫、灭灯反向,再循环
Mode 15: Stack AB 1 — bar travels and stacks at end, then fades out模式 15:堆叠 AB 1 — 光条前进至末端堆叠后渐隐
Mode 16: Stack AB 2 — stack then flicker (blink) before fade out模式 16:堆叠 AB 2 — 堆叠后闪烁再渐隐
Mode 17: Stack AB 3 — stack over a dim background模式 17:堆叠 AB 3 — 在暗底上堆叠
Mode 18: Integration 1 — two beams converge from edges, then separate模式 18:汇合 1 — 两束光从两侧汇合再分开
Mode 19: Integration 2 — two beams converge, merge and expand outward模式 19:汇合 2 — 两束汇合、融合后向外扩张
Mode 20: Interchange — two dots converge, collide, then diverge模式 20:交错 — 两点靠近、碰撞后分开
Mode 21: Bullets — alternating lit/dark segments streaming forward then reverse模式 21:子弹 — 明暗段交替前进再反向
Mode 22: Sequence — sequential fill cycling through 8 colors模式 22:序列 — 顺序填充并循环 8 色