Embedded HandbookApps
All sections › XVI. The circuit board
STM32F103 · level intermediate · 7 min read
Open in the interactive handbook

131. KiCad: a board from a script and checks from the command line

KiCad is a free suite for schematics and boards, and it has something that most commercial ones lack: the board is accessible from Python, and the checks and the export of factory files are available from the command line. So a board can be built, checked and released the same way as firmware: by a script, in a container, on a server.

That is exactly what is done here. A board with an STM32F103C8 — crystal, decoupling, SWD connector, regulator, LED — is placed by a pcbnew script, checked with DRC and exported to Gerber by the kicad-cli program from the kicad/kicad:9.0 image (KiCad 9.0.9). All the numbers in this section come from these runs; the principles the board follows are in section 130.

The board is built without a schematic — so that it can be constructed and checked by a script. In normal work the parts and nets come from the schematic: in the board editor that is Tools / Update PCB from Schematic (F8; in the Russian interface it has a translated name). And then the schematic is also checked from the command line: kicad-cli sch erc — the electrical rules, kicad-cli pcb drc --schematic-parity — whether the board matches the schematic.

Terms used in the documentation

Term Meaning
footprint the land pattern: pads and the outline of a part
symbol the sign of a part on the schematic
pad a copper contact area for a pin
track, via a copper trace, a plated hole between layers
zone, copper pour an area filled with copper
courtyard the area a part occupies on the board
silkscreen the printed text and markings on the board
solder mask the protective coating over the copper
Design Rules Check (DRC) the check of the layout rules
unconnected items connections not routed yet
netclass a class of nets: track width and clearances
Gerber, drill file layer and drilling files for the factory

131.1 What is in a project

File What is in it
*.kicad_pro project settings: rules, net classes
*.kicad_sch the schematic
*.kicad_pcb the board: footprints, tracks, zones
*.kicad_sym a symbol library
*.pretty/*.kicad_mod a footprint library: a folder with one file per package

All the files are text in an S-expression notation, convenient to keep in git and to diff. The standard libraries are installed together with KiCad: the STM32F103 symbol is in MCU_ST_STM32F1.kicad_sym, the package in Package_QFP.pretty/LQFP-48_7x7mm_P0.5mm.kicad_mod.


131.2 A board from Python

The pcbnew module is the same code that draws the board in the window. A footprint is taken from the library, placed at a point and gets nets on its pins:

def place(lib, name, ref, x, y, rot=0, pins=None):
    fp = pcbnew.FootprintLoad(FP + lib + ".pretty", name)
    fp.SetReference(ref)
    fp.SetPosition(VECTOR2I(mm(x), mm(y)))
    fp.SetOrientationDegrees(rot)
    b.Add(fp)
    for pad in fp.Pads():
        n = (pins or {}).get(pad.GetNumber())
        if n:
            pad.SetNet(net(n))
    return fp

The controller is one line with a pin table: which pin goes to which net. This board has 15 footprints and 13 nets.


131.3 Decoupling: along the normal to the pin row

The first placement was naive: the capacitor next to the pin, the "first" capacitor pin to the supply, the track straight across. DRC found 20 violations in it, and three of them were short circuits: the track from the capacitor to VDD ran diagonally and touched neighbouring package pins — GND and an unused pin (131.5).

The right way is to put the capacitor on the normal to the pin row, pointing outward, and connect to VDD the capacitor pin nearest to it. The track is then straight and passes by the neighbours, and the ground pin of the capacitor goes straight down through a via into the solid ground of the bottom layer:

    for i, vdd in enumerate(("24", "36", "48", "9"), 1):
        px, py = xy(pad(mcu, vdd))
        dx, dy = px - MCU[0], py - MCU[1]
        if abs(dy) > abs(dx):                         # верхний или нижний ряд: нормаль по Y
            nx, ny, rot = 0.0, (1.0 if dy > 0 else -1.0), 90
        else:                                         # левый или правый ряд: нормаль по X
            nx, ny, rot = (1.0 if dx > 0 else -1.0), 0.0, 0
        c = place("Capacitor_SMD", "C_0603_1608Metric", "C%d" % i, px + nx * 2.8, py + ny * 2.8, rot)
        track((px, py), xy(near), "+3V3")             # прямая дорожка по нормали — мимо соседних выводов
        fx, fy = xy(far)
        v = (fx + nx * 1.0, fy + ny * 1.0)
        track((fx, fy), v, "GND")
        via(v, "GND")                                 # земля конденсатора — сразу в полигон нижнего слоя

The track from each VDD pin to its capacitor is 2.025 mm: the capacitor centre is 2.8 mm from the pin, and the pad of the 0603 package is 0.775 mm from its centre, 2.8 − 0.775 = 2.025.

Decoupling at a VDD pin: a diagonal track to the first capacitor pin touches the neighbouring GND, along the normal — a straight 2.025 mm track and, 1.0 mm further, a via to ground

The board after the fixes: on top — decoupling at the four supply pins, on the bottom — a solid ground


131.4 Ground: the zone and its fill

The whole bottom layer is a zone of net GND. The outline is given by points, and the copper inside is computed by a separate step — the fill: it goes around foreign pads with a clearance and connects its own ones through thermal reliefs.

Here is a trap: filling a board built by a script in memory crashes KiCad 9 without a window — Segmentation fault inside ZONE_FILLER.Fill. It works if you first save the board, load it from the file and build the connectivity:

b = pcbnew.LoadBoard(path)
b.BuildConnectivity()
filler = pcbnew.ZONE_FILLER(b)
ok = filler.Fill(b.Zones())
pcbnew.SaveBoard(path, b)

On the finished board the fill copper is 1330.2 mm².


131.5 DRC from the command line

kicad-cli pcb drc --severity-all --format json -o drc.json board.kicad_pcb

The JSON report has two parts: rule violations and unrouted connections. The naive placement from 131.3:

Violation Count What it is
shorting_items 3 the +3V3 track touches foreign package pins
solder_mask_bridge 3 the same places: no mask is left between different nets
courtyards_overlap 4 parts overlap each other — the regulator, capacitors, connector
copper_edge_clearance 1 a connector pin is closer to the board edge than allowed
silk_over_copper 5 silkscreen text on pads
silk_edge_clearance 2 silkscreen at the very edge
silk_overlap 2 silkscreen texts overlap each other

In total 20 violations of seven types and 29 unrouted connections. After the fixes from 131.3 and moving the power block — 0 violations and 24 unrouted connections: these are signal nets left to be routed by hand, not errors.

The --exit-code-violations option makes the exit code non-zero — and it counts not only violations but also unrouted connections. The fixed board has 0 violations and 24 unrouted connections — and the exit code is the same, 5, as for the naive one. So on a build server (section 110) the option is used once the board is fully routed; until then DRC is run without it and the number of violations is checked in the JSON report.


131.6 Files for the factory

kicad-cli pcb export gerbers --layers F.Cu,B.Cu,F.Mask,B.Mask,F.Silkscreen,B.Silkscreen,F.Paste,Edge.Cuts -o fab/ board.kicad_pcb
kicad-cli pcb export drill -o fab/ board.kicad_pcb

Without a list of layers export gerbers exports all the board layers — 24 layers and a job file, 25 files, including user layers, fabrication layers and part courtyards that the factory does not need. With the list — exactly what manufacturing needs: eight layers, the job description file .gbrjob and the drill file .drl, 10 files in total.

The board pipeline in the kicad/kicad:9.0 container: make_board.py builds board.kicad_pcb, the zone fill happens only after saving and loading from the file, otherwise Segmentation fault, DRC in JSON — 20 violations on the naive board and 0 after the fixes with 24 unrouted, export with a layer list — 10 files instead of 25


131.7 Pitfalls

Symptom Cause
DRC: shorting_items on package pins the track from a decoupling capacitor runs diagonally across neighbouring pins
Segmentation fault in the script filling a zone on a board built in memory — fill a board loaded from a file
"extra layers" at the factory Gerber without a layer list — all 24 layers exported
DRC fails in the build although there are no violations --exit-code-violations also counts unrouted connections
DRC is clean but the board does not work unrouted connections are a separate list, not violations
a colleague has different rules rules and net classes live in *.kicad_pro — keep it in git together with the board

131.8 Summary

Next: section 132 "DipTrace" — the same path in another program.

Open in the interactive handbook