diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 146819335..650cc3127 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -119,6 +119,7 @@ run_vsim: - axi_modify_address - axi_serializer - axi_sim_mem + - axi_to_apb # Modules that pull in shared submodules keep explicit additional dependencies # to avoid missing a rerun when a shared submodule changes. diff --git a/Bender.yml b/Bender.yml index 7105eca20..f85557e3c 100644 --- a/Bender.yml +++ b/Bender.yml @@ -91,6 +91,7 @@ sources: - src/axi_interleaved_xbar.sv - src/axi_iw_converter.sv - src/axi_lite_xbar.sv + - src/axi_to_apb.sv - src/axi_xbar_unmuxed.sv - src/axi_to_mem_banked.sv - src/axi_to_mem_interleaved.sv @@ -131,6 +132,7 @@ sources: - test/tb_axi_lite_regs.sv - test/tb_axi_iw_converter.sv - test/tb_axi_lite_to_apb.sv + - test/tb_axi_to_apb.sv - test/tb_axi_lite_to_axi.sv - test/tb_axi_lite_xbar.sv - test/tb_axi_modify_address.sv diff --git a/CHANGELOG.md b/CHANGELOG.md index c47ae6a05..d95ea9abf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +### Added +- `axi_to_apb`: AXI4+ATOP to APB4 bridge supporting data-width downsizing and address truncation, + built on `axi_to_detailed_mem`. Adds the `axi_to_apb_intf` interface variant and the + `tb_axi_to_apb` testbench. + ## 0.39.10 - 2026-06-19 ### Added diff --git a/Makefile b/Makefile index 98c1d3e00..b67e1c557 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ TBS ?= axi_addr_test \ axi_modify_address \ axi_serializer \ axi_sim_mem \ + axi_to_apb \ axi_to_axi_lite \ axi_to_mem_banked \ axi_xbar diff --git a/README.md b/README.md index 8408d861f..366bdb394 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ In addition to the documents linked in the following table, we are setting up [d | [`axi_serializer`](src/axi_serializer.sv) | Serializes transactions with different IDs to the same ID. | | | [`axi_throttle`](src/axi_throttle.sv) | Limits the maximum number of outstanding transfers sent to the downstream logic. | | | [`axi_test`](src/axi_test.sv) | A set of testbench utilities for AXI interfaces. | | +| [`axi_to_apb`](src/axi_to_apb.sv) | AXI4 to APB4 protocol converter with data-width downsizing and optional address decoding. | | | [`axi_to_axi_lite`](src/axi_to_axi_lite.sv) | AXI4 to AXI4-Lite protocol converter. | | | [`axi_to_mem`](src/axi_to_mem.sv) | AXI4 to memory protocol (req, gnt, rvalid) converter. Additional banked, interleaved, split variant. | | | [`axi_xbar`](src/axi_xbar.sv) | Fully-connected AXI4+ATOP crossbar with an arbitrary number of slave and master ports. | [Doc](doc/axi_xbar.md) | diff --git a/scripts/run_vsim.sh b/scripts/run_vsim.sh index aaf8c3f22..b95d99836 100755 --- a/scripts/run_vsim.sh +++ b/scripts/run_vsim.sh @@ -241,6 +241,12 @@ exec_test() { done done ;; + axi_to_apb) + for DW in 32 64; do + call_vsim tb_axi_to_apb -gTbAxiDataWidth=$DW \ + -t 1ns -coverage -voptargs="+acc +cover=bcesfx" + done + ;; *) call_vsim tb_$1 -t 1ns -coverage -voptargs="+acc +cover=bcesfx" ;; diff --git a/src/axi_to_apb.sv b/src/axi_to_apb.sv new file mode 100644 index 000000000..fb42b2fe1 --- /dev/null +++ b/src/axi_to_apb.sv @@ -0,0 +1,421 @@ +// Copyright (c) 2024 ETH Zurich and University of Bologna. +// Copyright and related rights are licensed under the Solderpad Hardware +// License, Version 0.51 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at +// http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law +// or agreed to in writing, software, hardware and materials distributed under +// this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. +// +// Authors: +// - Tim Fischer + +// Description: AXI4 to APB4 bridge +// +// Bridges one AXI4 slave port onto one or more APB4 slaves, supporting data-width downsizing +// (`AxiDataWidth` -> `ApbDataWidth`) and address truncation (`AxiAddrWidth` -> `ApbAddrWidth`). It +// composes an `axi_to_detailed_mem` front-end with an inline APB master. +// +// Behaviour notes: +// - ATOPs are not supported (by this module and also APB), and are filtered out with an `axi_atop_filter`. +// - Only `INCR` bursts are supported (`axi_to_detailed_mem` restriction). +// - A wide access is serialized into one APB transfer per active bank, selected via `lzc`. +// Zero-strobe write lanes are dropped by `axi_to_detailed_mem` (`HideStrb`) and never reach the +// bus. For reads, *all* banks are read (even lanes outside a narrow beat's byte range), so avoid +// mapping read-sensitive registers into unaddressed lanes. +// - Address-decode misses are reported as `RESP_SLVERR` (not `RESP_DECERR`; `axi_to_detailed_mem` +// has no decode-error path) and exclusive accesses complete as `RESP_OKAY` (no `EXOKAY`). + +`include "axi/typedef.svh" +`include "axi/assign.svh" +`include "common_cells/registers.svh" + +module axi_to_apb #( + /// Number of connected APB slaves. + parameter int unsigned NoApbSlaves = 32'd1, + /// Number of APB address-decode rules. Set to `0` to omit the address decoder entirely. + ///`NoRules == `0` is only legal with `NoApbSlaves == 1`. + parameter int unsigned NoRules = 32'd1, + /// AXI4 address width. Decoding uses the full width; `paddr` is truncated to `ApbAddrWidth`. + parameter int unsigned AxiAddrWidth = 32'd0, + /// AXI4 data width. Must be an integer power-of-two multiple of `ApbDataWidth`. + parameter int unsigned AxiDataWidth = 32'd0, + /// AXI4 ID width. + parameter int unsigned AxiIdWidth = 32'd0, + /// AXI4 user width. + parameter int unsigned AxiUserWidth = 32'd0, + /// Maximum number of outstanding writes tracked by the `axi_atop_filter`. + parameter int unsigned AxiMaxWriteTxns = 32'd1, + /// Response buffer depth of the internal `axi_to_detailed_mem`. + parameter int unsigned BufDepth = 32'd1, + /// Output FIFO depth of the internal `axi_to_detailed_mem`. + parameter int unsigned OutFifoDepth = 32'd1, + /// APB4 address width. Must be `<= AxiAddrWidth`; the upper AXI address bits are discarded. + parameter int unsigned ApbAddrWidth = 32'd0, + /// APB4 data width. Must be a multiple of 8 and `<= 32` (`ApbDataWidth == 8` is supported). + parameter int unsigned ApbDataWidth = 32'd0, + /// AXI4+ATOP request struct, see `axi/typedef.svh`. + parameter type axi_req_t = logic, + /// AXI4+ATOP response struct, see `axi/typedef.svh`. + parameter type axi_resp_t = logic, + /// APB4 request struct. + parameter type apb_req_t = logic, + /// APB4 response struct. + parameter type apb_resp_t = logic, + /// Address-decode rule struct from `common_cells` (`addr_decode`). + parameter type rule_t = logic +) ( + input logic clk_i, // Clock + input logic rst_ni, // Asynchronous reset active low + // AXI4+ATOP slave port + input axi_req_t axi_req_i, + output axi_resp_t axi_resp_o, + // APB master port + output apb_req_t [NoApbSlaves-1:0] apb_req_o, + input apb_resp_t [NoApbSlaves-1:0] apb_resp_i, + // APB slave address map (unused when `NoRules == 0`) + input rule_t [((NoRules > 0) ? NoRules : 1)-1:0] addr_map_i +); + + // Build the address decoder only when rules are provided; otherwise forward everything to slave 0. + localparam bit UseAddrDecode = (NoRules > 32'd0); + + localparam int unsigned NumBanks = AxiDataWidth / ApbDataWidth; + localparam int unsigned SelIdxWidth = (NoApbSlaves > 32'd1) ? $clog2(NoApbSlaves) : 32'd1; + localparam int unsigned BankIdxWidth = (NumBanks > 32'd1) ? $clog2(NumBanks) : 32'd1; + + typedef logic [AxiAddrWidth-1:0] axi_addr_t; + typedef logic [ApbAddrWidth-1:0] apb_addr_t; + typedef logic [ApbDataWidth-1:0] apb_data_t; + typedef logic [ApbDataWidth/8-1:0] apb_strb_t; + typedef logic [SelIdxWidth-1:0] sel_idx_t; + + typedef enum logic { + Setup = 1'b0, // APB idle / setup phase + Access = 1'b1 // APB access phase + } apb_state_e; + + // 1. Reject atomics with a slave error (APB has no atomic support). + axi_req_t filtered_req; + axi_resp_t filtered_resp; + + axi_atop_filter #( + .AxiIdWidth ( AxiIdWidth ), + .AxiMaxWriteTxns ( AxiMaxWriteTxns ), + .axi_req_t ( axi_req_t ), + .axi_resp_t ( axi_resp_t ) + ) i_axi_atop_filter ( + .clk_i, + .rst_ni, + .slv_req_i ( axi_req_i ), + .slv_resp_o ( axi_resp_o ), + .mst_req_o ( filtered_req ), + .mst_resp_i ( filtered_resp ) + ); + + // 2. AXI bursts -> single-word memory stream, with data-width downsize via banking. + logic [NumBanks-1:0] mem_req, mem_gnt; + axi_addr_t [NumBanks-1:0] mem_addr; + apb_data_t [NumBanks-1:0] mem_wdata; + apb_strb_t [NumBanks-1:0] mem_strb; + logic [NumBanks-1:0] mem_we; + axi_pkg::prot_t [NumBanks-1:0] mem_prot; + logic [NumBanks-1:0] mem_rvalid; + apb_data_t [NumBanks-1:0] mem_rdata; + logic [NumBanks-1:0] mem_err; + logic [NumBanks-1:0] mem_exokay; + + axi_to_detailed_mem #( + .axi_req_t ( axi_req_t ), + .axi_resp_t ( axi_resp_t ), + .AddrWidth ( AxiAddrWidth ), + .DataWidth ( AxiDataWidth ), + .IdWidth ( AxiIdWidth ), + .UserWidth ( AxiUserWidth ), + .NumBanks ( NumBanks ), + .BufDepth ( BufDepth ), + .HideStrb ( 1'b1 ), // drop zero-strobe write lanes so they issue no APB access + .OutFifoDepth ( OutFifoDepth ) + ) i_axi_to_detailed_mem ( + .clk_i, + .rst_ni, + .busy_o ( /* unused */ ), + .axi_req_i ( filtered_req ), + .axi_resp_o ( filtered_resp ), + .mem_req_o ( mem_req ), + .mem_gnt_i ( mem_gnt ), + .mem_addr_o ( mem_addr ), + .mem_wdata_o ( mem_wdata ), + .mem_strb_o ( mem_strb ), + .mem_atop_o ( /* unused */ ), + .mem_lock_o ( /* unused */ ), + .mem_we_o ( mem_we ), + .mem_id_o ( /* unused */ ), + .mem_user_o ( /* unused */ ), + .mem_cache_o ( /* unused */ ), + .mem_prot_o ( mem_prot ), + .mem_qos_o ( /* unused */ ), + .mem_region_o ( /* unused */ ), + .mem_rvalid_i ( mem_rvalid ), + .mem_rdata_i ( mem_rdata ), + .mem_err_i ( mem_err ), + .mem_exokay_i ( mem_exokay ) + ); + + // 3. Inline APB master: serialize the active bank requests onto the single APB bus. + apb_state_e apb_state_q, apb_state_d; + logic [BankIdxWidth-1:0] bank_sel; + logic no_req; + + // Select the lowest-index lane that still has a pending request. Lanes with no request - + // including zero-strobe write lanes dropped by `axi_to_detailed_mem` (`HideStrb`) - are skipped + // for free (no wasted cycles). The selected lane's request stays asserted until we grant it, so + // `bank_sel` is stable across the SETUP and ACCESS phases of a transfer. + lzc #( + .WIDTH ( NumBanks ), + .MODE ( 1'b0 ) // count trailing zeros -> lowest set bit + ) i_bank_sel ( + .in_i ( mem_req ), + .cnt_o ( bank_sel ), + .empty_o ( no_req ) + ); + + // Address decode on the selected lane's (full-width) address. With no rules (`NoRules == 0`) the + // decoder is bypassed: the lone APB slave is always selected and every address is a hit. + logic apb_dec_valid; + sel_idx_t apb_sel_idx; + if (UseAddrDecode) begin : gen_addr_decode + addr_decode #( + .NoIndices ( NoApbSlaves ), + .NoRules ( NoRules ), + .addr_t ( axi_addr_t ), + .rule_t ( rule_t ) + ) i_apb_decode ( + .addr_i ( mem_addr[bank_sel] ), + .addr_map_i ( addr_map_i ), + .idx_o ( apb_sel_idx ), + .dec_valid_o ( apb_dec_valid ), + .dec_error_o ( /* unused */ ), + .en_default_idx_i ( 1'b0 ), + .default_idx_i ( '0 ) + ); + end else begin : gen_no_decode + assign apb_sel_idx = '0; // only slave 0 exists + assign apb_dec_valid = 1'b1; // every transaction is a hit + end + + always_comb begin + // Default assignments. + apb_state_d = apb_state_q; + apb_req_o = '0; + mem_gnt = '0; + mem_rvalid = '0; + mem_rdata = '0; + mem_err = '0; + mem_exokay = '0; + + unique case (apb_state_q) + Setup: begin + if (!no_req) begin + if (apb_dec_valid) begin + // SETUP phase: assert psel, keep penable low. + apb_req_o[apb_sel_idx] = '{ + paddr: apb_addr_t'(mem_addr[bank_sel]), + pprot: mem_prot[bank_sel], + psel: 1'b1, + penable: 1'b0, + pwrite: mem_we[bank_sel], + pwdata: mem_wdata[bank_sel], + pstrb: mem_strb[bank_sel] + }; + apb_state_d = Access; + end else begin + // Decode miss: no APB request, answer this lane with a slave error and move on. + mem_gnt[bank_sel] = 1'b1; + mem_rvalid[bank_sel] = 1'b1; + mem_err[bank_sel] = 1'b1; + end + end + end + Access: begin + // ACCESS phase: assert psel and penable until the slave is ready. + apb_req_o[apb_sel_idx] = '{ + paddr: apb_addr_t'(mem_addr[bank_sel]), + pprot: mem_prot[bank_sel], + psel: 1'b1, + penable: 1'b1, + pwrite: mem_we[bank_sel], + pwdata: mem_wdata[bank_sel], + pstrb: mem_strb[bank_sel] + }; + if (apb_resp_i[apb_sel_idx].pready) begin + mem_gnt[bank_sel] = 1'b1; + mem_rvalid[bank_sel] = 1'b1; + mem_rdata[bank_sel] = apb_resp_i[apb_sel_idx].prdata; + mem_err[bank_sel] = apb_resp_i[apb_sel_idx].pslverr; + mem_exokay[bank_sel] = 1'b0; // APB has no exclusive access + apb_state_d = Setup; + end + end + default: /* do nothing */ ; + endcase + end + + `FFARN(apb_state_q, apb_state_d, Setup, clk_i, rst_ni) + + // parameter check + // pragma translate_off + `ifndef VERILATOR + initial begin : check_params + apb_data_le: assert (ApbDataWidth <= AxiDataWidth) else + $fatal(1, $sformatf("ApbDataWidth has to be <= AxiDataWidth")); + apb_addr_le: assert (ApbAddrWidth <= AxiAddrWidth) else + $fatal(1, $sformatf("ApbAddrWidth has to be <= AxiAddrWidth")); + apb_data_div: assert (AxiDataWidth % ApbDataWidth == 0) else + $fatal(1, $sformatf("AxiDataWidth has to be an integer multiple of ApbDataWidth")); + apb_no_decode: assert (UseAddrDecode || NoApbSlaves == 32'd1) else + $fatal(1, $sformatf("NoRules == 0 (no address decoder) is only allowed with a single APB slave")); + end + `endif + // pragma translate_on +endmodule + +module axi_to_apb_intf #( + /// Number of connected APB slaves. + parameter int unsigned NoApbSlaves = 32'd1, + /// Number of APB address-decode rules. Set to `0` to omit the address decoder (see `axi_to_apb`); + /// only legal with `NoApbSlaves == 1`, in which case `addr_map_i` may be left unconnected. + parameter int unsigned NoRules = 32'd1, + /// AXI4 address width. Decoding uses the full width; `paddr` is truncated to `ApbAddrWidth`. + parameter int unsigned AxiAddrWidth = 32'd0, + /// AXI4 data width. Must be an integer power-of-two multiple of `ApbDataWidth`. + parameter int unsigned AxiDataWidth = 32'd0, + /// AXI4 ID width. + parameter int unsigned AxiIdWidth = 32'd0, + /// AXI4 user width. + parameter int unsigned AxiUserWidth = 32'd0, + /// Maximum number of outstanding writes tracked by the `axi_atop_filter`. + parameter int unsigned AxiMaxWriteTxns = 32'd1, + /// Response buffer depth of the internal `axi_to_detailed_mem`. + parameter int unsigned BufDepth = 32'd1, + /// Output FIFO depth of the internal `axi_to_detailed_mem`. + parameter int unsigned OutFifoDepth = 32'd1, + /// APB4 address width. Must be `<= AxiAddrWidth`; the upper AXI address bits are discarded. + parameter int unsigned ApbAddrWidth = 32'd0, + /// APB4 data width. Must be a multiple of 8 and `<= 32` (`ApbDataWidth == 8` is supported). + parameter int unsigned ApbDataWidth = 32'd0, + /// Address-decode rule struct from `common_cells` (`addr_decode`). + parameter type rule_t = logic, + // DEPENDENT PARAMETERS, DO NOT OVERWRITE! + parameter type apb_addr_t = logic [ApbAddrWidth-1:0], + parameter type apb_data_t = logic [ApbDataWidth-1:0], + parameter type apb_strb_t = logic [ApbDataWidth/8-1:0], + parameter type sel_t = logic [NoApbSlaves-1:0] +) ( + input logic clk_i, // Clock + input logic rst_ni, // Asynchronous reset active low + // AXI4+ATOP slave port + AXI_BUS.Slave slv, + // APB master port + output apb_addr_t paddr_o, + output logic [2:0] pprot_o, + output sel_t pselx_o, + output logic penable_o, + output logic pwrite_o, + output apb_data_t pwdata_o, + output apb_strb_t pstrb_o, + input logic [NoApbSlaves-1:0] pready_i, + input apb_data_t [NoApbSlaves-1:0] prdata_i, + input [NoApbSlaves-1:0] pslverr_i, + // APB slave address map (unused when `NoRules == 0`; port width kept at >=1 to stay well-formed) + input rule_t [((NoRules > 0) ? NoRules : 1)-1:0] addr_map_i +); + localparam int unsigned SelIdxWidth = NoApbSlaves > 1 ? $clog2(NoApbSlaves) : 1; + + typedef logic [AxiAddrWidth-1:0] axi_addr_t; + typedef logic [AxiDataWidth-1:0] axi_data_t; + typedef logic [AxiIdWidth-1:0] axi_id_t; + typedef logic [AxiDataWidth/8-1:0] axi_strb_t; + typedef logic [AxiUserWidth-1:0] axi_user_t; + + typedef struct packed { + apb_addr_t paddr; + axi_pkg::prot_t pprot; + logic psel; + logic penable; + logic pwrite; + apb_data_t pwdata; + apb_strb_t pstrb; + } apb_req_t; + + typedef struct packed { + logic pready; + apb_data_t prdata; + logic pslverr; + } apb_resp_t; + + `AXI_TYPEDEF_AW_CHAN_T(aw_chan_t, axi_addr_t, axi_id_t, axi_user_t) + `AXI_TYPEDEF_W_CHAN_T(w_chan_t, axi_data_t, axi_strb_t, axi_user_t) + `AXI_TYPEDEF_B_CHAN_T(b_chan_t, axi_id_t, axi_user_t) + `AXI_TYPEDEF_AR_CHAN_T(ar_chan_t, axi_addr_t, axi_id_t, axi_user_t) + `AXI_TYPEDEF_R_CHAN_T(r_chan_t, axi_data_t, axi_id_t, axi_user_t) + `AXI_TYPEDEF_REQ_T(axi_req_t, aw_chan_t, w_chan_t, ar_chan_t) + `AXI_TYPEDEF_RESP_T(axi_resp_t, b_chan_t, r_chan_t) + + axi_req_t axi_req; + axi_resp_t axi_resp; + apb_req_t [NoApbSlaves-1:0] apb_req; + apb_resp_t [NoApbSlaves-1:0] apb_resp; + logic [SelIdxWidth-1:0] apb_sel; + + `AXI_ASSIGN_TO_REQ(axi_req, slv) + `AXI_ASSIGN_FROM_RESP(slv, axi_resp) + + onehot_to_bin #( + .ONEHOT_WIDTH ( NoApbSlaves ) + ) i_onehot_to_bin ( + .onehot ( pselx_o ), + .bin ( apb_sel ) + ); + + assign paddr_o = apb_req[apb_sel].paddr; + assign pprot_o = apb_req[apb_sel].pprot; + assign penable_o = apb_req[apb_sel].penable; + assign pwrite_o = apb_req[apb_sel].pwrite; + assign pwdata_o = apb_req[apb_sel].pwdata; + assign pstrb_o = apb_req[apb_sel].pstrb; + for (genvar i = 0; i < NoApbSlaves; i++) begin : gen_apb_resp_assign + assign pselx_o[i] = apb_req[i].psel; + assign apb_resp[i].pready = pready_i[i]; + assign apb_resp[i].prdata = prdata_i[i]; + assign apb_resp[i].pslverr = pslverr_i[i]; + end + + axi_to_apb #( + .NoApbSlaves ( NoApbSlaves ), + .NoRules ( NoRules ), + .AxiAddrWidth ( AxiAddrWidth ), + .AxiDataWidth ( AxiDataWidth ), + .AxiIdWidth ( AxiIdWidth ), + .AxiUserWidth ( AxiUserWidth ), + .AxiMaxWriteTxns ( AxiMaxWriteTxns ), + .BufDepth ( BufDepth ), + .OutFifoDepth ( OutFifoDepth ), + .ApbAddrWidth ( ApbAddrWidth ), + .ApbDataWidth ( ApbDataWidth ), + .axi_req_t ( axi_req_t ), + .axi_resp_t ( axi_resp_t ), + .apb_req_t ( apb_req_t ), + .apb_resp_t ( apb_resp_t ), + .rule_t ( rule_t ) + ) i_axi_to_apb ( + .clk_i, + .rst_ni, + .axi_req_i ( axi_req ), + .axi_resp_o ( axi_resp ), + .apb_req_o ( apb_req ), + .apb_resp_i ( apb_resp ), + .addr_map_i + ); +endmodule diff --git a/src_files.yml b/src_files.yml index 31575309e..5ee92a759 100644 --- a/src_files.yml +++ b/src_files.yml @@ -67,6 +67,7 @@ axi: - src/axi_interleaved_xbar.sv - src/axi_iw_converter.sv - src/axi_lite_xbar.sv + - src/axi_to_apb.sv - src/axi_xbar_unmuxed.sv - src/axi_to_mem_banked.sv - src/axi_to_mem_interleaved.sv diff --git a/test/axi_synth_bench.sv b/test/axi_synth_bench.sv index 334846abc..58ee95e0e 100644 --- a/test/axi_synth_bench.sv +++ b/test/axi_synth_bench.sv @@ -90,6 +90,19 @@ module axi_synth_bench ( end end + // AXI4+ATOP to APB bridge (with data-width downsize and address truncation). + // `ApbDataWidth` is swept over {8, 16, 32} (banking-based downsizing supports 8-bit APB). + for (genvar i_data = 0; i_data < 3; i_data++) begin + localparam int unsigned ApbDataWidth = (2**i_data) * 8; + for (genvar i_slv = 0; i_slv < 3; i_slv++) begin + synth_axi_to_apb #( + .NoApbSlaves ( NUM_SLAVE_MASTER[i_slv] ), + .AxiDataWidth ( 32'd64 ), + .ApbDataWidth ( ApbDataWidth ) + ) i_axi_to_apb (.*); + end + end + // AXI4-Lite Mailbox for (genvar i_irq_mode = 0; i_irq_mode < 4; i_irq_mode++) begin localparam bit EDGE_TRIG = i_irq_mode[0]; @@ -360,6 +373,93 @@ module synth_axi_lite_to_apb #( endmodule +module synth_axi_to_apb #( + parameter int unsigned NoApbSlaves = 0, + parameter int unsigned AxiDataWidth = 0, + parameter int unsigned ApbDataWidth = 0 +) ( + input logic clk_i, // Clock + input logic rst_ni // Asynchronous reset active low +); + + localparam int unsigned AxiAddrWidth = 32'd48; + localparam int unsigned ApbAddrWidth = 32'd32; + localparam int unsigned AxiIdWidth = 32'd4; + localparam int unsigned AxiUserWidth = 32'd1; + + typedef logic [ApbAddrWidth-1:0] apb_addr_t; + typedef logic [ApbDataWidth-1:0] apb_data_t; + typedef logic [ApbDataWidth/8-1:0] apb_strb_t; + + typedef struct packed { + apb_addr_t paddr; + axi_pkg::prot_t pprot; + logic psel; + logic penable; + logic pwrite; + apb_data_t pwdata; + apb_strb_t pstrb; + } apb_req_t; + + typedef struct packed { + logic pready; + apb_data_t prdata; + logic pslverr; + } apb_resp_t; + + typedef logic [AxiAddrWidth-1:0] addr_t; + typedef logic [AxiDataWidth-1:0] data_t; + typedef logic [AxiIdWidth-1:0] id_t; + typedef logic [AxiDataWidth/8-1:0] strb_t; + typedef logic [AxiUserWidth-1:0] user_t; + + typedef struct packed { + int unsigned idx; + addr_t start_addr; + addr_t end_addr; + } rule_t; + + `AXI_TYPEDEF_AW_CHAN_T(aw_chan_t, addr_t, id_t, user_t) + `AXI_TYPEDEF_W_CHAN_T(w_chan_t, data_t, strb_t, user_t) + `AXI_TYPEDEF_B_CHAN_T(b_chan_t, id_t, user_t) + `AXI_TYPEDEF_AR_CHAN_T(ar_chan_t, addr_t, id_t, user_t) + `AXI_TYPEDEF_R_CHAN_T(r_chan_t, data_t, id_t, user_t) + `AXI_TYPEDEF_REQ_T(axi_req_t, aw_chan_t, w_chan_t, ar_chan_t) + `AXI_TYPEDEF_RESP_T(axi_resp_t, b_chan_t, r_chan_t) + + axi_req_t axi_req; + axi_resp_t axi_resp; + apb_req_t [NoApbSlaves-1:0] apb_req; + apb_resp_t [NoApbSlaves-1:0] apb_resp; + + rule_t [NoApbSlaves-1:0] addr_map; + + axi_to_apb #( + .NoApbSlaves ( NoApbSlaves ), + .NoRules ( NoApbSlaves ), + .AxiAddrWidth ( AxiAddrWidth ), + .AxiDataWidth ( AxiDataWidth ), + .AxiIdWidth ( AxiIdWidth ), + .AxiUserWidth ( AxiUserWidth ), + .ApbAddrWidth ( ApbAddrWidth ), + .ApbDataWidth ( ApbDataWidth ), + .axi_req_t ( axi_req_t ), + .axi_resp_t ( axi_resp_t ), + .apb_req_t ( apb_req_t ), + .apb_resp_t ( apb_resp_t ), + .rule_t ( rule_t ) + ) i_axi_to_apb_dut ( + .clk_i ( clk_i ), + .rst_ni ( rst_ni ), + .axi_req_i ( axi_req ), + .axi_resp_o ( axi_resp ), + .apb_req_o ( apb_req ), + .apb_resp_i ( apb_resp ), + .addr_map_i ( addr_map ) + ); + +endmodule + module synth_axi_cdc #( parameter int unsigned AXI_ADDR_WIDTH = 0, parameter int unsigned AXI_DATA_WIDTH = 0, diff --git a/test/tb_axi_to_apb.sv b/test/tb_axi_to_apb.sv new file mode 100644 index 000000000..f0f8cd000 --- /dev/null +++ b/test/tb_axi_to_apb.sv @@ -0,0 +1,405 @@ +// Copyright (c) 2024 ETH Zurich and University of Bologna. +// Copyright and related rights are licensed under the Solderpad Hardware +// License, Version 0.51 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at +// http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law +// or agreed to in writing, software, hardware and materials distributed under +// this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. +// +// Authors: +// - Tim Fischer + +// Description: Testbench for `axi_to_apb`. +// +// A full-AXI random master drives the DUT, whose APB master ports are attached to four behavioural +// APB slaves with different timing: an ideal (0-wait), a slow (fixed-wait), a random-wait, and an +// error slave. The first three are memory-backed, so `DUT + memory slaves` behaves as an AXI memory +// and an `axi_scoreboard` on the AXI port checks end-to-end data integrity (address mapping, bank +// placement, strobe masking, burst handling). The random master only targets the three memory +// slaves; the error slave and an unmapped address are exercised by a few directed transactions that +// check the error / decode-miss responses. APB-protocol legality is checked with assertions. + +`include "axi/typedef.svh" +`include "axi/assign.svh" + +module tb_axi_to_apb #( + // Swept externally over {32, 64} to cover NumBanks = 1 and 2 (with ApbDataWidth = 32). + parameter int unsigned TbAxiDataWidth = 32'd64 +); + + // Timing + localparam time CyclTime = 10ns; + localparam time ApplTime = 2ns; + localparam time TestTime = 8ns; + + // Random master transaction counts + localparam int unsigned NoWrites = 2000; + localparam int unsigned NoReads = 2000; + + // Widths. Keep AxiAddrWidth == ApbAddrWidth so the decode (full AXI address) and the APB memories + // (truncated `paddr`) stay consistent for the scoreboard, i.e. no truncation aliasing. + localparam int unsigned AxiAddrWidth = 32'd32; + localparam int unsigned ApbAddrWidth = 32'd32; + localparam int unsigned ApbDataWidth = 32'd32; + localparam int unsigned AxiIdWidth = 32'd4; + localparam int unsigned AxiUserWidth = 32'd2; + localparam int unsigned AxiStrbWidth = TbAxiDataWidth/8; + + localparam int unsigned NoApbSlaves = 32'd4; + localparam int unsigned NoRules = NoApbSlaves; + + // APB slave indices / behaviour + typedef enum int unsigned { + IdealSlv = 0, + SlowSlv = 1, + RandSlv = 2, + ErrSlv = 3 + } apb_slv_e; + + typedef axi_pkg::xbar_rule_32_t rule_t; // {idx, start_addr, end_addr}, 32-bit address + + // One 4 KiB page per slave; 4 KiB alignment guarantees a burst never crosses a slave boundary. + localparam rule_t [NoRules-1:0] AddrMap = '{ + '{idx: 32'd3, start_addr: 32'h0000_3000, end_addr: 32'h0000_4000}, // error + '{idx: 32'd2, start_addr: 32'h0000_2000, end_addr: 32'h0000_3000}, // random + '{idx: 32'd1, start_addr: 32'h0000_1000, end_addr: 32'h0000_2000}, // slow + '{idx: 32'd0, start_addr: 32'h0000_0000, end_addr: 32'h0000_1000} // ideal + }; + localparam logic [AxiAddrWidth-1:0] MemLo = 32'h0000_0000; // ideal+slow+random span + localparam logic [AxiAddrWidth-1:0] MemHi = 32'h0000_3000; + localparam logic [AxiAddrWidth-1:0] ErrAddr = 32'h0000_3040; // inside error slave + localparam logic [AxiAddrWidth-1:0] DecAddr = 32'h0000_8000; // unmapped + + // APB request/response structs (as expected by `axi_to_apb`) + typedef logic [ApbAddrWidth-1:0] apb_addr_t; + typedef logic [ApbDataWidth-1:0] apb_data_t; + typedef logic [ApbDataWidth/8-1:0] apb_strb_t; + + typedef struct packed { + apb_addr_t paddr; + axi_pkg::prot_t pprot; + logic psel; + logic penable; + logic pwrite; + apb_data_t pwdata; + apb_strb_t pstrb; + } apb_req_t; + + typedef struct packed { + logic pready; + apb_data_t prdata; + logic pslverr; + } apb_resp_t; + + // Full AXI types + typedef logic [AxiAddrWidth-1:0] axi_addr_t; + typedef logic [TbAxiDataWidth-1:0] axi_data_t; + typedef logic [AxiIdWidth-1:0] axi_id_t; + typedef logic [AxiStrbWidth-1:0] axi_strb_t; + typedef logic [AxiUserWidth-1:0] axi_user_t; + + `AXI_TYPEDEF_ALL(axi, axi_addr_t, axi_id_t, axi_data_t, axi_strb_t, axi_user_t) + + // ----------------------------------------------------------------------------------------------- + // Clock / reset + // ----------------------------------------------------------------------------------------------- + logic clk, rst_n; + logic end_of_sim; + + clk_rst_gen #( + .ClkPeriod ( CyclTime ), + .RstClkCycles ( 5 ) + ) i_clk_gen ( + .clk_o ( clk ), + .rst_no ( rst_n ) + ); + + // ----------------------------------------------------------------------------------------------- + // AXI master interface + driver + scoreboard monitor + // ----------------------------------------------------------------------------------------------- + AXI_BUS_DV #( + .AXI_ADDR_WIDTH ( AxiAddrWidth ), + .AXI_DATA_WIDTH ( TbAxiDataWidth ), + .AXI_ID_WIDTH ( AxiIdWidth ), + .AXI_USER_WIDTH ( AxiUserWidth ) + ) master_dv (clk); + + AXI_BUS #( + .AXI_ADDR_WIDTH ( AxiAddrWidth ), + .AXI_DATA_WIDTH ( TbAxiDataWidth ), + .AXI_ID_WIDTH ( AxiIdWidth ), + .AXI_USER_WIDTH ( AxiUserWidth ) + ) master (); + `AXI_ASSIGN(master, master_dv) + + axi_req_t axi_req; + axi_resp_t axi_resp; + `AXI_ASSIGN_TO_REQ(axi_req, master) + `AXI_ASSIGN_FROM_RESP(master, axi_resp) + + apb_req_t [NoApbSlaves-1:0] apb_req; + apb_resp_t [NoApbSlaves-1:0] apb_resp; + + typedef axi_test::axi_rand_master #( + .AW ( AxiAddrWidth ), .DW ( TbAxiDataWidth ), .IW ( AxiIdWidth ), .UW ( AxiUserWidth ), + .TA ( ApplTime ), .TT ( TestTime ), + .MAX_READ_TXNS ( 4 ), .MAX_WRITE_TXNS ( 4 ), + .AXI_MAX_BURST_LEN ( 16 ), + // INCR only (`axi_to_detailed_mem` restriction); no atomics/exclusives. + .AXI_BURST_FIXED ( 1'b0 ), .AXI_BURST_INCR ( 1'b1 ), .AXI_BURST_WRAP ( 1'b0 ), + .AXI_ATOPS ( 1'b0 ), .AXI_EXCLS ( 1'b0 ), + // Master-side backpressure. + .AX_MIN_WAIT_CYCLES ( 0 ), .AX_MAX_WAIT_CYCLES ( 5 ), + .W_MIN_WAIT_CYCLES ( 0 ), .W_MAX_WAIT_CYCLES ( 3 ), + .RESP_MIN_WAIT_CYCLES ( 0 ), .RESP_MAX_WAIT_CYCLES ( 8 ) + ) axi_rand_master_t; + + typedef axi_test::axi_driver #( + .AW ( AxiAddrWidth ), .DW ( TbAxiDataWidth ), .IW ( AxiIdWidth ), .UW ( AxiUserWidth ), + .TA ( ApplTime ), .TT ( TestTime ) + ) axi_drv_t; + + axi_rand_master_t axi_rand_master = new (master_dv); + + // Directed single-beat write, checks the B response. + task automatic directed_write(input axi_addr_t addr, input axi_pkg::resp_t exp); + automatic axi_drv_t::ax_beat_t ax = new; + automatic axi_drv_t::w_beat_t w = new; + automatic axi_drv_t::b_beat_t b; + ax.ax_addr = addr; + ax.ax_id = '0; + ax.ax_len = '0; + ax.ax_size = axi_pkg::size_t'($clog2(AxiStrbWidth)); + ax.ax_burst = axi_pkg::BURST_INCR; + w.w_data = {(TbAxiDataWidth/32){32'hC0FF_EE00}}; + w.w_strb = '1; + w.w_last = 1'b1; + fork + axi_rand_master.drv.send_aw(ax); + axi_rand_master.drv.send_w(w); + join + axi_rand_master.drv.recv_b(b); + assert (b.b_resp == exp) else + $error("directed_write @%0h: expected B resp %0d, got %0d", addr, exp, b.b_resp); + endtask + + // Directed single-beat read, checks the R response. + task automatic directed_read(input axi_addr_t addr, input axi_pkg::resp_t exp); + automatic axi_drv_t::ax_beat_t ax = new; + automatic axi_drv_t::r_beat_t r; + ax.ax_addr = addr; + ax.ax_id = '0; + ax.ax_len = '0; + ax.ax_size = axi_pkg::size_t'($clog2(AxiStrbWidth)); + ax.ax_burst = axi_pkg::BURST_INCR; + axi_rand_master.drv.send_ar(ax); + axi_rand_master.drv.recv_r(r); + assert (r.r_resp == exp) else + $error("directed_read @%0h: expected R resp %0d, got %0d", addr, exp, r.r_resp); + endtask + + initial begin : proc_stimuli + end_of_sim <= 1'b0; + axi_rand_master.reset(); + // Random traffic only hits the three memory slaves. + axi_rand_master.add_memory_region(MemLo, MemHi, axi_pkg::WTHRU_NOALLOCATE); + @(posedge rst_n); + repeat (5) @(posedge clk); + + // Directed error / decode-miss checks (this DUT answers both with SLVERR). + directed_write(ErrAddr, axi_pkg::RESP_SLVERR); // error slave + directed_read (ErrAddr, axi_pkg::RESP_SLVERR); + directed_write(DecAddr, axi_pkg::RESP_SLVERR); // unmapped address + directed_read (DecAddr, axi_pkg::RESP_SLVERR); + + // Randomized data-integrity + backpressure run. + axi_rand_master.run(NoReads, NoWrites); + + end_of_sim <= 1'b1; + repeat (20) @(posedge clk); + $display("[tb_axi_to_apb] finished (AxiDataWidth=%0d, NumBanks=%0d).", + TbAxiDataWidth, TbAxiDataWidth/ApbDataWidth); + $stop(); + end + + // Watchdog: fail instead of hanging on a deadlock. + initial begin : proc_watchdog + repeat (32'd5_000_000) @(posedge clk); + if (!end_of_sim) $fatal(1, "[tb_axi_to_apb] timeout - possible deadlock."); + end + + // ----------------------------------------------------------------------------------------------- + // Scoreboard (end-to-end data integrity on the AXI side) + // ----------------------------------------------------------------------------------------------- + AXI_BUS_DV #( + .AXI_ADDR_WIDTH ( AxiAddrWidth ), + .AXI_DATA_WIDTH ( TbAxiDataWidth ), + .AXI_ID_WIDTH ( AxiIdWidth ), + .AXI_USER_WIDTH ( AxiUserWidth ) + ) monitor_dv (clk); + `AXI_ASSIGN_MONITOR(monitor_dv, master) + + typedef axi_test::axi_scoreboard #( + .IW ( AxiIdWidth ), .AW ( AxiAddrWidth ), .DW ( TbAxiDataWidth ), + .UW ( AxiUserWidth ), .TT ( TestTime ) + ) axi_scoreboard_t; + axi_scoreboard_t axi_scoreboard = new(monitor_dv); + initial begin : proc_scoreboard + axi_scoreboard.enable_all_checks(); + @(posedge rst_n); + axi_scoreboard.monitor(); + end + + // ----------------------------------------------------------------------------------------------- + // DUT + // ----------------------------------------------------------------------------------------------- + axi_to_apb #( + .NoApbSlaves ( NoApbSlaves ), + .NoRules ( NoRules ), + .AxiAddrWidth ( AxiAddrWidth ), + .AxiDataWidth ( TbAxiDataWidth ), + .AxiIdWidth ( AxiIdWidth ), + .AxiUserWidth ( AxiUserWidth ), + .ApbAddrWidth ( ApbAddrWidth ), + .ApbDataWidth ( ApbDataWidth ), + .axi_req_t ( axi_req_t ), + .axi_resp_t ( axi_resp_t ), + .apb_req_t ( apb_req_t ), + .apb_resp_t ( apb_resp_t ), + .rule_t ( rule_t ) + ) i_dut ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .test_i ( 1'b0 ), + .axi_req_i ( axi_req ), + .axi_resp_o ( axi_resp ), + .apb_req_o ( apb_req ), + .apb_resp_i ( apb_resp ), + .addr_map_i ( AddrMap ) + ); + + // ----------------------------------------------------------------------------------------------- + // APB slaves: ideal, slow, random (memory-backed) and error. + // ----------------------------------------------------------------------------------------------- + for (genvar i = 0; i < NoApbSlaves; i++) begin : gen_apb_slv + localparam bit IsErr = (i == ErrSlv); + localparam bit IsRand = (i == RandSlv); + localparam int unsigned FixedWait = (i == SlowSlv) ? 4 : 0; + tb_apb_mem_slave #( + .AddrWidth ( ApbAddrWidth ), + .DataWidth ( ApbDataWidth ), + .FixedWait ( FixedWait ), + .Random ( IsRand ), + .Error ( IsErr ), + .apb_req_t ( apb_req_t ), + .apb_resp_t ( apb_resp_t ) + ) i_apb_slv ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .apb_req_i ( apb_req[i] ), + .apb_resp_o ( apb_resp[i] ) + ); + end + + // ----------------------------------------------------------------------------------------------- + // APB protocol assertions (per slave) + // ----------------------------------------------------------------------------------------------- + // pragma translate_off + `ifndef VERILATOR + default disable iff (!rst_n); + for (genvar i = 0; i < NoApbSlaves; i++) begin : gen_apb_assertions + sequence APB_SETUP; apb_req[i].psel && !apb_req[i].penable; endsequence + sequence APB_ACCESS; apb_req[i].psel && apb_req[i].penable; endsequence + sequence APB_TRANSFER; APB_SETUP ##1 APB_ACCESS; endsequence + + apb_complete: assert property ( @(posedge clk) (APB_SETUP |-> APB_TRANSFER)); + apb_penable: assert property ( @(posedge clk) + (apb_req[i].penable && apb_req[i].psel && apb_resp[i].pready |=> (!apb_req[i].penable))); + control_stable: assert property ( @(posedge clk) + (APB_TRANSFER |-> $stable({apb_req[i].pwrite, apb_req[i].paddr}))); + write_stable: assert property ( @(posedge clk) + ((apb_req[i].penable && apb_req[i].pwrite) |-> $stable(apb_req[i].pwdata))); + strb_stable: assert property ( @(posedge clk) + ((apb_req[i].penable && apb_req[i].pwrite) |-> $stable(apb_req[i].pstrb))); + end + `endif + // pragma translate_on + +endmodule + +// ------------------------------------------------------------------------------------------------- +// Behavioural APB slave. +// - `Error` : always answers with `pslverr`, stores nothing (0-wait). +// - `Random` : asserts `pready` on a random cycle during the access phase. +// - else : asserts `pready` after `FixedWait` access cycles (0 = ideal). +// Memory-backed (except the error slave): stores `pwdata` under `pstrb`, returns it on reads. +// ------------------------------------------------------------------------------------------------- +module tb_apb_mem_slave #( + parameter int unsigned AddrWidth = 32, + parameter int unsigned DataWidth = 32, + parameter int unsigned FixedWait = 0, + parameter bit Random = 1'b0, + parameter bit Error = 1'b0, + parameter type apb_req_t = logic, + parameter type apb_resp_t = logic +) ( + input logic clk_i, + input logic rst_ni, + input apb_req_t apb_req_i, + output apb_resp_t apb_resp_o +); + typedef logic [AddrWidth-1:0] addr_t; + typedef logic [DataWidth-1:0] data_t; + + data_t mem [addr_t]; // word-addressed by `paddr` + logic in_access; + logic pready; + int unsigned wait_cnt; + logic rand_bit; + data_t rd_data; + + assign in_access = apb_req_i.psel & apb_req_i.penable; + + // Wait-state counter: counts access cycles until the transfer completes. + always_ff @(posedge clk_i or negedge rst_ni) begin + if (!rst_ni) begin + wait_cnt <= 0; + rand_bit <= 1'b0; + end else begin + rand_bit <= $urandom_range(0, 1); + wait_cnt <= (in_access && !pready) ? wait_cnt + 1 : 0; + end + end + + always_comb begin + pready = 1'b0; + if (in_access) begin + if (Error) pready = 1'b1; // error slave completes immediately + else if (Random) pready = rand_bit; + else pready = (wait_cnt >= FixedWait); + end + end + + always_comb begin + rd_data = '0; + if (!Error && apb_req_i.psel && mem.exists(apb_req_i.paddr)) rd_data = mem[apb_req_i.paddr]; + end + + assign apb_resp_o.pready = pready; + assign apb_resp_o.prdata = rd_data; + assign apb_resp_o.pslverr = Error; + + // Commit writes (byte-strobed) on the completing access cycle. + always_ff @(posedge clk_i) begin + if (in_access && pready && apb_req_i.pwrite && !Error) begin + automatic data_t cur = mem.exists(apb_req_i.paddr) ? mem[apb_req_i.paddr] : '0; + for (int b = 0; b < DataWidth/8; b++) begin + if (apb_req_i.pstrb[b]) cur[8*b +: 8] = apb_req_i.pwdata[8*b +: 8]; + end + mem[apb_req_i.paddr] = cur; + end + end + +endmodule