From 608e78a965d2d66b33338bcce3b37d8b66ec6190 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Tue, 4 Aug 2026 22:38:48 -0400 Subject: [PATCH 01/10] axi_blocks: fix reversed view assignments in axil8_resizer 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. --- hdl/ip/vhd/axi_blocks/axil8_resizer.vhd | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/hdl/ip/vhd/axi_blocks/axil8_resizer.vhd b/hdl/ip/vhd/axi_blocks/axil8_resizer.vhd index 6e51eb0d..04c07165 100644 --- a/hdl/ip/vhd/axi_blocks/axil8_resizer.vhd +++ b/hdl/ip/vhd/axi_blocks/axil8_resizer.vhd @@ -31,13 +31,15 @@ begin responder.write_data.strb <= fabric.write_data.strb; responder.write_response.ready <= fabric.write_response.ready; - fabric.read_address.valid <= responder.read_address.valid; - responder.read_address.addr <= fabric.read_address.addr(responder.read_address.addr'length - 1 downto 0); - fabric.read_data.ready <= responder.read_data.ready; fabric.write_response.resp <= responder.write_response.resp; fabric.write_response.valid <= responder.write_response.valid; + + responder.read_address.valid <= fabric.read_address.valid; + responder.read_address.addr <= fabric.read_address.addr(responder.read_address.addr'length - 1 downto 0); fabric.read_address.ready <= responder.read_address.ready; - responder.read_data.resp <= fabric.read_data.resp; - responder.read_data.valid <= fabric.read_data.valid; + + responder.read_data.ready <= fabric.read_data.ready; + fabric.read_data.resp <= responder.read_data.resp; + fabric.read_data.valid <= responder.read_data.valid; fabric.read_data.data <= responder.read_data.data; end rtl; \ No newline at end of file From 962746df541f008f5e8e8441e9769dbc4dc340b4 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Tue, 4 Aug 2026 22:38:48 -0400 Subject: [PATCH 02/10] axil_interconnect: rework address decode and transaction muxing 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. --- hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd | 94 ++++++ .../vhd/axi_blocks/axil_interconnect_2k8.vhd | 301 +++++++++++------- 2 files changed, 287 insertions(+), 108 deletions(-) diff --git a/hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd b/hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd index 76e59ed7..c389e72b 100644 --- a/hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd +++ b/hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd @@ -23,4 +23,98 @@ package axil_common_pkg is constant EXOKAY : std_logic_vector(1 downto 0) := "01"; constant SLVERR : std_logic_vector(1 downto 0) := "10"; + -- Ones in the low addr_span_bits positions, zeros above. Used both to mask + -- a fabric address down to what a responder actually decodes and to select + -- the bits that participate in the address compare. + function span_mask (constant addr_span_bits : integer) return std_logic_vector; + + -- Every responder base address must be aligned to its own span, otherwise + -- the upper-bits equality compare in the interconnect is not equivalent to + -- a base/limit compare. + function bases_aligned (constant cfg : axil_responder_cfg_array_t) return boolean; + + -- No two responder address ranges may overlap, otherwise more than one bit + -- of the interconnect's one-hot select can be set at once. + function ranges_disjoint (constant cfg : axil_responder_cfg_array_t) return boolean; + + -- Every responder must be reachable from an initiator of the given address + -- width, otherwise it silently falls through to the error responder. + function bases_reachable ( + constant cfg : axil_responder_cfg_array_t; + constant initiator_addr_width : integer + ) return boolean; + end package; + +package body axil_common_pkg is + + function span_mask (constant addr_span_bits : integer) return std_logic_vector is + variable mask : std_logic_vector(31 downto 0) := (others => '0'); + begin + -- Set every bit below addr_span_bits. Walking the bits one at a time + -- rather than slicing keeps addr_span_bits out of a slice bound, so this + -- stays legal for the 0 and 32 cases (which would be null slices) and + -- stays synthesizable under both Vivado and ghdl. + for i in mask'reverse_range loop + if i < addr_span_bits then + mask(i) := '1'; + end if; + end loop; + return mask; + end function; + + function bases_aligned (constant cfg : axil_responder_cfg_array_t) return boolean is + begin + -- Visit every responder and bail out on the first misaligned base. A base + -- is aligned when none of the bits inside its own span are set, which is + -- exactly the bits span_mask selects. + for i in cfg'range loop + if (cfg(i).base_addr and span_mask(cfg(i).addr_span_bits)) /= 32x"0" then + return false; + end if; + end loop; + return true; + end function; + + function ranges_disjoint (constant cfg : axil_responder_cfg_array_t) return boolean is + variable wider : integer; + begin + -- Visit each unordered pair of responders once (the j > i guard is what + -- skips the self comparison and the mirror of a pair already checked) and + -- bail out on the first overlap. + for i in cfg'range loop + for j in cfg'range loop + if j > i then + -- both ranges are power of two aligned, so they overlap if + -- and only if the bases agree above the wider of the spans + wider := cfg(i).addr_span_bits; + if cfg(j).addr_span_bits > wider then + wider := cfg(j).addr_span_bits; + end if; + if ((cfg(i).base_addr xor cfg(j).base_addr) and not span_mask(wider)) = 32x"0" then + return false; + end if; + end if; + end loop; + end loop; + return true; + end function; + + function bases_reachable ( + constant cfg : axil_responder_cfg_array_t; + constant initiator_addr_width : integer + ) return boolean is + begin + -- Visit every responder and bail out on the first base the initiator + -- cannot drive, which is any base with a bit set at or above the + -- initiator's address width. Reusing span_mask here treats the initiator + -- width as a span: everything outside it must be zero. + for i in cfg'range loop + if (cfg(i).base_addr and not span_mask(initiator_addr_width)) /= 32x"0" then + return false; + end if; + end loop; + return true; + end function; + +end package body; diff --git a/hdl/ip/vhd/axi_blocks/axil_interconnect_2k8.vhd b/hdl/ip/vhd/axi_blocks/axil_interconnect_2k8.vhd index e155bd6a..7a33f4c6 100644 --- a/hdl/ip/vhd/axi_blocks/axil_interconnect_2k8.vhd +++ b/hdl/ip/vhd/axi_blocks/axil_interconnect_2k8.vhd @@ -15,6 +15,11 @@ use work.axilite_if_2k8_pkg.all; -- It is intended to be function as an MVP implementation allowing for basic multi-responder -- usecases. It is not currently a full cross-bar implementation, but may grow to be one in the future. -- This is the VHDL 2k8 version which does not use interface views. +-- +-- Only one transaction, read or write, is in flight at a time: a registered +-- decode stage selects a responder, the transaction runs to completion, and then +-- the fabric is torn down and re-armed. Everything downstream of here relies on +-- that, so it is worth stating plainly. entity axil_interconnect_2k8 is generic ( @@ -35,7 +40,7 @@ entity axil_interconnect_2k8 is initiator_write_data_strb : in std_logic_vector(3 downto 0); initiator_write_data_ready : out std_logic; initiator_write_data_valid : in std_logic; - + initiator_write_response_valid : out std_logic; initiator_write_response_resp : out std_logic_vector(1 downto 0); initiator_write_response_ready : in std_logic; @@ -54,12 +59,12 @@ entity axil_interconnect_2k8 is responders_write_address_valid : out std_logic_vector(config_array'range); responders_write_address_ready : in std_logic_vector(config_array'range); responders_write_address_addr : out tgt_addr32_t(config_array'range); - + responders_write_data_valid : out std_logic_vector(config_array'range); responders_write_data_ready : in std_logic_vector(config_array'range); responders_write_data_data: out tgt_dat32_t(config_array'range); responders_write_data_strb: out tgt_strb_t(config_array'range); - + responders_write_response_ready : out std_logic_vector(config_array'range); responders_write_response_resp : in tgt_resp_t(config_array'range); responders_write_response_valid : in std_logic_vector(config_array'range); @@ -78,134 +83,214 @@ end entity; architecture rtl of axil_interconnect_2k8 is - constant default_idx : integer := config_array'length; - -- We implement a catch-all responder that will respond with an error if no other responder does - -- so this signal is one larger than the number of responders - signal responder_sel : integer range 0 to config_array'length := default_idx; - signal responder_addr_width : integer range 0 to 32 := 8; + constant ZERO32 : std_logic_vector(31 downto 0) := (others => '0'); + + signal wr_addr32 : std_logic_vector(31 downto 0); + signal rd_addr32 : std_logic_vector(31 downto 0); + signal wr_hit : std_logic_vector(config_array'range); + signal rd_hit : std_logic_vector(config_array'range); + + -- Registered select. One bit per responder, at most one set, plus a separate + -- bit for the catch-all error responder. Keeping this one-hot rather than an + -- integer index means the return path is a flat AND-OR tree instead of an + -- integer-to-one-hot decode buried in combinational logic. + signal sel_onehot : std_logic_vector(config_array'range); + signal sel_default : std_logic; + signal sel_is_write : std_logic; + signal in_txn : boolean; + + -- A write request is only a request once both AW and W are on the bus, which + -- is the condition every responder applies before asserting AWREADY (see + -- axil_target_txn). Decoding on AWVALID alone is what let the FMC target's + -- spurious AWVALID re-assert issue a second write. + signal wr_req : std_logic; + signal rd_req : std_logic; + + -- Set when a transaction completes with its request still asserted, and held + -- until the initiator drops it. Initiators here deassert VALID a cycle after + -- the handshake, so without this a stale request re-arms the fabric and a + -- duplicate transaction goes out behind the initiator's back. + signal wr_hold : std_logic; + signal rd_hold : std_logic; + signal write_done : std_logic; signal read_done : std_logic; - signal in_txn : boolean; begin + assert config_array'low = 0 + report "config_array must be indexed from 0" + severity failure; + assert bases_aligned(config_array) + report "every responder base address must be aligned to its own addr_span_bits" + severity failure; + assert ranges_disjoint(config_array) + report "responder address ranges must not overlap" + severity failure; + assert bases_reachable(config_array, initiator_addr_width) + report "a responder base address is outside the initiator's address space" + severity failure; + + wr_addr32 <= resize(initiator_write_address_addr, 32); + rd_addr32 <= resize(initiator_read_address_addr, 32); + + wr_req <= initiator_write_address_valid and initiator_write_data_valid; + rd_req <= initiator_read_address_valid; + write_done <= '1' when initiator_write_response_valid = '1' and initiator_write_response_ready = '1' else '0'; read_done <= '1' when initiator_read_data_valid = '1' and initiator_read_data_ready = '1' else '0'; - -- we're going to stall all the transactions until we have decoded and selected a responder, - -- flipped the muxes and then we can let the txn_through, and we keep the responder selected until - -- the txn is done. - -- There's a lot of combo logic here, we'll see how this goes. + -- Address decode. Spans are powers of two and bases are aligned to their own + -- span (asserted above), so "inside the range" is just "the bits above the + -- span match the base". The masked-off low bits fold away in synthesis, + -- leaving an equality compare on the upper bits rather than the pair of + -- magnitude compares, and their carry chains, this used to build. Comparing + -- over the full 32 bits matters: a narrow initiator must not match a base + -- that it cannot actually address, and the extra bits are constant zeros. + hit_gen: for i in config_array'range generate + constant span : natural := config_array(i).addr_span_bits; + begin + + wr_hit(i) <= '1' when ((wr_addr32 xor config_array(i).base_addr) and not span_mask(span)) = ZERO32 else + '0'; + rd_hit(i) <= '1' when ((rd_addr32 xor config_array(i).base_addr) and not span_mask(span)) = ZERO32 else + '0'; + + end generate; + + -- Stall the transaction for one cycle while the responder is selected, then + -- hold that selection until the transaction completes. decode: process(clk, reset) begin if reset = '1' then - responder_sel <= default_idx; - responder_addr_width <= 8; + sel_onehot <= (others => '0'); + sel_default <= '0'; + sel_is_write <= '0'; in_txn <= false; + wr_hold <= '0'; + rd_hold <= '0'; elsif rising_edge(clk) then - if initiator_write_address_valid = '1' then - for i in 0 to config_array'length - 1 loop - if (initiator_write_address_addr >= config_array(i).base_addr) and - (initiator_write_address_addr < config_array(i).base_addr + 2**config_array(i).addr_span_bits) then - responder_sel <= i; - responder_addr_width <= config_array(i).addr_span_bits; - end if; - end loop; - in_txn <= true; - elsif initiator_read_address_valid = '1' then - for i in 0 to config_array'length - 1 loop - if (initiator_read_address_addr >= config_array(i).base_addr) and - (initiator_read_address_addr < config_array(i).base_addr + 2**config_array(i).addr_span_bits) then - responder_sel <= i; - responder_addr_width <= config_array(i).addr_span_bits; - end if; - end loop; - in_txn <= true; - elsif write_done or read_done then - responder_sel <= default_idx; + -- Per channel re-arm guard, tracked independently so a permanently + -- asserted AWVALID cannot block reads. + if write_done = '1' then + wr_hold <= wr_req; + elsif wr_hold = '1' and wr_req = '0' then + wr_hold <= '0'; + end if; + + if read_done = '1' then + rd_hold <= rd_req; + elsif rd_hold = '1' and rd_req = '0' then + rd_hold <= '0'; + end if; + + -- Teardown takes priority over arming. An initiator that leaves + -- AWVALID asserted past the end of its write must not be able to + -- pin the fabric to the previous selection. + if write_done = '1' or read_done = '1' then + sel_onehot <= (others => '0'); + sel_default <= '0'; + sel_is_write <= '0'; in_txn <= false; - responder_addr_width <= 8; + elsif not in_txn then + if wr_req = '1' and wr_hold = '0' then + sel_onehot <= wr_hit; + sel_default <= not (or wr_hit); + sel_is_write <= '1'; + in_txn <= true; + elsif rd_req = '1' and rd_hold = '0' then + sel_onehot <= rd_hit; + sel_default <= not (or rd_hit); + sel_is_write <= '0'; + in_txn <= true; + end if; end if; end if; end process; - mux: process(all) - variable masked_addr : std_logic_vector(31 downto 0); + -- Return path: a one-hot AND-OR mux of the selected responder, or the + -- catch-all error responder when nothing matched. + ret_mux: process(all) + variable awready : std_logic; + variable wready : std_logic; + variable bvalid : std_logic; + variable bresp : std_logic_vector(1 downto 0); + variable arready : std_logic; + variable rvalid : std_logic; + variable rresp : std_logic_vector(1 downto 0); + variable rdata : std_logic_vector(31 downto 0); begin - -- default no transaction state for all responders - responders_write_address_addr <= (others => (others => '0')); - responders_read_address_addr <= (others => (others => '0')); - masked_addr := resize(initiator_write_address_addr, 32); - for i in 31 downto 0 loop - if i >= responder_addr_width or i >= initiator_write_address_addr'length then - masked_addr(i) := '0'; - else - masked_addr(i) := initiator_write_address_addr(i); - end if; - end loop; - for i in 0 to config_array'length - 1 loop - responders_write_address_valid(i) <= '0'; - responders_write_data_valid(i) <= '0'; - responders_write_data_data(i) <= initiator_write_data_data; - responders_write_data_strb(i) <= initiator_write_data_strb; - responders_write_address_addr(i) <= masked_addr; - responders_write_response_ready(i) <= '0'; - - responders_read_address_valid(i) <= '0'; - - responders_read_address_addr(i)<= masked_addr; - responders_read_data_ready(i) <= '0'; + awready := '0'; + wready := '0'; + bvalid := '0'; + bresp := SLVERR; + arready := '0'; + rvalid := '0'; + rresp := SLVERR; + rdata := (others => '0'); + for i in config_array'range loop + if sel_onehot(i) = '1' then + awready := responders_write_address_ready(i); + wready := responders_write_data_ready(i); + bvalid := responders_write_response_valid(i); + bresp := responders_write_response_resp(i); + arready := responders_read_address_ready(i); + rvalid := responders_read_data_valid(i); + rresp := responders_read_data_resp(i); + rdata := responders_read_data_data(i); + end if; end loop; - -- deal with in-txn muxing - if in_txn and responder_sel < default_idx then - - -- responder mux - -- we already assigned addresses to the responder addresss above, no need to overwrite here - responders_write_address_valid(responder_sel) <= initiator_write_address_valid; - responders_write_data_valid(responder_sel) <= initiator_write_data_valid; - responders_write_data_data(responder_sel) <= initiator_write_data_data; - responders_write_data_strb(responder_sel) <= initiator_write_data_strb; - responders_write_response_ready(responder_sel) <= initiator_write_response_ready; - - responders_read_address_valid(responder_sel) <= initiator_read_address_valid; - responders_read_data_ready(responder_sel) <= initiator_read_data_ready; - -- initiator mux - initiator_write_address_ready <= responders_write_address_ready(responder_sel); - initiator_write_data_ready <= responders_write_data_ready(responder_sel); - initiator_write_response_resp <= responders_write_response_resp(responder_sel); - initiator_write_response_valid <= responders_write_response_valid(responder_sel); - initiator_read_address_ready <= responders_read_address_ready(responder_sel); - initiator_read_data_resp <= responders_read_data_resp(responder_sel); - initiator_read_data_valid <= responders_read_data_valid(responder_sel); - initiator_read_data_data <= responders_read_data_data(responder_sel); - - elsif in_txn then - -- default response to not hang bus - initiator_write_address_ready <= '1'; - initiator_write_data_ready <= '1'; - initiator_write_response_resp <= SLVERR; - initiator_write_response_valid <= '1'; - initiator_read_address_ready <= '1'; - initiator_read_data_resp <= SLVERR; - initiator_read_data_valid <= '1'; - initiator_read_data_data <= X"DEADBEEF"; - - else - -- hold for decode - -- default response to not hang bus - initiator_write_address_ready <= '0'; - initiator_write_data_ready <= '0'; - initiator_write_response_resp <= SLVERR; - initiator_write_response_valid <= '0'; - initiator_read_address_ready <= '0'; - initiator_read_data_resp <= SLVERR; - initiator_read_data_valid <= '0'; - initiator_read_data_data <= (others => '0'); + if sel_default = '1' then + -- Nothing decoded, so answer immediately with an error rather than + -- hanging the bus. Only the channel that was actually decoded is + -- answered, so an unmapped read cannot hand an AWREADY to a write + -- that has not presented its data yet. + if sel_is_write = '1' then + awready := '1'; + wready := '1'; + bvalid := '1'; + bresp := SLVERR; + else + arready := '1'; + rvalid := '1'; + rresp := SLVERR; + rdata := X"DEADBEEF"; + end if; end if; + + initiator_write_address_ready <= awready; + initiator_write_data_ready <= wready; + initiator_write_response_valid <= bvalid; + initiator_write_response_resp <= bresp; + initiator_read_address_ready <= arready; + initiator_read_data_valid <= rvalid; + initiator_read_data_resp <= rresp; + initiator_read_data_data <= rdata; end process; - -end rtl; \ No newline at end of file + + -- Forward path. The payload is broadcast to every responder, masked to that + -- responder's own span, and only the selected responder sees a valid. The + -- masked-off address bits are hard constant zeros, so the fabric side of + -- each responder's address bus collapses to span real wires. + resp_gen: for i in config_array'range generate + constant span : natural := config_array(i).addr_span_bits; + begin + + responders_write_address_addr(i) <= wr_addr32 and span_mask(span); + responders_read_address_addr(i) <= rd_addr32 and span_mask(span); + responders_write_data_data(i) <= initiator_write_data_data; + responders_write_data_strb(i) <= initiator_write_data_strb; + + responders_write_address_valid(i) <= initiator_write_address_valid and sel_onehot(i); + responders_write_data_valid(i) <= initiator_write_data_valid and sel_onehot(i); + responders_write_response_ready(i) <= initiator_write_response_ready and sel_onehot(i); + responders_read_address_valid(i) <= initiator_read_address_valid and sel_onehot(i); + responders_read_data_ready(i) <= initiator_read_data_ready and sel_onehot(i); + + end generate; + +end rtl; From 8830a095da60452abbebea0514434dfe856f7bee Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Tue, 4 Aug 2026 22:57:27 -0400 Subject: [PATCH 03/10] axil_interconnect: add a VUnit testbench 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. --- hdl/ip/vhd/axi_blocks/BUCK | 12 +- .../sims/axil_interconnect_sim_pkg.vhd | 70 ++++ .../axi_blocks/sims/axil_interconnect_tb.vhd | 342 +++++++++++++++++ .../axi_blocks/sims/axil_interconnect_th.vhd | 361 ++++++++++++++++++ .../axi_blocks/sims/axil_slow_responder.vhd | 203 ++++++++++ .../axi_blocks/sims/axil_sram_responder.vhd | 113 ++++++ 6 files changed, 1100 insertions(+), 1 deletion(-) create mode 100644 hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd create mode 100644 hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd create mode 100644 hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd create mode 100644 hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd create mode 100644 hdl/ip/vhd/axi_blocks/sims/axil_sram_responder.vhd diff --git a/hdl/ip/vhd/axi_blocks/BUCK b/hdl/ip/vhd/axi_blocks/BUCK index 66ede9a4..5c28c1a9 100644 --- a/hdl/ip/vhd/axi_blocks/BUCK +++ b/hdl/ip/vhd/axi_blocks/BUCK @@ -1,4 +1,4 @@ -load("//tools:hdl.bzl", "vhdl_unit") +load("//tools:hdl.bzl", "vhdl_unit", "vunit_sim") vhdl_unit( name = "axilite_common_pkg", @@ -96,6 +96,16 @@ vhdl_unit( visibility = ['PUBLIC'], ) +vunit_sim( + name = "axil_interconnect_tb", + srcs = glob(["sims/*.vhd"]), + deps = [ + ":axil_interconnect_2k8", + ], + standard = "2008", + visibility = ['PUBLIC'], +) + vhdl_unit( name = "axist_if_2k19_pkg", srcs = glob(["axist*2k19_pkg.vhd"]), diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd new file mode 100644 index 00000000..6b760dcf --- /dev/null +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd @@ -0,0 +1,70 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + context vunit_lib.vc_context; + +use work.axil_common_pkg.all; + +package axil_interconnect_sim_pkg is + + -- The fabric under test is instantiated with a 26 bit initiator, matching the + -- FMC target in the real designs. + constant INITIATOR_ADDR_WIDTH : integer := 26; + + constant bus_handle : bus_master_t := new_bus( + data_length => 32, + address_length => INITIATOR_ADDR_WIDTH + ); + + -- Responder map for the harness. The gap between 0x300 and 0x7FFF, plus + -- everything at or above 0x10000, is deliberately unmapped so the catch-all + -- SLVERR path gets exercised. + constant SRAM_A_IDX : integer := 0; + constant SRAM_B_IDX : integer := 1; + constant SLOW_IDX : integer := 2; + constant WIDE_IDX : integer := 3; + + constant config_array : axil_responder_cfg_array_t(0 to 3) := + (SRAM_A_IDX => (base_addr => x"00000000", addr_span_bits => 8), + SRAM_B_IDX => (base_addr => x"00000100", addr_span_bits => 8), + SLOW_IDX => (base_addr => x"00000200", addr_span_bits => 8), + WIDE_IDX => (base_addr => x"00008000", addr_span_bits => 15)); + + --! An integer as a bus address + function ba (constant addr : integer) return std_logic_vector; + + --! Base address of a responder, plus a byte offset, as a bus address + function ba (constant idx : integer; constant offset : integer) return std_logic_vector; + + --! An integer as a 32 bit data word + function w32 (constant value : integer) return std_logic_vector; + +end package; + +package body axil_interconnect_sim_pkg is + + function ba (constant addr : integer) return std_logic_vector is + begin + return std_logic_vector(to_unsigned(addr, INITIATOR_ADDR_WIDTH)); + end function; + + function ba (constant idx : integer; constant offset : integer) return std_logic_vector is + begin + return std_logic_vector(unsigned(config_array(idx).base_addr(INITIATOR_ADDR_WIDTH - 1 downto 0)) + + to_unsigned(offset, INITIATOR_ADDR_WIDTH)); + end function; + + function w32 (constant value : integer) return std_logic_vector is + begin + return std_logic_vector(to_unsigned(value, 32)); + end function; + +end package body; diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd new file mode 100644 index 00000000..a2cb046e --- /dev/null +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd @@ -0,0 +1,342 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.com_context; + context vunit_lib.vunit_context; + context vunit_lib.vc_context; +use vunit_lib.axi_lite_master_pkg.all; + +use work.axil_common_pkg.all; +use work.axil_interconnect_sim_pkg.all; + +entity axil_interconnect_tb is + generic ( + runner_cfg : string + ); +end entity; + +architecture tb of axil_interconnect_tb is + +begin + + th: entity work.axil_interconnect_th; + + -- Note: external names are broken in the GHDL llvm backend + -- (https://github.com/ghdl/ghdl/issues/2610) so this sim is nvc only. + bench: process + alias clk is <>; + alias reset is <>; + alias reset_force is <>; + + alias man_mode is <>; + alias man_awvalid is <>; + alias man_awaddr is + <>; + alias man_wvalid is <>; + alias man_wdata is <>; + alias man_wstrb is <>; + alias man_bready is <>; + alias man_arvalid is <>; + alias man_araddr is + <>; + alias man_rready is <>; + + alias init_awready is <>; + alias init_bvalid is <>; + alias init_bresp is <>; + alias init_rvalid is <>; + alias init_rdata is <>; + alias init_rresp is <>; + + alias init_aw_hs is <>; + alias init_b_hs is <>; + alias init_ar_hs is <>; + alias init_r_hs is <>; + alias resp_aw_hs is <>; + alias resp_ar_hs is <>; + + -- generous enough that a working fabric never hits it, short enough that + -- a wedged one fails instead of running the watchdog out + constant TXN_TIMEOUT : time := 5 us; + + variable rdata : std_logic_vector(31 downto 0); + + -- Every accepted write must produce exactly one B, every accepted read + -- exactly one R, and (for mapped addresses) exactly one responder side + -- address handshake. A duplicated write shows up as resp_aw_hs running + -- ahead of init_aw_hs even when the data lands correctly. + procedure check_handshake_accounting ( + signal net : inout network_t; + constant mapped_only : boolean + ) is + begin + -- writes are only queued by write_axi_lite, so let the bus + -- functional model drain before counting anything + wait_until_idle(net, bus_handle); + check_equal(init_aw_hs, init_b_hs, "write address and write response handshakes disagree"); + check_equal(init_ar_hs, init_r_hs, "read address and read data handshakes disagree"); + if mapped_only then + check_equal(resp_aw_hs, init_aw_hs, "responder saw a different number of writes than the initiator issued"); + check_equal(resp_ar_hs, init_ar_hs, "responder saw a different number of reads than the initiator issued"); + end if; + end procedure; + + procedure clear_manual is + begin + man_awvalid <= '0'; + man_wvalid <= '0'; + man_bready <= '0'; + man_arvalid <= '0'; + man_rready <= '0'; + wait until rising_edge(clk); + man_mode <= '0'; + wait until rising_edge(clk); + end procedure; + + -- Drive one write straight at the fabric, emulating the FMC target: + -- AWVALID goes up first and WVALID only follows once the write data FIFO + -- has something in it. + procedure manual_write ( + constant addr : in std_logic_vector; + constant data : in std_logic_vector(31 downto 0); + constant aw_lead : in integer; + constant expect_resp : in std_logic_vector(1 downto 0) + ) is + begin + man_mode <= '1'; + man_awaddr <= addr; + man_wdata <= data; + man_wstrb <= "1111"; + man_bready <= '1'; + man_awvalid <= '1'; + man_wvalid <= '0'; + + -- While the initiator has only presented AW, the fabric must not + -- accept the write. If it does, the FMC clears AWVALID early and + -- then re-raises it, which is what wedges the bus. + for i in 1 to aw_lead loop + wait until rising_edge(clk); + check_equal(init_awready, '0', "fabric asserted AWREADY before WVALID was presented"); + end loop; + + man_wvalid <= '1'; + wait until rising_edge(clk) and init_bvalid = '1' for TXN_TIMEOUT; + check_equal(init_bvalid, '1', "timed out waiting for the write response"); + check_equal(init_bresp, expect_resp, "unexpected write response"); + wait until rising_edge(clk); + man_awvalid <= '0'; + man_wvalid <= '0'; + wait until rising_edge(clk); + end procedure; + + procedure manual_read ( + constant addr : in std_logic_vector; + variable data : out std_logic_vector(31 downto 0); + constant expect_resp : in std_logic_vector(1 downto 0) + ) is + begin + man_mode <= '1'; + man_araddr <= addr; + man_rready <= '1'; + man_arvalid <= '1'; + wait until rising_edge(clk) and init_rvalid = '1' for TXN_TIMEOUT; + check_equal(init_rvalid, '1', "timed out waiting for read data"); + data := init_rdata; + check_equal(init_rresp, expect_resp, "unexpected read response"); + wait until rising_edge(clk); + man_arvalid <= '0'; + man_rready <= '0'; + wait until rising_edge(clk); + end procedure; + + begin + test_runner_setup(runner, runner_cfg); + wait until reset = '0'; + wait for 500 ns; + + while test_suite loop + if run("write_read_each_responder") then + for idx in config_array'range loop + write_axi_lite(net, bus_handle, ba(idx, 16#00#), x"C0DE0000" or w32(idx * 16)); + write_axi_lite(net, bus_handle, ba(idx, 16#08#), x"FEED0000" or w32(idx * 16)); + end loop; + -- read back after all the writes, so a fabric that leaks a write + -- into the wrong responder is caught rather than masked + for idx in config_array'range loop + check_axi_lite(net, bus_handle, ba(idx, 16#00#), axi_resp_okay, + x"C0DE0000" or w32(idx * 16), "responder 0x00 readback"); + check_axi_lite(net, bus_handle, ba(idx, 16#08#), axi_resp_okay, + x"FEED0000" or w32(idx * 16), "responder 0x08 readback"); + end loop; + check_handshake_accounting(net, mapped_only => true); + + elsif run("back_to_back_same_responder") then + for word in 0 to 7 loop + write_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 4 * word), + x"A5A50000" or w32(word)); + end loop; + for word in 0 to 7 loop + check_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 4 * word), axi_resp_okay, + x"A5A50000" or w32(word), "back to back readback"); + end loop; + check_handshake_accounting(net, mapped_only => true); + + elsif run("back_to_back_alternating") then + -- alternate between the fastest and the slowest responder with no + -- idle time in between + for word in 0 to 7 loop + write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 4 * word), x"11110000" or w32(word)); + write_axi_lite(net, bus_handle, ba(SLOW_IDX, 4 * word), x"22220000" or w32(word)); + end loop; + for word in 0 to 7 loop + check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 4 * word), axi_resp_okay, + x"11110000" or w32(word), "sram_a readback"); + check_axi_lite(net, bus_handle, ba(SLOW_IDX, 4 * word), axi_resp_okay, + x"22220000" or w32(word), "slow readback"); + end loop; + check_handshake_accounting(net, mapped_only => true); + + elsif run("read_after_write_same_addr") then + for idx in config_array'range loop + write_axi_lite(net, bus_handle, ba(idx, 16#10#), x"5A5A1234"); + check_axi_lite(net, bus_handle, ba(idx, 16#10#), axi_resp_okay, x"5A5A1234", + "read immediately after write"); + write_axi_lite(net, bus_handle, ba(idx, 16#10#), x"A5A54321"); + check_axi_lite(net, bus_handle, ba(idx, 16#10#), axi_resp_okay, x"A5A54321", + "read immediately after overwrite"); + end loop; + check_handshake_accounting(net, mapped_only => true); + + elsif run("unmapped_slverr") then + -- the gap between the 8 bit responders and the wide one, the top + -- of that gap, and an address past every responder + write_axi_lite(net, bus_handle, ba(16#000300#), x"DEADDEAD", axi_resp_slverr); + check_axi_lite(net, bus_handle, ba(16#000300#), axi_resp_slverr, x"DEADBEEF", + "unmapped read at 0x300"); + write_axi_lite(net, bus_handle, ba(16#007FFC#), x"DEADDEAD", axi_resp_slverr); + check_axi_lite(net, bus_handle, ba(16#007FFC#), axi_resp_slverr, x"DEADBEEF", + "unmapped read at 0x7FFC"); + write_axi_lite(net, bus_handle, ba(16#010000#), x"DEADDEAD", axi_resp_slverr); + check_axi_lite(net, bus_handle, ba(16#010000#), axi_resp_slverr, x"DEADBEEF", + "unmapped read at 0x10000"); + -- an unmapped access must not have disturbed a mapped one + write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#04#), x"600D600D"); + check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#04#), axi_resp_okay, x"600D600D", + "mapped access after unmapped"); + check_handshake_accounting(net, mapped_only => false); + + elsif run("boundary_addresses") then + -- first and last word of each mapped region, which is where an + -- equality based decode and a magnitude compare could disagree + write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), x"00000001"); + write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#FC#), x"000000FC"); + write_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#00#), x"00000100"); + write_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#FC#), x"000001FC"); + write_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#0000#), x"00008000"); + write_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#0FFC#), x"00008FFC"); + + -- 0x00 and 0xFC land in different words of sram_a, and sram_b's + -- 0x100 must not have aliased on top of sram_a's 0x00 + check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), axi_resp_okay, x"00000001", + "sram_a low boundary"); + check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#FC#), axi_resp_okay, x"000000FC", + "sram_a high boundary"); + check_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#00#), axi_resp_okay, x"00000100", + "sram_b low boundary"); + check_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#FC#), axi_resp_okay, x"000001FC", + "sram_b high boundary"); + check_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#0000#), axi_resp_okay, x"00008000", + "wide low boundary"); + check_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#0FFC#), axi_resp_okay, x"00008FFC", + "wide high boundary"); + check_handshake_accounting(net, mapped_only => true); + + elsif run("glitchy_aw_initiator") then + manual_write(ba(SRAM_A_IDX, 16#10#), x"6060BEEF", 6, OKAY); + clear_manual; + check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#10#), axi_resp_okay, x"6060BEEF", + "readback after an AW leading write"); + check_handshake_accounting(net, mapped_only => true); + + elsif run("glitchy_aw_unmapped") then + -- the catch-all responder is the one that used to assert AWREADY + -- with no regard for WVALID + manual_write(ba(16#000300#), x"6060BEEF", 6, SLVERR); + clear_manual; + check_axi_lite(net, bus_handle, ba(16#000300#), axi_resp_slverr, x"DEADBEEF", + "unmapped read after an AW leading write"); + check_handshake_accounting(net, mapped_only => false); + + elsif run("stuck_awvalid") then + -- seed a known value in sram_a that the stuck write must not + -- be able to shadow. write_axi_lite only queues the write, so + -- wait for the bus functional model to actually retire it + -- before taking the bus over by hand. + write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#04#), x"600DF00D"); + wait_until_idle(net, bus_handle); + + man_mode <= '1'; + man_awaddr <= ba(SRAM_B_IDX, 16#00#); + man_wdata <= x"11112222"; + man_wstrb <= "1111"; + man_bready <= '1'; + man_awvalid <= '1'; + man_wvalid <= '1'; + wait until rising_edge(clk) and init_bvalid = '1' for TXN_TIMEOUT; + check_equal(init_bvalid, '1', "timed out waiting for the write response"); + wait until rising_edge(clk); + + -- the FMC only clears AWVALID on an AWREADY it happened to + -- observe, so it can still be asserted here. The fabric has to + -- tear the transaction down anyway and decode the next one. + man_wvalid <= '0'; + man_bready <= '0'; + manual_read(ba(SRAM_A_IDX, 16#04#), rdata, OKAY); + check_equal(rdata, std_logic_vector'(x"600DF00D"), + "read decoded against a stale write address"); + clear_manual; + check_handshake_accounting(net, mapped_only => true); + + elsif run("reset_mid_transaction") then + -- kick off a read at the slow responder and reset while it is in + -- flight, then confirm the fabric comes back clean + man_mode <= '1'; + man_araddr <= ba(SLOW_IDX, 16#00#); + man_rready <= '1'; + man_arvalid <= '1'; + wait for 40 ns; + reset_force <= '1'; + wait for 40 ns; + man_arvalid <= '0'; + man_rready <= '0'; + wait for 40 ns; + reset_force <= '0'; + wait until reset = '0'; + wait for 200 ns; + man_mode <= '0'; + wait for 200 ns; + + write_axi_lite(net, bus_handle, ba(SLOW_IDX, 16#00#), x"AF7E8E5E"); + check_axi_lite(net, bus_handle, ba(SLOW_IDX, 16#00#), axi_resp_okay, x"AF7E8E5E", + "slow responder after a mid transaction reset"); + write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), x"C1EA4000"); + check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), axi_resp_okay, x"C1EA4000", + "sram_a after a mid transaction reset"); + check_handshake_accounting(net, mapped_only => true); + end if; + end loop; + + wait for 2 us; + test_runner_cleanup(runner); + wait; + end process; + + test_runner_watchdog(runner, 10 ms); + +end tb; diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd new file mode 100644 index 00000000..b3af2941 --- /dev/null +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd @@ -0,0 +1,361 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +library vunit_lib; + context vunit_lib.vunit_context; + context vunit_lib.com_context; + context vunit_lib.vc_context; + +use work.axil_common_pkg.all; +use work.axilite_if_2k8_pkg.all; +use work.axil_interconnect_sim_pkg.all; + +entity axil_interconnect_th is +end entity; + +architecture th of axil_interconnect_th is + + signal clk : std_logic := '0'; + signal reset : std_logic; + signal reset_por : std_logic := '1'; + --! testbench driven, so a reset can be injected mid transaction + signal reset_force : std_logic := '0'; + + -- The bus functional model drives these + signal bfm_awvalid : std_logic; + signal bfm_awaddr : std_logic_vector(INITIATOR_ADDR_WIDTH - 1 downto 0); + signal bfm_wvalid : std_logic; + signal bfm_wdata : std_logic_vector(31 downto 0); + signal bfm_wstrb : std_logic_vector(3 downto 0); + signal bfm_bready : std_logic; + signal bfm_arvalid : std_logic; + signal bfm_araddr : std_logic_vector(INITIATOR_ADDR_WIDTH - 1 downto 0); + signal bfm_rready : std_logic; + + -- The testbench drives these directly when man_mode is set, so it can + -- reproduce initiator handshake patterns the BFM never generates (notably + -- the FMC target's habit of raising AWVALID well before WVALID, and + -- re-raising it after an AWREADY pulse). + signal man_mode : std_logic := '0'; + signal man_awvalid : std_logic := '0'; + signal man_awaddr : std_logic_vector(INITIATOR_ADDR_WIDTH - 1 downto 0) := (others => '0'); + signal man_wvalid : std_logic := '0'; + signal man_wdata : std_logic_vector(31 downto 0) := (others => '0'); + signal man_wstrb : std_logic_vector(3 downto 0) := "1111"; + signal man_bready : std_logic := '0'; + signal man_arvalid : std_logic := '0'; + signal man_araddr : std_logic_vector(INITIATOR_ADDR_WIDTH - 1 downto 0) := (others => '0'); + signal man_rready : std_logic := '0'; + + -- initiator side of the fabric + signal init_awvalid : std_logic; + signal init_awready : std_logic; + signal init_awaddr : std_logic_vector(INITIATOR_ADDR_WIDTH - 1 downto 0); + signal init_wvalid : std_logic; + signal init_wready : std_logic; + signal init_wdata : std_logic_vector(31 downto 0); + signal init_wstrb : std_logic_vector(3 downto 0); + signal init_bvalid : std_logic; + signal init_bready : std_logic; + signal init_bresp : std_logic_vector(1 downto 0); + signal init_arvalid : std_logic; + signal init_arready : std_logic; + signal init_araddr : std_logic_vector(INITIATOR_ADDR_WIDTH - 1 downto 0); + signal init_rvalid : std_logic; + signal init_rready : std_logic; + signal init_rdata : std_logic_vector(31 downto 0); + signal init_rresp : std_logic_vector(1 downto 0); + + -- responder side of the fabric + signal responders_write_address_valid : std_logic_vector(config_array'range); + signal responders_write_address_ready : std_logic_vector(config_array'range); + signal responders_write_address_addr : tgt_addr32_t(config_array'range); + signal responders_write_data_valid : std_logic_vector(config_array'range); + signal responders_write_data_ready : std_logic_vector(config_array'range); + signal responders_write_data_data : tgt_dat32_t(config_array'range); + signal responders_write_data_strb : tgt_strb_t(config_array'range); + signal responders_write_response_ready : std_logic_vector(config_array'range); + signal responders_write_response_resp : tgt_resp_t(config_array'range); + signal responders_write_response_valid : std_logic_vector(config_array'range); + signal responders_read_address_valid : std_logic_vector(config_array'range); + signal responders_read_address_addr : tgt_addr32_t(config_array'range); + signal responders_read_address_ready : std_logic_vector(config_array'range); + signal responders_read_data_ready : std_logic_vector(config_array'range); + signal responders_read_data_resp : tgt_resp_t(config_array'range); + signal responders_read_data_valid : std_logic_vector(config_array'range); + signal responders_read_data_data : tgt_dat32_t(config_array'range); + + -- handshake accounting, checked by the testbench + signal init_aw_hs : integer := 0; + signal init_b_hs : integer := 0; + signal init_ar_hs : integer := 0; + signal init_r_hs : integer := 0; + signal resp_aw_hs : integer := 0; + signal resp_ar_hs : integer := 0; + +begin + + -- 125 MHz, matching the fabric clock in cosmo_seq and grapefruit + clk <= not clk after 4 ns; + reset_por <= '0' after 200 ns; + reset <= reset_por or reset_force; + + axi_lite_master_inst: entity vunit_lib.axi_lite_master + generic map ( + bus_handle => bus_handle + ) + port map ( + aclk => clk, + arready => init_arready, + arvalid => bfm_arvalid, + araddr => bfm_araddr, + rready => bfm_rready, + rvalid => init_rvalid, + rdata => init_rdata, + rresp => init_rresp, + awready => init_awready, + awvalid => bfm_awvalid, + awaddr => bfm_awaddr, + wready => init_wready, + wvalid => bfm_wvalid, + wdata => bfm_wdata, + wstrb => bfm_wstrb, + bvalid => init_bvalid, + bready => bfm_bready, + bresp => init_bresp + ); + + init_awvalid <= man_awvalid when man_mode = '1' else bfm_awvalid; + init_awaddr <= man_awaddr when man_mode = '1' else bfm_awaddr; + init_wvalid <= man_wvalid when man_mode = '1' else bfm_wvalid; + init_wdata <= man_wdata when man_mode = '1' else bfm_wdata; + init_wstrb <= man_wstrb when man_mode = '1' else bfm_wstrb; + init_bready <= man_bready when man_mode = '1' else bfm_bready; + init_arvalid <= man_arvalid when man_mode = '1' else bfm_arvalid; + init_araddr <= man_araddr when man_mode = '1' else bfm_araddr; + init_rready <= man_rready when man_mode = '1' else bfm_rready; + + dut: entity work.axil_interconnect_2k8 + generic map ( + initiator_addr_width => INITIATOR_ADDR_WIDTH, + config_array => config_array + ) + port map ( + clk => clk, + reset => reset, + initiator_write_address_addr => init_awaddr, + initiator_write_address_valid => init_awvalid, + initiator_write_address_ready => init_awready, + initiator_write_data_data => init_wdata, + initiator_write_data_strb => init_wstrb, + initiator_write_data_ready => init_wready, + initiator_write_data_valid => init_wvalid, + initiator_write_response_valid => init_bvalid, + initiator_write_response_resp => init_bresp, + initiator_write_response_ready => init_bready, + initiator_read_address_addr => init_araddr, + initiator_read_address_ready => init_arready, + initiator_read_address_valid => init_arvalid, + initiator_read_data_valid => init_rvalid, + initiator_read_data_ready => init_rready, + initiator_read_data_resp => init_rresp, + initiator_read_data_data => init_rdata, + responders_write_address_valid => responders_write_address_valid, + responders_write_address_ready => responders_write_address_ready, + responders_write_address_addr => responders_write_address_addr, + responders_write_data_valid => responders_write_data_valid, + responders_write_data_ready => responders_write_data_ready, + responders_write_data_data => responders_write_data_data, + responders_write_data_strb => responders_write_data_strb, + responders_write_response_ready => responders_write_response_ready, + responders_read_address_valid => responders_read_address_valid, + responders_read_address_addr => responders_read_address_addr, + responders_read_data_ready => responders_read_data_ready, + responders_write_response_resp => responders_write_response_resp, + responders_write_response_valid => responders_write_response_valid, + responders_read_address_ready => responders_read_address_ready, + responders_read_data_resp => responders_read_data_resp, + responders_read_data_valid => responders_read_data_valid, + responders_read_data_data => responders_read_data_data + ); + + sram_a: entity work.axil_sram_responder + generic map ( + addr_width => 8 + ) + port map ( + clk => clk, + reset => reset, + awvalid => responders_write_address_valid(SRAM_A_IDX), + awready => responders_write_address_ready(SRAM_A_IDX), + awaddr => responders_write_address_addr(SRAM_A_IDX)(7 downto 0), + wvalid => responders_write_data_valid(SRAM_A_IDX), + wready => responders_write_data_ready(SRAM_A_IDX), + wdata => responders_write_data_data(SRAM_A_IDX), + wstrb => responders_write_data_strb(SRAM_A_IDX), + bvalid => responders_write_response_valid(SRAM_A_IDX), + bready => responders_write_response_ready(SRAM_A_IDX), + bresp => responders_write_response_resp(SRAM_A_IDX), + arvalid => responders_read_address_valid(SRAM_A_IDX), + arready => responders_read_address_ready(SRAM_A_IDX), + araddr => responders_read_address_addr(SRAM_A_IDX)(7 downto 0), + rvalid => responders_read_data_valid(SRAM_A_IDX), + rready => responders_read_data_ready(SRAM_A_IDX), + rdata => responders_read_data_data(SRAM_A_IDX), + rresp => responders_read_data_resp(SRAM_A_IDX) + ); + + sram_b: entity work.axil_sram_responder + generic map ( + addr_width => 8 + ) + port map ( + clk => clk, + reset => reset, + awvalid => responders_write_address_valid(SRAM_B_IDX), + awready => responders_write_address_ready(SRAM_B_IDX), + awaddr => responders_write_address_addr(SRAM_B_IDX)(7 downto 0), + wvalid => responders_write_data_valid(SRAM_B_IDX), + wready => responders_write_data_ready(SRAM_B_IDX), + wdata => responders_write_data_data(SRAM_B_IDX), + wstrb => responders_write_data_strb(SRAM_B_IDX), + bvalid => responders_write_response_valid(SRAM_B_IDX), + bready => responders_write_response_ready(SRAM_B_IDX), + bresp => responders_write_response_resp(SRAM_B_IDX), + arvalid => responders_read_address_valid(SRAM_B_IDX), + arready => responders_read_address_ready(SRAM_B_IDX), + araddr => responders_read_address_addr(SRAM_B_IDX)(7 downto 0), + rvalid => responders_read_data_valid(SRAM_B_IDX), + rready => responders_read_data_ready(SRAM_B_IDX), + rdata => responders_read_data_data(SRAM_B_IDX), + rresp => responders_read_data_resp(SRAM_B_IDX) + ); + + slow: entity work.axil_slow_responder + generic map ( + addr_width => 8, + seed => x"5A" + ) + port map ( + clk => clk, + reset => reset, + awvalid => responders_write_address_valid(SLOW_IDX), + awready => responders_write_address_ready(SLOW_IDX), + awaddr => responders_write_address_addr(SLOW_IDX)(7 downto 0), + wvalid => responders_write_data_valid(SLOW_IDX), + wready => responders_write_data_ready(SLOW_IDX), + wdata => responders_write_data_data(SLOW_IDX), + wstrb => responders_write_data_strb(SLOW_IDX), + bvalid => responders_write_response_valid(SLOW_IDX), + bready => responders_write_response_ready(SLOW_IDX), + bresp => responders_write_response_resp(SLOW_IDX), + arvalid => responders_read_address_valid(SLOW_IDX), + arready => responders_read_address_ready(SLOW_IDX), + araddr => responders_read_address_addr(SLOW_IDX)(7 downto 0), + rvalid => responders_read_data_valid(SLOW_IDX), + rready => responders_read_data_ready(SLOW_IDX), + rdata => responders_read_data_data(SLOW_IDX), + rresp => responders_read_data_resp(SLOW_IDX) + ); + + wide: entity work.axil_sram_responder + generic map ( + addr_width => 15 + ) + port map ( + clk => clk, + reset => reset, + awvalid => responders_write_address_valid(WIDE_IDX), + awready => responders_write_address_ready(WIDE_IDX), + awaddr => responders_write_address_addr(WIDE_IDX)(14 downto 0), + wvalid => responders_write_data_valid(WIDE_IDX), + wready => responders_write_data_ready(WIDE_IDX), + wdata => responders_write_data_data(WIDE_IDX), + wstrb => responders_write_data_strb(WIDE_IDX), + bvalid => responders_write_response_valid(WIDE_IDX), + bready => responders_write_response_ready(WIDE_IDX), + bresp => responders_write_response_resp(WIDE_IDX), + arvalid => responders_read_address_valid(WIDE_IDX), + arready => responders_read_address_ready(WIDE_IDX), + araddr => responders_read_address_addr(WIDE_IDX)(14 downto 0), + rvalid => responders_read_data_valid(WIDE_IDX), + rready => responders_read_data_ready(WIDE_IDX), + rdata => responders_read_data_data(WIDE_IDX), + rresp => responders_read_data_resp(WIDE_IDX) + ); + + -- Handshake accounting. The testbench compares these at the end of every + -- test: one initiator write must produce exactly one responder-side AW + -- handshake, so a fabric or pipeline stage that issues a duplicate write + -- shows up here even when the data happens to land correctly. + counters: process(clk, reset) + begin + if reset = '1' then + init_aw_hs <= 0; + init_b_hs <= 0; + init_ar_hs <= 0; + init_r_hs <= 0; + resp_aw_hs <= 0; + resp_ar_hs <= 0; + elsif rising_edge(clk) then + if init_awvalid = '1' and init_awready = '1' then + init_aw_hs <= init_aw_hs + 1; + end if; + if init_bvalid = '1' and init_bready = '1' then + init_b_hs <= init_b_hs + 1; + end if; + if init_arvalid = '1' and init_arready = '1' then + init_ar_hs <= init_ar_hs + 1; + end if; + if init_rvalid = '1' and init_rready = '1' then + init_r_hs <= init_r_hs + 1; + end if; + for i in config_array'range loop + if responders_write_address_valid(i) = '1' and responders_write_address_ready(i) = '1' then + resp_aw_hs <= resp_aw_hs + 1; + end if; + if responders_read_address_valid(i) = '1' and responders_read_address_ready(i) = '1' then + resp_ar_hs <= resp_ar_hs + 1; + end if; + end loop; + end if; + end process; + + -- AXI payload stability: while a VALID is asserted without its READY, the + -- payload it carries must not change. + stability: process(clk) + variable prev_awaddr : std_logic_vector(init_awaddr'range); + variable prev_araddr : std_logic_vector(init_araddr'range); + variable prev_rdata : std_logic_vector(init_rdata'range); + variable had_aw : boolean := false; + variable had_ar : boolean := false; + variable had_r : boolean := false; + begin + if rising_edge(clk) then + if reset = '0' and man_mode = '0' then + if had_aw and init_awvalid = '1' then + check_equal(init_awaddr, prev_awaddr, "AWADDR changed while AWVALID was stalled"); + end if; + if had_ar and init_arvalid = '1' then + check_equal(init_araddr, prev_araddr, "ARADDR changed while ARVALID was stalled"); + end if; + if had_r and init_rvalid = '1' then + check_equal(init_rdata, prev_rdata, "RDATA changed while RVALID was stalled"); + end if; + end if; + + had_aw := init_awvalid = '1' and init_awready = '0'; + prev_awaddr := init_awaddr; + had_ar := init_arvalid = '1' and init_arready = '0'; + prev_araddr := init_araddr; + had_r := init_rvalid = '1' and init_rready = '0'; + prev_rdata := init_rdata; + end if; + end process; + +end th; diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd new file mode 100644 index 00000000..8ed47073 --- /dev/null +++ b/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd @@ -0,0 +1,203 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.axil_common_pkg.all; + +-- Sim-only AXI-Lite responder that deliberately does *not* look like +-- axil_target_txn. Every responder in the tree today ties wready to awready and +-- makes bvalid a single-cycle pulse, which means the interconnect has only ever +-- seen one handshake shape. This model: +-- * accepts AW and W independently, each after its own stall +-- * delays B and R by a variable number of cycles after the address phase +-- * holds BVALID/RVALID until the corresponding READY +-- The stalls come from an LFSR so the pattern varies across a test but is +-- identical run to run. + +entity axil_slow_responder is + generic ( + addr_width : integer := 8; + --! LFSR seed, so multiple instances can stall differently + seed : std_logic_vector(7 downto 0) := x"A5" + ); + port ( + clk : in std_logic; + reset : in std_logic; + + awvalid : in std_logic; + awready : out std_logic; + awaddr : in std_logic_vector(addr_width - 1 downto 0); + + wvalid : in std_logic; + wready : out std_logic; + wdata : in std_logic_vector(31 downto 0); + wstrb : in std_logic_vector(3 downto 0); + + bvalid : out std_logic; + bready : in std_logic; + bresp : out std_logic_vector(1 downto 0); + + arvalid : in std_logic; + arready : out std_logic; + araddr : in std_logic_vector(addr_width - 1 downto 0); + + rvalid : out std_logic; + rready : in std_logic; + rdata : out std_logic_vector(31 downto 0); + rresp : out std_logic_vector(1 downto 0) + ); +end entity; + +architecture rtl of axil_slow_responder is + + constant NUM_WORDS : integer := 16; + constant MAX_DELAY : integer := 3; + + type storage_t is array (0 to NUM_WORDS - 1) of std_logic_vector(31 downto 0); + + signal storage : storage_t; + signal lfsr : std_logic_vector(7 downto 0); + + signal aw_done : std_logic; + signal w_done : std_logic; + signal ar_done : std_logic; + + signal aw_cnt : integer range 0 to MAX_DELAY; + signal w_cnt : integer range 0 to MAX_DELAY; + signal b_cnt : integer range 0 to MAX_DELAY; + signal ar_cnt : integer range 0 to MAX_DELAY; + signal r_cnt : integer range 0 to MAX_DELAY; + + signal awaddr_reg : std_logic_vector(addr_width - 1 downto 0); + signal wdata_reg : std_logic_vector(31 downto 0); + signal wstrb_reg : std_logic_vector(3 downto 0); + signal rdata_reg : std_logic_vector(31 downto 0); + + function delay_of (constant bits : std_logic_vector(1 downto 0)) return integer is + begin + return to_integer(unsigned(bits)); + end function; + +begin + + bresp <= OKAY; + rresp <= OKAY; + rdata <= rdata_reg; + + lfsr_gen: process(clk, reset) + begin + if reset = '1' then + lfsr <= seed; + elsif rising_edge(clk) then + lfsr <= lfsr(6 downto 0) & (lfsr(7) xor lfsr(5) xor lfsr(4) xor lfsr(3)); + end if; + end process; + + txn: process(clk, reset) + variable idx : integer range 0 to NUM_WORDS - 1; + begin + if reset = '1' then + storage <= (others => (others => '0')); + awready <= '0'; + wready <= '0'; + bvalid <= '0'; + arready <= '0'; + rvalid <= '0'; + aw_done <= '0'; + w_done <= '0'; + ar_done <= '0'; + aw_cnt <= 0; + w_cnt <= 1; + b_cnt <= 1; + ar_cnt <= 0; + r_cnt <= 1; + elsif rising_edge(clk) then + -- readys are single cycle pulses + awready <= '0'; + wready <= '0'; + arready <= '0'; + + -- capture whatever handshaked this cycle + if awready = '1' and awvalid = '1' then + aw_done <= '1'; + awaddr_reg <= awaddr; + end if; + if wready = '1' and wvalid = '1' then + w_done <= '1'; + wdata_reg <= wdata; + wstrb_reg <= wstrb; + end if; + if arready = '1' and arvalid = '1' then + ar_done <= '1'; + idx := to_integer(unsigned(araddr(5 downto 2))); + rdata_reg <= storage(idx); + end if; + + -- stall, then accept. AW and W are completely independent. + if aw_done = '0' and awready = '0' and awvalid = '1' then + if aw_cnt = 0 then + awready <= '1'; + else + aw_cnt <= aw_cnt - 1; + end if; + end if; + if w_done = '0' and wready = '0' and wvalid = '1' then + if w_cnt = 0 then + wready <= '1'; + else + w_cnt <= w_cnt - 1; + end if; + end if; + if ar_done = '0' and arready = '0' and arvalid = '1' then + if ar_cnt = 0 then + arready <= '1'; + else + ar_cnt <= ar_cnt - 1; + end if; + end if; + + -- responses, held until ready + if aw_done = '1' and w_done = '1' and bvalid = '0' then + if b_cnt = 0 then + bvalid <= '1'; + idx := to_integer(unsigned(awaddr_reg(5 downto 2))); + for byte in 0 to 3 loop + if wstrb_reg(byte) = '1' then + storage(idx)(8 * byte + 7 downto 8 * byte) <= wdata_reg(8 * byte + 7 downto 8 * byte); + end if; + end loop; + else + b_cnt <= b_cnt - 1; + end if; + end if; + if ar_done = '1' and rvalid = '0' then + if r_cnt = 0 then + rvalid <= '1'; + else + r_cnt <= r_cnt - 1; + end if; + end if; + + -- teardown, and pick the next set of stalls + if bvalid = '1' and bready = '1' then + bvalid <= '0'; + aw_done <= '0'; + w_done <= '0'; + aw_cnt <= delay_of(lfsr(1 downto 0)); + w_cnt <= delay_of(lfsr(3 downto 2)); + b_cnt <= delay_of(lfsr(5 downto 4)); + end if; + if rvalid = '1' and rready = '1' then + rvalid <= '0'; + ar_done <= '0'; + ar_cnt <= delay_of(lfsr(4 downto 3)); + r_cnt <= delay_of(lfsr(7 downto 6)); + end if; + end if; + end process; + +end rtl; diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_sram_responder.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_sram_responder.vhd new file mode 100644 index 00000000..2e2d1a82 --- /dev/null +++ b/hdl/ip/vhd/axi_blocks/sims/axil_sram_responder.vhd @@ -0,0 +1,113 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.axil_common_pkg.all; + +-- Sim-only AXI-Lite responder: a handful of read/write words behind the *real* +-- axil_target_txn block. Using the production transaction block is the whole +-- point of this model -- it reproduces the handshake contract that every +-- register block in the tree presents to the interconnect: +-- * awready is a registered one-cycle pulse gated on awvalid AND wvalid +-- * wready is tied to awready, so AW and W always handshake together +-- * arready is combinational (not rvalid), so AR handshakes immediately +-- * bvalid is a single-cycle pulse when bready is already asserted +-- That last property in particular catches a pipeline stage that goes looking +-- for B a cycle too late. + +entity axil_sram_responder is + generic ( + addr_width : integer := 8 + ); + port ( + clk : in std_logic; + reset : in std_logic; + + awvalid : in std_logic; + awready : out std_logic; + awaddr : in std_logic_vector(addr_width - 1 downto 0); + + wvalid : in std_logic; + wready : out std_logic; + wdata : in std_logic_vector(31 downto 0); + wstrb : in std_logic_vector(3 downto 0); + + bvalid : out std_logic; + bready : in std_logic; + bresp : out std_logic_vector(1 downto 0); + + arvalid : in std_logic; + arready : out std_logic; + araddr : in std_logic_vector(addr_width - 1 downto 0); + + rvalid : out std_logic; + rready : in std_logic; + rdata : out std_logic_vector(31 downto 0); + rresp : out std_logic_vector(1 downto 0) + ); +end entity; + +architecture rtl of axil_sram_responder is + + constant NUM_WORDS : integer := 16; + + type storage_t is array (0 to NUM_WORDS - 1) of std_logic_vector(31 downto 0); + + signal storage : storage_t; + signal active_read : std_logic; + signal active_write : std_logic; + signal rdata_reg : std_logic_vector(31 downto 0); + +begin + + axil_target_txn_inst: entity work.axil_target_txn + port map ( + clk => clk, + reset => reset, + arvalid => arvalid, + arready => arready, + awvalid => awvalid, + awready => awready, + wready => wready, + wvalid => wvalid, + bvalid => bvalid, + bready => bready, + bresp => bresp, + rvalid => rvalid, + rready => rready, + rresp => rresp, + active_read => active_read, + active_write => active_write + ); + + rdata <= rdata_reg; + + -- word addressed off bits 5:2, so 16 words repeat through the space + sram: process(clk, reset) + variable idx : integer range 0 to NUM_WORDS - 1; + begin + if reset = '1' then + storage <= (others => (others => '0')); + rdata_reg <= (others => '0'); + elsif rising_edge(clk) then + if active_write = '1' then + idx := to_integer(unsigned(awaddr(5 downto 2))); + for byte in 0 to 3 loop + if wstrb(byte) = '1' then + storage(idx)(8 * byte + 7 downto 8 * byte) <= wdata(8 * byte + 7 downto 8 * byte); + end if; + end loop; + end if; + + if active_read = '1' then + idx := to_integer(unsigned(araddr(5 downto 2))); + rdata_reg <= storage(idx); + end if; + end if; + end process; + +end rtl; From 4dcc96a15e887197382208f078d346dac0934fa3 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Tue, 4 Aug 2026 22:59:05 -0400 Subject: [PATCH 04/10] axil_interconnect: add a pipe_stages field to the responder config 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. --- hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd | 25 +++++++++++++++++++ .../sims/axil_interconnect_sim_pkg.vhd | 8 +++--- .../spi_nor_controller/sims/spi_nor_th.vhd | 2 +- hdl/projects/cosmo_hp/cosmo_hp_top.vhd | 8 +++--- hdl/projects/cosmo_seq/cosmo_seq_top.vhd | 16 ++++++------ hdl/projects/grapefruit/grapefruit_top.vhd | 8 +++--- 6 files changed, 46 insertions(+), 21 deletions(-) diff --git a/hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd b/hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd index c389e72b..b10687de 100644 --- a/hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd +++ b/hdl/ip/vhd/axi_blocks/axil_common_pkg.vhd @@ -13,10 +13,24 @@ package axil_common_pkg is type axil_responder_config is record base_addr : std_logic_vector(31 downto 0); addr_span_bits : integer; + -- Register stages the interconnect inserts in *each* direction between + -- the fabric and this responder, to buy back setup time on a responder + -- that is physically far from the fabric. Added round trip latency is + -- 2 * pipe_stages cycles plus a few fixed handshake cycles, so raise it + -- only where timing needs it. 0 is a zero cost pass-through. + pipe_stages : natural; end record; type axil_responder_cfg_array_t is array (natural range <>) of axil_responder_config; + -- Constructor for a responder config, so adding a field later does not + -- break every aggregate in the tree. + function resp_cfg ( + constant base_addr : std_logic_vector(31 downto 0); + constant addr_span_bits : integer; + constant pipe_stages : natural := 0 + ) return axil_responder_config; + type int_array is array (natural range <>) of integer; constant OKAY : std_logic_vector(1 downto 0) := "00"; @@ -48,6 +62,17 @@ end package; package body axil_common_pkg is + function resp_cfg ( + constant base_addr : std_logic_vector(31 downto 0); + constant addr_span_bits : integer; + constant pipe_stages : natural := 0 + ) return axil_responder_config is + begin + return (base_addr => base_addr, + addr_span_bits => addr_span_bits, + pipe_stages => pipe_stages); + end function; + function span_mask (constant addr_span_bits : integer) return std_logic_vector is variable mask : std_logic_vector(31 downto 0) := (others => '0'); begin diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd index 6b760dcf..0e1e92f9 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd @@ -33,10 +33,10 @@ package axil_interconnect_sim_pkg is constant WIDE_IDX : integer := 3; constant config_array : axil_responder_cfg_array_t(0 to 3) := - (SRAM_A_IDX => (base_addr => x"00000000", addr_span_bits => 8), - SRAM_B_IDX => (base_addr => x"00000100", addr_span_bits => 8), - SLOW_IDX => (base_addr => x"00000200", addr_span_bits => 8), - WIDE_IDX => (base_addr => x"00008000", addr_span_bits => 15)); + (SRAM_A_IDX => resp_cfg(base_addr => x"00000000", addr_span_bits => 8), + SRAM_B_IDX => resp_cfg(base_addr => x"00000100", addr_span_bits => 8), + SLOW_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8), + WIDE_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15)); --! An integer as a bus address function ba (constant addr : integer) return std_logic_vector; diff --git a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_th.vhd b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_th.vhd index 396574bb..4e275c1f 100644 --- a/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_th.vhd +++ b/hdl/ip/vhd/spi_nor_controller/sims/spi_nor_th.vhd @@ -32,7 +32,7 @@ architecture th of spi_nor_th is signal io_oe : std_logic_vector(3 downto 0); constant config_array : axil_responder_cfg_array_t(0 downto 0) := ( - 0 => (base_addr => x"00000100", addr_span_bits => 8) + 0 => resp_cfg(base_addr => x"00000100", addr_span_bits => 8) ); signal responders : axil32x32_pkg.axil_array_t(0 downto 0); signal responders_8b : axil8x32_pkg.axil_array_t(0 downto 0); diff --git a/hdl/projects/cosmo_hp/cosmo_hp_top.vhd b/hdl/projects/cosmo_hp/cosmo_hp_top.vhd index 8277ba1e..0a91d227 100644 --- a/hdl/projects/cosmo_hp/cosmo_hp_top.vhd +++ b/hdl/projects/cosmo_hp/cosmo_hp_top.vhd @@ -205,10 +205,10 @@ architecture rtl of cosmo_hp_top is signal pca_int_n : std_logic_vector(io_i2c_addr'length - 1 downto 0); constant config_array : axil_responder_cfg_array_t := - (0 => (base_addr => x"00000000", addr_span_bits => 8), - 1 => (base_addr => x"00000100", addr_span_bits => 8), - 2 => (base_addr => x"00000200", addr_span_bits => 8), - 3 => (base_addr => x"00000300", addr_span_bits => 8) + (0 => resp_cfg(base_addr => x"00000000", addr_span_bits => 8), + 1 => resp_cfg(base_addr => x"00000100", addr_span_bits => 8), + 2 => resp_cfg(base_addr => x"00000200", addr_span_bits => 8), + 3 => resp_cfg(base_addr => x"00000300", addr_span_bits => 8) ); signal sp_write_address_addr : std_logic_vector(15 downto 0); diff --git a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd index 0b5eeb46..2e9a220b 100644 --- a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd +++ b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd @@ -345,14 +345,14 @@ architecture rtl of cosmo_seq_top is constant ESPI_RESP_IDX: integer := 7; constant config_array : axil_responder_cfg_array_t := - (INFO_RESP_IDX => (base_addr => x"00000000", addr_span_bits => 8), - SPINOR_RESP_IDX => (base_addr => x"00000100", addr_span_bits => 8), - SEQ_RESP_IDX => (base_addr => x"00000200", addr_span_bits => 8), - SP_I2C_RESP_IDX => (base_addr => x"00000300", addr_span_bits => 8), - SP5_HP_RESP_IDX => (base_addr => x"00000400", addr_span_bits => 8), - SPD_PROXY_RESP_IDX => (base_addr => x"00000500", addr_span_bits => 8), - DBG_CTRL_RESP_IDX => (base_addr => x"00000600", addr_span_bits => 8), - ESPI_RESP_IDX => (base_addr => x"00008000", addr_span_bits => 15) + (INFO_RESP_IDX => resp_cfg(base_addr => x"00000000", addr_span_bits => 8), + SPINOR_RESP_IDX => resp_cfg(base_addr => x"00000100", addr_span_bits => 8), + SEQ_RESP_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8), + SP_I2C_RESP_IDX => resp_cfg(base_addr => x"00000300", addr_span_bits => 8), + SP5_HP_RESP_IDX => resp_cfg(base_addr => x"00000400", addr_span_bits => 8), + SPD_PROXY_RESP_IDX => resp_cfg(base_addr => x"00000500", addr_span_bits => 8), + DBG_CTRL_RESP_IDX => resp_cfg(base_addr => x"00000600", addr_span_bits => 8), + ESPI_RESP_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15) ); signal fmc_axi_if : axil26x32_pkg.axil_t; signal fabric_responders : axil32x32_pkg.axil_array_t(config_array'range); diff --git a/hdl/projects/grapefruit/grapefruit_top.vhd b/hdl/projects/grapefruit/grapefruit_top.vhd index ec1b8add..5d70aad9 100644 --- a/hdl/projects/grapefruit/grapefruit_top.vhd +++ b/hdl/projects/grapefruit/grapefruit_top.vhd @@ -205,10 +205,10 @@ architecture rtl of grapefruit_top is -- TODO: someday I'd like the rdl stuff to both generate this and the fabric maybe? constant config_array : axil_responder_cfg_array_t := - (0 => (base_addr => x"00000000", addr_span_bits => 8), - 1 => (base_addr => x"00000100", addr_span_bits => 8), - 2 => (base_addr => x"00000200", addr_span_bits => 8), - 3 => (base_addr => x"00008000", addr_span_bits => 15) + (0 => resp_cfg(base_addr => x"00000000", addr_span_bits => 8), + 1 => resp_cfg(base_addr => x"00000100", addr_span_bits => 8), + 2 => resp_cfg(base_addr => x"00000200", addr_span_bits => 8), + 3 => resp_cfg(base_addr => x"00008000", addr_span_bits => 15) ); signal fabric_responders : axil32x32_pkg.axil_array_t(config_array'range); signal responders_8b : axil8x32_pkg.axil_array_t(config_array'range); From 3f8b5de6f7db80855731a097b68269dbbe3d23f0 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Tue, 4 Aug 2026 23:09:33 -0400 Subject: [PATCH 05/10] axil_interconnect: add configurable per responder pipeline stages 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. --- hdl/ip/vhd/axi_blocks/BUCK | 11 + .../vhd/axi_blocks/axil_interconnect_2k8.vhd | 106 ++++- hdl/ip/vhd/axi_blocks/axil_pipe.vhd | 383 ++++++++++++++++++ .../sims/axil_interconnect_sim_pkg.vhd | 8 +- .../axi_blocks/sims/axil_interconnect_tb.vhd | 72 +++- 5 files changed, 551 insertions(+), 29 deletions(-) create mode 100644 hdl/ip/vhd/axi_blocks/axil_pipe.vhd diff --git a/hdl/ip/vhd/axi_blocks/BUCK b/hdl/ip/vhd/axi_blocks/BUCK index 5c28c1a9..a939e1e0 100644 --- a/hdl/ip/vhd/axi_blocks/BUCK +++ b/hdl/ip/vhd/axi_blocks/BUCK @@ -86,11 +86,22 @@ vhdl_unit( visibility = ['PUBLIC'], ) +vhdl_unit( + name = "axil_pipe", + srcs = glob(["axil_pipe.vhd"]), + deps = [ + ":axilite_common_pkg", + ], + standard = "2008", + visibility = ['PUBLIC'], +) + vhdl_unit( name = "axil_interconnect_2k8", srcs = glob(["axil_interconnect_2k8.vhd"]), deps = [ ":axilite_if_2k8", + ":axil_pipe", ], standard = "2008", visibility = ['PUBLIC'], diff --git a/hdl/ip/vhd/axi_blocks/axil_interconnect_2k8.vhd b/hdl/ip/vhd/axi_blocks/axil_interconnect_2k8.vhd index 7a33f4c6..6326972f 100644 --- a/hdl/ip/vhd/axi_blocks/axil_interconnect_2k8.vhd +++ b/hdl/ip/vhd/axi_blocks/axil_interconnect_2k8.vhd @@ -116,6 +116,26 @@ architecture rtl of axil_interconnect_2k8 is signal write_done : std_logic; signal read_done : std_logic; + -- Fabric side of each responder's optional pipeline. With pipe_stages = 0 + -- these are wired straight through to the responder ports. + signal mux_write_address_valid : std_logic_vector(config_array'range); + signal mux_write_address_ready : std_logic_vector(config_array'range); + signal mux_write_address_addr : tgt_addr32_t(config_array'range); + signal mux_write_data_valid : std_logic_vector(config_array'range); + signal mux_write_data_ready : std_logic_vector(config_array'range); + signal mux_write_data_data : tgt_dat32_t(config_array'range); + signal mux_write_data_strb : tgt_strb_t(config_array'range); + signal mux_write_response_valid : std_logic_vector(config_array'range); + signal mux_write_response_ready : std_logic_vector(config_array'range); + signal mux_write_response_resp : tgt_resp_t(config_array'range); + signal mux_read_address_valid : std_logic_vector(config_array'range); + signal mux_read_address_ready : std_logic_vector(config_array'range); + signal mux_read_address_addr : tgt_addr32_t(config_array'range); + signal mux_read_data_valid : std_logic_vector(config_array'range); + signal mux_read_data_ready : std_logic_vector(config_array'range); + signal mux_read_data_data : tgt_dat32_t(config_array'range); + signal mux_read_data_resp : tgt_resp_t(config_array'range); + begin assert config_array'low = 0 @@ -233,14 +253,14 @@ begin for i in config_array'range loop if sel_onehot(i) = '1' then - awready := responders_write_address_ready(i); - wready := responders_write_data_ready(i); - bvalid := responders_write_response_valid(i); - bresp := responders_write_response_resp(i); - arready := responders_read_address_ready(i); - rvalid := responders_read_data_valid(i); - rresp := responders_read_data_resp(i); - rdata := responders_read_data_data(i); + awready := mux_write_address_ready(i); + wready := mux_write_data_ready(i); + bvalid := mux_write_response_valid(i); + bresp := mux_write_response_resp(i); + arready := mux_read_address_ready(i); + rvalid := mux_read_data_valid(i); + rresp := mux_read_data_resp(i); + rdata := mux_read_data_data(i); end if; end loop; @@ -276,20 +296,70 @@ begin -- responder's own span, and only the selected responder sees a valid. The -- masked-off address bits are hard constant zeros, so the fabric side of -- each responder's address bus collapses to span real wires. + -- + -- Each responder then optionally goes through axil_pipe, which inserts + -- config_array(i).pipe_stages register stages in each direction so a + -- responder that sits a long way from the fabric does not have to be reached + -- and answered within one clock period. pipe_stages = 0 is a pass-through and + -- costs nothing. resp_gen: for i in config_array'range generate constant span : natural := config_array(i).addr_span_bits; begin - responders_write_address_addr(i) <= wr_addr32 and span_mask(span); - responders_read_address_addr(i) <= rd_addr32 and span_mask(span); - responders_write_data_data(i) <= initiator_write_data_data; - responders_write_data_strb(i) <= initiator_write_data_strb; - - responders_write_address_valid(i) <= initiator_write_address_valid and sel_onehot(i); - responders_write_data_valid(i) <= initiator_write_data_valid and sel_onehot(i); - responders_write_response_ready(i) <= initiator_write_response_ready and sel_onehot(i); - responders_read_address_valid(i) <= initiator_read_address_valid and sel_onehot(i); - responders_read_data_ready(i) <= initiator_read_data_ready and sel_onehot(i); + mux_write_address_addr(i) <= wr_addr32 and span_mask(span); + mux_read_address_addr(i) <= rd_addr32 and span_mask(span); + mux_write_data_data(i) <= initiator_write_data_data; + mux_write_data_strb(i) <= initiator_write_data_strb; + + mux_write_address_valid(i) <= initiator_write_address_valid and sel_onehot(i); + mux_write_data_valid(i) <= initiator_write_data_valid and sel_onehot(i); + mux_write_response_ready(i) <= initiator_write_response_ready and sel_onehot(i); + mux_read_address_valid(i) <= initiator_read_address_valid and sel_onehot(i); + mux_read_data_ready(i) <= initiator_read_data_ready and sel_onehot(i); + + axil_pipe_inst: entity work.axil_pipe + generic map ( + stages => config_array(i).pipe_stages, + addr_width => span + ) + port map ( + clk => clk, + reset => reset, + sink_awaddr => mux_write_address_addr(i), + sink_awvalid => mux_write_address_valid(i), + sink_awready => mux_write_address_ready(i), + sink_wdata => mux_write_data_data(i), + sink_wstrb => mux_write_data_strb(i), + sink_wvalid => mux_write_data_valid(i), + sink_wready => mux_write_data_ready(i), + sink_bvalid => mux_write_response_valid(i), + sink_bresp => mux_write_response_resp(i), + sink_bready => mux_write_response_ready(i), + sink_araddr => mux_read_address_addr(i), + sink_arvalid => mux_read_address_valid(i), + sink_arready => mux_read_address_ready(i), + sink_rvalid => mux_read_data_valid(i), + sink_rdata => mux_read_data_data(i), + sink_rresp => mux_read_data_resp(i), + sink_rready => mux_read_data_ready(i), + source_awaddr => responders_write_address_addr(i), + source_awvalid => responders_write_address_valid(i), + source_awready => responders_write_address_ready(i), + source_wdata => responders_write_data_data(i), + source_wstrb => responders_write_data_strb(i), + source_wvalid => responders_write_data_valid(i), + source_wready => responders_write_data_ready(i), + source_bvalid => responders_write_response_valid(i), + source_bresp => responders_write_response_resp(i), + source_bready => responders_write_response_ready(i), + source_araddr => responders_read_address_addr(i), + source_arvalid => responders_read_address_valid(i), + source_arready => responders_read_address_ready(i), + source_rvalid => responders_read_data_valid(i), + source_rdata => responders_read_data_data(i), + source_rresp => responders_read_data_resp(i), + source_rready => responders_read_data_ready(i) + ); end generate; diff --git a/hdl/ip/vhd/axi_blocks/axil_pipe.vhd b/hdl/ip/vhd/axi_blocks/axil_pipe.vhd new file mode 100644 index 00000000..96bd9a95 --- /dev/null +++ b/hdl/ip/vhd/axi_blocks/axil_pipe.vhd @@ -0,0 +1,383 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + + +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +use work.axil_common_pkg.all; + +-- Configurable pipeline stages between the interconnect fabric and one +-- responder, so a responder that is physically far from the fabric does not have +-- to be reached and answered inside a single clock period. +-- +-- The fabric admits one transaction, read or write, at a time. That means this +-- does not have to be five independent AXI channel register slices: the whole +-- transaction serializes into one request bundle going out and one response +-- bundle coming back, which is roughly a third of the flops a pair of full +-- register slices would cost, with a single token chain instead of five sets of +-- valid/ready control. +-- +-- 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 is what +-- keeps the far end of the chain stable across a multi-cycle handshake, and it is +-- why no separate capture register is needed at either end. Gating the whole +-- chain on the OR of all the tokens instead would clobber the last stage on the +-- cycle the token reached it. +-- +-- The single-outstanding-transaction property of the fabric is load bearing here: +-- it is what makes it safe for a stage to hold its payload indefinitely, and what +-- guarantees a new request cannot enter the chain while a response is still on +-- its way back. +-- +-- Round trip latency is 2 * stages cycles plus a handful of fixed handshake +-- cycles. stages = 0 is a plain pass-through and costs nothing. + +entity axil_pipe is + generic ( + --! Register stages inserted in *each* direction + stages : natural; + --! Address bits this responder actually decodes + addr_width : natural + ); + port ( + clk : in std_logic; + reset : in std_logic; + + -- Fabric facing side, a *target* interface + sink_awaddr : in std_logic_vector(31 downto 0); + sink_awvalid : in std_logic; + sink_awready : out std_logic; + + sink_wdata : in std_logic_vector(31 downto 0); + sink_wstrb : in std_logic_vector(3 downto 0); + sink_wvalid : in std_logic; + sink_wready : out std_logic; + + sink_bvalid : out std_logic; + sink_bresp : out std_logic_vector(1 downto 0); + sink_bready : in std_logic; + + sink_araddr : in std_logic_vector(31 downto 0); + sink_arvalid : in std_logic; + sink_arready : out std_logic; + + sink_rvalid : out std_logic; + sink_rdata : out std_logic_vector(31 downto 0); + sink_rresp : out std_logic_vector(1 downto 0); + sink_rready : in std_logic; + + -- Responder facing side, a *controller* interface + source_awaddr : out std_logic_vector(31 downto 0); + source_awvalid : out std_logic; + source_awready : in std_logic; + + source_wdata : out std_logic_vector(31 downto 0); + source_wstrb : out std_logic_vector(3 downto 0); + source_wvalid : out std_logic; + source_wready : in std_logic; + + source_bvalid : in std_logic; + source_bresp : in std_logic_vector(1 downto 0); + source_bready : out std_logic; + + source_araddr : out std_logic_vector(31 downto 0); + source_arvalid : out std_logic; + source_arready : in std_logic; + + source_rvalid : in std_logic; + source_rdata : in std_logic_vector(31 downto 0); + source_rresp : in std_logic_vector(1 downto 0); + source_rready : out std_logic + ); +end entity; + +architecture rtl of axil_pipe is + +begin + + pipe_gen: if stages > 0 generate + + -- request bundle: is_write & addr & wdata & wstrb + constant STRB_LO : natural := 0; + constant STRB_HI : natural := 3; + constant DATA_LO : natural := 4; + constant DATA_HI : natural := 35; + constant ADDR_LO : natural := 36; + constant ADDR_HI : natural := 36 + addr_width - 1; + constant IS_WRITE : natural := ADDR_HI + 1; + constant REQ_W : natural := IS_WRITE + 1; + + -- response bundle: rdata & resp + constant RESP_LO : natural := 0; + constant RESP_HI : natural := 1; + constant RDATA_LO : natural := 2; + constant RDATA_HI : natural := 33; + constant RSP_W : natural := RDATA_HI + 1; + + constant LAST : natural := stages - 1; + + type req_sr_t is array (0 to stages - 1) of std_logic_vector(REQ_W - 1 downto 0); + type rsp_sr_t is array (0 to stages - 1) of std_logic_vector(RSP_W - 1 downto 0); + + -- deliberately not reset: only the tokens need to come up clean, and + -- leaving the payload chain resetless lets Vivado map it to SRLs + signal req_sr : req_sr_t; + signal rsp_sr : rsp_sr_t; + + signal fwd_tok : std_logic_vector(stages - 1 downto 0); + signal ret_tok : std_logic_vector(stages - 1 downto 0); + + type sink_state_t is (armed, wait_rsp, respond, request_gap); + + signal sink_state : sink_state_t; + signal sink_is_write : std_logic; + --! registered one-shot, so AW and W always handshake in the same cycle + --! and AWREADY never depends combinationally on AWVALID + signal sink_wr_ack : std_logic; + signal sink_rd_ack : std_logic; + signal sink_resp_valid : std_logic; + + type source_state_t is (idle, issue); + + signal source_state : source_state_t; + signal src_is_write : std_logic; + signal src_awvalid : std_logic; + signal src_wvalid : std_logic; + signal src_arvalid : std_logic; + signal src_bready : std_logic; + signal src_rready : std_logic; + signal src_aw_done : std_logic; + signal src_w_done : std_logic; + signal src_b_done : std_logic; + signal src_ar_done : std_logic; + signal src_r_done : std_logic; + + begin + + sink_awready <= sink_wr_ack; + sink_wready <= sink_wr_ack; + sink_arready <= sink_rd_ack; + sink_bvalid <= sink_resp_valid and sink_is_write; + sink_rvalid <= sink_resp_valid and not sink_is_write; + sink_bresp <= rsp_sr(LAST)(RESP_HI downto RESP_LO); + sink_rresp <= rsp_sr(LAST)(RESP_HI downto RESP_LO); + sink_rdata <= rsp_sr(LAST)(RDATA_HI downto RDATA_LO); + + source_awvalid <= src_awvalid; + source_wvalid <= src_wvalid; + source_arvalid <= src_arvalid; + source_bready <= src_bready; + source_rready <= src_rready; + source_wdata <= req_sr(LAST)(DATA_HI downto DATA_LO); + source_wstrb <= req_sr(LAST)(STRB_HI downto STRB_LO); + -- the fabric already masked the address to this responder's span, so the + -- bits above it are constant zeros + source_awaddr <= (31 downto addr_width => '0') & req_sr(LAST)(ADDR_HI downto ADDR_LO); + source_araddr <= (31 downto addr_width => '0') & req_sr(LAST)(ADDR_HI downto ADDR_LO); + + -- Outbound: accept a transaction from the fabric, walk it down to the + -- responder, and present the response that comes back. + fwd: process(clk, reset) + begin + if reset = '1' then + sink_state <= armed; + sink_is_write <= '0'; + sink_wr_ack <= '0'; + sink_rd_ack <= '0'; + sink_resp_valid <= '0'; + fwd_tok <= (others => '0'); + elsif rising_edge(clk) then + sink_wr_ack <= '0'; + sink_rd_ack <= '0'; + + for j in stages - 1 downto 1 loop + fwd_tok(j) <= fwd_tok(j - 1); + if fwd_tok(j - 1) = '1' then + req_sr(j) <= req_sr(j - 1); + end if; + end loop; + fwd_tok(0) <= '0'; + + case sink_state is + when armed => + -- A write is only taken once AW and W are both present, + -- which is what every responder in the tree requires + -- before asserting AWREADY. WDATA has to be captured + -- here: the FMC target's write data FIFO pops on the W + -- handshake, so it is not valid afterwards. + if sink_awvalid = '1' and sink_wvalid = '1' then + req_sr(0)(IS_WRITE) <= '1'; + req_sr(0)(ADDR_HI downto ADDR_LO) <= sink_awaddr(addr_width - 1 downto 0); + req_sr(0)(DATA_HI downto DATA_LO) <= sink_wdata; + req_sr(0)(STRB_HI downto STRB_LO) <= sink_wstrb; + fwd_tok(0) <= '1'; + sink_wr_ack <= '1'; + sink_is_write <= '1'; + sink_state <= wait_rsp; + elsif sink_arvalid = '1' then + req_sr(0)(IS_WRITE) <= '0'; + req_sr(0)(ADDR_HI downto ADDR_LO) <= sink_araddr(addr_width - 1 downto 0); + fwd_tok(0) <= '1'; + sink_rd_ack <= '1'; + sink_is_write <= '0'; + sink_state <= wait_rsp; + end if; + + when wait_rsp => + if ret_tok(LAST) = '1' then + sink_resp_valid <= '1'; + sink_state <= respond; + end if; + + when respond => + if (sink_is_write = '1' and sink_bready = '1') or + (sink_is_write = '0' and sink_rready = '1') then + sink_resp_valid <= '0'; + sink_state <= request_gap; + end if; + + when request_gap => + -- Do not re-arm until the request that just completed is + -- off the bus. Initiators here deassert VALID a cycle + -- after the handshake, so without this a stale request + -- would be issued a second time. + if (sink_is_write = '1' and not (sink_awvalid = '1' and sink_wvalid = '1')) or + (sink_is_write = '0' and sink_arvalid = '0') then + sink_state <= armed; + end if; + + end case; + end if; + end process; + + -- Inbound: run the transaction against the responder and walk the + -- response back up. AW and W are tracked separately because responders + -- are allowed to accept them independently, and the handshakes are all + -- sampled in one state because axil_target_txn presents BVALID as a + -- single cycle pulse when BREADY is already asserted and ARREADY + -- combinationally, so a dedicated wait-for-response state would miss + -- them. + ret: process(clk, reset) + variable aw_done : std_logic; + variable w_done : std_logic; + variable b_done : std_logic; + variable ar_done : std_logic; + variable r_done : std_logic; + begin + if reset = '1' then + source_state <= idle; + src_is_write <= '0'; + src_awvalid <= '0'; + src_wvalid <= '0'; + src_arvalid <= '0'; + src_bready <= '0'; + src_rready <= '0'; + src_aw_done <= '0'; + src_w_done <= '0'; + src_b_done <= '0'; + src_ar_done <= '0'; + src_r_done <= '0'; + ret_tok <= (others => '0'); + elsif rising_edge(clk) then + for j in stages - 1 downto 1 loop + ret_tok(j) <= ret_tok(j - 1); + if ret_tok(j - 1) = '1' then + rsp_sr(j) <= rsp_sr(j - 1); + end if; + end loop; + ret_tok(0) <= '0'; + + case source_state is + when idle => + if fwd_tok(LAST) = '1' then + src_is_write <= req_sr(LAST)(IS_WRITE); + if req_sr(LAST)(IS_WRITE) = '1' then + src_awvalid <= '1'; + src_wvalid <= '1'; + src_bready <= '1'; + else + src_arvalid <= '1'; + src_rready <= '1'; + end if; + src_aw_done <= '0'; + src_w_done <= '0'; + src_b_done <= '0'; + src_ar_done <= '0'; + src_r_done <= '0'; + source_state <= issue; + end if; + + when issue => + aw_done := src_aw_done; + w_done := src_w_done; + b_done := src_b_done; + ar_done := src_ar_done; + r_done := src_r_done; + + if src_awvalid = '1' and source_awready = '1' then + src_awvalid <= '0'; + aw_done := '1'; + end if; + if src_wvalid = '1' and source_wready = '1' then + src_wvalid <= '0'; + w_done := '1'; + end if; + if source_bvalid = '1' and src_bready = '1' then + b_done := '1'; + rsp_sr(0)(RESP_HI downto RESP_LO) <= source_bresp; + end if; + if src_arvalid = '1' and source_arready = '1' then + src_arvalid <= '0'; + ar_done := '1'; + end if; + if source_rvalid = '1' and src_rready = '1' then + r_done := '1'; + rsp_sr(0)(RDATA_HI downto RDATA_LO) <= source_rdata; + rsp_sr(0)(RESP_HI downto RESP_LO) <= source_rresp; + end if; + + src_aw_done <= aw_done; + src_w_done <= w_done; + src_b_done <= b_done; + src_ar_done <= ar_done; + src_r_done <= r_done; + + if (src_is_write = '1' and aw_done = '1' and w_done = '1' and b_done = '1') or + (src_is_write = '0' and ar_done = '1' and r_done = '1') then + ret_tok(0) <= '1'; + src_bready <= '0'; + src_rready <= '0'; + source_state <= idle; + end if; + + end case; + end if; + end process; + + else generate + + sink_awready <= source_awready; + sink_wready <= source_wready; + sink_bvalid <= source_bvalid; + sink_bresp <= source_bresp; + sink_arready <= source_arready; + sink_rvalid <= source_rvalid; + sink_rdata <= source_rdata; + sink_rresp <= source_rresp; + + source_awaddr <= sink_awaddr; + source_awvalid <= sink_awvalid; + source_wdata <= sink_wdata; + source_wstrb <= sink_wstrb; + source_wvalid <= sink_wvalid; + source_bready <= sink_bready; + source_araddr <= sink_araddr; + source_arvalid <= sink_arvalid; + source_rready <= sink_rready; + + end generate; + +end rtl; diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd index 0e1e92f9..5c91dc9e 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd @@ -33,10 +33,10 @@ package axil_interconnect_sim_pkg is constant WIDE_IDX : integer := 3; constant config_array : axil_responder_cfg_array_t(0 to 3) := - (SRAM_A_IDX => resp_cfg(base_addr => x"00000000", addr_span_bits => 8), - SRAM_B_IDX => resp_cfg(base_addr => x"00000100", addr_span_bits => 8), - SLOW_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8), - WIDE_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15)); + (SRAM_A_IDX => resp_cfg(base_addr => x"00000000", addr_span_bits => 8, pipe_stages => 0), + SRAM_B_IDX => resp_cfg(base_addr => x"00000100", addr_span_bits => 8, pipe_stages => 1), + SLOW_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8, pipe_stages => 3), + WIDE_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15, pipe_stages => 2)); --! An integer as a bus address function ba (constant addr : integer) return std_logic_vector; diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd index a2cb046e..4abb6e5d 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd @@ -66,6 +66,9 @@ begin constant TXN_TIMEOUT : time := 5 us; variable rdata : std_logic_vector(31 downto 0); + variable lat0 : integer; + variable lat1 : integer; + variable lat2 : integer; -- Every accepted write must produce exactly one B, every accepted read -- exactly one R, and (for mapped addresses) exactly one responder side @@ -155,6 +158,36 @@ begin wait until rising_edge(clk); end procedure; + --! Like manual_read, but also reports how many clocks the fabric took to + --! answer, so the testbench can show the configured stages are really in + --! the path rather than being generated away. + procedure manual_read_timed ( + constant addr : in std_logic_vector; + variable data : out std_logic_vector(31 downto 0); + variable cycles : out integer + ) is + variable count : integer := 0; + begin + man_mode <= '1'; + man_araddr <= addr; + man_rready <= '1'; + man_arvalid <= '1'; + loop + wait until rising_edge(clk); + exit when init_rvalid = '1'; + count := count + 1; + if count > 100 then + check(false, "timed out waiting for read data"); + exit; + end if; + end loop; + data := init_rdata; + cycles := count; + man_arvalid <= '0'; + man_rready <= '0'; + wait until rising_edge(clk); + end procedure; + begin test_runner_setup(runner, runner_cfg); wait until reset = '0'; @@ -177,13 +210,17 @@ begin check_handshake_accounting(net, mapped_only => true); elsif run("back_to_back_same_responder") then - for word in 0 to 7 loop - write_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 4 * word), - x"A5A50000" or w32(word)); - end loop; - for word in 0 to 7 loop - check_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 4 * word), axi_resp_okay, - x"A5A50000" or w32(word), "back to back readback"); + -- run the burst against every responder in turn, so token and + -- payload reuse is covered at each configured pipe depth + for idx in config_array'range loop + for word in 0 to 7 loop + write_axi_lite(net, bus_handle, ba(idx, 4 * word), + x"A5A50000" or w32(16 * idx + word)); + end loop; + for word in 0 to 7 loop + check_axi_lite(net, bus_handle, ba(idx, 4 * word), axi_resp_okay, + x"A5A50000" or w32(16 * idx + word), "back to back readback"); + end loop; end loop; check_handshake_accounting(net, mapped_only => true); @@ -303,6 +340,27 @@ begin clear_manual; check_handshake_accounting(net, mapped_only => true); + elsif run("pipe_latency") then + -- sram_a has no pipe, sram_b has one stage and the wide responder + -- two, so the answer must arrive strictly later each time + write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), x"00000011"); + write_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#00#), x"00000022"); + write_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#00#), x"00000033"); + wait_until_idle(net, bus_handle); + + manual_read_timed(ba(SRAM_A_IDX, 16#00#), rdata, lat0); + check_equal(rdata, std_logic_vector'(x"00000011"), "unpiped readback"); + manual_read_timed(ba(SRAM_B_IDX, 16#00#), rdata, lat1); + check_equal(rdata, std_logic_vector'(x"00000022"), "one stage readback"); + manual_read_timed(ba(WIDE_IDX, 16#00#), rdata, lat2); + check_equal(rdata, std_logic_vector'(x"00000033"), "two stage readback"); + info("read latency in clocks: 0 stages=" & to_string(lat0) & + " 1 stage=" & to_string(lat1) & " 2 stages=" & to_string(lat2)); + check(lat1 > lat0, "the one stage pipe added no latency"); + check(lat2 > lat1, "the two stage pipe added no latency over one stage"); + clear_manual; + check_handshake_accounting(net, mapped_only => true); + elsif run("reset_mid_transaction") then -- kick off a read at the slow responder and reset while it is in -- flight, then confirm the fabric comes back clean From 44da2da2c4f9d78552b8a9b88a7f29a427241e08 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Tue, 4 Aug 2026 23:18:04 -0400 Subject: [PATCH 06/10] cosmo_seq: pipeline the eSPI responder by one stage 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. --- hdl/projects/cosmo_seq/cosmo_seq_top.vhd | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd index 2e9a220b..45893855 100644 --- a/hdl/projects/cosmo_seq/cosmo_seq_top.vhd +++ b/hdl/projects/cosmo_seq/cosmo_seq_top.vhd @@ -352,7 +352,10 @@ architecture rtl of cosmo_seq_top is SP5_HP_RESP_IDX => resp_cfg(base_addr => x"00000400", addr_span_bits => 8), SPD_PROXY_RESP_IDX => resp_cfg(base_addr => x"00000500", addr_span_bits => 8), DBG_CTRL_RESP_IDX => resp_cfg(base_addr => x"00000600", addr_span_bits => 8), - ESPI_RESP_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15) + -- eSPI is the largest register file and the most distant block, and it + -- owns the worst 125MHz path in the design, so give the fabric a cycle + -- in each direction to get there and back. + ESPI_RESP_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15, pipe_stages => 1) ); signal fmc_axi_if : axil26x32_pkg.axil_t; signal fabric_responders : axil32x32_pkg.axil_array_t(config_array'range); From f3776862eb856c415558579fa2ad6596fe4d416c Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 12 Aug 2026 13:12:37 -0400 Subject: [PATCH 07/10] axil_interconnect: document the fabric and the pipeline Covers the single-outstanding-transaction property everything leans on, the equality-based address decode and its elaboration-checked invariants, the one-hot select, the error responder, and how axil_pipe serializes a transaction into one request bundle out and one response bundle back rather than five independent channel register slices. Two draw.io diagrams, sources embedded in the SVG content attribute as with the other drawings in the tree. --- .../axi_blocks/docs/axil_interconnect.adoc | 219 ++++++++++++++++++ .../docs/axil_interconnect_block.drawio.svg | 83 +++++++ .../vhd/axi_blocks/docs/axil_pipe.drawio.svg | 83 +++++++ 3 files changed, 385 insertions(+) create mode 100644 hdl/ip/vhd/axi_blocks/docs/axil_interconnect.adoc create mode 100644 hdl/ip/vhd/axi_blocks/docs/axil_interconnect_block.drawio.svg create mode 100644 hdl/ip/vhd/axi_blocks/docs/axil_pipe.drawio.svg diff --git a/hdl/ip/vhd/axi_blocks/docs/axil_interconnect.adoc b/hdl/ip/vhd/axi_blocks/docs/axil_interconnect.adoc new file mode 100644 index 00000000..c3522068 --- /dev/null +++ b/hdl/ip/vhd/axi_blocks/docs/axil_interconnect.adoc @@ -0,0 +1,219 @@ +:showtitle: +:toc: left +:numbered: +:icons: font +:revision: 1.0 +:revdate: 2026-08-05 + += AXI-Lite Interconnect + +`axil_interconnect_2k8` is the shared register-access fabric used by cosmo_seq, grapefruit and +cosmo_hp. One initiator (the FMC target on the Spartan-7 designs, the SPI AXI controller on +cosmo_hp) reaches a handful of responders, each of which owns a power-of-two slice of the address +space. + +`axil_interconnect` is a thin VHDL-2019 wrapper that presents the same thing with interface views; +all of the logic lives in the 2008 flat-port entity so that cosmo_hp, which builds through GHDL and +yosys, can use it too. + +== Overview +image::axil_interconnect_block.drawio.svg[align="center"] + +The single most important property of this block, and the one everything else leans on: + +[IMPORTANT] +==== +Exactly one transaction, read *or* write, is in flight at a time. A registered decode stage selects +a responder, the transaction runs to completion, and only then is the fabric torn down and re-armed. +==== + +Both initiators cooperate with this — each dispatches a read or a write from an idle state and does +not return to idle until B or R has landed, and neither ever asserts `AWVALID` and `ARVALID` +together. + +=== Address decode + +Every responder base address is aligned to its own span, and every span is a power of two. That +makes "is this address inside the range" equivalent to "do the address bits above the span match the +base", which is a plain equality compare rather than the pair of magnitude compares (and their carry +chains) that a base/limit check would build. + +[source,vhdl] +---- +wr_hit(i) <= '1' when ((wr_addr32 xor config_array(i).base_addr) and not span_mask(span)) = ZERO32 + else '0'; +---- + +The compare is deliberately over the full 32 bits of a *resized* address. Narrowing it to the +initiator's own width would let a 16-bit initiator falsely match a base it has no way to drive; the +extra bits compare against constant zeros and fold away in synthesis. + +The invariants this relies on are checked at elaboration by `bases_aligned`, `ranges_disjoint` and +`bases_reachable` in `axil_common_pkg`, so a bad map is a build error rather than a silent misroute. + +=== Responder select + +The select is a registered *one-hot* vector, `sel_onehot`, one bit per responder, plus a separate +`sel_default` bit for the catch-all. Keeping it one-hot rather than an integer index means the +return path is a flat AND-OR tree; an integer index forces synthesis to build an +integer-to-one-hot decoder inside the combinational mux. + +A write is only decoded once `AWVALID` *and* `WVALID` are both on the bus. That is the same +condition every responder already applies before asserting `AWREADY` (see `axil_target_txn`), and it +keeps a lone `AWVALID` from arming the fabric. + +Teardown takes priority over arming, and re-arming is blocked per channel until the initiator drops +the request it just completed. Initiators here deassert VALID one cycle *after* the handshake, so +without that guard a stale request re-arms the fabric and a duplicate transaction goes out behind +the initiator's back. The guard is tracked separately for reads and writes so that a permanently +asserted `AWVALID` cannot block reads. + +=== Error responder + +When no responder matches, `sel_default` is set instead of any bit of `sel_onehot`, and the fabric +answers by itself: `SLVERR`, with `RDATA` of `0xDEADBEEF` on reads. It answers only the channel that +was actually decoded, tracked by `sel_is_write`, so an unmapped read cannot hand an `AWREADY` to a +write that has not presented its data yet. + +Because the response is registered, an unmapped access costs the same one cycle of decode plus one +cycle of response as a mapped access to a fast responder. It never stalls, which is the entire point +of having it. + +== Pipelining + +Adding pipeline stages is how a responder that sits a long way from the fabric stops having to be +reached *and* answered inside a single clock period. Each responder gets its own depth, set by +`pipe_stages` in the config record: + +[source,vhdl] +---- +constant config_array : axil_responder_cfg_array_t := + (INFO_RESP_IDX => resp_cfg(base_addr => x"00000000", addr_span_bits => 8), + ... + ESPI_RESP_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15, pipe_stages => 1)); +---- + +`pipe_stages = 0` generates a plain pass-through and costs nothing, so the knob is free where it is +left alone. + +image::axil_pipe.drawio.svg[align="center"] + +=== Why it is not five register slices + +The obvious implementation is a register slice per AXI channel, which for AW/W/B/AR/R means five +independent sets of valid/ready control and roughly 292 flops per stage per responder. + +Because the fabric admits one transaction at a time, `axil_pipe` does not need that. The whole +transaction serializes into *one* request bundle going out and *one* response bundle coming back: + +[cols="1,3"] +|=== +| request bundle | `is_write & addr(addr_width) & wdata(32) & wstrb(4)` +| response bundle | `rdata(32) & resp(2)` +|=== + +Only the address bits the responder actually decodes are carried, so an 8-bit responder pipes 45 +bits rather than a 32-bit address. That lands at roughly a third of the flops of a pair of full +register slices, with a single 1-bit token chain instead of five sets of handshake control. + +Deliberately, no timing exceptions are required. cosmo_hp builds through yosys and nextpnr, where +there is no way to express a `set_multicycle_path`, so an approach that leaned on constraints would +have bought nothing on the one design whose critical path was actually in this block. + +=== How the chain advances + +Each payload stage advances *only behind its own token*: + +[source,vhdl] +---- +for j in stages - 1 downto 1 loop + fwd_tok(j) <= fwd_tok(j - 1); + if fwd_tok(j - 1) = '1' then + req_sr(j) <= req_sr(j - 1); + end if; +end loop; +---- + +That is what makes every stage hold its payload until the next transaction pushes through it, which +in turn keeps the far end of the chain stable across a multi-cycle handshake — and that is why no +separate capture register is needed at either end. Stage 0 *is* the sink's capture register and +stage `stages-1` *is* the source's. + +[WARNING] +==== +Gating the whole chain on the OR of all the tokens instead would clobber the last stage on the cycle +the token reached it, corrupting the payload the source FSM is still driving. +==== + +The payload chain is deliberately left resetless — only the tokens come up clean — which on 7-series +also lets Vivado map it to SRLs. + +=== The two ends + +The *sink* FSM (`armed`, `wait_rsp`, `respond`, `request_gap`) faces the fabric. It accepts a write +only when `AWVALID` and `WVALID` are both present and asserts `AWREADY` and `WREADY` together in the +same cycle, as a registered one-shot — never combinationally off `AWVALID`, which would create a +path straight back to the initiator that does not exist today. It captures `WDATA` on acceptance +because the FMC target's write-data FIFO pops on the W handshake and the data is not valid +afterwards. It will not re-arm until the request that just completed is off the bus. + +The *source* FSM (`idle`, `issue`) faces the responder. It tracks the AW and W handshakes separately, +because responders are free to accept them independently, and samples every handshake in one state. +That last part matters: `axil_target_txn` presents `BVALID` as a single-cycle pulse when `BREADY` is +already asserted, and `ARREADY` combinationally as `not rvalid`, so a dedicated wait-for-response +state would miss both and hang. + +=== Latency + +Round trip is `2 * pipe_stages` cycles plus a few fixed handshake cycles. Measured by the +`pipe_latency` testbench case, as clocks from `ARVALID` to `RVALID`: + +[cols="1,1"] +|=== +| `pipe_stages` | read latency + +| 0 | 2 clocks +| 1 | 6 clocks +| 2 | 8 clocks +|=== + +Each additional stage costs exactly 2 clocks; the step from 0 to 1 also picks up the pipe's fixed +handshake overhead. At 125 MHz one stage is about 40 ns of extra SP bus stall per access, which the +FMC covers with wait states — but it is a real cost, so raise `pipe_stages` only where timing needs +it and re-measure. + +== Simulation + +`buck2 run //hdl/ip/vhd/axi_blocks:axil_interconnect_tb` + +The harness drives the flat-port entity with a mixed responder map (pipe depths 0, 1, 3 and 2, plus +a deliberately 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 with deliberately different handshake shapes: + +* `axil_sram_responder` wraps the production `axil_target_txn`, so it reproduces the contract every + register block in the tree presents. +* `axil_slow_responder` accepts AW and W independently after LFSR-driven stalls and holds its + responses until READY — a 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. + +[NOTE] +==== +`write_axi_lite` only *queues* a write; it does not block. Call `wait_until_idle` before taking the +bus over by hand or before sampling the handshake counters. +==== + +== Things worth knowing + +* A concurrent read and write from one initiator is serialized, not run in parallel. Neither + initiator does this today. +* `axil_interconnect` hard-codes a 26-bit initiator. A differently sized initiator needs either the + 2008 entity directly (as cosmo_hp does) or a new wrapper. +* Registering B and R at the *initiator* boundary would cut the remaining combinational return mux, + but `RDATA` cannot be registered without also delaying `RVALID`, which would cost a cycle for + every responder including the unpiped ones. If it is ever needed it belongs behind its own + generic, not `pipe_stages`. diff --git a/hdl/ip/vhd/axi_blocks/docs/axil_interconnect_block.drawio.svg b/hdl/ip/vhd/axi_blocks/docs/axil_interconnect_block.drawio.svg new file mode 100644 index 00000000..efdbf957 --- /dev/null +++ b/hdl/ip/vhd/axi_blocks/docs/axil_interconnect_block.drawio.svg @@ -0,0 +1,83 @@ + + + + + + +axil_interconnect_2k8 +one transaction, read XOR write, in flight at a time + + +fabric + +address decode +wr_hit / rd_hit + +registered select +sel_onehot +sel_default / sel_is_write + +forward broadcast +static mask + VALID gate + +return mux +one-hot AND-OR + +error responder +SLVERR, RDATA = DEADBEEF, on the decoded channel only + + +select + +no hit + + +initiator +(FMC / SPI) +target I/F + + +AW / W / AR +B / R + +axil_pipe +pipe_stages = 0 +(pass-through) + +responder 0 +info + + + + + +axil_pipe +pipe_stages = N + +responder i +sequencer + + + + + +axil_pipe +pipe_stages = M + +responder n +eSPI + + + + +the pipe is where the long trip to a distant +responder is broken + + +decode +spans are powers of two and bases are span-aligned, so range membership is an equality compare on the address bits above the span +select +registered one-hot, so the return path is a flat AND-OR tree rather than an integer-to-one-hot decode buried in the mux +pipe +per responder, set by pipe_stages in the config record; 0 generates a plain pass-through and costs nothing + diff --git a/hdl/ip/vhd/axi_blocks/docs/axil_pipe.drawio.svg b/hdl/ip/vhd/axi_blocks/docs/axil_pipe.drawio.svg new file mode 100644 index 00000000..11a20e41 --- /dev/null +++ b/hdl/ip/vhd/axi_blocks/docs/axil_pipe.drawio.svg @@ -0,0 +1,83 @@ + + + + + + +axil_pipe (shown with stages = 3) +one request bundle out, one response bundle back, each walked down a chain of registers by a 1-bit token + +sink FSM +(fabric side) + +armed +wait_rsp +respond +request_gap + +source FSM +(responder side) + +idle +issue +AWREADY and WREADY +asserted together, as a +registered one-shot +all responder handshakes +sampled in one state +forward: request + +fwd_tok(0) + +req_sr(0) + +fwd_tok(1) + +req_sr(1) + + + +fwd_tok(2) + +req_sr(2) + + + + + + +token arrives +return: response + +rsp_sr(2) + +ret_tok(2) + +rsp_sr(1) + +ret_tok(1) + + + +rsp_sr(0) + +ret_tok(0) + + + + + + + +req_sr(j) advances only when fwd_tok(j-1) is set +so every stage holds its payload until the next transaction pushes through it, which is what keeps +the far end stable across a multi-cycle handshake and removes the need for a capture register at either end +gating the whole chain on the OR of all the tokens instead would clobber the last stage +on the cycle the token reached it + +request bundle +is_write & addr(addr_width) & wdata(32) & wstrb(4) + +response bundle +rdata(32) & resp(2) + From cd289648ff84afec3016fe1ef8a7dbcbda115651 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 12 Aug 2026 13:13:06 -0400 Subject: [PATCH 08/10] axi_blocks: drop --! doc comment markers in the interconnect sources Plain -- everywhere in this block, matching axil_common_pkg. Comment text only, no functional change. --- hdl/ip/vhd/axi_blocks/axil_pipe.vhd | 8 ++++---- hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd | 6 +++--- hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd | 6 +++--- hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd | 2 +- hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/hdl/ip/vhd/axi_blocks/axil_pipe.vhd b/hdl/ip/vhd/axi_blocks/axil_pipe.vhd index 96bd9a95..d633dac0 100644 --- a/hdl/ip/vhd/axi_blocks/axil_pipe.vhd +++ b/hdl/ip/vhd/axi_blocks/axil_pipe.vhd @@ -37,9 +37,9 @@ use work.axil_common_pkg.all; entity axil_pipe is generic ( - --! Register stages inserted in *each* direction + -- Register stages inserted in *each* direction stages : natural; - --! Address bits this responder actually decodes + -- Address bits this responder actually decodes addr_width : natural ); port ( @@ -134,8 +134,8 @@ begin signal sink_state : sink_state_t; signal sink_is_write : std_logic; - --! registered one-shot, so AW and W always handshake in the same cycle - --! and AWREADY never depends combinationally on AWVALID + -- registered one-shot, so AW and W always handshake in the same cycle + -- and AWREADY never depends combinationally on AWVALID signal sink_wr_ack : std_logic; signal sink_rd_ack : std_logic; signal sink_resp_valid : std_logic; diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd index 5c91dc9e..4558352f 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd @@ -38,13 +38,13 @@ package axil_interconnect_sim_pkg is SLOW_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8, pipe_stages => 3), WIDE_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15, pipe_stages => 2)); - --! An integer as a bus address + -- An integer as a bus address function ba (constant addr : integer) return std_logic_vector; - --! Base address of a responder, plus a byte offset, as a bus address + -- Base address of a responder, plus a byte offset, as a bus address function ba (constant idx : integer; constant offset : integer) return std_logic_vector; - --! An integer as a 32 bit data word + -- An integer as a 32 bit data word function w32 (constant value : integer) return std_logic_vector; end package; diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd index 4abb6e5d..4aebe2bb 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd @@ -158,9 +158,9 @@ begin wait until rising_edge(clk); end procedure; - --! Like manual_read, but also reports how many clocks the fabric took to - --! answer, so the testbench can show the configured stages are really in - --! the path rather than being generated away. + -- Like manual_read, but also reports how many clocks the fabric took to + -- answer, so the testbench can show the configured stages are really in + -- the path rather than being generated away. procedure manual_read_timed ( constant addr : in std_logic_vector; variable data : out std_logic_vector(31 downto 0); diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd index b3af2941..44d384d7 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd @@ -23,7 +23,7 @@ architecture th of axil_interconnect_th is signal clk : std_logic := '0'; signal reset : std_logic; signal reset_por : std_logic := '1'; - --! testbench driven, so a reset can be injected mid transaction + -- testbench driven, so a reset can be injected mid transaction signal reset_force : std_logic := '0'; -- The bus functional model drives these diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd index 8ed47073..a3b0c32d 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd @@ -21,7 +21,7 @@ use work.axil_common_pkg.all; entity axil_slow_responder is generic ( addr_width : integer := 8; - --! LFSR seed, so multiple instances can stall differently + -- LFSR seed, so multiple instances can stall differently seed : std_logic_vector(7 downto 0) := x"A5" ); port ( From f3528695ff7182d3eae4e584c9d5195a5e44f302 Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 12 Aug 2026 16:23:59 -0400 Subject: [PATCH 09/10] axil_interconnect: give the testbench helpers descriptive names ba/w32 were too terse to read at the call sites. responder_addr also drops the overload with bus_addr, since the two do quite different things. ba(idx, offset) -> responder_addr(idx, offset) 34 uses ba(addr) -> bus_addr(addr) 8 uses w32(value) -> data_word(value) 10 uses Kept rather than inlined as to_std_logic_vector(). responder_addr does a config array lookup, so it is not a conversion at all. The other two are thin wrappers, but inlining them would push ten lines past the 120 column limit (worst case 133), and bus_addr keeps the initiator width in one place. Also wrapped three lines that were already over the limit. --- .../sims/axil_interconnect_sim_pkg.vhd | 19 +-- .../axi_blocks/sims/axil_interconnect_tb.vhd | 123 +++++++++--------- 2 files changed, 73 insertions(+), 69 deletions(-) diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd index 4558352f..dd751a65 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd @@ -38,31 +38,32 @@ package axil_interconnect_sim_pkg is SLOW_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8, pipe_stages => 3), WIDE_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15, pipe_stages => 2)); - -- An integer as a bus address - function ba (constant addr : integer) return std_logic_vector; + -- An integer as an initiator-width bus address + function bus_addr (constant addr : integer) return std_logic_vector; - -- Base address of a responder, plus a byte offset, as a bus address - function ba (constant idx : integer; constant offset : integer) return std_logic_vector; + -- Bus address of a register in a responder: its configured base address + -- plus a byte offset + function responder_addr (constant idx : integer; constant offset : integer) return std_logic_vector; - -- An integer as a 32 bit data word - function w32 (constant value : integer) return std_logic_vector; + -- An integer as a 32 bit AXI data word + function data_word (constant value : integer) return std_logic_vector; end package; package body axil_interconnect_sim_pkg is - function ba (constant addr : integer) return std_logic_vector is + function bus_addr (constant addr : integer) return std_logic_vector is begin return std_logic_vector(to_unsigned(addr, INITIATOR_ADDR_WIDTH)); end function; - function ba (constant idx : integer; constant offset : integer) return std_logic_vector is + function responder_addr (constant idx : integer; constant offset : integer) return std_logic_vector is begin return std_logic_vector(unsigned(config_array(idx).base_addr(INITIATOR_ADDR_WIDTH - 1 downto 0)) + to_unsigned(offset, INITIATOR_ADDR_WIDTH)); end function; - function w32 (constant value : integer) return std_logic_vector is + function data_word (constant value : integer) return std_logic_vector is begin return std_logic_vector(to_unsigned(value, 32)); end function; diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd index 4aebe2bb..0d80c750 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_tb.vhd @@ -85,8 +85,10 @@ begin check_equal(init_aw_hs, init_b_hs, "write address and write response handshakes disagree"); check_equal(init_ar_hs, init_r_hs, "read address and read data handshakes disagree"); if mapped_only then - check_equal(resp_aw_hs, init_aw_hs, "responder saw a different number of writes than the initiator issued"); - check_equal(resp_ar_hs, init_ar_hs, "responder saw a different number of reads than the initiator issued"); + check_equal(resp_aw_hs, init_aw_hs, + "responder saw a different number of writes than the initiator issued"); + check_equal(resp_ar_hs, init_ar_hs, + "responder saw a different number of reads than the initiator issued"); end if; end procedure; @@ -196,16 +198,16 @@ begin while test_suite loop if run("write_read_each_responder") then for idx in config_array'range loop - write_axi_lite(net, bus_handle, ba(idx, 16#00#), x"C0DE0000" or w32(idx * 16)); - write_axi_lite(net, bus_handle, ba(idx, 16#08#), x"FEED0000" or w32(idx * 16)); + write_axi_lite(net, bus_handle, responder_addr(idx, 16#00#), x"C0DE0000" or data_word(idx * 16)); + write_axi_lite(net, bus_handle, responder_addr(idx, 16#08#), x"FEED0000" or data_word(idx * 16)); end loop; -- read back after all the writes, so a fabric that leaks a write -- into the wrong responder is caught rather than masked for idx in config_array'range loop - check_axi_lite(net, bus_handle, ba(idx, 16#00#), axi_resp_okay, - x"C0DE0000" or w32(idx * 16), "responder 0x00 readback"); - check_axi_lite(net, bus_handle, ba(idx, 16#08#), axi_resp_okay, - x"FEED0000" or w32(idx * 16), "responder 0x08 readback"); + check_axi_lite(net, bus_handle, responder_addr(idx, 16#00#), axi_resp_okay, + x"C0DE0000" or data_word(idx * 16), "responder 0x00 readback"); + check_axi_lite(net, bus_handle, responder_addr(idx, 16#08#), axi_resp_okay, + x"FEED0000" or data_word(idx * 16), "responder 0x08 readback"); end loop; check_handshake_accounting(net, mapped_only => true); @@ -214,12 +216,12 @@ begin -- payload reuse is covered at each configured pipe depth for idx in config_array'range loop for word in 0 to 7 loop - write_axi_lite(net, bus_handle, ba(idx, 4 * word), - x"A5A50000" or w32(16 * idx + word)); + write_axi_lite(net, bus_handle, responder_addr(idx, 4 * word), + x"A5A50000" or data_word(16 * idx + word)); end loop; for word in 0 to 7 loop - check_axi_lite(net, bus_handle, ba(idx, 4 * word), axi_resp_okay, - x"A5A50000" or w32(16 * idx + word), "back to back readback"); + check_axi_lite(net, bus_handle, responder_addr(idx, 4 * word), axi_resp_okay, + x"A5A50000" or data_word(16 * idx + word), "back to back readback"); end loop; end loop; check_handshake_accounting(net, mapped_only => true); @@ -228,24 +230,25 @@ begin -- alternate between the fastest and the slowest responder with no -- idle time in between for word in 0 to 7 loop - write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 4 * word), x"11110000" or w32(word)); - write_axi_lite(net, bus_handle, ba(SLOW_IDX, 4 * word), x"22220000" or w32(word)); + write_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 4 * word), + x"11110000" or data_word(word)); + write_axi_lite(net, bus_handle, responder_addr(SLOW_IDX, 4 * word), x"22220000" or data_word(word)); end loop; for word in 0 to 7 loop - check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 4 * word), axi_resp_okay, - x"11110000" or w32(word), "sram_a readback"); - check_axi_lite(net, bus_handle, ba(SLOW_IDX, 4 * word), axi_resp_okay, - x"22220000" or w32(word), "slow readback"); + check_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 4 * word), axi_resp_okay, + x"11110000" or data_word(word), "sram_a readback"); + check_axi_lite(net, bus_handle, responder_addr(SLOW_IDX, 4 * word), axi_resp_okay, + x"22220000" or data_word(word), "slow readback"); end loop; check_handshake_accounting(net, mapped_only => true); elsif run("read_after_write_same_addr") then for idx in config_array'range loop - write_axi_lite(net, bus_handle, ba(idx, 16#10#), x"5A5A1234"); - check_axi_lite(net, bus_handle, ba(idx, 16#10#), axi_resp_okay, x"5A5A1234", + write_axi_lite(net, bus_handle, responder_addr(idx, 16#10#), x"5A5A1234"); + check_axi_lite(net, bus_handle, responder_addr(idx, 16#10#), axi_resp_okay, x"5A5A1234", "read immediately after write"); - write_axi_lite(net, bus_handle, ba(idx, 16#10#), x"A5A54321"); - check_axi_lite(net, bus_handle, ba(idx, 16#10#), axi_resp_okay, x"A5A54321", + write_axi_lite(net, bus_handle, responder_addr(idx, 16#10#), x"A5A54321"); + check_axi_lite(net, bus_handle, responder_addr(idx, 16#10#), axi_resp_okay, x"A5A54321", "read immediately after overwrite"); end loop; check_handshake_accounting(net, mapped_only => true); @@ -253,60 +256,60 @@ begin elsif run("unmapped_slverr") then -- the gap between the 8 bit responders and the wide one, the top -- of that gap, and an address past every responder - write_axi_lite(net, bus_handle, ba(16#000300#), x"DEADDEAD", axi_resp_slverr); - check_axi_lite(net, bus_handle, ba(16#000300#), axi_resp_slverr, x"DEADBEEF", + write_axi_lite(net, bus_handle, bus_addr(16#000300#), x"DEADDEAD", axi_resp_slverr); + check_axi_lite(net, bus_handle, bus_addr(16#000300#), axi_resp_slverr, x"DEADBEEF", "unmapped read at 0x300"); - write_axi_lite(net, bus_handle, ba(16#007FFC#), x"DEADDEAD", axi_resp_slverr); - check_axi_lite(net, bus_handle, ba(16#007FFC#), axi_resp_slverr, x"DEADBEEF", + write_axi_lite(net, bus_handle, bus_addr(16#007FFC#), x"DEADDEAD", axi_resp_slverr); + check_axi_lite(net, bus_handle, bus_addr(16#007FFC#), axi_resp_slverr, x"DEADBEEF", "unmapped read at 0x7FFC"); - write_axi_lite(net, bus_handle, ba(16#010000#), x"DEADDEAD", axi_resp_slverr); - check_axi_lite(net, bus_handle, ba(16#010000#), axi_resp_slverr, x"DEADBEEF", + write_axi_lite(net, bus_handle, bus_addr(16#010000#), x"DEADDEAD", axi_resp_slverr); + check_axi_lite(net, bus_handle, bus_addr(16#010000#), axi_resp_slverr, x"DEADBEEF", "unmapped read at 0x10000"); -- an unmapped access must not have disturbed a mapped one - write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#04#), x"600D600D"); - check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#04#), axi_resp_okay, x"600D600D", + write_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#04#), x"600D600D"); + check_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#04#), axi_resp_okay, x"600D600D", "mapped access after unmapped"); check_handshake_accounting(net, mapped_only => false); elsif run("boundary_addresses") then -- first and last word of each mapped region, which is where an -- equality based decode and a magnitude compare could disagree - write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), x"00000001"); - write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#FC#), x"000000FC"); - write_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#00#), x"00000100"); - write_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#FC#), x"000001FC"); - write_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#0000#), x"00008000"); - write_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#0FFC#), x"00008FFC"); + write_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#00#), x"00000001"); + write_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#FC#), x"000000FC"); + write_axi_lite(net, bus_handle, responder_addr(SRAM_B_IDX, 16#00#), x"00000100"); + write_axi_lite(net, bus_handle, responder_addr(SRAM_B_IDX, 16#FC#), x"000001FC"); + write_axi_lite(net, bus_handle, responder_addr(WIDE_IDX, 16#0000#), x"00008000"); + write_axi_lite(net, bus_handle, responder_addr(WIDE_IDX, 16#0FFC#), x"00008FFC"); -- 0x00 and 0xFC land in different words of sram_a, and sram_b's -- 0x100 must not have aliased on top of sram_a's 0x00 - check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), axi_resp_okay, x"00000001", + check_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#00#), axi_resp_okay, x"00000001", "sram_a low boundary"); - check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#FC#), axi_resp_okay, x"000000FC", + check_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#FC#), axi_resp_okay, x"000000FC", "sram_a high boundary"); - check_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#00#), axi_resp_okay, x"00000100", + check_axi_lite(net, bus_handle, responder_addr(SRAM_B_IDX, 16#00#), axi_resp_okay, x"00000100", "sram_b low boundary"); - check_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#FC#), axi_resp_okay, x"000001FC", + check_axi_lite(net, bus_handle, responder_addr(SRAM_B_IDX, 16#FC#), axi_resp_okay, x"000001FC", "sram_b high boundary"); - check_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#0000#), axi_resp_okay, x"00008000", + check_axi_lite(net, bus_handle, responder_addr(WIDE_IDX, 16#0000#), axi_resp_okay, x"00008000", "wide low boundary"); - check_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#0FFC#), axi_resp_okay, x"00008FFC", + check_axi_lite(net, bus_handle, responder_addr(WIDE_IDX, 16#0FFC#), axi_resp_okay, x"00008FFC", "wide high boundary"); check_handshake_accounting(net, mapped_only => true); elsif run("glitchy_aw_initiator") then - manual_write(ba(SRAM_A_IDX, 16#10#), x"6060BEEF", 6, OKAY); + manual_write(responder_addr(SRAM_A_IDX, 16#10#), x"6060BEEF", 6, OKAY); clear_manual; - check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#10#), axi_resp_okay, x"6060BEEF", + check_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#10#), axi_resp_okay, x"6060BEEF", "readback after an AW leading write"); check_handshake_accounting(net, mapped_only => true); elsif run("glitchy_aw_unmapped") then -- the catch-all responder is the one that used to assert AWREADY -- with no regard for WVALID - manual_write(ba(16#000300#), x"6060BEEF", 6, SLVERR); + manual_write(bus_addr(16#000300#), x"6060BEEF", 6, SLVERR); clear_manual; - check_axi_lite(net, bus_handle, ba(16#000300#), axi_resp_slverr, x"DEADBEEF", + check_axi_lite(net, bus_handle, bus_addr(16#000300#), axi_resp_slverr, x"DEADBEEF", "unmapped read after an AW leading write"); check_handshake_accounting(net, mapped_only => false); @@ -315,11 +318,11 @@ begin -- be able to shadow. write_axi_lite only queues the write, so -- wait for the bus functional model to actually retire it -- before taking the bus over by hand. - write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#04#), x"600DF00D"); + write_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#04#), x"600DF00D"); wait_until_idle(net, bus_handle); man_mode <= '1'; - man_awaddr <= ba(SRAM_B_IDX, 16#00#); + man_awaddr <= responder_addr(SRAM_B_IDX, 16#00#); man_wdata <= x"11112222"; man_wstrb <= "1111"; man_bready <= '1'; @@ -334,7 +337,7 @@ begin -- tear the transaction down anyway and decode the next one. man_wvalid <= '0'; man_bready <= '0'; - manual_read(ba(SRAM_A_IDX, 16#04#), rdata, OKAY); + manual_read(responder_addr(SRAM_A_IDX, 16#04#), rdata, OKAY); check_equal(rdata, std_logic_vector'(x"600DF00D"), "read decoded against a stale write address"); clear_manual; @@ -343,16 +346,16 @@ begin elsif run("pipe_latency") then -- sram_a has no pipe, sram_b has one stage and the wide responder -- two, so the answer must arrive strictly later each time - write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), x"00000011"); - write_axi_lite(net, bus_handle, ba(SRAM_B_IDX, 16#00#), x"00000022"); - write_axi_lite(net, bus_handle, ba(WIDE_IDX, 16#00#), x"00000033"); + write_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#00#), x"00000011"); + write_axi_lite(net, bus_handle, responder_addr(SRAM_B_IDX, 16#00#), x"00000022"); + write_axi_lite(net, bus_handle, responder_addr(WIDE_IDX, 16#00#), x"00000033"); wait_until_idle(net, bus_handle); - manual_read_timed(ba(SRAM_A_IDX, 16#00#), rdata, lat0); + manual_read_timed(responder_addr(SRAM_A_IDX, 16#00#), rdata, lat0); check_equal(rdata, std_logic_vector'(x"00000011"), "unpiped readback"); - manual_read_timed(ba(SRAM_B_IDX, 16#00#), rdata, lat1); + manual_read_timed(responder_addr(SRAM_B_IDX, 16#00#), rdata, lat1); check_equal(rdata, std_logic_vector'(x"00000022"), "one stage readback"); - manual_read_timed(ba(WIDE_IDX, 16#00#), rdata, lat2); + manual_read_timed(responder_addr(WIDE_IDX, 16#00#), rdata, lat2); check_equal(rdata, std_logic_vector'(x"00000033"), "two stage readback"); info("read latency in clocks: 0 stages=" & to_string(lat0) & " 1 stage=" & to_string(lat1) & " 2 stages=" & to_string(lat2)); @@ -365,7 +368,7 @@ begin -- kick off a read at the slow responder and reset while it is in -- flight, then confirm the fabric comes back clean man_mode <= '1'; - man_araddr <= ba(SLOW_IDX, 16#00#); + man_araddr <= responder_addr(SLOW_IDX, 16#00#); man_rready <= '1'; man_arvalid <= '1'; wait for 40 ns; @@ -380,11 +383,11 @@ begin man_mode <= '0'; wait for 200 ns; - write_axi_lite(net, bus_handle, ba(SLOW_IDX, 16#00#), x"AF7E8E5E"); - check_axi_lite(net, bus_handle, ba(SLOW_IDX, 16#00#), axi_resp_okay, x"AF7E8E5E", + write_axi_lite(net, bus_handle, responder_addr(SLOW_IDX, 16#00#), x"AF7E8E5E"); + check_axi_lite(net, bus_handle, responder_addr(SLOW_IDX, 16#00#), axi_resp_okay, x"AF7E8E5E", "slow responder after a mid transaction reset"); - write_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), x"C1EA4000"); - check_axi_lite(net, bus_handle, ba(SRAM_A_IDX, 16#00#), axi_resp_okay, x"C1EA4000", + write_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#00#), x"C1EA4000"); + check_axi_lite(net, bus_handle, responder_addr(SRAM_A_IDX, 16#00#), axi_resp_okay, x"C1EA4000", "sram_a after a mid transaction reset"); check_handshake_accounting(net, mapped_only => true); end if; From 7a18476fdec0a9d1e259585cd9ebc0b017b0661d Mon Sep 17 00:00:00 2001 From: Nathanael Huffman Date: Wed, 12 Aug 2026 16:14:45 -0400 Subject: [PATCH 10/10] Implement review fixes Make the slow responder's longest stall a generic and drive it, along with the deepest pipe_stages in the responder map, from a single MAX_DELAY constant in axil_interconnect_sim_pkg. Previously the responder hard-coded 3 internally while the map separately said pipe_stages => 3, so the two could drift apart silently. The LFSR stall fields are only two bits, so delay_of now clamps to max_delay and the reset values go through the same clamp, which keeps the counters in range for a max_delay below 3. Verified the suite passes with MAX_DELAY at 1, 3 and 5. --- .../sims/axil_interconnect_sim_pkg.vhd | 7 ++++- .../axi_blocks/sims/axil_interconnect_th.vhd | 1 + .../axi_blocks/sims/axil_slow_responder.vhd | 28 ++++++++++++------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd index dd751a65..3ecce019 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_sim_pkg.vhd @@ -32,10 +32,15 @@ package axil_interconnect_sim_pkg is constant SLOW_IDX : integer := 2; constant WIDE_IDX : integer := 3; + -- Drives both the deepest pipe in the map below and the slow responder's worst + -- case stall, so one number sets how much delay the testbench has to tolerate + -- on either side of the fabric. + constant MAX_DELAY : integer := 3; + constant config_array : axil_responder_cfg_array_t(0 to 3) := (SRAM_A_IDX => resp_cfg(base_addr => x"00000000", addr_span_bits => 8, pipe_stages => 0), SRAM_B_IDX => resp_cfg(base_addr => x"00000100", addr_span_bits => 8, pipe_stages => 1), - SLOW_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8, pipe_stages => 3), + SLOW_IDX => resp_cfg(base_addr => x"00000200", addr_span_bits => 8, pipe_stages => MAX_DELAY), WIDE_IDX => resp_cfg(base_addr => x"00008000", addr_span_bits => 15, pipe_stages => 2)); -- An integer as an initiator-width bus address diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd index 44d384d7..379e86d2 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_interconnect_th.vhd @@ -239,6 +239,7 @@ begin slow: entity work.axil_slow_responder generic map ( addr_width => 8, + max_delay => MAX_DELAY, seed => x"5A" ) port map ( diff --git a/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd b/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd index a3b0c32d..7542b9cb 100644 --- a/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd +++ b/hdl/ip/vhd/axi_blocks/sims/axil_slow_responder.vhd @@ -21,6 +21,8 @@ use work.axil_common_pkg.all; entity axil_slow_responder is generic ( addr_width : integer := 8; + -- Longest stall this model will insert on any channel + max_delay : integer := 3; -- LFSR seed, so multiple instances can stall differently seed : std_logic_vector(7 downto 0) := x"A5" ); @@ -55,7 +57,6 @@ end entity; architecture rtl of axil_slow_responder is constant NUM_WORDS : integer := 16; - constant MAX_DELAY : integer := 3; type storage_t is array (0 to NUM_WORDS - 1) of std_logic_vector(31 downto 0); @@ -66,20 +67,27 @@ architecture rtl of axil_slow_responder is signal w_done : std_logic; signal ar_done : std_logic; - signal aw_cnt : integer range 0 to MAX_DELAY; - signal w_cnt : integer range 0 to MAX_DELAY; - signal b_cnt : integer range 0 to MAX_DELAY; - signal ar_cnt : integer range 0 to MAX_DELAY; - signal r_cnt : integer range 0 to MAX_DELAY; + signal aw_cnt : integer range 0 to max_delay; + signal w_cnt : integer range 0 to max_delay; + signal b_cnt : integer range 0 to max_delay; + signal ar_cnt : integer range 0 to max_delay; + signal r_cnt : integer range 0 to max_delay; signal awaddr_reg : std_logic_vector(addr_width - 1 downto 0); signal wdata_reg : std_logic_vector(31 downto 0); signal wstrb_reg : std_logic_vector(3 downto 0); signal rdata_reg : std_logic_vector(31 downto 0); + -- The LFSR fields are two bits, so a caller asking for a shorter longest + -- stall than 3 needs the value clamped to stay inside the counter ranges. function delay_of (constant bits : std_logic_vector(1 downto 0)) return integer is + variable value : integer; begin - return to_integer(unsigned(bits)); + value := to_integer(unsigned(bits)); + if value > max_delay then + value := max_delay; + end if; + return value; end function; begin @@ -111,10 +119,10 @@ begin w_done <= '0'; ar_done <= '0'; aw_cnt <= 0; - w_cnt <= 1; - b_cnt <= 1; + w_cnt <= delay_of("01"); + b_cnt <= delay_of("01"); ar_cnt <= 0; - r_cnt <= 1; + r_cnt <= delay_of("01"); elsif rising_edge(clk) then -- readys are single cycle pulses awready <= '0';