AXI-Stream in Verilog: TVALID/TREADY/TLAST with Code
AXI4-Stream is the point-to-point dataflow interface used everywhere in modern FPGA and SoC design: DMA engines, video pipelines, FFTs, Ethernet MACs and AI accelerators all move data as an AXI-Stream. This page gives you working, simulator-verified AXI-Stream Verilog code for the two building blocks every stream design needs — a packet source (master) and a register slice (skid buffer) — and explains the one rule that governs every transfer.
The entire protocol reduces to a single sentence: a data beat moves on a rising clock edge only when tvalid and tready are both high. tdata carries the payload and tlast marks the final beat of a packet. Get that handshake right and back-pressure, flow control and packetization all fall into place.
A stream master emits a fixed packet of N beats (values 0x10, 0x11, …), asserting tlast on the final beat, into a clocked sink that applies a repeating back-pressure pattern on tready. The self-checking testbench records every accepted beat and proves each one arrives exactly once, in order, that tlast lands only on the last beat, and that tdata stays stable — with nothing consumed — while tready is low.
// AXI4-Stream master emits a fixed packet of N beats; sink applies back-pressure.
// A beat transfers only at a rising clock edge when tvalid && tready are both high.
module axis_master #(
parameter DW = 8,
parameter N = 6
)(
input wire clk,
input wire rst_n,
output reg [DW-1:0] m_tdata,
output reg m_tvalid,
output reg m_tlast,
input wire m_tready
);
reg [31:0] idx;
// Payload/flags are a pure function of the current beat index.
// tvalid is NOT gated by tready (avoids handshake deadlock).
always @(*) begin
m_tdata = idx[DW-1:0] + 8'h10; // beat k carries value 0x10 + k
m_tvalid = (idx < N);
m_tlast = (idx == (N-1));
end
// Advance to the next beat only when the current beat is accepted.
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
idx <= 32'd0;
else if (m_tvalid && m_tready)
idx <= idx + 32'd1;
end
endmodule
// Clocked sink: records every accepted beat so the testbench can audit order.
module axis_sink #(
parameter DW = 8,
parameter N = 6
)(
input wire clk,
input wire rst_n,
input wire [DW-1:0] s_tdata,
input wire s_tvalid,
input wire s_tlast,
input wire s_tready,
output reg [31:0] count,
output reg last_ok, // tlast seen exactly on final beat
output reg last_err // tlast seen on a non-final beat
);
reg [DW-1:0] mem [0:N-1]; // Verilog-2005: arrays cannot be ports; keep internal
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 32'd0;
last_ok <= 1'b0;
last_err <= 1'b0;
end else if (s_tvalid && s_tready) begin
mem[count[2:0]] <= s_tdata;
if (s_tlast && (count == (N-1))) last_ok <= 1'b1;
if (s_tlast && (count != (N-1))) last_err <= 1'b1;
count <= count + 32'd1;
end
end
endmodule
module tb;
localparam DW = 8;
localparam N = 6;
reg clk, rst_n, tready;
wire [DW-1:0] tdata;
wire tvalid, tlast;
wire [31:0] count;
wire last_ok, last_err;
axis_master #(.DW(DW), .N(N)) m (
.clk(clk), .rst_n(rst_n),
.m_tdata(tdata), .m_tvalid(tvalid), .m_tlast(tlast), .m_tready(tready)
);
axis_sink #(.DW(DW), .N(N)) s (
.clk(clk), .rst_n(rst_n),
.s_tdata(tdata), .s_tvalid(tvalid), .s_tlast(tlast), .s_tready(tready),
.count(count), .last_ok(last_ok), .last_err(last_err)
);
initial clk = 1'b0;
always #5 clk = ~clk;
integer errors;
integer k;
integer cyc;
reg [DW-1:0] held; // value observed while stalled
reg held_armed;
initial begin
rst_n = 1'b0;
tready = 1'b0;
errors = 0;
held_armed = 1'b0;
@(negedge clk); rst_n = 1'b1;
// Drive a fixed, deterministic back-pressure pattern on tready.
// Pattern 1,0,1,1,0,... repeats; stalls prove data holds and nothing drops.
for (cyc = 0; cyc < 40; cyc = cyc + 1) begin
@(negedge clk);
case (cyc % 3)
1: tready = 1'b0; // stall
default: tready = 1'b1;
endcase
// While stalled with a valid beat pending, remember tdata to confirm
// it is unchanged on the next stalled observation.
if (!tready && tvalid) begin
if (held_armed && (tdata !== held)) begin
$display("FAIL: data changed while stalled 0x%02x -> 0x%02x", held, tdata);
errors = errors + 1;
end
held = tdata;
held_armed = 1'b1;
end else begin
held_armed = 1'b0;
end
if (count == N) cyc = 40;
end
// Audit: exactly N beats, in order, each exactly once.
if (count !== N) begin
$display("FAIL: got %0d beats, expected %0d", count, N);
errors = errors + 1;
end
for (k = 0; k < N; k = k + 1) begin
if (s.mem[k] !== (k[DW-1:0] + 8'h10)) begin
$display("FAIL: beat %0d = 0x%02x expected 0x%02x", k, s.mem[k], (k[DW-1:0]+8'h10));
errors = errors + 1;
end else begin
$display("PASS: beat %0d received in order = 0x%02x", k, s.mem[k]);
end
end
if (!last_ok) begin $display("FAIL: tlast not seen on final beat"); errors = errors + 1; end
else $display("PASS: tlast landed on final beat %0d", N-1);
if (last_err) begin $display("FAIL: tlast asserted on a non-final beat"); errors = errors + 1; end
else $display("PASS: no premature tlast");
if (errors == 0) $display("ALL TESTS PASSED");
else $display("TESTS FAILED: %0d error(s)", errors);
$finish;
end
endmodule
PASS: beat 0 received in order = 0x10 PASS: beat 1 received in order = 0x11 PASS: beat 2 received in order = 0x12 PASS: beat 3 received in order = 0x13 PASS: beat 4 received in order = 0x14 PASS: beat 5 received in order = 0x15 PASS: tlast landed on final beat 5 PASS: no premature tlast ALL TESTS PASSEDThe log shows all six beats (0x10–0x15) received in strict order with
tlastasserted only on beat 5, under a deterministic stall pattern. Because the sink is a clocked recorder, a dropped or duplicated beat would corrupt the orderedmemarray and print a FAIL; the clean PASS lines confirm the handshake obeys the beat-moves-only-when-both-high rule.A 1-deep AXI-Stream register slice (skid buffer) sits between a master and a slave. It registers the payload so it can break a long combinational
tvalid/treadypath for timing closure, while guaranteeing no beat is ever dropped or duplicated. When the downstream stalls, the in-flight beat is captured in the skid register ands_treadyis deasserted upstream. The testbench streams a 10-beat packet through it under a repeating stall pattern and checks the output stream against a golden ordered reference.// 1-deep AXI4-Stream skid-buffer register slice. // Registers the data path so it breaks the combinational tvalid/tready path, // yet drops no beat and duplicates none under arbitrary back-pressure. module axis_skid #( parameter DW = 8 )( input wire clk, input wire rst_n, input wire [DW-1:0] s_tdata, input wire s_tlast, input wire s_tvalid, output wire s_tready, output wire [DW-1:0] m_tdata, output wire m_tlast, output wire m_tvalid, input wire m_tready ); reg [DW-1:0] skid_data; reg skid_last; reg skid_full; assign s_tready = ~skid_full; // accept only when empty assign m_tdata = skid_full ? skid_data : s_tdata; assign m_tlast = skid_full ? skid_last : s_tlast; assign m_tvalid = skid_full ? 1'b1 : s_tvalid; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin skid_full <= 1'b0; skid_data <= {DW{1'b0}}; skid_last <= 1'b0; end else if (!skid_full) begin // empty: capture a beat only if upstream offers one that downstream // is NOT taking this cycle (otherwise it passes straight through). if (s_tvalid && !m_tready) begin skid_data <= s_tdata; skid_last <= s_tlast; skid_full <= 1'b1; end end else if (m_tready) begin skid_full <= 1'b0; // drain on downstream accept end end endmodule module tb; localparam DW = 8; localparam N = 10; reg clk, rst_n; reg [DW-1:0] up_data; reg up_last; reg up_valid; wire up_ready; wire [DW-1:0] dn_data; wire dn_last; wire dn_valid; reg dn_ready; axis_skid #(.DW(DW)) dut ( .clk(clk), .rst_n(rst_n), .s_tdata(up_data), .s_tlast(up_last), .s_tvalid(up_valid), .s_tready(up_ready), .m_tdata(dn_data), .m_tlast(dn_last), .m_tvalid(dn_valid), .m_tready(dn_ready) ); initial clk = 1'b0; always #5 clk = ~clk; integer errors; integer send_idx; // next payload value index to present upstream integer recv_idx; // beats accepted downstream integer cyc; reg [DW-1:0] exp_mem [0:N-1]; integer g; initial begin for (g = 0; g < N; g = g + 1) exp_mem[g] = g + 8'h20; rst_n = 1'b0; up_valid = 1'b0; up_data = 8'h00; up_last = 1'b0; dn_ready = 1'b0; errors = 0; send_idx = 0; recv_idx = 0; @(negedge clk); rst_n = 1'b1; for (cyc = 0; cyc < 100; cyc = cyc + 1) begin // Drive inputs just after the negedge, then let continuous assigns settle. @(negedge clk); if (send_idx < N) begin up_valid = 1'b1; up_data = send_idx[DW-1:0] + 8'h20; up_last = (send_idx == (N-1)); end else begin up_valid = 1'b0; end case (cyc % 4) 2: dn_ready = 1'b0; // deterministic stall every 4th cycle default: dn_ready = 1'b1; endcase #1; // allow s_tready / m_tvalid / m_tdata to propagate before sampling // Sample stable pre-posedge handshakes. if (up_valid && up_ready) send_idx = send_idx + 1; if (dn_valid && dn_ready) begin if (dn_data !== exp_mem[recv_idx]) begin $display("FAIL: out beat %0d = 0x%02x expected 0x%02x", recv_idx, dn_data, exp_mem[recv_idx]); errors = errors + 1; end else begin $display("PASS: out beat %0d = 0x%02x tlast=%b", recv_idx, dn_data, dn_last); end if ((recv_idx == (N-1)) && !dn_last) begin $display("FAIL: tlast missing on final out beat"); errors = errors + 1; end if ((recv_idx != (N-1)) && dn_last) begin $display("FAIL: premature tlast on out beat %0d", recv_idx); errors = errors + 1; end recv_idx = recv_idx + 1; end @(posedge clk); if (recv_idx == N) cyc = 100; end if (recv_idx !== N) begin $display("FAIL: delivered %0d beats, expected %0d", recv_idx, N); errors = errors + 1; end if (errors == 0) $display("ALL TESTS PASSED"); else $display("TESTS FAILED: %0d error(s)", errors); $finish; end endmodulePASS: out beat 0 = 0x20 tlast=0
PASS: out beat 1 = 0x21 tlast=0
PASS: out beat 2 = 0x22 tlast=0
PASS: out beat 3 = 0x23 tlast=0
PASS: out beat 4 = 0x24 tlast=0
PASS: out beat 5 = 0x25 tlast=0
PASS: out beat 6 = 0x26 tlast=0
PASS: out beat 7 = 0x27 tlast=0
PASS: out beat 8 = 0x28 tlast=0
PASS: out beat 9 = 0x29 tlast=1
ALL TESTS PASSEDThe log shows all ten beats (0x20–0x29) emerging downstream in order with
tlaston the final beat, despite periodic back-pressure. Each output beat is checked against a precomputed golden array, so any reordering, loss or duplication would print a FAIL — the all-PASS run demonstrates the skid buffer is lossless and transparent.The valid/ready contract
AXI-Stream flow control rests on two signals travelling in opposite directions. The source drives
tvalidto say “I have a valid beat ontdatathis cycle.” The sink drivestreadyto say “I can accept a beat this cycle.” A transfer — one beat — happens on the rising clock edge where both are high at the same time. Nothing else moves data.The protocol imposes a stability obligation on the source: once
tvalidis asserted, the source must keeptvalidhigh and must holdtdata,tlastand the sideband signals unchanged until the beat is accepted. It may not withdraw an offered beat or change its value just because the sink is slow. In Example 1 the payload is a pure function of a beat index that only advances on an accepted transfer, which is exactly why the data is stable across every stall.
- tvalid — asserted by the source; once high, stays high and data stays frozen until accepted.
- tready — asserted by the sink; may toggle freely cycle to cycle (that is back-pressure).
- Transfer — occurs the cycle both are high; the source may then present the next beat.
tlast and packetization
tlastmarks the last beat of a packet (a frame, a descriptor, a burst — whatever the application defines). It is valid only on an accepted beat and must be asserted on exactly the final beat of each packet. Downstream blocks use it to know where one packet ends and the next begins without a separate length signal: a video line, an Ethernet frame boundary, or the end of a DMA transfer.A common interview point:
tlastis optional in the AXI-Stream spec. A pure, endless data stream (for example a continuous ADC feed) may omit it entirely and simply never signal a boundary. But if you drive it, it must track real packet boundaries — both example testbenches explicitly check thattlastis high on the final beat and low on every other beat, because a mis-placedtlastsilently corrupts framing downstream.tkeep and tstrb
Real AXI-Stream links are usually wider than one byte, and two optional byte-qualifier signals say which bytes of
tdatamatter.tkeepmarks which byte lanes carry content that must be delivered; a null byte (tkeep low) is a position byte that can be removed from the stream — this is how you signal that the last beat of a packet is only partially full.tstrbdistinguishes data bytes from position bytes within the kept set and is rarely used in practice.For a byte-aligned stream where every beat is full, you can omit both; a receiver treats all bytes as valid. The examples here use a single-byte
tdata, so no byte qualifiers are needed. When you widen the bus — say 64-bittdatawith an 8-bittkeep— the handshake logic is unchanged; you only add the lane-enable bits alongside the payload.Why AXI-Stream is the standard FPGA dataflow interface
AXI-Stream won because it is minimal and composable. There are no addresses, no bursts and no responses — just a ready/valid handshake plus payload — so it maps directly onto streaming compute, and any two AXI-Stream blocks can be chained as long as widths and sideband signals agree. Xilinx/AMD and Intel/Altera IP (DMA, FFT, FIR, video processing, AI engines) expose AXI-Stream ports precisely so they drop into a pipeline without glue logic.
The ready/valid discipline also gives you elastic pipelines for free. Insert a register slice (Example 2) anywhere the path is too long for timing, and the stream keeps its exact semantics — same beats, same order, same packet boundaries — just one cycle later. This is why skid buffers are the most-instantiated module in large stream designs.
Deadlock avoidance: never gate tvalid on tready
The single most dangerous mistake in AXI-Stream Verilog code is making
tvalidcombinationally depend ontready(for exampleassign tvalid = have_data & tready;). If the source only assertstvalidwhen it already seestready, and the sink only assertstreadywhen it seestvalid, neither ever fires and the link deadlocks.The rule is one-directional: a source may assert
tvalidas soon as it has data, regardless oftready; a sink may asserttreadyregardless oftvalid. Only the sink is allowed to look at the other side's signal to form its decision cheaply — and even then, makingtreadydepend combinationally ontvalidacross many stages builds a long timing path, which is the exact problem the skid buffer solves. In Example 1, noticem_tvalidis driven purely from the beat index and never readsm_tready.FAQ
Can tready be asserted before tvalid?
Yes. A sink may hold
treadyhigh permanently if it can always accept. The transfer still only occurs on the cycletvalidalso goes high. Assertingtreadyearly costs nothing and avoids a combinational dependency.What is the difference between a register slice and a FIFO?
A register slice (skid buffer) is a depth-1 or depth-2 element used purely for timing isolation and holds at most one or two beats. A FIFO provides real buffering and rate decoupling across many beats. Both present AXI-Stream ports; the skid buffer in Example 2 is the minimal lossless case.
Why does a simple pass-through register drop throughput?
A naive single register that deasserts
treadywhenever it holds data can only accept a new beat every other cycle. A full skid buffer adds the logic to accept while draining; the 1-deep version here guarantees correctness (no drop, no duplicate) and is the clearest starting point before moving to a 2-deep full-throughput slice.Must tdata be held stable during a stall?
Yes — once
tvalidis asserted,tdataandtlastmust not change until the beat is accepted. Example 1's testbench explicitly samples and compares the held value across stall cycles to prove this.Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres