正弦波(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 文件。
代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
`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 |
注意这行代码:
|
1 |
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)。
这就是公式是标红部分所表达的逻辑:
|
1 |
temp =<span style="color: #ff0000;"> (SAMPLE_AMPLITUDE - 1)*</span>($sin((i*2*3.1415926)/INPUT_DATA_DEPTH)) <span style="color: #ff0000;">+ (SAMPLE_AMPLITUDE - 1)</span>; |
注意生成正弦波使用的是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
|
1 |
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)顶层例化文件
例化文件如下所示:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
`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 |
注意这里
|
1 2 3 4 |
reg [7:0] addr = 0; always@(posedge clk) addr <= addr + 1; |
表示的是生成最大幅值为255,周期为256个时钟周期的锯齿波。
3.工程仿真
仿真程序如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
`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的仿真工程,工程如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
`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 |
修改的位置主要如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// ====================== user parameter setting ======================= localparam INPUT_DATA_RADIX = 10; // 16, 10 , 2 localparam INPUT_DATA_WID = 8; localparam INPUT_DATA_DEPTH =<span style="color: #0000ff;"> 90</span>; //只采样[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 < <span style="color: #0000ff;">INPUT_DATA_DEPTH + 1</span>; i = i + 1) //sin function repeated for every pi/2 radius begin temp = (SAMPLE_AMPLITUDE - 1)*($sin((i*2*3.1415926)/<span style="color: #0000ff;">360</span>)); //分为360份 input_data[i] = temp; wave_r = temp; #10; end data_ena = 1; end // ===================================================================== |
注意这里仿真重新生成的*.coe文件,需要再次加载到ROM IP核中(以防Vivado使用之前缓存住的IP核信息)。
- sine函数模块
顶层模块代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
`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)工程仿真结果
仿真工程代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
`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生成余弦波,双曲线正弦波等。