简介:本资源是一套基于AT89C51单片机的多功能电子琴系统完整开发包,面向嵌入式初学者、单片机课程设计学生及电子类实训人员,解决从硬件仿真到软件驱动的一体化实践难题。资源共23个文件,包含4个C源文件(main.c、MatrixKey.c、LCD1602.c、Delay.c)、3个头文件(MatrixKey.h、LCD1602.h、Delay.h)、4个Keil工程相关文件(.uvproj/.uvopt等)、1个Proteus仿真项目(.pdsprj)、1个可执行hex文件及README.md说明文档等,压缩包仅96KB,轻量易解压,结构清晰便于模块化学习与调试。已有58人下载学习,适合开展单片机定时器音调生成、矩阵键盘扫描、LCD1602动态显示、LED音符指示及独立按键功能扩展等核心实验。读者可直接导入Keil与Proteus运行仿真,完整复现键盘演奏、#音切换、LCD实时曲名/音符显示、蜂鸣器发声与LED同步反馈等全部功能,配套代码注释详尽,逻辑分层明确,是理解C语言在51单片机中工程化应用的典型范例。 如果你在电子信息类专业的课设清单里看到“基于AT89C51的多功能电子琴”这个题目,不用怀疑,这几乎是每个学单片机的人都要过一遍的经典项目。项目名字看起来长,拆开其实就三件事:用C语言写程序,用Proteus搭电路做仿真,以AT89C51为控制核心实现一台能弹奏、能自动播放音乐的电子琴。这篇文章就围绕这个组合,把从需求拆分、硬件设计到软件实现、仿真排查的完整过程捋一遍,适合正在做单片机课程设计的学生,也适合第一次接触Proteus仿真但又不想直接焊板子的人参考。
这类项目最大的价值不是“电子琴”本身,而是它把单片机最核心的几个知识点全部串起来了:I/O口读写、矩阵键盘扫描、定时器中断、查表法、数码管动态显示。你把这套逻辑吃透,后面再做温度采集、信号发生器、简易示波器之类的课设,基本就是换汤不换药。
1. 项目需求拆解与整体功能规划
1.1 “多功能”到底包含哪些功能
拿到题目之后,先别急着打开Proteus画电路。第一步是把“多功能电子琴”这句话翻译成具体的功能清单。我在帮学生审课设的时候,见过太多人看着题目发呆,半天憋不出一个功能点。这里我给你一个参考方案,依据是课程设计任务书里最常见的要求,也结合了我自己做这类项目时的习惯:
| 功能模块 | 具体说明 | 实现难度 |
|---|---|---|
| 手动弹奏模式 | 按下矩阵键盘上对应的琴键,蜂鸣器发出对应音阶的声音 | 低 |
| 内置乐曲自动播放 | 预置若干首歌曲,可自动循环播放,如《小星星》《生日快乐》 | 中 |
| 曲目切换 | 支持上一曲、下一曲切换,数码管显示当前曲目编号 | 中 |
| 播放控制 | 支持开始、暂停、继续,复位后回到待机状态 | 中 |
| 数码管显示 | 弹奏时显示当前音符,播放时显示曲目编号 | 低 |
如果你想把项目做得更有竞争力,可以再加一个速度切换功能:按一次“速度+”,播放速度提高一档,适合做演示时向老师展示“多功能”的定义。我见过有的同学还加了音量调节,但用蜂鸣器做音量控制比较受限,通常是用PWM调占空比或者加电位器衰减,仿真里做起来性价比不高,不建议作为核心功能。
1.2 为什么选AT89C51加Proteus这套方案
先说AT89C51。这颗芯片是Intel 8051内核的经典衍生型号,4KB Flash、128字节RAM、两个16位定时器、4组8位I/O口。放到现在看性能确实不够看,但它有个很无解的优势:教科书里讲的、网上的例程、老师上课举的例子,百分之八九十都是围绕51内核展开的。你做课设最怕的不是功能难,而是遇到问题搜不到答案。选AT89C51,意味着你踩过的坑十有八九早就有人踩过并且写了解决方案。
再说Proteus。这个软件最大的好处就是“先仿真,后实物”。它不像Keil那样只是个编译器,而是把原理图绘制、SPICE仿真、单片机运行全部集成到了一起。你可以直接在电路里放置一个AT89C51元件,加载编译好的hex文件,点运行就看到单片机真实地执行程序。和实物开发板相比,Proteus不需要焊接、不需要担心烧坏芯片、可以在线修改电路参数,对课设阶段来说效率和容错率高得多。
当然这套方案也有边界。Proteus仿真终究是“模拟环境”,它不会真实反映引脚驱动能力不足、信号反射、电源噪声这类硬件问题。比如蜂鸣器在三极管驱动下实际带负载的效果,就必须靠实物验证。所以我的建议是:仿真通过以后,如果时间和经费允许,再花几十块钱买一块最小系统板和蜂鸣器模块,把实物跑通,这个项目的说服力会完全不一样。
1.3 系统整体架构与工作流程
整个系统可以拆成五个模块:最小系统电路、矩阵键盘输入电路、蜂鸣器发声电路、数码管显示电路、电源和复位电路。输入设备是4×4矩阵键盘,输出设备是无源蜂鸣器和两位数码管,核心控制是AT89C51。
程序运行的主流程是:上电后先完成系统初始化,包括定时器配置、I/O口初始状态、显示初始界面。然后主循环里不断做三件事:第一,扫描矩阵键盘,判断有没有按键按下;第二,根据按键执行对应动作,比如设置当前音调、切换曲目、暂停播放;第三,刷新数码管显示。蜂鸣器的发声是由定时器中断驱动的,也就是说发声这件事不占用主循环的时间,这样设计的好处是键盘扫描和数码管刷新不会因为发声而卡顿。
这里有一个理念我想提前强调:把“发声”和“控制”分开。很多新手写电子琴程序,喜欢在按下按键后用延时函数控制发声时间,松开时关断,代码看起来简单,但一旦要同时处理数码管显示和自动播放,就会各种卡死。用定时器中断驱动发声,主循环只管扫描按键和刷新显示,整个程序的逻辑会清爽很多。
2. 硬件电路设计与Proteus仿真搭建
2.1 最小系统电路:时钟、复位、电源一个都不能少
很多人从网上下载现成的Proteus工程,打开以后发现单片机啥都没接也能跑,就以为复位电路和晶振是可有可无的。这是仿真环境给学生造成的最大错觉。Proteus里的单片机模型内部默认已经有一个时钟源,所以即使不画晶振也能运行。但课程设计报告里如果只放一个孤零零的单片机,答辩时老师大概率会问:你的时钟电路在哪?复位电路在哪?
所以正确做法是:在Proteus里把完整的最小系统画出来。AT89C51的X1和X2引脚接一个12MHz晶振,两边各接一个30pF左右的电容到地。复位电路用经典的RC复位:复位引脚接一个10μF电解电容到VCC,再接一个10kΩ电阻到地。这个电路在按下复位按键时,RST引脚获得高电平,单片机复位;松开后电容通过电阻放电,RST恢复低电平,程序从头开始运行。
还有一个很容易被忽略的引脚:EA/VPP(第31脚)。AT89C51读取的是内部程序存储器,所以EA必须接高电平。如果你在仿真里发现程序死活跑不起来,检查一下是不是这个引脚悬空了。P0口作为准双向口使用时,内部没有上拉电阻,如果P0要接数码管或LED,外部必须加上拉电阻。这个细节我在2.4节单独讲。
2.2 矩阵按键输入电路设计
4×4矩阵键盘的原理,说白了就是用8根I/O口线实现16个按键的识别。4根线做行线,4根线做列线,每个按键跨接在一条行线和一条列线之间。按下某个键时,对应的行线和列线导通,单片机通过“逐行拉低,读列电平”的方式判断是哪个按键被按下。
具体到AT89C51,我习惯用P1口的低四位做行输出,高四位做列输入。行线接P1.0到P1.3,列线接P1.4到P1.7。行输出方式是把所有行线先清零,然后逐行拉低;列线则读取电平变化,哪一列变成低电平,说明该行该列的按键被按下。
这里有个经验:51单片机P1口内部有上拉电阻,所以矩阵键盘的列线在Proteus里不接外部上拉也能正常读到高电平。但如果你用的端口是P0口,就必须外接上拉电阻。另外,按键按下时电平是跳变的,机械触点会有大约5到10毫秒的抖动,程序里如果不做消抖,会出现按一次键识别成两三次的情况,这个在3.3节里我给出了具体处理办法。
2.3 蜂鸣器发声电路与驱动原理
蜂鸣器分有源和无源两种。有源蜂鸣器内部自带振荡源,只要通电就会响,但它只能发出单一频率的声音,音量还特别难控制,电子琴里绝对不能用它。我们要用的是无源蜂鸣器,它内部没有振荡源,必须由外部提供一定频率的方波才能发声。方波的频率决定了音调的高低,51单片机定时器中断的作用就是产生这个方波。
Proteus元件库里搜索“BUZZER”,放置的默认器件就是无源蜂鸣器模型。如果你放了之后发现它不响,先别怀疑代码,检查一下是不是误用了“BUZZER”的符号,或者是否忘记给蜂鸣器接驱动电路。单片机I/O口的驱动能力比较弱,拉电流也就几毫安,直接驱动蜂鸣器会出现声音小甚至不响的问题。正确的做法是加一个NPN三极管做电流放大。
我常用的驱动电路是这样:蜂鸣器一端接VCC,另一端接三极管的集电极,三极管的发射极接地,基极通过一个1kΩ电阻连接到单片机引脚。当单片机引脚输出高电平时,三极管导通,蜂鸣器通电发声;引脚输出低电平时,三极管截止,蜂鸣器断电。在Proteus里用2N3904这个型号,实测没有问题。如果用在实物上,建议在蜂鸣器两端反向并联一个续流二极管,防止断电瞬间的感应电动势损坏三极管。
2.4 数码管显示电路与动态扫描
数码管我选的是两位共阴数码管,型号在Proteus里搜“7SEG-MPX2-CC”就行。两位一体的数码管好处是引脚少、仿真连线简洁,适合电子琴这类只需要显示曲目编号或音符的应用。如果显示信息更多,可以换成四位的“7SEG-MPX4-CC”,原理是一样的。
共阴数码管的段码规则是:公共端接地,某段接高电平就点亮。0到9的段码表是固定的,我直接给出:
code unsigned char seg_table[] = { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 };这个表的意思是:段码0x3F对应二进制0011 1111,也就是a、b、c、d、e、f段全亮,g段灭,正好显示数字0。如果你想把小数点也点亮,把段码的最高位置1就行。
两位数码管用动态扫描方式驱动:位选端分别接P2.0和P2.1,段选端接P0口,P0口要接一个排阻上拉到VCC。所谓动态扫描,就是先让第一位显示某个数字,延时1到2毫秒,然后关断第一位、打开第二位显示另一个数字,再延时1到2毫秒,如此循环。因为人眼有视觉暂留效应,只要刷新频率够快,看起来两个数码管是同时亮的。这个刷新任务放在主循环里执行,不能被延时函数阻塞,否则会明显闪烁。
2.5 Proteus仿真环境的搭建步骤
新建Proteus工程后,左侧工具栏点“P”打开元件库,把需要的元件都搜出来。常用搜索词我列一下:AT89C51、BUTTON、BUZZER、7SEG-MPX2-CC、RESPACK(排阻)、CRYSTAL、CAP-ELEC、RES。如果你用的Proteus版本是8.x,元件库名称基本一致;如果是更老的7.x,部分元件名称有差异,可以用“7SEG-MPX2-CC”这个关键词搜不到就换“7SEG-MPX2-CA”看极性。
连线完成后,双击单片机,把Clock Frequency改成12MHz。这个步骤很容易被忽略,因为Proteus默认可能是1MHz,会导致后面定时器时间计算全乱。然后是编译程序生成hex文件:在Keil里打开工程,点“Options for Target”,在Output选项卡勾选“Create HEX File”,重新编译后,工程目录下就会生成一个.hex文件。回到Proteus,双击单片机,在Program File一栏选择这个hex文件,点运行。
如果一切正常,按下矩阵键盘对应的按键,蜂鸣器应该发出对应音调的声音,数码管同步显示。如果你做到这一步,说明整个系统的硬件通路已经打通,接下来要做的就是把软件逻辑一点点填进去。
3. C语言软件实现与核心代码解析
3.1 定时器初值计算:从频率到计数器初值
电子琴发声的核心是“让蜂鸣器引脚输出指定频率的方波”。以中音1(频率523Hz)为例,一个方波周期是1/523秒,约等于1.9毫秒。单片机要做的,就是让蜂鸣器引脚每0.95毫秒翻转一次电平。这个0.95毫秒的时间,就由定时器来控制。
AT89C51使用12MHz晶振时,机器周期是12个时钟周期,也就是1微秒。定时器每计数一次,耗时1微秒。要定时0.95毫秒,也就是950微秒,需要计数950次。16位定时器最大计数65536,所以初值 = 65536 - 950 = 64586。写成通用公式就是:
初值 = 65536 - (500000 / 频率)
这个公式推导很简单:一个周期翻转两次,所以定时时间 = 1 / (2 × 频率)。12MHz晶振下每个计数是1微秒,所以计数值 = 定时时间(微秒) = 1000000 / (2 × 频率) = 500000 / 频率。
我把常用音阶的频率和定时器初值列成一张表,方便直接查用:
| 音名 | 简谱 | 频率(Hz) | 定时初值(十进制) | 定时初值(十六进制) |
|---|---|---|---|---|
| C5 | 1 | 523 | 64584 | 0xFC48 |
| D5 | 2 | 587 | 64684 | 0xFCAC |
| E5 | 3 | 659 | 64777 | 0xFD09 |
| F5 | 4 | 698 | 64824 | 0xFD38 |
| G5 | 5 | 784 | 64896 | 0xFD80 |
| A5 | 6 | 880 | 64968 | 0xFDC8 |
| B5 | 7 | 988 | 65030 | 0xFE06 |
| C6 | 高音1 | 1047 | 65058 | 0xFE22 |
这里要说明一下,这张表用的是十二平均律的近似频率,工程上用完全没问题。你不需要在51单片机上用math.h去算对数函数,那会严重拖慢运行速度,直接查表是最符合嵌入式开发习惯的写法。
3.2 音符频率表与曲谱编码设计
在C语言里,把频率初值表用code关键字定义在程序存储区,不占用宝贵的128字节RAM。这样写:
code unsigned int tone_table[] = { 64584, 64684, 64777, 64824, 64896, 64968, 65030, 65058 };这个数组的下标0对应简谱1,下标1对应简谱2,以此类推。程序里只要用一个音符编号去索引,就能拿到对应的定时器初值。
曲谱编码的核心思想,是把一首歌拆成“音高”和“节拍”两个序列。以《小星星》为例,简谱是:1 1 5 5 6 6 5 — | 4 4 3 3 2 2 1 —。我可以用两个数组来存:
code unsigned char music_star_note[] = { 1, 1, 5, 5, 6, 6, 5, 0, 4, 4, 3, 3, 2, 2, 1, 0 }; code unsigned char music_star_beat[] = { 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1 };音符数组里我用了0表示休止,节拍数组里1表示一个基本节拍单位,2表示两拍。播放时按顺序取出一个音符和对应的节拍,先把蜂鸣器按该音符的频率发声,持续“节拍×基本单位时间”,然后把蜂鸣器关掉。这里0音符的作用是让音乐有呼吸感,如果没有休止符,音符和音符之间会是连续的嗡嗡声,听起来很糊。
这种“双数组并行”的曲谱编码方式简单直观,而且扩展性强。你要加第二首歌,只需要再定义一组note数组和beat数组,然后通过一个曲目索引去选择用哪组数组。这也是我推荐的做法,而不是把音符和节拍打包成一个结构体数组,虽然结构体更“高级”,但在51单片机上多一层间接访问反而增加了代码复杂度。
3.3 矩阵键盘扫描与消抖实现
矩阵键盘扫描是这类电子琴项目里最容易写出bug的部分。我先把扫描流程说清楚,再给核心代码。
第一步,全部行线输出低电平,读取列值。如果四列都是高电平,说明没有任何按键按下,直接返回0。如果有某一列变成低电平,说明该列有按键按下,但还不能确定是哪一行,需要进入第二步。
第二步,逐行扫描。先把第一行拉低,其他行输出高电平,读取列值;如果第一行的某列是低电平,说明按键在第一行。如果所有列都是高电平,说明按键不在第一行,再把第二行拉低,继续读列。如此循环,直到找到按键所在的行和列。
第三步,根据“行号×4+列号”换算按键编号,再通过一个映射表,把按键编号换算成音符编号或功能编号。
按键消抖的原理是:第一次检测到按键按下后,延时10毫秒,再检测一次,确认确实是按下状态,而不是机械抖动造成的一次误触发。松开时同样做10毫秒延时确认。下面是一段可以直接移植的代码:
unsigned char key_scan(void) { unsigned char row, col, key_code; unsigned char temp; // 全部行线拉低,检测是否有按键按下 P1 = 0x0F; if ((P1 & 0xF0) == 0xF0) { return 0xFF; // 无按键 } delay_ms(10); // 消抖 // 逐行扫描 for (row = 0; row < 4; row++) { P1 = ~(0x01 << row); // 将第row行拉低,其他行拉高 temp = P1; temp = temp >> 4; // 读取高四位列值 if (temp != 0x0F) { for (col = 0; col < 4; col++) { if ((temp & 0x01) == 0) { key_code = row * 4 + col; while ((P1 & 0xF0) != 0xF0); // 等待按键松开 delay_ms(10); return key_code; } temp = temp >> 1; } } } return 0xFF; }注意这段代码里有一个while等待按键松开的语句。如果没有它,主循环会反复执行按键扫描,同一个键会被识别成很多次,导致音符连发。等待松开是必须的,代价是按键松开前主循环被阻塞。如果你后续想加“长按连续改变速度”这类功能,需要把这段等待松开改成“检测到按下执行一次,然后标记按键状态,直到松开再清除标记”,这样设计更灵活,但教学阶段先按最简单的写法来。
3.4 定时器中断驱动发声与播放控制
有了音符频率表和按键扫描函数,接下来就是最核心的定时器中断部分。先配置定时器T0工作在方式1,也就是16位定时模式:
void timer0_init(void) { TMOD &= 0xF0; TMOD |= 0x01; // T0工作方式1:16位定时器 TH0 = 0xFC; // 初始初值,对应中音1 TL0 = 0x48; ET0 = 1; // 使能T0中断 EA = 1; // 开总中断 TR0 = 0; // 先不启动,需要发声时再启动 }中断服务函数里,要做的事情非常简单:翻转蜂鸣器引脚,然后重装初值。我把蜂鸣器接在P3.7引脚:
void timer0_isr(void) interrupt 1 { TH0 = timer_value >> 8; TL0 = timer_value & 0x00FF; buzzer_pin = ~buzzer_pin; // 翻转电平,产生方波 }这里我用了一个全局变量timer_value来存放当前音符对应的定时器初值。主程序要发某个音时,只需要把timer_value改成表格里的对应初值,然后启动TR0,蜂鸣器就会以对应频率持续发声。声音风格是通过音符值和节拍值控制,每次播放一个音符时先设置timer_value,再延时节拍时间,最后关闭TR0。
自动播放和手动弹奏的区别在于:手动弹奏模式下,按下一个琴键就启动TR0,松开就关闭TR0,发声时长由手指按多久决定;自动播放模式下,程序从曲谱数组里依次取出音符和节拍,每个音符持续设定的节拍时间后自动切换到下一个。
暂停功能的实现思路是:播放过程中按下暂停键,执行TR0 = 0停止发声,同时保存当前播放到的曲谱位置。继续播放时,重新设置timer_value,然后TR0 = 1,从暂停的位置继续取音符。这里有一个容易被忽略的点:暂停时最好把蜂鸣器引脚也输出低电平,否则恢复播放的一瞬间,引脚电平是随机的,会先产生一声爆音。
3.5 数码管动态显示实现
数码管动态显示放在主循环里,与发声互不干扰。我定义了一个显示刷新函数,它不关心具体显示什么数字,只负责把外部传入的两个数字实时刷到数码管上:
code unsigned char seg_table[] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F }; void display_num(unsigned char num1, unsigned char num2) { P2 = 0xFE; // 第一位选通 P0 = seg_table[num1]; delay_ms(2); P2 = 0xFD; // 第二位选通 P0 = seg_table[num2]; delay_ms(2); }每次调用会先点亮第一位,延时2毫秒,再点亮第二位,延时2毫秒。只要主循环以足够快的频率反复调用display_num,人眼就会看到两位数字稳定显示。注意每次切换位选前不需要清除段码,因为位选切换后,一位数码管的公共端被断开,即使段码没变也不会点亮。如果用了共阳数码管,位选的控制逻辑正好相反,段码表也完全不同,我建议统一用共阴。
实际使用时,你要根据当前状态决定显示什么数字。比如手动弹奏模式下,显示当前音符的编号;自动播放模式下,显示当前曲目编号(1到9)。可以在主循环里做一个状态判断:
if (mode == MODE_PLAY) { display_num(song_index / 10, song_index % 10); } else { display_num(0, current_note); }这里current_note是当前按下的音符编号,弹奏时如果按下的是中音5,这个变量就被赋值为5,松开按键后仍然保留显示。这样数码管的作用就很直观了。
4. 仿真调试、常见问题与避坑指南
4.1 典型问题排查速查表
这个项目在Proteus仿真阶段会遇到的问题,翻来覆去就那么几个。我把它们整理成了速查表,方便你照着一个个排除:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 按下按键没反应 | 端口定义错误或行列接反 | 检查按键行线、列线接到P1口的哪几个引脚,对照代码里的端口定义 |
| 按键有时灵有时不灵 | 没有消抖或消抖时间太短 | 确认消抖延时在10ms左右,不能只做一次电平判断 |
| 蜂鸣器完全不出声 | 用了有源蜂鸣器、三极管接错、引脚没输出方波 | 先用示波器看单片机引脚有没有方波;如果没有方波,查定时器配置 |
| 蜂鸣器声音很怪 | 定时器初值重装错误或频率表算错 | 用示波器测量方波频率,和预想频率对比,逐项检查初值 |
| 数码管显示乱码 | 段码表错误、位选顺序不对 | 单独写一个测试程序,让某个数字固定显示,逐步缩小问题范围 |
| 数码管闪烁严重 | 延时过长、刷新频率太低 | 动态扫描的每一位延时不要超过3ms,主循环里不要有长时间阻塞延时 |
| 下载hex后仿真没反应 | 没勾选Create HEX File、EA引脚悬空 | 确认Keil输出设置、确认EA接VCC、确认晶振频率设置为12MHz |
| 自动播放时按键无响应 | 播放函数里用了大延时阻塞 | 改用非阻塞节拍控制,或者把暂停功能放到外部中断里处理 |
这里面最值得展开的是“自动播放时按键无响应”。很多人的第一版代码长这样:进入播放模式后,用delay_ms(500)去控制节拍,循环里取音符、发声、延时、再取下一个音符。按下暂停键根本没反应,因为主循环卡在延时里了。解决的思路我在3.4节埋了伏笔:不要用阻塞延时控制节拍,而是用定时器或者主循环计数来产生“节拍脉冲”。
简单来说,就是维护一个全局变量beat_counter,每次进入定时器中断就加1,主循环每次检查这个值。当它达到某个阈值时,说明一个节拍的时间到了,才去取下一个音符并清零计数器。按键扫描是放在主循环里的,每次迭代都能执行到,所以即使正在播歌,按键也能及时响应。
4.2 实操心得:几个容易被忽略的细节
这个项目我做过多遍,也帮人调过不少bug,有几个细节是平时教程里很少提到的。第一个是示波器的使用。Proteus左下角工具栏里就有虚拟示波器,调试蜂鸣器发声问题时,把示波器的探头接到单片机引脚上,运行后一眼就能看到方波频率对不对。如果波形频率和预期不符,先改初值,确认波形对了再去找代码逻辑问题。很多同学一上来就“听不到声音”,结果其实是蜂鸣器模型音量没开或者电脑音量太小,用示波器看波形就能立刻排除这个因素。
第二个是切换歌曲或音符时,先关闭蜂鸣器再改变初值。如果你在播放一首歌的过程中直接切入另一首歌,旧歌的最后一个音符可能还在发声,新歌的第一个音符的初值就覆盖了timer_value,中间会产生一个短促的“咔”声。处理办法是切换前先执行TR0 = 0和buzzer_pin = 0,做一个干净利落的收尾。
第三个是节拍时间的设法。我习惯以100毫秒为基本节拍单位,用宏定义#define BEAT_BASE 100控制。这样调整播放速度只需要改这一个宏,而不需要去改曲谱数组里的每一个数值。如果你要做速度切换,就把这个宏改成变量,通过按键加减,效果立竿见影。
第四个是关于矩阵键盘的端口复用。很多同学在仿真中喜欢把按键和数码管接到同一个端口上,比如都用P1口,结果扫描键盘时数码管也受干扰。我的建议是:按键专用P1口,数码管段选用P0口、位选用P2口,蜂鸣器用P3.7,各司其职,互相之间没有任何冲突。这样无论是写代码还是查问题,思路都会清晰很多。
4.3 仿真通过之后,你的文档应该怎么写
这类课程设计最后通常要交文档,很多人在程序上花了两周,写文档只花了一晚上,答辩时被老师问几句就哑火了。我的经验是,文档一定要和代码同步写,至少要把这几块内容准备好:
需求分析部分,要说清楚“多功能电子琴”包括哪些功能,功能之间的优先级是什么。方案论证部分,要交代为什么选择AT89C51、为什么用Proteus仿真、为什么用无源蜂鸣器。硬件设计部分,要画出完整原理图,每个模块单独配一张图,并解释每个元器件的作用。软件设计部分,给出主程序流程图,配合关键代码段说明定时器初值计算过程、键盘扫描算法、曲谱编码方式。仿真结果部分,用Proteus的运行截图佐证,并附上示波器测得的方波波形图。最后是总结和参考文献。
还有一个加分项:如果你实现了实物,可以把实物照片放进去,并和仿真结果做对比分析,比如“仿真中音量稳定的现象在实物中也复现了,但发现三极管基极电阻从1k降到330Ω时音量明显提升”。这样的对比内容比任何空洞的总结都有说服力。
这个项目做完之后,我个人最大的感受是:单片机课设选对题目等于成功了一半。电子琴这个题目看似简单,实际上把51单片机最核心的IO操作、定时器中断、查表法、状态机思想全串在了一起。你把它做完,后面再看什么信号发生器、频率计、智能小车,很多思路都是现成的。如果时间充裕,可以在现有基础上再加一个“和弦”功能:用两个定时器分别输出两个频率的方波,同一个引脚叠加输出,按下双键时产生和弦效果。这在Proteus里也能仿真,是让项目从“良”冲到“优”的一条捷径。
本文还有配套的精品资源,点击获取