
简介压缩包内含一份51单片机蜂鸣器播放音乐的C语言实验代码主要面向单片机初学者、嵌入式爱好者及电子设计相关课程学习者帮助解决用C语言控制蜂鸣器发出不同音调并演奏乐曲的问题。压缩包共24个文件容量约109KB以两个Keil工程为主包含C源文件、头文件、hex烧录文件、uv2工程文件以及obj、lst、m51等编译中间文件另附一篇PDF文档说明音乐演奏时音调和节拍的确定方法便于对照理解。已有6748人学习下载。通过本资源可以掌握蜂鸣器发声原理、定时器产生不同频率音调的方法以及节拍控制思路两个实验程序从基础到综合配合文档中音调频率表与节拍公式能够快速上手并自行编写播放其他乐曲的代码。1. C语言控制51单片机蜂鸣器播放音乐卡住你的往往不是C语言C语言控制51单片机蜂鸣器播放音乐看标题像入门演示真做起来第一道坎反而不是C语言而是搞清楚你手上那只蜂鸣器到底是哪种。有源蜂鸣器内部自带振荡电路通电就响频率焊死只能发出干巴巴的“嘀嘀”声真正能放旋律的是无源蜂鸣器它没有振荡源外部输入什么频率的方波它就发出什么音高的声音。所以整套方案的核心只有两件事把音符频率折算成定时器初值用定时器中断产生精确方波再把简谱拆成“音高节拍”两列数据存进数组让主循环按时间点逐个取音。文章从驱动电路一路写到Proteus仿真蜂鸣器没声音怎么排查适合51单片机课程设计、智能报警提示音这类需要把音乐塞进8位单片机的场景。2. 蜂鸣器选型与驱动电路无源蜂鸣器才有“演奏”资格2.1 有源与无源蜂鸣器到底差在哪里判断蜂鸣器类型最直接的办法是看引脚和外壳封装但更可靠的是做一次直流测试给两只引脚直接接5V直流电马上响的是有源蜂鸣器沉默的是无源蜂鸣器。有源蜂鸣器内部集成了一个固定频率的振荡电路常见规格是2.7kHz左右供电后内部振荡器直接驱动压电片或电磁线圈声音频率不可调。无源蜂鸣器必须由外部提供交流信号通常是一只压电陶瓷片或电磁线圈输入方波的频率就是它发声的基频。对比项有源蜂鸣器无源蜂鸣器内部振荡源有无声音频率固定如2.7kHz由输入方波频率决定控制方式给高电平或低电平即响需要方波、PWM或定时器翻转能否播放旋律不能只能当报警器可以常见封装长引脚、顶部有胶贴引脚短、壳体背面有两个焊点做旋律播放必须选无源蜂鸣器这一点在Proteus仿真里同样成立。仿真库里那个叫“BUZZER”的器件模型大多模拟的是有源蜂鸣器输入直流电平才发声输入方波反而可能不响或只在极低频率响。后面第5章会专门展开仿真选型的问题。2.2 5V供电下的三极管驱动S8050一个电路就够了51单片机GPIO高电平拉电流能力很弱标准8051在VCC5V时单个引脚高电平输出电流通常不超过几百微安直接驱动无源蜂鸣器这种需要2040mA电流的负载电压会被拉垮声音也软绵绵的。常见做法是用一个NPN三极管做开关典型电路是S8050加上基极限流电阻蜂鸣器接在VCC和集电极之间发射极直接接地GPIO通过电阻接基极。VCC 5V ---- 无源蜂鸣器正极 无源蜂鸣器负极 ---- S8050集电极 S8050发射极 ---- GND P1.5 ---- 4.7kΩ电阻 ---- S8050基极基极电阻取4.7kΩ时5V减去三极管be结压降约0.7V基极电流约0.9mA。S8050的直流放大倍数hFE最小也在40以上集电极电流可以轻松达到3040mA足以驱动一只额定电流20mA的蜂鸣器。如果需要同时驱动多只蜂鸣器或者继电器混合电路可以换成ULN2003达林顿管阵列它的输入端可以直接接5V逻辑电平输出端内部已经集成了续流二极管省掉不少外围元件。驱动方案核心元件适用场景三极管S8050 4.7kΩ单只蜂鸣器学习板最常见达林顿阵列ULN2003多路蜂鸣器、继电器混合驱动直接驱动无仅限额定电流小于1mA的压电蜂鸣器蜂鸣器是感性或容性负载关断瞬间会产生反向电动势。电磁式无源蜂鸣器建议在两端反向并联一只1N4148二极管阴极接VCC阳极接集电极否则长时间高频率切换可能缩短三极管寿命。压电式蜂鸣器容性较强一般不用加。2.3 硬件自检的3个步骤硬件搭好后先别急着跑音乐程序用最小测试代码确认电路能出声。第一步写一个while(1)循环里翻转P1.5引脚、翻转周期约1kHz的死循环程序烧录后能听到“嘀”声说明三极管和蜂鸣器通路正常。第二步如果没声音用万用表蜂鸣器挡直接测蜂鸣器两只引脚有蜂鸣器档响声说明蜂鸣器本身没坏。第三步测量S8050基极电压P1.5输出高电平时应在0.7V左右如果接近0V检查GPIO配置和排针跳线。这里有个容易踩的坑很多51开发板上的蜂鸣器跳线默认接的是有源蜂鸣器或者板上同时集成了有源和无源两个蜂鸣器位。跑旋律前先看丝印和原理图确认当前使能的是无源那一路。Proteus仿真里的“蜂鸣器没声音”八成也是模型选错后面第5.3节单独讲。3. 定时器中断生成精确方波从初值计算到完整代码3.1 为什么不用延时函数控制频率有人会想最简单的方式是GPIO拉高、延时、拉低、再延时循环往复。这个思路在频率精度要求不高时勉强能响但它有两个硬伤一是delay函数本身不是定时器CPU执行循环指令数量受编译器优化选项影响换一个优化等级频率就变了二是延时期间CPU被完全占死什么都干不了。51单片机要想边播放音乐边扫描按键、刷新数码管必须把“产生方波”这件事从主循环里剥离出去。常见做法是使用定时器中断每次定时器溢出进入中断在中断里翻转一次蜂鸣器引脚然后重新装载初值让“翻转”周期稳定在目标频率对应的时间长度上。主循环只负责“每隔一段时间换一个音符”两个任务互不阻塞这就是无源蜂鸣器非阻塞播放音乐的原理。3.2 从音符频率到定时器初值51单片机定时器本质是一个计数器每个机器周期加1加满溢出。以最常见的12MHz晶振为例8051的机器周期是12个时钟周期所以定时器每计数一次正好过去1微秒。定时器0工作方式1是16位计数器从初值N数到65536溢出一次完整溢出周期就是(65536 - N)微秒。方波一个完整周期包含高电平和低电平两段蜂鸣器引脚每次中断翻转一次翻转间隔就是半个周期。假设目标频率是fHz半周期时间就是:半周期us 1000000 / (2 × f) 500000 / f 定时器初值 65536 - 500000 / f以中音do262Hz为例半周期us500000/262≈1908初值65536-190863628。这个初值在初始化时写入TH0和TL0中断每次进入后都要重新装载一次否则下一次溢出周期会变成65536us声音瞬间从262Hz跌到约15Hz变成“哒哒哒”的爆音。3.3 定时器0初始化与中断翻转代码下面是一个可直接编译进Keil工程的最小实现用定时器0做方波发生器P1.5接驱动三极管基极。#include reg52.h sbit BEEP P1^5; unsigned int freq_count; // 当前音符的半周期计数值单位1us unsigned char beep_en; // 蜂鸣器输出使能1输出声音0静音 void Timer0_Init(void) { TMOD 0xF0; // 只修改定时器0相关位 TMOD | 0x01; // 定时器0工作方式116位定时 TH0 0xFF; TL0 0xFF; ET0 1; // 使能定时器0中断 TR0 1; // 启动定时器0 } void Timer0_ISR(void) interrupt 1 { // 重新装载初值保证下一个周期频率不变 TH0 (unsigned char)((0x10000UL - freq_count) 8); TL0 (unsigned char)(0x10000UL - freq_count); if (beep_en) { BEEP ~BEEP; // 翻转引脚输出方波 } else { BEEP 0; // 静音状态下锁定低电平 } } void main(void) { EA 1; // 总中断开关 Timer0_Init(); freq_count 1908; // 中音do约262Hz beep_en 1; while (1) { } }代码里有三个关键参数需要说明。TMOD | 0x01把定时器0设置为16位定时模式这是最常用的工作方式计数范围065535适合几百赫兹到几千赫兹的音频频率。interrupt 1对应定时器0中断入口这个是8051中断向量表定死的写错数字编译可能通过但中断永远不执行。freq_count用unsigned int类型装的是半周期微秒数1908对应262Hz。如果在Keil里编译报警告注意把0x10000写成0x10000UL否则16位int会把高位截掉。3.4 最小测试工程怎么验证下载程序后应该听到一个持续的中音do。用示波器探头测P1.5引脚能看到频率约262Hz、占空比50%的方波。没有示波器就把beep_en置0听不到声音说明静音逻辑生效然后改用手机分贝软件测频率误差在几赫兹以内基本正常。声音不对优先检查晶振。Proteus仿真默认的AT89C52晶振频率经常是4MHz不是程序里预期的12MHz定时器计数周期变成3微秒所有音符频率都除以3听感上整体低了一个半八度加一点出现“嗡嗡”声。双击单片机模型把Crystal Frequency改成12MHz即可。4. 乐谱转数组与主循环播放小星星从简谱到蜂鸣器4.1 一张表搞定常用音阶的定时器计数值要让单片机识别音符先建一张“音高→半周期计数值”的查找表。以自然大调为例把低音5到高音3这十三个音列出来按前面公式计算好直接抄进程序。音名频率(Hz)半周期(us)定时器初值低音5196255162985低音6220227363263低音7247202463512中音1262190863628中音2294170163835中音3330151564021中音4349143364103中音5392127664260中音6440113664400中音7494101264524高音152395664580高音258785264684高音365975964777这个表用产品代码的code关键字存到ROM里不占宝贵的片内RAM。code unsigned int freq_tab[] { 0, // 0休止符无声音 2551, // 1低音5 2273, // 2低音6 2024, // 3低音7 1908, // 4中音1 1701, // 5中音2 1515, // 6中音3 1433, // 7中音4 1276, // 8中音5 1136, // 9中音6 1012, // 10中音7 956, // 11高音1 852, // 12高音2 759 // 13高音3 };数组第0项是休止符对应freq_tab[0]0。注意中断里每次都会做0x10000UL - freq_count如果freq_count真的被赋成0初值变成65536TH0和TL0都装成0定时器会以最快速度连续溢出虽然beep_en0不发声但中断会疯狂触发拖慢主循环。所以播放休止符时程序里要给freq_count一个安全值比如1000。4.2 用两个数组把简谱拆成数据结构音乐包含两类信息一类是“这个音多高”一类是“这个音多长”。用两个code数组分别存放音符编号和节拍是最直观的做法。以《小星星》前两句为例1 1 5 5 | 6 6 5 - | 4 4 3 3 | 2 2 1 - |code unsigned char music[] { 4, 4, 8, 8, 9, 9, 8, 0, // 第一句中音1中音1中音5中音5中音6中音6中音5 休止 7, 7, 6, 6, 5, 5, 4, 0, // 第二句中音4中音4中音3中音3中音2中音2中音1 休止 0xFF // 结束标志读到它停止播放 }; code unsigned char beat[] { 1, 1, 1, 1, 1, 1, 2, 1, // 前六个音符各1拍中音5持续2拍休止1拍 1, 1, 1, 1, 1, 1, 2, 1, 1 // 结束标志对应的占位实际不会使用 };music数组里存的是freq_tab的下标例如4表示“查freq_tab第4项得到1908us”8表示中音59表示中音6。beat数组的单位不是毫秒而是“四分音符的整数倍”1表示四分音符2表示二分音符。这样设计的好处是换曲目时不用改播放引擎只要重新写两个数组。4.3 用T1做10ms时基在后台推进曲谱方波仍然由定时器0驱动节拍推进交给定时器1。定时器1每10ms产生一次中断在中断里置一个tick_flag标志。主循环轮询这个标志标志有效时检查当前音符还剩下多少拍。#define TICK_MS 10 #define QUARTER_TICK 48 // 一个四分音符 48个tick 480ms unsigned char music_index; // 当前播放到music数组的第几个音 unsigned int beat_left; // 当前音符剩余tick数 bit tick_flag; // 10ms时基标志 void Timer1_Init(void) { TMOD 0x0F; TMOD | 0x10; // 定时器1工作方式1 TH1 (unsigned char)((0x10000UL - 10000) 8); TL1 (unsigned char)(0x10000UL - 10000); // 12MHz下定时10ms ET1 1; TR1 1; } void Timer1_ISR(void) interrupt 3 { TH1 (unsigned char)((0x10000UL - 10000) 8); TL1 (unsigned char)(0x10000UL - 10000); tick_flag 1; } void Music_Proc(void) { unsigned char note; if (!tick_flag) return; // 10ms未到直接返回 tick_flag 0; if (beat_left 0) { beat_left--; // 当前音符还没播完 return; } if (music[music_index] 0xFF) { beep_en 0; // 播放结束静音 return; } note music[music_index]; beat_left beat[music_index] * QUARTER_TICK; music_index; if (note 0) { freq_count 1000; // 休止符频率随便给关掉beep即可 beep_en 0; } else { freq_count freq_tab[note]; beep_en 1; } } void main(void) { EA 1; Timer0_Init(); Timer1_Init(); music_index 0; beat_count 0; beep_en 0; while (1) { Music_Proc(); // Key_Scan(); // Lcd_Update(); } }播放逻辑的核心是beat_left字段。每个音符进入播放状态时beat_left被赋值为该音符的总tick数之后主循环每收到一次10ms标志就减1减到0再取下一个音符。这样主循环完全不用关心当前音符是什么只需要知道“时间到了没有”。QUARTER_TICK设为48意味着一个四分音符持续480ms这是大概每分钟125拍的速度适合大多数儿歌。想加快就把宏改成40想变慢改成56。4.4 双音和和弦能用这套方案吗严格说单只无源蜂鸣器只能同时发一个基频做不了真正意义上的和弦。51单片机如果要用两只蜂鸣器播放双声部可以把定时器0分配给高音声部、定时器1的10ms时基改为产生第二路方波但定时器1同时承担节拍校准和频率输出会比较勉强。更常见的做法是只保留旋律低音声部拆成独立音轨、用软件分时切换播放听感上接近“伴奏”。这个进阶点留到第5章末尾再提。5. 非阻塞播放、音符断音与Proteus仿真无声排查5.1 从死等改成状态机播放音乐不占主循环第4章的Music_Proc已经实现了非阻塞播放但很多网上教程还在用“每个音符delay几百毫秒”的串行写法。那种写法有个致命问题播放期间按键无响应、数码管亮度抖动、串口数据丢包。改进方向是把播放流程改成一个状态机主循环每轮调用一次Music_Proc函数内部根据tick_flag决定是否推进状态。while (1) { Music_Proc(); Key_Scan(); // 播放期间按键照常扫描 Lcd_Update(); // 显示屏照常刷新 }只要Music_Proc内部没有任何死循环和阻塞延时主循环的实时性就有保障。51单片机的RAM虽然小但这一步的设计思路和ARM裸机上的非阻塞驱动完全一致值得在课程设计报告里专门写一段。5.2 音符之间加一个短静音解决“黏音”问题直接连续切换音符时前一个音的余振会拖到下一个音里节奏密集时听起来像“连音线”旋律发黏。解决手段是每个音符切换后先插入23个tick的静音模拟钢琴演奏中“抬指”的瞬间。修改Music_Proc在取新音符前强制静音2030ms。#define GAP_TICK 2 // 断音长度2个tick等于20ms unsigned char gap_count; if (gap_count 0) { gap_count--; beep_en 0; return; } if (beat_left 0) { // 取新音符... gap_count GAP_TICK; }GAP_TICK取2即可让同音连续出现时听得清每个音的分界。如果播放的是连奏风格的慢曲把GAP_TICK改成1或0。这个参数对听感影响很大录制演示视频时值得多试几个值。5.3 Proteus仿真蜂鸣器没声音按波形定位Proteus仿真是51单片机学习的高频场景蜂鸣器没声音往往集中在三个原因。第一器件选型错误。仿真库里的BUZZER大多模拟有源蜂鸣器输入直流才发声换成SOUNDEWR或SPEAKER后无源模型才能被方波驱动。具体操作是删除当前器件在元件搜索框输入“SOUNDER”重新放置。第二晶振频率设置不匹配。仿真模型默认晶振可能是4MHz而程序按12MHz计算定时初值导致频率低了三倍。双击单片机把Crystal Frequency改为12MHz。第三程序里beep_en根本没有置1。用Proteus左侧的虚拟示波器OSCILLOSCOPE接P1.5引脚如果有方波但蜂鸣器不响问题在蜂鸣器模型没有方波问题在定时器初始化或中断使能。有波形无声优先换SOUNDER模型这是我在仿真中遇到的最常见情况。没波形而程序确认烧录成功检查EA和ET0是否都置1再检查TMOD的低4位有没有被其他代码意外改写。把示波器挂在P1.5上看波形这步做扎实5分钟就能定位比反复下载改参数快得多。本文还有配套的精品资源点击获取