SI Usage Patterns#

The objects and methods discussed in Topologies and Constraints are often considered the “primitives” of the JITX Signal Integrity (SI) system. These primitives must be strung together in a particular way to achieve the desired results. To make this process easier, we’ve developed various higher level constructs to support differential pairs and other more complex protocols.

Signal Constraint Base Class#

The SignalConstraint object is a base class for defining a modular set of constraints that apply to a particular protocol. This base class has a type parameter that defines the bundle type that a particular signal constraint applies to. This type parameter allows the language server to confirm that you, the user, are using this constraint correctly.

The easiest example to look at is the DiffPairConstraint. Here is an excerpt:

class DiffPairConstraint(SignalConstraint[DiffPair]):
    """Basic signal constraint for a differential pair signal topology.
       ...
    """

    def __init__(
        self,
        skew: Toleranced,
        loss: float,
        structure: DifferentialRoutingStructure | None = None,
    ):
    ...

Notice that the parent class for DiffPairConstraint is SignalConstraint[DiffPair]. This is indicating that this type only applies to topologies of the type DiffPair.

The DiffPairConstraint object is a helpful abstraction for applying all of the necessary constraints for a typical differential pair. This includes:

  1. skew - Constrains the time delay between the p and n signals of the DiffPair bundle.

  2. loss - Maximum acceptable loss from endpoint to endpoint.

  3. structure - Applies a medium constraint for this diff pair.

This constraint doesn’t handle any inter-topology constraints - it only handles the constraints that apply to a differential pair. Deriving from this class would allow us to add additional constraints - such as a timing delay constraint - for custom applications.

Here is a simple example:


  def __init__(self):
    ...

    diff_rs = current.substrate.differential_routing_structure(100.0)

    self.ck_cst = DiffPairConstraint(
      skew=Toleranced(0, 1e-12), # Seconds
      loss=12.0, # dB
      structure=diff_rs
    )

    self += src.CK >> dst.CK

    self.ck_cst.constrain(src.CK, dst.CK)

The main pieces are:

  1. Create the DiffPairConstraint object and attach it to the circuit via self.ck_cst = ...

  2. Create the topology with the >> operator.

  3. Apply the self.ck_cst constraint by defining the endpoints of the topology.

    1. Notice that src.CK and dst.CK must be the same port type, DiffPair.

Custom SignalConstraint Example#

To create a constraint for a custom protocol, subclass SignalConstraint[T] where T is the port bundle type. Here is an example for a serial link with TX and RX differential pairs:

from dataclasses import dataclass
from jitx import current
from jitx.net import DiffPair, Port
from jitx.si import (
    DiffPairConstraint,
    DifferentialRoutingStructure,
    SignalConstraint,
)
from jitx.toleranced import Toleranced


class SerialLink(Port):
    """Port bundle for a serial link with TX and RX differential pairs."""
    tx = DiffPair()
    rx = DiffPair()


@dataclass(frozen=True)
class SerialStandard:
    """Parameters for a serial link standard."""
    skew: Toleranced = Toleranced(0, 5e-12)  # max 5ps intra-pair skew
    loss: float = 12.0                        # max 12dB insertion loss
    impedance: Toleranced = Toleranced(100, 10)  # 100 ohm +/- 10%


class SerialLinkConstraint(SignalConstraint["SerialLink"]):
    """Applies differential pair constraints to both TX and RX paths."""

    def __init__(
        self,
        standard: SerialStandard,
        structure: DifferentialRoutingStructure | None = None,
    ):
        if structure is None:
            structure = current.substrate.differential_routing_structure(
                standard.impedance
            )
        self._dp_cst = DiffPairConstraint(
            skew=standard.skew,
            loss=standard.loss,
            structure=structure,
        )

    def constrain(self, src: SerialLink, dst: SerialLink):
        # TX-to-RX crossover: src transmits, dst receives (and vice versa)
        self._dp_cst.constrain(src.tx, dst.rx)
        self._dp_cst.constrain(dst.tx, src.rx)

Usage in a circuit:

import jitx

class MyCircuit(jitx.Circuit):
    def __init__(self):
        ...
        std = SerialStandard()
        cst = SerialLinkConstraint(std)

        # Create topologies
        self += self.fpga.serial.tx >> self.phy.serial.rx
        self += self.phy.serial.tx >> self.fpga.serial.rx

        # Apply constraints to both directions at once
        cst.constrain(self.fpga.serial, self.phy.serial)

The SignalConstraint pattern allows you to encapsulate all the constraints for a protocol into a single reusable class. The type parameter (SerialLink in this case) enables the language server to verify that the constraint is applied to the correct port types.

More Complex Protocols#

This model extends to more complex protocols. For example, consider the RGMII Protocol Constraint. This constraint type is defined as an internal type to the RGMII bundle type. The RGMII.Standard.Constraint encapsulates the constraints for an RGMII interface and makes constraint application a single method call. This allows for highly re-usable constraints.

Signal Ends#

Often times when constructing a sub-circuit, the actual endpoint of a topology is buried inside that sub-circuit (or recursively within a sub-sub-circuit). This makes it difficult to apply the routing structure to the terminal endpoints. This manifests in a section of a trace or differential pair that lacks a routing structure assignment. For example, consider the following:

TODO