声明:本文只对设计原理和过程作粗略的阐述,详细可以研究我贴出来的完整源代码,也可以私信交流。

若干略缩语解释:

FPGA(Field Programmable Gate Array):现场可编程逻辑门阵列

DDS(Direct Digital Synthesizer):直接数字频率合成技术

ROM(Read-Only Memory):只读存储器

DAC(Digital to Analog Convertor):数字模拟转换器

MUX(multiplexer​​​​​​​):数据选择器

​​​​​​​

一、设计思路:

        DDS(Direct Digital Synthesizer)即直接数字频率合成技术,其具体原理在这就不赘述了,不了解的可以自己查阅资料。

        利用DDS来实现信号发生器,首先就是要把波形数据存在ROM里,然后在时钟信号的控制下,用ROM的地址对ROM里的波形数据进行查表,从而将ROM里的波形数据读出,最后通过DA转换输出模拟信号的波形。

        要实现波形可调,信号发生器就要能分时输出多种波形,本次设计选用了4种常用的模拟信号:正弦波、锯齿波、方波、三角波,那么就需要4个ROM来分别存储4种波形的波形数据,在写Verilog代码时,直接调用BLOCK ROM的IP核即可。

        要实现波、幅、频、相可调,就需要在输入端采用某种方式来控制波形、幅度、频率和相位的改变,这个方式可以是按键、矩阵键盘、上位机等等,本次设计选择最简单的按键方式(按下时为高电平):每次按键按下时,改变波形、幅度、频率和相位。既然使用了按键,那么为了消除按键按下和松开时的机械干扰,在写Verilog代码时,按键消抖模块就必不可少。

        消抖后的按键信号进入一个DDS控制模块,该模块根据相应按键的次数计算波形、幅度、频率和相位的值(本次设计可实现幅度1到15倍整数可调、频率1到50倍整数倍频、相位15度的整数倍可调),从而产生2位波形控制信号、4位幅度控制信号、6位频率控制信号、9位相位控制信号。这些信号控制ROM的地址,从而控制对ROM里的数据的读操作。

        最后,利用一个4选1的MUX(用波形控制信号来做地址)即可实现12位数字波形数据的输出。

        根据以上分析,作出本设计功能框图如图1所示:

 图1

        由于我个人的开发板上没有DAC,所以没有设计DAC模块,不过可以在仿真时,用Vivado软件自带的仿真工具直接把12位数字波形输出显示为模拟形式就能看到模拟波形。(这里说一句Vivado牛批)

二、设计过程:

2.1 按键消抖模块:

2.1.1 基本原理:

        由于机械按键里有一个复位弹簧,相当于一个欠阻尼二阶振荡系统,具有一定的惯性,所以当按键按下和松开时,会先振荡一段时间,然后才稳定到一个确定的电平。图2为理想和实际的按键电平时序图:

 图2

        这些振荡就称为按键抖动,毫无疑问,这种意料之外的电平抖动会对逻辑造成干扰,必须滤除(消抖)。常用的按键消抖方式有硬件消抖和软件消抖,前者是在按键后加一个硬件滤波电路,如RC电路等等;后者是通过类似中断的方式,当按键按下或松开时,先经过一定时间的延时(一般为20ms),之后再判断按键的状态是否已经稳定。

        本次设计采用软件消抖,并利用Verilog代码编写状态机来实现。具体是将按键消抖的整个过程分为4个状态:

        状态S0:即空闲态,该状态等待按键按下,若按键按下,则跳转到S1;若按键未按下,则不跳转。

        状态S1:该状态在按键按下时控制一个计时器进行计时,若计时器还未计到20ms,按键电平就拉低,说明产生了一次按下抖动,状态不跳转且计时器清零;若计时器计到20ms,按键电平还未拉低,说明按下已经稳定,计时器清零且状态跳转到S2。

        状态S2:该状态等待按键松开,若按键松开,则跳转到S3;若按键未松开,则不跳转。

        状态S3:该状态在按键松开时控制一个计时器进行计时,若计时器还未计到20ms,按键电平就拉高,说明产生了一次松开抖动,状态不跳转且计时器清零;若计时器计到20ms,按键电平还未拉高,说明松开已经稳定,计时器清零且状态跳转到S0。

        上述状态机的状态转换,是由按键按下时产生的上升沿和按键松开时产生的下降沿来触发的。因此,首先就需要采集这两个边沿,本设计采用一个2位寄存器来实现边沿采集,如图3:

    图3 

        由上图可知,若某一时刻,D1为“0”且D0为“1”,则采集到按键输入的一个上升沿;若某一时刻,D1为“1”且D0为“0”,则采集到按键输入的一个下降沿。

        至此,可以得到按键消抖状态转换图如图4(I和O代表按键输入和消抖输出的逻辑值):

图4

2.1.2 具体描述:

        根据状态机,就可以用RTL语言将按键消抖模块描述出来,编写的Verilog代码如下,状态机部分采用三段式描述:

`timescale 1ms / 1ps

module KEY_filter(
    input clk, //50MHz主时钟
    input rst, //复位信号,高电平有效
    input key_in, //输入的按键信号,按下时为高电平
    
    output key_filter //消抖后的按键信号
    );
    
    reg [1:0] key_test; //按键边沿检测寄存器
    reg key_filter_R; //消抖后的按键信号寄存器

/*************检测边沿************/    
    always @ (posedge clk)
    key_test <= {key_test[0],key_in};
    
    reg [1:0] c_sta = 2'd0; //现态
    reg [1:0] n_sta = 2'd0; //次态
	
	reg [19:0] cnt = 20'd0; //计时器

/**********计时器控制**********/
    always @ (posedge clk or negedge rst)
    begin
        if (rst)
        cnt <= 20'd0;
        else if (cnt == 20'd999999)
        cnt <= 20'd0;
        else
            case (n_sta)
            2'd0:cnt <= 20'd0;
            2'd1:
            begin
                if ( (key_test == 2'b10 && cnt < 20'd999999) || (key_test != 2'b10 && cnt == 20'd999999) )
                cnt <= 20'd0;
                else
                cnt <= cnt + 1'd1;
            end
            2'd2:cnt <= 20'd0;
            2'd3:
            begin
                if ( (key_test == 2'b01 && cnt < 20'd999999) || (key_test != 2'b01 && cnt == 20'd999999) )
                cnt <= 20'd0;
                else
                cnt <= cnt + 1'd1;
            end
            default:;
            endcase
    end
    
/*************状态的转换***********/
    always @ (posedge clk or negedge rst)
    begin
        if (rst)
        c_sta <= 2'd0;
        else
        c_sta <= n_sta;
    end
    
/*************各状态转换条件***********/
    always @ (*)
    begin
        case(c_sta)
            2'd0:
            begin
                if (key_test == 2'b01)
                    n_sta = 2'd1;
                else
                    n_sta = 2'd0;
            end
            2'd1:
            begin
                if (key_test == 2'b10 && cnt < 20'd999999)
                    n_sta = 2'd1;
                else if (key_test != 2'b10 && cnt == 20'd999999)
                   n_sta = 2'd2;
                else
                    n_sta = 2'd1;
            end
            2'd2:
            begin
                if (key_test == 2'b10)
                    n_sta = 2'd3;
                else
                    n_sta = 2'd2;
            end
            2'd3:
            begin
                if (key_test == 2'b01 && cnt < 20'd999999)
                    n_sta = 2'd2;
                else if (key_test != 2'b01 && cnt == 20'd999999)
                    n_sta = 2'd0;
                else
                    n_sta = 2'd3;
            end
            default:;
        endcase
    end
    
/*************各状态的输出***********/
    always @ (posedge clk or negedge rst)
    begin
        if (rst)
        key_filter_R <= 1'd0;
        else
            case (n_sta)
            2'd0:key_filter_R <= 1'd0;
            2'd2:key_filter_R <= 1'd1;
            default:;
        endcase
    end

/*************给消抖后的按键信号赋值***********/
assign key_filter = key_filter_R;

endmodule

2.1.3 仿真测试:

        编写的testbeach如下:

`timescale 1ms / 1ps

module TB_KEY_filter(

    );
    
    reg clk; //50MHz主时钟
    reg rst; //复位信号,高电平有效
    reg key_in; //输入的按键信号,按下时为高电平
    
    wire key_filter; //消抖后的按键信号
    
    KEY_filter uut_KEY_filter(
    .clk(clk), //50MHz主时钟
    .rst(rst), //复位信号,高电平有效
    .key_in(key_in), //输入的按键信号,按下时为低电平
    
    .key_filter(key_filter) //消抖后的按键信号 
    );
    
/**************产生50MHz时钟**************/    
    always #0.00001 clk = ~clk;

/**************输入初始化**************/ 
    initial 
    begin
        clk = 1'd0;
        rst = 1'd1;
        key_in = 1'd0;

/**************按键第一次按下**************/ 
        #0.1
        rst = 1'd0;
        key_in = 1'd1;

/**************模拟按下时的抖动**************/         
        #0.1
        key_in = 1'd0;
        #0.2
        key_in = 1'd1;
        #0.1
        key_in = 1'd0;
        #0.2
        key_in = 1'd1;
        
/**************按键第一次松开**************/ 
        #50
        key_in = 1'd0;
        
/**************模拟松开时的抖动**************/ 
        #0.1
        key_in = 1'd1;
        #0.1
        key_in = 1'd0;
        #0.2
        key_in = 1'd1;
        #0.1
        key_in = 1'd0;
    end
    
endmodule

        在Vivado自带的仿真器上跑出的按键消抖模块仿真结果如图5至图8所示:

图5 

        由上图可以看到,在按键按下抖动期间,按键消抖模块的输出依然为“0”。

图6 

        由上图可以看到,在按键松开抖动期间,按键消抖模块的输出依然为“1”。 

图7 

        由上图可以看到,按键按下后,从最后一次抖动到按键消抖模块的输出变为“1”,中间的时间就是20ms。   

图8 

        由上图可以看到,按键松开后,从最后一次抖动到按键消抖模块的输出变为“0”,中间的时间也是20ms。 

        至此,按键消抖模块设计成功。

2.2 波形数据的存储:

2.2.1 生成COE文件:

        首先要生成每种波形的数字波形数据,这可以使用MATLAB、Guagle等软件生成一个MIF文件或者COE文件的方式来实现。MIF文件和COE文件都是用来列出存储器里存放的所有数据的文件,但两者的文件格式不同。本次设计利用Guagle软件先生成各种波形的MIF文件,再将MIF文件改成COE文件,图9是生成的正弦波COE文件的一部分:

图9 

2.2.2 调用BLOCK ROM IP核:

        Xilinx的FPGA中具有很多不同功能的IP核,调用这些IP核可以大大提高设计效率。上面说到,本次设计利用BLOCK ROM的IP核来存放波形数据,具体是调用数据宽度为8,数据深度为512的单口ROM,IP配置如图10所示:

图10 

        之后,将生成的波形COE文件,放到IP核里即完成了波形数据的存储。

2.3 DDS控制模块: 

2.3.1 波形调整:

        波形调整实现是利用输入的波形控制键控制2位波形控制信号,从而改变4种输出波形。具体过程为:当波形控制键按下时,产生波形按键输入信号,该信号经过按键消抖后存入一个边沿检测寄存器(原理同按键边沿检测寄存器),每当边沿检测寄存器检测到一个上升沿,波形控制信号就加1,加到3时清零,最后,波形控制信号再去控制一个4选1的数据选择器即可实现4种输出波形的调整。 

2.3.2 幅度调整:

        实现幅度调整是比较简单的,只需利用一个乘法器,把波形输出先乘上一个放大倍数再输出即可。但采用这种方式只能实现放大倍数较低的放大,因为随着放大倍数的增加,输出的位宽势必也会增加,不过在实际应用中,可以在本设计的输出端加上模拟放大器,从而实现高增益放大。本设计能实现的最高增益是15倍,而在ROM里存放的8位波形数据最大为“0xFF”,故波形输出的最大数据为“0xEF1”,故波形输出位宽应该为12位。 

        具体过程为:当幅度控制键按下时,产生幅度按键输入信号,该信号经过按键消抖后存入一个幅度边沿检测寄存器,每当检测到一个上升沿,幅度控制信号就加1,加到15时清零,最后,每个从ROM里输出的波形数据乘上幅度控制信号即实现幅度调整。

2.3.3 相位调整:

        由于存放在ROM里的波形数据是512个离散的数据点,若从地址为0的数据开始输出,那么输出的波形相位就为0°,由此不难想到,只要改变ROM的初始输出地址,就能实现相位调整。

        设需要产生的相移为N°,则对应的相位控制信号P为:

P=\frac{511N}{360}

        基于以上算法,本设计实现相位15°的整数倍可调,那么每次产生的相位控制信号就应该为21的整数倍,具体过程为:当相位控制键按下时,产生相位按键输入信号,该信号经过按键消抖后存入一个相位边沿检测寄存器,每当检测到一个上升沿,相位控制信号就加21,加到504时清零,然后将原来的ROM初始输出地址赋值为新的相位控制信号,从而实现相位调整。

2.3.4 频率调整:

        利用波形数据的离散性,也能实现频率调整。在一定的初始输出地址下,若每间隔一个数据点输出一次,那么输出的波形频率就为原来的2倍,依此类推,只要改变ROM数据采样的间隔,就能实现频率调整。但是,根据奈奎斯特采样定理(采样频率必须大于等于被采样信号最高频率的2倍,否则采样后信号会失真),采样间隔不可能无限增大。

        本次设计实现频率1到50倍整数倍频。具体过程为:当频率控制键按下时,产生频率按键输入信号,该信号经过按键消抖后存入一个频率边沿检测寄存器,每当检测到一个上升沿,频率控制信号就加1,加到50时清零,然后在主时钟的控制下,每次主时钟到来,原来的ROM初始输出地址就增加一个频率控制信号,从而实现频率调整。

         综上,每次主时钟到来,ROM地址W的算法为(W1为上一时刻的地址,P为相位控制信号,F为频率控制信号):

W1=P

W=W1+F

2.4 顶层描述:

        根据以上的设计思路,编写的Verilog顶层代码如下:

`timescale 1ms / 1ps

module DDS_top(
	input clk, //50MHz主时钟
    input rst, //复位信号,高电平有效

    input W_ctrl, //波形控制键
    input A_ctrl, //幅度控制键
    input P_ctrl, //相位控制键
    input F_ctrl, //频率控制键
    
    output reg [11:0] wave_out //波形输出信号
    );
    
    wire W_ctrl_key; //消抖后的波形控制键
    wire A_ctrl_key; //消抖后的幅度控制键
    wire P_ctrl_key; //消抖后的相位控制键
    wire F_ctrl_key; //消抖后的频率控制键

    wire [7:0] wave_sin; //正弦波输出信号
    wire [7:0] wave_Sawtooth; //锯齿波输出信号
    wire [7:0] wave_square; //方波输出信号
    wire [7:0] wave_Triangular; //三角波输出信号
    
    reg [1:0] r_M; //波形边沿检测寄存器
    reg [1:0] r_A; //幅度边沿检测寄存器
    reg [1:0] r_P; //相位边沿检测寄存器
    reg [1:0] r_F; //频率边沿检测寄存器
    
    reg [1:0] WAVE = 2'd0; //波形控制信号,实现输出波形相位超前15度的整数倍可调
    reg [3:0] AMPLITUDE = 4'd1; //幅值控制信号,实现输出波形幅值1到15整数倍可调
    reg [8:0] PHASE = 9'd0; //相位控制信号,实现输出波形相位超前15度的整数倍可调
    reg [5:0] FREQUENCY = 6'd1; //频率控制信号,实现输出波形频率倍频1到50倍整数可调
    reg [8:0] wave_add = 9'd0; //波形数据的地址

/*****************调用按键消抖模块实现波形选择信号消抖*****************/
    KEY_filter key_WAVE(
        .key_filter     (W_ctrl_key),
        .clk            (clk),
        .rst            (rst),
        .key_in         (W_ctrl)
    );
    
/*****************调用按键消抖模块实现幅度控制字消抖*****************/
    KEY_filter key_AMPLITUDE(
        .key_filter     (A_ctrl_key),
        .clk            (clk),
        .rst            (rst),
        .key_in         (A_ctrl)
    );
    
/*****************调用按键消抖模块实现相位控制字消抖*****************/
    KEY_filter key_PHASE(
        .key_filter     (P_ctrl_key),
        .clk            (clk),
        .rst            (rst),
        .key_in         (P_ctrl)
    );
    
/*****************调用按键消抖模块实现频率控制字消抖*****************/
    KEY_filter key_FREQUENCY(
        .key_filter     (F_ctrl_key),
        .clk            (clk),
        .rst            (rst),
        .key_in         (F_ctrl)
    );
 
/*****************波形边沿检测*****************/
    always @ (posedge clk)
    r_M <= {r_M[0],W_ctrl_key};

/*****************幅度边沿检测*****************/
    always @ (posedge clk)
    r_A <= {r_A[0],A_ctrl_key};
    
/*****************相位边沿检测*****************/
    always @ (posedge clk)
    r_P <= {r_P[0],P_ctrl_key};

/*****************频率边沿检测*****************/
    always @ (posedge clk)
    r_F <= {r_F[0],F_ctrl_key};

/*****************波形控制*****************/
    always @ (posedge clk or negedge rst)
    begin
        if (rst)
            WAVE <= 2'd0;
        else if (r_M == 2'b01)
            if (WAVE == 2'd3)
                WAVE <= 2'd0;
            else
            WAVE <= WAVE + 1'b1;
        else 
            WAVE <= WAVE;
    end
    
/*****************幅度控制*****************/
    always @ (posedge clk or negedge rst)
    begin
        if (rst)
            AMPLITUDE <= 6'd1;
        else if (r_A == 2'b01)
            if (AMPLITUDE == 4'd15)
                AMPLITUDE <= 6'd1;
            else
            AMPLITUDE <= AMPLITUDE + 1'b1;
        else 
            AMPLITUDE <= AMPLITUDE;
    end
    
/*****************相位和地址控制*****************/
    always @ (posedge clk or negedge rst)
    begin
        if (rst)
        begin
            PHASE <= 9'd0;
            wave_add <= 9'd0;
        end
        else if (r_P == 2'b01)
            if (PHASE == 9'd504)
                PHASE <= 9'd0;
            else
            begin
            PHASE = PHASE + 15*511/360;
            wave_add = PHASE;
            end
        else
        begin 
            PHASE <= PHASE;
            wave_add <= wave_add + FREQUENCY;
        end
    end

/*****************频率控制*****************/
    always @ (posedge clk or negedge rst)
    begin
        if (rst)
            FREQUENCY <= 6'd1;
        else if (r_F == 2'b01)
            if (FREQUENCY == 6'd50)
                FREQUENCY <= 6'd1;
            else
            FREQUENCY <= FREQUENCY + 1'b1;
        else 
            FREQUENCY <= FREQUENCY;
    end

/*****************输出波形*****************/
    always @ (posedge clk)
    begin
        case (WAVE)
            2'd0:wave_out <= wave_sin * AMPLITUDE;
            2'd1:wave_out <= wave_Sawtooth * AMPLITUDE;
            2'd2:wave_out <= wave_square * AMPLITUDE;
            2'd3:wave_out <= wave_Triangular * AMPLITUDE;
            default:wave_out <= 12'd0;
            endcase
    end

/*****************调用正弦波块ROM*****************/
sin u1 (
  .clka(clk),   
  .ena(1'd1),      
  .addra(wave_add),  
  .douta(wave_sin)  
);

/*****************调用锯齿波块ROM*****************/
Sawtooth u2 (
  .clka(clk),    
  .ena(1'd1),     
  .addra(wave_add), 
  .douta(wave_Sawtooth)  
);

/*****************调用方波块ROM*****************/
square u3 (
  .clka(clk),    
  .ena(1'd1),     
  .addra(wave_add),  
  .douta(wave_square) 
);

/*****************调用三角波块ROM*****************/
Triangular u4 (
  .clka(clk),  
  .ena(1'd1),     
  .addra(wave_add),
  .douta(wave_Triangular)  
);

endmodule

2.5 顶层仿真测试:

        编写的testbeach如下:

`timescale 1ms / 1ps

module TB_DDS_top(

    );
    reg clk; //系统主时钟
    reg rst; //复位信号,高电平有效
    
    reg W_ctrl; //波形控制键
    reg A_ctrl; //幅度控制键
    reg P_ctrl; //相位控制键
    reg F_ctrl; //频率控制键
    
    wire [11:0] wave_out; //波形输出信号
    
/**************模块例化**************/
    DDS_top TB_UUT(
    .clk(clk), 
    .rst(rst),
    .W_ctrl(W_ctrl),
    .wave_out(wave_out),
    .A_ctrl(A_ctrl),
    .P_ctrl(P_ctrl),
    .F_ctrl(F_ctrl)
    );
   
/**************产生50MHz时钟**************/ 
    always #0.00001 clk = ~clk;

/**************输入初始化**************/        
    initial 
    begin
        clk = 1'd0;
        rst = 1'd0;
        W_ctrl = 1'd0;
        A_ctrl = 1'd0;
        P_ctrl = 1'd0;
        F_ctrl = 1'd0;
        
/**************4个控制键第一次按下**************/ 
        #1
        W_ctrl = 1'd1;
        A_ctrl = 1'd1;
        P_ctrl = 1'd1;
        F_ctrl = 1'd1;
/**************4个控制键第一次松开**************/ 
        #21
        W_ctrl = 1'd0;
        A_ctrl = 1'd0;
        P_ctrl = 1'd0;
        F_ctrl = 1'd0;
/**************4个控制键第二次按下**************/ 
        #21
        W_ctrl = 1'd1;
        A_ctrl = 1'd1;
        P_ctrl = 1'd1;
        F_ctrl = 1'd1;
/**************4个控制键第二次松开**************/ 
        #21
        W_ctrl = 1'd0;
        A_ctrl = 1'd0;
        P_ctrl = 1'd0;
        F_ctrl = 1'd0;
/**************4个控制键第三次按下**************/ 
        #21
        W_ctrl = 1'd1;
        A_ctrl = 1'd1;
        P_ctrl = 1'd1;
        F_ctrl = 1'd1;
/**************4个控制键第三次松开**************/
        #21
        W_ctrl = 1'd0;
        A_ctrl = 1'd0;
        P_ctrl = 1'd0;
        F_ctrl = 1'd0;
/**************4个控制键第四次按下**************/
        #21
        W_ctrl = 1'd1;
        A_ctrl = 1'd1;
        P_ctrl = 1'd1;
        F_ctrl = 1'd1;
/**************4个控制键第四次松开**************/        
        #21
        W_ctrl = 1'd0;
        A_ctrl = 1'd0;
        P_ctrl = 1'd0;
        F_ctrl = 1'd0;
    end
    
endmodule

        顶层仿真结果如图11至图15所示: 

图11 

        由上图可以看到,当4个控制键第一次按下时,波形由正弦波变为锯齿波,同时相移15°,倍频2倍,幅度放大2倍。

图12 

        由上图可以看到,当4个控制键第二次按下时,波形由锯齿波变为方波(图中模拟波形显示的是三角波,但是根据图13所示,转为数字显示后确实是方波的波形数据,所以在这里我怀疑应该是Vivado仿真器分辨率的问题),同时相较一开始的正弦波,相移30°,倍频3倍,幅度放大3倍。

图13 

图14 

        由上图可以看到,当4个控制键第三次按下时,波形由方波变为三角波,同时相较一开始的正弦波,相移45°,倍频4倍,幅度放大4倍。

图15 

        由上图可以看到,当4个控制键第四次按下时,波形由三角波变回正弦波,同时相较一开始的正弦波,相移60°,倍频5倍,幅度放大5倍。

        至此,整个设计完成。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐