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.
What this importer does:
- Execute a Python script that declares IP instances, bus connections, and ad hoc connections using the API below.
- Resolve each IP reference to a real
ipxact.Componentvia aLibrarythe script itself provides. - Build the corresponding
ipxact.Designobject: oneComponentInstanceper IP instance, oneInterconnectionper bus connection (aMonitorInterconnectionwhen one side is a monitor), oneAdHocConnectionper 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'sxactflow.link()'s job, via its interconnection-compatibility SCR checks, run after this importer hands off theDesign. - 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 thatDesignis semantically sound, correct cross-references, compatible interfaces, valid addressing, sound hierarchy, and so on, isSCR's job, unchanged from how every otherDesign(hand-written or otherwise imported) is checked today.
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: ...vlnv: a colon-separated"vendor:library:name:version"string identifying the design itself. Parsed eagerly; a malformed string (not exactly 4 fields) raisesValueErrorat construction.library: an already-scannedxactflow.Library. The script builds this itself (Library.scan(...)) and passes it in,Designnever constructs or owns aLibraryimplicitly. This keeps the script fully self-contained.
Declares one component instance and resolves it immediately, not deferred to build():
-
instance_namemust 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 raisesValueErrorimmediately, naming both the new call and the earlier one that already used it. -
vlnvis resolved againstlibraryright away. An unresolvable VLNV raisesValueErrorat theadd_ip()call site, not at the end of the script, so a mistake is reported on the line that made it. -
parametersmaps a parameter's name (as declared on the resolved component) to an override value. This is IP-XACT's ownconfigurableElementValuesfeature. For each entry,add_iplooks up the matchingParameterby 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 aparameterIdandresolve="user". The standard says animmediateparameter (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.
- Unknown name:
-
transformsnames 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 modifiedipxact.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.
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 upnameincomponent.model.ports; unknown name raisesValueErrorlisting the ports that do exist. Used to build ad hoc connections..bus(name)looks upnameincomponent.bus_interfaces; unknown name raisesValueErrorthe same way. Used only whenconnect_bus()needs help picking the right bus (see below).instance.some_portis a shorthand forinstance.port("some_port"). If a port has the same name as anIPInstanceattribute, such asname, use.port(...)instead.
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:
- 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 earlierconnect_bus()call has not already used. - A pair
(interface_a, interface_b)matches if they share the same bus type, and theirmodes 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. - If exactly one matching pair exists, it is used.
- If more than one exists,
connect_busraisesValueErrorlisting every candidate interface name on both sides. The caller must then say exactly which one to use, by passinga.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. - If none matches,
ValueErrorexplains 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.
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.
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
outand onein, or either sideinout. Twoouts or twoins raiseValueErrornaming both ports and their directions. - A port cannot be connected to itself, and the same two ports cannot be connected twice.
- An
inport 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.
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.
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.
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.
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.
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
XactFlowlinksDesignConfiguration/addressSpace. - Protocol bridges (e.g. an APB-to-OBI adapter between two incompatible bus
segments): not addressed.
connect_buseither finds a matching pair or fails; nothing here automatically adds a bridge component. A bridge, if one exists as a real component, is just anotheradd_ipthe 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_busin this first version (see above). - Bus type parameter values:
connect_buscompares 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, untilXactFlowevaluates parameter values well enough to do it.