FPGA-based DDS Signal Generator

FPGA-based DDS Signal Generator

    It was like writing this document two weeks ago, but it has been pigeonholed until now, mainly because of people. Another technical reason is that I want to use the serial port screen to display the waveform. The data returned by the serial port debugging assistant is correct, but there is no response when sending it to the serial port screen. The engineering and human-computer interaction interface of the previous HMI serial screen were done in vain. Back to the topic, let's talk about the theory and code implementation of the DDS signal generator.

1. Theoretical part

    The theoretical part is mainly learned from the design and verification course of Wildfire's simple DDS signal generator , adding a lot of my understanding. The code part has made a lot of extensions to the Wildfire code to make it more perfect.

1. What is DDS?

    Randomly picked a bit from somewhere:

DDS is the English abbreviation of Direct Digital Synthesizer (Direct Digital Synthesizer), which is a key digital technology. Compared with traditional frequency synthesizers, DDS has the advantages of low cost, low power consumption, high resolution and fast conversion time. It is widely used in the field of telecommunications and electronic instruments, and is a key technology to realize the full digitalization of equipment. As designers, we are used to calling it a signal generator, which is generally used to generate sine waves, sawtooth waves, square waves and other waveforms or signal waveforms of different frequencies, which are widely used in electronic design and testing.

In short, DDS is a signal generator that can generate waveforms of different types, frequencies and initial phases.

2. Overall block diagram

alt

    The above figure shows the basic structure of DDS , which is mainly composed of four major structures: phase accumulator, phase modulator, waveform data table ROM , and D/A converter. CLK is the system working clock, the frequency is f CLK f_{CLK}fCLK;Frequency word input F_WORD , which is an integer, controls the frequency of the output signal, which can be understood as a step value (detailed later); phase word input P_WORD , which is an integer, controls the phase offset of the output signal; since I do not have D /A converter, so the 8-bit digital signal is directly output, and its frequency is f OUT f_{OUT}fOUT.
    In addition, I wanted to use the serial port screen as an oscilloscope to display waveforms before, but the serial port screen itself is still a digital device, not an analog device. It also uses the quantization level value of 0~255 as the voltage value, showing different heights. The pixel points form a curve, which is different from the analog voltage value (like 0.2 V , 1.4 V , etc.) converted by the D/A converter received by the oscilloscope.

3. Module introduction

  1. Input buffer : After the frequency word and phase word are input, there is an accumulation register, which is used for data storage under the synchronization of the system clock, so that the data change will not interfere with the normal operation of the subsequent phase accumulator and phase modulator.
  2. Phase accumulator :
    • This part is the core part of DDS , where the phase accumulation is completed and the phase code is generated. Why is it called a phase accumulator? My idea is that the essence of DDS signal generation is to read the value of each signal point in a cycle from the ROM for cyclic output. In this cycle, from reading the value of a point to Reading the value of the next point is the phase offset. The total phase code corresponds to all the data of one cycle in the ROM address (in fact, only the high-order part of the phase code is taken).
    • The input of the phase accumulator is the frequency word input F_WORD , which indicates the increment of the phase accumulation in each clock cycle, and can also be understood as a step value, which I fre_stepuse . When the phase code (I use fre_addit ) accumulates and overflows, it means that the signal output of one cycle is completed.
    • Working clock signal frequency f CLK f_{CLK}fCLK, the relationship between the output signal frequency and the frequency word input F_WORD is f OUT = FWORD ∗ f CLK / 2 N f_{OUT} = F_{WORD} * f_{CLK} / 2^NfOUT=FWORDfCLK/2N. _ Among them,Nis the bit width of frequency word inputF_WORDand phase code, which I set to 32 in the code, and can be changed parameterthroughThe above formula can be understood in this way. From the perspective of hardware, when the frequency word inputF_WORDis 1, there is a relationshipf OUT = f CLK / 2 N f_{OUT} = f_{CLK} / 2^NfOUT=fCLK/2N , in2 N 2^N2Within the N counting capacity, each clock cycle increases by 1, and it is full of2 N 2^N2After N , outputROM; it can also be understood from a mathematical point of view that the output frequency is the system clock frequency divided by2 N 2^N2N. _ f OUT at this timef_{OUT}fOUTIt is the minimum resolution of DDS , and the output signal frequency is the lowest. When the frequency word input F_WORD increases, the cumulative increment of each clock cycle is expanded by F_WORD times, so the output frequency is multiplied by this multiple on the basis of the minimum resolution.
  3. Phase modulator : The phase modulator receives the phase code output by the phase accumulator, and adds the phase offset value (phase word input) P_WORD at the same time , which is used for phase modulation of the signal. The relationship related to the phase word input P_WORD is θ = PWORD ∗ 2 π / 2 M \theta = P_{WORD} * 2\pi / 2^Mi=PWORD2 p / 2M. _ θ \thetaθ is the initial phase of the waveform,Misthe ROMaddress, and the phase word input in the code ispha_step, which means a one-step phase offset. The above formula can be understood in this way, a periodic signal corresponds to the angle2 π 2\pi2 π corresponds to4096 data inROM2 π / 2 M 2\pi / 2^M2 p / 2M represents the phase increase value corresponding to the output of each data, multiplied by the phase offset value to obtain the total offset initial phase.
  4. Waveform data table :
    • The waveform data table is a ROM IP core, which stores a complete cycle of sine wave signal. In the code, the ROM IP core depth I set is 4096, the address bit width is 12, and the data storage bit width is 8 bits. Use MATLAB to generate the .mif file required by the ROM IP core, and sample a period of sine wave signal (also square wave, sawtooth wave and triangular wave signal) 4096 times at equal intervals along the horizontal axis, and use a Byte data representation, the maximum value is 255, and the minimum value is 0. The 4096 sampling results are written into the 4096 storage units of the ROM in sequence, and the digital amplitude signal of a complete cycle of the sine wave is written into the waveform data table ROM . The waveform data table ROM uses the phase code input by the phase modulator as the ROM read address, and outputs the digital value of the voltage amplitude in the storage unit corresponding to the address.
    • About addressing the ROM with the phase code obtained from the phase accumulator . According to the above, N is the bit width of the phase accumulator, M is the bit width of the ROM address, and M is determined by the number of sampling points in one signal cycle. I don't know how to determine the size of N. For an N -bit phase accumulator, the maximum value of the phase code is 2 N 2^N2N , ifthe number of storage units inROM2 N 2^N2If N is used, this problem can be easily solved, but the requirement forROMstorage capacity is relatively high. In practice, the high bits of the phase code can be used toaddressthe ROMthe ROM, but read once in multiple clocks.

4. Give a chestnut to the above theory

    Suppose: the depth of the ROM storage unit is 4096, the bit width of the ROM address is 12 bits, the bit width of each data storage unit is 8 bits, and the bit width of the phase accumulator is 32 bits.
    From the above conditions, according to the DDS principle, the 32 bits of the phase accumulator and the frequency control word are continuously accumulated; while in the phase modulator and the phase control word are accumulated, the upper 12 bits of the phase accumulator are used. Since the upper 12 bits of the phase accumulator are used as ROM addressing, when the lower 20 bits overflow and add one to the upper 12 bits, the data in a data table is output once to the ROM addressing.
    Take the frequency control word F_WORD = 1 as an example, the lower 20 bits of the phase accumulator will continuously add one in each clock cycle, until the lower 20 bits overflow to the upper 12 bits, and before the overflow, the address of the read ROM is always 0 , that is to say, the data in address 0 of ROM is read 2 20 2^{20}220 times. Continue to add one to the address after overflow, read toROMaddress 1, this data is read again2 20 2^{20}220 times. This is true for all subsequent points. The final output waveform frequency should be 1/2 of the working clock frequency20 1/2^{20}1/220 , the period is expanded by2 20 2^{20}220 times.
    Similarly, when the frequency control wordF_WORD= 100, the lower 20 bits of the phase accumulator will always add 100, then the overflow time of the lower 20 bits of the phase accumulator will be 100 times faster than the above, and each point in the ROM will becomparedtoThe above will read 100 times less, so the final output frequency is 100 times the above.

Two, the code part

1. Waveform control part code

module wave_ctrl #(
    parameter DATA_WIDTH_ROM = 8,  //输出数据位宽
    parameter N = 32,  //相位累加器位宽
    parameter M = 12,  //相位调制器位宽
    parameter ADDR = 12  //ROM数据表位宽
) (
    input wire clk,
    input wire rstn,

    input wire [3:0] wave_sel,  //波形选择

    input wire [N-1:0] fre_step,  //频率字输入,相当于一个步进值,每个时钟周期增加的值
    input wire [M-1:0] pha_step,  //相位字输入,相当于一个步进值,每个时钟周期增加的值

    output wire [2*DATA_WIDTH_ROM-1:0] data_out
);

  //四种波形信号选择参数定义
  parameter sin_wave = 4'b0001;  //正弦波
  parameter squ_wave = 4'b0010;  //方波
  parameter tri_wave = 4'b0100;  //三角波
  parameter saw_wave = 4'b1000;  //锯齿波

  //频率字和相位字输入缓存
  reg [N-1:0] fre_step_reg;
  reg [M-1:0] pha_step_reg;

  //相位累加信号和相位调制后信号
  reg [N-1:0] fre_add;  //相位码
  reg [M-1:0] pha_add;

  //四种波形的ROM读使能信号
  reg [0:0] sin_wave_en;
  reg [0:0] squ_wave_en;
  reg [0:0] tri_wave_en;
  reg [0:0] saw_wave_en;

  //ROM读地址
  reg [ADDR-1:0] rom_addr;

  reg [ADDR-1:0] sin_wave_rom_addr;
  reg [ADDR-1:0] squ_wave_rom_addr;
  reg [ADDR-1:0] tri_wave_rom_addr;
  reg [ADDR-1:0] saw_wave_rom_addr;


  //四种波形的ROM输出信号
  wire [DATA_WIDTH_ROM-1:0] sin_wave_data_out;
  wire [DATA_WIDTH_ROM-1:0] squ_wave_data_out;
  wire [DATA_WIDTH_ROM-1:0] tri_wave_data_out;
  wire [DATA_WIDTH_ROM-1:0] saw_wave_data_out;

  //波形选择
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      sin_wave_en <= 1'b0;
      squ_wave_en <= 1'b0;
      tri_wave_en <= 1'b0;
      saw_wave_en <= 1'b0;
      sin_wave_rom_addr <= 0;
      squ_wave_rom_addr <= 0;
      tri_wave_rom_addr <= 0;
      saw_wave_rom_addr <= 0;
    end
    case (wave_sel)
      sin_wave: begin
        sin_wave_en <= 1'b1;
        squ_wave_en <= 1'b0;
        tri_wave_en <= 1'b0;
        saw_wave_en <= 1'b0;
        sin_wave_rom_addr <= rom_addr;
      end
      squ_wave: begin
        sin_wave_en <= 1'b0;
        squ_wave_en <= 1'b1;
        tri_wave_en <= 1'b0;
        saw_wave_en <= 1'b0;
        squ_wave_rom_addr <= rom_addr;
      end
      tri_wave: begin
        sin_wave_en <= 1'b0;
        squ_wave_en <= 1'b0;
        tri_wave_en <= 1'b1;
        saw_wave_en <= 1'b0;
        tri_wave_rom_addr <= rom_addr;
      end
      saw_wave: begin
        sin_wave_en <= 1'b0;
        squ_wave_en <= 1'b0;
        tri_wave_en <= 1'b0;
        saw_wave_en <= 1'b1;
        saw_wave_rom_addr <= rom_addr;
      end
    endcase
  end

  //频率字输入缓存器
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      fre_step_reg <= 0;
    end else begin
      fre_step_reg <= fre_step;
    end
  end

  //相位字输入缓存器
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      pha_step_reg <= 0;
    end else begin
      pha_step_reg <= pha_step;
    end
  end

  //相位累加器
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      fre_add <= 0;
    end else begin
      fre_add <= fre_add + fre_step_reg;
    end
  end

  //相位调制器
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      pha_add <= 0;
    end else begin
      pha_add <= fre_add[N-1:N-M] + pha_step_reg;
    end
  end

  //将相位调制后信号作为ROM读地址输入
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      rom_addr <= 0;
    end else begin
      rom_addr <= pha_add;
    end
  end

  sin_wave_rom_8x4096 sin_wave_rom_8x4096_inst (
      .address(sin_wave_rom_addr),
      .clock  (clk),
      .rden   (sin_wave_en),
      .q      (sin_wave_data_out)
  );

  squ_wave_rom_8x4096 squ_wave_rom_8x4096_inst (
      .address(squ_wave_rom_addr),
      .clock  (clk),
      .rden   (squ_wave_en),
      .q      (squ_wave_data_out)
  );

  tri_wave_rom_8x4096 tri_wave_rom_8x4096_inst (
      .address(tri_wave_rom_addr),
      .clock  (clk),
      .rden   (tri_wave_en),
      .q      (tri_wave_data_out)
  );

  saw_wave_rom_8x4096 saw_wave_rom_8x4096_inst (
      .address(saw_wave_rom_addr),
      .clock  (clk),
      .rden   (saw_wave_en),
      .q      (saw_wave_data_out)
  );

  assign data_out = (sin_wave_en ? sin_wave_data_out : 0) + (squ_wave_en ? squ_wave_data_out : 0) + (tri_wave_en ? tri_wave_data_out : 0) + (saw_wave_en ? saw_wave_data_out : 0);

endmodule  //dds_ctrl

2. Frequency and phase word control code:

    The function of the following code is to input the frequency of fre_x MHz , fre_y kHz and fre_z Hz and pha_x π \pi respectivelyπ( 1 / pha _ y ) ∗ π (1/pha\_y)*\pi( 1/ p has _ y )The initial phase of π , and the above input data are converted into codes of frequency and phase words through the formula.

module fre_pha_data_ctrl #(
    parameter N = 32,  //相位累加器位宽
    parameter M = 12,  //相位调制器位宽
    parameter FRE_WIDTH = 10,  //三路频率输入的位宽,三路频率的单位分别为MHz,kHz,Hz
    parameter PHA_WIDTH = 8,  //两路相位输入的位宽,(x+1/y)pi
    parameter DATA_WIDTH = 64
) (
    input wire clk,
    input wire rstn,

    input wire [FRE_WIDTH-1:0] fre_x,  //MHz
    input wire [FRE_WIDTH-1:0] fre_y,  //kHz
    input wire [FRE_WIDTH-1:0] fre_z,  //Hz

    input wire [PHA_WIDTH-1:0] pha_x,  //x*pi
    input wire [PHA_WIDTH-1:0] pha_y,  //(1/y)*pi

    output wire [N-1:0] fre_step,  //频率字输入,相当于一个步进值,每个时钟周期增加的值
    output wire [M-1:0] pha_step   //相位字输入,相当于一个步进值,每个时钟周期增加的值
);

  /* ----------频率数据处理 fre_step---------- */
  parameter _1MHZ = 1_000_000;
  parameter _1KHZ = 1_000;
  parameter CLK_IN = 64'd50 * _1MHZ;

  wire [DATA_WIDTH-1:0] fre_out;  //实际输出信号频率
  wire [DATA_WIDTH-1:0] temp;  //中间值,fre_out*(2^N)的值

  reg  [DATA_WIDTH-1:0] fre_reg_x;  //频率输入x缓存信号
  reg  [DATA_WIDTH-1:0] fre_reg_y;  //频率输入y缓存信号
  reg  [DATA_WIDTH-1:0] fre_reg_z;  //频率输入z缓存信号

  wire [DATA_WIDTH-1:0] fre_step_temp;

  //单位MHz,化为Hz
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      fre_reg_x <= 64'd0;
    end else begin
      fre_reg_x <= fre_x * _1MHZ;
    end
  end

  //单位kHz,化为Hz
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      fre_reg_y <= 64'd0;
    end else begin
      fre_reg_y <= fre_y * _1KHZ;
    end
  end

  //单位Hz,寄存器缓存
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      fre_reg_z <= 64'd0;
    end else begin
      fre_reg_z <= fre_z;
    end
  end

  //将三者相加得到实际输出频率
  assign fre_out = fre_reg_x + fre_reg_y + fre_reg_z;

  //将实际输出频率乘以2^N次方,即左移N位
  assign temp = fre_out << N;

  //将temp除以时钟频率CLK_IN
  div_64_64_inst #(
      .DATA_WIDTH(DATA_WIDTH)
  ) u_div_64_64_inst1 (
      .numer_sig   (temp),
      .denom_sig   (CLK_IN),
      .quotient_sig(fre_step_temp),
      .remain_sig  ()
  );

  assign fre_step = fre_step_temp[N-1:0];

  /* ----------相位数据处理 pha_step---------- */
  reg  [DATA_WIDTH-1:0] pha_reg_x;  //相位输入x缓存信号
  reg  [DATA_WIDTH-1:0] pha_reg_y;  //相位输入y缓存信号

  wire [DATA_WIDTH-1:0] temp_x;
  wire [DATA_WIDTH-1:0] temp_y;

  wire [DATA_WIDTH-1:0] pha_step_temp;

  //相位输入x缓存器
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      pha_reg_x <= 1'd1;
    end else begin
      pha_reg_x <= pha_x;
    end
  end

  //相位输入y缓存器
  always @(posedge clk or negedge rstn) begin
    if (rstn == 1'b0) begin
      pha_reg_y <= 64'd1;
    end else begin
      pha_reg_y <= pha_y;
    end
  end

  //计算X*2^(M-1)
  assign temp_x = pha_reg_x << (M - 1);

  //计算(1/Y)*2^(M-1)
  div_64_64_inst #(
      .DATA_WIDTH(DATA_WIDTH)
  ) u_div_64_64_inst (
      .numer_sig   (64'd2048),
      .denom_sig   (pha_reg_y),
      .quotient_sig(temp_y),
      .remain_sig  ()
  );

  //计算pha_step,总表达式为(X+1/Y)*2^(M-1)
  assign pha_step_temp = temp_x + temp_y;

  assign pha_step = pha_step_temp[M-1:0];

endmodule  //fre_pha_data_ctrl

3. Merge the top-level files of the above two modules:

module wave_ctrl_fre_pha_data_ctrl #(
    parameter N = 32,  //相位累加器位宽
    parameter M = 12,  //相位调制器位宽
    parameter FRE_WIDTH = 10,  //三路频率输入的位宽,三路频率的单位分别为MHz,kHz,Hz
    parameter PHA_WIDTH = 8,  //两路相位输入的位宽,(x+1/y)pi
    parameter DATA_WIDTH = 64,
    parameter DATA_WIDTH_ROM = 8,  //输出数据位宽
    parameter ADDR = 12  //ROM数据表位宽
) (
    input wire [0:0] clk,
    input wire [0:0] rstn,

    input wire [FRE_WIDTH-1:0] fre_x,  //MHz
    input wire [FRE_WIDTH-1:0] fre_y,  //kHz
    input wire [FRE_WIDTH-1:0] fre_z,  //Hz

    input wire [PHA_WIDTH-1:0] pha_x,  //x*pi
    input wire [PHA_WIDTH-1:0] pha_y,  //(1/y)*pi

    input wire [3:0] wave_sel,  //波形选择

    output wire [2*DATA_WIDTH_ROM-1:0] data_out
);

wire [N-1:0] fre_step;
wire [M-1:0] pha_step;

  fre_pha_data_ctrl #(
      .N         (N),
      .M         (M),
      .FRE_WIDTH (FRE_WIDTH),
      .PHA_WIDTH (PHA_WIDTH),
      .DATA_WIDTH(DATA_WIDTH)
  ) u_fre_pha_data_ctrl (
      .clk     (clk),
      .rstn    (rstn),
      .fre_x   (fre_x),
      .fre_y   (fre_y),
      .fre_z   (fre_z),
      .pha_x   (pha_x),
      .pha_y   (pha_y),
      .fre_step(fre_step),
      .pha_step(pha_step)
  );

  wave_ctrl #(
      .DATA_WIDTH_ROM(DATA_WIDTH_ROM),
      .N         (N),
      .M         (M),
      .ADDR      (ADDR)
  ) u_wave_ctrl (
      .clk     (clk),
      .rstn    (rstn),
      .wave_sel(wave_sel),
      .fre_step(fre_step),
      .pha_step(pha_step),
      .data_out(data_out)
  );

endmodule  //tb_wave_ctrl_fre_pha_data_ctrl

4. About the IP core:

The four single-port 8x4096 ROM IP cores     instantiated in the first code , the process of configuring, calling and instantiating the IP core will not be described here, and the MATLAB code for generating the .mif configuration data file will be given in other articles , will be placed in the MATLAB Digital Signal Processing column.     In the second code, the divider is instantiated for data operation.

5. The testbench code of the top-level code:

`timescale 1ns / 1ns

module tb_wave_ctrl_fre_pha_data_ctrl;

  // wave_ctrl_fre_pha_data_ctrl Parameters
  parameter PERIOD = 10;
  parameter N = 32;
  parameter M = 12;
  parameter FRE_WIDTH = 10;
  parameter PHA_WIDTH = 8;
  parameter DATA_WIDTH = 64;
  parameter DATA_WIDTH_ROM = 8;

  // wave_ctrl_fre_pha_data_ctrl Inputs
  reg  [             0:0] clk = 0;
  reg  [             0:0] rstn = 0;
  reg  [   FRE_WIDTH-1:0] fre_x = 0;
  reg  [   FRE_WIDTH-1:0] fre_y = 0;
  reg  [   FRE_WIDTH-1:0] fre_z = 0;
  reg  [   PHA_WIDTH-1:0] pha_x = 0;
  reg  [   PHA_WIDTH-1:0] pha_y = 0;
  reg  [             3:0] wave_sel = 4'b001;

  // wave_ctrl_fre_pha_data_ctrl Outputs
  wire [2*DATA_WIDTH-1:0] data_out;


  always #PERIOD clk = ~clk;

  initial begin
    #(PERIOD * 2 + PERIOD / 2) rstn = 1;

    #(PERIOD) wave_sel = 4'b0001;
    fre_x = 10;
    pha_x = 0;
    pha_y = 2;

    #(PERIOD * 1000) wave_sel = 4'b0001;
    fre_x = 2;
    pha_x = 0;
    pha_y = 2;

    #(PERIOD * 1000) wave_sel = 4'b0010;
    fre_x = 1;
    pha_x = 1;
    pha_y = 1;

    #(PERIOD * 1000) wave_sel = 4'b0100;
    fre_x = 1;
    pha_x = 1;
    pha_y = 1;

    #(PERIOD * 1000) wave_sel = 4'b1000;
    fre_x = 1;
    pha_x = 1;
    pha_y = 1;
  end

  wave_ctrl_fre_pha_data_ctrl #(
      .N             (N),
      .M             (M),
      .FRE_WIDTH     (FRE_WIDTH),
      .PHA_WIDTH     (PHA_WIDTH),
      .DATA_WIDTH    (DATA_WIDTH),
      .DATA_WIDTH_ROM(DATA_WIDTH_ROM)
  ) u_wave_ctrl_fre_pha_data_ctrl (
      .clk     (clk[0:0]),
      .rstn    (rstn[0:0]),
      .fre_x   (fre_x[FRE_WIDTH-1:0]),
      .fre_y   (fre_y[FRE_WIDTH-1:0]),
      .fre_z   (fre_z[FRE_WIDTH-1:0]),
      .pha_x   (pha_x[PHA_WIDTH-1:0]),
      .pha_y   (pha_y[PHA_WIDTH-1:0]),
      .wave_sel(wave_sel[3:0]),

      .data_out(data_out[2*DATA_WIDTH-1:0])
  );

  initial begin

  end

endmodule

Guess you like

Origin blog.csdn.net/SnowyForest___/article/details/128340855