FPGA development board with ribbon cables on a bench, a waveform viewer with square wave signals and data buses on the monitor behind it, titled AXI4-Stream Width Converter

AXI4-Stream Width Converter: Waves and Architecture

A study companion for the agentic loop article: the AXI4-Stream handshake on waveforms, what a 32 to 128 bit width converter must do with tkeep and tlast, a register level architecture, and what the testbench checks.

By Leonardo Capossio
Contact us Subscribe

This is the appendix to Agentic AI for FPGA Design: How the Loop Works. That article uses a 32 bit to 128 bit AXI4-Stream width converter as its worked example and hands the design to an agent. This page is for the person who wants to understand the block itself: what the interface promises, what the converter must do on every clock edge, one way to build it, and what the testbench in the companion repository checks. Read it before you run the loop, or after, when a mismatch report needs decoding.

The AXI4-Stream handshake

AXI4-Stream carries data in one direction, from a master to a slave, in beats. Every beat is a bundle of signals qualified by two handshake lines.[1] The converter has two such interfaces: a slave side that receives 32 bit beats, prefixed s_, and a master side that sends 128 bit words, prefixed m_.

Signal Driven by Meaning
tdatamasterThe payload of the beat
tvalidmasterThe beat on tdata is real and may be taken
treadyslaveThe slave can accept a beat this cycle
tlastmasterThis beat ends a packet
tkeepmasterOne bit per byte of tdata: 1 if the byte carries data

A beat transfers on a clock edge where tvalid and tready are both high. The specification adds three rules that every AXI4-Stream block lives by, and the second exists to make chains of blocks deadlock free:

  1. Once the master raises tvalid, it keeps it high, and keeps tdata, tkeep and tlast unchanged, until the transfer happens. The master does not get to change its mind.
  2. The master must not wait for tready before raising tvalid. A slave is allowed to wait for tvalid before raising tready. If both waited for each other, nothing would ever move.
  3. tready may go high and low freely while tvalid is low. Only the cycle where both are high means anything.
Waveform of an AXI4-Stream handshake over eight cycles: beat A transfers in cycle 1, beat B is held through cycle 2 while tready is low and transfers in cycle 3, then C and D transfer on consecutive cycles
Cycles are numbered from 0 along the top, and a transfer happens on the edge that ends the cycle. A transfers in cycle 1. B is presented in cycle 2 but tready is low, so the master holds it and it transfers in cycle 3. C and D go through back to back.

Our converter has the slave role on its input and the master role on its output, so it must obey rule 1 on m_tvalid and is free to drive s_tready from its own state, including from m_tready.

What the converter does

The input carries 32 bit beats, four bytes each, all of them valid; there is no s_tkeep. The output carries 128 bit words. Beat i of a packet lands in output word i / 4, in lane i mod 4, where lane 0 is bits 31:0 and lane 3 is bits 127:96. The first beat of a word sits in the least significant lanes.

Waveform of eight input beats b0 to b7 accepted on consecutive cycles, with the first output word b3 b2 b1 b0 and tkeep FFFF appearing in cycle 4, one cycle after b3
Four beats in, one word out. With no back pressure the converter accepts a beat every cycle and emits a word every fourth cycle, one cycle after the beat that completed it.

A word is complete when it holds four beats, or when the beat it just took was marked s_tlast. The second case is the whole difficulty. A packet whose length is not a multiple of four ends with a word that is only partly filled, and the converter has to emit it anyway, with two things set correctly:

  • m_tkeep marks which bytes hold data. A word with n beats has its low 4n bits set: 000F for one beat, 00FF for two, 0FFF for three, FFFF for four. The bytes above are unspecified and a downstream block must not read them.
  • m_tlast is high on that word and on no other.
Waveform of a six beat packet: word b3 b2 b1 b0 with tkeep FFFF in cycle 4, then s_tlast on b5 in cycle 5, then in cycle 6 a second word holding b5 and b4 in the low lanes with tkeep 00FF and m_tlast high
A six beat packet. The second word holds only b4 and b5, so tkeep is 00FF and m_tlast closes the packet. The upper two lanes are unspecified.

Two more rules follow from "packets are never merged". The word after an m_tlast word starts a new packet with its first beat in lane 0, whatever the previous packet's length was. And a packet of length exactly four, or eight, or any multiple, ends with a full word that has tkeep = FFFF and m_tlast high at the same time; that is one word, not a full word followed by an empty one.

This is where the bug in the main article's walkthrough lives. A converter that only emits on "four beats collected" never flushes the partial word, so the last one to three beats of every packet whose length is not a multiple of four vanish. The checker then reports a mismatch on the next packet's first word, because the reference model was still expecting the word that never came.

Back pressure

The output can stall: m_tready goes low while the converter is holding a word. Rule 1 says the converter must keep m_tvalid high and the word unchanged. The question is what happens to the input meanwhile.

Waveform where m_tready drops in cycles 4 and 5 while the converter holds word b3 b2 b1 b0; s_tready drops in the same cycles and beat b4 is held on the input until cycle 6, when the word drains and b4 is accepted on the same edge
The output word is held for two cycles. With a single output register there is nowhere to put b4, so s_tready drops. On the edge that ends cycle 6 the word drains and b4 is accepted at the same time.

With the simplest architecture, one output register, the answer is that the input stalls too: s_tready follows m_tready whenever the register is occupied. Beat b4 sits on the input for two cycles and is accepted on the very edge the word drains, because on that edge the register is being emptied and refilled at once. That is legal and it is what the reference model expects. What is not legal is accepting b4 while the word is still held, because the only place to put it is the register that rule 1 says must not change.

The other direction is simpler. If s_tvalid drops in the middle of a packet, the converter waits. It holds the beats it has, emits nothing, and continues when the next beat arrives.

An architecture

The specification does not prescribe a structure, and the agent in the main article is free to find its own. This is the one the reference solution uses, and it is close to the smallest thing that works.

Block diagram: s_tdata enters a lane select and is written into one of four 32 bit lanes of a 128 bit data register; valid, tkeep and tlast registers sit beside it and drive the output; a two bit fill counter selects the lane, a complete condition loads the output registers, and s_tready is high when the output register is empty or being drained
Datapath on top, control below. The four lanes of data_r are written one at a time under the fill counter; the output registers load when a word completes.
Register Width Role
data_r128The word being assembled and, once complete, the word being presented. Each lane has its own write enable.
fill2How many beats the pending word holds. Counts 0, 1, 2, 3 and returns to 0 on complete.
valid_r1m_tvalid. Set on complete, cleared when the word transfers.
keep_r16m_tkeep. Loaded on complete from fill.
last_r1m_tlast. Loaded on complete from s_tlast.

The control is three equations:

  • s_tready = rst_n and (not valid_r or m_tready). The input may deliver when the output register is empty, or when it is full but draining this cycle, and never during reset.
  • accept = s_tvalid and s_tready. On accept, s_tdata is written into lane fill of data_r.
  • complete = accept and (fill = 3 or s_tlast). On complete, valid_r is set, keep_r gets the low 4 (fill + 1) bits, last_r gets s_tlast, and fill returns to 0. On an accept that does not complete a word, fill increments. When nothing is accepted, fill holds.

One subtlety is worth tracing. On the edge where a full word drains and a new beat is accepted at the same time, valid_r is cleared by the transfer and, if that beat also completes a word (a single beat packet, say), set again by complete. Complete has to win, so the assignment for complete comes after the assignment for the transfer in the same process. Get the order wrong and single beat packets that arrive during a drain disappear.

A second subtlety is that data_r doubles as the assembly buffer and the output register. That works because the lanes not being written hold their value, and because s_tready guarantees no lane is written while the word is being held. Lane 0 of the next word is written on the same edge the previous word transfers, which is fine: the downstream block sampled the old value on that edge.

What this architecture gives up is a registered s_tready. It is a combinational function of m_tready, so a long tready path through a chain of such blocks adds up. The standard cure is a skid buffer on the 32 bit input, which lets s_tready come from a register for about 34 more flops, one beat of data, tlast and a valid bit. On the 100 MHz project clock in the repository the simple version has room to spare, so the exercise does not need it, but it is the first thing to reach for when it does not.

What the testbench checks

The testbench in the companion repository is written from the specification and does not know how the converter is built. Three parts matter.

The reference model watches the input handshake, packs the beats exactly as described above, and queues the word it expects next, with its tkeep and tlast. It also handles the zero latency case where a word completes on the same edge it is transferred, so a combinational converter is judged fairly.

The checker compares each transferred output word against the head of that queue. Data is compared only on the bytes tkeep marks as valid. It also enforces the handshake: m_tvalid dropping or the word changing while m_tready is low is a protocol error, and so is a tkeep pattern that is not one of the four legal ones. Beat payloads are {packet, beat} in the upper and lower 16 bits, so in a mismatch line a lane reading 0025_0002 is packet 37, beat 2, and the hex dump shows lane 3 on the left and lane 0 on the right.

Coverage is fourteen functional points, collected by the checker and reported at the end of every run: words of every fill from one to four, stalls while holding each of those, tlast on a full word, single beat packets, a stall of three or more cycles, back to back transfers, a gap inside a packet on the input, and an input beat held while s_tready is low. Out of the box the stimulus drives random packet lengths and random gaps on the input but keeps m_tready high, so the six points that need a stall are not reachable, and python run.py cov fails even on a correct design. Adding back pressure in tb/stimulus.sv is part of the exercise, and it is the gap the agent finds in step 5 of the main article.

Once back pressure is in, the report the agent reads ends like this:

WORDS: 633
OUTPUT STALL CYCLES: 561
INPUT STALL CYCLES: 365
MISMATCHES: 0
PROTOCOL ERRORS: 0
COVERAGE: 14/14
RESULT: PASS

The mistakes the checker catches

These are the failures the reference solution was mutated into, one at a time, to confirm the testbench sees them. Each one is also a plausible first attempt.

Mistake What the report shows
Only emit on four beats, ignore s_tlastMismatches on every packet whose length is not a multiple of four: the model expects a partial word that never arrives, so the next packet's first word is compared against it.
m_tkeep always FFFFMismatches on every partial word, data correct, keep wrong.
Clear m_tvalid every cycle instead of on transferProtocol errors: valid dropped while stalled. Words are lost, so mismatches follow.
s_tready high while the output is heldProtocol errors: the held word changes under the downstream block. Mismatches follow.
Beats packed into the wrong lanesMismatches on every word, with the payload showing which beat landed where.

If you are running the loop from the main article, this table is a decoder for the agent's third and fifth steps. If you are writing the converter yourself, it is the checklist.

References

2 sources
  1. Arm IHI 0051, AMBA 4 AXI4-Stream Protocol Specification: the signal definitions, the handshake rules, and the byte qualifiers.
  2. Agentic AI for FPGA Design: How the Loop Works: the article this page accompanies.

Our FPGA and AI training builds the loop from the main article on designs like this one, with the checks and limits in place.

Discuss your project with our engineering team

Comments

Loading comments...

Leave a Comment

By posting a comment, you agree that we may store your submitted details for moderation and site operation. See the privacy policy.

We use analytics and marketing tools to analyze traffic and improve our services. By clicking "Accept", you agree to our use of these tools.