diff options
Diffstat (limited to 'fpga/usrp3/lib')
-rw-r--r-- | fpga/usrp3/lib/ip/dds_sin_cos_lut_only/dds_sin_cos_lut_only.xci | 14 | ||||
-rw-r--r-- | fpga/usrp3/lib/rfnoc/Makefile.srcs | 2 | ||||
-rw-r--r-- | fpga/usrp3/lib/rfnoc/dds_freq_tune_duc.v | 228 | ||||
-rw-r--r-- | fpga/usrp3/lib/rfnoc/dds_timed.v | 718 | ||||
-rw-r--r-- | fpga/usrp3/lib/rfnoc/dds_wrapper.v | 160 | ||||
-rw-r--r-- | fpga/usrp3/lib/rfnoc/sim/dds_timed_tb/Makefile | 68 | ||||
-rw-r--r-- | fpga/usrp3/lib/rfnoc/sim/dds_timed_tb/dds_timed_tb.sv | 886 |
7 files changed, 1858 insertions, 218 deletions
diff --git a/fpga/usrp3/lib/ip/dds_sin_cos_lut_only/dds_sin_cos_lut_only.xci b/fpga/usrp3/lib/ip/dds_sin_cos_lut_only/dds_sin_cos_lut_only.xci index 4220fa7c4..0efbb6929 100644 --- a/fpga/usrp3/lib/ip/dds_sin_cos_lut_only/dds_sin_cos_lut_only.xci +++ b/fpga/usrp3/lib/ip/dds_sin_cos_lut_only/dds_sin_cos_lut_only.xci @@ -35,7 +35,7 @@ <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.M_AXIS_DATA.TDATA_NUM_BYTES">4</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.M_AXIS_DATA.TDEST_WIDTH">0</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.M_AXIS_DATA.TID_WIDTH">0</spirit:configurableElementValue> - <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.M_AXIS_DATA.TUSER_WIDTH">0</spirit:configurableElementValue> + <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.M_AXIS_DATA.TUSER_WIDTH">1</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.M_AXIS_PHASE.CLK_DOMAIN"/> <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.M_AXIS_PHASE.FREQ_HZ">100000000</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.M_AXIS_PHASE.HAS_TKEEP">0</spirit:configurableElementValue> @@ -74,7 +74,7 @@ <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.S_AXIS_PHASE.TDATA_NUM_BYTES">3</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.S_AXIS_PHASE.TDEST_WIDTH">0</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.S_AXIS_PHASE.TID_WIDTH">0</spirit:configurableElementValue> - <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.S_AXIS_PHASE.TUSER_WIDTH">0</spirit:configurableElementValue> + <spirit:configurableElementValue spirit:referenceId="BUSIFPARAM_VALUE.S_AXIS_PHASE.TUSER_WIDTH">1</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_ACCUMULATOR_WIDTH">24</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_AMPLITUDE">1</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_CHANNELS">1</spirit:configurableElementValue> @@ -95,7 +95,7 @@ <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_MEM_TYPE">1</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_MODE_OF_OPERATION">0</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_MODULUS">9</spirit:configurableElementValue> - <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_M_DATA_HAS_TUSER">0</spirit:configurableElementValue> + <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_M_DATA_HAS_TUSER">2</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_M_DATA_TDATA_WIDTH">32</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_M_DATA_TUSER_WIDTH">1</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_M_PHASE_HAS_TUSER">0</spirit:configurableElementValue> @@ -117,7 +117,7 @@ <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_RESYNC">0</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_S_CONFIG_SYNC_MODE">0</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_S_CONFIG_TDATA_WIDTH">1</spirit:configurableElementValue> - <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_S_PHASE_HAS_TUSER">0</spirit:configurableElementValue> + <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_S_PHASE_HAS_TUSER">2</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_S_PHASE_TDATA_WIDTH">24</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_S_PHASE_TUSER_WIDTH">1</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="MODELPARAM_VALUE.C_USE_DSP48">0</spirit:configurableElementValue> @@ -136,7 +136,7 @@ <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Has_TREADY">true</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Latency">12</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Latency_Configuration">Configurable</spirit:configurableElementValue> - <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.M_DATA_Has_TUSER">Not_Required</spirit:configurableElementValue> + <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.M_DATA_Has_TUSER">User_Field</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.M_PHASE_Has_TUSER">Not_Required</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Memory_Type">Block_ROM</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Mode_of_Operation">Standard</spirit:configurableElementValue> @@ -220,7 +220,7 @@ <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Phase_offset">None</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Resync">false</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.S_CONFIG_Sync_Mode">On_Vector</spirit:configurableElementValue> - <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.S_PHASE_Has_TUSER">Not_Required</spirit:configurableElementValue> + <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.S_PHASE_Has_TUSER">User_Field</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.S_PHASE_TUSER_Width">1</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.Spurious_Free_Dynamic_Range">45</spirit:configurableElementValue> <spirit:configurableElementValue spirit:referenceId="PARAM_VALUE.explicit_period">false</spirit:configurableElementValue> @@ -256,6 +256,7 @@ <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.M_AXIS_DATA.TDATA_NUM_BYTES" xilinx:valueSource="auto"/> <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.M_AXIS_DATA.TDEST_WIDTH" xilinx:valueSource="constant"/> <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.M_AXIS_DATA.TID_WIDTH" xilinx:valueSource="constant"/> + <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.M_AXIS_DATA.TUSER_WIDTH" xilinx:valueSource="auto"/> <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.M_AXIS_PHASE.HAS_TKEEP" xilinx:valueSource="constant"/> <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.M_AXIS_PHASE.HAS_TREADY" xilinx:valueSource="auto"/> <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.M_AXIS_PHASE.HAS_TSTRB" xilinx:valueSource="constant"/> @@ -275,6 +276,7 @@ <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.S_AXIS_PHASE.TDATA_NUM_BYTES" xilinx:valueSource="auto"/> <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.S_AXIS_PHASE.TDEST_WIDTH" xilinx:valueSource="constant"/> <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.S_AXIS_PHASE.TID_WIDTH" xilinx:valueSource="constant"/> + <xilinx:configElementInfo xilinx:referenceId="BUSIFPARAM_VALUE.S_AXIS_PHASE.TUSER_WIDTH" xilinx:valueSource="auto"/> <xilinx:configElementInfo xilinx:referenceId="PARAM_VALUE.Amplitude_Mode" xilinx:valueSource="user"/> <xilinx:configElementInfo xilinx:referenceId="PARAM_VALUE.Channels" xilinx:valueSource="user"/> <xilinx:configElementInfo xilinx:referenceId="PARAM_VALUE.DATA_Has_TLAST" xilinx:valueSource="user"/> diff --git a/fpga/usrp3/lib/rfnoc/Makefile.srcs b/fpga/usrp3/lib/rfnoc/Makefile.srcs index ab4a9e179..512569748 100644 --- a/fpga/usrp3/lib/rfnoc/Makefile.srcs +++ b/fpga/usrp3/lib/rfnoc/Makefile.srcs @@ -101,6 +101,8 @@ fir_filter_slice.v \ axi_fir_filter_dec.v \ addsub.vhd \ dds_freq_tune.v \ +dds_freq_tune_duc.v \ dds_timed.v \ +dds_wrapper.v \ datapath_gatekeeper.v \ )) diff --git a/fpga/usrp3/lib/rfnoc/dds_freq_tune_duc.v b/fpga/usrp3/lib/rfnoc/dds_freq_tune_duc.v new file mode 100644 index 000000000..4b8a299b8 --- /dev/null +++ b/fpga/usrp3/lib/rfnoc/dds_freq_tune_duc.v @@ -0,0 +1,228 @@ +// +// Copyright 2021 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Module: dds_freq_tune_duc +// +// Description: +// +// Performs a frequency shift on a signal by multiplying it with a complex +// sinusoid synthesized from a DDS. This module expects samples data to be in +// {Q,I} order. +// +// The din input is expected to contain a complex 24-bit signed fixed-point +// values with 15 fractional bits. The phase input is expected to contain +// unsigned 24-bit fixed-point with 24 fractional bits, and therefore +// represents the range [0,1), which corresponds to the range [0,2π) radians. +// The output will then be a complex 24-bit signed fixed-point with 15 +// fractional bits. +// +// This version does the same thing as dds_freq_tune, but does not +// reset/flush the DDS between packets or when an EOB occurs, and it includes +// a FIFO on the din data path. This separate version was created to avoid +// affecting the behavior of the DDC. +// +// ┌───┐ +// phase >──┤DDS├──┐ ┌───────┐ +// └───┘ └─┤Complex│ ┌─────┐ +// │ Mult ├──┤Round├───> dout +// ┌────┐ ┌─┤ │ └─────┘ +// din >──┤FIFO├─┘ └───────┘ +// └────┘ +// +// Parameters: +// +// Note: The parameters should NOT be changed, since they depend on the IP +// configurations. +// +// INPUT_W : Width of each component of din. +// PHASE_W : Width of the phase input. +// OUTPUT_W : Width of each component of dout. +// + +`default_nettype none + + +module dds_freq_tune_duc #( + parameter INPUT_W = 24, + parameter PHASE_W = 24, + parameter OUTPUT_W = 24 +) ( + input wire clk, + input wire reset, + + // IQ input (Q in the upper, I in the lower bits) + input wire [INPUT_W*2-1:0] s_axis_din_tdata, + input wire s_axis_din_tlast, + input wire s_axis_din_tvalid, + output wire s_axis_din_tready, + + // Phase input from NCO + input wire [PHASE_W-1:0] s_axis_phase_tdata, + input wire s_axis_phase_tlast, + input wire s_axis_phase_tvalid, + output wire s_axis_phase_tready, + + // IQ output (Q in the upper, I in the lower bits) + output wire [OUTPUT_W*2-1:0] m_axis_dout_tdata, + output wire m_axis_dout_tlast, + output wire m_axis_dout_tvalid, + input wire m_axis_dout_tready +); + + //--------------------------------------------------------------------------- + // Reset Generation + //--------------------------------------------------------------------------- + + reg reset_d1, reset_int; + + // Create a local reset, named reset_int, which will always be asserted for + // at least 2 clock cycles, which is required by Xilinx DDS and complex + // multiplier IP. + always @(posedge clk) begin + reset_d1 <= reset; + reset_int <= reset | reset_d1; + end + + + //--------------------------------------------------------------------------- + // Data Input FIFO + //--------------------------------------------------------------------------- + // + // We want the din and phase inputs paths to be balanced, so that a new + // data/phase pair can be input on each clock cycles. This FIFO allows the + // din data path to queue up samples while the DDS is processing. + // + //--------------------------------------------------------------------------- + + wire [INPUT_W*2-1:0] s_axis_fifo_tdata; + wire s_axis_fifo_tlast; + wire s_axis_fifo_tvalid; + wire s_axis_fifo_tready; + + axi_fifo #( + .WIDTH (2*INPUT_W+1), + .SIZE (5) + ) axi_fifo_i ( + .clk (clk), + .reset (reset), + .clear (1'b0), + .i_tdata ({ s_axis_din_tlast, s_axis_din_tdata }), + .i_tvalid (s_axis_din_tvalid), + .i_tready (s_axis_din_tready), + .o_tdata ({ s_axis_fifo_tlast, s_axis_fifo_tdata }), + .o_tvalid (s_axis_fifo_tvalid), + .o_tready (s_axis_fifo_tready), + .space (), + .occupied () + ); + + + //--------------------------------------------------------------------------- + // DDS/NCO + //--------------------------------------------------------------------------- + + // Width of each component of the DDS output. This width is fixed by the IP + // configuration. + parameter DDS_W = 16; + + wire m_axis_dds_tlast; + wire m_axis_dds_tvalid; + wire m_axis_dds_tready; + wire [DDS_W*2-1:0] m_axis_dds_tdata; + + // DDS to convert the phase input to a unit-length complex number with that + // phase. It takes in an unsigned 24-bit phase with 24 fractional bits and + // outputs two signed 16-bit fixed point values with 14 fractional bits. The + // output has sin(2*pi*phase) in the upper 16 bits and cos(2*pi*phase) in the + // lower 16-bits. + dds_wrapper dds_wrapper_i ( + .clk (clk), + .rst (reset_int), + .s_axis_phase_tdata (s_axis_phase_tdata), + .s_axis_phase_tvalid (s_axis_phase_tvalid), + .s_axis_phase_tlast (s_axis_phase_tlast), + .s_axis_phase_tready (s_axis_phase_tready), + .m_axis_data_tdata (m_axis_dds_tdata), + .m_axis_data_tvalid (m_axis_dds_tvalid), + .m_axis_data_tlast (m_axis_dds_tlast), + .m_axis_data_tready (m_axis_dds_tready) + ); + + + //--------------------------------------------------------------------------- + // Complex Multiplier + //--------------------------------------------------------------------------- + // + // Use a complex multiplier to multiply the DDS complex sinusoid by the input + // data samples. + // + //--------------------------------------------------------------------------- + + // Width of each component on the output of the complex_multiplier_dds IP. + // This width is fixed by the IP configuration. + localparam MULT_OUT_W = 32; + + // Width is set by the IP + wire [2*MULT_OUT_W-1:0] mult_out_tdata; + wire mult_out_tvalid; + wire mult_out_tready; + wire mult_out_tlast; + + // The complex multiplier IP is configured so that the A input is 21 bits + // with 15 fractional bits, and the B input (dds) is 16 bits with 14 + // fractional bits. Due to AXI-Stream requirements, A is rounded up to 24 + // bits in width. The full multiplier output result would be 21+16+1 = 38 + // bits, but the output is configured for 32, dropping the lower 6 bits. + // Therefore, the result has 15+14-6 = 23 fractional bits. + // + // The IP is configured to pass the TLAST from port A through, but we connect + // the B path anyway for completeness. + complex_multiplier_dds complex_multiplier_dds_i ( + .aclk (clk), + .aresetn (~reset_int), + .s_axis_a_tvalid (s_axis_fifo_tvalid), + .s_axis_a_tready (s_axis_fifo_tready), + .s_axis_a_tlast (s_axis_fifo_tlast), + .s_axis_a_tdata (s_axis_fifo_tdata), + .s_axis_b_tvalid (m_axis_dds_tvalid), + .s_axis_b_tready (m_axis_dds_tready), + .s_axis_b_tlast (m_axis_dds_tlast), + .s_axis_b_tdata (m_axis_dds_tdata), + .m_axis_dout_tvalid (mult_out_tvalid), + .m_axis_dout_tready (mult_out_tready), + .m_axis_dout_tlast (mult_out_tlast), + .m_axis_dout_tdata (mult_out_tdata) + ); + + + //--------------------------------------------------------------------------- + // Round + //--------------------------------------------------------------------------- + // + // Round the 32-bit multiplier result down to 24 bits. This moves the binary + // point so that we go from 23 fractional bits down to 15 fractional bits. + // + //--------------------------------------------------------------------------- + + axi_round_complex #( + .WIDTH_IN (MULT_OUT_W), + .WIDTH_OUT (OUTPUT_W) + ) axi_round_complex_i ( + .clk (clk), + .reset (reset_int), + .i_tdata (mult_out_tdata), + .i_tlast (mult_out_tlast), + .i_tvalid (mult_out_tvalid), + .i_tready (mult_out_tready), + .o_tdata (m_axis_dout_tdata), + .o_tlast (m_axis_dout_tlast), + .o_tvalid (m_axis_dout_tvalid), + .o_tready (m_axis_dout_tready) + ); + +endmodule + + +`default_nettype wire diff --git a/fpga/usrp3/lib/rfnoc/dds_timed.v b/fpga/usrp3/lib/rfnoc/dds_timed.v index fd03f6a23..1af82683d 100644 --- a/fpga/usrp3/lib/rfnoc/dds_timed.v +++ b/fpga/usrp3/lib/rfnoc/dds_timed.v @@ -1,9 +1,75 @@ // -// Copyright 2016 Ettus Research, a National Instruments Company +// Copyright 2021 Ettus Research, a National Instruments Brand // // SPDX-License-Identifier: LGPL-3.0-or-later // -// DDS that supports timed commands via the settings bus +// Module: dds_timed +// +// Description: +// +// DDS (direct digital synthesis) and frequency shift block that supports +// timed commands via the settings bus. +// +// This block takes in samples on i_t* and performs a complex multiplication +// with a digitally synthesized oscillator to implement a digital RF mixer. +// The output is then scaled (optionally), rounded, and clipped if necessary, +// then output on o_t*. +// +// Timed commands allow you to update the SR_FREQ register (the phase +// increment) at the desired time. +// +// The TUSER port contains the packet header information: +// +// tuser[125] : Has timestamp +// tuser[124] : End of burst (EOB) +// tuser[63:0] : Timestamp +// +// For the input, i_tuser should be valid for the duration of the packet. For +// the output, o_tuser is only guaranteed to be valid for the last sample of +// the packet. +// +// Registers: +// +// SR_FREQ : Frequency shift to apply to the input signal. This can be +// thought of as an unsigned PHASE_ACCUM_WIDTH-bit register +// with PHASE_ACCUM_WIDTH fractional bits. That is, the range +// of this register maps to the real values [0,1). This +// register controls the amount by which the phase accumulator +// for the DDS is incremented each clock cycle. It can +// therefore be thought of as a phase angle corresponding to +// the range [0,2π) radians. +// SR_SCALE_IQ : Scaler by which to multiply the IQ outputs. This is a +// SCALING_WIDTH-bit signed fixed-point register with 15 +// fractional bits. If SCALING_WIDTH is 18, then it has the +// range [-4,4). +// +// Parameters: +// +// Note: Care must be used when overriding these parameters because there are +// many dependencies on them. For example, the DDS_WIDTH and +// PHASE_WIDTH depend on the configuration of the underlying DDS IP and +// should only be modified to match that IP. +// +// SR_FREQ_ADDR : Register offset to assign to the SR_FREQ register, +// which contains the phase increment per sample needed +// to achieve the desired DDS frequency. +// SR_SCALE_IQ_ADDR : Register offset to assign to the SR_SCALE_IQ register. +// CMD_FIFO_SIZE : Log2 of the size of the timed command FIFO to use. +// WIDTH : Data width of the I/Q components of the input/output +// samples, typically 16. +// DDS_WIDTH : Bit width to use for the DDS and complex multiplier. +// PHASE_WIDTH : Bit width to use for the phase provided to the DDS IP. +// PHASE_ACCUM_WIDTH : Bit width to use for the phase increment values. +// SCALING_WIDTH : Bit width to use for the IQ scale registers. +// HEADER_WIDTH : Width of the header info (tuser). +// HEADER_FIFO_SIZE : Log2 of the size of the header FIFO. +// SR_AWIDTH : Settings bus address width. +// SR_DWIDTH : Settings bus data width. +// SR_TWIDTH : Settings bus time width. +// + +`default_nettype none + module dds_timed #( parameter SR_FREQ_ADDR = 0, @@ -19,142 +85,280 @@ module dds_timed #( parameter SR_AWIDTH = 8, parameter SR_DWIDTH = 32, parameter SR_TWIDTH = 64 -)( - input clk, input reset, input clear, - output timed_cmd_fifo_full, - input set_stb, input [SR_AWIDTH-1:0] set_addr, input [SR_DWIDTH-1:0] set_data, - input [SR_TWIDTH-1:0] set_time, input set_has_time, - input [2*WIDTH-1:0] i_tdata, input i_tlast, input i_tvalid, output i_tready, input [HEADER_WIDTH-1:0] i_tuser, - output [2*WIDTH-1:0] o_tdata, output o_tlast, output o_tvalid, input o_tready, output [HEADER_WIDTH-1:0] o_tuser +) ( + input wire clk, + input wire reset, + input wire clear, + + // Indicates if the timed command FIFO is full + output wire timed_cmd_fifo_full, + + // Settings bus for register access + input wire set_stb, + input wire [SR_AWIDTH-1:0] set_addr, + input wire [SR_DWIDTH-1:0] set_data, + input wire [SR_TWIDTH-1:0] set_time, + input wire set_has_time, + + // Input sample stream + input wire [ 2*WIDTH-1:0] i_tdata, + input wire i_tlast, + input wire i_tvalid, + output wire i_tready, + input wire [HEADER_WIDTH-1:0] i_tuser, + + // Output sample stream + output wire [ 2*WIDTH-1:0] o_tdata, + output wire o_tlast, + output wire o_tvalid, + input wire o_tready, + output wire [HEADER_WIDTH-1:0] o_tuser ); - /************************************************************************** - * Track VITA time - *************************************************************************/ - wire [2*WIDTH-1:0] int_tdata; + //--------------------------------------------------------------------------- + // Time Tracking + //--------------------------------------------------------------------------- + + wire [ 2*WIDTH-1:0] int_tdata; wire [HEADER_WIDTH-1:0] int_tuser; - wire int_tlast, int_tvalid, int_tready, int_tag; - wire [SR_AWIDTH-1:0] out_set_addr, timed_set_addr; - wire [SR_DWIDTH-1:0] out_set_data, timed_set_data; - wire out_set_stb, timed_set_stb; - wire eob; + wire int_tlast; + wire int_tvalid; + wire int_tready; + wire int_tag; + wire [ SR_AWIDTH-1:0] out_set_addr; + wire [ SR_AWIDTH-1:0] timed_set_addr; + wire [ SR_DWIDTH-1:0] out_set_data; + wire [ SR_DWIDTH-1:0] timed_set_data; + wire out_set_stb; + wire timed_set_stb; + // This module checks for timed writes to SR_FREQ_ADDR and outputs the + // register write on timed_set_* (if it was timed) or set_* (if it was not + // timed). It then tags the sample for which the timed command to + // SR_FREQ_ADDR should occur by asserting m_axis_data_tag when that sample is + // output. axi_tag_time #( - .WIDTH(2*WIDTH), - .NUM_TAGS(1), - .SR_TAG_ADDRS(SR_FREQ_ADDR)) - axi_tag_time ( - .clk(clk), - .reset(reset), - .clear(clear), - .tick_rate(16'd1), - .timed_cmd_fifo_full(timed_cmd_fifo_full), - .s_axis_data_tdata(i_tdata), .s_axis_data_tlast(i_tlast), - .s_axis_data_tvalid(i_tvalid), .s_axis_data_tready(i_tready), - .s_axis_data_tuser(i_tuser), - .m_axis_data_tdata(int_tdata), .m_axis_data_tlast(int_tlast), - .m_axis_data_tvalid(int_tvalid), .m_axis_data_tready(int_tready), - .m_axis_data_tuser(int_tuser), .m_axis_data_tag(int_tag), - .in_set_stb(set_stb), .in_set_addr(set_addr), .in_set_data(set_data), - .in_set_time(set_time), .in_set_has_time(set_has_time), - .out_set_stb(out_set_stb), .out_set_addr(out_set_addr), .out_set_data(out_set_data), - .timed_set_stb(timed_set_stb), .timed_set_addr(timed_set_addr), .timed_set_data(timed_set_data)); - - wire [2*WIDTH-1:0] dds_in_tdata, unused_tdata; - wire [HEADER_WIDTH-1:0] header_in_tdata, header_out_tdata, unused_tuser; - wire dds_in_tlast, dds_in_tvalid, dds_in_tready, dds_in_tag; - wire header_in_tvalid, header_in_tready, header_in_tlast, unused_tag; - wire header_out_tvalid, header_out_tready; + .WIDTH (2*WIDTH), + .NUM_TAGS (1), + .SR_TAG_ADDRS (SR_FREQ_ADDR) + ) axi_tag_time ( + .clk (clk), + .reset (reset), + .clear (clear), + .tick_rate (16'd1), + .timed_cmd_fifo_full (timed_cmd_fifo_full), + .s_axis_data_tdata (i_tdata), + .s_axis_data_tlast (i_tlast), + .s_axis_data_tvalid (i_tvalid), + .s_axis_data_tready (i_tready), + .s_axis_data_tuser (i_tuser), + .m_axis_data_tdata (int_tdata), + .m_axis_data_tlast (int_tlast), + .m_axis_data_tvalid (int_tvalid), + .m_axis_data_tready (int_tready), + .m_axis_data_tuser (int_tuser), + .m_axis_data_tag (int_tag), + .in_set_stb (set_stb), + .in_set_addr (set_addr), + .in_set_data (set_data), + .in_set_time (set_time), + .in_set_has_time (set_has_time), + .out_set_stb (out_set_stb), + .out_set_addr (out_set_addr), + .out_set_data (out_set_data), + .timed_set_stb (timed_set_stb), + .timed_set_addr (timed_set_addr), + .timed_set_data (timed_set_data) + ); + + wire [ 2*WIDTH-1:0] dds_in_tdata; + wire [ 2*WIDTH-1:0] unused_tdata; + wire [HEADER_WIDTH-1:0] header_in_tdata; + wire [HEADER_WIDTH-1:0] header_out_tdata; + wire [HEADER_WIDTH-1:0] dds_in_tuser; + wire dds_in_tlast; + wire dds_in_tvalid; + wire dds_in_tready; + wire dds_in_tag; + wire header_in_tvalid; + wire header_in_tready; + wire header_in_tlast; + wire unused_tag; + wire header_out_tvalid; + wire header_out_tready; + + + //--------------------------------------------------------------------------- + // Split Stream + //--------------------------------------------------------------------------- + // + // Split the data stream into two streams, one with the data/tag (dds_in_t*) + // and the other with the header (header_in_t*). + // + //--------------------------------------------------------------------------- split_stream #( - .WIDTH(2*WIDTH+HEADER_WIDTH+1), .ACTIVE_MASK(4'b0011)) - split_head ( - .clk(clk), .reset(reset), .clear(clear), - .i_tdata({int_tdata,int_tuser,int_tag}), .i_tlast(int_tlast), - .i_tvalid(int_tvalid), .i_tready(int_tready), - .o0_tdata({dds_in_tdata,unused_tuser,dds_in_tag}), .o0_tlast(dds_in_tlast), - .o0_tvalid(dds_in_tvalid), .o0_tready(dds_in_tready), - .o1_tdata({unused_tdata,header_in_tdata,unused_tag}), .o1_tlast(header_in_tlast), - .o1_tvalid(header_in_tvalid), .o1_tready(header_in_tready), - .o2_tready(1'b0), .o3_tready(1'b0)); + .WIDTH (2*WIDTH+HEADER_WIDTH+1), + .ACTIVE_MASK (4'b0011) + ) split_head ( + .clk (clk), + .reset (reset), + .clear (clear), + .i_tdata ({ int_tdata, int_tuser, int_tag }), + .i_tlast (int_tlast), + .i_tvalid (int_tvalid), + .i_tready (int_tready), + .o0_tdata ({ dds_in_tdata, dds_in_tuser, dds_in_tag }), + .o0_tlast (dds_in_tlast), + .o0_tvalid (dds_in_tvalid), + .o0_tready (dds_in_tready), + .o1_tdata ({ unused_tdata, header_in_tdata, unused_tag }), + .o1_tlast (header_in_tlast), + .o1_tvalid (header_in_tvalid), + .o1_tready (header_in_tready), + .o2_tdata (), + .o2_tlast (), + .o2_tvalid (), + .o2_tready (1'b0), + .o3_tdata (), + .o3_tlast (), + .o3_tvalid (), + .o3_tready (1'b0) + ); + + + //--------------------------------------------------------------------------- + // Header FIFO + //--------------------------------------------------------------------------- + // + // Store each packet header in a FIFO to be read out when the packet is + // output. + // + //--------------------------------------------------------------------------- axi_fifo #( - .WIDTH(HEADER_WIDTH), .SIZE(HEADER_FIFO_SIZE)) - axi_fifo_header ( - .clk(clk), .reset(reset), .clear(clear), - .i_tdata(header_in_tdata), .i_tvalid(header_in_tvalid & header_in_tlast), .i_tready(header_in_tready), - .o_tdata(header_out_tdata), .o_tvalid(header_out_tvalid), - .o_tready(header_out_tready), // Consume header on last output sample - .space(), .occupied()); - - assign eob = header_in_tdata[124]; - - /************************************************************************** - * Settings Regs - *************************************************************************/ - wire [PHASE_ACCUM_WIDTH-1:0] phase_inc_tdata, phase_inc_timed_tdata; - wire phase_inc_tlast, phase_inc_tvalid, phase_inc_tready; - wire phase_inc_timed_tlast, phase_inc_timed_tready , phase_inc_timed_tvalid; + .WIDTH (HEADER_WIDTH), + .SIZE (HEADER_FIFO_SIZE) + ) axi_fifo_header ( + .clk (clk), + .reset (reset), + .clear (clear), + .i_tdata (header_in_tdata), + .i_tvalid (header_in_tvalid & header_in_tlast), + .i_tready (header_in_tready), + .o_tdata (header_out_tdata), + .o_tvalid (header_out_tvalid), + .o_tready (header_out_tready), // Consume header on last output sample + .space (), + .occupied () + ); + + + //--------------------------------------------------------------------------- + // Settings Bus Registers + //--------------------------------------------------------------------------- + wire [PHASE_ACCUM_WIDTH-1:0] phase_inc_tdata; + wire [PHASE_ACCUM_WIDTH-1:0] phase_inc_timed_tdata; + wire phase_inc_tlast; + wire phase_inc_tvalid; + wire phase_inc_tready; + wire phase_inc_timed_tlast; + wire phase_inc_timed_tready; + wire phase_inc_timed_tvalid; + + // Frequency register (phase increment) used for *un-timed* commands axi_setting_reg #( - .ADDR(SR_FREQ_ADDR), .AWIDTH(SR_AWIDTH), .WIDTH(PHASE_ACCUM_WIDTH), .STROBE_LAST(1)) - set_freq ( - .clk(clk), .reset(reset), - .set_stb(out_set_stb), .set_addr(out_set_addr), .set_data(out_set_data), - .o_tdata(phase_inc_tdata), .o_tlast(phase_inc_tlast), .o_tvalid(phase_inc_tvalid), .o_tready(phase_inc_tready)); + .ADDR (SR_FREQ_ADDR), + .AWIDTH (SR_AWIDTH), + .WIDTH (PHASE_ACCUM_WIDTH), + .STROBE_LAST (1) + ) set_freq ( + .clk (clk), + .reset (reset), + .set_stb (out_set_stb), + .set_addr (out_set_addr), + .set_data (out_set_data), + .o_tdata (phase_inc_tdata), + .o_tlast (phase_inc_tlast), + .o_tvalid (phase_inc_tvalid), + .o_tready (phase_inc_tready) + ); + // Frequency register (phase increment) used for *timed* commands axi_setting_reg #( - .ADDR(SR_FREQ_ADDR), .USE_FIFO(1), .FIFO_SIZE(CMD_FIFO_SIZE), .AWIDTH(SR_AWIDTH), .WIDTH(PHASE_ACCUM_WIDTH), .STROBE_LAST(1)) - set_freq_timed ( - .clk(clk), .reset(reset), - .set_stb(timed_set_stb), .set_addr(timed_set_addr), .set_data(timed_set_data), - .o_tdata(phase_inc_timed_tdata), .o_tlast(phase_inc_timed_tlast), .o_tvalid(phase_inc_timed_tvalid), .o_tready(phase_inc_timed_tready)); + .ADDR (SR_FREQ_ADDR), + .USE_FIFO (1), + .FIFO_SIZE (CMD_FIFO_SIZE), + .AWIDTH (SR_AWIDTH), + .WIDTH (PHASE_ACCUM_WIDTH), + .STROBE_LAST (1) + ) set_freq_timed ( + .clk (clk), + .reset (reset), + .set_stb (timed_set_stb), + .set_addr (timed_set_addr), + .set_data (timed_set_data), + .o_tdata (phase_inc_timed_tdata), + .o_tlast (phase_inc_timed_tlast), + .o_tvalid (phase_inc_timed_tvalid), + .o_tready (phase_inc_timed_tready) + ); wire [SCALING_WIDTH-1:0] scaling_tdata; - wire scaling_tvalid, scaling_tready; + wire scaling_tready; + // Scale value register axi_setting_reg #( - .ADDR(SR_SCALE_IQ_ADDR), .AWIDTH(SR_AWIDTH), .WIDTH(SCALING_WIDTH), .REPEATS(1)) - set_scale ( - .clk(clk), .reset(reset), - .set_stb(out_set_stb), .set_addr(out_set_addr), .set_data(out_set_data), - .o_tdata(scaling_tdata), .o_tlast(), .o_tvalid(scaling_tvalid), .o_tready(scaling_tready)); - - /************************************************************************** - * DDS + Complex Mult + Phase Accumulator - *************************************************************************/ + .ADDR (SR_SCALE_IQ_ADDR), + .AWIDTH (SR_AWIDTH), + .WIDTH (SCALING_WIDTH), + .REPEATS (1) + ) set_scale ( + .clk (clk), + .reset (reset), + .set_stb (out_set_stb), + .set_addr (out_set_addr), + .set_data (out_set_data), + .o_tdata (scaling_tdata), + .o_tlast (), + .o_tvalid (), + .o_tready (scaling_tready) + ); + + + //--------------------------------------------------------------------------- + // Phase Accumulator for DDS + //--------------------------------------------------------------------------- + wire [PHASE_ACCUM_WIDTH-1:0] phase_inc_mux_tdata; - reg [PHASE_ACCUM_WIDTH-1:0] phase_inc; - wire phase_inc_mux_tlast, phase_inc_mux_tvalid, phase_inc_mux_tready; - reg [PHASE_ACCUM_WIDTH-1:0] phase; - - wire [PHASE_WIDTH-1:0] phase_tdata = phase[PHASE_ACCUM_WIDTH-1:PHASE_ACCUM_WIDTH-PHASE_WIDTH]; - wire phase_tvalid, phase_tready, phase_tlast; - - wire [WIDTH*2-1:0] dds_in_fifo_tdata; - wire dds_in_fifo_tvalid, dds_in_fifo_tready, dds_in_fifo_tlast; - wire dds_out_tlast, dds_out_tvalid, dds_out_tready; - - wire [DDS_WIDTH-1:0] dds_in_i_tdata, dds_in_q_tdata; - wire [DDS_WIDTH-1:0] dds_out_i_tdata, dds_out_q_tdata; - wire [15:0] dds_input_fifo_space, dds_input_fifo_occupied; - - wire [WIDTH*2-1:0] dds_in_sync_tdata; - wire dds_in_sync_tvalid, dds_in_sync_tready, dds_in_sync_tlast; - wire [PHASE_WIDTH-1:0] phase_sync_tdata; - wire phase_sync_tvalid, phase_sync_tready, phase_sync_tlast; - - assign phase_inc_mux_tdata = phase_inc_timed_tready ? phase_inc_timed_tdata : phase_inc_tdata; - assign phase_inc_mux_tlast = phase_inc_timed_tready ? phase_inc_timed_tlast : phase_inc_tlast; + reg [PHASE_ACCUM_WIDTH-1:0] phase_inc; + wire phase_inc_mux_tlast; + wire phase_inc_mux_tvalid; + wire phase_inc_mux_tready; + reg [PHASE_ACCUM_WIDTH-1:0] phase; + + wire [PHASE_WIDTH-1:0] phase_tdata = phase[PHASE_ACCUM_WIDTH-1:PHASE_ACCUM_WIDTH-PHASE_WIDTH]; + wire phase_tvalid; + wire phase_tready; + wire phase_tlast; + + wire dds_in_teob = dds_in_tuser[124]; + + // Multiplexer to select between the timed and un-timed phase registers. + assign phase_inc_mux_tdata = phase_inc_timed_tready ? phase_inc_timed_tdata : phase_inc_tdata; + assign phase_inc_mux_tlast = phase_inc_timed_tready ? phase_inc_timed_tlast : phase_inc_tlast; assign phase_inc_mux_tvalid = phase_inc_timed_tready ? phase_inc_timed_tvalid : phase_inc_tvalid; assign phase_inc_tready = phase_inc_mux_tready; assign phase_inc_timed_tready = phase_inc_mux_tready & dds_in_tag; - assign phase_inc_mux_tready = phase_tready; + assign phase_inc_mux_tready = phase_tready; - // phase is only valid when input i/q data stream is valid + // Phase is only valid when input IQ data stream is valid assign phase_tvalid = dds_in_tvalid; - assign phase_tlast = dds_in_tlast; + assign phase_tlast = dds_in_tlast; + // Phase increment register, sourced from either the timed or un-timed + // SR_FREQ register. always @(posedge clk) begin if (reset | clear) begin phase_inc <= 0; @@ -162,129 +366,219 @@ module dds_timed #( phase_inc <= phase_inc_mux_tdata; end end - - // NCO, increment phase input to DDS SIN/COS LUT + + // Phase accumulator for DDS. This increments the "phase" input provided to + // the DDS IP. always @(posedge clk) begin - if (reset | clear | (phase_inc_mux_tvalid & phase_inc_mux_tready) | eob) begin + if (reset | clear | (phase_inc_mux_tvalid & phase_inc_mux_tready)) begin + // Reset the phase on reset or clear, but also whenever the phase + // increment is updated. phase <= 0; - end else if (dds_in_tvalid & dds_in_tready) begin //only increment phase when data into dds is valid and data fifo is ready - phase <= phase + phase_inc; + end else if (dds_in_tvalid & dds_in_tready) begin + if (dds_in_tlast & dds_in_teob) begin + // Reset the phase at the end of each burst so we get predictable + // output. + phase <= 0; + end else begin + // Increment the phase for each new sample. + phase <= phase + phase_inc; + end end end + //--------------------------------------------------------------------------- + // AXI Sync + //--------------------------------------------------------------------------- + // + // Sync the IQ and phase paths' pipeline delay. This is needed to ensure that + // applying the phase update happens on the correct sample regardless of + // differences in path delays. + // + //--------------------------------------------------------------------------- + + + wire [PHASE_WIDTH-1:0] phase_sync_tdata; + wire phase_sync_tvalid; + wire phase_sync_tready; + wire phase_sync_tlast; + + wire [ WIDTH*2-1:0] dds_in_sync_tdata; + wire dds_in_sync_tvalid; + wire dds_in_sync_tready; + wire dds_in_sync_tlast; - // Sync the two path's pipeline delay. - // This is needed to ensure that applying the phase update happens on the - // correct sample regardless of differing downstream path delays. axi_sync #( - .SIZE(2), - .WIDTH_VEC({PHASE_WIDTH,2*WIDTH}), // Vector of widths, each width is defined by a 32-bit value - .FIFO_SIZE(0)) - axi_sync ( - .clk(clk), .reset(reset), .clear(clear), - .i_tdata({phase_tdata,dds_in_tdata}), - .i_tlast({phase_tlast,dds_in_tlast}), - .i_tvalid({phase_tvalid,dds_in_tvalid}), - .i_tready({phase_tready,dds_in_tready}), - .o_tdata({phase_sync_tdata,dds_in_sync_tdata}), - .o_tlast({phase_sync_tlast,dds_in_sync_tlast}), - .o_tvalid({phase_sync_tvalid,dds_in_sync_tvalid}), - .o_tready({phase_sync_tready,dds_in_sync_tready})); - - // fifo to hold input data while pipeline catches up in dds - // this is blocked by the axi_sync following the dds - axi_fifo #(.WIDTH(2*WIDTH+1), .SIZE(5)) dds_input_fifo( - .clk(clk), .reset(reset), .clear(clear), - .i_tdata({dds_in_sync_tlast,dds_in_sync_tdata}), .i_tvalid(dds_in_sync_tvalid), .i_tready(dds_in_sync_tready), - .o_tdata({dds_in_fifo_tlast,dds_in_fifo_tdata}), .o_tvalid(dds_in_fifo_tvalid), .o_tready(dds_in_fifo_tready), - .space(dds_input_fifo_space), .occupied(dds_input_fifo_occupied) + .SIZE (2), + .WIDTH_VEC ({ PHASE_WIDTH, 2*WIDTH }), // Vector of 32-bit width values + .FIFO_SIZE (0) + ) axi_sync_i ( + .clk (clk), + .reset (reset), + .clear (clear), + .i_tdata ({ phase_tdata, dds_in_tdata }), + .i_tlast ({ phase_tlast, dds_in_tlast }), + .i_tvalid ({ phase_tvalid, dds_in_tvalid }), + .i_tready ({ phase_tready, dds_in_tready }), + .o_tdata ({ phase_sync_tdata, dds_in_sync_tdata }), + .o_tlast ({ phase_sync_tlast, dds_in_sync_tlast }), + .o_tvalid ({ phase_sync_tvalid, dds_in_sync_tvalid }), + .o_tready ({ phase_sync_tready, dds_in_sync_tready }) ); - - // after fifo, do q quick sign extend op to get up to 24 bits. to match how the cordic deals with the data path. - sign_extend #( - .bits_in(WIDTH), .bits_out(DDS_WIDTH)) - sign_extend_dds_i ( - .in(dds_in_fifo_tdata[2*WIDTH-1:WIDTH]), .out(dds_in_i_tdata)); + + //--------------------------------------------------------------------------- + // DDS and Complex Multiplier + //--------------------------------------------------------------------------- + + wire [DDS_WIDTH-1:0] dds_in_i_tdata; + wire [DDS_WIDTH-1:0] dds_in_q_tdata; + + wire [DDS_WIDTH-1:0] dds_out_i_tdata; + wire [DDS_WIDTH-1:0] dds_out_q_tdata; + wire dds_out_tlast; + wire dds_out_tvalid; + wire dds_out_tready; + + // Sign extend I and Q to get up to 24 bits. sign_extend #( - .bits_in(WIDTH), .bits_out(DDS_WIDTH)) - sign_extend_dds_q ( - .in(dds_in_fifo_tdata[WIDTH-1:0]), .out(dds_in_q_tdata)); - - - // Wrapper for Xilinx IP AXI DDS + Complex Multiply - // NOTE: Seems Xilinx IP expects opposite I/Q combined complex data buses, so they are swapped here. - dds_freq_tune dds_freq_tune_inst ( - .clk(clk), - .reset(reset | clear), - .eob(eob), - .rate_changed(1'b0), - .dds_input_fifo_occupied(dds_input_fifo_occupied), - /* IQ input */ - .s_axis_din_tlast(dds_in_fifo_tlast), - .s_axis_din_tvalid(dds_in_fifo_tvalid), - .s_axis_din_tready(dds_in_fifo_tready), - .s_axis_din_tdata({dds_in_q_tdata, dds_in_i_tdata}), - /* Phase input from NCO */ - .s_axis_phase_tlast(phase_sync_tlast), + .bits_in (WIDTH), + .bits_out (DDS_WIDTH) + ) sign_extend_i ( + .in (dds_in_sync_tdata[2*WIDTH-1:WIDTH]), + .out (dds_in_i_tdata) + ); + sign_extend #( + .bits_in (WIDTH), + .bits_out (DDS_WIDTH) + ) sign_extend_q ( + .in (dds_in_sync_tdata[WIDTH-1:0]), + .out (dds_in_q_tdata) + ); + + // Wrapper for DDS + Complex Multiply. This block expects {q,i} instead of + // {i,q} data ordering. + dds_freq_tune_duc dds_freq_tune_duc_i ( + .clk (clk), + .reset (reset | clear), + // IQ input (signed 24-bit number with 15 fractional bits) + .s_axis_din_tlast (dds_in_sync_tlast), + .s_axis_din_tvalid (dds_in_sync_tvalid), + .s_axis_din_tready (dds_in_sync_tready), + .s_axis_din_tdata ({ dds_in_q_tdata, dds_in_i_tdata }), + // Phase input from DDS (unsigned 24-bit number with 24 fractional bits) + .s_axis_phase_tlast (phase_sync_tlast), .s_axis_phase_tvalid(phase_sync_tvalid), .s_axis_phase_tready(phase_sync_tready), - .s_axis_phase_tdata(phase_sync_tdata), //24 bit - /* IQ output */ - .m_axis_dout_tlast(dds_out_tlast), - .m_axis_dout_tvalid(dds_out_tvalid), - .m_axis_dout_tready(dds_out_tready), - .m_axis_dout_tdata({dds_out_q_tdata, dds_out_i_tdata}) - //debug signals + .s_axis_phase_tdata (phase_sync_tdata), // 24-bit + // IQ output (signed 24-bit number with 15 fractional bits) + .m_axis_dout_tlast (dds_out_tlast), + .m_axis_dout_tvalid (dds_out_tvalid), + .m_axis_dout_tready (dds_out_tready), + .m_axis_dout_tdata ({dds_out_q_tdata, dds_out_i_tdata}) ); - /************************************************************************ - * Perform scaling on the IQ output - ************************************************************************/ - wire [DDS_WIDTH+SCALING_WIDTH-1:0] scaled_i_tdata, scaled_q_tdata; - wire scaled_tlast, scaled_tvalid, scaled_tready; + + + //--------------------------------------------------------------------------- + // Scale the IQ Output + //--------------------------------------------------------------------------- + + wire [DDS_WIDTH+SCALING_WIDTH-1:0] scaled_i_tdata; + wire [DDS_WIDTH+SCALING_WIDTH-1:0] scaled_q_tdata; + wire scaled_tlast; + wire scaled_tvalid; + wire scaled_tready; mult #( - .WIDTH_A(DDS_WIDTH), - .WIDTH_B(SCALING_WIDTH), - .WIDTH_P(DDS_WIDTH+SCALING_WIDTH), - .DROP_TOP_P(4), - .LATENCY(3), - .CASCADE_OUT(0)) - i_mult ( - .clk(clk), .reset(reset | clear), - .a_tdata(dds_out_i_tdata), .a_tlast(dds_out_tlast), .a_tvalid(dds_out_tvalid), .a_tready(dds_out_tready), - .b_tdata(scaling_tdata), .b_tlast(1'b0), .b_tvalid(dds_out_tvalid /* aligning scaling_tdata with dds_tdata */), .b_tready(scaling_tready), - .p_tdata(scaled_i_tdata), .p_tlast(scaled_tlast), .p_tvalid(scaled_tvalid), .p_tready(scaled_tready)); + .WIDTH_A (DDS_WIDTH), + .WIDTH_B (SCALING_WIDTH), + .WIDTH_P (DDS_WIDTH+SCALING_WIDTH), + .DROP_TOP_P (4), + .LATENCY (3), + .CASCADE_OUT (0) + ) mult_i ( + .clk (clk), + .reset (reset | clear), + .a_tdata (dds_out_i_tdata), + .a_tlast (dds_out_tlast), + .a_tvalid (dds_out_tvalid), + .a_tready (dds_out_tready), + .b_tdata (scaling_tdata), + .b_tlast (1'b0), + .b_tvalid (dds_out_tvalid), // Align scaling_tdata with dds_out_tdata + .b_tready (scaling_tready), + .p_tdata (scaled_i_tdata), + .p_tlast (scaled_tlast), + .p_tvalid (scaled_tvalid), + .p_tready (scaled_tready) + ); mult #( - .WIDTH_A(DDS_WIDTH), - .WIDTH_B(SCALING_WIDTH), - .WIDTH_P(DDS_WIDTH+SCALING_WIDTH), - .DROP_TOP_P(4), - .LATENCY(3), - .CASCADE_OUT(0)) - q_mult ( - .clk(clk), .reset(reset | clear), - .a_tdata(dds_out_q_tdata), .a_tlast(), .a_tvalid(dds_out_tvalid), .a_tready(), - .b_tdata(scaling_tdata), .b_tlast(1'b0), .b_tvalid(dds_out_tvalid /* aligning scaling_tdata with dds_tdata */), .b_tready(), - .p_tdata(scaled_q_tdata), .p_tlast(), .p_tvalid(), .p_tready(scaled_tready)); + .WIDTH_A (DDS_WIDTH), + .WIDTH_B (SCALING_WIDTH), + .WIDTH_P (DDS_WIDTH+SCALING_WIDTH), + .DROP_TOP_P (4), + .LATENCY (3), + .CASCADE_OUT (0) + ) mult_q ( + .clk (clk), + .reset (reset | clear), + .a_tdata (dds_out_q_tdata), + .a_tlast (), + .a_tvalid (dds_out_tvalid), + .a_tready (), + .b_tdata (scaling_tdata), + .b_tlast (1'b0), + .b_tvalid (dds_out_tvalid), // Align scaling_tdata with dds_out_tdata + .b_tready (), + .p_tdata (scaled_q_tdata), + .p_tlast (), + .p_tvalid (), + .p_tready (scaled_tready) + ); + + + //--------------------------------------------------------------------------- + // Round + //--------------------------------------------------------------------------- wire [2*WIDTH-1:0] sample_tdata; - wire sample_tlast, sample_tvalid, sample_tready; + wire sample_tlast; + wire sample_tvalid; + wire sample_tready; axi_round_and_clip_complex #( - .WIDTH_IN(DDS_WIDTH+SCALING_WIDTH), .WIDTH_OUT(WIDTH), .CLIP_BITS(12)) - axi_round_and_clip_complex ( - .clk(clk), .reset(reset | clear), - .i_tdata({scaled_i_tdata, scaled_q_tdata}), .i_tlast(scaled_tlast), .i_tvalid(scaled_tvalid), .i_tready(scaled_tready), - .o_tdata(sample_tdata), .o_tlast(sample_tlast), .o_tvalid(sample_tvalid), .o_tready(sample_tready)); + .WIDTH_IN (DDS_WIDTH+SCALING_WIDTH), + .WIDTH_OUT (WIDTH), + .CLIP_BITS (12) + ) axi_round_and_clip_complex_i ( + .clk (clk), + .reset (reset | clear), + .i_tdata ({scaled_i_tdata, scaled_q_tdata}), + .i_tlast (scaled_tlast), + .i_tvalid (scaled_tvalid), + .i_tready (scaled_tready), + .o_tdata (sample_tdata), + .o_tlast (sample_tlast), + .o_tvalid (sample_tvalid), + .o_tready (sample_tready) + ); + + + //--------------------------------------------------------------------------- + // Output Logic + //--------------------------------------------------------------------------- // Throttle output on last sample if header is not valid assign header_out_tready = sample_tlast & sample_tvalid & o_tready; - assign sample_tready = (sample_tvalid & sample_tlast) ? (header_out_tvalid & o_tready) : o_tready; - assign o_tvalid = (sample_tvalid & sample_tlast) ? header_out_tvalid : sample_tvalid; + assign sample_tready = (sample_tvalid & sample_tlast) ? + (header_out_tvalid & o_tready) : o_tready; + assign o_tvalid = (sample_tvalid & sample_tlast) ? + header_out_tvalid : sample_tvalid; assign o_tlast = sample_tlast; assign o_tdata = sample_tdata; assign o_tuser = header_out_tdata; - + endmodule + + +`default_nettype wire diff --git a/fpga/usrp3/lib/rfnoc/dds_wrapper.v b/fpga/usrp3/lib/rfnoc/dds_wrapper.v new file mode 100644 index 000000000..d9151afa8 --- /dev/null +++ b/fpga/usrp3/lib/rfnoc/dds_wrapper.v @@ -0,0 +1,160 @@ +// +// Copyright 2021 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Module: dds_wrapper +// +// Description: +// +// This module computes the complex number e^(j*2*pi*phase). The phase input +// can be thought of as a 24-bit unsigned fixed-point value with 24 +// fractional bits. In other words, the integer range of the input maps to a +// phase in the range [0, 1.0). The output consists of two 16-bit signed +// fixed-point values with 14 fractional bits. The value sin(2*pi*phase) is +// in the upper 16 bits and cos(2*pi*phase) is in the lower 16-bits. This +// puts the output in {Q,I} order. +// +// This is a wrapper for the dds_sin_cos_lut_only IP, which is based on the +// Xilinx DDS Compiler. This IP has the undesirable behavior that input must +// be provided to flush out any data stuck in its pipeline. This wrapper +// hides that behavior so that every input causes a corresponding output, +// even if the input stops. +// +// NOTE: The DDS IP requires at least 2 cycles of reset. +// +// Parameters: +// +// The parameters in this module should not be modified. They match the IP +// configuration. +// + + +module dds_wrapper #( + parameter PHASE_W = 24, + parameter OUTPUT_W = 32 +) ( + input wire clk, + input wire rst, + + // Phase input + input wire [ PHASE_W-1:0] s_axis_phase_tdata, + input wire s_axis_phase_tvalid, + input wire s_axis_phase_tlast, + output wire s_axis_phase_tready, + + // IQ output (Q in the upper, I in the lower bits) + output wire [OUTPUT_W-1:0] m_axis_data_tdata, + output wire m_axis_data_tvalid, + output wire m_axis_data_tlast, + input wire m_axis_data_tready +); + + // Width of number needed to represent the DDS fullness. This value was + // determined experimentally. The max fullness was 33. + localparam FULLNESS_W = 6; + + wire [PHASE_W-1:0] phase_tdata; + wire phase_tvalid; + wire phase_tlast; + wire phase_tready; + + wire [OUTPUT_W-1:0] dds_tdata; + wire dds_tvalid; + wire dds_tlast; + wire dds_tready; + + + //--------------------------------------------------------------------------- + // DDS Fullness Counter + //--------------------------------------------------------------------------- + // + // Count the number of valid samples in the DDS's data pipeline. + // + //--------------------------------------------------------------------------- + + // The fullness counter must be large enough for DDS's latency. + reg [FULLNESS_W-1:0] fullness = 0; + reg dds_has_data = 0; + + wire increment = s_axis_phase_tvalid & s_axis_phase_tready; + wire decrement = m_axis_data_tvalid & m_axis_data_tready; + + always @(posedge clk) begin + if (rst) begin + fullness <= 0; + dds_has_data <= 0; + end else begin + if (increment && !decrement) begin + //synthesis translate_off + if (fullness+1'b1 == 1'b0) begin + $display("ERROR: Fullness overflowed!"); + end + //synthesis translate_on + fullness <= fullness + 1; + dds_has_data <= 1; + end else if (decrement && !increment) begin + //synthesis translate_off + if (fullness-1'b1 > fullness) begin + $display("ERROR: Fullness underflowed!"); + end + //synthesis translate_on + fullness <= fullness - 1; + dds_has_data <= (fullness > 1); + end else begin + dds_has_data <= (fullness > 0); + end + end + end + + + //--------------------------------------------------------------------------- + // Input Logic + //--------------------------------------------------------------------------- + + assign s_axis_phase_tready = phase_tready; + assign phase_tlast = s_axis_phase_tlast; + assign phase_tdata = s_axis_phase_tdata; + + // Always input something when the DDS has data stuck inside it so that all + // data gets flushed out automatically. + assign phase_tvalid = s_axis_phase_tvalid || dds_has_data; + + + //--------------------------------------------------------------------------- + // DDS IP + //--------------------------------------------------------------------------- + + // Use the TUSER path on the DDS IP to indicate if the sample is empty and is + // just to flush the output. + wire flush_in = ~s_axis_phase_tvalid; // It's a flush if input is not valid + wire flush_out; + + dds_sin_cos_lut_only dds_sin_cos_lut_only_i ( + .aclk (clk), + .aresetn (~rst), + .s_axis_phase_tvalid (phase_tvalid), + .s_axis_phase_tready (phase_tready), + .s_axis_phase_tdata (phase_tdata), + .s_axis_phase_tlast (phase_tlast), + .s_axis_phase_tuser (flush_in), + .m_axis_data_tvalid (dds_tvalid), + .m_axis_data_tready (dds_tready), + .m_axis_data_tdata (dds_tdata), + .m_axis_data_tlast (dds_tlast), + .m_axis_data_tuser (flush_out) + ); + + + //--------------------------------------------------------------------------- + // Output Logic + //--------------------------------------------------------------------------- + + assign m_axis_data_tdata = dds_tdata; + assign m_axis_data_tlast = dds_tlast; + + // Discard the current sample if it was for flushing. + assign m_axis_data_tvalid = dds_tvalid & ~flush_out; + assign dds_tready = m_axis_data_tready | flush_out; + +endmodule diff --git a/fpga/usrp3/lib/rfnoc/sim/dds_timed_tb/Makefile b/fpga/usrp3/lib/rfnoc/sim/dds_timed_tb/Makefile new file mode 100644 index 000000000..22dd93ecb --- /dev/null +++ b/fpga/usrp3/lib/rfnoc/sim/dds_timed_tb/Makefile @@ -0,0 +1,68 @@ +# +# Copyright 2021 Ettus Research, A National Instruments Brand +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# + +#------------------------------------------------- +# Top-of-Makefile +#------------------------------------------------- +# Define BASE_DIR to point to the "top" dir +BASE_DIR = $(abspath ../../../../top) +# Include viv_sim_preamble after defining BASE_DIR +include $(BASE_DIR)/../tools/make/viv_sim_preamble.mak + +#------------------------------------------------- +# Design Specific +#------------------------------------------------- +# Include makefiles and sources for the DUT and its dependencies +include $(BASE_DIR)/../lib/rfnoc/Makefile.srcs + +DESIGN_SRCS += $(abspath \ +$(RFNOC_SRCS) \ +) + +#------------------------------------------------- +# IP Specific +#------------------------------------------------- +# If simulation contains IP, define the IP_DIR and point +# it to the base level IP directory. +IP_DIR = $(BASE_DIR)/x300/ip + +# Include makefiles and sources for all IP components +# *after* defining the IP_DIR +include $(LIB_IP_DIR)/complex_multiplier_dds/Makefile.inc +include $(LIB_IP_DIR)/dds_sin_cos_lut_only/Makefile.inc + +DESIGN_SRCS += $(abspath \ +$(LIB_IP_COMPLEX_MULTIPLIER_DDS_SRCS) \ +$(LIB_IP_DDS_SIN_COS_LUT_ONLY_SRCS) \ +) + +#------------------------------------------------- +# ModelSim Specific +#------------------------------------------------- + +modelsim vlint : DESIGN_SRCS += $(abspath \ +$(IP_BUILD_DIR)/dds_sin_cos_lut_only/sim/dds_sin_cos_lut_only.vhd \ +$(IP_BUILD_DIR)/complex_multiplier_dds/sim/complex_multiplier_dds.vhd \ +) + +MODELSIM_ARGS = glbl + +#------------------------------------------------- +# Testbench Specific +#------------------------------------------------- +SIM_TOP ?= dds_timed_tb + +SIM_SRCS = \ +$(abspath $(SIM_TOP).sv) \ +$(VIVADO_PATH)/data/verilog/src/glbl.v \ + +#------------------------------------------------- +# Bottom-of-Makefile +#------------------------------------------------- +# Include all simulator specific makefiles here +# Each should define a unique target to simulate +# e.g. xsim, vsim, etc and a common "clean" target +include $(BASE_DIR)/../tools/make/viv_simulator.mak diff --git a/fpga/usrp3/lib/rfnoc/sim/dds_timed_tb/dds_timed_tb.sv b/fpga/usrp3/lib/rfnoc/sim/dds_timed_tb/dds_timed_tb.sv new file mode 100644 index 000000000..c9cc29edd --- /dev/null +++ b/fpga/usrp3/lib/rfnoc/sim/dds_timed_tb/dds_timed_tb.sv @@ -0,0 +1,886 @@ +// +// Copyright 2021 Ettus Research, A National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Module: dds_timed_tb +// + +`default_nettype none + + +module dds_timed_tb; + + // Include macros and time declarations for use with PkgTestExec + `include "test_exec.svh" + + import PkgTestExec::*; + import PkgAxiStreamBfm::*; + import PkgComplex::*; + import PkgMath::*; + import PkgRandom::*; + + //--------------------------------------------------------------------------- + // Testbench Configuration + //--------------------------------------------------------------------------- + + localparam real CLK_PERIOD = 10.0; + + // Values needed by the DUT (use the same values as the DUC) + localparam int SR_FREQ_ADDR = 132; + localparam int SR_SCALE_IQ_ADDR = 133; + localparam int SR_AWIDTH = 8; + localparam int SR_DWIDTH = 32; + localparam int SR_TWIDTH = 64; + localparam int PHASE_ACCUM_WIDTH = 32; + localparam int SCALING_WIDTH = 18; + + // Bit widths for our sample size + localparam int FRAC_W = 15; // Number of fixed point fractional bits + localparam int COMP_W = 16; // Width of just the imag/real part + localparam int SAMPLE_W = 2*COMP_W; // Width of a complex sample + + // Max min possible values for the components of a sample + localparam bit signed [COMP_W-1:0] MAX_COMP = 2**(COMP_W-1) - 1; + localparam bit signed [COMP_W-1:0] MIN_COMP = -2**(COMP_W-1); + + // Max/min possible values for the scale register + localparam real MAX_SCALE = +(2**(SCALING_WIDTH-1) - 1) / (2.0**FRAC_W); + localparam real MIN_SCALE = -(2**(SCALING_WIDTH-1) ) / (2.0**FRAC_W); + + // TUSER bit positions + localparam int HAS_TIME_POS = 125; + localparam int EOB_POS = 124; + localparam int TIMESTAMP_POS = 0; + + // AXI-Stream data bus parameters + localparam int DATA_W = SAMPLE_W; + localparam int USER_W = 128; + + // Amount of rounding error to allow (in ULPs). We generally expect the error + // in computation to be +/- 1 ULP, but since the DUT performs several + // computations, error can accumulate. The testbench computations also + // introduce some error. In particular, using the scale register in the DUT + // also scales the error. The MAX_ERROR can be reduced to 2 if you keep the + // scale register < 1.0. + localparam MAX_ERROR = 8; + + + //--------------------------------------------------------------------------- + // Type Definitions + //--------------------------------------------------------------------------- + + // Burst test configuration + typedef struct { + int spp; // Samples per packet to generate. + int spp_last; // Length of last packet, if different from SPP. + // Set to -1 to use spp value. + int num_packets; // Number of packets in each burst. + int num_bursts; // Number of bursts to send. + real amp; // Amplitude of the test signal to generate. + real freq; // Normalized frequency of test signal to generate. + bit timed; // Set to 1 for timed packet, 0 for non-timed. If + // doing a timed tune, this must be 1. + real scale; // Scale value to use, in the range [-4,4). + real freq_shift; // Initial frequency shift to use. + real tune_freq_shift; // New frequency shift to tune to. + longint tune_time; // Time after which to tune the frequency (set a + // new frequency shift). Set to -1 to disable. + } burst_cfg_t; + + typedef AxiStreamPacket #(.DATA_WIDTH(DATA_W), .USER_WIDTH(USER_W)) axis_pkt_t; + typedef axis_pkt_t axis_pkt_queue_t[$]; + + // Default settings to use for burst_cfg_t. This creates a nice complex + // sinusoid and the output should match the input, unchanged. + localparam burst_cfg_t DEFAULT_BURST_CFG = '{ + spp : 256, + spp_last : -1, + num_packets : 1, + num_bursts : 1, + freq : 1.0/16.0, + amp : 0.75, + timed : 1, + scale : 1.0, + freq_shift : 0.0, + tune_freq_shift : 0.0, + tune_time : -1.0 + }; + + + //--------------------------------------------------------------------------- + // Clocks and Resets + //--------------------------------------------------------------------------- + + bit clk, rst; + + sim_clock_gen #(CLK_PERIOD) clk_gen (clk, rst); + + + //--------------------------------------------------------------------------- + // AXI-Stream BFM + //--------------------------------------------------------------------------- + + // AXI-Stream interfaces to/from DUT + AxiStreamIf #(.DATA_WIDTH(DATA_W), .USER_WIDTH(USER_W), .TKEEP(0)) + to_dut (clk, rst); + AxiStreamIf #(.DATA_WIDTH(DATA_W), .USER_WIDTH(USER_W), .TKEEP(0)) + from_dut (clk, rst); + + // BFM for the AXI-Stream interface to DUT + AxiStreamBfm #(.DATA_WIDTH(DATA_W), .USER_WIDTH(USER_W), .TKEEP(0)) + axis_bfm = new(to_dut, from_dut); + + + //--------------------------------------------------------------------------- + // DUT + //--------------------------------------------------------------------------- + + logic clear = 1'b0; + logic timed_cmd_fifo_full; + logic set_stb = 1'b0; + logic [SR_AWIDTH-1:0] set_addr; + logic [SR_DWIDTH-1:0] set_data; + logic [SR_TWIDTH-1:0] set_time; + logic set_has_time; + logic [ SAMPLE_W-1:0] i_tdata; + logic i_tlast; + logic i_tvalid; + logic i_tready; + logic [ USER_W-1:0] i_tuser; + logic [ SAMPLE_W-1:0] o_tdata; + logic o_tlast; + logic o_tvalid; + logic o_tready; + logic [ USER_W-1:0] o_tuser; + + dds_timed #( + .SR_FREQ_ADDR (SR_FREQ_ADDR), + .SR_SCALE_IQ_ADDR (SR_SCALE_IQ_ADDR), + .PHASE_ACCUM_WIDTH (PHASE_ACCUM_WIDTH), + .SCALING_WIDTH (SCALING_WIDTH), + .SR_AWIDTH (SR_AWIDTH), + .SR_DWIDTH (SR_DWIDTH), + .SR_TWIDTH (SR_TWIDTH) + ) dds_timed_i ( + .clk (clk), + .reset (rst), + .clear (clear), + .timed_cmd_fifo_full (timed_cmd_fifo_full), + .set_stb (set_stb), + .set_addr (set_addr), + .set_data (set_data), + .set_time (set_time), + .set_has_time (set_has_time), + .i_tdata (to_dut.tdata), + .i_tlast (to_dut.tlast), + .i_tvalid (to_dut.tvalid), + .i_tready (to_dut.tready), + .i_tuser (to_dut.tuser), + .o_tdata (from_dut.tdata), + .o_tlast (from_dut.tlast), + .o_tvalid (from_dut.tvalid), + .o_tready (from_dut.tready), + .o_tuser (from_dut.tuser) + ); + + + //--------------------------------------------------------------------------- + // Timer + //--------------------------------------------------------------------------- + // + // Count the samples going into the DUT so we have something that tracks + // packet timestamp in the testbench. + // + //--------------------------------------------------------------------------- + + longint current_time = 0; + + always @(posedge clk) begin + if (to_dut.tvalid && to_dut.tready) begin + current_time <= current_time + 1; + end + end + + + //--------------------------------------------------------------------------- + // Expected Output + //--------------------------------------------------------------------------- + // + // This assigns the expected output to a signal so we can visualize what the + // testbench is expecting. Error checking isn't done here. This is only to + // aid in debug. + // + //--------------------------------------------------------------------------- + + mailbox #(axis_pkt_t) exp_pkts_mb = new(); + bit exp_data_mismatch = 0; + bit exp_user_mismatch = 0; + + logic [SAMPLE_W-1:0] exp_tdata; + logic [ USER_W-1:0] exp_tuser; + + always @(posedge clk) begin + if (rst) begin + exp_tdata = 'X; + exp_tuser = 'X; + end else begin + static axis_pkt_t exp_pkt = null; + static bit out_valid = 0; + + // Give time for the DUT to update its status, so we know what to do. + #(0.01ns); + + // Output the next expected sample if we haven't done so already + if (from_dut.tvalid && !out_valid) begin + int rval; + + // Get the next packet from the mailbox if needed + if (exp_pkt == null) begin + rval = exp_pkts_mb.try_get(exp_pkt); + `ASSERT_ERROR(rval, "Couldn't get first packet from exp_pkts_mb."); + end else if (exp_pkt.data.size() == 0) begin + rval = exp_pkts_mb.try_get(exp_pkt); + `ASSERT_ERROR(rval, "Couldn't get next packet from exp_pkts_mb."); + end + + // Output the next sample + `ASSERT_ERROR(exp_pkt.data.size(), "exp_pkt.data is empty"); + exp_tdata = exp_pkt.data.pop_front(); + `ASSERT_ERROR(exp_pkt.user.size(), "exp_pkt.user is empty"); + exp_tuser = exp_pkt.user.pop_front(); + out_valid = 1; + end + + exp_data_mismatch = compare_samples(exp_tdata, from_dut.tdata); + exp_user_mismatch = compare_samples(exp_tuser, from_dut.tuser); + + // Check if the output has been accepted and needs to update + if (from_dut.tvalid && from_dut.tready) begin + out_valid = 0; + end + end + end + + + //--------------------------------------------------------------------------- + // Helper Functions + //--------------------------------------------------------------------------- + + // Round a floating point number to num_bits bits of precision. + function automatic real round_bits(real num, int num_bits); + return real'(longint'(num * (2.0**num_bits))) / (2.0**num_bits); + endfunction : round_bits + + + // Compare the samples a and b to see if either component differs by more + // than MAX_ERROR. + function automatic bit compare_samples(sc16_t a, sc16_t b); + Math #(s16_t) m; + sc16_t diff; + diff = sub_sc16(a, b); + if (m.abs(diff.re) > MAX_ERROR || m.abs(diff.im) > MAX_ERROR) return 1; + return 0; + endfunction : compare_samples + + + // Compare the packets, sample by sample. Returns a string error message + // explaining the nature of the mismatch. If packets match, an empty string + // is returned. + function automatic string compare_packets(axis_pkt_t actual, axis_pkt_t expected); + if (actual.data.size() != expected.data.size()) begin + return $sformatf("Packet lengths do not match. Actual is %0d, expected is %0d.", + actual.data.size(), expected.data.size()); + end + + foreach(actual.data[i]) begin + sc16_t a, b; + + // Check the samples in TDATA. + // Calculate the difference between the actual end expected values. + a = actual.data[i]; + b = expected.data[i]; + if (compare_samples(a, b)) begin + `ASSERT_WARNING(0, "compare_packets: Skipping rest of packet due to mismatch.") + return $sformatf("Word %0d in packet TDATA does not match. Actual is 0x%X, expected is 0x%X.", + i, actual.data[i], expected.data[i]); + end + + // Check TUSER. This is only guaranteed to be valid on the last sample of + // each packet due to the way it's currently implemented. + if (i == actual.data.size()-1 && actual.user[i] != expected.user[i]) begin + string fields; + if (actual.user[i][EOB_POS] != expected.user[i][EOB_POS]) begin + fields = {fields, "(EOB)"}; + end + if (actual.user[i][HAS_TIME_POS] != expected.user[i][HAS_TIME_POS]) begin + fields = {fields, "(HAS_TIME)"}; + end + if (actual.user[i][TIMESTAMP_POS+:64] != expected.user[i][TIMESTAMP_POS+:64]) begin + fields = {fields, "(TIMESTAMP)"}; + end + if (fields == "") fields = "<None>"; + `ASSERT_WARNING(0, "compare_packets: Skipping rest of packet due to mismatch.") + return $sformatf({ + "Word %0d in packet TUSER does not match. ", + "Fields not matching: %s. ", + "Actual is %X, expected is %X."}, + i, fields, actual.user[i], expected.user[i]); + end + end + // Return empty string if all is well + return ""; + endfunction : compare_packets + + + // Generate a test packet containing a complex sinusoid signal e^(j∙2π∙f∙t) + // and return it. + // + // length: The length of the packet to generate in samples. + // freq: Normalized frequency of the signal to generate. + // eob: EOB flag for the packet. + // timed: Set to 1 for a timed packet, 0 for non-timed. Timed is the + // default. + // timestamp: Timestamp for the first packet. Leave at the default value + // to continue from the time of the previous packet. + // init: Initial phase value to use (t in e^jt). Leave at the default + // value to use the last value of the previous packet. Must be + // in the range [0,1), where 1.0 corresponds to 2*pi radians. + // + function automatic axis_pkt_t gen_test_packet( + int length, + real freq, + real amp = 0.75, + bit eob = 0, + longint timed = 1, + longint timestamp = -1, + real init = -1.0 + ); + static real phase; + static longint next_time = 0; + bit signed [COMP_W-1:0] re, im; + int re_int, im_int; + logic [USER_W-1:0] user; + axis_pkt_t packet; + + if (init != -1.0) begin + phase = init; + end + + if (timestamp >= 0) begin + next_time = timestamp; + end + + packet = new(); + for (int sample_num = 0; sample_num < length; sample_num++) begin + // Calculate I/Q + re_int = $cos(phase*TAU) * amp * 2**FRAC_W; + im_int = $sin(phase*TAU) * amp * 2**FRAC_W; + + // Saturate + if(re_int > MAX_COMP) re = MAX_COMP; + else if(re_int < MIN_COMP) re = MIN_COMP; + else re = re_int; + if(im_int > MAX_COMP) im = MAX_COMP; + else if(im_int < MIN_COMP) im = MIN_COMP; + else im = im_int; + + // Calculate TUSER (header) + user = '0; + user[EOB_POS] = eob; + user[HAS_TIME_POS] = timed; + user[TIMESTAMP_POS +: 64] = timed ? next_time : 'X; + + // Enqueue the sample + packet.data.push_back({re, im}); + packet.user.push_back(user); + phase += freq; + end + + // Calculate the timestamp for the next packet + next_time += length; + + return packet; + endfunction : gen_test_packet + + + // Apply a frequency shift to the packet data, by multiplying each sample by + // the output of a complex NCO. The implementation here models the HDL so + // that we don't accumulate error over time. + // + // packet : Input packet with the samples to frequency shift. + // freq : Normalized frequency shift to apply. + // reset_nco : If 1, reset the NCO to 0 before beginning. Otherwise + // continue from previous value. + // first_sample : First sample to frequency shift + // last_sample : Last sample to frequency shift (inclusive) + // + // Returns: A new packet with the frequency-shifted data. + // + function automatic axis_pkt_t freq_shift_pkt( + axis_pkt_t packet, + real freq, + bit reset_nco = 0, + int first_sample = 0, + int last_sample = -1 + ); + // Normalized phase angle in the range [0,1), corresponding to [0,2π) + // radians. + static bit [PHASE_ACCUM_WIDTH-1:0] phase = 0; + bit [PHASE_ACCUM_WIDTH-1:0] phase_inc; + axis_pkt_t new_packet; + + new_packet = packet.copy(); + + phase_inc = freq * (2.0**PHASE_ACCUM_WIDTH); + if (reset_nco) begin + phase = 0; + end + + if (packet == null) return null; + + last_sample = last_sample < 0 ? packet.data.size()-1 : last_sample; + for (int i = first_sample; i <= last_sample; i++) begin + // There are a lot of redundant variables in this loop. This was done to + // aid in debugging so we can correlate what's calculated here to what + // the DUT computes, and to have both fixed-point and floating point + // values. + sc16_t in_sc16, out_sc16; + complex_t nco; + complex_t in_c, out_c; + real phase_real; + + // Get the next input sample and convert it + in_sc16 = packet.data[i]; + in_c = sc16_to_complex(in_sc16); + + // Convert the phase + phase_real = real'(phase) / (2.0**PHASE_ACCUM_WIDTH); + + // Compute the new NCO value: nco = exp(j∙2π∙phase) + nco = polar_to_complex(1.0, TAU * phase_real); + + // Compute the new data output: sample_out = nco * sample_in + out_c = mul(nco, in_c); + out_sc16 = complex_to_sc16(out_c); + new_packet.data[i] = out_sc16; + + // Update the phase for the next iteration + phase = phase + phase_inc; + end + return new_packet; + endfunction : freq_shift_pkt + + // Return a scaled version of the input data packet. That is, where each + // sample is multiplied by scale. This models the precision provided by the + // scaler in the DUT. + function automatic axis_pkt_t scale_packet(axis_pkt_t packet, real scale); + bit [SAMPLE_W-1:0] sample; + bit signed [COMP_W-1:0] re, im, a, b; + int re_tmp, im_tmp; + axis_pkt_t new_packet; + + // Make sure scale is in the range supported by hardware + if (scale > MAX_SCALE) scale = MAX_SCALE; + else if (scale < MIN_SCALE) scale = MIN_SCALE; + + new_packet = packet.copy(); + foreach (packet.data[i]) begin + sample = packet.data[i]; + re = sample[1*COMP_W +: COMP_W]; + im = sample[0*COMP_W +: COMP_W]; + // Scale with full precision + re_tmp = re * scale; + im_tmp = im * scale; + // Saturate the values + if (re_tmp > MAX_COMP) re = MAX_COMP; + else if (re_tmp < MIN_COMP) re = MIN_COMP; + else re = re_tmp; + if (im_tmp > MAX_COMP) im = MAX_COMP; + else if (im_tmp < MIN_COMP) im = MIN_COMP; + else im = im_tmp; + new_packet.data[i] = { re, im }; + end + return new_packet; + endfunction : scale_packet + + // Generate the output packets we expect from the DUT given the provided + // burst of packets and configuration. + // + // cfg : Burst test configuration used + // packets : Queue of packets that were input to the DUT + // + // returns : Expected packets from DUT + // + function automatic axis_pkt_queue_t generate_expected( + burst_cfg_t cfg, + axis_pkt_queue_t packets + ); + static longint timestamp = 0; + axis_pkt_t expected[$]; + axis_pkt_t packet; + bit reset_nco; + int first_sample; + int last_sample; + real freq_shift; + + freq_shift = cfg.freq_shift; + + foreach(packets[i]) begin + // Make a copy of the input + packet = packets[i].copy(); + + // Check if we're supposed to tune the frequency in this packet + first_sample = 0; + if (cfg.timed && timestamp <= cfg.tune_time && + timestamp + packet.data.size() > cfg.tune_time) begin + last_sample = cfg.tune_time - timestamp; + end else begin + last_sample = -1; + end + + // Apply a frequency shift (reset the NCO before each burst) + reset_nco = i % cfg.num_packets == 0; + packet = freq_shift_pkt(packet, freq_shift, reset_nco, first_sample, last_sample); + + // If there was a tune, shift the rest of the packet differently + if (last_sample >= 0 && last_sample < packet.data.size()) begin + freq_shift = cfg.tune_freq_shift; + reset_nco = 1; + first_sample = last_sample + 1; + last_sample = -1; + packet = freq_shift_pkt(packet, freq_shift, reset_nco, first_sample, last_sample); + end + + // Multiply packet samples by a scaler + packet = scale_packet(packet, cfg.scale); + + // Add this packet to the queue + expected.push_back(packet); + + // Send this packet to the expected packets mailbox, for debug + `ASSERT_ERROR(exp_pkts_mb.try_put(packet.copy()), "Unable to put expected packet"); + + // Calculate new timestamp + timestamp += packet.data.size(); + end + + return expected; + endfunction : generate_expected + + + // Generate a queue of packets modeled after the burst test configuration + // defined by cfg. + function automatic axis_pkt_queue_t generate_bursts(burst_cfg_t cfg); + axis_pkt_t packets[$]; + + // Reset initial phase and time to 0 in generated packets by calling the + // generator with init and timestamp set to 0. + void'(gen_test_packet(.length(0), .freq(0), .init(0))); + + // Build the packets to send + for (int burst_num = 0; burst_num < cfg.num_bursts; burst_num++) begin + for (int packet_num = 0; packet_num < cfg.num_packets; packet_num++) begin + axis_pkt_t packet; + bit eob; + int length; + + // Set EOB and use spp_last for the last packet + if (packet_num == cfg.num_packets-1) begin + eob = 1; + length = (cfg.spp_last > 0) ? cfg.spp_last : cfg.spp; + end else begin + eob = 0; + length = cfg.spp; + end + + packet = gen_test_packet( + .length (length), + .freq (cfg.freq), + .amp (cfg.amp), + .eob (eob), + .timed (cfg.timed)); + packets.push_back(packet); + end + end + + return packets; + endfunction : generate_bursts + + + // Write a value to a settings register. + // + // addr : Address of the register to write to. + // value : Value to write to the register. + // timestamp : Timestamp to provide with the write. Set to -1 if the write + // should not be timed. + // + task automatic write_reg( + bit [SR_AWIDTH-1:0] addr, + bit [SR_DWIDTH-1:0] value, + longint timestamp = -1 + ); + @(posedge clk); + set_stb <= 1; + set_addr <= addr; + set_data <= value; + set_time <= (timestamp > 0) ? timestamp : 'X; + set_has_time <= (timestamp > 0); + @(posedge clk); + set_stb <= 0; + set_addr <= 'X; + set_data <= 'X; + set_time <= 'X; + set_has_time <= 'X; + @(posedge clk); + endtask : write_reg + + + // Write a value to the frequency register. + // + // freq : Normalized frequency to write to the register. E.g., in the + // range [-0.5,0.5) or [0,1). Numerically, either works. + // timestamp : Timestamp to provide with the write. Set to -1 if the write + // should not be timed. + // + task automatic write_reg_freq(real freq, longint timestamp = -1); + write_reg(SR_FREQ_ADDR, freq * (2.0**PHASE_ACCUM_WIDTH), timestamp); + endtask : write_reg_freq + + + // Write a value to the scale register. + // + // scale : Scaler to write to the register, in the range [-4,4). + // timestamp : Timestamp to provide with the write. Set to -1 if the write + // should not be timed. + // + task automatic write_reg_scale(real scale); + // Saturate to the range allowed by the register + scale = scale > MAX_SCALE ? MAX_SCALE : scale; + scale = scale < MIN_SCALE ? MIN_SCALE : scale; + write_reg(SR_SCALE_IQ_ADDR, scale * (2.0**FRAC_W)); + endtask : write_reg_scale + + + // Check that the output matches what we would expect. + // + // cfg: Test configuration + // packets: The packets that were input to the DUT + // + task automatic verify_output(burst_cfg_t cfg, axis_pkt_queue_t packets); + axis_pkt_t expected[$]; + + expected = generate_expected(cfg, packets); + + foreach(packets[i]) begin + axis_pkt_t recvd; + string msg; + axis_bfm.get(recvd); + msg = compare_packets(recvd, expected[i]); + `ASSERT_ERROR(msg == "", + $sformatf("Error in packet %0d: %s", i, msg)); + end + endtask : verify_output + + + // Test a burst (i.e., multiple packets ending with EOB) through the DUT + // using the provided configuration. + task automatic test_bursts(burst_cfg_t cfg); + axis_pkt_t packets[$]; + + // Are we doing timed packets? + cfg.timed = (cfg.timed || cfg.tune_time >= 0); + + // Set the registers + write_reg_scale(cfg.scale); + write_reg_freq(cfg.freq_shift); + + // Schedule a timed tune, if requested + if (cfg.tune_time >= 0) begin + write_reg_freq(cfg.tune_freq_shift, cfg.tune_time); + end + + // Wait a bit for the register changes to take effect + clk_gen.clk_wait_r(10); + + + // Generate test packets to send + packets = generate_bursts(cfg); + + // Send the packets + foreach (packets[i]) axis_bfm.put(packets[i]); + + // Check the results + verify_output(cfg, packets); + endtask : test_bursts + + + //--------------------------------------------------------------------------- + // Test Procedures + //--------------------------------------------------------------------------- + + // This performs a few directed test as a sanity check and to test a few + // corner cases. + task automatic directed_tests(); + burst_cfg_t cfg; + + // Iterate over different flow control settings to exercise different + // scenarios. + for (int bfm_config = 0; bfm_config < 4; bfm_config++) begin + case (bfm_config) + 0 : begin + // No stalls: on input or output to DUT + axis_bfm.set_master_stall_prob(0); + axis_bfm.set_slave_stall_prob(0); + end + 1 : begin + // Overflow: Input to DUT faster than output + axis_bfm.set_master_stall_prob(10); + axis_bfm.set_slave_stall_prob(30); + end + 2 : begin + // Underflow: Input to DUT slower than output + axis_bfm.set_master_stall_prob(30); + axis_bfm.set_slave_stall_prob(10); + end + 3 : begin + // Lots of stalls: Input and output stall frequently + axis_bfm.set_master_stall_prob(40); + axis_bfm.set_slave_stall_prob(40); + end + endcase + + //------------------------------- + // Test Basic Configurations + //------------------------------- + + // Test the default configuration + cfg = DEFAULT_BURST_CFG; + test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg)); + test_bursts(cfg); + test.end_test(); + + // Test a somewhat arbitrary but different configuration + cfg = DEFAULT_BURST_CFG; + cfg.spp = 97; + cfg.spp_last = 33; + cfg.num_bursts = 2; + cfg.num_packets = 3; + cfg.amp = 0.5; + cfg.scale = 1.25; + cfg.freq = 0.23; + cfg.freq_shift = 0.17; + test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg)); + test_bursts(cfg); + test.end_test(); + + // Repeat with a single-sample packet + cfg.spp = 1; + cfg.spp_last = 1; + test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg)); + test_bursts(cfg); + test.end_test(); + + //------------------------------- + // Test timed tunes + //------------------------------- + + cfg = DEFAULT_BURST_CFG; + cfg.spp = 135; + cfg.freq = 1.0/32.0; + cfg.freq_shift = 0.0; + cfg.num_bursts = 1; + cfg.num_packets = 3; + cfg.scale = 0.75; + cfg.freq_shift = 0.0; // Initial frequency shift + cfg.tune_freq_shift = 0.13; // New frequency shift + + // Test tuning in the middle of a packet + cfg.tune_time = current_time + cfg.num_packets*cfg.spp/2; + test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg)); + test_bursts(cfg); + test.end_test(); + + // Test tuning at the end of the first packet + cfg.tune_time = current_time + cfg.spp-1; + test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg)); + test_bursts(cfg); + test.end_test(); + + // Test tuning at the beginning of a packet + cfg.tune_time = current_time + cfg.spp; + test.start_test($sformatf("Directed Test: bfm_config: %0d, %p", bfm_config, cfg)); + test_bursts(cfg); + test.end_test(); + end + endtask : directed_tests + + + // This generates a randomized configuration exercises the DUT with that + // configuration. This is repeated num_tests times, with a unique + // configuration each time. + task automatic random_tests(int num_tests); + burst_cfg_t cfg; + int master_stall_prob, slave_stall_prob; + + repeat (num_tests) begin + // Choose random values for this run. Round the floating point numbers to + // a smaller number of bits to reduce rounding differences between the + // testbench and the DUT. + cfg = DEFAULT_BURST_CFG; + cfg.spp = $urandom_range(1, 64); + cfg.spp_last = $urandom_range(1, 64); + cfg.num_packets = $urandom_range(1, 3); + cfg.num_bursts = $urandom_range(1, 2); + cfg.amp = round_bits(frand_range(1.0/16.0, 15.0/16.0), 15); + cfg.freq = frand(0.5); + cfg.timed = $urandom_range(0, 1); + cfg.scale = round_bits(frand_range(-4.0, 4.0), 15); + cfg.freq_shift = round_bits(frand(0.5), 32); + cfg.tune_freq_shift = round_bits(frand(0.5), 32); + if (cfg.timed) begin + cfg.tune_time = current_time + + $urandom_range(0, (cfg.num_packets-1)*cfg.spp + cfg.spp_last - 1); + end + master_stall_prob = $urandom_range(0, 50); + slave_stall_prob = $urandom_range(0, 50); + + // Run the test + test.start_test($sformatf("Random Test: InStall: %0d, OutStall: %0d, %p", + master_stall_prob, slave_stall_prob, cfg)); + axis_bfm.set_master_stall_prob(master_stall_prob); + axis_bfm.set_slave_stall_prob(slave_stall_prob); + test_bursts(cfg); + test.end_test(); + end + endtask : random_tests + + + //--------------------------------------------------------------------------- + // Main Test Process + //--------------------------------------------------------------------------- + + initial begin : main + test.start_tb("dds_timed_tb"); + + // Start the BFMs running + axis_bfm.run(); + + // Reset + clk_gen.reset(); + @(negedge rst); + + //------------------------------- + // Run Tests + //------------------------------- + + directed_tests(); + random_tests(200); + + test.end_tb(); + end + +endmodule + + +`default_nettype wire |