51单片机定时器实战:电子贺卡音乐播放的设计与仿真 简介这是一份基于51单片机的电子贺卡设计文档面向单片机初学者、电子类课程设计与毕业设计学生。内容围绕AT89C51芯片展开涵盖Protel硬件原理图绘制、Proteus仿真调试以及C语言/汇编实现思路并提供音调、节拍、定时中断等关键知识点可直接用于理解歌曲播放与LCD文字同步显示的完整设计流程。压缩包内只有1个doc文档大小约4.22MB内容集中、便于离线阅读已有427人学习。文档从系统总体方案、时钟复位、按键选择、音频发生、蜂鸣器、音频放大等硬件模块讲到软件定时器频率生成与显示控制并附有音符频率、节拍延时、中断次数等参数能够帮助读者掌握电子贺卡的软硬件设计方法也为音频播放类单片机课题的复现与扩展提供了详细参考。1. 一张能唱歌的贺卡把51单片机的定时器彻底弄明白电子贺卡这题目听起来像玩具但拆开看它把51单片机最核心的三件事全占了定时器产生精确方波、单片机驱动外设蜂鸣器和LCD、按键中断响应。很多人学单片机时写流水灯很顺一到发声就糊——因为声音要求的是“在精确的时间点翻转引脚”这比单纯置高低电平苛刻得多。AT89C51工作在12MHz晶振下机器周期1us定时器T0要用方式1做16位计数初值算错一个数音高就偏半个调。更麻烦的是节拍音乐不只是频率对还要每个音持续多久这又牵扯到延时程序的中断响应时间。这篇东西把音调计算、节拍编码、硬件接法和Proteus仿真里的坑按我实际调过的顺序讲一遍适合刚做完单片机课设、想把手里的板子真正发出好听声音的人。2. 音调与节拍先算出定时器初值再谈播放2.1 十二平均律和51单片机的频率映射音乐里C、D、E、F、G、A、B对应唱名DO、RE、MI、FA、SO、LA、SI相邻音级之间频率比不是线性的而是按十二平均律每半音频率乘2的1/12次方约1.059463。比如中音A的频率是440Hz高八度的A就是880Hz低八度就是220Hz正好翻倍或减半。这个关系决定了我们查表时每高一个八度频率翻倍定时器初值对应的计数值就要减半。AT89C51用12MHz晶振时机器周期是12个时钟周期即1us。定时器T0在方式1下是16位计数器从初值N计数到65536溢出溢出周期为(65536-N)us。要产生频率f的方波需要引脚电平每半个周期翻转一次所以定时器溢出周期应为半周期时间即T_half 1 / (2 * f)那么初值N 65536 - T_half / 1us 65536 - 500000 / f。例如中音1C4频率约261.6HzN 65536 - 500000 / 261.6 ≈ 65536 - 1911 63625。注意这个公式算出来的是理论值实际听感还要考虑中断响应时间后面第5章会讲怎么修正。2.1.1 常用音阶的定时初值表下表给出基于12MHz晶振、定时器方式1的常用音调初值十六进制是为了方便写代码时直接填入TH0和TL0音名频率(Hz)计数值NTH0TL0低音1 (C3)130.8617260xF10x1E低音3 (E3)164.8625300xF40x42中音1 (C4)261.6636250xF80x89中音3 (E4)329.6640980xFA0x62中音5 (G4)392.0643600xFB0x68高音1 (C5)523.3645800xFC0x44表中初值N越大溢出周期越长频率越低这和直觉相反因为初值越接近65536计数到溢出需要的时间越长。实际写程序时我建议把这张表直接定义成常量数组而不是每次运行都算浮点51单片机做浮点除法太慢会在中断里浪费大量时间。2.2 节拍的本质是延时但不要用delay()硬扛音调决定声音的频率节拍决定这个频率持续多久。1拍的时间没有绝对标准通常以1拍1s为基准那么1/4拍就是0.25s。在一个播放程序里最自然的做法是控制T0产生指定频率的方波同时用另一个定时器T1或者主循环延时来控制这个音的持续时间。这里有个常见误区很多人写播放程序时每播放一个音符就调用delay_ms()然后全程不响应按键。结果就是按暂停键要等这首歌唱完才有反应。这个设计里要求“随时可以停止”所以节拍控制不能用阻塞式延时而要用定时器中断累加时间或者至少保证延时循环里能扫描按键状态。2.2.1 把节拍转成中断次数如果取1/4拍0.125s作为基准T0中断每次溢出周期取决于当前音符频率不确定但我们可以单独用T1做成固定125ms的时基。这样1/4拍就是1次T1中断1/2拍是2次1拍是4次2拍是8次。这样的映射关系非常干净也方便把乐谱编码成“音调拍数”的字节对。拍数T1中断次数(125ms基准)1/811/411/223/43141又1/2628你看中断次数直接等于“以1/4拍为单位的数值”乘2除了1/8拍特殊。实际编码时可以把每拍写成中断次数这样播放子程序只需要做减法计数不用再乘除。3. 硬件电路按键选歌、蜂鸣器驱动和LCD1602并行接口3.1 六个模块怎么搭在一起这套设计里的硬件分成六个部分AT89C51单片机、时钟复位电路、按键输入电路、音频发生器、蜂鸣器、音频放大器。音频发生器实际上就是单片机引脚输出的方波信号本身经过三极管放大后驱动蜂鸣器。很多人以为蜂鸣器直接接I/O口就能响其实51单片机I/O口驱动能力很弱高电平只能提供几十微安必须用一个NPN三极管常见8050做电流放大。时钟和复位电路是标准接法12MHz晶振接XTAL1和XTAL2两个20~30pF负载电容到地复位端RST接10uF电解电容到VCC10k电阻到地按键并联在电容两端实现手动复位。这部分没什么可发挥的但要注意复位时间要大于2个机器周期所以电容不得小于1uF。3.2 按键选歌电路读P1口比矩阵键盘简单设计任务要求11个按键10首歌加1个停止键。实际上更合理的是8个选歌键用P1口直接读取。键盘接法可以做成按键一端接P1.0~P1.7另一端接地平时上拉电阻保持高电平按下时读到低电平。这样在程序里直接读取P1口的低电平位就知道哪个键被按下。我一般会按下面这张表接线信号连接说明P1.0~P1.78个选歌按键按下为0P3.2/INT0暂停/停止按键外部中断触发P3.0方波输出到音频放大也是RXD复用P1.6LCD1602 RS寄存器选择P1.7LCD1602 RW读写选择P2.7LCD1602 E使能信号P3.0用作方波输出时要注意它在编程下载时是RXD但在运行阶段可以当作普通I/O口用。另外一个细节蜂鸣器发声时P3.0的方波经过4.7k电阻接8050基极集电极接蜂鸣器到VCC发射极接地。这样低电平时候三极管截止高电平时导通注意蜂鸣器是感性负载最好并联一个续流二极管否则关断瞬间的反电动势可能损坏三极管。3.3 LCD1602的并行时序要点LCD1602是16x2字符液晶工作电压5V数据线可以接P0口或P2口。但在这种方案里P0口没被占用常规接法是把DB0~DB7接P0口RS接P1.6RW接P1.7E接P2.7。P0口内部没有上拉电阻必须外接10k排阻上拉到VCC否则读不到正确数据。LCD1602的写时序很简单先设RS为高数据或低指令RW置低然后给E一个从高到低的下降沿数据线上的8位数据就被锁存进去。很多人第一次调LCD不亮除了对比度电位器没调好更多是E引脚时序不对——E的高电平持续时间至少要450ns单片机12MHz下一条指令1us所以直接用普通赋值语句就能满足但不要在赋值前多加循环否则会看起来“闪烁”或者写不进数据。4. 软件实现音符编码表、T0中断和歌曲播放主循环4.1 音符编码一个字节存音调一个字节存拍数乐谱要存进单片机需要一个紧凑的编码。常见做法是每个音符占两个字节低字节存音调索引高字节存拍数以1/8拍为单位。比如0x01表示低音10x02表示低音#10x1C表示中音1拍数0x04代表1/2拍0x08代表1拍0x10代表2拍。曲子结束用0xFF, 0xFF标志。下面的数组是一段《欢乐颂》的开头注意拍数编码// 音符编码低字节为音调索引高字节为拍数(每1/8拍为1) // 码值: 0x12表示中音1唱名do0x14表示中音30x15表示中音5 code unsigned char music_happy[] { 0x12, 0x08, 0x12, 0x08, 0x14, 0x08, 0x15, 0x08, // 3 3 5 5 0x14, 0x10, 0x13, 0x08, 0x12, 0x08, 0x11, 0x08, // 4 4 2 2 0x10, 0x10, 0x12, 0x08, 0x12, 0x08, 0x14, 0x08, // 1 1 3 3 0x15, 0x10, 0x14, 0x08, 0x12, 0x08, 0x11, 0x08, // 5 4 2 2 0x10, 0x20, // 1(两拍) 0xFF, 0xFF // 结束 };这里code关键字把数组放到程序存储区节省宝贵的128字节内部RAM。取数据时用查表指令从程序区读取不能用普通数组下标因为51的data空间只有128字节一首歌几十个字节很容易爆。读取方式是用DPTR指针每次取完两个字节后把DPTR加2。4.2 定时器T0中断翻转P3.0注意中断响应时间产生方波的核心是T0中断服务程序每次溢出就把P3.0取反然后重新装载TH0和TL0void timer0_isr() interrupt 1 { P3_0 ~P3_0; // 翻转引脚产生方波 TH0 th0_value; // 当前音符的高8位初值 TL0 tl0_value; // 当前音符的低8位初值 }这里th0_value和tl0_value是全局变量每次切换音符时由播放程序更新。中断里不要做复杂计算只做装载和翻转否则方波频率会漂移。另外T0中断服务函数的执行时间本身是额外的相当于让实际溢出周期比理论值长了几个机器周期这就是为什么实测音高总是偏低一点点。解决方案有两个一是把初值算大一点补偿中断响应时间二是把T0设为高优先级避免被其他中断打扰。这个设计里T1用作节拍计时如果不设优先级T1和T0同时请求中断时默认按自然优先级先响应T0这没问题但如果主循环里的延时较长关中断时间太久T0溢出的脉冲会丢失所以播放期间可考虑关闭其他不必要的中断。4.2.1 播放子程序怎么切换音符播放子程序的逻辑是查表拿到音调索引找到对应的定时初值启动T0再查拍数字节换算成延时时间延时结束后关闭T0继续取下一个音。伪代码如下void play_note(unsigned int interval, unsigned char beats) { // interval为溢出需要的计数次数beats为1/8拍个数 th0_value (65536 - interval) 8; tl0_value (65536 - interval) 0xFF; TR0 1; // 启动T0 delay_beats(beats); // 按节拍延时内部扫描按键 TR0 0; // 停止T0实现休止 }这里有一个容易被忽略的细节音符与音符之间要留一点静音时间否则听起来像滑音。我一般会在每次音符结束后让T0停止几个毫秒相当于一个极短的休止符。如果谱子本身就有休止符编码0x00那就不启动T0只延时节拍。4.3 节拍延时的实现要能随时响应暂停键不能粗暴地写for(i0;i100000;i);因为这样按暂停键没反应。设计里用T1中断做节拍计时每125ms进一次中断将当前音符的剩余拍数减一减到零就标志音符结束。主循环只需不断查询标志位。volatile unsigned char beat_countdown; // 剩余中断次数 volatile bit note_finished; // 当前音符是否结束 void timer1_isr() interrupt 3 { if (beat_countdown 0) { beat_countdown--; if (beat_countdown 0) note_finished 1; } }播放时把拍数转换成中断次数赋给beat_countdown然后等待note_finished变成1。在这期间主循环可以扫描按键若按下暂停就复位beat_countdown并关闭T0。这个结构让音乐播放和按键控制解耦是整套软件的关键。4.4 手持歌曲超过254字节怎么办51单片机的DPTR是16位一首歌超过254个字节不会让DPTR溢出真正的问题是有些编译器默认用8位变量存数组长度或者你用code数组索引时用了unsigned char下标超过255就归零了。解决办法是用unsigned int索引并且在播放完一段后重新设置DPTR基址。另一个更隐蔽的问题是如果歌曲数据存放在多个数组里切换歌曲时要重新指向该歌曲的数组首地址。我习惯把歌曲组织成指针数组code unsigned char *song_list[] { music_happy, music_birthday, music_ode, // ... }; code unsigned char song_count 3;选歌时根据按键编号取出对应的指针播放子程序对这个指针操作。注意code数组指针本身也占存储空间但13个指针最多26字节相比把歌谱全放在RAM里还是划算。5. Proteus仿真验证与两个必调的坑5.1 仿真时务必检查晶振频率和初始电平在Proteus里搭建AT89C51、LCD1602、蜂鸣器模型时双击单片机设置晶振频率为12MHz否则定时器初值是按12MHz算的你会在调音时发现音调完全不准。还有一点Proteus的蜂鸣器模型有源和无源要选对电子贺卡用的是无源蜂鸣器需要外部方波驱动如果选了有源蜂鸣器模型通电自己就响根本不受P3.0控制。仿真中最常见的现象是P3.0引脚有方波输出但蜂鸣器不响。先检查蜂鸣器两端是否接了电源和三极管基极电阻同时看引脚的数字波形是否为5V方波。如果波形是锯齿状说明驱动能力不够把仿真里的蜂鸣器模型阻抗调高或者加一级反相器后再驱动不过更稳妥的是在实物中用三极管放大。5.2 初值修正把中断时间算进TH0/TL0实际播放高音时T0中断服务程序大约要执行8~10个机器周期指令调用、翻转、装载这些时间都不在被定时器计数的范围内导致实际频率比理论值低。低音频段相对误差小听感不明显高音频率本身周期就短比如2000Hz时半周期250us多10us就是4%的误差已经能听出跑调。我采用的经验修正是把按公式算出的计数值再减去中断开销对应的计数值。12MHz下一个机器周期1us中断服务程序实测约8us那么初始计数值调整量约为8。也就是说N_actual N_theory - 8;更精确的做法是在Keil里用逻辑分析仪看P3.0波形测实际频率然后二分法修正初值。但仿真环境稳定修正一次后面所有音都能用同一个offset。另外T0要设优先级高避免节拍中断打断T0导致抖动Protues里中断优先级在Interrupt菜单里可调C51里用EA1; ET01; PT01;把T0设为高优先级即可。5.3 验证方法用示波器量P3.0再听音仿真调试时我一般先放一段单音比如连续播放中音1打开Proteus的虚拟示波器看P3.0波形周期是否为1/261.63.82ms左右。波形是方波且周期误差在3%以内再连歌曲播放。这比直接听声音可靠得多因为人的耳朵对绝对音高不敏感但相对音准很敏感——如果每个音都偏同一个比例听起来还是协调的只有某些音偏移比例不一致才会明显觉得怪。等歌曲能顺畅播放后再检查LCD1602是否在换歌时同步显示对应歌词。这里有个技巧LCD显示有等待时间如果音乐播放和LCD写在同一个主循环里不要用阻塞式写LCD函数否则音乐节拍会被拉长。我一般先把歌词全部放进一个二维数组切歌时先显示第一行然后后台播放等音符播到某个位置再刷新第二行这样不会卡顿。最后提一个实物制作才有的坑蜂鸣器直接并联在VCC和GND之间时启动瞬间会有冲击电流可能导致单片机复位。这时在电源两端并一个100uF电解电容再串一个10欧电阻到蜂鸣器能明显减少噪声干扰。仿真里没这个问题但做板子时务必加上。本文还有配套的精品资源点击获取