Capstone: an AXI-Stream filter

Build an AXI-Stream stage that drops whole packets by the type in their header beat, holding the decision until TLAST, with a skid buffer on the output.

Lesson
R8.3
Time
40 min
Runs on
Your browser
Check
5 questions

A stream filter was told to drop every packet whose header said type 0xd0. It compared the header beat, dropped that one beat, and kept no record of the decision. The rest of each such packet went through, and the consumer received two packets with no header.

The header is dropped, the body goes onThe lab bench, values just before edges e9 to e14. s and m name the beat on each port: p3.1 is beat 1 of packet 3, whose type is 0xd0. first is the filter's register that marks the next beat as a header.
012345clksp3.0p3.1p3.2p3.3p4.0s_treadys_tlastfirstm_tvalidm_treadymp2.0p3.1p3.2p3.3m_tlast
  1. Cycle 0

    Edge e9: the header p3.0 arrives with first at 1. Its byte 0 is 0xd0, so the filter takes it and drops it.

  2. Cycle 1

    Edge e10: p3.1 arrives with first at 0. The filter compares nothing now, so p3.1 goes on to the output register.

  3. Cycle 2

    Edge e11: p3.1 leaves. The consumer starts a packet with byte 4 of packet 3.

  4. Cycle 5

    Edge e14: p3.3 carries TLAST and leaves. The consumer closes a packet of 9 bytes with no header.

Measured on the lab bench. The bench prints e14 9 bytes from byte 4 of packet 3: headless.

AXI-Stream in one table

AXI-Stream is a valid/ready link with no addresses. A transfer (a beat) moves at a rising edge where TVALID and TREADY are both 1. A packet is a run of beats; the beat with TLAST = 1 ends it. Nothing marks the start: the first beat after a TLAST is the next packet's first beat.

SignalDriven byThis lessonMeaning
TVALIDtransmitters_tvalid, m_tvalida beat is offered; once 1, it stays 1, with the beat unchanged, until taken
TREADYreceivers_tready, m_treadythe receiver takes the offered beat at this edge
TDATAtransmitter32 bits, 4 byte laneslane l is bits 8l+7 to 8l; lane 0 holds the earliest byte
TKEEPtransmitter4 bitsbit l = 1: lane l holds a byte of the stream; 0: a null byte the receiver may drop
TLASTtransmitter1 bitthis beat ends the packet
TSTRB, TID, TDEST, TUSERtransmitternot usedposition bytes, stream and route numbers, sideband bits

The AXI-Stream signals (IHI 0051B §2.1). A port without TKEEP behaves as if every bit were 1.

A packet of 10 bytes on this 4-byte bus takes three beats. The last beat has TKEEP = 0011: lanes 0 and 1 hold bytes 8 and 9, and lanes 2 and 3 are null. The filter passes TKEEP through unchanged with its beat.

The brief and the sketch

Design brief
PortDirectionWidthMeaning
clkinput1the one clock
rst_ninput1reset, active-low, asynchronous
s_tvalidinput1the stream in
s_treadyoutput1comes from a flop: no path from m_tready to s_tready
s_tdatainput[31:0]byte lane l is s_tdata[8l+7:8l]; lane 0 comes first
s_tkeepinput[3:0]1: this lane holds a byte of the packet
s_tlastinput1the last beat of a packet
m_tvalidoutput1the stream out; 0 in reset
m_treadyinput1the consumer is ready
m_tdataoutput[31:0]as s_tdata, out
m_tkeepoutput[3:0]as s_tkeep, out
m_tlastoutput1as s_tlast, out
Clock
one clock
Reset
rst_n active-low, asynchronous; m_tvalid is 0 in reset
Latency
a passed beat appears on the output one edge after it is taken
Throughput
one beat per edge while the consumer is ready
Parameters
Rule: byte 0 of a packet's first beat is its type; a packet of type DROP_TYPE (0xd0) is dropped whole, every other packet passes unchanged
Corner cases
one-beat packets, dropped and passed; a stall in the middle of a packet; partial TKEEP on a last beat; packets back to back. Dropped beats: taken from the input like any beat, never offered on the output
The filterArrows carry beats, except the two between the drop logic and the packet state, which carry the decision and the flags, and s_tready.
The filterArrows carry beats, except the two between the drop logic and the packet state, which carry the decision and the flags, and `s_tready`. source Source offers beats, holdsuntil taken logic Drop logic first_q ? byte 0 ==0xd0 : drop_q flop Packet state first_q, drop_q flop Main register m_tdata, m_tkeep,m_tlast flop Skid register a beat that cameduring a stall sink Consumer splits packets atTLAST beaton a take: decision,TLASTfirst_q, drop_qpassed beatpassed beat, mainstalleddrainbeat outs_tready =!skid_valid
  1. Header

    first_q is 1, so the drop logic compares byte 0 of the beat. It is 0xd0: the beat is taken and dropped, and drop_q becomes 1.

    Follow the calls, values and waits
    1. data p3.0 type 0xd0: Source → Drop logic
    2. data drop: Drop logic → Packet state
  2. Body

    first_q is 0 now, so the drop logic uses drop_q. Every beat up to TLAST is taken and dropped.

    Follow the calls, values and waits
    1. data drop_q = 1: Packet state → Drop logic
    2. data p3.1 .. p3.3: Source → Drop logic
  3. TLAST

    Taking the beat with TLAST sets first_q and clears drop_q. The next beat is treated as a header again.

    Follow the calls, values and waits
    1. data TLAST: Drop logic → Packet state
  4. A passed packet

    Packet 4 has type 0x33. Its beats go to the main register, or to the skid while the main register is stalled.

    Follow the calls, values and waits
    1. data p4.0, p4.1: Source → Drop logic
    2. data p4.0, p4.1: Drop logic → Main register
    3. data p4.0, p4.1: Main register → Consumer
Read the diagram as text
  • Source: offers beats, holds until taken
  • Drop logic: first_q ? byte 0 == 0xd0 : drop_q
  • Packet state: first_q, drop_q
  • Main register: m_tdata, m_tkeep, m_tlast
  • Skid register: a beat that came during a stall
  • Consumer: splits packets at TLAST
  • Source → Drop logic: beat
  • Drop logic → Packet state: on a take: decision, TLAST
  • Packet state → Drop logic: first_q, drop_q
  • Drop logic → Main register: passed beat
  • Drop logic → Skid register: passed beat, main stalled
  • Skid register → Main register: drain
  • Main register → Consumer: beat out
  • Skid register → Source: s_tready = !skid_valid

The output stage is R4.4's skid buffer; the packet state is two flops in front of it.

Hold the decision until TLAST

The type is only in the header beat. Later beats carry data and nothing else, so they cannot be checked alone. The filter must store the decision with the header and use it for every beat up to TLAST.

Decide on the header only

wire drop = first_q && (s_tdata[7:0] == DROP_TYPE);
// on a take:
first_q <= s_tlast;

Two packets out with no header; 13 beats out where 9 belong.

Decide once, hold to TLAST

wire drop = first_q ? (s_tdata[7:0] == DROP_TYPE)
                     : drop_q;
// on a take:
first_q <= s_tlast;
drop_q  <= drop && !s_tlast;

Four packets out, all whole: 9 beats.

Both flags change only when a beat is taken, never on TVALID alone. While a beat waits for TREADY it has not arrived, and the source must hold it unchanged. TLAST resets the packet state: first_q returns to 1 and drop_q to 0.

A packet dropped whole, then one passedThe same bench with the fix, edges e9 to e16. Packet 3 (0xd0, four beats) is dropped; packet 4 (0x33) passes; packet 5 is a one-beat packet of type 0xd0.
01234567clksp3.0p3.1p3.2p3.3p4.0p4.1p5.0p6.0s_tlastfirstdrop_qm_tvalidm_treadymp2.0-p4.0p4.1
  1. Cycle 0

    Edge e9: the header p3.0 is compared and dropped. drop_q is 1 from here.

  2. Cycle 3

    Edge e12: p3.3 carries TLAST. It is dropped, and taking it clears drop_q and sets first.

  3. Cycle 4

    Edge e13: p4.0 is a header of type 0x33. It passes and shows on the output at e14.

  4. Cycle 6

    Edge e15: the consumer stalls, so p4.1 waits in the main register. p5.0 is a one-beat packet of type 0xd0; it is dropped while the output waits.

Measured on the lab bench with the fix. Packet 3 never reaches the output; nothing waits while it is dropped.

What the stage costs

The output stage is R4.4's skid buffer, so s_tready comes from a flop. A plain register with s_tready = m_tready || !m_tvalid is smaller. It also stalls dropped beats whenever the output is stalled, though they need no room.

FilterCells (flops)Levels of logicEdges for 16 beats in, consumer stalls every third edge
decide on the header only (lab)131 (77)719, and 2 packets headless
hold to TLAST, skid output133 (78)717
hold to TLAST, one register, ready passed through53 (40)619

Measured with Yosys 0.70 on this flow and on the lab bench. 74 of the skid version's flops are its two 37-bit payload registers.

Holding the decision costs one flop and two cells. The skid doubles the payload registers and keeps the input moving: with no stalls every version takes 16 beats in 16 edges.

The lab

Run the lab. The bench sends seven packets of 10, 7, 4, 13, 6, 2 and 9 bytes; packets 1, 3 and 5 have type 0xd0. Byte k of packet p reads as hex digits p and k, so 0x34 is byte 4 of packet 3.

The consumer stalls before every third edge and rebuilds each packet from the lanes with TKEEP = 1. Two lines end in "headless". Apply the right-hand side of the compare block: declare drop_q, reset it to 0, and update it beside first_q. Run again: four packets, all ok.

Asked in interviews
  1. What is AXI-Stream?

    A point-to-point valid/ready link with no addresses. A beat moves when TVALID and TREADY are both 1. TLAST ends a packet, TKEEP marks which byte lanes hold data, and TID, TDEST and TUSER are optional sideband.

  2. How do you drop a packet in a streaming pipeline?

    Decide on the header beat and store the decision in a flag. Apply it to every beat up to TLAST. Take dropped beats from the input like any other beat, but never offer them downstream.

  3. What does TLAST reset?

    The packet state: the flag that says the next beat is a header, and any per-packet decision. Update both only on a handshake, never on TVALID alone.

Try it

Each line is one packet as the consumer rebuilt it, at the edge where its TLAST beat left. Packets 1 and 3 lost only their header beat. Packet 5 has one beat, so dropping the header dropped all of it.

With the fix the run prints e7 packet 0, e10 packet 2, e16 packet 4 and e20 packet 6, all ok. The summary reads 7 packets in; out: 4 ok, 0 headless, 0 wrong; 16 beats in (e2-e18), 9 out (e4-e20). Synthesize reports 133 cells, 78 flops and 7 levels of logic on this flow.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e7 packet 0 type 0x11, 10 bytes: ok e8 3 bytes from byte 4 of packet 1: headless e10 packet 2 type 0x22, 4 bytes: ok e14 9 bytes from byte 4 of packet 3: headless e17 packet 4 type 0x33, 6 bytes: ok e22 packet 6 type 0x55, 9 bytes: ok 7 packets in; out: 4 ok, 2 headless, 0 wrong; 16 beats in (e2-e20), 13 out (e4-e22)

Original design · 131 cells · 77 flip-flops · 0 latches · 7 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic        s_tvalid = 0, s_tlast = 0, m_tready = 0;
  logic [31:0] s_tdata = 0;
  logic [3:0]  s_tkeep = 0;
  logic        s_tready, m_tvalid, m_tlast;
  logic [31:0] m_tdata;
  logic [3:0]  m_tkeep;
  bit quiet = 0, watch = 0;
  int ptype[$] = '{17, 208, 34, 208, 51, 208, 85};   // packet types
  int plen[$]  = '{10, 7, 4, 13, 6, 2, 9};   // packet lengths in bytes
  int p = 0, b = 0;                      // the source offers packet p from byte b
  int e = -1;                            // the number of the last rising edge
  bit taken = 0;                         // the beat offered was taken at the last edge
  int expect_q[$];                       // the packets the rule lets through, in order
  logic [7:0] rx[$];                     // the bytes of the packet arriving at the consumer
  int n_in = 0, n_out = 0, pk_in = 0, ok_n = 0, headless = 0, wrong = 0;
  int in_first = -1, in_last = -1, out_first = -1, out_last = -1;
  // Byte k of packet p.
  function automatic logic [7:0] byte_of(input int p, input int k);
    if (k == 0) return 8'(ptype[p]);
    return {4'(p), 4'(k)};
  endfunction
  // A beat's name: p2.1 is beat 1 of packet 2 (beat 0 is the header beat).
  function automatic string name(input logic v, input logic [31:0] d);
    if (!v) return "-";
    if (d[3:0] == 4'h4 || d[3:0] == 4'h8 || d[3:0] == 4'hc) return $sformatf("p%0d.%0d", d[7:4], d[3:0] / 4);
    return $sformatf("p%0d.0", d[15:12]);
  endfunction
  // The consumer has a whole packet: compare it with the next one the rule lets through.
  task automatic judge(input int at);
    int q;
    bit same;
    logic [7:0] b0;
    same = 0;
    b0 = rx[0];
    if (expect_q.size() > 0) begin
      q = expect_q[0];
      same = (rx.size() == plen[q]);
      for (int k = 0; k < rx.size(); k++) if (rx[k] !== byte_of(q, k)) same = 0;
    end
    if (same) begin
      ok_n++; q = expect_q.pop_front();
      if (!quiet) $display("e%0d packet %0d type 0x%h, %0d bytes: ok", at, q, 8'(ptype[q]), rx.size());
    end else if (b0[3:0] == 4'h4) begin
      headless++;
      if (!quiet) $display("e%0d %0d bytes from byte 4 of packet %0d: headless", at, rx.size(), b0[7:4]);
    end else begin
      wrong++;
      if (!quiet) $display("e%0d %0d bytes from 0x%h: wrong", at, rx.size(), b0);
    end
    rx.delete();
  endtask
  // At every edge: what moved, judged on the values just before the edge.
  always @(posedge clk) begin
    e = e + 1;
    taken = 0;
    if (watch && e >= 2)
      $display("w e%0d s_tvalid=%b s_tready=%b s=%s s_tkeep=%b s_tlast=%b m_tvalid=%b m_tready=%b m=%s m_tkeep=%b m_tlast=%b",
               e, s_tvalid, s_tready, name(s_tvalid, s_tdata), s_tkeep, s_tlast,
               m_tvalid, m_tready, name(m_tvalid, m_tdata), m_tkeep, m_tlast);
    if (rst_n && s_tvalid && s_tready) begin
      taken = 1; n_in++; if (s_tlast) pk_in++;
      if (in_first < 0) in_first = e;
      in_last = e;
    end
    if (rst_n && m_tvalid && m_tready) begin
      n_out++;
      if (out_first < 0) out_first = e;
      out_last = e;
      for (int l = 0; l < 4; l++) if (m_tkeep[l]) rx.push_back(m_tdata[8*l +: 8]);
      if (m_tlast) judge(e);
    end
  end

  axis_filter dut (.clk(clk), .rst_n(rst_n), .s_tvalid(s_tvalid), .s_tready(s_tready), .s_tdata(s_tdata),
                   .s_tkeep(s_tkeep), .s_tlast(s_tlast), .m_tvalid(m_tvalid), .m_tready(m_tready),
                   .m_tdata(m_tdata), .m_tkeep(m_tkeep), .m_tlast(m_tlast));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (edge_n >= 1) begin
      // The source moves on when its beat was taken, then offers the next one; the consumer may stall.
      if (taken) begin b += 4; if (b >= plen[p]) begin p++; b = 0; end end
      s_tvalid = (p < ptype.size()) && !(0);
      if (p < ptype.size()) begin
        for (int l = 0; l < 4; l++) begin
          s_tdata[8*l +: 8] = byte_of(p, b + l);
          s_tkeep[l] = (b + l < plen[p]);
        end
        s_tlast = (b + 4 >= plen[p]);
      end
      m_tready = !(e % 3 == 2);
    end
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    for (int k = 0; k < ptype.size(); k++) if (ptype[k] != 'hd0) expect_q.push_back(k);
    repeat (30) tick();
    $display("%0d packets in; out: %0d ok, %0d headless, %0d wrong; %0d beats in (e%0d-e%0d), %0d out (e%0d-e%0d)",
             pk_in, ok_n, headless, wrong, n_in, in_first, in_last, n_out, out_first, out_last);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe filter with the fix. Two packets of 12 bytes (three beats each) arrive back to back; the first has type 0xd0, the second 0x11. The consumer never stalls. What does the program print?
// axis_filter: an AXI-Stream stage that drops every packet whose header beat (the first beat
// of a packet) carries DROP_TYPE in byte 0. Other packets pass unchanged. A beat moves on a port
// at an edge where TVALID and TREADY are both 1; the beat with TLAST = 1 ends its packet.
module axis_filter #(parameter logic [7:0] DROP_TYPE = 8'hd0) (
  input  logic        clk,
  input  logic        rst_n,          // active-low, asynchronous
  input  logic        s_tvalid,       // the stream in
  output logic        s_tready,
  input  logic [31:0] s_tdata,        // byte lane l is s_tdata[8l+7:8l]; lane 0 comes first
  input  logic [3:0]  s_tkeep,        // 1: this lane holds a byte of the packet
  input  logic        s_tlast,
  output logic        m_tvalid,       // the stream out
  input  logic        m_tready,
  output logic [31:0] m_tdata,
  output logic [3:0]  m_tkeep,
  output logic        m_tlast
);
  // The packet state.
  logic first_q;                         // the next beat to arrive is a header beat
  logic drop_q;                          // the packet in progress is being dropped
  wire  take = s_tvalid && s_tready;     // a beat arrives at this edge
  wire  drop = first_q ? (s_tdata[7:0] == DROP_TYPE) : drop_q;   // decided once per packet
  wire  fwd  = take && !drop;            // the beat arriving goes on to the output

  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      first_q <= 1'b1; drop_q <= 1'b0;
    end else if (take) begin
      first_q <= s_tlast;                // after a last beat comes a header beat
      drop_q  <= drop && !s_tlast;       // hold the decision until the last beat
    end

  // The output stage: a main register and a skid register (R4.4). A dropped beat is taken
  // from the input and never reaches them.
  logic        skid_valid;
  logic [36:0] main_q, skid_q;           // {last, keep, data}
  wire         move = m_tready || !m_tvalid;   // the main register may load at this edge
  assign s_tready = !skid_valid;         // from a flop

  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      m_tvalid <= 1'b0; skid_valid <= 1'b0;
    end else if (move) begin
      if (skid_valid) begin m_tvalid <= 1'b1; skid_valid <= 1'b0; end   // drain the skid first
      else            m_tvalid <= fwd;
    end else if (fwd) begin
      skid_valid <= 1'b1;                // main is stalled: the beat waits in the skid
    end

  always_ff @(posedge clk)               // the payload registers have no reset
    if (move) begin
      if (skid_valid) main_q <= skid_q;
      else if (fwd)   main_q <= {s_tlast, s_tkeep, s_tdata};
    end else if (fwd) begin
      skid_q <= {s_tlast, s_tkeep, s_tdata};
    end
  assign {m_tlast, m_tkeep, m_tdata} = main_q;
endmodule

`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic        s_tvalid = 0, s_tlast = 0, m_tready = 0;
  logic [31:0] s_tdata = 0;
  logic [3:0]  s_tkeep = 0;
  logic        s_tready, m_tvalid, m_tlast;
  logic [31:0] m_tdata;
  logic [3:0]  m_tkeep;
  bit quiet = 1, watch = 0;
  int ptype[$] = '{208, 17};   // packet types
  int plen[$]  = '{12, 12};   // packet lengths in bytes
  int p = 0, b = 0;                      // the source offers packet p from byte b
  int e = -1;                            // the number of the last rising edge
  bit taken = 0;                         // the beat offered was taken at the last edge
  int expect_q[$];                       // the packets the rule lets through, in order
  logic [7:0] rx[$];                     // the bytes of the packet arriving at the consumer
  int n_in = 0, n_out = 0, pk_in = 0, ok_n = 0, headless = 0, wrong = 0;
  int in_first = -1, in_last = -1, out_first = -1, out_last = -1;
  // Byte k of packet p.
  function automatic logic [7:0] byte_of(input int p, input int k);
    if (k == 0) return 8'(ptype[p]);
    return {4'(p), 4'(k)};
  endfunction
  // A beat's name: p2.1 is beat 1 of packet 2 (beat 0 is the header beat).
  function automatic string name(input logic v, input logic [31:0] d);
    if (!v) return "-";
    if (d[3:0] == 4'h4 || d[3:0] == 4'h8 || d[3:0] == 4'hc) return $sformatf("p%0d.%0d", d[7:4], d[3:0] / 4);
    return $sformatf("p%0d.0", d[15:12]);
  endfunction
  // The consumer has a whole packet: compare it with the next one the rule lets through.
  task automatic judge(input int at);
    int q;
    bit same;
    logic [7:0] b0;
    same = 0;
    b0 = rx[0];
    if (expect_q.size() > 0) begin
      q = expect_q[0];
      same = (rx.size() == plen[q]);
      for (int k = 0; k < rx.size(); k++) if (rx[k] !== byte_of(q, k)) same = 0;
    end
    if (same) begin
      ok_n++; q = expect_q.pop_front();
      if (!quiet) $display("e%0d packet %0d type 0x%h, %0d bytes: ok", at, q, 8'(ptype[q]), rx.size());
    end else if (b0[3:0] == 4'h4) begin
      headless++;
      if (!quiet) $display("e%0d %0d bytes from byte 4 of packet %0d: headless", at, rx.size(), b0[7:4]);
    end else begin
      wrong++;
      if (!quiet) $display("e%0d %0d bytes from 0x%h: wrong", at, rx.size(), b0);
    end
    rx.delete();
  endtask
  // At every edge: what moved, judged on the values just before the edge.
  always @(posedge clk) begin
    e = e + 1;
    taken = 0;
    if (watch && e >= 2)
      $display("w e%0d s_tvalid=%b s_tready=%b s=%s s_tkeep=%b s_tlast=%b m_tvalid=%b m_tready=%b m=%s m_tkeep=%b m_tlast=%b",
               e, s_tvalid, s_tready, name(s_tvalid, s_tdata), s_tkeep, s_tlast,
               m_tvalid, m_tready, name(m_tvalid, m_tdata), m_tkeep, m_tlast);
    if (rst_n && s_tvalid && s_tready) begin
      taken = 1; n_in++; if (s_tlast) pk_in++;
      if (in_first < 0) in_first = e;
      in_last = e;
    end
    if (rst_n && m_tvalid && m_tready) begin
      n_out++;
      if (out_first < 0) out_first = e;
      out_last = e;
      for (int l = 0; l < 4; l++) if (m_tkeep[l]) rx.push_back(m_tdata[8*l +: 8]);
      if (m_tlast) judge(e);
    end
  end

  axis_filter dut (.clk(clk), .rst_n(rst_n), .s_tvalid(s_tvalid), .s_tready(s_tready), .s_tdata(s_tdata),
                   .s_tkeep(s_tkeep), .s_tlast(s_tlast), .m_tvalid(m_tvalid), .m_tready(m_tready),
                   .m_tdata(m_tdata), .m_tkeep(m_tkeep), .m_tlast(m_tlast));
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (edge_n >= 1) begin
      // The source moves on when its beat was taken, then offers the next one; the consumer may stall.
      if (taken) begin b += 4; if (b >= plen[p]) begin p++; b = 0; end end
      s_tvalid = (p < ptype.size()) && !(0);
      if (p < ptype.size()) begin
        for (int l = 0; l < 4; l++) begin
          s_tdata[8*l +: 8] = byte_of(p, b + l);
          s_tkeep[l] = (b + l < plen[p]);
        end
        s_tlast = (b + 4 >= plen[p]);
      end
      m_tready = !(0);
    end
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    repeat (12) tick();
    $display("beats in: %0d, beats out: %0d", n_in, n_out);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule
Spot the bugEach option is the line that updates drop_q when a beat is taken (inside else if (take)). The lab bench expects four whole packets and none headless. Which line fails?
ConceptWhy must the filter store its decision in a register?
ConceptA packet of 10 bytes crosses the 4-byte bus. Its last beat has TKEEP = 0011. What does that say?
Read the waveformThe filter with the fix, values just before edges e9 to e16. Over which edges are the beats of packet 3 taken and dropped?
Packet 3 is dropped
01234567sp3.0p3.1p3.2p3.3p4.0p4.1p5.0p6.0s_tlastfirstdrop_qm_tvalidmp2.0-p4.0p4.1

Edges e9 to e16, left to right; s_tready is 1 throughout.

Remember

  • An AXI-Stream beat moves when TVALID and TREADY are both 1; TLAST ends a packet, and the next beat starts the next one.
  • A filter decides on the header beat and must hold the decision in a flag for every beat up to TLAST.
  • Update the packet state only on a handshake; TLAST resets it.
  • Dropped beats are taken from the input and never offered downstream; TKEEP travels with its beat unchanged.
  • On this flow the decision flag cost one flop and two cells; the skid output kept the input moving through stalls.