
简介一份基于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 的工作方式 116 位定时器从初值 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)C4261.6191163625261.6D4293.7170363833293.6E4329.6151764019329.6F4349.2143264104349.2G4392.0127664260391.8A4440.0113664400440.1B4493.9101264524494.1C5523.395664580523.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 的下标。键位与音符的对应关系如下表矩阵键盘编号音符矩阵键盘编号音符0C44G41D45A42E46B43F47C5剩余编号 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; // 定时器016位定时方式 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 或 STC89C52RCProteus 仿真里用 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 再听一次连发密度和触发时机的差异立刻就能感知。本文还有配套的精品资源点击获取