astra/am3352-sbc

A 4-layer AM3352 single-board computer with 128‑MiB DDR3, PMIC power regulation, USB‑C 5‑V PD input, dual USB host ports, HDMI output with ESD protection, microSD/UART/JTAG interfaces, addressable RGB indicators, and a PWM-driven buzzer.

Version
0.1.19
License
unset
Stars
0

design/ddr-compliance.ts

import type { SimpleRouteJson, Trace } from "@tscircuit/bus-lanes-solver";
import { DDR_RULES, ddrTimingGroups, ddrPairs, ddrTraceName } from "./ddr-rules";
import { sharedNets } from "./ddr-netlist";

export type SignalNames = Record<string, string>;
/** Core exports bus members in the declared connections order. Used only by
 * the router adapter; the independent board audit resolves actual source nets.
 */
export function signalNamesFromDeclaredBuses(input: SimpleRouteJson): SignalNames {
  const names: SignalNames = {};
  for (const group of ddrTimingGroups) {
    const bus = input.buses?.find(b => b.busId === group.name || b.name === group.name);
    if (!bus || bus.connectionNames.length !== group.signals.length)
      throw new Error(`Missing or incomplete timing bus: ${group.name}`);
    bus.connectionNames.forEach((name, i) => {
      if (names[name]) throw new Error(`Signal occurs in more than one DDR timing group: ${name}`);
      names[name] = group.signals[i];
    });
  }
  const remaining = input.connections.filter(c => !names[c.name]);
  if (remaining.length !== 1) throw new Error("Expected only RESET# outside clocked DDR timing groups");
  names[remaining[0].name] = "DDR_RESETn_SIGNAL";
  assertDdrConstraints(input, names);
  return names;
}
export function assertDdrConstraints(input: SimpleRouteJson, names: SignalNames) {
  const problems: string[] = [];
  const connectionBySignal = new Map(input.connections.map(c => [names[c.name], c.name]));
  const expectedSignals = sharedNets.map(ddrTraceName);
  if (input.connections.length !== expectedSignals.length ||
      expectedSignals.some(n => !connectionBySignal.has(n)))
    problems.push("DDR input must contain every one of the 47 CPU-to-RAM signals exactly once");
  for (const group of ddrTimingGroups) {
    const actual = input.buses?.find(b => b.name === group.name || b.busId === group.name);
    const expected = group.signals.map(s => connectionBySignal.get(s)).sort();
    if (!actual || JSON.stringify([...actual.connectionNames].sort()) !== JSON.stringify(expected) ||
        actual.maxLengthSkew === undefined || !Number.isFinite(actual.maxLengthSkew) || actual.maxLengthSkew < 0 || actual.maxLengthSkew > group.maxLengthSkew)
      problems.push(`Missing, incomplete or relaxed timing bus: ${group.name}`);
    if (!actual?.allowedLayers?.length || actual.allowedLayers.some(l => !DDR_RULES.signalLayers.includes(l as any)))
      problems.push(`Missing inner-layer restriction: ${group.name}`);
  }
  for (const pair of ddrPairs) {
    const members = pair.signals.map(s => connectionBySignal.get(s));
    const actual = input.differentialPairs?.find(p => p.connectionNames.length === 2 && p.connectionNames[0] === members[0] && p.connectionNames[1] === members[1]);
    if (!actual || !Number.isFinite(actual.lengthTolerance) || actual.lengthTolerance < 0 || actual.lengthTolerance > DDR_RULES.pairSkewMm)
      problems.push(`Missing or relaxed differential pair: ${pair.name}`);
  }
  if (problems.length) throw new Error(problems.join("; "));
}

/** Pad-to-pad copper lengths include surface escapes. Through-via delays are
 * not guessed: stackup, package flight times, impedance and SI remain separate gates.
 */
export function auditDdrGeometry(input: SimpleRouteJson, traces: Trace[], names: SignalNames) {
  const failures: string[] = [];
  try { assertDdrConstraints(input, names); } catch (e) { failures.push(String(e)); }
  const eps = 1e-7;
  const measurements = input.connections.map(c => {
    const failureCountBefore = failures.length;
    const matches = traces.filter(t => (t.connection_name ?? t.source_trace_id) === c.name);
    const trace = matches[0];
    if (matches.length !== 1) failures.push(`${names[c.name]}: expected one complete route, got ${matches.length}`);
    let lengthMm = 0;
    for (let i = 1; i < (trace?.route.length ?? 0); i++) {
      const a = trace.route[i-1], b = trace.route[i];
      if (![a.x,a.y,b.x,b.y].every(Number.isFinite)) failures.push(`${names[c.name]}: nonfinite route coordinate`);
      if (a.route_type === "wire" && b.route_type === "wire") {
        if (a.layer !== b.layer) failures.push(`${names[c.name]}: layer change without a via`);
        else lengthMm += Math.hypot(a.x-b.x, a.y-b.y);
      }
    }
    const endpoints = trace ? [trace.route[0], trace.route.at(-1)!] : [];
    if (endpoints.length !== 2 || c.pointsToConnect.some(p => !endpoints.some(q => Math.hypot(p.x-q.x,p.y-q.y)<1e-5)))
      failures.push(`${names[c.name]}: route endpoints do not match pads`);
    const vias = trace?.route.flatMap((p,i) => p.route_type === "via" ? [i] : []) ?? [];
    const innerLayers = [...new Set(trace?.route.flatMap(p => p.route_type === "wire" && p.layer !== "top" ? [p.layer] : []))];
    if (vias.length !== 2 || innerLayers.length !== 1 || !DDR_RULES.signalLayers.includes(innerLayers[0] as any) ||
        trace?.route.slice(vias[0]+1,vias[1]).some(p => p.route_type === "wire" && p.layer === "top"))
      failures.push(`${names[c.name]}: expected one inner carrier and two pad escapes`);
    const [a,b] = c.pointsToConnect;
    return {name:names[c.name], connectionName:c.name, lengthMm, padManhattanMm:Math.abs(a.x-b.x)+Math.abs(a.y-b.y), innerLayers, routePass:failures.length===failureCountBefore};
  });
  const groups = ddrTimingGroups.map(group => {
    const members = group.signals.map(s => measurements.find(m => m.name === s)!);
    if (members.some(m => !m)) { failures.push(`${group.name}: missing members`); return {name:group.name,pass:false}; }
    const lengths = members.map(m => m.lengthMm), min = Math.min(...lengths), max = Math.max(...lengths);
    const isCA = group.name === "DDR_ADDR_CTRL_CK";
    // Table 7-69 footnotes 4/5 define DQLM from DQ[x] (DQ+DM),
    // not the associated DQS clock class. Apply that ceiling to DQS too
    // as a conservative project requirement.
    const nominalMembers = isCA ? members : members.filter(m => !/^DDR_DQSn?/.test(m.name));
    const manhattan = Math.max(...nominalMembers.map(m => m.padManhattanMm));
    const nominalMm = manhattan + (isCA ? DDR_RULES.caManhattanAllowanceMm : 0);
    const minimumMm = isCA ? nominalMm-DDR_RULES.caNominalToleranceMm : 0;
    const maximumMm = isCA ? Math.min(nominalMm+DDR_RULES.caNominalToleranceMm,DDR_RULES.caMaximumLengthMm) : nominalMm;
    const skewMm = max-min;
    const skewPass = skewMm <= group.maxLengthSkew+eps;
    const absoluteLengthPass = min >= minimumMm-eps && max <= maximumMm+eps;
    const routingPass = members.every(m => m.routePass);
    if (!skewPass) failures.push(`${group.name}: ${skewMm.toFixed(4)} mm skew exceeds ${group.maxLengthSkew} mm`);
    if (!absoluteLengthPass) failures.push(`${group.name}: lengths ${min.toFixed(4)}–${max.toFixed(4)} mm outside ${minimumMm.toFixed(4)}–${maximumMm.toFixed(4)} mm`);
    return {name:group.name,memberCount:members.length,minLengthMm:min,maxLengthMm:max,skewMm,maxSkewMm:group.maxLengthSkew,nominalMm,minimumMm,maximumMm,skewPass,absoluteLengthPass,routingPass,pass:routingPass&&skewPass&&absoluteLengthPass};
  });
  const pairs = ddrPairs.map(pair => {
    const members = pair.signals.map(s => measurements.find(m => m.name === s)!);
    const skewMm = members.every(Boolean) ? Math.abs(members[0].lengthMm-members[1].lengthMm) : Infinity;
    const sameLayer = members.every(Boolean) && members[0].innerLayers.length===1 && members[0].innerLayers[0]===members[1].innerLayers[0];
    const pass = members.every(m => m?.routePass) && skewMm<=DDR_RULES.pairSkewMm+eps && sameLayer;
    if (!pass) failures.push(`${pair.name}: pair skew or carrier-layer check failed`);
    return {name:pair.name,skewMm,sameLayer,pass};
  });
  return {pass:failures.length===0,failures,groups,pairs,measurements,
    notVerified:["Fabricator stackup and controlled impedance", "Reference-plane continuity and return-current stitching", "Spacing/coupling geometry and native copper DRC", "Package/via flight times and unterminated CA/CK signal integrity"],
    fabricationReady:false};
}