简介:这套资料以51单片机为核心,完整呈现心形灯光音乐盒的设计过程,适合电子技术初学者、单片机爱好者及嵌入式硬件设计人员用于课程实践或项目参考。资源包共105个文件,压缩后约2.57MB,包含C语言源程序、电路原理图、设计文档、仿真文件以及辅助工具,覆盖从硬件连接、程序控制到音乐播放的完整链路。目前已有500人学习下载,内容经过整理,易于按需查阅。资料中不仅提供了程序源码与电路图,还包含器件选型说明和操作指南,能够帮助读者理解LED矩阵的动态显示原理以及单片机与音乐芯片的通信方式,从而掌握灯光与音乐同步实现的关键方法。无论是入门自学还是毕业设计借鉴,这套资料都具有较强的实用性和参考价值。
1. 基于51单片机的心形灯光音乐盒:把音符和心跳做成一个看得见的桌面玩具
很多人学51单片机都是从流水灯和蜂鸣器"嘀嘀嘀"开始的,搞完就扔一边了。这个心形灯光音乐盒不一样,它把一首完整的曲子、16颗LED排成的心形图案、以及按键切换的几种灯效塞进了一块巴掌大的洞洞板里。它不属于那种炫技的项目,但恰好是嵌入式硬件入门最该有的样子:硬件上有一颗单片机、一组LED、一个蜂鸣器、两个按键;代码上有定时器中断、音谱数据结构、状态机切换。你照着原理图焊完、烧完程序,它能一边播《小星星》一边让心形灯像心跳一样闪,按一下按键还能切换成呼吸灯模式。这篇文章会把这个项目的电路设计、音符播放原理、LED联动逻辑和几个踩坑点全部拆开讲,工程文件和相关文档资料在文末下载页可以拿到。
2. 项目全貌与技术选型:为什么这类项目永远值得做一遍
2.1 这个音乐盒到底能干什么:曲目、灯效、按键交互
先说清楚它的功能边界。默认固件里预置了三首曲子,第一次上电自动播放第一首,音乐是单音旋律,由无源蜂鸣器发出;同时16颗LED会根据当前音符实时点亮,形成"声随光动"的效果。按键K1负责切换曲目,短按从当前曲跳到下一首,三首循环;按键K2负责切换灯光模式,有静态心形、呼吸渐变、心跳快闪、音符联动四种,灯光模式独立于曲目,也就是说你在播放任何一首歌时都能切灯效。
这个交互逻辑看似简单,但它是典型的单片机状态机应用。每首曲子有独立的音谱数据,灯光模式有独立的显示函数,主循环负责读取按键、刷新LED、驱动蜂鸣器,定时器中断负责掐节拍,每个功能模块之间通过全局变量通信,不用操作系统,不用复杂调度。正因为它简单,反而适合拿来读代码、改代码——你想加一首歌,只需要往音谱表里塞一组数据;你想加一种灯效,只需要写一个函数然后在按键分支里挂上去。
我用一个表格把整机的功能清单列出来,这样你拿到工程后可以对照着验证自己是否完全跑起来了:
| 功能项 | 实现方式 | 验证方法 |
|---|---|---|
| 三首曲目循环播放 | 音谱数组 + 定时器中断驱动 | 短按K1切换,听旋律变化 |
| 心形LED阵列 | P0/P2端口驱动16颗LED | 上电默认全亮心形 |
| 四种灯光模式 | 主循环模式标志位分支 | 短按K2逐一切换 |
| 音符联动灯效 | 音符值映射LED亮灭 | 播放时观察LED跟随旋律跳动 |
| 曲目播完自动重播 | 曲目索引循环 | 等待一首曲播完观察行为 |
2.2 技术选型:为什么是STC89C52而不是STM32或Arduino
这个项目用的是意法半导体STC89C52RC,一颗经典的8051内核单片机,工作电压5V,12MHz晶振,内置8KB Flash、256字节RAM。选择它不是因为性能强,而是因为这个场景根本不需要强:一个蜂鸣器加16个LED,C52的IO口全都能直接驱动,不像STM32那样要先配置一堆时钟树和复用引脚。而且51单片机的寄存器数量少,定时器、串口、外部中断全是直接在寄存器上操作,Keil里写几行代码就能看见效果,这对想弄懂"单片机到底是怎么工作的"来说是最短路径。
对比一下几种常见方案你就明白了。Arduino UNO用AVR芯片,写起来确实快,但那套Wire库、Serial库把底层全封装掉了,你调一个PWM呼吸灯实际上是在调库函数,硬件细节隔了一层;STM32功能强,但GPIO初始化就要五六行,定时器还要挂RCC时钟,对一个心形音乐盒来说属于杀鸡用牛刀。51单片机的好处在于你的代码和硬件行为是直接对应的:往P0口写一个字节,LED立刻亮灭;往定时器初值寄存器写一个数,蜂鸣器音调立刻变化,没有中间层。
电路设计上也只是最基础的几个元件:单片机最小系统(晶振、复位、电源滤波)、两个按键、一个蜂鸣器驱动三极管、16颗LED加限流电阻。整机成本不超过二十块钱,洞洞板焊接一小时能完事。比起各种带屏幕带WiFi的开发板,这个项目把"单片机最小系统能干什么"展示得非常纯粹——它就是一颗芯片加上外围器件,没有任何多余部分。
2.3 代码工程结构:一个main文件跑全场的经典布局
这个项目的Keil工程结构是我非常熟悉的经典51布局,整个工程只有三个源码文件加一个头文件,没有用任何库:
heart_music_box/ ├── main.c // 主循环、按键扫描、模式切换 ├── music.c // 音谱数据、音符播放控制 ├── timer.c // 定时器0/1初始化与中断服务 ├── leds.c // LED阵列显示函数、四种灯效 ├── delay.c // 毫秒级/微秒级软件延时 ├── reg52.h // 寄存器定义头文件(Keil自带) └── heart_music_box.uvproj // Keil工程文件main.c是整个程序的骨架,它做的事用一个死循环就能概括:扫描按键、根据按键更新状态、刷新LED。真正掐时间的活全在timer.c里,定时器0负责产生音符频率的方波,定时器1负责10ms节拍中断。music.c只是一堆code关键字修饰的常量数组,存放音符频率表的初值和三首歌曲的简谱数据,程序运行时不修改这些数据,所以全部放在Flash里省RAM。
这种把数据、驱动、逻辑分文件放的组织方式,对51项目来说已经算结构清晰了。你拿到工程后第一件事应该是打开main.c从上往下扫一遍,看到全局变量定义处有几个状态标志位——current_song、light_mode、playing_flag——这几个变量就是整个程序的"记忆",理解了它们你就能理解全部逻辑。
3. 硬件设计拆解:心形LED阵列、蜂鸣器驱动与按键电路
3.1 心形LED阵列:16颗LED排成一个看得懂的心
心形图案由16颗红色LED组成,布局是上两行少、中间行多、底部收尖的标准心形轮廓。这16颗LED分成两组,P0口接上方8颗,P2口接下方8颗,所有LED采用共阳极接法——每颗LED的阳极通过330Ω限流电阻接到VCC,阴极接到单片机的P0或P2引脚,单片机引脚输出低电平时LED点亮。
这里有个关键设计决策:为什么用共阳极而不是共阴极。51单片机的P0口是开漏输出,内部没有上拉电阻,拉电流能力很弱,高电平时基本上供不出电流;而P2口虽然内部有上拉,但8颗LED同时点亮时电流也偏紧。反过来看灌电流能力,51的每个IO口都能承受20mA左右的灌入电流,低电平时把LED点亮反而更亮更均匀。所以这个项目让LED阳极统一接正极,单片机只需要"往下拉"就能点亮,这也是老51项目里最常见的做法。
限流电阻的取值可以算一下:电源5V,红色LED压降约2V,330Ω电阻上的压降是3V,电流约9mA,16颗全亮总电流约144mA,对USB供电或者三节镍氢电池串联来说毫无压力。你如果觉得亮度不够,可以把330Ω换成220Ω,电流升到约13mA,但需要注意总电流不要超过单片机的地线承载能力,不然复位的瞬间电压跌落容易导致程序重启。
心形图案本身没有存在Flash里,而是用一个16位的变量表示:高8位对应P0口的8颗LED,低8位对应P2口的8颗LED,哪个bit是0哪颗灯就亮。比如想让心形上半部分全亮、下半部分全灭,只需要给LED_Data赋一个上半全部为0的值,然后一次P0、P2口的写操作就完成了。16颗LED看起来多,实际上两个端口各写一次字节就能刷新全部状态,速度比逐颗扫描快得多。
3.2 蜂鸣器驱动电路:三极管放大,别让单片机直接推
蜂鸣器是这个项目声音的来源,用的是无源蜂鸣器——注意,是无源,不是有源。有源蜂鸣器内部自带振荡电路,通电就响,频率固定没法编程;无源蜂鸣器需要外部给它一个方波信号才会发声,频率由方波周期决定。我们的音乐播放靠定时器改变方波频率,所以必须用无源蜂鸣器。
单片机的P1.0引脚输出方波信号,经过一个NPN三极管(常见型号8050)放大后驱动蜂鸣器。三极管的基极串联一个1kΩ电阻接到P1.0,集电极接蜂鸣器的负极,蜂鸣器正极接VCC,发射极直接接地。当P1.0输出高电平时三极管导通,蜂鸣器通电发声;输出低电平时三极管截止,蜂鸣器停止。之所以要加三极管,是因为单片机的IO口直接驱动蜂鸣器时电流不够,声音发闷还容易把引脚烧掉,让三极管当开关去控制蜂鸣器的供电电流是最稳妥的做法。
实际焊接时还应该在蜂鸣器两端反并联一个续流二极管,比如1N4148,因为蜂鸣器内部是线圈,断电瞬间会产生反向电动势,如果没有续流回路,这个尖峰电压可能通过三极管反向冲击单片机引脚。很多初学者焊完发现蜂鸣器响几声之后单片机就死机,多半就是没加这个二极管,或者是三极管引脚接错了——8050的引脚顺序是E、B、C,面对型号面从左到右,接反了完全没声音。
这个电路的另一个坑是P1.0的初始化状态。单片机复位后所有引脚默认高电平,如果程序还没开始跑,三极管已经导通,蜂鸣器会一直长鸣,直到main函数里把P1.0清零才停下。所以固件里启动代码的第一件事就是关蜂鸣器,这个细节在避坑章节会再展开。
3.3 按键输入电路:上拉电阻与消抖的两种做法
两个按键K1、K2分别接在P3.2和P3.3引脚上,这两个引脚同时也是外部中断0和外部中断1的输入口。按键另一端接地,引脚通过10kΩ上拉电阻接VCC,默认状态是高电平,按下后变成低电平,单片机通过读取引脚电平判断按键状态。
这个电路可以用两种方式读取。一种是在主循环里轮询扫描,每隔一小段时间读一次引脚,检测到低电平时软件延时消抖再确认;另一种是利用外部中断,按键按下时引脚电平跳变触发中断,在中断服务函数里处理按键逻辑。这个项目的代码用的是前者——主循环轮询,因为它的主循环本身就在不停地刷新LED,轮询按键几乎不增加额外负担,而且消抖逻辑写起来更直观。
消抖的本质是解决机械按键的抖动问题。按键按下和松开的瞬间,簧片会回弹几次,产生持续几毫秒的不稳定电平跳变,如果不处理,一次按键可能被识别成四五次。这个项目的软件消抖做法是:检测到P3.2变成低电平后,先延时10ms再读一次,如果仍然是低电平才确认有效按键,然后等待按键释放(读到高电平)才结束这次按键处理。这个"确认按下再等释放"的流程能顺带解决长按连发的问题——只有短按会触发一次,按住不放不会反复切换。
如果按键接在P3.2和P3.3上还有个隐藏好处:这两个引脚支持上升沿和下降沿触发的外部中断,以后你想改成中断方式处理按键,硬件电路完全不用动,只需要改程序里的中断配置就行。这也是某些老工程师推荐按键优先接P3.2/P3.3的原因,属于硬件设计上给后续留的后手。
4. 核心代码实现:定时器、音谱数据与灯效联动的完整链路
4.1 定时器0与音符频率:方波是这样产生的
音乐播放的核心是让蜂鸣器产生不同频率的方波。12MHz晶振下12T模式,51单片机的机器周期是1MHz,也就是1微秒。定时器0工作在方式1(16位定时),每计一个数就是1微秒,当它从初值计数到65536溢出时,就在中断服务函数里把P1.0的电平翻转一次,这样蜂鸣器上就得到了一个方波。
方波的频率由定时器初值决定。假设我们要产生中音Do,频率523Hz,它的周期是1/523秒约1.912毫秒,半个周期是956微秒。定时器从初值开始累加到溢出正好经过956个数,那么初值就是65536减去956,等于64580。我把常用音符的初值算好放进频率表,用一个数组索引音符编号:
// 音符频率表:12MHz晶振, 12T模式, 机器周期1us // 初值 = 65536 - (1000000 / (2 * freq)) code unsigned int tune_table[] = { 0, // 索引0: 休止符 64580, // 索引1: 中音Do 523Hz 64684, // 索引2: 中音Re 587Hz 64777, // 索引3: 中音Mi 659Hz 64820, // 索引4: 中音Fa 698Hz 64898, // 索引5: 中音Sol 784Hz 64968, // 索引6: 中音La 880Hz 65030, // 索引7: 中音Si 988Hz 65058, // 索引8: 高音Do 1046Hz 65110, // 索引9: 高音Re 1175Hz 65157, // 索引10: 高音Mi 1319Hz 63628, // 索引11: 低音Do 262Hz 63836, // 索引12: 低音Re 294Hz 64021, // 索引13: 低音Mi 330Hz 64260 // 索引14: 低音Sol 392Hz };定时器0的初始化代码放在timer.c里,只做两件事:设置工作方式、装初值并开中断:
void Timer0_Init(void) { TMOD &= 0xF0; // 清零T0的控制位,保留T1的配置 TMOD |= 0x01; // 设置T0为方式1:16位定时器 TH0 = 0xFC; // 装入高8位初值,对应64580 TL0 = 0x44; // 装入低8位初值 ET0 = 1; // 允许T0中断 EA = 1; // 开总中断 TR0 = 1; // 启动T0 }这里有个容易踩的坑:TH0和TL0要分开写,因为定时器是16位的,初值必须同时装入高8位和低8位。从频率表里取值时注意高位字节是初值除以256取整,低位是初值对256取余,如果直接按表里的十进制写数字,就少了这一步拆字节的动作。
中断服务函数每溢出一次翻转一次P1.0:
void Timer0_ISR(void) interrupt 1 { P1_0 = !P1_0; // 翻转蜂鸣器引脚,产生方波 TH0 = tune_table[current_note] / 256; // 重新装初值 TL0 = tune_table[current_note] % 256; }中断服务函数的开销很小,只有翻转引脚和重装初值两件事。如果当前演奏的是休止符,tune_table[0]的值为0,初值也是0,定时器直接溢出,P1.0快速翻转产生一个刺耳的高频噪声——所以程序里对休止符做了单独处理,停在中断服务函数里直接返回,不翻转P1.0,同时把引脚拉成低电平让蜂鸣器完全安静。这个细节很多初学者会漏掉,导致乐谱里的停顿变成一阵啸叫。
4.2 定时器1与节拍控制:让音乐的速度稳定下来
有了音高还不够,一首曲子还得有节奏。节奏在代码里体现为每个音符持续的时间长短,这个时间由定时器1来掐。定时器1也工作在方式1,每10毫秒产生一次中断,中断里对一个beat_counter变量做递减计数来控制音符的播放时长。
乐谱数据结构里,每个音符既包含音高信息也包含节拍信息。为了让数据紧凑,这个项目用两个并行的数组:一个存音符编号,一个存节拍数。以《小星星》前两句为例:
// 音符序列:索引指向tune_table code unsigned char song_star[] = { 1, 1, 5, 5, 6, 6, 5, // 一闪一闪亮晶晶 4, 4, 3, 3, 2, 2, 1, // 满天都是小星星 0xFF // 结束标志 }; // 节拍序列:每个音符的持续时长(单位:100ms) code unsigned char beat_star[] = { 4, 4, 4, 4, 4, 4, 8, // 前三个音各0.4秒,第五个音0.8秒 4, 4, 4, 4, 4, 4, 8, // 和上面完全对称 0xFF };定时器1的中断服务函数负责推进节拍:
void Timer1_ISR(void) interrupt 3 { TH1 = 0xD8; // 10ms定时初值,12MHz下约10000个机器周期 TL1 = 0xF0; if (playing_flag == 1) { beat_counter--; // 每10ms减一次 if (beat_counter <= 0) { // 当前音符时间到,切换到下一个音符 note_index++; if (song_data[note_index] == 0xFF) { // 整首曲子弹完,回到开头重播 note_index = 0; } current_note = song_data[note_index]; beat_counter = beat_array[note_index]; } } }节拍控制的关键在于:定时器1中断是固定10ms一次,无论主循环在干什么,节拍计数都不会被影响。这比在主循环里用delay()函数来掐节拍要准得多——主循环里如果插入了LED刷新、按键扫描这些操作,软件延时的时长会被拉伸,音乐速度就会忽快忽慢。第一次写完这个项目时我就犯过这个错,用两个delay()嵌套控制音符时长,结果按键一按旋律就变慢,松开又恢复,后来改成定时器驱动才稳定下来。
4.3 灯光驱动的四种模式:从静态心形到音符联动
LED控制的入口是一个刷新函数,它根据当前灯光模式的标志值决定这16颗LED应该处于什么状态。因为只用P0和P2两个端口,一次刷新就是两条赋值语句的事,所以它可以被主循环非常频繁地调用。
静态心形模式最简单,直接把一个预先设计好的心形图案数值赋给端口。所谓图案数值,就是16个bit的排列,对应心形轮廓上每颗灯的位置。比如高8位给P0对应上半部分,低8位给P2对应下半部分,如果想上半部分亮两颗、下半部分亮一颗,就把对应位置写成0。写一个专门的常量数组存不同的心形阶段图案,流水效果就能实现了。
呼吸灯模式用PWM方式调亮度,但51单片机没有硬件PWM模块,只能靠软件模拟。做法是每隔一小段时间把LED数据分成多个档位循环输出——比如亮度分成20档,每档点亮的时间占比不同,人眼视觉暂留效应下看起来就是亮度从暗到亮再到暗的渐变。这个模式的代码要放在主循环里反复执行,并且每执行完一轮要更新一次亮度档位的方向,否则会卡在最大或最小亮度上。
音符联动模式的逻辑最有趣,它直接读取当前播放音符的索引值,通过一个映射关系来决定LED的亮法。比如音高越高点亮的LED越多,形成一种"音量条"的效果;还可以让音高高的区间点亮心形上半部的灯,音高低时点亮下半部的灯,听起来像是一颗心在随着旋律上下跳动。映射关系可以自己定义,玩法很灵活。
4.4 主循环与状态机:按键、播放、灯效怎么协作
主循环的框架是整个程序的心脏,它把按键扫描、LED刷新、播放状态维护这三件事按顺序循环执行。整个程序采用状态机设计思想,current_song和light_mode这两个全局变量就是状态变量,每一次按键按下只是修改状态变量的值,真正的行为在循环体里根据状态决定:
void main(void) { Timer0_Init(); // 初始化定时器0,产生音符频率 Timer1_Init(); // 初始化定时器1,产生10ms节拍 led_mode = 0; // 默认灯光模式:静态心形 song_index = 0; // 默认播放第一首曲子 P1_0 = 0; // 关闭蜂鸣器引脚 while (1) { Key_Scan(); // 扫描按键,更新状态变量 Led_Refresh(); // 根据当前模式刷新LED delay_ms(5); // 给主循环一个基本的时间节拍 } }按键扫描函数的逻辑用一句话可以概括:检测低电平、延时消抖、确认后更新状态变量。它有一个专门的last_key_state变量记录上一次的按键状态,用来识别"按下"这个边沿动作,避免按住不放时连续触发。K1按下让song_index加1并对曲目总数取模,K2按下让led_mode加1并对4取模,这样在任何状态下按按键都不会越界。
主循环里的delay_ms(5)既是一个简单的节拍器,也是让整个循环体保持在一个可预测的运行频率上。5毫秒的延时对按键扫描来说足够快,对LED刷新来说也足够流畅,对蜂鸣器——蜂鸣器靠定时器中断驱动完全不依赖主循环——更是没有任何影响。整个状态机设计里,main函数只负责"策展",真正的"演出"全在中断服务函数里进行,这种职责分离让代码逻辑非常清爽。
5. 避坑指南:电源、蜂鸣器、LED亮度的四个真实踩坑记录
5.1 蜂鸣器不出声,程序却跑得欢
现象:程序烧进去,LED的心形图案正常亮灭,按键切换灯效也正常,但蜂鸣器一点声音都没有。
原因:当时查了半天发现是蜂鸣器选错了型号,手里拿着有源蜂鸣器当无源用。有源蜂鸣器通电就响,给它一个固定电平它反而安静;只有无源蜂鸣器才需要方波驱动。另一个常见原因是三极管接错引脚,8050的E、B、C三脚顺序搞反,信号根本送不到蜂鸣器。
解决:先写一个最简单的测试程序,让P1.0引脚输出一个固定频率的方波,比如用定时器溢出翻转引脚生成1kHz信号,如果这样都不响就查硬件电路,响了再回来跑完整程序。顺便检查一下蜂鸣器型号或者标签,无源蜂鸣器通常标称3.3V或5V且没有内部振荡电路标识,有源蜂鸣器底部会标一个"+"并自带驱动电路。
5.2 音乐速度忽快忽慢,像卡带一样
现象:刚上电时音乐速度正常,按一下按键切歌之后,旋律明显变慢,再按几次干脆像卡住了一样,过几秒又突然恢复正常。
原因:第一次写节拍逻辑时用的是软件延时,delay_ms()函数里包含了音符时长的等待。按键扫描和LED刷新插在延时函数之间,每执行一次按键检测、IES刷新都会让延时的实际时间变长,所以音乐速度被主循环的负载拖慢了。更糟的是切歌时延时时间被重置,出现卡带效果。
解决:把节拍计时完全交给定时器1,音符切换不在主循环里做,而是在10ms中断服务函数里做判断。主循环只管按键和LED,无论如何繁忙,中断里的计时都是准的。从那以后我做任何带时间控制的项目,都会强制让时间基准跑在中断里,主循环只做逻辑不做延时。
5.3 LED亮度不均匀,心形看起来缺了一角
现象:心形图案正常点亮,但部分LED明显比其他亮,还有一两个暗到几乎看不出来,整体观感很差。
原因:限流电阻取值不一致。手头330Ω电阻不够,有几颗位置用了470Ω,电流差了几毫安,亮度差异肉眼可见。另一个原因是P0口和P2口的驱动能力不同,P0口是开漏输出,虽然灌流能力强,但接法稍有差异就会和P2口拉开差距。
解决:把所有限流电阻统一成同一阻值,并优先选用低电平点亮的共阳极接法让灌电流均匀分布。焊接前先用万用表把电阻量一遍,误差超过5%就换掉。如果你发现某颗特别暗,优先检查焊接有没有虚焊——LED引脚焊盘松动会导致接触电阻变大,电流变小,亮度自然就降了。
5.4 按键按下没反应,偶尔还跳两下
现象:按K1切歌经常没反应,多按几次又出现一次切两首的情况,很不稳定。
原因:两个问题叠加在一起。一个是按键没有做软件消抖,机械抖动导致一次按下被识别成多次;另一个是把按键引脚和LED共用了一个P0口,LED刷新的时候端口电平毛刺干扰了按键读取。
解决:按键改到P3口独立引脚,和LED彻底分开。软件上加上10ms延时消抖,也就是检测到低电平后延时10ms再读一次,确认还是低电平才算有效,同时加上"等待释放"逻辑防止按住连发。如果你用的是P3.2/P3.3这种带外部中断的引脚,改成中断方式读取也可以,但消抖逻辑不能省。
6. 二次开发指南:把固定曲目扩展成可切换的曲库
想给这个音乐盒加歌,关键不是改代码,而是改数据。现有的曲目数据结构是两组并行数组——音符序列和节拍序列,结束标志是0xFF。我建议你把它们封装成曲目表,用一个结构体把音符数组和节拍数组捆绑起来,这样每首歌就是一个独立的实体,切歌逻辑只需要改变指针指向。
// 曲目结构体:一首歌的音符序列和节拍序列 struct Song { code unsigned char *notes; // 指向音符数组 code unsigned char *beats; // 指向节拍数组 }; // 注册新歌:以《欢乐颂》片段为例 code unsigned char notes_ode[] = { 3, 3, 4, 5, 5, 4, 3, 2, // 欢乐颂前两句旋律 1, 1, 2, 3, 3, 2, 2, 0xFF }; code unsigned char beats_ode[] = { 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, 0xFF }; // 曲目表:把三首歌统一编排,方便按键循环切换 struct Song song_list[] = { {song_star, beat_star}, // 第一首:小星星 {notes_ode, beats_ode}, // 第二首:欢乐颂 {song_birth, beat_birth} // 第三首:生日快乐 };切歌逻辑从"对数组索引加1"变成"对song_list数组下标加1并取模",整个代码几乎不用动,只是把原来直接读音符数组的写法改成先取出当前曲目的结构体再读里面的指针。我实际改的时候花了大约20分钟,剩下的时间是抄简谱。
灯光联动也值得扩展。现在音符联动模式只是简单的"音高越高灯越亮",你可以把它改成按旋律走向扫描心形——从心尖开始逐颗点亮,音符往高处走就往上亮,往低处走就往下灭,看起来像一颗心在呼吸。改这个要先在LED刷新函数里加一个note_scan_index变量,每次中断切换到新音符时步进这个变量,然后按它去查图案数值表。这个效果做出来比原版的声色联动更有感染力。
最后提醒一句:改完代码一定要用STC-ISP软件重新烧录,如果板子用的是老款STC89C52,冷启动烧录前需要先断电再上电,这个操作细节很多人第一次搞程序员都会卡住。整个项目的原理图和固件工程都可以在下载页拿到,我的习惯是每次改版前先把原工程的hex文件备份一份,改坏了随时能撤回,省得重新焊板子——希望你用得上,也希望你在上面改动出一点自己的东西。
本文还有配套的精品资源,点击获取