//
// Copyright 2019 Ettus Research, A National Instruments Company
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: rfnoc_block_radio_tb
//
// Description: This is the testbench for rfnoc_block_radio.
//


module rfnoc_block_radio_tb #(
    parameter int CHDR_W          = 128,  // CHDR bus width
    parameter int ITEM_W          = 32,   // Sample width
    parameter int NIPC            = 2,    // Number of samples per radio clock cycle
    parameter int NUM_PORTS       = 2,    // Number of radio channels
    parameter int STALL_PROB      = 25,   // Probability of AXI BFM stall
    parameter int STB_PROB        = 80,   // Probability of radio STB asserting
    parameter bit TEST_REGS       = 1,    // Do register tests
    parameter bit EN_COMP_GAIN_TX = 0,    // Enable TX gain compensation
    parameter bit EN_COMP_GAIN_RX = 0,    // Enable RX gain compensation
    parameter bit EN_FIFO_OUT_REG = 0     // Add output register after SRL FIFOs
);

  // Include macros and time declarations for use with PkgTestExec
  `include "test_exec.svh"

  import PkgTestExec::*;
  import rfnoc_chdr_utils_pkg::*;
  import PkgChdrData::*;
  import PkgRfnocBlockCtrlBfm::*;
  import PkgAxisCtrlBfm::*;
  import PkgChdrBfm::*;
  import PkgRandom::*;
  import PkgComplex::*;
  import PkgMath::*;
  import PkgCtrlIfaceBfm::RESERVED_TS;
  import ctrlport_pkg::*;

  // Pull in radio register offsets and constants
  `include "rfnoc_block_radio_regs.vh"
  `include "../../../dsp/timed_complex_gain_regs.svh"


  // Simulation Parameters
  localparam int          NOC_ID         = 32'h12AD1000;
  localparam logic [ 9:0] THIS_PORTID    = 10'h17;
  localparam logic [15:0] THIS_EPID      = 16'hDEAD;
  localparam int          MTU            = 8;
  localparam int          RADIO_W        = NIPC * ITEM_W;       // Radio word size
  localparam int          SPP            = 64;                  // Samples per packet
  localparam int          WPP            = SPP*ITEM_W/RADIO_W;  // Radio words per packet
  localparam int          CHDR_CLK_PER   = 5;                   // rfnoc_chdr_clk period in ns
  localparam int          CTRL_CLK_PER   = 25;                  // rfnoc_ctrl_clk period in ns
  localparam int          RADIO_CLK_PER  = 10;                  // radio_clk_per period in ns

  localparam int NUM_TESTS = 2;   // Number of times to run each test
  localparam bit VERBOSE   = 0;   // Display lots of output, or not

  // Amount of time to wait for a packet to be fully acquired
  localparam realtime MAX_PKT_WAIT = 4*WPP*(RADIO_CLK_PER+CTRL_CLK_PER)*1ns;

  // Error reporting values to use
  localparam bit [ 9:0] TX_ERR_DST_PORT     = 10'h2B5;
  localparam bit [ 9:0] TX_ERR_REM_DST_PORT = 10'h14C;
  localparam bit [15:0] TX_ERR_REM_DST_EPID = 16'hA18E;
  localparam bit [19:0] TX_ERR_ADDRESS      = 20'hA31D3;

  // Radio latency expected in due to time alignment. There is a fixed amount
  // of latency between the radio_time and when the corresponding sample is
  // strobed in or out. We need to make sure this latency is constant. The
  // actual amount of latency is not critical since there's always an unknown
  // but constant amount of latency in the RF front end.
  localparam int FEATURE_COMP_GAIN_LATENCY = 6;

  localparam int CORE_TX_LATENCY  = (NIPC > 1) ? 4 : 2;
  localparam int CORE_RX_LATENCY  = (NIPC > 1) ? 2 : 0;


  localparam int RADIO_TX_LATENCY = (EN_COMP_GAIN_TX) ?
    CORE_TX_LATENCY + FEATURE_COMP_GAIN_LATENCY :
    CORE_TX_LATENCY;
  localparam int RADIO_RX_LATENCY = (EN_COMP_GAIN_RX) ?
    CORE_RX_LATENCY + FEATURE_COMP_GAIN_LATENCY :
    CORE_RX_LATENCY;

  localparam int ALIGN_W  = (NIPC > 1) ? $clog2(NIPC) : 1; // Width of radio alignment counters

  // Calculate an appropriate delay to use for future timed TX/RX tests in
  // terms of the radio time ticks. RX takes a lot longer because you have to
  // queue a command using a register write. The higher the NIPC, the faster
  // the tick rate for the same clock. We want it to be short-ish to reduce
  // simulation time.
  localparam int TX_CMD_DELAY = 200;
  localparam int RX_CMD_DELAY = 500*NIPC;

  // Feature Registers
  localparam int FEATURES_EN_COMP_GAIN_TX_POS = 0;
  localparam int FEATURES_EN_COMP_GAIN_RX_POS = 1;
  localparam logic [31:0] EN_COMP_GAIN_TX_MSK = 1 << FEATURES_EN_COMP_GAIN_TX_POS;
  localparam logic [31:0] EN_COMP_GAIN_RX_MSK = 1 << FEATURES_EN_COMP_GAIN_RX_POS;

  localparam int MAX_COMPONENT_VALUE = 2**(ITEM_W/2-1)-1;
  localparam int MIN_COMPONENT_VALUE = -2**(ITEM_W/2-1);

  // Complex gain parameters
  localparam int COMPLEX_GAIN_COEFF_W = CTRLPORT_DATA_W; // Width of complex gain input
  localparam int GAIN_FXP_FRACT_W     = 14;              // Fractional bits in complex gain

  typedef logic [COMPLEX_GAIN_COEFF_W/2-1:0] coeff_component_t;

  localparam logic [COMPLEX_GAIN_COEFF_W-1:0] DEFAULT_GAIN_FXP =
    {coeff_component_t'(1 << GAIN_FXP_FRACT_W),  // Real = 1.0
     coeff_component_t'(0)};                     // Imag = 0.0

  // FIFO size calculation
  localparam int CMD_FIFO_SPACE_MAX = 2**CMD_FIFO_SIZE_LOG2 +
                                      (EN_FIFO_OUT_REG ? CMD_FLOP_SIZE : 0);


  //---------------------------------------------------------------------------
  // Clocks and Resets
  //---------------------------------------------------------------------------

  bit rfnoc_chdr_clk;
  bit rfnoc_ctrl_clk;
  bit radio_clk;

  // Don't start the clocks automatically (AUTOSTART=0), since we expect
  // multiple instances of this testbench to run in sequence. They will be
  // started before the first test.
  sim_clock_gen #(.PERIOD(CHDR_CLK_PER), .AUTOSTART(0))
    rfnoc_chdr_clk_gen (.clk(rfnoc_chdr_clk), .rst());
  sim_clock_gen #(.PERIOD(CTRL_CLK_PER), .AUTOSTART(0))
    rfnoc_ctrl_clk_gen (.clk(rfnoc_ctrl_clk), .rst());
  sim_clock_gen #(.PERIOD(RADIO_CLK_PER), .AUTOSTART(0))
    radio_clk_gen (.clk(radio_clk), .rst());



  //---------------------------------------------------------------------------
  // Bus Functional Models
  //---------------------------------------------------------------------------

  // Connections to DUT as interfaces:
  RfnocBackendIf        backend            (rfnoc_chdr_clk, rfnoc_ctrl_clk);
  AxiStreamIf #(32)     m_ctrl             (rfnoc_ctrl_clk, 1'b0);
  AxiStreamIf #(32)     s_ctrl             (rfnoc_ctrl_clk, 1'b0);
  AxiStreamIf #(CHDR_W) m_chdr [NUM_PORTS] (rfnoc_chdr_clk, 1'b0);
  AxiStreamIf #(CHDR_W) s_chdr [NUM_PORTS] (rfnoc_chdr_clk, 1'b0);

  typedef ChdrData #(CHDR_W, ITEM_W)::chdr_word_t chdr_word_t;
  typedef ChdrData #(CHDR_W, ITEM_W)::item_t      sample_t;
  typedef bit signed [ITEM_W/2-1:0]               complex_component_t;

  // Bus functional model for a software block controller
  RfnocBlockCtrlBfm #(CHDR_W, ITEM_W) blk_ctrl = new(backend, m_ctrl, s_ctrl);

  // Connect block controller to BFMs
  for (genvar i = 0; i < NUM_PORTS; i++) begin : gen_bfm_connections
    initial begin
      blk_ctrl.connect_master_data_port(i, m_chdr[i]);
      blk_ctrl.connect_slave_data_port(i, s_chdr[i]);

      // Set the initial CHDR BFM stall probability
      blk_ctrl.set_master_stall_prob(i, STALL_PROB);
      blk_ctrl.set_slave_stall_prob(i, STALL_PROB);
    end
  end



  //---------------------------------------------------------------------------
  // Radio Data Model
  //---------------------------------------------------------------------------

  logic [NUM_PORTS*RADIO_W-1:0] radio_rx_data;
  logic [        NUM_PORTS-1:0] radio_rx_stb;

  logic [63:0] radio_time;

  // Radio data generation
  sim_radio_gen #(
    .NSPC         (NIPC),
    .SAMP_W       (ITEM_W),
    .NUM_CHANNELS (NUM_PORTS),
    .STB_PROB     (STB_PROB),
    .INCREMENT    (NIPC)
  ) radio_gen (
    .radio_clk     (radio_clk),
    .radio_rst     (1'b0),
    .radio_rx_data (radio_rx_data),
    .radio_rx_stb  (radio_rx_stb),
    .radio_time    (radio_time)
  );



  //---------------------------------------------------------------------------
  // DUT
  //---------------------------------------------------------------------------

  logic [NUM_PORTS-1:0] radio_rx_running;

  logic [NUM_PORTS*RADIO_W-1:0] radio_tx_data;
  logic [        NUM_PORTS-1:0] radio_tx_stb;
  logic [        NUM_PORTS-1:0] radio_tx_running;

  logic [NUM_PORTS*CHDR_W-1:0] s_rfnoc_chdr_tdata_flat;
  logic [       NUM_PORTS-1:0] s_rfnoc_chdr_tlast_flat;
  logic [       NUM_PORTS-1:0] s_rfnoc_chdr_tvalid_flat;
  logic [       NUM_PORTS-1:0] s_rfnoc_chdr_tready_flat;

  logic [NUM_PORTS*CHDR_W-1:0] m_rfnoc_chdr_tdata_flat;
  logic [       NUM_PORTS-1:0] m_rfnoc_chdr_tlast_flat;
  logic [       NUM_PORTS-1:0] m_rfnoc_chdr_tvalid_flat;
  logic [       NUM_PORTS-1:0] m_rfnoc_chdr_tready_flat;

  // Use the same strobe for both Rx and Tx
  assign radio_tx_stb = radio_rx_stb;

  // Flatten the data stream arrays into concatenated vectors
  genvar i;
  for (i = 0; i < NUM_PORTS; i++) begin : gen_radio_connections
    assign s_rfnoc_chdr_tdata_flat[CHDR_W*i+:CHDR_W] = m_chdr[i].tdata;
    assign s_rfnoc_chdr_tlast_flat[i]                = m_chdr[i].tlast;
    assign s_rfnoc_chdr_tvalid_flat[i]               = m_chdr[i].tvalid;
    assign m_chdr[i].tready                          = s_rfnoc_chdr_tready_flat[i];

    assign s_chdr[i].tdata             = m_rfnoc_chdr_tdata_flat[CHDR_W*i+:CHDR_W];
    assign s_chdr[i].tlast             = m_rfnoc_chdr_tlast_flat[i];
    assign s_chdr[i].tvalid            = m_rfnoc_chdr_tvalid_flat[i];
    assign m_rfnoc_chdr_tready_flat[i] = s_chdr[i].tready;
  end


  rfnoc_block_radio #(
    .THIS_PORTID      (THIS_PORTID),
    .CHDR_W           (CHDR_W),
    .NIPC             (NIPC),
    .ITEM_W           (ITEM_W),
    .NUM_PORTS        (NUM_PORTS),
    .MTU              (MTU),
    .EN_COMP_GAIN_TX  (EN_COMP_GAIN_TX),
    .EN_COMP_GAIN_RX  (EN_COMP_GAIN_RX),
    .EN_FIFO_OUT_REG  (EN_FIFO_OUT_REG)
  ) rfnoc_block_radio_i (
    .rfnoc_chdr_clk          (backend.chdr_clk),
    .s_rfnoc_chdr_tdata      (s_rfnoc_chdr_tdata_flat),
    .s_rfnoc_chdr_tlast      (s_rfnoc_chdr_tlast_flat),
    .s_rfnoc_chdr_tvalid     (s_rfnoc_chdr_tvalid_flat),
    .s_rfnoc_chdr_tready     (s_rfnoc_chdr_tready_flat),
    .m_rfnoc_chdr_tdata      (m_rfnoc_chdr_tdata_flat),
    .m_rfnoc_chdr_tlast      (m_rfnoc_chdr_tlast_flat),
    .m_rfnoc_chdr_tvalid     (m_rfnoc_chdr_tvalid_flat),
    .m_rfnoc_chdr_tready     (m_rfnoc_chdr_tready_flat),
    .rfnoc_core_config       (backend.cfg),
    .rfnoc_core_status       (backend.sts),
    .rfnoc_ctrl_clk          (backend.ctrl_clk),
    .s_rfnoc_ctrl_tdata      (m_ctrl.tdata),
    .s_rfnoc_ctrl_tlast      (m_ctrl.tlast),
    .s_rfnoc_ctrl_tvalid     (m_ctrl.tvalid),
    .s_rfnoc_ctrl_tready     (m_ctrl.tready),
    .m_rfnoc_ctrl_tdata      (s_ctrl.tdata),
    .m_rfnoc_ctrl_tlast      (s_ctrl.tlast),
    .m_rfnoc_ctrl_tvalid     (s_ctrl.tvalid),
    .m_rfnoc_ctrl_tready     (s_ctrl.tready),
    .m_ctrlport_req_wr       (),
    .m_ctrlport_req_rd       (),
    .m_ctrlport_req_addr     (),
    .m_ctrlport_req_data     (),
    .m_ctrlport_req_byte_en  (),
    .m_ctrlport_req_has_time (),
    .m_ctrlport_req_time     (),
    .m_ctrlport_resp_ack     (1'b0),
    .m_ctrlport_resp_status  (2'b0),
    .m_ctrlport_resp_data    (32'b0),
    .radio_clk               (radio_clk),
    .radio_time              (radio_time),
    .radio_rx_data           (radio_rx_data),
    .radio_rx_stb            (radio_rx_stb),
    .radio_rx_running        (radio_rx_running),
    .radio_tx_data           (radio_tx_data),
    .radio_tx_stb            (radio_tx_stb),
    .radio_tx_running        (radio_tx_running)
  );



  //---------------------------------------------------------------------------
  // Helper Tasks
  //---------------------------------------------------------------------------

  // Read a 32-bit register at offset "addr" from shared radio registers
  task automatic read_shared(logic [19:0] addr, output logic [31:0] data);
    addr = addr + SHARED_BASE_ADDR;
    blk_ctrl.reg_read(addr, data);
  endtask : read_shared

  // Write a 32-bit register at offset "addr" in shared radio registers
  task automatic write_shared(logic [19:0] addr, logic [31:0] data);
    addr = addr + SHARED_BASE_ADDR;
    blk_ctrl.reg_write(addr, data);
  endtask : write_shared

  // Read a 32-bit register at offset "addr" from radio "radio_num"
  task automatic read_radio(int radio_num, logic [19:0] addr, logic [RADIO_ADDR_W:0] reg_offset,
                            output logic [31:0] data,
                            input chdr_timestamp_t timestamp = RESERVED_TS);
    addr = addr + RADIO_BASE_ADDR + (radio_num * 2 ** RADIO_ADDR_W) + reg_offset;
    blk_ctrl.reg_read(addr, data, timestamp);
  endtask : read_radio

  // Read a 64-bit register at offset "addr" from radio "radio_num"
  task automatic read_radio_64(int radio_num, logic [19:0] addr, logic [RADIO_ADDR_W:0] reg_offset,
                               output logic [63:0] data);
    addr = addr + RADIO_BASE_ADDR + (radio_num * 2 ** RADIO_ADDR_W) + reg_offset;
    blk_ctrl.reg_read(addr,   data[31:0]);
    blk_ctrl.reg_read(addr+4, data[63:32]);
  endtask : read_radio_64

  // Write a 32-bit register at offset "addr" in radio "radio_num"
  task automatic write_radio(int radio_num, logic [19:0] addr, logic [RADIO_ADDR_W:0] reg_offset,
                             logic [31:0] data, chdr_timestamp_t timestamp = RESERVED_TS);
    addr = addr + RADIO_BASE_ADDR + (radio_num * 2 ** RADIO_ADDR_W) + reg_offset;
    blk_ctrl.reg_write(addr, data, timestamp);
  endtask : write_radio

  // Write a 64-bit register at offset "addr" in radio "radio_num"
  task automatic write_radio_64(int radio_num, logic [19:0] addr, logic [RADIO_ADDR_W:0] reg_offset,
                                logic [63:0] data);
    addr = addr + RADIO_BASE_ADDR + (radio_num * 2 ** RADIO_ADDR_W) + reg_offset;
    blk_ctrl.reg_write(addr,   data[31:0]);
    blk_ctrl.reg_write(addr+4, data[63:32]);
  endtask : write_radio_64

  // Apply gain to sample value
  function automatic bit [ITEM_W-1:0] apply_gain(bit [ITEM_W-1:0] sample, complex_t gain);
    real samp_real = real'(signed'(sample[ITEM_W/2 +: ITEM_W/2]));
    real samp_imag = real'(signed'(sample[0 +: ITEM_W/2]));
    complex_t samp_cplx = '{samp_real, samp_imag};
    complex_t out_cplx = mul(samp_cplx, gain);

    // Round and clip
    int out_rounded_real = $rtoi(round(out_cplx.re, ROUND_HALF_UP));
    int out_rounded_imag = $rtoi(round(out_cplx.im, ROUND_HALF_UP));
    int out_clipped_real = (out_rounded_real > MAX_COMPONENT_VALUE) ?
                              MAX_COMPONENT_VALUE :   // if too big, coerce to max
                              (out_rounded_real < MIN_COMPONENT_VALUE) ?
                                MIN_COMPONENT_VALUE : // if too small, coerce to min
                                out_rounded_real;     // otherwise, keep value as is
    int out_clipped_imag = (out_rounded_imag >  MAX_COMPONENT_VALUE) ?
                              MAX_COMPONENT_VALUE :   // if too big, coerce to max
                              (out_rounded_imag < MIN_COMPONENT_VALUE) ?
                                MIN_COMPONENT_VALUE : // if too small, coerce to min
                                out_rounded_imag;     // otherwise, keep value as is
    return { complex_component_t'(out_clipped_real) , complex_component_t'(out_clipped_imag) };
  endfunction : apply_gain

  // Convert complex gain from floating point to fixed point representation
  function automatic logic [COMPLEX_GAIN_COEFF_W-1:0] complex_to_fixed(complex_t gain);
    int signed real_fxp = $rtoi(gain.re * (1 << GAIN_FXP_FRACT_W));
    int signed imag_fxp = $rtoi(gain.im * (1 << GAIN_FXP_FRACT_W));
    return {coeff_component_t'(real_fxp[COMPLEX_GAIN_COEFF_W/2-1:0]),
            coeff_component_t'(imag_fxp[COMPLEX_GAIN_COEFF_W/2-1:0])};
  endfunction : complex_to_fixed

  // Start an Rx acquisition
  task automatic start_rx (
    int        radio_num,     // Radio channel to use
    bit [63:0] num_words = 0  // Number of radio words
  );
    logic [31:0] cmd;

    if (num_words == 0) begin
      // Do a continuous acquisition
      if (VERBOSE) $display("Radio %0d: Start RX, continuous receive", radio_num);
      cmd = RX_CMD_CONTINUOUS;
    end else begin
      // Do a finite acquisition (num samps and done)
      if (VERBOSE) $display("Radio %0d: Start RX, receive %0d words", radio_num, num_words);
      write_radio_64(radio_num, REG_RX_CMD_NUM_WORDS_LO, REG_PORT_RX_ADDR_OFFSET, num_words);
      cmd = RX_CMD_FINITE;
    end

    // Write command to radio
    write_radio(radio_num, REG_RX_CMD, REG_PORT_RX_ADDR_OFFSET, cmd);
  endtask : start_rx


  // Start an Rx acquisition at a specific time
  task automatic start_rx_timed (
    int        radio_num,      // Radio channel to use
    bit [63:0] num_words = 0,  // Number of radio words (0 means continuous)
    bit [63:0] start_time
  );
    logic [31:0] cmd;

    if (num_words == 0) begin
      // Do a continuous acquisition
      if (VERBOSE) $display("Radio %0d: Start RX, continuous receive (timed)", radio_num);
      cmd = RX_CMD_CONTINUOUS;
    end else begin
      // Do a finite acquisition (num samps and done)
      if (VERBOSE) $display("Radio %0d: Start RX, receive %0d words (timed)", radio_num, num_words);
      write_radio_64(radio_num, REG_RX_CMD_NUM_WORDS_LO, REG_PORT_RX_ADDR_OFFSET, num_words);
      cmd = RX_CMD_FINITE;
    end

    // Mark that this is a timed command
    cmd[RX_CMD_TIMED_POS] = 1'b1;

    // Set start time for command
    write_radio_64(radio_num, REG_RX_CMD_TIME_LO, REG_PORT_RX_ADDR_OFFSET, start_time);

    // Write command to radio
    write_radio(radio_num, REG_RX_CMD, REG_PORT_RX_ADDR_OFFSET, cmd);
  endtask : start_rx_timed


  // Send the Rx stop command to the indicated radio channel
  task automatic stop_rx(int radio_num);
    if (VERBOSE) $display("Radio %0d: Stop RX", radio_num);
    write_radio(radio_num, REG_RX_CMD, REG_PORT_RX_ADDR_OFFSET, RX_CMD_STOP);
  endtask : stop_rx


  // Receive num_words from the indicated radio channel and verify that it's
  // sequential and contiguous data aligned on packet boundaries.
  task automatic check_rx(
    int radio_num,                // Radio to receive from and check
    int num_words,                // Number of radio words to expect
    complex_t gain = '{1.0, 0.0}, // Gain applied to samples by the block
    int gain_offset = 0           // Offset to apply to gain index
  );
    int              sample_count;              // Counter to track number of samples generated
    bit [ITEM_W-1:0] sample_val;                // Value of the next sample
    chdr_word_t      data[$], metadata[$];      // Array of data/metadata for the received packet
    int              num_samples;               // Number of samples to send
    int              byte_length;               // Number of data bytes in next packet
    int              expected_length;           // Expected byte length of the next packet
    int              valid_words;               // Number of valid chdr_word_t in next packet
    packet_info_t    pkt_info;                  // Header information for the received packet
    complex_t        default_gain = '{1.0, 0.0};// Default gain (no change)


    num_samples = num_words * NIPC;

    sample_count = 0;
    while (sample_count < num_samples) begin
      // Fetch the next packet
      blk_ctrl.recv_adv(radio_num, data, byte_length, metadata, pkt_info);

      // Take the first sample as a starting count for the remaining samples
      if (sample_count == 0) begin
        // Workaround for  XSim bug with following complex_t struct comparison:
        // ```
        // if (gain != default_gain || gain_offset != 0) begin
        // ```
        // This comparison does not work as expected in Vivado 2021.1 simulator,
        // so we break it down into component comparisons.
        if (gain.re != default_gain.re ||
            gain.im != default_gain.im ||
            gain_offset != 0) begin
          sample_val = pkt_info.timestamp[ITEM_W-1:0] - (NIPC*RADIO_RX_LATENCY);
        end else begin
          sample_val = data[0][ITEM_W-1:0];
        end
      end

      // Calculate expected length in bytes
      if (num_samples - sample_count >= SPP) begin
        // Expecting a full packet
        expected_length = SPP*ITEM_W/8;
      end else begin
        // Expecting partial packet
        expected_length = (num_samples - sample_count) * ITEM_W/8;
      end

      // Check that the length matches our expectation
      `ASSERT_ERROR(
        byte_length == expected_length,
        $sformatf({"Received packet didn't have expected length.\n",
          "Expected 0x%X, Received 0x%X"}, expected_length, byte_length)
      );

      // Loop over the packet, one chdr_word_t at a time
      valid_words = int'($ceil(real'(byte_length) / ($bits(chdr_word_t)/8)));
      for (int i = 0; i < valid_words; i++) begin
        // Check each sample of the next chdr_word_t value
        for (int sub_sample = 0; sub_sample < $bits(chdr_word_t)/ITEM_W; sub_sample++) begin
          sample_t actual, expected_val;
          actual = data[i][ITEM_W*sub_sample +: ITEM_W];  // Work around Vivado 2018.3 issue
          if (sample_count < gain_offset) begin
            expected_val = sample_val;
          end else begin
            expected_val = apply_gain(sample_val, gain);
          end
          `ASSERT_ERROR(
            actual == expected_val,
            $sformatf(
              "Sample %0d (0x%X) didn't match expected value (0x%X)",
              sample_count, actual, expected_val
            )
          );
          sample_val++;
          sample_count++;

          // Check if the word is only partially full
          if (sample_count >= num_samples) break;
        end
      end
    end
  endtask : check_rx


  // Send num_words to the indicated radio for transmission at the given time.
  task automatic start_tx_timed (
    int                radio_num,       // Radio channel to transmit on
    bit   [63:0]       num_words,       // Number of radio words to transmit
    logic [63:0]       start_time = 'X, // Time at which to begin transmit
    bit   [ITEM_W-1:0] start_val  = 1,  // Initial sample value
    bit                eob        = 1   // Set EOB flag at the end
  );
    int              sample_count;    // Counter to track number of samples generated
    bit [ITEM_W-1:0] sample_val;      // Value of the next sample
    chdr_word_t      data[$];         // Array of data for the packet
    int              num_samples;     // Number of samples to send
    int              byte_length;     // Number of bytes for next packet
    chdr_word_t      chdr_word;       // Next word to send to BFM
    packet_info_t    pkt_info = 0;    // Flags/timestamp for next packet

    if (VERBOSE) $display("Radio %0d: Start TX, send %0d words", radio_num, num_words);

    num_samples = num_words * NIPC;

    if (!$isunknown(start_time)) pkt_info.has_time = 1;

    sample_val   = start_val;
    sample_count = 0;
    while (sample_count < num_samples) begin
      // Calculate timestamp for this packet
      if (pkt_info.has_time) begin
        pkt_info.timestamp = start_time + sample_count;
      end

      // Clear the payload
      data = {};

      // Loop until we've built up a packet
      forever begin
        // Generate the next word
        for (int sub_sample = 0; sub_sample < $bits(chdr_word_t)/ITEM_W; sub_sample++) begin
          chdr_word[ITEM_W*sub_sample +: ITEM_W] = sample_val;
          sample_val++;
          sample_count++;
        end

        // Add next word to the queue
        data.push_back(chdr_word);

        // Send the packet if we're at a packet boundary
        if (sample_count % SPP == 0) begin
          pkt_info.eob = (sample_count == num_samples && eob) ? 1 : 0;
          byte_length = SPP * ITEM_W/8;
          blk_ctrl.send(radio_num, data, byte_length, {}, pkt_info);
          break;
        end else if (sample_count >= num_samples) begin
          pkt_info.eob = eob;
          byte_length = (sample_count % SPP) * ITEM_W/8;
          blk_ctrl.send(radio_num, data, byte_length, {}, pkt_info);
          break;
        end
      end
    end
  endtask : start_tx_timed


  // Send num_words to the indicated radio for transmission.
  task automatic start_tx (
    int              radio_num,       // Radio channel to transmit on
    bit [63:0]       num_words,       // Number of radio words to transmit
    bit [ITEM_W-1:0] start_val = 1,   // Initial sample value
    bit              eob = 1          // Set EOB flag at the end
  );
    // Passing 'X tells the underlying BFM to not insert a timestamp
    start_tx_timed(radio_num, num_words, 'X, start_val, eob);
  endtask : start_tx


  // Verify the output of a packet, expecting it at a specific time
  task automatic check_tx_timed (
    int                radio_num,               // Radio channel to transmit on
    bit   [63:0]       num_words,               // Number of radio words to expect
    logic [63:0]       start_time = 'X,         // Expected start time
    bit   [ITEM_W-1:0] start_val  = 1,          // Initial sample value
    complex_t          gain     = '{1.0, 0.0},  // Expected complex gain
    int                gain_offset = 0          // Offset to apply to gain index
  );
    int sample_val;           // Expected value of next sample
    bit found = 0;
    int offset = 0;
    int num_samps;
    bit [ITEM_W-1:0] expected_sample, expected_start;

    // Wait for the expected packet to start. Look for the start value in any
    // sample position. Save the sample offset so we can verify it's correct.
    if (gain_offset != 0) begin
      expected_start = start_val;
    end else begin
      expected_start = apply_gain(start_val, gain);
    end
    while (!found) begin
      @(posedge radio_clk);
      if (radio_tx_stb[radio_num]) begin
        for (int samp_i = 0; samp_i < NIPC; samp_i++) begin
          if (radio_tx_data[radio_num*RADIO_W + samp_i*ITEM_W +: ITEM_W] == expected_start) begin
            found = 1;
            offset = samp_i;
            break;
          end
        end
      end
    end

    // Check the time
    if (!$isunknown(start_time)) begin
      `ASSERT_ERROR(
        radio_time+offset == start_time + RADIO_TX_LATENCY*NIPC,
        $sformatf("First sample transmitted at radio time 0x%0X but expected 0x%0X",
          radio_time+offset, start_time + RADIO_TX_LATENCY*NIPC)
      );
    end

    // Verify output one word at a time
    sample_val = start_val;
    num_samps = num_words * NIPC;
    for (int samp_count = 0; samp_count < num_samps; samp_count++) begin
      if (samp_count >= gain_offset) begin
        expected_sample = apply_gain(sample_val, gain);
      end else begin
        expected_sample = sample_val;
      end
      `ASSERT_ERROR(
        radio_tx_data[radio_num*RADIO_W + offset*ITEM_W +: ITEM_W] == expected_sample,
        $sformatf({"Radio output doesn't match expected value\n",
          "Expected 0x%X but found 0x%X at sample %0d (word offset %0d)."},
          expected_sample, radio_tx_data[radio_num*RADIO_W + ITEM_W*offset +: ITEM_W],
          samp_count, offset)
      );
      sample_val++;
      offset++;

      if (offset == NIPC) begin
        offset = 0;
        // Wait for the next radio word to be output
        do begin
          @(posedge radio_clk);
        end while (!radio_tx_stb[radio_num]);
      end
    end
  endtask : check_tx_timed


  // Verify the output of a packet
  task automatic check_tx (
    int              radio_num,               // Radio to transmit on
    bit [63:0]       num_words,               // Number of radio words to expect
    bit [ITEM_W-1:0] start_val  = 1,          // Initial sample value
    complex_t        gain       = '{1.0, 0.0} // Expected complex gain
  );
    check_tx_timed(radio_num, num_words, 'X, start_val, gain);
  endtask : check_tx


  // When we expect and error, this task will check that control packets were
  // received and that they have the expected values.
  task check_error (int error);
    AxisCtrlPacket ctrl_packet;
    chdr_word_t word;

    // Get error code
    blk_ctrl.get_ctrl_bfm().get_ctrl(ctrl_packet);
    word = ctrl_packet.data[0];   // Work around Vivado 2018.3 issue
    `ASSERT_ERROR(
      word                            == error &&
      ctrl_packet.op_word.op_code     == CTRL_OP_WRITE &&
      ctrl_packet.op_word.address     == TX_ERR_ADDRESS &&
      ctrl_packet.header.is_ack       == 1'b0 &&
      ctrl_packet.header.has_time     == 1'b1 &&
      ctrl_packet.header.num_data     == 1 &&
      ctrl_packet.header.dst_port     == TX_ERR_DST_PORT &&
      ctrl_packet.header.rem_dst_port == TX_ERR_REM_DST_PORT &&
      ctrl_packet.header.rem_dst_epid == TX_ERR_REM_DST_EPID,
      "Unexpected error code response");

    // Send acknowledgment
    ctrl_packet.header = 0;
    ctrl_packet.header.is_ack = 1;
    blk_ctrl.get_ctrl_bfm().put_ctrl(ctrl_packet);
  endtask : check_error


  // This function is a copy of the one in sim_radio_gen, but due to a Vivado
  // 2021.1 bug, we need to copy it here.
  typedef sample_t [     NIPC-1:0] radio_t;     // Radio output word
  typedef radio_t  [NUM_PORTS-1:0] data_t;      // Radio output for all channels
  function radio_t radio_init(
    sample_t first_sample = '0
  );
    radio_t ret_val;

    for (int samp_i = 0; samp_i < NIPC; samp_i++) begin
      ret_val[samp_i] = first_sample + samp_i;
    end

    return ret_val;
  endfunction : radio_init



  //---------------------------------------------------------------------------
  // Test Procedures
  //---------------------------------------------------------------------------

  task automatic test_block_info();
    test.start_test("Verify Block Info", 2us);

    // Get static block info and validate it
    `ASSERT_ERROR(blk_ctrl.get_noc_id() == NOC_ID, "Incorrect noc_id Value");
    `ASSERT_ERROR(blk_ctrl.get_num_data_i() == NUM_PORTS, "Incorrect num_data_i Value");
    `ASSERT_ERROR(blk_ctrl.get_num_data_o() == NUM_PORTS, "Incorrect num_data_o Value");
    `ASSERT_ERROR(blk_ctrl.get_mtu() == MTU, "Incorrect mtu Value");

    test.end_test();
  endtask : test_block_info



  task automatic test_shared_registers();
    logic [31:0] val;
    logic [63:0] time1;
    logic [63:0] time2;
    test.start_test("Shared Registers", 10us);

    // Compatibility number
    read_shared(REG_COMPAT_NUM, val);
    `ASSERT_ERROR(
      val == {
        rfnoc_block_radio_i.compat_major,
        rfnoc_block_radio_i.compat_minor
      },
      "REG_COMPAT_NUM didn't read correctly"
    );
    do begin
      read_shared(REG_TIME_LO, time1[31:0]);
      read_shared(REG_TIME_HI, time1[63:32]);
    end while ($isunknown(time1));
    do begin
      read_shared(REG_TIME_LO, time2[31:0]);
      read_shared(REG_TIME_HI, time2[63:32]);
    end while ($isunknown(time2));
    `ASSERT_ERROR(
      time2 > time1,
      $sformatf({"Time did not increment in REG_TIME_HI and REG_TIME_LO",
      "Time1: 0x%X, Time2: 0x%X"}, time1, time2)
    );
    test.end_test();
  endtask : test_shared_registers



  task automatic test_general_registers(int radio_num);
    logic [31:0] val;
    test.start_test("General Registers", 10us);

    // Test loopback enable register (read/write)
    read_radio(radio_num, REG_LOOPBACK_EN, REG_PORT_GENERAL_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "Initial value of REG_LOOPBACK_EN is incorrect");
    write_radio(radio_num, REG_LOOPBACK_EN, REG_PORT_GENERAL_ADDR_OFFSET, 32'hFFFFFFFF);
    read_radio(radio_num, REG_LOOPBACK_EN, REG_PORT_GENERAL_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 1, "REG_LOOPBACK_EN didn't update correctly");
    write_radio(radio_num, REG_LOOPBACK_EN, REG_PORT_GENERAL_ADDR_OFFSET, 0);

    // Read ITEM_W and NIPC (read only)
    read_radio(radio_num, REG_RADIO_WIDTH, REG_PORT_GENERAL_ADDR_OFFSET, val);
    `ASSERT_ERROR(val[15:0] == NIPC, "Value of NIPC register is incorrect");
    `ASSERT_ERROR(val[31:16] == ITEM_W, "Value of ITEM_W register is incorrect");

    test.end_test();
  endtask : test_general_registers



  task test_rx_registers(int radio_num);
    logic [63:0] val, expected, radio_val_0, radio_val_1;
    localparam int num_words_len = RX_CMD_NUM_WORDS_LEN;

    test.start_test("Rx Registers", 50us);

    // REG_RX_CMD_STATUS (read only)
    expected = CMD_FIFO_SPACE_MAX;
    read_radio(radio_num, REG_RX_STATUS, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_RX_STATUS not initially CMD_FIFO_SPACE_MAX");

    // REG_RX_CMD (read/write). Test a bogus timed stop command just to check
    // read/write of the register.
    expected = 0;
    expected[RX_CMD_POS +: RX_CMD_LEN] = RX_CMD_STOP;
    expected[RX_CMD_TIMED_POS] = 1'b1;
    write_radio(radio_num, REG_RX_CMD, REG_PORT_RX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_RX_CMD, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_RX_CMD didn't update correctly");

    // REG_RX_CMD_NUM_WORDS (read/write)
    read_radio_64(radio_num, REG_RX_CMD_NUM_WORDS_LO, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_RX_CMD_NUM_WORDS not initially 0");
    expected = 64'hFEDCBA9876543210;
    write_radio_64(radio_num, REG_RX_CMD_NUM_WORDS_LO, REG_PORT_RX_ADDR_OFFSET, expected);
    read_radio_64(radio_num, REG_RX_CMD_NUM_WORDS_LO, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(
      val == expected[num_words_len-1:0],
     "REG_RX_CMD_NUM_WORDS didn't update correctly"
    );

    // REG_RX_CMD_TIME (read/write)
    read_radio_64(radio_num, REG_RX_CMD_TIME_LO, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_RX_CMD_TIME not initially 0");
    expected = 64'hBEADFEED0123F1FE;
    write_radio_64(radio_num, REG_RX_CMD_TIME_LO, REG_PORT_RX_ADDR_OFFSET, expected);
    read_radio_64(radio_num, REG_RX_CMD_TIME_LO, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_RX_CMD_TIME didn't update correctly");

    // REG_RX_MAX_WORDS_PER_PKT (read/write)
    read_radio(radio_num, REG_RX_MAX_WORDS_PER_PKT, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 64, "REG_RX_MAX_WORDS_PER_PKT not initially 64");
    expected = 32'hABBEC001;
    write_radio(radio_num, REG_RX_MAX_WORDS_PER_PKT, REG_PORT_RX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_RX_MAX_WORDS_PER_PKT, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_RX_MAX_WORDS_PER_PKT didn't update correctly");

    // REG_RX_ERR_PORT (read/write)
    read_radio(radio_num, REG_RX_ERR_PORT, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_RX_ERR_PORT not initially 0");
    expected = $urandom() & 32'h000001FF;
    write_radio(radio_num, REG_RX_ERR_PORT, REG_PORT_RX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_RX_ERR_PORT, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_RX_ERR_PORT didn't update correctly");

    // REG_RX_ERR_REM_PORT (read/write)
    read_radio(radio_num, REG_RX_ERR_REM_PORT, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_RX_ERR_REM_PORT not initially 0");
    expected = $urandom() & 32'h000001FF;
    write_radio(radio_num, REG_RX_ERR_REM_PORT, REG_PORT_RX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_RX_ERR_REM_PORT, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_RX_ERR_REM_PORT didn't update correctly");

    // REG_RX_ERR_REM_EPID (read/write)
    read_radio(radio_num, REG_RX_ERR_REM_EPID, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_RX_ERR_REM_EPID not initially 0");
    expected = $urandom() & 32'h0000FFFF;
    write_radio(radio_num, REG_RX_ERR_REM_EPID, REG_PORT_RX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_RX_ERR_REM_EPID, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_RX_ERR_REM_EPID didn't update correctly");

    // REG_RX_ERR_ADDR (read/write)
    read_radio(radio_num, REG_RX_ERR_ADDR, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_RX_ERR_ADDR not initially 0");
    expected = $urandom() & 32'h000FFFFF;
    write_radio(radio_num, REG_RX_ERR_ADDR, REG_PORT_RX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_RX_ERR_ADDR, REG_PORT_RX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_RX_ERR_ADDR didn't update correctly");

    // REG_RX_DATA (read-only)
    do begin
      // Loop until we get a valid sample from the register (not X's)
      do @(posedge radio_clk); while (!radio_rx_stb[radio_num]);
      radio_val_0 = radio_rx_data[RADIO_W*radio_num +: RADIO_W] & {32{1'b1}};
      read_radio(radio_num, REG_RX_DATA, REG_PORT_RX_ADDR_OFFSET, val);
      do @(posedge radio_clk); while (!radio_rx_stb[radio_num]);
      radio_val_1 = radio_rx_data[RADIO_W*radio_num +: RADIO_W] & {32{1'b1}};
    end while ($isunknown(val));
    `ASSERT_ERROR(
      radio_val_0 < val && val < radio_val_1,
      $sformatf({"REG_RX_DATA wasn't in the expected range\n",
        "Radio Value 0: 0x%X, Reg Value: 0x%X, Radio Value 1: 0x%X"},
        radio_val_0, val, radio_val_1)
    );

    test.end_test();
  endtask : test_rx_registers



  task automatic test_tx_registers(int radio_num);
    logic [31:0] val, expected;

    test.start_test("Tx Registers", 50us);

    // REG_TX_IDLE_VALUE (read/write)
    read_radio(radio_num, REG_TX_IDLE_VALUE, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_TX_IDLE_VALUE not initially 0");
    expected = $urandom() & {ITEM_W{1'b1}};
    write_radio(radio_num, REG_TX_IDLE_VALUE, REG_PORT_TX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_TX_IDLE_VALUE, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_TX_IDLE_VALUE didn't update correctly");

    // REG_TX_ERROR_POLICY (read/write)
    read_radio(radio_num, REG_TX_ERROR_POLICY, REG_PORT_TX_ADDR_OFFSET, val);
    expected = TX_ERR_POLICY_PACKET;
    `ASSERT_ERROR(val == expected, "REG_TX_ERROR_POLICY not initially 'PACKET'");
    expected = TX_ERR_POLICY_BURST;
    write_radio(radio_num, REG_TX_ERROR_POLICY, REG_PORT_TX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_TX_ERROR_POLICY, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_TX_ERROR_POLICY didn't update to 'BURST'");
    expected = TX_ERR_POLICY_PACKET;
    write_radio(radio_num, REG_TX_ERROR_POLICY, REG_PORT_TX_ADDR_OFFSET,
                32'h03);  // Try to set both bits!
    read_radio(radio_num, REG_TX_ERROR_POLICY, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_TX_ERROR_POLICY didn't revert to 'PACKET'");

    // REG_TX_ERR_PORT (read/write)
    read_radio(radio_num, REG_TX_ERR_PORT, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_TX_ERR_PORT not initially 0");
    expected = $urandom() & 32'h000001FF;
    write_radio(radio_num, REG_TX_ERR_PORT, REG_PORT_TX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_TX_ERR_PORT, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_TX_ERR_PORT didn't update correctly");

    // REG_TX_ERR_REM_PORT (read/write)
    read_radio(radio_num, REG_TX_ERR_REM_PORT, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_TX_ERR_REM_PORT not initially 0");
    expected = $urandom() & 32'h000001FF;
    write_radio(radio_num, REG_TX_ERR_REM_PORT, REG_PORT_TX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_TX_ERR_REM_PORT, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_TX_ERR_REM_PORT didn't update correctly");

    // REG_TX_ERR_REM_EPID (read/write)
    read_radio(radio_num, REG_TX_ERR_REM_EPID, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_TX_ERR_REM_EPID not initially 0");
    expected = $urandom() & 32'h0000FFFF;
    write_radio(radio_num, REG_TX_ERR_REM_EPID, REG_PORT_TX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_TX_ERR_REM_EPID, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_TX_ERR_REM_EPID didn't update correctly");

    // REG_TX_ERR_ADDR (read/write)
    read_radio(radio_num, REG_TX_ERR_ADDR, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == 0, "REG_TX_ERR_ADDR not initially 0");
    expected = $urandom() & 32'h000FFFFF;
    write_radio(radio_num, REG_TX_ERR_ADDR, REG_PORT_TX_ADDR_OFFSET, expected);
    read_radio(radio_num, REG_TX_ERR_ADDR, REG_PORT_TX_ADDR_OFFSET, val);
    `ASSERT_ERROR(val == expected, "REG_TX_ERR_ADDR didn't update correctly");

    test.end_test();
  endtask : test_tx_registers

  task automatic test_comp_gain_registers(int radio_num, bit is_tx);
    logic [31:0] val, expected;
    int tx_rx_addr_offset = is_tx ? REG_CGAIN_TX_OFFSET : REG_CGAIN_RX_OFFSET;

    // Test default coefficients
    read_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + tx_rx_addr_offset,
      val);
    expected = {16'h4000,  // Default real coefficient (1.0)
                16'h0000}; // Default imag coefficient (0.0)
    `ASSERT_ERROR(val == expected, "REG_CGAIN_COEFF not initially correct");

    // Test setting coefficients
    expected = Rand#(32)::rand_bit();
    write_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + tx_rx_addr_offset,
      expected);
    read_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + tx_rx_addr_offset,
      val);
    `ASSERT_ERROR(val == expected, "REG_CGAIN_COEFF didn't update correctly");

    // Reset coefficients to defaults
    write_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + tx_rx_addr_offset,
      {16'h4000, 16'h0000});

  endtask : test_comp_gain_registers

  task automatic test_feature_registers(int radio_num);
    logic [31:0] val, expected;

    test.start_test("Feature Registers", 10us);

    if (EN_COMP_GAIN_RX) begin
      // REG_FEATURES (read only)
      read_radio(radio_num, REG_FEATURES_PRESENT, REG_PORT_GENERAL_ADDR_OFFSET, val);
      expected = EN_COMP_GAIN_RX_MSK;
      `ASSERT_ERROR(val & EN_COMP_GAIN_RX_MSK == expected,
        "REG_FEATURES does not indicate COMP_GAIN_RX support.");

      test_comp_gain_registers(radio_num, 0);
    end;

    if (EN_COMP_GAIN_TX) begin
      // REG_FEATURES (read only)
      read_radio(radio_num, REG_FEATURES_PRESENT, REG_PORT_GENERAL_ADDR_OFFSET, val);
      expected = EN_COMP_GAIN_TX_MSK;
      `ASSERT_ERROR(val & EN_COMP_GAIN_TX_MSK == expected,
        "REG_FEATURES does not indicate COMP_GAIN_TX support.");

      test_comp_gain_registers(radio_num, 1);
    end;

    test.end_test();
  endtask : test_feature_registers

  task automatic test_rx(int radio_num);

    // Set default packet length
    write_radio(radio_num, REG_RX_MAX_WORDS_PER_PKT, REG_PORT_RX_ADDR_OFFSET, WPP);

    //---------------------
    // Finite Acquisitions
    //---------------------

    test.start_test("Rx (finite)", 50us);

    // Grab and verify a partial packet
    start_rx(radio_num, WPP/2);
    check_rx(radio_num, WPP/2);

    // Grab a minimally-sized packet
    start_rx(radio_num, 1);
    check_rx(radio_num, 1);

    // Grab and verify several packets
    start_rx(radio_num, WPP*15/2);
    check_rx(radio_num, WPP*15/2);

    // Wait long enough to receive another packet and then make sure we didn't
    // receive anything. That is, make sure Rx actually stopped.
    #MAX_PKT_WAIT;
    `ASSERT_ERROR(
      blk_ctrl.num_received(radio_num) == 0,
      "Received more packets than expected"
    );

    test.end_test();


    //-------------------------
    // Continuous Acquisitions
    //-------------------------

    test.start_test("Rx (continuous)", 100us);

    start_rx(radio_num);

    // Grab and verify several packets
    check_rx(radio_num, WPP*7);
    stop_rx(radio_num);

    // Grab and discard any remaining packets
    do begin
      while (blk_ctrl.num_received(radio_num) != 0) begin
        ChdrPacket #(CHDR_W) chdr_packet;
        blk_ctrl.get_chdr(radio_num, chdr_packet);
      end
      #MAX_PKT_WAIT;
    end while (blk_ctrl.num_received(radio_num) != 0);

    test.end_test();


    //--------------------------
    // Finite Timed Acquisition
    //--------------------------

    begin
      ChdrPacket #(CHDR_W) chdr_packet;
      bit [ITEM_W-1:0] expected_samp;
      bit [      63:0] new_time;
      bit [      63:0] expected_time;

      test.start_test("Rx (finite, timed)", 100us);

      // Set radio time and data so we know which sample value to expect
      radio_clk_gen.clk_wait_f();
      new_time = radio_time;
      radio_gen.set_time(new_time);
      radio_gen.set_data(radio_num, radio_init(new_time));
      radio_clk_gen.clk_wait_f();

      // Send Rx command with time in the future
      expected_time = new_time + RX_CMD_DELAY;
      start_rx_timed(radio_num, WPP, expected_time);

      // Take a peak at the timestamp in the received packet to check it
      blk_ctrl.peek_chdr(radio_num, chdr_packet);
      `ASSERT_ERROR(
        chdr_packet.timestamp == expected_time,
        $sformatf({"Received packet didn't have expected timestamp.\n",
          "Expected 0x%X, Received 0x%X"}, expected_time, chdr_packet.timestamp)
      );
      expected_samp = expected_time[ITEM_W-1:0] - RADIO_RX_LATENCY*NIPC;
      `ASSERT_ERROR(
        chdr_packet.data[0][0+:ITEM_W] == expected_samp,
        $sformatf({"Received packet didn't have expected start value.\n",
          "Expected 0x%X, Received 0x%X"}, expected_samp,
          chdr_packet.data[0][0+:ITEM_W])
      );

      // Verify the packet data
      check_rx(radio_num, WPP);
      test.end_test();
    end


    //------------------------------
    // Continuous Timed Acquisition
    //------------------------------

    begin
      ChdrPacket #(CHDR_W) chdr_packet;
      bit [ITEM_W-1:0] expected_samp;
      bit [      63:0] new_time;
      bit [      63:0] expected_time;

      test.start_test("Rx (continuous, timed)", 100us);

      // Set radio time and data so we know which sample value to expect
      radio_clk_gen.clk_wait_f();
      new_time = radio_time;
      radio_gen.set_time(new_time);
      radio_gen.set_data(radio_num, radio_init(new_time));
      radio_clk_gen.clk_wait_f();

      // Send Rx command with time in the future
      expected_time = new_time + RX_CMD_DELAY;
      start_rx_timed(radio_num, 0, expected_time);

      // Take a peak at the timestamp in the received packet to check it
      blk_ctrl.peek_chdr(radio_num, chdr_packet);
      `ASSERT_ERROR(
        chdr_packet.timestamp == expected_time,
        "Received packet didn't have expected timestamp"
      );
      expected_samp = expected_time[ITEM_W-1:0] - RADIO_RX_LATENCY*NIPC;
      `ASSERT_ERROR(
        chdr_packet.data[0][0+:ITEM_W] == expected_samp,
        $sformatf({"Received packet didn't have expected start value.\n",
          "Expected 0x%X, Received 0x%X"}, expected_samp,
          chdr_packet.data[0][0+:ITEM_W])
      );

      // Verify a few packets
      check_rx(radio_num, WPP*3);
      stop_rx(radio_num);

      // Grab and discard any remaining packets
      do begin
        while (blk_ctrl.num_received(radio_num) != 0) begin
          ChdrPacket #(CHDR_W) chdr_packet;
          blk_ctrl.get_chdr(radio_num, chdr_packet);
        end
        #(MAX_PKT_WAIT);
      end while (blk_ctrl.num_received(radio_num) != 0);

      test.end_test();
    end


    //--------------------------
    // RX Sample Time Alignment
    //--------------------------

    if (NIPC > 1) begin
      ChdrPacket #(CHDR_W) chdr_packet;
      localparam int NUM_WORDS = 4;
      bit [ALIGN_W-1:0] radio_align;     // Radio alignment
      bit [ALIGN_W-1:0] req_align;       // Request alignment
      bit [       63:0] new_time;
      bit [       63:0] expected_time;
      bit [ ITEM_W-1:0] expected_samp;

      test.start_test("Rx (time alignment)", NIPC*NIPC*10us);

      // Iterate over all possible alignments
      repeat(2**ALIGN_W) begin
        repeat(2**ALIGN_W) begin
          if (VERBOSE) $display("Testing Rx alignment radio: %0d, request: %0d",
            radio_align, req_align);

          // Set radio alignment and set the data to be the same as the time to
          // make it easier to validate.
          radio_clk_gen.clk_wait_f();
          new_time = radio_time;
          new_time[ALIGN_W-1:0] = radio_align;
          radio_gen.set_time(new_time);
          radio_gen.set_data(radio_num, radio_init(new_time));
          radio_clk_gen.clk_wait_f();

          // Create future time that's aligned for our request
          expected_time = new_time + RX_CMD_DELAY;
          expected_time[ALIGN_W-1:0] = req_align;

          // Send Rx command with time in the future
          start_rx_timed(radio_num, NUM_WORDS, expected_time);
          blk_ctrl.peek_chdr(radio_num, chdr_packet);

          // Check the timestamp in the received packet
          `ASSERT_ERROR(
            chdr_packet.timestamp == expected_time,
            $sformatf({"Received packet didn't have expected timestamp.\n",
              "Expected 0x%X, Received 0x%X"}, expected_time,
              chdr_packet.timestamp)
          );
          // Check the first sample to make sure it matches the sample for the
          // requested time.
          expected_samp = expected_time[ITEM_W-1:0] - RADIO_RX_LATENCY*NIPC;
          `ASSERT_ERROR(
            chdr_packet.data[0][0+:ITEM_W] == expected_samp,
            $sformatf({"Received packet didn't have expected start value.\n",
              "Expected 0x%X, Received 0x%X"}, expected_samp,
              chdr_packet.data[0][0+:ITEM_W])
          );

          // Verify the rest of the packet
          check_rx(radio_num, NUM_WORDS);
          req_align++;
        end
        radio_align++;
      end

      // Reset the radio time and outputs
      radio_gen.set_time(0);
      radio_gen.set_data_all(radio_gen.radio_init_all(0));
      radio_clk_gen.clk_wait_r(2);

      test.end_test();
    end


    //-------------
    // Rx Overflow
    //-------------

    begin
      logic [31:0] val;

      test.start_test("Rx (now, overflow)", 200us);

      // Configure the error reporting registers
      write_radio(radio_num, REG_RX_ERR_PORT, REG_PORT_RX_ADDR_OFFSET, TX_ERR_DST_PORT);
      write_radio(radio_num, REG_RX_ERR_REM_PORT, REG_PORT_RX_ADDR_OFFSET, TX_ERR_REM_DST_PORT);
      write_radio(radio_num, REG_RX_ERR_REM_EPID, REG_PORT_RX_ADDR_OFFSET, TX_ERR_REM_DST_EPID);
      write_radio(radio_num, REG_RX_ERR_ADDR, REG_PORT_RX_ADDR_OFFSET, TX_ERR_ADDRESS);

      // Stall the BFM to force a backup of data
      blk_ctrl.set_slave_stall_prob(radio_num, 100);

      // Acquire continuously until we get an error
      start_rx(radio_num);

      // Check that we're acquiring
      read_radio(radio_num, REG_RX_STATUS, REG_PORT_RX_ADDR_OFFSET, val);
      `ASSERT_ERROR(
        val[CMD_FIFO_SPACE_POS +: CMD_FIFO_SPACE_LEN] != CMD_FIFO_SPACE_MAX,
        "Rx radio reports that it is not busy"
      );

      // Verify that we receive an error
      check_error(ERR_RX_OVERRUN);

      // Restore the BFM stall probability
      blk_ctrl.set_slave_stall_prob(radio_num, STALL_PROB);

      // Verify that Rx stopped
      read_radio(radio_num, REG_RX_STATUS, REG_PORT_RX_ADDR_OFFSET, val);
      `ASSERT_ERROR(
        val[CMD_FIFO_SPACE_POS +: CMD_FIFO_SPACE_LEN] == CMD_FIFO_SPACE_MAX,
        "Rx radio reports that it is still busy after overflow"
      );

      // Discard any packets we received. Rx should eventually stop
      // automatically after an overflow.
      do begin
        while (blk_ctrl.num_received(radio_num) != 0) begin
          ChdrPacket #(CHDR_W) chdr_packet;
          blk_ctrl.get_chdr(radio_num, chdr_packet);
        end
        #(MAX_PKT_WAIT);
      end while (blk_ctrl.num_received(radio_num) != 0);

     test.end_test();
    end


    //--------------
    // Late Command
    //--------------

    test.start_test("Rx (timed, late)", 100us);

    start_rx_timed(radio_num, WPP, radio_time);
    check_error(ERR_RX_LATE_CMD);

    // Late command should be ignored. Make sure we didn't receive any packets.
    begin
      ChdrPacket #(CHDR_W) chdr_packet;
      #(MAX_PKT_WAIT);
      `ASSERT_ERROR(
        blk_ctrl.num_received(radio_num) == 0,
        "Packets received for late Rx command"
      );

      // Discard any remaining packets
      while (blk_ctrl.num_received(radio_num)) blk_ctrl.get_chdr(radio_num, chdr_packet);
    end

    test.end_test();


    //---------------
    // Command Queue
    //---------------

    test.start_test("Rx (queue multiple commands)");

    begin
      logic [31:0] expected, val;

      // Send one continuous command and verify the queue fullness
      start_rx(radio_num);
      expected = CMD_FIFO_SPACE_MAX-1;
      read_radio(radio_num, REG_RX_STATUS, REG_PORT_RX_ADDR_OFFSET, val);
      `ASSERT_ERROR(
        val[CMD_FIFO_SPACE_POS+:CMD_FIFO_SPACE_LEN] == expected,
        "CMD_FIFO_SPACE did not decrement"
      );

      // Fill the command FIFO
      for (int i = 0; i < CMD_FIFO_SPACE_MAX - 1; i++) begin
        start_rx(radio_num, WPP);
      end
      expected = 0;
      read_radio(radio_num, REG_RX_STATUS, REG_PORT_RX_ADDR_OFFSET, val);
      `ASSERT_ERROR(
        val[CMD_FIFO_SPACE_POS+:CMD_FIFO_SPACE_LEN] == expected,
        "CMD_FIFO_SPACE did not reach 0"
      );

      // Issue stop command and verify that the FIFO empties
      stop_rx(radio_num);
      expected = CMD_FIFO_SPACE_MAX;
      read_radio(radio_num, REG_RX_STATUS, REG_PORT_RX_ADDR_OFFSET, val);
      `ASSERT_ERROR(
        val[CMD_FIFO_SPACE_POS+:CMD_FIFO_SPACE_LEN] == expected,
        "CMD_FIFO_SPACE did not return to max"
      );

      // Grab and discard any remaining packets
      do begin
        while (blk_ctrl.num_received(radio_num) != 0) begin
          ChdrPacket #(CHDR_W) chdr_packet;
          blk_ctrl.get_chdr(radio_num, chdr_packet);
        end
        #MAX_PKT_WAIT;
      end while (blk_ctrl.num_received(radio_num) != 0);

      // Queue several long commands back-to-back and make sure they all
      // complete. The lengths are unique to ensure we execute the right
      // commands in the expected order.
      for (int i = 0; i < 3; i++) start_rx(radio_num, WPP*20+i);
      for (int i = 0; i < 3; i++) check_rx(radio_num, WPP*20+i);

      // Make sure we don't get any more data
      do begin
        while (blk_ctrl.num_received(radio_num) != 0) begin
          `ASSERT_ERROR(0, "Received unexpected packets");
        end
        #MAX_PKT_WAIT;
      end while (blk_ctrl.num_received(radio_num) != 0);
    end

    test.end_test();

  endtask : test_rx



  task automatic test_tx(int radio_num);
    logic [RADIO_W-1:0] radio_data;
    enum { WAIT_FOR_EOP, WAIT_FOR_EOB } policy;

    //-------
    // Setup
    //-------

    test.start_test("Tx Init", 50us);

    // Configure the error reporting registers
    write_radio(radio_num, REG_TX_ERR_PORT, REG_PORT_TX_ADDR_OFFSET, TX_ERR_DST_PORT);
    write_radio(radio_num, REG_TX_ERR_REM_PORT, REG_PORT_TX_ADDR_OFFSET, TX_ERR_REM_DST_PORT);
    write_radio(radio_num, REG_TX_ERR_REM_EPID, REG_PORT_TX_ADDR_OFFSET, TX_ERR_REM_DST_EPID);
    write_radio(radio_num, REG_TX_ERR_ADDR, REG_PORT_TX_ADDR_OFFSET, TX_ERR_ADDRESS);

    test.end_test();


    //---------------
    // Test Tx (now)
    //---------------

    test.start_test("Tx (now)", 50us);

    // Grab and verify a partial packet
    start_tx(radio_num, WPP*3/4);
    check_tx(radio_num, WPP*3/4);
    check_error(ERR_TX_EOB_ACK);

    // Grab and verify multiple packets
    start_tx(radio_num, WPP*3/2);
    check_tx(radio_num, WPP*3/2);
    check_error(ERR_TX_EOB_ACK);

    // Test a minimally-sized packet
    start_tx(radio_num, 1);
    check_tx(radio_num, 1);
    check_error(ERR_TX_EOB_ACK);

    test.end_test();


    //---------------------
    // Test Tx (underflow)
    //---------------------

    test.start_test("Tx (now, underflow)", 50us);

    write_radio(radio_num, REG_TX_ERROR_POLICY, REG_PORT_TX_ADDR_OFFSET, TX_ERR_POLICY_PACKET);

    // Send some bursts without EOB
    start_tx(radio_num, WPP*3/4, 1, 0);  // Skip EOB
    check_tx(radio_num, WPP*3/4);
    check_error(ERR_TX_UNDERRUN);

    start_tx(radio_num, WPP*2, 1, 0);  // Skip EOB
    check_tx(radio_num, WPP*2);
    check_error(ERR_TX_UNDERRUN);

    test.end_test();


    //-----------------
    // Test Tx (timed)
    //-----------------

    test.start_test("Tx (timed)", 50us);

    // Grab and verify a partial packet
    start_tx_timed(radio_num, WPP*3/4, radio_time + TX_CMD_DELAY);
    check_tx_timed(radio_num, WPP*3/4, radio_time + TX_CMD_DELAY);
    check_error(ERR_TX_EOB_ACK);

    // Grab and verify whole packets
    start_tx_timed(radio_num, WPP*2, radio_time + TX_CMD_DELAY);
    check_tx_timed(radio_num, WPP*2, radio_time + TX_CMD_DELAY);
    check_error(ERR_TX_EOB_ACK);

    test.end_test();


    //----------------------------
    // Test Tx (timed, underflow)
    //----------------------------

    test.start_test("Tx (timed, underflow)", 50us);

    // Send some bursts without EOB
    start_tx_timed(radio_num, WPP*3/4, radio_time + TX_CMD_DELAY, 1, 0);
    check_tx_timed(radio_num, WPP*3/4, radio_time + TX_CMD_DELAY);
    check_error(ERR_TX_UNDERRUN);

    start_tx_timed(radio_num, WPP*2, radio_time + TX_CMD_DELAY, 1, 0);
    check_tx_timed(radio_num, WPP*2, radio_time + TX_CMD_DELAY);
    check_error(ERR_TX_UNDERRUN);

    test.end_test();


    //-------------------------------
    // Test Tx Sample Time Alignment
    //-------------------------------

    if (NIPC > 1) begin
      ChdrPacket #(CHDR_W) chdr_packet;
      localparam int NUM_WORDS = 4;
      bit [ALIGN_W-1:0] radio_align;     // Radio alignment
      bit [ALIGN_W-1:0] req_align;       // Request alignment
      bit [       63:0] new_time;
      bit [       63:0] expected_time;
      bit [ ITEM_W-1:0] expected_samp;


      test.start_test("Tx (time alignment)", 200us);

      // Iterate over all possible alignments
      repeat(2**ALIGN_W) begin
        repeat(2**ALIGN_W) begin
          if (VERBOSE) $display("Testing Tx alignment radio: %0d, request: %0d",
            radio_align, req_align);

          // Set radio alignment
          radio_clk_gen.clk_wait_f();
          new_time = radio_time;
          new_time[ALIGN_W-1:0] = radio_align;
          radio_gen.set_time(new_time);
          radio_clk_gen.clk_wait_f();

          // Create future time that's aligned for our request
          expected_time = new_time + TX_CMD_DELAY;
          expected_time[ALIGN_W-1:0] = req_align;

          // Transmit and verify the output
          start_tx_timed(radio_num, NUM_WORDS, expected_time);
          check_tx_timed(radio_num, NUM_WORDS, expected_time);
          check_error(ERR_TX_EOB_ACK);

          req_align++;
        end
        radio_align++;
      end

      // Reset the radio time and outputs
      radio_gen.set_time(0);
      radio_gen.set_data_all(radio_gen.radio_init_all(0));
      radio_clk_gen.clk_wait_r(2);

      test.end_test();
    end


    //---------------------------
    // Test Tx (timed, late)
    //---------------------------

    test.start_test("Tx (timed, late)", 50us);

    // Test each error policy
    policy = policy.first();
    do begin
      // Set the policy
      if (policy == WAIT_FOR_EOP) begin
        write_radio(radio_num, REG_TX_ERROR_POLICY, REG_PORT_TX_ADDR_OFFSET, TX_ERR_POLICY_PACKET);
      end else if (policy == WAIT_FOR_EOB) begin
        write_radio(radio_num, REG_TX_ERROR_POLICY, REG_PORT_TX_ADDR_OFFSET, TX_ERR_POLICY_BURST);
      end

      radio_data = radio_tx_data[radio_num];
      fork : tx_fork
        // In this branch of the fork, we send the packets
        repeat (2) begin
          // Send late packets with random start value
          start_tx_timed(radio_num, WPP*3, 0, $urandom());

          if (policy == WAIT_FOR_EOP) begin
            // We should get three errors, one for each packet
            repeat (3) check_error(ERR_TX_LATE_DATA);
          end else if (policy == WAIT_FOR_EOB) begin
            // We should get one error for the entire burst
            check_error(ERR_TX_LATE_DATA);
          end
        end

        // The packets sent in the above branch of the fork should be
        // dropped. In this branch of the fork we make sure that the Tx
        // output doesn't change.
        begin
          forever begin
            @(posedge radio_clk)
            if (radio_tx_stb[radio_num]) begin
              `ASSERT_ERROR(
                radio_data === radio_tx_data[radio_num],
                "Radio Tx output changed when late Tx packet should have been ignored"
              );
            end
          end
        end
      join_any

      // Stop checking the output
      disable tx_fork;

      policy = policy.next();
    end while (policy != policy.first());

    // Make sure good transmissions can go through now.
    start_tx_timed(radio_num, WPP, radio_time + TX_CMD_DELAY);
    check_tx_timed(radio_num, WPP, radio_time + TX_CMD_DELAY);
    check_error(ERR_TX_EOB_ACK);

    test.end_test();

  endtask : test_tx



  // Test internal loopback and idle value
  task automatic test_loopback_and_idle(int radio_num);
    int              byte_length;
    chdr_word_t      data[$];
    bit [ITEM_W-1:0] idle;

    //----------------------------
    // Use IDLE value to loopback
    //----------------------------

    test.start_test("Idle Loopback", 50us);

    // Turn on loopback
    write_radio(radio_num, REG_LOOPBACK_EN, REG_PORT_GENERAL_ADDR_OFFSET, 1);

    // This test ensures we get the Tx output on Rx and not the TB's simulated
    // radio data. It also tests updating the idle value. Run the test twice to
    // make sure the IDLE value updates.
    repeat (2) begin
      // Set idle value
      idle = $urandom();
      write_radio(radio_num, REG_TX_IDLE_VALUE, REG_PORT_TX_ADDR_OFFSET, idle);

      // Grab a radio word and check that it equals the IDLE value
      write_radio_64(radio_num, REG_RX_CMD_NUM_WORDS_LO, REG_PORT_RX_ADDR_OFFSET, WPP);
      write_radio(radio_num, REG_RX_CMD, REG_PORT_RX_ADDR_OFFSET, RX_CMD_FINITE);
      blk_ctrl.recv(radio_num, data, byte_length);

      // Check the length
      `ASSERT_ERROR(byte_length == WPP*RADIO_W/8, "Didn't receive expected length");

      // Check the payload
      foreach (data[i]) begin
        chdr_word_t word;
        word = data[i];   // Work around Vivado 2018.3 issue
        `ASSERT_ERROR(
          word == {$bits(chdr_word_t)/ITEM_W{idle}},
          "Loopback data didn't match expected"
        );
      end
    end

    test.end_test();


    //---------------------
    // Loopback Tx packets
    //---------------------

    test.start_test("Tx Loopback", 50us);

    // This test ensures that loopback isn't reordering words or anything else
    // unexpected.

    // Configure the Tx error reporting registers
    write_radio(radio_num, REG_TX_ERR_PORT, REG_PORT_TX_ADDR_OFFSET, TX_ERR_DST_PORT);
    write_radio(radio_num, REG_TX_ERR_REM_PORT, REG_PORT_TX_ADDR_OFFSET, TX_ERR_REM_DST_PORT);
    write_radio(radio_num, REG_TX_ERR_REM_EPID, REG_PORT_TX_ADDR_OFFSET, TX_ERR_REM_DST_EPID);
    write_radio(radio_num, REG_TX_ERR_ADDR, REG_PORT_TX_ADDR_OFFSET, TX_ERR_ADDRESS);

    // Set packet length
    write_radio(radio_num, REG_RX_MAX_WORDS_PER_PKT, REG_PORT_RX_ADDR_OFFSET, WPP);

    // Loopback a few packets, back-to-back. This code has a race condition
    // since there's a delay between when we start Tx and when Rx starts, due
    // to how long it takes to write the Rx registers. Therefore, we transmit a
    // lot more packets than we receive to ensure we're still transmitting by
    // the time we receive.
    start_tx(radio_num, WPP*20);
    start_rx(radio_num, WPP*2);

    // Check the results
    check_rx(radio_num, WPP*2);
    check_error(ERR_TX_EOB_ACK);

    // Turn off loopback
    write_radio(radio_num, REG_LOOPBACK_EN, REG_PORT_GENERAL_ADDR_OFFSET, 0);

    test.end_test();
  endtask : test_loopback_and_idle;

  task automatic test_complex_gain_tx(int radio_num);
    logic [31:0] start_val = 1;
    chdr_timestamp_t ts, ts_gain;
    complex_t test_gain_coeffs[4] =
      '{'{1.0, 0.0}, '{0.5, 0.5}, '{-0.5, -0.5}, '{0.0, 1.0}};
    bit unsigned [ALIGN_W-1:0] radio_align = 0;     // Radio alignment
    bit unsigned [ALIGN_W-1:0] req_align   = 0;     // Request alignment
    chdr_timestamp_t new_time;

    // Comp Gain Tx - untimed

    test.start_test("Comp Gain TX (now)", 50us);

    // Set default packet length
    write_radio(radio_num, REG_RX_MAX_WORDS_PER_PKT, REG_PORT_RX_ADDR_OFFSET, WPP);

    foreach (test_gain_coeffs[index]) begin
      // Set coefficients
      write_radio(radio_num,
        REG_CGAIN_COEFF,
        REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_TX_OFFSET,
        complex_to_fixed(test_gain_coeffs[index]));
      if (VERBOSE) begin
        $display("Radio %0d Tx Comp Gain Coefficients: %0f + j%0f",
          radio_num,
          test_gain_coeffs[index].re,
          test_gain_coeffs[index].im);
      end
      // Grab and verify a partial packet
      start_tx(radio_num, WPP*4, start_val);
      check_tx(radio_num, WPP*4, start_val, test_gain_coeffs[index]);
      check_error(ERR_TX_EOB_ACK);
    end
    // Reset coefficients to defaults
    write_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_TX_OFFSET,
      DEFAULT_GAIN_FXP);

    test.end_test();

    // Comp Gain Tx - timed
    test.start_test("Comp Gain TX (timed)", 100us);

    foreach (test_gain_coeffs[index]) begin
      while ($isunknown(radio_time)) @(posedge radio_clk);
      ts = radio_time + NIPC*TX_CMD_DELAY;
      write_radio(radio_num,
        REG_CGAIN_COEFF,
        REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_TX_OFFSET,
        complex_to_fixed(test_gain_coeffs[index]),
        ts + CORE_TX_LATENCY * NIPC
      );
      if (VERBOSE) begin
        $display("Radio %0d Tx Comp Gain Coefficients: %0f + j%0f",
          radio_num,
          test_gain_coeffs[index].re,
          test_gain_coeffs[index].im);
      end
      start_tx_timed(radio_num, WPP*4, ts, start_val);
      check_tx_timed(radio_num, WPP*4, ts, start_val, test_gain_coeffs[index]);
      check_error(ERR_TX_EOB_ACK);
    end

    // Reset coefficients to defaults
    write_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_TX_OFFSET,
      DEFAULT_GAIN_FXP);

    test.end_test();

    // Skip if NIPC=1 since alignment doesn't matter
    if (NIPC == 1) return;
    // Comp Gain Tx - timed, aligned
    test.start_test("Comp Gain TX (timed, aligned)", NIPC*200us);
    repeat (NIPC) begin : align_loop_radio
      repeat (NIPC) begin : align_loop_request
        foreach (test_gain_coeffs[index]) begin
          // Set radio alignment
          while ($isunknown(radio_time)) begin
            radio_clk_gen.clk_wait_f();
          end
          new_time = radio_time;
          new_time[ALIGN_W-1:0] = radio_align;
          radio_gen.set_time(new_time);
          radio_gen.set_data(radio_num, radio_init(new_time));
          radio_clk_gen.clk_wait_f();
          ts = new_time + NIPC*TX_CMD_DELAY;
          ts[ALIGN_W-1:0] = req_align;
          // Set gain to be applied halfway through the packet.
          ts_gain = ts + SPP*2;
          write_radio(radio_num,
            REG_CGAIN_COEFF,
            REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_TX_OFFSET,
            complex_to_fixed(test_gain_coeffs[index]),
            ts_gain + CORE_TX_LATENCY * NIPC
          );
          if (VERBOSE) begin
            $display({"Radio %0d Tx Comp Gain : %0f + j%0f, req align %0d, radio align %0d"},
              radio_num,
              test_gain_coeffs[index].re,
              test_gain_coeffs[index].im,
              req_align,
              radio_align);
          end
          start_tx_timed(radio_num, WPP*4, ts, start_val);
          check_tx_timed(radio_num, WPP*4, ts, start_val, test_gain_coeffs[index], SPP*2);
          check_error(ERR_TX_EOB_ACK);

          // Reset coefficients to defaults
          write_radio(radio_num,
            REG_CGAIN_COEFF,
            REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_TX_OFFSET,
            DEFAULT_GAIN_FXP);
        end
        req_align++;
      end
      radio_align++;
    end


    // Reset the radio time and outputs
    radio_gen.set_time(0);
    radio_gen.set_data_all(radio_gen.radio_init_all(0));
    radio_clk_gen.clk_wait_r(2);

    // Reset coefficients to defaults
    write_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_TX_OFFSET,
      DEFAULT_GAIN_FXP);

    test.end_test();

  endtask : test_complex_gain_tx

  task automatic test_complex_gain_rx(int radio_num);
    chdr_timestamp_t ts, ts_gain;
    complex_t test_gain_coeffs[4] =
      '{'{1.0, 0.0}, '{0.5, 0.5}, '{-0.5, -0.5}, '{0.0, 1.0}};
    bit [ALIGN_W-1:0] radio_align = 0;     // Radio alignment
    bit [ALIGN_W-1:0] req_align   = 0;     // Request alignment
    chdr_timestamp_t new_time;

    // Comp Gain RX - untimed

    test.start_test("Comp Gain RX (now)", NIPC * 50us);

    // Set default packet length
    write_radio(radio_num, REG_RX_MAX_WORDS_PER_PKT, REG_PORT_RX_ADDR_OFFSET, WPP);

    foreach (test_gain_coeffs[index]) begin
      // Set coefficients
      write_radio(radio_num,
        REG_CGAIN_COEFF,
        REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_RX_OFFSET,
        complex_to_fixed(test_gain_coeffs[index]));
      if (VERBOSE) begin
        $display("Radio %0d Rx Comp Gain Coefficients: %0f + j%0f",
          radio_num,
          test_gain_coeffs[index].re,
          test_gain_coeffs[index].im);
      end
      // Grab and verify a partial packet
      start_rx(radio_num, WPP*4);
      check_rx(radio_num, WPP*4, test_gain_coeffs[index]);
    end
    // Reset coefficients to defaults
    write_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_RX_OFFSET,
      DEFAULT_GAIN_FXP);

    test.end_test();

    // Comp Gain RX - timed
    test.start_test("Comp Gain RX (timed)",(NIPC * 50us));

    foreach (test_gain_coeffs[index]) begin
      while ($isunknown(radio_time)) @(posedge radio_clk);
      ts = radio_time + NIPC*RX_CMD_DELAY;
      write_radio(radio_num,
        REG_CGAIN_COEFF,
        REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_RX_OFFSET,
        complex_to_fixed(test_gain_coeffs[index]),
        ts - (RADIO_RX_LATENCY*NIPC)
      );
      if (VERBOSE) begin
        $display("Radio %0d Rx Comp Gain Coefficients: %0f + j%0f",
          radio_num,
          test_gain_coeffs[index].re,
          test_gain_coeffs[index].im);
      end
      start_rx_timed(radio_num, WPP*4, ts);
      check_rx(radio_num, WPP*4, test_gain_coeffs[index]);
    end
    // Reset coefficients to defaults
    write_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_RX_OFFSET,
      DEFAULT_GAIN_FXP);

    test.end_test();

    // Skip if NIPC=1 since alignment doesn't matter
    if (NIPC == 1) return;
    // Comp Gain RX - timed, aligned
    test.start_test("Comp Gain RX (timed, aligned)", NIPC * 300us);
    repeat (NIPC) begin : align_loop_radio
      repeat (NIPC) begin : align_loop
        foreach (test_gain_coeffs[index]) begin
          // Set radio alignment
          while ($isunknown(radio_time)) begin
            radio_clk_gen.clk_wait_f();
          end
          new_time = radio_time;
          new_time[ALIGN_W-1:0] = radio_align;
          radio_gen.set_time(new_time);
          radio_gen.set_data(radio_num, radio_init(new_time));
          radio_clk_gen.clk_wait_f();
          // Set request alignment
          ts = new_time + RX_CMD_DELAY;
          ts[ALIGN_W-1:0] = req_align;
          // Set gain to be applied halfway through the packet.
          ts_gain = ts + SPP*2;
          write_radio(radio_num,
            REG_CGAIN_COEFF,
            REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_RX_OFFSET,
            complex_to_fixed(test_gain_coeffs[index]),
            ts_gain - (RADIO_RX_LATENCY*NIPC)
          );
          if (VERBOSE) begin
            $display({"Radio %0d Rx Comp Gain: %0f + j%0f,",
              " req align %0d, radio align %0d"},
              radio_num,
              test_gain_coeffs[index].re,
              test_gain_coeffs[index].im,
              req_align,
              radio_align);
          end
          start_rx_timed(radio_num, WPP*4, ts);
          check_rx(radio_num, WPP*4, test_gain_coeffs[index], ts_gain - ts);
          // Reset coefficients to defaults
          write_radio(radio_num,
            REG_CGAIN_COEFF,
            REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_RX_OFFSET,
            DEFAULT_GAIN_FXP);
        end
        req_align++;
      end
      radio_align++;
    end


    // Reset the radio time and outputs
    radio_gen.set_time(0);
    radio_gen.set_data_all(radio_gen.radio_init_all(0));
    radio_clk_gen.clk_wait_r(2);

    // Reset coefficients to defaults
    write_radio(radio_num,
      REG_CGAIN_COEFF,
      REG_PORT_FEAT_ADDR_OFFSET + REG_CGAIN_RX_OFFSET,
      DEFAULT_GAIN_FXP);
    test.end_test();

  endtask : test_complex_gain_rx



  //---------------------------------------------------------------------------
  // Test Process
  //---------------------------------------------------------------------------

  timeout_t timeout;

  initial begin : main
    string tb_name;

    //-------------------------------------------------------------------------
    // Initialization
    //-------------------------------------------------------------------------

    // Generate a string for the name of this instance of the testbench
    tb_name = $sformatf(
      {"rfnoc_block_radio_tb\nCHDR_W = %0D, ITEM_W = %0D, NIPC = %0D, ",
      "NUM_PORTS = %0D, STALL_PROB = %0D, STB_PROB = %0D, TEST_REGS = %0D",
      "\nEN_COMP_GAIN_TX = %0D, EN_COMP_GAIN_RX = %0D"},
      CHDR_W, ITEM_W, NIPC, NUM_PORTS, STALL_PROB, STB_PROB, TEST_REGS,
      EN_COMP_GAIN_TX, EN_COMP_GAIN_RX
    );

    test.start_tb(tb_name, NUM_TESTS*5ms);

    // Don't start the clocks until after start_tb() returns. This ensures that
    // the clocks aren't toggling while other instances of this testbench are
    // running, which speeds up simulation time.
    rfnoc_chdr_clk_gen.start();
    rfnoc_ctrl_clk_gen.start();
    radio_clk_gen.start();

    // Start the BFMs running
    blk_ctrl.run();


    //-------------------------------------------------------------------------
    // Reset
    //-------------------------------------------------------------------------

    test.start_test("Flush block then reset it", 10us);
    blk_ctrl.flush_and_reset();
    test.end_test();


    //-------------------------------------------------------------------------
    // Test Sequences
    //-------------------------------------------------------------------------

    // Run register tests first, since they check that initial values are
    // correct.

    test_block_info();
    if (TEST_REGS) test_shared_registers();

    for (int radio_num = 0; radio_num < NUM_PORTS; radio_num++) begin
      $display("************************************************************");
      $display("Testing Radio Channel %0d", radio_num);
      $display("************************************************************");
      if (TEST_REGS) begin
        test_general_registers(radio_num);
        test_rx_registers(radio_num);
        test_tx_registers(radio_num);
        test_feature_registers(radio_num);
      end
      repeat (NUM_TESTS) begin
        test_rx(radio_num);
        test_tx(radio_num);
        test_loopback_and_idle(radio_num);
        if (EN_COMP_GAIN_TX) test_complex_gain_tx(radio_num);
        if (EN_COMP_GAIN_RX) test_complex_gain_rx(radio_num);
      end
    end


    //-------------------------------------------------------------------------
    // Finish
    //-------------------------------------------------------------------------

    // End the TB, but don't $finish, since we don't want to kill other
    // instances of this testbench that may be running.
    test.end_tb(0);

    // Kill the clocks to end this instance of the testbench
    rfnoc_chdr_clk_gen.kill();
    rfnoc_ctrl_clk_gen.kill();
    radio_clk_gen.kill();

  end : main

endmodule : rfnoc_block_radio_tb
