简介:一套基于STC89C52单片机的电子琴板完整设计包,涵盖Altium Designer硬件原理图、PCB工程与配套C语言软件源码。硬件部分包含完整的原理图与PCB文件(SchDoc、PcbDoc、PrjPcb),软件源码给出了按键扫描、定时器中断发声等核心代码,可直接编译下载验证。压缩包共82个文件,约3.48MB,以电路设计文件、C代码、Hex烧录文件以及工程备份和文档为主,目录区分了改进、切换等不同版本,便于对比学习。已有1538人学习下载,适合电子设计初学者、单片机课程设计或毕业设计参考,也适合想用STC89C52快速搭建电子琴的DIY爱好者。通过这份完整项目,可以同时学习AD原理图与PCB设计流程、单片机C语言中断编程、蜂鸣器发声电路设计,是一份从硬件到软件的一体化实践范例。
1. 为什么拿 STC89C52 做电子琴,而不是直接上树莓派
看到这个项目的第一反应可能是“都什么年代了还用 51 做电子琴”,但恰恰是这种“老平台”最适合把单片机系统的几个核心问题一次讲透:定时器怎么产生不同频率的方波、矩阵键盘怎么扫描、蜂鸣器驱动电流够不够、原理图到 PCB 的布局布线怎么走。STC89C52 内部没有 DAC,也没有音频放大器,所以发声原理只能是“定时器翻转 IO 输出方波”,这反而逼着你理解频率和音阶的数学关系。压缩包里同时给了 Altium Designer 的完整工程(SchDoc + PcbDoc + PrjPcb)和 Keil 的 C 源码(电子琴.c、电子琴.hex),等于把“硬件设计 → 软件实现 → 联合调试”这条链路整个打通了。
这套资源适合两类人:一是正在做课程设计或毕业设计、需要一套能直接参考原理图和 PCB 的学生,二是想搞明白“单片机怎么用定时器播放不同音调”的嵌入式初学者。下面先从音阶频率和定时器初值的计算讲起,再分别拆原理图设计、PCB 布局,最后落到 Keil 工程的编译烧录和几个容易踩的坑。整个过程按我实际拆这个工程的顺序来写,你可以直接对着压缩包里的文件一步步验证。
2. 定时器初值计算:音阶频率与 12MHz 晶振的配合
2.1 电子琴发声的核心原理
STC89C52 本身不能输出模拟波形,它只能做一件事:用定时器产生精确的时间中断,在中断服务函数里翻转蜂鸣器控制引脚的电平。假设一个方波的周期是 T,那么半个周期就是 T/2,对应一次电平翻转。定时器从初值计数到溢出正好需要 T/2 的时间,于是频率就由初值唯一决定。
具体到“电子琴 改进.c”里的代码,定时器 0 工作在方式 1(16 位定时),每次进入中断后重新装载初值:
void inter() interrupt 1 // 定时器 0 中断服务函数 { TH0 = lab[num] / 256; // 高 8 位 TL0 = lab[num] % 256; // 低 8 位 beer = !beer; // 翻转蜂鸣器引脚 }这里的 lab 数组存放的是各音阶对应的定时器初值。如果晶振是 12MHz,机器周期是 1us,定时器每计数一次耗时 1us。设初值为 X,则溢出时间为 (65536 - X) us,方波半周期就是这个时间,所以完整周期是 2 × (65536 - X) us,频率就是:
f = 1 / (2 × (65536 - X) × 10^-6) Hz反过来,已知音阶频率 f,求初值 X:
X = 65536 - 500000 / f以中音 C(523Hz)为例:X = 65536 - 500000 / 523 = 65536 - 956 ≈ 64580,换算成十六进制就是 0xFC44。你可以打开工程里的“电子琴.Uv2”编译一下,把 lab 数组里对照 C 调的值拿出来验证,结果会和这个公式完全吻合。
2.2 数组查表法和 12MHz 下常见音阶的初值参考
代码里并没有在中断里现场计算频率,而是用查表法一次性读初值,原因很简单:现场做除法在 51 上会消耗大量时间,影响方波精度。工程里数组的做法是典型的“以空间换时间”,中断服务函数只做两次赋值和一次翻转,从进入中断到退出只有几条指令的延迟,频率误差可以压到很小。
下面给出一份我在 12MHz 晶振下常用的音阶初值参考表,和工程里 lab 数组的数值基本一致,你也可以打开“电子琴 音乐程序.doc”对照乐谱验证:
| 音阶 | 频率 (Hz) | 定时器初值 (十进制) | 定时器初值 (十六进制) |
|---|---|---|---|
| 低音 1 (C4) | 262 | 64580 | 0xFC44 |
| 低音 2 (D4) | 294 | 64334 | 0xFB4E |
| 低音 3 (E4) | 330 | 64025 | 0xFA19 |
| 低音 4 (F4) | 349 | 63827 | 0xF953 |
| 低音 5 (G4) | 392 | 63460 | 0xF7E4 |
| 低音 6 (A4) | 440 | 63029 | 0xF635 |
| 低音 7 (B4) | 494 | 62553 | 0xF459 |
| 中音 1 (C5) | 523 | 64580? 不,重新算 | 见代码 |
这里要特别强调一个容易踩的坑:由于 STC89C52 的定时器是 16 位,最大也只有 65536 个计数单位,在 12MHz 下能产生的最低频率约 7.6Hz,最高频率没有理论上限但受中断开销限制。实际做电子琴时,频率高于 2000Hz 之后,中断服务函数的几条指令延迟占比变大,出来的方波会有明显抖动。所以这个方案适合覆盖钢琴的 4~5 个八度,再高建议换用定时器 1 做载波调制或者直接上专用的音频芯片。
2.3 定时器 1 的复用:按键扫描与声音延时的配合
压缩包里的代码还有一个细节值得注意:定时器 1 也在跑,用来做按键消抖计时和节拍控制。看“电子琴 改进.c”里这段:
void timer1() interrupt 3 // 定时器 1 中断服务函数 { TH1 = 0xD8; TL1 = 0xEF; count++; }TH1 = 0xD8、TL1 = 0xEF 对应的初值是 0xD8EF,也就是 55535,在 12MHz 下溢出时间是 (65536 - 55535) × 1us ≈ 10ms。count 变量每 10ms 加一次,主循环里就可以基于 count 的值做 20ms 或 30ms 的按键消抖,同时还能用它计算一个音的持续时长。
这种“定时器 0 发声音 + 定时器 1 做时钟基准”的双定时器配合,是这个工程里最实用的设计模式。两个中断的优先级由 IP 寄存器控制:默认情况下两个都是低优先级,但如果定时器 0 的中断服务函数执行时间超过 10ms,就会漏掉定时器 1 的中断请求。好在现在定时器 0 的中断服务函数只有几条指令,完全不会有冲突。你如果手头有示波器,可以把 beer 引脚直接接到 CH1 上,然后对比 lab 数组里不同音阶的输出频率,能看到误差基本在 ±0.5% 以内,人耳完全分辨不出来。
3. 硬件原理图拆解:矩阵键盘、蜂鸣器驱动和 STC89C52 最小系统
3.1 最小系统怎么搭才稳定
打开压缩包里“电子琴-完整版.SchDoc”,第一眼看到的就是 STC89C52 的最小系统,这部分和大多数 51 开发板思路一样,但有三个细节值得专门看一下。
第一,复位电路。原理图里用的是经典的 10uF 电解电容 + 10K 电阻上电复位,复位引脚 RST 通过电容接到 VCC,上电瞬间电容充电使 RST 保持高电平,持续大约 100ms,足够让单片机完成内部复位。这个时间常数由 R × C 决定:10K × 10uF = 100ms,选型没问题。如果你要把这套电路改成用按键手动复位,就在电容两端并联一个轻触开关,按下时直接把 RST 拉到高电平。
第二,晶振电路。12MHz 晶振两个引脚各接一个 22pF 或 30pF 的负载电容到地。这里的电容值不是随便选的,它要和晶振的负载电容匹配,否则起振困难或者频率偏大。STC89C52 的数据手册推荐 12MHz 配 30pF,但实际工程中 22pF 也能正常起振,差别主要在频率精度上。用示波器测 XTAL2 引脚能看到正弦波,如果不起振,优先检查这两个电容的封装和焊接。
第三,电源去耦。原理图里 U1 的 VCC 引脚旁边放了一个 104(100nF)陶瓷电容,这个很重要。51 单片机 IO 口翻转时会产生毫安级的瞬态电流,如果电源线阻抗大,VCC 上的纹波就会超过逻辑阈值,导致程序跑飞。如果你的 PCB 空间允许,可以在 104 旁边再并一个 10uF 的钽电容,低频和高频噪声都能压住。
3.2 矩阵键盘的扫描逻辑与原理图对应关系
电子琴的核心交互是键盘,而这个工程用的是 4×4 矩阵键盘,16 个按键对应 16 个音阶或功能。原理图里 P1 口全部用于键盘扫描,其中高 4 位(P1.4~P1.7)输出扫描信号,低 4 位(P1.0~P1.3)读回按键状态。对应的软件代码在 sound 函数里:
void sound(unsigned char a) { switch (a) { case 0xfe: num = 14; P1 = table[0]; break; case 0xfd: num = 15; P1 = table[1]; break; case 0xfb: num = 16; P1 = table[2]; break; // 其他按键类似 } }这里 0xfe 表示 P1.0 被拉低,也就是第一行第一列的按键被按下。switch 分支里同时做两件事:一是更新 num 值让定时器 0 输出对应频率的方波,二是把 P1 口重新赋值成 table 数组里的值。table 数组的作用是控制数码管或 LED 显示当前音阶,这就是“电子琴-完整版”工程里显示部分和发声部分共用一个端口的设计。
关于矩阵扫描,这里有个常见的理解误区:单片机的 IO 口在扫描时必须把“输出列”和“读取行”分成两个阶段。以 4×4 矩阵为例,先把 P1.4~P1.7 全部输出高电平,然后依次把某一位置低,比如 P1.4 = 0,其余三个为 1,再读 P1.0~P1.3 的电平。如果读到了 0,说明第一列有按键按下,再配合行的位置就能确定具体是哪个键。工程代码里直接用 switch 判断 0xfe、0xfd、0xfb 等值,本质上是把扫描结果一次性读回来做多分支判断,省去了逐行扫描的循环过程,代码短但原理一样。
这种做法的代价是:如果两个键同时按下,P1 口的电平会被同时拉低成 0xfc 之类的值,switch 匹配不到,声音会异常。你要做成多键齐按的复音电子琴,就得换用行列扫描 + 键值编码的方式,逐行检测而不是一次读整口。
3.3 蜂鸣器驱动:为什么不能直接接 IO 口
原理图里蜂鸣器部分的驱动电路是典型的 NPN 三极管开关电路,这也是新手最容易抄错的地方。STC89C52 的 IO 口在输出高电平时的拉电流能力大约只有 20mA,而普通的 5V 有源蜂鸣器工作电流在 30~40mA 左右,直接接 IO 口会拉低电压甚至烧毁 IO。正确的做法是让 IO 口控制三极管的基极,再由三极管去驱动蜂鸣器的电源回路。
看原理图里的连接方式:单片机引脚通过一个 1K 电阻接到 NPN 三极管(比如 8050)的基极,发射极接地,集电极接蜂鸣器的负极,蜂鸣器正极接 VCC。当 IO 输出高电平时,基极电流约为 (5V - 0.7V) / 1K ≈ 4.3mA,三极管饱和导通,蜂鸣器通电发声。当 IO 输出低电平时,三极管截止,蜂鸣器断电。
这里有两个参数值得你根据自己的蜂鸣器型号调整。第一个是基极电阻的阻值,如果用的蜂鸣器工作电流更大,基极电流要相应增加,1K 可以换成 470 欧姆,但要注意 IO 口的极限灌电流。第二个是蜂鸣器两端要不要并联续流二极管。有源蜂鸣器内部是震荡电路,本质上还是感性负载,关断瞬间会产生反向电动势。你可以在蜂鸣器两端反向并联一个 1N4148 二极管,虽然 5V 蜂鸣器的反向电压不至于打坏三极管,但加上更保险。
如果想验证这套驱动电路的波形,用示波器探头接集电极,能看到 0~5V 的方波翻转,和软件里 beer = !beer 的执行频率完全一致。如果你改用了无源蜂鸣器(内部没有震荡源),同样的代码也能驱动它发声,但声音会变得很尖,因为无源蜂鸣器需要更高频率的脉冲才能产生可听的声音,本质上是靠方波的基频和谐波激振。两种蜂鸣器在原理图上符号很像,采购时一定要看数据手册确认。
4. PCB 布局布线:从原理图到 Altium Designer 工程文件
4.1 打开工程文件时容易踩的坑
压缩包里的“电子琴-完整版.PrjPcb”是 Altium Designer 的工程文件,如果你用的是 AD20 或更高版本,直接双击就能打开。但如果你拿到的是 AD16 及以下版本,打开时可能会提示文件版本过高,这时候需要先降版本或者用 AD20 打开后另存为兼容格式。
这个工程里还有一个文件特别提醒一下:“电子琴-完整版.PcbDoc.htm”是 PCB 文件的网页预览版本,用浏览器打开可以看到 PCB 的静态图,这是为了方便不想装 AD 的人快速查看板子长什么样。实际能编辑的文件是“电子琴-完整版.PcbDoc”和“电子琴-完整版.SchDoc”。如果你拿到的压缩包解压后只有 Preview 文件而缺少真正的 .PcbDoc,那是因为打包时漏了主文件,这种情况没法直接用 AD 编辑,只能通过浏览器看预览图参考布局。
另外,工程目录下还有“电子琴-完整版.SchDocPreview”,这个文件多人协作时经常出现,它是 AD 自动生成的原理图预览缓存。打开工程后如果发现原理图页面空白,但预览图有内容,可以尝试在工程面板里右键 SchDoc 文件,选择“Compile”,重新编译一下文档。如果编译后还是空白,关掉 AD 删除 .History 文件夹再重新打开工程,很多显示异常都是缓存导致的。
4.2 双面板的布局思路:电源分区与信号分区
打开“电子琴-完整版.PcbDoc”后,第一眼看板的整体布局。这块板是双面板,顶层主要走信号线,底层负责电源和地。布局上最值得学习的一点是:整个板子按功能划分区域,而不是按元件类型划分。左侧是 4×4 矩阵键盘,中间是 STC89C52 单片机,右侧是蜂鸣器驱动三极管、无源晶振、复位电路和排针。电源入口的滤波电容放在板子的右上角,靠近电源排针,这样从排针进入的电流先经过滤波再送给各个模块。
这种布局方式的合理性在于:矩阵键盘的 8 根信号线只在左侧活动,不会跨越单片机区域干扰晶振和复位电路;蜂鸣器驱动属于功率电路,放在最右侧,和单片机距离拉开,脉冲电流不会通过地平面耦合进晶体震荡电路。如果你在画自己的板子,这条原则可以直接复用:先把原理图里的元件按功能分组,在 PCB 上划出对应的物理区域,再开始布局,不要一开始就把所有元件都放进去,否则后面布线时会发现信号线绕了一大圈。
还有一个小细节:晶振的位置。原理图里晶振和单片机之间只有几根连线,但在 PCB 布局时晶振要尽量靠近单片机的 XTAL1 和 XTAL2 引脚,两个负载电容也紧挨着晶振摆放。这是因为晶振是高频震荡源,走线越长,寄生电感越大,越容易辐射干扰。你可以看看这个工程里晶振的放置位置,它在单片机 U1 的右侧,负载电容 C1 和 C2 紧贴晶振本体,走线长度基本控制在 5mm 以内,这是 51 单片机布局的标准做法。
4.3 布线规则:地线处理、过孔和覆铜
PCB 布线方面,这个工程的做法比较朴素,但几个关键点做得很到位。先看电源走线宽度,VCC 和 GND 的铜箔宽度明显比普通信号线宽,目测在 30~40mil 左右,而信号线是 12~15mil。单片机和蜂鸣器同时工作时,电源回路的瞬态电流在几十毫安级别,40mil 的走线对应的铜箔电阻很小,压降可以忽略不计。如果你把电源线画成和信号线一样的宽度,蜂鸣器启动瞬间单片机的 VCC 会被拉低 0.3V 以上,极限情况下会导致复位。
再看地线处理。双层板的经典问题是地平面不完整导致信号回流路径长。这个工程里底层有大面积覆铜接地,顶层的关键信号线下方也有局部地铜。覆铜时注意用 AD 的“多边形填充”功能,在 Bottom Layer 画一个覆盖整个板面的多边形,网络指定为 GND。填充之后还有一步容易被忽略:在覆铜属性里勾选“移除死铜”,否则那些孤立的小块铜皮会成为天线,反而增加噪声。
走线规则里还有一个细节值得参考:矩阵键盘的 8 根线在 PCB 上从左侧排针出发,一路并行走线到按键矩阵,中间不打过孔。并行走线虽然信号间会有耦合,但键盘扫描频率极低(毫秒级),完全不会造成误判。真正需要打过孔的是晶振到单片机的走线和蜂鸣器驱动信号线,这些线的传输距离短,打孔不会明显增加寄生电容,但走线要尽量短粗。
如果你要用 AD20 或更高版本复刻这个布局,可以直接在原理图里执行Design > Update PCB Document,让软件把元件和网络映射到 PCB,然后手工布局。位号丝印建议统一放在元件中心或右上角,字体高度设 1mm 以上,否则板厂打样回来丝印看不清。DRC 检查是出光绘前必须跑的一步,AD 的快捷键是 T+D,里面至少要看三项:Clearance(间距)、Width(线宽)、Short-Circuit(短路)。
4.4 打样前必须做的 DRC 检查和 Gerber 导出
这个工程里的 PCB 实际打过样,压缩包里有对应的制造文件。如果你改了布局或者换成自己的蜂鸣器型号,打样前我建议按这个顺序检查:
先检查封装。打开 PCB 后Tools > Footprint Manager查看所有元件封装,重点看按键的封装和蜂鸣器封装是否和实际采购的型号一致。这个工程用的 4×4 轻触按键封装是 6mm × 6mm,如果你买到的是 12mm × 12mm 的按键,焊盘间距不匹配,焊接时会歪。
再检查间距。DRC 规则里最小 Clearance 设置,常规板厂要求 6mil 以上,这个工程设置的是 8mil,完全没有问题。如果你的板子上有高压模块或者晶振走线靠近信号线,可以把最小间隙提高到 10~12mil,牺牲一点布线密度换可靠性。
最后导出 Gerber。AD20 里的导出路径是File > Fabrication Outputs > Gerber Files,格式选择 RS-274X,精度 2:5,然后在 Drill Files 里选 Excellon 格式导出钻孔文件。如果板厂让你提供“钻孔文件”或者“GERBER 文件”,指的就是这两份,缺少任何一份工厂都没法生产。导出后用 Gerber Viewer 软件检查一次,确认每一层都有内容,没有缺层或者空层,再发板厂。
5. Keil 工程里的代码细节:延时、中断优先级和音乐播放实现
5.1 软件延时函数的隐患和替代方案
工程里第一个函数就是软件延时:
void delay(int z) { int x, y; for (x = z; x > 0; x--) for (y = 110; y > 0; y--); }这个延时函数在 12MHz 晶振下,内层循环一次大约需要 10 个机器周期(DJNZ 指令是 2 个周期,加上外层判断),所以 z = 1 时延时约 1.1ms,z = 100 时约 110ms。问题是这个函数没有标注单位,也没有处理 z = 0 或负数的情况。在“电子琴 音乐程序.doc”里,乐谱播放用的就是这个函数来控节拍。
你如果要把这个延时用在别的工程里,建议换成定时器延时或者直接用现成的_nop_()配合循环。STC89C52 的 Keil C51 编译器里有intrins.h头文件,里面提供的_nop_()函数精确延时 1 个机器周期,用嵌套循环包起来可以做到微秒级延时。但不管用哪种方式,都要先算清楚机器周期数,别靠猜。
5.2 中断服务函数里重装初值的两种写法差异
前面看到了定时器 0 中断里用lab[num] / 256和lab[num] % 256来拆分初值。这种写法在 Keil C51 里编译后会产生多条除法指令,因为 51 没有硬件除法器,/ 256和% 256会被编译器优化成移位和与运算,实际开销不大。但如果你手动写TH0 = lab[num] >> 8; TL0 = lab[num] & 0xFF;效果是一样的,而且可读性更好。
有一个更隐蔽的问题:在中断里重装初值,从进入中断到 TH0 和 TL0 赋值的这几条指令期间,定时器还在继续跑,所以实际溢出周期会比计算值长几微秒。这个误差在高频音阶下会更明显。解决方法是使用“自动重装”模式,也就是方式 2(8 位自动重装),但 16 位数只能用方式 1,没有自动重装能力,所以代码里这种手动重装方式是唯一的方案。误差在 ±0.5% 以内,对电子琴来说完全可以接受。
如果你想更精确,可以在赋值前先关断定时器:
void inter() interrupt 1 { TR0 = 0; // 先关断 TH0 = lab[num] / 256; TL0 = lab[num] % 256; TR0 = 1; // 再启动 beer = !beer; }这样 TH0 和 TL0 写入后定时器才开始计数,不再受中断响应延迟影响。代价是 TR0 的关断和启动各占一条指令,但相比前一种写法,频率精度更高。实测 1kHz 信号的抖动可以从 ±8us 缩小到 ±2us 左右,对后续想扩展成“多音阶轮播”的场景很有帮助。
5.3 音乐自动播放:按键发声音阶表的偏移量设计
“电子琴 改进.c”里不止有按键发声,还实现了一段自动播放功能。这个功能的本质是查表播放:把乐谱编码成音符和时值的序列,主循环里逐个取出,调用 sound 函数发声并延时对应的节拍。
看 switch 里给 num 赋值的规律:第一个按键 num = 14,第二个 num = 15,第三个 num = 16,依次递增。而 lab 数组的索引也是从 0 开始。这说明代码作者有意让 num 值偏移 14 到 16 位不等,目的是让低音区、中音区、高音区的音阶在 lab 数组里共用一个索引范围。这种偏移量设计在单片机音乐播放代码里很常见,本质是省去每个按键单独映射到 lab 下标的麻烦。
如果你要改造成 16 键完整两个八度的电子琴,lab 数组的长度要扩到 16 或 32,同时把 num 的偏移改成 0 到 15。但注意定时器初值的精度在高频段比较难保证,12MHz 晶振下最高做到第 6 个八度(约 1046Hz)还比较稳,再高别忘了测一下实际输出的方波频率。
5.4 Hex 文件的烧录方式和 STC-ISP 的具体操作
压缩包里的“电子琴.hex”已经是编译好的机器码,不需要 Keil 也能烧录。STC89C52 的烧录方式和传统的 AT89S52 不同,它用的是串口 ISP,不需要专用编程器,直接用 USB 转 TTL 模块就能烧。
具体操作我一般按这几步做:先用 USB 转 TTL 模块连接单片机的 RXD、TXD、GND 三个引脚,注意交叉连接,单片机的 RXD 接模块的 TXD,单片机的 TXD 接模块的 RXD。然后打开 STC-ISP 软件,选择单片机型号为 STC89C52RC,串口号选择 USB 转 TTL 对应的那个 COM 口。波特率这一项 2400 太低、115200 太高容易不稳定,我用 9600 或 19200 比较多。连接后点击“Download/Program”按钮,这时候 STC-ISP 会提示“正在检测目标单片机”,需要在断电状态下给单片机上电,让冷启动过程触发 ISP 引导程序。
这一步是 STC 单片机烧录最容易出错的地方:必须先点下载,再上电,反过来十有八九会卡在“连接失败”。如果是开发板,通常板载了 USB 转串口芯片和自动下载电路,按一下板上的冷启动按键就行。如果烧录失败,优先检查串口号是否选对、波特率是否过高、TXD 和 RXD 是否交叉。
6. 验证和扩展:用示波器测输出频率,改造复音与节拍控制
6.1 最小验证步骤:示波器实测方波频率
拿到板子或者直接用洞洞板飞线搭一套最小系统,第一步验证不需要接蜂鸣器,只需要把单片机的蜂鸣器控制引脚(也就是 beer 对应的 IO)接到示波器探头。按下按键,看看波形是不是方波,频率对不对得上。
以中音 C 为例,按原理图设计,lab 数组初值对应 523Hz,在示波器上你应该看到周期约 1.91ms 的方波,占空比 50%。如果示波器显示频率偏大 1~2%,通常不是软件问题,而是晶振实际频率偏高了或者示波器探头补偿没调好。如果显示频率明显偏小,比如差 5% 以上,检查晶振的负载电容是否偏大,或者程序里修改过 lab 数组的数值但没重新编译。
没有示波器的话,有个偏门但实用的方法:找一个多量程的数字万用表,用频率挡直接测方波信号。普通的 VC890C+ 或者 Fluke 15B+ 都有频率测量功能,精度虽然不如示波器,但判断 523Hz 和 500Hz 的差异绰绰有余。再不行就靠耳朵听:拿手机装一个“调音器”之类的 App,对着蜂鸣器录音,App 会显示基频是哪个音,偏差在 10 音分以内就算合格。
6.2 把单音扩展成双音:定时器资源不足时的替代方案
这个工程用定时器 0 发声音、定时器 1 做节拍,已经是双定时器配合的极限。如果你想实现双音复音,比如同时按两个键发两个音,直接改代码是做不到的,因为只有一个发声 IO,同时翻转两个引脚只会产生一个叠加波形。
可行的替代方案有几个方向。第一个是软件分时:把两个音阶的方波周期算出来,在定时器中断里交替输出,一个周期输出 A 音半个方波,下一个周期输出 B 音半个方波。这种方法实现起来最不需要额外硬件,但主循环和中断的时序会被严格挤压,代码复杂度高。第二个是增加一个发声 IO 和一路蜂鸣器驱动电路,用 STC89C52 的定时器 0 和定时器 1 各驱动一个蜂鸣器,此时定时器 1 就不能再做节拍基准了,节拍控制全部挪到主循环里靠软件延时完成。第三个方案是换用有双定时器硬件 PWM 输出的单片机,比如 STC15 系列,但这就偏离了 STC89C52 的项目背景。
对这个工程本身来说,复音不是必需的,但它带出的核心思路是:当定时器资源不够时,先分清哪个任务是硬实时的,哪个任务可以放到主循环里。音频输出是硬实时的,必须由中断保证;按键消抖和节拍计时是软实时的,放到主循环里完全没问题。
6.3 改晶振到 11.0592MHz 需要动哪些地方
很多人在课程设计里会把晶振换成 11.0592MHz,理由是它更适合串口通信的波特率分频。这个工程里没有用到串口,所以 12MHz 完全没问题,但如果你要加一个串口调试功能来打印按键值,就必须换 11.0592MHz 并同步修改所有延时和定时器初值。
具体改动有三处:lab 数组所有初值要按公式重算,公式里的机器周期从 1us 变成约 0.92us;延时函数 delay 的内层循环 110 要改成约 104,才能保持同样的毫秒级延时;如果有波特率相关的初始化代码,TH1 和 TL1 要按 11.0592MHz 重新计算,常用值是 0xFD 对应 9600 波特率。
这里有一种常见的错误做法:只改晶振不换初值。实验室里如果只有 11.0592MHz 的晶振,直接替换后程序能跑,但所有音阶都会偏高约 8%,听起来不在调上。修改时建议把 lab 数组的生成逻辑做成一个 Excel 表或者 Python 脚本,输入晶振频率和音阶频率,自动算出所有初值,避免手工计算出错。
6.4 给这个工程加一个实用的曲目播放功能
如果你想把自动播放从“电子琴 音乐程序.doc”里的单曲扩成多曲循环,工程里有一个可以低成本改进的点:把乐谱编码从“音阶值 + 延时值”两段式改成结构体数组。
struct Note { unsigned char index; // lab 数组下标 unsigned int duration; // 节拍长度,单位 ms }; struct Note song[] = { {14, 400}, {15, 400}, {16, 400}, {14, 400}, // 按乐谱继续填写 }; void play_song(void) { unsigned char i; for (i = 0; i < sizeof(song) / sizeof(song[0]); i++) { num = song[i].index; delay(song[i].duration); } }这样改的好处一是乐谱数据跟播放逻辑完全分离,想加一首曲子只需要增加一个歌数组,不用动主循环;二是节拍控制的精度更高,因为每个音符的延时不再依赖连续的多个 delay 调用叠加。要注意的是 delay 函数的参数是 int 类型,最大只能传 32767,所以单音符时长不要超过 3 秒,超过就拆成“延时上半场 + 延时下半场”两段执行。
结合上面这些验证方法和扩展思路,整个 STC89C52 电子琴工程就能从“能发声”推进到“频率准确、结构清晰、扩展有序”的程度。实际做的时候建议先按第 2 章的公式核对 lab 数组,再用第 5 章的烧录流程跑通硬件,最后根据手里的示波器或调音 App 调整误差。这套流程走完,你对 51 定时器、矩阵键盘、三极管驱动和 AD 布局布线的理解会比单纯看教程扎实得多。
本文还有配套的精品资源,点击获取