Menu Close

Vivado下锯齿波通过ROM 查找表转换成正弦波

正弦波(sine wave)在工程中有着重要的意义,因为正弦波是交流信号变化的最基本和最常见的波形,并且傅里叶定理指出所有可能的波形都可以由无数个正弦波组成。

正弦波也在有电感和电容的振荡电路(oscillator circuit)中自然产生。 如果将电容和电感以正确的方向放置在电路中,则电路将产生与电路中电感和电容的量成正比的正弦波。 因此,正弦波在许多电路和设备中自然产生,这也是工程师对它们进行研究的原因。在数字电路中,正弦波可以直接由EDA软件生成,本文介绍Vivado下一种常见的使用锯齿波(sawtooth wave)生成正弦波的方法。图1所示是正弦波和锯齿波。

 

图 1 (1)正弦波

图 1 (2)锯齿波

 

之前的文章中介绍了Vivado下关于ROM的使用,参考下列文章了解更多信息。

 

1.工程设计思路

本文将使用在Verilog中常见的锯齿波,通过ROM查找表,生成正弦波。主要做法是将连续的正弦波离散化,并将离散化后,一个周期内的幅值数据全部转化成非负数据,储存进ROM,在Verilog中使用递增的地址(对应为锯齿波)控制ROM中读出正弦波数据,并在仿真中用波形模拟观察结果,如图2所示。图2中红色的线是正弦波被相同周期的锯齿波上的点采样得到的离散值。

 

图2 使用锯齿波采样正弦波

 

 

2.工程建立步骤

(1)生成相应的*.coe文件

这里使用的是Vivado的仿真工程来生成*.coe文件,关于Vivado仿真工程生成coe/mif文件的信息,参见文章Verilog 仿真工程应用-生成coe/mif 文件。

代码如下:

`timescale 1ns / 1ps
module sim_top(
);
// ====================== user parameter setting =======================

localparam  INPUT_DATA_RADIX    = 10;   // 16,  10 , 2
localparam  INPUT_DATA_WID      = 8;
localparam  INPUT_DATA_DEPTH     = 256;

localparam  SAMPLE_AMPLITUDE	= (2**INPUT_DATA_WID)/2;

// data need to convert
real temp;
reg [INPUT_DATA_WID - 1:0] input_data [0:INPUT_DATA_DEPTH - 1];
//real input_data_r;

reg data_ena = 0;
integer i;
reg [INPUT_DATA_WID - 1:0] wave_r;
initial
begin
    data_ena = 0;
    for(i = 0; i < INPUT_DATA_DEPTH; i = i + 1)
    begin
        temp = (SAMPLE_AMPLITUDE - 1)*($sin((i*2*3.1415926)/INPUT_DATA_DEPTH)) +  (SAMPLE_AMPLITUDE - 1);
        input_data[ i ] = temp;
        wave_r = temp;
        #10;
    end
    data_ena = 1;
end

// =====================================================================
reg stop_flag = 0;
            
localparam FILE_COE = "../../../test.coe";

integer n;
integer fd_coe;


initial begin
    n = 0;
    fd_coe = $fopen(FILE_COE, "w");
    if(fd_coe == 0)
    begin
        $display("$open coe file failed") ;
        $stop;
    end
    
    while(data_ena == 0) #10;
    
    $display("\n ============= coe file opened... ============= ") ;
// =====================================================================
// coe file    
    $fwrite(fd_coe,"memory_initialization_radix = %0d;\n",INPUT_DATA_RADIX);
    $fwrite(fd_coe,"memory_initialization_vector = ");
    #10;

    for (n = 0; n < INPUT_DATA_DEPTH; n = n + 1)
    begin
        if( INPUT_DATA_RADIX == 16)
            $fwrite(fd_coe,"\n%h", input_data[n]);
        if( INPUT_DATA_RADIX == 2)
            $fwrite(fd_coe,"\n%b", input_data[n]);
        if( INPUT_DATA_RADIX == 10)
            $fwrite(fd_coe,"\n%d", input_data[n]);
        
        #10;
    end
    
    $fwrite(fd_coe,";");
    #10;
    $fclose(fd_coe) ;
    $display("\n ============= coe file closed... ============= ") ;
// =====================================================================

    stop_flag = 1;
    #100;
    $stop;
end

    
endmodule

 

注意这行代码:

temp = (SAMPLE_AMPLITUDE - 1)*($sin((i*2*3.1415926)/INPUT_DATA_DEPTH)) + (SAMPLE_AMPLITUDE - 1);

SAMPLE_AMPLITUDE是通过(2**INPUT_DATA_WID)/2计算出来的。例如,数据宽度为8位时,可表达的无符号数量程是(0,2^8 -1 )= (0,255),那么正弦函数的峰峰值(peak to peak amplitude)应该是255,那么该正弦波的量程应该是(-128,127)。这里为了换算方便,舍弃最小值(-127,127),并将整个正弦波向上平移127,使其量程变为(0,254)。

这就是公式是标红部分所表达的逻辑:

temp = (SAMPLE_AMPLITUDE - 1)*($sin((i*2*3.1415926)/INPUT_DATA_DEPTH)) + (SAMPLE_AMPLITUDE - 1);

注意生成正弦波使用的是Verilog的系统函数$sin(不可综合,只可在仿真中使用)。数据深度= INPUT_DATA_DEPTH,表示在正弦波一个周期内的采样点个数,例如,将正弦波的输入分为256份,INPUT_DATA_DEPTH= 256。在(0,2*pi)一个周期内每次采样的点位置应该是

这里特别需要注意的是:i是范围在(0,255)之间的整数,i/256的结果永远是0,这里可以通过交换乘除法的位置,将整数运算结果类型转换到实数范围,所以可以执行

因为Verilog下没有π,所以这里使用了3.1415926近似值,如果对精度有更高要求,可以使用更精确的值。注意这个时候 i X 3.1415926 的结果是用实数表示的,即使之后再除以256,结果也不是整数。之后的运算都是有小数部分,最后的结果也是由实数temp承接。

生成的*.coe文件如下(注册用户可下载):

[content_control] test.zip [/content_control]

图 3  生成*.coe文件部分截图

注意下列代码生成*.coe文件的位置指示的是…\verilog_coe_mif\verilog_coe_mif.sim\sim_1\behav\xsim 的往前数第三级目录,即…\verilog_coe_mif\verilog_coe_mif.sim

localparam FILE_COE = "../../../test.coe";

 

(2)生成ROM IP核

这里选用的是Block Memory Generator,在Interface Type处选择 Native,Memory Type处选择Single Port ROM,如图4所示:

图 4

 

Port A Width 选择8,Port A Depth选择256。Enable Port Type选择Always Enabled,勾选Primitives Output Register。

图 5

 

在Memory Initialization下勾选Load Init File,并将上面仿真工程生成的test.coe文件装载,完成ROM IP核的生成。

图 6

 

(3)顶层例化文件

例化文件如下所示:

`timescale 1ns / 1ps



module top(
    input clk,
    output [7:0] data
    );
    
reg [7:0] addr = 0;

always@(posedge clk)
addr <= addr + 1;


blk_mem_gen_0 sine_rom_inst (
  .clka(clk),    // input wire clka
  .addra(addr),  // input wire [7 : 0] addra
  .douta(data)  // output wire [7 : 0] douta
);

endmodule

 

注意这里

reg [7:0] addr = 0;

always@(posedge clk)
    addr <= addr + 1;

表示的是生成最大幅值为255,周期为256个时钟周期的锯齿波。

 

3.工程仿真

仿真程序如下:

`timescale 1ns / 1ps

module tb(
);

reg clk = 0;
wire [7:0] data;
always #10 clk = ~clk;    //50 MHz
 
 
top top_inst(
.clk(clk),
.data(data)
);
endmodule

 

仿真波形如下:

图 7

 

从仿真图中可以看出ROM的输出比地址要慢两个时钟周期,符合预期结果。并且地址中的值与*.coe文件对比也是正确的。

接下来,观察输入和输出的波形,右键点击data和addr,选择Waveform Style > Analog,如图所示。

图 8

最终的波形图如下所示,注意需要缩小时间范围来观察,因为一个完整的正弦波形需要占用5 us左右:

图 9 仿真波形

 

4.扩展实验-自建sine函数IP

前文提到过$sin函数是系统函数,只可进行仿真,不能进行综合。如果在实体程序中需要使用sine函数,需要在工程中自建。有了前文实验的基础,实现起来并不难。接下来,就带领大家做一个扩展工程,建立一个可以综合的sine函数。

工程主要构成如下:

  • 由Vivado仿真工程采样系统函数$sin生成的*.coe文件
  • 使用ROM IP核装载*.coe文件
  • 在顶层工程中将ROM地址(锯齿波)输入ROM IP核,读出ROM中存储的波形离散点,组合变化后生成相应的正弦波后输出

(1)工程设计

  • 通过Vivado仿真工程采样$sin系统函数。采样间隔是1度,采样区间在[0,90] (以360度为一个正弦函数周期划分),幅值为 127,如图10所示。这里利用了正弦函数波形的规律,只采样连续的1/4波形,然后在适当变换后还原一个完整周期的波形。这样做可以节省内存资源。

图 10 采样区间和采样递增区间

 

  • 通过参数传递,可以选择将ROM中的sine函数的幅值拉伸;同时也可以选择波形的符号:波形可以和原始的正弦函数一样,既有正值,也有负值;或是全为正值,如图11所示。

图 11 选择波形的符号

 

(2)工程建立步骤

  • 稍微修改下用来生成*.coe文件的Vivado的仿真工程,工程如下:
`timescale 1ns / 1ps
module sim_top(
);
// ====================== user parameter setting =======================

localparam  INPUT_DATA_RADIX    = 10;   // 16,  10 , 2
localparam  INPUT_DATA_WID      = 8;
localparam  INPUT_DATA_DEPTH     = 90;

localparam  SAMPLE_AMPLITUDE	= (2**INPUT_DATA_WID)/2;

// data need to convert
real temp;
reg [INPUT_DATA_WID - 1:0] input_data [0:INPUT_DATA_DEPTH];
//real input_data_r;

reg data_ena = 0;
integer i;
reg [INPUT_DATA_WID - 1:0] wave_r;
initial
begin
    data_ena = 0;
    for(i = 0; i < INPUT_DATA_DEPTH + 1; i = i + 1)   //sin function repeated for every pi/2 radius
    begin
        temp = (SAMPLE_AMPLITUDE - 1)*($sin((i*2*3.1415926)/360));
        input_data[ i ] = temp;
        wave_r = temp;
        #10;
    end
    data_ena = 1;
end

// =====================================================================
reg stop_flag = 0;
            
localparam FILE_COE = "../../../test.coe";

integer n;
integer fd_coe;


initial begin
    n = 0;
    fd_coe = $fopen(FILE_COE, "w");
    if(fd_coe == 0)
    begin
        $display("$open coe file failed") ;
        $stop;
    end
    
    while(data_ena == 0) #10;
    
    $display("\n ============= coe file opened... ============= ") ;
// =====================================================================
// coe file    
    $fwrite(fd_coe,"memory_initialization_radix = %0d;\n",INPUT_DATA_RADIX);
    $fwrite(fd_coe,"memory_initialization_vector = ");
    #10;

    for (n = 0; n < INPUT_DATA_DEPTH + 1; n = n + 1)
    begin
        if( INPUT_DATA_RADIX == 16)
            $fwrite(fd_coe,"\n%h", input_data[n]);
        if( INPUT_DATA_RADIX == 2)
            $fwrite(fd_coe,"\n%b", input_data[n]);
        if( INPUT_DATA_RADIX == 10)
            $fwrite(fd_coe,"\n%d", input_data[n]);
        
        #10;
    end
    
    $fwrite(fd_coe,";");
    #10;
    $fclose(fd_coe) ;
    $display("\n ============= coe file closed... ============= ") ;
// =====================================================================

    stop_flag = 1;
    #100;
    $stop;
end

    
endmodule

 

修改的位置主要如下:

// ====================== user parameter setting =======================

localparam INPUT_DATA_RADIX = 10; // 16, 10 , 2
localparam INPUT_DATA_WID = 8;
localparam INPUT_DATA_DEPTH = 90;   //只采样[0,90]的区间

localparam SAMPLE_AMPLITUDE = (2**INPUT_DATA_WID)/2;

// data need to convert
real temp;
reg [INPUT_DATA_WID - 1:0] input_data [0:INPUT_DATA_DEPTH];
//real input_data_r;

reg data_ena = 0;
integer i;
reg [INPUT_DATA_WID - 1:0] wave_r;
initial
begin
    data_ena = 0;
    for(i = 0; i < INPUT_DATA_DEPTH + 1; i = i + 1) //sin function repeated for every pi/2 radius
    begin
        temp = (SAMPLE_AMPLITUDE - 1)*($sin((i*2*3.1415926)/360));  //分为360份
        input_data[i] = temp;
        wave_r = temp;
        #10;
    end     
    data_ena = 1;
end

// =====================================================================

注意这里仿真重新生成的*.coe文件,需要再次加载到ROM IP核中(以防Vivado使用之前缓存住的IP核信息)。

 

  • sine函数模块

顶层模块代码如下:

`timescale 1ns / 1ps



module top#(
parameter WAVE_AMPLITUDE_WIDTH = 8,          //正弦波幅值范围
parameter WAVE_SIGN = "NEGATIVE_POSTIVIE"    //正弦波的符号
)
(
    input clk,
    output [ WAVE_AMPLITUDE_WIDTH-1: 0 ] amplified_data   //被调整幅值后的正弦波
);
    
reg [ 8: 0 ] addr = 0;       //原本的[0,360]范围

reg [ 8: 0 ] rom_addr;       //输入ROM地址范围为[0,90]
wire signed [ 7: 0 ] rom_data;     //输出ROM波形范围为[0,127]    
reg signed [ 7: 0 ] signed_rom_data;     //加上符号的波形范围为[-127,127]


localparam AMPLITUE = (2**WAVE_AMPLITUDE_WIDTH)/2 - 1;    //根据数据宽度计算幅值
localparam SIM_AMP = 127;                                 //仿真时使用的幅值,用来按比例缩放幅值

//-----------根据正弦波符号的选择,来调整幅值----------
assign amplified_data = (WAVE_SIGN == "NEGATIVE_POSTIVIE")? (signed_rom_data*AMPLITUE/SIM_AMP)
                        :((signed_rom_data*AMPLITUE/SIM_AMP)+AMPLITUE);

//-----------生成锯齿波-------------
always@(posedge clk)
begin
    addr <= addr + 1;
    
    if(addr >= 360)
        addr <= 0;
end


//----------将addr [0,360]转换为rom_addr[0,90]------------
always@(posedge clk)
    rom_addr <= ((addr <= 90) && (addr >= 0)) ? addr :(
                (addr < 180) ? (180 - addr) : (
                (addr < 270 ) ? (addr - 180) :
                (360 - addr)
                ));
                

//---------ROM输出数据比addr延迟一个时钟周期,所以这里判断addr也需要往后落一个度------------
always@(posedge clk)
    signed_rom_data <= ((addr <= 181) && (addr > 1)) ? (rom_data) : (~rom_data + 1);

//---------ROM IP例化------------
blk_mem_gen_0 sine_rom_inst (
  .clka(clk),    // input wire clka
  .addra(rom_addr),  // input wire [7 : 0] addra
  .douta(rom_data)  // output wire [7 : 0] douta
);



endmodule

 

在上面的模块中主要将ROM IP核中的结果读出来,并由1/4周期的波形,拼凑出一个完整的时钟周期。并根据传递进来的参数进行波形幅值,符号的变换。

 

(3)工程仿真结果

仿真工程代码如下:

`timescale 1ns / 1ps

module tb(
);

reg clk = 0;
wire [7:0] amplified_data;
always #10 clk = ~clk;

localparam  WAVE_AMPLITUDE_WIDTH = 10;
localparam WAVE_SIGN = "POSTIVIE";
    
   
top #(	
    WAVE_AMPLITUDE_WIDTH, 
    WAVE_SIGN
)top_inst(
    .clk(clk),
    .amplified_data(amplified_data)
);
endmodule

 

仿真波形如图12所示:其中绿色的波形rom_data是从ROM IP核中读出来的数据,注意这个时候还没有加上符号,蓝色的波形signed_rom_data已经将波形范围调整为[-127,127],粉色的波形是根据仿真时传递的幅度和符号进行调整后的波形。其中,可以明显的看到粉色波形amplified_data全为正,蓝色波形signed_rom_data有负值(见黄色光标处对应的值)。

 

图 12

 

图12中黄色光标停留显示的是粉色波形amplified_data和蓝色波形signed_rom_data的最低值,图13显示的是两个波形的最大值。可以看出经过放大后,粉色波形amplified_data最大值由蓝色波形signed_rom_data的127放大到1022(因为WAVE_AMPLITUDE_WIDTH为10)。

图 13

 

接下来,试着修改下传入参数WAVE_SIGN , 由“POSITIVE” 改为“NEGATIVE_POSITIVE”,WAVE_AMPLITUDE_WIDTH不变,还是设为10。仿真结果如图14所示:

图 14

 

从图14中可以看出粉色波形amplified_data的符号有负值,粉色波形amplified_data最低值由图12的0变为了-511。图15中可以看出粉色波形amplified_data的最大值是511。

图15

图12-13,对比图14-15,可以看出同样WAVE_AMPLITUDE_WIDTH=10的波形,WAVE_SIGN 为“POSITIVE”时,生成波形的量程是[0,1022];WAVE_SIGN 为“NEGATIVE_POSITIVE”时,生成波形的量程是[-511,511],符合实验的设计。

通过分析以上波形,可以看出自建sine函数IP可以正确工作,并且可以控制传入参数WAVE_SIGN ,WAVE_AMPLITUDE_WIDTH,来控制最终生成波形的幅值和符号。之后,如果需要在实体工程使用$sin函数,可以用这个自建sine函数IP,并且可以根据当前项目的需要,自行调整。如果需要更高精度的sine函数,可以将采样间隔缩小,如从每一度采一次样变为每0.5度采一次样。

5.实验思考和讨论

  • 将INPUT_DATA_DEPTH 不同值(离散采样点多和少)或INPUT_DATA_WID不同值(采样数据精度不同)得到的正弦波比较并分析。
  • 利用*.coe文件,使用ROM生成余弦波,双曲线正弦波等。
Posted in FPGA, FPGA 教材教案, FPGA开发板, Verilog, Verilog, Vivado, Vivado, 开发工具, 开发工具, 开发板, 开发语言, 教材与教案, 文章, 编程语言

发表回复

相关链接