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:
- The controller fanout escapes package pads to a shared boundary.
- The global channel connects the two fanout boundaries.
- 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:
| Bus | Typical members | Why keep it separate |
|---|---|---|
| Byte lane | DQ, DMI/DM, and the lane's DQS pair | These signals share a local package region and timing relationship. |
| Address/command | Address, bank, command, and control signals | This group usually has a different destination region and length budget. |
| Clock | CK_t and CK_c | The clock is a differential pair with its own matching requirement. |
| Reset or other asynchronous controls | RESET_n and similar controls | These 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.
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:
- Add one bus at a time to identify which group consumes the corridor.
- Check that both packages send the bus toward facing boundary regions.
- Check that the bus and breakout layer sets have a non-empty intersection.
- Increase the relevant boundary padding or breakout size.
- 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.
// 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.
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 />.