Verilog上的DDS合成器


在这篇文章中,我将分享我在Verilog上编写DDS合成器的理解。 它将用于产生正弦振荡,其频率和初始相位可以进行调整和计算,以与8位单极性DAC配合使用。 该合成器的工作方式在“ 组件和技术 ”杂志上的一篇文章中有很好的阐述。 为了减少正弦表的已用内存量,使用对称性。


为了在Linux下进行编译,我使用了Iverilog,并显示了GTKWave。 为了方便起见,编写了一个简单的Makefile,也许它将对您有用。 最初,使用iverilog编译器获取tb.out文件,然后将其发送到与iverilog一起安装的vvp模拟器。 结果,vvp将生成out.vcd,其中包含项目中使用的所有变量(信号)。 除上述显示目标外,还将使用变量文件启动GTKWave,您可以看到信号的波形。


SRC = nco.v TB = nco_tb.v all: iverilog -o tb.out $(TB) vvp -lxt tb.out check: iverilog -v $(TB) display: iverilog -o tb.out $(TB) vvp -lxt tb.out gtkwave out.vcd & clean: rm -rf *.out *.vcd *.vvp 

首先,您需要在内存中放置一个未来正弦表,因为我编写了一个简单的Python脚本,该脚本将正弦周期的四分之一分成64个点,并以可以复制到源代码的格式生成它。 由于我设想了将DDS用于位容量不超过8位的外部单极性DAC的实现,因此正弦幅度应在0到256的范围内,其中负半周期的范围是0 ... 127,正半周期的范围是128 ... 255 。 在这方面,将获得的正弦值(从0到pi / 4)乘以127,然后将它们相加127,结果获得了周期的第一季度的值,其幅度为128 ... 256。


我提请注意以下事实:通过这种形式,D​​AC输出的正弦将具有恒定分量。 为了将其移除,必须使其通过电容器。


 import numpy as np x=np.linspace(0,np.pi/2,64) print(np.sin(x)) y=127*np.sin(x) print(len(y)) print(y) z=[] i = 0 for elem in y: if int(elem)<=16: print("lut[%d] = 7'h0%X;" % (i, int(elem))) else: print("lut[%d] = 7'h%X;" % (i, int(elem))) z.append(hex(int(elem))) i = i + 1 

由于正弦函数是对称的(奇数),因此您可以找到第一个对称的sin(x)=-sin(pi + x)。 第二种对称性的特征在于,拥有一个表用于周期的四分之一,可以通过以相反的顺序遍历表来获得第二个四分之一(因为半周期的正弦先增大,然后减小)。


我们形成正弦


DDS合成器的大部分是相电池。 本质上,它是查找表(LUT)中元素的索引。 对于时钟信号的每个周期,其中的值都会增加一定值,结果,在输出端会获得正弦。 输出信号的频率将取决于相位累加器的增量值-越大,频率越高。 但是,根据Kotelnikov准则,采样频率应至少为信号频率的2倍(以避免频谱叠加的影响),因此对最大增量的限制是相位累加器的一半。 通常,工程标准是采样频率=信号频率的2.2,因此,决定不采用极限频率后,我又移除了一位,用8位相位电池增加了6位的增量(即使正弦波峰石已经存在)。


由于使用了对称性,因此仅2 ^ 6 = 64的低6位将直接用于索引采样。 高2位用于标识正弦波生成的四分之一周期,并因此更改表遍历的方向。 您应该得到类似于以下内容:


 module nco(clk, rst, out ); input clk, rst; output reg [7:0] out; reg [5:0] phase_inc = 6'h1; reg [7:0] phase_acc = 0; parameter LUT_SIZE = 64; reg [6:0] lut [0:LUT_SIZE-1]; always @(posedge clk) begin if (rst) begin phase_inc = 6'h1; phase_acc = 0; out = 0; lut[0] = 7'h00; //     lut[63] = 7'h7F; end else begin //      1    if (phase_acc[7:6] == 2'b00) begin //        LUT out <= {1'b1,lut[phase_acc[5:0]]}; end if (phase_acc[7:6] == 2'b01) begin out <= {1'b1,lut[~phase_acc[5:0]]}; end if (phase_acc[7:6] == 2'b10) begin out <= {1'b0,~lut[phase_acc[5:0]]}; end if (phase_acc[7:6] == 2'b11) begin out <= {1'b0,~lut[~phase_acc[5:0]]}; end phase_acc <= phase_acc + {2'b0,phase_inc}; end end endmodule 

重置时,我们将所有零初始化,除了相位增量值,我们将其设置为1。 为了保持代码的可综合性,在复位期间,表中还将填充值。 在实际项目中,建议将FPGA内置的块存储器用于此目的,并为其创建单独的配置文件,并在项目本身中使用IP内核。


关于对称如何工作的一些解释。 在每个周期中(在2个最高有效位上)检查相位累加器当前位于哪个季度。 如果最高= 00,则最高位的输出为1(负责正半波),较低位的输出-根据索引来自LUT的值。 在相位累加器的值超过63(第一个四分之一将通过)之后,01将出现在最高位,而较低的位将再次用零填充。


要以相反的顺序传递LUT,足以反转相位累加器的最低有效位(对于每个周期,它将继续增加,而其反转值将减小)。


为了形成负半波,我们写0。在输出的高位,我们现在需要将正弦表中的值本身反转。 这里的重点是您需要获得正弦四分之一的镜像副本,如果不这样做,您将获得与第一季度相同的图像,但是降低了127。 您可以通过删除代码中的反函数来验证这一点。


我们改变频率和初始相位


如上所述,为了改变频率,必须改变相位增量的值。 新输入将出现:


 input [5:0] freq_res; input [7:0] phase; 

要更改相位增量的值,我们只需在每个周期将其捕捉:


 always @(posedge clk) begin if (rst) begin //... end else begin //... phase_inc <= freq_res; end end 

在初始阶段,一切都不是那么简单。 您必须首先将其写入中间寄存器,并且仅当输入端的初始相位值与先前存储的相位值不一致时,才用该值填充相位累加器。 这就提出了另一个与比赛状态有关的重要观点。 我们已经有一个地方phase_acc在寄存器中写入phase_acc 。 您无法在多个位置同时记录,因为将记录第一个出现的数据。 因此,设计将如下所示:


 reg change_phase = 0; //     //     (  ) //     : prev_phase <= phase; if (phase != prev_phase) begin //       change_phase <= 1'b1; end if (change_phase) begin //        phase_acc <= prev_phase; change_phase <= 1'b0; end else begin //           phase_acc <= phase_acc + {2'b0,phase_inc}; end 

测试台


Iverilog和GTKWave的测试平台代码具有一些设计(带有美元符号),而这些设计在熟悉的ISE设计套件或Quartus中没有使用。 它们的含义归结为选择受监视的信号并将其加载到文件中,以便随后将其传输到模拟器。 测试台本身的工作是微不足道的-我们进行重置,设置频率/初始相位并等待一段时间。


 `include "nco.v" `timescale 1ns / 1ps module nco_tb; reg clk = 0, rst = 0; reg [7:0] phase = 0; reg [5:0] freq_res; wire [7:0] out; nco nco_inst ( .clk(clk), .rst(rst), .phase(phase), .freq_res(freq_res), .out(out) ); always #2 clk <= ~clk; initial begin $dumpfile("out.vcd"); $dumpvars(0, nco_tb); //$monitor("time =%4d out=%h",$time,out); rst = 1'b1; freq_res = 1; #8 rst = 1'b0; #300 phase = 8'b00100011; #300 phase = 8'b00001111; #1200 freq_res = 6'b111101; #1200 freq_res = 6'b001111; #1200 freq_res = 6'b011111; #400 phase = 8'b00010011; #1200 $finish; end endmodule 

时序图


在输出中,我们得到了类似于正弦的东西,其频率和初始相位在测试台中设置的时间点发生了变化。 值得注意的是,随着频率的增加,沿其的分辨率(每个周期的采样数)分别降低,合成器的时钟频率及其LUT的大小在再现纯正弦时起着决定性的作用(其形状越接近理想值,得到的频谱中的副分量就越少)。信号和峰值将已经在生成的频率上)。



可以看出,具有第二频率的信号已经没有其他信号平滑。 让我们仔细看看。



可以看出,这仍然与正弦有点类似,在这样的信号通过抗混叠滤波器(低通滤波器)之后,结果将变得更好。


项目资源可在此处找到


资料来源


Source: https://habr.com/ru/post/zh-CN457634/


All Articles