Source code for jitxexamples.protocols.memory.ddr4

"""
DDR4
====

Port bundles and signal-integrity constraints for a point-to-point DDR4 link
between a controller and one memory device.

Source and scope
----------------

The defaults are Table 116, "Skew Matching Guidelines for DDR4 Discrete
Topologies", in Altera, *External Memory Interfaces Agilex 7 F-Series and
I-Series FPGA IP User Guide* (683216, version 23.2), section 6.5.6.4, "Skew
Matching Guidelines for DDR4 Discrete Configurations",
https://docs.altera.com/r/docs/683216/23.2/external-memory-interfaces-agilextm-7-f-series-and-i-series-fpga-ip-user-guide/skew-matching-guidelines-for-ddr4-discrete-configurations.
That section covers single-rank x8 and single-rank x16 discrete
(device-down) topologies. The module accepts other widths and ranks, but
the table does not cover them, and it does not support several memories
on one bus (see Fly-by below).

- CK to CK#: < 1 ps (applied as +/-1 ps)
- DQS to DQS#: < 1 ps (applied as +/-1 ps)
- DQ to DQS within a byte: -3.5 ps < DQ - DQS < 3.5 ps (applied to DQ and
  DM_n)
- CMD/ADDR/CTRL to clock: -20 ps < CLK - CMD/ADDR/CTRL < 20 ps
- CMD/ADDR/CTRL among themselves within each channel: < 20 ps. The
  constraint holds each of them within +/-10 ps of CKE[0], so any two
  differ by at most 20 ps. That is stricter than the table, which allows
  every other signal to sit together 15 ps from CKE[0]; the constraint fails
  that layout.
- DQS to CLK: -85 ps < CLK - DQS < 935 ps. On jitx 4.6,
  ``ConstrainReferenceDifference(guide, target)`` measures guide delay minus
  target delay (checked on a code-routed pair), so CK is the guide and the
  window is used as written.

The constraint applies the CMD/ADDR/CTRL rules to the address bits (A0 to
A16 in this bundle; A14 to A16 double as WE_n, CAS_n and RAS_n), BA, BG,
ACT_n, CKE, CS_n, ODT and PAR. The section says ALERT_n needs no skew
matching. It does not name RESET_n; this module gives RESET_n no skew limit.
Both get the routing structure and the optional loss limit.

The section also asks for skew matching in time rather than length:
"Include both package per-pin skew and PCB delay when performing skew
matching." The design-issue report counts a package's delay only where its
pins carry a :py:class:`~jitx.si.TerminatingPinModel`, so give the
controller's pins one from its package delay data. The controller in
:py:mod:`.ddr4_example` is fictional and has none, so its numbers would be
PCB delay only. The table gives no rule between CK
pairs, no insertion-loss limit and no impedance target, so the module sets
none of them: pass ``loss`` in the parameter classes to add a loss limit,
and pass the routing structures from the board's substrate.

Use
---

Net the controller's bundle to the memory's bundle with an ordinary net and
apply the constraint to the same two bundles::

    self.link = self.controller.ddr4 + self.memory.ddr4
    self.si = DDR4Constraint(
        ck_structure=..., dqs_structure=..., dq_structure=..., acc_structure=...
    )
    self.si.constrain(self.controller.ddr4, self.memory.ddr4)

The issue report measures a constraint only once the path between its two ends
is routed.

Fly-by
------

With several memories on one address/command bus, the bus is a daisy chain:
controller, first memory, second memory. This module does not support that:
its constraint covers one controller-to-memory link, and the cited table
covers discrete single-device topologies only. In a fly-by design, the chain
order is part of the design, and ``>>`` (``TopologyNet``) declares it.
"""

from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass
from enum import Enum

from jitx.net import DiffPair, Port
from jitx.si import (
    Constrain,
    ConstrainReferenceDifference,
    DifferentialRoutingStructure,
    ConstrainDiffPair,
    RoutingStructure,
    SignalConstraint,
    Topology,
)
from jitx.toleranced import Toleranced


[docs] class DDR4Width(Enum): """DDR4 data width: the number of DQ signals.""" x4 = 4 "4 bits, one byte lane" x8 = 8 "8 bits, one byte lane" x16 = 16 "16 bits, two byte lanes" x32 = 32 "32 bits, four byte lanes" x64 = 64 "64 bits, eight byte lanes" x72 = 72 "72 bits (64 plus ECC), nine byte lanes"
[docs] def num_byte_lanes(width: DDR4Width) -> int: """Return the number of byte lanes (DQS pairs) for a data width.""" return max(1, width.value // 8)
[docs] class DDR4Rank(Enum): """DDR4 rank count: the number of CS_n, CKE and ODT signals.""" SingleRank = 1 "One rank" DualRank = 2 "Two ranks" QuadRank = 4 "Four ranks"
[docs] class DDR4DataChannel(Port): """DDR4 data signals: DQ, one DQS pair and one DM_n per byte lane. Args: width: Data width. """ DQ: Sequence[Port] "Data bits" DQS: Sequence[DiffPair] "Data strobes, one per byte lane (``p`` is DQS_t, ``n`` is DQS_c)" DM_n: Sequence[Port] "Data masks, one per byte lane" def __init__(self, width: DDR4Width) -> None: lanes = num_byte_lanes(width) self.DQ = tuple(Port() for _ in range(width.value)) self.DQS = tuple(DiffPair() for _ in range(lanes)) self.DM_n = tuple(Port() for _ in range(lanes))
[docs] class DDR4AccChannel(Port): """DDR4 address, command, control and clock signals. Args: rank: Rank count, which sets the number of CKE, CS_n and ODT signals. ck_count: Number of CK pairs. bg_count: Number of bank-group signals (one on x16 parts). ba_count: Number of bank-address signals. """ CK: Sequence[DiffPair] "Clock pairs (``p`` is CK_t, ``n`` is CK_c)" CKE: Sequence[Port] "Clock enable, one per rank" A: Sequence[Port] "Address A0 to A16 (A14 to A16 double as WE_n, CAS_n, RAS_n)" ACT_n = Port() "Activate command" BG: Sequence[Port] "Bank group" BA: Sequence[Port] "Bank address" CS_n: Sequence[Port] "Chip select, one per rank" RESET_n = Port() "Reset" ODT: Sequence[Port] "On-die termination, one per rank" PAR = Port() "Command and address parity" ALERT_n = Port() "Alert" def __init__( self, rank: DDR4Rank, ck_count: int = 1, bg_count: int = 2, ba_count: int = 2, ) -> None: self.CK = tuple(DiffPair() for _ in range(ck_count)) self.CKE = tuple(Port() for _ in range(rank.value)) self.A = tuple(Port() for _ in range(17)) self.BG = tuple(Port() for _ in range(bg_count)) self.BA = tuple(Port() for _ in range(ba_count)) self.CS_n = tuple(Port() for _ in range(rank.value)) self.ODT = tuple(Port() for _ in range(rank.value))
[docs] class DDR4(Port): """DDR4 interface: data channel plus address/command/control channel. Args: width: Data width. rank: Rank count. ck_count: Number of CK pairs. bg_count: Number of bank-group signals. ba_count: Number of bank-address signals. """ data: DDR4DataChannel "Data channel" acc: DDR4AccChannel "Address, command, control and clock channel" def __init__( self, width: DDR4Width, rank: DDR4Rank = DDR4Rank.SingleRank, ck_count: int = 1, bg_count: int = 2, ba_count: int = 2, ) -> None: self.data = DDR4DataChannel(width) self.acc = DDR4AccChannel(rank, ck_count, bg_count, ba_count)
[docs] def signal_ports(bus: DDR4) -> list[Port]: """Return every single-pin port of ``bus`` in a fixed order. Data first (DQ, DQS p and n per lane, DM_n), then CK p and n per pair, CKE, A, ACT_n, BG, BA, CS_n, RESET_n, ODT, PAR and ALERT_n. Two bundles built with the same arguments give lists that line up signal for signal. """ d, a = bus.data, bus.acc ports: list[Port] = [*d.DQ] for dqs in d.DQS: ports += [dqs.p, dqs.n] ports += [*d.DM_n] for ck in a.CK: ports += [ck.p, ck.n] ports += [*a.CKE, *a.A, a.ACT_n, *a.BG, *a.BA, *a.CS_n, a.RESET_n] ports += [*a.ODT, a.PAR, a.ALERT_n] return ports
[docs] @dataclass(frozen=True) class DDR4DataConstraintParams: """DDR4 data-channel limits. Defaults are the cited rules in this module. Attributes: skew_dqs: DQS intra-pair skew. skew_dq_dqs: DQ and DM_n to DQS.P skew within a byte lane. loss: Maximum insertion loss in dB for DQ, DM_n and DQS, or ``None`` for no loss limit. """ skew_dqs: Toleranced = Toleranced(0, 1.0e-12) skew_dq_dqs: Toleranced = Toleranced(0, 3.5e-12) loss: float | None = None
[docs] @dataclass(frozen=True) class DDR4AccConstraintParams: """DDR4 address/command/control limits. Defaults are the cited rules. Attributes: skew_ck: CK intra-pair skew. skew_acc_ck: CK.P delay minus address, command and control delay. spread_acc: Largest delay difference among the address, command and control signals of the channel, in seconds. Applied as +/- half of it around CKE[0]. loss: Maximum insertion loss in dB for CK and the address, command and control signals, or ``None`` for no loss limit. """ skew_ck: Toleranced = Toleranced(0, 1.0e-12) skew_acc_ck: Toleranced = Toleranced(0, 20.0e-12) spread_acc: float = 20.0e-12 loss: float | None = None def __post_init__(self) -> None: if self.spread_acc <= 0: raise ValueError(f"spread_acc must be positive, got {self.spread_acc}")
[docs] @dataclass(frozen=True) class DDR4DataAccConstraintParams: """DDR4 data-to-clock limit. The default is the cited rule. Attributes: skew_ck_dqs: CK.P delay minus DQS.P delay. """ skew_ck_dqs: Toleranced = Toleranced.min_max(-85.0e-12, 935.0e-12)
def _single_ended( loss: float | None, structure: RoutingStructure, topo: Topology ) -> Constrain: constrain = Constrain(topo) constrain.structure(structure) if loss is not None: constrain.insertion_loss(loss) return constrain def _diff_pair( loss: float | None, skew: Toleranced, structure: DifferentialRoutingStructure, src: DiffPair, dst: DiffPair, ) -> ConstrainDiffPair: constrain = ConstrainDiffPair(Topology(src, dst)).timing_difference(skew) constrain.structure(structure) if loss is not None: constrain.insertion_loss(loss) return constrain
[docs] class DDR4DataConstraint(SignalConstraint[DDR4DataChannel]): """Constraint for the DDR4 data channel. Per byte lane: DQS intra-pair skew, and DQ and DM_n skew against DQS.P. Byte lane ``i`` holds DQ ``8 * i`` to ``8 * i + 7``. Args: dqs_structure: Differential routing structure for DQS. dq_structure: Single-ended routing structure for DQ and DM_n. params: Limits. Defaults to :py:class:`DDR4DataConstraintParams`. """ def __init__( self, dqs_structure: DifferentialRoutingStructure, dq_structure: RoutingStructure, params: DDR4DataConstraintParams | None = None, ) -> None: super().__init__() self.params = params or DDR4DataConstraintParams() self.dqs_structure = dqs_structure self.dq_structure = dq_structure
[docs] def constrain(self, src: DDR4DataChannel, dst: DDR4DataChannel) -> None: """Apply the data-channel constraints between two netted channels. Raises: ValueError: If the channels differ in width. """ if len(src.DQ) != len(dst.DQ): raise ValueError( f"DDR4 width mismatch: {len(src.DQ)} DQ against {len(dst.DQ)}" ) p = self.params lanes = zip(src.DQS, dst.DQS, src.DM_n, dst.DM_n, strict=True) for i, (s_dqs, d_dqs, s_dm, d_dm) in enumerate(lanes): self.add(_diff_pair(p.loss, p.skew_dqs, self.dqs_structure, s_dqs, d_dqs)) guide = Topology(s_dqs.p, d_dqs.p) bits = range(8 * i, min(8 * i + 8, len(src.DQ))) topos = [Topology(src.DQ[j], dst.DQ[j]) for j in bits] topos.append(Topology(s_dm, d_dm)) self.add( ConstrainReferenceDifference(guide, topos).timing_difference( p.skew_dq_dqs ) ) for topo in topos: self.add(_single_ended(p.loss, self.dq_structure, topo))
[docs] class DDR4AccConstraint(SignalConstraint[DDR4AccChannel]): """Constraint for the DDR4 address, command, control and clock channel. Applies CK intra-pair skew to every CK pair, then holds CKE, A, ACT_n, BG, BA, CS_n, ODT and PAR within ``skew_acc_ck`` of CK[0].P and within half of ``spread_acc`` of CKE[0]. RESET_n and ALERT_n get the routing structure and loss limit only. There is no limit between CK pairs, since the cited table gives none. Args: ck_structure: Differential routing structure for CK. acc_structure: Single-ended routing structure for the other signals. params: Limits. Defaults to :py:class:`DDR4AccConstraintParams`. """ def __init__( self, ck_structure: DifferentialRoutingStructure, acc_structure: RoutingStructure, params: DDR4AccConstraintParams | None = None, ) -> None: super().__init__() self.params = params or DDR4AccConstraintParams() self.ck_structure = ck_structure self.acc_structure = acc_structure
[docs] def constrain(self, src: DDR4AccChannel, dst: DDR4AccChannel) -> None: """Apply the address/command/control constraints between two channels.""" p = self.params guide_ck = Topology(src.CK[0].p, dst.CK[0].p) for s_ck, d_ck in zip(src.CK, dst.CK, strict=True): self.add(_diff_pair(p.loss, p.skew_ck, self.ck_structure, s_ck, d_ck)) guide_cke = Topology(src.CKE[0], dst.CKE[0]) self.add(_single_ended(p.loss, self.acc_structure, guide_cke)) timed: list[tuple[Port, Port]] = [ *zip(src.CKE[1:], dst.CKE[1:], strict=True), *zip(src.A, dst.A, strict=True), (src.ACT_n, dst.ACT_n), *zip(src.BG, dst.BG, strict=True), *zip(src.BA, dst.BA, strict=True), *zip(src.CS_n, dst.CS_n, strict=True), *zip(src.ODT, dst.ODT, strict=True), (src.PAR, dst.PAR), ] others: list[Topology] = [] for s, d in timed: topo = Topology(s, d) self.add(_single_ended(p.loss, self.acc_structure, topo)) others.append(topo) for s, d in ((src.RESET_n, dst.RESET_n), (src.ALERT_n, dst.ALERT_n)): self.add(_single_ended(p.loss, self.acc_structure, Topology(s, d))) self.add( ConstrainReferenceDifference( guide_ck, [guide_cke, *others] ).timing_difference(p.skew_acc_ck), ConstrainReferenceDifference(guide_cke, others).timing_difference( Toleranced(0, p.spread_acc / 2) ), )
[docs] class DDR4DataAccConstraint(SignalConstraint["DDR4"]): """Constraint between the two channels: each DQS.P against CK[0].P. Args: params: Limit. Defaults to :py:class:`DDR4DataAccConstraintParams`. """ def __init__(self, params: DDR4DataAccConstraintParams | None = None) -> None: super().__init__() self.params = params or DDR4DataAccConstraintParams()
[docs] def constrain(self, src: DDR4, dst: DDR4) -> None: """Apply the DQS-to-CK window between two netted bundles.""" guide_ck = Topology(src.acc.CK[0].p, dst.acc.CK[0].p) dqs = [ Topology(s.p, d.p) for s, d in zip(src.data.DQS, dst.data.DQS, strict=True) ] self.add( ConstrainReferenceDifference(guide_ck, dqs).timing_difference( self.params.skew_ck_dqs ) )
[docs] class DDR4Constraint(SignalConstraint["DDR4"]): """Complete DDR4 constraint: data, address/command/control, and the DQS-to-CK window between them. Args: ck_structure: Differential routing structure for CK. dqs_structure: Differential routing structure for DQS. dq_structure: Single-ended routing structure for DQ and DM_n. acc_structure: Single-ended routing structure for address, command and control. data_params: Data-channel limits. acc_params: Address/command/control limits. data_acc_params: DQS-to-CK window. """ def __init__( self, ck_structure: DifferentialRoutingStructure, dqs_structure: DifferentialRoutingStructure, dq_structure: RoutingStructure, acc_structure: RoutingStructure, data_params: DDR4DataConstraintParams | None = None, acc_params: DDR4AccConstraintParams | None = None, data_acc_params: DDR4DataAccConstraintParams | None = None, ) -> None: super().__init__() self.data_constraint = DDR4DataConstraint( dqs_structure, dq_structure, data_params ) self.acc_constraint = DDR4AccConstraint(ck_structure, acc_structure, acc_params) self.data_acc_constraint = DDR4DataAccConstraint(data_acc_params)
[docs] def constrain(self, src: DDR4, dst: DDR4) -> None: """Apply every DDR4 constraint between two netted bundles. Args: src: Controller-side bundle. dst: Memory-side bundle, built with the same arguments. """ self.data_constraint.constrain(src.data, dst.data) self.acc_constraint.constrain(src.acc, dst.acc) self.data_acc_constraint.constrain(src, dst)