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

src/exits.ts

import { getAM3352TracePaths } from './saved-paths'
import { AM3352_PINS } from './pin-map'
import { assertLayoutProfile, getAM3352Bounds, getAM3352Layers, LAYOUT_PROFILES, type LayoutProfile } from './profiles'

export type ExitSide = 'left' | 'right' | 'top' | 'bottom'
export interface ExitVector { x: number; y: number }
export interface AM3352Exit {
  profile: LayoutProfile
  signal: string
  ball: string
  pinNumber: number
  /** Side in the unrotated module coordinate frame. */
  side: ExitSide
  x: number
  y: number
  layer: string
  /** Boundary normal, directed away from the module; unit vector. */
  outwardDirection: ExitVector
  /** Last nonzero saved segment, directed from source toward exit; unit vector. */
  finalSegmentDirection: ExitVector
  /** First nonzero saved segment, directed away from the BGA ball; unit vector. */
  sourceEscapeDirection: ExitVector
  /** Centerline copper length in XY only; excludes via vertical length and package delay. */
  planarLengthMm: number
  planarLengthByLayerMm: Record<string, number>
  logicalVias: Array<{ x: number; y: number; fromLayer: string; toLayer: string }>
  /** Actual barrels are through-hole, regardless of logical route transitions. */
  physicalViaLayers: readonly string[]
}
export interface AM3352ExitPlacement { pcbX?: number; pcbY?: number; pcbRotation?: number }
const normals: Record<ExitSide, ExitVector> = {
  left: { x: -1, y: 0 }, right: { x: 1, y: 0 },
  top: { x: 0, y: 1 }, bottom: { x: 0, y: -1 },
}
const epsilon = 1e-6

/** Derive the boundary contract from saved copper, never requested solver targets. */
export function getAM3352Exits(profile: LayoutProfile = 'native'): AM3352Exit[] {
  assertLayoutProfile(profile)
  const bounds = getAM3352Bounds(profile)
  return getAM3352TracePaths(profile).flatMap(path => {
    const last = path.route.at(-1)
    if (!last || last.route_type !== 'wire') return []
    const boundarySides = (Object.keys(normals) as ExitSide[]).filter(side => {
      if (side === 'left') return Math.abs(last.x - bounds.minX) < epsilon
      if (side === 'right') return Math.abs(last.x - bounds.maxX) < epsilon
      if (side === 'top') return Math.abs(last.y - bounds.maxY) < epsilon
      return Math.abs(last.y - bounds.minY) < epsilon
    })
    if (!boundarySides.length) return [] // Fixed local supply/decoupling connection.
    if (boundarySides.length !== 1 || last.x < bounds.minX-epsilon || last.x > bounds.maxX+epsilon || last.y < bounds.minY-epsilon || last.y > bounds.maxY+epsilon) {
      throw Error(`Ambiguous boundary endpoint for ${path.connection}`)
    }
    const pin = AM3352_PINS.find(pin => path.connection === `U1.${pin.ballName}`)
    if (!pin) throw Error(`Unknown signal exit connection ${path.connection}`)
    const first = path.route[0]
    if (!first || Math.hypot(first.x - pin.x, first.y - pin.y) > epsilon) {
      throw Error(`Saved path does not start at source ball ${pin.ballName}`)
    }
    const planarLengthByLayerMm: Record<string, number> = {}
    const segments: ExitVector[] = []
    const logicalVias: AM3352Exit['logicalVias'] = []
    for (let i = 0; i < path.route.length; i++) {
      const point = path.route[i]!
      if (point.route_type === 'via') logicalVias.push({ x: point.x, y: point.y, fromLayer: point.from_layer, toLayer: point.to_layer })
      const next = path.route[i + 1]
      if (!next) continue
      const dx = next.x - point.x, dy = next.y - point.y
      const length = Math.hypot(dx, dy)
      if (length < 1e-12) continue
      // A nonzero span must join two wire points on the same copper layer.
      if (point.route_type !== 'wire' || next.route_type !== 'wire' || point.layer !== next.layer) {
        throw Error(`Discontinuous saved route at ${path.connection} segment ${i}`)
      }
      planarLengthByLayerMm[point.layer] = (planarLengthByLayerMm[point.layer] ?? 0) + length
      segments.push({ x: dx / length, y: dy / length })
    }
    if (!segments.length) throw Error(`No planar copper in ${path.connection}`)
    return [{ profile, signal: pin.name, ball: pin.ballName, pinNumber: pin.pinNumber,
      side: boundarySides[0]!, x: last.x, y: last.y, layer: last.layer,
      outwardDirection: { ...normals[boundarySides[0]!] },
      finalSegmentDirection: segments.at(-1)!, sourceEscapeDirection: segments[0]!,
      planarLengthMm: Object.values(planarLengthByLayerMm).reduce((sum, length) => sum + length, 0),
      planarLengthByLayerMm, logicalVias, physicalViaLayers: getAM3352Layers(profile),
    }]
  }).sort((a, b) => a.pinNumber - b.pinNumber)
}

/** CCW rotation in PCB XY (+Y up), then translation. Layer and local side remain unchanged. */
export function transformAM3352Exit(exit: AM3352Exit, placement: AM3352ExitPlacement = {}): AM3352Exit {
  const { pcbX = 0, pcbY = 0, pcbRotation = 0 } = placement
  if (![pcbX, pcbY, pcbRotation].every(Number.isFinite)) throw Error('Exit placement must be finite')
  const angle = pcbRotation * Math.PI / 180
  const rotate = ({ x, y }: ExitVector): ExitVector => ({ x: x * Math.cos(angle) - y * Math.sin(angle), y: x * Math.sin(angle) + y * Math.cos(angle) })
  const position = (point: ExitVector) => { const p = rotate(point); return { x: p.x + pcbX, y: p.y + pcbY } }
  return { ...exit, ...position(exit), outwardDirection: rotate(exit.outwardDirection),
    finalSegmentDirection: rotate(exit.finalSegmentDirection), sourceEscapeDirection: rotate(exit.sourceEscapeDirection),
    planarLengthByLayerMm: { ...exit.planarLengthByLayerMm }, physicalViaLayers: [...exit.physicalViaLayers],
    logicalVias: exit.logicalVias.map(via => ({ ...via, ...position(via) })),
  }
}