CoHDL
  1. Docs
  2. Layout & fabrication

Layout & fabrication

A schematic that compiles is not yet a board. This page covers the physical half of the language — pads, footprints, silkscreen, mount holes, placement — and the artifacts cohdl build emits for the tools that do the physical work: KiCad footprints, layout.json, IPC-2581 and physics hints for the autorouter.

The boundary

CoHDL emits everything downstream of the schematic, and stops there. It does not route, and it never invents a placement — the compiler carries the physical facts an author declares (land geometry, locked positions, routing constraints) into formats a layout tool consumes, and the layout tool does the placing and routing. In practice that tool has been Quilter for autorouting and KiCad for review and fabrication output.

A build writes, under out/:

  • the KiCad netlist (<package>.net) and BOM (<package>-bom.csv) — the schematic-side outputs, covered on the CLI page;
  • footprints/*.kicad_mod — one KiCad footprint file per land pattern the design uses, projected from the pad and footprint declarations below;
  • <package>-layout.json — the layout-constraint artifact: board outline, locked placements, net classes, differential pairs, placement hints;
  • eight physics-constraint CSV files, emitted when the design declares physics facts;
  • <package>.xml — an IPC-2581 handoff document, with build --emit ipc2581;
  • <package>.kicad_pcb — a native KiCad 10 board file (placements, net-bound footprints, board outline), with build --emit kicad_pcb — written by the compiler itself, no KiCad installation required.

All of it is deterministic: same source and same locked dependencies produce the same bytes, so every one of these artifacts can sit in version control and diff meaningfully.

Pads

A pad is a standalone, reusable declaration — defined once, referenced by any number of footprints. This is the Cadence-style split: fixing a land dimension in one pad fixes it in every footprint that references it. The vocabulary is closed. shape is one of rect, circle, oval or annulus; size is shape-dependent — (w, h) for rect and oval, (d) for circle, (outer, inner) for annulus. layer is top_copper, bottom_copper or through_all, and plating is smd or plated_through_hole.

lib/passive/src/pads.cohdl excerpt
// KiCad R_0402_1005Metric pad (0.54mm x 0.64mm).
pub pad P_Chip_0402 {
    shape: rect
    size: (0.54mm, 0.64mm)
    layer: top_copper
    plating: smd
}

Every dimension is a Length, written with the mm unit. The language's zero-coercion rule applies to geometry exactly as it does to volts: a bare number where a dimension is expected is a compile error naming the expected unit. A through-hole pad must declare its drill — and declaring one on an SMD pad is an error the other way (code E805 covers the whole family of malformed pads):

lib/connectors/src/headers/micro_fit_3.cohdl excerpt
pub pad P_MicroFit3_PTH {
    shape: circle
    size: (1.5mm)
    layer: through_all
    plating: plated_through_hole
    drill: 1.02mm
}

A small set of bounded fabrication controls — a corner chamfer or corner radius on rectangular SMD pads, solder-mask expansion, reduced or segmented paste apertures — exists as provisional syntax for footprints that need them; omitting them preserves the plain geometry above.

Footprints

A footprint composes pad references into a land pattern. Each pad N: Symbol at (x, y) line places one pad instance relative to the footprint's origin; courtyard declares the keep-out rectangle and silkscreen_ref positions the reference designator's silkscreen text:

lib/led/src/chip.cohdl excerpt
pub footprint FP_LED_0603_1608Metric {
    pad 1: P_Chip_LED0603 at (-0.75mm, 0mm)
    pad 2: P_Chip_LED0603 at (0.75mm, 0mm)
    silkscreen {
        polarity_marker cathode_pin 1 shape band
    }
    courtyard { shape: rect, at: (0mm, 0mm), size: (2.8mm, 1.4mm) }
    silkscreen_ref { at: (0mm, -1.2mm) }
}

Pad numbers are checked against the bound device's declared pin numbers at cohdl build — a footprint with a missing or extra number is an error naming exactly which numbers disagree (E807). The same number may appear in several placement lines: that models one electrical terminal implemented by several physical features, such as an exposed pad's thermal vias. An empty footprint body is also legal — a stage-one placeholder that lets a part bind a named footprint before anyone has drawn its geometry; the pad-consistency check applies once the body has content.

A pad placement may carry rotate ANGLE. Quad packages are the motivating case — the same lead pad is placed on all four sides, rotated rather than redefined:

lib/qfn/src/footprints.cohdl excerpt
pub footprint QFN56N40P700X700_1EP400X400 {
    pad 1: P_QFN56N40P700X700_LEAD_CORNER at (-3.425mm, -2.6mm)
    pad 2: P_QFN56N40P700X700_LEAD at (-3.425mm, -2.2mm)
    // ...
    pad 15: P_QFN56N40P700X700_LEAD at (-2.6mm, 3.425mm) rotate 90
    pad 16: P_QFN56N40P700X700_LEAD at (-2.2mm, 3.425mm) rotate 90
    // ...
}

The rotation is preserved as a fact: the emitted KiCad pad carries the declared size plus a real rotation angle, rather than the common library trick of silently swapping width and height. Pad rotation accepts any whole degree from 0 to 359 — the same range as component placement below (E811).

That QFN's name is not decoration. For the common package families — QFP, QFN, SOIC/SOP, SOT, BGA, and CHIP/MELF passives — a footprint's identifier must be its IPC-7351 land-pattern designator (with - mapped to _), and for geometrically regular layouts the compiler cross-checks it: the pin count and pitch the name encodes must agree with what the pad placements actually draw, and a mismatch is a compile error naming the disagreement. Footprints outside those families — connectors, modules, the LED above — use ordinary identifiers.

Silkscreen

The silkscreen { } block draws legend graphics from four closed primitives — line, circle, arc and polygon, all with Length-typed geometry — plus two semantic markers. pin_1_marker near pad N shape dot|triangle and polarity_marker cathode_pin N shape band|arrow say what the mark means; the compiler expands each into ordinary checked primitives, and both emitters consume that one expansion, so the KiCad and IPC-2581 outputs cannot drift apart. The marker's standoff is measured from the pad's edge, not its centre — otherwise the mark would sit on the copper it is supposed to point at.

lib/@espressif/esp32/src/footprints.cohdl excerpt
    silkscreen {
        pin_1_marker near pad 1 shape dot
        line from (-9mm, -12.75mm) to (9mm, -12.75mm) width 0.15mm
        line from (-9mm, -12.75mm) to (-9mm, -6.75mm) width 0.15mm
        line from (9mm, -12.75mm) to (9mm, -6.75mm) width 0.15mm
        line from (-9mm, -6.75mm) to (9mm, -6.75mm) width 0.15mm
    }

A marker must name a pad the footprint actually declares, and a polarity marker needs a second distinct terminal to orient toward — both checked at declaration (E812). There is deliberately no auto-inference: the compiler never guesses pin 1 or a cathode from pin roles; every mark is an explicit statement. On the KiCad side each primitive becomes its native counterpart (fp_line, fp_circle, fp_arc, fp_poly) on the silk layer; the IPC-2581 emitter carries the same graphics as contour polygons.

Mount holes

Some footprints need holes with no electrical function — a connector shell's alignment pegs, a switch's mounting legs. mount_hole is the construct for exactly that, with its own numbering that is fully disjoint from pad numbering: a footprint can have pad 1..16 and mount_hole 1..2 without collision, and mount-hole numbers are never checked against the device's pins.

lib/connectors/src/headers/micro_fit_3.cohdl excerpt
pub footprint FP_Molex_43045_0212 {
    pad 1: P_MicroFit3_PTH at (0mm, 0mm)
    pad 2: P_MicroFit3_PTH at (0mm, 3mm)
    mount_hole 1: non_plated at (-3mm, 3.94mm) diameter 1.02mm
    mount_hole 2: non_plated at (3mm, 3.94mm) diameter 1.02mm
    // ...
}

Plating is non_plated (the common case) or plated — there is no SMD value, because a mount hole is definitionally a hole, and no layer field, because it always spans the board. The default shape is a circle taking diameter D; rectangular and oval holes — a keyswitch's locating legs, for instance — are written mount_hole 1: non_plated shape: rect size: (2.0mm, 1.5mm) at (-6.75mm, 0mm). Writing diameter with a rect shape, or size: with a circle, is a compile error naming the mismatch (E810). In the KiCad output a non-plated mount hole becomes KiCad's own np_thru_hole pad type; a plated one becomes an ordinary through-hole pad with no net.

Placement

Placement lives in a design's layout { } block, alongside the routing constraints of the next section. place locks one top-level instance at a coordinate: place NAME at (x, y) [rotate ANGLE] [side top|bottom]. Anything you do not place stays the layout tool's problem — that split is the point. Board-edge and mechanically-fixed parts get locked; parts whose best position depends on routing are left to the autorouter. From the compiler's Raspberry Pi Pico 2 recreation example:

examples/rpi-pico2/src/main.cohdl excerpt
layout {
    net_class HighSpeed { USBC_DP, USBC_DM, USB_DP, USB_DM, USB_DPX, USB_DMX }
    diff_pair(USBC_DP,
              USBC_DM) [differential_impedance: 100ohm, single_ended_impedance: 50ohm, frequency: 1GHz]
    diff_pair(USB_DPX,
              USB_DMX) [differential_impedance: 100ohm, single_ended_impedance: 50ohm, frequency: 1GHz]
    length_match(USB_DPX, USB_DMX) [tolerance: 0.15mm]
    net_class Clock { XIN, XR }
    board_outline: "mechanical/pico2-outline.dxf"
    // Interface ports are pre-positioned (locked) at their board-edge
    // locations, oriented; the layout partner places the rest around them.
    place hdr at (0mm, 0mm)
    place usb at (-23.64mm, 0mm) rotate 270
    place esd at (-13mm, 0mm)
    place swd at (22mm, 0mm) rotate 90
    place mcu at (0mm, -2mm)
    place xtal at (0mm, 5mm)
    place c_xin at (-4mm, 5mm) rotate 90
    place c_xout at (4mm, 5mm) rotate 270
}

board_outline references a mechanical DXF and extracts exactly one closed polyline from its Edge.Cuts layer — straight segments and arcs — at build time. The outline is a mechanical engineering artifact, so CoHDL reads it rather than re-authoring it; a missing or non-closed outline is a compile error naming the problem (E1006). CoHDL is not a DXF viewer: nothing else in the file is read.

rotate accepts any whole degree from 0 to 359. A value of 360 or above is an error rather than being reduced — a full turn is 0, and rotate 450 is more likely a slip than a deliberate 90. Because emitted coordinates are byte-stability-critical, rotated geometry is computed with a checked-in fixed-point sine table rather than the platform's floating-point math library, whose last bit differs across platforms — a rotated pad lands on the same bytes on every machine, and the table is exact at the four cardinal angles.

side bottom puts a component on the back of the board — the OpenMicroKbd macropad places its entire MCU that way, as place mcu at (-40mm, 24mm) rotate 270 side bottom. The footprint is authored exactly once, for its natural top-side orientation; the emitters apply the mirroring (following KiCad's own flip-then-orient convention), so the part lands correctly when the board is flipped and no hand-maintained mirrored footprint ever exists. side defaults to top and composes freely with rotate (E1008). One honest limitation: place reaches only a design's own top-level instances — a part instantiated inside a sub-circuit fn cannot currently be locked, a disclosed gap rather than a hidden one.

layout.json

Everything the layout { } block and the placement-adjacent attributes declare is emitted as one JSON artifact, <package>-layout.json — written only when the design actually carries layout metadata, and removed by a rebuild that no longer does. Its notable fields, from the Pico 2 example's real output:

out/rpi-pico2-layout.json excerpt
{
  "schema_version": 1,
  "net_classes": [
    { "name": "HighSpeed", "nets": ["USBC_DP", "USBC_DM", "USB_DP", "USB_DM", "USB_DPX", "USB_DMX"] },
    { "name": "Clock", "nets": ["XIN", "XR"] }
  ],
  "diff_pairs": [
    { "p": "USBC_DP", "n": "USBC_DM" },
    { "p": "USB_DPX", "n": "USB_DMX" }
  ],
  "length_matches": [
    { "nets": ["USB_DPX", "USB_DMX"], "tolerance": "0.15mm" }
  ],
  ...
}

Beyond the constraint arrays it carries board_outline (the DXF source path plus the extracted segment geometry), placements (instance path, coordinates, rotation, and a "side" key present only for back-side parts) and placement_hints. A hint is the soft counterpart to place: the attribute #[placement_hint("board edge, USB-C cutout")] on an instance passes an opaque suggestion through to the layout tool without locking anything. Net names in this artifact are the final netlist names — identical to the .net file, so the two always agree. CoHDL never verifies these constraints are physically met; it has no geometry to check them against, and says so.

Physics hints for the autorouter

An autorouter can infer some electrical intent from a netlist, but the design source simply knows more. Seven structured attributes attach physics facts directly to the declaration each fact is about — three on nets:

  • #[ground(primary|secondary)] — ground nets; at most one primary per design;
  • #[high_current(500mA)] — power nets and their current;
  • #[impedance(50ohm, frequency: 1GHz)] — single-ended controlled impedance;

and four on instances:

  • #[bypass(mcu.VDD, 100nF)] — on the bypass capacitor itself, naming the pin it serves;
  • #[crystal_oscillator(mcu, XIN, XOUT)] — on the crystal, naming the oscillator's pins;
  • #[switching_converter(inductor: l1, input_capacitor: c_in, output_capacitor: c_out)] — on the converter, naming its power components (only the inductor is required);
  • #[bga_fanout] — on a BGA that needs fanout generation.
examples/rpi-pico2/src/main.cohdl excerpt
#[high_current(500mA)]
net VBUS [5V]: usb.VBUS, esd.VBUS, d_vbus.Anode, r_vbus_top.A, hdr.VBUS

#[ground(primary)]
net GND [gnd]: mcu.GND, mcu.VREG_PGND, reg.GND, flash.GND, xtal.GND, usb.GND, usb.SHIELD,
               esd.GND, rcc1.B, rcc2.B, swd.P2, boot.B, led.Cathode, hdr.GND, hdr.AGND,
               // ...

The arguments are structurally checked, not opaque prose: pin and instance references must resolve, and the numeric values are unit-typed, so #[high_current(100nF)] is a compile error naming the expected unit (structural mistakes are E1009; unit mismatches are E110). The attributes also work inside reusable sub-circuits — a #[bypass] written once inside a decouple function produces one independently-resolved fact per call site. A differential pair's physics ride the diff_pair(...) [...] bracket shown in the placement excerpt above, since that fact belongs to a pair of nets rather than one declaration.

When a design carries any of these facts, cohdl build emits an eight-file CSV set matching Quilter's physics-constraint templates exactly: bga_components.csv, bypass_capacitors.csv, crystal_oscillators.csv, differential_pairs.csv, ground_nets.csv, high_current_nets.csv, single_ended_impedance_signals.csv and switching_converters.csv — designators and pad-number pins, values in the templates' own scales:

out/bypass_capacitors.csv excerpt
capacitor,bypassed_component,bypassed_pin,capacitance
C9,U2,1,100
C9,U2,11,100

Nothing is auto-inferred: every constraint in these files is an attribute an author wrote, and omitting an attribute simply leaves that decision to the autorouter's own detection. The OpenMicroKbd use case shows the whole loop on a shipped board — attributes in source, CSVs out, Quilter placing and routing from them.

IPC-2581

cohdl build --emit ipc2581 writes <package>.xml beside the netlist — an IPC-2581 revision B1 document, the vendor-neutral handoff format, validated against the consortium's published schema in the compiler's test suite. It carries the logical netlist, every instance's designator, MPN and unit-typed spec values, the layout constraints, and the real physical geometry a layout tool needs: per-pin pad stacks with copper, mask and paste, the board profile extracted from the DXF, and the silkscreen graphics. It is also honest about what it is not: the document marks itself logical-complete,placement-staged,unrouted. Instances you placed are fixed inside the outline, where a layout tool treats them as locked; everything else is staged in a deterministic grid just outside it — the standard "please place me" signal — and no routing is claimed, because none exists yet. Where the schematic ends, the layout tool begins.