简介:通过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 的系统时钟。按下不同的琴键,就选不同的分频系数,计数器计到目标值后翻转一次输出电平,蜂鸣器就得到对应频率的方波。用这个方法做出来的音高稳定性很高,因为晶振本身精度高,计数器翻转时间完全由硬件逻辑决定,不会像模拟振荡器那样受温度影响。我们以 A4=440Hz 为基准,往前推出 C4(261.63Hz)到 C5(523.25Hz)这一组常用音阶,作为整个工程的分频依据。2.2 一张表算出所有分频系数分频系数的推导不复杂,但很多初次接触原理图设计的同学在这里翻车。假设系统时钟是 clk=50MHz,目标音高频率为 f,输出方波的一个完整周期时长就是 1/f。理想情况下,计数器从 0 数到某个值后翻转一次,再数同样长的时间再翻回来,这样一个完整周期需要翻转两次,所以计数器要计的数量 N 由下面这个关系决定:计数器计一个数的耗时是 1/clk,两次翻转之间计数 N 次,半周期时间 = N/clk。既然半周期要等于 1/(2f),那就有 N/clk = 1/(2f),也就是 N = clk / (2f)。代入 clk=50MHz,就得到 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.67+0.01E4329.637584637923329.630.00F4349.237158635793349.22-0.01G4392.006377631888392.02+0.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 位很容量被误判。我一般先在画图前把表里的十进制全部换算成十六进制,填进去后编译时检查一下总线宽度提示,确保没有高位被切断。那个 N=95556 的十六进制是 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.62Hz),C5 约 1.911ms(对应 523.25Hz)。误差在一两个时钟周期内都算正常。如果波形没有翻转,先查分频系数有没有传进分频器参数端口;如果翻转频率明显偏快,再回查 N 是不是被截断或者比较器接反。4.2 引脚分配:最后一步的“证书”仿真通过后,下一步是引脚分配。我习惯先把引脚分配写好再编译一次,避免直接手动在分配编辑器里点选容易出错。下面是一个典型的引脚分配文件内容,其中 clk 接晶振输入引脚,四个按键分别接四个 GPIO,speaker 接蜂鸣器引脚。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.67+0.01稳定无E4329.62-0.01稳定无F4349.24+0.01稳定无G4392.000.00稳定无A4440.02+0.02稳定无B4493.89+0.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 <= 1'b0; end else if (tick) begin counter <= 0; tone_out <= ~tone_out; end else begin counter <= counter + 1; end end endmodule这里 N 是半周期计数值,C5 用 23889,C4 用 47778,由时钟频率和目标频率共同决定。计数器位宽 32 位,保证低频音阶使用时不会溢出。混合设计的好处是:顶层连线的全貌依然清晰,底层逻辑又不必被原理图的图形约束拖累。我后来再做类似项目,都默认用这种混合方式,把原理图当成系统框图来画,而不是把它当成所有逻辑的最终表达。从那以后,我每次改完顶层连线图,都会强制走一遍固定流程:编译一次,跑一遍波形仿真,再拔掉 USB 线重新上电听声音。这套流水线帮我筛掉过不少连线和参数上的低级问题,也让我养成了改图必看警告信息的习惯。希望帮到你。本文还有配套的精品资源,点击获取
简介:通过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 的系统时钟。按下不同的琴键,就选不同的分频系数,计数器计到目标值后翻转一次输出电平,蜂鸣器就得到对应频率的方波。用这个方法做出来的音高稳定性很高,因为晶振本身精度高,计数器翻转时间完全由硬件逻辑决定,不会像模拟振荡器那样受温度影响。我们以 A4=440Hz 为基准,往前推出 C4(261.63Hz)到 C5(523.25Hz)这一组常用音阶,作为整个工程的分频依据。2.2 一张表算出所有分频系数分频系数的推导不复杂,但很多初次接触原理图设计的同学在这里翻车。假设系统时钟是 clk=50MHz,目标音高频率为 f,输出方波的一个完整周期时长就是 1/f。理想情况下,计数器从 0 数到某个值后翻转一次,再数同样长的时间再翻回来,这样一个完整周期需要翻转两次,所以计数器要计的数量 N 由下面这个关系决定:计数器计一个数的耗时是 1/clk,两次翻转之间计数 N 次,半周期时间 = N/clk。既然半周期要等于 1/(2f),那就有 N/clk = 1/(2f),也就是 N = clk / (2f)。代入 clk=50MHz,就得到 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.67+0.01E4329.637584637923329.630.00F4349.237158635793349.22-0.01G4392.006377631888392.02+0.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 位很容量被误判。我一般先在画图前把表里的十进制全部换算成十六进制,填进去后编译时检查一下总线宽度提示,确保没有高位被切断。那个 N=95556 的十六进制是 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.62Hz),C5 约 1.911ms(对应 523.25Hz)。误差在一两个时钟周期内都算正常。如果波形没有翻转,先查分频系数有没有传进分频器参数端口;如果翻转频率明显偏快,再回查 N 是不是被截断或者比较器接反。4.2 引脚分配:最后一步的“证书”仿真通过后,下一步是引脚分配。我习惯先把引脚分配写好再编译一次,避免直接手动在分配编辑器里点选容易出错。下面是一个典型的引脚分配文件内容,其中 clk 接晶振输入引脚,四个按键分别接四个 GPIO,speaker 接蜂鸣器引脚。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.67+0.01稳定无E4329.62-0.01稳定无F4349.24+0.01稳定无G4392.000.00稳定无A4440.02+0.02稳定无B4493.89+0.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 <= 1'b0; end else if (tick) begin counter <= 0; tone_out <= ~tone_out; end else begin counter <= counter + 1; end end endmodule这里 N 是半周期计数值,C5 用 23889,C4 用 47778,由时钟频率和目标频率共同决定。计数器位宽 32 位,保证低频音阶使用时不会溢出。混合设计的好处是:顶层连线的全貌依然清晰,底层逻辑又不必被原理图的图形约束拖累。我后来再做类似项目,都默认用这种混合方式,把原理图当成系统框图来画,而不是把它当成所有逻辑的最终表达。从那以后,我每次改完顶层连线图,都会强制走一遍固定流程:编译一次,跑一遍波形仿真,再拔掉 USB 线重新上电听声音。这套流水线帮我筛掉过不少连线和参数上的低级问题,也让我养成了改图必看警告信息的习惯。希望帮到你。本文还有配套的精品资源,点击获取