老师给了我一道题,让我用Verilog编写出来:通过100M时钟产生3M、5M和20M正弦波,并将产生的三个不同频率的正弦波加在一起,然后从这个和信号中将20M正弦波提取出来。
首先通过DDS分别产生3M、5M、20M正弦波,通过加法器将这三个正弦波加在一起,再通过设计FIR数字滤波器将20M正弦波从和信号中滤出来。整个过程思路还是很清楚的。
关于DDS原理介绍的文章很多,csdn上就很多的文章,可以参考下。
DDS,即直接数字式频率合成法,基本思想就是在存储器中存入正弦波的N个均匀间隔样值,然后以均匀速度将样值传输到数模转换器,将其转换成模拟信号。
这一块我感觉很多文章写的不是很清楚,我直接给大家看看课本上关于DDS原理的介绍:
简单的说就是在存储器ROM中,每隔k个点取一个值,实现k倍频。但由于要满足Nquist定理,所以DDS输出频率小于等于时钟频率的二分之一,即:fo <= fc/2。
DDS输出频率:
M为频率控制字,N为相位累加器的位宽。
首先我们需要生成ROM存储单元。可以直接用Verilog编写,也可以在MATLAB中生成.coe文件,通过Vivado调用ROM的IP核来实现(我更推荐第二种办法)。
MATLAB生成ROM存储单元的.coe文件(.coe文件google便可明白)。波形数据表ROM中存有一个完整周期的正弦波信号。使波形文件ROM的地址为12位,存储数据位宽11位。将一个周期的正弦波,沿横轴等间隔采样212=4096次,每次采集的信号幅度用11位数据表示,最大值为2048,最小值为0。将采集的4096个数据按顺序存入ROM中,然后以相位累加器的输出为地址,进行访问。
MATLAB生成ROM存储单元的.coe文件:
clear;clc;
depth = 4096; %4096个存储空间
width = 11; %存储数据位宽
t = 0:depth-1;
ADC = 2^11 - 1; %直流分量
A = 2^11; %幅度
s = A * sin(2*pi*t/(depth-1)) + ADC;
plot(t,s);
fid = fopen('sine.coe','wt');
fprintf(fid,'MEMORY_INITIALIZATION_RADIX=10;\n');
fprintf(fid,'MEMORY_INITIALIZATION_VECTOR=\n');
for i = 1:depth-1
fprintf(fid,'%d,\n',ceil(s(i)));
end
fprintf(fid,'%d;\n',ceil(s(depth)));
fclose(fid);
在Vivado中调用ROM的IP核
点击IP Catalog
在search中搜索rom,选择Block Memory Generator
点击Port A Option进行修改,主要修改Width和Depth的修改,与之前matlab中保持一致。
点击Other Option
之后一路OK便可。IP核设置完成之后,我们需要在模块中调用。
我们先来写3M正弦波的产生文件。
程序中ROM IP核的调用方法:点击IP Source,找到。veo文件,将框中的代码复制进DDS_3M文件中,实现IP核调用。
module DDS_3M(
input clk,
input reset,
output [11:0] data
);
parameter fre_word = 32'd128849018; //频率控制字 fre_word = f_out * 2^N / fclk N为累加器位宽
reg [31:0] addr_sin;
//相位累加器
always @(posedge clk or reset)
begin
if(reset == 0)
addr_sin <= 32'b0;
else
addr_sin <= addr_sin + fre_word;
end
wire [11:0]addra = addr_sin[31:20];
//ROM IP核的调用
sin_rom your_instance_name (
.clka(clk), // input wire clk 时钟
.addra(addra), // input wire [11 : 0] addra 相位累加器输入给rom的地址
.douta(data) // output wire [11 : 0] douta 从ROM返回的数据(3M正弦波的采样点)
);
endmodule
5M、20M正弦波的产生方法与3M完全类似,只是需要改变相位控制字即可,此处不再呈现。
下面的代码为了让后续代码编写起来更简洁
module DDS(
input clk,
input reset,
output [11:0] data_3M,
output [11:0] data_5M,
output [11:0] data_20M
);
DDS_3M DDS_3M_inst(
.clk(clk),
.reset(reset),
.data(data_3M)
);
DDS_5M DDS_5M_inst(
.clk(clk),
.reset(reset),
.data(data_5M)
);
DDS_20M DDS_20M_inst(
.clk(clk),
.reset(reset),
.data(data_20M)
);
endmodule
至此,DDS产生正弦波成功。
此处非常简单,我直接上代码:
需要注意的就是求和之后,和的位数可能会超过之前单个正弦波样值的12位,所以需要拓宽正弦波和的位宽。
module wave_summation(
input clk,
input reset,
input [11:0] wave_3M,
input [11:0] wave_5M,
input [11:0] wave_20M,
output reg [13:0] wave_sum
);
DDS wave(
.clk(clk),
.reset(reset),
.data_3M(wave_3M),
.data_5M(wave_5M),
.data_20M(wave_20M)
);
always @(posedge clk or reset)
if(reset == 0)
wave_sum = 14'b0;
else
wave_sum = wave_3M + wave_5M + wave_20M;
endmodule
滤波器的设计是我感觉比较难的一个地方。
先说说我自己遇到的问题:就是DDS模块和求和模块中,符号都是unsigned,而在滤波器设计模块,我按照一篇文章将符号改为signed,导致仿真一直出错。
滤波器的设计及调用我推荐大家这篇文章:https://blog.csdn.net/follow_moon/article/details/80376118
只是其中需要注意的是,在保存.coe文件之前需要将hn的系数改为无符号数:
注:其他操作与我推荐的博客中的操作来便可。
fir IP核调用推荐文章:
https://blog.csdn.net/judas1801/article/details/108506433
这篇文章非常使用,照着设置IP核,便可成功。之后的调用方式与ROM的调用方式完全一样。
https://blog.csdn.net/keilzc/article/details/104249702
这篇文章对于fir IP核调用的理解有很大帮助。
下面是fir滤波器的代码:
module bondpass_filter(
input clk,
input [13:0] wave_in,
output [39:0] wave_out,
output m_tvalid,
output s_tready
);
wire [15:0] s_tdata;
assign s_tdata = {2'b0,wave_in}; //s_tdata为16bit位宽,输入信号wave_in为14bit位宽,通过拼接运算填补高位
fir_compiler_0 your_instance_name (
.aclk(clk),
.s_axis_data_tvalid(1'b1),
.s_axis_data_tready(s_tready), //准备好信号,置1表示FIR可以接收数据输入
.s_axis_data_tdata(s_tdata), // input wire [15 : 0] s_tdata
.m_axis_data_tvalid(m_tvalid), //FIR输出数据有效信号,置1表示输出有效
.m_axis_data_tdata(wave_out) // output wire [39 : 0] m_tdata
);
endmodule
`timescale 1ns / 1ps
module wave_summation(
input clk,
input reset,
input [11:0] wave_3M,
input [11:0] wave_5M,
input [11:0] wave_20M,
output reg [13:0] wave_sum
);
DDS wave(
.clk(clk),
.reset(reset),
.data_3M(wave_3M),
.data_5M(wave_5M),
.data_20M(wave_20M)
);
always @(posedge clk or reset)
if(reset == 0)
wave_sum = 14'b0;
else
wave_sum = wave_3M + wave_5M + wave_20M;
endmodule
加粗样式