找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2277

积分

0

好友

321

主题
发表于 5 天前 | 查看: 21| 回复: 0

提到FPGA,很多人想到的是接口、时序和状态机。但当你用它来生成音乐,让抽象的代码驱动扬声器发出具体的声音时,对硬件的理解会进入一个全新的维度。

这次,我使用了一块STEP-MXO2开发板,从最基础的PWM调制和查表法起步,逐步构建了一个具备弹奏、和弦叠加甚至自动播放功能的简易电子琴系统。这个过程更像是一场用声音去理解FPGA内部逻辑的实践。

硬件设计

1. 工作原理

电子琴系统硬件框图

图1 电子琴系统硬件框图

整个系统的核心是“小脚丫”STEP-MXO2-C开发板。这款板卡支持网页版Web IDE进行编程,无需安装本地软件,当然也兼容传统的Lattice Diamond开发工具。

系统输入端共有15个按键。其中13个被定义为标准琴键,另外两个则用于扩展音程(升/降调)。

输出端提供了两种发声方式:喇叭或蜂鸣器,通过一个拨动开关进行选择。这两种发声器件的驱动方式和最终音效存在明显差异。

STEP-MXO2开发板实物图

2. 硬件原理图

清晰的原理图是硬件设计的基石。一份布局清爽、连线明确的原理图,远比杂乱堆砌的图纸更有助于建立系统级认知,也方便后续的调试与查错。

电子琴完整硬件原理图

图2 硬件原理图(包含按键输入、FPGA核心、喇叭驱动及蜂鸣器电路)

3. 蜂鸣器与喇叭的驱动差异

蜂鸣器:其发声主要依靠电压驱动压电陶瓷片。给压电材料施加交变电压,会导致其规律性形变,从而带动金属振膜振动发声。压电陶瓷通常在谐振频率附近工作,频带较窄。由于阻抗较高、工作电流较小,蜂鸣器的输出功率也相对较低。在本设计中,FPGA生成一路PWM波,通过控制三极管的开关来驱动蜂鸣器,并通过调节PWM的占空比来控制音量。

喇叭:其发声原理是基于电磁效应。音频信号流经线圈产生变化的磁场,该磁场与喇叭内的永磁体相互作用,产生力驱动音圈及连接的纸盆振动发声。喇叭阻抗较低,可以承载更大的功率,从而获得更高的音量。本设计采用PWM模拟DAC的方式产生模拟音频信号,经过后续的放大电路驱动喇叭发声,通过调节模拟信号的幅值来控制音量。

4. 声音特性对比

由于蜂鸣器谐振频率高、频带窄,其发出的声音较为尖锐,通常用于报警提示。而喇叭的频率响应更宽,在人耳可听范围内能还原更多声音细节,因此更适合用于播放音乐。

5. 模拟放大电路仿真与分析

FPGA的IO口通常只有高/低电平两种数字状态。要获得平滑的模拟信号,需要利用PWM(脉宽调制)来模拟DAC(数模转换)功能。通过精确调节PWM的占空比,可以在输出端得到一个等效的平均电压值。

这里的关键在于PWM频率的选择:频率需要与后端由R16、R17、C3组成的滤波电路相匹配。频率过高可能无法被有效滤波,频率过低则会引起信号失真。

功放芯片内部功能框图

图3 音频功放芯片功能框图

上图是所用功放芯片的内部框图。输入信号首先经过一个增益为 Av = 20log(2×Rf/Ri) 的放大级,放大后的信号一端直接驱动喇叭,另一端经过反相后驱动喇叭的另一端,从而形成完整的推挽输出。

音频放大电路仿真波形图

图4 放大电路仿真波形(绿色为输入,黄蓝色为两路输出)

功能实现

经过设计与调试,最终实现了以下功能:存储并自动播放一段乐谱、通过按键切换不同音调、使用上下按键扩展音程、以及同时按下两个琴键生成和声。

详细实现过程

1. 正弦波生成:查表法

本设计采用查表法来生成基础正弦波。即预先将一个周期正弦波的数字量化值(波表)存储在FPGA的存储器中,然后以固定频率依次读取这些值,通过后续处理生成PWM波形。

波表模块(部分代码)

module sin_table(address,sin);
output [8:0] sin;        // 实际波形表为9位分辨率(1/4周期)
input  [5:0] address;    // 64个点来生成1/4个周期的波形,完整的一个周期为256个点

reg    [8:0] sin;

always @(address)
begin
                 case(address)
                     6'h0:  sin=9'h0;
                     6'h1:  sin=9'h24;
                     6'h2:  sin=9'h4A;
                     6'h3:  sin=9'h6F;
                     6'h4:  sin=9'h93;
                     6'h5:  sin=9'hB6;
                     6'h6:  sin=9'hD8;
                     6'h7:  sin=9'hF8;
                     6'h8:  sin=9'h116;
                     6'h9:  sin=9'h134;
                     // ... 其余波表数据
                 endcase
             end
endmodule

PWM生成模块

module pwm(
input   clk,
input   rst,
input   [12:0]  duty,
output pwm_out
);
reg [9:0] PWM_accumulator;
always @(posedge clk or negedge rst)
    begin
    if(!rst)
        PWM_accumulator <= 0;
    else
        PWM_accumulator <= PWM_accumulator[8:0] + duty[8:0];
    end
assign pwm_out = ~PWM_accumulator[9];
endmodule

2. 生成不同频率的波形

通过调节读取波表地址的累加步进值(即相位增量),可以控制波形输出的频率。步进值越大,读取波表的速度越快,生成的音频频率就越高。

音频合成器模块(实例化不同音调)

module synthesizer(
    input clk,
    input rst,
    input [12:0] key,
    input [1:0]updown,
    output [12:0] wavecnt
    );
wire pwm_bit_synthesizer;
wire [9:0] wavec4, waved4b, waved4, wavee4b, wavee4, wavef4, waveg4b, waveg4, wavea4b, wavea4, waveb4b, waveb4, wavec5;

wave #(366) c4(.clk(clk), .rst(rst), .enable(key[0]), .interval(updown), .waveout(wavec4));
wave #(388) d4b(.clk(clk),.rst(rst), .enable(key[1]), .interval(updown),.waveout(waved4b));
wave #(411) d4(.clk(clk), .rst(rst), .enable(key[2]), .interval(updown),.waveout(waved4));
wave #(435) e4b(.clk(clk),.rst(rst), .enable(key[3]), .interval(updown),.waveout(wavee4b));
wave #(461) e4(.clk(clk), .rst(rst), .enable(key[4]), .interval(updown),.waveout(wavee4));
wave #(488) f4(.clk(clk), .rst(rst), .enable(key[5]), .interval(updown),.waveout(wavef4));
wave #(517) g4b(.clk(clk),.rst(rst), .enable(key[6]), .interval(updown),.waveout(waveg4b));
wave #(548) g4(.clk(clk), .rst(rst), .enable(key[7]), .interval(updown),.waveout(waveg4));
wave #(581) a4b(.clk(clk),.rst(rst), .enable(key[8]), .interval(updown),.waveout(wavea4b));
wave #(615) a4(.clk(clk), .rst(rst), .enable(key[9]), .interval(updown),.waveout(wavea4));
wave #(652) b4b(.clk(clk),.rst(rst), .enable(key[10]),.interval(updown), .waveout(waveb4b));
wave #(690) b4(.clk(clk), .rst(rst), .enable(key[11]),.interval(updown), .waveout(waveb4));
wave #(732) c5(.clk(clk), .rst(rst), .enable(key[12]),.interval(updown), .waveout(wavec5));

assign wavecnt = (wavec4+waved4b+waved4+wavee4b+wavee4+wavef4+waveg4b+waveg4+wavea4b+wavea4+waveb4b+waveb4+wavec5)/4;
endmodule

3. 加入谐波分量

为了丰富音色、使声音听起来更自然,我在基础正弦波中加入了谐波分量。具体方法是使用Excel预先计算并生成了一个包含基波、3次谐波和5次谐波分量的混合波表。

包含3次、5次谐波分量的波表示例

图5. 谐波分量波表数据

4. 实现双键和弦

和弦通过将两个激活琴键对应的波形值简单相加来实现。但这引入了新问题:两个波形相加后的幅值可能超过存储寄存器的上限,导致截断失真。解决方法是对相加后的结果进行幅度衰减处理,例如除以一个常数因子。

assign wavecnt = (wavec4+waved4b+waved4+wavee4b+wavee4+wavef4+waveg4b+waveg4+wavea4b+wavea4+waveb4b+waveb4+wavec5)/4;

5. 自动播放乐曲

自动播放功能再次利用了查表法的思想。首先将一段乐谱(一系列音符代码)预先存储在ROM中。然后设计一个节拍发生器,以固定的时间间隔(如每0.25秒)读取下一个音符代码,并将其映射到对应的琴键控制信号上,循环执行即可实现自动播放。

always@(posedge clk or negedge rst)      //自动播放音乐节拍计数
        begin
        if(!rst)
            tempo[23:0]<= 0;
        else
            tempo[23:0]<=tempo[23:0]+24'b1;
                if(tempo>=1500000) // 达到节拍时间
                begin
                tempo[23:0]<= 0;
                tempo_flag=~tempo_flag;        //计数到节拍标志翻转
                end
        end

    always@(posedge tempo_flag or negedge rst)       //自动播放音节累加
        begin
        if(!rst)
            music_count[7:0]<= 0;

        else if(music_flag==1)
           begin
                music_count[7:0]<=music_count[7:0]+8'b1;
                if(music_count>=96) // 乐谱长度
                begin
                music_count[7:0]<= 0;
                end
            end
            else
            music_count[7:0]<= 0;
        end

    musicable musicable_u0(
    .micount(music_count),
    .musickey(music)
    );

    autokey autokey_u0(                       //将单个音节映射到13个按键
    .key_value (music),
    .key_bit(music_keybit)
    );

项目总结与展望

不足之处与改进思路

  1. 按键音头/音尾处理:按键按下和释放的瞬间,能听到声音的突变。优化思路是加入“包络”控制,例如在按键按下时实现振幅的淡入(由小到大),释放时实现淡出(由大到小),这涉及到更深入的数字信号处理知识。
  2. 蜂鸣器音量调节:当前方案中蜂鸣器的音量尚不能灵活调节,需要进一步研究其驱动特性。
  3. 自动播放和弦:目前的自动播放功能仅支持单音旋律,未来可以扩展为支持读取和播放和弦序列。

实践收获

通过这个项目,不仅走通了完整的FPGA开发流程,也切实理解了音频功放电路的基本驱动原理。项目中大量的时间并非用于编写代码,而是投入在反复验证上:波表数据是否准确、PWM参数与滤波电路是否匹配、功放能否有效驱动负载。幸运的是,所使用的电子琴底板与STEP-MXO2核心板已经妥善处理了按键、音频接口和电源等基础硬件问题,使我能够将精力聚焦于FPGA逻辑与系统设计本身。




上一篇:SpringBoot接口防刷:基于Redis与自定义注解的实战指南
下一篇:Spring Boot 接口设计:@RequestBody 与 @RequestParam 的核心区别与选用原则
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:54 , Processed in 0.296956 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表