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-directions.ts
import assert from 'node:assert/strict'
import { readFileSync, writeFileSync } from 'node:fs'
import { fanoutTracePath } from '@tscircuit/props'
import { validateDdrCopper } from './ddr-copper-check'
import { compactFanout } from './compact-fanout'
import { assertRouteAngles } from './check-route-angles'
import { AM3352_PINS } from '../src/pin-map'
// Same CPU orientation, independently routed north/south escape corridors.
// The eight neighbouring west auxiliary signals move with DDR so their copper
// does not cut across the new corridor. Existing outer pair meanders are
// removed and replaced with broad 45-degree tuning in the vertical corridor.
const candidatePath = process.argv.find(a => a.startsWith('--candidate='))?.slice(12)
const baseline = JSON.parse(readFileSync(candidatePath ?? 'dist/solver/ddr_left-output.json', 'utf8'))
const fixture = candidatePath ? { input: baseline.input, options: baseline.seed.options } : JSON.parse(readFileSync('scripts/fixtures/ddr_left.input.json', 'utf8'))
const pitch = .4064
const signal = (t: any) => AM3352_PINS.find(p => Math.hypot(p.x - t.route[0].x, p.y - t.route[0].y) < 1e-6)!
const planarLength = (t: any): number => t.route.slice(1).reduce((sum: number, p: any, i: number) => sum + Math.hypot(p.x - t.route[i].x, p.y - t.route[i].y), 0)
const fixedLeft = Math.min(
...fixture.input.obstacles.map((o: any) => o.center.x - o.width / 2),
...fixture.input.traces.flatMap((t: any) => t.route.map((p: any) => p.x - (p.route_type === 'via' ? p.via_diameter : p.width) / 2)),
)
for (const side of ['top', 'bottom'] as const) {
const profile = `ddr_${side}`, dir = side === 'top' ? 1 : -1
let traces = structuredClone(baseline.output.fanoutTraces)
const west = traces.filter((t: any) => Math.abs(t.route.at(-1).x - fixture.input.bounds.minX) < 1e-6)
assert.equal(west.length, 60, 'Expected 52 DDR-related and eight adjacent west signals')
for (const t of west) {
let cut = false
for (let i = 1; i < t.route.length; i++) {
const a = t.route[i - 1], b = t.route[i]
if (a.route_type !== 'wire' || b.route_type !== 'wire' || a.layer !== b.layer || a.x <= fixedLeft || b.x > fixedLeft) continue
assert.ok(!t.route.slice(i).some((p: any) => p.route_type === 'via'), 'Only external wire tails can be replaced')
const point = { ...b, x: fixedLeft, y: a.y + (b.y - a.y) * (fixedLeft - a.x) / (b.x - a.x) }
t.route = [...t.route.slice(0, i), point]
cut = true
break
}
assert.ok(cut, `No external west wire crossing for ${t.connection_name}`)
}
const columns: Record<string, number> = {}
for (const layer of ['top', 'inner2', 'inner4', 'inner6']) {
const group = west.filter((t: any) => t.route.at(-1).layer === layer).sort((a: any, b: any) => dir * (a.route.at(-1).y - b.route.at(-1).y))
assert.ok(group.length)
// First column is outside the actual fixed copper envelope. Quantization
// is outward to 0.01 mm; independent DRC checks the resulting geometry.
const nearestWidth = group.at(-1).route.at(-1).width
columns[layer] = Math.floor((fixedLeft - nearestWidth / 2 - .1016) * 100) / 100
group.forEach((t: any, i: number) => {
const end = t.route.at(-1)
end.x = columns[layer]! - (group.length - 1 - i) * pitch
// Replace the horizontal-to-vertical right angle with two 45° turns.
// End the horizontal run early, then join its assigned vertical column.
const chamfer = .2032
end.x += chamfer
t.route.push({ ...end, x: end.x - chamfer, y: end.y + dir * chamfer }, { ...end, x: end.x - chamfer, y: dir * 30 })
})
}
// Put every other external terminal on a temporary common rectangle so the
// final fitter can shorten its collinear tail using actual retained copper.
for (const t of traces.filter((t: any) => !west.includes(t))) {
const end = t.route.at(-1), b = fixture.input.bounds
if (end.route_type !== 'wire') continue
if (Math.abs(end.x - b.maxX) < 1e-6) end.x = 30
else if (Math.abs(end.y - b.maxY) < 1e-6) end.y = 30
else if (Math.abs(end.y - b.minY) < 1e-6) end.y = -30
}
for (const [a, b] of [['DDR_CK', 'DDR_CKn'], ['DDR_DQS0', 'DDR_DQSn0'], ['DDR_DQS1', 'DDR_DQSn1']]) {
const pair = [traces.find((t: any) => signal(t).name === a), traces.find((t: any) => signal(t).name === b)].sort((a: any, b: any) => planarLength(a) - planarLength(b))
const extra = planarLength(pair[1]) - planarLength(pair[0])
if (extra <= .127) continue
const t = pair[0]
// A trapezoid uses 45° diagonals separated by >=0.1016 mm straight
// sections. Use available horizontal run first, then the exit column.
// Each cell adds 0.1683 mm, leaving <=0.0842 mm quantized pair skew.
const count = Math.round(extra / (.4064 * (Math.SQRT2 - 1)))
const original = structuredClone(t.route)
const horizontalEnd = original.length - 3
const horizontalLength = original[horizontalEnd - 1].x - original[horizontalEnd].x
const capacity = Math.max(0, Math.min(count, Math.floor((horizontalLength - .4064) / .6096)))
let accepted = false
for (let horizontalCount = capacity; horizontalCount >= 0 && !accepted; horizontalCount--) {
for (const waveSign of [1, -1]) {
t.route = structuredClone(original)
if (horizontalCount) {
const a = original[horizontalEnd - 1], b = original[horizontalEnd]
assert.ok(Math.abs(a.y - b.y) < 1e-6)
const wave = [{ ...a, x: a.x - .2032 }]
for (let j = 0; j < horizontalCount; j++) {
const x = a.x - .2032 - j * .6096
wave.push(
{ ...a, x: x - .2032, y: a.y + waveSign * .2032 },
{ ...a, x: x - .3048, y: a.y + waveSign * .2032 },
{ ...a, x: x - .5080 },
{ ...a, x: x - .6096 },
)
}
t.route.splice(horizontalEnd, 0, ...wave)
}
const end = t.route.pop(), start = dir * t.route.at(-1).y + .4064
if (count > horizontalCount) t.route.push({ ...end, y: dir * start })
for (let j = 0; j < count - horizontalCount; j++) {
const y = dir * (start + j * .6096)
t.route.push(
{ ...end, x: end.x + .2032, y: y + dir * .2032 },
{ ...end, x: end.x + .2032, y: y + dir * .3048 },
{ ...end, y: y + dir * .5080 },
{ ...end, y: y + dir * .6096 },
)
}
t.route.push(end)
const candidateDdr = traces.filter((trace: any) => signal(trace).name.startsWith('DDR_'))
const check = validateDdrCopper(fixture.input, traces, candidateDdr)
if (check.drc.valid && check.ddrDrc.valid) {
console.log(`${profile} ${signal(t).name}: ${horizontalCount} horizontal, ${count - horizontalCount} vertical tuning cells`)
accepted = true
break
}
}
}
assert.ok(accepted, `${profile}: no clear 45° tuning for ${signal(t).name}`)
}
const fitted = compactFanout(fixture.input, traces, { minX: -30, maxX: 30, minY: -30, maxY: 30 })
traces = fitted.traces
assertRouteAngles(traces)
const bounds = fitted.bounds
const input = { ...fixture.input, bounds }
const ddr = traces.filter((t: any) => signal(t).name.startsWith('DDR_'))
const { drc, ddrDrc } = validateDdrCopper(input, traces, ddr)
const pairs = [['DDR_CK', 'DDR_CKn'], ['DDR_DQS0', 'DDR_DQSn0'], ['DDR_DQS1', 'DDR_DQSn1']].map(names => {
const lengths = names.map(name => planarLength(traces.find((t: any) => signal(t).name === name)))
const skew = Math.abs(lengths[0]! - lengths[1]!)
assert.ok(skew <= .127 + 1e-6, `${names.join('/')}: pair skew ${skew}`)
return { signals: names, planarSkewMm: skew, planarLengthsMm: lengths }
})
writeFileSync(`dist/solver/${profile}-drc.json`, JSON.stringify({ drc, ddrDrc, pairs, bounds, columns }, null, 2) + '\n')
assert.ok(drc.valid && ddrDrc.valid, JSON.stringify({ profile, drc, ddrDrc }))
assert.equal(traces.length, 313)
assert.equal(ddr.length, 52)
assert.ok(ddr.every((t: any) => Math.abs(t.route.at(-1).y - (side === 'top' ? bounds.maxY : bounds.minY)) < 1e-6))
const paths = traces.map((t: any) => fanoutTracePath.parse({ connection: `U1.${signal(t).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({ ...fixture, options: { ...fixture.options, sharedBoundary: bounds }, compaction: { marginMm: fitted.marginMm, gridMm: fitted.gridMm, fixedLeft, columnPitchMm: pitch, columns }, input, requestedExits: Object.fromEntries(ddr.map((t: any) => [t.connection_name, side])) }, null, 2) + '\n')
const paddingMm = { left: -bounds.minX - 7.5, right: bounds.maxX - 7.5, top: bounds.maxY - 7.5, bottom: -bounds.minY - 7.5 }
writeFileSync(`dist/solver/${profile}-output.json`, JSON.stringify({ output: { ...baseline.output, fanoutTraces: traces }, drc, ddrDrc, pairs, bounds, columns, paddingMm }, null, 2) + '\n')
const allBounds = JSON.parse(readFileSync('src/generated/profile-bounds.json', 'utf8'))
allBounds[profile] = bounds
writeFileSync('src/generated/profile-bounds.json', JSON.stringify(allBounds, null, 2) + '\n')
console.log(`${profile}: ${JSON.stringify(bounds)}, padding ${JSON.stringify(paddingMm)}; full copper and DDR DRC pass`)
}