This RFC proposes a dialect for representing AXI4 networks, drafted by myself and @moritzs - if there’s an ODM slot available to discuss this any time soon then that would be great, and any feedback is very welcomed! For anyone interested, I’ve begun to prototype the contents of this RFC on this branch.
Motivation
Designing AXI networks can be high-effort and fraught with the risk of creating networks that allow for invalid requests. This dialect aims to provide a high-level representation of AXI networks that uses types and verifiers to catch cases where an invalid or non-compliant network has been specified.
We’ve designed this dialect with three primary use-cases in mind:
- Using the AXI dialect (or more practically some front-end that compiles to it) as a high-level description language; Once you have RTL specifications of your endpoints, you can use the dialect or front-end to describe your desired network and use
--lower-axi4-to-hw(see below) to generate an implementation. - Using the AXI dialect to abstractly model an architecture; the dialect is designed to allow a network to be specified without concrete RTL sources (see the optional node operations on the manager_port and subordinate_port operations below). This allows the dialect to be used to specify a network early in the design process and validate that it meets sanity checks and validation criteria. A later lowering could also use such a description to generate an interconnect-only RTL implementation with top-level ports corresponding to the abstract ports given in the model.
- Extracting existing full RTL models to the AXI dialect; from an RTL system design with a sufficiently identifiable structure for AXI interfaces, it would theoretically be possible to produce a higher-level description raised to the AXI dialect. This allows for static analysis of the network, and is a potential path to accelerated verification.
This RFC describes only a minimal subset of the operations that would be needed to describe the range of AXI networks designed in practice - this is to establish a solid proof of concept before adding more complex components like clock domain crossings and aliasing.
Types
axi4.clock
Defines a clock domain over AXI components.
axi4.reset
Defines a reset domain (active low) over AXI components.
axi4.node
Used to associate ports with blackbox module instances.
axi4.port<addr_width : ui32, data_width : ui32, id_w_width : ui32, id_r_width : ui32, user_width : ui32>
Primary interface to be passed between endpoints and interconnect/routing primitives - address, data, read/write ID and user widths are included in the type. The AXI4 specification leaves user widths on each channel up to implementation (this changed in AXI5) - for the first version, we restrict it to a single width to avoid bloating the type too much. If need be, wrappers around modules can work around this by zero-extending/extracting user lines at each end.
Verifies that widths are within the ranges permitted by the AXI specification (addr_width <= 64, data_width <= 1024, id_w_width <= 32, id_r_width <= 32).
e.g.,
!axi4.port<32, 64, 4, 4, 0>
With so many parameters, there’s obviously a trade-off w.r.t. verbosity vs. intelligibility here, so I’d be curious to hear thoughts on the right assembly format for this. A possible intermediate option could be named parameters in the type annotation of e.g. just endpoints or just manager_ports.
Attributes
axi4.burst_kind
Describes a kind of AXI burst supported by an endpoint. Enum; can be fixed, incr, or wrap.
axi4.burst_spec<burst_kind: #axi4.burst_kind, len (optional): ui32>
Describes the burst capabilities of an endpoint. Consists of a burst_kind and an burst length if incr/wrap is given as the burst_kind e.g.,
#axi4.burst_spec<fixed>
#axi4.burst_spec<incr, len = 16>
axi4.window<base: ui64, size: ui64, burst_specs: ArrayAttr<#axi4.burst_spec>>
Describes an access window supported by an endpoint. Consists of a base address, a size, and an array of supported burst_specs.
e.g.,
#axi4.window<base = 0x4000, size = 0x100, burst_specs = [<fixed>]>
#axi4.window<base = 0x4000, size = 0x100, burst_specs = [<wrap, len = 8>, <incr, len = 16>]>
axi4.port_mapping
This is used to create a mapping between manager/subordinate_port operations and the module ports defined in the corresponding RTL.
This contains one of several possible subattributes that defines a mapping between dialect and RTL ports. This is intended to be extensible so that new subattributes can be added when we want to support a new style of RTL implementation. Since the full specifications are verbose (as they contain all the lines involved in AXI communications) we point below to established examples in the PULP project for a layout these could have. In each of the below, a clock port name is given to indicate where the corresponding !axi4.clock should be routed to, and a reset port name is similarly given for routing the !axi4.reset value. The initial proposed attributes are:
axi4.port_wires<clock: StringAttr, reset: StringAttr, name: StringAttr>
Maps to a flat set of values containing the AXI connections in the order used in the PULP Platform AXI port macros
e.g.,
#axi4.port_wires<"clk", "rst", "axi_in">
axi4.req_resp_structs<clock: StringAttr, reset: StringAttr, req_name: StringAttr, resp_name: StringAttr>
Maps to two structs, which respectively contain the req and resp channels in order specified by the types defined in the PULP Platform AXI typedefs
e.g.,
#axi4.req_resp_structs<"clk", "rst", "axi_sub_req_i", "axi_sub_resp_o">,
axi4.port_interface<clock: StringAttr, reset: StringAttr, name: StringAttr>
Maps to an interface (or more specifically, the set of signal names generated by Slang/circt-verilog from the interface) specifying an AXI interface as in the PULP AXI interface
e.g.,
#axi4.port_interface<"clk", "rst", "axi_in_if">,
Operations
axi4.node
Describes an instantiated blackbox hardware module that has AXI4 ports (annotated with the *_port ops below) along with other ports of any type (annotated with the generic_* ops below). Takes:
- A symbol identifying the module this instantiates (verifier checks that this corresponds to an HWModuleLike)
Returns an !axi4.node
(n.b. This operation is the attachment point for existing RTL, whether that be as a front-end or a raising target. Abstract networks can be defined by excluding this op - see the optional !axi.node operand of the port operations below)
axi4.manager_port
Describes a manager port on a node. Takes:
- An !axi4.clock (verifier checks that all ports on a node that share a clock/reset port name in their port_mapping (below) must also share a clock/reset operand)
- An !axi4.reset
- An !axi4.node that indicates the node this port is attached to (optional)
- An !axi4.port_mapping to indicate which port on the node this corresponds to (optional; must be given iff node is given)
- An array of access windows (verifier checks they do not overlap) that this manager port can send requests to
- Maximal numbers of concurrent outstanding reads and writes (verifier checks that these are not greater than 2^(corresponding ID width in return type))
Returns an !axi4.port. The verifier ensures that this value has only one use (since switching must be explicit through an xbar op).
e.g.,
%manager = axi4.manager_port %clk, %rst_ni node %node {
port_mapping = #axi4.port_wires<"clk", "rst", "axi_in">,
access = [#axi4.window<base = 0x0, size = 0x1000, burst_specs = [<fixed>]>],
outstanding_reads = 4 : ui64,
outstanding_writes = 4 : ui64,
} : !axi4.port<32, 64, 4, 4, 0>
%nodeless_manager = axi4.manager_port %clk, %rst_ni {
access = [#axi4.window<base = 0x0, size = 0x1000, burst_specs = [<fixed>]>],
outstanding_reads = 4 : ui64,
outstanding_writes = 4 : ui64,
} : !axi4.port<32, 64, 4, 4, 0>
axi4.subordinate_port
Describes a subordinate port on a node. Takes:
- An !axi4.port representing the upstream network
- An !axi4.clock (verifier checks that all ports on a node that share a clock/reset port name in their port_mapping (below) must also share a clock/reset operand)
- An !axi4.reset
- An !axi4.node that indicates the node this port is attached to (optional)
- An !axi4.port_mapping to indicate which port on the node this corresponds to (optional; must be given iff node is given)
- An array of access windows (verifier checks they do not overlap) that this subordinate can handle requests to
- A maximal number of outstanding requests that the subordinate can concurrently handle (verifier checks that these are not greater than 2^(sum of ID widths in input port type))
Has no return value.
e.g.,
axi4.subordinate_port %manager, %clk, %rst_ni node %node {
port_mapping = #axi4.req_resp_structs<"clk", "rst", "axi_sub_req_i", "axi_sub_resp_o">,
access = [#axi4.window<base = 0x0, size = 0x1000, burst_specs = [<fixed>]>],
outstanding_requests = 4 : ui64
} : !axi4.port<32, 64, 4, 4, 0>
axi4.subordinate_port %nodeless_manager, %clk, %rst_ni {
access = [#axi4.window<base = 0x0, size = 0x1000, burst_specs = [<fixed>]>],
outstanding_requests = 4 : ui64
} : !axi4.port<32, 64, 4, 4, 0>
axi4.xbar
Describes a crossbar interconnect. Takes:
- An !axi4.clock
- An !axi4.reset
- A (variadic) number of !axi4.ports, indicating its upstream connections (manager types must all match, and verifier checks that the ID widths in the return type are at least corresponding manager id width + clog2(number of managers))
Returns an !axi4.port.
e.g.,
%xbar = axi4.xbar %clk, %rst_ni mgrs %manager1, %manager2 : (!axi4.port<32, 64, 4, 4, 0>, !axi4.port<32, 64, 4, 4, 0>) -> !axi4.port<32, 64, 5, 5, 0>
Unlike !axi4.manager_port, an !axi4.xbar can have multiple uses - each of these uses represents a downstream subordinate port.
axi4.generic_input
Allows a generic (any-typed) input of a node to be connected to an SSA value (so that node operations can instantiate modules with ports beyond AXI interfaces without ignoring their connectivity). In this case, the node is mandatory to enforce that this op is only used in the case of a concrete module specification.
Takes:
- A value of any type
- A StringAttr that names the input the value should be connected to
- An !axi4.node representing the node owning this input
e.g.,
axi4.generic_input %foo, "sum_i" node %node : i8
axi4.generic_output
Defines a value connected to a generic (any-typed) output of a node (as above, so that node operations can instantiate modules with ports beyond AXI interfaces without ignoring their connectivity). In this case, the node is mandatory to enforce that this op is only used in the case of a concrete module specification.
Takes:
- A StringAttr that names the output that the value is connected to
- A type for the value (provided by annotation)
- An !axi4.node representing the node owning this output
e.g.,
%bar = axi4.generic_output "carry_o" node %node : i1
Passes
Initial versions of these PRs will require that specified networks contain only a single clock/reset domain (crossings will remain a todo in the initial implementation). The axi4.clock and axi4.reset types are still provided in this PR so that we have explicit clocking when we lower to RTL (and therefore don’t have to modify the top-level hw.module’s port signature to synthesize implicit clock/reset signals).
verify-axi4-networks (analysis)
Performs global verifications of networks that are too expensive to be included in operation verifiers. Ensures that:
- Reachable addresses cannot be reached multiple ways
- manager_ports do not have any part of any window that does not map to an endpoint
- manager_ports cannot trigger bursts to addresses that do not support them i.e., the burst types given in manager_port access windows must correspond to those on the subordinate_port windows downstream
- The network is loop-free
lower-axi4-to-hw
Converts a network specification into a concrete RTL description.
- Replaces nodes with hw.instances of their referenced module
- The inputs and outputs of these instances are wired up according to the manager_port/subordinate_port/generic_input/generic_output users of the node (eventually we plan to add an option to wrap these ports in hardware that ensures that AXI transactions leaving these ports are valid and consistent with the specification)
- Replaces axi4.xbars with generic external module instances, and optionally emits a wrapper matching this generic instance that instantiates a concrete xbar module (first targetting the PULP Platform AXI library as this is our use-case, using sv.verbatim ops since the axi_xbar module in PULP is quite parameter-heavy on config and address mapping). Might be fun to have a lowering to an actual hw/comb/seq implementation some day.
Example
Consider the following simple example network:
Note that Core is a manager, Memory and Peripheral are subordinates, and Debug has both a manager and a subordinate port. There is a non-AXI connection, some_port, which flows between Core and Debug. Such a network’s representation in the AXI dialect might look as follows:
hw.module @Demo(in %clk : !axi4.clock, in %rst_ni : !axi4.reset) {
// Create a node to instantiate each module
%core_node = axi4.node @Core
%memory_node = axi4.node @Memory
%peripheral_node = axi4.node @Peripheral
%debug_node = axi4.node @Debug
// Core's manager port
%core = axi4.manager_port %clk, %rst_ni node %core_node {
port_mapping = #axi4.port_wires<"clk", "rst_ni", "axi_out">,
access = [
#axi4.window<base = 0x00000000, size = 0x10000000, burst_specs = [<incr, len = 16>]>,
#axi4.window<base = 0x10000000, size = 0x00001000, burst_specs = [<fixed>]>,
#axi4.window<base = 0x20000000, size = 0x00001000, burst_specs = [<fixed>]>
],
outstanding_reads = 4 : ui64,
outstanding_writes = 4 : ui64
} : !axi4.port<32, 64, 4, 4, 0>
// Debug has both a manager and subordinate port (both below)
%debug_mgr = axi4.manager_port %clk, %rst_ni node %debug_node {
port_mapping = #axi4.port_wires<"clk", "rst_ni", "axi_mgr">,
access = [
#axi4.window<base = 0x00000000, size = 0x10000000, burst_specs = [<incr, len = 16>]>
],
outstanding_reads = 2 : ui64,
outstanding_writes = 2 : ui64
} : !axi4.port<32, 64, 4, 4, 0>
axi4.subordinate_port %xbar, %clk, %rst_ni node %debug_node {
port_mapping = #axi4.port_wires<"clk", "rst_ni", "axi_sub">,
access = [
#axi4.window<base = 0x20000000, size = 0x00001000, burst_specs = [<fixed>]>
],
outstanding_requests = 4 : ui64
} : !axi4.port<32, 64, 5, 5, 0>
%xbar = axi4.xbar %clk, %rst_ni mgrs %core, %debug_mgr
: (!axi4.port<32, 64, 4, 4, 0>, !axi4.port<32, 64, 4, 4, 0>)
-> !axi4.port<32, 64, 5, 5, 0>
// Memory's subordinate port
axi4.subordinate_port %xbar, %clk, %rst_ni node %memory_node {
port_mapping = #axi4.req_resp_structs<"clk", "rst_ni", "axi_req_i", "axi_resp_o">,
access = [
#axi4.window<base = 0x00000000, size = 0x10000000, burst_specs = [<incr, len = 16>, <fixed>]>
],
outstanding_requests = 8 : ui64
} : !axi4.port<32, 64, 5, 5, 0>
// Peripheral's subordinate oirt
axi4.subordinate_port %xbar, %clk, %rst_ni node %peripheral_node {
port_mapping = #axi4.port_interface<"clk", "rst_ni", "axi_if">,
access = [
#axi4.window<base = 0x10000000, size = 0x00001000, burst_specs = [<fixed>]>
],
outstanding_requests = 4 : ui64
} : !axi4.port<32, 64, 5, 5, 0>
// Non-AXI connection: Core drives some_port, Debug consumes it
%some_port = axi4.generic_output "some_port_o" node %core_node : i8
axi4.generic_input %some_port, "some_port_i" node %debug_node : i8
hw.output
}
