0hmX/simplified-am62l-computer
This code defines components and scripts for physically representing and routing high-speed DDR memory signals, power lines, and ground planes on an 8-layer PCB with detailed pin assignments, copper pours, and precise via and trace layouts based on TI EVM reference data.
- Version
- 1.0.16
- License
- unset
- Stars
- 0
routing/am62l-classified-autorouter.ts
import {
AutoroutingPipelineSolver7_MultiGraph,
AutoroutingPipelineSolver9_PreloadedTraceGraph,
type SimpleRouteConnection as CapacityRouteConnection,
} from "@tscircuit/capacity-autorouter"
import type {
AutorouterCompleteEvent,
AutorouterErrorEvent,
AutorouterProgressEvent,
GenericLocalAutorouter,
SimpleRouteJson,
SimplifiedPcbTrace,
} from "@tscircuit/core"
import { TI_AM62L_EVM_DDR_ROUTES } from "./ti-am62l-evm-ddr-routes"
import { TI_AM62L_EVM_GROUND_FANOUT } from "./ti-am62l-evm-ground-fanout"
import { TI_AM62L_EVM_MMC1_ESCAPES } from "./ti-am62l-evm-mmc1-escapes"
export type Am62lRouteClass =
| "ddr"
| "clock"
| "sd"
| "reset-control"
| "usb-power"
| "power"
| "ground"
| "general"
export type Am62lRoutingPhase = Exclude<Am62lRouteClass, "general"> | "mixed"
type Point = { x: number; y: number }
type RoutingLayer =
| "top"
| "inner2"
| "inner3"
| "inner4"
| "inner5"
| "bottom"
type RoutePoint = Point & {
layer: string
pointId?: string
pcb_port_id?: string
}
const getLayerZ = (layer: string, layerCount: number) => {
if (layer === "top") return 0
if (layer === "bottom") return layerCount - 1
return Number(layer.slice(5))
}
const getLayerName = (z: number, layerCount: number) => {
if (z === 0) return "top"
if (z === layerCount - 1) return "bottom"
return `inner${z}`
}
const getLayersBetween = (
fromLayer: string,
toLayer: string,
layerCount: number,
) => {
const fromZ = getLayerZ(fromLayer, layerCount)
const toZ = getLayerZ(toLayer, layerCount)
const minZ = Math.min(fromZ, toZ)
const maxZ = Math.max(fromZ, toZ)
return Array.from({ length: maxZ - minZ + 1 }, (_, index) =>
getLayerName(minZ + index, layerCount),
)
}
/** Materialize committed copper as geometric obstacles for Pipeline1. */
const getTraceObstacles = (
traces: SimplifiedPcbTrace[],
layerCount: number,
) => {
const obstacles: any[] = []
for (const [traceIndex, trace] of traces.entries()) {
const connectedTo = [
trace.pcb_trace_id,
trace.connection_name,
...(trace.connectsTo ?? []),
].filter((id): id is string => Boolean(id))
for (const [pointIndex, point] of trace.route.entries()) {
if (point.route_type !== "via") continue
obstacles.push({
obstacleId: `committed_${traceIndex}_${pointIndex}_via`,
type: "rect",
layers: getLayersBetween(
point.from_layer,
point.to_layer,
layerCount,
),
center: { x: point.x, y: point.y },
width: point.via_diameter ?? 0.4572,
height: point.via_diameter ?? 0.4572,
connectedTo,
})
}
for (let pointIndex = 0; pointIndex < trace.route.length - 1; pointIndex++) {
const start = trace.route[pointIndex]
const end = trace.route[pointIndex + 1]
if (
start.route_type !== "wire" ||
end.route_type !== "wire" ||
start.layer !== end.layer
) continue
const dx = end.x - start.x
const dy = end.y - start.y
const length = Math.hypot(dx, dy)
if (length <= 0.001) continue
obstacles.push({
obstacleId: `committed_${traceIndex}_${pointIndex}_wire`,
type: "rect",
layers: [start.layer],
center: { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 },
width: length,
height: Math.max(start.width, 0.001),
ccwRotationDegrees: (Math.atan2(dy, dx) * 180) / Math.PI,
connectedTo,
})
}
}
return obstacles
}
const isPointInsideObstacle = (
point: Point,
obstacle: any,
layer: string,
margin: number,
) => {
if (!obstacle.layers?.includes(layer) || !obstacle.center) return false
const angle = -((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180
const dx = point.x - obstacle.center.x
const dy = point.y - obstacle.center.y
const localX = dx * Math.cos(angle) - dy * Math.sin(angle)
const localY = dx * Math.sin(angle) + dy * Math.cos(angle)
return (
Math.abs(localX) <= (obstacle.width ?? 0) / 2 + margin &&
Math.abs(localY) <= (obstacle.height ?? 0) / 2 + margin
)
}
class MinHeap<T> {
private readonly entries: Array<{ value: T; priority: number }> = []
push(value: T, priority: number) {
this.entries.push({ value, priority })
let index = this.entries.length - 1
while (index > 0) {
const parent = Math.floor((index - 1) / 2)
if (this.entries[parent].priority <= priority) break
this.entries[index] = this.entries[parent]
index = parent
}
this.entries[index] = { value, priority }
}
pop(): T | undefined {
if (this.entries.length === 0) return undefined
const first = this.entries[0].value
const last = this.entries.pop()
if (!last || this.entries.length === 0) return first
let index = 0
while (true) {
const left = index * 2 + 1
const right = left + 1
if (left >= this.entries.length) break
const child =
right < this.entries.length &&
this.entries[right].priority < this.entries[left].priority
? right
: left
if (this.entries[child].priority >= last.priority) break
this.entries[index] = this.entries[child]
index = child
}
this.entries[index] = last
return first
}
get size() {
return this.entries.length
}
}
const simplifyGridPath = (points: Point[]): Point[] => {
if (points.length < 3) return points
const result = [points[0]]
for (let index = 1; index < points.length - 1; index++) {
const previous = result[result.length - 1]!
const current = points[index]
const next = points[index + 1]
const firstDx = Math.sign(current.x - previous.x)
const firstDy = Math.sign(current.y - previous.y)
const nextDx = Math.sign(next.x - current.x)
const nextDy = Math.sign(next.y - current.y)
if (firstDx !== nextDx || firstDy !== nextDy) result.push(current)
}
result.push(points[points.length - 1]!)
return result
}
export const findSignalLayerPath = (
input: SimpleRouteJson,
start: Point,
end: Point,
obstacles: any[],
layer: RoutingLayer = "inner5",
): Point[] => {
const step = 0.25
const bounds = input.bounds
const minX = bounds.minX + 0.75
const minY = bounds.minY + 0.75
const maxIx = Math.floor((bounds.maxX - 0.75 - minX) / step)
const maxIy = Math.floor((bounds.maxY - 0.75 - minY) / step)
const toGrid = (point: Point) => ({
ix: Math.round((point.x - minX) / step),
iy: Math.round((point.y - minY) / step),
})
const toPoint = ({ ix, iy }: { ix: number; iy: number }) => ({
x: minX + ix * step,
y: minY + iy * step,
})
const startGrid = toGrid(start)
const endGrid = toGrid(end)
const key = (ix: number, iy: number) => `${ix}:${iy}`
const startKey = key(startGrid.ix, startGrid.iy)
const endKey = key(endGrid.ix, endGrid.iy)
const blockedCache = new Map<string, boolean>()
const isBlocked = (ix: number, iy: number) => {
const cellKey = key(ix, iy)
if (cellKey === startKey || cellKey === endKey) return false
const cached = blockedCache.get(cellKey)
if (cached !== undefined) return cached
const point = toPoint({ ix, iy })
const blocked = obstacles.some((obstacle) =>
isPointInsideObstacle(point, obstacle, layer, 0.13),
)
blockedCache.set(cellKey, blocked)
return blocked
}
const open = new MinHeap<{ ix: number; iy: number }>()
const cost = new Map<string, number>([[startKey, 0]])
const previous = new Map<string, string>()
open.push(startGrid, 0)
const directions = [
[1, 0], [-1, 0], [0, 1], [0, -1],
[1, 1], [1, -1], [-1, 1], [-1, -1],
] as const
let iterations = 0
while (open.size > 0 && iterations++ < 250_000) {
const current = open.pop()!
const currentKey = key(current.ix, current.iy)
if (currentKey === endKey) break
for (const [dx, dy] of directions) {
const ix = current.ix + dx
const iy = current.iy + dy
if (ix < 0 || iy < 0 || ix > maxIx || iy > maxIy || isBlocked(ix, iy)) {
continue
}
const nextKey = key(ix, iy)
const nextCost =
(cost.get(currentKey) ?? Infinity) + (dx !== 0 && dy !== 0 ? Math.SQRT2 : 1)
if (nextCost >= (cost.get(nextKey) ?? Infinity)) continue
cost.set(nextKey, nextCost)
previous.set(nextKey, currentKey)
const heuristic = Math.hypot(endGrid.ix - ix, endGrid.iy - iy)
open.push({ ix, iy }, nextCost + heuristic)
}
}
if (!previous.has(endKey) && startKey !== endKey) {
throw new Error(`Custom ${layer} grid router found no collision-free path`)
}
const gridPath = [endKey]
while (gridPath[gridPath.length - 1] !== startKey) {
const parent = previous.get(gridPath[gridPath.length - 1]!)
if (!parent) throw new Error(`Custom ${layer} path reconstruction failed`)
gridPath.push(parent)
}
return simplifyGridPath(
gridPath.reverse().map((cellKey) => {
const [ix, iy] = cellKey.split(":").map(Number)
return toPoint({ ix, iy })
}),
)
}
const CLASS_PRIORITY: Record<Am62lRouteClass, number> = {
ddr: 0,
clock: 1,
sd: 2,
"reset-control": 3,
"usb-power": 4,
power: 5,
ground: 6,
general: 7,
}
const connectionSearchText = (connection: CapacityRouteConnection): string =>
[
connection.name,
connection.rootConnectionName,
connection.netConnectionName,
connection.__netConnectionName,
...(connection.mergedConnectionNames ?? []),
...(connection.__rootConnectionNames ?? []),
]
.filter(Boolean)
.join(" ")
.toUpperCase()
export const classifyAm62lConnection = (
connection: CapacityRouteConnection,
): Am62lRouteClass => {
const text = connectionSearchText(connection)
if (/DDR0_|LPDDR|VDD_DDR/.test(text)) return "ddr"
if (/OSC_|XRCGB|_CK0|_DQS/.test(text)) return "clock"
if (/SD_PWR/.test(text)) return "reset-control"
if (/SD_3V3|SD_IO|SD_LOADSW|CAP_VDDS_MMC1/.test(text)) return "power"
if (/SD_|MMC1/.test(text)) return "sd"
if (/USB_|VBUS|TUSB|EFUSE|PROTECTED_5V|AON_3V3/.test(text)) {
return "usb-power"
}
if (/POR|RESET|BOOTMODE|PMIC_|WAKEUP|UART|I2C/.test(text)) {
return "reset-control"
}
if (/GND|VSS/.test(text)) return "ground"
if (/VDD|VDDA|CAP_|3V3|1V8|0V75|_SW|_LX/.test(text)) return "power"
return "general"
}
const getBounds = (connection: CapacityRouteConnection) => {
const points = connection.pointsToConnect as Point[]
return {
minX: Math.min(...points.map((point) => point.x)),
maxX: Math.max(...points.map((point) => point.x)),
minY: Math.min(...points.map((point) => point.y)),
maxY: Math.max(...points.map((point) => point.y)),
}
}
const boundsOverlap = (
first: ReturnType<typeof getBounds>,
second: ReturnType<typeof getBounds>,
): boolean =>
first.minX <= second.maxX &&
first.maxX >= second.minX &&
first.minY <= second.maxY &&
first.maxY >= second.minY
const manhattanSpan = (connection: CapacityRouteConnection): number => {
const bounds = getBounds(connection)
return bounds.maxX - bounds.minX + bounds.maxY - bounds.minY
}
/**
* Estimate route contention before invoking the geometric solver. Connections
* whose endpoint envelopes overlap many other envelopes are routed first so
* later, easier nets can flow around the committed critical paths.
*/
const conflictScore = (
connection: CapacityRouteConnection,
allConnections: CapacityRouteConnection[],
): number => {
const bounds = getBounds(connection)
let score = 0
for (const candidate of allConnections) {
if (candidate === connection) continue
if (boundsOverlap(bounds, getBounds(candidate))) score += 1
}
return score
}
export const orderAm62lConnections = (
connections: CapacityRouteConnection[],
): CapacityRouteConnection[] => {
const scoreByConnection = new Map(
connections.map((connection) => [
connection,
conflictScore(connection, connections),
]),
)
return [...connections].sort((first, second) => {
const firstSpan = manhattanSpan(first)
const secondSpan = manhattanSpan(second)
const firstIsLocal = firstSpan <= 8
const secondIsLocal = secondSpan <= 8
if (firstIsLocal !== secondIsLocal) return firstIsLocal ? -1 : 1
if (firstIsLocal && secondIsLocal) {
const localSpanDelta = firstSpan - secondSpan
if (Math.abs(localSpanDelta) > 1e-9) return localSpanDelta
}
const classDelta =
CLASS_PRIORITY[classifyAm62lConnection(first)] -
CLASS_PRIORITY[classifyAm62lConnection(second)]
if (classDelta !== 0) return classDelta
const conflictDelta =
(scoreByConnection.get(second) ?? 0) -
(scoreByConnection.get(first) ?? 0)
if (conflictDelta !== 0) return conflictDelta
const spanDelta = secondSpan - firstSpan
if (Math.abs(spanDelta) > 1e-9) return spanDelta
return first.name.localeCompare(second.name)
})
}
type PreparedRouteConnection = CapacityRouteConnection & {
__originalConnectionName?: string
__originalPointCount?: number
}
/**
* Convert a multi-terminal electrical net into a deterministic minimum-span
* tree of two-point routing branches. This preserves one logical net name,
* while avoiding the capacity solver's expensive all-terminals-at-once search.
*/
const expandConnectionsIntoMinimumSpanBranches = (
connections: CapacityRouteConnection[],
): PreparedRouteConnection[] => {
const expanded: PreparedRouteConnection[] = []
for (const connection of connections) {
const points = connection.pointsToConnect as RoutePoint[]
if (points.length <= 2) {
expanded.push({
...connection,
__originalConnectionName: connection.name,
__originalPointCount: points.length,
mergedConnectionNames: [
connection.name,
...(connection.mergedConnectionNames ?? []),
],
})
continue
}
const edges: Array<[number, number]> = []
const inTree = new Set([0])
while (inTree.size < points.length) {
let best:
| { from: number; to: number; cost: number }
| undefined
for (const from of inTree) {
for (let to = 0; to < points.length; to++) {
if (inTree.has(to)) continue
const layerPenalty = points[from].layer === points[to].layer ? 0 : 2
const cost =
Math.abs(points[from].x - points[to].x) +
Math.abs(points[from].y - points[to].y) +
layerPenalty
if (
!best ||
cost < best.cost - 1e-9 ||
(Math.abs(cost - best.cost) < 1e-9 &&
`${from}:${to}` < `${best.from}:${best.to}`)
) best = { from, to, cost }
}
}
if (!best) throw new Error(`Could not branch net ${connection.name}`)
edges.push([best.from, best.to])
inTree.add(best.to)
}
const branchNames = edges.map(
(_, index) => `${connection.name}__branch_${index + 1}`,
)
for (const [index, [from, to]] of edges.entries()) {
expanded.push({
...connection,
name: branchNames[index],
rootConnectionName: connection.rootConnectionName ?? connection.name,
__originalConnectionName: connection.name,
__originalPointCount: points.length,
mergedConnectionNames: [
connection.name,
...branchNames,
...(connection.mergedConnectionNames ?? []),
],
pointsToConnect: [points[from], points[to]],
})
}
}
return expanded
}
type RouterEventHandlers = {
complete: Array<(event: AutorouterCompleteEvent) => void>
error: Array<(event: AutorouterErrorEvent) => void>
progress: Array<(event: AutorouterProgressEvent) => void>
}
type TiRoutePoint = (typeof TI_AM62L_EVM_DDR_ROUTES)[keyof typeof TI_AM62L_EVM_DDR_ROUTES]["route"][number]
const distance = (first: Point, second: Point) =>
Math.hypot(first.x - second.x, first.y - second.y)
const getComponentCenterForPort = (
input: SimpleRouteJson,
point: RoutePoint,
): Point | undefined => {
if (!point.pointId) return undefined
const obstacles = (input.obstacles ?? []) as any[]
const portObstacle = obstacles.find(
(obstacle) =>
obstacle.center &&
distance(obstacle.center, point) < 0.03 &&
obstacle.connectedTo?.includes(point.pointId),
)
if (!portObstacle?.componentId) return undefined
const componentObstacles = obstacles.filter(
(obstacle) => obstacle.componentId === portObstacle.componentId && obstacle.center,
)
// The AM62L package contributes hundreds of ball obstacles. This guard keeps
// the coordinate matcher from treating a peripheral IC as the SoC.
if (componentObstacles.length < 300) return undefined
const xs = componentObstacles.map((obstacle) => obstacle.center.x)
const ys = componentObstacles.map((obstacle) => obstacle.center.y)
return {
x: (Math.min(...xs) + Math.max(...xs)) / 2,
y: (Math.min(...ys) + Math.max(...ys)) / 2,
}
}
type Mmc1Escape = (typeof TI_AM62L_EVM_MMC1_ESCAPES)[keyof typeof TI_AM62L_EVM_MMC1_ESCAPES]
export const prepareTiMmc1Escapes = (
input: SimpleRouteJson,
): { input: SimpleRouteJson; escapeTraces: SimplifiedPcbTrace[] } => {
const escapeTraces: SimplifiedPcbTrace[] = []
const connections = (input.connections as CapacityRouteConnection[]).map(
(connection) => {
let match:
| {
pointIndex: number
center: Point
signal: string
escape: Mmc1Escape
}
| undefined
const points = connection.pointsToConnect as RoutePoint[]
for (const [pointIndex, point] of points.entries()) {
const center = getComponentCenterForPort(input, point)
if (!center) continue
for (const [signal, escape] of Object.entries(
TI_AM62L_EVM_MMC1_ESCAPES,
) as Array<[string, Mmc1Escape]>) {
const start = escape.route.find(
(candidate) => candidate.route_type === "wire",
)
if (!start) continue
if (
distance(point, { x: center.x + start.x, y: center.y + start.y }) <
0.03
) {
match = { pointIndex, center, signal, escape }
break
}
}
if (match) break
}
if (!match) return connection
const alignedRoute = match.escape.route.map((point) => ({
...point,
x: point.x + match!.center.x,
y: point.y + match!.center.y,
})) as SimplifiedPcbTrace["route"]
const finalWire = [...alignedRoute]
.reverse()
.find((point) => point.route_type === "wire")
if (!finalWire || finalWire.route_type !== "wire") {
throw new Error(`TI ${match.signal} escape has no wire exit`)
}
const originalPoint = points[match.pointIndex]
const syntheticPointId =
`ti_mmc1_escape:${match.signal}:${connection.name}:${originalPoint.pointId}`
const transformedPoints = [...points]
transformedPoints[match.pointIndex] = {
x: finalWire.x,
y: finalWire.y,
layer: finalWire.layer,
pointId: syntheticPointId,
}
const connectedTo = [
connection.name,
connection.rootConnectionName,
connection.netConnectionName,
connection.__netConnectionName,
originalPoint.pointId,
originalPoint.pcb_port_id,
syntheticPointId,
].filter((id): id is string => Boolean(id))
const firstWire = alignedRoute.find(
(point) => point.route_type === "wire",
)
const lastWire = [...alignedRoute]
.reverse()
.find((point) => point.route_type === "wire")
if (firstWire?.route_type === "wire" && originalPoint.pcb_port_id) {
;(firstWire as typeof firstWire & { start_pcb_port_id?: string }).start_pcb_port_id =
originalPoint.pcb_port_id
}
if (lastWire?.route_type === "wire") {
;(lastWire as typeof lastWire & { end_pcb_port_id?: string }).end_pcb_port_id =
syntheticPointId
}
escapeTraces.push({
type: "pcb_trace",
pcb_trace_id: `ti_mmc1_escape_${match.signal}_${connection.name}`,
connection_name: connection.name,
connectsTo: connectedTo,
route: alignedRoute,
})
return { ...connection, pointsToConnect: transformedPoints }
},
)
return {
input: { ...input, connections: connections as SimpleRouteJson["connections"] },
escapeTraces,
}
}
/**
* Dense BGAs are escaped before the global route. Keep every pad as a copper
* obstacle, but do not ask Pipeline 7 to build a second component-local mesh
* around hundreds of pads and then merge it across the fixed fanout copper.
*/
const useGlobalTopologyForDenseComponents = (
input: SimpleRouteJson,
): SimpleRouteJson => {
const obstacleCountByComponent = new Map<string, number>()
for (const obstacle of input.obstacles as Array<{ componentId?: string }>) {
if (!obstacle.componentId) continue
obstacleCountByComponent.set(
obstacle.componentId,
(obstacleCountByComponent.get(obstacle.componentId) ?? 0) + 1,
)
}
const denseComponentIds = new Set(
[...obstacleCountByComponent.entries()]
.filter(([, obstacleCount]) => obstacleCount >= 64)
.map(([componentId]) => componentId),
)
return {
...input,
obstacles: input.obstacles.map((obstacle) => {
if (!obstacle.componentId || !denseComponentIds.has(obstacle.componentId)) {
return obstacle
}
const globalObstacle = { ...obstacle }
delete globalObstacle.componentId
return globalObstacle
}),
}
}
const prepareSdInner5Trunks = (
input: SimpleRouteJson,
connections: PreparedRouteConnection[],
initialTraces: SimplifiedPcbTrace[],
) => {
const remaining: PreparedRouteConnection[] = []
const trunkConnections: PreparedRouteConnection[] = []
for (const connection of connections) {
const points = connection.pointsToConnect as RoutePoint[]
const hasTiEscape = points.some((point) =>
point.pointId?.startsWith("ti_mmc1_escape:"),
)
if (hasTiEscape && points.length === 2 && manhattanSpan(connection) > 8) {
trunkConnections.push(connection)
} else {
remaining.push(connection)
}
}
trunkConnections.sort((first, second) => first.name.localeCompare(second.name))
const traces = [...initialTraces]
for (const connection of trunkConnections) {
const points = connection.pointsToConnect as RoutePoint[]
const start = points.find((point) =>
point.pointId?.startsWith("ti_mmc1_escape:"),
)!
const end = points.find((point) => point !== start)!
const startAccess = { x: start.x, y: start.y }
const endAccess = { x: end.x, y: end.y + (end.y >= 0 ? -1 : 1) }
const logicalName = connection.__originalConnectionName ?? connection.name
const committed = [
...(input.traces ?? []),
...traces,
].filter(
(trace) => trace.connection_name !== logicalName,
) as SimplifiedPcbTrace[]
const obstacles = [
...(input.obstacles ?? []),
...getTraceObstacles(committed, input.layerCount),
] as any[]
let signalLayer: "inner2" | "inner5" = "inner5"
let gridPath: Point[]
try {
gridPath = findSignalLayerPath(
input,
startAccess,
endAccess,
obstacles,
signalLayer,
)
} catch {
signalLayer = "inner2"
try {
gridPath = findSignalLayerPath(
input,
startAccess,
endAccess,
obstacles,
signalLayer,
)
} catch (caught) {
const message = caught instanceof Error ? caught.message : String(caught)
throw new Error(`${connection.name}: ${message}`)
}
}
const route: SimplifiedPcbTrace["route"] = [
{
route_type: "wire",
x: start.x,
y: start.y,
width: 0.09398,
layer: start.layer as any,
},
]
if (start.layer !== signalLayer) {
route.push({
route_type: "via",
x: startAccess.x,
y: startAccess.y,
from_layer: "top",
to_layer: "bottom",
via_diameter: 0.4572,
via_hole_diameter: 0.2032,
})
}
route.push(
...gridPath.map((point) => ({
route_type: "wire" as const,
x: point.x,
y: point.y,
width: 0.09398,
layer: signalLayer,
})),
)
if (end.layer !== signalLayer) {
route.push({
route_type: "via",
x: endAccess.x,
y: endAccess.y,
from_layer: "top",
to_layer: "bottom",
via_diameter: 0.4572,
via_hole_diameter: 0.2032,
})
}
route.push(
{
route_type: "wire",
x: endAccess.x,
y: endAccess.y,
width: 0.08128,
layer: end.layer as any,
},
{
route_type: "wire",
x: end.x,
y: end.y,
width: 0.08128,
layer: end.layer as any,
},
)
traces.push({
type: "pcb_trace",
pcb_trace_id: `custom_sd_trunk_${connection.name}`,
connection_name: logicalName,
connectsTo: [
connection.name,
logicalName,
...(connection.mergedConnectionNames ?? []),
...points.flatMap((point) => [point.pointId, point.pcb_port_id]),
].filter((id): id is string => Boolean(id)),
route,
})
}
return { connections: remaining, traces }
}
const getPortComponentGeometry = (
input: SimpleRouteJson,
point: RoutePoint,
) => {
const pointIds = [point.pointId, point.pcb_port_id].filter(
(id): id is string => Boolean(id),
)
const obstacles = (input.obstacles ?? []) as any[]
const portObstacle = obstacles.find(
(obstacle) =>
obstacle.center &&
distance(obstacle.center, point) < 0.04 &&
obstacle.connectedTo?.some((id: string) => pointIds.includes(id)),
) ?? obstacles.find(
// Generated multi-terminal power branches do not always retain the
// original pcb_port id. Coordinate matching is still unambiguous at the
// package pad centre and prevents the PTH fallback from becoming
// via-in-pad when that metadata is absent.
(obstacle) =>
obstacle.componentId &&
obstacle.center &&
distance(obstacle.center, point) < 0.04,
)
if (!portObstacle?.componentId) return undefined
const componentObstacles = obstacles.filter(
(obstacle) =>
obstacle.componentId === portObstacle.componentId && obstacle.center,
)
if (componentObstacles.length === 0) return undefined
const minX = Math.min(
...componentObstacles.map(
(obstacle) => obstacle.center.x - (obstacle.width ?? 0) / 2,
),
)
const maxX = Math.max(
...componentObstacles.map(
(obstacle) => obstacle.center.x + (obstacle.width ?? 0) / 2,
),
)
const minY = Math.min(
...componentObstacles.map(
(obstacle) => obstacle.center.y - (obstacle.height ?? 0) / 2,
),
)
const maxY = Math.max(
...componentObstacles.map(
(obstacle) => obstacle.center.y + (obstacle.height ?? 0) / 2,
),
)
return {
componentId: portObstacle.componentId as string,
center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
obstacleCount: componentObstacles.length,
}
}
const getDogboneAccessPoint = (
input: SimpleRouteJson,
point: RoutePoint,
otherPoint: RoutePoint,
obstacles: any[],
requireAllLayerPth = false,
): Point => {
const geometry = getPortComponentGeometry(input, point)
if (!geometry) return { x: point.x, y: point.y }
const rawDirection = {
x: point.x - geometry.center.x,
y: point.y - geometry.center.y,
}
const fallbackDirection = {
x: point.x - otherPoint.x,
y: point.y - otherPoint.y,
}
const magnitude = Math.hypot(rawDirection.x, rawDirection.y)
const fallbackMagnitude = Math.hypot(
fallbackDirection.x,
fallbackDirection.y,
)
const direction =
magnitude > 0.05
? { x: rawDirection.x / magnitude, y: rawDirection.y / magnitude }
: fallbackMagnitude > 0.05
? {
x: fallbackDirection.x / fallbackMagnitude,
y: fallbackDirection.y / fallbackMagnitude,
}
: { x: 1, y: 0 }
const ownIds = [point.pointId, point.pcb_port_id].filter(
(id): id is string => Boolean(id),
)
let leastConflictingCandidate:
| { point: Point; blockerCount: number }
| undefined
const angularStep = 10
const angleOffsets = Array.from(
{ length: Math.ceil(360 / angularStep) },
(_, index) => {
if (index === 0) return 0
const magnitude = Math.ceil(index / 2) * angularStep
return index % 2 === 1 ? magnitude : -magnitude
},
)
// A 0.4572 mm PTH plus the 0.08128 mm manufacturing clearance needs about
// 0.31 mm of radial keepout. Inner AM62L balls can require a surface escape
// all the way beyond the package edge before a through-via can be placed.
for (const distanceFromPad of [
0.6, 0.8, 1, 1.25, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 10, 12, 15,
]) {
for (const angleOffset of angleOffsets) {
const angle = (angleOffset * Math.PI) / 180
const rotated = {
x: direction.x * Math.cos(angle) - direction.y * Math.sin(angle),
y: direction.x * Math.sin(angle) + direction.y * Math.cos(angle),
}
const candidate = {
x: point.x + rotated.x * distanceFromPad,
y: point.y + rotated.y * distanceFromPad,
}
const blockerCount = obstacles.reduce((count, obstacle) => {
if (
obstacle.connectedTo?.some((id: string) => ownIds.includes(id))
) {
return count
}
const blocked = requireAllLayerPth
? (obstacle.layers ?? []).some((layer: string) =>
isPointInsideObstacle(candidate, obstacle, layer, 0.31),
)
: isPointInsideObstacle(candidate, obstacle, point.layer, 0.13)
return count + (blocked ? 1 : 0)
}, 0)
if (blockerCount === 0) return candidate
if (
requireAllLayerPth &&
(!leastConflictingCandidate ||
blockerCount < leastConflictingCandidate.blockerCount)
) {
leastConflictingCandidate = { point: candidate, blockerCount }
}
}
}
if (!requireAllLayerPth) {
return {
x: point.x + direction.x,
y: point.y + direction.y,
}
}
// Preserve a complete routed artifact for DRC/visual review even when the
// current placement has no fabrication-legal PTH site. The normal DRC pass
// will report the chosen least-conflicting location; it is never treated as
// a clean route.
if (leastConflictingCandidate) return leastConflictingCandidate.point
throw new Error(`No PTH candidate generated near (${point.x}, ${point.y})`)
}
/**
* Complete deterministic point-to-point peripheral branches. SD leaves one
* short branch to Pipeline 7; USB power leaves its longest branches so shared
* capacity planning remains focused on the distribution paths that need it.
*/
const prepareCustomBranches = (
input: SimpleRouteJson,
connections: PreparedRouteConnection[],
initialTraces: SimplifiedPcbTrace[],
phase: "sd" | "usb-power" | "reset-control" | "power",
) => {
// Pipeline 7's final short SD branch is nondeterministic on this dense
// placement and can exhaust iterations. Route the complete SD set through
// the same committed-copper-aware deterministic pathfinder.
const pipelineConnections: PreparedRouteConnection[] = []
const customConnections =
phase === "power"
? [...connections].sort((first, second) => {
const firstHasBottom = (first.pointsToConnect as RoutePoint[]).some(
(point) => point.layer === "bottom",
)
const secondHasBottom = (second.pointsToConnect as RoutePoint[]).some(
(point) => point.layer === "bottom",
)
if (firstHasBottom !== secondHasBottom) return firstHasBottom ? -1 : 1
return (
manhattanSpan(first) - manhattanSpan(second) ||
first.name.localeCompare(second.name)
)
})
: connections
const traces = [...initialTraces]
for (const connection of customConnections) {
const points = connection.pointsToConnect as RoutePoint[]
if (points.length <= 1) continue
if (points.length !== 2) {
throw new Error(
`Custom ${phase} branch ${connection.name} must have exactly two endpoints`,
)
}
const [start, end] = points
const logicalName = connection.__originalConnectionName ?? connection.name
const committed = [
...(input.traces ?? []),
...traces,
].filter(
(trace) => trace.connection_name !== logicalName,
) as SimplifiedPcbTrace[]
const obstacles = [
...(input.obstacles ?? []),
...getTraceObstacles(committed, input.layerCount),
] as any[]
const hasDenseBgaEndpoint =
phase === "power" &&
[start, end].some(
(point) =>
(getPortComponentGeometry(input, point)?.obstacleCount ?? 0) >= 64,
)
const surfaceSpanLimit = hasDenseBgaEndpoint ? 15 : Number.POSITIVE_INFINITY
const localSurfaceLayer =
start.layer === end.layer &&
(start.layer === "top" || start.layer === "bottom") &&
manhattanSpan(connection) <= surfaceSpanLimit
? start.layer
: undefined
const requiresAllLayerPth = phase === "power" && !localSurfaceLayer
const startAccess = getDogboneAccessPoint(
input,
start,
end,
obstacles,
requiresAllLayerPth,
)
const endAccess = getDogboneAccessPoint(
input,
end,
start,
obstacles,
requiresAllLayerPth,
)
let selected:
| { signalLayer: RoutingLayer; gridPath: Point[] }
| undefined
let lastError: unknown
const candidateLayers: RoutingLayer[] = []
if (localSurfaceLayer) candidateLayers.push(localSurfaceLayer)
// The two central layers are the split power-distribution pair. Power
// branches enter them through PTH drops; signal phases retain inner2 and
// inner5 as their preferred global-routing layers.
if (phase === "power") {
candidateLayers.push("inner3", "inner4", "bottom", "top", "inner5", "inner2")
} else {
candidateLayers.push("inner5", "inner2", "top", "bottom")
}
const isUsbPowerDistribution =
phase === "usb-power" && (connection.__originalPointCount ?? 0) >= 8
if (isUsbPowerDistribution) candidateLayers.push("inner3")
if (!hasDenseBgaEndpoint) {
for (const signalLayer of candidateLayers) {
try {
selected = {
signalLayer,
gridPath: findSignalLayerPath(
input,
startAccess,
endAccess,
obstacles,
signalLayer,
),
}
break
} catch (caught) {
lastError = caught
}
}
}
// Backside decouplers sit directly below the SoC/LPDDR supply clusters, and
// a few inner BGA rail balls are enclosed by the provisional PTH ground
// fanout. Only after every collision-aware layer search fails, place that
// branch on one of the split power layers. Short backside branches stay on
// the capacitor side; longer trapped branches are deterministically spread
// across inner3/inner4 instead of aborting the entire completed phase set.
if (!selected && phase === "power") {
const isLocalBacksideBranch = manhattanSpan(connection) <= 4
const railHash = [...logicalName].reduce(
(hash, character) => (hash * 33 + character.charCodeAt(0)) >>> 0,
5381,
)
const signalLayer: RoutingLayer = isLocalBacksideBranch
? start.layer === "bottom" || end.layer === "bottom"
? "bottom"
: "inner4"
: railHash % 2 === 0
? "inner3"
: "inner4"
const elbow =
railHash % 2 === 0
? { x: endAccess.x, y: startAccess.y }
: { x: startAccess.x, y: endAccess.y }
selected = {
signalLayer,
gridPath: [
startAccess,
elbow,
endAccess,
],
}
}
if (!selected) {
const message =
lastError instanceof Error ? lastError.message : String(lastError)
throw new Error(`${connection.name}: ${message}`)
}
const { signalLayer, gridPath } = selected
const nominalWidth = Math.max(
connection.nominalTraceWidth ?? 0.08128,
isUsbPowerDistribution ? 0.3 : 0,
phase === "power"
? (connection.__originalPointCount ?? 0) >= 8
? 0.25
: 0.15
: 0,
0.08128,
)
const route: SimplifiedPcbTrace["route"] = [
{
route_type: "wire",
x: start.x,
y: start.y,
width: nominalWidth,
layer: start.layer as any,
},
{
route_type: "wire",
x: startAccess.x,
y: startAccess.y,
width: nominalWidth,
layer: start.layer as any,
},
]
if (start.layer !== signalLayer) {
route.push({
route_type: "via",
x: startAccess.x,
y: startAccess.y,
from_layer: "top",
to_layer: "bottom",
via_diameter: 0.4572,
via_hole_diameter: 0.2032,
})
}
route.push(
...gridPath.map((point) => ({
route_type: "wire" as const,
x: point.x,
y: point.y,
width: Math.max(nominalWidth, 0.09398),
layer: signalLayer,
})),
)
if (end.layer !== signalLayer) {
route.push({
route_type: "via",
x: endAccess.x,
y: endAccess.y,
from_layer: "top",
to_layer: "bottom",
via_diameter: 0.4572,
via_hole_diameter: 0.2032,
})
}
route.push(
{
route_type: "wire",
x: endAccess.x,
y: endAccess.y,
width: nominalWidth,
layer: end.layer as any,
},
{
route_type: "wire",
x: end.x,
y: end.y,
width: nominalWidth,
layer: end.layer as any,
},
)
const firstWire = route.find((point) => point.route_type === "wire")
const lastWire = [...route]
.reverse()
.find((point) => point.route_type === "wire")
if (firstWire?.route_type === "wire" && start.pcb_port_id) {
;(firstWire as typeof firstWire & { start_pcb_port_id?: string })
.start_pcb_port_id = start.pcb_port_id
}
if (lastWire?.route_type === "wire" && end.pcb_port_id) {
;(lastWire as typeof lastWire & { end_pcb_port_id?: string })
.end_pcb_port_id = end.pcb_port_id
}
traces.push({
type: "pcb_trace",
pcb_trace_id: `custom_${phase}_branch_${connection.name}`,
connection_name: logicalName,
connectsTo: [
connection.name,
logicalName,
...(connection.mergedConnectionNames ?? []),
...points.flatMap((point) => [point.pointId, point.pcb_port_id]),
].filter((id): id is string => Boolean(id)),
route,
})
}
return {
connections: pipelineConnections,
traces,
}
}
/**
* The two unbroken GND pours are the electrical backbone for ground. Turning a
* 291-terminal ground net into a point-to-point MST is both slow and physically
* wrong, so emit one local plane drop per pad and remove GND from global search.
* Every pad uses a dogbone access point before dropping to the ground plane;
* PTH via-in-pad is intentionally avoided for the dense BGA packages.
*/
const prepareGroundPlaneFanout = (
input: SimpleRouteJson,
): { input: SimpleRouteJson; traces: SimplifiedPcbTrace[] } => {
const groundConnection = (
input.connections as CapacityRouteConnection[]
).find((connection) => connection.pointsToConnect.length >= 200)
if (!groundConnection) return { input, traces: [] }
const traces: SimplifiedPcbTrace[] = []
const points = groundConnection.pointsToConnect as RoutePoint[]
const committed = (input.traces ?? []) as SimplifiedPcbTrace[]
const obstacles = [
...(input.obstacles ?? []),
...getTraceObstacles(committed, input.layerCount),
] as any[]
const planeAnchor = `${groundConnection.name}:unbroken-ground-planes`
for (const [pointIndex, point] of points.entries()) {
const access = getDogboneAccessPoint(input, point, point, obstacles, true)
const width = Math.max(groundConnection.nominalTraceWidth ?? 0.08128, 0.2)
const route: SimplifiedPcbTrace["route"] = [
{
route_type: "wire",
x: point.x,
y: point.y,
width,
layer: point.layer as any,
},
]
if (distance(point, access) > 0.001) {
route.push({
route_type: "wire",
x: access.x,
y: access.y,
width,
layer: point.layer as any,
})
}
route.push(
{
route_type: "via",
x: access.x,
y: access.y,
from_layer: "top",
to_layer: "bottom",
via_diameter: 0.4572,
via_hole_diameter: 0.2032,
},
{
route_type: "wire",
x: access.x,
y: access.y,
width,
layer: "inner1",
},
)
const firstWire = route[0]
if (firstWire.route_type === "wire" && point.pcb_port_id) {
;(firstWire as typeof firstWire & { start_pcb_port_id?: string })
.start_pcb_port_id = point.pcb_port_id
}
traces.push({
type: "pcb_trace",
pcb_trace_id: `custom_ground_plane_drop_${pointIndex + 1}`,
connection_name: groundConnection.name,
connectsTo: [
groundConnection.name,
planeAnchor,
point.pointId,
point.pcb_port_id,
].filter((id): id is string => Boolean(id)),
route,
})
}
return {
input: {
...input,
connections: input.connections.filter(
(connection) => connection !== groundConnection,
),
},
traces,
}
}
/**
* Replays TI's shared package-local DGND breakout for U1/U2, then converts
* every remaining peripheral ground pad into a small independent plane-drop
* connection. This avoids presenting one 291-terminal hyper-net to the global
* router and preserves TI's 18/8 mil PTH convention for the two BGAs.
*/
const prepareTiGroundPlaneFanout = (
input: SimpleRouteJson,
options: { denseBga?: boolean; peripheral?: boolean } = {},
): { input: SimpleRouteJson; traces: SimplifiedPcbTrace[] } => {
const { denseBga = true, peripheral = true } = options
const groundConnection = (
input.connections as CapacityRouteConnection[]
).find((connection) => connection.pointsToConnect.length >= 200)
if (!groundConnection) return { input, traces: [] }
const obstacles = (input.obstacles ?? []) as any[]
const groups = new Map<string, any[]>()
for (const obstacle of obstacles) {
if (!obstacle.componentId || !obstacle.center) continue
const members = groups.get(obstacle.componentId) ?? []
members.push(obstacle)
groups.set(obstacle.componentId, members)
}
const denseComponents = [...groups.entries()]
.filter(([, members]) => members.length >= 150)
.map(([componentId, members]) => {
const xs = members.map((member) => member.center.x)
const ys = members.map((member) => member.center.y)
return {
componentId,
obstacleCount: members.length,
center: {
x: (Math.min(...xs) + Math.max(...xs)) / 2,
y: (Math.min(...ys) + Math.max(...ys)) / 2,
},
}
})
.sort((first, second) => second.obstacleCount - first.obstacleCount)
const componentCenters: Record<"U1" | "U2", Point> = {
U1: denseComponents[0]?.center ?? { x: 2, y: 1 },
U2: denseComponents[1]?.center ?? { x: 22.117, y: 0.949 },
}
const planeAnchor = `${groundConnection.name}:inner1-ground-plane`
const fanoutTraces: SimplifiedPcbTrace[] = []
if (denseBga) {
for (const [segmentIndex, segment] of
TI_AM62L_EVM_GROUND_FANOUT.segments.entries()) {
const center = componentCenters[segment.component]
fanoutTraces.push({
type: "pcb_trace",
pcb_trace_id: `ti_ground_segment_${segmentIndex + 1}`,
connection_name: groundConnection.name,
connectsTo: [groundConnection.name, planeAnchor],
route: [
{
route_type: "wire",
x: center.x + segment.x1,
y: center.y + segment.y1,
width: segment.width,
layer: "top",
},
{
route_type: "wire",
x: center.x + segment.x2,
y: center.y + segment.y2,
width: segment.width,
layer: "top",
},
],
})
}
for (const [viaIndex, via] of TI_AM62L_EVM_GROUND_FANOUT.vias.entries()) {
const center = componentCenters[via.component]
const x = center.x + via.x
const y = center.y + via.y
fanoutTraces.push({
type: "pcb_trace",
pcb_trace_id: `ti_ground_via_${viaIndex + 1}`,
connection_name: groundConnection.name,
connectsTo: [groundConnection.name, planeAnchor],
route: [
{
route_type: "wire",
x,
y,
width: 0.127,
layer: "top",
},
{
route_type: "via",
x,
y,
from_layer: "top",
to_layer: "bottom",
via_diameter: via.diameter,
via_hole_diameter: via.holeDiameter,
},
{
route_type: "wire",
x,
y,
width: 0.127,
layer: "inner1",
},
],
})
}
}
const committedObstacles = [
...obstacles,
...getTraceObstacles(
[...((input.traces ?? []) as SimplifiedPcbTrace[]), ...fanoutTraces],
input.layerCount,
),
] as any[]
if (peripheral) for (const [pointIndex, point] of (
groundConnection.pointsToConnect as RoutePoint[]
).entries()) {
const geometry = getPortComponentGeometry(input, point)
if ((geometry?.obstacleCount ?? 0) >= 150) continue
const access = getDogboneAccessPoint(
input,
point,
point,
committedObstacles,
true,
)
const syntheticPointId =
`${groundConnection.name}:plane-drop:${pointIndex + 1}`
const width = Math.max(groundConnection.nominalTraceWidth ?? 0.08128, 0.2)
const route: SimplifiedPcbTrace["route"] = [
{
route_type: "wire",
x: point.x,
y: point.y,
width,
layer: point.layer as any,
},
{
route_type: "wire",
x: access.x,
y: access.y,
width,
layer: point.layer as any,
},
{
route_type: "via",
x: access.x,
y: access.y,
from_layer: "top",
to_layer: "bottom",
via_diameter: 0.4572,
via_hole_diameter: 0.2032,
},
{
route_type: "wire",
x: access.x,
y: access.y,
width,
layer: "inner1",
},
]
const firstWire = route[0]
if (firstWire.route_type === "wire" && point.pcb_port_id) {
;(firstWire as typeof firstWire & { start_pcb_port_id?: string })
.start_pcb_port_id = point.pcb_port_id
}
fanoutTraces.push({
type: "pcb_trace",
pcb_trace_id: `ground_plane_drop_${pointIndex + 1}`,
connection_name: groundConnection.name,
connectsTo: [
groundConnection.name,
planeAnchor,
point.pointId,
point.pcb_port_id,
syntheticPointId,
].filter((id): id is string => Boolean(id)),
route,
})
}
return {
input: {
...input,
connections: input.connections.filter(
(connection) => connection !== groundConnection,
),
traces: [
...(input.traces ?? []),
...fanoutTraces,
] as SimpleRouteJson["traces"],
},
traces: fanoutTraces,
}
}
const getWireEndpoint = (route: readonly TiRoutePoint[], fromStart: boolean) => {
const ordered = fromStart ? route : [...route].reverse()
const point = ordered.find((candidate) => candidate.route_type === "wire")
if (!point || point.route_type !== "wire") {
throw new Error("TI EVM DDR route has no wire endpoint")
}
return point
}
const reverseTiRoute = (route: readonly TiRoutePoint[]): TiRoutePoint[] =>
[...route].reverse().map((point) =>
point.route_type === "via"
? {
...point,
from_layer: point.to_layer,
to_layer: point.from_layer,
}
: point,
) as TiRoutePoint[]
const findTiSignal = (connection: CapacityRouteConnection) => {
const text = connectionSearchText(connection)
for (const signal of Object.keys(TI_AM62L_EVM_DDR_ROUTES).sort(
(first, second) => second.length - first.length,
)) {
if (text.includes(signal.toUpperCase())) return signal
}
if (text.includes("DDR_LINK_RESET0_N")) return "DDR0_RESET0_n"
const points = connection.pointsToConnect as Point[]
if (points.length !== 2) return undefined
const actualForward = {
x: points[1].x - points[0].x,
y: points[1].y - points[0].y,
}
const actualReverse = { x: -actualForward.x, y: -actualForward.y }
const candidates = Object.entries(TI_AM62L_EVM_DDR_ROUTES)
.map(([signal, reference]) => {
const start = getWireEndpoint(reference.route, true)
const end = getWireEndpoint(reference.route, false)
const expected = { x: end.x - start.x, y: end.y - start.y }
return {
signal,
error: Math.min(
distance(actualForward, expected),
distance(actualReverse, expected),
),
}
})
.sort((first, second) => first.error - second.error)
return candidates[0]?.error < 0.03 ? candidates[0].signal : undefined
}
/**
* Replays TI's released TMDS62LEVM U28-to-U29 escape topology after aligning
* it to this board's two package endpoints. This is deterministic: every DDR
* net has its own released path, PTH-via positions, layer changes, and width.
*/
class TiEvmDdrAutorouter implements GenericLocalAutorouter {
isRouting = false
private readonly handlers: RouterEventHandlers = {
complete: [],
error: [],
progress: [],
}
private traces: SimplifiedPcbTrace[] = []
constructor(public readonly input: SimpleRouteJson) {}
private solve(): SimplifiedPcbTrace[] {
const traces: SimplifiedPcbTrace[] = []
const seenSignals = new Set<string>()
for (const connection of this.input.connections as CapacityRouteConnection[]) {
const signal = findTiSignal(connection)
if (!signal) {
throw new Error(
`TI DDR phase received an unclassified connection: ${connection.name}`,
)
}
if (seenSignals.has(signal)) {
throw new Error(`TI DDR phase received duplicate connection ${signal}`)
}
seenSignals.add(signal)
const reference =
TI_AM62L_EVM_DDR_ROUTES[
signal as keyof typeof TI_AM62L_EVM_DDR_ROUTES
]
const points = connection.pointsToConnect as Array<Point & { layer: string }>
if (points.length !== 2) {
throw new Error(`${signal} must be point-to-point; got ${points.length} endpoints`)
}
const referenceStart = getWireEndpoint(reference.route, true)
const referenceEnd = getWireEndpoint(reference.route, false)
const directError = distance(
{
x: points[1].x - points[0].x,
y: points[1].y - points[0].y,
},
{
x: referenceEnd.x - referenceStart.x,
y: referenceEnd.y - referenceStart.y,
},
)
const reverseError = distance(
{
x: points[0].x - points[1].x,
y: points[0].y - points[1].y,
},
{
x: referenceEnd.x - referenceStart.x,
y: referenceEnd.y - referenceStart.y,
},
)
const reversed = reverseError < directError
const actualStart = reversed ? points[1] : points[0]
const actualEnd = reversed ? points[0] : points[1]
const alignedRoute = reversed
? reverseTiRoute(reference.route)
: [...reference.route]
const alignedStart = getWireEndpoint(alignedRoute, true)
const alignedEnd = getWireEndpoint(alignedRoute, false)
const offset = {
x: actualStart.x - alignedStart.x,
y: actualStart.y - alignedStart.y,
}
const endpointError = distance(actualEnd, {
x: alignedEnd.x + offset.x,
y: alignedEnd.y + offset.y,
})
if (endpointError > 0.03) {
throw new Error(
`${signal} placement differs from TI EVM topology by ${endpointError.toFixed(3)}mm; ` +
"keep U1/U2 orientation and spacing aligned to the reference",
)
}
traces.push({
type: "pcb_trace",
pcb_trace_id: connection.name,
connection_name: connection.name,
connectsTo: points
.map((point) => (point as typeof point & { pointId?: string }).pointId)
.filter((pointId): pointId is string => Boolean(pointId)),
route: alignedRoute.map((point) =>
point.route_type === "via"
? {
...point,
x: point.x + offset.x,
y: point.y + offset.y,
// TI specifies conventional PTH escape vias. The trace may
// connect on inner2, but the drilled/plated barrel spans the
// complete eight-layer stack rather than stopping there.
from_layer: "top",
to_layer: "bottom",
}
: {
...point,
x: point.x + offset.x,
y: point.y + offset.y,
},
) as SimplifiedPcbTrace["route"],
})
}
if (traces.length !== Object.keys(TI_AM62L_EVM_DDR_ROUTES).length) {
throw new Error(
`TI DDR phase routed ${traces.length}/` +
`${Object.keys(TI_AM62L_EVM_DDR_ROUTES).length} released DDR nets`,
)
}
this.traces = traces
return traces
}
start(): void {
if (this.isRouting) return
this.isRouting = true
queueMicrotask(() => {
try {
const traces = this.solve()
this.isRouting = false
for (const handler of this.handlers.progress) {
handler({
type: "progress",
steps: traces.length,
progress: 1,
phase: "ddr:ti-vca-reference",
})
}
for (const handler of this.handlers.complete) {
handler({ type: "complete", traces })
}
} catch (caught) {
this.isRouting = false
const error = caught instanceof Error ? caught : new Error(String(caught))
for (const handler of this.handlers.error) handler({ type: "error", error })
}
})
}
stop(): void {
this.isRouting = false
}
on(event: "complete", callback: (event: AutorouterCompleteEvent) => void): void
on(event: "error", callback: (event: AutorouterErrorEvent) => void): void
on(event: "progress", callback: (event: AutorouterProgressEvent) => void): void
on(
event: "complete" | "error" | "progress",
callback:
| ((event: AutorouterCompleteEvent) => void)
| ((event: AutorouterErrorEvent) => void)
| ((event: AutorouterProgressEvent) => void),
): void {
;(this.handlers[event] as Array<(event: any) => void>).push(callback)
}
solveSync(): SimplifiedPcbTrace[] {
return this.solve()
}
getOutputSimpleRouteJson(): SimpleRouteJson | undefined {
if (this.traces.length === 0) return undefined
return {
...this.input,
traces: [...(this.input.traces ?? []), ...this.traces] as SimpleRouteJson["traces"],
}
}
}
class Am62lClassifiedAutorouter implements GenericLocalAutorouter {
isRouting = false
private timer: ReturnType<typeof setTimeout> | undefined
private solver: AutoroutingPipelineSolver7_MultiGraph | undefined
private readonly handlers: RouterEventHandlers = {
complete: [],
error: [],
progress: [],
}
private steps = 0
private readonly routedTraces: SimplifiedPcbTrace[]
private readonly connectionBatches: SimpleRouteJson["connections"][]
private batchIndex = 0
constructor(
public readonly input: SimpleRouteJson,
private readonly phase: Am62lRoutingPhase,
private readonly effort: number,
initialTraces: SimplifiedPcbTrace[] = [],
) {
this.routedTraces = [...initialTraces]
const batchSize = this.phase === "usb-power" ? 1 : Infinity
this.connectionBatches = []
for (
let startIndex = 0;
startIndex < this.input.connections.length;
startIndex += batchSize
) {
this.connectionBatches.push(
this.input.connections.slice(startIndex, startIndex + batchSize),
)
}
this.startPipelineBatch()
}
private startPipelineBatch(): void {
const connections = this.connectionBatches[this.batchIndex]
if (!connections || connections.length === 0) {
this.solver = undefined
return
}
this.solver = new AutoroutingPipelineSolver7_MultiGraph(
{
...this.input,
connections,
buses: [
...(this.input.buses ?? []),
{
busId: `am62l_${this.phase}_batch_${this.batchIndex}`,
name: `${this.phase} signal-layer reservation`,
connectionNames: connections.map((connection) => connection.name),
allowedLayers: ["top", "inner2", "inner5", "bottom"],
},
],
traces: [
...(this.input.traces ?? []),
...this.routedTraces,
],
} as any,
{
// Custom escape/trunk copper has already reduced the search space.
// Pipeline 7 negotiates each shared phase/batch against committed
// copper, avoiding an expensive mesh rebuild for every single net.
effort: Math.min(this.effort, 1),
},
)
}
private acceptPipelineOutput(): void {
if (!this.solver) return
const output = this.solver.getOutputSimpleRouteJson()
const existingTraceIds = new Set([
...(this.input.traces ?? []).map((trace) => trace.pcb_trace_id),
...this.routedTraces.map((trace) => trace.pcb_trace_id),
])
const activeConnections = this.connectionBatches[
this.batchIndex
] as PreparedRouteConnection[]
const activeEndpointAliases = activeConnections.map((connection) =>
(connection.pointsToConnect as RoutePoint[]).map((point) =>
[point.pointId, point.pcb_port_id].filter(
(id): id is string => Boolean(id),
),
),
)
const generatedTraces = ((output.traces ?? []) as SimplifiedPcbTrace[])
.filter((trace) => {
if (existingTraceIds.has(trace.pcb_trace_id)) return false
const connectedIds = new Set(trace.connectsTo ?? [])
return activeEndpointAliases.some((connectionEndpoints) =>
connectionEndpoints.every((endpointAliases) =>
endpointAliases.some((id) => connectedIds.has(id)),
),
)
})
if (generatedTraces.length === 0) {
throw new Error(
`Pipeline 7 batch ${this.batchIndex + 1} during ${this.phase} ` +
"emitted no new traces",
)
}
this.routedTraces.push(
...generatedTraces.map((trace, traceIndex) => ({
...trace,
pcb_trace_id:
`${trace.pcb_trace_id}_pipeline7_${this.phase}_` +
`${this.batchIndex}_${traceIndex}`,
connectsTo: [
trace.connection_name,
...(trace.connectsTo ?? []),
].filter((id): id is string => Boolean(id)),
})),
)
this.batchIndex += 1
this.solver = undefined
this.startPipelineBatch()
}
private emitError(caught: unknown) {
const error = caught instanceof Error ? caught : new Error(String(caught))
for (const handler of this.handlers.error) {
handler({ type: "error", error })
}
}
private async runChunk() {
if (!this.isRouting) return
try {
if (!this.solver) {
this.isRouting = false
for (const handler of this.handlers.complete) {
handler({ type: "complete", traces: this.routedTraces })
}
return
}
const startedAt = performance.now()
while (
performance.now() - startedAt < 150 &&
!this.solver.solved &&
!this.solver.failed
) {
const asyncSolver = this.solver as typeof this.solver & {
stepAsync?: () => Promise<void>
}
if (typeof asyncSolver.stepAsync === "function") {
await asyncSolver.stepAsync()
} else {
this.solver.step()
}
}
this.steps += 1
if (this.solver.failed) {
this.isRouting = false
this.emitError(
this.solver.error ??
`Pipeline 7 failed during ${this.phase}`,
)
return
}
if (this.solver.solved) {
this.acceptPipelineOutput()
this.timer = setTimeout(() => void this.runChunk(), 0)
return
}
for (const handler of this.handlers.progress) {
const batchProgress = this.solver.progress ?? 0
handler({
type: "progress",
steps: this.steps,
progress:
(this.batchIndex + batchProgress) /
Math.max(this.connectionBatches.length, 1),
phase: `${this.phase}:pipeline7:batch-${this.batchIndex + 1}/` +
`${this.connectionBatches.length}:` +
`${this.connectionBatches[this.batchIndex]?.length ?? 0}-branches:` +
this.solver.getCurrentPhase(),
debugGraphics: this.solver.preview?.(),
})
}
this.timer = setTimeout(() => void this.runChunk(), 0)
} catch (error) {
this.isRouting = false
this.emitError(error)
}
}
start(): void {
if (this.isRouting) return
this.isRouting = true
void this.runChunk()
}
stop(): void {
this.isRouting = false
;(this.solver as
| (AutoroutingPipelineSolver7_MultiGraph & { stop?: () => void })
| undefined)?.stop?.()
if (this.timer) clearTimeout(this.timer)
this.timer = undefined
}
on(event: "complete", callback: (event: AutorouterCompleteEvent) => void): void
on(event: "error", callback: (event: AutorouterErrorEvent) => void): void
on(event: "progress", callback: (event: AutorouterProgressEvent) => void): void
on(
event: "complete" | "error" | "progress",
callback:
| ((event: AutorouterCompleteEvent) => void)
| ((event: AutorouterErrorEvent) => void)
| ((event: AutorouterProgressEvent) => void),
): void {
;(this.handlers[event] as Array<(event: any) => void>).push(callback)
}
solveSync(): SimplifiedPcbTrace[] {
while (this.solver) {
this.solver.solve()
if (this.solver.failed) {
throw new Error(
this.solver.error ?? `Pipeline 7 failed during ${this.phase}`,
)
}
this.acceptPipelineOutput()
}
return this.routedTraces
}
getOutputSimpleRouteJson(): SimpleRouteJson | undefined {
if (this.solver || this.batchIndex < this.connectionBatches.length) {
return undefined
}
return {
...this.input,
traces: [...(this.input.traces ?? []), ...this.routedTraces],
}
}
}
/**
* Final selective reroute pass. Pipeline 9 treats the already released TI DDR
* geometry as immutable preloaded copper and rebuilds every selected
* non-DDR connection around it. The joint-preload repair and generic length
* matcher are deliberately omitted: the former is allowed to move the fixed
* DDR copper, while the latter has no usable non-DDR timing groups and adds
* minutes without improving DRC.
*/
class Am62lPipeline9RerouteAutorouter implements GenericLocalAutorouter {
isRouting = false
private timer: ReturnType<typeof setTimeout> | undefined
private readonly solver: AutoroutingPipelineSolver9_PreloadedTraceGraph
private readonly inputConnectionNames: Set<string>
private readonly handlers: RouterEventHandlers = {
complete: [],
error: [],
progress: [],
}
private steps = 0
constructor(
public readonly input: SimpleRouteJson,
effort: number,
private readonly fixedPhaseTraces: SimplifiedPcbTrace[] = [],
) {
this.inputConnectionNames = new Set(
input.connections.flatMap((connection) => {
const extended = connection as typeof connection & {
__netConnectionName?: string
__rootConnectionNames?: string[]
}
return [
connection.name,
connection.rootConnectionName,
connection.netConnectionName,
extended.__netConnectionName,
...(connection.mergedConnectionNames ?? []),
...(extended.__rootConnectionNames ?? []),
]
}).filter((name): name is string => Boolean(name)),
)
this.solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph(input as any, {
effort: Math.max(1, effort),
})
this.solver.pipelineDef = this.solver.pipelineDef.filter(
(step) =>
step.solverName !== "pipeline9JointDrcRepairSolver" &&
step.solverName !== "lengthMatchingPostProcessingSolver",
)
}
private getSelectedOutputTraces(): SimplifiedPcbTrace[] {
return this.solver.getOutputSimplifiedPcbTraces().filter((trace) => {
const aliases = [
trace.connection_name,
trace.pcb_trace_id,
...(trace.connectsTo ?? []),
]
return aliases.some((alias) =>
alias ? this.inputConnectionNames.has(alias) : false,
)
})
}
private emitError(caught: unknown): void {
const error = caught instanceof Error ? caught : new Error(String(caught))
for (const handler of this.handlers.error) {
handler({ type: "error", error })
}
}
private async runChunk(): Promise<void> {
if (!this.isRouting) return
try {
const startedAt = performance.now()
while (
performance.now() - startedAt < 150 &&
!this.solver.solved &&
!this.solver.failed
) {
const asyncSolver = this.solver as typeof this.solver & {
stepAsync?: () => Promise<void>
}
if (typeof asyncSolver.stepAsync === "function") {
await asyncSolver.stepAsync()
} else {
this.solver.step()
}
}
this.steps += 1
if (this.solver.failed) {
this.isRouting = false
this.emitError(this.solver.error ?? "Pipeline 9 final reroute failed")
return
}
if (this.solver.solved) {
this.isRouting = false
const traces = [
...this.fixedPhaseTraces,
...this.getSelectedOutputTraces(),
]
for (const handler of this.handlers.complete) {
handler({ type: "complete", traces })
}
return
}
for (const handler of this.handlers.progress) {
handler({
type: "progress",
steps: this.steps,
progress: this.solver.progress ?? 0,
phase: `mixed:pipeline9:${this.solver.getCurrentPhase()}`,
debugGraphics: this.solver.preview?.(),
})
}
this.timer = setTimeout(() => void this.runChunk(), 0)
} catch (caught) {
this.isRouting = false
this.emitError(caught)
}
}
start(): void {
if (this.isRouting) return
this.isRouting = true
void this.runChunk()
}
stop(): void {
this.isRouting = false
if (this.timer) clearTimeout(this.timer)
this.timer = undefined
}
on(event: "complete", callback: (event: AutorouterCompleteEvent) => void): void
on(event: "error", callback: (event: AutorouterErrorEvent) => void): void
on(event: "progress", callback: (event: AutorouterProgressEvent) => void): void
on(
event: "complete" | "error" | "progress",
callback:
| ((event: AutorouterCompleteEvent) => void)
| ((event: AutorouterErrorEvent) => void)
| ((event: AutorouterProgressEvent) => void),
): void {
;(this.handlers[event] as Array<(event: any) => void>).push(callback)
}
solveSync(): SimplifiedPcbTrace[] {
this.solver.solve()
if (this.solver.failed) {
throw new Error(this.solver.error ?? "Pipeline 9 final reroute failed")
}
return [...this.fixedPhaseTraces, ...this.getSelectedOutputTraces()]
}
getOutputSimpleRouteJson(): SimpleRouteJson | undefined {
if (!this.solver.solved) return undefined
return {
...this.input,
traces: [
...(this.input.traces ?? []),
...this.getSelectedOutputTraces(),
],
}
}
}
export const createAm62lClassifiedAutorouter = ({
phase,
effort = 2,
}: {
phase: Am62lRoutingPhase
effort?: number
}) =>
async (simpleRouteJson: SimpleRouteJson): Promise<GenericLocalAutorouter> => {
if (phase === "ddr") return new TiEvmDdrAutorouter(simpleRouteJson)
if (phase === "power") {
const denseGroundPrepared = prepareTiGroundPlaneFanout(simpleRouteJson, {
denseBga: true,
peripheral: false,
})
const routingInput: SimpleRouteJson = {
...denseGroundPrepared.input,
// The TI/peripheral ground fanout is emitted by this phase. Keep it
// out of the preloaded list to avoid duplicating the same trace IDs in
// the transformed phase output, while still passing it as committed
// copper to the branch planner below.
traces: simpleRouteJson.traces,
}
const globalTopologyInput = useGlobalTopologyForDenseComponents(
routingInput,
)
const branchedConnections = expandConnectionsIntoMinimumSpanBranches(
globalTopologyInput.connections as CapacityRouteConnection[],
)
const customPrepared = prepareCustomBranches(
globalTopologyInput,
branchedConnections,
denseGroundPrepared.traces,
"power",
)
const peripheralGroundPrepared = prepareTiGroundPlaneFanout(
{
...simpleRouteJson,
traces: [
...((simpleRouteJson.traces ?? []) as SimplifiedPcbTrace[]),
...customPrepared.traces,
],
},
{ denseBga: false, peripheral: true },
)
const preparedInput: SimpleRouteJson = {
...globalTopologyInput,
connections: orderAm62lConnections(
customPrepared.connections,
) as SimpleRouteJson["connections"],
}
return new Am62lClassifiedAutorouter(
preparedInput,
phase,
effort,
[...customPrepared.traces, ...peripheralGroundPrepared.traces],
)
}
if (phase === "sd") {
const preparedEscapes = prepareTiMmc1Escapes(simpleRouteJson)
const globalTopologyInput = useGlobalTopologyForDenseComponents(
preparedEscapes.input,
)
const branchedConnections = expandConnectionsIntoMinimumSpanBranches(
globalTopologyInput.connections as CapacityRouteConnection[],
)
const branchNamesByLogical = new Map<string, string[]>()
for (const connection of branchedConnections) {
const logical = connection.__originalConnectionName ?? connection.name
const branchNames = branchNamesByLogical.get(logical) ?? []
branchNames.push(connection.name)
branchNamesByLogical.set(logical, branchNames)
}
const escapeTraces = preparedEscapes.escapeTraces.map((trace) => ({
...trace,
connectsTo: [
...(trace.connectsTo ?? []),
...(branchNamesByLogical.get(trace.connection_name ?? "") ?? []),
],
}))
const trunkPrepared = prepareSdInner5Trunks(
globalTopologyInput,
branchedConnections,
escapeTraces,
)
const customPrepared = prepareCustomBranches(
globalTopologyInput,
trunkPrepared.connections,
trunkPrepared.traces,
"sd",
)
const preparedInput: SimpleRouteJson = {
...globalTopologyInput,
connections: orderAm62lConnections(
customPrepared.connections,
) as SimpleRouteJson["connections"],
}
return new Am62lClassifiedAutorouter(
preparedInput,
phase,
effort,
customPrepared.traces,
)
}
if (
phase === "reset-control" ||
phase === "usb-power"
) {
const globalTopologyInput = useGlobalTopologyForDenseComponents(
simpleRouteJson,
)
const branchedConnections = expandConnectionsIntoMinimumSpanBranches(
globalTopologyInput.connections as CapacityRouteConnection[],
)
const customPrepared = prepareCustomBranches(
globalTopologyInput,
branchedConnections,
[],
phase,
)
const preparedInput: SimpleRouteJson = {
...globalTopologyInput,
connections: orderAm62lConnections(
customPrepared.connections,
) as SimpleRouteJson["connections"],
}
return new Am62lClassifiedAutorouter(
preparedInput,
phase,
effort,
customPrepared.traces,
)
}
return new Am62lPipeline9RerouteAutorouter(simpleRouteJson, effort)
}