简介:面向FPGA入门与数字电路课程实践的Quartus原理图电子琴工程,适合希望用图形化方式理解音乐合成、按键控制与音频输出链路的学生和自学者。压缩包共413个文件、约5.42MB,包含bdf/bsf原理图与符号文件、vhd/tdf硬件描述补充、qpf/qsf工程配置、sof下载比特流、vwf仿真波形以及mif存储初始化、编译报告和日志等,基本覆盖从设计、编译到板级验证的完整工程痕迹。已有1045人学习下载。工程围绕音符频率生成、定时计数、DA接口、控制器与乐谱存储展开,可直接打开参考原理图连接与模块划分,结合仿真波形和报告排查时序、资源占用问题,进而掌握Quartus项目组织、编译下载和电子琴发声调试思路,对课程设计或综合实验选题也有参考价值。
1. 一块板子、八个键、七个音:Quartus 原理图版电子琴到底做了什么
实验室那种“按下就有声”的小板子,很多人第一次接触 FPGA 就是从它开始的。这份资源给的是纯原理图路线的电子琴:不写一行 HDL,全靠 Quartus 里拖 74161、74138、74LS148、lpm_counter 这些符号,把 50MHz 晶振一路切到 261Hz 的方波,推蜂鸣器出声。它适合刚学完数字逻辑、想让作品真正上板响一次的开发者,也适合课程设计需要交付、又不想在 Verilog 语法上翻车的情况。真正有价值的地方不在音符本身,而在于它把“分频、编码、选择、输出”这四件事画成了一条能拿示波器逐点量的信号链。
2. 系统架构与工程组织:一张图里的四条支路
2.1 顶层信号链:从 50MHz 到 261Hz 的完整路径
原理图项目最怕的是“画完了不知道信号怎么走”。这个设计的顶层其实只有四条支路,全部围绕一个 50MHz 有源晶振展开。
第一条是时钟支路。50MHz 先进一级基础分频,降到一个整数基准频率,后面所有音符分频都基于这个基准来数。选 1MHz 是个折中:既不至于让 12 位计数器溢出,又能把最低音 C4 的分频系数控制在 1911 这个量级,精度误差不到万分之五,人耳完全听不出来。
第二条是输入支路。八个独立按键进 74LS148 优先级编码器,输出 3 位二进制音符编号。这里之所以用优先级编码器而不是普通编码器,是因为手快的时候很容易两个键同时按下去,普通编码器会输出一堆无效组合,优先级编码器至少能保证“总有一个音在响”。
第三条是选择支路。3 位音符编号要去选一组 12 位分频预置值,原理图里用 lpm_mux 或者直接例化一个 8×12 的 lpm_rom 都行。这一步是整个设计里最容易被做丑的地方,后面 3.3 会展开讲怎么画得干净。
第四条是输出支路。12 位计数器数满翻转一次,输出 50% 占空比的方波,经一个三极管或者直接 GPIO 推蜂鸣器。方波的高次谐波很重,听感偏“电子味”,但这恰好是这个项目该有的样子——真要正弦波就得上 DAC 了,那就不是原理图能干完的活了。
四条支路合起来,顶层 .bdf 上大概二十来个符号、三四十根连线。听起来不多,但每根线的位宽都得对上,这是原理图设计比写代码更容易翻车的地方。
2.2 工程目录与 .bdf 层次:顶层一张图,子模块各建各的符号
这类工程的目录结构通常很朴素,没有脚本、没有 IP 打包,就是一堆文件平铺:
organ/ ├── organ.qpf # Quartus 工程文件 ├── organ.qsf # 器件、引脚、时序约束都在这儿 ├── organ_top.bdf # 顶层原理图,只放符号和连线 ├── clk_div.bdf # 基础分频:50MHz -> 1MHz ├── key_encode.bdf # 按键编码 + 消抖 ├── note_div.bdf # 音符分频,12 位预置数计数器 ├── auto_play.bdf # 自动演奏:节拍计数 + ROM 查表 ├── note_rom.mif # 自动演奏的乐谱数据 ├── organ_top.vwf # 仿真波形文件 └── db/ incremental_db/ output_files/ # 编译产物,不用管关键在于层次划分的粒度。我一般按“能被单独仿真验证”来切:分频器是一个层次,编码器是一个层次,音符分频是一个层次。每画完一个子 .bdf,就在 File 菜单里执行一次 Create Symbol Files for Current File,生成一个 .bsf 符号文件,顶层再把它当元件拖进来。
这里有个隐形的坑:子模块的端口一改,.bsf 不会自动更新。顶层看起来还是那个框,实际端口早就对不上了,编译会报端口位宽不匹配或者干脆连不上。所以养成习惯,改完子模块立刻重建符号,再回顶层看一眼。
顶层文件本身要尽量“空”。除了符号、输入输出引脚、几根连线,不要放任何逻辑门。逻辑门一多,图和思路就脱节了,过两周自己都看不懂这根线为什么这么绕。这也是原理图设计相比 HDL 唯一的优势——图就是文档,前提是你别把它画成蜘蛛网。
2.3 分频系数表:先算数,再画图
原理图里最不该靠试的就是分频系数。先把表算清楚,画图的时候照着填,能省掉大量“烧进去听着不对、再改、再烧”的循环。
音符分频的公式是 N = f_base / (2 × f_note)。分母那个 2 是因为要生成方波,一个完整周期包含两次翻转,计数器只需要数半个周期。以 1MHz 基准为例,八个音的分频系数和 12 位预置值(4096 − N,配合 74161 的同步预置数法)如下:
| 音名 | 标准频率 (Hz) | N = 1e6/(2f) | 12 位预置值 (4096−N) | 实际输出 (Hz) |
|---|---|---|---|---|
| C4 | 261.63 | 1911 | 2185 | 261.64 |
| D4 | 293.66 | 1703 | 2393 | 293.60 |
| E4 | 329.63 | 1517 | 2579 | 329.60 |
| F4 | 349.23 | 1432 | 2664 | 349.16 |
| G4 | 392.00 | 1276 | 2820 | 391.85 |
| A4 | 440.00 | 1136 | 2960 | 440.14 |
| B4 | 493.88 | 1012 | 3084 | 494.07 |
| C5 | 523.25 | 956 | 3140 | 523.01 |
这张表要打印出来贴在显示器边上。G4 那一行误差看着最大(0.15Hz),但那是相对误差 0.04%,人耳对 400Hz 附近 0.15Hz 的偏差毫无感知。真正会听出问题的不是这里,而是基础分频那一级算错了——如果 50MHz 被分成了 2MHz 而不是 1MHz,所有音会整体高一个八度,听起来“尖”,但初学者第一反应往往是“按键坏了”,方向就跑偏了。
顺带说一句,12 位计数器能表示的最大值是 4095,而 74161 是四位一片,三片级联正好 12 位。如果你用的基准是 500kHz,C4 的 N 会变成 956,三片仍然够用;但如果基准定到 100kHz,N 只有 191,用 12 位就浪费了 8 位资源,这时候更划算的是直接上单片 8 位计数器。
3. 核心模块拆解:原理图画到什么程度算合格
3.1 基础分频:50MHz 压到 1MHz 的计数器该怎么填参数
基础分频是整个系统的心跳,它错一次,后面全错。50MHz 要得到 1MHz,需要每 25 个时钟翻转一次输出——周期 50 个时钟,正好 1MHz。
在原理图里,这一步有两种画法。一种是用 lpm_counter,参数设成“模 25 的计数器 + 一个 T 触发器”;另一种是直接上两片 74161 级联,用 RCO 反馈预置数。前者干净,后者在教科书里更常见。我倾向 lpm_counter,因为它能省掉一堆连线,而且参数窗口里看得见“25”这个数,比一堆门电路直观。
不过 lpm_counter 的模值设置有个细节:它是从 0 数到 Modulus−1 然后回卷,不是数到 Modulus。所以模 25 意味着 0 到 24 循环,一共 25 个状态,翻转频率就是 50MHz/50 = 1MHz。填 26 就变成了 50MHz/52,输出 961.5kHz,整个音阶会整体偏低约 1.9%,弹《小星星》能听出来跑调,但单音听不出来——这就是那种“录个视频发给老师才发现不对”的坑。
对应的 HDL 行为模型如下,用来在仿真里核对参数,实际工程里还是用 LPM 元件:
// 行为对照模型:50MHz -> 1MHz,仅用于验证参数,不参与综合 module clk_1m ( input wire clk_50m, input wire rst_n, output wire clk_1m ); reg [4:0] cnt; // 0~24,共 25 个时钟周期 reg div; always @(posedge clk_50m or negedge rst_n) if (!rst_n) cnt <= 5'd0; else cnt <= (cnt == 5'd24) ? 5'd0 : cnt + 1'b1; always @(posedge clk_50m or negedge rst_n) if (!rst_n) div <= 1'b0; else if (cnt == 5'd24) div <= ~div; // 每 25 拍翻转,输出周期 50 拍 assign clk_1m = div; endmodulecnt的位宽是 5 位,因为 24 需要 5 位二进制表示。div每 25 个 50MHz 时钟翻转一次,所以clk_1m的周期是 50 × 20ns = 1000ns,频率 1MHz,占空比 50%。这段代码不是给综合器看的,是让你在仿真里一眼确认“25 这个数到底对应多少频率”,然后再回头去填 LPM 的参数框。
至于异步复位,原理图里对应的是一个全局的 rst 引脚接到每个计数器的 clrn 端。别图省事把 clrn 拉高就完事,上电瞬间计数器状态是随机的,虽然 FPGA 复位后一般会初始化,但 CPLD 就不一定了。
3.2 按键编码:74LS148 优先级编码 + 一个 20ms 消抖
八个按键对应八个音,最直接的接法是每个键接一个 GPIO,然后写 8 个判断。原理图里没地方“写判断”,所以要用编码器把 8 根线压成 3 位。
74LS148 是 8 线到 3 线的优先级编码器,低电平有效,输入 IN0~IN7 里编号大的优先级高,输出 A0~A2 是反码。用它的时候要注意两点:一是输出是反码,接下游之前要么加三个非门,要么在下游的译码逻辑里反过来理解;二是 GS(组信号)引脚可以用来判断“有没有键按下”,很多人不接,结果没按键的时候编码器输出 111,被当成“第 8 个音”,一上电就自鸣。
按键抖动是另一个必须处理的东西。机械按键按下和松开时会有 5~20ms 的抖动,反映到编码器输出上就是音符编号来回跳几次,听感上像是“咔咔”的杂音。原理图里的标准做法是给每个按键配一个 RS 触发器(两个与非门交叉)做硬件消抖,或者用一个 20ms 的计数器在检测到按键状态变化后延时采样。
我更推荐后者加在编码器之后:用 1kHz(可由 1MHz 再分频得到)的采样时钟,对编码器输出连续采样,只有连续 20 次一致才更新输出寄存器。这样一个模块解决全部八个键,不用画八个 RS 触发器,图也清爽。
还有一个容易被忽略的点:换音的时候,音符分频器的计数器必须重新装载。如果不管它,切换瞬间计数器可能还停在旧音的中间位置,新音的第一个半周期会特别长或者特别短,听起来就是一声“噗”。所以编码器输出变化时,要额外产生一个 reload 脉冲送到分频器,这在原理图上就是一根线的事,但不画就是会有杂音。
3.3 音符分频:12 位预置数计数器的画法
这是整个设计里最“手工”的部分。用三片 74161 级联成 12 位计数器,每片的 CLK 接同一个 1MHz 基准(同步级联,不是行波进位),低片的 RCO 接高片的 ENT/ENP。三片的预置数端 D0~D11 分别接到“分频系数选择器”输出的 12 根线上。
工作过程是:计数器从预置值开始向上数,数到 4095 时 RCO 有效,这个信号同时做两件事——一是把三片重新装载回预置值(同步置数,所有片的 LOAD 连在一起),二是驱动一个 T 触发器翻转,产生输出的方波。
这里有个时序上的讲究。74161 的同步置数是“下一个时钟沿生效”,所以从 RCO 有效到计数器真正回到预置值,中间隔了一个时钟。这意味着实际的分频比是 N+1 而不是 N。对于 C4 的 N=1911,误差 0.05%,可以忽略;但如果你给高音区用了 N=95 这种小值,误差就到 1% 了,能听出来。修正办法是在计算预置值时按 4096−N−1 来填,或者在预置值上加 1。
行为模型长这样,注意 reload 分支不带翻转,这是换音不拖音的关键:
// 音符分频器行为模型:与三片 74161 + T 触发器的原理图一一对应 module note_div ( input wire clk_1m, // 1MHz 基准 input wire rst_n, input wire [11:0] preset, // 4096 - N,由音符选择器给出 input wire reload, // 换音脉冲,强制重装但不翻转 output reg wave_out ); reg [11:0] cnt; wire tc = (cnt == 12'hFFF); // 数到 4095,对应 RCO always @(posedge clk_1m or negedge rst_n) begin if (!rst_n) begin cnt <= preset; wave_out <= 1'b0; end else if (reload) begin cnt <= preset; // 只重装,不翻转,避免切换瞬间的怪音 end else if (tc) begin cnt <= preset; wave_out <= ~wave_out; // 每满 N 个基准周期翻转一次 end else begin cnt <= cnt + 1'b1; end end endmodule参数说明:preset是 12 位,直接对应三片 74161 的并行数据输入端;reload是高有效脉冲,宽度只要一个 1MHz 周期就够;wave_out就是最终推蜂鸣器的那根线。整个模块的时序深度只有一级触发器,从 tc 有效到 wave_out 翻转是一个时钟周期,延迟固定,不影响音准。
选择器那边,如果只有八个音,可以用三片 74151(8 选 1)分别选 12 位中的 4 位,也可以直接例化一个 8×12 的 lpm_rom,把预置值烧进 .mif。后者改音符只要改文件,不用重画图,二次开发时优势明显。
3.4 自动演奏:ROM 查表 + 节拍地址计数器
自动演奏模块是这个项目里唯一“有点意思”的部分,也是最能体现原理图设计上限的地方。它的结构非常直接:一个地址计数器 + 一片 ROM + 一个节拍分频。
节拍分频把 1MHz 再降一次,降到几赫兹,比如 4Hz 对应每拍 250ms,也就是每分钟 240 拍的快板。这个频率决定曲子跑多快,接到地址计数器的时钟端。
地址计数器数的是“第几个音”,每来一个节拍加一。ROM 的数据宽度是 8 位,高 4 位存音符编号,低 4 位存时值编号(1 表示一拍,2 表示两拍)。播放的时候高 4 位送进音符选择器,低 4 位送进一个可重装的时值计数器,数完才允许地址加一。
乐谱数据用 MIF 文件描述,Quartus 的 lpm_rom 初始化就认这个格式:
DEPTH = 16; WIDTH = 8; ADDRESS_RADIX = DEC; DATA_RADIX = HEX; CONTENT BEGIN 0 : 11; -- 音 1,时值 1 拍 1 : 12; -- 音 1,时值 2 拍 2 : 33; -- 音 3,时值 1 拍 3 : 31; -- 音 3,时值 1 拍 4 : 54; -- 音 5,时值 2 拍 5 : 42; -- 音 4,时值 1 拍 6 : 31; -- 音 3,时值 1 拍 7 : 22; -- 音 2,时值 1 拍 8 : 11; 9 : 12; 10 : 33; 11 : 31; 12 : 54; 13 : 42; 14 : 31; 15 : 22; END;DEPTH和WIDTH必须和 lpm_rom 参数窗口里填的一致,否则编译时会报初始化文件与存储深度不匹配。ADDRESS_RADIX = DEC表示地址用十进制写,读起来直观;DATA_RADIX = HEX是十六进制,所以11表示高 4 位是 1、低 4 位是 1。
要停播的时候,用一个开关把节拍时钟门控掉,或者直接把地址计数器清零。别用“把 ROM 输出强制置零”这种办法,那只是不出声,地址还在跑,恢复的时候会从曲子中间接着放。
4. 编译、约束与下载:让 .bdf 变成板子上的声音
4.1 建工程与器件选型
打开 Quartus,New Project Wizard 走一遍,重点是第二步的器件选择。这类项目资源消耗极小,一片 Cyclone IV E 的 EP4CE6 就用掉不到 5%,MAX 10 或者 MAX II 的 CPLD 也完全够。选器件不是选“最好的”,是选“你手上那块板子上焊的”。
工程建完之后,在 Project Navigator 里把顶层实体设成organ_top,这个设置会写进 .qsf。如果你后来改了顶层文件名却忘了改这个设置,编译会报“top-level entity not found”,而且错误信息藏在编译报告的很后面。
器件型号里末尾那个速度等级和温度等级(比如 C8)也别填错,填错了引脚位置可能全都对不上,Pin Planner 里一片红。
4.2 引脚约束与 IO 标准
引脚分配是这个项目里最容易出人命的一步。逻辑全对、编译全过、下载成功,就是不响——八成是引脚分到了不能用的地方。
分配方式有两种,图形化的 Pin Planner 和直接改 .qsf。我更倾向改 .qsf,因为它能进版本管理,换台电脑不用重新点一遍:
# organ.qsf 节选:器件与顶层 set_global_assignment -name FAMILY "Cyclone IV E" set_global_assignment -name DEVICE EP4CE6E22C8 set_global_assignment -name TOP_LEVEL_ENTITY organ_top # 时钟与按键(引脚号以自己板子的原理图为准) set_location_assignment PIN_E1 -to clk_50m set_location_assignment PIN_M2 -to key[0] set_location_assignment PIN_M1 -to key[1] set_location_assignment PIN_N2 -to key[2] set_location_assignment PIN_N1 -to key[3] # 音频输出 set_location_assignment PIN_A3 -to buzzer set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to buzzer # 时序约束文件 set_global_assignment -name SDC_FILE organ_top.sdc每一行的作用:set_location_assignment把顶层端口绑到物理引脚,set_instance_assignment指定 IO 标准。IO 标准这一步很多人省掉,靠默认值,结果 3.3V 的板子配上 2.5V 标准,蜂鸣器声音小得像蚊子叫。
时钟约束单独放在 SDC 里,虽然这个设计跑 50MHz 毫无压力,但不加约束 Quartus 会按默认的宽松模型优化,仿真和实际可能对不上:
# organ_top.sdc create_clock -name clk50 -period 20.000 [get_ports clk_50m] derive_pll_clocks derive_clock_uncertainty-period 20.000就是 50MHz 对应的 20ns 周期,单位是纳秒。写完记得在 Assignments 里确认 SDC 文件已经被工程引用,没引用的话这段等于没写。
4.3 编译报告怎么读,波形仿真怎么验
编译分三步:Analysis & Synthesis、Fitter、Assembler。前两步过了第三步基本没问题。真正要看的是编译报告里的三块内容。
第一块是 Fitter 的引脚分配结果,确认每个端口都分到了你想要的引脚号。这里如果出现User assigned pin is reserved之类的警告,说明该引脚是专用配置引脚(比如 nCEO、DCLK、DATA0),不能当普通 IO 用,必须换一个。
第二块是时序分析的 Summary。这个设计只有一个时钟域,Fmax 通常在 200MHz 以上,只要不出现负的 slack 就没事。如果出现了,多半是你在组合逻辑里串了太多级,比如编码器输出直接喂到译码器再喂到比较器。
第三块是 Resource Utilization。看一下用了多少个逻辑单元、多少个寄存器,和你的预期对不对得上。如果数字大得离谱,可能是某个子模块被例化了两次而你没注意。
功能验证靠 Vector Waveform File。新建一个 .vwf,把 clk_50m、key[0]、buzzer 这几个信号拖进去,给 clk 设一个 20ns 的周期,key 手动拉低一段时间,然后跑功能仿真。重点看 buzzer 的周期对不对——在波形上量两个上升沿之间的时间,C4 应该是 3.8ms 左右。这一步花十分钟,能省掉后面半小时的“烧进去听不出来”。
烧录之前还有个小动作值得做:把 wave_out 引到一个空闲的 LED 上,或者把一个 1Hz 的分频信号引到 LED。板子一上电 LED 闪,说明时钟链路是通的,这时候再没声,问题就锁定在按键或者蜂鸣器电路上,排查范围一下小一半。这种“加个指示灯”的习惯,是原理图项目里最便宜的调试手段。
5. 避坑与排查:五个上板后没声、跑飞、乱跳的真实原因
5.1 现象:编译通过、下载成功,蜂鸣器一点动静都没有
原因最常见的有两个。一是蜂鸣器引脚被分配到了专用配置引脚或者电源引脚,Quartus 在编译时会给出警告但不会中止,你很容易忽略掉。二是板子上的蜂鸣器是无源的,而你直接给了方波但驱动电流不够——FPGA 的 IO 一般只有 8~24mA,无源蜂鸣器启动电流往往要 30mA 以上。
解决办法:先在 Pin Planner 里把所有输出引脚过一遍,带RESERVED标记的一律换掉。然后在蜂鸣器前面加一级 NPN 三极管(比如 S8050)做开关,基极串 1k 电阻,发射极接地,集电极接蜂鸣器负极,蜂鸣器正极接 3.3V。这一步加完,声音立刻从“没有”变成“有点吵”。
5.2 现象:整体音高不对,听起来差了一个八度
原因在基础分频的模值上。想要 50MHz 得到 1MHz,需要的是“每 25 个时钟翻转一次”,而不是“每 50 个时钟翻转一次”。很多人在 lpm_counter 的模值窗口里看到“周期”两个字,下意识填了 50,结果输出是 500kHz,所有音低了一个八度。
解决办法:把模值改成 25(如果参数名是 Modulus)或者 24(如果参数名是“计数值”),具体看元件版本的语义。更稳妥的办法是拿示波器或者逻辑分析仪量一下 clk_1m 的实际频率,1MHz 对应 1μs 周期,量准了再往下走。没有仪器的话,用 LED 做个 1Hz 的心跳灯,从 1MHz 再分频 500000 次,灯闪的节奏对不对也能粗糙验证时钟链路。
5.3 现象:按键按下去,声音“咔咔”响,或者按一下出好几个音
原因是机械按键抖动,代码里叫 bounce,硬件上没有消抖。抖动持续 5~20ms,而音符分频器响应速度是微秒级,抖动期间编码器的输出会被反复采样,每个采样值都触发了 reload,分频器被反复重装,输出就变成了一段杂音。
解决办法:在编码器和选择器之间插一级消抖。用 1kHz 采样时钟对编码器输出连续采样 20 次,全部一致才更新输出寄存器。原理图上就是一片 74161 加一片 74164 移位寄存器,或者直接例化一个 lpm_counter 配一个比较器。别用 RC 低通加施密特触发器,虽然理论可行,但 RC 参数和你手上电阻电容的误差会让延迟在 3~50ms 之间飘,不同批次的板子表现不一样。
5.4 现象:换音的时候有一声短促的“噗”,或者两个音叠在一起
原因有两个层面。硬件层面,是分频计数器换音时没有重装,计数器的当前值还是旧音的,新音的第一个半周期长度错误。逻辑层面,是选择器的输出在按键变化的瞬间有竞争冒险,12 位预置值里某几位同时翻转,出现一个毛刺,被分频器当成有效值锁存了。
解决办法:编码器输出先打一拍(用一个 D 触发器),让 12 位预置值稳定下来,再产生 reload 脉冲给分频器。同时 reload 的处理逻辑要写成“只重装、不翻转”,翻转只在计数到 4095 的时候发生。这两条做到,换音就是干净的。
5.5 现象:自动演奏节奏忽快忽慢,或者放几个音就卡住
原因是节拍时钟和音符时钟来自同一个分频链,但地址计数器用的时钟沿和 ROM 的读时序没对齐。lpm_rom 默认是异步读或者单时钟同步读,如果你用的是一个时钟去驱动地址计数器、另一个时钟去读 ROM,就会出现地址已经变了但数据还是旧的,或者反过来。
解决办法:把地址计数器和 ROM 输出寄存器放在同一个时钟域,用节拍时钟的上升沿同时更新地址和锁存数据。如果 lpm_rom 参数里勾选了“Register data output”,那就再多等一拍,时值计数器要相应延长一个节拍单位。另外 MIF 文件里的 DEPTH 一定要和 ROM 参数一致,深度写成 16 但实际写了 32 行数据,编译会通过,仿真会读到越界数据,上板表现就是放到一半突然静音。
6. 二次开发:从单音到和弦、从方波到包络的改造路线
这个设计的结构是干净的,干净就意味着好改。想在它上面继续往前走的,可以按下面三条路线挑一条。
第一条是扩键盘。现在的八个音只是 C4 到 C5 一个八度,想要两个八度就得换 4×4 矩阵键盘。做法是加一个 2 位行扫描计数器(用 1kHz 时钟驱动),行信号经 74LS138 译码输出,列信号回读后送 74LS148 编码。按键编号变成 4 位,预置值表从 8 项扩到 16 项,用一片 lpm_rom 存 16×12 位就行,MIF 文件改一改,图不用重画。扫描时钟别低于 500Hz,否则快速连按会丢键。
第二条是改音色。方波听起来“电子味”重,是因为只有奇次谐波。想要柔和一点,最省事的是把方波送进一个 RC 低通再输出,但这是模拟手段,参数不好控。数字手段是用 lpm_rom 存一个周期的正弦采样表(比如 64 点 × 8 位),用相位累加器读地址,输出送一个简易 R-2R 电阻网络做 DAC。代价是要多花几十个 IO 和一个外部电阻网络,但音色提升非常明显。再加一个 ADSR 包络——用四个状态机控制一个乘法器的系数——就能做出“按下渐强、松开渐弱”的钢琴感。
第三条是加录音回放。原理图里加一片双端口 RAM(lpm_ram_dp),一路写一路读。按键编码器的输出和时值计数器的输出拼成一个 8 位字,在按键变化时写入 RAM,地址用一个写指针;回放时用读指针顺序读出,送进音符选择器。RAM 深度 256 就够录一首短曲子,资源占用很小。这套东西做完,你会发现它已经不只是个电子琴了,是个带存储的简易音序器。
改造时的验证方法我一般固定走两步:先在 .vwf 里把新加的信号拉出来,仿真看时序;再烧到板子上,用 SignalTap 抓内部节点,确认实际跑起来和仿真一致。SignalTap 抓的信号别超过 32 个,采样深度 1K 就够,深了编译时间会翻倍。
血泪经验是:每次改完子模块,必须回顶层把 .bsf 重新生成一遍,再重新编译。有一次我改了一个音符分频器的端口位宽,从 12 位改成 11 位,顶层还是旧的符号,Quartus 只给了一个 warning,我直接下载,结果高音区全乱。从那以后我每次改完子模块,都会先 Clean Project 再全编译一遍,确认 Port 列表和顶层连线的位宽对得上,才敢下载。原理图这东西,图上一根线的错,比代码里一个分号的错难查十倍。希望帮到你。
本文还有配套的精品资源,点击获取