51 单片机播放《起风了》:从数字简谱到音符与节拍编码
1. 项目与实物
这是我学习 51 单片机时完成的蜂鸣器音乐实验,使用普中 A2 开发板。程序通过 Timer0 中断产生方波,用音谱数组控制音高和持续时间,启动后顺序播放一遍参考旋律,结束后停止。
基础驱动与播放框架参考江协科技(原江科大自化协)51 单片机教程。我完成的部分是将《起风了》的参考旋律整理成音符、音区与时值数据;“自己编写音谱数据”指把参考旋律转换成程序数据,不代表原创作曲。!
图:本人使用的普中 A2 开发板。本文是历史学习工程回顾,未在本次整理期间重新进行硬件播放和声音测量。
| 项目 | 平台与计算条件 |
|---|---|
| 开发板 | 普中 A2 |
| 配套主控说明 | 按课程配套的 STC89C52RC 说明,Keil 工程目标选择 AT89C52;照片未清晰读出实物芯片丝印 |
| 音高与延时计算条件 | 12 MHz 外部晶振、12T,这是原代码采用的计算条件 |
| 发声控制 | P2.5,通过板载发声电路产生声音 |
| 开发工具 | Windows、Keil µVision、C51 工具链 |
| 当前功能 | 阻塞式单曲播放,没有暂停、选曲或自动循环 |
时钟条件要与实际板子区分。普中配套手册提到 12 MHz 和 11.0592 MHz 两种晶振选择,STC89C52RC 也支持 6T/12T;本文按原代码的 12 MHz/12T 条件计算,不把照片中无法读出的参数当作实测结果。更换晶振或时钟模式后,需要重新计算音高表和延时。资料可从普中官方产品资料入口获取,芯片时钟模式见STC 官方说明。
2. 音高:改变定时器溢出间隔
每次 Timer0 中断都将控制引脚翻转一次,两次翻转才组成一个完整方波周期。要产生频率为f的音符,理想的中断间隔是1 / (2f)。
Timer0 使用 16 位模式,并在中断里手动重装计数值。在内部定时、12T、晶振频率为Fosc的条件下,理论计算为:
计数次数 N ≈ Fosc / (12 × 2 × f) 重装值 R = 65536 - N例如,原表中M1的重装值为64580,计数次数为956。在 12 MHz/12T 条件下,理论输出频率约为523 Hz;该值由代码计算得到,不是示波器实测值。手动重装、中断响应与执行开销也会造成误差。
原中断中的关键代码如下:
/*-------------------------------------------------------------- * 按当前音符重装 Timer0,再翻转蜂鸣器控制引脚。 * 节选自原工程,FreqSelect 必须在频率表有效范围内。 *-------------------------------------------------------------*/if(FreqTable[FreqSelect]){TL0=FreqTable[FreqSelect]%256;TH0=FreqTable[FreqSelect]/256;Buzzer=!Buzzer;}这里的频率表保存的是重装值,不是以 Hz 为单位的音符频率。Buzzer.c中另外保留了固定频率的发声函数,但音乐主程序实际使用的是上述中断逻辑。
3. 编谱:数字简谱转成音符—时值数据
参考图片属于数字简谱。数字表示音级,数字上方或下方的点区分音区,时值线等符号记录节奏。我将它转换成低、中、高音区宏与时值数据:L7是低音区的 7,M1是中音区的 1,H1是高音区的 1,下划线表示对应的升音。
图:编谱时使用的网络参考简谱,保留原图署名。图片由项目整理者提供并说明可以公开使用;个人贡献为音符与时值数据整理,不将参考谱面标为自己的原创作品。
程序数组按“音符索引、时值单位”成对存储,最后用0xff表示结束。下面只展示格式,不是完整曲目:
/*-------------------------------------------------------------- * 每两个元素为一个事件:音符、时值;0xff 为结束标记。 *-------------------------------------------------------------*/unsignedintcode Music[]={L7,1,M1,1,M2,1,M3,1,0xff};code是 Keil C51 的存储类型,用来将数组放在程序存储区,并非普通标准 C 的关键字,详见Keil C51 文档。
工程设置SPEED = 500,通过delay(SPEED / 4 * 时值)控制持续时间。按软件延时的设计目标,时值对应关系如下:
| 时值 | 延时目标 | 相对关系 |
|---|---|---|
| 1 | 125 ms | 一个基本时间单位 |
| 2 | 250 ms | 两个时间单位 |
| 3 | 375 ms | 三个时间单位 |
| 4 | 500 ms | 若四个单位为一拍,则是一拍 |
| 8 | 1000 ms | 按上述约定,则是两拍 |
软件延时会受到实际时钟、编译结果和发声中断影响,表中不是实测时间。原程序还会在音符后停表并延时约 5 ms,形成短间隔。
参考简谱标注1 = #F、4/4 拍、每分钟 77 拍。原程序直接使用现有频率表和SPEED = 500,不等于精确还原原谱的调号与速度:数字音级还需要映射到正确调性的绝对频率;若四个时值单位为一拍,500 ms 的目标拍长对应约 120 拍/分钟。
仓库保留了个人音符—时值笔记和编谱资料说明。笔记及当前程序各有 285 个事件,但第 189~192 个事件存在差异;本次没有修改它们,复现当前程序以mian.c的Music[]为准,不把原始笔记误当作最终播放数组。
上面的参考谱图与仓库中的个人编谱笔记配合使用,可以对照理解音区、音符顺序与时值编码;本文不为第三方曲目或谱面另行声明代码许可证。
4. 工程运行与后续改进
完整工程位于 GitHub:51 蜂鸣器音乐实验。在 Windows 上使用带 C51 工具链的 Keil 打开project.uvproj,核对硬件参数后构建并下载。主文件保留历史拼写mian.c,不要只改文件名而不更新工程引用。
历史构建日志记录过 0 个错误、3 个未调用函数警告,并生成了 HEX;它只能证明那次构建的情况,不是本次复测结果。本次没有重新构建、播放或测量音高、节拍,因此不提供未验证的精度或稳定性指标。
| 当前边界 | 后续可以做的实验 |
|---|---|
| 阻塞式播放,软件延时控制节拍 | 用定时器节拍和状态机实现非阻塞播放 |
中断与主循环共享音符索引,原代码有额外的FreqSelect++ | 整理索引更新与范围检查,关注 8 位 MCU 更新 16 位变量的一致性 |
| 休止符只是不翻转引脚 | 按实际驱动电路管理静音电平与定时器,验证休止符 |
| 音高表、延时注释和工程时钟配置需要核对 | 按真实时钟计算参数,测量方波频率与拍长 |
这个实验帮助我把简谱符号与程序数据联系起来,也将音高、节拍和播放流程区分开。后续改进会保留实验条件与测量结果,逐步完善为可复现的学习项目。