ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于单片机的简易电子琴设计与实现:音阶、定时器与矩阵键盘

基于单片机的简易电子琴设计与实现:音阶、定时器与矩阵键盘 简易电子琴这个题目在电子系统设计与创新实践这类课程项目里出镜率相当高。我最早接手的时候以为无非就是单片机驱动一个蜂鸣器响两声把音阶播出来就算完事真正做完才发现这里面的门道比想象中多得多音阶频率怎么算、定时器初值怎么定、按键怎么扫描才能不互相干扰、蜂鸣器驱动电路怎么搭才能发出像样的音色每一个环节都有讲究。这篇文章我按自己的实际经验来写从方案选型到电路设计从音阶计算到代码实现再到调试踩坑尽量把整套思路讲清楚。不管你是正在为课程设计发愁的电子类专业学生还是想用单片机做个好玩东西的爱好者都能照着这份记录一步步把琴做出来而且做完之后你对定时器、中断、矩阵键盘这些基础内容的理解会比看十遍教材都深刻。1. 项目整体设计与方案选型1.1 需求拆解一台“简单但不简陋”的电子琴先把这个项目的需求想明白。题目叫“简易电子琴”那到底要做到什么程度才算合格我给自己列了三条底线第一至少能弹一个八度也就是do re mi fa sol la si do不然只能叫蜂鸣器玩具第二按下去要发声松手要停止不能出现按一个键响半天的奇葩体验第三音调得准不能按出个do结果听着像re这种琴送给我都不要。如果还要在“创新实践”这四个字上做出点文章那就得加上一些超出“响就行”的功能比如切换音色、自动播放演示曲目、录音回放之类。这些作为加分项放在后面先把基础功能稳稳当当立起来。确定了需求后我给这个项目定了个性能指标输出16键1.5个八度的可弹奏音阶每个按键对应一个音符按下发声、松开静音整体响应延迟不超过20ms。这个响应速度用人耳听是完全感觉不到的。1.2 方案对比为什么选单片机加无源蜂鸣器做电子琴的方案有不少我身边就有同学走了完全不同路线。最传统的是用555定时器搭振荡电路通过改变电阻电容来改变频率这个方案理论可行但一个按键对应一组RC参数16个键就要16组电阻电容电路复杂不说频率精度也差调试起来能让人崩溃。还有用专用音源芯片的比如UM3567这类音乐IC外围电路简单能发出多种音效但可玩性太低固定就那么几段旋律完全谈不上“弹琴”。权衡之后我选了市面上最常见的方案STC89C52单片机加无源蜂鸣器加矩阵键盘。选择这个方案的理由很实在STC89C52在学校实验室和网上资料里铺天盖地烧录用串口就能解决不需要额外买下载器无源蜂鸣器便宜几毛钱一个只要有方波信号就能发声频率由程序完全控制想发什么音就发什么音矩阵键盘把16个按键压缩到8个IO口板上走线干净利落。这个方案的核心理念是把频率生成这个最精细的活交给程序去干硬件只负责“听话地响”。单片机定时器产生精确到微秒级的中断在中断里翻转蜂鸣器引脚电平就能生成任意频率的方波。这比用一堆电阻电容搭的振荡器稳定得多也好调得多。1.3 元器件清单几十块钱搞定全部硬件具体需要哪些东西我列个清单参考。STC89C52RC单片机一片12MHz晶振一个两个30pF瓷片电容做晶振负载电容一个10uF电解电容和10k欧电阻组成复位电路一个5V电源USB转TTL模块或者开发板供电都行无源蜂鸣器一个S8050三极管一个1k欧和10k欧电阻若干轻触按键开关16个LED加限流电阻若干万能板和若干飞线。全部加起来不超过五十块钱但能学到的东西远超这个数。这里提醒一句蜂鸣器一定要买无源的也就是那种内部不带振荡电路的封装上通常标着“无源”或者规格书里写明需要外部驱动。如果你买成了有源蜂鸣器通电就响固定音调程序写得再好也发不出不同的音高这个坑有人踩过而且踩得相当结实。2. 核心原理音阶频率是怎么算出来的2.1 十二平均律与C调音阶频率做电子琴第一件事就是把音阶对应的频率搞明白。现在乐器通用的定音体系是十二平均律把一个八度平均分成12个半音每个半音之间的频率比都是2的12次方根约等于1.059463。国际上通用标准音是A4等于440Hz也就是钢琴上中央C往右第五个白键的频率。从这个基准出发任何一个音符的频率都能通过公式算出来。我弹的是C调基准音是中央C记作C4频率261.63Hz。C4往上数几个半音每数一个半音频率乘一次1.059463这样把C大调自然音阶的七个音全部算出来D4是293.66HzE4是329.63HzF4是349.23HzG4是392.00HzA4正是440HzB4是493.88Hz再往上的高音C5是523.25Hz。这些数字不是随手写的是标准音律表里的固定值。实际做的时候不需要每次都算直接把频率表做成常量数组写进程序里就行。不过建议还是自己动手算一遍体会一下十二平均律的等比关系后面调试遇到音不对的时候你会需要理解这个体系才能找到问题在哪。2.2 定时器装载值半个周期的艺术有了目标频率还得想办法让单片机“吐”出对应频率的方波。STC89C52的定时器就像一个精度极高的计时沙漏我让蜂鸣器引脚的电平每隔半个周期翻转一次这样引脚输出的方波频率就等于目标频率。计算过程是这样的。STC89C52用的是12MHz晶振这个单片机比较有年代感12个时钟周期才是一个机器周期所以定时器每计数一次实际耗时12除以12MHz等于1微秒。一个周期内要翻转两次电平所以定时器的定时时长应该是目标频率的半周期也就是1除以2倍频率。把这个时长换算成微秒再换成定时器需要计数的个数最终装载值等于65536减去计数个数。拿标准音A4来举例频率440Hz半周期是1除以880约等于1136微秒换算成定时器计数就是1136个那么定时器初值就是65536减1136等于64400换算成十六进制就是0xFB90。把这个值写进TH0和TL0定时器中断每次触发就翻转一次蜂鸣器引脚A4这个音就出来了。找规律的话频率越高半周期越短定时器初值离65536越远频率越低初值越靠近65536。这个反比关系要记清楚调试的时候最容易搞反的地方就在这里。2.3 节拍与发声时长控制频率问题解决后还有一个旋律层面的问题一个音要响多久电子琴和自动播放音乐还不太一样电子琴是弹奏者按下按键的时长决定音符长度松开就停自动播放则需要程序里设定每个音符的持续时间。电子琴模式的做法很简单主循环里不断扫描按键检测到某个键被按住就开启定时器发声检测到松开就关闭定时器停止发声。这样音符长短完全由演奏者控制程序代码量小逻辑也直白。自动播放模式就得引入节拍概念。我在程序里定义了四分音符时长为500ms那么一个四分音符就是500ms八分音符是250ms二分音符是1000ms。播放的时候先根据乐谱查表得到音符频率再根据时值确定发音时长到点后切换到下一个音符。音与音之间最好留5到10ms的间隙不然连续弹奏时音符之间会黏在一起听起来糊成一片。3. 硬件设计与实现细节3.1 单片机最小系统稳定运行的底座别小看最小系统这三个字很多琴做出来不响或者跑飞问题恰恰出在这个最基础的地方。最小系统包含三块电源、晶振、复位。电源部分要在单片机VCC和GND之间并一个100nF瓷片电容做去耦大电流波动时能稳住电压这个小电容经常被人省略然后就会遇到莫名其妙的重启问题。晶振用12MHz两个负载电容一端接晶振引脚另一端接地这两个电容的作用是让晶振起振更可靠、频率更稳定。复位电路用经典的10uF电解电容加10k电阻上电瞬间RESET引脚保持高电平等电容充电结束后复位释放单片机开始正常运行。连线原则有一条要牢记晶振和复位电路尽量靠近单片机对应引脚地线回路要短粗。面包板或者万能板上飞线又长又乱的时候晶振容易受干扰导致频率漂移会有音调忽高忽低的怪问题出现。3.2 按键矩阵用8个IO口搞定16个键16个按键如果用独立按键接法每个键占一个IO口总共要16个STC89C52的P0和P1加起来刚好够用但IO口全被按键占满了LED和蜂鸣器就没地方接了。所以矩阵键盘几乎是必然选择。矩阵键盘的原理是把16个按键排成4行4列的网格每个按键连接在某一行和某一列的交叉点上。扫描的时候先把4个行线全部输出高电平再逐行拉低每拉低一行就读取4个列线的电平状态。如果某个按键被按下它所在的行和列就接通了列线就能读到低电平于是行列坐标一结合这个按键就被准确定位了。实际程序里我用P1口的高四位当行线输出低四位当列线输入扫描一轮16个键只需要检测4次列线状态非常高效。这里有个细节列线要接上拉电阻默认状态是高电平不然按键没按下时列线悬空读到的电平随机跳会造成一堆假按键触发。3.3 蜂鸣器驱动不能省略的三极管很多初学者直接把蜂鸣器一脚接单片机IO另一脚接地结果发现声音小得跟蚊子一样甚至干脆不响。原因是单片机的IO口驱动能力有限常规模式下最多输出几毫安的电流根本推不动蜂鸣器这种需要几十毫安的负载。正确做法是加一个NPN三极管做开关放大。我用的S8050电路接法是这样单片机IO口经1k电阻接到三极管基极蜂鸣器一端接5V电源另一端接三极管集电极发射极直接接地。当IO口输出高电平时基极有电流流过三极管导通蜂鸣器通电发声IO口输出低电平时三极管截止蜂鸣器断电停止。还要特别提醒反向电动势的问题。蜂鸣器本质上是感性器件尤其是电磁式的电流突变时会产生反向电动势搞不好会击穿三极管。保险起见我在蜂鸣器两端反向并联一个1N4148二极管让反向电流有个泄放回路。这一步教科书上写得清清楚楚但每次都有同学忘记加烧掉三极管后才想起来。3.4 LED指示与整体布局加几个LED能让作品看起来更有诚意。我给每个琴键对应的IO口复用一个LED按下按键发声的同时点亮LED松开就熄灭起到按键反馈的作用。矩阵键盘的IO口本来就复用了这里LED再接在行线上扫描时行线刚好有电平变化顺带就把LED点亮了不额外占用资源。硬件布局方面我把按键按照钢琴键盘的黑白键排列方式焊成两排白键长、黑键短这样外观更像一台真正的小琴。面板上用记号笔标上音名C D E F G A B方便演示和演奏。电源入口处还加了一个自锁开关方便随时断电做实验的时候不用频繁拔线。这个细节很小但能明显提升使用体验。4. 软件逻辑与代码实现4.1 按键扫描状态机告别延时消抖机械按键都有一个物理特性叫抖动按下和松开的过程中电平会在几毫秒内反复跳变好几次。很多新手用延时消抖按下检测到低电平就delay几十毫秒这在单个按键场景下能用但用在电子琴上完全不行整个主循环被延时卡住定时器中断虽然还能跑但按键响应变得迟钝快速弹奏时音符跟手指完全对不上。我在这个项目里用的是状态机扫描法。每个按键有三种状态释放态、可能按下态、按下态。主循环每隔几毫秒扫描一次按键检测到按下信号后不立即确认而是等第二次扫描仍然是按下状态才判定为有效触发。两次扫描中间隔了足够的时间抖动造成的毛刺信号会被自然过滤掉同时主循环不会陷入任何阻塞延时按键响应依然实时。这个思路本质上是用时间换稳定主循环跑得飞快但只有当某个状态持续存在时才做响应既解决了抖动又不牺牲响应速度。4.2 发声控制按下响、松开停发声控制的逻辑不复杂我把它拆成三个清晰的任务扫描按键、更新输出、产生方波。扫描按键在键盘状态机里完成输出更新在主循环里做方波产生完全交给定时器中断。具体流程是主循环先扫描键盘得到当前按下的键号如果键号和上一次不同说明发生了新的按键事件就根据键号从音符频率表里查到对应的定时器初值写入TH0和TL0然后启动定时器。同时把对应的LED点亮。如果扫描结果是没有按键按下并且定时器还在运行就关闭定时器熄灭LED。这样一个音符在松开手指后最多几十毫秒内就会停止弹奏手感是跟手的。这里有个关键点定时器中断回调里只做一件事翻转蜂鸣器IO口电平其他什么都不做。千万不能在中断里写一堆判断逻辑或者调用延时函数那样会严重拉长中断处理时间造成频率不准。中断函数越短越好这是写单片机程序的一条铁律。4.3 完整代码框架C语言Keil工程这里我贴出核心代码骨架用了STC89C52的标准寄存器定义在Keil C51环境下直接可以编译。头文件用的reg52.h比较通用。#include reg52.h sbit BEEP P2^0; // 蜂鸣器驱动引脚 sbit ROW1 P1^4; // 矩阵键盘行线 sbit ROW2 P1^5; sbit ROW3 P1^6; sbit ROW4 P1^7; #define COL_IN P1 // P1低4位为列线输入 // C调音阶定时器初值表12MHz晶振方式1 // 计算方式: 65536 - 500000 / 频率 // 0~7为C4到C58~15为D5高半音等扩展 unsigned int code tone_table[] { 63625, // C4 261.63Hz 63833, // D4 293.66Hz 64019, // E4 329.63Hz 64104, // F4 349.23Hz 64260, // G4 392.00Hz 64400, // A4 440.00Hz 64524, // B4 493.88Hz 64580, // C5 523.25Hz }; unsigned char key_state[4][4] {0}; // 按键状态矩阵 unsigned char current_tone 0xFF; // 当前发声的音符索引 bit tone_on 0; // 是否正在发声 void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x01; // 定时器0方式116位 ET0 1; // 开启定时器0中断 EA 1; // 开启总中断 } void beep_on(unsigned char tone_index) { TR0 0; TH0 tone_table[tone_index] 8; TL0 tone_table[tone_index] 0xFF; TR0 1; tone_on 1; current_tone tone_index; } void beep_off(void) { TR0 0; BEEP 0; tone_on 0; current_tone 0xFF; } // 定时器0中断服务函数翻转蜂鸣器引脚 void Timer0_ISR(void) interrupt 1 { TH0 tone_table[current_tone] 8; TL0 tone_table[current_tone] 0xFF; BEEP !BEEP; } // 矩阵键盘扫描返回按键编号-1表示无按键 signed char key_scan(void) { unsigned char row, col; for (row 0; row 4; row) { P1 0x7F; // 先全高再拉低对应行 switch (row) { case 0: ROW1 0; break; case 1: ROW2 0; break; case 2: ROW3 0; break; case 3: ROW4 0; break; } for (col 0; col 4; col) { if (!(COL_IN (0x01 col))) { return row * 4 col; } } } return -1; } void main(void) { Timer0_Init(); while (1) { signed char key key_scan(); if (key 0) { beep_on((unsigned char)key % 8); // 映射到音阶 } else { beep_off(); } } }这段代码把核心逻辑浓缩得很简单主循环扫描按键按下就开定时器翻转蜂鸣器松开就关定时器。实际做的时候建议把按键扫描改成状态机版本这里为了演示发声控制的基本骨架用了最直白的判断方式。有一点要注意tone_table里的音阶索引只有8个而矩阵键盘有16个键后者可以映射到两个八度这就要把第二个八度的频率值也补进表里。原理一模一样就是继续用十二平均律公式算出高一个八度的频率值比如C6是1046.5Hz。5. 调试实录与常见问题排查5.1 蜂鸣器不响或声音刺耳先说蜂鸣器不响的问题。第一步用万用表量蜂鸣器两端电压按下按键时应该有电压变化如果没有说明三极管没导通检查基极电阻和IO口输出。更基础的一步是拿一节电池直接碰一下蜂鸣器引脚能听到“咔哒”声说明蜂鸣器本身是好的。还有一种情况无源蜂鸣器必须要方波驱动才响哪怕是手动碰一下引脚产生个跳变它也只会“咔”一声如果程序里只给它输出一个固定高电平它是不出声的先确认定时器有没有工作。声音刺耳大多数时候是频率不对。刺耳的高频声往往来自定时器初值算错或者中断里没及时更新初值导致输出频率远高于目标。还有一种情况是中断抢占主循环主循环刚把蜂鸣器引脚改成一个状态中断立刻又翻转回来出现了高频毛刺声。解决办法是按我前面说的蜂鸣器引脚只在中断服务函数里翻转主循环别碰它权利职责要分开。5.2 音调不对从晶振到定时器的全面检查弹琴弹出来每个音都偏高或偏低先别急着改程序量一下晶振频率。我遇到过同学因为焊的时候把晶振旁边的负载电容虚焊了实际振荡频率偏了好多程序里按12MHz算好的初值实际执行根本不是这个频率音当然不准。晶振没有问题的话再看定时器重装初值的代码。中断函数里要手动重新赋TH0和TL0的初值如果漏了定时器从0开始继续计数整个定时周期变成了65536个计数单位不管什么音都会变成一个极低的频率琴出来的全是“嗡嗡”声而不是清脆的音阶。还有一个很容易被忽略的因素中断服务函数里的代码执行时间。如果中断里写的东西太多每条指令都要花去若干个机器周期等效的定时周期被拉长实际发声频率就会比算出来的低。我们前面说过中断函数尽量短正是为了把这个误差压到最小。5.3 按键失灵、连音和跳音按下一个键却响出两个音或者快速弹奏时丢音这是矩阵键盘扫描最经典的问题。先检查列线有没有加上拉电阻没有的话悬空引脚拾取环境噪声程序会误判成按下状态。然后是扫描速度的问题主循环转得太快同一个按键可能被循环多次读到每次读到的状态变化都被当成新事件弹一个键可能触发两三次鸣响。解决方式就是前面说的状态机消抖加事件检测只有按键状态从“未按下”变成“已按下”才触发发声持续按住只维持当前状态不重复触发。跳音往往是矩阵键盘行和列接反了。行线输出0扫描列线读输入如果接线时把行和列的概念搞混了程序里拉低的行线实际接的是列线引脚读到的电平状态全乱。这种时候别急着改程序代码先用万用表把每根线的实际连接点测一遍画个接线图比对着程序排查要快得多。5.4 常见问题速查表现象可能原因排查步骤所有按键都不响蜂鸣器驱动电路故障用电池直接测蜂鸣器检查三极管基极电压万用表测IO口是否有方波输出单个按键不响矩阵键盘该按键接触不良用万用表通断档测按键检查对应行列走线是否断路音调偏高或偏低晶振频率不准确用示波器或频率计测晶振脚实际频率检查负载电容是否焊好音调闷沉不清脆定时器初值被覆盖或中断耗时过长检查中断里是否有被主循环改掉的变量精简中断函数代码快速弹奏丢音扫描逻辑中延时阻塞去掉扫描中的delay改用状态机消抖确认定时器中断始终开启声音断续有杂音电源供电不足增加去耦电容USB供电电压不稳时换独立5V适配器这张表基本覆盖了我做这个项目过程中遇到的大部分离谱问题。建议打印出来贴在实验台旁边遇到问题先对着表排除一半可能性再深入定位剩下的一半。6. 创新加分项与扩展方向6.1 低成本实现的音色切换和自动演示如果只是把按键连通蜂鸣器发声课程项目可能只算勉强及格。想要在“创新实践”里拿高分我建议加一对音色切换和自动演奏功能。音色切换的实现思路不复杂改变方波的占空比。同样的频率下50%占空比的声音听起来比较硬朗30%占空比的声音会柔和一些通过按键切换时把中断里的翻转策略从单纯的50%方波改成带死区的脉冲输出就能模拟出两种不同的音色观感。这个效果成本为零只改代码就能做出来对没有硬音源芯片的项目来说是性价比最高的差异化方案。自动演示功能也好实现预先在程序里用数组存好一段旋律比如《小星星》或者《欢乐颂》每个音存两个信息频率索引和时值。播放时用一个计数器读取数组按照时值设定音符持续时长到点后切换到下一个音符。这段程序不难但演示效果极其拉风老师来检查的时候你按一下演示键琴自己就叮叮当当地演奏起完整的曲子比单纯弹几个单音有说服力得多。6.2 加显示、加录音、加无线控制扩展思路其实非常多只要IO口和程序空间还有余量。加一个LCD1602显示屏把当前按下的音名实时显示出来可以直接看出弹的是哪个音这个对调试音准非常有帮助。录音回放功能需要一块存储芯片比如AT24C02或者直接利用单片机内部EEPROM按下录音键后把音符序列和时间间隔存起来再按回放键逐条读出音符间隔用定时器精确计时回放出来效果相当不错。如果是想走无线方向可以加一块蓝牙串口模块把按键扫描的结果通过串口发给手机手机端用一个简单的音乐App播放大提琴或钢琴音色这样手机就变成了音源单片机变成了琴键玩起来完全是另一个维度。不过对课程项目来说这个方向可能有点超纲了更适合作为个人兴趣再深挖。6.3 一点扩展思路无论走哪个方向我建议都遵循一个原则新功能从简单到复杂逐步加每加一个功能就完整测试一遍不要攒一堆改动到最后一起调。我见过太多同学把所有扩展一口气写完结果一编译几十个报错根本不知道从哪下手排查。功能迭代开发每一步都能跑你才有信心继续往下走。做这个项目的整个过程中我最深的体会是所谓“简易电子琴”重点不在“电子琴”而在“简易”这两个字背后那些看似简单实则精巧的系统设计。一个按键按下到发出正确音高的声音背后是十二平均律的频率计算、定时器的工作方式配置、中断服务函数的合理设计、矩阵键盘扫描的防抖逻辑还有三极管驱动电路的电流控制。每一个环节单独拿出来都能讲上一节课真正整合到一块板子上的时候你就会发现所谓“电子系统设计”并不是哪个单一模块厉害而是所有模块协同工作得顺滑。这个项目做完之后我顺手把代码里的音阶表扩展成了三个八度又加了一首自选的乐曲播放功能。每次接通电源听着蜂鸣器奏出整段旋律心里还挺有成就感的。如果你也正准备动手做这个题目我的建议是把PCB布局做整齐一点把功能分模块写清楚不要只想着“能响就行”——认真做好每一个细节这个项目能带给你的收获绝对超出一门课程作业的预期。
返回列表