| name | jitx-pin-assignment |
| description | This skill should be used when the user asks about "provide/require patterns", "@provide.one_of" or "@provide.subset_of" decorators, "programmatic Provide", "pin muxing" (MCU peripherals on shared pins), "DiffPair P/N polarity swapping", "PCIe lane swapping" or width variants, "DDR4 byte/bit swapping", "LPDDR5 channel swapping", "hierarchical provider composition", topology (>>) on pin-assigned ports, ConstrainDiffPair or ConstrainReferenceDifference with provide/require, or "flexible pin mapping" for FPGAs and MCUs. Covers provide, Provide, require, all_of, one_of, subset_of, and protocol-specific pin flexibility with SI constraints. |
JITX Pin Assignment
Model flexible pin mappings between circuit-level interfaces and component-level pins. JITX's provide/require system lets the layout engine choose optimal pin assignments during routing.
Environment
Environment setup is handled by the base jitx skill. Ensure it has been invoked first.
Package Architecture
from jitx.net import Port, DiffPair, provide, Provide
from jitx.common import Power, GPIO
from jitxlib.protocols.serial import I2C, SPI, UART
from jitx.si import (
Constrain,
ConstrainDiffPair,
ConstrainReferenceDifference,
DiffPairConstraint,
ReferencePlanes,
)
from jitx.net import Topology
from jitx import Circuit, Net
These DO NOT EXIST — never import:
jitx.provide, jitx.providers, jitx.pin_assignment, jitx.assign,
jitx.net.provide_one_of, jitxlib.pin_assignment, jitx.pin_assign
Key locations:
provide (lowercase, decorator) is in jitx.net
Provide (uppercase, constructor class) is in jitx.net (also re-exported from jitx)
GPIO is in jitx.common
I2C, SPI, UART are in jitxlib.protocols.serial
- All constraint classes are in
jitx.si
When to Use Pin Assignment
Use pin assignment when a component's physical pins can validly serve more than one logical role, and the layout engine should choose the optimal mapping.
Use pin assignment for:
- MCU GPIO — any of N pins can drive an LED, sensor, etc.
- Peripheral muxing — I2C/SPI/UART available on alternate pin groups
- DiffPair polarity — some protocols allow P/N swap (PCIe, USB3)
- Lane ordering — PCIe lane reversal, width variants (x1/x2/x4)
- Byte/bit swapping — DDR controllers that support data bus reordering
Use fixed wiring instead when:
- The mapping is 1:1 with no flexibility (e.g., DDR address pins, CK polarity)
- The component datasheet specifies a single valid pin function
- A deterministic connection is needed regardless of layout
Bundles: The Language of Provide/Require
A bundle is a Port subclass that groups related signals. Bundles are the type parameter for @provide and require() — they define what interface is being offered and consumed.
Built-in Bundles and Their Sub-Ports
Discover sub-ports by reading the class source: grep -A 10 "class BundleName" .venv/lib/python*/site-packages/jitx*/
| Bundle | Import | Sub-ports | Notes |
|---|
GPIO | jitx.common | .gpio | Single pin |
Power | jitx.common | .Vp, .Vn | Power/ground pair |
DiffPair | jitx.net | .p, .n | Positive/negative pair |
I2C | jitxlib.protocols.serial | .sda, .scl | Always present |
SPI | jitxlib.protocols.serial | .sck, .copi, .cipo, .cs | cs, copi, cipo are optional: SPI(cs=True) |
UART | jitxlib.protocols.serial | .tx, .rx, .cts, .rts, ... | tx/rx default on; flow control optional: UART(cts=True, rts=True) |
Defining Custom Bundles
When no built-in bundle matches your interface, subclass Port:
from jitx.net import Port, DiffPair
class TXLink(Port):
"""Single TX differential pair."""
tx = DiffPair()
class PCIeLane(Port):
"""Single PCIe lane: TX + RX diff pairs."""
TX = DiffPair()
RX = DiffPair()
class PCIeLink(Port):
"""Multi-lane PCIe link."""
lane = [PCIeLane() for _ in range(4)]
class DDR4ByteLane(Port):
"""One DDR4 byte lane: DQ bits + strobe + mask."""
DQ = [Port() for _ in range(8)]
DQS = DiffPair()
DM = Port()
Rules for custom bundles:
- Subclass
Port, not Circuit or Component
- Sub-ports are class attributes (Port, DiffPair, or lists of them)
- Use lists for indexed groups:
DQ = [Port() for _ in range(8)]
- Nest bundles for hierarchical interfaces:
lane = [PCIeLane() for _ in range(4)]
The Provide/Require Architecture
Pin assignment uses a three-tier architecture:
Component Circuit Wrapper Application Circuit
┌──────────┐ ┌──────────────────┐ ┌───────────────────┐
│ GPIO[0] │←──────→│ @provide(GPIO) │ │ │
│ GPIO[1] │ maps │ maps bundle │◄─────│ .require(GPIO) │
│ GPIO[2] │ pins │ ports to pins │ │ gets a bundle │
│ GPIO[3] │ │ │ │ instance to wire │
└──────────┘ └──────────────────┘ └───────────────────┘
(physical) (declares flexibility) (consumes interface)
- Component — defines physical pins (
Port() class attributes)
- Circuit wrapper — wraps the component, declares what interfaces it can provide and which pins map to each
- Application circuit — calls
.require() to acquire an interface, then wires its sub-ports
The Mapping Return Type
Every @provide method returns a list of dictionaries. Each dictionary maps bundle sub-ports → component pins:
@provide(GPIO)
def provide_gpio(self, g: GPIO):
return [
{g.gpio: self.mcu.GPIO[0]},
{g.gpio: self.mcu.GPIO[1]},
]
@provide.one_of(I2C)
def provide_i2c(self, i2c: I2C):
return [
{i2c.sda: self.mcu.GPIO[0], i2c.scl: self.mcu.GPIO[1]},
{i2c.sda: self.mcu.GPIO[2], i2c.scl: self.mcu.GPIO[3]},
]
The keys are always sub-ports of the bundle parameter (g.gpio, i2c.sda, etc.).
The values are always component pins from self.<component>.<port>.
Carve-out from the anti-string-hacking rule
jitx/SKILL.md Don'ts say "don't key design state by hand-built strings" and "don't build intermediate list[dict[str, Any]] spec models." The @provide return type — list[dict[Port, Port]] — is the carve-out: the dict is keyed by Port objects (not strings), and the list is the framework's pin-mapping contract (not a parallel data model). Use this shape exactly when implementing @provide methods. Do not introduce string keys anywhere in the return value — keys are always Port objects from the bundle parameter. See jitx/references/architectural-patterns.md § "String-keyed dicts → structural objects" for the discriminator (key type), and § "Build the scene graph directly" for the general rule the carve-out exempts you from.
For a same-model self-critique pass on the pin-assignment code after writing, invoke jitx-skills:jitx-code-review. Optional for single-task use.
The Two APIs
Decorator API (preferred for static configurations)
from jitx.net import provide
class MCUCircuit(Circuit):
@provide(GPIO)
def provide_gpio(self, g: GPIO):
return [{g.gpio: pin} for pin in self.mcu.GPIO]
Constructor API (preferred for dynamic/computed port counts)
from jitx.net import Provide
class MCUCircuit(Circuit):
def __init__(self, num_gpio: int = 8):
self.mcu = MCU()
gpio_pins = self.mcu.GPIO[:num_gpio]
self.gpios = Provide(GPIO).all_of(
lambda g: [{g.gpio: pin} for pin in gpio_pins]
)
When to use which:
- Decorator: Port count is known at class definition time
- Constructor: Port count depends on
__init__ parameters or runtime values
Provider Patterns
@provide(Bundle) — Multiple independent offers (all_of)
Creates one provider per mapping. Each can be independently assigned. Use for GPIO, where any pin can independently serve as a GPIO.
@provide(GPIO)
def provide_gpio(self, g: GPIO):
return [{g.gpio: pin} for pin in self.mcu.GPIO]
@provide.one_of(Bundle) — Single selection from alternatives
Only ONE option from the returned list is selected. Use for peripheral pin muxing where an entire bus can appear on one pin group OR another, but not both.
@provide.one_of(I2C)
def provide_i2c(self, i2c: I2C):
return [
{i2c.sda: self.mcu.GPIO[0], i2c.scl: self.mcu.GPIO[1]},
{i2c.sda: self.mcu.GPIO[2], i2c.scl: self.mcu.GPIO[3]},
]
@provide.subset_of(Bundle, count) — N from M
From the full set of mappings, at most count may be assigned. Use for resource-constrained scenarios (e.g., limited current budget across GPIO).
@provide.subset_of(GPIO, 4)
def provide_gpio(self, g: GPIO):
"""8 GPIO pins available, but only 4 may be assigned."""
return [{g.gpio: pin} for pin in self.mcu.GPIO]
Constructor equivalents
self.gpios = Provide(GPIO).all_of(lambda g: [{g.gpio: p} for p in pins])
self.i2c = Provide(I2C).one_of(lambda i2c: [
{i2c.sda: self.mcu.GPIO[0], i2c.scl: self.mcu.GPIO[1]},
{i2c.sda: self.mcu.GPIO[2], i2c.scl: self.mcu.GPIO[3]},
])
self.gpios = Provide(GPIO).subset_of(4, lambda g: [{g.gpio: p} for p in pins])
Consuming Providers with require()
The consumer circuit calls .require(BundleType) on the provider circuit instance. This returns a bundle instance whose sub-ports can be wired with + or >>.
class AppCircuit(Circuit):
def __init__(self):
self.mcu_circuit = MCUCircuit()
gpio = self.mcu_circuit.require(GPIO)
self.led_net = gpio.gpio + self.led.anode
i2c = self.mcu_circuit.require(I2C)
self.sda_net = i2c.sda + self.sensor.SDA
self.scl_net = i2c.scl + self.sensor.SCL
Complete End-to-End Example
"""MCU with GPIO pin assignment driving 2 LEDs."""
from jitx import Circuit, Net
from jitx.common import GPIO, Power
from jitx.net import Port, provide
from jitxlib.parts import Resistor, Capacitor
class MCUComponent(jitx.Component):
"""8-pin MCU — physical component with pins."""
VCC = Port()
GND = Port()
GPIO = [Port() for _ in range(4)]
RESET = Port()
NC = Port()
class MCUCircuit(Circuit):
"""Wrapper that declares pin flexibility via @provide."""
power = Power()
@provide(GPIO)
def provide_gpio(self, g: GPIO):
"""Any of 4 GPIO pins can independently serve as GPIO."""
return [{g.gpio: pin} for pin in self.mcu.GPIO]
def __init__(self):
self.mcu = MCUComponent()
self.VCC = Net(name="VCC")
self.GND = Net(name="GND")
self.VCC += self.power.Vp + self.mcu.VCC
self.GND += self.power.Vn + self.mcu.GND
self.c_bypass = Capacitor(capacitance=100e-9)
self.c_bypass.insert(self.mcu.VCC, self.mcu.GND)
self.r_reset = Resistor(resistance=10e3)
self.r_reset.insert(self.mcu.VCC, self.mcu.RESET)
class LEDDriverApp(Circuit):
"""Application that consumes GPIOs to drive LEDs."""
vin = Power()
def __init__(self):
self.GND = Net(name="GND")
self.VCC = Net(name="VCC")
self.GND += self.vin.Vn
self.VCC += self.vin.Vp
self.mcu = MCUCircuit()
self.VCC += self.mcu.power.Vp
self.GND += self.mcu.power.Vn
self.r_leds = []
for i in range(2):
gpio = self.mcu.require(GPIO)
r = Resistor(resistance=330.0)
self.r_leds.append(r)
r.insert(gpio.gpio, ...)
Device = LEDDriverApp
Hierarchical Provider Composition
Providers that internally require from sub-providers. Use for peripheral muxing where signals (SDA, SCL) can be independently selected from different pin options.
class MCUCircuit(Circuit):
power = Power()
class I2C_SDA(Port):
p = Port()
class I2C_SCL(Port):
p = Port()
@provide.one_of(I2C_SDA)
def provide_sda(self, sda: I2C_SDA):
return [{sda.p: self.mcu.GPIO[0]}, {sda.p: self.mcu.GPIO[2]}]
@provide.one_of(I2C_SCL)
def provide_scl(self, scl: I2C_SCL):
return [{scl.p: self.mcu.GPIO[1]}, {scl.p: self.mcu.GPIO[3]}]
@provide(I2C)
def provide_i2c(self, i2c: I2C):
sda = self.require(self.I2C_SDA)
scl = self.require(self.I2C_SCL)
return [{i2c.sda: sda.p, i2c.scl: scl.p}]
def __init__(self):
self.mcu = MCUComponent()
This creates 4 possible I2C configurations (2 SDA options x 2 SCL options) that the layout engine evaluates simultaneously.
Key rules:
- Inner Port classes are defined as nested classes on the Circuit
self.require() inside @provide consumes the circuit's own providers
- The layout engine resolves the full constraint tree simultaneously
DiffPair P/N Polarity Swapping
Only model P/N swap when the part or protocol explicitly supports it. See references/protocol-pin-flexibility.md for per-protocol rules.
When the component has individual P/N pins (not a DiffPair bundle), the Provide mapping can offer both polarities:
class TXLink(Port):
tx = DiffPair()
class FlexTXCircuit(Circuit):
@provide.one_of(TXLink)
def provide_tx(self, link: TXLink):
return [
{link.tx.p: self.ic.TXP, link.tx.n: self.ic.TXN},
{link.tx.p: self.ic.TXN, link.tx.n: self.ic.TXP},
]
Topology and Constraints on Pin-Assigned Ports
Pin assignment and SI constraints compose naturally. The pattern is:
require() to get ports from a provider
>> to build topology on those ports
Constrain / ConstrainDiffPair / ConstrainReferenceDifference to apply SI constraints
Basic pattern: DiffPair provide + topology + constraint
class App(Circuit):
def __init__(self):
self.src = FlexTXCircuit()
self.dst = DiffPairReceiver()
tx = self.src.require(TXLink)
self += tx.tx.p >> self.dst.INP.p
self += tx.tx.n >> self.dst.INP.n
topo = Topology(tx.tx, self.dst.INP)
with ReferencePlanes(self.GND):
self.dp_cst = ConstrainDiffPair(topo).timing_difference(5e-12)
PCIe pattern: lane providers + per-lane constraints
link = self.switch.require(PCIeLink)
dp_cst = DiffPairConstraint(skew=Toleranced(0, 5e-12), loss=3.0)
with ReferencePlanes(self.GND):
for i in range(num_lanes):
self += link.lane[i].TX.p >> self.endpoints[i].INP.p
self += link.lane[i].TX.n >> self.endpoints[i].INP.n
dp_cst.constrain(link.lane[i].TX, self.endpoints[i].INP)
DDR4 pattern: byte swap + DQ-to-DQS matching
data = self.controller.require(DDR4Data)
with ReferencePlanes(self.GND):
for bl in range(2):
offset = bl * bits_per_lane
self += data.byte_lane[bl].DQS.p >> self.mem.DQS_P[bl]
self += data.byte_lane[bl].DQS.n >> self.mem.DQS_N[bl]
dqs_topo = Topology(data.byte_lane[bl].DQS.p, self.mem.DQS_P[bl])
dq_topos = []
for i in range(bits_per_lane):
self += data.byte_lane[bl].DQ[i] >> self.mem.DQ[offset + i]
dq_topos.append(
Topology(data.byte_lane[bl].DQ[i], self.mem.DQ[offset + i])
)
ConstrainReferenceDifference(
guide=dqs_topo,
topologies=dq_topos,
).timing_difference(Toleranced(0, 20e-12))
PCIe Lane Flexibility
Model PCIe width variants and lane flexibility using the constructor Provide API. The component has individual P/N pins per lane; the Provide mapping connects them to DiffPair bundle sub-ports.
class PCIeSwitchCircuit(Circuit):
def __init__(self):
self.sw = PCIeSwitchComponent()
self.pcie_x4 = Provide(PCIeLink4).one_of(
lambda b: [self._create_mapping(b, lane_offset=0)]
)
self.pcie_x2 = Provide(PCIeLink2).one_of(
lambda b: [
self._create_mapping(b, lane_offset=0),
self._create_mapping(b, lane_offset=2),
]
)
def _create_mapping(self, b, lane_offset: int) -> dict:
mapping = {}
for i in range(len(b.lane)):
mapping[b.lane[i].TX.p] = self.sw.PTXP[i + lane_offset]
mapping[b.lane[i].TX.n] = self.sw.PTXN[i + lane_offset]
mapping[b.lane[i].RX.p] = self.sw.PRXP[i + lane_offset]
mapping[b.lane[i].RX.n] = self.sw.PRXN[i + lane_offset]
return mapping
DDR4 Byte/Bit Swapping
DDR4 controllers often support byte lane reordering and bit swapping within a byte lane. Model with Provide().one_of():
class DDR4ControllerCircuit(Circuit):
def __init__(self):
self.ctrl = DDR4ControllerComponent()
self.data_provide = Provide(DDR4Data).one_of(
lambda b: [
self._create_mapping(b, byte_swap=False),
self._create_mapping(b, byte_swap=True),
]
)
def _create_mapping(self, b: DDR4Data, byte_swap: bool) -> dict:
mapping = {}
phys = [1, 0] if byte_swap else [0, 1]
for logical in range(2):
p = phys[logical]
for i in range(8):
mapping[b.byte_lane[logical].DQ[i]] = self.ctrl.DQ[p * 8 + i]
mapping[b.byte_lane[logical].DQS.p] = self.ctrl.DQS_P[p]
mapping[b.byte_lane[logical].DQS.n] = self.ctrl.DQS_N[p]
mapping[b.byte_lane[logical].DM] = self.ctrl.DM[p]
return mapping
Important rules:
- DQS must stay with its byte lane (DQS0 always with byte lane 0's DQ bits)
- Address and command signals are NOT swappable — use fixed wiring
- CK polarity is NOT swappable — use fixed wiring
For protocol-specific swap rules and constraint parameters, see references/protocol-pin-flexibility.md.
Common Mistakes
@provide(GPIO)
def provide_gpio(self, g: GPIO):
return self.mcu.GPIO[0]
@provide(GPIO)
def provide_gpio(self, g: GPIO):
return [{g.gpio: self.mcu.GPIO[0]}]
gpio = self.mcu.require(GPIO)
self.led_pin = gpio.gpio
self.led_net = gpio.gpio + self.led.anode
@provide.one_of(GPIO)
def provide_gpio(self, g: GPIO):
return [{g.gpio: p} for p in self.mcu.GPIO]
@provide(GPIO)
def provide_gpio(self, g: GPIO):
return [{g.gpio: p} for p in self.mcu.GPIO]
tx = self.src.require(TXLink)
tx.tx.p >> self.dst.INP.p
self += tx.tx.p >> self.dst.INP.p
tx = self.src.require(TXLink)
topo = Topology(tx.tx, self.dst.INP)
self += tx.tx.p >> self.dst.INP.p
self += tx.tx.p >> self.dst.INP.p
self += tx.tx.n >> self.dst.INP.n
topo = Topology(tx.tx, self.dst.INP)
@provide.one_of(DDR4DQS)
def provide_dqs(self, dqs: DDR4DQS):
return [
{dqs.p: self.ctrl.DQS_P, dqs.n: self.ctrl.DQS_N},
{dqs.p: self.ctrl.DQS_N, dqs.n: self.ctrl.DQS_P},
]
Verification
Step 1: Type Check
pyright path/to/circuit.py
Step 2: Build Test
jitx build <module.path.DesignClass>
Don't run parallel JITX builds against the same project — sequence them. See jitx/SKILL.md "Build Safety".
Pin assignment errors appear as "Unsatisfiable pin assignment" in the Issues List. Constraint violations appear under "Unsatisfied Signal Constraints".
API Reference
For complete class definitions, all parameters, and method signatures:
For protocol-specific pin flexibility rules, see references/protocol-pin-flexibility.md.
Formatting
ruff format path/to/circuit.py