diff --git a/lib/axi/Makefile.srcs b/lib/axi/Makefile.srcs index c562449..2068f02 100644 --- a/lib/axi/Makefile.srcs +++ b/lib/axi/Makefile.srcs @@ -34,4 +34,5 @@ axis_downsizer.v \ axis_width_conv.v \ axis_split.v \ axis_packetize.v \ +axis_pkt_throttle.sv \ )) diff --git a/lib/axi/axis_pkt_throttle.sv b/lib/axi/axis_pkt_throttle.sv new file mode 100644 index 0000000..829d606 --- /dev/null +++ b/lib/axi/axis_pkt_throttle.sv @@ -0,0 +1,179 @@ +// +// Copyright 2023 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Module: axis_pkt_throttle +// +// Description: +// +// This module takes in AXI-Stream and outputs the same stream, inserting +// gaps between packets in order to maintain a specific data rate. The amount +// of time between packets is controlled in such a way that the length of a +// packet divided by the time between the start of that packet and the next +// does not exceed some rate R. +// +// This module does NOT insert stalls within a packet, only between packets, +// so the peak data rate is not restricted and packet contiguity is not +// affected. Also, the average data rate could be slightly higher than the +// configured rate due to rounding error (within a single clock cycle per +// packet). +// +// The "throttle" input port controls the rate. In order to set the rate to +// R, where R is a fraction in the range (0,1], set throttle (T) using the +// formula T = (1/R)-1. In other words, R = 1/(T+1). +// +// The "throttle" input is represented as an unsigned fixed-point value with +// THROTTLE_W/2 whole bits and THROTTLE_W/2 fractional bits (UQn.n format). +// Throttle is therefore in the range [0,1). +// +// For example, if THROTTLE_W is 8 bits then throttle is in UQ4.4 format (4 +// whole bits and 4 fractional bits). In this case, a throttle value of 0 +// corresponds to a rate of 1/(0+1) = 1.0, which is 100% or full throttle. A +// throttle value of 15.9375 (i.e., 0xFF, or the max value) corresponds to a +// rate of 1/(15.9375+1) = 0.05904059, or 5.9% of the maximum rate. +// +// The throttle input is sampled between packets. Changing the throttle +// during a packet, or before its inserted stall time has elapsed, has no +// effect until the next packet. +// +// Parameters: +// +// THROTTLE_W : Width of the throttle input in bits. +// DATA_W : Width of data bus for AXI-Stream in bits. +// MTU : The maximum supported packet length is 2**MTU. +// + +`default_nettype none + + +module axis_pkt_throttle #( + parameter int THROTTLE_W = 8, + parameter int DATA_W = 64, + parameter int MTU = 10 +) ( + input wire clk, + input wire rst, + + input wire [THROTTLE_W-1:0] throttle, + + // Input AXI-Stream + input wire [ DATA_W-1:0] i_tdata, + input wire i_tlast, + input wire i_tvalid, + output wire i_tready, + + // Output AXI-Stream + output wire [ DATA_W-1:0] o_tdata, + output wire o_tlast, + output wire o_tvalid, + input wire o_tready +); + + //--------------------------------------------------------------------------- + // Throttle Control Logic + //--------------------------------------------------------------------------- + // + // This logic monitors the data flow and determines when we should pass data + // through and when we should stall in order to limit the data rate. + // + //--------------------------------------------------------------------------- + + // Length of the fractional part of our fixed-point throttle and count. + localparam int FRAC_W = THROTTLE_W/2; + // Length of the whole-number part of our fixed-point throttle. + localparam int WHOLE_W = THROTTLE_W - FRAC_W; + // Width of an unsigned fixed-point value to track the amount of time to + // ensure that we have between the starts of packets. This must be large + // enough to store (2**MTU) * (2**THROTTLE_W-1). + localparam int TIME_W = MTU + THROTTLE_W; + // Width of an unsigned counter to track time between packets. Same as + // TIME_W, but the whole-number part. + localparam int COUNT_W = TIME_W - FRAC_W; + // Compute the minimum count value we can have and still guarantee that the + // next stall time adjustment won't cause underflow. + localparam longint MIN_COUNT = -(2**TIME_W) + (2**THROTTLE_W-1); + + // Fixed-point accumulator that tracks amount of time to stall between + // packets. We add an extra bit for the sign since this value can be negative. + logic signed [TIME_W:0] stall_time; + // Counter to track the whole number of clock cycles to stall. + logic [COUNT_W-1:0] wait_count; + // Flag to indicate if underflow occurred and our count can't be trusted. + logic underflow; + // Register to control the flow of packets through this module. When 1, data + // flow is gated (stopped). + logic gate = 1'b0; + // Start of packet flag. + logic sop = 1'b1; + + always_ff @(posedge clk) begin : throttle_control + if (gate) begin + wait_count <= wait_count-1; + gate <= (wait_count > 1); + // Update stall_time for next packet, in case throttle changes. + stall_time <= throttle; + end else begin + if (i_tvalid && o_tready) begin + if (i_tlast) begin + sop <= 1; + // End of the packet. Start stalling, if needed, and reset for the + // next packet. + if (!underflow && !stall_time[TIME_W]) begin + // No underflow and stall_time is non-negative, so start stalling + // the accumulated amount. + wait_count <= stall_time[FRAC_W+:COUNT_W]; + gate <= (stall_time[FRAC_W+:COUNT_W] != 0); + end else begin + // We underflowed or stall_time was negative, so don't stall. + wait_count <= 0; + gate <= 0; + end + // Reset for next packet + underflow <= 0; + stall_time <= throttle; + end else begin + // A transfer is happening this cycle. Update stall time. Note that + // overflow is not possible as long as the MTU is honored. + stall_time <= stall_time + throttle; + sop <= 0; + end + end else begin + if (sop) begin + // We're in between packets. Update stall_time for next packet, in + // case throttle changes. + stall_time <= throttle; + end else begin + // An idle cycle (no transfer) is occurring this cycle so subtract + // 1.0 from our stall time. We must check for underflow since there + // is no limit to the number of idle cycles we might see. + stall_time <= stall_time - (1 << FRAC_W); + if (stall_time < MIN_COUNT) begin + underflow <= 1; + end + end + end + end + + if (rst) begin + sop <= 1; + stall_time <= 0; + wait_count <= 'X; // Don't care + gate <= 0; + underflow <= 0; + end + end : throttle_control + + //--------------------------------------------------------------------------- + // Data Pass-Through + //--------------------------------------------------------------------------- + + assign o_tdata = i_tdata; + assign o_tlast = i_tlast; + assign o_tvalid = i_tvalid & ~gate; + assign i_tready = o_tready & ~gate; + +endmodule : axis_pkt_throttle + + +`default_nettype wire diff --git a/lib/sim/axis_pkt_throttle/Makefile b/lib/sim/axis_pkt_throttle/Makefile new file mode 100644 index 0000000..1dce9cd --- /dev/null +++ b/lib/sim/axis_pkt_throttle/Makefile @@ -0,0 +1,38 @@ +# +# Copyright 2023 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# + +#------------------------------------------------- +# Top-of-Makefile +#------------------------------------------------- +# Define BASE_DIR to point to the "top" dir +BASE_DIR = $(abspath ../../../top) +# Include viv_sim_preamble after defining BASE_DIR +include $(BASE_DIR)/../tools/make/viv_sim_preamble.mak + +#------------------------------------------------- +# Design Specific +#------------------------------------------------- +# Include makefiles and sources for the DUT and its dependencies + +DESIGN_SRCS += $(abspath \ +$(abspath ../../axi/axis_pkt_throttle.sv) \ +) + +#------------------------------------------------- +# Testbench Specific +#------------------------------------------------- +SIM_TOP = axis_pkt_throttle_tb + +SIM_SRCS = \ +$(abspath axis_pkt_throttle_tb.sv) \ + +#------------------------------------------------- +# Bottom-of-Makefile +#------------------------------------------------- +# Include all simulator specific makefiles here +# Each should define a unique target to simulate +# e.g. xsim, vsim, etc and a common "clean" target +include $(BASE_DIR)/../tools/make/viv_simulator.mak diff --git a/lib/sim/axis_pkt_throttle/axis_pkt_throttle_tb.sv b/lib/sim/axis_pkt_throttle/axis_pkt_throttle_tb.sv new file mode 100644 index 0000000..0433df4 --- /dev/null +++ b/lib/sim/axis_pkt_throttle/axis_pkt_throttle_tb.sv @@ -0,0 +1,371 @@ +// +// Copyright 2023 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Module: align_samples_tb +// +// Description: +// +// Testbench for axis_pkt_throttle. +// + +`default_nettype none + + +module axis_pkt_throttle_tb (); + + // Include macros and time declarations for use with PkgTestExec + `include "test_exec.svh" + import PkgTestExec::*; + import PkgRandom::*; + + localparam real CLK_PERIOD = 10.0; + localparam int THROTTLE_W = 8; + localparam int DATA_W = 32; + localparam int MTU = 4; + localparam int MAX_PKT_LEN = 2**MTU; + localparam int NUM_PACKETS = 100; + + localparam int FRAC_W = THROTTLE_W/2; + localparam int WHOLE_W = THROTTLE_W - FRAC_W; + + //--------------------------------------------------------------------------- + // Clocks and Resets + //--------------------------------------------------------------------------- + + bit clk; + bit rst; + + sim_clock_gen #(.PERIOD(CLK_PERIOD)) + clk_gen (.clk(clk), .rst(rst)); + + //--------------------------------------------------------------------------- + // Device Under Test (DUT) + //--------------------------------------------------------------------------- + + logic [THROTTLE_W-1:0] throttle; + + logic [DATA_W-1:0] i_tdata; + logic i_tlast; + logic i_tvalid; + logic i_tready; + + logic [DATA_W-1:0] o_tdata; + logic o_tlast; + logic o_tvalid; + logic o_tready; + + axis_pkt_throttle #( + .THROTTLE_W(THROTTLE_W), + .DATA_W (DATA_W), + .MTU (MTU) + ) axis_pkt_throttle_dut ( + .clk (clk ), + .rst (rst ), + .throttle(throttle), + .i_tdata (i_tdata ), + .i_tlast (i_tlast ), + .i_tvalid(i_tvalid), + .i_tready(i_tready), + .o_tdata (o_tdata ), + .o_tlast (o_tlast ), + .o_tvalid(o_tvalid), + .o_tready(o_tready) + ); + + //--------------------------------------------------------------------------- + // Tests + //--------------------------------------------------------------------------- + + // Run a test using the following parameters. + // + // num_pkts : Number of packets to generate, each with a random + // length. + // input_stall_prob : Probability of a stall on the input, a whole number + // from 0 to 99. + // output_stall_prob : Probability of a stall on the output, a whole number + // from 0 to 99. + // rate : Floating point value in the range (0, 1.0]. + // min_pkt_length : Minimum packet length to generate. + // + task automatic run_test( + int num_pkts, + real rate = 1.0, + int input_stall_prob = 0, + int output_stall_prob = 0, + int min_pkt_length = 1 + ); + real actual_rate; + test.start_test( + $sformatf({ "num_pkts=%0d, rate=%0.3f, input_stall_prob=%0d, ", + "output_stall_prob=%0d, min_pkt_length=%0d"}, + num_pkts, rate, input_stall_prob, output_stall_prob) + ); + throttle = (1.0/rate-1.0) * 2.0**FRAC_W; + actual_rate = 1.0/(real'(throttle)/(2.0**FRAC_W) + 1.0); + $display("Setting throttle to 0x%X (rate = %0.3f)", throttle, actual_rate); + i_tdata <= 'X; + i_tlast <= 'X; + i_tvalid <= 0; + o_tready <= 0; + @(posedge clk); + + fork + + // The writer generates random packets to input to the DUT + begin : writer + logic [DATA_W-1:0] data = 0; + for (int pkt_count = 0; pkt_count < num_pkts; pkt_count++) begin + int pkt_length = $urandom_range(min_pkt_length, MAX_PKT_LEN); + for (int word_count = 0; word_count < pkt_length; word_count++) begin + // Write the next word + i_tdata <= data; + i_tlast <= (word_count == pkt_length-1); + i_tvalid <= 1; + do @(posedge clk); while (!(i_tvalid && i_tready)); + data = data + 1; + // Randomly stall between words + if ($urandom_range(99) < input_stall_prob) begin + i_tdata <= 'X; + i_tlast <= 'X; + i_tvalid <= 0; + do @(posedge clk); while ($urandom_range(99) < input_stall_prob); + end + end + end + end : writer + + // The data_checker verifies that the data output is correct and handles + // random stalling of the output stream. + begin : data_checker + logic [DATA_W-1:0] data = 0; + for (int pkt_count = 0; pkt_count < num_pkts; pkt_count++) begin + int word_count = 0; + forever begin + @(posedge clk); + if (o_tvalid && o_tready) begin + `ASSERT_ERROR( + o_tdata == data, + $sformatf({ + "Data didn't match expected on packet %0d word offset %0d. ", + "Expected %X, read %X"}, + pkt_count, word_count, data, o_tdata + ) + ) + data++; + if (i_tlast) break; + word_count++; + end + // Randomly stall this cycle + o_tready <= ($urandom_range(99) >= output_stall_prob); + end + end + end : data_checker + + // The throttle_checker measures the packet rate and confirms that it + // matches the configured rate. + begin : throttle_checker + bit sop = 0; // Start of packet indicator + realtime sop_time; // Time at which the packet started + int pkt_length; // Counter to measure packet length (number of transfers) + int pkt_duration; // Counter to measure packet duration (from start to tlast) + + // Wait for the start of the first packet + forever begin + @(posedge clk); + if (o_tvalid && o_tready) begin + sop_time = $realtime; + sop = o_tlast; + break; + end + end + + // Iterate through packets + pkt_length = 1; + pkt_duration = 1; + for (int pkt_count = 0; pkt_count < num_pkts; pkt_count++) begin : pkt_loop + int exp_pkt_cycles; + + forever begin : cycle_loop + @(posedge clk); + if (!sop) pkt_duration++; + if (o_tvalid && o_tready) begin : transfer_cycle + // Calculate the minimum allowed time between packets assuming + // continuous data. + exp_pkt_cycles = int'(real'(pkt_length) / actual_rate); + + // Check if this is the first transfer of a packet. If so, verify + // that the duration of the previous packet was not shorter than + // the configured rate would allow. + if (sop) begin : first_transfer + int pkt_cycles; + + // Calculate the actual time between packets. + pkt_cycles = ($realtime - sop_time) / CLK_PERIOD; + + if (input_stall_prob == 0 && output_stall_prob == 0) begin + // If there are no stalls, the actual time should exactly + // match the expected, or one less due to rounding. + `ASSERT_ERROR( + pkt_cycles == exp_pkt_cycles || pkt_cycles == exp_pkt_cycles-1, + $sformatf({ + "Time for packet %0d did not match expected range.\n", + " Actual Rate: %f\n", + " Packet Length: %0d\n", + " Packet Cycles: %0d\n", + " Expected Cycles: %0d"}, + pkt_count, actual_rate, pkt_length, pkt_cycles, exp_pkt_cycles + ) + ) + end else begin + // If there are stalls, the actual length should never be + // less than the min expected, but could be substantially + // more. + `ASSERT_ERROR( + pkt_cycles >= exp_pkt_cycles-1, + $sformatf({ + "Time for packet %0d was less than expected.\n", + " Actual Rate: %f\n", + " Packet Length: %0d\n", + " Packet Cycles: %0d\n", + " Expected Cycles: %0d"}, + pkt_count, actual_rate, pkt_length, pkt_cycles, exp_pkt_cycles + ) + ) + end + + // Setup measurement of the packet we just started + sop = o_tlast; + pkt_length = 1; + pkt_duration = 1; + sop_time = $realtime; + break; + end : first_transfer + else begin : subsequent_transfer + pkt_length++; + end : subsequent_transfer + + sop = o_tlast; + end : transfer_cycle + else begin : idle_cycle + // Calculate the minimum allowed time between packets assuming + // continuous data. + int exp_pkt_cycles = int'(real'(pkt_length) / actual_rate); + + // If the packet duration was longer than the expected packet + // time, then we should NOT be gating packet flow. + if (pkt_duration > exp_pkt_cycles) begin + `ASSERT_ERROR( + axis_pkt_throttle_dut.gate == 0, + "The throttle gate engaged when it should not have." + ) + end + end + + // If we're on the last packet, then there won't be another start + // of packet to measure against, so there's nothing left to check. + if (sop && (pkt_count == num_pkts-1)) break; + end : cycle_loop + end : pkt_loop + end : throttle_checker + + join + test.end_test(); + endtask : run_test + + //--------------------------------------------------------------------------- + // Underflow Tracker + //--------------------------------------------------------------------------- + + int uflow_rise_count = 0; + int uflow_fall_count = 0; + logic uflow_prev = 0; + + always @(posedge clk) begin + uflow_prev <= axis_pkt_throttle_dut.underflow; + + if (axis_pkt_throttle_dut.underflow && !uflow_prev) begin + uflow_rise_count <= uflow_rise_count + 1; + end + + if (!axis_pkt_throttle_dut.underflow && uflow_prev) begin + uflow_fall_count <= uflow_fall_count + 1; + end + end + + //--------------------------------------------------------------------------- + // Main Test Process + //--------------------------------------------------------------------------- + + initial begin : tb_main + string tb_name; + + tb_name = $sformatf("axis_pkt_throttle"); + test.start_tb(tb_name, 20ms); + + //-------------------------------- + // Reset + //-------------------------------- + + test.start_test("Reset", 1ms); + clk_gen.reset(); + if (rst) @rst; + test.end_test(); + + //-------------------------------- + // Test Sequences + //-------------------------------- + + begin + // List various rate settings and handshake stall behaviors to test + automatic real rates[] = '{ + 0.10, + 0.25, + 0.50, + 0.75, + 0.90, + 1.00 + }; + automatic real stall_probs[] = '{ + 0, + 25, + 50, + 75 + }; + + // Test each permutation of rates and stall probabilities + foreach (rates[i]) begin + foreach (stall_probs[j]) begin + foreach (stall_probs[k]) begin + run_test(NUM_PACKETS, rates[i], stall_probs[j], stall_probs[k]); + end + end + end + end + + begin + // Test really slow packets to make sure the internal counters handle + // underflow correctly. + localparam int NUM_UFLOW_PACKETS = 4; + run_test(NUM_UFLOW_PACKETS, 0.1, 99, 99, MAX_PKT_LEN/2); + // Follow up with regular packets + run_test(2); + `ASSERT_ERROR( + uflow_rise_count == uflow_fall_count && uflow_rise_count == NUM_UFLOW_PACKETS, + "Underflow did not occur as expected." + ) + end + + //-------------------------------- + // Finish Up + //-------------------------------- + + test.end_tb(); + + end : tb_main + +endmodule : axis_pkt_throttle_tb + + +`default_nettype wire