Skip to content

Latest commit

 

History

History
237 lines (215 loc) · 16.1 KB

File metadata and controls

237 lines (215 loc) · 16.1 KB

The IPC-2581 output contract (<name>.xml)

RFC-015's partner-handoff artifact: cohdl build --emit ipc2581 writes out/<package-name>.xml, an IPC-2581 revision B1 document conforming to the IPC 2581 Consortium's published IPC-2581B1.xsd (vendored for the test gate at tests/schema/IPC-2581B1.xsd; validated with xmllint --schema in tests/ipc2581.rs). It is a fourth output alongside the KiCad .net, BOM CSV, and layout.json — additive, never a replacement, and emitted only when --emit ipc2581 is requested (build --json then lists it under build.ipc2581, the same only-when-emitted pattern as build.layout).

Physical geometry, and the one rule: the document is honestly partial

The document carries the real physical geometry a layout tool routes — a DictionaryStandard of primitive shapes, PadStackDefs (copper/mask/paste + a plated PadstackHoleDef for through-hole pads), real layers (F.Cu/B.Cu/ masks/paste) + a stackup, and side-aware LayerFeatures for every PLACED copper/mask/enabled-paste feature, each at its absolute board position and tied to its component pin (PinRef) and net (Set/@net). Component and pin mount types are real (SMT vs THMT; SURFACE_MOUNT_PAD / THROUGH_HOLE_PIN). Without this physical layer, an XSD-valid document that carried only abstract Package/Pin land patterns showed no copper/holes/ratsnest in Quilter (finding: .co/invalid-ipc2581.xml) — so it is emitted.

What is still NOT done, and the markers say so (never overclaiming): final component placement (unlocked components are STAGED just outside the outline — the "please place me" idiom — not placed) and routing (no copper traces). The document therefore carries the marker logical-complete,placement-staged,unrouted in three machine-or-human-visible places:

  • FunctionMode/@commentlogical-complete,placement-staged,unrouted — layout not yet performed. @mode is ASSEMBLY: the mode names the data function the document serves (assembly-level: BOM + components + nets + land patterns + padstacks), not a completeness claim — and real consumers key their fabrication-layer rendering off a standard mode (under the earlier USERDEF a consumer legitimately skipped every LayerFeature, drawing components without pads). The honest completeness disclosure lives in this comment and the machine-readable marker below;
  • HistoryRecord/FileRevision/@comment — same marker;
  • a machine-readable NonstandardAttribute on the Step: name="COHDL_COMPLETENESS" value="logical-complete,placement-staged,unrouted".

A consumer must check the marker before assuming physical data is present. The Step Datum is a degenerate (0,0). Component placement depends on whether a board outline exists: a layout tool such as Quilter treats every component inside the outline as pre-placed/locked and only places the ones left outside it (docs.quilter.ai). So when the design declares a board_outline, components are staged in a deterministic, non-overlapping grid just OUTSIDE the outline (to its right) — the standard "unplaced, please place me" signal — rather than piled at (0,0) (which is inside the outline and would read as N components locked at the board centre). A component named by a place <inst> at (x, y) [rotate ANGLE] statement (RFC-020) is instead emitted at that fixed position (inside the outline = the layout tool treats it as pre-placed/locked) and is NOT staged — this is how board-edge/mechanical parts (the Pico's 40-pin castellated header, USB, SWD headers) are fixed while the rest are placed. An optional rotate (a closed set {0, 90, 180, 270}) rides Component/Xform/@rotation (schema order: NonstandardAttribute*, Xform?, Location); an unrotated component emits no Xform. Staging and placement positions are exact over the femto integers and byte-stable.

Coordinate frame. IPC-2581 is a +y-up (screen-up) frame; CoHDL authors geometry +y-down (as KiCad does internally, and as the design's place/DXF coordinates read). Every emitted y is therefore NEGATED into the IPC frame — component Locations, placed-copper/hole Locations, Package land-pattern Pin and courtyard Outline corners, and the board Profile (whose arc winding also flips). Rotation values are UNCHANGED: this reproduces KiCad's own kicad-cli pcb export ipc2581 --version B placement of rotated, y-offset pads exactly (verified component-for-component and pad-for-pad against a KiCad reference export), so the document renders in a consumer (Quilter) the same way up as the design and as KiCad's board. The pad computation negates the local offset BEFORE rotation and the component position, not the final absolute — a naive reflection of the absolute position mis-places rotated pads. (The .kicad_mod files stay in KiCad's native +y-down frame, so their y is the negation of the IPC land pattern's — same magnitude, exact.)

With no outline there is nothing to stage against, so Component/Location keeps the (0,0) placeholder. Either way the staged positions are NOT a real placement — they are the input a placement engine consumes, and the marker still governs. A Package whose footprint has RFC-018 pad placements carries its land pattern — its courtyard (or, when a footprint deliberately omits the courtyard, a non-degenerate Outline derived from the pad extents — a degenerate (0,0)-(0,0) polygon made such packages invisible) and one logical Pin element per distinct electrical pad number. Its Location and StandardPrimitiveRef into the shared DictionaryStandard come from a deterministic representative placement (footprint-local top SMD before bottom SMD before PTH, then the largest land), so a thermal via authored before its exposed-pad land cannot turn that logical pin into a through-hole pin. The dictionary form is the encoding real importers resolve; an inline primitive under <Pin> is schema-valid via the StandardShape substitution group but was invisible to a real importer. type SURFACE/THRU, electricalType="ELECTRICAL", and mountType SURFACE_MOUNT_PAD/THROUGH_HOLE_PIN describe that logical terminal. Every repeated physical placement is retained separately below. The placed copper — with layer + drill — is in the PadStackDef/ LayerFeature structures below; a footprint still at the RFC-017 placeholder stage (empty body) keeps the zero-size-polygon idiom.

Board outline (Step/Profile, RFC-020). When the design declares a board_outline: "path.dxf" in its layout {} block, cohdl build opens the referenced DXF and extracts exactly one closed outline entity — a closed LWPOLYLINE/POLYLINE on the Edge.Cuts layer (the documented convention; everything else in the file is ignored). Its real geometry is embedded in a Step/Profile/Polygon between Datum and the first Package: straight edges become PolyStepSegment, DXF arc bulges become PolyStepCurve (end point + center + clockwise). Vertex coordinates are parsed to exact femto-mm integers; only an arc's center (which a bulge implies but does not state) is computed in f64 and rounded to femto ONCE at extraction, so both emitters read identical integers. A missing / unparseable / non-closed / no- entity-on-Edge.Cuts DXF is a named E1006 build error — never a silent wrong Profile. A design with no board_outline emits no Profile. The outline also rides layout.json ("board_outline": { "source", "start", "segments" } / null). This is a pragmatic PCB-partner extension beyond RFC-015's original scope, formalized by RFC-020 (docs/compliance-report.md).

Physical padstacks + placed copper (closes review R5-8). The copper layer and drill the abstract Package/Pin subset can't carry are now emitted properly: a PadStackDef per unique copper/mask/paste geometry, plating, drill, and physical side. A through-hole padstack has copper/mask on both outer faces plus a plated PadstackHoleDef carrying its real drill. An SMD padstack has copper/mask and, unless suppressed, paste only on the physical front or back face selected by the footprint pad's local layer and the component-side flip. A LayerFeature per layer the padstack lands on places each padstack at its absolute board position, tied to the component pin (PinRef) and net (Set/@net). Bottom-side placement mirrors the pad's local x, reflects an asymmetric chamfer, and reverses the pad-local rotation before the component rotation is applied; this is the same mirror-then-rotate model used for the position. The primitives preserve the bounded per-pad chamfer, four-corner RectRound radius, mask_expansion, and paste: none | (width, height) | circle(diameter) overrides from the provisional syntax, including a circular aperture either smaller or larger than its copper land. When one electrical pad number has multiple physical placements, every placement remains in these side-aware layer features with the same PinRef; the package-level logical list contains one Pin per distinct number. Annular copper is an exact Contour with a circular Cutout; segmented annular paste becomes four separate filled contour primitives on the one paste layer while retaining a single logical/electrical pin identity. A consumer compositing "visible pad = copper through a mask aperture" sees no pads at all when only the copper is instanced. A drill LayerFeature (F.Cu_B.Cu, a DRILL-function Layer spanning F.CuB.Cu) carries one located, plated <Hole> per through-hole pad (Set/@geometry names the padstack) — the board-level form hole-rendering consumers key on; the padstack-level PadstackHoleDef alone stays invisible. Every fabrication layer is declared as a Content/LayerRef and a Layer, and the Stackup lists the full 9-row top→bottom fab sequence (silk/paste/mask/F.Cu(0.035)/DIELECTRIC_1(1.51)/B.Cu(0.035)/mask/ paste/silk, masks 0.01 — summing to the declared 1.6mm; the single FAB_LAYERS table feeds all three). Listing the mask/paste rows and the dielectric matters: a consumer builds its renderable-layer model from the StackupLayer sequence, and a copper-only stack (two directly-adjacent conductors, no mask rows) never composites a visible pad. This is what makes the copper, holes, and ratsnest visible/routable in a consumer (Quilter) — the finding in .co/invalid-ipc2581.xml. (The earlier "moot for an all-SMD board" note was stale: rpi-pico2's USB + castellations are through-hole with real 0.44/1.0/1.35 mm drills.) Still not projected into the IPC document: the footprint's silkscreen_ref (present in the .kicad_mod).

What the document carries (all from already-validated CoHDL data)

Section Content Source of truth it must agree with
Ecad/CadData/Step/LogicalNet every merged net; one PinRef (refdes, pin number) per device physical pin number. Repeated footprint placements reuse that reference in physical layer features rather than duplicating the logical net member the .net's node list, exactly
LogicalNet/@netClass GROUND for [gnd] nets, POWER for voltage-annotated nets, else SIGNAL the net annotations
Step/Component one per instance, designator order; @part = MPN, @packageRef = footprint-derived package the .net comps
Component/NonstandardAttribute COHDL_DEVICE, COHDL_PATH, one COHDL_SPEC_<field> per resolved spec value (RFC-001 literal text), COHDL_PLACEMENT_HINT when present the IR / layout.json hints
Bom + Avl one BomItem per MPN group (quantity, RefDes children, MPN/MFR/VALUE Textual characteristics), backed by one AvlItem per group (the XSD's keyrefs require the AVL). AvlMpn/@name is the group's collision-free key; the TRUE MPN rides AvlMpn/@other — two distinct MPNs never collapse to one AVL identity the BOM CSV rows, exactly — one item per row, quantity = the Designator cell's count, MPN/MFR Textuals = the Comment/Manufacturer cells; VALUE is XML-only (the assembly-style CSV carries no value column) and is pinned to the IR's principal value instead
CadHeader/Spec RFC-013 constraints as CoHDL vendor extensions (NOT native IPC constraint semantics — IPC-2581B1 has no user-named net-class element, and there is no SpecRef linking these to a LogicalNet): cohdl:net_class:<name>, cohdl:diff_pair:<p>:<n>, cohdl:length_match:<i>, each with General/Property members (net, positive/negative, tolerance). Recoverable only by a CoHDL-aware decoder; a generic IPC/Quilter consumer will not honor them. See review F2 in docs/compliance-report.md (deviation pending note-side amendment). layout.json

Fidelity is enforced by tests/ipc2581.rs over the fixture corpus AND both repo examples: nets/components against the .net, BOM items against the CSV, constraints/hints generically against the parsed layout.json, and every COHDL_SPEC_* map against the IR's resolved spec values — the emitters are independent consumers of the same DesignIr, and the tests fail the moment any of them disagrees for the same design.

Determinism

Same source + same std → same bytes (Constitution hard constraint). Two consequences visible in the document:

  • every schema-required xsd:dateTime (HistoryRecord, AvlHeader) is the fixed epoch instant 1970-01-01T00:00:00Z — the wall clock never enters an artifact;
  • every list is explicitly ordered (components by designator, nets by name, BOM groups by MPN, packages by footprint name).

Name sanitization

The XSD restricts key/reference names (qualifiedNameType, shortName) to an identifier charset. Names CoHDL controls (designators, net names) already fit. Free-text part fields are handled as:

  • Package names (from the footprint symbol): the CoHDL fully-qualified separator :: is collapsed to a single - (rpi_pico2-CHIP_0805, KiCad's own convention) and any other character outside [A-Za-z0-9_\-.+] becomes _. Although the XSD's qualifiedNameType permits :, it is the XML QName/namespace delimiter — a consumer whose pin/pad resolution treats a package name as an NCName can fail to bind pins to their land pattern, so the emitted name is colon-free. The same string is written verbatim as both Package/@name and every Component/@packageRef, so they stay matched; when sanitizing changes the name the raw footprint symbol is preserved in Package/@comment. Post-sanitize collisions get _ suffixes (deterministic).
  • BOM/AVL keys (from the MPN): same rule (no :), raw MPN always in the MPN Textual characteristic.
  • Designators (Component/@refDes, RefDes/@name, PinRef/@componentRef): the document's XSD-enforced componentKey. All three spellings come from one collision-free table (same rule as above); identity for every ordinary designator, and only reachable at all via a hostile designator_prefix string.
  • MPN in Component/@part, MFR, hints, specs: plain xsd:string attributes — raw values, XML-escaped.

Character fidelity in attribute values: tab and CR are emitted as character references (&#9;/&#13;) so conforming parsers preserve them (a literal tab would be normalized to a space, silently diverging from layout.json/the BOM CSV). Characters XML 1.0 forbids outright (C0 controls other than tab/LF/CR) are replaced with U+FFFD — the emitter's one non-value-preserving projection; the alternative is a document no parser will open. Everything else is value-preserving.

Named non-goals (RFC-015 / DR-021)

No board outline/stackup (footprint geometry itself landed with RFC-018 — Package/Outline + Pin above — but component placement stays degenerate: layout has not been performed), no ECO or incremental re-route story (a regenerated document is a fresh full handoff), and no claim of a usable Quilter handoff: a Quilter starter board needs a valid board outline and PLACED footprints, and this document has neither (no Profile; every Component/Location is (0,0) — layout has not been performed). The artifact is a schema-valid logical-interchange document with real footprint geometry, and its real-partner gate (an actual import/ job-creation pass against a consuming tool) is open — tracked as an RFC-015 acceptance blocker in docs/compliance-report.md (review F1).