uhd/fpga/usrp3/lib/dsp/cic_interpolator.sv

322 lines
10 KiB
Systemverilog

//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: cic_interpolator
//
// Description:
//
// A 1-to-N CIC interpolator stage that produces N output words for every
// input word. This interpolation factor can be changed at runtime.
//
// The interpolation factor can be safely updated between packets while the
// module is idle, or after reset is asserted and before data arrives.
// Updating the interpolation factor takes a few clock cycles to complete
// internally and no data should be input during that time. Updating the
// interpolation factor while the module is processing data results in
// undefined output.
//
// The interpolation factor is driven directly on interp_factor. It must be
// valid before reset is released and remain stable while data is processed.
//
// Parameters:
//
// SAMP_W : Bit width of each input/output sample.
// SPC : Number of samples processed per clock cycle.
// R_MAX : Maximum interpolation factor supported. Used to size internal
// registers.
//
// Signals:
//
// clk : Clock
// rst : Reset
// data_in : AXI-Stream data input
// data_out : AXI-Stream data output
// interp_factor : Interpolation factor to use.
//
`default_nettype none
module cic_interpolator #(
parameter int SAMP_W = 32,
parameter int SPC = 4,
parameter int R_MAX = 255,
localparam int FACTOR_W = $clog2(R_MAX + 1)
) (
input wire clk,
input wire rst,
AxiStreamIf.slave data_in,
AxiStreamIf.master data_out,
input wire [FACTOR_W-1:0] interp_factor
);
`include "usrp_utils.svh"
localparam int WORD_IDX_W = `MAX(1, $clog2(R_MAX));
localparam int SAMP_IDX_W = `MAX(1, $clog2(SPC));
//---------------------------------------------------------------------------
// Assertions
//---------------------------------------------------------------------------
if (SPC < 1 || SPC != 2**$clog2(SPC)) begin : gen_spc_check
$error(
"cic_interpolator: SPC must be a power of 2 and at least 1. Got SPC=%0d",
SPC
);
end
if (R_MAX <= 1) begin : gen_rate_check
$error("cic_interpolator: R_MAX must be at least 2. Got R_MAX=%0d", R_MAX);
end
//---------------------------------------------------------------------------
// Signal Declarations
//---------------------------------------------------------------------------
// Create a version of the output bus we can index
logic gen_tvalid;
logic gen_tready;
logic [SPC-1:0][SAMP_W-1:0] gen_tdata;
logic gen_tlast;
// Input register
logic input_busy = 1'b0;
logic input_last;
logic [SPC-1:0][SAMP_W-1:0] input_samps;
//---------------------------------------------------------------------------
// Interpolation Factor
//---------------------------------------------------------------------------
logic [FACTOR_W-1:0] factor_m2 = 0; // Current factor minus 2
logic factor_is_one = 1; // High when factor == 1
always_ff @(posedge clk) begin
factor_m2 <= interp_factor - 2;
factor_is_one <= (interp_factor == 1);
end
//---------------------------------------------------------------------------
// Sample and Word Indices
//---------------------------------------------------------------------------
//
// Determine the output word and sample indices into which each input sample
// go after interpolation.
//
//---------------------------------------------------------------------------
localparam int ACCUM_W = WORD_IDX_W + SAMP_IDX_W;
// An array of indices that stores the mapping from input sample to output
// sample.
logic [ACCUM_W-1:0] accum [SPC];
// This array contains the output word index for each input sample
logic [WORD_IDX_W-1:0] word_idx [SPC];
// This array contains the output sample index for each input sample
logic [SAMP_IDX_W-1:0] samp_idx [SPC];
always_ff @(posedge clk) begin
for (int idx = 0; idx < SPC; idx++) begin
accum[idx] <= idx * interp_factor;
end
end
// Extract from accumulator the word and sample indices.
always_comb begin
for (int idx = 0; idx < SPC; idx++) begin
word_idx[idx] = accum[idx][SAMP_IDX_W +: WORD_IDX_W];
samp_idx[idx] = accum[idx][ 0 +: SAMP_IDX_W];
end
end
//---------------------------------------------------------------------------
// Word Counter
//---------------------------------------------------------------------------
//
// For each one input word we output factor words. This counter tracks the
// number of output words that have been output for each input word.
//
//---------------------------------------------------------------------------
logic [WORD_IDX_W-1:0] word_cnt;
logic word_cnt_last; // Assert on last word of output group
always_ff @(posedge clk) begin
if (rst) begin
word_cnt <= '0;
word_cnt_last <= 1'b1;
end else if (!input_busy) begin
word_cnt <= '0;
word_cnt_last <= factor_is_one;
end else if (gen_tvalid && gen_tready) begin
if (word_cnt_last) begin
word_cnt <= '0;
word_cnt_last <= factor_is_one;
end else begin
word_cnt <= word_cnt + 1;
word_cnt_last <= (word_cnt == factor_m2);
end
end
end
//---------------------------------------------------------------------------
// Input Word Logic
//---------------------------------------------------------------------------
//
// Captures the input word when accepted and holds it until all corresponding
// output words are generated.
//
// The input_busy flag tracks when we're holding a loaded input word. It
// starts low after reset so the first word is accepted immediately. It sets
// when a word is accepted and clears on the last output transfer for the
// current input word.
//
//---------------------------------------------------------------------------
// Assert data_in.tready when not busy, or combinationally on the cycle the
// last output word of the current interpolation group transfers.
assign data_in.tready = !input_busy ||
(gen_tvalid && gen_tready && word_cnt_last);
// Output is valid whenever we have a loaded input word to work from.
assign gen_tvalid = input_busy;
// Assert tlast on the last output word of the group when the input was last.
assign gen_tlast = input_last && word_cnt_last;
always_ff @(posedge clk) begin
if (rst) begin
input_busy <= 1'b0;
end else begin
if (data_in.tvalid && data_in.tready) begin
// Accepted a new input word; busy until end of interpolation group.
input_busy <= 1'b1;
input_last <= data_in.tlast;
input_samps <= data_in.tdata;
end else if (gen_tvalid && gen_tready && word_cnt_last) begin
// Last output word transferred with no new input; no longer busy.
input_busy <= 1'b0;
end
end
end
//---------------------------------------------------------------------------
// Output Word Generation
//---------------------------------------------------------------------------
//
// For each output word, we look at the current word_cnt and use that to
// determine which input samples, if any, should be output for each sample
// position. We pull the sample values from the stored input_samps register.
// We decide where those input samples go on the output by looking up the
// word_idx and samp_idx for that sample. All other sample positions are set
// to 0.
//
//---------------------------------------------------------------------------
// Intermediate results to pipeline the array indexing
// Stage 1: Determine which input samples match the current word count
logic [SPC-1:0] word_idx_match;
always_ff @(posedge clk) begin
if (gen_tready) begin
for (int src_samp = 0; src_samp < SPC; src_samp++) begin
word_idx_match[src_samp] <= (word_idx[src_samp] == word_cnt);
end
end
end
// Stage 2: Generate the sample index for each output sample position
logic [SAMP_IDX_W-1:0] out_mux_select [SPC];
logic [SPC-1:0] word_idx_en;
always_ff @(posedge clk) begin
automatic logic [SAMP_IDX_W-1:0] next_out_mux_select [SPC] = '{default: '0};
automatic logic [SPC-1:0] next_word_idx_en = '0;
for (int src_samp = 0; src_samp < SPC; src_samp++) begin
if (word_idx_match[src_samp]) begin
next_out_mux_select[samp_idx[src_samp]] |= SAMP_IDX_W'(src_samp);
next_word_idx_en[samp_idx[src_samp]] = '1;
end
end
if (gen_tready) begin
out_mux_select <= next_out_mux_select;
word_idx_en <= next_word_idx_en;
end
end
// create shift register for tdata, tvalid and tlast to pipeline them to the output
// FIFO
logic [1:0] tvalid_pipe;
logic [1:0] tlast_pipe;
logic [1:0][SPC-1:0][SAMP_W-1:0] tdata_pipe;
always_ff @(posedge clk) begin
if (gen_tready) begin
tlast_pipe <= {tlast_pipe[0], gen_tlast};
tdata_pipe <= {tdata_pipe[0], input_samps};
end
end
always_ff @(posedge clk) begin
if (rst) begin
tvalid_pipe <= 2'b00;
end else if (gen_tready) begin
tvalid_pipe <= {tvalid_pipe[0], gen_tvalid};
end
end
always_comb begin
// All samples are set to zero by default
gen_tdata = '0;
// Iterate over each sample and see if the word count and sample position
// match what we precomputed in samp_idx and word_idx.
for (int out_samp = 0; out_samp < SPC; out_samp++) begin
if (word_idx_en[out_samp]) begin
gen_tdata[out_samp] = tdata_pipe[1][out_mux_select[out_samp]];
end
end
end
//---------------------------------------------------------------------------
// Output Register
//---------------------------------------------------------------------------
wire [SPC*SAMP_W:0] fifo_out;
axi_fifo #(
.WIDTH (SPC*SAMP_W + 1 ),
.SIZE (1 )
) axi_fifo_i (
.clk (clk ),
.reset (rst ),
.clear (1'b0 ),
.i_tdata ({tlast_pipe[1], gen_tdata}),
.i_tvalid (tvalid_pipe[1] ),
.i_tready (gen_tready ),
.o_tdata (fifo_out ),
.o_tvalid (data_out.tvalid ),
.o_tready (data_out.tready ),
.space ( ),
.occupied ( )
);
assign {data_out.tlast, data_out.tdata} = fifo_out;
endmodule : cic_interpolator
`default_nettype wire