JITX Pin Assignment Patterns is a development Claude Skill built by Diego Rodrigues de Sa e Souza. Best for: Hardware engineers using JITX programmatically define flexible pin assignments for MCUs, FPGAs, DDR controllers, and PCIe interfaces to let layout engines optimize routing..
Model flexible pin mappings in JITX designs using provide/require decorators, peripheral muxing, and SI constraints.
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 setup is handled by the base jitx skill. Ensure it has been invoked first.
# Pin assignment imports
from jitx.net import Port, DiffPair, provide, Provide
# Common bundles
from jitx.common import Power, GPIO
# Protocol bundles (for peripheral muxing)
from jitxlib.protocols.serial import I2C, SPI, UART
# SI constraint imports (for topology + constraint composition)
from jitx.si import (
Constrain,
ConstrainDiffPair,
ConstrainReferenceDifference,
DiffPairConstraint,
ReferencePlanes,
)
from jitx.net import Topology
# Core framework
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.netProvide (uppercase, constructor class) is in jitx.net (also re-exported from jitx)GPIO is in jitx.commonI2C, SPI, UART are in jitxlib.protocols.serialjitx.siUse pin assignment when a component's physical pins can validly serve more than one logical role, and you want the layout engine to choose the optimal mapping.
Use pin assignment for:
Use fixed wiring instead when:
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.
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) |
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:
Port, not Circuit or ComponentDQ = [Port() for _ in range(8)]lane = [PCIeLane() for _ in range(4)]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)
Port() class attributes).require() to acquire an interface, then wires its sub-portsEvery @provide method returns a list of dictionaries. Each dictionary maps bundle sub-ports → component pins:
@provide(GPIO)
def provide_gpio(self, g: GPIO):
# g is a GPIO bundle instance — it has a .gpio sub-port
# Each dict maps {bundle_sub_port: component_pin}
return [
{g.gpio: self.mcu.GPIO[0]}, # Option 1: GPIO[0] fulfills this GPIO
{g.gpio: self.mcu.GPIO[1]}, # Option 2: GPIO[1] fulfills this GPIO
]
@provide.one_of(I2C)
def provide_i2c(self, i2c: I2C):
# i2c has .sda and .scl — map BOTH in each option
return [
{i2c.sda: self.mcu.GPIO[0], i2c.scl: self.mcu.GPIO[1]}, # Option A
{i2c.sda: self.mcu.GPIO[2], i2c.scl: self.mcu.GPIO[3]}, # Option B
]
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>.
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]
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:
__init__ parameters or runtime values@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 alternativesOnly 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]}, # Option A
{i2c.sda: self.mcu.GPIO[2], i2c.scl: self.mcu.GPIO[3]}, # Option B
]
@provide.subset_of(Bundle, count) — N from MFrom 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]
# all_of (same as @provide)
self.gpios = Provide(GPIO).all_of(lambda g: [{g.gpio: p} for p in pins])
# one_of (same as @provide.one_of)
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]},
])
# subset_of (same as @provide.subset_of)
self.gpios = Provide(GPIO).subset_of(4, lambda g: [{g.gpio: p} for p in pins])
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()
# Request a GPIO — returns a GPIO bundle instance
gpio = self.mcu_circuit.require(GPIO)
# Wire using the bundle's sub-ports with + (net) operator
self.led_net = gpio.gpio + self.led.anode
# Request an I2C — returns an I2C bundle instance
i2c = self.mcu_circuit.require(I2C)
# Wire both sub-ports
self.sda_net = i2c.sda + self.sensor.SDA
self.scl_net = i2c.scl + self.sensor.SCL
"""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()
# ... landpattern, symbol, mapping ...
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
# Instantiate the provider circuit
self.mcu = MCUCircuit()
self.VCC += self.mcu.power.Vp
self.GND += self.mcu.power.Vn
# Consume GPIOs — layout engine decides which physical pins
for i in range(2):
gpio = self.mcu.require(GPIO)
r = Resistor(resistance=330.0)
setattr(self, f"r_led{i}", r)
r.insert(gpio.gpio, ...) # wire to LED anode
Device = LEDDriverApp
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()
# Step 1: Define inner bundle types for each muxed signal
class I2C_SDA(Port):
p = Port()
class I2C_SCL(Port):
p = Port()
# Step 2: Provide each signal independently with one_of
@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]}]
# Step 3: Compose into the real I2C bundle
@provide(I2C)
def provide_i2c(self, i2c: I2C):
sda = self.require(self.I2C_SDA) # self.require() consumes own providers
scl = self.require(self.I2C_SCL)
return [{i2c.sda: sda.p, i2c.scl: scl.p}]
def __init__(self):
self.mcu = MCUComponent()
# ... power wiring ...
This creates 4 possible I2C configurations (2 SDA options x 2 SCL options) that the layout engine evaluates simultaneously.
Key rules:
self.require() inside @provide consumes the circuit's own providersOnly 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}, # Normal
{link.tx.p: self.ic.TXN, link.tx.n: self.ic.TXP}, # Swapped
]
Pin assignment and SI constraints compose naturally. The pattern is:
require() to get ports from a provider>> to build topology on those portsConstrain / ConstrainDiffPair / ConstrainReferenceDifference to apply SI constraintsclass App(Circuit):
def __init__(self):
self.src = FlexTXCircuit() # has @provide.one_of(TXLink)
self.dst = DiffPairReceiver()
tx = self.src.require(TXLink)
# Build topology with >>
self += tx.tx.p >> self.dst.INP.p
self += tx.tx.n >> self.dst.INP.n
# Constrain — create >> FIRST, then identify with Topology
topo = Topology(tx.tx, self.dst.INP)
with ReferencePlanes(self.GND):
self.dp_cst = ConstrainDiffPair(topo).timing_difference(5e-12)
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)
data = self.controller.require(DDR4Data)
with ReferencePlanes(self.GND):
for bl in range(2):
offset = bl * bits_per_lane
# DQS topology (reference signal for matching)
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 topologies
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])
)
# Match DQ to DQS within 20ps per byte lane
ConstrainReferenceDifference(
guide=dqs_topo,
topologies=dq_topos,
).timing_difference(Toleranced(0, 20e-12))
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()
# x4 link using all 4 lanes
self.pcie_x4 = Provide(PCIeLink4).one_of(
lambda b: [self._create_mapping(b, lane_offset=0)]
)
# x2 links: lanes 0-1 or lanes 2-3
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 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:
For protocol-specific swap rules and constraint parameters, see references/protocol-pin-flexibility.md.
# WRONG: Returning the port directly instead of a mapping list
@provide(GPIO)
def provide_gpio(self, g: GPIO):
return self.mcu.GPIO[0] # WRONG: must return list of dicts
# CORRECT: Return list of {bundle_port: component_port} dicts
@provide(GPIO)
def provide_gpio(self, g: GPIO):
return [{g.gpio: self.mcu.GPIO[0]}]
# WRONG: Using = to connect required ports (Python assignment)
gpio = self.mcu.require(GPIO)
self.led_pin = gpio.gpio # WRONG: assignment, not connection
# CORRECT: Use + to create a net
self.led_net = gpio.gpio + self.led.anode
# WRONG: Using @provide.one_of when you want multiple instances
@provide.one_of(GPIO) # Only gives ONE GPIO total
def provide_gpio(self, g: GPIO):
return [{g.gpio: p} for p in self.mcu.GPIO]
# CORRECT: Use @provide (all_of) for multiple independent instances
@provide(GPIO)
def provide_gpio(self, g: GPIO):
return [{g.gpio: p} for p in self.mcu.GPIO]
# WRONG: Topology not stored (silently dropped)
tx = self.src.require(TXLink)
tx.tx.p >> self.dst.INP.p # BAD: topology lost!
# CORRECT: Store with +=
self += tx.tx.p >> self.dst.INP.p
# WRONG: Creating Topology before creating >> connection
tx = self.src.require(TXLink)
topo = Topology(tx.tx, self.dst.INP) # Path doesn't exist yet!
self += tx.tx.p >> self.dst.INP.p
# CORRECT: Create >> first, then identify with Topology
self += tx.tx.p >> self.dst.INP.p
self += tx.tx.n >> self.dst.INP.n
topo = Topology(tx.tx, self.dst.INP)
# WRONG: Modeling P/N swap for a protocol that doesn't support it
# DDR4 DQS polarity is FIXED — never swap
@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}, # WRONG: not swappable
]
pyright path/to/circuit.py
python -m jitx build <module.path.DesignClass>
Pin assignment errors appear as "Unsatisfiable pin assignment" in the Issues List. Constraint violations appear under "Unsatisfied Signal Constraints".
For complete class definitions, all parameters, and method signatures:
For protocol-specific pin flexibility rules, see references/protocol-pin-flexibility.md.
ruff format path/to/circuit.py
/plugin install jitx-pin-assignment-patterns@diegosouzapwRequires Claude Code CLI.
Hardware engineers using JITX programmatically define flexible pin assignments for MCUs, FPGAs, DDR controllers, and PCIe interfaces to let layout engines optimize routing.
No reviews yet. Be the first to review this skill.