app
AI Flow: Routing
Public Made by Adomby adom
The routing stage of AI Flow: routes a placed KiCad board connection by connection and reports what it made, what it could not and why. A guide, never a gate.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
//! The placement rules routing depends on, measured on the copper pads (4.1). They grade the layouts
//! The designer rejected: a router can't rescue a crystal or a decoupling cap in the wrong place, and his
//! rule (R1, 4.1) is that a layout which looks fine by distances alone is not fine.
use crate::board::*;
use crate::check::Finding;
use crate::classify::*;
use crate::geom::*;
use crate::rules::Rules;
fn at_mm(p: P) -> (f64, f64) {
(to_mm(p.0), to_mm(p.1))
}
fn d_center(board: &Board, a: usize, b: usize) -> f64 {
hypot(sub(board.pads[a].center.into(), board.pads[b].center.into()))
}
fn d_edge(board: &Board, a: usize, b: usize) -> f64 {
board.pads[a].shape.dist_shape(&board.pads[b].shape)
}
/// An IC on this board. A molecule plugged in as a part (benchmark B's U2, Molecule_RP2350A_v1)
/// carries its own decoupling on its own board, so its contacts are not supply pins here.
fn is_ic(board: &Board, fp: usize) -> bool {
let f = &board.footprints[fp];
matches!(ref_prefix(&f.reference), "U" | "IC") && f.pads.len() > 4 && !f.lib.to_ascii_lowercase().contains("molecule")
}
/// Distance of `p` from the line a-b, and how far along it (0 at a, 1 at b).
fn along(p: V, a: V, b: V) -> (f64, f64) {
let ab = sub(b, a);
let l2 = dot(ab, ab).max(1.0);
let t = dot(sub(p, a), ab) / l2;
let q = V(a.0 + ab.0 * t, a.1 + ab.1 * t);
(hypot(sub(p, q)), t)
}
/// Crystal laid out as a circuit (4.1): the XOUT series resistor inline at the MCU pin, each load
/// cap's signal pad on the line from the pin to the crystal pad. The crystal's distance from the
/// MCU is not the measure; the line is.
pub fn crystal_rules(board: &Board, f: &mut Vec<Finding>) {
for (fi, x) in board.footprints.iter().enumerate() {
if !is_crystal(board, fi) {
continue;
}
for &xp in &x.pads {
let net = board.pads[xp].net;
let name = board.net_name(net);
if net == NO_NET || name.starts_with("unconnected-") || looks_ground(name) {
continue;
}
let on_net: Vec<usize> = (0..board.pads.len()).filter(|&i| board.pads[i].net == net && i != xp).collect();
// The pin is on this net, or one series resistor away.
let mut pin = on_net.iter().copied().find(|&i| is_ic(board, board.pads[i].fp));
let mut series: Option<(usize, usize)> = None;
if pin.is_none() {
for &i in &on_net {
let fp = &board.footprints[board.pads[i].fp];
if ref_prefix(&fp.reference) != "R" || fp.pads.len() != 2 {
continue;
}
let Some(&other) = fp.pads.iter().find(|&&q| q != i) else { continue };
let on = board.pads[other].net;
if on == NO_NET {
continue;
}
if let Some(u) = (0..board.pads.len()).find(|&q| board.pads[q].net == on && is_ic(board, board.pads[q].fp)) {
pin = Some(u);
series = Some((other, i));
break;
}
}
}
let Some(pin) = pin else { continue };
let (pv, xv): (V, V) = (board.pads[pin].center.into(), board.pads[xp].center.into());
if let Some((near, far)) = series {
let straight = d_center(board, pin, xp);
let path = d_center(board, pin, near) + d_center(board, near, far) + d_center(board, far, xp);
let off = along(board.pads[near].center.into(), pv, xv).0.max(along(board.pads[far].center.into(), pv, xv).0);
let gap = d_edge(board, pin, near);
if path > 1.10 * straight || off > 1.0 * NM || gap > 2.5 * NM {
let r = &board.footprints[board.pads[near].fp].reference;
f.push(Finding { rule: "xtal-series-line", severity: "error", at: at_mm(board.pads[near].center), text: format!("{r} on {name}: path {:.2} mm against {:.2} mm straight (limit 1.10 x), {:.2} mm off the line (limit 1.0), {:.2} mm from {} (limit 2.5) (4.1)", path / NM, straight / NM, off / NM, gap / NM, board.pad_name(pin)) });
}
}
for &i in &on_net {
let fp = &board.footprints[board.pads[i].fp];
if ref_prefix(&fp.reference) != "C" || fp.pads.len() != 2 {
continue;
}
let (off, t) = along(board.pads[i].center.into(), pv, xv);
// The pad's edge may touch the line, so the 0.4 mm is measured from the pad's copper.
let shape_off = (off - board.pads[i].size.0.min(board.pads[i].size.1) / 2.0).max(0.0);
if shape_off > 0.4 * NM || !(0.0..=1.0).contains(&t) {
f.push(Finding { rule: "xtal-load-cap-line", severity: "error", at: at_mm(board.pads[i].center), text: format!("{} on {name}: {:.2} mm off the line from {} to {} (limit 0.4), {:.2} of the way along (limit 0 to 1) (4.1)", board.pad_name(i), shape_off / NM, board.pad_name(pin), board.pad_name(xp), t) });
}
}
}
}
}
/// A net the spec says is filled under a part (4.7: the supply rings) has a fill there. Net names
/// are compared without case: the spec says +3v3 and his v1.3 says +3V3 (R1).
pub fn inner_fill_rule(board: &Board, rules: &Rules, f: &mut Vec<Finding>) {
for (r, n) in &rules.inner_fills {
let Some(fp) = board.footprints.iter().find(|x| &x.reference == r) else { continue };
let mut bb = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for &p in &fp.pads {
let s = board.pads[p].shape.bbox();
bb = (bb.0.min(s.0), bb.1.min(s.1), bb.2.max(s.2), bb.3.max(s.3));
}
let under = board.zones.iter().filter(|z| board.net_name(z.net).eq_ignore_ascii_case(n)).any(|z| {
z.outline.iter().any(|v| v.0 >= bb.0 && v.0 <= bb.2 && v.1 >= bb.1 && v.1 <= bb.3)
});
if !under {
f.push(Finding { rule: "inner-tie-no-fill", severity: "error", at: at_mm(fp.at), text: format!("{r}: the spec fills {n} under the part and no zone of that net is there (4.7)") });
}
}
}
/// Each supply pin has its own cap at the pin (4.1; 4.7: a tie never replaces it). No distance limit:
/// his document gives none, and a limit read off the reference boards is what P23 and R1 warn about (3.5 mm sat
/// between v1.3's 3.05 and the rejected 3.66). 4.1 says "on the same side", and that alone grades
/// the fixture set correctly: v1.3's far caps are on their pin's side, the rejected board's C8 and C9
/// are across the package. Distances go in the score, for reading. Pins next to each
/// other on the package count as one (v1.3's 43/44, 48/49; benchmark C's U9.1-3), and no cap serves two
/// groups (P25: C16 serving pins 45 and 50). A supply pin is what the schematic says it is: an
/// Benchmark C U5 feedback pin on +5V and an INA228 sense input on +3V3_EXT are not.
/// Whether a cap's pad sits on the side of the package its pin is on (4.1: "at its own supply pin,
/// on the same side"). The side is the package edge nearest the pin; the cap is on it when it lies
/// beyond the pin's inner end on that side's outward direction.
pub fn same_side(board: &Board, pin: usize, cap: usize) -> bool {
let fp = &board.footprints[board.pads[pin].fp];
let mut bb = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for &q in &fp.pads {
let s = board.pads[q].shape.bbox();
bb = (bb.0.min(s.0), bb.1.min(s.1), bb.2.max(s.2), bb.3.max(s.3));
}
let b = board.pads[pin].shape.bbox();
let c = board.pads[pin].shape.center();
// Distance from the pin to each package edge, and that edge's outward direction.
let edges = [(c.0 - bb.0, -1.0, 0.0, b.2), (bb.2 - c.0, 1.0, 0.0, b.0), (c.1 - bb.1, 0.0, -1.0, b.3), (bb.3 - c.1, 0.0, 1.0, b.1)];
// A pin within its own length of a corner belongs to both sides.
let reach = (b.2 - b.0).max(b.3 - b.1);
let near = edges.iter().map(|e| e.0).fold(f64::INFINITY, f64::min) + reach;
let cc = board.pads[cap].shape.center();
edges.iter().filter(|e| e.0 <= near).any(|e| {
let inner = if e.1 != 0.0 { (cc.0 - e.3) * e.1 } else { (cc.1 - e.3) * e.2 };
inner >= 0.0
})
}
/// A supply pin by what the schematic says: typed power_in, or named VCC or VDD whatever its type
/// (v1.3's buffers have VCC typed power_out, a symbol quirk). A feedback or sense input on a rail is
/// not one (benchmark C U5.9 FB, U11.10 Vin+).
pub fn supply_typed(p: &Pad) -> bool {
p.pintype.is_empty() || p.pintype.starts_with("power_in") || p.pinfunction.starts_with("VCC") || p.pinfunction.starts_with("VDD")
}
/// A supply pin of an IC.
pub fn is_supply_pin(board: &Board, i: usize) -> bool {
let p = &board.pads[i];
let name = board.net_name(p.net);
is_ic(board, p.fp) && p.net != NO_NET && !p.layers.is_empty() && looks_power(name) && supply_typed(p)
}
pub fn decoupling_rules(board: &Board, _rules: &Rules, f: &mut Vec<Finding>, farthest: &mut Vec<(f64, String)>) {
let is_supply = |i: usize| {
let p = &board.pads[i];
let name = board.net_name(p.net);
p.net != NO_NET && !p.layers.is_empty() && looks_power(name) && supply_typed(p)
};
let mut by_net: std::collections::BTreeMap<NetId, Vec<Vec<usize>>> = Default::default();
for (fi, u) in board.footprints.iter().enumerate() {
if !is_ic(board, fi) || u.dnp {
continue;
}
let mut pins: Vec<usize> = u.pads.iter().copied().filter(|&p| is_supply(p)).collect();
pins.sort_by_key(|&p| board.pads[p].number.parse::<i64>().unwrap_or(i64::MAX));
let mut groups: Vec<Vec<usize>> = Vec::new();
for pin in pins {
let joins = groups.last().and_then(|g| g.last()).map(|&q| {
let adjacent = board.pads[pin].number.parse::<i64>().ok().zip(board.pads[q].number.parse::<i64>().ok()).map(|(a, b)| a - b <= 1).unwrap_or(false);
board.pads[q].net == board.pads[pin].net && adjacent && d_edge(board, q, pin) <= 0.5 * NM
});
if joins == Some(true) {
groups.last_mut().unwrap().push(pin);
} else {
groups.push(vec![pin]);
}
}
for g in groups {
by_net.entry(board.pads[g[0]].net).or_default().push(g);
}
}
for (net, groups) in by_net {
let caps: Vec<usize> = (0..board.pads.len()).filter(|&i| board.pads[i].net == net && ref_prefix(&board.footprints[board.pads[i].fp].reference) == "C" && !board.footprints[board.pads[i].fp].dnp).collect();
if caps.is_empty() {
continue;
}
// Every (group, cap) by distance, nearest first; each takes the closest still free.
let mut options: Vec<(f64, usize, usize)> = Vec::new();
for (gi, g) in groups.iter().enumerate() {
for &c in &caps {
let d = g.iter().map(|&p| d_edge(board, p, c)).fold(f64::INFINITY, f64::min);
options.push((d, gi, c));
}
}
options.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut own: Vec<Option<(usize, f64)>> = vec![None; groups.len()];
let mut taken: Vec<usize> = Vec::new();
for (d, gi, c) in options {
if own[gi].is_none() && !taken.contains(&c) {
own[gi] = Some((c, d));
taken.push(c);
}
}
for (gi, g) in groups.iter().enumerate() {
let pin = g[0];
let name = board.net_name(net);
match own[gi] {
Some((c, _)) if g.iter().any(|&q| same_side(board, q, c)) => {}
Some((c, d)) => f.push(Finding { rule: "decoupling-pin", severity: "error", at: at_mm(board.pads[pin].center), text: format!("{} ({name}): its own cap {} is {:.2} mm away on another side of the package (4.1: at its own pin, on the same side)", board.pad_name(pin), board.pad_name(c), d / NM) }),
None => f.push(Finding { rule: "decoupling-pin", severity: "error", at: at_mm(board.pads[pin].center), text: format!("{} ({name}): no cap of its own; each one already serves another pin (4.1)", board.pad_name(pin)) }),
}
if let Some((_, d)) = own[gi] {
farthest.push((d / NM, board.pad_name(pin)));
}
}
}
}