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.
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.- Cycle 0
Edge e9: the header p3.0 arrives with
firstat 1. Its byte 0 is 0xd0, so the filter takes it and drops it. - Cycle 1
Edge e10: p3.1 arrives with
firstat 0. The filter compares nothing now, so p3.1 goes on to the output register. - Cycle 2
Edge e11: p3.1 leaves. The consumer starts a packet with byte 4 of packet 3.
- 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.
| Signal | Driven by | This lesson | Meaning |
|---|---|---|---|
| TVALID | transmitter | s_tvalid, m_tvalid | a beat is offered; once 1, it stays 1, with the beat unchanged, until taken |
| TREADY | receiver | s_tready, m_tready | the receiver takes the offered beat at this edge |
| TDATA | transmitter | 32 bits, 4 byte lanes | lane l is bits 8l+7 to 8l; lane 0 holds the earliest byte |
| TKEEP | transmitter | 4 bits | bit l = 1: lane l holds a byte of the stream; 0: a null byte the receiver may drop |
| TLAST | transmitter | 1 bit | this beat ends the packet |
| TSTRB, TID, TDEST, TUSER | transmitter | not used | position 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
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | the one clock |
| rst_n | input | 1 | reset, active-low, asynchronous |
| s_tvalid | input | 1 | the stream in |
| s_tready | output | 1 | comes from a flop: no path from m_tready to s_tready |
| s_tdata | input | [31:0] | byte lane l is s_tdata[8l+7:8l]; lane 0 comes first |
| s_tkeep | input | [3:0] | 1: this lane holds a byte of the packet |
| s_tlast | input | 1 | the last beat of a packet |
| m_tvalid | output | 1 | the stream out; 0 in reset |
| m_tready | input | 1 | the consumer is ready |
| m_tdata | output | [31:0] | as s_tdata, out |
| m_tkeep | output | [3:0] | as s_tkeep, out |
| m_tlast | output | 1 | as s_tlast, out |
- Clock
- one clock
- Reset
rst_nactive-low, asynchronous;m_tvalidis 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
s_tready.- Header
first_qis 1, so the drop logic compares byte 0 of the beat. It is 0xd0: the beat is taken and dropped, anddrop_qbecomes 1.Follow the calls, values and waits
- data p3.0 type 0xd0: Source → Drop logic
- data drop: Drop logic → Packet state
- Body
first_qis 0 now, so the drop logic usesdrop_q. Every beat up to TLAST is taken and dropped.Follow the calls, values and waits
- data drop_q = 1: Packet state → Drop logic
- data p3.1 .. p3.3: Source → Drop logic
- TLAST
Taking the beat with TLAST sets
first_qand clearsdrop_q. The next beat is treated as a header again.Follow the calls, values and waits
- data TLAST: Drop logic → Packet state
- 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
- data p4.0, p4.1: Source → Drop logic
- data p4.0, p4.1: Drop logic → Main register
- 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.
- Cycle 0
Edge e9: the header p3.0 is compared and dropped.
drop_qis 1 from here. - Cycle 3
Edge e12: p3.3 carries TLAST. It is dropped, and taking it clears
drop_qand setsfirst. - Cycle 4
Edge e13: p4.0 is a header of type 0x33. It passes and shows on the output at e14.
- 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.
| Filter | Cells (flops) | Levels of logic | Edges for 16 beats in, consumer stalls every third edge |
|---|---|---|---|
| decide on the header only (lab) | 131 (77) | 7 | 19, and 2 packets headless |
| hold to TLAST, skid output | 133 (78) | 7 | 17 |
| hold to TLAST, one register, ready passed through | 53 (40) | 6 | 19 |
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
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.
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.
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.
Synthesize the design to measure its cells and inspect the mapped gates.
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
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.