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`)