项目总结报告
2022暑期在家一起练(3) - 基于FPGA的电子琴设计
目标:自己组装,并通过编程驱动模拟扬声器实现电子琴的功能
参加本平台所需完成的任务:
- 基于提供的套件和工具,自己组装电子琴
- 编程基于FPGA实现:
- 存储一段音乐,并可以进行音乐播放,
- 可以自己通过板上的按键进行弹奏,支持两个按键同时按下(和弦)并且声音不能失真,板上的按键只有13个,可以通过有上方的“上“、”下”两个按键对音程进行扩展
- 使用扬声器进行播放时,输出的音调信号除了对应于该音调的单频正弦波外,还必须包含至少一个谐波分量
- 音乐的播放支持两种方式,这两种方式可以通过开关进行切换:
- 当开关切换到蜂鸣器端,可以通过蜂鸣器来进行音乐播放
- 当开关切换到扬声器端,可以通过模拟扬声器来进行音乐播放,每个音符都必须包含基频 + 至少一个谐波分量
一、电子琴的工作原理和框图
电子琴的组成:
使用一块小脚丫STEP-MXO2-C的FPGA开发板加上一套Piano Kit扩展板组成,将FPGA开发板安装在扩展底板上,并插入Micro usb数据线,即可接入电源开始运行。
Piano Kit扩展板上配有15个按键,一个切换开关,一个旋转电位器,以及PWM dac模拟电路和音频功率放大电路组成
这15个按键均直接接入FPGA核心板引脚中,同时加入上拉电阻使得在没有按键按下时,引脚为高电平,有13个按键对应钢琴键盘,另外两个按键用于扩展音调。切换开关接入的一侧接入电源,另一侧接地,输出直接接入FPGA的核心板引脚中,旋转电位器是模拟电路中的一部分,用于调节驱动信号的幅值,从而控制扬声器声音都大小
小脚丫STEP-MXO2第二代硬件结构
核心器件:Lattice LCMXO2-4000HC-4MG132
132脚BGA封装,引脚间距0.5mm,芯片尺寸8mm x 8mm;
上电瞬时启动,启动时间<1ms;
4320个LUT资源, 96Kbit 用户闪存,92Kbit RAM;
2+2路PLL+DLL;
嵌入式功能块(硬核):一路SPI、一路定时器、2路I2C
支持DDR/DDR2/LPDDR存储器;
104个可热插拔I/O;
内核电压2.5-3.3V;
板载资源:
两位7段数码管;
两个RGB三色LED;
8路用户LED;
4路拨码开关;
4路按键;
外部时钟频率:12MHz
36个用户可扩展I/O(其中包括一路SPI硬核接口和一路I2C硬核接口)
支持的开发工具Lattice Diamond
支持MICO32/8软核处理器
板上集成FPGA编程器
一路Micro USB接口
板卡尺寸52mm x 18mm
工作原理:
拨动切换开关,按下对应的按键,经过小脚丫FPGA核心板识别并处理,继而在相应的引脚上输出相应的数字信号,经过模拟电路转换,驱动扬声器发声,或直接输出PWM信号至蜂鸣器,使其发声。
二、蜂鸣器和扬声器的差别以及音效差别分析
使用蜂鸣器,输入PWM信号的频率便是其发声的频率,发出的音色单一,听起来较为刺耳。
一般主要用于提示或报警,驱动电路也较为简单,直接使用数字信号(PWM)便可驱动其发出声响,但发出的声音比较简单,改变PWM信号的频率,可发出不同频率的声音,但是音色不被改变。
扬声器通常成为“喇叭”,扬声器需要配备专门的驱动电路,扬声器音色丰富;能够发出多种音调,基于相应的信号,便可以播放各种声音,一般使用正弦信号作为输入信号。
使用扬声器,输入正弦信号,改变频率,便可改变发生的频率,改变信号的各次谐波/幅值/相位,便可使其发出千变万化各种音色的声音,听起来也较为柔和。
三、模拟放大电路的仿真及分析
由于FPGA只处理数字信号,便需要将数字信号经过DAC电路的转换变为相应的模拟信号,从而输入到音频功率放大电路,再驱动扬声器发声
DAC电路分析
在Piano Kit扩展板的原理图中,我们可以看出,DAC电路为阻容滤波电路,而输入端直接接入FPGA核心板的引脚,这就要求FPGA使用1bitDAC的方式,产生一定占空比的PWM信号,经过阻容滤波后,还原为对应的直流电压信号,该直流电压信号跟随输入的PWM信号占空比变化而变化,这样经过处理后便可还原出相应的波形信号,再进行功率放大处理,输出到扬声器中发出声音。
使用Multisim对电路进行仿真
DAC电路仿真
使用一个比较器来产生PWM信号,比较器两端分别输入正弦波,三角载波,当正弦波幅值的幅值越大,输出的信号的占空比越大,从而就可以产生占空比跟随正弦波变化而变化的PWM信号,经过阻容滤波器后,即可还原出原始的正弦波信号,信号的失真度与载波频率有关,载波频率越高,被还原度信号越完整
如图所示,绿色信号为440Hz正弦波,红色信号为117Khz三角载波,蓝色信号为PWM波,黄色信号为经过滤波后的信号,可以看出还原 信号与原始正弦波信号基本相似,且可以用滑动变阻器调整输出信号的幅值。
当红色信号为17Khz三角载波时,可以看到,经过滤波后的波形已经明显失真,所以PWM频率需要稍高方可保证1bitDAC效果。
音频功率放大电路分析
音频功率放大电路使用了一个英锐芯生产的AD8002芯片,该芯片为3W 单声道带关断模式音频功率放大器,内部原理框图及典型应用图如下:
该电路为BTL桥接式结构,在典型应用图中可以看出,AD8002B外围电阻Rf和Ri构成了放大器1的闭环增益,而两个内部20kΩ电阻组成了放大器2反向
端闭环增益。
放大器需要驱动的扬声器负载,接在两个放大器输出端之间。同时放大器1的输出端作为放大器2的输入端。两个放大器输出的信号大小相同,但是相位互差180度。
在BTL桥式模式下,输出构成差分信号驱动。这种结构最大的优点是:差分输出使负载两端的增加一倍,在相同条件下就产生了相当于单端放大器四倍的输出功率;以及另一个优点是不会在负载上产生直流失调电压。
功率放大电路的增益由外围电阻Rf和Ri所决定,计算公式为A=20log(2*Rf/Ri)
在Piano Kit扩展板的原理图中,可以看出Rf为47kΩ,Ri为10kΩ,增益计算为A=20log(2*47kΩ/10kΩ)=19.46dB
为了保证输出的THD和PSRR尽可能小,在AD8002B芯片的2脚中放置一个ESR值适当的陶瓷电容,电容大小为1uF。
对于AD8002B芯片4脚的输入信号,需要串联电容Ci,与电阻Ri构成高通滤波器,截止频率为Fc=1/(2π*Ri*Ci)。
这样便完成了数字信号到扬声器声音的转换电路。
音频功率方案电路仿真
在PWM 1bit RC DAC电路的基础上,根据AD8002芯片的内部原理图及典型应用图搭建模拟电路,进行仿真
如图所示,黄色信号为输入信号,红色信号为放大器1的输出信号,绿色信号为放大器2的输出信号,可以清楚看出,放大电路正常工作,对输入信号进行了放大,同时放大器1与放大器2的输出信号相位互差180度。放大后波形较为正常,确定电路可以正常使用。
四、主要代码片段及说明
这次还是我首次接触FPGA的使用,所以在代码的编写上,选用了与C语言较为相似的Verilog语言进行编程开发,也可以快速上手
FPGA开发的一大特点就是模块化,首先根据目标以及任务要求,制定总体计划,再将总计划进行拆分,形成一个个的模块,在总的一个大的模块中将这一个个模块进行连接,便形成了最终的方案。
我的项目方案是,18个按键(13个琴键+2个升降调键+3个自动演奏键)进行消抖后,
使用13个按键信号分别控制13个DDS信号发生器(实际上是26个,除了基波,还产生2倍频的谐波),从产生对应频率的正弦波信号,将这些信号进行叠加后,按照按下的按键个数进行除法计算,所得到的结果再进行PWM信号生成,
同时也使用这13个按键信号产生一路对应频率的pwm信号
最后输出根据切换开关的信号值,选择对应的引脚进行信号的输出
对于自动演奏,类似状态机,自动演奏按键被按下时,启动,设置一个地址变量,间隔一定的时间后转入下一个地址,根据该地址查找rom中储存的按键信号,选择对应的按键信号还原成13个自动按键,在主模块中对手动的按键及自动按键相与即可自动演奏,同时也不影响在自动播放时手动演奏。
最后输出根据切换开关的信号值,选择对应的引脚进行信号的输出
对于自动演奏,类似状态机,自动演奏按键被按下时,启动,设置一个地址变量,间隔一定的时间后转入下一个地址,根据该地址查找rom中储存的按键信号,选择对应的按键信号还原成13个自动按键,在主模块中对手动的按键及自动按键相与即可自动演奏,同时也不影响在自动播放时手动演奏。
总体结构框图
DDS模块,不直接产生信号,只输出地址,例化13个dds模块
module dds_sin
(
input wire clk_in,//输入120m时钟
input wire clk_200,
input wire rst_n,
input wire [15:0] F_WORD,//输入频率控制字
input wire [ 0:0] vol_ok,//输入是否有音量
output wire [10:0] rom_addr_out,//输出11位地址
output wire [0:0] rom_addr_sign, //输出符号,用于判断象限
output wire [10:0] rom_addr2_out,
output wire [0:0] rom_addr_sign2
// output wire [9:0] out_data//最终dds输出的数据
);
parameter P_WORD = 1'd0;//定义参数相位控制字
parameter P_WORD2 = 1'd0;//定义参数相位控制字,谐波
reg [31:0] fre_add; //累加寄存器
reg [31:0] fre_add2; //累加寄存器,谐波
wire [17:0] F_WORD2;
assign F_WORD2 = F_WORD + F_WORD;//2倍
wire [12:0] rom_addr; //8192_13位
wire [12:0] rom_addr2;
wire [0:0] addr_sign;//1/4
wire [0:0] addr_sign2;
//dds部分
always @(posedge clk_in or negedge rst_n) begin
if(!rst_n)begin
fre_add <= 32'd0;
fre_add2 <= 32'd0;
end
else begin
fre_add <= fre_add + F_WORD;
fre_add2 <= fre_add2 + F_WORD2;
end
end
assign rom_addr = fre_add[31:19];
assign rom_addr2 = fre_add2[31:19];
wire [10:0] rom_address; //实际上rom数据个数2048_11位
assign addr_sign = ((rom_addr[12:11]==2'b00)|(rom_addr[12:11]==2'b01)) ? 1'b0 : 1'b1;
assign rom_address = ((rom_addr[12:11]==2'b00)|(rom_addr[12:11]==2'b10)) ? (rom_addr[10:0]) : (~rom_addr[10:0]);
wire [10:0] rom_address2; //实际上rom数据个数2048_11位
assign addr_sign2 = ((rom_addr2[12:11]==2'b00)|(rom_addr2[12:11]==2'b01)) ? 1'b0 : 1'b1;
assign rom_address2 = ((rom_addr2[12:11]==2'b00)|(rom_addr2[12:11]==2'b10)) ? (rom_addr2[10:0]) : (~rom_addr2[10:0]);
assign rom_addr_out = rom_address ;
assign rom_addr2_out = rom_address2 ;
assign rom_addr_sign = addr_sign ;
assign rom_addr_sign2 = addr_sign2 ;
endmodule
DDS地址还原数据模块,输入26个地址,输出26个数据
module dds_sin_data
#(parameter DW = 9,AW = 11)//位宽8(0-511),地址宽11(0-2047)
(
input clk,//时钟
input rst_n, //复位
input [AW-1:0]addr0,//address,输入26个地址
input [AW-1:0]addr1,
input [AW-1:0]addr2,
input [AW-1:0]addr3,
input [AW-1:0]addr4,
input [AW-1:0]addr5,
input [AW-1:0]addr6,
input [AW-1:0]addr7,
input [AW-1:0]addr8,
input [AW-1:0]addr9,
input [AW-1:0]addr10,
input [AW-1:0]addr11,
input [AW-1:0]addr12,
input [AW-1:0]addr13,
input [AW-1:0]addr14,
input [AW-1:0]addr15,
input [AW-1:0]addr16,
input [AW-1:0]addr17,
input [AW-1:0]addr18,
input [AW-1:0]addr19,
input [AW-1:0]addr20,
input [AW-1:0]addr21,
input [AW-1:0]addr22,
input [AW-1:0]addr23,
input [AW-1:0]addr24,
input [AW-1:0]addr25,
output [DW-1:0]data0 ,//输出26个数据
output [DW-1:0]data1 ,
output [DW-1:0]data2 ,
output [DW-1:0]data3 ,
output [DW-1:0]data4 ,
output [DW-1:0]data5 ,
output [DW-1:0]data6 ,
output [DW-1:0]data7 ,
output [DW-1:0]data8 ,
output [DW-1:0]data9 ,
output [DW-1:0]data10,
output [DW-1:0]data11,
output [DW-1:0]data12,
output [DW-1:0]data13,
output [DW-1:0]data14,
output [DW-1:0]data15,
output [DW-1:0]data16,
output [DW-1:0]data17,
output [DW-1:0]data18,
output [DW-1:0]data19,
output [DW-1:0]data20,
output [DW-1:0]data21,
output [DW-1:0]data22,
output [DW-1:0]data23,
output [DW-1:0]data24,
output [DW-1:0]data25
);
parameter DP = 1 << AW;// depth ,1向左移aw次
// reg [DW-1:0]mem[0:DP-1];//mem的个数,0-2047,位宽dw
wire [AW-1:0]a0;//address,分时复用,13次,每次选一个进入
wire [AW-1:0]a1;
reg [DW-1:0]reg_d0 ;//数据暂存,最后一个时钟一起赋值输出,其他时候不变
reg [DW-1:0]reg_d1 ;
reg [DW-1:0]reg_d2 ;
reg [DW-1:0]reg_d3 ;
reg [DW-1:0]reg_d4 ;
reg [DW-1:0]reg_d5 ;
reg [DW-1:0]reg_d6 ;
reg [DW-1:0]reg_d7 ;
reg [DW-1:0]reg_d8 ;
reg [DW-1:0]reg_d9 ;
reg [DW-1:0]reg_d10;
reg [DW-1:0]reg_d11;
reg [DW-1:0]reg_d12;
reg [DW-1:0]reg_d13;
reg [DW-1:0]reg_d14;
reg [DW-1:0]reg_d15;
reg [DW-1:0]reg_d16;
reg [DW-1:0]reg_d17;
reg [DW-1:0]reg_d18;
reg [DW-1:0]reg_d19;
reg [DW-1:0]reg_d20;
reg [DW-1:0]reg_d21;
reg [DW-1:0]reg_d22;
reg [DW-1:0]reg_d23;
reg [DW-1:0]reg_d24;
reg [DW-1:0]reg_d25;
reg [3:0] dds_cnt; //计数
// parameter dds_max = 12; //计数13次,13个按键,sin的位宽10位
wire [8:0] rom_data ;
wire [8:0] rom_data2 ;
//产生计数器cnt1
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
dds_cnt<=4'd0;
end
else if(dds_cnt>=4'd12)
dds_cnt<=1'b0;
else
dds_cnt<=dds_cnt+1'b1;
end
wire [12:0] dds_cnt_temp; //计数
assign dds_cnt_temp = (dds_cnt == 4'd0) ? 13'b0000_000_000_001 :
(dds_cnt == 4'd1) ? 13'b0000_000_000_010 :
(dds_cnt == 4'd2) ? 13'b0000_000_000_100 :
(dds_cnt == 4'd3) ? 13'b0000_000_001_000 :
(dds_cnt == 4'd4) ? 13'b0000_000_010_000 :
(dds_cnt == 4'd5) ? 13'b0000_000_100_000 :
(dds_cnt == 4'd6) ? 13'b0000_001_000_000 :
(dds_cnt == 4'd7) ? 13'b0000_010_000_000 :
(dds_cnt == 4'd8) ? 13'b0000_100_000_000 :
(dds_cnt == 4'd9) ? 13'b0001_000_000_000 :
(dds_cnt == 4'd10) ? 13'b0010_000_000_000 :
(dds_cnt == 4'd11) ? 13'b0100_000_000_000 :
(dds_cnt == 4'd12) ? 13'b1000_000_000_000 :
13'd0;
assign a0 = (dds_cnt_temp[0]) ? addr0 :
(dds_cnt_temp[1]) ? addr2 :
(dds_cnt_temp[2]) ? addr4 :
(dds_cnt_temp[3]) ? addr6 :
(dds_cnt_temp[4]) ? addr8 :
(dds_cnt_temp[5]) ? addr10 :
(dds_cnt_temp[6]) ? addr12 :
(dds_cnt_temp[7]) ? addr14 :
(dds_cnt_temp[8]) ? addr16 :
(dds_cnt_temp[9]) ? addr18 :
(dds_cnt_temp[10]) ? addr20 :
(dds_cnt_temp[11]) ? addr22 :
(dds_cnt_temp[12]) ? addr24 :
11'd0;
assign a1 = (dds_cnt_temp[0]) ? addr1 :
(dds_cnt_temp[1]) ? addr3 :
(dds_cnt_temp[2]) ? addr5 :
(dds_cnt_temp[3]) ? addr7 :
(dds_cnt_temp[4]) ? addr9 :
(dds_cnt_temp[5]) ? addr11 :
(dds_cnt_temp[6]) ? addr13 :
(dds_cnt_temp[7]) ? addr15 :
(dds_cnt_temp[8]) ? addr17 :
(dds_cnt_temp[9]) ? addr19 :
(dds_cnt_temp[10]) ? addr21 :
(dds_cnt_temp[11]) ? addr23 :
(dds_cnt_temp[12]) ? addr25 :
11'd0;
//滞后两个时钟周期,补回来
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
reg_d0 <= 9'd0;
reg_d1 <= 9'd0;
reg_d2 <= 9'd0;
reg_d3 <= 9'd0;
reg_d4 <= 9'd0;
reg_d5 <= 9'd0;
reg_d6 <= 9'd0;
reg_d7 <= 9'd0;
reg_d8 <= 9'd0;
reg_d9 <= 9'd0;
reg_d10 <= 9'd0;
reg_d11 <= 9'd0;
reg_d12 <= 9'd0;
reg_d13 <= 9'd0;
reg_d14 <= 9'd0;
reg_d15 <= 9'd0;
reg_d16 <= 9'd0;
reg_d17 <= 9'd0;
reg_d18 <= 9'd0;
reg_d19 <= 9'd0;
reg_d20 <= 9'd0;
reg_d21 <= 9'd0;
reg_d22 <= 9'd0;
reg_d23 <= 9'd0;
reg_d24 <= 9'd0;
reg_d25 <= 9'd0;
end
else
case(dds_cnt[3:0])
4'b0000:begin
// a0 <= addr0;
// a1 <= addr1;
reg_d22 <= rom_data;
reg_d23 <= rom_data2;
end
4'b0001:begin
// a0 <= addr2;
// a1 <= addr3;
reg_d24 <= rom_data;
reg_d25 <= rom_data2;
end
4'b0010:begin
// a0 <= addr4;
// a1 <= addr5;
reg_d0 <= rom_data;
reg_d1 <= rom_data2;
end
4'b0011:begin
// a0 <= addr6;
// a1 <= addr7;
reg_d2 <= rom_data;
reg_d3 <= rom_data2;
end
4'b0100:begin
// a0 <= addr8;
// a1 <= addr9;
reg_d4 <= rom_data;
reg_d5 <= rom_data2;
end
4'b0101:begin
// a0 <= addr10;
// a1 <= addr11;
reg_d6 <= rom_data;
reg_d7 <= rom_data2;
end
4'b0110:begin
// a0 <= addr12;
// a1 <= addr13;
reg_d8 <= rom_data;
reg_d9 <= rom_data2;
end
4'b0111:begin
// a0 <= addr14;
// a1 <= addr15;
reg_d10 <= rom_data;
reg_d11 <= rom_data2;
end
4'b1000:begin
// a0 <= addr16;
// a1 <= addr17;
reg_d12 <= rom_data;
reg_d13 <= rom_data2;
end
4'b1001:begin
// a0 <= addr18;
// a1 <= addr19;
reg_d14 <= rom_data;
reg_d15 <= rom_data2;
end
4'b1010:begin
// a0 <= addr20;
// a1 <= addr21;
reg_d16 <= rom_data;
reg_d17 <= rom_data2;
end
4'b1011:begin
// a0 <= addr22;
// a1 <= addr23;
reg_d18 <= rom_data;
reg_d19 <= rom_data2;
end
4'b1100:begin
// a0 <= addr24;
// a1 <= addr25;
reg_d20 <= rom_data;
reg_d21 <= rom_data2;
end
default: ;
endcase
end
//rom查找表,10*8192
sin_rom sin_rom
(
.Address(a0), //0-2047
.OutClock(clk), //120M
.OutClockEn(1'b1), //输出使能,高有效
.Reset(1'b0), //复位,高有效
.Q(rom_data) //改9位
);
//谐波rom
sin_rom sin_rom2
(
.Address(a1), //0-2047
.OutClock(clk), //120M
.OutClockEn(1'b1), //输出使能,高有效
.Reset(1'b0), //复位,高有效
.Q(rom_data2) //
);
assign data0 = (dds_cnt_temp[12])?reg_d0:data0;
assign data1 = (dds_cnt_temp[12])?reg_d1:data1;
assign data2 = (dds_cnt_temp[12])?reg_d2:data2;
assign data3 = (dds_cnt_temp[12])?reg_d3:data3;
assign data4 = (dds_cnt_temp[12])?reg_d4:data4;
assign data5 = (dds_cnt_temp[12])?reg_d5:data5;
assign data6 = (dds_cnt_temp[12])?reg_d6:data6;
assign data7 = (dds_cnt_temp[12])?reg_d7:data7;
assign data8 = (dds_cnt_temp[12])?reg_d8:data8;
assign data9 = (dds_cnt_temp[12])?reg_d9:data9;
assign data10 = (dds_cnt_temp[12])?reg_d10:data10;
assign data11 = (dds_cnt_temp[12])?reg_d11:data11;
assign data12 = (dds_cnt_temp[12])?reg_d12:data12;
assign data13 = (dds_cnt_temp[12])?reg_d13:data13;
assign data14 = (dds_cnt_temp[12])?reg_d14:data14;
assign data15 = (dds_cnt_temp[12])?reg_d15:data15;
assign data16 = (dds_cnt_temp[12])?reg_d16:data16;
assign data17 = (dds_cnt_temp[12])?reg_d17:data17;
assign data18 = (dds_cnt_temp[12])?reg_d18:data18;
assign data19 = (dds_cnt_temp[12])?reg_d19:data19;
assign data20 = (dds_cnt_temp[12])?reg_d20:data20;
assign data21 = (dds_cnt_temp[12])?reg_d21:data21;
assign data22 = (dds_cnt_temp[12])?reg_d22:data22;
assign data23 = (dds_cnt_temp[12])?reg_d23:data23;
assign data24 = (dds_cnt_temp[12])?reg_d24:data24;
assign data25 = (dds_cnt_temp[12])?reg_d25:data25;
endmodule
按键消抖模块,用于防止按键抖动造成的影响
module key_filter
#(parameter N = 1)
(
input wire clk,
input wire rst,
input wire [N-1:0] key, //输入的按键
output wire [N-1:0] key_out //按键动作产生的脉冲
);
reg [N-1:0] key_now_pre; //定义一个寄存器型变量存储上一个触发时的按键值
reg [N-1:0] key_now; //定义一个寄存器变量储存储当前时刻触发的按键值
reg [N-1:0] key_out_reg;
wire [N-1:0] key_edge; //检测到按键由高到低变化是产生一个高脉冲
//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
always @(posedge clk or negedge rst)
begin
if (!rst) begin
key_now <= {N{1'b1}}; //初始化时给key_now赋值全为1,{}中表示N个1
key_now_pre <= {N{1'b1}};
end
else begin
key_now <= key; //第一个时钟上升沿触发之后key的值赋给key_now,同时key_now的值赋给key_now_pre
key_now_pre <= key_now; //非阻塞赋值。相当于经过两个时钟触发,key_now存储的是当前时刻key的值,key_now_pre存储的是前一个时钟的key的值
end
end
assign key_edge = key_now_pre & (~key_now);//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平
reg [17:0] cnt; //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器
//产生20ms延时,当检测到key_edge有效是计数器清零开始计数
always @(posedge clk or negedge rst)
begin
if(!rst)
cnt <= 18'h0;
else if(key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end
//reg [N-1:0] key_sec_pre; //延时后检测电平寄存器变量
//reg [N-1:0] key_sec;
//延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
always @(posedge clk or negedge rst)
begin
if (!rst)
key_out_reg <= {N{1'b1}};
else if (cnt==18'h3ffff)
key_out_reg <= key;
end
assign key_out = key_out_reg;
endmodule
蜂鸣器PWM信号产生模块
module pwm_buzz
(
input wire clk, //时钟
input wire rst_n, //复位
input wire [19:0] cycle, //一个周期计数的值
output wire buzz_out //输出pwm波形
);
// reg [WIDTH-1:0] duty, //半个周期计数的值
reg [19:0] cnt1; //计数器1,记1024个,频率120_000_000/1024=117_187hz,117khz,经过低通滤波
//产生计数器cnt1
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt1<=20'd0;
end
else if(cnt1>=cycle)
cnt1<=1'b0;
else
cnt1<=cnt1+1'b1;
end
//比较计数器1和dac_data的值产生自动调整占空比输出的信号,
assign buzz_out = (cnt1<cycle[19:1])?1'b1:1'b0;
endmodule
1bitDAC的PWM信号产生模块
module pwm_dac
(
input wire clk_in,//120m
input wire rst_n,
input wire [9:0] dac_data,//输入dac的数据
output wire pwm_dac_out
);
reg [9:0] cnt1; //计数器1,记1024个,频率120_000_000/1024=117_187hz,117khz,经过低通滤波
parameter cnt1_max = 10'd1023; //计数器的最大值,sin的位宽10位
//产生计数器cnt1
always@(posedge clk_in or negedge rst_n) begin
if(!rst_n) begin
cnt1<=13'd0;
end
else if(cnt1>=cnt1_max)
cnt1<=1'b0;
else
cnt1<=cnt1+1'b1;
end
//比较计数器1和dac_data的值产生自动调整占空比输出的信号,
assign pwm_dac_out = (cnt1<dac_data)?1'b1:1'b0;
endmodule
自动演奏模块,输出自动演奏的按键(模拟手动按键)
module auto_song
(
input wire clk_1m, //输入1m时钟
input wire rst_n, //复位信号
input wire [2:0] key_song, //按键3个
output wire [14:0] auto_key //14个自动按键
);
reg [19:0] cnt;
reg [9:0] addr_temp; //1024个数据
wire [9:0] addr; //1024个数据
wire [0:0] finish; //歌曲停止复位 标志
reg [14:0]key_temp;//选择按键输出
//位宽6,1024个
reg [2:0] key_song_pre ;//一个周期的高电平
wire [2:0] key_ok_sign;
reg [2:0] song_case;//歌曲选择
reg [0:0] song_ing;//为1正在自动演奏
reg [5:0] song_key_temp;//读rom39个按键,1-39,000000是停止信号,111111是延续,不松开按键,111110是休止符,全松开按键
//reg [5:0] song_key;
reg [5:0]songmem[0:1024];
initial
begin: read_file_HEX
$readmemh("../software/song1.mem", songmem);
end
// 取出一个周期的高电平
always @(posedge clk_1m or negedge rst_n)
begin
if (!rst_n)
key_song_pre <= 3'b111;
else
key_song_pre <= key_song;
end
assign key_ok_sign = key_song_pre & (~key_song); //key_ok_sign输出一个周期的高电平信号
assign finish = key_ok_sign?1'b0:song_key_temp?1'b0:1'b1;
// 根据按键选择歌曲
always @(posedge clk_1m or negedge rst_n) begin
if(!rst_n) begin
song_case <= 3'b000;
song_ing <= 1'b0;
end
else
case (key_ok_sign)
3'b001: begin song_case <= 3'b001;song_ing <= 1'b1;end//第一首
3'b010: begin song_case <= 3'b010;song_ing <= 1'b1;end
3'b100: begin song_case <= 3'b100;song_ing <= 1'b1;end
3'b000: begin
if (finish)//如果没有按键按下和演奏已经结束,歌曲选择变0,正在演奏变1,否则不变
begin song_case <= 3'b000;song_ing <= 1'b0;end
else begin song_case <= song_case;song_ing <= song_ing;end
end
default: ;//停
endcase
end
//速度四分音符100拍/min,每拍0.6秒,8分音符就是0.3s,计数值1_000_000*0.3 = 300_000,就换下一拍
parameter time_cnt = 20'd299_999; //0.5s计数值
//cnt:0.6s循环计数器
always@(posedge clk_1m or negedge rst_n)
begin
if(!rst_n)
cnt <= 20'd0;
else if(cnt == time_cnt )
cnt <= 20'd0;
else
cnt <= cnt + 1'b1;
end
//地址选择器,每个拍0.3s
always@(posedge clk_1m or negedge rst_n)
if(rst_n == 1'b0)
addr_temp <= 10'd0;
else if((!song_ing )|| (key_ok_sign))//按下按键,和没有开始自动演奏,使地址一直为0,否则进下一步
addr_temp <= 10'd0;
else if(cnt == time_cnt)begin//计数值到,地址就+1,0.3秒变一次
addr_temp <= addr_temp + 1'b1;
end
assign addr = song_case[0]? addr_temp : song_case[1]?addr_temp+8'd255:song_case[2]?addr_temp+9'd511:10'd0;
//读rom
always@(posedge clk_1m or negedge rst_n)
begin
if(!rst_n)//低电平复位有效
begin
song_key_temp <= 6'd0;
end
else
begin
song_key_temp <= songmem[addr];
end
end
//按键选择
always@(song_key_temp)begin
case (song_key_temp)
6'd0 : key_temp <= 15'b111_111_111_111_111;
6'd1 : key_temp <= 15'b101_111_111_111_110;
6'd2 : key_temp <= 15'b101_111_111_111_101;
6'd3 : key_temp <= 15'b101_111_111_111_011;
6'd4 : key_temp <= 15'b101_111_111_110_111;
6'd5 : key_temp <= 15'b101_111_111_101_111;
6'd6 : key_temp <= 15'b101_111_111_011_111;
6'd7 : key_temp <= 15'b101_111_110_111_111;
6'd8 : key_temp <= 15'b101_111_101_111_111;
6'd9 : key_temp <= 15'b101_111_011_111_111;
6'd10: key_temp <= 15'b101_110_111_111_111;
6'd11: key_temp <= 15'b101_101_111_111_111;
6'd12: key_temp <= 15'b101_011_111_111_111;
6'd13: key_temp <= 15'b100_111_111_111_111;
6'd14: key_temp <= 15'b111_111_111_111_110;
6'd15: key_temp <= 15'b111_111_111_111_101;
6'd16: key_temp <= 15'b111_111_111_111_011;
6'd17: key_temp <= 15'b111_111_111_110_111;
6'd18: key_temp <= 15'b111_111_111_101_111;
6'd19: key_temp <= 15'b111_111_111_011_111;
6'd20: key_temp <= 15'b111_111_110_111_111;
6'd21: key_temp <= 15'b111_111_101_111_111;
6'd22: key_temp <= 15'b111_111_011_111_111;
6'd23: key_temp <= 15'b111_110_111_111_111;
6'd24: key_temp <= 15'b111_101_111_111_111;
6'd25: key_temp <= 15'b111_011_111_111_111;
6'd26: key_temp <= 15'b110_111_111_111_111;
6'd27: key_temp <= 15'b011_111_111_111_110;
6'd28: key_temp <= 15'b011_111_111_111_101;
6'd29: key_temp <= 15'b011_111_111_111_011;
6'd30: key_temp <= 15'b011_111_111_110_111;
6'd31: key_temp <= 15'b011_111_111_101_111;
6'd32: key_temp <= 15'b011_111_111_011_111;
6'd33: key_temp <= 15'b011_111_110_111_111;
6'd34: key_temp <= 15'b011_111_101_111_111;
6'd35: key_temp <= 15'b011_111_011_111_111;
6'd36: key_temp <= 15'b011_110_111_111_111;
6'd37: key_temp <= 15'b011_101_111_111_111;
6'd38: key_temp <= 15'b011_011_111_111_111;
6'd39: key_temp <= 15'b010_111_111_111_111;
default: key_temp <= 15'b111_111_111_111_111;
endcase
end
assign auto_key = key_temp;
// assign song_key = song_key_temp?song_key_temp:6'b0;
// assign finish = song_data?1'b0:1'b0;//data有数据,finish为0,没数据,为1
// assign auto_key //根据39个音符选14个按键的值
endmodule //auto_song
主模块绝大部分都是例化其他模块
assign auto_key_in[14:0] = (key_in_ok[14:0] & auto_key[14:0]);//自动按键和手动按键相与
assign dac_sum = dac_data[0]+dac_data[1]+dac_data[2]+dac_data[3]+
dac_data[4]+dac_data[5]+dac_data[6]+dac_data[7]+dac_data[8]+dac_data[9]+
dac_data[10]+dac_data[11]+dac_data[12];//13个数据之和
assign vol_num = vol_ok[0]+vol_ok[1]+vol_ok[2]+vol_ok[3]+vol_ok[4]+vol_ok[5]+vol_ok[6]+
vol_ok[7]+vol_ok[8]+vol_ok[9]+vol_ok[10]+vol_ok[11]+vol_ok[12];
assign pwmdac_in_temp = dac_sum / vol_num ;//有几个按键按下就除以几
assign pwmdac_in = pwmdac_in_temp[9:0] ;
assign dac_out = key_mode ? dac_out_temp : 1'b0;//选择蜂鸣器输出还是扬声器输出
assign buzz_out = key_mode ? 1'b0 : buzz_out_temp;
五、逻辑资源使用情况
Design Summary
Number of registers: 1439 out of 4635 (31%)
PFU registers: 1439 out of 4320 (33%)
PIO registers: 0 out of 315 (0%)
Number of SLICEs: 1485 out of 2160 (69%)
SLICEs as Logic/ROM: 1485 out of 2160 (69%)
SLICEs as RAM: 0 out of 1620 (0%)
SLICEs as Carry: 1047 out of 2160 (48%)
Number of LUT4s: 2966 out of 4320 (69%)
Number used as logic LUTs: 872
Number used as distributed RAM: 0
Number used as ripple logic: 2094
Number used as shift registers: 0
Number of PIO sites used: 42 + 4(JTAG) out of 105 (44%)
Number of block RAMs: 5 out of 10 (50%)
Number of GSRs: 1 out of 1 (100%)
EFB used : No
JTAG used : No
Readback used : No
Oscillator used : No
Startup used : No
POR : On
Bandgap : On
Number of Power Controller: 0 out of 1 (0%)
Number of Dynamic Bank Controller (BCINRD): 0 out of 6 (0%)
Number of Dynamic Bank Controller (BCLVDSO): 0 out of 1 (0%)
Number of DCCA: 0 out of 8 (0%)
Number of DCMA: 0 out of 2 (0%)
Number of PLLs: 1 out of 2 (50%)
Number of DQSDLLs: 0 out of 2 (0%)
Number of CLKDIVC: 0 out of 4 (0%)
Number of ECLKSYNCA: 0 out of 4 (0%)
Number of ECLKBRIDGECS: 0 out of 2 (0%)
Notes:-
1. Total number of LUT4s = (Number of logic LUT4s) + 2*(Number of
distributed RAMs) + 2*(Number of ripple logic)
2. Number of logic LUT4s does not include count of distributed RAM and
ripple logic.
Number of clocks: 4
Net clk_120m: 607 loads, 607 rising, 0 falling (Driver: pll_inst/PLLInst_0
)
Net clk_in_c: 1 loads, 1 rising, 0 falling (Driver: PIO clk_in )
Net clk_1m: 30 loads, 30 rising, 0 falling (Driver: pll_inst/PLLInst_0 )
Net dds_sin_data/dds_cnt[0]_derived_234: 117 loads, 117 rising, 0 falling
(Driver: dds_sin_data/i14706_2_lut_rep_186_2_lut_3_lut_4_lut )
Number of Clock Enables: 18
Net key_filter/clk_120m_enable_92: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_67: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_44: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_75: 10 loads, 10 LSLICEs
Net dds_sin_data/clk_120m_enable_252: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_236: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_220: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_204: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_188: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_172: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_156: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_140: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_124: 9 loads, 9 LSLICEs
Net dds_sin_data/clk_120m_enable_108: 9 loads, 9 LSLICEs
Net auto_song/clk_1m_enable_1: 1 loads, 1 LSLICEs
Net auto_song/clk_1m_enable_2: 1 loads, 1 LSLICEs
Net auto_song/clk_1m_enable_13: 6 loads, 6 LSLICEs
Net auto_song/clk_1m_enable_14: 1 loads, 1 LSLICEs
Number of LSRs: 8
Net key_filter/cnt_17__N_144: 10 loads, 10 LSLICEs
Net dds_sin_data/n22441: 2 loads, 2 LSLICEs
Net pwm_buzz/cnt1_19__N_1213: 11 loads, 11 LSLICEs
Net pwm_dac/cnt1_9__N_1170: 6 loads, 6 LSLICEs
Net rst_N_89: 1 loads, 0 LSLICEs
Net div_1m_200_inst/n16711: 7 loads, 7 LSLICEs
Net auto_song/n22411: 6 loads, 6 LSLICEs
Net auto_song/cnt_19__N_1114: 10 loads, 10 LSLICEs
Number of nets driven by tri-state buffers: 0
Top 10 highest fanout non-clock nets:
Net n22398: 315 loads
Net n22399: 285 loads
Net auto_key_14: 89 loads
Net key_in_ok_14: 89 loads
Net n22390: 82 loads
Net segment_led_c_7: 50 loads
Net segment_led_c_10: 48 loads
Net segment_led_c_12: 47 loads
Net n22330: 45 loads
Net n22354: 45 loads
Number of warnings: 0
Number of errors: 0
六、遇到的主要难题及解决方法
最大的难题就是第一次学习使用FPGA,很多东西都一知半解,一点一点查资料,才逐步解决,并实现所需的功能,以及FPGA并行处理的方式,需要很完善的逻辑,verilog语言也非常严谨,稍有不慎,就无法实现所需功能(比如信号未初始化,数据处理过程所需消耗的时钟
以及Diamond软件和ModuleSim软件使用过程中的一些问题,这些大部分百度都可以找到类似的解决办法,也特别感谢群友的帮助
一些错误记录
if语句和case语句都只能用于always语句内部,如果要在always语句之外应用条件语句,可用三目运算符?:如下:assigndata = ( sel ) ? a : b;
循环语句 for(i = 0; i < n; i++)中的 i++ 在Verilog中是不可以直接使用的,需要变为i = i + 1;。
在 module1 中调用 module2 ,并需要将module2的输出储存的时候,存储中间变量不能为reg,而应为wire!!!否则将会出现如下错误 :
Illegal output or inout port connection for port 'data_out1'.
但是输入并不需要置于wire型,且如果输入需要改动,则必须是reg型的,否则会报错如下:Illegal reference to net "data_in1".
LUT4S的总数=(逻辑LUT4S的数量) + 2*(分布式RAM的数量) + 2*(波纹逻辑的数量)
逻辑LUT4的数量不包括分布式RAM和Ripple Logic的计数。
七、改进建议
自我感觉做的项目还不是很完善,只是实现了一些最基础的演奏,声音也算不上有多好听,以后有机会的话再慢慢完善它,(似乎逻辑资源有那么些不太够用了,希望能好好的优化一下,减少一下资源的使用)
还有最重要的ADSR,因为时间仓促,也没来得及完成,这只能等以后有时间再慢慢完善了
八、参考资料
1.仅50元!用雪糕棒DIY电子琴弹奏一曲 https://www.bilibili.com/video/BV1jy4y1y7nM
2.DDS生成任意波形的方法及Verilog代码 https://www.eetree.cn/wiki/dds_verilog
3.【仿真】Lattice_Diamond_调用Modelsim_仿真 http://t.zoukankan.com/tony-ning-p-5731681.html
4.Verilog HDL优化简述 https://blog.csdn.net/weixin_44545174/article/details/112969331
5.简单绘制波形 https://zuotu.91maths.com/
6.一些错误总结 https://blog.csdn.net/weiyunguan8611/article/details/100342631
7.Ram初始化的方法https://blog.csdn.net/zhanshen112/article/details/121539464
8.双端口ram 设计https://blog.csdn.net/Reborn_Lee/article/details/90647784
9.使用assig来避免使用if-else/case https://blog.csdn.net/vivid117/article/details/108825998
10.在线verilog练习 https://hdlbits.01xz.net/wiki/Main_Page
11.野火资料中心https://doc.embedfire.com/products/link/zh/latest/index.html
12.高效使用IP核实现DDS(续)——华为工程师带你规范做FPGA实例之四http://www.bilibili.com/video/av81810464