基于小脚丫FPGA的电子琴设计
使用FPGA编程,实现电子琴弹奏,自动演奏,蜂鸣器/扬声器切换,和弦功能
标签
FPGA
数字逻辑
DDS
模拟电路
音频放大
six
更新2022-09-06
河南科技大学
955

项目总结报告

2022暑期在家一起练(3) - 基于FPGA的电子琴设计

目标:自己组装,并通过编程驱动模拟扬声器实现电子琴的功能

参加本平台所需完成的任务:

  1. 基于提供的套件和工具,自己组装电子琴
  2. 编程基于FPGA实现:
    1. 存储一段音乐,并可以进行音乐播放,
    2. 可以自己通过板上的按键进行弹奏,支持两个按键同时按下(和弦)并且声音不能失真,板上的按键只有13个,可以通过有上方的“上“、”下”两个按键对音程进行扩展
    3. 使用扬声器进行播放时,输出的音调信号除了对应于该音调的单频正弦波外,还必须包含至少一个谐波分量
    4. 音乐的播放支持两种方式,这两种方式可以通过开关进行切换: 
      1. 当开关切换到蜂鸣器端,可以通过蜂鸣器来进行音乐播放
      2. 当开关切换到扬声器端,可以通过模拟扬声器来进行音乐播放,每个音符都必须包含基频 + 至少一个谐波分量

 

一、电子琴的工作原理和框图

      电子琴的组成:

      使用一块小脚丫STEP-MXO2-C的FPGA开发板加上一套Piano Kit扩展板组成,将FPGA开发板安装在扩展底板上,并插入Micro usb数据线,即可接入电源开始运行。

      Piano Kit扩展板上配有15个按键,一个切换开关,一个旋转电位器,以及PWM dac模拟电路和音频功率放大电路组成

      这15个按键均直接接入FPGA核心板引脚中,同时加入上拉电阻使得在没有按键按下时,引脚为高电平,有13个按键对应钢琴键盘,另外两个按键用于扩展音调。切换开关接入的一侧接入电源,另一侧接地,输出直接接入FPGA的核心板引脚中,旋转电位器是模拟电路中的一部分,用于调节驱动信号的幅值,从而控制扬声器声音都大小

         

Fj26nw63OwR4FM8XTchQCFgsAza7

FrBX3bXRotPo4-GoqFFUYUZkTGfA

 

小脚丫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信号至蜂鸣器,使其发声。

Fq_i-wX8hWfw6ybWLY4bBkFHuQrr

 

二、蜂鸣器和扬声器的差别以及音效差别分析

      使用蜂鸣器,输入PWM信号的频率便是其发声的频率,发出的音色单一,听起来较为刺耳。

      一般主要用于提示或报警,驱动电路也较为简单,直接使用数字信号(PWM)便可驱动其发出声响,但发出的声音比较简单,改变PWM信号的频率,可发出不同频率的声音,但是音色不被改变。

 

      扬声器通常成为“喇叭”,扬声器需要配备专门的驱动电路,扬声器音色丰富;能够发出多种音调,基于相应的信号,便可以播放各种声音,一般使用正弦信号作为输入信号。

      使用扬声器,输入正弦信号,改变频率,便可改变发生的频率,改变信号的各次谐波/幅值/相位,便可使其发出千变万化各种音色的声音,听起来也较为柔和。

 

三、模拟放大电路的仿真及分析

      由于FPGA只处理数字信号,便需要将数字信号经过DAC电路的转换变为相应的模拟信号,从而输入到音频功率放大电路,再驱动扬声器发声

      FjGSgTMGe6JWU8AP5TJcunVh7qCw

      DAC电路分析

      在Piano Kit扩展板的原理图中,我们可以看出,DAC电路为阻容滤波电路,而输入端直接接入FPGA核心板的引脚,这就要求FPGA使用1bitDAC的方式,产生一定占空比的PWM信号,经过阻容滤波后,还原为对应的直流电压信号,该直流电压信号跟随输入的PWM信号占空比变化而变化,这样经过处理后便可还原出相应的波形信号,再进行功率放大处理,输出到扬声器中发出声音。

      使用Multisim对电路进行仿真

    DAC电路仿真

      使用一个比较器来产生PWM信号,比较器两端分别输入正弦波,三角载波,当正弦波幅值的幅值越大,输出的信号的占空比越大,从而就可以产生占空比跟随正弦波变化而变化的PWM信号,经过阻容滤波器后,即可还原出原始的正弦波信号,信号的失真度与载波频率有关,载波频率越高,被还原度信号越完整

      如图所示,绿色信号为440Hz正弦波,红色信号为117Khz三角载波,蓝色信号为PWM波,黄色信号为经过滤波后的信号,可以看出还原 信号与原始正弦波信号基本相似,且可以用滑动变阻器调整输出信号的幅值。

      FiciVknlcx8CAQ8DWlmuuNfdQLjw

当红色信号为17Khz三角载波时,可以看到,经过滤波后的波形已经明显失真,所以PWM频率需要稍高方可保证1bitDAC效果。

      FkBmuXcDXr3Udm4HpCVLw4MDrREC

      音频功率放大电路分析

      音频功率放大电路使用了一个英锐芯生产的AD8002芯片,该芯片为3W 单声道带关断模式音频功率放大器,内部原理框图及典型应用图如下:

FvGury5gAAMHVppGsgwH8paeTH4I

 

Fv9K7URWaMaCIjW99F2FFpzKxxXC

      该电路为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度。放大后波形较为正常,确定电路可以正常使用。

      FlEHuOJPvG9LQb2g1gfHlxj2YkJ4

   

四、主要代码片段及说明

      这次还是我首次接触FPGA的使用,所以在代码的编写上,选用了与C语言较为相似的Verilog语言进行编程开发,也可以快速上手

      FPGA开发的一大特点就是模块化,首先根据目标以及任务要求,制定总体计划,再将总计划进行拆分,形成一个个的模块,在总的一个大的模块中将这一个个模块进行连接,便形成了最终的方案。

      我的项目方案是,18个按键(13个琴键+2个升降调键+3个自动演奏键)进行消抖后,

      使用13个按键信号分别控制13个DDS信号发生器(实际上是26个,除了基波,还产生2倍频的谐波),从产生对应频率的正弦波信号,将这些信号进行叠加后,按照按下的按键个数进行除法计算,所得到的结果再进行PWM信号生成,

 

      同时也使用这13个按键信号产生一路对应频率的pwm信号

      最后输出根据切换开关的信号值,选择对应的引脚进行信号的输出

      对于自动演奏,类似状态机,自动演奏按键被按下时,启动,设置一个地址变量,间隔一定的时间后转入下一个地址,根据该地址查找rom中储存的按键信号,选择对应的按键信号还原成13个自动按键,在主模块中对手动的按键及自动按键相与即可自动演奏,同时也不影响在自动播放时手动演奏。

      最后输出根据切换开关的信号值,选择对应的引脚进行信号的输出

      对于自动演奏,类似状态机,自动演奏按键被按下时,启动,设置一个地址变量,间隔一定的时间后转入下一个地址,根据该地址查找rom中储存的按键信号,选择对应的按键信号还原成13个自动按键,在主模块中对手动的按键及自动按键相与即可自动演奏,同时也不影响在自动播放时手动演奏。

      总体结构框图

FhXp28JpnMluPaDA1nQlFqqdNBPi

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

 

 

 

 

 

附件下载
fpga_piano.part01.rar
超过10M,分成两个文件压缩了,解压请同时下载两个文件
fpga_piano.part02.rar
上传的为使用diamond创建的整个工程,software文件夹中是基本的代码
团队介绍
河南科技大学在校学生
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号