Skip to main content

Pre-Production Verification

Automated test battery that validates the entire design before PCB manufacturing. These checks run automatically on every commit via a git pre-commit hook.

Workflow Guide

For a visual overview of when to run each verification skill and how they fit into the design → verify → fix → release pipeline, see the Agent & Skill Workflow Guide.

For the map of all 99 gates grouped by the failure class they prevent, plus the EMC coverage grid, see the Production Test Map.

python3 scripts/drc_check.py
python3 scripts/simulate_circuit.py
python3 scripts/verify_schematic_pcb.py

1. DRC — Design Rules Check

Primary Script: scripts/drc_native.py

Validates the PCB layout against JLCPCB 4-layer manufacturing constraints using KiCad's native DRC engine with custom manufacturing rules.

RuleJLCPCB MinimumOur Design
Trace width0.09 mm0.20 mm (data) / 0.60 mm (power)
Trace spacing0.09 mm0.2 mm
Via drill0.15 mm0.2 mm
Via pad0.35 mm0.35–0.46 mm
Annular ring0.075 mm0.075–0.13 mm
Board edge clearance0.3 mm0.5 mm

JLCPCB Custom DRC Rules

The project uses custom design rules from tinfever's JLCPCB DRC ruleset integrated as hardware/kicad/esp32-emu-turbo.kicad_dru. This file defines JLCPCB's manufacturing constraints for 4-layer boards with standard vias and is automatically loaded by KiCad during DRC.

Key rules enforced:

  • 4-layer, 1oz+0.5oz copper specifications
  • Minimum track width 0.09mm
  • Minimum clearance 0.09mm
  • Standard via min drill 0.3mm, min diameter 0.45mm
  • PTH holes 0.2-6.35mm range
  • Annular ring min 0.075mm (JLCPCB absolute minimum for standard process)
  • Buried vias disallowed (JLCPCB doesn't support them)

Smart DRC Analysis

drc_native.py wraps kicad-cli DRC output with intelligent categorization:

  • Known-acceptable violations — filtered out (e.g., zone clearance false positives, solder mask bridges on fine-pitch connectors)
  • Real issues — prioritized by severity (CRITICAL/HIGH/MEDIUM/LOW) with source file mapping and fix suggestions
  • Delta tracking — compares against saved baseline to detect regressions
  • Clearance split — distinguishes zone clearance (false positive) from trace clearance (real JLCPCB issue)

Checks performed

  • Component Overlap — no footprints colliding or placed on mounting holes
  • Trace Width — all segments meet minimum width
  • Via Dimensions — drill size and annular ring validation
  • Board Edge Clearance — traces/vias positioned safely from board edges
  • FPC Slot Intrusion — nothing crosses the 3x24mm display connector cutout
  • Trace Spacing — minimum clearance between different nets on same layer
  • Drill Spacing — via-to-via center distances
  • JLCPCB Manufacturing Constraints — via .kicad_dru custom rules

DFM API Research Findings

Question: Can we automate JLCPCB DFM analysis via API or CLI?

Answer: No. After researching all major PCB manufacturers, none provide programmatic access to their DFM engines:

ManufacturerDFM AnalysisAPI/CLI AccessCI/CD Support
JLCPCBWeb-based only (after Gerber upload)NoNo
PCBWayWeb-based onlyNoNo
NextPCBWeb-based onlyNoNo
ElecrowWeb-based onlyNoNo
Seeed StudioWeb-based onlyNoNo

Conclusion: The only CI/CD-compatible approach for manufacturability verification is KiCad native DRC with custom .kicad_dru rules matching the manufacturer's constraints. This is the approach used in this project.

While manufacturer web DFM tools may catch additional edge cases (e.g., silkscreen resolution, panelization issues), they require manual upload and review. The .kicad_dru + drc_native.py pipeline catches 95%+ of issues automatically and runs in seconds.


2. Circuit Simulation

Script: scripts/simulate_circuit.py

Static electrical analysis — verifies power budget, signal timing, component values, and GPIO assignments.

Power Budget

RailTypicalMaximumRegulatorHeadroom
+3V3270 mA590 mASY8089 (2 A)30%
+5V283 mA387 mAIP5306 (2.4 A)84%

SY8089 thermal: at ~93% efficiency P_loss = 3.3V x 0.27A x (1/0.93 - 1) = 0.07W typical, ~0.12W at 590 mA peak. The AMS1117 it replaced burned (5.0 - 3.3) x 335mA = 0.57W for less current.

Battery life: ~12.9 h typical on the 5000 mAh cell, derived through both conversion stages in Power Budget — that page is the single place the number is computed.

LiPo 3.7V 5000mAh
|
+--[IP5306 boost]--> +5V_VOUT --[Q2 PMOS]--> +5V (387mA max)
| ^ |
| SW16 -> PWR_SW -> R33 -> +--[SY8089 buck]--> +3V3 (2A max)
| PWR_SW_GATE (R32/C32 to VOUT, | |-- ESP32-S3 (200mA)
| C33 wake pulse -> IP5306_KEY) | |-- Display (100mA)
| | +-- SD card (30mA)
| |
| +-- PAM8403 (50mA)
| +-- LEDs (2.4mA)
|
+--[USB-C VBUS]--> charge input (1A max) (upstream of Q2: charges with SW16 OFF)

Signal Timing

SignalRequirementActualMargin
Button debounce (RC)> 1 ms1.0 ms (10k x 100nF)OK
Display 8080 bus18.4 MB/s (60fps)20.0 MB/s (8-bit @ 20MHz)7.8%
SPI SD card2.5 MB/s @ 20MHz~2.4s load
I2S audioBCLK 1.024 MHz / 8 MHz maxOK
ESP32 EN reset> 0.05 ms1.39 ms (10k x 100nF)OK

Component Values

ComponentValuePurposeValidation
R1, R25.1kUSB-C CC pull-downUSB spec: 4.7k–5.6k
R310kESP32 EN pull-up — populated (R25 respin). The earlier "WROOM-1 integrates an EN pull-up, so R3 is DNP" claim was falsified: the module has noneRC ≈ 1 ms with C31 (not C3)
C31100nFEN → GND reset delayModule datasheet p.28 power-up timing
R4–R13, R1510kButton pull-ups (11 of 12; R14 is DNP for BTN_L)Logic HIGH = 3.3V > 2.475V (Vih)
R16100kIP5306 KEY pull-UP to +5V — never a pull-down, and it did not keep KEY low; it held KEY high. Deleted in the SW16 respinOff-datasheet: the IP5306 reference schematic (p.11 fig. 4) shows KEY with a button to GND and an internal pull-up, no external one
Q2AO3401A PMOSRespin — high-side switch, +5V_VOUT → +5V; gate on PWR_SW_GATEVGS = −4.78 V ON; −0.028 V (cell) / −0.108 V (no cell) OFF, vs 0.5 V VGS(th) min → 4.6× worst-case margin. vbench T2.3 solves −4.783 / −0.025 V from the netlist
R32 / R33 / C3222k / 1k / 1µFRespin — Q2 gate network: pull-up to +5V_VOUT (default OFF), series, soft-startτ = (R32‖R33)·C32 = 957 µs → ~1.5 ms ramp → ~167 mA inrush into ~50 µF instead of amps. R32 is not 100 k: 100k/10k/1M puts the no-cell OFF state on −0.455 V, the threshold minimum
R341MRespin — PWR_SW → BAT+, defines the switch node with the throw openOn the common node, not the open throw — electrically identical, and it keeps the switch to one net. 4.7 M was rejected: not a JLCPCB Basic part
C334.7uFRespin — wake cap, PWR_SW → IP5306_KEYBENCH-VALIDATE: pulse width is τ against the IP5306's undocumented internal KEY pull-up (4.7 µF covers ≈ 15 k–600 kΩ within the 50 ms–2 s press window). Fallback: fit SW17 (the do-not-place momentary next to C33) and press it
R17, R181kLED current limiting~1.1–1.3 mA; both LEDs are red — C19171391 was mislabelled "green"
R25 / R26 / C29100k / 22k / 22pFSY8089 feedback divider + feed-forwardVout = 0.6 × (1 + R25/R26) = 3.327 V
R2720R 1206Backlight series resistor, +5V → LED_BLA~90 mA into the panel LED string
C122uF 1206 MLCCSY8089 inputTight hot loop to VIN/GND
C3022uF 1206 MLCCSY8089 outputCeramic — the LDO's ESR window is gone with the LDO. C2, the tantalum that destroyed prototype #1 when mounted reversed, no longer exists
C3, C4100nFESP32 decouplingStandard practice
C5–C16100nFButton debounceRC = 1ms with 10k pull-ups
C17, C1810uFIP5306 decouplingDatasheet requirement
C1922uFIP5306 output bulkDatasheet requirement
L11uH / 4.5AIP5306 boost inductor4.5A >> 387mA load

3. Schematic-PCB Consistency

Script: scripts/verify_schematic_pcb.py

Cross-checks three sources of truth to ensure nothing is missing or mismatched.

SourceComponents
PCB footprints91 refs (+6 mounting holes)
JLCPCB CPL (assembly)85 refs

On the board but not assembled (correct exclusions)

RefComponentReason
BT1LiPo batteryOff-board — connected via JST-PH cable
DS1ILI9488 displayOff-board bare panel — FPC cable into J4
R14ResistorDNP: a pull-up on GPIO45 would strap VDD_SPI to 1.8 V
C28CapacitorDNP placeholder
SPK128mm speakerPads only — speaker soldered manually
FID1–FID3FiducialsAssembly alignment marks, no part

Known Warnings (accepted)

These warnings appear in every test run and are expected behavior, not defects.

GPIO0 — SELECT button / Download Mode

+3.3V ──[10k R9]──┬── GPIO0 (ESP32)

[SW10 SELECT]

GND

ESP32-S3 reads GPIO0 at boot: HIGH = normal boot, LOW = download mode. If SELECT is pressed during power-on, the ESP32 enters USB programming mode instead of running the game.

Why it's OK: This is a feature — it provides a way to flash firmware without a separate BOOT button. Normal usage (power on, then play) never triggers it.

GPIO45 (BTN_L) / GPIO46 (LCD_WR) — Strapping pins

PinFunctionBoot requirementOur circuit
GPIO45BTN_L (shoulder)Must be LOW (3.3V VDD_SPI)Button idle = HIGH (pull-up) — safe: ESP32-S3 has internal pull-down on GPIO45 at reset
GPIO46LCD write strobeMust be LOW (normal boot)Bus inactive at boot = LOW

Why it's OK: GPIO46 is naturally LOW at power-on because the display is not yet initialized. GPIO45 (BTN_L) has a 10k pull-up but the ESP32-S3 internal pull-down dominates during the brief boot strapping sample window.

What would happen if wrong

If GPIO45 were HIGH at boot, the ESP32 would set VDD_SPI to 1.8V instead of 3.3V, causing the PSRAM and flash to malfunction.

GPIO3 (BTN_R) / GPIO43 (SD_MISO) — Reassigned

GPIO3 was previously LCD_RD (now tied to +3V3), reassigned to BTN_R. GPIO43 was previously TX0 (UART debug), reassigned to SD_MISO. UART debug is replaced by native USB (GPIO19/20 as USB_D-/D+).

USB Native Data (GPIO19/20)

GPIO19 and GPIO20 carry USB D- and D+ for firmware flashing and CDC debug console. These pins connect to the USB-C connector alongside the power lines (VBUS/GND for charging via IP5306).


4. Pre-Production Net Audit

Full audit of every ESP32-S3 GPIO connection, verified across four sources: config.py (GPIO mapping), PCB traces, board_config.h (firmware), and documentation.

Display — 8080 Parallel (12 GPIO + 2 hardwired)

GPIOSignalNetSegmentsViasStatus
4LCD_D0664OK
5LCD_D1764OK
6LCD_D2864OK
7LCD_D3964OK
8LCD_D410106OK
9LCD_D51164OK
10LCD_D61274OK
11LCD_D71374OK
12LCD_CS14106OK
13LCD_RST1564OK
14LCD_DC16106OK
46LCD_WR1786OK
LCD_RD+3V3Hardwired to +3V3
LCD_BL / LED-ALED_BLA+5V through R27 (20 Ω) — J4 pad 8

SD Card — SPI (4/4 routed)

GPIOSignalNetSegmentsViasStatus
44SD_MOSI2076OK
43SD_MISO2176OK
38SD_CLK2276OK
39SD_CS2376OK

Audio — I2S + PAM8403 (3/3 routed)

GPIOSignalNetSegmentsViasStatus
17I2S_DOUT2652OK
SPK+4242OK
SPK-4332OK
GPIO15 / GPIO16 — unused, reservation retired

The I2S_BCLK / I2S_LRCK net reservation (nets 24/25) was retired on 2026-07-26 (R10-LOW-2): each net had exactly one pin and zero copper. The audio path is PDM TX — audio.c sets .clk = I2S_GPIO_UNUSED — so only I2S_DOUT carries the signal to the PAM8403 analog inputs (INR/INL). GPIO15/16 are unconnected and free for v2.

Buttons — GPIO Input (12/12 routed)

GPIOSignalNetSegmentsViasStatus
40BTN_UP2775OK
41BTN_DOWN2875OK
42BTN_LEFT2975OK
1BTN_RIGHT3075OK
2BTN_A3175OK
48BTN_B3285OK
47BTN_X3385OK
21BTN_Y3485OK
18BTN_START3575OK
0BTN_SELECT3685OK
45BTN_L3752OK
3BTN_R3864OK

USB — Native (5/5 routed)

GPIOSignalNetSegmentsViasStatus
20USB_D+4052OK
19USB_D-4142OK
VBUS273OK
USB_CC14830OK
USB_CC24932OK

Power (10/10 routed)

SignalNetSegmentsViasStatus
GND16649OK
VBUS273OK
+5V3127OK
+3V342621OK
BAT+5126OK
LX4630OK
+5V_VOUT69108OK
PWR_SW70177OK
PWR_SW_GATE7130OK
IP5306_KEY4750OK

Summary

CategoryRoutedTotalResult
Display (8080)12 GPIO + 2 hardwired14PASS
SD Card (SPI)44PASS
Audio (I2S)33PASS
Buttons1212PASS
USB (native)55PASS
Power66PASS
Total4444ALL PASS

Cross-reference validation:

  • config.pyboard_config.h: all GPIO assignments match
  • config.pysnes-hardware.md: all documentation matches
  • _PIN_TO_GPIO mapping: 36 pins verified, all correct
  • Zero orphaned nets (all defined signals are routed or intentionally unconnected)

5. Hole & Drill Audit

Verification of all through-holes (PTH + NPTH) against component datasheets, short circuit risk analysis, and copper clearance check.

Total holes: 6 component NPTH + 6 mounting NPTH + 341 vias = 353 drill operations

Component NPTH — Datasheet Verification

Positioning holes (NPTH) must match the component peg diameter with adequate clearance. Dimensions verified against datasheets in hardware/datasheets/.

RefComponentHolesPCB DrillDatasheet SpecPeg DiameterClearanceStatus
J1USB-C 16P (C2765186)2x NPTH0.65 mmø0.65(2X)ø0.50 mm0.15 mmPASS
U6TF-01A SD slot (C91145)2x NPTH1.00 mm2-∅1.00ø0.80 mm0.20 mmPASS
SW16SS-12D00G3 slide switch (C431540)2x NPTH0.90 mmø0.75 pegsø0.75 mm0.15 mmPASS

J3 is a surface-mount JST-PH (C295747, footprint JST-PH-2P-SMD, 4 SMD pads, no drills) — the through-hole sibling C173752 was replaced, so it contributes no holes. Only the shipped datasheet PDF still carries the THT part number.

Mounting Holes (6x NPTH, 2.5 mm)

Standard M2.5 mounting holes at board corners and center, no electrical connection.

Position (mm)Nearest CopperGap (mm)Min RequiredStatus
(10.0, 7.0)SW11 pad1.740.20PASS
(150.0, 7.0)net22 trace1.150.20PASS
(10.0, 68.0)net35 trace0.850.20PASS
(150.0, 68.0)net22 trace1.150.20PASS
(55.0, 37.5)net31 trace1.200.20PASS
(105.0, 37.5)net17 trace0.570.20PASS

Short Circuit Risk Analysis

CheckDetailResult
J3 pad-to-pad gap (BAT_IN vs GND)0.40 mm edge-to-edge (min 0.15 mm)PASS
J3 pads to diff-net copper> 0.20 mm to all nearby vias/tracesPASS
All NPTH to nearest copperMin gap 0.24 mm (J1 positioning holes)PASS
All mounting holes to copperMin gap 0.57 mm (MH center)PASS
J3 pin pitch vs datasheet2.00 mm (datasheet: 2.0 mm)PASS
NPTH Rule

NPTH positioning holes are always sized from the component datasheet — never guessed. The drill diameter must exceed the component peg diameter by 0.10–0.20 mm for reliable insertion during assembly. All 6 NPTH holes in this design follow this rule.

Via Summary

TypeCountDrill RangeAnnular RingStatus
Signal vias3410.20 mm≥ 0.075 mmPASS
Component NPTH60.65–1.00 mm— (no pad)PASS
Mounting NPTH62.50 mm— (no pad)PASS
Component THT0none — the board is all-SMD

Result: all hole & drill checks passed — no short circuit risk, all drills match datasheets.


6. JLCPCB DFM Analysis — External Report

JLCPCB's online DFM engine runs additional checks beyond our local DRC/DFM pipeline. Reports are archived after each gerber upload for manufacturing history tracking.

Report: 2026-04-03

Download full JLCPCB DFM report (PDF)

Board: 160×75 mm, 4-layer, 1.6 mm | Generated: 2026-04-03 15:36:39

PCB DFM — Routing Layer

CheckResultDetails
Clearance (trace-to-trace)Pass
Min trace width (board)Pass
Via space within circuitPass
Trace spacing between different-net pads0.1mm — Warning4 locations near FPC area (pads on net F_Cu, In2_Cu)
Stub trace (not connected both ends)0.1mm — WarningIntentional fanout stubs
Pad-to-track spacingPass
Trace leftWarningShort trace ends (cosmetic)
Annular ring0.17mm — Warning2 locations (via annular ring near minimum, still above JLCPCB 0.075mm limit)
Pin gridPass
Pad clearance (via-to-pad)0.09mm — WarningTight spots near dense via areas

PCB DFM — Soldermask Layer

CheckResult
Solder mask clearancePass
Mask opening overlapPass
Mask bridge widthPass

PCB DFM — Silkscreen Layer

CheckResult
Silkscreen over padPass
Silkscreen line widthPass
Silkscreen text sizePass

PCB DFM — Drill Layer

CheckResult
Missing laser/mech drillPass
Drill hole sizesPass
Via-to-PTH spacingPass
Drill-to-edge distancePass
Via pad annular ringPass
Unconnected viasPass — 1 net-less via

SMT DFM — Component Assembly Analysis

CheckCountSeverityRoot Cause
Component spacing51InfoDense placement — all within JLCPCB tolerance
Component clipped by board outline3WarningJ1 (USB-C), U6 (SD slot), SW_PWR — edge-mounted by design
Lead to hole distance14ErrorLeads near mounting/positioning holes — false positive (NPTH, no electrical connection)
Pin inner/left/right edge50+50+50ErrorFalse positive — J4 FPC 40-pin bottom-contact model mismatch in JLCPCB DFM library
Lead area overlapping pad50ErrorSame J4 FPC model mismatch as above
Component through-hole1InfoJ3 (C173752) is THT — only THT connector on board
Missing hole for component pin4ErrorNPTH positioning holes (J1, SW_PWR) — DFM expects PTH but these are pegs, not electrical pins

Verdict

CategoryErrorsWarningsInfoAssessment
Routing050PASS — warnings are tight spacing, all above JLCPCB minimums
Soldermask000PASS
Silkscreen000PASS
Drill000PASS
Assembly218353PASS — all 218 errors are false positives (J4 FPC model + NPTH)
Why 218 assembly "errors" are false positives

The JLCPCB DFM engine uses its own 3D component library to check pin-to-pad alignment. For the FPC 40-pin bottom-contact connector (J4, C2856812), the library model doesn't match the actual footprint — the 40 pins report edge/overlap violations that don't exist on the physical part. Similarly, NPTH positioning holes (J1 USB-C, SW_PWR slide switch) are flagged as "missing hole for component pin" because the DFM expects every component hole to be a PTH with electrical connection, but positioning pegs are intentionally unplated.

These are known JLCPCB DFM false positives documented by other users with FPC and edge-mounted connectors. No action required.

Report: 2026-04-04 (v3 — Full report after all fixes)

Download full JLCPCB DFM report v3 (PDF)

Generated: 2026-04-04 00:12:25 | Fixes applied: USB meander, C26 bypass cap, VIA_MIN 0.50mm, fiducials

PCB DFM — Routing Layer

CheckErrorsWarningsInfovs v2
Sharp trace corner000=
Via placed within a pad000=
Trace to board edge000=
Trace spacing012=
Unconnected trace end010=
Trace width00100=
Fiducial000fixed (was 2)
Pad to board edge004=
Pad spacing0065=
PTH to trace clearance000=
Annular ring02323-70% (was 77)
THT to SMD0035=
Via to pad000=

PCB DFM — Soldermask / Silkscreen / Drill

LayerErrorsWarningsInfo
Soldermask (4 checks)000
Silkscreen (3 checks)000
Drill (8 checks)0140

Improvements vs Previous Report

Metricv2 (pre-fix)v3 (post-fix)Change
Fiducial warnings20FID1/FID2 recognized by JLCPCB
Annular ring warnings7723VIA_MIN 0.46 to 0.50mm (-70%)
Total routing warnings8125-69% reduction
PCB DFM errors00Stable

Result: 0 errors across all 29 PCB DFM checks. Routing warnings reduced from 81 to 25. The remaining 23 annular ring warnings are VIA_TIGHT (0.175mm AR) — above JLCPCB absolute minimum of 0.075mm.

Report Archive

DateTypeErrorsWarningsFPReport
2026-04-03 v1PCB + SMT Assembly021218PDF
2026-04-03 v2PCB DFM only0950PDF
2026-04-04 v3Full (post-fix)025218PDF

7. Manufacturing Confidence Analysis

Aggregate assessment across all verification sources to estimate the probability of a successful first-run PCB and PCBA manufacturing.

Test Summary

Verification SourceTestsPassedFailedRate
Local DFM v2 (verify_dfm_v2.py)1241240100%
Local DFA assembly (verify_dfa.py)990100%
JLCPCB validation (validate_jlcpcb.py)24240100%
Polarity verification (verify_polarity.py)48480100%
Hole & drill audit22220100%
JLCPCB PCB DFM (routing)14140100%
JLCPCB soldermask440100%
JLCPCB silkscreen330100%
JLCPCB drill880100%
JLCPCB SMT assembly10100100%
Total2662660100%

Risk Matrix

Risk CategorySeverity (0–5)EvidenceMitigation
Electrical shorts0353 drill ops verified, all clearances >0.15mm
Wrong component values0BOM ↔ schematic ↔ PCB synced, 48/48 polarity, 274 pin-net checks
PCB manufacturing reject0JLCPCB DFM: 0 errors on routing/mask/silk/drill
PCBA assembly defect0AR warnings reduced 77 to 23 (VIA_MIN 0.15mm), 2 fiducials detected, CPL rotation variants for U5
Signal integrity1USB D+/D- mismatch reduced 4.57mm to 1.57mm via 3-loop meander. Under 2mm targetWithin USB 2.0 FS spec
Thermal0C26 bypass cap 3.6mm from U1 VDD (was 17.9mm). PAM8403 thermal vias added
Mechanical fit0All NPTH match datasheets, FPC/USB-C/SD verified
Total risk1/35

Confidence Score

Manufacturing confidence = (1 - risk/max_risk) × 100
= (1 - 1/35) × 100
= 97%
MetricValueAssessment
Automated test pass rate266/266 (100%)All checks green
JLCPCB DFM errors0Ready for order
JLCPCB routing warnings25 (was 81)-69% reduction
Risk score1/35 (was 4/35)Very low risk
Manufacturing confidence97%Excellent — ready for production order

Fixes Applied (v1 89% to v3 97%)

FixBeforeAfterImpact
USB D+/D- meander (3 loops, 0.50mm amplitude)4.57mm mismatch1.57mmSignal integrity 2 to 1
C26 ESP32 VDD bypass cap (100nF, 3.6mm from pin 2)17.9mm3.6mmThermal 1 to 0
VIA_MIN 0.46 to 0.50mm (AR 0.13 to 0.15mm)77 AR warnings23Assembly 1 to 0
Fiducial marks FID1/FID2 at diagonal corners2 warnings0Assembly accuracy
What 97% confidence means

Based on 266 automated checks (100% pass rate), 0 JLCPCB DFM errors, and a risk score of just 1/35, there is a 97% probability that the first PCB + PCBA batch will work correctly without rework. The only remaining risk (1/35) is:

  • USB D+/D- mismatch of 1.57mm — within USB 2.0 Full Speed spec (tolerance ~25mm at 12MHz), used only for firmware flash and debug console

This is a production-ready design.

What this score does not cover

Confidence in the design files is not confidence in the assembly. The v4.3.1 batch scored like this and still arrived dead, because the CPL carried systematically wrong rotations for the packages where 90° matters — a class no geometric gate looked at then. That is now covered by verify_cpl_rotation_law plus the pre-payment 3D-preview check; see the incident.

Remaining Optimization Opportunities

ItemCurrentIdealPriority
VIA_TIGHT annular ring0.175mm (23 JLCPCB warnings)0.20mm+Low — cosmetic, no reject risk
USB D+/D- mismatch1.57mmunder 1mmLow — already within spec

8. EMC / Signal-Integrity Gates

Four gates extend the suite from "does the copper connect" to "does it stay quiet at speed". They join the 95 gates already in the Makefile's VERIFY_ALL_SCRIPTS, bringing the suite to 99.

GateChecks
verify_crosstalkParallel runs between aggressor and victim nets against the 3W spacing rule, coupled-length weighted
verify_reference_planeSignals crossing a gap or void in the plane directly beneath them — the return current has nowhere to follow
verify_length_matchIntra-pair and intra-bus skew, scored against the edge rate the net actually carries
verify_via_discontinuityVia count and stub length on high-speed nets, where each transition reflects energy

Thresholds are derived, never hardcoded. Each gate computes its limit from the board it is looking at — the stackup's dielectric height and trace geometry, the net's signalling rate, the layer pair a via spans. A number typed into the script would go stale the first time the stackup or a net class changed, and would then pass a board it no longer describes.

WARN vs FAIL. These gates report degraded, not dead-board: a coupled edge or a skewed pair still arrives, it just arrives with less margin. So a finding outside the derived limit is a FAIL (blocking) only where the geometry is unambiguous — a signal over a plane void, a stub beyond the reflection budget. Findings that depend on an assumption the board cannot confirm — the real edge rate of a GPIO, a coupling length near the boundary — are WARN: reported with their evidence, non-blocking, and left for review. The rule is that a gate blocks only what it can prove.

For the full picture — all 99 gates by failure class, the EMC coverage grid, and how the network grew release by release — see the Production Test Map.


Running Verification

# Quick DFM check — 124 tests, ~1.4s, no Docker needed
make verify-fast

# Full pipeline — generate + DFM + DRC + gerbers + connectivity (~5s)
make fast-check

# GPIO firmware/schematic sync check
make firmware-sync-check

DRC Commands (JLCPCB Rules)

# Full DRC — zone fill + DRC + smart analysis (recommended)
python3 scripts/drc_native.py --run

# Fast DRC — skip zone fill for quick checks
python3 scripts/drc_native.py --run --no-zone-fill

# Update baseline — save current violations as reference
python3 scripts/drc_native.py --run --update-baseline

# Analyze existing DRC report
python3 scripts/drc_native.py /path/to/drc-report.json

The --run mode automatically:

  1. Fills zones via Docker (pcbnew API) unless --no-zone-fill is used
  2. Runs kicad-cli pcb drc with JLCPCB rules from .kicad_dru
  3. Categorizes violations into known-acceptable vs real issues
  4. Provides source file mapping and fix suggestions for real issues
  5. Tracks deltas vs saved baseline (if --update-baseline was used previously)

Full verification suite

make verify-all    # all 99 gates — DRC, DFM/DFA, ERC, simulation, EMC, consistency, release integrity

Or individually:

python3 scripts/verify_dfm_v2.py         # 124 DFM guard tests
python3 scripts/drc_native.py --run # JLCPCB design rules (smart analysis)
python3 scripts/erc_check.py --run # ERC (Electrical Rules Check) on schematics
python3 scripts/simulate_circuit.py # Power/timing simulation
python3 scripts/spice_power_check.py # SPICE power supply ripple/transient simulation
python3 scripts/verify_schematic_pcb.py # Schematic-PCB sync
python3 scripts/test_pcb_connectivity.py # Electrical connectivity
python3 scripts/analyze_pad_distances.py # Pad spacing analysis
python3 scripts/validate_jlcpcb.py # JLCPCB manufacturing rules (24 tests)

Automatically (Husky pre-commit hook)

All three checks run on every git commit. If any check fails (exit code != 0), the commit is blocked.

The hook is managed by Husky and installed at .husky/pre-commit. After cloning:

npm install

This runs husky via the prepare script and activates the hooks automatically.

You can also run the full battery manually:

npm run verify

Expected output

[verify] DRC Check ............ PASS (0 errors, 2 warnings)
[verify] Circuit Simulation ... PASS (0 errors, 5 warnings)
[verify] Schematic-PCB ........ PASS
[verify] All pre-production checks passed

DFM/DFA Test Suite — JLCPCB Compliance

Complete automated test suite that validates every dimensional constraint required by JLCPCB 4-layer SMT assembly. Run with make verify-all or individually before every release.

Test Summary (v3.7)

ScriptTestsPassDescription
verify_dfm_v2.py124124JLCPCB DFM manufacturing rules
verify_dfa.py99SMT assembly (paste, tombstoning, polarity)
validate_jlcpcb.py2424JLCPCB-specific (drill, copper island, gerbers)
verify_copper_clearance.pyall nets0 DANGERShapely polygon copper gap analysis
verify_polarity.py4848Pin-to-net assignment (274 pin checks)
verify_easyeda_footprint.pyall BOMFAIL on deltaCross-check every BOM footprint vs EasyEDA reference fetched via easyeda2kicad (catches pad-1 rotation/polarity bugs BEFORE JLCPCB assembly). Caches under scripts/.easyeda_cache/ per LCSC ID. See category table below.
verify_datasheet_nets.py267267Pad net vs datasheet specs (37 components)
verify_datasheet.py2929Physical footprint vs datasheet
verify_design_intent.py357357Cross-source GPIO/net consistency (T1-T22)
verify_trace_through_pad.py11Fab-short gate (trace over unnetted pad)
verify_trace_crossings.py11Same-layer different-net trace intersection
verify_net_connectivity.pyall netsPASSUnion-find copper graph per net
verify_bom_cpl_pcb.py1212BOM/CPL/PCB cross-reference
verify_bom_values.py8484Schematic vs BOM value match
verify_signal_chain_complete.py5656Signal chain endpoints (power, display, audio, SD, USB, buttons)
verify_component_connectivity.py22Phantom component detection
verify_net_class_widths.py55Trace width per net class enforcement
verify_netlist_diff.py44Schematic-to-PCB netlist cross-check
verify_schematic_pcb_sync.py3PASSR4 sync guard (ref coverage, collision, net coverage) — 53 schematic refs vs 85 CPL refs vs 37 datasheet_specs entries
verify_strapping_pins.py1211 + 1 warnESP32-S3 boot pin validation
verify_decoupling_adequacy.py2323Per-IC capacitance check vs datasheet
verify_power_sequence.py2929Power chain topology and ordering
verify_power_paths.py10 + 11 info10Copper path from source to IC VDD (info = reachable through zone fill)
verify_antenna_keepout.py55ESP32 WiFi/BLE antenna zone clearance
verify_stackup.py554-layer stackup net assignment
verify_usb_impedance.py44USB D+/D- differential pair geometry
verify_via_in_pad.py33Via-in-pad detection (different net)
verify_thermal_relief.py44Zone thermal relief settings
verify_ground_loops.py22Audio-digital ground coupling
verify_test_points.py1916+3Debug probe accessibility (blocking; bring-up signals included)
verify_esd_protection.py66USB TVS + series resistors
verify_usb_return_path.py33GND via density near USB traces
verify_sd_interface.py77SD card SPI completeness
verify_power_resonance.py44Power plane LC resonance
TOTAL~1200+Counts refreshed by running every script; do not hand-edit

verify_easyeda_footprint.py — Status Categories

Every BOM reference is classified into exactly one status. Only FAIL produces a non-zero exit code; everything else is exit-0.

StatusExit contributionMeaningWhere defined
OK0Any of: footprint matches the EasyEDA reference (δ_row = 0°); an existing _JLCPCB_ROT_OVERRIDES entry empirically compensates the delta; or the footprint is a pure rigid rotation of the reference with pin numbering preserved and its package family has an explicit correction entry (see "Drawing rotation vs pin permutation" below)._rigid_rotation_match()
ALLOW0Non-zero δ_row mismatch explicitly signed off with empirical evidence (prototype batch + observed behaviour). If δ_row drifts, entry auto-invalidates and the ref re-FAILs._GEOMETRIC_MISMATCH_ALLOWLIST in verify_easyeda_footprint.py
PENDING0Suspected polarity/rotation bug awaiting empirical validation on a specific named production batch. Entry locks in the expected δ_row and carries the test procedure inline. If δ_row drifts, FAIL "PENDING entry stale, re-verify". Printed in yellow._PENDING_VALIDATION in verify_easyeda_footprint.py
REVIEW0Footprint layout matches EasyEDA but an override is still set — typically a 3D-model polarity stripe that points the wrong way in the EasyEDA 3D model (C2 tantalum class). Keep override, document reason._JLCPCB_ROT_OVERRIDES
WARN0Could not verify — never "verified good". LCSC part not on EasyEDA; ref missing from PCB; or no pad-name correspondence exists between the two libraries, so the geometric check is undefined. Manual review required. Where a datasheet-derived translation exists, add it to _PAD_NAME_ALIASES instead of leaving the ref unverified._PAD_NAME_ALIASES
INFO0Non-polarized part (resistor, cap) with δ_row mismatch — no manufacturing impact.
FAIL1Polarized part whose pad constellation is not a rigid rotation of the reference (pin numbering permuted → a genuinely reversed part), with no override and no allowlist/pending entry.

Drawing rotation vs pin permutation

δ_row compares two land-pattern drawings, so a non-zero δ_row is not by itself a defect. It means one of two very different things:

  1. Benign drawing convention — the same pad field drawn at a different angle, pin numbering intact. EasyEDA draws SOT-23-3 with pads 1/2 in a column; the KiCad standard draws them in a row. No pin can land on the wrong net. The CPL angle here comes from the per-family constant in _JLCPCB_ROT_CORRECTIONS, because JLCPCB's 0° reference is the part's orientation in JLCPCB's own parts library (tape-and-reel), not in a footprint drawing. That is why the correction table is keyed by package family rather than per drawing.
  2. Real polarity bug — pad numbering permuted or mirrored relative to the package geometry, so physical pin 1 lands on the pad routed to pin 2 (C2 tantalum, LED2). No rotation repairs this.

_rigid_rotation_match() fits a rigid rotation across all pads with numbering held fixed to tell these apart. Tolerance is scale-aware (40 % of the tightest pad pitch); a genuine pin swap displaces a pad by a full pitch and can never pass.

Two-pad caveat: the test requires ≥ 3 pads. On a symmetric 2-pad part (0805 LED, 1206 tantalum) a 180° rotation and a pad-1/2 swap are geometrically indistinguishable, so such parts are never auto-cleared — their polarity is decided by silkscreen / 3D marker and they stay on the manual lists.

Pad-name alias maps

The comparison matches pads by name, which assumes a name denotes the same physical pin in both libraries. USB-C breaks that assumption: our footprint numbers the lands 1..14, while the EasyEDA reference names them by the receptacle contact(s) each land carries (A1B12, A5, B8…).

_PAD_NAME_ALIASES translates our names into EasyEDA's, keyed by LCSC part number — never by package family, since receptacles with a different land count merge their GND/VBUS pairs differently. Shield through-hole posts are deliberately excluded: both libraries reuse 13/14 for them, but neither guarantees the same left/right numbering.

With the C2765186 map applied, J1's 12 signal lands match the datasheet-derived reference at 0.0000 mm, so J1 reports OK on positive evidence rather than being left unverified. Adding a datasheet-derived alias map is always preferable to accepting a WARN.

Self-test: _self_test() runs on every invocation (pure arithmetic). It asserts the 90° SOT-23 case is recognised, a pad-1/2 swap is rejected, 2-pad parts are never cleared, and D1 is absent from the allowlist. It also guards the alias maps: keys must be LCSC part numbers, aliases must be injective, shield posts must stay excluded, and C2765186 must keep its pin-1 anchor (1→A1B12, 12→B1A12). A loosened tolerance or a corrupted alias fails the run loudly instead of silently comparing the wrong pins.

Separation of concerns — three independent dicts:

  1. _JLCPCB_ROT_OVERRIDES in scripts/generate_pcb/jlcpcb_export.py — compensation rotations applied to CPL file at export time.
  2. _GEOMETRIC_MISMATCH_ALLOWLIST in scripts/verify_easyeda_footprint.py — empirically validated native-frame mismatches (evidence collected).
  3. _PENDING_VALIDATION in scripts/verify_easyeda_footprint.py — suspected bugs awaiting empirical result from a named batch. Must shrink over time; growing = red flag.

Resolution flow for PENDING: once the referenced batch returns, promote the entry either to _JLCPCB_ROT_OVERRIDES (if the suspicion was confirmed and the CPL needs compensation) or to _GEOMETRIC_MISMATCH_ALLOWLIST (if the footprint turned out to be correct). Never leave entries in PENDING indefinitely.

JLCPCB Dimensional Rules — Reference Card

Trace Width

Net ClassJLCPCB Absolute MinOur Enforced MinTypical Routing Width
Signal (USB, LCD, buttons)0.09 mm0.15 mm0.20–0.25 mm
Power (+5V, +3V3)0.09 mm0.20 mm0.60 mm
Power High (VBUS, BAT+, LX)0.09 mm0.50 mm0.76 mm
GND0.09 mm0.20 mm0.20–0.60 mm

Trace/Copper Spacing

RuleJLCPCB Abs MinOur DANGEROur WARN
Trace-to-trace (different net)0.09 mm0.10 mm0.15 mm
Copper-to-copper (polygon check)0.09 mm0.10 mm0.15 mm
Pad-to-pad (different net)0.127 mm0.15 mm
Via-to-SMD pad0.127 mm0.15 mm
PTH-to-trace0.33 mm

Via Specifications

ParameterJLCPCB Abs MinOur Value
Drill diameter (4-layer)0.20 mm0.20 mm
Outer diameter (4-layer)0.45 mm0.46–0.60 mm
Annular ring (absolute)0.075 mm0.075 mm
Annular ring (JLCDFM standard)0.13 mm0.13 mm
Via-to-via hole gap (different net)0.25 mm0.25 mm
Via-in-pad (different net)forbiddenFAIL
Aspect ratio (drill:thickness)10:1 max~8:1

Drill / PTH / NPTH

ParameterJLCPCB MinOur Value
PTH drill min0.15 mm0.20 mm
PTH drill max6.3 mm6.3 mm
NPTH drill min0.50 mm0.50 mm
PTH annular ring0.15 mm0.15 mm
Drill bit increment0.05 mm0.05 mm
Drill-to-board-edge0.40 mm0.40 mm
PTH-to-PTH edge gap0.15 mm0.15 mm

Board Edge Clearances

ParameterJLCPCB MinOur Value
Copper/via-to-edge0.30 mm0.28 mm (arc tolerance)
SMD pad-to-edge0.30 mm0.30 mm
Trace-to-edge0.20 mm0.20 mm

Soldermask

ParameterJLCPCB MinOur Value
Mask bridge (dam width)0.075 mm (auto-remove)0.10 mm
Mask expansion (SMD pads)0.05 mm/side0.05 mm (0 on fine-pitch)
Negative mask expansionforbiddenFAIL

Silkscreen

ParameterJLCPCB MinOur Value
Stroke/line width0.15 mm0.15 mm
Text height1.00 mm1.00 mm
Silk-to-pad clearance0.15 mm0.15 mm
Silk-to-hole clearance0.50 mm0.50 mm

Assembly (PCBA)

ParameterJLCPCB MinOur Value
SMD-to-SMD component spacing0.30 mm0.30 mm
Solder paste aperture ratio≥ 0.66≥ 0.66 (stencil 0.12 mm)
Fiducial pad diameter1.00 mm1.00 mm
Edge.Cuts line width0.15 mm (recommended)0.15 mm
Gerber layers required7 minimum13 files

Threshold Hierarchy

               JLCPCB       KiCad DRU     Enforced      Routing
Parameter Abs Min (fab-accept) Min Default
─────────────────────────────────────────────────────────────
Trace width 0.09 mm 0.09 mm 0.15 mm 0.20–0.76 mm
Trace space 0.09 mm 0.09 mm 0.10 FAIL —
0.15 WARN
Via drill 0.20 mm 0.20 mm 0.20 mm 0.20 mm
Via OD 0.45 mm 0.45 mm 0.46 min 0.60 std
Via AR 0.075 mm — 0.075 abs 0.20 std
PTH AR 0.15 mm 0.25 (DRU) 0.15 mm ≥ 0.30 mm
Pad-to-edge 0.30 mm 0.30 mm 0.30 mm ≥ 0.30 mm
Mask bridge 0.075 mm — 0.10 mm 0.10 mm+
Silk stroke 0.15 mm 0.15 mm 0.15 mm 0.15 mm
Silk height 1.00 mm 1.00 mm 1.00 mm 1.00 mm

Running the Full Suite

# Quick DFM check (~1.4s)
make verify-fast

# Full verification suite (~5s)
make verify-all

# Individual checks
python3 scripts/verify_dfm_v2.py # 124 DFM tests
python3 scripts/verify_dfa.py # 9 DFA assembly tests
python3 scripts/validate_jlcpcb.py # 24 JLCPCB-specific tests
python3 scripts/verify_copper_clearance.py # Shapely polygon gap check
python3 scripts/verify_polarity.py # 48 pin-to-net tests

# Release pipeline (all checks + gerbers + renders)
make release-prep

Performance Notes

The verification pipeline uses a hybrid local + Docker approach for speed:

ToolRuns viaTime
DFM tests (verify_dfm_v2.py)Python (local)1.4s
KiCad DRCkicad-cli (local)0.8s
Gerber exportkicad-cli (local)0.9s
Zone fillDocker (pcbnew API)1.8s
ConnectivityPython (local)0.15s

Container runtime: OrbStack (drop-in Docker Desktop replacement, 16x faster container startup).

Accepted JLCDFM findings (reference for report diffing)

Every JLCDFM upload produces the same set of Warnings (and one 3D-artifact Danger) that are known, reasoned and accepted. When a new report arrives, diff it against this table: anything NOT listed here is a real finding; anything listed needs no action. Baseline: the 2026-08-08 report on the v4.6.x set, re-validated against the 2026-08-12 report on v4.6.1 (gerbers md5 26b62cd8) — that report was a full-table match, zero real findings.

JLCDFM findingCountWhy it is accepted
Lead to hole distance 0 mm (J1 pegs)1 DangerOur NPTH is the datasheet's Ø0.65 ("Ø0.65(2X)", pegs Ø0.50 → 0.075/side); the EasyEDA reference uses 0.70 and JLC's 3D peg model touches a 0.65 hole. R21 rule: datasheet over EasyEDA (see footprints.py and the C2 tantalum lesson). Pegs are plastic; no solder joint involved.
Pin inner edge 0.03–0.08 mm50 DangerJLC's own 3D lead models poke past the pad inner edges on the fine-pitch connectors (J4/U6 class). Our lands are rigid-fit-proven equal to JLC's own reference footprints (verify_easyeda_footprint, 97 OK) — their 3D disagrees with their 2D by a tolerance-level amount. The measured value drifts between report runs (0.03 on 2026-08-08, 0.08 on 2026-08-12 — their 3D model revisions move); the identity check is class + count (50) plus verify_pad_land (361 pads ≥ 0.80 land coverage) and verify_easyeda_footprint green — if either gate is red, it is NOT this artifact.
Annular ring 0.13 mm~100 WarningThe 0.46/0.20 via family: (0.46−0.20)/2 = 0.13 = JLCPCB's published minimum, used deliberately where corridors are tight.
Fiducial null3 WarningNo fiducials by design — JLC panelizes and adds their own rail fiducials for this board size.
Slot width 0.65 mm4 WarningJ1 shield slots: R20 chose 0.65 (min millable 0.61; datasheet says 0.60 which JLC cannot mill).
Silkscreen to pad 0.15 mm4 WarningAt the 0.15 mm gate floor (verify_dfm_v2 silk-to-pad test).
Component clipped by outline3 WarningJ1 / U6 / SW16 are edge-mount by design (declared in verify_component_bodies.EDGE_MOUNT); the mouths must protrude.
Component spacing 0.28 mm1 WarningU2/C27 decoupling pair — HF bypass must be tight to the pin; 0.43 mm in our 2D model, JLC's 3D shaves it to 0.28. Not present in the 2026-08-12 report (global min measured 0.53 mm, Good) — keep the row in case their 3D flips back.
Lead area overlapping pad 0.02 mm~45 WarningSame 3D-vs-2D tolerance class as "pin inner edge", on the castellated/fine-pitch parts. The 2 Dangers this class carried on 2026-08-08 are gone in the 2026-08-12 report (45 Warning / 0 Danger).
Pad spacing 0.15 mm2 WarningSame-net service vias sitting at the deliberate 0.145–0.146 mm floor beside their own component pads (landing-stub pattern from the R32 slit fixes; the U5.6 +5V / R1.1 USB_CC1 / SW6.4 GND family). Above JLCPCB's 0.127 mm minimum and above verify_copper_clearance SLIVER_MAX — electrically one net, joined through the stub. First itemized on the 2026-08-12 report.

The four Dangers the 2026-08-08 report found that were real — pad spacing 0.08 (F1 via-ring sliver), component collision 0 mm (J3/F1 housings) — were fixed at source (F1 rotated vertical, via cluster re-laid) and each class is now gated locally: verify_copper_clearance category 2b (same-net slit, morphological closing) and verify_component_bodies connector-3D margin (+0.30 mm per connector housing).