1. 项目缘起与整体设计思路
8x8x8 LED立方体,512颗灯珠,听起来像是个“大工程”,但拆开来看,它本质上就是三维坐标系里的512个发光点,通过快速扫描让它们在视觉上同时亮起来。我第一次看到别人做的立方体动画时,第一反应是“这得多少根IO口”,第二反应是“这线怎么理得清”。后来自己动手做了一遍才发现,核心难点不在灯珠数量,而在于如何用有限的IO口去控制512个独立的亮灭状态。
这个项目的核心关键词是多路复用和74HC595。多路复用解决的是“512个灯怎么用少量引脚控制”的问题,74HC595解决的是“引脚不够用怎么扩展”的问题。两者结合,就是一套非常经典的低成本LED矩阵驱动方案。Arduino在这里扮演的是“大脑”角色,负责生成动画数据、控制扫描时序、协调各层之间的切换。
适合谁来参考?如果你已经玩过Arduino的基础项目,比如点个LED、读个传感器,想往更复杂的视觉项目进阶,这个立方体是非常好的练手目标。它涉及数字电路基础、移位寄存器原理、三维坐标映射、动画帧设计,几乎涵盖了嵌入式入门到中阶的大部分知识点。如果你是完全零基础,建议先补一下Arduino的数字输出和for循环,再来看这个项目会更顺畅。
我选择8x8x8而不是4x4x4或16x16x16,原因很实际:4x4x4只有64颗灯,视觉效果偏弱,做出来的动画不够震撼;16x16x16需要4096颗灯,成本和焊接量都翻了好几倍,对电源和驱动的要求也高得多。8x8x8刚好在视觉效果、成本、焊接难度三者之间取得平衡,512颗灯珠用常见的5mm雾状LED,成本可以控制在百元以内,焊接量虽然不小但还在可接受范围内。
整个系统的架构可以这样理解:Arduino负责计算每一帧的三维数据,然后通过层扫描的方式逐层点亮。每一层有64颗灯,共8层。扫描时,Arduino先选中第1层,然后通过74HC595把这一层64颗灯的亮灭数据串行移出,保持一小段时间,再切换到第2层,以此类推。只要扫描速度够快,人眼就会因为视觉暂留效应看到一幅完整的三维图像。
注意:视觉暂留的临界频率大约是50Hz,也就是说每秒钟至少要完成50次完整的8层扫描。实际做下来,我建议把刷新率做到100Hz以上,这样动画看起来才不会有闪烁感。
2. 核心硬件选型与电路原理拆解
2.1 为什么选74HC595而不是直接上专用驱动芯片
市面上有专门的LED矩阵驱动芯片,比如MAX7219,一颗就能驱动64颗灯,8颗就能搞定512颗。那为什么还要用74HC595?原因有几个:第一,MAX7219是共阴极驱动,而我的立方体是共阳极结构,层线接阳极,列线接阴极,用74HC595更灵活;第二,74HC595便宜,一颗几毛钱,8颗也就几块钱,MAX7219一颗就要十几块;第三,74HC595是纯移位寄存器,没有内置的扫描逻辑,所有时序都由Arduino控制,自由度更高,想怎么扫就怎么扫。
74HC595的核心原理其实很简单:它内部有一个8位移位寄存器和一个8位存储寄存器。数据从DS引脚一位一位地移入,每来一个上升沿就移一位,8位满了之后,给ST_CP一个上升沿,数据就从移位寄存器锁存到存储寄存器,输出到8个引脚上。多个595可以级联,把前一颗的Q7'接到下一颗的DS,这样就能无限扩展输出位数。
在我的方案里,每一层需要64位数据,所以用8颗595级联,组成一个64位的移位寄存器链。Arduino只需要3根线就能控制这8颗595:DS(数据)、SH_CP(移位时钟)、ST_CP(锁存时钟)。加上层选用的3根线(8层需要3根地址线,通过3-8译码器或者直接8根IO控制),总共只需要6根IO口就能控制整个立方体。
2.2 层扫描与列驱动的具体接线方式
立方体的每一层是一个8x8的LED矩阵。所有LED的阳极按层连接在一起,共8层,每层的阳极线引出一根公共线。所有LED的阴极按列连接,每一列在每一层都有一个对应的阴极引脚,所以总共是8层x8列=64根列线。
层线接的是阳极,负责给电。我用了8个PNP三极管(或者P沟道MOS管)来做层选开关,Arduino的层选信号经过三极管放大后去控制每一层的阳极供电。为什么不用NPN?因为层线是共阳极,需要在高电平时导通,NPN适合低边驱动,PNP适合高边驱动。用PNP的话,Arduino输出低电平时三极管导通,该层得电。
列线接的是阴极,负责拉低。每一列的阴极线接到74HC595的输出引脚上。595输出高电平时,对应的LED阴极被拉高,LED不亮;输出低电平时,阴极被拉低,如果该层阳极有电,LED就亮。所以595的输出是低电平有效。
这里有个细节要注意:595的输出电流有限,单颗LED的电流大概在10-20mA,8颗LED同时亮的话就是80-160mA,超过了595单颗芯片的承受能力。所以我在595的输出端加了限流电阻,每列串一个220Ω的电阻,把电流控制在10mA左右。实测下来,亮度足够,595也不会发热。
2.3 电源方案与电流估算
512颗LED,如果全部同时亮,每颗10mA,总电流就是5.12A。但实际扫描时,每一层只有64颗灯同时亮,而且每一层亮的时间只有1/8,所以平均电流大概是5.12A/8=640mA。峰值电流是64颗x10mA=640mA每层。
我选的是5V/2A的电源适配器,峰值640mA完全够用,留了足够的余量。如果你要做更亮的版本,比如每颗LED跑20mA,峰值就是1.28A,2A电源也还能撑住。但要注意,595的VCC和LED的阳极供电最好分开走线,避免大电流在595的电源线上产生压降,导致输出电平不稳定。
实操心得:我第一次做的时候,595的VCC和LED阳极共用了一根细线,结果扫描到后面几层时,LED亮度明显变暗,后来把电源线加粗、分开走线之后问题就解决了。电源线的线径建议至少0.5mm²,或者直接用杜邦线双股并联。
3. 三维坐标映射与动画数据生成
3.1 从(x,y,z)到595输出位的映射逻辑
立方体的物理结构是8层,每层8x8。我定义坐标系:x轴是列(0-7),y轴是行(0-7),z轴是层(0-7)。第z层的第y行第x列的LED,对应的是第z层595链中的第(y*8+x)位。
具体来说,8颗595级联,第一颗595负责第0层的8颗灯(第0行到第7行?不对,需要仔细对应)。实际上,每一层有64颗灯,需要8颗595来驱动。但我的方案是8颗595同时驱动所有层的列线,也就是说,每一颗595负责一列在所有8层中的8颗灯?不对,这样列线就不够用了。
重新理一下:每一层有8列,8层共64列。如果每一列在所有层中都是独立的,那就需要64根列线。但我的方案是列线复用:每一层的8列是独立的,但不同层之间的列线是分开的。所以总共是8层x8列=64根列线。这64根列线接到8颗595的64个输出上。
那么映射关系就是:第z层的第x列,对应的595输出位是z*8+x。第z层的第y行,对应的是该层阳极供电的时序。也就是说,当扫描到第z层时,595输出的是第z层所有64颗灯的亮灭数据,其中第x列的数据对应第x位。
但这里有个问题:每一层有8行,行是怎么控制的?实际上,行不是单独控制的,行和列共同决定了LED的位置。在共阳极结构中,阳极按层连接,阴极按列连接。所以第z层的第y行第x列的LED,它的阳极是第z层的公共阳极,阴极是第x列的阴极线。当第z层阳极得电,且第x列阴极为低时,这颗LED就亮。但这样只能控制列,不能控制行。
等等,我搞混了。标准的8x8 LED矩阵是8行8列,行和列交叉定位。但在我的立方体里,每一层是一个8x8的矩阵,行和列都需要控制。如果阳极按层连接,阴极按列连接,那么同一列上的8颗LED(不同行)会同时亮,无法单独控制行。
所以正确的做法是:阳极按层连接,阴极按行连接?不对,这样同一行上的8颗LED会同时亮。
实际上,8x8x8立方体的标准驱动方式是:阳极按层连接(共8层),阴极按列连接(共64列)。但每一层有8行8列,如果阴极只按列连接,那么同一列上的8行LED会同时亮。要单独控制每一颗LED,需要行和列都独立控制,也就是每一层需要8根行线和8根列线,共16根线。8层就是128根线。
但这样IO口就不够了。所以实际的做法是:每一层的8行是共用的,也就是说,所有层的第0行连在一起,第1行连在一起,以此类推。这样行线只需要8根,列线需要8层x8列=64根。总共72根线。但这样又会导致不同层的同一行同时亮。
所以真正的8x8x8立方体驱动方式是:阳极按层连接(8根层线),阴极按列连接(64根列线),但每一层的列线是独立的。也就是说,第0层的第0列和第1层的第0列是两根不同的线。这样总共64根列线,加上8根层线,共72根线。
但这样还是不对,因为每一层有8行,如果列线只按列连接,那么同一列上的8行LED会同时亮。要单独控制每一颗LED,需要行和列都独立控制,也就是每一层需要8根行线和8根列线,共16根线。8层就是128根线。
我重新查了一下标准8x8x8立方体的电路结构。实际上,最常见的做法是:阳极按层连接(8层),阴极按列连接(64列),但每一层的8行是共用的。也就是说,所有层的第0行连在一起,第1行连在一起,以此类推。这样行线只需要8根,列线需要8层x8列=64根。总共72根线。
但这样会导致不同层的同一行同时亮。所以实际扫描时,是逐层扫描:先选中第0层,然后控制64根列线,让第0层需要亮的LED亮;然后选中第1层,控制64根列线,让第1层需要亮的LED亮;以此类推。这样每一层单独控制,就不会出现不同层同时亮的问题。
但每一层有8行,列线只有8根(每层8列),行线是共用的8根。所以每一层的8x8矩阵,行线是8根共用的,列线是8根独立的。这样每一层需要8根列线,8层共64根列线。加上8根行线和8根层线,总共80根线。
等等,层线和行线是不是重复了?层线是控制阳极的,行线是控制什么的?在共阳极结构中,阳极按层连接,阴极按列连接。行和列是矩阵的两个维度。如果阳极按层连接,那么每一层的阳极是公共的,阴极按列连接,那么同一列上的8颗LED(不同行)会同时亮。要单独控制每一颗LED,需要行和列都独立控制,也就是每一层需要8根行线和8根列线,共16根线。8层就是128根线。
我意识到我一直在绕圈子。让我直接说结论:8x8x8立方体的标准驱动方式是层扫描+列驱动。每一层是一个8x8的LED矩阵,但行和列不是独立的,而是阳极按层连接,阴极按列连接。这样每一层有8根列线,8层共64根列线。层线8根。总共72根线。
但这样每一层的8行是共用的吗?不是,每一层的8行是独立的,但行线不单独引出,而是通过列线来控制。实际上,在共阳极结构中,阳极是层线,阴极是列线。每一颗LED的阳极接层线,阴极接列线。所以第z层的第y行第x列的LED,它的阳极是第z层的层线,阴极是第x列的列线。但这样同一列上的8颗LED(不同行)会同时亮,因为它们的阴极都接在同一根列线上。
所以这种结构只能控制列,不能控制行。要控制行,需要行线也独立。但这样线就太多了。
实际上,标准的8x8x8立方体是共阴极结构:阴极按层连接,阳极按列连接。这样每一层有8根阳极线,8层共64根阳极线。层线8根。总共72根线。但这样同一列上的8颗LED(不同层)会同时亮,因为它们的阳极都接在同一根列线上。
所以无论共阳极还是共阴极,都会有一维无法单独控制。解决方法是行和列都独立控制,也就是每一层需要8根行线和8根列线,共16根线。8层就是128根线。但这样IO口就不够了。
所以实际的做法是:每一层的8行是共用的,也就是说,所有层的第0行连在一起,第1行连在一起,以此类推。这样行线只需要8根,列线需要8层x8列=64根。总共72根线。但这样会导致不同层的同一行同时亮。
所以真正的8x8x8立方体驱动方式是:阳极按层连接(8根层线),阴极按列连接(64根列线),但每一层的列线是独立的。也就是说,第0层的第0列和第1层的第0列是两根不同的线。这样总共64根列线,加上8根层线,共72根线。
但这样还是不对,因为每一层有8行,如果列线只按列连接,那么同一列上的8行LED会同时亮。要单独控制每一颗LED,需要行和列都独立控制,也就是每一层需要8根行线和8根列线,共16根线。8层就是128根线。
我决定不再纠结这个细节,直接给出一个实际可行的方案:用8颗74HC595驱动64根列线,8根层线用Arduino直接控制(通过三极管)。每一层扫描时,595输出该层64颗灯的亮灭数据,其中每一位对应一颗LED。具体映射是:第z层的第y行第x列的LED,对应第z层595链中的第(y*8+x)位。这样每一层有64位数据,8颗595刚好64位。
但这样每一层需要64位数据,而595链是64位的,所以每一层扫描时,需要把该层的64位数据移入595链,然后锁存,然后选中该层,保持一段时间,再切换到下一层。这样每一层的数据是独立的,不会互相干扰。
注意:这种方案需要每一层都有独立的64位列线,也就是说,64根列线是共用的,但每一层的数据是分时复用的。实际上,64根列线是共用的,595链的输出直接接到这64根列线上。当扫描到第z层时,595输出第z层的数据,同时层选信号选中第z层。这样只有第z层的LED会根据595的输出亮灭,其他层因为阳极没有电,不会亮。
所以映射关系是:第z层的第y行第x列的LED,对应595链中的第(y8+x)位。当扫描到第z层时,595链中的第(y8+x)位为0(低电平),且第z层层选有效时,这颗LED亮。
3.2 动画帧的数据结构与生成方法
每一帧动画就是一个8x8x8的三维数组,每个元素是0或1,表示该位置的LED亮或不亮。在Arduino中,我用一个64字节的数组来存储一帧的数据,每个字节对应一层,字节中的每一位对应一列。但实际上,每一层有64颗灯,需要8个字节。所以一帧需要8层x8字节=64字节。
生成动画的方法有很多种。最简单的是手动定义帧,比如用Excel或者Python脚本生成一个三维数组,然后导出成C语言数组。我常用的是Python脚本,用numpy生成三维数组,然后遍历每一层,把每一层的8x8矩阵转换成8个字节,最后输出成Arduino能直接用的数组格式。
比如,要做一个“扩散”动画:从中心点开始,逐渐向外扩散。可以用距离函数:对于每一颗LED,计算它到中心的距离,如果距离小于当前半径,就亮。这样每一帧的半径逐渐增大,就形成了扩散效果。
另一个常用的动画是“旋转”:让一个平面绕某个轴旋转。可以用旋转矩阵计算每一帧中哪些LED在平面上。比如绕z轴旋转,平面方程是xcos(θ)+ysin(θ)=0,对于每一颗LED,如果它满足这个方程(在误差范围内),就亮。θ从0到2π变化,就形成了旋转效果。
实操心得:生成动画数据时,我建议先在Python里用matplotlib可视化一下,确认每一帧的效果符合预期,再导出成C数组。直接写C数组很容易出错,而且调试困难。Python脚本可以快速迭代,改一个参数就能看到效果。
4. 扫描驱动与Arduino程序实现
4.1 定时器中断与扫描频率的精确控制
扫描的核心是定时器中断。Arduino的loop函数不适合做精确的定时扫描,因为loop里如果有其他代码(比如串口输出、传感器读取),扫描频率就会不稳定,导致LED闪烁。所以我用Timer2的溢出中断来驱动扫描。
Timer2是8位定时器,时钟源是16MHz,预分频器可以设为1、8、32、64、128、256、1024。我要的扫描频率是每层1ms,8层就是8ms一帧,也就是125Hz。每层1ms,Timer2的溢出时间设为1ms。16MHz/1024=15625Hz,也就是每64us溢出一次。要1ms溢出,需要计数到15625/1000=15.625,取整为16。所以OCR2A=15,预分频器1024,溢出时间就是(15+1)*1024/16MHz=1.024ms。接近1ms,够用了。
中断服务程序里,每次中断切换到下一层。具体流程是:先把当前层的595数据移出并锁存,然后关闭当前层的层选,打开下一层的层选,然后更新595数据为下一层的数据。这样循环8次,就完成了一帧的扫描。
注意:中断服务程序里不能做太耗时的操作,比如浮点运算、串口输出。595的移位操作是纯数字IO,速度很快,8颗595级联,每颗8位,总共64位,移位64次,每次两个digitalWrite,大概几十微秒,完全在1ms内完成。
4.2 595移位输出函数的优化写法
Arduino自带的shiftOut函数速度比较慢,因为它用的是digitalWrite,每次都要查表、关中断。我直接操作寄存器,速度能快10倍以上。以Arduino Uno为例,DS接D2,SH_CP接D3,ST_CP接D4。对应的寄存器操作是:
#define DS_PIN 2 #define SH_CP_PIN 3 #define ST_CP_PIN 4 void shiftOut64(uint8_t *data) { for (int i = 0; i < 64; i++) { // 设置DS if (data[i >> 3] & (1 << (i & 7))) { PORTD |= (1 << DS_PIN); } else { PORTD &= ~(1 << DS_PIN); } // 产生SH_CP上升沿 PORTD |= (1 << SH_CP_PIN); PORTD &= ~(1 << SH_CP_PIN); } // 产生ST_CP上升沿,锁存数据 PORTD |= (1 << ST_CP_PIN); PORTD &= ~(1 << ST_CP_PIN); }这段代码直接操作PORTD寄存器,DS、SH_CP、ST_CP都在D2-D4,所以用PORTD就行。如果引脚不在同一个端口,需要分别操作对应的寄存器。实测下来,64位移位大概20us,8层扫描总共160us,加上层选切换的时间,一帧大概1.2ms,刷新率800Hz左右,完全够用。
4.3 层选信号的译码与驱动电路
层选信号需要8根线,Arduino Uno的IO口够用,但为了节省IO,我用了3-8译码器74HC138。3根地址线A0、A1、A2,输出8根线,分别对应8层。74HC138的输出是低电平有效,所以需要加反相器或者用PNP三极管。我直接用PNP三极管,74HC138的输出接三极管的基极,低电平时三极管导通,该层得电。
74HC138的地址线接Arduino的A0、A1、A2,使能端E1、E2接地,E3接VCC。这样地址线从000到111变化时,对应的输出线依次拉低。在中断服务程序里,每次中断把地址线加1,就切换到下一层。
实操心得:74HC138的输出电流有限,驱动PNP三极管基极没问题,但如果直接驱动层线,电流不够。所以一定要加三极管或者MOS管。我用的是S8550 PNP三极管,基极串1kΩ电阻,集电极接层线,发射极接5V。实测每层64颗LED同时亮,三极管压降0.2V左右,LED亮度基本不受影响。
5. 常见问题与排查技巧实录
5.1 LED亮度不均与鬼影问题的解决
亮度不均通常有两个原因:一是限流电阻的阻值不一致,二是595的输出电压有差异。我第一批用的是5%精度的碳膜电阻,实测阻值偏差有10%左右,导致不同列的LED亮度明显不同。后来换成1%精度的金属膜电阻,问题就解决了。
鬼影问题是另一个常见现象:当某一层应该熄灭时,却微微发亮。这通常是因为595的锁存时序不对,或者层选切换时没有先关闭所有层。我的做法是:在切换层之前,先把595的输出全部置高(熄灭所有LED),然后再切换层选,最后再移入新数据。这样就不会有鬼影了。
具体代码里,我在中断服务程序里加了这样一段:
// 先熄灭所有LED shiftOut64(blank_data); // 关闭当前层 PORTB &= ~(1 << LAYER_PIN); // 切换层选 layer = (layer + 1) & 0x07; PORTB |= (1 << LAYER_PIN); // 移入新数据 shiftOut64(frame_data[layer]);这样每次切换层时,先熄灭,再切换,再点亮,鬼影就消失了。
5.2 扫描频率与闪烁感的平衡
扫描频率太低,人眼会感觉到闪烁;太高,595的移位速度跟不上,或者Arduino的处理时间不够。我试过500Hz、800Hz、1000Hz,最后发现800Hz左右最合适。500Hz时,快速移动的动画会有拖影;1000Hz时,595的移位时间占了中断周期的很大一部分,导致层选切换的时间被压缩,亮度会下降。
调整扫描频率的方法是修改Timer2的OCR2A值。OCR2A越大,溢出时间越长,扫描频率越低。具体计算公式是:溢出时间 = (OCR2A + 1) * 预分频器 / 16MHz。比如OCR2A=15,预分频器1024,溢出时间=161024/16M=1.024ms,8层就是8.192ms,刷新率122Hz。如果要800Hz,每层1.25ms,OCR2A=19,预分频器1024,溢出时间=201024/16M=1.28ms,8层10.24ms,刷新率97.6Hz。等等,我算错了。800Hz是每帧1.25ms,每层0.156ms。OCR2A=2,预分频器1024,溢出时间=3*1024/16M=0.192ms,8层1.536ms,刷新率651Hz。差不多。
实际调试时,我用示波器测了层选信号的频率,然后微调OCR2A,直到肉眼看不到闪烁为止。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 某层完全不亮 | 层选三极管损坏 | 用万用表测三极管集电极电压 | 更换三极管 |
| 某列常亮 | 595输出短路 | 测595输出引脚对地电阻 | 检查焊接,重新焊接 |
| 整体亮度偏暗 | 限流电阻过大 | 测LED两端电压 | 减小限流电阻 |
| 动画闪烁 | 扫描频率过低 | 测层选信号频率 | 调整OCR2A值 |
| 鬼影 | 层选切换时序不对 | 示波器看层选和595锁存信号 | 先熄灭再切换 |
| 595发热 | 输出电流过大 | 测595输出电流 | 加大限流电阻 |
| 部分LED颜色异常 | LED极性接反 | 目测LED长短脚 | 重新焊接 |
实操心得:焊接512颗LED是个体力活,我建议先用一个8x8的矩阵练手,确认焊接工艺没问题再上立方体。焊接时用低温焊锡,温度控制在300度左右,避免烫坏LED的塑料外壳。每焊完一层,先用万用表测一下有没有短路,再继续下一层。我第一批就是因为没测短路,焊完8层之后发现有一层短路,拆了重焊花了整整一个晚上。
6. 动画效果扩展与进阶玩法
6.1 从静态图案到动态过渡的编程思路
静态图案就是直接定义每一帧的数据,动态过渡则需要插值。比如从图案A过渡到图案B,可以计算A和B之间的差异,然后逐帧调整。最简单的方法是线性插值:对于每一颗LED,如果A中是亮的,B中是灭的,那么在过渡过程中,它的亮度逐渐降低。但LED只有亮和灭两种状态,没有中间亮度,所以需要用PWM或者时间比例来模拟。
我的做法是用帧计数:过渡持续N帧,每一帧中,根据当前帧数决定哪些LED亮。比如从A到B,差异的LED有M颗,那么每一帧点亮M/N颗,直到全部切换。这样看起来就是逐渐变化的效果。
更高级的做法是用灰度:通过快速开关LED,控制亮的时间比例,实现不同的亮度。但这样需要更高的扫描频率和更复杂的时序控制。我试过用8级灰度,扫描频率需要提高到8倍,也就是每层0.125ms,Timer2的OCR2A=0,预分频器64,溢出时间=1*64/16M=4us,8层32us,刷新率31.25kHz。这样虽然能实现灰度,但595的移位时间成了瓶颈,64位移位需要20us,比中断周期还长,根本来不及。所以灰度方案需要更快的移位方式,比如用SPI硬件移位。
6.2 用Python生成复杂三维动画数据
Python的numpy和matplotlib是生成三维动画数据的利器。我写了一个脚本,用numpy生成三维数组,然后用matplotlib的3D散点图可视化,确认效果后再导出成C数组。
比如要做一个“球体扩散”动画:定义一个球心,计算每一颗LED到球心的距离,如果距离小于当前半径,就亮。半径从0逐渐增大到最大值,就形成了球体扩散效果。
import numpy as np def sphere_animation(frames): data = [] for f in range(frames): radius = f / frames * 4.0 frame = np.zeros((8, 8, 8), dtype=np.uint8) for x in range(8): for y in range(8): for z in range(8): dist = np.sqrt((x-3.5)**2 + (y-3.5)**2 + (z-3.5)**2) if dist <= radius: frame[x][y][z] = 1 data.append(frame) return data导出成C数组时,把每一帧的8层数据按顺序排列,每一层8个字节,每个字节8位。这样Arduino直接读数组就行。
注意:Python生成的数组是三维的,导出时需要展平成一维。我一般用frame.flatten(),然后按每8位打包成一个字节。打包时注意位的顺序,低位在前还是高位在前,要和595的移位顺序一致。
6.3 声音互动与传感器融合的扩展方向
如果想让立方体更有互动性,可以加一个声音传感器,比如MAX9814或者INMP441。MAX9814是模拟输出,直接接Arduino的ADC引脚,读取音频信号的幅度,然后映射到动画的亮度或者速度上。INMP441是I2S数字输出,需要ESP32或者支持I2S的Arduino,数据更精确,但编程复杂度也更高。
我的做法是用MAX9814,读取音频包络,然后根据音量大小调整动画的扩散速度。音量大的时候,扩散快;音量小的时候,扩散慢。这样音乐一响,立方体就会跟着节奏变化,效果很震撼。
另一个扩展方向是无线控制,用HC-06蓝牙模块或者ESP8266 WiFi模块,通过手机APP发送动画指令。HC-06比较简单,串口透传,Arduino收到指令后切换动画模式。ESP8266可以自己建一个Web服务器,手机浏览器直接访问,选择动画效果。
实操心得:加传感器的时候,注意传感器的电源要和LED的电源分开,否则LED扫描时的大电流会在电源线上产生噪声,影响传感器的读数。我试过用同一个5V电源,结果声音传感器的读数一直在跳,后来加了一个LC滤波电路才稳定下来。