AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
schematic check: read a part's context before calling a pin an amplifier input #71
Versions: base 693054013f7294bf5f94ca3d82b23fd048d1589f (Publish 0.2.55) · first seen on 0.2.48 · reproduced on 0.2.55 · KiCad 10.0.7
Problem. schematic check raises a pin-type note on an MCU GPIO because its name contains IN and a -:
pin type: U4 pin 2 (PCM_smart-laser-target-strip:STM32C011F4P6): amplifier input `PC14-OSCX_IN` is bidirectional; amplifier inputs are input
amp_input reads any name that contains IN and a - as an inverting amplifier input, so the - that separates the port (PC14) from its function (OSCX_IN) is taken as the sign. amp_input_on then applies it on any part with three or more pins. Nothing about the part is consulted. #126 (0.2.43) fixed the same kind of misreading for bare +/- and for two-terminal parts; a name with IN in it on a larger part was still read from the name alone.
Why it is a false positive. The symbol types the pin bidirectional, and that is right. STM32C011 datasheet DS13866 Rev 3, Table 12 (pin definitions), lists PC14-OSCX_IN as an FT I/O pin; the oscillator input is one of its alternate functions. On this board no crystal is fitted and the pin is a GPIO carrying SYNC_IN through a series resistor. Under an "every NOTE is a refusal" rule, the note stops the run for a human review.
Change (crates/adom-aiflow/src/schematic_symbols.rs):
- The rule reads the part's context before it reads a pin name.
amp_input_onreturns a sign only whenamp_likesays the part is an amplifier; on any other part the symbol's own pin type stands. amp_likelooks at context first:- not an amplifier when the keywords, description, value or library/symbol name say MCU, microcontroller, Cortex, RISC-V, FPGA, CPLD or GPIO, or when four or more pins are named as GPIO port bits (PA0, PB7, PC14-OSCX_IN). This covers vendor symbols that carry no keywords, like this one. These win, because an MCU's description can list its built-in op-amps;
- an amplifier when those fields say op-amp, operational, comparator, amplifier or instrumentation (
amplifierandinstrumentationare new); - otherwise, as before, a
+and-input pair.
amp_inputrequires the sign to belong to the input name:IN+,IN1-,1IN+,INA-,-IN,+INA, plusINP/INN/NONINVand bare+/-as before. A separator-(PC14-OSCX_IN,PB2-INT0) is no longer a sign. This also keeps a port name from counting toward the+/-pair.- The module doc and skills/aiflow-schematic-to-board/SKILL.md (pin types) say so in one sentence each.
Not changed: tools/altium-lane.py has its own copy of the name rule (from before #126) with the same gap. It is left for a separate change so this one stays on the KiCad check.
Tests (unit tests in schematic_symbols.rs):
an_mcu_gpio_named_like_an_input_keeps_its_symbol_type: the strip's STM32C011F4P6 pins (no keywords, PC14-OSCX_IN bidirectional) raise no amplifier-input finding. Two more MCUs also raise none: one with comparator pinsPA2-COMP1_INP/PA3-COMP1_INNnext to its port pins, and one whose description says "microcontroller with op-amp", with pinsRA0-AN0_IN+/RA1-AN1_IN-. On 0.2.55 this test fails with the exact note above.an_op_amp_input_typed_wrong_is_still_flagged: the strip's TLV9004 pin names (also no keywords) withIN1-passive is still an error,IN1+bidirectional is still a warning, and the correctly typed section 2 is quiet. An instrumentation amp named-IN/+INwith-INbidirectional is still a warning.amplifier_input_signs_belong_to_the_input_name: the name forms above, both ways.- The existing #126 and op-amp tests pass unchanged.
cargo build --release -p adom-aiflow: OK.cargo test --workspace: 604 passed, 0 failed, 11 ignored.
Before and after on the strip schematic. I re-ran schematic check --sch revk_strip.kicad_sch on copies of the run folder (223 symbols, 28 library symbols). ERC and the netlist came from the KiCad 10.0.7 outputs the run had already recorded for this exact sheet (sha256 27e9b356...). The symbol check ran live.
- 0.2.55: OK, 0 errors, 1 warning, the note above. The report matches the one the run recorded on 0.2.48, field for field.
- This change: OK, 0 errors, 0 warnings. The rest of the report (sheets, symbol counts, custom symbols) is identical. The eight TLV9004 op-amps (U11 to U18, IN1+ to IN4- all
input) are still read as amplifiers and stay quiet, as before.
🤖 Generated with Claude Code
Diff Skip to comments (1)
@@ -1,940 +1,1028 @@⋯ 13 unchanged lines ⋯ //! power pin is an error on op-amps and comparators and a warning on other ICs (vendor symbols often //! do it). Stacked pins (KiCad's convention: one typed pin, the rest passive at the same place) and //! crystal pins are skipped. Transistor (Q) pins are left alone: KiCad's own transistor symbols-//! declare them passive.+//! declare them passive. A pin is read as an amplifier input only on a part whose context says it+//! is an amplifier (keywords, description, value, library, a + and - input pair, and not an MCU by+//! its words or its PA0/PB7 port pins); on any other part the symbol's own pin type stands. //! 3. Readability per sheet: many local labels and almost no wires, the same local label used for a //! short hop inside one block, free text over pins or symbol bodies, and parts or text outside the //! drawing border.⋯ 133 unchanged lines ⋯ name.trim().to_ascii_uppercase().replace(['{', '}', '~', ' ', '_'], "").replace(['\u{2212}', '\u{2013}'], "-") } -/// `+` or `-` for a pin named as an amplifier input.+/// `+` or `-` for a pin named as an amplifier input. The sign must belong to the input itself:+/// `IN+`, `IN1-`, `1IN+`, `-IN`, `+INA`, `INP`, `INN`. A `-` that only separates a port from its+/// function, as in an MCU's `PC14-OSCX_IN` or `PB2-INT0`, is not a sign. Whether the part has+/// amplifier inputs at all is `amp_input_on`'s call, from the part's context. fn amp_input(name: &str) -> Option<char> { let n = norm(name); if n == "+" { return Some('+'); } if n == "-" { return Some('-'); }- if n.contains("IN") && !n.starts_with('V') {- if n.contains('+') { return Some('+'); }- if n.contains('-') { return Some('-'); }- if n.ends_with("INP") || n == "NONINV" { return Some('+'); }- if n.ends_with("INN") { return Some('-'); }+ if n.starts_with('V') { return None; }+ if n == "NONINV" || n.ends_with("INP") { return Some('+'); }+ if n.ends_with("INN") { return Some('-'); }+ let b = n.as_bytes();+ let sign = |c: u8| (c == b'+' || c == b'-').then_some(c as char);+ for (i, _) in n.match_indices("IN") {+ let after = &b[i + 2..];+ // IN+ / IN-, or one channel digit or letter between: IN1-, INA++ if let Some(s) = after.first().and_then(|c| sign(*c)) { return Some(s); }+ if after.len() >= 2 && after[0].is_ascii_alphanumeric() { if let Some(s) = sign(after[1]) { return Some(s); } }+ // +IN / -IN, followed by at most a channel digit or letter: -IN, +INA (not PB2-INT0)+ if i > 0 && after.len() <= 1 && after.iter().all(u8::is_ascii_alphanumeric) { if let Some(s) = sign(b[i - 1]) { return Some(s); } } } None }⋯ 20 unchanged lines ⋯ /// A pin named bare `+` or `-`, which says nothing about amplifiers on its own, unlike `IN+`. fn bare_sign(name: &str) -> bool { matches!(norm(name).as_str(), "+" | "-") } +/// Keywords, description, value or library name that say a part is an amplifier.+const AMP_WORDS: &[&str] = &["opamp", "op amp", "op-amp", "operational", "comparator", "amplifier", "instrumentation"];+/// Context that says a part's pins are GPIO, whatever their names spell: an MCU, an FPGA. These+/// win over AMP_WORDS (an MCU's description may list its built-in op-amps).+const NOT_AMP_WORDS: &[&str] = &["microcontroller", "mcu", "cortex", "risc-v", "fpga", "cpld", "gpio"];++/// Four or more pins named as GPIO port bits (PA0, PB7, PC14-OSCX_IN): a microcontroller, even when+/// its symbol carries no keywords or description.+fn port_pins(s: &LibSym) -> bool {+ let port = |n: &str| { let b = n.as_bytes(); b.len() >= 3 && b[0] == b'P' && (b'A'..=b'K').contains(&b[1]) && b[2].is_ascii_digit() };+ s.pins.iter().filter(|p| port(&norm(&p.name))).map(|p| p.number.as_str()).collect::<BTreeSet<_>>().len() >= 4+}++/// Whether a part is an amplifier (op-amp, comparator, instrumentation or current-sense amp), read+/// from its context first (keywords, description, value, library and symbol name, its pins as a+/// whole) and only then from pins named as a + and - input pair. The pin-type rule trusts the+/// symbol's own types on everything else. fn amp_like(s: &LibSym) -> bool { if two_terminal(s) { return false; } let i = &s.info;- if ["opamp", "op amp", "op-amp", "operational", "comparator"].iter().any(|k| i.contains(k)) { return true; }+ if NOT_AMP_WORDS.iter().any(|k| i.contains(k)) || port_pins(s) { return false; }+ if AMP_WORDS.iter().any(|k| i.contains(k)) { return true; } let both = s.pins.iter().any(|p| amp_input(&p.name) == Some('+')) && s.pins.iter().any(|p| amp_input(&p.name) == Some('-')); // bare +/- make an amplifier only with an output beside them: a `+ - NTC` battery connector is not one both && s.pins.iter().any(|p| (amp_input(&p.name).is_some() && !bare_sign(&p.name)) || is_output(&p.name) || ["output", "open_collector", "open_emitter", "tri_state"].contains(&p.etype.as_str())) } -/// The sign of a pin that really is an amplifier input on this symbol.+/// The sign of a pin that really is an amplifier input on this symbol: only an amplifier has them,+/// so a GPIO named `PC14-OSCX_IN` on an MCU keeps the type its symbol gives it. fn amp_input_on(s: &LibSym, name: &str) -> Option<char> {- if two_terminal(s) || (bare_sign(name) && !amp_like(s)) { return None; }+ if !amp_like(s) { return None; } amp_input(name) } ⋯ 628 unchanged lines ⋯ let errs = errors_of(&plain_lib("Trial:SENSE", &[("IN+", "1"), ("IN-", "2"), ("REF", "3")]), "Trial:SENSE", "U2"); assert!(errs.iter().any(|e| e.contains("amplifier input")), "{errs:?}"); }+ /// A symbol of typed pins, as (name, number, electrical type), with no keywords or description.+ fn typed_lib(name: &str, pins: &[(&str, &str, &str)]) -> String {+ let rows: Vec<String> = pins.iter().enumerate().map(|(i, (n, num, et))| format!(r#"(pin {et} line (at -7.62 {} 0) (length 2.54) (name "{n}" (effects (font (size 1.27 1.27)))) (number "{num}" (effects (font (size 1.27 1.27)))))"#, i as f64 * -2.54)).collect();+ format!(r#"(symbol "{name}" (property "Reference" "U" (at 0 5 0) (effects (font (size 1.27 1.27)))) (property "Value" "{v}" (at 0 -5 0) (effects (font (size 1.27 1.27)))) (symbol "T_1_1" (rectangle (start -5.08 2.54) (end 5.08 -40) (stroke (width 0.254)) (fill (type background))) {}))"#, rows.join(" "), v = name.rsplit(':').next().unwrap())+ }+ fn findings_of(lib: &str, sym: &str, r: &str) -> Vec<String> {+ let f = Fixture::new();+ let p = f.write("s.kicad_sch", &sheet(&[lib.to_string()], &placed(sym, r, 120.0, 90.0)));+ let rep = check(&p, &Value::Null).unwrap();+ ["errors", "warnings"].iter().flat_map(|k| rep[*k].as_array().unwrap().iter().map(|e| e.as_str().unwrap().to_string())).collect()+ } #[test]+ fn amplifier_input_signs_belong_to_the_input_name() {+ for (n, s) in [("IN+", '+'), ("IN1-", '-'), ("1IN+", '+'), ("2IN\u{2013}", '-'), ("-IN", '-'), ("+IN A", '+'), ("INA-", '-'), ("INP", '+'), ("INN", '-'), ("+", '+')] {+ assert_eq!(amp_input(n), Some(s), "{n}");+ }+ for n in ["PC14-OSCX_IN", "PC14-OSC32_IN", "PB2-INT0", "PF2-NRST", "VIN+", "IN", "EN"] { assert_eq!(amp_input(n), None, "{n}"); }+ }+ #[test]+ fn an_mcu_gpio_named_like_an_input_keeps_its_symbol_type() {+ // AF27: the Rev K strip's STM32C011F4P6 (U4), a vendor symbol with no keywords or description.+ // DS13866 Table 12 lists PC14-OSCX_IN as an FT I/O; with no crystal fitted it is a GPIO.+ let pins = [("PB7", "1", "bidirectional"), ("PC14-OSCX_IN", "2", "bidirectional"), ("PC15-OSCX_OUT", "3", "bidirectional"), ("VDD/VDDA", "4", "power_in"),+ ("VSS/VSSA", "5", "power_in"), ("PF2-NRST", "6", "bidirectional"), ("PA0", "7", "bidirectional"), ("PA1", "8", "bidirectional"), ("PA14-BOOT0", "19", "bidirectional")];+ let found = findings_of(&typed_lib("Trial:STM32C011F4P6", &pins), "Trial:STM32C011F4P6", "U4");+ assert!(found.iter().all(|e| !e.contains("amplifier input")), "{found:?}");+ // a larger MCU with a comparator's INP/INN pins (its port pins say MCU), and one whose description says so+ let mut more = pins.to_vec();+ more.extend([("PA2-COMP1_INP", "9", "bidirectional"), ("PA3-COMP1_INN", "10", "bidirectional")]);+ let found = findings_of(&typed_lib("Trial:STM32G4", &more), "Trial:STM32G4", "U1");+ assert!(found.iter().all(|e| !e.contains("amplifier input")), "{found:?}");+ let lib = typed_lib("Trial:MCU", &[("RA0-AN0_IN+", "1", "bidirectional"), ("RA1-AN1_IN-", "2", "bidirectional"), ("RA2", "3", "bidirectional")])+ .replacen("(property \"Value\"", "(property \"ki_description\" \"8-bit microcontroller with op-amp\" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes))) (property \"Value\"", 1);+ let found = findings_of(&lib, "Trial:MCU", "U2");+ assert!(found.iter().all(|e| !e.contains("amplifier input")), "{found:?}");+ }+ #[test]+ fn an_op_amp_input_typed_wrong_is_still_flagged() {+ // the strip's TLV9004 symbol, also without keywords, with section 1's inputs typed wrong+ let pins = [("OUT1", "1", "output"), ("IN1-", "2", "passive"), ("IN1+", "3", "bidirectional"), ("V+", "4", "power_in"), ("IN2+", "5", "input"),+ ("IN2-", "6", "input"), ("OUT2", "7", "output"), ("V-", "11", "power_in")];+ let f = Fixture::new();+ let p = f.write("s.kicad_sch", &sheet(&[typed_lib("Trial:TLV9004IPWR", &pins)], &placed("Trial:TLV9004IPWR", "U3", 120.0, 90.0)));+ let rep = check(&p, &Value::Null).unwrap();+ let errs: Vec<&str> = rep["errors"].as_array().unwrap().iter().map(|e| e.as_str().unwrap()).collect();+ let warns: Vec<&str> = rep["warnings"].as_array().unwrap().iter().map(|e| e.as_str().unwrap()).collect();+ assert_eq!(errs, ["pin type: U3 pin 2 (Trial:TLV9004IPWR): amplifier input `IN1-` (-) is passive; declare it input"], "{rep}");+ assert!(warns.iter().any(|w| w.contains("pin 3") && w.contains("amplifier input `IN1+` is bidirectional")), "{rep}");+ assert!(warns.iter().chain(errs.iter()).all(|w| !w.contains("IN2")), "{rep}");+ // and named the way ADI does, with a description that says so+ let lib = typed_lib("Trial:AD8221", &[("-IN", "1", "bidirectional"), ("+IN", "4", "input"), ("RG", "2", "passive"), ("VOUT", "7", "output")])+ .replacen("(property \"Value\"", "(property \"ki_description\" \"instrumentation amplifier\" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes))) (property \"Value\"", 1);+ let found = findings_of(&lib, "Trial:AD8221", "U5");+ assert!(found.iter().any(|w| w.contains("pin 1") && w.contains("amplifier input `-IN` is bidirectional")), "{found:?}");+ }++ #[test] fn passive_amplifier_and_power_pins_are_errors_unless_justified() { let f = Fixture::new(); let p = f.write("s.kicad_sch", &sheet(&[opamp_lib("Trial:TL072", "input", 2.54)], &placed("Trial:TL072", "U1", 120.0, 90.0)));⋯ 95 unchanged lines ⋯ assert!(tool_gate(&f.0).is_ok()); } }+@@ -1,141 +1,142 @@⋯ 134 unchanged lines ⋯ `schematic check --sch <root>.kicad_sch` (and `netlist check`, which runs the same part) reads the root sheet and every child sheet with the symbols they embed, beyond ERC: 1. **Polarity drawings agree with the pin names.** A diode's cathode bar and triangle point at the pin named K, a transistor's emitter arrow sits on the E lead, an op-amp's drawn `+`/`-` sit at the inputs named that way. A custom symbol that contradicts its pins is an error (a standard KiCad library symbol only warns). A drawing you reviewed against the datasheet goes in the spec: `"schematic": {"polarityReviewed": [{"symbol": "<lib id or reference>", "why": "..."}]}`.-2. **Pin electrical types are real.** Amplifier inputs are `input`, outputs `output`, power pins `power_in`; `passive` on an op-amp or comparator input, output or power pin is an error (a warning on other ICs). When the datasheet truly says otherwise: `"schematic": {"allowPassive": [{"ref": "U1", "pin": "3", "why": "datasheet ..."}]}`. Prefer library symbols (one triangle per op-amp section, a conventional transistor) over custom rectangles.+2. **Pin electrical types are real.** Amplifier inputs are `input`, outputs `output`, power pins `power_in`; `passive` on an op-amp or comparator input, output or power pin is an error (a warning on other ICs). A pin counts as an amplifier input only on a part that is an amplifier by its keywords, description, value, library or a + and - input pair; an MCU's GPIO named `PC14-OSCX_IN` keeps the type its symbol gives it. When the datasheet truly says otherwise: `"schematic": {"allowPassive": [{"ref": "U1", "pin": "3", "why": "datasheet ..."}]}`. Prefer library symbols (one triangle per op-amp section, a conventional transistor) over custom rectangles. 3. **Readable sheets** (warnings): many local labels and almost no wires, the same local label used for a short hop inside one block, free text over pins or symbol bodies, and parts or text outside the drawing border. Draw the signal path with wires inside each functional block; keep labels for long or cross-sheet nets, so feedback loops can be traced by eye. The result is `<run>/schematic-check.json` and the run outcome `schematic-check`, bound to every sheet's hash; `finish` requires a pass on the hierarchy as it is then. Limits: heuristic geometry (polylines, rectangles, circles, arcs), one diode per unit with pins named K/A, transistors with pins named E/C, op-amp inputs named `+`/`-`, `IN+`/`IN-`, `INP`/`INN`.+
Comments
Log in to comment.