0hmX/am3352
This code suite comprises TypeScript scripts that analyze, verify, and assemble complex DDR memory interface hardware, focusing on physical routing, via and pad placement, electrical clearance, and physical constraints, often involving precise geometric calculations and consistent provenance tracking.
- Version
- 1.0.5
- License
- unset
- Stars
- 0
scripts/generate-ddr.ts
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
import assert from 'node:assert/strict'
import { FanoutSolver } from '@tscircuit/fanout-solver'
import { fanoutTracePath } from '@tscircuit/props'
import { AM3352_PINS } from '../src/pin-map'
import { AM3352_PLANE_DROPS } from '../src/layout'
import { DDR_PROFILE_DEFINITIONS } from '../src/profiles'
import { validateDdrCopper } from './ddr-copper-check'
import { compactFanout, simplifyExitTail } from './compact-fanout'
// Keep the checked non-DDR copper; solve DDR around it with TI-sized geometry.
const profile = 'ddr_left'
const settings = DDR_PROFILE_DEFINITIONS[profile]
// Smallest successful checked solver bracket (9.10 mm west failed pair skew).
// The final enclosure is fitted to actual copper after solving.
const solveBounds = { minX: -9.25, maxX: 8.5, minY: -8.5, maxY: 9.25 }
const f = JSON.parse(readFileSync('scripts/fixtures/native.input.json', 'utf8'))
const { input, options } = f
const layers = ['top', ...Array.from({ length: 8 }, (_, i) => `inner${i + 1}`), 'bottom']
const signalLayer = (l: string): string => ({ top: 'top', inner1: 'inner1', inner2: 'inner2', inner3: 'inner4', inner4: 'inner6', bottom: 'bottom' }[l] ?? l)
const originals = structuredClone(input.connections)
const pinFor = (n: string) => {
const c = originals.find((c: any) => c.name === n)
const pin = AM3352_PINS.find(p => Math.hypot(p.x - c.pointsToConnect[0].x, p.y - c.pointsToConnect[0].y) < 1e-6)
assert.ok(pin, `Missing pin for ${n}`)
return pin
}
const connectionFor = (ball: string) => originals.find((c: any) => pinFor(c.name).ballName === ball)
input.layerCount = 10
input.allowBlindAndBuriedVias = false
input.bounds = options.sharedBoundary = solveBounds
for (const o of input.obstacles) o.layers = o.circuitJsonMetadata?.pcb_via_id ? layers : o.layers.map(signalLayer)
const remapRoute = (route: any[], layerMap = signalLayer) => route.map(q => {
if (q.route_type === 'wire') return { ...q, layer: layerMap(q.layer) }
return { ...q, from_layer: layerMap(q.from_layer), to_layer: layerMap(q.to_layer), layers }
})
for (const t of input.traces) t.route = remapRoute(t.route)
const fixed = structuredClone(input.traces)
const saved = JSON.parse(readFileSync('src/generated/native.trace-paths.json', 'utf8'))
const retained = saved.filter((p: any) => !AM3352_PINS.find(pin => `U1.${pin.ballName}` === p.connection)!.name.startsWith('DDR_')).map((p: any) => {
const ball = p.connection.split('.')[1]
const c = connectionFor(ball)
const pin = pinFor(c.name)
const plane = AM3352_PLANE_DROPS.find(d => d.pinNumber === pin.pinNumber)
const planeLayer = plane ? settings.powerPlaneLayers[plane.netName]! : undefined
// Full barrels contact top I/O copper; retain a nonzero logical span.
const layerMap = plane ? (l: string) => l === 'top' ? 'top' : planeLayer === 'top' ? 'inner2' : planeLayer! : signalLayer
const route = remapRoute(p.route, layerMap)
const end = route.at(-1)!
if (!plane) {
const tail = { ...end }
if (Math.abs(end.x) > 8.499) tail.x = end.x < 0 ? solveBounds.minX : solveBounds.maxX
else tail.y = end.y < 0 ? solveBounds.minY : solveBounds.maxY
route.push(tail)
}
return { type: 'pcb_trace', pcb_trace_id: `retained_${ball}`, connection_name: c.name, route: simplifyExitTail(route) }
})
// D4 cannot escape directly west around the fixed through-vias. Preserve its
// northward channel, widen it, then turn west outside the package via field.
const a8 = saved.find((p: any) => p.connection === 'U1.D4')
const a8Route = remapRoute(a8.route).map(q => q.route_type === 'wire'
? { ...q, width: settings.traceWidthMm }
: { ...q, via_diameter: settings.viaPadDiameterMm, via_hole_diameter: settings.viaHoleDiameterMm })
a8Route.push(
{ route_type: 'wire', x: -4.4, y: 8.8, layer: 'inner6', width: settings.traceWidthMm },
{ route_type: 'wire', x: -4.8, y: 9.2, layer: 'inner6', width: settings.traceWidthMm },
{ route_type: 'wire', x: solveBounds.minX, y: 9.2, layer: 'inner6', width: settings.traceWidthMm },
)
const a8Trace = { type: 'pcb_trace', pcb_trace_id: 'ddr_a8_channel', connection_name: connectionFor('D4').name, route: a8Route }
input.traces = [...fixed, ...retained, a8Trace]
const ddrNames = new Set(originals.filter((c: any) => pinFor(c.name).name.startsWith('DDR_')).map((c: any) => c.name))
input.connections = originals.filter((c: any) => ddrNames.has(c.name) && c.name !== a8Trace.connection_name)
for (const c of input.connections) {
c.width = c.nominalTraceWidth = settings.traceWidthMm
for (const p of c.pointsToConnect) if (p.layer) p.layer = signalLayer(p.layer)
}
input.buses = input.buses.filter((b: any) => b.connectionNames.some((n: string) => ddrNames.has(n)) && !b.connectionNames.includes(a8Trace.connection_name))
for (const b of input.buses) {
b.direction = 'left'; b.exitEdge = 'left'; b.preferredExit = 'left'
b.allowedLayers = ['top', 'inner2', 'inner4', 'inner6']
options.busDirections[b.busId] = 'left'
}
input.differentialPairs = input.differentialPairs.filter((p: any) => p.connectionNames.some((n: string) => ddrNames.has(n)))
Object.assign(options, {
buses: input.buses, escapeLayers: ['top', 'inner2', 'inner4', 'inner6'],
traceWidth: settings.traceWidthMm, viaDiameter: settings.viaPadDiameterMm, viaHoleDiameter: settings.viaHoleDiameterMm,
clearance: settings.clearanceMm, maxLayerCombinations: 16, singleLayerAdaptiveExits: false,
densePlaneReservationBusIds: [], denseUnrestrictedPlaneRoutingBusIds: [], allowBlindAndBuriedVias: false,
})
mkdirSync('dist/solver', { recursive: true })
writeFileSync(`dist/solver/${profile}.input.json`, JSON.stringify(f, null, 2) + '\n')
const solver = new FanoutSolver(input, options)
let last = Date.now()
while (!solver.solved && !solver.failed) {
solver.step()
if (Date.now() - last > 15000) { console.log(solver.stats); last = Date.now() }
}
assert.ok(solver.solved, solver.error ?? 'DDR solver failed')
const output = solver.getOutput()
assert.ok(output.validation.valid, JSON.stringify(output.validation))
const fullInput = { ...input, connections: originals, traces: fixed }
const compacted = compactFanout(fullInput, [...retained, ...output.fanoutTraces, a8Trace], solveBounds)
const all = compacted.traces
const ddrTraces = all.filter(t => ddrNames.has(t.connection_name))
fullInput.bounds = compacted.bounds
const boundsPath = "src/generated/profile-bounds.json"
const profileBounds = JSON.parse(readFileSync(boundsPath, "utf8"))
profileBounds[profile] = compacted.bounds
const { drc, ddrDrc } = validateDdrCopper(fullInput, all, ddrTraces)
assert.ok(ddrDrc.valid && drc.valid, JSON.stringify({ ddrDrc, drc }))
assert.equal(all.length, 313)
assert.equal(new Set(all.map(t => t.connection_name)).size, 313)
for (const t of ddrTraces) assert.ok(Math.abs(t.route.at(-1)!.x - compacted.bounds.minX) < 1e-6, `${t.connection_name} must exit left`)
writeFileSync(boundsPath, JSON.stringify(profileBounds, null, 2) + "\n")
const paths = all.map(t => fanoutTracePath.parse({ connection: `U1.${pinFor(t.connection_name!).ballName}`, route: t.route }))
writeFileSync(`src/generated/${profile}.trace-paths.json`, JSON.stringify(paths, null, 2) + '\n')
writeFileSync(`scripts/fixtures/${profile}.input.json`, JSON.stringify({ ...f, options: { ...options, sharedBoundary: compacted.bounds }, input: fullInput, compaction: { solveBounds, marginMm: compacted.marginMm, gridMm: compacted.gridMm }, requestedExits: Object.fromEntries(ddrTraces.map(t => [t.connection_name, 'left'])) }, null, 2) + '\n')
writeFileSync(`dist/solver/${profile}-output.json`, JSON.stringify({ output: { ...output, fanoutTraces: all }, drc, ddrDrc }, null, 2) + '\n')
console.log(`${profile}: 313 saved paths; all 52 DDR-related terminals exit left; DDR and full copper DRC pass`)