Skip to content

rewrite the axi fabric to support pipelines. - #513

Open
nathanaelhuffman wants to merge 6 commits into
mainfrom
ndh/axi-pipes
Open

rewrite the axi fabric to support pipelines.#513
nathanaelhuffman wants to merge 6 commits into
mainfrom
ndh/axi-pipes

Conversation

@nathanaelhuffman

@nathanaelhuffman nathanaelhuffman commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

As #480 mentions, our somewhat naive axi fabric has some improvement opportunities.

This PR rewrites the bock to support user-specified pipeline stages between each of the axi responders, and adds more test coverage since the block is not as simple. Some additional logic improvements are integrated in the rewrite including a one-hot decode methodolgy to reduce logic and increase fmax.

This also fixes a couple of bugs in the axi8 responder that we weren't experiencing in hardware given the previous design but would experience now given the implementation removed some of the simplifications.

This is running successfully on cosmo hw.

Fixes #480

The read-address valid, read-data ready, and read-data resp/valid assignments
drove the wrong side of the interface view, so the responder never saw
ARVALID and the fabric never saw RVALID/RRESP. Only spi_nor_th used this
entity and spi_nor_tb never reads, which is why no read had ever actually
traversed it.
The fabric was a purely combinational crossbar behind a one cycle decode, and
the decode itself built two 32 bit magnitude compares per responder plus a
variable width address mask shared by every responder path. On cosmo_hp that
mask and its carry chains are the reported critical path.

Spans are powers of two and bases are aligned to their own span, so range
membership is just an equality compare on the address bits above the span. The
compare stays 32 bits wide against a resized address so a narrow initiator
cannot match a base it has no way to reach; the extra bits are constant zeros
and fold away. Each responder now masks the address to its own span with a
compile time constant, which also lets the fabric side of each responder's
address bus collapse to the bits it actually decodes.

The responder select becomes a registered one-hot instead of an integer index,
so the return path is a flat AND-OR tree rather than an integer to one-hot
decode buried inside combinational logic.

Three behavioural fixes fall out of the rework:

- The responder read address was driven from the initiator *write* address. This
  only worked because both initiators drive AWADDR and ARADDR from the same
  register.
- Teardown now takes priority over arming, and a write is only decoded once AW
  and W are both present, matching the condition every responder already applies
  before asserting AWREADY. Previously an initiator that left AWVALID asserted
  pinned the fabric to the completed transaction's responder, so the next read
  decoded against a stale address.
- Re-arming is blocked per channel until the initiator drops the request it just
  completed. Initiators here deassert VALID a cycle after the handshake, so
  without this a stale request re-armed the fabric and a duplicate transaction
  went out behind the initiator's back.

The error responder also only answers the channel that was actually decoded, so
an unmapped read can no longer hand an AWREADY to a write that has not presented
its data yet.

Add span_mask/bases_aligned/ranges_disjoint/bases_reachable to axil_common_pkg
along with elaboration asserts for the invariants the decode now relies on.
The interconnect had no testbench, and because axil8_resizer drove four
assignments in the wrong direction and spi_nor_tb never reads, no read had ever
traversed this fabric in simulation.

The harness instantiates the flat port axil_interconnect_2k8 with a mixed
responder map that includes an unmapped hole, and can be driven either by
vunit_lib.axi_lite_master or by hand so the testbench can reproduce initiator
handshake patterns the bus functional model never generates.

Two responder models, deliberately with different handshake shapes:

- axil_sram_responder wraps the production axil_target_txn, so it reproduces the
  contract every register block in the tree presents: a registered AWREADY pulse
  gated on AWVALID and WVALID, combinational ARREADY, and a single cycle BVALID
  pulse when BREADY is already asserted.
- axil_slow_responder accepts AW and W independently after LFSR driven stalls and
  holds its responses until READY, which is the shape nothing in the tree
  currently exercises.

The harness also counts handshakes on both sides of the fabric so a duplicated
transaction is caught even when the data happens to land correctly, and checks
that a stalled channel's payload stays put.

Nine of the ten cases fail against the fabric as it was before the previous
commit.
Adds the knob that the next commit implements. Every site is set to 0, so this is
functionally and structurally a no-op.

VHDL records have no field defaults and aggregates must be complete, so adding a
field breaks every config aggregate in the tree. Since they all had to be touched
anyway, they now go through a resp_cfg() constructor with defaulted arguments, so
the next field addition will not break them.
axil_pipe inserts config_array(i).pipe_stages register stages in each direction
between the fabric and one responder, so a responder that sits a long way from
the fabric no longer has to be reached and answered inside a single clock period.

Because the fabric admits one transaction at a time, this does not need five
independent AXI channel register slices. The whole transaction serializes into
one request bundle out and one response bundle back, which is roughly a third of
the flops a pair of full register slices costs and needs one token chain instead
of five sets of valid/ready control. Only the bits a responder actually decodes
are carried, so an 8 bit responder pipes 45 bits rather than 32 bit addresses.

Each payload stage advances only behind its own token, so every stage holds what
it was given until the next transaction pushes through it. That keeps the far end
stable across a multi-cycle handshake and removes the need for a separate capture
register at either end. Gating the chain on the OR of all tokens instead would
clobber the last stage on the cycle the token reached it.

Deliberately no timing exceptions are required, because cosmo_hp goes through
yosys and nextpnr where there is no way to express one.

The sink side asserts AWREADY and WREADY together as a registered one-shot, never
combinationally off AWVALID, and will not re-arm until the request that just
completed is off the bus. The source side samples all the responder handshakes in
one state, because axil_target_txn presents BVALID as a single cycle pulse when
BREADY is already asserted and ARREADY combinationally.

stages = 0 generates a plain pass-through, so it is free and the netlist is
unchanged where the knob is left alone.

The testbench now runs a mixed 0/1/3/2 stage map and adds a latency case that
proves the stages are really in the path: reads answer in 2, 6 and 8 clocks at 0,
1 and 2 stages respectively.

Note: multitool format could not be run here, vsg is not installed in this
environment.
eSPI is the largest register file and the most distant block in the design, and
it owned the worst clk_125m path. With one stage in each direction WNS goes from
-0.003ns (failing) to +0.120ns and the worst path moves out of eSPI entirely into
the DIMM SPD proxy.

The other responders stay unpiped. cosmo_hp and grapefruit need no stages at all
after the decode rework.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

We're starting to feel timing pressure on the axi interconnect as the design scales

1 participant