用原理图设计FPGA电子琴:分频表驱动蜂鸣器音乐

发布时间:2026/10/20 13:14:07
用原理图设计FPGA电子琴:分频表驱动蜂鸣器音乐
简介通过Quartus原理图设计方法实现的电子琴FPGA工程包面向数字电路、EDA与FPGA入门学习者演示从音符频率生成、定时计数、数模转换到乐曲存储与播放的完整音乐合成系统。资源共413个文件压缩包5.42MB主要包含原理图文件bdf/bsf、硬件描述文件vhd/tdf、工程设置与报告qpf/qsf/summary/rpt、波形仿真文件vwf及MIF初始化文件等可清晰还原Quartus项目结构。已有1045人学习下载。设计采用查找表实现音符频率映射用定时器/计数器控制音长并包含外部DA接口、按键控制和乐谱存储模块资料内附顶层模块图、完整HDL代码、仿真实例和可下载的SOF配置适合对照原理图理解查找表、计数器、状态控制等数字逻辑。还可在此基础上扩展音色、和弦或自动演奏功能作为FPGA课程设计或电子设计竞赛的参考资料。1. 一个电子琴程序放到FPGA里最该先算清楚的是一张分频表一个电子琴程序放进FPGA实验板最核心的往往不是状态机而是一个分频器。我用某主流FPGA集成开发环境的原理图设计方法把一块入门级开发板改造成了能弹出C大调音阶的电子琴全程几乎没写一行代码。整套设计就是“按键编码 → 查表选分频系数 → 计数翻转 → 驱动蜂鸣器”。对刚接触原理图设计的入门者来说这是一条能把时钟分频、模块例化、总线连接都走一遍的捷径对熟手来说它的价值在于调试思路——频率误差、按键抖动、有源与无源蜂鸣器差异这些坑踩一遍比看十遍教程管用。我们从音阶频率怎么算成数字开始。2. 把抽象频率落成数字音阶频率、分频系数与模块划分2.1 音从哪里来十二平均律与基准频率电子琴要发出准确的音高频率必须和十二平均律对齐。相邻半音之间的频率比是 2 的 12 次方根大约是 1.05946 倍每高一个八度频率翻一倍。绝大多数入门实验板上的晶振是 50MHz这个频率和你要发的音高之间没有直接关系必须靠分频器完成换算。整套系统只有一个频率基准——50MHz 的系统时钟。按下不同的琴键就选不同的分频系数计数器计到目标值后翻转一次输出电平蜂鸣器就得到对应频率的方波。用这个方法做出来的音高稳定性很高因为晶振本身精度高计数器翻转时间完全由硬件逻辑决定不会像模拟振荡器那样受温度影响。我们以 A4440Hz 为基准往前推出 C4261.63Hz到 C5523.25Hz这一组常用音阶作为整个工程的分频依据。2.2 一张表算出所有分频系数分频系数的推导不复杂但很多初次接触原理图设计的同学在这里翻车。假设系统时钟是 clk50MHz目标音高频率为 f输出方波的一个完整周期时长就是 1/f。理想情况下计数器从 0 数到某个值后翻转一次再数同样长的时间再翻回来这样一个完整周期需要翻转两次所以计数器要计的数量 N 由下面这个关系决定计数器计一个数的耗时是 1/clk两次翻转之间计数 N 次半周期时间 N/clk。既然半周期要等于 1/(2f)那就有 N/clk 1/(2f)也就是 N clk / (2f)。代入 clk50MHz就得到 N 50000000 / (2f)。C4 的频率是 261.63Hz算下来 N 50000000 / (2×261.63) ≈ 95556。为了让输出方波的两个半周期尽量对称我一般先把 N 算出来再取最接近的偶数把 N/2 作为计数器翻转点。下面这张表直接给出了 C4 到 C5 每个音的取值做工程时直接抄。音名目标频率 (Hz)分频系数 N半周期计数值实际输出频率 (Hz)误差C4261.639555647778261.62-0.01D4293.668513442567293.670.01E4329.637584637923329.630.00F4349.237158635793349.22-0.01G4392.006377631888392.020.02A4440.005681828409440.000.00B4493.885062025310493.86-0.02C5523.254777823889523.250.00注意 N 必须取偶数。如果 N 是奇数两个半周期长度差一个时钟周期宏观上听不出差别但在示波器上看方波会明显不对称。取偶数后计数器的翻转点就是 N/2。这张表的数值范围最大接近十万计数器位宽至少要 17 位实际工程里我统一用 32 位计数器省得以后扩展音域时不够用。2.3 顶层模块怎么切分频器、音符选择、音色、按键把整个电子琴拆成六个模块数据流就很清晰。每个模块只干一件事后面排查问题时也能按图索骥。这六个模块分别是时钟分频器、按键防抖与编码、音符查找表、音符分频器、PWM 音色控制器、输出驱动。模块输入输出作用按键防抖与编码4 个按键信号3 位按键编码消除机械抖动输出稳定键值音符查找表3 位按键编码32 位分频系数总线把键值映射到 N音符分频器32 位 N、系统时钟音符方波按 N 分频输出目标频率PWM 音色控制器音符方波、载波时钟调制后音频信号控制发声强度和音色输出驱动调制后音频信号蜂鸣器引脚电流匹配与引脚分配数据流是一条直线按键状态 → 防抖 → 编码 → 查表 → 分频系数 → 分频器 → PWM → 蜂鸣器。为什么把查找表单独做成一个模块因为原理图设计里改连线比改代码麻烦做成独立模块后后面要扩展更多音阶只需要改查找表的映射关系不用动分频器。这个划分方式来自我自己的习惯每个模块只暴露必要的输入输出其他全部封装在内部。2.4 从图纸到工程新建工程与符号生成打开某主流 FPGA 集成开发环境后新建工程的第一步是选定实验板对应的 FPGA 型号。这一步不要随便选目标器件型号不对后面的引脚分配全部要返工。我一般会在拿到板卡时先看丝印或者板卡手册确认主控芯片的具体型号再填进工程向导里。File → New Project Wizard 工程名: key_elec 目标器件: 实验板对应的 FPGA 型号 设计入口: 原理图编辑器工程建好之后默认进入空白原理图编辑界面。画原理图的流程和写代码不一样先画底层模块再生成符号最后在顶层把这些符号拖进来连线。每个子模块画完后用 File → Create Symbol from Current File 生成一个方框符号这个符号会出现在元件库里顶层设计时直接调用。这个“自底向上生成符号”的流程是整个原理图设计方法的骨架先有底层模块符号顶层连线才有东西可连。3. 用原理图拼出能发声的电子琴三张表、四条连线3.1 分频器模块计数器到你需要的半周期分频器是整套设计的核心模块。原理图里没有循环语句可用分频逻辑全要靠计数器符号和比较器搭出来。我在原理图里放了一个 32 位二进制计数器计数器的输出接到一个比较器比较器的另一端设成半周期计数值-1。计数器从 0 开始递增计到比较值时输出一个脉冲这个脉冲同时反馈到计数器的同步清除端让计数器归零重新开始。按照 2.2 的表C4 音符的半周期计数值是 47778比较值就是 47777。这个比较值在模块内部作为参数保存生成符号后在顶层双击符号就能改不需要重新画内部电路。这种方法的好处是八个音符共用同一个分频器模块结构只是参数不同。我在实际工程里生成了八个分频器符号实例每个实例的参数分别设置成 C4 到 C5 对应的值这一个动作就把模块复用做完了。计数器位宽选择直接决定能分频的范围。C4 的分频系数接近十万二进制需要 17 位但后续要扩展低音的话比如 C3 频率 130.81Hz系数接近 20 万需要 18 位。我统一用 32 位计数器避免以后扩展音域时模块内部结构还要重画。3.2 用查找表做音符选择按键与分频系数的一一对应有了八个分频器实例之后下一个问题是按下某个按键怎样让对应的分频器工作常见做法是做一个查找表把按键编码映射到分频系数上。按键经防抖和编码后产生一个 3 位二进制值这个值作为多路选择器的选择信号选择器的数据输入端分别接八个分频参数常数。原理图里放一组常数符号常数分别填成 95556、85134、75846、71586、63776、56818、50620、47778。这些常数以总线形式接到多路选择器的数据端口选择信号来自按键编码。选择器输出的 32 位总线接到音符分频器的参数端口分频器立刻改用新的 N 值输出对应频率。这里有个容易踩的细节常数符号在原理图里直接填十进制即可但为了保险起见建议把 N 值换算成十六进制再填进去。因为原理图里有时候会自动截断高位填十进制时如果某一位超过 16 位很容量被误判。我一般先在画图前把表里的十进制全部换算成十六进制填进去后编译时检查一下总线宽度提示确保没有高位被切断。那个 N95556 的十六进制是 0x17544超过 16 位 0xFFFF 的范围必须确认参数总线位宽是 32 位。3.3 音色控制用 PWM 代替直接输出直接拿分频器输出的方波去驱动蜂鸣器声音会非常刺耳听起来像老式报警器。原因是方波的高次谐波成分太多而且占空比固定 50%没有任何音色变化。我对电路做了一处小改动在音符分频器后面加一个 PWM 音色控制器把音符方波和一个更高频率的载波做逻辑与。具体做法是先用一个小分频器产生几十 kHz 的 PWM 载波载波频率远高于音符频率然后让音符方波和载波信号经过一个与门再输出。这样做的效果是音符方波为高电平时蜂鸣器收到一串高频脉冲发声能量被“斩碎”听感变得柔和音符方波为低电平时输出为低电平蜂鸣器不响。调节 PWM 载波的占空比就是调节发声强度这为后面的音量调节和力度包络留了接口。PWM 载波频率的选择有讲究。太低了会听到明显的“滋滋”声一般在 16kHz 以上人耳感知就比较弱太高了又会超过蜂鸣器的响应范围有源蜂鸣器可能根本反应不过来。我习惯取 16kHz 到 20kHz 之间板载无源蜂鸣器在这个区间响应最好电流声也不明显。3.4 顶层连线把六块模块拼成一条链所有底层模块符号生成之后顶层原理图要做的事情就是拉线。我从按键输入端开始一步步把模块串起来。连线顺序如下其中 key[3:0] 是四个物理按键经过防抖和编码后变成 key_code[2:0]再进查找表得到 n_value[31:0]然后进音符分频器得到 tone_out再和 pwm_carrier 做逻辑与后输出到 speaker。key[3:0] → debounce_encode → key_code[2:0] key_code[2:0] → lookup_table → n_value[31:0] n_value[31:0] → tone_divider → tone_out tone_out AND pwm_carrier → speaker_out顶层连线最容易出问题的是总线顺序。比如 n_value[31:0] 是一条总线输出到分频器的 N 端口时如果分频器内部定义 N[31:0]两边含义一致但如果分频器参数端口定义成 N[16:0]总线的高位就会被自动截断。检查方法很简单编译后看信息窗口有没有 width mismatch 警告有的话逐个查连接特别是查找表输出和分频器参数输入之间。我每次连完线都会强制拉一遍总线的“锯齿”样式让总线里的单线视觉上聚拢在一起减少手误拉错线位的概率。另一件事是全局时钟和复位信号的分配。系统时钟 clk 要分配给所有用到时钟的模块复位信号 rst_n 要先接一个上拉电阻符号再分发到各模块。很多初学者只给分频器接了时钟没接复位下载到板卡后模块上电状态不确定输出完全乱套。我在顶层对所有模块的统一端口都连好时钟和复位再开始连线最后检查一遍再编译。4. 上板验证从波形仿真到实测的六个关键步骤4.1 先用波形仿真把逻辑“听”出来直接下载到板卡听声音当然可以但效率太低。我先用集成的波形仿真功能验证逻辑是否正确。在激励文件里把系统时钟 clk 的周期设为 20ns对应 50MHz按键输入给一段足够长的脉冲比如按下时间 20ms 以上保证分频器能完成多个完整周期。观察窗口开 50ms可以覆盖几个音阶的多个完整周期。仿真验证的重点不是听感而是看频率值是否落准。把鼠标放在输出波形上量一个完整周期的时间C4 应该是约 3.822ms对应 261.62HzC5 约 1.911ms对应 523.25Hz。误差在一两个时钟周期内都算正常。如果波形没有翻转先查分频系数有没有传进分频器参数端口如果翻转频率明显偏快再回查 N 是不是被截断或者比较器接反。4.2 引脚分配最后一步的“证书”仿真通过后下一步是引脚分配。我习惯先把引脚分配写好再编译一次避免直接手动在分配编辑器里点选容易出错。下面是一个典型的引脚分配文件内容其中 clk 接晶振输入引脚四个按键分别接四个 GPIOspeaker 接蜂鸣器引脚。clk: PIN_E3 rst_n: PIN_M1 key[0]: PIN_M2 key[1]: PIN_M5 key[2]: PIN_M6 key[3]: PIN_M7 speaker: PIN_G1分配完成后必须全量重新编译一次因为引脚约束要重新做布局布线。下载到板卡之前还要检查一个关键点板载蜂鸣器通常是低电平接通的负极触发有些板子输出高电平才响这个极性差异直接决定你能不能听到声音。我在分配前会看板卡原理图确认蜂鸣器类型有源蜂鸣器和无源蜂鸣器在实验板上的驱动逻辑正好相反这一点下一章详细说。4.3 上板实测三个动作和一张记录表下载程序后我按照固定的三步实测法流程走第一步依次按下八个琴键确认每个键都有声音第二步用调音器测每个音的实际频率记录误差第三步听长音是否稳定有没有沙哑或断续。下面是一张我用于记录测试结果的表格直接把每次的数据填进去就行。音名实测频率 (Hz)与理论误差 (Hz)听感处理动作C4261.61-0.02稳定无D4293.670.01稳定无E4329.62-0.01稳定无F4349.240.01稳定无G4392.000.00稳定无A4440.020.02稳定无B4493.890.01稳定无C5523.24-0.01稳定无实测误差在 ±0.1Hz 内说明分频系数计算无误。如果某个键实测频率差距超过 2Hz大概率是该音符的 N 值取错回查表里的参数即可。整套流程走下来电子琴的基本功能就算落地了。但这只是最稳的一条路真正的麻烦发生在“声音不对”的时候。5. 避坑排查声音不对的时候我这样处理5.1 坑一按键按下去没反应现象按下某个琴键蜂鸣器不发声或者偶尔响一声再按一下又正常。原因机械按键在按下和松开的过程中会产生抖动电平在几千微秒内反复跳变。原理图里如果没有做防抖按键信号进入编码器时可能被识别成多个不同键值分频系数不断跳变听起来就像没按到。解决在按键进入编码器之前加一级防抖逻辑。常见做法是用一个低频时钟对按键信号采样连续两次采样值相同才认为信号稳定。我在原理图里用了 1kHz 的采样时钟两个 D 触发器级联后做边沿检测效果很稳。具体到原理图实现放一个计数器生成 1kHz 时钟再用两个 D 触发器串联第三个触发器的反相输出和第二个触发器的输出相与产生一个单时钟宽度的按键有效信号。5.2 坑二音高整体跑偏现象用调音器一测每个音都高了大约一倍E4 听起来像 E5。原因这是初学者最常踩的坑——把 N 直接用成了计数器计数目标而没有考虑“半周期翻转”。当你设计数器计到 N 时翻转一次输出方波的完整周期是 2×N/clk实际频率是 clk/(2N)而不是 clk/N。如果用 clk/N 去套频率就比预期高一倍。解决确认音符分频器内部的比较目标写的是 N/2-1也就是表里的“半周期计数值”那一列。比如 C4 的 N 是 95556计数器应该数到 47777 翻转不是 95555。另外检查一下如果某个音偏低而不是偏高很可能是比较器接成了大于等于导致计数器在同一点翻转两次输出频率减半。我从那以后每建一个分频器都会先算一遍半周期再填到比较器里绝不再直接填 N。5.3 坑三声音沙哑、带毛刺现象音符能响但声音粗糙按住一个键不放后半段明显有“嘶嘶”的杂音。原因两个可能。一是 PWM 载波频率偏低低于 10kHz 时蜂鸣器的机械响应会明显感知到载波周期人耳就能听到高频毛刺。二是输出逻辑直接把音符方波和载波叠加后接蜂鸣器没有错开相位两个信号同时翻转时产生瞬态电流冲击。解决把 PWM 载波频率提到 16kHz 以上只要蜂鸣器响应够快毛刺就会消失。同时检查蜂鸣器驱动引脚有没有串限流电阻没有的话建议在输出加一个几十欧的电阻有的实验板直接用三极管驱动这时候要把输出引脚配置成推挽输出而不是开漏。实测中我用 20kHz 载波占空比调到 40%听感基本没有毛刺。5.4 坑四只有一种音高或者整体无声现象按下所有键都发同一个音高或者完全无声。原因查找表的按键编码没有正确对应到八个分频系数可能是按键编码的线与查找表选择信号接反导致所有按键都选中了同一个通道。完全无声的情况更常见于把有源蜂鸣器和无源蜂鸣器搞混有源蜂鸣器通电就响无源蜂鸣器必须由外部方波驱动如果你板子上是有源蜂鸣器直接给一个恒定高电平也只会响一声而不是持续发声。解决先确认板卡上的蜂鸣器类型。有源蜂鸣器做音符输出时要用“电平变化”来发声那就不能靠方波驱动得改成按键按下时给高电平、松开给低电平。无源蜂鸣器才适合用分频器输出方波。如果是多音问题回查查找表的选择信号连接把编码器的三条输出逐一对应到查找表的三个选择位用仿真验证每个编码值对应的 N 是否按序递增。5.5 一张排查表快速定位在实际调试时我不按固定顺序排查而是靠一张对照表快速缩小范围。这张表配合波形仿真一起用能省掉大量盲目试错时间。现象可能原因检查点全部无声蜂鸣器类型判断错误 / 引脚未分配确认发声器件类型检查 speaker 引脚 net 名称单键无声按键防抖失败 / 查表映射错误看按键编码波形确认该键对应的 N 值是否送到分频器全部音偏高N 直接用成完整周期计数核对分频器是否按半周期目标翻转单音偏低该音符 N 值写错对照分频系数表逐个检查实例参数声音沙哑PWM 载波频率过低提高载波到 16kHz 以上调节占空比音忽然跳变按键抖动 / 总线位宽截断检查防抖模块输出确认总线未出现宽度警告这张表打印出来贴在显示器旁边基本能覆盖我在这类设计里遇到过的 90% 问题。6. 进阶扩展把单音电子琴升级成多功能工具电子琴能正常发声后我往工程里加了三个扩展功能难度逐级提升但每加一个都是对原理图设计能力的一次检验。给音符加上力度包络是听感提升最大的一步。原理图里做一个数字积分器从音符开始时刻起让 PWM 占空比从 10% 慢慢升到 50%持续几个毫秒后再缓慢下降。这个“起音—衰减”包络让声音更像真实乐器而不是方波发生器。具体参数我习惯取起音 5ms、衰减 30ms调的太短会变成爆音太长则像音量踏板。这个模块放在音色控制器内部并把占空比输入做成总线端口外部再接一个拨码开关调整包络速度。第二个扩展是从单音到多音。同时按两个键时直接在输出端做逻辑或会产生混乱的信号叠加两个不同频率的方波相或后占空比完全不可控。更可行的做法是时分复用让一个高速时钟轮流选择不同音符通道每通道占一个小时间片听感上接近多音同时发声。这个方案的代价是需要并行多组分频器并且合并器要能在一个音符周期内完成多次切换复杂度明显上升。我在原理图里实现了两个音符通道并行再用一个计数器产生通道选择信号测试效果不错。但别指望在入门级原理图里无限扩展多音超过三个通道后连线和延迟都会变得难以控制。第三个扩展是自动演示模式。用一个慢速计数器在无人按键时循环产生键值送到编码器的输出端电子琴就能自动播放一段旋律。这个模块原理上就是一个预置的键值 ROM被分频时钟周期性地读出。速度参数调节每隔 0.5 秒切换一个音就能听到清晰的旋律线。这个模式最大的价值是上板验收时不用一个个手按直接看蜂鸣器能不能按顺序发声。最后一个建议是“代码与原理图混合设计”。原理图负责顶层的模块装配和连线底层复杂逻辑用 HDL 描述。比如音符分频器用一段简洁的 HDL 实现比在原理图上画十几个计数器符号要直观得多。下面是一段参考实现半周期翻转逻辑与原理图一致参数 N 可以直接用分频系数表的数值module tone_div #( parameter integer N 47778 // 半周期计数值 )( input wire clk, input wire rst_n, output reg tone_out ); reg [31:0] counter; wire tick (counter N - 1); always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 0; tone_out 1b0; end else if (tick) begin counter 0; tone_out ~tone_out; end else begin counter counter 1; end end endmodule这里 N 是半周期计数值C5 用 23889C4 用 47778由时钟频率和目标频率共同决定。计数器位宽 32 位保证低频音阶使用时不会溢出。混合设计的好处是顶层连线的全貌依然清晰底层逻辑又不必被原理图的图形约束拖累。我后来再做类似项目都默认用这种混合方式把原理图当成系统框图来画而不是把它当成所有逻辑的最终表达。从那以后我每次改完顶层连线图都会强制走一遍固定流程编译一次跑一遍波形仿真再拔掉 USB 线重新上电听声音。这套流水线帮我筛掉过不少连线和参数上的低级问题也让我养成了改图必看警告信息的习惯。希望帮到你。本文还有配套的精品资源点击获取