ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于FPGA的音乐演奏电路设计:从分频原理到Verilog实现

基于FPGA的音乐演奏电路设计:从分频原理到Verilog实现 简介本资源是一套基于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.6395553D4293.6685129E4329.6375833F4349.2371581G4392.0063776A4440.0056818B4493.8850619C5523.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休止, 1C4, 2D4 ... 8C5 output reg [16:0] half_period ); always (*) begin case (note_idx) 5d0: half_period 17d0; // 休止 5d1: half_period 17d95553; // C4 261.63Hz 5d2: half_period 17d85129; // D4 293.66Hz 5d3: half_period 17d75833; // E4 329.63Hz 5d4: half_period 17d71581; // F4 349.23Hz 5d5: half_period 17d63776; // G4 392.00Hz 5d6: half_period 17d56818; // A4 440.00Hz 5d7: half_period 17d50619; // B4 493.88Hz 5d8: half_period 17d47776; // C5 523.25Hz default: half_period 17d0; // 未定义音高默认休止 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 17d0; tone 1b0; end else if (half_period 17d0) begin cnt 17d0; tone 1b0; // 休止符输出低电平 end else if (cnt half_period - 1) begin cnt 17d0; tone ~tone; // 到半周期翻转一次 end else begin cnt cnt 1b1; end end endmodule这个模块有两个细节值得注意。第一counter的位宽选17位是因为最大计数值95553对应的二进制需要17位如果位宽不够综合工具不会报错但计数器会提前回绕输出的频率就完全错了。保险的做法是用系统函数$clog2计算localparam WIDTH $clog2(MAX_CNT 1);一劳永逸。第二休止符的处理是独立的因为无声和堵住嘴的方波是两回事把tone固定拉低才是真正的休止。3.3 节拍控制与曲谱状态机有了音高下一步是节奏。我一直觉得能响不算成功能按节拍把一首曲子弹完才算。节拍的本质就是每个音符响多久这同样需要一个计数器。以120BPM每分钟120拍为例一拍持续0.5秒在50MHz时钟下就是25000000个周期。我定义一个beat_cnt计数器每数到2500万就认为过去了1拍。曲谱里每个音符的时长用拍数表示比如四分音符是1拍八分音符是半拍二分音符是2拍。曲谱怎么存我用的方法是把一首曲子切成一个个步骤每个步骤包含音符索引和持续拍数用一个case语句描述播放状态机逐条读取。以《小星星》第一句为例简谱是 1 1 5 5 6 6 5后面接休止对应的状态机片段如下localparam NOTE_C4 5d1; localparam NOTE_G4 5d5; localparam NOTE_A4 5d6; localparam REST 5d0; // 每个 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 1b1; end end else begin beat_cnt beat_cnt 1b1; 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 20d0; key_stable key_in; end else begin cnt cnt 1b1; end end else begin cnt 20d0; 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 按钮失控与乱音消抖和状态机边界按键问题大多不是消抖不够而是没有输出单脉冲。很多人写完消抖模块直接把稳定的电平信号接到播放逻辑上结果按下期间状态机一直在触发自然就乱了。记得在消抖模块后面加一个上升沿检测输出一个时钟周期的脉冲。另外手动演奏模式下如果有多个按键同时按下我的处理是给按键编号设定优先级比如低编号的优先这样至少在逻辑上可预期不至于两个音符信号的边沿打架。状态机的边界条件也要检查播完最后一拍能不能正确回到IDLERESET按下时能不能从任意状态回到初始这些边界问题在仿真里不一定会暴露上板后却会变成偶尔卡死的玄学问题。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学习最迷人的地方。本文还有配套的精品资源点击获取
返回列表