mirror of
https://github.com/EttusResearch/uhd.git
synced 2026-10-25 17:36:10 +00:00
fpga: dsp: add FSM for HB FIR temporal decimation
Added a state machine to fix a bug in the temporal decimation code of the decimating halfband filter module. Additionally added a testcase to the testbench for the issue this change is fixing.
This commit is contained in:
parent
58ab7a88cc
commit
aaaca5bff9
@ -293,57 +293,102 @@ module axis_hb_decim_fir #(
|
||||
|
||||
// Decimation logic to reduce samples by 2
|
||||
if ((SPC_IN == 1) && (SPC_OUT == 1)) begin : decim_over_time
|
||||
// For single sample per clock, decimation is achieved by taking every 2nd sample in time
|
||||
logic toggle_sample;
|
||||
logic pair_phase;
|
||||
logic delayed_tlast;
|
||||
logic tlast_fifo_i_data, tlast_fifo_i_valid;
|
||||
logic tlast_fifo_o_data, tlast_fifo_o_valid, tlast_fifo_o_ready;
|
||||
// For a scalar stream, decimate continuously across packet boundaries.
|
||||
// Hold each retained sample until the following dropped sample determines
|
||||
// whether the retained sample is the last output of its packet.
|
||||
// The state describes the action applied to the next accepted input sample:
|
||||
// KEEP_ST retains it, while DROP_ST discards it.
|
||||
typedef enum logic {
|
||||
KEEP_ST,
|
||||
DROP_ST
|
||||
} decim_state_t;
|
||||
|
||||
decim_state_t decim_state;
|
||||
// Indicates that output_tdata contains a retained sample whose TLAST is not
|
||||
// yet known. It becomes AXI-valid when the following dropped sample arrives.
|
||||
logic pending_tvalid;
|
||||
logic [SPC_OUT*SAMP_W-1:0] output_tdata = 'X;
|
||||
logic output_tvalid;
|
||||
logic output_tlast = 'X;
|
||||
logic output_tready;
|
||||
|
||||
// Internal output-register load enable: The register can be loaded when
|
||||
// empty or when its current value transfers downstream on this cycle.
|
||||
// (similar logic to axi_fifo_flop)
|
||||
assign output_tready = !output_tvalid | out_tready;
|
||||
|
||||
// A DROP_ST input does not consume output storage. If it releases a pending
|
||||
// sample, the output register is guaranteed to be invalid, so the input may
|
||||
// be accepted independently of downstream backpressure.
|
||||
assign post_filter_tready = output_tready | decim_state == DROP_ST;
|
||||
|
||||
always_ff @(posedge clk) begin
|
||||
if (rst | clear | in_tlast_i) begin
|
||||
pair_phase <= 1'b0;
|
||||
delayed_tlast <= 1'b0;
|
||||
end else if (in_tvalid_i && enable && in_tready_i) begin
|
||||
pair_phase <= ~pair_phase;
|
||||
delayed_tlast <= in_tlast_i;
|
||||
// Clear the output register when the downstream can accept it. A later
|
||||
// assignment below may refill the register on the same edge.
|
||||
if (out_tready) begin
|
||||
output_tvalid <= 1'b0;
|
||||
end
|
||||
end
|
||||
|
||||
// Store one tlast bit for each pair of input samples.
|
||||
assign tlast_fifo_i_valid = in_tvalid_i && enable && in_tready_i && (pair_phase | in_tlast_i);
|
||||
assign tlast_fifo_i_data = delayed_tlast | in_tlast_i;
|
||||
assign tlast_fifo_o_ready = out_tvalid && out_tready;
|
||||
// update state machine on transfer from halfband filter
|
||||
if (post_filter_tvalid && post_filter_tready) begin
|
||||
// toggle state for each new sample
|
||||
decim_state <= decim_state == KEEP_ST ? DROP_ST : KEEP_ST;
|
||||
pending_tvalid <= 1'b0;
|
||||
|
||||
axi_fifo #(
|
||||
.WIDTH(1),
|
||||
.SIZE (1)
|
||||
) tlast_fifo (
|
||||
.clk (clk),
|
||||
.reset (rst),
|
||||
.clear (clear),
|
||||
.i_tdata (tlast_fifo_i_data),
|
||||
.i_tvalid (tlast_fifo_i_valid),
|
||||
.i_tready (),
|
||||
.o_tdata (tlast_fifo_o_data),
|
||||
.o_tvalid (tlast_fifo_o_valid),
|
||||
.o_tready (tlast_fifo_o_ready),
|
||||
.space (),
|
||||
.occupied ()
|
||||
);
|
||||
case (decim_state)
|
||||
KEEP_ST: begin
|
||||
// always store the current sample in the output register
|
||||
output_tdata <= post_filter_tdata;
|
||||
|
||||
// if this is the last sample of the packet, it should be output immediately
|
||||
if (post_filter_tlast) begin
|
||||
output_tdata <= post_filter_tdata;
|
||||
output_tvalid <= 1'b1;
|
||||
output_tlast <= 1'b1;
|
||||
// otherwise, the sample will be held until the next one arrives
|
||||
end else begin
|
||||
pending_tvalid <= 1'b1;
|
||||
end
|
||||
end
|
||||
DROP_ST: begin
|
||||
// when dropping a sample, nothing is stored in the output register
|
||||
// Depending on a previously retained sample, it may be output now
|
||||
if (pending_tvalid) begin
|
||||
output_tvalid <= 1'b1;
|
||||
output_tlast <= post_filter_tlast;
|
||||
end
|
||||
end
|
||||
endcase
|
||||
end
|
||||
|
||||
always_ff @(posedge clk) begin
|
||||
if (rst | clear) begin
|
||||
toggle_sample <= ~DECIMATION_PHASE;
|
||||
end else if (post_filter_tvalid && post_filter_tready) begin
|
||||
toggle_sample <= ~toggle_sample;
|
||||
decim_state <= DECIMATION_PHASE ? DROP_ST : KEEP_ST;
|
||||
pending_tvalid <= 1'b0;
|
||||
output_tvalid <= 1'b0;
|
||||
end
|
||||
end
|
||||
assign out_tdata = post_filter_tdata; // output the current sample directly
|
||||
assign out_tvalid = post_filter_tvalid && toggle_sample && tlast_fifo_o_valid;
|
||||
assign out_tlast = tlast_fifo_o_valid && tlast_fifo_o_data;
|
||||
assign post_filter_tready = toggle_sample
|
||||
? (out_tready && tlast_fifo_o_valid) : 1'b1;
|
||||
|
||||
assign out_tdata = output_tdata;
|
||||
assign out_tvalid = output_tvalid;
|
||||
assign out_tlast = output_tlast;
|
||||
|
||||
// synthesis translate_off
|
||||
property p_packet_has_retained_sample;
|
||||
@(posedge clk) disable iff (rst || clear)
|
||||
post_filter_tvalid && post_filter_tready
|
||||
&& decim_state == DROP_ST && post_filter_tlast
|
||||
|-> pending_tvalid;
|
||||
endproperty
|
||||
assert property (p_packet_has_retained_sample)
|
||||
else $error("Temporal decimation cannot represent an empty output packet");
|
||||
|
||||
property p_pending_sample_matches_state;
|
||||
@(posedge clk) disable iff (rst || clear)
|
||||
decim_state == KEEP_ST |-> !pending_tvalid;
|
||||
endproperty
|
||||
assert property (p_pending_sample_matches_state)
|
||||
else $error("Temporal decimation retained-sample state is inconsistent");
|
||||
// synthesis translate_on
|
||||
|
||||
end else begin : decim_by_2
|
||||
// For multiple samples per clock, decimation is achieved by taking every 2nd sample in the vector
|
||||
|
||||
@ -262,6 +262,61 @@ module axis_hb_decim_fir_tb #(
|
||||
endtask : test_filter_response
|
||||
|
||||
|
||||
// Verify packet boundaries when temporal decimation phase continues across
|
||||
// consecutive odd-length packets. Three 3-word packets retain 2, 1, and 2
|
||||
// samples respectively.
|
||||
task automatic test_temporal_packet_boundaries();
|
||||
localparam int NUM_PACKETS = 3;
|
||||
localparam int PACKET_LEN_WORDS = 3;
|
||||
localparam int EXPECTED_LENGTHS [NUM_PACKETS] = '{2, 1, 2};
|
||||
axis_input_pkt_t input_pkt;
|
||||
axis_output_pkt_t received_pkt;
|
||||
axis_input_pkt_mbox_t input_pkts = new();
|
||||
axis_pkt_single_sample_mbox_t input_samples = new();
|
||||
axis_pkt_single_sample_mbox_t expected_samples = new();
|
||||
axis_pkt_single_sample_t expected_sample;
|
||||
int input_stall_prob;
|
||||
int output_stall_prob;
|
||||
AxiFirHBDecimModel #(
|
||||
.SAMP_W(SAMP_W),
|
||||
.NUM_COEFFS(NUM_COEFFS)
|
||||
) hb_model = new();
|
||||
|
||||
input_stall_prob = axi_bfm_in.get_master_stall_prob();
|
||||
output_stall_prob = axi_bfm_out.get_slave_stall_prob();
|
||||
axi_bfm_in.set_master_stall_prob(0);
|
||||
axi_bfm_out.set_slave_stall_prob(75);
|
||||
|
||||
repeat (NUM_PACKETS) begin
|
||||
input_pkt = filter_utils_t::generate_zero_packet(PACKET_LEN_WORDS);
|
||||
input_pkts.put(input_pkt.copy());
|
||||
axi_bfm_in.put(input_pkt);
|
||||
end
|
||||
|
||||
filter_utils_t::collect_and_serialize_packets(input_pkts, input_samples);
|
||||
hb_model.process_samples(input_samples, expected_samples);
|
||||
|
||||
for (int packet = 0; packet < NUM_PACKETS; packet++) begin
|
||||
axi_bfm_out.get(received_pkt);
|
||||
`ASSERT_ERROR(received_pkt.data.size() == EXPECTED_LENGTHS[packet],
|
||||
$sformatf("Output packet %0d: expected %0d words, received %0d",
|
||||
packet, EXPECTED_LENGTHS[packet], received_pkt.data.size()));
|
||||
foreach (received_pkt.data[word]) begin
|
||||
`ASSERT_ERROR(expected_samples.try_get(expected_sample),
|
||||
$sformatf("Output packet %0d word %0d has no expected sample", packet, word));
|
||||
`ASSERT_ERROR(received_pkt.data[word] == expected_sample.data[0],
|
||||
$sformatf("Output packet %0d word %0d: expected 0x%0h, received 0x%0h",
|
||||
packet, word, expected_sample.data[0], received_pkt.data[word]));
|
||||
end
|
||||
end
|
||||
|
||||
`ASSERT_ERROR(expected_samples.num() == 0,
|
||||
$sformatf("Expected model contains %0d unconsumed samples", expected_samples.num()));
|
||||
axi_bfm_in.set_master_stall_prob(input_stall_prob);
|
||||
axi_bfm_out.set_slave_stall_prob(output_stall_prob);
|
||||
endtask : test_temporal_packet_boundaries
|
||||
|
||||
|
||||
// Test that after a `clear` pulse (without full reset) the PRELOAD_ZEROES
|
||||
// mechanism restores a clean filter state, producing an impulse response
|
||||
// identical to what follows a hard reset (both start from zero tap history).
|
||||
@ -291,9 +346,8 @@ module axis_hb_decim_fir_tb #(
|
||||
task automatic test_preload_after_clear();
|
||||
// Enough words to fill the entire FIR data shift register with non-zero data:
|
||||
// SHIFT_REG_WIDTH = (SPC+1)*NUM_COEFFS entries; each word contributes SPC entries.
|
||||
// Using 2*NUM_COEFFS+1 words ensures an odd packet length so that the last
|
||||
// output always falls on a "kept" toggle-sample (toggle=1), preventing a
|
||||
// pending_tlast deadlock in axi_bfm_out.get().
|
||||
// Using 2*NUM_COEFFS+1 words ensures the packet contains enough retained
|
||||
// samples to drain and contaminate the complete filter history.
|
||||
localparam int CONTAM_LEN_WORDS = 2 * NUM_COEFFS + 1;
|
||||
localparam int IMPULSE_LEN_WORDS = FILTER_PIPELINE_DELAY + NUM_COEFFS + 1;
|
||||
// Number of DSP-pipeline output samples to skip before checking impulse data.
|
||||
@ -327,12 +381,7 @@ module axis_hb_decim_fir_tb #(
|
||||
// Send the contam packet. Wait for full TX completion before draining output:
|
||||
// this guarantees no residual contam words remain queued in axi_bfm_in before
|
||||
// the clear pulse, so data_shift_reg is not re-dirtied after the preload.
|
||||
// Use an odd packet length to guarantee the contam tlast falls on a "kept"
|
||||
// toggle-sample slot (toggle_sample=1 at test start; odd length means the
|
||||
// last word arrives on an even beat index, i.e., toggle=1 -> tlast fires
|
||||
// immediately instead of setting pending_tlast, which would deadlock get()).
|
||||
// Use a fixed odd length (not random) so the property holds regardless of
|
||||
// test ordering or RNG seed.
|
||||
// Use a fixed length so the test remains independent of RNG ordering.
|
||||
contam_pkt = filter_utils_t::generate_random_packet(
|
||||
CONTAM_LEN_WORDS, CONTAM_LEN_WORDS);
|
||||
axi_bfm_in.put(contam_pkt);
|
||||
@ -446,6 +495,14 @@ module axis_hb_decim_fir_tb #(
|
||||
test.end_test();
|
||||
end
|
||||
|
||||
if (SPC == 1) begin
|
||||
test.start_test("Temporal Packet Boundary Test", 100us);
|
||||
test_temporal_packet_boundaries();
|
||||
clk_gen.reset();
|
||||
@(negedge rst);
|
||||
test.end_test();
|
||||
end
|
||||
|
||||
clk_gen.kill();
|
||||
test.end_tb(0);
|
||||
end : main
|
||||
|
||||
Loading…
Reference in New Issue
Block a user