提到FPGA,很多人想到的是接口、时序和状态机。但当你用它来生成音乐,让抽象的代码驱动扬声器发出具体的声音时,对硬件的理解会进入一个全新的维度。
这次,我使用了一块STEP-MXO2开发板,从最基础的PWM调制和查表法起步,逐步构建了一个具备弹奏、和弦叠加甚至自动播放功能的简易电子琴系统。这个过程更像是一场用声音去理解FPGA内部逻辑的实践。
硬件设计
1. 工作原理

图1 电子琴系统硬件框图
整个系统的核心是“小脚丫”STEP-MXO2-C开发板。这款板卡支持网页版Web IDE进行编程,无需安装本地软件,当然也兼容传统的Lattice Diamond开发工具。
系统输入端共有15个按键。其中13个被定义为标准琴键,另外两个则用于扩展音程(升/降调)。
输出端提供了两种发声方式:喇叭或蜂鸣器,通过一个拨动开关进行选择。这两种发声器件的驱动方式和最终音效存在明显差异。

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次谐波分量的混合波表。

图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)
);
项目总结与展望
不足之处与改进思路
- 按键音头/音尾处理:按键按下和释放的瞬间,能听到声音的突变。优化思路是加入“包络”控制,例如在按键按下时实现振幅的淡入(由小到大),释放时实现淡出(由大到小),这涉及到更深入的数字信号处理知识。
- 蜂鸣器音量调节:当前方案中蜂鸣器的音量尚不能灵活调节,需要进一步研究其驱动特性。
- 自动播放和弦:目前的自动播放功能仅支持单音旋律,未来可以扩展为支持读取和播放和弦序列。
实践收获
通过这个项目,不仅走通了完整的FPGA开发流程,也切实理解了音频功放电路的基本驱动原理。项目中大量的时间并非用于编写代码,而是投入在反复验证上:波表数据是否准确、PWM参数与滤波电路是否匹配、功放能否有效驱动负载。幸运的是,所使用的电子琴底板与STEP-MXO2核心板已经妥善处理了按键、音频接口和电源等基础硬件问题,使我能够将精力聚焦于FPGA逻辑与系统设计本身。