imrishabh18/pedometer

This code defines and assembles a simple radio receiver hardware circuit using specific imported capacitors, inductors, RF connectors, and oscillator components with precise footprints and schematic attributes.

Version
1.1.3
License
unset
Stars
0

bga-autorouter.ts

import { repairViaClearances } from "./via-clearance";
import { planBgaTerminalEscapes } from "./bga-fanout";
import { enableThroughViaDrc } from "./through-via-drc";
import { AutoroutingPipelineSolver7_MultiGraph, AutoroutingPipelineSolver9_PreloadedTraceGraph, type SimpleRouteJson, type SimplifiedPcbTrace } from "@tscircuit/capacity-autorouter";

/** Detect enclosed BGA balls and QFN center pads from footprint geometry.
 * Terminal-via hints are permitted only when the caller enables via-in-pad. */
export function prepareBgaRouting(input: SimpleRouteJson): SimpleRouteJson {
  const srj = structuredClone(input);
  const bgaEscapes = planBgaTerminalEscapes(input);
  // Core represents an unbroken rectangular pour as touching horizontal
  // strips. Coalesce exact rectangular tilings to avoid excessive graph
  // subdivision, while preserving the obstacle's area, layer and net.
  const pours = srj.obstacles.filter(o=>o.isCopperPour);
  if(pours.length>1 && pours.every(o=>JSON.stringify(o.layers)===JSON.stringify(pours[0].layers) && JSON.stringify(o.connectedTo)===JSON.stringify(pours[0].connectedTo))){
    const left=Math.min(...pours.map(o=>o.center.x-o.width/2)),right=Math.max(...pours.map(o=>o.center.x+o.width/2));
    const bottom=Math.min(...pours.map(o=>o.center.y-o.height/2)),top=Math.max(...pours.map(o=>o.center.y+o.height/2));
    const ordered=[...pours].sort((a,b)=>a.center.y-b.center.y);
    const tiled=ordered.every((o,i)=>Math.abs(o.width-(right-left))<1e-6 && Math.abs(o.center.x-(left+right)/2)<1e-6 && (i===0||Math.abs(o.center.y-o.height/2-(ordered[i-1].center.y+ordered[i-1].height/2))<1e-6));
    if(tiled){srj.obstacles=srj.obstacles.filter(o=>!o.isCopperPour);srj.obstacles.push({...pours[0],center:{x:(left+right)/2,y:(bottom+top)/2},width:right-left,height:top-bottom});}
  }
  // Route with margin above the board's 0.075 mm DRC minimum.
  srj.defaultObstacleMargin=0.10;
  srj.minTraceToPadEdgeClearance=0.10;
  srj.minViaEdgeToPadEdgeClearance=0.10;
  const groups = new Map<string, typeof srj.obstacles>();
  for (const pad of srj.obstacles) {
    if (!pad.componentId || !pad.circuitJsonMetadata?.pcb_smtpad_id) continue;
    const pads = groups.get(pad.componentId) ?? [];
    pads.push(pad); groups.set(pad.componentId, pads);
  }
  const qfnPeripheralPads = new Set<(typeof srj.obstacles)[number]>();
  for (const pads of input.allowViaInPad === true ? groups.values() : []) {
    if (pads.length < 9) continue;
    for (const pad of pads) {
      const {x,y} = pad.center;
      // Large exposed center lands are also enclosed by peripheral QFN pads.
      // A solver-generated terminal via avoids routing ground across those pins.
      const exposedCenter = pad.width>=1 && pad.height>=1 &&
        pads.some(p=>p.center.x+p.width/2<x-pad.width/2) &&
        pads.some(p=>p.center.x-p.width/2>x+pad.width/2) &&
        pads.some(p=>p.center.y+p.height/2<y-pad.height/2) &&
        pads.some(p=>p.center.y-p.height/2>y+pad.height/2);
      const bgaEscape=bgaEscapes.get(pad.circuitJsonMetadata!.pcb_smtpad_id!);
      if (!bgaEscape && !exposedCenter) continue;
      if (exposedCenter) for (const other of pads) if (other !== pad) qfnPeripheralPads.add(other);
      let used = false;
      for (const connection of srj.connections) for (const point of connection.pointsToConnect) {
        if (Math.hypot(point.x-x,point.y-y)>0.001 || !("layer" in point) || point.layer!=="top") continue;
        point.layer=bgaEscape?.layer ?? "bottom";
        point.terminalVia=bgaEscape?.terminalVia ?? {toLayer:"top",viaDiameter:srj.minViaPadDiameter ?? 0.3};
        used=true;
      }
      if (used) srj.obstacles.push({
        type:"rect", obstacleId:`bga-terminal-${pad.circuitJsonMetadata?.pcb_smtpad_id}`,
        center:{x,y}, width:srj.minViaPadDiameter ?? 0.3, height:srj.minViaPadDiameter ?? 0.3,
        layers:["top","inner1","inner2","bottom"], connectedTo:[...pad.connectedTo],
      });
    }
  }
  // Keep routes out of the nominal 0.25 mm gaps between QFN pins.
  // This is a planning envelope only; physical copper and DRC limits are unchanged.
  for (const pad of qfnPeripheralPads) { pad.width += 0.10; pad.height += 0.10; }
  // Reserve drill-to-copper clearance during global planning, where space
  // can still be allocated. Local via movement alone cannot fix a packed
  // junction. Terminal via diameters above retain their physical pad size.
  const physicalPad = input.minViaPadDiameter ?? 0.3;
  const drillRadius = (input.minViaHoleDiameter ?? 0.15) / 2;
  srj.minViaPadDiameter = Math.max(physicalPad, 2 * (drillRadius + 0.205 - 0.10));
  // The current solver prefers snake_case and some stages read the legacy
  // field. Keep all three aliases consistent so this clearance is applied.
  srj.min_via_pad_diameter = srj.minViaPadDiameter;
  srj.minViaDiameter = srj.minViaPadDiameter;
  return srj;
}

/** The solver emits a terminal via without the wire endpoint on the pad side.
 * Include that endpoint so tscircuit can associate the physical pad with the
 * generated through-via. This preserves the solver's copper geometry. */
export function attachPadEndpoints(traces: SimplifiedPcbTrace[], input: SimpleRouteJson) {
  const points=input.connections.flatMap(c=>c.pointsToConnect);
  for(const trace of traces){
    const first=trace.route[0], last=trace.route.at(-1);
    if(first?.route_type==="via"){
      const next=trace.route[1];
      const point=points.find(p=>Math.hypot(p.x-first.x,p.y-first.y)<0.001 && "layer" in p && p.layer!==(next?.route_type==="wire" ? next.layer : undefined));
      if(point && "layer" in point && next?.route_type==="wire"){
        first.from_layer=point.layer;first.to_layer=next.layer;
        trace.route.unshift({route_type:"wire",x:point.x,y:point.y,layer:point.layer,width:input.minTraceWidth,start_pcb_port_id:point.pcb_port_id});
      }
    }
    if(last?.route_type==="via"){
      const prev=trace.route.at(-2);
      const point=points.find(p=>Math.hypot(p.x-last.x,p.y-last.y)<0.001 && "layer" in p && p.layer!==(prev?.route_type==="wire" ? prev.layer : undefined));
      if(point && "layer" in point && prev?.route_type==="wire"){
        last.from_layer=prev.layer;last.to_layer=point.layer;
        trace.route.push({route_type:"wire",x:point.x,y:point.y,layer:point.layer,width:input.minTraceWidth,end_pcb_port_id:point.pcb_port_id});
      }
    }
  }
  return traces;
}

export async function bgaAutorouter(input: SimpleRouteJson) {
  const routing = prepareBgaRouting(input);
  // Reserve physical inner1 for the RF ground reference. Route on a virtual
  // three-layer stack (top, physical inner2, bottom), then restore layer names.
  // The physical board and all generated vias remain four-layer through vias.
  routing.layerCount = 3;
  const mapLayer = (layer: string) => layer === "inner2" ? "inner1" : layer;
  const mapLayers = (layers: string[]) => layers.filter(l=>l!=="inner1").map(mapLayer);
  routing.obstacles = routing.obstacles.map(o=>{
    const layers = mapLayers(o.layers);
    return {...o,layers,zLayers:layers.map(l=>l==="top"?0:l==="bottom"?2:1),__zLayers:layers.map(l=>l==="top"?0:l==="bottom"?2:1)};
  }).filter(o=>o.layers.length);
  for (const c of routing.connections) for (const p of c.pointsToConnect) {
    if ("layers" in p) p.layers = mapLayers(p.layers);
    else { p.layer = mapLayer(p.layer); if(p.terminalVia)p.terminalVia.toLayer=mapLayer(p.terminalVia.toLayer); }
  }
  for (const trace of routing.traces ?? []) for (const p of trace.route) {
    if (p.route_type === "wire") p.layer = mapLayer(p.layer);
    if (p.route_type === "via") {
      p.from_layer = mapLayer(p.from_layer);
      p.to_layer = mapLayer(p.to_layer);
    }
  }
  return runCapacityRouter(input,routing,true);
}

/** Route the clock nets first on top copper; the crystal allows no vias. */
export async function clockAutorouter(input: SimpleRouteJson) {

  return routePlanar(input,input.minTraceWidth);
}

/** Prevent the router from narrowing the RF feed below its intended width. */
export async function rfAutorouter(input: SimpleRouteJson) {
  return routePlanar(input,0.2);
}

function routePlanar(input: SimpleRouteJson, width: number) {
  const planar=structuredClone(input);
  planar.layerCount=1;
  planar.minTraceWidth=width;
  planar.nominalTraceWidth=width;
  for(const c of planar.connections)c.nominalTraceWidth=width;
  planar.defaultObstacleMargin=0.10;
  planar.minTraceToPadEdgeClearance=0.10;
  planar.obstacles=planar.obstacles.filter(o=>o.layers.includes("top")).map(o=>({...o,layers:["top"],__zLayers:[0]}));
  return runCapacityRouter(input,planar);
}

async function runCapacityRouter(input: SimpleRouteJson, routingInput: SimpleRouteJson, restoreInner2=false) {
  // Pipeline 7 treats previously routed clock copper as fixed obstacles.
  const Solver = routingInput.layerCount === 1 ? AutoroutingPipelineSolver9_PreloadedTraceGraph : AutoroutingPipelineSolver7_MultiGraph;
  const solver = new Solver(routingInput, {powerTraceExpansion:{onlyConnectionNames:[]}});
  if (routingInput.layerCount > 1) enableThroughViaDrc(solver,routingInput.layerCount);
  const handlers: Record<string, ((event: any)=>void)[]> = {complete:[],error:[],progress:[]};
  let stopped=false;
  let lastReport=Date.now();
  const run=()=>{
    if(stopped)return;
    try {
      const until=Date.now()+40;
      while(!solver.solved&&!solver.failed&&Date.now()<until)solver.step();
      if(solver.failed){handlers.error.forEach(h=>h({error:new Error(solver.error||"Routing failed")}));return;}
      if(solver.solved){
        // The solver includes preloaded copper in its output. Core retains it
        // across phases already, so emit only new or explicitly replaced routes.
        const preloadedIds = new Set((input.traces ?? []).map(t=>t.pcb_trace_id));
        const traces = (solver.getOutputSimpleRouteJson().traces ?? []).filter(t=>
          !preloadedIds.has(t.pcb_trace_id) || t.__replaces_pcb_trace_id);
        if(restoreInner2)for(const trace of traces)for(const p of trace.route){
          if(p.route_type==="wire"&&p.layer==="inner1")p.layer="inner2";
          if(p.route_type==="via"){
            if(p.from_layer==="inner1")p.from_layer="inner2";
            if(p.to_layer==="inner1")p.to_layer="inner2";
          }
        }
        // Remove the planning-only clearance envelope; all drill centers are
        // still solver-generated and the physical via dimensions remain fixed.
        for (const trace of traces) for (const point of trace.route) {
          if (point.route_type === "via") point.via_diameter = input.minViaPadDiameter ?? 0.3;
        }
        const repaired = repairViaClearances(attachPadEndpoints(traces,input),input);
        handlers.complete.forEach(h=>h({traces:repaired}));
        return;
      }
      if(Date.now()-lastReport>10000){console.log("BGA autorouter phase:",solver.getCurrentPhase());lastReport=Date.now();}
      setTimeout(run,0);
    } catch(error){handlers.error.forEach(h=>h({error}));}
  };
  return {on:(event:string,handler:(event:any)=>void)=>{handlers[event].push(handler);},start:()=>setTimeout(run,0),stop:()=>{stopped=true;}};
}