基于FPGA的音乐演奏电路:从分频器到状态机的完整设计
发布时间:2026/8/31 19:36:25来源:尧图网络
简介本资源是一套完整的基于FPGA的音乐演奏电路Verilog开发工程面向数字电路初学者、电子类专业学生及FPGA入门开发者解决从硬件逻辑设计到音频信号生成的全流程实践问题。项目涵盖音符解码、频率发生、PWM音量控制、时序调度等核心模块融合数字系统设计与基础音乐理论适用于课程设计、综合实验或竞赛备赛场景。压缩包共166个文件约13.85MB包含21个hdb/cdb编译数据库、21个v源文件含decoder.v、clk_divide.v、music_top.v等关键Verilog模块、7个qmsg/rpt综合与仿真报告、5个txt/readme文档说明与配置指引以及sof配置文件、qsf引脚约束、mif音符数据表等典型Quartus工程要素。已有750人学习下载提供可直接编译下载运行的完整工程结构支持FPGA板卡实机验证并附带备份文件与层次化设计信息便于理解模块划分与信号流向快速掌握数字音频硬件实现方法。 按键按下去的瞬间蜂鸣器里冒出一个干干净净的“Do”然后跟着节奏一个一个往下走最后串成一句完整的《小星星》。第一次在FPGA开发板上把音乐“跑”起来的时候那种整条信号链路都在自己掌控里的踏实感确实让人有点上瘾。这个被称为“基于FPGA的音乐演奏电路”的小项目是我带新人入门时非常喜欢让他们动手做的一个题目也适合所有刚接触Verilog和数字电路设计的同学。它看起来只是让蜂鸣器唱歌但背后其实是时钟分频、计数器、状态机、查表ROM、仿真验证、引脚约束这一整套FPGA开发基本功的浓缩。这篇文章就把这个项目的完整拆解写出来从音乐原理到模块设计从仿真到上板再到我踩过的各种坑一次讲清楚。1. 音乐演奏电路的核心原理和整体设计思路先说个很多人第一次听到这个题目都会有的疑问FPGA是干逻辑控制的怎么能“演奏音乐”要回答这个得先把音乐的本质拆开看。1.1 音乐的三要素音高、音长、音色任何一段旋律扒开来看就三个维度音高频率、音长时值、音色波形谐波构成。FPGA这种以时钟和逻辑为基本单元的数字器件恰好对前两个维度有天然的控制力。音高由频率决定。标准音A4是440HzC调的Do中央C是261.63Hz。要发出这些声音本质上就是让输出引脚按对应的频率翻转电平产生方波推动蜂鸣器发声。FPGA有固定的系统时钟比如常见的50MHz通过分频就能得到任意低于系统时钟的频率。音长由时值决定。一首曲子里的四分音符、八分音符对应的是一段确定的时间长度。FPGA用计数器就能精确计时到点就切换音符。精度能到纳秒级比人靠节拍器数拍子准得多。音色这个维度对于入门项目来说可以先放一放。用方波直接推蜂鸣器出来的声音就是老式电子表那种“滴滴”声谈不上音色。不过后面如果想升级音质可以在FPGA内部做DDS直接数字频率合成或PWM调制再配合外部DAC或低通滤波器来逼近正弦波。这个属于进阶玩法后面单独说。1.2 方案选型从蜂鸣器到音频输出的三条路线硬件输出方案直接决定了你的电路复杂度和音质上限做项目前要先把这条路线给定下来。我见过不少人在这一步没想清楚买了两三种器件反复折腾非常耽误时间。无源蜂鸣器直推这是最省事的方案。所谓无源蜂鸣器是指内部没有振荡电路必须外部给一个方波才会响。频率由你给的方波决定这和FPGA分频输出的思路完全对口。有源蜂鸣器则相反通电就响频率固定只能做报警器做不了音乐买的时候千万看清楚。无源蜂鸣器驱动电流很小FPGA引脚通常能直接带起来串一个几百欧的电阻保护引脚就够了。喇叭PWM功放蜂鸣器音量小、频响窄想声音大一点、低频好一点可以用喇叭。但喇叭需要比较大的驱动电流FPGA引脚带不动常用的做法是输出PWM波形经过一个三极管或者专用功放芯片比如LM386放大。PWM的载波频率要远高于音频频率一般用几百kHz再用RC低通滤波器把方波的高频谐波滤掉。DAC音频放大器这算音质天花板了。在FPGA内部用DDS或查找表生成正弦波采样数据通过并行或串行接口送给外部DAC芯片然后进功放。FPGA里存的也不再是一串频率值而是一段段波形采样点能做真正细腻的波形控制。这块的复杂度上了一个台阶但很多做信号发生器的项目都是这条路。对新手来说我的建议非常明确第一版先走无源蜂鸣器直推把逻辑全部调通音准、节奏都对了再考虑升级PWM喇叭方案。上来就搞DAC逻辑和硬件同时调问题一下子混在一起很难排查。1.3 系统框架模块划分和信号流向想清楚目标和输出路线之后整个电路就可以划分成几个职责单一的模块。这也是FPGA开发的一个基本素养先画架构再写代码别一上来就在一个always块里堆所有功能。我的系统分成了四块时钟模块把开发板上的50MHz或你板子的实际频率系统时钟接进来不做分频也可以用它作为全局时钟驱动所有模块同步工作。分频的事情交给具体功能模块做。分频模块输入一个音符编码比如用4位二进制表示低音到高音的常用音输出该音符对应的方波。内部就是把系统时钟按目标频率进行计数分频。节拍控制模块以固定频率产生节拍脉冲一拍一个脉冲状态机靠它推进到一个音符的结束时点。节拍速度用拨码开关或者参数调整。控制状态机模块内部ROM存着一首曲子的音符序列每收到一个节拍脉冲就切换到下一个音符把对应的音符编码送给分频模块。信号流向其实是一条非常清晰的单向链控制状态机从ROM取音符编码送到分频模块分频模块按频率翻转输出最终驱动蜂鸣器。时钟模块负责给所有时序逻辑打拍子。任何一环出问题都只影响自己这一段的逻辑定位起来特别方便。2. 核心模块的细节设计与参数计算整个项目的精华都在参数计算里。很多新手拿到代码能跑但让他换一首曲子、换一块晶振就不知道怎么改了。这章把每个模块的所以然讲透。2.1 分频器怎么用50MHz时钟“弹”出Do-Re-Mi分频器是整个电路最核心的模块。它的任务也很纯粹输入一个时钟频率输出一个指定频率的方波。以50MHz系统时钟为例要产生中央C261.63Hz的Do方法如下。先明确输出频率和时钟频率的关系。我们让计数器从0累加到某个阈值后翻转输出信号一次那么输出信号翻转一次需要N个时钟周期一个完整周期就需要2N个时钟周期。所以[ N \frac{F_{clk}}{2 \times F_{note}} ]把50MHz和261.63Hz代进去[ N \frac{50,000,000}{2 \times 261.63} \approx 95,555 ]意味着计数器从0计数到95,554每次到顶就把输出翻转一次这样输出的方波频率就是261.63Hz。同理每个音符的N值都可以算出来。我把C调低音5到高音1的常用音对照表直接整理出来开发时照着填就行。音符频率Hz分频计数N50MHz音符编码低音5 Sol196.00127,5514d1低音6 La220.00113,6364d2低音7 Si246.94101,2394d3中音1 Do261.6395,5554d4中音2 Re293.6685,1344d5中音3 Mi329.6375,8424d6中音4 Fa349.2371,5834d7中音5 Sol392.0063,7764d8中音6 La440.0056,8184d9中音7 Si493.8850,6244d10高音1 Do523.2547,7794d11高音2 Re587.3342,5674d12高音3 Mi659.2637,9214d13休止符0无输出4d0这里有两个容易被忽视的细节。第一N值要取整数所以频率会有微小偏差。以中央C为例95,555代入反推实际频率是261.626Hz和261.63Hz的偏差在万分之几人耳完全听不出来。但如果你把N算错一位比如少了个0频率就差了10倍听起来就完全对不上号了。第二计数器位宽要留够。95,555这个数转成二进制是17位2^17131,072所以计数器至少要声明成[16:0]或[17:0]。新手直接写个[7:0] counter仿真时看不出问题一上板音高全乱原因就在这。实际代码里我会用一个查找表实现音符编码到分频阈值的映射。用case语句把上面表格里的N值填进去就行简单直观综合器也会自动优化成ROM或MUX。2.2 音符编码与ROM查表方案分频模块解决了“单个音怎么发声”的问题那“整首曲子怎么唱”就得靠音符序列来控制。常见的做法有两种。第一种是ROM查表法。预先把一首曲子的音符编码按顺序写进一个ROM或数组状态机用地址计数器逐个读出每个节拍地址加一。这种做法的优点是非常规整想换曲子只需要替换ROM的内容加一首歌就是多一份ROM的事很适合做个点歌器。缺点是多了一个地址计数逻辑对新手来说状态机的状态转移要理清楚。第二种是状态机直接枚举。把每个音符做成一个状态状态转移条件就是节拍脉冲。比如《小星星》前几句就是Do状态 - Do状态 - Sol状态 - Sol状态 - La状态 - La状态 - Sol状态。在Verilog里用parameter定义每个状态状态机里一条条列出来。我带人做项目反而更推荐第二种。原因很简单ROM查表法虽然“专业”但状态机枚举能把音符编码、分频、节奏的关系一次性理清楚。等把状态机玩熟了再去看ROM查表法简直就是降维打击。而且状态机枚举天然支持跳转比如做个循环播放或者按键暂停都很好改。不过代码风格上要提醒一点状态机的状态不要用魔法数字一定要用parameter定义。不然一首歌几十个状态代码里全是4d4、4d8过两周你自己都看不懂在唱什么。2.3 节拍控制用计数器撑起节奏音高解决了接下来是音长。这是很多人做这个项目最容易翻车的地方。节拍的本质是一个精确的时间基准。假设一首歌是4/4拍每分钟120拍那一拍就是0.5秒。我们要让系统每0.5秒产生一个脉冲控制模块收到脉冲就切到下一个音符。实现方法和分频器几乎一样计数器从0累加到阈值就清零并输出一个单周期的脉冲信号。这里计数的是50MHz的时钟周期数[ M 50,000,000 \times 0.5 25,000,000 ]计数器位宽就要满足25,000,000这个数是25bit2^2416,777,216不够2^2533,554,432够了。在设计节拍参数时我习惯把“每拍计数数”定义成一个parameter这样想改歌曲速度只需要改一个数字。比如parameter BEAT_CNT 25_000_000;这是每分钟120拍的速度。如果想让曲子慢一点改成30_000_000就是每分钟100拍。另外不同音符的时值是通过“占几个节拍脉冲”实现的。四分音符占1拍二分音符占2拍八分音符占0.5拍。在状态机里八分音符就需要在同一个音符状态停留半个节拍周期。实现上有两种做法一是把状态机设计成每一个节拍脉冲切到下一个状态同时允许两个连续状态是同一个音符编码这样八分音符就写成两个相同的连续状态二是在状态机里再细分计数允许一个状态持续半拍。我倾向第一种因为状态的推进逻辑单一不容易出错。2.4 顶层控制逻辑状态机的正确打开方式状态机是这个项目的大脑。它的工作方式可以用一句通俗的话描述每个节拍脉冲到来我把当前的音符编码发给分频模块然后自己想一下个音符是什么。写状态机最关键的是分清“组合逻辑输出”和“时序逻辑状态跳变”。很多新手把这两件事写在一个always块里综合出来一堆奇怪的锁存器。我推荐三段式状态机写法一个always块负责状态寄存器更新一个always块负责次态判断一个always块或者assign语句负责输出。以《小星星》开头几个音符为例状态跳变大概是这样的localparam S_REST 3d0; localparam S_DO1 3d1; localparam S_DO2 3d2; localparam S_SOL1 3d3; localparam S_SOL2 3d4; localparam S_LA1 3d5; localparam S_LA2 3d6; localparam S_SOL 3d7; always (posedge clk or negedge rst_n) begin if (!rst_n) state S_REST; else if (beat_pulse) state next_state; end always (*) begin case (state) S_REST: next_state S_DO1; S_DO1: next_state S_DO2; S_DO2: next_state S_SOL1; S_SOL1: next_state S_SOL2; S_SOL2: next_state S_LA1; S_LA1: next_state S_LA2; S_LA2: next_state S_SOL; default: next_state S_REST; endcase end always (*) begin case (state) S_REST: note_code 4d0; S_DO1, S_DO2: note_code 4d4; S_SOL1, S_SOL2: note_code 4d8; S_LA1, S_LA2: note_code 4d9; S_SOL: note_code 4d8; default: note_code 4d0; endcase end上面这段代码很直观地展示了“一拍一切”的思路。八分音符就是把同一个音符编码放在两个连续状态里每个状态占一个节拍脉冲。这首曲子的完整版本状态数比较多但单个状态的功能很简单扩展起来也顺手。这里再补充一个我走过的弯路状态编码要用独热码还是二进制码。在资源不紧张的入门级FPGA上二进制码完全够用代码也好读。独热码适合状态数很多、对时序性能要求高的场景这个项目用不上。3. 从仿真到上板的完整实操流程原理讲得再多不如动手跑一遍。这一章给出我推荐的一套实操流程从建工程到上板全走一遍中间穿插一些很具体的操作细节。3.1 工程搭建与代码组织开发环境我用的是Vivado如果你用的是QuartusIntel FPGA或者国产FPGA的IDE思路完全一致只是界面和约束文件的格式略有差异。新建工程的时候芯片型号一定要选对。不同开发板用的FPGA芯片不同比如常见的有Artix-7 XC7A35T、Cyclone IV EP4CE10等。选错了芯片后面综合布局布线会报错引脚约束也匹配不上。工程建好之后我习惯把源码按模块分文件组织top.v顶层模块负责例化所有子模块定义端口连接clk_div.v分频模块音符编码转方波输出music_ctrl.v控制状态机模块节拍推进和音符序列管理beat_gen.v节拍发生器输出节拍脉冲这样做的好处是每一个文件都短小精悍查问题的时候能快速定位。很多人喜欢把几百行代码写在一个文件里不是不行但等到上板出问题要调试的时候就会体会到模块化拆分的痛苦。3.2 仿真验证先“听”后“弹”写仿真测试平台testbench是这个项目里最值得花时间的环节。很多人喜欢直接上板靠耳朵听但那样调试效率极低。好的仿真能让你在几分钟内确认逻辑正确性。Testbench的基本结构是例化被测试的顶层模块生成时钟信号拉高复位然后观察波形。timescale 1ns / 1ps module tb_music(); reg clk; reg rst_n; wire buzzer_out; top u_top( .clk (clk), .rst_n (rst_n), .buzzer (buzzer_out) ); initial begin clk 0; rst_n 0; #100; rst_n 1; end always #10 clk ~clk; // 50MHz时钟周期20ns endmodule这里有一个关键的仿真技巧真实的节拍计数和分频计数都太大了。比如节拍计数器要数25,000,000个周期仿真需要跑25,000,000 × 20ns 0.5秒。这个时间在仿真器里能跑但波形看起来非常长而且如果你想验证整首曲子那就要等好几分钟。我的做法是仿真的时候把分频阈值和节拍阈值都临时缩小比如把BEAT_CNT从25,000,000改成2500把分频阈值也等比缩小。这样逻辑链路完全一致但仿真时间从几秒缩到几微秒波形一眼就能看完。逻辑验证通过之后再改回真实参数上板。这个“参数临时缩小”的方法在实际开发中非常实用所有带计数器的项目都能这么干。仿真时要重点看三个信号分频模块的输出是否按预期翻转、节拍脉冲是否周期出现、状态机是否按顺序切换音符。如果你用ROM查表方案还要检查地址是不是按顺序加一。3.3 引脚约束与硬件连接仿真通过后真正的硬件挑战才开始。首先要把FPGA的引脚和实际硬件对应起来。你需要翻开开发板的原理图找到三样东西系统时钟引脚、蜂鸣器引脚、复位按键引脚。比如某块常见的开发板上系统时钟是50MHz连接在FPGA的E3引脚蜂鸣器连接在M14引脚复位按键连接在C12引脚。这些引脚号每个板子都不一样必须查原理图确认不能照抄别人的约束文件。在Vivado里约束文件.xdc的核心内容就是告诉工具逻辑端口对应物理引脚、电平标准是什么。一个典型的引脚约束长这样set_property PACKAGE_PIN E3 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property PACKAGE_PIN M14 [get_ports buzzer] set_property IOSTANDARD LVCMOS33 [get_ports buzzer] set_property PACKAGE_PIN C12 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n]电平标准大多用LVCMOS33因为大部分开发板的银行电压是3.3V。如果你接的是2.5V或1.8V的外设电平标准就要相应调整否则可能工作不稳定甚至烧坏引脚。硬件连接方面蜂鸣器的长脚是正极短脚是负极。FPGA引脚输出方波直接驱动无源蜂鸣器即可。我强烈建议在蜂鸣器正极和FPGA引脚之间串一个100到300欧的电阻不仅限流保护引脚也能让声音稍微柔和一点。有些无源蜂鸣器内部有线圈直接连引脚在输出翻转瞬间会有反电动势串电阻能有效缓解。3.4 上板调试与最终验证上板之后第一件事不是听音乐好不好听而是先确认几个基本现象开发板电源灯正常程序下载成功没有报错按下复位按键后蜂鸣器开始发声声音有节奏不是一响到底也不是完全无声如果完全不响先用示波器或者逻辑分析仪测FPGA引脚有没有方波输出。没有示波器的话可以用万用表测引脚平均电压。方波的平均电压大约是1.65V左右。如果引脚电压一直是0或一直高电平说明分频逻辑有问题或者引脚约束不对。如果声音响了但节奏不对优先检查节拍计数器的阈值有没有写错。比如把50000000写成了5000000节拍速度就快了10倍。这个错误很隐蔽因为仿真时缩小参数反而掩盖了问题。如果音准不对比如听起来整体偏高或偏低先把某一个音符的频率用示波器量一下或者用手机上的调音器APP对准。大多数情况下是分频阈值算错或计数器位宽不够导致溢出。说到验证我推荐一个很实用的土办法把FPGA的同一个音符信号引到一个LED上。因为方波频率在几百赫兹LED看起来是常亮实际在快速闪烁但如果你设置一个很低的分频值比如10HzLED就会明显闪烁。用手机慢动作视频拍LED数一秒内闪几下就能验证分频逻辑是否正确。这个方法在没有仪器的条件下非常管用。4. 常见问题与调试技巧实录做硬件项目踩坑是常态。这一章把我见过和经历过的高频问题整理出来写成一个速查表再挑几个典型的展开讲讲。4.1 典型问题速查表现象可能原因排查方法蜂鸣器完全不响有源/无源蜂鸣器选错确认是无源蜂鸣器蜂鸣器完全不响引脚约束错误对照原理图检查XDC蜂鸣器完全不响复位一直拉低检查复位按键电平蜂鸣器一直响不停分频器输出恒为高/低示波器测引脚检查N值有声音但音调不对分频阈值计算错误调音APP对准单音有声音但音调不对计数器位宽不够确认位宽 ≥ log2(N)节奏忽快忽慢节拍阈值参数不一致检查BEAT_CNT声音发闷/声音小蜂鸣器驱动电流不足换三极管放大驱动仿真正确上板不响引脚未约束或约束错误检查时序报告和XDC上电偶尔不响复位时序不稳定增加复位延时或外部复位芯片4.2 仿真与上板不一致的坑最常见也最让人抓狂的就是“仿真明明对了上板就是不行”。根据我的经验原因通常集中在三处。第一引脚没有约束或约束错误。仿真时信号是虚拟的不需要管引脚。上板后如果没有正确的引脚约束工具会随机分配或者直接报错。最坑的是有些工具在未约束时会自动分配引脚看起来下载成功了实际上信号根本没接到蜂鸣器上。所以烧录之前一定要看implementation的pin report确认每个端口都绑到了正确的引脚。第二复位逻辑的差异。仿真时复位信号是理想化的从0变1只是一瞬间的事。但实际上电时FPGA内部的复位信号可能因为电源爬坡、时钟未稳定等原因出现毛刺。我习惯在顶层模块里加一个上电延时复位用计数器对系统时钟计数比如数满100,000个周期后再释放复位。这个做法能解决很大一部分“上电不响”的问题。第三时序约束缺失导致的亚稳态或时序违例。这个项目逻辑简单通常不会有时序收敛问题但如果你的状态机跳变沿和时钟边沿配合不当个别情况下会出现偶尔跳错状态的现象。解决方法是在XDC里加上主时钟约束create_clock -period 20.000 -name sys_clk [get_ports clk]时钟约束加上之后工具会做真正的时序分析如果存在严重违例会明确报出来方便提前处理。这个习惯要从入门就养成等以后做高速接口的时候会省非常多事。4.3 音色和音质的进一步提升如果你做完第一版觉得“电子琴”效果太塑料了那么可以朝这几个方向做无损升级。第一个方向是PWM模拟正弦波。原理是让FPGA输出一个高频PWM信号占空比按正弦波的幅度变化再经过一个低通滤波器把高频载波滤掉剩下的就是平滑的正弦波。这个技术本质上就是1bit DAC在很多音频应用里都有。比直接用方波推蜂鸣器声音会柔和非常多。第二个方向是查表DDS。先把一个周期的正弦波采样成256个点存到ROM里然后用相位累加器按目标频率去扫ROM输出对应的采样值。通过改变相位累加步长就能精确控制输出频率。相比分频器DDS的频率分辨率可以做得很高而且输出波形连续性好。这个方案在函数信号发生器里非常经典。第三个方向是增加交互。比如用按键输入音符做成一个可弹奏的电子琴或者用拨码开关选择不同的歌曲做个点歌机再或者用UART接收电脑端下发的音符序列实现随时换曲目。这样项目就从单纯的“播放器”变成了“乐器播放器”的综合体。还有一个非常值得做的扩展是把音乐播放和数码管显示结合起来。用数码管显示当前播放音符的简谱数字这样调试和演示效果都会上一个档次。音符编码到数码管段码的映射又是一张查找表的事。回到这个项目本身我觉得它最大的价值不在于让蜂鸣器唱歌这件事本身而在于它把数字电路设计里最核心的几个要素——时钟、计数、状态、查表——全部串在了一起而且每一步都能用耳朵听到结果。这种“听得见”的反馈比单纯看LED闪烁更直观也更能建立对数字电路的直觉。我后来接触DDS信号发生器、PCIE、千兆以太网这些复杂项目回头想很多基本功都是在这个小项目里打下的。如果你正在学FPGA或者带新人入门不妨从这个音乐演奏电路开始。先让它唱起来再让它唱得准最后让它唱得好听每一个阶段都有实实在在的收获。本文还有配套的精品资源点击获取
网站建设高端定制企业官网