☰
基于FPGA的音乐演奏电路设计:从分频原理到Verilog实现
2026/10/14 7:29:44 网站建设 项目流程

简介:本资源是一套基于FPGA实现音乐自动演奏的完整Verilog开发工程,面向数字电路初学者、嵌入式硬件爱好者及高校电子类课程实践者,解决从音符编码、频率生成到音频输出的全流程硬件设计问题。压缩包共166个文件,约13.85MB,涵盖核心Verilog源码(如decoder.v、clk_divide.v、music_top.v等)、Quartus工程配置文件(.qpf、.qsf、.bdf)、综合与仿真报告(.rpt、.vwf、.logdb)、IP配置信息(.mif、.sof)及备份与调试辅助文件(.bak、.hdb、.cdb),结构完整,便于理解FPGA音乐系统各模块协同逻辑。已有750人学习下载,资源包含可直接编译下载的顶层工程、分模块注释清晰的RTL代码、典型音阶频率映射表及基础DA转换接口说明,有助于掌握时序控制、分频设计、ROM查表发声等关键技能,并为扩展多音轨、PWM音量调节等进阶功能提供可靠起点。

1. 项目概述:一块FPGA板子能演奏什么

做FPGA的第一年,我干过一件特别有成就感的事:让一块普通的FPGA开发板上电之后,从蜂鸣器里完整地放出了《小星星》。说实话,音质和早年那种"叮咚"门铃差不多,但旁边不懂硬件的室友听完愣了半天——"这玩意儿还能唱歌?"

这个"基于FPGA的音乐演奏电路"项目,本质上就是做一个以FPGA为控制核心的音调发生器。它能做两件事:一是按预设曲谱自动播放音乐,二是通过按键当作简易电子琴来实时演奏。无论哪种模式,核心都是同一件事:把乐谱上的"音符"翻译成硬件逻辑能理解的"频率",再把频率转换成驱动扬声器的电信号。

为什么选FPGA来做这个看似"杀鸡用牛刀"的项目?我自己做完之后体会很深:首先是时序的可控性,音高要求的是高精度的周期性波形,FPGA的时钟分频能做到百万分之一量级的频率精度;其次是并行性,学完这个项目,你会对"状态机+数据通路"这套FPGA设计的核心方法论有实实在在的体感,而不是停留在课本上。无论你是刚学Verilog的初学者,还是想给学生找一个综合性教学项目的老师,这个题目都值得动手做一遍。它涵盖的模块拆解、参数计算、仿真调试、上板验证,几乎是一部微型FPGA开发流程的缩影。

2. 核心原理:数字世界里怎么描述一个音符

2.1 十二平均律与音符频率表

要生成音乐,得先回答一个基本问题:一个音符在物理上到底是什么?答案是振动频率。国际标准把A4定为440Hz,也就是钢琴上小字一组的la,每秒振动440次。其他所有音符的频率,都不是拍脑袋定的,而是按照十二平均律算出来的:每相邻两个半音之间的频率比是常数,等于2的十二次方根。

[ f(n) = 440 \times 2^{\frac{n-69}{12}} ]

这里n是MIDI标准里的音符编号,A4对应69。刚才这个公式说的是"从A4往上数n-69个半音",每上升一个半音,频率乘一次1.059463。拿C4来算,C4比A4低9个半音,所以C4频率约等于440除以1.059463的9次方,算出来就是261.63Hz。这个公式可以一直往上套,也一直往下套,所以整个十二平均律体系是"自洽"的:任何一个音高,都能用唯一一个频率来描述。

我在做查表之前,习惯把要用的音符频率先算好列成一张表,再根据系统时钟算出分频参数。下面这张表是C大调一个八度里最常见的8个音,以及50MHz系统时钟下对应的分频计数参数,后面写代码会直接用。

音符频率(Hz)50MHz下分频半周期计数值(四舍五入)
休止符00(输出直接置低)
C4261.6395553
D4293.6685129
E4329.6375833
F4349.2371581
G4392.0063776
A4440.0056818
B4493.8850619
C5523.2547776

这张表看着简单,但其中两个细节值得说清楚。第一,分频计数值用的是"半周期",因为我们想输出方波,要先数半个周期的时钟数,翻转一次电平,再数半个周期,再翻转回来,这样一个完整周期才是目标频率。第二,四舍五入很重要,取整误差经过人耳放大后一般听不出来,但如果你直接截断取整,误差方向是固定的,个别音符的偏差会比四舍五入明显。为了这一点差别,不值得,老老实实四舍五入。

2.2 三种产生音调的方法,我为什么选了查表分频

确定了目标频率之后,下一步就是"怎么用数字电路产生这个频率"。我自己尝试过三种方案,这里把它们的思路和取舍都讲一下。

方案一:纯分频计数器。最直观的做法是给每个音符单独写一个计数器,各自数到对应的半周期计数值,然后翻转输出。好处是逻辑清晰,初学者一眼就能看懂;坏处是代码冗余,8个音符就得写8个几乎一样的计数器模块,如果要扩展到多个八度,代码会膨胀得非常难受。

方案二:查表+动态装载分频器。这是我在最终版里采用的方案。分频器模块只有一个,它的计数上限不是写死的,而是来自一个外部的输入信号。系统切换音符时,只需要把查表得到的新计数值送给分频器,分频器就会自动按新频率工作。这样音符越多,只需要扩展查表,不需要新增硬件逻辑。这个方案的缺点是需要想清楚"切换边界":如果计数到一半突然改计数值,当前这个周期不算完整,但对应到听觉上,这一瞬间的过渡对人耳来说是无法感知的,所以实际用起来完全没问题。

方案三:DDS直接数字频率合成。DDS的思路和前面两种完全不同,它用一个累加器来做"相位累加",输出查找表里的波形幅度值。核心公式是相位增量 = (目标频率 × 2^N) / 系统时钟。以32位累加器、50MHz时钟为例,A4对应的相位增量约等于37796。DDS的优势是可以无缝切换频率,可以做滑音、扫频、多声道叠加,改频率只需要改一个增量值。但代价是逻辑复杂,还要配波形ROM,对只想播放一首固定曲子的入门项目来说,属于重量级武器。我个人的建议是:第一次做这个项目,老老实实用方案二,把"响→准→唱完整曲"这条链路跑通;等你理解了底层逻辑,再回到DDS,感受会完全不一样。

2.3 音频输出路:蜂鸣器、喇叭还是功放

信号生成出来了,最后还得让它"出声"。这一环节的教训特别多,我自己在这里栽过跟头,新手尤其要注意。

有源蜂鸣器 vs 无源蜂鸣器,这是第一个必须搞清楚的坑。有源蜂鸣器内部自带振荡电路,通电就响,而且频率固定,你给它外部方波信号它也不会变调,它只响应"通还是断"。这种蜂鸣器只能做报警器,做不了音乐演奏。无源蜂鸣器没有内部振荡源,你给它什么频率的方波,它就发出什么频率的声音,这才是我们要的。区分方法很简单:看引脚旁边的丝印,或者拿万用表测电阻,无源的通常电阻都偏小。买之前直接问清楚"无源电磁式蜂鸣器",不然快递到了你会收获一个只能"哔——"的喇叭。

驱动方式也要注意。FPGA引脚的驱动能力有限,直接驱动蜂鸣器经常出现声音发闷、响度不够的问题。我试过用NPN三极管(S8050这类)做开关放大,效果立竿见影。三极管基极接FPGA引脚,通过一个1kΩ电阻限流,集电极接蜂鸣器负极,发射极接地,蜂鸣器正极接电源。FPGA输出高电平时三极管导通,蜂鸣器通电发声;输出低电平时断开。如果你用的是带功放的开发板(板载喇叭或者音频接口),那更省事,FPGA直接输出方波到功放输入就行。

外部喇叭+功放情况也常见,比如手头只有普通8Ω小喇叭,就需要LM386或者PAM8403这类功放模块。方波信号通过功放放大后驱动喇叭,音量比蜂鸣器大得多,适合在教室里演示。如果想追求更好的音质,可以用PWM输出加RC低通滤波,把方波变成近似正弦波,但这是后话了,第一版不用追求到这个程度。

3. 实操实现:从模块到一首完整的曲子

3.1 顶层架构与管脚规划

老规矩,设计之前先画顶层框图。我的划分是四个模块:按键消抖模块、曲谱播放状态机、音符查表分频器、音频输出驱动。每个模块各司其职,互相之间只通过清晰的总线信号连接。

clk_50MHz ──→ 按键消抖 ──→ 播放状态机 ──→ 音符查表 ──→ 分频器 ──→ 蜂鸣器 rst_n ──→ 模式切换 按键[3:0] ──→ 手动演奏输入

管脚规划这一环看似琐碎,但特别容易出错。开发板上的物理引脚五花八门,蜂鸣器可能连在某个特定引脚上,按键分布在另一组引脚。务必先在原理图或者板卡手册里找到"蜂鸣器"、"按键"对应的FPGA引脚编号,再写约束文件。我之前见过有同学凭感觉写引脚号,结果综合下载后没反应,查了半天才发现引脚约束根本不对。另外,如果蜂鸣器是负逻辑驱动(低电平响),还要在代码里多取一次反,这个细节在约束文件旁边顺手记一笔,免得忘了。

3.2 音符查表与分频器实现

这里把最终版的Verilog贴出来,代码不复杂,但包含几个容易踩坑的点,我会逐个说明。

首先是音符查表模块。它就是一个纯组合逻辑case,输入音符索引,输出对应的半周期计数值:

module note_lut ( input wire [4:0] note_idx, // 0=休止, 1=C4, 2=D4 ... 8=C5 output reg [16:0] half_period ); always @(*) begin case (note_idx) 5'd0: half_period = 17'd0; // 休止 5'd1: half_period = 17'd95553; // C4 261.63Hz 5'd2: half_period = 17'd85129; // D4 293.66Hz 5'd3: half_period = 17'd75833; // E4 329.63Hz 5'd4: half_period = 17'd71581; // F4 349.23Hz 5'd5: half_period = 17'd63776; // G4 392.00Hz 5'd6: half_period = 17'd56818; // A4 440.00Hz 5'd7: half_period = 17'd50619; // B4 493.88Hz 5'd8: half_period = 17'd47776; // C5 523.25Hz default: half_period = 17'd0; // 未定义音高默认休止 endcase end endmodule

然后是分频器模块。它接收查表过来的半周期计数值,输出方波:

module tone_gen ( input wire clk, input wire rst_n, input wire [16:0] half_period, // 0 表示休止 output reg tone ); reg [16:0] cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 17'd0; tone <= 1'b0; end else if (half_period == 17'd0) begin cnt <= 17'd0; tone <= 1'b0; // 休止符:输出低电平 end else if (cnt >= half_period - 1) begin cnt <= 17'd0; tone <= ~tone; // 到半周期,翻转一次 end else begin cnt <= cnt + 1'b1; end end endmodule

这个模块有两个细节值得注意。第一,counter的位宽选17位,是因为最大计数值95553对应的二进制需要17位,如果位宽不够,综合工具不会报错,但计数器会提前回绕,输出的频率就完全错了。保险的做法是用系统函数$clog2计算,localparam WIDTH = $clog2(MAX_CNT + 1);,一劳永逸。第二,休止符的处理是独立的,因为"无声"和"堵住嘴的方波"是两回事,把tone固定拉低才是真正的休止。

3.3 节拍控制与曲谱状态机

有了音高,下一步是"节奏"。我一直觉得,能响不算成功,能按节拍把一首曲子弹完才算。节拍的本质就是"每个音符响多久",这同样需要一个计数器。

以120BPM(每分钟120拍)为例,一拍持续0.5秒,在50MHz时钟下就是25,000,000个周期。我定义一个beat_cnt计数器,每数到2500万就认为过去了1拍。曲谱里每个音符的时长用拍数表示,比如四分音符是1拍,八分音符是半拍,二分音符是2拍。

曲谱怎么存?我用的方法是把一首曲子切成一个个"步骤",每个步骤包含音符索引和持续拍数,用一个case语句描述,播放状态机逐条读取。以《小星星》第一句为例,简谱是 1 1 5 5 6 6 5(后面接休止),对应的状态机片段如下:

localparam NOTE_C4 = 5'd1; localparam NOTE_G4 = 5'd5; localparam NOTE_A4 = 5'd6; localparam REST = 5'd0; // 每个 step 用 {duration(拍数), note_idx} 表示 // 以 4 拍为一小节,这里只示意第一句 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin step_idx <= 0; beat_cnt <= 0; note_idx <= REST; state <= IDLE; end else begin case (state) IDLE: begin if (start_flag) begin state <= PLAY; step_idx <= 0; beat_cnt <= 0; end end PLAY: begin // 读取当前step的音符 step_note_sel(step_idx); // 设置 note_idx if (beat_cnt >= BEAT_CNT_MAX - 1) begin beat_cnt <= 0; // 当前拍结束,进入下一个step if (step_idx == LAST_STEP) begin state <= DONE; // 播完 end else begin step_idx <= step_idx + 1'b1; end end else begin beat_cnt <= beat_cnt + 1'b1; end end // ... DONE 状态回到 IDLE endcase end end

这里我把"半拍"的处理方式说一下。如果曲子里有八分音符,简单起见可以全部用最小节拍单位(比如半拍)作基准,四分音符记作2,八分音符记作1,二分音符记作4。这样整个曲谱状态机只需要一个统一的拍号逻辑,不用为"半拍能不能整除"操心。

3.4 按键消抖与手动演奏模式

自动播放只是"听",手动演奏才是"玩"。这需要把按键接进来,每个按键代表一个音符,按下就发对应的频率,松开就回到休止。

机械按键有个典型毛病:按下和松开的瞬间,触点会弹跳,持续5~20ms,期间电平忽高忽低。如果不做消抖,你按一次按键,FPGA可能看到的是七八个"一次按压",演奏出来的就是一团乱音。

消抖的原理也不复杂:连续采样按键电平,只有当电平稳定持续一段时间(我习惯用20ms)才认为按键状态真正变化了。在50MHz下,20ms是100万个时钟周期,需要一个20位的计数器:

module key_debounce #( parameter CNT_MAX = 1_000_000 // 20ms @ 50MHz )( input wire clk, input wire key_in, output wire key_pressed_pulse // 按下瞬间的单脉冲 ); reg [19:0] cnt; reg key_stable, key_prev; always @(posedge clk) begin if (key_in != key_stable) begin // 输入与稳定值不一致,开始计数 if (cnt == CNT_MAX) begin cnt <= 20'd0; key_stable <= key_in; end else begin cnt <= cnt + 1'b1; end end else begin cnt <= 20'd0; end end // 边沿检测,输出一个时钟周期的脉冲 always @(posedge clk) begin key_prev <= key_stable; end assign key_pressed_pulse = key_stable & ~key_prev; endmodule

另一个细节是:手动演奏模式和自动播放模式需要互斥。我的做法是一个模式选择键,每按一次在两种模式之间切换。切到手动模式时,自动播放状态机进入IDLE,播放用音符全部置为休止,避免两个信号同时在驱动蜂鸣器。同一条信号线被两个模块同时赋值,在FPGA里是致命的Multiple Driver错误,这个后面排查章节还会说。

3.5 上板调试流程

最后说说从写完代码到听到声音的调试顺序。我的习惯是严格按下面四步走,每步都验证通过再进下一步,避免问题叠加后无从排查。

第一步,仿真验证。不用写太复杂的testbench,就测一件最重要的事:给一个音符索引,比如C4,看仿真波形里tone信号的周期是不是约等于1/261.63秒,也就是3.82ms。这一步能排除大部分逻辑错误。

第二步,单音验证。把代码烧进板子,让蜂鸣器持续播放一个固定的音符,比如A4。用耳朵听,对比标准音叉或手机调音软件,确认音高基本准确。如果有示波器或者逻辑分析仪,直接量FPGA引脚出的频率,更靠谱。

第三步,播放全曲。把状态机换成完整的曲谱,听整首曲子的节奏是否自然、音符切换是否有突兀的杂音。

第四步,加入按键模式和模式切换。到这一步再处理的,才是交互层面的问题,比如消抖时长是否需要调整、多个按键同时按下的优先级怎么定。

我自己强烈建议不要跳步。很多同学急性子,一上来就整曲播放加按键模式一起上,结果上板后又是无声又是乱音,根本分不清是分频的错还是节拍的错。踏实走完四步,反而最快。

4. 常见问题与排查技巧实录

这个项目我做了不止一遍,也帮同学排查过无数回,下面这几个问题出现的频率最高,我按"症状—原因—解法"的格式整理成速查表。

症状大概率原因排查手法
上电完全无声用了有源蜂鸣器,只响应通断换无源蜂鸣器,或先单独给不同频率方波测试
上电完全无声引脚约束错误,信号没送到蜂鸣器脚看原理图核对约束,用ILA或逻辑分析仪抓引脚
上电完全无声使能信号没拉高,播放状态机停在IDLE加一个调试LED输出,观察状态是否流转
有声音但音高偏低系统时钟实际不是50MHz查板卡手册确认时钟频率,重新计算分频参数
有声音但音偏高计数器位宽不够提前回绕检查counter声明位宽,用$clog2计算
按一次按键出好几个音消抖时间太短或没做消抖把消抖计数器加大到20ms,确认输出是单脉冲
自动播放和手动模式互相干扰两个模块同时驱动蜂鸣器信号用模式信号与门隔离,确保任一时刻只有一路有效
综合报Multiple Driver同一个reg在多个always块里赋值重构代码,把逻辑合并到一个always块,或拆成独立信号

4.1 上电无声:先分清三类原因

无声是新手遇到最多的拦路虎,但好消息是它的原因就三大类:硬件选型、管脚连接、逻辑使能。硬件选型在有源/无源蜂鸣器这一关就能卡住一批人,我前面已经强调过了。管脚连接排错的思路是"信号到底有没有到引脚",用开发板自带的LED做验证很有效——把驱动蜂鸣器的信号同时接一个LED,如果LED有规律闪烁而蜂鸣器不响,说明信号到了引脚,问题在蜂鸣器驱动电路;如果LED也不闪,那就是逻辑层面根本没输出,回头查状态机和使能信号。

4.2 音准不对:几乎都是分频比的问题

音准不对,十有八九是分频参数和实际系统时钟不匹配。最常见的情况是初学者以为所有开发板都是50MHz,结果板子是25MHz或100MHz,一上来频率就差一倍,整个曲子听起来像开了倍速或慢放。还有的情况是计数器比较条件写错:cnt >= half_period - 1和cnt == half_period - 1区别在于前者永远不会漏掉最大值,后者如果half_period变化而cnt还没来得及归零,可能永远等不到相等那一刻。我用的是>=,稳妥。

4.3 按钮失控与乱音:消抖和状态机边界

按键问题大多不是消抖不够,而是"没有输出单脉冲"。很多人写完消抖模块,直接把稳定的电平信号接到播放逻辑上,结果按下期间状态机一直在触发,自然就乱了。记得在消抖模块后面加一个上升沿检测,输出一个时钟周期的脉冲。另外,手动演奏模式下如果有多个按键同时按下,我的处理是给按键编号设定优先级,比如低编号的优先,这样至少在逻辑上可预期,不至于两个音符信号的边沿打架。状态机的边界条件也要检查:播完最后一拍能不能正确回到IDLE?RESET按下时能不能从任意状态回到初始?这些边界问题在仿真里不一定会暴露,上板后却会变成"偶尔卡死"的玄学问题。

4.4 综合时序报错的排查

最后一个常见问题是综合和实现阶段的报错。Multiple Driver是我见过最多的错误,它的含义是一个信号(wire或reg)被多个always块或者assign语句赋值。FPGA里每个信号只能有一个驱动源,如果你在播放状态机里给note_idx赋值,又在手动模式逻辑里给note_idx赋值,综合工具就会报错。解法是把这两段逻辑合并到一个always块,用模式信号做分支;或者把信号拆成两个内部信号,最后用一个三目选择器合并。另一个高频问题是计数器位宽不够,综合工具不会报错,但行为就是不对,所以我一再强调用$clog2算位宽,别靠目测。

5. 扩展方向与几点个人心得

5.1 从单声部到多声部:DDS带来的升级空间

做完上面这些,你已经拥有一个能自动播放、能按键演奏的单音音乐电路了。如果还想往前走,我建议的下一站是把单音升级成多声部和弦。这时候前面提到的DDS方案就派上用场了:一个NCO产生一个声部,两个NCO同时工作,各自产生对应音高的方波/正弦波,然后在输出端做加法,就得到了双音效果。配合相位累加器"改一个增量值就能无缝切换频率"的特性,你还可以做滑音、颤音这些音乐效果。

具体来说,32位DDS累加器、50MHz时钟下,生成一个440Hz的A4信号,相位增量是(440 × 2^32) / 50000000 ≈ 37796。想生成两个声部,就做两个相位累加器,增量分别按各自音高算好,输出相加后经过脉冲密度调制或者PWM送给功放。当然,输出加法要注意幅度,两个信号相加可能溢出,需要做截位或者缩放。这一步做通之后,再去研究用ROM存波形、做包络控制,就是一条清晰的学习路径。

5.2 我踩过的坑与最终建议

最后说几句掏心窝子的话。这个项目我前前后后做过三次,每次都有新收获,也每次都会踩不同的坑。第一次栽在有源蜂鸣器上,买回来怎么调都不变调,最后才意识到硬件选型就错了。第二次在引脚约束上栽了跟头,信号逻辑没问题,但物理上没送到蜂鸣器,白查了好几天代码。最让我印象深刻的还是"先仿真后上板"这条铁律——仿真里波形对了,上板才敢保证大概率对;仿真里稀里糊涂,上板面对的就只能是玄学。

如果让我给正在做或者准备做这个项目的人一条最核心的建议,那就是:第一版一定用最简单的结构,查表分频也好,单音播放也好,把链路完整跑通。先让它响,再让它准,最后再谈让它好听。任何一步跨过去,都会在后面的环节花几倍的时间来还债。

这个项目的可扩展空间其实比想象中大得多。曲谱存在ROM里之后,换歌只是改数据文件的事;按键加多了,就是一台小型电子琴;再多研究一步I2S协议和DAC,就能输出真正的立体声音乐。从一个蜂鸣器到一套完整的数字音频系统,中间缺的每一块知识,都会在下一步扩展中逼着你补上。而这,恰恰是FPGA学习最迷人的地方。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询