正弦波(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生成余弦波,双曲线正弦波等。