1. 项目概述当分形算法遇见南瓜灯又快到万圣节了作为一个玩了十多年嵌入式开发的老创客每年这个时候总想折腾点新花样。传统的南瓜灯里放根蜡烛氛围是有了但明火带来的安全隐患、蜡烛燃烧时间短、容易被风吹灭这些问题实在让人头疼。这几年用LED模拟烛光成了主流但市面上的电子蜡烛要么光线死板要么就是简单的随机闪烁离真实火焰那种灵动、跳跃的感觉总差那么一口气。今年我决定自己动手用一块Adafruit Circuit Playground开发板结合一个在数学和计算机图形学里都挺有意思的概念——分形算法来做一个真正“有灵魂”的电子烛光。这个项目的核心就是用代码去“欺骗”我们的眼睛让一排彩色的LED灯NeoPixel模拟出火焰那种看似随机、实则蕴含内在规律的动态变化。听起来有点玄乎其实背后的原理非常优雅而且代码量出奇地少。Circuit Playground这块板子特别适合做这种创意小项目它集成了10个可编程的RGB LED、运动传感器、麦克风、按钮等一堆外设自带USB接口用3节AAA电池就能驱动完全不需要焊接。你可以把它塞进南瓜里也可以放进纸灯笼、玻璃瓶甚至自己设计的3D打印外壳里。更重要的是它支持Arduino、CircuitPython和MakeCode图形化编程三种开发方式无论你是编程老手还是刚入门的学生都能找到适合自己的上手路径。接下来我就带你从硬件准备、代码原理到实际雕刻南瓜完整走一遍这个项目。你会发现用数学之美点亮一个节日装饰是一件特别有成就感的事。2. 硬件准备与方案选型2.1 核心硬件为什么是Circuit Playground在做任何嵌入式项目前选择合适的硬件平台是第一步。我选择Adafruit Circuit Playground主要是基于以下几点考量集成度高开箱即用对于灯光类项目最核心的需求就是可控的光源。Circuit Playground Classic和Express版本都在板载了一圈10个NeoPixel LED。NeoPixel的优势在于每个LED都可以独立寻址并且只需要一个数据引脚就能控制一整圈极大地简化了布线。此外板子还内置了稳压电路和USB串口你不需要再额外购买和连接LED驱动模块、电平转换芯片省去了大量繁琐的硬件调试工作。供电灵活适合移动场景南瓜灯需要独立运行不能一直拖着根USB线。Circuit Playground支持通过板载的JST PH 2mm接口连接3节AAA电池盒供电。我实测下来使用全新的碱性电池驱动10个LED以中等亮度模拟烛光连续工作超过12小时毫无压力。如果追求更长的续航或者想用充电电池也可以使用常见的USB移动电源充电宝通过Micro-USB口供电灵活性非常高。多编程环境支持受众面广这是Circuit Playground系列一个很大的亮点。Arduino (C/C)适合有编程基础追求极致性能和底层控制的开发者。代码执行效率高社区资源极其丰富。CircuitPython基于Python 3语法简洁易懂交互性强连接电脑后直接出现一个U盘修改代码文件后自动运行。特别适合编程初学者、教育场景或者需要快速原型验证。MakeCode (图形化)完全基于浏览器的积木式编程通过拖拽代码块就能完成逻辑。这是引导完全没有编程经验的人比如小朋友入门的最佳方式。对于这个烛光模拟项目三种方式我都实现了效果略有不同但核心逻辑一致。你可以根据自己的技术背景选择。物料清单主控板Adafruit Circuit Playground Classic 或 Express 一块。两者在这个项目上功能几乎一样Express性能更强且支持CircuitPython和MakeCode。供电方案二选一3xAAA电池盒带开关和JST接头这是最“正统”的移动方案体积小巧易于隐藏在南瓜内部。USB移动电源 Micro-USB数据线如果你手头有闲置的充电宝这是零成本的方案。注意有些充电宝在输出电流过小时会自动关机而我们的项目功耗很低可能会触发此保护。选择电路板或电池盒供电更稳妥。被照物体南瓜是经典选择但绝非唯一。纸灯笼的效果出奇地好光线柔和且扩散均匀。玻璃罐、镂空的塑料盒、甚至3D打印的模型比如一个镂空的骷髅头都是绝佳的载体。发挥你的创意。防水防污措施如使用真南瓜一个密封塑料袋。南瓜内部潮湿且有瓜瓤直接用电子设备风险很大。将Circuit Playground和电池盒一起放入密封袋既能防潮防污也不影响无线信号的传输本项目不用无线和灯光透出。2.2 备选方案与扩展思路虽然本项目聚焦于Circuit Playground但同样的算法可以轻松移植到其他平台上Adafruit HalloWing这是一款为万圣节项目量身定做的板子形状像一只蝙蝠。它没有板载LED但有一个专用的“NEOPIX”端口可以连接最多30个NeoPixel灯带。如果你要做一个大号的、需要更多灯光的装饰比如一个发光的鬼屋模型HalloWing配合灯带是更好的选择。代码只需修改引脚定义和LED数量即可。通用的Arduino开发板如Uno, Nano NeoPixel灯环/灯带如果你手头有其他Arduino板子完全可以实现。你需要额外连接一个NeoPixel组件并为其提供5V电源注意电流需求。代码的核心算法完全通用只需调整对应的NeoPixel库初始化部分。ESP32/ESP8266系列如果你希望加入Wi-Fi控制比如通过手机App远程切换灯光模式、调整亮度或颜色那么ESP系列是更强大的选择。你可以在实现烛光算法的基础上轻松增加网络服务功能。注意无论使用哪种硬件务必注意LED的电流消耗。10个NeoPixel全白最亮时总电流可能接近600mA。我们的烛光模拟以黄色/橙色为主且亮度动态变化平均电流会小很多通常在50-150mA之间AAA电池或小型USB电源足以应付。但如果使用几十个甚至上百个LED的灯带就必须计算总电流并配备足够功率的电源。3. 烛光模拟的核心分形算法解析市面上很多LED烛光代码其本质是让亮度在一个范围内完全随机地跳动。这种效果生硬、不自然因为真实的火焰变化是有“惯性”和“连续性”的下一个时刻的亮度与上一个时刻紧密相关。我们的目标是模拟这种平滑中带有随机扰动的变化而分形算法恰好是描述这种“自然噪声”的绝佳工具。3.1 什么是分形从西兰花到火焰分形Fractal的一个核心特征是自相似性无论你放大多少倍去观察它的局部其形状都与整体相似。自然界中经典的例子是罗马花椰菜Romanesco broccoli它的每一个小花簇都是整个花球的一个缩小版。火焰、云朵、山脉、海岸线这些看似杂乱无章的自然形态在数学上都表现出分形特征。这意味着我们可以用一个相对简单的、重复的规则去生成极其复杂且逼真的图案。在计算机图形学中分形常被用来生成自然地形。我们的任务更简单我们不需要生成三维地形只需要生成一条随时间变化的、模拟火焰亮度的一维“地形线”。3.2 一维中点置换法算法的具象化理解我们采用的算法是一种简化版的“中点置换法”Midpoint Displacement常用于生成分形地形。这里我们把它应用在“亮度-时间”这个二维平面上。假设我们要生成一段时间内比如2秒的亮度曲线。亮度用0-255的数值表示0最暗255最亮。确立起点和终点我们随机选择一个起始亮度PREV比如128中间值再随机选择一个结束亮度LVL比如在64到192之间随机。这样我们在时间轴上就有了两个点可以连成一条线段。这条线段代表了亮度变化的“大趋势”。第一次细分与扰动找到这条线段的时间中点计算其对应的亮度中点(PREV LVL)/2。然后关键的一步来了我们给这个中点的亮度值加上一个随机扰动范围是±OFFSET比如±32。这个扰动模拟了火焰在变化过程中不可预测的“跳跃”。于是我们得到了三个点起点、被扰动后的中点、终点。递归细分现在我们有了两条更短的线段起点到中点中点到终点。对每一条新线段我们重复第2步的操作但有一个重要变化扰动范围OFFSET减半比如从±32变成±16。这体现了分形的自相似性大尺度的波动幅度大小尺度的波动幅度小。重复直到停止继续对产生的新线段进行细分和扰动每次都将扰动范围减半。当扰动范围OFFSET减少到0时由于我们使用整数运算OFFSET/2最终会变成0递归停止。此时我们已经得到了一条由许多点构成的、充满大大小小“峰谷”的亮度曲线。这个过程就像是在反复地“折纸”或者“挤压弹簧”先定下两头然后在中间随机推一下再把每一半中间随机推一下但力度更小如此反复最终得到一条非常自然、具有多尺度细节的波动曲线。3.3 递归函数算法的代码骨架在代码中这个“细分-扰动”的过程是通过一个递归函数split()来实现的。递归简单说就是函数自己调用自己。// 伪代码示意 void split(起点亮度 y1, 终点亮度 y2, 扰动范围 offset) { if (offset 0) { // 如果还能继续细分 // 1. 计算中点并加上随机扰动 中点亮度 mid (y1 y2) / 2 random(-offset, offset); // 2. 扰动范围减半 int 新扰动范围 offset / 2; // 3. 递归处理左半段和右半段 split(y1, mid, 新扰动范围); // 处理起点到中点 split(mid, y2, 新扰动范围); // 处理中点到终点 } else { // 递归到底了此时 y1 就是当前时间点应该设置的亮度值 设置LED亮度为 y1; 等待一小段时间; } }递归的深度与停止条件每次调用split()offset参数都会减半。因为offset是整数经过几次除2后它最终会变成0例如 32-16-8-4-2-1-0。当offset为0时if条件不成立函数不再调用自身而是执行else分支去实际设置LED的亮度。这个机制完美地防止了函数无限递归下去导致程序崩溃栈溢出。如何动起来在主循环loop()中我们完成一段曲线的生成后会把当前的终点亮度LVL赋值给PREV作为下一段曲线的起点然后再随机生成一个新的终点LVL开始下一轮递归。这样亮度变化就连绵不断地持续下去了。4. 代码实现与逐行解析理解了算法原理我们来看具体的代码实现。我将以最经典的Arduino C/C版本为例进行深度解析因为它最接近底层能清晰地展示所有细节。CircuitPython版本逻辑完全一致只是语法不同MakeCode版本因图形化限制实现方式有差异但目标相同。4.1 Arduino代码深度剖析// SPDX-FileCopyrightText: 2018 Phillip Burgess for Adafruit Industries // SPDX-License-Identifier: MIT #include Adafruit_CircuitPlayground.h uint8_t prev 128; // 初始亮度设为中间值 void setup() { CircuitPlayground.begin(); // 初始化板载所有功能 CircuitPlayground.setBrightness(255); // 设置NeoPixel全局亮度为最大后续通过颜色值控制实际亮度 } void loop() { // 1. 随机确定本段“旅程”的终点亮度128 ± 64即范围在64-192之间 uint8_t lvl random(64, 192); // 2. 从上一个亮度(prev)平滑变化到新亮度(lvl)初始扰动幅度为32 split(prev, lvl, 32); // 3. 本次的终点成为下一次的起点 prev lvl; // 注意这里没有额外的delay因为split函数内部的延时已经控制了变化节奏 } // 核心递归函数分形细分亮度区间 void split(uint8_t y1, uint8_t y2, uint8_t offset) { if(offset) { // 如果扰动范围不为0继续细分 // 计算中点并施加随机扰动 // (y1 y2 1) / 2 是一种四舍五入的整数除法技巧比直接除以2更精确 uint8_t mid (y1 y2 1) / 2 random(-offset, offset); // 递归处理左半段和右半段扰动范围减半 split(y1, mid, offset / 2); // 处理前半段 split(mid, y2, offset / 2); // 处理后半段 } else { // 递归到底offset为0此时y1即为当前时刻的目标亮度 // --- 关键步骤将亮度值转换为LED颜色 --- // 目标生成一个从亮黄到暗橙的渐变色模拟火焰色温 // 原始亮度 y1 范围是 0-255 // a) 伽马校正人眼对光强的感知是非线性的暗处的变化更敏感。 // 使用 pow(..., 2.7) 进行校正使亮度变化更符合视觉感受。 // 公式校正后值 255.0 * (y1/255.0)^2.7 float gammaCorrected pow((float)y1 / 255.0, 2.7) * 255.0 0.5; // 0.5 是为了在转换为整数时实现四舍五入 // b) 将单亮度值扩展为RGB颜色。 // 火焰颜色高亮度时偏白黄 (R255, G~255, B很低) // 低亮度时偏暗橙/红 (R中等, G较低, B接近0) // 这里使用一个巧妙的位操作来同时计算R、G、B分量 // 0x1004004 这个魔数分解到32位二进制是 // R分量0x1000000 左移24位不让我们拆解 // 实际上代码是 (gammaCorrected * 0x1004004) 8 // 0x1004004 0001 0000 0000 0100 0000 0000 0100 (二进制) // || | | // R位(24-31) G位(8-15) B位(0-7)的系数 // 更直观的理解是它相当于 // R gammaCorrected * 1.0 // G gammaCorrected * (4.0 / 256.0) ≈ gammaCorrected / 64 // B gammaCorrected * (4.0 / (256*256)) ≈ gammaCorrected / 16384 // 最后 0xFF3F03 是一个掩码用于限制各分量的最大值并微调。 // 最终效果R分量基本等于亮度G分量约为亮度的1/8B分量约为亮度的1/48。 // 这产生了从亮黄(255,~32,~5)到暗橙(64,8,1)的渐变。 uint32_t c (((int)gammaCorrected * 0x1004004) 8) 0xFF3F03; // c) 将计算出的颜色设置给所有10个LED for(uint8_t i0; i10; i) { CircuitPlayground.strip.setPixelColor(i, c); } CircuitPlayground.strip.show(); // 更新LED显示 // d) 控制变化速度。这个延时决定了亮度“采样点”之间的时间间隔。 // 4毫秒意味着每秒约250次亮度更新非常流畅。 // 调整这个值可以改变火焰“跳动”的速度值越大变化越慢、越柔和值越小变化越快、越急促。 delay(4); } }代码精要解读与参数调优random(64, 192)这个范围决定了亮度变化的基线。64-192避免了最暗(0)和最亮(255)的极端情况让火焰看起来更柔和。你可以调整这个范围比如random(32, 224)会让明暗对比更强烈。初始offset值 (32)这是最大的扰动幅度。它控制了亮度曲线“波动”的剧烈程度。增大它如64火焰会有更突然的、大幅度的明暗跳跃减小它如16火焰则显得更平稳。伽马校正pow(..., 2.7)这是提升视觉效果的关键一步。未经校正的线性亮度变化在人眼看来暗部变化不明显亮部变化突兀。2.7是常用的伽马值它让低亮度区域的变化被“拉伸”高亮度区域的变化被“压缩”从而使整个变化过程看起来更平滑、更自然。颜色转换魔法0x1004004和0xFF3F03这行“魔数”代码是效率与效果的权衡。它用一次整数乘法和移位操作替代了三次浮点数乘法计算R、G、B在资源有限的单片机上能显著提升性能。其本质是生成了一个R : G : B ≈ 1 : 1/8 : 1/48的颜色比例完美模拟了火焰的色温。如果你想让火焰偏红一些可以增大G和B的除数比如让G亮度/10 B亮度/96想偏白一些则减小除数。延时delay(4)这是控制动画帧率的核心。4ms的间隔产生了250Hz的更新率远超人眼的视觉暂留因此看起来是连续变化的。如果增大到delay(10)或delay(20)你会看到明显的闪烁感火焰会显得“卡顿”。不建议小于2ms因为NeoPixel库的数据写入需要时间。4.2 CircuitPython版本适配CircuitPython版本的逻辑与Arduino版完全一致但语法更简洁得益于Python的动态类型和丰富的内置函数。最大的区别在于颜色计算部分CircuitPython的neopixel库直接接受RGB元组因此我们可以用更直观的数学公式来计算颜色避免了晦涩的位操作。# CircuitPython 关键代码段 import math import random # ... 初始化代码 ... def split(first, second, offset): if offset ! 0: mid ((first second 1) // 2) random.randint(-offset, offset) offset offset // 2 split(first, mid, offset) split(mid, second, offset) else: # 更直观的颜色计算 level math.pow(first / 255.0, 2.7) * 255.0 0.5 r int(level) g int(level / 8) # G分量约为R的1/8 b int(level / 48) # B分量约为R的1/48 STRIP.fill((r, g, b)) STRIP.write() time.sleep(0.004) # 相当于delay(4)CircuitPython使用心得开发体验极佳将板子通过USB连接到电脑它会显示为一个名为CIRCUITPY的U盘。直接编辑里面的code.py文件保存后代码会自动重启运行。调试和修改非常快速。性能注意CircuitPython是解释型语言运行效率低于编译型的Arduino C。对于这个烛光算法它依然能非常流畅地运行。但如果你未来要做更复杂的、对实时性要求极高的项目可能需要考虑Arduino。适用于HalloWing只需修改NUMPIXLED数量和PIXPIN引脚定义两个变量就能轻松驱动外接的NeoPixel灯带。4.3 MakeCode图形化实现MakeCode现在叫MakeCode Arcade的图形化编程环境不支持递归函数。因此它采用了一种替代方案状态机加随机漫步。其核心逻辑是设定一个目标亮度。让当前亮度逐步向目标亮度靠近类似于“缓动动画”。在移动过程中不断加入小的随机扰动。当当前亮度接近目标亮度时再随机生成一个新的目标亮度。这种方法虽然数学上不同于分形递归但通过精心调整“步长”和“扰动幅度”同样能产生非常自然的闪烁效果而且避免了递归可能带来的理解门槛。对于初学者来说在MakeCode里拖拽积木来实现这个逻辑是一个非常好的编程思维训练。5. 南瓜雕刻与装配实战代码跑通了硬件在闪烁接下来就是赋予它灵魂的载体——南瓜或其他东西。5.1 设计图样与转印选择图样你可以在网上搜索“pumpkin stencil PDF”找到海量免费或付费的模板。我提供的龙和狼人模板就是为“浅雕”技法设计的。所谓浅雕就是只刮掉南瓜表皮和部分果肉让灯光能透过较薄的部分透出形成明暗层次而不是完全镂空。这种技法能保留更多细节成品在白天看也有立体感。挑选南瓜带上你的设计图去挑南瓜这是最重要的技巧之一。你需要一个表面相对平整、形状与设计图适配的南瓜。如果设计图是长脸选个高瘦的如果是圆脸选个扁圆的。表面光滑、无严重疤痕的南瓜雕刻起来更轻松。转印图案投影法如果有投影仪直接将图案投影到南瓜上描边这是最准确的方法。复写纸法将图案打印出来在南瓜表面相应位置贴上透明胶带固定。用复写纸垫在图案和南瓜之间再用圆珠笔用力描图案的线条。铅笔拓印法低成本如图中所示用软铅笔6B以上在打印纸的背面涂满。然后将纸正面朝外贴在南瓜上用圆珠笔描图。背面的石墨会被压印到南瓜上。这个方法需要一点耐心线条可能不连续需要后续修补。5.2 雕刻技法与心得工具专业的南瓜雕刻刀套装最好用包含各种形状的锯片。如果没有美工刀、水果刀甚至结实的勺子也可以但要注意安全。浅雕步骤描边用细针或锥子沿着转印的线条轻轻扎出轮廓点阵这样即使铅笔印被擦掉也有迹可循。去皮对于需要最亮的部分如龙的眼睛用雕刻刀或勺子的边缘小心地刮去橙色的表皮露出下面浅黄色的果肉。刮得越深透光越多越亮。你可以分层刮创造灰度效果。雕刻对于需要完全镂空的部分如背景用刀切穿南瓜壁。先从小块区域内部下刀再处理轮廓。我踩过的坑白色南瓜慎用于浅雕我试过白皮南瓜晚上打光效果很梦幻。但白天看刮掉表皮后露出的果肉也是浅黄白色与表皮对比度极低图案几乎看不见。解决方法是在雕刻区域涂上深色丙烯颜料如黑色让图案在白天也能凸显。泡沫南瓜是“灾难”为了可重复使用我买了泡沫塑料南瓜。结果发现它完全不适合浅雕刀一刮就是一大片碎屑边缘毛糙根本无法做出精细的渐变效果。它只适合完全镂空的图案。密封与防腐真南瓜会很快脱水萎缩。雕刻完成后在内壁和雕刻断面涂抹一层凡士林或植物油可以显著延缓脱水。如果条件允许可以将整个南瓜浸泡在加了少量漂白剂的水里十几分钟晾干后再涂抹油脂能抑制霉菌生长。5.3 内部装配与最终调试防水处理务必用密封袋将Circuit Playground和电池盒完整包好。袋口朝上用胶带固定在南瓜内壁较高处防止南瓜汁液流入。固定与散热可以用蓝丁胶、泡沫胶或甚至一根竹签将电路板固定在南瓜底部中心。确保NeoPixel灯环朝上光线能均匀照射到南瓜顶部。光效调试合上南瓜盖在黑暗环境中观察效果。你可能需要调整电路板的位置或角度来优化面部特征的照明。如果觉得灯光太刺眼或颜色不对可以回头修改代码中的setBrightness全局亮度、伽马值或RGB比例参数。开关访问确保电池盒的开关留在南瓜底部或某个容易够到的隐蔽位置方便开关。6. 常见问题与进阶优化在实际制作和调试过程中你可能会遇到以下问题问题现象可能原因排查与解决LED完全不亮1. 供电问题2. 程序未上传/运行3. 硬件损坏1. 检查电池极性、电量或USB连接。2. Arduino检查端口和板卡类型选择是否正确上传时板载LED会闪烁。CircuitPython检查code.py文件是否存在且代码正确。3. 尝试运行一个简单的测试程序如让所有LED亮白色。灯光颜色不对非黄橙色颜色转换代码错误检查Arduino代码中魔数运算部分或CircuitPython中RGB计算部分。确保G和B分量是R分量的1/8和1/48左右。闪烁频率太快/太慢delay()值设置不当修改split()函数else分支中的delay(4)。增大数值变慢减小变快最小建议2ms。灯光变化生硬不自然1. 随机范围offset太小2. 伽马校正被注释或错误1. 尝试增大split(prev, lvl, 32)中的32这个值。2. 确保pow((float)y1 / 255.0, 2.7)这部分代码存在且正确。使用电池供电时灯光微弱或不稳定电池电量不足NeoPixel在亮度高时耗电大。更换全新碱性电池或镍氢充电电池。避免使用碳性电池。程序运行一段时间后卡死或重启递归深度过深导致栈溢出理论上可能我们的代码中offset是uint8_t类型递归深度最多7-8层远在安全范围内。如果发生检查是否有其他代码冲突或尝试重启板子。进阶优化思路添加声音或运动感应Circuit Playground内置麦克风和加速度计。你可以修改代码让拍手声或晃动南瓜来触发灯光模式切换例如从烛光模式切换到彩虹渐变或警灯模式。实现多区域独立控制目前10个LED是同步变化的。你可以修改算法让不同的LED组有略微不同的起始参数或扰动幅度模拟火焰根部、中部、焰尖的不同亮度效果会更立体。引入温度传感器虽然Circuit Playground没有直接的温度传感器但你可以通过测量LED驱动芯片或供电电压的微小变化来间接感知“热量”并让光色随之微微变暖增加红色或变冷增加蓝色虽然不真实但很有趣。制作无线遥控版本如果使用ESP32等带Wi-Fi的开发板可以制作一个Web服务器界面用手机就能远程调节火焰颜色、闪烁强度甚至模式。这个项目最吸引我的地方在于它用如此简洁的代码生动地演绎了一个深刻的数学原理。当你看到自己写的几行递归函数驱动着灯光产生出如此逼真的生命感时那种跨越了代码与物理世界的创造快乐是任何现成产品都无法给予的。它不仅仅是一个万圣节装饰更是一个关于算法、自然模拟和嵌入式开发的微型课堂。希望你在制作过程中也能感受到这份乐趣。