NollieOS_2.1 — LED Animation EngineNollieOS_2.1 — LED 动画引擎
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,并拥有各自的灯效模式、颜色、速度与定时。
Every frame, the system checks timing, then calls each canvas's effect function. Four key parameters control the behavior:每一帧系统会检查时序,再调用各画布对应的灯效函数。以下四个核心参数控制行为:
| Parameter参数 | Scope作用域 | Role作用 |
|---|---|---|
| FX_FPS | Global (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
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 fps | Very smooth, high CPU load on the MCU running the effect engine — not your PC/phone viewing this page很流畅,运行灯效引擎的微控制器 CPU 占用高(非浏览本页的电脑/手机) |
| 33 ms (default)(默认) | ~30 fps | Balanced — smooth enough for most effects均衡 — 多数灯效已足够顺滑 |
| 50 ms | ~20 fps | Noticeable stepping; lower CPU load on the MCU — not your PC/phone viewing this page可见阶梯感,微控制器 CPU 占用较低(指板载 MCU,非浏览设备) |
| 100 ms | ~10 fps | Slow, choppy — only for dramatic effects慢且顿挫 — 适合强调性效果 |
Live demo — Scanning dot at different FPS:实时演示 — 不同 FPS 下的扫描点:
The diagram below shows how FX_FPS gates the engine tick cycle:下图说明 FX_FPS 如何门控引擎节拍:
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:
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_delay | Executes every执行间隔 | With FX_FPS=33ms当 FX_FPS=33ms |
|---|---|---|
| 0 | Every frame (no skip)每帧执行(不跳) | 33ms → ~30 fps |
| 1 | Every 2nd frame每 2 帧一次 | 66ms → ~15 fps |
| 2 | Every 3rd frame每 3 帧一次 | 99ms → ~10 fps |
| 5 | Every 6th frame每 6 帧一次 | 198ms → ~5 fps |
Animated timeline动画时间轴 (FX_delay = 2 — watch the highlight move):(FX_delay = 2,观察高亮移动):
Frame:帧: 0 1 2 3 4 5 6 7 8 9
Live demo — Two strips, same step, different delay:实时演示 — 两条灯带,step 相同、delay 不同:
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:
size < total_LEDs / 2 for best results.相对灯带总长,FX_size 过大可能导致整条被填满或表现异常。建议保持 size < total_LEDs / 2。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_FPS | 33 | Engine ticks every 33ms (~30 fps)引擎每 33ms 节拍一次(约 30 fps) |
| FX_delay[0] | 2 | Canvas 0 updates every 3rd tick = 99ms per update画布 0 每第 3 次节拍更新 = 每次间隔 99ms |
| FX_step[0] | 3 | Moves 3 pixels per update每次更新移动 3 颗 |
| FX_size[0] | 5 | Renders 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):实时演示 — 全部参数联动(可交互):
Perceived speed体感速度 = FX_step / (FX_FPS × (FX_delay + 1))
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 级平滑插值。
| Parameter参数 | Role in this mode在本模式中的作用 | Range范围 |
|---|---|---|
| FX_dynamic_1 | Palette group index — selects which of the 8 palettes to use调色板组索引 — 选用 8 组中的哪一组 | 0 – 7 |
| FX_dynamic_2 | Blend mode — 0 = direct 16-color cycling, 1 = 128-level lerp smooth混合模式 — 0 = 16 色直接循环,1 = 128 级平滑插值 | 0 or 1 |
| FX_step | Scroll speed — how fast the palette scrolls across the strip each frame滚动速度 — 每帧调色板沿灯带滚多快 | 1 – 255 |
| FX_size | Color density — the index gap between adjacent LEDs (SHOW_SIZE = size + 1)颜色密度 — 相邻 LED 的索引间隔(SHOW_SIZE = size + 1) | 0 – 255 |
| FX_brightness | Global brightness scaling (0–100, converted to 0–255 internally)全局亮度(0–100,内部换算为 0–255) | 0 – 100 |
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
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)
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_size | SHOW_SIZE | Effect on strip对灯带的影响 |
|---|---|---|
| 0 | 1 | Palette spread across many LEDs — gentle gradient, takes 128 (or 16) LEDs to complete one full palette cycle调色板摊在很多 LED 上 — 渐变柔和,完整走一圈约需 128(平滑)或 16(直接)颗 |
| 3 | 4 | 4× denser — palette repeats every ~32 LEDs (smooth) or ~4 LEDs (direct)4 倍更密 — 约每 32 颗(平滑)或 4 颗(直接)重复一轮 |
| 7 | 8 | 8× denser — tight, compressed color bands8 倍更密 — 色带紧凑 |
| 15 | 16 | One full palette per 8 LEDs (smooth) or every single LED shows a different color (direct)平滑模式下约每 8 颗一轮完整调色板;直接模式下每颗 LED 可对应不同颜色 |
Live demo — Select palette group, adjust step & density:实时演示 — 选择调色板组并调节 step 与密度:
Click any color swatch to edit it. Changes update the preview strip in real time.点击任意色块即可编辑,预览灯带会实时更新。
All effect modes animated live. Mode 23 (ColorFromPalette) is demonstrated in its own section above.以下模式均为实时动画。模式 23(ColorFromPalette)见上文专节。