Skip to main content
Guides

Routing DDR

DDR routing is a board-level timing and signal-integrity problem, not only a shortest-path problem. Use the memory and processor data sheets to establish the topology, impedance, allowed layers, reference planes, length budgets, skew limits, trace spacing, and via geometry before configuring the router. The values in this guide are illustrative rather than requirements for a particular DDR generation or component.

Divide the channel into routing regions​

Treat the routed channel as three connected regions:

  1. The controller fanout escapes package pads to a shared boundary.
  2. The global channel connects the two fanout boundaries.
  3. The memory fanout enters the memory package from its shared boundary.

This separation makes crowded package escapes independently debuggable. A <breakout /> around each package owns its local fanout, while the enclosing board routes the connection between the two breakouts.

Model timing groups as buses​

Create every electrical connection with a named <trace />, then group related trace names with <bus />. A bus does not electrically connect its members.

A practical starting decomposition is:

BusTypical membersWhy keep it separate
Byte laneDQ, DMI/DM, and the lane's DQS pairThese signals share a local package region and timing relationship.
Address/commandAddress, bank, command, and control signalsThis group usually has a different destination region and length budget.
ClockCK_t and CK_cThe clock is a differential pair with its own matching requirement.
Reset or other asynchronous controlsRESET_n and similar controlsThese signals often do not need the same skew constraint as synchronous buses.

Keep lane-level rules distinct even when two buses happen to use the same layers. Do not place every DDR signal in one large bus: that unnecessarily couples routing direction, layer selection, and skew solving across unrelated timing groups.

<bus
name="DDR_BYTE0"
connections={["DQ0", "DQ1", "DMI0", "DQS0_t", "DQS0_c"]}
preferredLayers={["top", "inner4"]}
maxLengthSkew="0.5mm"
/>

<differentialpair
name="DDR_DQS0_PAIR"
positiveConnection="DQS0_t"
negativeConnection="DQS0_c"
maxLengthSkew="0.1mm"
/>

maxLengthSkew on the bus constrains the spread between all bus members. <differentialpair /> adds a separate relationship between the positive and negative strobe or clock traces. These properties express length matching; they do not calculate stackup impedance or differential-pair spacing.

Point both fanouts into the channel​

Use explicit, opposing busFanoutDirections on the controller and memory breakouts. Canonical direction names start with the physical boundary edge. For example, rightside_top terminates on the right boundary in its upper region. Directions use board coordinates and do not rotate with the component.

This preview routes a byte lane end to end between a controller and memory package. Each package is in its own breakout, so tscircuit creates implicit breakout points for every boundary-crossing trace. The winding solver coordinates the opposing boundary points in bus order before the fanout and global routing phases run.

The dashed boxes are pcb_debug_object overlays emitted for the two package fanouts and the board-level channel. Showing them makes the ownership and order of the three routing phases visible without changing the circuit.

PCB Circuit Preview
const laneSignals = [
"DQ0",
"DQ1",
"DQS0_t",
"DQS0_c",
]
const bgaPinNumbers = [6, 7, 10, 11]
const byteLanePinLabels = Object.fromEntries(
laneSignals.map((signal, index) => [
"pin" + bgaPinNumbers[index],
signal,
]),
)

export default () => (
<board
width="24mm"
height="12mm"
layers={6}
defaultTraceWidth="0.1mm"
minTraceWidth="0.1mm"
minTraceToPadEdgeClearance="0.1mm"
minViaEdgeToPadEdgeClearance="0.1mm"
minViaHoleDiameter="0.2mm"
minViaPadDiameter="0.45mm"
isViaInPadAllowed={false}
>
<breakout
name="CONTROLLER_FANOUT"
autorouter="fanout"
pcbX={-6}
width="8mm"
height="8mm"
fanoutRoutingLayers={["top", "inner2"]}
busFanoutDirections={{
DDR_BYTE0: "rightside_center",
}}
>
<chip
name="U_CONTROLLER"
footprint="bga16_grid4x4_p0.8mm_pad0.35mm_circularpads"
pinLabels={byteLanePinLabels}
/>
</breakout>

<breakout
name="MEMORY_FANOUT"
autorouter="fanout"
pcbX={6}
width="8mm"
height="8mm"
fanoutRoutingLayers={["top", "inner2"]}
busFanoutDirections={{
DDR_BYTE0: "leftside_center",
}}
>
<chip
name="U_MEMORY"
footprint="bga16_grid4x4_p0.8mm_pad0.35mm_circularpads"
pinLabels={byteLanePinLabels}
/>
</breakout>

<bus
name="DDR_BYTE0"
connections={laneSignals}
preferredLayers={["top", "inner2"]}
maxLengthSkew="0.5mm"
/>
<differentialpair
name="DDR_DQS0_PAIR"
positiveConnection="DQS0_t"
negativeConnection="DQS0_c"
maxLengthSkew="0.1mm"
/>

{laneSignals.map((signal) => (
<trace
key={signal}
name={signal}
from={"U_CONTROLLER." + signal}
to={"U_MEMORY." + signal}
/>
))}
</board>
)

Allocate layers deliberately​

fanoutRoutingLayers defines the layers available to boundary-terminated fanout buses. A bus's preferredLayer, preferredLayers, and pcbAllowedLayers narrow that choice. With the fanout router these values are currently a hard allowed-layer set, not a soft preference, so an empty intersection cannot route.

Choose those layers from a reviewed stackup:

  • Keep each high-speed signal layer adjacent to an appropriate reference plane.
  • Avoid unnecessary reference-plane changes and layer transitions.
  • Give dense byte lanes separate routing resources when their escapes compete.
  • Configure board trace, clearance, via-hole, and via-pad rules from the chosen fabricator's capabilities.
  • Keep isViaInPadAllowed={false} unless the fabrication and assembly process explicitly supports the via-in-pad construction you intend to use.

Power and ground escapes are not ordinary signal buses. Route them to their planes with copper pours and fanoutPourNetMap, or let tscircuit infer the map from matching board-level pours:

<copperpour layer="inner1" connectsTo="net.GND" />
<copperpour layer="inner2" connectsTo="net.VDDQ" />

<breakout
fanoutRoutingLayers={["top", "inner4", "bottom"]}
fanoutPourNetMap={{ inner1: "GND", inner2: "VDDQ" }}
>
{/* Package, buses, and traces */}
</breakout>

Leave room for the fanout​

fanoutBoundaryPadding is the tuning and spreading corridor between the package pads and the shared fanout boundary. Begin with enough room for the dense buses and their legal vias. If a bus cannot escape:

  1. Add one bus at a time to identify which group consumes the corridor.
  2. Check that both packages send the bus toward facing boundary regions.
  3. Check that the bus and breakout layer sets have a non-empty intersection.
  4. Increase the relevant boundary padding or breakout size.
  5. Include nearby series resistors or decoupling parts inside the breakout when their pads should participate in the local route instead of obstructing its boundary.

Explicit exit regions also prevent multiple buses from choosing the same crowded strip. Keep related lanes adjacent, but reserve different regions for lanes that would otherwise cross.

Connect the fanouts with bus lanes​

Use autorouter="bus_lanes" on an <autoroutingphase /> to connect compatible controller and memory fanout exits without adding vias. Select the DDR endpoint ports with connections and set maxLengthSkew and pcbTraceWidth on each <bus />. Leave other connections for the subsequent global router. The phase preserves existing fanout copper and includes its planar lengths when matching each bus. Layer mismatches or impossible planar routes produce errors; adjust the fanouts rather than expecting a layer change inside the bus route.

Example: AM3352 to DDR3 with bus_lanes​

For a direct package-to-package connection, you can skip boundary fanouts. A bus_lanes phase adds short local dogbones where an unrouted pad must reach a signal layer, then routes each interconnect without intermediate vias. It also length-matches buses and couples declared differential pairs. You do not need an algorithmFn, saved routes, or board-specific route guides.

<autoroutingphase
name="DDR_BUS_LANES"
phaseIndex={1}
autorouter="bus_lanes"
/>
<trace name="DDR_D0" from="U1.M3" to="U3.E3" routingPhaseIndex={1} />
<bus
name="DDR_BYTE0"
connections={[
"DDR_D0", "DDR_D1", "DDR_D2", "DDR_D3",
"DDR_D4", "DDR_D5", "DDR_D6", "DDR_D7",
"DDR_DQM0", "DDR_DQS0", "DDR_DQSn0",
]}
maxLengthSkew={0.635}
/>
<differentialpair
name="DDR_DQS_PAIR0"
positiveConnection="DDR_DQS0"
negativeConnection="DDR_DQSn0"
maxLengthSkew={0.127}
pcbTraceGap={0.12}
/>

Use the complete pin mapping below rather than copying this abbreviated lane. The example connects an AM3352 ZCZ to W631GG6MB DDR3 RAM, with all 47 shared signals declared as traces. Its two byte lanes have a 0.635 mm length-skew limit; the DQS pairs and clock pair have a 0.127 mm limit and 0.12 mm requested gap. The controller is at (0, 0) and RAM at (0, -27) mm. The four-layer board uses 0.1 mm traces and clearance, 0.3 mm via pads, and 0.15 mm drills.

The solver chooses signal layers automatically. To constrain a bus to a reviewed layer, set pcbAllowedLayers={["inner1"]} on that bus. preferredLayers also restricts the allowed set in this routing flow. Keep corresponding fanout exits on a common permitted layer if you supply fanouts yourself.

Each signal uses two local vias to reach its routing layer, with no vias along the interconnect. Choose length and spacing constraints for your board's timing budget and stackup; this example focuses on the DDR signal connections.

Complete routing example​

The example includes both chip footprints, the AM3352 ball map, buses, and differential pairs. routeRemaining={false} keeps unrelated CPU pins out of this signal-only example.

PCB Circuit Preview
// Source: https://tscircuit.com/seveibar/am3352-ram-dogbone-and-single-layer-route-test (v0.0.9)
// Preserves components, placement and constraints; replaces custom routing with the public preset.
import { TimingConstraints } from "./design/timing-constraints"
import { AM3352, ballMap } from "./components/AM3352"
import { W631GG6MB_12, pinLabels as ramPins } from "./imports/W631GG6MB_12"

const ramNet = (s: string): string | undefined => {
if (/^VSS/.test(s)) return "GND"
if (/^VDD/.test(s)) return "DDR_1V5"
if (/^VREF/.test(s)) return "DDR_VREF"
if (/^DQL\d/.test(s)) return `DDR_D${s.slice(3)}`
if (/^DQU\d/.test(s)) return `DDR_D${8 + Number(s.slice(3))}`
if (/^A\d+$/.test(s) || /^BA\d/.test(s)) return `DDR_${s}`
return (
{
ODT: "DDR_ODT",
CKE: "DDR_CKE",
CK: "DDR_CK",
N_CK: "DDR_CKn",
N_CS: "DDR_CSn0",
N_RESET: "DDR_RESETn",
N_RAS: "DDR_RASn",
N_CAS: "DDR_CASn",
N_WE: "DDR_WEn",
DML: "DDR_DQM0",
DMU: "DDR_DQM1",
DQSL: "DDR_DQS0",
N_DQSl: "DDR_DQSn0",
DQSU: "DDR_DQS1",
N_DQSU: "DDR_DQSn1",
ZQ: "DDR_ZQ",
} as Record<string, string>
)[s]
}

// Keep only original nets shared by the two retained chips.
const cpuNet = (signal: string) => {
if (/^VSS/.test(signal) || ["VREFN", "RTC_KALDO_ENn", "VPP"].includes(signal))
return "GND"
if (signal === "VDDS_DDR") return "DDR_1V5"
return signal
}
const cpuConnections = Object.entries(ballMap).map(([pin, signal]) => ({
pin,
net: cpuNet(signal),
}))
const ramConnections = Object.entries(ramPins).map(([pin, labels]) => ({
pin,
net: ramNet(labels[1] ?? ""),
}))
const sharedNets = [
...new Set(
ramConnections.flatMap(({ net }) =>
net &&
!["GND", "DDR_1V5", "DDR_VREF"].includes(net) &&
cpuConnections.some((c) => c.net === net)
? [net]
: [],
),
),
]

export default function Board() {
return (
<board
width={22}
height={44}
outlineOffsetY={-13}
layers={4}
thickness={1.2}
minTraceWidth={0.1}
minTraceToPadEdgeClearance={0.1}
minViaPadDiameter={0.3}
minViaHoleDiameter={0.15}
routeRemaining={false}
schAutoLayoutEnabled={false}
title="AM3352 / RAM — DDR3 bus routing example"
>
<autoroutingphase
name="DDR_BUS_LANES"
phaseIndex={1}
autorouter="bus_lanes"
/>
<pcbnotetext
pcbX={0}
pcbY={8}
text="AM3352 / DDR3: automatic dogbones and via-free bus lanes"
fontSize={0.6}
/>
<TimingConstraints />
<AM3352 name="U1" noSchematicRepresentation pcbX={0} pcbY={0} />
<W631GG6MB_12 name="U3" noSchematicRepresentation pcbX={0} pcbY={-27} />
{sharedNets.map((net) => {
const cpu = cpuConnections.find((c) => c.net === net)!
const ram = ramConnections.find((c) => c.net === net)!
return (
<trace
key={net}
name={net}
routingPhaseIndex={1}
from={`.U1 > .${cpu.pin}`}
to={`.U3 > .${ram.pin}`}
/>
)
})}
</board>
)
}

The live PCB preview sends these TSX files to svg.tscircuit.com, which computes the dogbones and bus routes. This example requires @tscircuit/core 0.0.2030 or later. It uses no custom algorithm or saved route geometry.

VCC and GND dogbones​

Power escapes need explicit net connections too. bus_lanes routes the selected signal connections; it does not infer every supply ball or create reference planes. Keep AM3352 voltage domains separate: the DDR supply is VDDS_DDR, not all of the processor's VDD* pins.

This separate power-escape example connects both packages' VSS pins to GND and their DDR supply pins to DDR_1V5, then adds 89 local dogbones with <fanout autorouter="dogbone" />. The RAM footprint uses its nominal 0.8 mm grid.

PCB Circuit Preview
import { AM3352, ballMap } from "./components/AM3352"
import { pinLabels } from "./imports/W631GG6MB_12"

// Local escape study, separate from the DDR interconnect example.
// RAM pads use the nominal 0.8 mm grid to avoid imported coordinate rounding.
// DDR supply balls only; the CPU's other voltage domains remain separate.
const cpuPower = Object.fromEntries(Object.entries(ballMap).flatMap(([ball, signal]) =>
signal.startsWith("VSS") ? [[ball, "net.GND"]] :
signal === "VDDS_DDR" ? [[ball, "net.DDR_1V5"]] : []))
const ramPower = Object.fromEntries(Object.entries(pinLabels).flatMap(([pin, labels]) => {
const signal = labels[1] ?? ""
return signal.startsWith("VSS") ? [[pin, "net.GND"]] :
signal.startsWith("VDD") ? [[pin, "net.DDR_1V5"]] : []
}))

export default () => <board width={70} height={70} layers={4}
routeRemaining={false} schAutoLayoutEnabled={false}
minTraceWidth={0.1} minTraceToPadEdgeClearance={0.1}
minViaEdgeToPadEdgeClearance={0.1} minViaPadDiameter={0.3} minViaHoleDiameter={0.15}>
<fanout autorouter="dogbone" fanoutRoutingLayers={["inner1"]}>
<AM3352 name="U1" noSchematicRepresentation connections={cpuPower} />
</fanout>
<fanout autorouter="dogbone" fanoutRoutingLayers={["inner1"]} pcbY={-27}>
<chip name="U3" noSchematicRepresentation connections={ramPower} pinLabels={pinLabels}
footprint={<footprint>{Object.entries(pinLabels).map(([pin, [ball]]) =>
<smtpad portHints={[pin, ball]} shape="circle" radius={0.2}
pcbX={(Number(ball.slice(1)) - 5) * 0.8}
pcbY={(7.5 - "ABCDEFGHJKLMNPRT".indexOf(ball[0])) * 0.8} />
)}</footprint>} />
</fanout>
<pcbnotetext pcbY={11} text="Power escape study: GND and DDR_1V5" fontSize={0.6} />
<pcbnotetext pcbY={9} text="Local dogbones only; no plane connections" fontSize={0.4} />
</board>

These are pad-to-via escapes only. routeRemaining={false} deliberately stops before joining the power nets, and no copper planes are included. Do not treat this example as a powered DDR board or simply overlay its vias on the signal routes. Integrate the power nets and reviewed stackup before routing the final board, then rerun clearance and connectivity checks. For plane-terminated fanout configuration, see Allocate layers deliberately.

Verify the complete channel​

A successful local fanout is not proof that the complete DDR channel meets its requirements. Before fabrication:

  • Confirm that every expected signal has a continuous controller-to-memory PCB trace and that there are no autorouting errors.
  • Measure end-to-end routed lengths. A skew constraint satisfied independently in both fanouts does not by itself prove the total channel skew.
  • Run clearance and placement DRC, then visually inspect vias near BGA and decoupling pads. Same-net copper still needs to match the intended manufacturing process.
  • Check layer transitions, reference planes, return-current paths, and breakout congestion in the PCB view.
  • Keep visual snapshots while adding buses progressively so a new lane's impact is reviewable.
  • Perform stackup-aware signal-integrity review against the processor, memory, and PCB-fabricator requirements. Autorouting constraints do not replace that analysis.

For general fanout controls, see <breakout /> and <bus />. For ordered board-level routing, see <autoroutingphase />.