简介:一份基于STC90C51单片机的简易电子琴课程设计报告,适合单片机入门者、电子设计课程学生及制作小型音乐类项目的开发者参考。资源围绕电子琴系统的完整设计与调试展开,先给出总体方案,再详细拆解4×6扫描矩阵键盘、LED音符显示、发声模块与5V电源等硬件电路;软件部分则覆盖主程序、定时计数实现不同频率方波、音乐自动播放与录音回放模块,并附有软硬件调试方法和课程设计体会。文档完整地展示了从低音到高音共21个音符的发音与显示逻辑,也兼顾功能键的扩展操作,能够帮助读者系统理解单片机产生音乐的原理。压缩包内为1个docx文档,大小约184KB,内容自成体系,可作为课程设计报告模板、项目参考或答辩备查材料直接使用。该资源已有232人学习下载,对需要快速上手51单片机音乐类项目、补充完整设计文档的读者很有价值。
1. 基于51单片机的简易电子琴,先别急着写代码
把8个按键排列成 Do Re Mi Fa Sol La Si Do,按下一个出对应音高,这个需求听起来再简单不过。真正动手时你会发现,51单片机简易电子琴把三件独立的事压在一起:矩阵键盘怎么把“哪颗键被按下”翻译成编号,定时器怎么把编号换算成方波周期,蜂鸣器又怎么把这种方波变成耳朵能分辨的音高。任何一个环节对不上,结果就是从“有声音但跑调”到“彻底不响”。
这个项目适合正在做课程设计的人,也适合想验证自己是不是真搞懂了51单片机定时器原理的读者。它不涉及复杂外设,只靠 P1 口的矩阵键盘、一路定时器中断和一根 IO 翻转,就构成一个完整的“输入到输出”闭环。本文沿着一套常见做法展开:发声原理和定时器初值计算放在前面,矩阵键盘扫描和按键长按发声放中间,最后落到 Keil5 工程、Proteus 仿真和频率验证上。下面这套参数和代码,可以直接复现。
2. 简易电子琴发声原理与51单片机定时器初值计算
2.1 有源蜂鸣器还是无源蜂鸣器:决定你能否控制音高
电子琴的“琴声”在简易硬件方案里就是方波。常见做法是用无源蜂鸣器,因为它内部没有震荡源,给什么频率的方波就出什么频率的声音。有源蜂鸣器通电就响,频率固定,拿来当报警器合适,做电子琴则没法改变音高。
51单片机产生方波的方式很简单:定时器按设定周期溢出,在中断里翻转一次 IO 口电平。翻转一次输出半个周期,翻转两次输出一个完整方波。所以方波频率由定时器的溢出周期决定,溢出周期又由重装初值决定。这里的关键是:初值不是拍脑袋填的,它由音符频率、晶振频率和定时器工作方式共同算出。
2.2 音符频率与定时器初值:一张表算清楚 C4 到 C5
以 12MHz 晶振为例,51单片机每个机器周期为 12 个时钟周期,即 1us。用定时器 0 的工作方式 1(16 位定时器),从初值 N 计数到 65536 溢出,溢出周期就是 (65536 - N) 微秒。方波半周期应该等于音符周期的一半,于是得到:
N = 65536 - (500000 / f)
其中 f 是音符频率,单位 Hz。以中央 C4 为例,频率 261.6Hz,周期约 3823us,半周期 1911us,初值就是 65536 - 1911 = 63625。下面这张表是 8 音符常用的 C 大调音阶初值,12MHz 晶振、定时器方式 1 直接可用。
| 音符 | 频率(Hz) | 半周期(us) | 定时器初值 | 实际输出频率(Hz) |
|---|---|---|---|---|
| C4 | 261.6 | 1911 | 63625 | 261.6 |
| D4 | 293.7 | 1703 | 63833 | 293.6 |
| E4 | 329.6 | 1517 | 64019 | 329.6 |
| F4 | 349.2 | 1432 | 64104 | 349.2 |
| G4 | 392.0 | 1276 | 64260 | 391.8 |
| A4 | 440.0 | 1136 | 64400 | 440.1 |
| B4 | 493.9 | 1012 | 64524 | 494.1 |
| C5 | 523.3 | 956 | 64580 | 523.0 |
实际输出频率和标准频率的误差全部小于 0.3Hz,人耳基本分辨不出来。你见到的其他版本表格初值和这张表差 1 到 3 个数,属于取整方式和半周期四舍五入的差异,不影响听感。
在代码里,音阶初值一般直接做成查表数组,避免运行时做除法。这里给出一个可以直接嵌入工程的数组写法:
code unsigned int NoteTable[8] = { 63625, // C4 261.6Hz 63833, // D4 293.7Hz 64019, // E4 329.6Hz 64104, // F4 349.2Hz 64260, // G4 392.0Hz 64400, // A4 440.0Hz 64524, // B4 493.9Hz 64580 // C5 523.3Hz };code关键字把数组放到程序存储区,不占用宝贵的片内 RAM。查表比每次按键后重新计算初值快得多,也能避免在主循环里做乘除法导致按键响应变慢。
2.3 晶振换成 11.0592MHz 时必须重算初值
很多开发板为了串口通信稳定,板载 11.0592MHz 晶振。这时机器周期变成 12 / 11.0592 ≈ 1.085us,上面表格里的全部初值都要改。拿 A4 举例,半周期 1136us,除以 1.085 后约 1047us,初值是 65536 - 1047 = 64489,和 12MHz 下的 64400 差了 89。这个差距反映到频率上约差 2.7Hz,单听一个音不明显,但和前一节音符连起来时拍会明显不对。
所以别直接抄网上的程序到自己的板子上。先确认板子晶振,再看代码注释里写的是多少。常见做法是在程序开头用宏统一替换:
#define FOSC 11059200UL // 机器周期(us) = 12 / (FOSC / 1000000),重装值 = 65536 - (500000 / f) * 12 * 1000000 / FOSC用宏保留计算关系,换晶振时只改一处。这个习惯在后续做电子时钟、红外解码等项目时能少踩很多坑。
3. 矩阵键盘8音符扫描与按键长按发声的驱动程序
3.1 行列反转扫键法:8个IO读16个键的常用方案
8 个音符如果用独立按键,需要占 8 个 IO,加上蜂鸣器和数码管,P0/P1/P2 会非常紧张。矩阵键盘 4x4 只用 8 个 IO 就能覆盖 16 个键位,留给后续扩展音阶或功能键的余地更大。
行列反转扫键法的思路分两步:第一步,列线全部输出低电平,读行线;如果某一行读到低电平,说明该行有按键,但还不知道是哪一列。第二步,行线全部输出低电平,读列线;哪一列读到低,就确定了列号。行列组合唯一对应一个按键。
unsigned char Key_Scan(void) { unsigned char key_value = 0xFF; P1 = 0x0F; // 高四位(P1.4~P1.7)输出低,低四位(P1.0~P1.3)读输入 if ((P1 & 0x0F) == 0x0F) // 低四位全是1,说明没有按键按下 return 0xFF; Delay10ms(); // 软件消抖,等待电平稳定 P1 = 0x0F; switch (P1 & 0x0F) // 再次读行线,判断是哪一行 { case 0x0E: key_value = 0; break; // P1.0 被拉低 -> 第0行 case 0x0D: key_value = 1; break; // P1.1 被拉低 -> 第1行 case 0x0B: key_value = 2; break; // P1.2 被拉低 -> 第2行 case 0x07: key_value = 3; break; // P1.3 被拉低 -> 第3行 default: return 0xFF; // 干扰或弹跳,丢弃本次扫描 } P1 = 0xF0; // 低四位输出低,高四位读输入 switch (P1 & 0xF0) // 判断是哪一列 { case 0xE0: key_value += 0; break; // 第0列 case 0xD0: key_value += 4; break; // 第1列 case 0xB0: key_value += 8; break; // 第2列 case 0x70: key_value += 12; break; // 第3列 default: return 0xFF; } return key_value; // 返回 0~15 的矩阵编号 }P1 = 0x0F时,高四位输出低电平,低四位置 1 作为输入。51 单片机的 P1 口是准双向口,读引脚前要先写 1,这是新手最容易忽略的一个细节。消抖延时放在检测到按下之后、判断具体行列之前,确保读取到的是稳定电平,而不是机械触点弹跳产生的毛刺。
Delay10ms 在 12MHz 晶振下的常见写法是一组嵌套空循环:
void Delay10ms(void) { unsigned char i = 20, j = 113; do { while (--j); } while (--i); }这个延时不要求精确到微秒,按键消抖给 5~20ms 都能工作。如果后面发现按键偶尔连响,就加大循环次数;如果按键响应迟钝,就减小。
3.2 键位到音符的映射:为什么只取前8个键
扫描函数返回 0~15 的编号,而音符表只有 8 个元素。常见做法是约定矩阵键盘的前两行对应音阶,也就是返回值 0~7 直接作为 NoteTable 的下标。键位与音符的对应关系如下表:
| 矩阵键盘编号 | 音符 | 矩阵键盘编号 | 音符 |
|---|---|---|---|
| 0 | C4 | 4 | G4 |
| 1 | D4 | 5 | A4 |
| 2 | E4 | 6 | B4 |
| 3 | F4 | 7 | C5 |
剩余编号 8~15 暂时不用,后续可以映射成高八度、低八度、延音或节拍切换。如果不想按这个顺序排按键,就在扫描返回值和音表下标之间加一张映射数组,例如unsigned char key_to_note[16] = {0,1,2,3,4,5,6,7,...};,这样硬件布线可以更自由,不用让代码迁就排线。
3.3 按键长按发声:按下出声、抬起停止的主循环逻辑
“按键长按发声”在简易电子琴里最直观的理解是:按键按下期间,蜂鸣器持续发出该音符;松开立刻停止。这不需要检测“长按了多长时间”,只需要主循环反复扫描键盘状态,按下就开定时器,松开就关定时器。主程序的核心逻辑如下:
sbit Buzzer = P2^0; void main(void) { unsigned char key; TMOD = 0x01; // 定时器0,16位定时方式 ET0 = 1; // 允许定时器0中断 EA = 1; // 开总中断 Buzzer = 0; // 初始不发声 while (1) { key = Key_Scan(); // 读取当前按键,0xFF表示无按键 if (key < 8) // 命中0~7号音符键 { TH0 = NoteTable[key] >> 8; // 取初值高8位 TL0 = NoteTable[key] & 0xFF; // 取初值低8位 TR0 = 1; // 启动定时器,开始发声 } else { TR0 = 0; // 停止定时器输出 Buzzer = 0; // 确保蜂鸣器停在低电平 } } }主循环每执行一次就去扫一次键盘,只要手指没松开,key就一直是有效值,定时器持续工作;一松开,下一次扫描返回 0xFF,定时器立刻关闭。这个方案的关键在于:扫描频率要足够快,否则松开手指后声音会多拖一小段。Key_Scan 加上消抖延时约 10~20ms,实际响应速度对弹奏来说完全够用。
定时器中断里只做一件事:重装初值并翻转蜂鸣器电平。这保证方波周期只由 NoteTable 里的初值决定,不依赖主循环的执行速度。
void Timer0_ISR(void) interrupt 1 { TH0 = NoteTable[current_key] >> 8; TL0 = NoteTable[current_key] & 0xFF; Buzzer = !Buzzer; }这里current_key是全局变量,用来在中断里拿到当前音符的初值。更常见的做法是在主循环里先把初值存入两个全局变量,中断里直接装载,避免中断函数里做数组索引。两种都能正常工作,后者在定时器频率较高时执行时间更短。
4. Keil5工程与Proteus仿真电路:让51单片机简易电子琴跑起来
4.1 Keil5工程里的三个必要配置
在 Keil5 里新建工程的步骤不复杂,但三个配置漏掉任何一个,程序都可能无法在 Proteus 里运行。第一,芯片型号选 AT89C52 或 STC89C52RC,Proteus 仿真里用 AT89C52 最常见,两者寄存器完全兼容。第二,Output 选项卡里勾选 Create HEX File,否则编译产物是 axf 文件,Proteus 的芯片属性里没法加载。第三,在 C/C++ 选项卡把晶振频率匹配好,仿真工程里用的晶振要和代码里假设的一致。
编译看到0 Error(s), 0 Warning(s)之后,在工程目录的 Objects 文件夹里就能找到 hex 文件。把这个文件完整路径填进 Proteus 单片机属性的 Program File 一栏,仿真电路才会真正跑起来。
4.2 Proteus仿真电路图接线:矩阵键盘与蜂鸣器驱动
Proteus 里搭建电路时,最容易出问题的是蜂鸣器选型。常见做法是用库里的 SOUNDER 当作无源蜂鸣器,它需要外部方波驱动;如果选了 BUZZER,内部自带震荡源,给直流电平就会响,反而验证不了定时器输出的频率。实际硬件上,无源蜂鸣器不能直接接在单片机 IO 和 GND 之间,IO 口驱动能力不够,需要加一个 NPN 三极管或 ULN2003 驱动。仿真验证时可以直接把 P2.0 接在蜂鸣器驱动端。
| P1/P2引脚 | 连接对象 | 说明 |
|---|---|---|
| P1.0 ~ P1.3 | 矩阵键盘行线 R1 ~ R4 | 对应 Key_Scan 里的低四位 |
| P1.4 ~ P1.7 | 矩阵键盘列线 C1 ~ C4 | 对应 Key_Scan 里的高四位 |
| P2.0 | 三极管基极或 SOUNDER 输入 | 方波输出 |
| VCC 5V | 蜂鸣器正极 | 通过集电极或驱动芯片接入 |
| GND | 矩阵键盘行列公共端 | 必须和单片机共地 |
矩阵键盘的公共端在 Proteus 里就是四行四列交叉点的引出线,不需要额外上拉电阻,51 单片机端口内部有上拉,但仿真时如果出现按键检测不到,可以在 P1 口加 10k 排阻排除电平不确定的问题。
4.3 完整程序骨架:初始化、扫描、发声三件事
把前面几节的代码拼在一起,就是一个可以完整编译的工程骨架。头文件、引脚定义、延时、扫描函数照抄前文,主程序和中断如下:
#include <reg52.h> code unsigned int NoteTable[8] = { 63625, 63833, 64019, 64104, 64260, 64400, 64524, 64580 }; sbit Buzzer = P2^0; unsigned char current_key = 0; void Timer0_ISR(void) interrupt 1 { TH0 = NoteTable[current_key] >> 8; TL0 = NoteTable[current_key] & 0xFF; Buzzer = !Buzzer; } void main(void) { unsigned char key; TMOD = 0x01; ET0 = 1; EA = 1; Buzzer = 0; TR0 = 0; while (1) { key = Key_Scan(); if (key < 8) { current_key = key; TH0 = NoteTable[key] >> 8; TL0 = NoteTable[key] & 0xFF; TR0 = 1; } else { TR0 = 0; Buzzer = 0; } } }current_key在主循环里更新,中断里只读取并装载。注意先赋值current_key再更新 TH0/TL0,确保中断发生时使用的初值和当前按下的键一致。TR0 的开关控制方波是否输出,这样可以在不改变中断使能状态的情况下快速静音。
4.4 仿真不响的排查顺序
如果 Proteus 里按下按键没有任何反应,按下面的顺序查最快。先看单片机有没有运行:晶振频率设置对不对,电源电压有没有加上。再看蜂鸣器模型:BUZZER 不需要方波,SOUNDER 必须靠方波驱动,选错模型会得到完全相反的结论。然后查定时器有没有启动:在中断函数里给一个 P2^1 翻转,如果 P2^1 没有波形,说明中断根本没进去,优先查 TMOD 和 EA/ET0 配置。最后确认 hex 文件路径有没有选对,Proteus 加载旧文件时经常出现“程序改了半天仿真没变”的情况,重新选一次文件并重启仿真就能解决。
5. 校准音符与扩展长按连发的两个实用技巧
5.1 Proteus 里用虚拟示波器验证实际输出频率
调试跑调问题不能靠耳朵猜。Proteus 左侧工具栏里找到虚拟仪器,放置一个 OSCILLOSCOPE,把通道 A 接到 P2.0。运行仿真后按下一个音符键,示波器上会显示一串方波。用光标测量两个相邻上升沿之间的时间差,这个值就是方波周期,频率等于 1 除以周期。比如测到周期约 3.82ms,对应 C4 的 261.6Hz,说明定时器初值正确。如果测到明显偏差,回到 2.2 节按实际晶振重算。这里给一个快速换算命令参考:
# 以周期 3.823ms 为例,计算频率 awk 'BEGIN {printf "%.1f Hz\n", 1000 / 3.823}'输出 261.6 Hz,与理论值一致。每一颗键都按一遍,把示波器读数记下来,和前面的表格对比,误差不超过 1Hz 就算合格。
5.2 主循环里加一个计数器实现长按自动连发
如果需求里的“长按发声”指的是按下一段时间后自动连续重复触发某个音符,可以在主循环里加一个软件计数器。每次扫到同一有效键就加一,计数值超过阈值后重新装载初值并翻转一个触发标志,实现类似电子琴延音轮播的效果。
unsigned int key_hold_cnt = 0; // 主循环按键处理部分 if (key < 8) { key_hold_cnt++; if (key_hold_cnt > 30) // 阈值约 30 * 20ms = 600ms { key_hold_cnt = 0; TR0 = 0; TH0 = NoteTable[key] >> 8; TL0 = NoteTable[key] & 0xFF; TR0 = 1; } } else { key_hold_cnt = 0; TR0 = 0; }阈值 30 对应约 600ms 的长按判定,具体值取决于主循环里 Key_Scan 和消抖的总耗时。按一次扫描 20ms 计算,30 次就是 0.6 秒。把这套逻辑和 3.3 节的“按下即发声”组合起来,短按触发单音,长按进入自动连发,演示时可玩性更高。调整阈值时记住一个规律:减小key_hold_cnt > 30里的 30,连发触发得更早;增大它,则需要按更久才触发。把阈值改成 20 再听一次,连发密度和触发时机的差异立刻就能感知。
本文还有配套的精品资源,点击获取