Skip to content

Latest commit

 

History

History
298 lines (245 loc) · 16.4 KB

python-design-description.md

File metadata and controls

298 lines (245 loc) · 16.4 KB

Python design description format

This document specifies a new XactFlow Importer: a way to describe a design's architecture (which IPs it instantiates, how they're wired together over buses, and any ad hoc signal connections) as a plain Python script, instead of hand-written IP-XACT XML. Running the script produces an ipxact.Design object, which the rest of XactFlow already knows what to do with: link() resolves and checks it against a Library, and XactFlow-design can serialize it back out to a real IP-XACT design XML document.

This builds on the design description concept from IP-XACT-Based SoC Generation: Tools and SoCMake Integration (M. Travaillard, B. Denkinger, August 2026), specifically its Section 4.3 and the design2ipxact transformation of Section 5.2. That transformation scoped a full SoCMake-integrated pipeline (sv2ipxact, design2ipxact, ipxact2sv, a shared bus library, radiation-hardening hooks, CMake integration); this document only covers the piece of it that becomes a standalone XactFlow Importer, with no SoCMake or CMake dependency, consistent with every other XactFlow plugin.

Scope

What this importer does:

  1. Execute a Python script that declares IP instances, bus connections, and ad hoc connections using the API below.
  2. Resolve each IP reference to a real ipxact.Component via a Library the script itself provides.
  3. Build the corresponding ipxact.Design object: one ComponentInstance per IP instance, one Interconnection per bus connection (a MonitorInterconnection when one side is a monitor), one AdHocConnection per ad hoc connection.

What it deliberately does not do, because XactFlow already does it elsewhere:

  • Bus protocol compatibility checking. connect_bus() only matches interfaces by bus type and interface mode (see below); it does not check signal-level compatibility, that's xactflow.link()'s job, via its interconnection-compatibility SCR checks, run after this importer hands off the Design.
  • Address conflict checking, memory map validation. Out of scope for the reasons in Address mapping below.
  • Anything else IP-XACT's semantic consistency rules cover. This importer's only job is producing a syntactically well-formed Design; whether that Design is semantically sound, correct cross-references, compatible interfaces, valid addressing, sound hierarchy, and so on, is SCR's job, unchanged from how every other Design (hand-written or otherwise imported) is checked today.

The Design object

class Design:
    def __init__(self, vlnv: str, library: xactflow.Library) -> None: ...
    def add_ip(self, instance_name: str, vlnv: str, parameters: dict[str, object] | None = None, transforms: list[str] | None = None) -> IPInstance: ...
    def connect_bus(self, a: IPInstance | BusInterfaceRef, b: IPInstance | BusInterfaceRef, *, base_addr: int | None = None, size: int | None = None) -> None: ...
    def connect(self, source: PortRef, target: PortRef) -> None: ...
    def build(self) -> ipxact.Design: ...

Design(vlnv, library)

  • vlnv: a colon-separated "vendor:library:name:version" string identifying the design itself. Parsed eagerly; a malformed string (not exactly 4 fields) raises ValueError at construction.
  • library: an already-scanned xactflow.Library. The script builds this itself (Library.scan(...)) and passes it in, Design never constructs or owns a Library implicitly. This keeps the script fully self-contained.

add_ip(instance_name, vlnv, parameters=None, transforms=None) -> IPInstance

Declares one component instance and resolves it immediately, not deferred to build():

  • instance_name must follow the IP-XACT naming rule: it starts with a letter or _, not a digit, followed by letters, digits, _, . or -. It must also be unique within the design. A duplicate raises ValueError immediately, naming both the new call and the earlier one that already used it.

  • vlnv is resolved against library right away. An unresolvable VLNV raises ValueError at the add_ip() call site, not at the end of the script, so a mistake is reported on the line that made it.

  • parameters maps a parameter's name (as declared on the resolved component) to an override value. This is IP-XACT's own configurableElementValues feature. For each entry, add_ip looks up the matching Parameter by name on the resolved component:

    • Unknown name: ValueError, naming the instance, the bad key, and the parameter names that are available on that component.
    • Name matches a parameter that cannot be overridden: ValueError. It needs a parameterId and resolve="user". The standard says an immediate parameter (the default) cannot be modified through configuration. This is a defect in the referenced component file, not something this importer can paper over (see Requirement on imported components).
    • Otherwise, the override is recorded against that parameter's parameterId.
  • transforms names zero or more transformations to apply to this instance, radiation hardening being one example. This importer does not apply them. It only records the names, in order, as a vendor extension on the resulting component instance.

    A separate tool, a transformer, does the work later. It takes the ipxact.Design, applies the transformations its instances ask for, and returns the modified ipxact.Design, ready for linking. A transformer can be kept private, and can be turned on or off without changing the design description.

Returns an IPInstance handle (see below) used to build bus and ad hoc connections against this instance.

IPInstance

A handle returned by add_ip(), carrying the instance name and the resolved ipxact.Component.

class IPInstance:
    name: str
    component: ipxact.Component

    def port(self, name: str) -> PortRef: ...
    def bus(self, name: str) -> BusInterfaceRef: ...
  • .port(name) looks up name in component.model.ports; unknown name raises ValueError listing the ports that do exist. Used to build ad hoc connections.
  • .bus(name) looks up name in component.bus_interfaces; unknown name raises ValueError the same way. Used only when connect_bus() needs help picking the right bus (see below).
  • instance.some_port is a shorthand for instance.port("some_port"). If a port has the same name as an IPInstance attribute, such as name, use .port(...) instead.

connect_bus(a, b, *, base_addr=None, size=None)

Connects one bus interface on a to one bus interface on b. Each argument is either a plain IPInstance (pick one of its bus interfaces automatically) or an IPInstance.bus(name) (use that exact one). Here is how it picks:

  1. For each side, the candidates are: the exact interface given, if a bus(name) was passed; otherwise every initiator or target interface on that instance's component that an earlier connect_bus() call has not already used.
  2. A pair (interface_a, interface_b) matches if they share the same bus type, and their modes are opposite. The same bus type means the same bus VLNV (interface_a.bus_type.vlnv == interface_b.bus_type.vlnv) and the same parameter values set on it (bus_type.config_element_values), compared after evaluation. The modes that connect are the ones the standard allows: initiator with target or mirrored initiator, mirrored target with target, and system with mirrored system. A monitor interface matches an interface of the mode it monitors. Only initiators and targets are picked automatically, the other modes need the exact, named call. There is no need to say which side is the initiator and which is the target: each side's mode is already set on its own component, so this importer reads it instead of asking the script to repeat it.
  3. If exactly one matching pair exists, it is used.
  4. If more than one exists, connect_bus raises ValueError listing every candidate interface name on both sides. The caller must then say exactly which one to use, by passing a.bus("...")/b.bus("...") for at least one side. A multi-port interconnect (several identical initiator interfaces, one per attached peripheral) will usually hit this case; it is expected, not an edge case.
  5. If none matches, ValueError explains why (no shared bus definition, no opposite mode available, or every candidate already used) and lists what bus interfaces are still free on each side.

On success, one ipxact.Interconnection is added to the design, with active_interface set from a's side and other_active_interfaces=[<b's side>]. Its name always follows the same pattern, <instance a>_to_<instance b>, built from the two instance names even when an interface is named with .bus(...) (a number is added at the end if that name is already taken, e.g. a second link between the same two instances). Both interfaces are now marked used, so a later connect_bus() call will not reuse them.

When one side is a monitor, an ipxact.MonitorInterconnection is added instead. Only the monitor interface is marked used: the interface it watches stays free for a normal connection.

Address mapping

base_addr/size are a convenience for the common case: a target sitting at some offset on an addressable bus. IP-XACT actually handles this through addressSpace/segment on the initiator side, resolved through a designConfiguration, but XactFlow doesn't link that yet (see the main XactFlow README). Doing this the real way is future work, once that support exists. Until then, when either is given, they're recorded as a vendor extension on the target side of the connection (a target or mirrored initiator interface). A connection with no such side, like a system or monitor one, raises ValueError. This is the same way IP-XACT represents anything the standard doesn't cover and isn't modeled here yet. Concretely, something to the effect of:

<xactflow:addressMapping baseAddr="0x40010000" size="0x1000"/>

is appended to that interface's vendor_extensions. This is a placeholder, not a standards-compliant addressing mechanism; anything reading it back needs to know to look for it specifically. It should be revisited (and likely replaced) once XactFlow links DesignConfiguration/addressSpace for real.

connect(source, target)

An ad hoc, signal-level connection between exactly two ports, each a PortRef (from .port(name) or the attribute shorthand). Unlike connect_bus, which leaves signal-level compatibility to link(), connect checks the ports itself, since the check is cheap and both ports are already in hand:

  • Both ports must belong to instances of this design.
  • Directions must be compatible: one out and one in, or either side inout. Two outs or two ins raise ValueError naming both ports and their directions.
  • A port cannot be connected to itself, and the same two ports cannot be connected twice.
  • An in port can only be driven by one connection.

On success, appends one ipxact.AdHocConnection with a single InternalPortReference pair (component_instance_ref/port_ref for each side), named f"{source.instance.name}_{source.name}_to_{target.instance.name}_{target.name}" (a number is added at the end if that name is already taken). Bit-slice or struct-sub-port ad hoc connections (SubPortReference/PartSelect in ipxact-compiler's model) are out of scope for this first version; connect() only wires whole ports to whole ports.

build() -> ipxact.Design

Returns the finished ipxact.Design: every add_ip as a ComponentInstance (in call order), every connect_bus as an Interconnection (or a MonitorInterconnection for a monitor), every connect as an AdHocConnection. It does not check that every bus interface got connected: an unconnected optional interface (e.g. an unused debug port) is a legitimate, common design, not an error. Completeness is SCR's concern, if and when a rule for it exists, not this importer's.

Scripts don't call build() themselves. The importer does, after running the script, see below.

How the importer runs a script

class ArchiDescImporter(xactflow.Importer):
    def import_(self, source: Path, **options) -> ipxact.Design: ...

It executes source as a plain Python file (runpy.run_path), then looks for a module-level variable named design, of type Design. Missing or wrong-typed design raises ValueError naming what was expected. It then calls .build() on it and returns the result. Nothing about Library setup is the importer's business, that already happened inside the script when it constructed its own Design.

Worked example

from xactflow import Library, link
from xactflow_archidesc import Design

library = Library.scan("path/to/ip/library")
design = Design("cern.ch:soc:example:0.1", library)

cpu   = design.add_ip("cpu",   "openhwgroup:core:ibex:1.0", transforms=["tmrg"])
uart  = design.add_ip("uart",  "cern.ch:ip:uart:1.0", parameters={"BAUDRATE": 115200})
timer = design.add_ip("timer", "pulp:ip:timer:1.0")
debug = design.add_ip("debug", "cern.ch:ip:debug_module:1.0")
bus   = design.add_ip("apb_bus", "cern.socgen:apb_rt:0.1.0")

design.connect_bus(cpu, bus)
design.connect_bus(bus, uart,  base_addr=0x4001_0000, size=0x1000)
design.connect_bus(bus, timer, base_addr=0x4002_0000, size=0x1000)

design.connect(debug.debug_req_o, cpu.debug_req_i)
design.connect(cpu.debug_resp_o, debug.debug_resp_i)

linked = link(design.build(), library)

design.build() produces the ipxact.Design: 5 ComponentInstances, 3 Interconnections (cpu_to_apb_bus, apb_bus_to_uart, apb_bus_to_timer, one per connect_bus call), and 2 AdHocConnections. Passing it straight to link() with the same library resolves and checks it exactly as it would a hand-written or hand-exported design, all in this one script, with no separate importer step needed. Running the same script through ArchiDescImporter instead (letting the CLI or another tool drive it) does that same build() call automatically; linking is then up to whatever calls the importer, not shown here.

Requirement on imported components

add_ip's parameters argument only works for a component whose parameters carry a parameterId; that's what config_element_values keys by. Any component supplied to this format, whether produced by XactFlow-sv, another importer, or hand-written IP-XACT, needs parameterId and resolve="user" set on each parameter meant to be overridable this way. One with no parameterId cannot be referenced for override at all, and one with resolve="immediate" cannot be modified through configuration, per the standard. This is a general requirement on the components this format consumes, not something specific to one importer.

Open questions, deferred rather than silently resolved

Things this first version leaves open, on purpose:

  • Hierarchical design description. A sub-design instantiated as a component inside a bigger one is not addressed here; this format currently describes exactly one flat design. XactFlow's own linker doesn't recurse into nested designs yet either (see its README's "No multi-level design hierarchy" limitation), so there is nothing downstream to hand a hierarchical result to yet regardless.
  • Address mapping, as above: a real system, not a vendor-extension placeholder, once XactFlow links DesignConfiguration/addressSpace.
  • Protocol bridges (e.g. an APB-to-OBI adapter between two incompatible bus segments): not addressed. connect_bus either finds a matching pair or fails; nothing here automatically adds a bridge component. A bridge, if one exists as a real component, is just another add_ip the script author wires in explicitly on both sides, sourced and placed by the user, not made automatically by the tool.
  • System/mirrored/monitor interface modes: real IP-XACT concepts, deliberately not auto-matched by connect_bus in this first version (see above).
  • Bus type parameter values: connect_bus compares the parameter values set on each side's bus type (a data width, for example), not just the bus VLNV. These values are raw expressions ("32" and "0x20" are equal, a value can refer to an instance parameter), so they have to be evaluated before they can be compared. This first version only compares the bus VLNV, until XactFlow evaluates parameter values well enough to do it.