DDS Synthesizer on Verilog


In this post I will share how I understood writing a DDS synthesizer on Verilog. It will be used to generate a sinusoidal oscillation, the frequency and initial phase of which can be adjusted and calculated for use with an 8-bit unipolar DAC. How the synthesizer works is well written in an article in the journal Components and Technologies . To reduce the amount of used memory of the sine table, symmetry is used.


For compilation under Linux I used Iverilog, and for display GTKWave. For convenience, a simple Makefile was written, maybe it will come in handy for someone. Initially, using the iverilog compiler, we get the tb.out file, and then send it to the vvp simulator, which is installed with iverilog. As a result, vvp will generate out.vcd, which contains all the variables (signals) used in the project. The display target, in addition to the above, will launch GTKWave with the variable file and you can see the waveforms of the signals.


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

First of all, you need to place a table of the future sine in memory, for I wrote a simple Python script that breaks a quarter of the sine period into 64 points and generates it in a format that can then be copied to the source code. Since I conceived the implementation of DDS for an external unipolar DAC with a bit capacity of no more than 8 bits, the sine amplitude should be in the range from 0 to 256, where the negative half-period lies in the range 0 ... 127, and the positive half in 128 ... 255 . In this regard, the obtained sine values ​​(from 0 to pi / 4) are multiplied by 127, and then 127 is added to them. As a result, the values ​​of the first quarter of the period are obtained, the amplitude of which is 128 ... 256.


I draw attention to the fact that with this formation, the sine at the output of the DAC will have a constant component. In order to remove it, it is necessary to pass it through a capacitor.


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

Since the sine function is symmetric (odd), you can find the first symmetry sin (x) = - sin (pi + x). The second symmetry is characterized by the fact that having a table for a quarter of the period, the second quarter can be obtained by going through the table in the reverse order (since the sine on the half-period first increases, then decreases).


We form a sine


The bulk of the DDS synthesizer is a phase battery. In essence, it is the index of an element from the Look Up Table (LUT). For each period of the clock signal, the value in it increases by a certain value, as a result, a sine is obtained at the output. The frequency of the signal at the output will depend on the value of the increment of the phase accumulator - the larger it is, the higher the frequency. However, according to the Kotelnikov criterion, the sampling frequency should be at least 2 times the signal frequency (to avoid the effect of superimposing the spectrum), hence the limitation on the maximum increment is half the phase accumulator. In general, the engineering criterion is the sampling frequency = 2.2 of the signal frequency, therefore, having decided not to take it to the extreme, I removed one more bit, leaving 6 bits incremented with a 8-bit phase battery (even though the sine jackalite already).


Due to the symmetry used, only the lower 6 bits of 2 ^ 6 = 64 will be used directly for index sampling. The high 2 bits are used to identify a quarter period of sine generation and, accordingly, change the direction of the table traversal. You should get something similar to:


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

When resetting, we initialize everything with zeros, except for the phase increment value, we set it to one. To maintain the synthesizability of the code, the table will also be filled with values ​​during the reset. In a real project, it is advisable to use the block memory built into the FPGA for such purposes and create a separate configuration file for it, and use the IP core in the project itself.


A little explanation on how symmetry works. At each cycle, it is checked (on the 2 most significant bits) in which quarter the phase accumulator is currently located. If the highest = 00, then the output in the highest digit is 1 (responsible for the positive half-wave), in the lower ones - the value from the LUT in accordance with the index. After the value of the phase accumulator exceeds 63 (the first quarter will pass), 01 will appear in the high bits, and the lower ones will be filled with zeros again.


To pass the LUT in the reverse order, it is enough to invert the least significant bits of the phase accumulator (it will continue to increase for each clock cycle, and its inverted value will decrease).


To form a negative half-wave, we write 0. In the upper bit of the output, we now need to invert the value itself from the sine table. The point here is that you need to get a mirror copy of the quarter of the sine, and if this is not done, you get the same picture as in the first quarter, but lowered by 127 down. You can verify this by removing the inverse in the code.


We change the frequency and the initial phase


As already described above, to change the frequency, it is necessary to change the value of the phase increment. New inputs will appear:


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

To change the value of the phase increment, we just snap it on each cycle:


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

With the initial phase, everything is not so simple. You must first write it to the intermediate register, and fill the phase accumulator with this value only if the value of the initial phase at the input does not coincide with the one previously stored. This raises another important point related to the state of the races. We already have a place where we write to the register phase_acc. You can’t record at the same time in several places, since the data that came first will be recorded. Therefore, the design will look like this:


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

Testbench


The testbench code for Iverilog and GTKWave has some designs (with a dollar sign) that are not used in the usual ISE Design Suite or Quartus. Their meaning boils down to selecting monitored signals and loading them into a file, so that they can then be transferred to the simulator. The work of the test bench itself is trivial - we do a reset, set the frequency / initial phase and wait a while.


`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

Timing charts


At the output, we get something similar to a sine with a changing frequency and initial phase at the time points set in the testbench. It is worth noting that with increasing frequency, the resolution along it (the number of samples per period) decreases, respectively, the synthesizer clock frequency and its LUT size play a decisive role in reproducing the pure sine (the more its shape approaches the ideal, the fewer side components in the spectrum of the resulting signal and the peak will already be at the generated frequency).



It can be seen that a signal with a second frequency has already not as smooth a sine as the others. Let's take a closer look.



It can be seen that this is still a bit similar to the sine, the result will become even better after such a signal is passed through an anti-aliasing filter (Low-Pass Filter).


Project sources are available here .


Sources



Also popular now: