51单片机实现三首音乐自动循环播放:无源蜂鸣器与定时器中断设计
2026/9/12 7:03:08 网站建设 项目流程

简介:基于51单片机的音乐播放器仿真实例,面向单片机初学者与电子爱好者,帮助理解用单片机控制无源蜂鸣器发声的设计思路。项目选用51/52单片机为主控,使用Keil4进行C语言编程,结合Proteus7.8绘制仿真电路,内置《挥动翅膀的女孩》《同一首歌》《两只蝴蝶》三首乐曲并自动循环播放,涵盖最小系统、晶振复位电路及蜂鸣器驱动等基础模块。资源包共22个文件,压缩包大小3.54MB,主要包含Keil工程文件(uvproj/c/h/hex)、Proteus仿真文件(dsn)、演示视频(mp4)、谱曲软件(exe)、电路截图和说明文档(docx/txt),从源码、仿真到音谱工具均有覆盖。目前已有1287人学习下载。资料完整度高,既可直接烧写hex文件验证效果,也可打开工程修改曲目和时序参数。演示视频便于对照操作,谱曲软件能自定义音乐编码,适合课程设计、毕业设计及入门自学使用,可帮助读者快速掌握51单片机音乐播放器的完整开发流程。

1. 从无源蜂鸣器到三首循环播放:51单片机音乐播放器的设计起点

很多人第一次把51单片机的IO口直接接到无源蜂鸣器上,写一个电平翻转程序,听到的是“咔哒咔哒”的爆音,而不是旋律。原因在于无源蜂鸣器内部没有振荡源,必须由外部提供特定频率的方波,音高才由方波频率决定。这套“基于51单片机的音乐播放器”方案,正是用AT89C51/STC89C52这类通用51/52芯片,配合定时器中断产生不同频率的方波,让一只无源蜂鸣器依次播放《挥动翅膀的女孩》《同一首歌》《两只蝴蝶》,并且三首音乐自动循环。压缩包内包含Keil uVision4工程源码、Proteus7.8仿真文件、演示视频和MusicEncode谱曲软件,适合做单片机课程设计、Proteus仿真入门,也适合想自己改曲目的人研究播放数据结构。

2. 定时器中断与音调频率表:51单片机发声的底层机制

2.1 无源蜂鸣器驱动为什么离不开方波

无源蜂鸣器内部是压电陶瓷片或电磁线圈,输入稳定的直流电平并不会持续发声。必须让蜂鸣器两端的电压以一定频率变化,振膜才能推动空气发出对应音调的声音。一个完整的方波周期内,高电平和低电平各占一半,所以IO口翻转频率是目标声音频率的两倍。这个关系决定了定时器初值计算不能直接用频率的倒数,而是要用半个周期。很多人第一次在这里算错,导致播放出来的音乐整体高八度或者低八度。

在Proteus中,“SOUNDER”器件默认就是无源蜂鸣器模型,和实际硬件里的无源蜂鸣器符号一致。资源里的蜂鸣器电路也是无源蜂鸣器,所以程序里才需要主动产生不同频率的方波。如果误用了带振荡源的有源蜂鸣器,接通电源后自己就会一直响,完全不受音调控制。因此拿到这套源码后,第一件事是确认Proteus仿真里放的是无源蜂鸣器,再看程序里的定时器初始化。

2.2 定时器T0方式1的初值计算与中断服务

使用定时器T0工作在方式1,也就是16位定时器。晶振为12MHz时,标准51的机器周期是12除以12MHz,等于1微秒。要产生频率f的声音,半个周期的时间是500000/f微秒,定时器计数器需要在这个时间内溢出。所以初始值写成:

reload = 65536 - 500000 / f

如果换成11.0592MHz晶振,机器周期约1.085微秒,初值公式里的500000要替换成11059200 / 12 / 2 / f。把晶振值直接写死成12MHz,换晶振后所有音调都会偏移,这是移植时最常见的坑。

以下代码给出定时器T0初始化、全局重装变量和中断服务程序:

unsigned int timer_reload; #define FOSC 12000000UL #define TIMER_RELOAD(freq) (65536UL - FOSC / 12UL / 2UL / (freq)) void timer0_init(void) { TMOD &= 0xF0; TMOD |= 0x01; // T0工作方式1:16位定时器 timer_reload = TIMER_RELOAD(262); // 先按中音C4=262Hz装载 TH0 = (unsigned char)(timer_reload >> 8); TL0 = (unsigned char)timer_reload; ET0 = 1; // 允许T0中断 TR0 = 1; // 启动T0 EA = 1; // 总中断使能 } void timer0_isr(void) interrupt 1 { TH0 = (unsigned char)(timer_reload >> 8); TL0 = (unsigned char)timer_reload; buzzer = !buzzer; // 翻转蜂鸣器引脚,输出方波 }

这段代码的逻辑是:先根据目标频率算出定时器初值,每次中断触发后立刻重装初值,然后把蜂鸣器引脚电平翻转一次。buzzerSOUNDPLAY.H中通常被定义成sbit buzzer = P2^3;,对应Proteus仿真图里蜂鸣器驱动的控制引脚。中断服务里只做重装和翻转两件事,不要在中断里查表或调用延时函数,否则中断占用时间过长会让实际输出频率偏低。

几个常用音符的初值参考如下表,晶振统一按12MHz计算:

音符频率(Hz)半周期计数定时器初值十六进制
C4 Do2621908636280xF88C
D4 Re2941701638350xF95B
E4 Mi3301515640210xFA15
G4 So3921276642600xFB04
A4 La4401136644000xFB90
C5 Do523956645800xFC44

如果仿真里声音整体偏低,可以检查是不是把半周期计数值当成了全周期。比如C5实际初值应是64580,装成64818就变成了低一个八度的效果。Proteus仿真中把单片机晶振频率设置成12MHz,并且与代码里的FOSC保持一致,音准才不会漂。

2.3 音调频率表与乐谱索引的组织

播放器里需要维护一张频率表,把“歌谱里的音”映射成具体频率。常见做法是把简谱1到7对应中音区,8到14对应高八度,15到21对应低八度,0号位置留给休止符。这样播放器逻辑不需要知道每个音符的真实频率,只需要根据音调索引去查表。

频率表可以这样定义:

code unsigned int freq_table[] = { 0, // 0: 休止 262, 294, 330, 349, 392, 440, 494, // 1-7: 中音1-7 523, 587, 659, 698, 784, 880, 988, // 8-14: 高音1-7 131, 147, 165, 175, 196, 220, 247 // 15-21: 低音1-7 }; void set_tone(unsigned char index) { unsigned int freq; if (index == 0) return; freq = freq_table[index]; timer_reload = TIMER_RELOAD(freq); }

这里用code关键字把频率表放在程序存储区,不占51单片机宝贵的RAM。set_tone只负责更新全局变量timer_reload,下一次T0中断就会装载新初值,实现音符切换。这张表是不包含半音阶的自然音阶,对《两只蝴蝶》《同一首歌》这类流行歌曲足够;如果遇到升降号,可以用相邻最近的音代替。想支持半音阶时,把频率表扩展为每半音一个项即可。

3. 三首音乐的乐谱编码:MusicEncode 谱曲软件与播放数据结构

3.1 MusicEncode.exe 在项目里的作用

资源里附带了一个MusicEncode.exe,这是给51单片机音乐播放器用的谱曲编码工具。它要解决的问题是:几十个音符手工写进数组,很容易写错音高或时值。常见用法是打开工具后,按简谱把“1 2 3 5 6”这样的旋律和节拍线录入,生成一段十六进制数据,然后把这段数据粘贴到Keil源码里。由于不同版本的工具输出格式可能有差异,我这里按这一类播放器最通用的格式来讲:每个音符用两字节表示,高字节是音调索引,低字节是节拍数。

3.2 播放数据结构:音调高字节、节拍低字节

为了使编码紧凑,可以用一个unsigned int保存一个音符。高字节表示频率表下标,低字节表示时值。例如0x0104表示音调索引1,持续4个节拍单位;0x0002表示休止2个单位;0xFFFF作为整首曲子的结束标志。代码中用宏让乐谱数据更可读:

#define N(tone, beat) (((tone) << 8) | (beat)) #define SONG_END 0xFFFF code unsigned int song1[] = { N(1,4), N(3,4), N(5,8), N(6,4), N(5,4), // 1 3 5 6 5 ... N(3,4), N(5,8), N(6,8), N(5,4), N(2,4), SONG_END };

beat的最小单位称为“最小节拍单位”。如果设定四分音符占4个单位,N(5,8)就是二分音符,N(3,2)是八分音符。附点音符尽量拆成“前短后长”两个相邻编码,避免播放器解码复杂化。song1里第一个音N(1,4)对应中音1,也就是262Hz,时长为四分音符。

播放函数负责读取数组并驱动蜂鸣器:

void play_song(code unsigned int *song) { while (1) { unsigned int note = *song++; unsigned char tone_index, beat; if (note == SONG_END) break; // 歌曲结束 tone_index = note >> 8; // 高字节:音调索引 beat = note & 0xFF; // 低字节:节拍数 if (tone_index == 0) { TR0 = 0; // 休止符:停止T0 buzzer = 0; // 引脚拉低 } else { set_tone(tone_index); // 查表并更新定时器初值 TR0 = 1; } delay_ms((unsigned int)beat * BEAT_UNIT_MS); } }

这段代码每次取一个音符,先判断是否结束,再拆出音调索引和节拍。休止符时关闭T0并把蜂鸣器引脚拉低,避免蜂鸣器上出现持续直流电平。正常音符则通过set_tone更新timer_reload,然后阻塞等待对应的毫秒数。阻塞期间T0中断仍然在后台翻转引脚,所以声音不会中断。BEAT_UNIT_MS根据曲速设置,比如每分钟120拍、四分音符500ms时,一个最小单位就是125ms。

3.3 三首曲子自动循环的调度

三首音乐数据都放在code区,再用一个指针数组把它们组织起来。主函数里的while(1)循环不断取出歌曲并调用play_song播放:

code unsigned int song2[] = { /* 《同一首歌》数据 */ }; code unsigned int song3[] = { /* 《两只蝴蝶》数据 */ }; code unsigned int *songs[] = { song1, song2, song3 }; void main(void) { unsigned char i = 0; timer0_init(); while (1) { play_song(songs[i]); i++; if (i >= 3) i = 0; } }

这里的songs数组必须声明为code unsigned int *,否则Keil C51可能把指针放到data区,浪费RAM且访问速度更慢。整首曲子播完后遇到SONG_END返回,然后立刻进入下一首。如果需要歌曲间有明显停顿,可以在play_song(songs[i])后加一个delay_ms(500)。三首歌的数据加起来只有几百字节,对8KB程序存储空间的51单片机来说压力很小。手工录入乐谱时最需要注意的是结尾必须有SONG_END,漏掉后播放器会越界读后面数组,产生乱码一样的声音。

4. Proteus 仿真与 Keil 联调:电路搭建、HEX 烧录和波形验证

4.1 Proteus 里的蜂鸣器驱动电路

打开仿真.DSN之前,建议先看压缩包里的系统框图.png,它清晰标出了单片机最小系统、晶振电路、复位电路和蜂鸣器电路之间的关系。仿真图里通常用P2口控制蜂鸣器,但单片机IO口直接驱动蜂鸣器的声音很小,实际电路也容易让IO口过流。常见做法是用NPN三极管做开关放大:单片机引脚输出方波,三极管把方波电流放大,驱动蜂鸣器线圈。

元件参数连接位置
AT89C5140脚DIP31脚接VCC,RST接复位电路
晶振12MHzXTAL1、XTAL2,两端对地接30pF电容
复位电路10uF电容 + 10k电阻RST复位电平
蜂鸣器SOUNDER一端接VCC,另一端接三极管集电极
NPN三极管2N3904或2SC1815基极经1k电阻接P2.3,发射极接地

在Proteus7.8中直接打开仿真.DSN可以看到元件已经连好。如果自己重新搭建,从AT89C51、12MHz晶振、两个30pF电容、10uF电解电容和10k电阻开始搭最小系统,然后把P2.3引脚通过1k电阻接到三极管基极。蜂鸣器使用“SOUNDER”模型,它不带振荡源,和无源蜂鸣器行为一致。

4.2 Keil 工程编译与 HEX 文件生成

源码里的PlayMusic.cSOUNDPLAY.H就是Keil工程的核心文件。用Keil uVision4打开PlayMusic.uvproj,在Options for TargetOutput选项卡中勾选Create HEX File,然后执行Rebuild。编译成功后会生成PlayMusic.hex。如果自己新建工程,单片机型号选择Atmel AT89C51或AT89C52,内存模型选择Small即可,晶振频率只影响调试模拟,不影响HEX文件生成结果。

编译失败最常见的问题是HEX文件没有输出,原因是Output选项卡里没勾选生成选项。另一个常见问题是曲谱数组太大导致OUT OF MEMORY,这时检查数组是不是误用了data关键字,应该把大数组放到code段。本项目的三首歌曲和频率表都放在code段,RAM里只保留timer_reload和循环变量,所以编译后资源占用很小。

4.3 Proteus 加载 HEX 与波形排查

在Proteus里双击单片机芯片,在Program File一栏选择PlayMusic.hex,将晶振频率设置为12MHz,点击运行。正常情况下蜂鸣器会依次播放三首音乐。如果没有声音,先不要改程序,右键点击P2.3引脚并添加Digital Trace,运行后观察该引脚波形。波形是一串不同频率的方波,说明定时器在正常工作。如果波形固定在高电平或低电平,说明T0没有启动,或者主循环卡在了某个死循环里。

Proteus8以上版本打开仿真.DSN时可能会提示这是低版本文件。常见做法是用兼容方式打开,或者新建一个Proteus工程后导入DSN。资源说明里已经提到,8.0以上版本参考“如何使用proteus8打开低版本proteus7的仿真文件”这篇文章。仿真中停止运行后,蜂鸣器可能停在最后的电平状态,再次运行前建议先按一下复位,否则波形观察会受到影响。

4.4 用演示视频验证三首音乐播放

压缩包里附带演示视频.mp4,记录了完整的三首音乐播放过程。仿真调试时把运行速度设置为和实时一致,不要开几十倍速,否则Proteus的音频模型无法按真实时间输出。视频里可以明显听出曲目之间的切换,主程序里的while(1)结构保证了三首曲子自动循环。如果想确认换曲次数,可以在play_song入口处让一个全局变量自增,用Keil的Debugger查看这个变量变化。

5. 进阶:让三首音乐播放更顺滑的几个细节

5.1 中断服务里坚决不查表

定时器中断服务程序里最忌讳做复杂计算。code区查表虽然快,但C51编译后仍会占用几十个机器周期,导致实际翻转间隔发生偏移。高音区半周期本来就短,中断处理时间占比越大,听到的音高就越低。正确做法是在主循环或切换音符时查表,把timer_reload预计算好,中断里只做“重装初值 + 翻转引脚”两个动作。

5.2 消除音符切换瞬间的咔哒声

蜂鸣器播放时最常见的杂音来自音符结束时引脚停在随机电平。一个简单办法是在休止符时先关闭中断,再拉低引脚,保证下一次发声从低电平开始:

void buzzer_off(void) { EA = 0; TR0 = 0; buzzer = 0; EA = 1; }

关闭全局中断能避免T0中断在高低字节赋值中间插入,防止初值不一致。音符切换很频繁时,这段操作会产生极短的静音间隙,但对《挥动翅膀的女孩》《同一首歌》《两只蝴蝶》这类慢节奏歌曲来说,听感反而更干净。

5.3 节拍精度交给定时器T1

软件delay_ms在只有一个播放任务时够用,但一旦加上按键扫描、LCD显示,节拍就会变慢。更稳定的做法是用T1产生10ms时基中断,在主循环中设置一个beat_flag,播放器每10ms把当前音符的剩余节拍减一次。这样播放音乐和刷新显示互不干扰。如果不需要并发任务,资源里的软件延时方案可以少写一个中断,代码更简单。

5.4 用 MusicEncode 替换成自己的歌

打开MusicEncode.exe,按简谱输入旋律,把音高对应到频率表索引。比如《两只蝴蝶》里出现的高音1对应索引8,中音5对应索引5,休止符用0表示。附点音符拆成短音符加长音符两个编码,生成后替换song3数组即可。替换时注意高音索引不要超过频率表长度,否则查表会越界或取到0,产生异常长音。把三首曲子都换成自己的目标歌后,定时器初值、中断服务、播放主循环这些底层代码完全不用动。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询