DDS详解——以安路PH1A90实现为例

正弦波有哪些要素?

简单地理解,一个正弦波的表达式如下:

y = A ( t ) ∗ cos ⁡ ( θ ) , θ = f ( t ) (式 1 ) y=A (t)*\cos(\theta),\theta=f(t)(式1) y=A(t)cos(θ),θ=f(t)(式1

令相位θ为对时间的某个函数上的积分,即:

θ = ∫ t 1 t 2 f ( t ) d t (式 2 ) \theta = \int_{t_{1}}^{t_{2}} f(t) dt(式2) θ=t1t2f(t)dt(式2

将式2代入到式1,则有:

y = A ( t ) ∗ cos ⁡ ( ∫ t 1 t 2 f ( t ) d t ) (式 3 ) y=A (t)*\cos( \int_{t_{1}}^{t_{2}} f(t) dt)(式3) y=A(t)cos(t1t2f(t)dt)(式3

将这个式子离散化。也就是转换成能够编程的形式,则可以得到:

y = A ( t ) ∗ cos ⁡ ( ∑ 0 N x ( n ) ) (式 4 ) y=A (t)*\cos( \sum_{0}^{N} x(n))(式4) y=A(t)cos(0Nx(n))(式4

现在来简单解释一下这个式子。首先,A(t)是一个时间的函数,指示当前信号的幅度;f(t)也是一个时间的函数,指示正弦波序列的下一个元素应该相对于上一个元素增加多少相位,简单来说就是指示了相位的增量。相位如果增加了2PI,就是过了一个周期,而增量的大小,就对应了周期的大小,也就对应了频率的大小。也就是说,f(t)实际上指示的是正弦波的频率。

通常,在实际工程中,在一些情况下,几个同时钟源生成的正弦波之间会有可以调节的相位差。那么,就可以给式4增加一个常量:

y = A ( n ) ∗ cos ⁡ ( ∑ 0 N x ( n ) + C ( n ) ) (式 5 ) y=A (n)*\cos( \sum_{0}^{N} x(n)+C(n))(式5) y=A(n)cos(0Nx(n)+C(n))(式5

其中,C(n)为初相位,也称相位偏移序列,可以是时间的函数。x(n)是相位增量序列,A(n)是幅度序列。

DDS是怎么工作的?

调制对象

从对于正弦波的基本概念讨论可以得知,我们要控制的主要有三个量:幅度、相位增量(频率)和相位偏移。

调制方法

从对于正弦波的基本概念讨论可以得知,我们要控制的三个量都是时间的函数。而“时间”在数字电路中的表述就是一个序列的索引值。数字逻辑电路的基本特性决定了它本身并不知道“时间”的概念,只会由每个时钟沿的状态决定下一个状态。但是一个数字逻辑系统的运行频率是可以被人为定义的,也就是下一时钟沿对于本时钟沿的时刻是可以确定的。

也就是说,可以确定唯一的A(n)x(n)C(n),同时引入一些其他自变量,用于确定某一个时刻的正弦波值。这些“其他自变量”可以是一个RAM储存的数值序列,也可以是ADC实时转换进来的数据。

但是,因为安路的DDS IP并不支持幅度调节,并未实现幅线性度调制,只是实现了一个非线性调幅器用作演示,并预留了相关接口。

如何用FPGA实现一个DDS?

硬件平台

FPGA:米联客的安路PH1A90SBG484-2一体化板子,带有一个残血FMC-HPC,当作FMC-LPC用。

也不知道多引出来那几个引脚有个啥用,用来碰运气看看哪个HPC子卡能上吗???要么就满血LPC的同时增加几个排针,要么就砍别的接口凑一个满血HPC得了。这比上不足比下有余的,也不知道我这钱是白花了还是花值了。

ADC:黑金AD9238模块,65MSPS,跑满(实测最高70MSPS时精度还是很高)

DAC:黑金AD9767模块,125MSPS,跑满(实测可以跑140MSPS以上)

ADC和DAC都是黑金经典40pin排针,用一个FMC-LPC转接板转接到安路开发板的FMC上。

不得不说,黑金的东西还是很好的,输入输出直接做了50Ω阻抗匹配了,AD模块采样电压在±3V以内都没问题,DA模块输出可以达到峰峰值8V不失真。最重要的是,板上有单电源转双电源的电路,FPGA只需要给子板5V供电即可,十分人性化,极大满足了当代大学生的电赛需求。

顶层设计与通信

请添加图片描述

看图说话。首先是SPI主从通道结构。这个结构是FPGA与MCU通信的主要渠道,双方均通过主机通道发起主动数据传输,均通过从通道接收数据并且返回应答。SPI通道模式可配置,但是双方模式都要一致。

MCU在发起一次通信时,先传给FPGA一个0x5A的开始字节,然后是要写入的寄存器的一个8位地址,然后是要写入的4字节数据,低位字节先传输。这样就完成了一次寄存器写入的操作。而在FPGA内部,会用一个FSM实现SPI的读取,改变相应的RAM内容,进行寄存器的相应配置。

大多数现代计算机系统都是小端序储存数据,也就是一个32位数据的高位字节储存在高位空间,低位字节储存在低位空间;而发送缓冲区一般而言是低地址先发送的。如果先传输低位字节,则可以直接使用内存复制(memcpy)函数将32位数据复制到SPI的发送缓冲区中,方便了MCU端的发送操作。

相应的代码如下:

// src/spi/spi.v
module spi #(
    parameter SYSCLK_FREQ = 50000000,  // System clock frequency, default 50MHz
    parameter SPICLK_FREQ = 1000000,   // SPI clock frequency, default 1MHz

    parameter CPOL  = 0,  // Clock Polarity, 0 and 1 allowed
    parameter CPHA  = 0,  // Clock Phase, 0 and 1 allowed
    parameter FSB   = 1,  // First bit, 0-LSB, 1-MSB
    parameter WIDTH = 8   // Data bit width
) (
    input clk,
    input rstn,

    input  spi_s_sclk,
    input  spi_s_ss_n,
    input  spi_s_mosi,
    output spi_s_miso,

    output spi_m_sclk,
    output spi_m_ss_n,
    output spi_m_mosi,
    input  spi_m_miso,

    output reg          param_wen,
    output     [31 : 0] dds_fword_source,
    output     [31 : 0] dds_pword_source,
    output     [31 : 0] dds_amp_source,
    output     [31 : 0] direct_fword,
    output     [31 : 0] direct_pword,
    output     [31 : 0] direct_amp,
    output     [31 : 0] drg_freq_start,
    output     [31 : 0] drg_freq_end,
    output     [31 : 0] drg_freq_step,
    output     [31 : 0] drg_freq_pulse,
    output     [31 : 0] drg_phase_start,
    output     [31 : 0] drg_phase_end,
    output     [31 : 0] drg_phase_step,
    output     [31 : 0] drg_phase_pulse,
    output     [31 : 0] drg_amp_start,
    output     [31 : 0] drg_amp_end,
    output     [31 : 0] drg_amp_step,
    output     [31 : 0] drg_amp_pulse,
    output     [31 : 0] adc_freq_center,
    output     [31 : 0] adc_freq_kf,
    output     [31 : 0] adc_freq_ch_sel,
    output     [31 : 0] adc_freq_zero_cal,
    output     [31 : 0] adc_phase_center,
    output     [31 : 0] adc_phase_kf,
    output     [31 : 0] adc_phase_ch_sel,
    output     [31 : 0] adc_phase_zero_cal,
    output     [31 : 0] adc_amp_center,
    output     [31 : 0] adc_amp_kf,
    output     [31 : 0] adc_amp_ch_sel,
    output     [31 : 0] adc_amp_zero_cal
);

  wire                 spi_m_valid;
  reg                  spi_m_ready;
  wire [WIDTH - 1 : 0] spi_m_rx_data;
  reg  [WIDTH - 1 : 0] spi_m_tx_data;
  spi_master #(
      .SYSCLK_FREQ(SYSCLK_FREQ),
      .SPICLK_FREQ(SPICLK_FREQ),

      .CPOL (CPOL),
      .CPHA (CPHA),
      .FSB  (FSB),
      .WIDTH(WIDTH)
  ) spi_master_inst (
      .clk (clk),
      .rstn(rstn),

      .spi_sclk(spi_m_sclk),
      .spi_ss_n(spi_m_ss_n),
      .spi_mosi(spi_m_mosi),
      .spi_miso(spi_m_miso),

      .spi_valid  (spi_m_valid),
      .spi_ready  (spi_m_ready),
      .spi_rx_data(spi_m_rx_data),
      .spi_tx_data(spi_m_tx_data)
  );

  wire                 spi_s_valid;
  reg                  spi_s_ready;
  wire [WIDTH - 1 : 0] spi_s_rx_data;
  reg  [WIDTH - 1 : 0] spi_s_tx_data;
  spi_slave #(
      .CPOL (CPOL),
      .CPHA (CPHA),
      .FSB  (FSB),
      .WIDTH(WIDTH)
  ) spi_slave_inst (
      .clk (clk),
      .rstn(rstn),

      .spi_sclk(spi_s_sclk),
      .spi_ss_n(spi_s_ss_n),
      .spi_mosi(spi_s_mosi),
      .spi_miso(spi_s_miso),

      .spi_valid  (spi_s_valid),
      .spi_ready  (spi_s_ready),
      .spi_rx_data(spi_s_rx_data),
      .spi_tx_data(spi_s_tx_data)
  );

  localparam CONFIG_STATE_RESET = 8'h00;
  localparam CONFIG_STATE_IDEL = 8'h01;
  localparam CONFIG_STATE_RECV_ADDR = 8'h02;
  localparam CONFIG_STATE_RECV_DATA_B0 = 8'h03;
  localparam CONFIG_STATE_RECV_DATA_B1 = 8'h04;
  localparam CONFIG_STATE_RECV_DATA_B2 = 8'h05;
  localparam CONFIG_STATE_RECV_DATA_B3 = 8'h06;
  localparam CONFIG_STATE_WRITE = 8'h07;
  reg [ 7 : 0] config_state;
  reg [ 7 : 0] addr_buf;
  reg [31 : 0] data_buf;

  localparam CONFIGURE_RAM_CI_MAX = 256;
  reg [31 : 0] configure_ram[0 : 255];
  reg [15 : 0] configure_ram_ci;

  always @(posedge clk) begin
    if (!rstn) begin
      configure_ram_ci <= 8'h0;

      addr_buf         <= 8'h0;
      data_buf         <= 32'h0;
      spi_s_ready      <= 1'b0;
      spi_s_tx_data    <= 8'h0;

      param_wen        <= 1'b0;

      config_state     <= CONFIG_STATE_RESET;
    end else begin
      if (configure_ram_ci < CONFIGURE_RAM_CI_MAX) begin
        configure_ram[configure_ram_ci] <= 32'h0;
        configure_ram_ci                <= configure_ram_ci + 1;

        addr_buf                        <= 8'h0;
        data_buf                        <= 32'h0;
        spi_s_ready                     <= 1'b0;
        spi_s_tx_data                   <= 8'h0;

        param_wen                       <= 1'b0;

        config_state                    <= CONFIG_STATE_RESET;
      end else begin
        configure_ram_ci <= CONFIGURE_RAM_CI_MAX;

        case (config_state)
          CONFIG_STATE_RESET: begin
            addr_buf      <= 8'h0;
            data_buf      <= 32'h0;
            spi_s_ready   <= 1'b0;
            spi_s_tx_data <= 8'h0;

            param_wen     <= 1'b0;

            config_state  <= CONFIG_STATE_IDEL;
          end

          CONFIG_STATE_IDEL: begin
            addr_buf      <= 8'h0;
            data_buf      <= 32'h0;
            spi_s_tx_data <= 8'h0;

            param_wen     <= 1'b0;

            if ((spi_s_valid && spi_s_ready) & (spi_s_rx_data == 8'h5a)) begin
              spi_s_ready  <= 1'b0;
              config_state <= CONFIG_STATE_RECV_ADDR;
            end else begin
              spi_s_ready  <= 1'b1;
              config_state <= CONFIG_STATE_IDEL;
            end
          end

          CONFIG_STATE_RECV_ADDR: begin
            data_buf      <= 32'h0;
            spi_s_tx_data <= 8'h0;

            param_wen     <= 1'b0;

            if (spi_s_valid && spi_s_ready) begin
              spi_s_ready  <= 1'b0;
              addr_buf     <= spi_s_rx_data;

              config_state <= CONFIG_STATE_RECV_DATA_B0;
            end else begin
              spi_s_ready  <= 1'b1;

              config_state <= CONFIG_STATE_RECV_ADDR;
            end
          end

          CONFIG_STATE_RECV_DATA_B0: begin
            addr_buf      <= addr_buf;
            spi_s_tx_data <= addr_buf;

            param_wen     <= 1'b0;

            if (spi_s_valid && spi_s_ready) begin
              spi_s_ready     <= 1'b0;
              data_buf[7 : 0] <= spi_s_rx_data;

              config_state    <= CONFIG_STATE_RECV_DATA_B1;
            end else begin
              spi_s_ready  <= 1'b1;

              config_state <= CONFIG_STATE_RECV_DATA_B0;
            end
          end

          CONFIG_STATE_RECV_DATA_B1: begin
            addr_buf      <= addr_buf;
            spi_s_tx_data <= 8'h0;

            param_wen     <= 1'b0;

            if (spi_s_valid && spi_s_ready) begin
              spi_s_ready      <= 1'b0;
              data_buf[15 : 8] <= spi_s_rx_data;

              config_state     <= CONFIG_STATE_RECV_DATA_B2;
            end else begin
              spi_s_ready  <= 1'b1;

              config_state <= CONFIG_STATE_RECV_DATA_B1;
            end
          end

          CONFIG_STATE_RECV_DATA_B2: begin
            addr_buf      <= addr_buf;
            spi_s_tx_data <= 8'h0;

            param_wen     <= 1'b0;

            if (spi_s_valid && spi_s_ready) begin
              spi_s_ready       <= 1'b0;
              data_buf[23 : 16] <= spi_s_rx_data;

              config_state      <= CONFIG_STATE_RECV_DATA_B3;
            end else begin
              spi_s_ready  <= 1'b1;

              config_state <= CONFIG_STATE_RECV_DATA_B2;
            end
          end

          CONFIG_STATE_RECV_DATA_B3: begin
            addr_buf      <= addr_buf;
            spi_s_tx_data <= 8'h0;

            param_wen     <= 1'b0;

            if (spi_s_valid && spi_s_ready) begin
              spi_s_ready       <= 1'b0;
              data_buf[31 : 24] <= spi_s_rx_data;

              config_state      <= CONFIG_STATE_WRITE;
            end else begin
              spi_s_ready  <= 1'b1;

              config_state <= CONFIG_STATE_RECV_DATA_B3;
            end
          end

          CONFIG_STATE_WRITE: begin
            addr_buf                <= addr_buf;
            data_buf                <= data_buf;
            spi_s_ready             <= 1'b0;
            spi_s_tx_data           <= 8'h0;

            param_wen               <= 1'b1;

            configure_ram[addr_buf] <= data_buf;

            config_state            <= CONFIG_STATE_IDEL;
          end

          default: begin
            addr_buf      <= 8'h0;
            data_buf      <= 32'h0;
            spi_s_ready   <= 1'b0;
            spi_s_tx_data <= 8'h0;

            param_wen     <= 1'b0;

            config_state  <= CONFIG_STATE_RESET;
          end
        endcase

      end
    end
  end

  assign dds_fword_source   = configure_ram[8'h01];
  assign dds_pword_source   = configure_ram[8'h02];
  assign dds_amp_source     = configure_ram[8'h03];
  assign direct_fword       = configure_ram[8'h11];
  assign direct_pword       = configure_ram[8'h12];
  assign direct_amp         = configure_ram[8'h13];
  assign drg_freq_start     = configure_ram[8'h21];
  assign drg_freq_end       = configure_ram[8'h22];
  assign drg_freq_step      = configure_ram[8'h23];
  assign drg_freq_pulse     = configure_ram[8'h24];
  assign drg_phase_start    = configure_ram[8'h25];
  assign drg_phase_end      = configure_ram[8'h26];
  assign drg_phase_step     = configure_ram[8'h27];
  assign drg_phase_pulse    = configure_ram[8'h28];
  assign drg_amp_start      = configure_ram[8'h29];
  assign drg_amp_end        = configure_ram[8'h2a];
  assign drg_amp_step       = configure_ram[8'h2b];
  assign drg_amp_pulse      = configure_ram[8'h2c];
  assign adc_freq_center    = configure_ram[8'h31];
  assign adc_freq_kf        = configure_ram[8'h32];
  assign adc_freq_ch_sel    = configure_ram[8'h33];
  assign adc_freq_zero_cal  = configure_ram[8'h34];
  assign adc_phase_center   = configure_ram[8'h35];
  assign adc_phase_kf       = configure_ram[8'h36];
  assign adc_phase_ch_sel   = configure_ram[8'h37];
  assign adc_phase_zero_cal = configure_ram[8'h38];
  assign adc_amp_center     = configure_ram[8'h39];
  assign adc_amp_kf         = configure_ram[8'h3a];
  assign adc_amp_ch_sel     = configure_ram[8'h3b];
  assign adc_amp_zero_cal   = configure_ram[8'h3c];
endmodule

继续看代码说话。我们先对前面的一大堆一看就知道是参数的输出接口和最后几行的所有assign语句进行选择性失明,在脑子里先留下主要的状态机部分。仔细观察状态机,从复位开始,先对configure_ram_ci进行清零操作。复位结束后,如果configure_ram_ci小于CONFIGURE_RAM_CI_MAX,则将对应的RAM条目进行清零,并且对configure_ram_ci进行加一操作。**这样,在复位之后,就可以保证全部RAM的数据都为零,也就是完成了RAM的全部清零。**在RAM清零完成之后,进入CONFIG_STATE_RESET状态。其实这个状态没啥用,大可以抛弃掉,因为进入这个状态之前已经对要操作的寄存器清零了很多遍了。

然后状态机直接进入CONFIG_STATE_IDEL并拉高spi_s_ready表示已经准备好数据接收,等待spi_s_valid被拉高时传递数据。如果spi_s_valid被拉高,则在spi_s_validspi_s_ready同时被拉高的一个时钟周期内,spi_s_rx_dataspi_s_tx_data有效,也就是数据可以进行发送和接收了。CONFIG_STATE_IDEL状态时,如果接收到的数据是0x5A,则进入下一个状态,如果接收到其他数据或者没有接收到任何数据,则停留在本状态。

在状态机进入CONFIG_STATE_RECV_ADDRCONFIG_STATE_RECV_B0CONFIG_STATE_RECV_B1CONFIG_STATE_RECV_B2以及CONFIG_STATE_RECV_B3时,则进行相应字节数据的接收,具体接收过程同CONFIG_STATE_IDEL状态。

再回到那一串参数和assign语句。再状态机完成一次传输之后,将会进入CONFIG_STATE_WRITE状态。这个状态中,状态机会将param_wen拉高一个时钟周期,也就是意味着进行了一次新的配置,FPGA内部的所有配置都更新一次。

控制字选择矩阵

先摆上代码:

// src/top.v

 always @(posedge dac_clk) begin
    if (!rstn) begin
      direct_en     <= 3'b0;
      drg_en        <= 3'b0;
      adc_en        <= 3'b0;

      phase_fword_i <= 32'h0;
      phase_pword_i <= 32'h0;
      amptitude     <= 32'h0;
    end
    begin
      case (dds_fword_source)
        32'h0000_0001: begin
          direct_en[0]  <= 1'b1;
          drg_en[0]     <= 1'b0;
          adc_en[0]     <= 1'b0;

          phase_fword_i <= direct_output_fword;
        end

        32'h0000_0002: begin
          direct_en[0]  <= 1'b0;
          drg_en[0]     <= 1'b1;
          adc_en[0]     <= 1'b0;

          phase_fword_i <= drg_output_fword;
        end

        32'h0000_0003: begin
          direct_en[0]  <= 1'b0;
          drg_en[0]     <= 1'b0;
          adc_en[0]     <= 1'b1;

          phase_fword_i <= adc_output_fword;
        end

        default: begin
          direct_en[0]  <= 1'b0;
          drg_en[0]     <= 1'b0;
          adc_en[0]     <= 1'b0;

          phase_fword_i <= 32'h0;
        end
      endcase

      case (dds_pword_source)
        32'h0000_0001: begin
          direct_en[1]  <= 1'b1;
          drg_en[1]     <= 1'b0;
          adc_en[1]     <= 1'b0;

          phase_pword_i <= direct_output_pword;
        end

        32'h0000_0002: begin
          direct_en[1]  <= 1'b0;
          drg_en[1]     <= 1'b1;
          adc_en[1]     <= 1'b0;

          phase_pword_i <= drg_output_pword;
        end

        32'h0000_0003: begin
          direct_en[1]  <= 1'b0;
          drg_en[1]     <= 1'b0;
          adc_en[1]     <= 1'b1;

          phase_pword_i <= adc_output_pword;
        end

        default: begin
          direct_en[1]  <= 1'b0;
          drg_en[1]     <= 1'b0;
          adc_en[1]     <= 1'b0;

          phase_pword_i <= 32'h0;
        end
      endcase

      case (dds_amp_source)
        32'h0000_0001: begin
          direct_en[2] <= 1'b1;
          drg_en[2]    <= 1'b0;
          adc_en[2]    <= 1'b0;

          amptitude    <= direct_output_amp;
        end

        32'h0000_0002: begin
          direct_en[2] <= 1'b0;
          drg_en[2]    <= 1'b1;
          adc_en[2]    <= 1'b0;

          amptitude    <= drg_output_amp;
        end

        32'h0000_0003: begin
          direct_en[2] <= 1'b0;
          drg_en[2]    <= 1'b0;
          adc_en[2]    <= 1'b1;

          amptitude    <= adc_output_amp;
        end

        default: begin
          direct_en[2] <= 1'b0;
          drg_en[2]    <= 1'b0;
          adc_en[2]    <= 1'b0;

          amptitude    <= 32'h0;
        end
      endcase
    end

其实所谓的选择矩阵就是一堆case语句。按照一般逻辑,一个数据源可以驱动多个数据受体(也就是数据的接收者,这里就是DDS IP核和输出除法器),但是一个数据受体是不能同时接收来自多个数据源的数据的。因此,这一堆条件判断就以数据受体为主体进行编写。通过判断dds_fword_sourcedds_pword_sourcedds_amp_source,使能不同的数据源的不同通道,给三个数据受体发送不同的数据。注意,每一个数据受体在这个逻辑中都只能在同一时刻接收一个数据源的数据。

核心功能

直接数据字生成器

这个模块相对简单,使用外部配置接口进行参数配置后可以直接进行输出。具体代码如下:

// src/dds/direct.v
module direct_core #(
    parameter DDS_FREQ = 32'd120000000
) (
    input clk,
    input rstn,

    input          param_wen,
    input [31 : 0] direct_word,

    input direct_en,

    input               dds_clk,
    output reg [31 : 0] direct_output
);
  reg [31 : 0] direct_word_buf;
  always @(posedge clk) begin
    if (!rstn) begin
      direct_word_buf <= 32'h0;
    end else begin
      if (param_wen) begin
        direct_word_buf <= direct_word;
      end else begin
        direct_word_buf <= direct_word_buf;
      end
    end
  end

  always @(posedge dds_clk) begin
    if (!rstn) begin
      direct_output <= 32'h0;
    end else begin
      if (direct_en) begin
        direct_output <= direct_word_buf;
      end else begin
        direct_output <= direct_output;
      end
    end
  end
endmodule

module direct #(
    parameter DDS_FREQ = 32'd120000000
) (
    input clk,
    input rstn,

    input          param_wen,
    input [31 : 0] direct_fword,
    input [31 : 0] direct_pword,
    input [31 : 0] direct_amp,

    input [2 : 0] direct_en,

    input           dds_clk,
    output [31 : 0] direct_output_fword,
    output [31 : 0] direct_output_pword,
    output [31 : 0] direct_output_amp
);

  direct_core #(
      .DDS_FREQ(DDS_FREQ)
  ) direct_freq_inst (
      .clk (clk),
      .rstn(rstn),

      .param_wen  (param_wen),
      .direct_word(direct_fword),

      .direct_en(direct_en[0]),

      .dds_clk(dds_clk),
      .direct_output(direct_output_fword)
  );

  direct_core #(
      .DDS_FREQ(DDS_FREQ)
  ) direct_phase_inst (
      .clk (clk),
      .rstn(rstn),

      .param_wen  (param_wen),
      .direct_word(direct_pword),

      .direct_en(direct_en[1]),

      .dds_clk(dds_clk),
      .direct_output(direct_output_pword)
  );

  direct_core #(
      .DDS_FREQ(DDS_FREQ)
  ) direct_amp_inst (
      .clk (clk),
      .rstn(rstn),

      .param_wen  (param_wen),
      .direct_word(direct_amp),

      .direct_en(direct_en[2]),

      .dds_clk(dds_clk),
      .direct_output(direct_output_amp)
  );

endmodule

明显,direct_core模块没啥好说的,只是将数据打了一拍,使其从系统主时钟域进入DDS时钟域。这个模块同时提供了使能信号输入,在不使能的情况下将会保持之前的输出(而不是清零)。

着重讲一讲direct模块。这个模块是直接数据字生成器的顶层模块,例化了三个direct_core模块,分别输出频率字、相位字和幅度字。这样的例化保证了这三个生成器都是对称的,保证了逻辑唯一性,也减少了后期的维护成本。接下来的数字斜坡生成器和ADC调制生成器都将使用这个模式构建。

数字斜坡生成器

摆上代码好说话:

// src/dds/drg.v
module drg_core #(
    parameter DDS_FREQ = 32'd120000000
) (
    input clk,
    input rstn,

    input          param_wen,
    input [31 : 0] drg_start,
    input [31 : 0] drg_end,
    input [31 : 0] drg_step,
    input [31 : 0] drg_pulse,

    input drg_en,

    input               dds_clk,
    output reg [31 : 0] drg_output
);
  reg [31 : 0] drg_start_buf;
  reg [31 : 0] drg_end_buf;
  reg [31 : 0] drg_step_buf;
  reg [31 : 0] drg_pulse_buf;
  always @(posedge clk) begin
    if (!rstn) begin
      drg_start_buf <= 32'h0;
      drg_end_buf   <= 32'h0;
      drg_step_buf  <= 32'h0;
      drg_pulse_buf <= 32'h0;
    end else begin
      if (param_wen) begin
        drg_start_buf <= drg_start;
        drg_end_buf   <= drg_end;
        drg_step_buf  <= drg_step;
        drg_pulse_buf <= drg_pulse;
      end else begin
        drg_start_buf <= drg_start_buf;
        drg_end_buf   <= drg_end_buf;
        drg_step_buf  <= drg_step_buf;
        drg_pulse_buf <= drg_pulse_buf;
      end
    end
  end

  reg [31 : 0] drg_pulse_cnt;
  always @(posedge dds_clk) begin
    if (!rstn) begin
      drg_pulse_cnt <= 32'h0;
      drg_output    <= drg_start_buf;
    end else begin
      if (drg_en) begin
        if (drg_pulse_cnt >= drg_pulse_buf) begin
          drg_pulse_cnt <= 32'h0;

          if (drg_output + drg_step_buf < drg_end_buf) begin
            drg_output <= drg_output + drg_step_buf;
          end else begin
            drg_output <= drg_start_buf;
          end
        end else begin
          drg_pulse_cnt <= drg_pulse_cnt + 1;

          drg_output <= drg_output;
        end
      end else begin
        drg_output <= drg_output;
      end
    end
  end

endmodule

module drg #(
    parameter DDS_FREQ = 32'd120000000
) (
    input clk,
    input rstn,

    input          param_wen,
    input [31 : 0] drg_freq_start,
    input [31 : 0] drg_freq_end,
    input [31 : 0] drg_freq_step,
    input [31 : 0] drg_freq_pulse,
    input [31 : 0] drg_phase_start,
    input [31 : 0] drg_phase_end,
    input [31 : 0] drg_phase_step,
    input [31 : 0] drg_phase_pulse,
    input [31 : 0] drg_amp_start,
    input [31 : 0] drg_amp_end,
    input [31 : 0] drg_amp_step,
    input [31 : 0] drg_amp_pulse,

    input [2 : 0] drg_en,

    input           dds_clk,
    output [31 : 0] drg_output_fword,
    output [31 : 0] drg_output_pword,
    output [31 : 0] drg_output_amp
);

  drg_core #(
      .DDS_FREQ(DDS_FREQ)
  ) drg_freqreq_inst (
      .clk (clk),
      .rstn(rstn),

      .param_wen(param_wen),
      .drg_start(drg_freq_start),
      .drg_end  (drg_freq_end),
      .drg_step (drg_freq_step),
      .drg_pulse(drg_freq_pulse),

      .drg_en(drg_en[0]),

      .dds_clk   (dds_clk),
      .drg_output(drg_output_fword)
  );

  drg_core #(
      .DDS_FREQ(DDS_FREQ)
  ) drg_phasehase_inst (
      .clk (clk),
      .rstn(rstn),

      .param_wen(param_wen),
      .drg_start(drg_phase_start),
      .drg_end  (drg_phase_end),
      .drg_step (drg_phase_step),
      .drg_pulse(drg_phase_pulse),

      .drg_en(drg_en[1]),

      .dds_clk   (dds_clk),
      .drg_output(drg_output_pword)
  );

  drg_core #(
      .DDS_FREQ(DDS_FREQ)
  ) drg_ampmp_inst (
      .clk (clk),
      .rstn(rstn),

      .param_wen(param_wen),
      .drg_start(drg_amp_start),
      .drg_end  (drg_amp_end),
      .drg_step (drg_amp_step),
      .drg_pulse(drg_amp_pulse),

      .drg_en(drg_en[2]),

      .dds_clk   (dds_clk),
      .drg_output(drg_output_amp)
  );

endmodule

在配置接口中,drg_core模块需要四个参数: drg_startdrg_enddrg_stepdrg_pulse。这四个参数分别是开始值、结束值、步进值和等待周期值。在复位时,drg_core模块将drg_start提前装入输出寄存器drg_output;在复位信号结束时,如果drg_core被使能,则每经过drg_pulse个DDS时钟周期之后,会将输出寄存器drg_output增加drg_step。与此同时,drg_core模块保证输出值不超过drg_end。如果在下一次输出寄存器drg_output增加之后会超过drg_end,则drg_core模块不会增加drg_output,而是会将drg_output重新装载 drg_start值。这样可以保证数据不超过限度,最大程度保证系统的稳定性。

同样地,作为数字斜坡发生器的顶层模块,drg也例化了三个drg_core实例,对称操作频率字、相位字和幅度字。

ADC实时调制

代码先端上来罢:

module adc_core #(
    parameter DDS_FREQ  = 32'd120000000,
    parameter ADC_WIDTH = 12
) (
    input clk,
    input rstn,

    input          param_wen,
    input [31 : 0] adc_center,
    input [31 : 0] adc_kf,
    input [31 : 0] adc_ch_sel,
    input [31 : 0] adc_zero_cal,

    input adc_en,

    input                     adc_clk,
    input [ADC_WIDTH - 1 : 0] adc_data_1,
    input [ADC_WIDTH - 1 : 0] adc_data_2,

    input           dds_clk,
    output [31 : 0] adc_core_output
);

  reg [31 : 0] adc_center_buf;
  reg [31 : 0] adc_kf_buf;
  reg [31 : 0] adc_ch_sel_buf;
  reg [31 : 0] adc_zero_cal_buf;
  always @(posedge clk) begin
    if (!rstn) begin
      adc_center_buf   <= 32'h0;
      adc_kf_buf       <= 32'h0;
      adc_ch_sel_buf   <= 32'h0;
      adc_zero_cal_buf <= 32'h0;
    end else begin
      if (param_wen) begin
        adc_center_buf   <= adc_center;
        adc_kf_buf       <= adc_kf;
        adc_ch_sel_buf   <= adc_ch_sel;
        adc_zero_cal_buf <= adc_zero_cal;
      end else begin
        adc_center_buf   <= adc_center_buf;
        adc_kf_buf       <= adc_kf_buf;
        adc_ch_sel_buf   <= adc_ch_sel_buf;
        adc_zero_cal_buf <= adc_zero_cal_buf;
      end
    end
  end

  reg [ADC_WIDTH - 1 : 0] adc_data;
  always @(posedge adc_clk) begin
    if (!rstn) begin
      adc_data <= {ADC_WIDTH{1'b0}};
    end else begin
      if (adc_ch_sel_buf == 32'h0000_0001) begin
        adc_data <= adc_data_1;
      end else if (adc_ch_sel_buf == 32'h0000_0002) begin
        adc_data <= adc_data_2;
      end else begin
        adc_data <= {ADC_WIDTH{1'b0}};
      end
    end
  end

  reg  [31 : 0] adc_core_output_i;
  wire [31 : 0] adc_mul_temp_0;
  always @(posedge adc_clk) begin
    if (!rstn) begin
      adc_core_output_i <= 32'h0;
    end else begin
      if (adc_data < adc_zero_cal_buf) begin
        adc_core_output_i <= adc_center_buf - adc_mul_temp_0;
      end else begin
        adc_core_output_i <= adc_center_buf + adc_mul_temp_0;
      end
    end
  end

  dds_adc_mul_0 dds_adc_mul_0_inst (
      .clk(adc_clk),
      .rst(~rstn),

      .a(adc_kf_buf),
      .y(adc_data < adc_zero_cal_buf ? adc_zero_cal_buf - adc_data : adc_data - adc_zero_cal_buf),
      .p(adc_mul_temp_0)
  );

  adc_core_fifo_0 adc_core_fifo_0_inst (
      .rst(~rstn),

      .clkw(adc_clk),
      .we  (adc_en),
      .di  (adc_core_output_i),

      .clkr(dds_clk),
      .re  (adc_en),
      .dout(adc_core_output),

      .valid     (),
      .full_flag (),
      .empty_flag(),
      .afull     (),
      .aempty    (),
      .wrusedw   (),
      .rdusedw   ()
  );

endmodule

module adc #(
    parameter DDS_FREQ  = 32'd120000000,
    parameter ADC_WIDTH = 12
) (
    input clk,
    input rstn,

    input          param_wen,
    input [31 : 0] adc_freq_center,
    input [31 : 0] adc_freq_kf,
    input [31 : 0] adc_freq_ch_sel,
    input [31 : 0] adc_freq_zero_cal,
    input [31 : 0] adc_phase_center,
    input [31 : 0] adc_phase_kf,
    input [31 : 0] adc_phase_ch_sel,
    input [31 : 0] adc_phase_zero_cal,
    input [31 : 0] adc_amp_center,
    input [31 : 0] adc_amp_kf,
    input [31 : 0] adc_amp_ch_sel,
    input [31 : 0] adc_amp_zero_cal,

    input [2 : 0] adc_en,

    input          adc_clk,
    input [11 : 0] adc_data_1,
    input [11 : 0] adc_data_2,

    input           dds_clk,
    output [31 : 0] adc_output_fword,
    output [31 : 0] adc_output_pword,
    output [31 : 0] adc_output_amp
);

  adc_core #(
      .DDS_FREQ (DDS_FREQ),
      .ADC_WIDTH(ADC_WIDTH)
  ) adc_freq_inst (
      .clk (clk),
      .rstn(rstn),

      .param_wen   (param_wen),
      .adc_center  (adc_freq_center),
      .adc_kf      (adc_freq_kf),
      .adc_ch_sel  (adc_freq_ch_sel),
      .adc_zero_cal(adc_freq_zero_cal),

      .adc_en(adc_en[0]),

      .adc_clk   (adc_clk),
      .adc_data_1(adc_data_1),
      .adc_data_2(adc_data_2),

      .dds_clk        (dds_clk),
      .adc_core_output(adc_output_fword)
  );

  adc_core #(
      .DDS_FREQ (DDS_FREQ),
      .ADC_WIDTH(ADC_WIDTH)
  ) adc_phase_inst (
      .clk (clk),
      .rstn(rstn),

      .param_wen   (param_wen),
      .adc_center  (adc_phase_center),
      .adc_kf      (adc_phase_kf),
      .adc_ch_sel  (adc_phase_ch_sel),
      .adc_zero_cal(adc_phase_zero_cal),

      .adc_en(adc_en[1]),

      .adc_clk   (adc_clk),
      .adc_data_1(adc_data_1),
      .adc_data_2(adc_data_2),

      .dds_clk        (dds_clk),
      .adc_core_output(adc_output_pword)
  );

  adc_core #(
      .DDS_FREQ (DDS_FREQ),
      .ADC_WIDTH(ADC_WIDTH)
  ) adc_amp_inst (
      .clk (clk),
      .rstn(rstn),

      .param_wen   (param_wen),
      .adc_center  (adc_amp_center),
      .adc_kf      (adc_amp_kf),
      .adc_ch_sel  (adc_amp_ch_sel),
      .adc_zero_cal(adc_amp_zero_cal),

      .adc_en(adc_en[2]),

      .adc_clk   (adc_clk),
      .adc_data_1(adc_data_1),
      .adc_data_2(adc_data_2),

      .dds_clk        (dds_clk),
      .adc_core_output(adc_output_amp)
  );

endmodule

ADC调制生成器的核心模块adc_core稍微有点复杂,具体可以参照以下框图:

在这里插入图片描述

明显,这个模块跨越了3个异步时钟域,可以说是一个有大凶之兆(必出问题)的模块。首先,从系统时钟域到ADC时钟域的问题比较小一点,因为这个参数的更新频率低,大部分时间时不会改变的,哪怕一个时钟周期传不过来,两个、三个之后总能稳定下来。事实上,我连拍都没打,也没有在这里出现任何时序问题。

但是,从ADC时钟域到DDS时钟域的数据可是时时刻刻会更新的。这就需要一个低深度异步时钟FIFO来进行数据过渡了。这里选择了安路的Soft_FIFO,很好用。没加这个FIFO之前,DDS打出来的数据根本不能看。用CWC抓了一下内部逻辑,发现是ADC调制发生器的输出数据紊乱了,所以立马加了一个FIFO,数据果然变得稳如老狗。

RAM调制(未实现)

按理来说,像AD9910那样,搞个1024深度的RAM也不难。但是我不想用这么low的办法,我想用的是DDR的512MByte深度,有点好高骛远,因此到现在为止暂时还没实现,还请等待一下吧,哈哈(悲伤的笑脸)。

除法器调幅

在dds的顶层模块中,实现了一个除法器作为数字调幅的部件。但是说实话,还不如不加,因为除法运算是非线性的,用drg作为幅度字、direct作为频率字输出时,得到波形包络并非一条直线,而是一个反比例函数的一部分。

但是技术细节还是可以讲一讲的。首先使用安路的DDS IP进行DDS初始数据的输出。这个IP核输出的数据位宽为32位,选择sin+cos(正交信号输出)模式,为后面的除法运算提供足够巨大的数据。

官方IP手册说DDS的IP核输出数据最多可以到26位,但是我写32的时候,TD并没有表现出什么异议。这句话的意思不是说安路的TD写得好,而是在催安路官方赶紧更新一下数据手册和参考手册。说实话,不是国产的器件我还懒得骂(甚至懒得用)了,就是因为安路是国产FPGA第一梯队之一,才要特地提醒一下

然后就是直接将DDS核的输出接到除法器中。注意,因为DDS核输出的是补码,因此除法器的分子应当设为singed(有符号数)。而我产生的幅度字是原码,因此分母设置为unsigned(无符号数)。这样,除法器就会输出一个有符号数的32位商一个有符号数的32位余数。余数直接忽略掉,把商的最高位取反(这里用的处理方法是在最高位加1)就是原码。此时的32位的商取低n位(n为DAC位数)就可以输出到DAC了。

外部接口

其实现在主要就是一个SPI Slave(FPGA作为从机)首先MCU到FPGA的数据传输。如果后续有需要的话会考虑实现一个SPI主从通道,实现FPGA高速采集之后向MCU的数据传输。不过高深度的数据采集仍然需要DDR3作为储存支撑,而一旦涉及DDR内存读写,就必然不可回避高速仲裁器的问题。如果以后写出来了,一定会第一时间出文章的。

参考资料

安路DDS IP核用户中文手册

AD9910官方数据手册(英文)

附件链接

这一组代码的Github仓库链接,恳请各位给个star鼓励鼓励吧(扭曲地渴望)

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值