← servodrive

Specification

Draft 0.6

A round, motor-mounted servodrive: a Ø65 mm two-board stack with the magnetic rotary encoder on the shaft axis. 60 V bus on 80 V silicon, three phases, RP2350A. Derived from rp2350-motor-controller, which supplies the encoder circuit, the power-stage part choices, and — more usefully — a review register of everything that went wrong there.

2026-09-07, revised 2026-09-22 · every number below is computed by tools/geometry.py and the floorplan by tools/placement.py · LCSC stock figures checked 2026-09-07 and perishable

1 · What it is

Two identical-diameter round boards stacked on the back of a motor.

Board A — motor board

Bolts to the motor through the encoder's four-hole pattern.

  • MT6701 magnetic rotary encoder on the shaft axis, on the motor-facing side
  • Three half-bridges — 6 × 80 V MOSFET, 3 × EG2103 gate driver
  • Two inline phase-current shunts and their amplifiers
  • RP2350A, crystal, QSPI flash, 3.3 V LDO
  • High-frequency DC-link ceramics, at the bridge
  • Three phase pads at the perimeter, on the motor-facing side — the leads come off the motor on that side, and the outward perimeter is spoken for

Board B — power / IO board

Mounts on six perimeter standoffs above board A.

  • Bus input on an XT30, 12–60 V — or USB-PD instead of it, never both (§7); TVS, bulk capacitance
  • VMOT → 12 V gate rail and VMOT → 5 V logic rail
  • USB-C with full PD — a 20 V contract feeds VMOT and runs the motor
  • Full-duplex RS-485, two ports, chained through
  • Status LED, debug header, BOOTSEL

The split is drawn where it costs least. Everything with a fast di/dt loop — gate loops and commutation loops both — stays on board A. What crosses the interconnect is a DC bus, three regulated rails and a handful of slow signals.

What the split costs — measured, not assumed. Draft 0.2 asserted that with ≈13 µF of ceramic on board A the interconnect would see average bus current rather than ripple, and asked for a loop-inductance estimate before layout. Both halves have now been done, in geometry.commutation_loop() and geometry.interconnect(), and they do not agree with each other.
  • The loop is fine. With the In1 ground plane 0.1 mm below F.Cu the PCB contributes 0.39 nH; two PowerPAK packages and the 1210 ESL bring it to 2.0 nH. At 0.87 A/ns that is 1.7 V of overshoot — a 61.7 V peak on 80 V silicon. The ceramics do their job and the TVS never conducts in normal operation.
  • The interconnect is not. At 20 A RMS per phase the DC link carries 21.2 A of bus current plus 12.4 A RMS of ripple, and at 20 kHz the ceramics on board A are a 0.61 Ω path against the bulk's 0.2 Ω, so three quarters of that ripple crosses the header anyway: 23.2 A RMS. A 2×8 at 3 A/pin is 24 A. Eight hundred milliamps is not a continuous rating, so the power link is two 2×5 and 30 A — see §8.
The ceramic budget itself stands: it is the megahertz content that matters locally, and above about 600 kHz the ceramics own it.

2 · Mechanical

Plan view of board A: 65 mm circle, four M3 motor mount holes at 25 mm and 19 mm spacings around a central MT6701, six M2.5 perimeter holes on a 59 mm bolt circle, three phase cells at 120 degrees interleaved with three utility wedges.
Board A, drawn from the placement itself rather than from an intention: tools/geometry.py reads the same coordinates gen_boards.py writes into the KiCad file. Solid courtyards are on the outward face, dashed ones on the motor-facing face. The three phase cells sit at 60°/180°/300° so all three commutation loops are the same shape; the three wedges between them carry the interconnect and the CPU; the gold arcs at the perimeter are bare copper for the heatsink ring, and are the reason every part stops at R 29.2. Nothing but the encoder and the motor screws lives inside Ø32.
Geometry — single source of truth is tools/geometry.py
ItemValueWhere it comes from
Board diameter, both boardsØ65.0 mmSet 2026-09-07
Motor mount4 × Ø3.2 (M3) at (±12.5, 0) and (0, ±9.5) Inherited verbatim from hardware/encoder H1–H4
Encoder centre(0, 0) — coincides with the shaft axisMT6701 sensing centre
Central keepoutR < 16 mm, both facesScrew heads + magnet clearance
SectionsThree phase cells of 60° at 30/90/150°, adjacent, then three 60° wedges at 210/270/330° Grouped rather than alternating, and narrowed — see below
Board-B mounting6 × M2.5 on a Ø59 bolt circle, on the six section boundaries: 0, 60, 120, 180, 240, 300° Every section gets one at each edge — and at 60/60 that is an even circle again
Phase pads3 arcs, R 28.1–31.3 over 14°, 23 mm² each, at 30°/90°/150°, on B.Cu14 AWG motor leads. An arc, because a straight pad this wide at this radius puts its corners off the board
Heatsink land3 arcs, R 29.2–31.5 over 48°, plus six Ø4.8 mm bosses = 250 mm² of bare copper§4 — the joint a heatsink would clamp, 0.33 K/W; kept although no fins are planned
Parts placed, board A124 by the floorplan — 69 outward, 55 motor-facing, 10 DNP — and 12 generated (lands, lead pads, bosses)tools/placement.py
Netlist, board A136 components, 87 nets, 425 connectionstools/schematic.py, checked against the placement both ways
Usable annulusR 16 → 31.5 = 2313 mm² per sideComputed
Board A stackup6 layer, 2 oz outer / 1 oz inner, 1.6 mm, 0.1 mm F.Cu→In1 prepregSame as the sister board, except the prepreg, which is what §1's 0.39 nH rests on
Board B stackup4 layer, 2 oz outer, 1.6 mmNo dense routing; 4 is enough
Standoff, motor face → board A10.75 mm Derived: 5.0 shaft + 2.5 magnet + 1.5 air gap + 1.75 MT6701 body
Standoff, board A → board B11.0 mmBoard A parts + mated interconnect height
Total above the motor rear face36.0 mmConnectors and bulk cans included
The three phase cells are adjacent, and the three wedges with them. Draft 0.3 alternated them — phase, wedge, phase, wedge — which put the RP2350A with a half-bridge 25° away on each side. Grouping costs nothing inside a cell (the width is unchanged at 70°) and buys three things:
  • The CPU moves to the middle of the wedge group, as far from every switching node as a round board allows.
  • The three motor leads leave together, so one bundle leaves the enclosure through one hole instead of three.
  • The DC bus feeds one contiguous 210° block from the power link at its edge, rather than reaching around the board past the CPU.

The six M2.5 sit on the six section boundaries, so every section has a bolt at each edge and every phase cell's heatsink land runs the full width between its own two. At 70/50 that cost the even spacing; at 60/60 it does not, and the bolt circle is regular again.

And the cells came in from 70° to 60°, using the channel interior to the screws. A phase cell is short of angle, not of area, and angle costs radius: a millimetre of arc at R 20 is 2.9°, the same millimetre at R 29.5 is 1.9°. The cell was 70° because small parts sat at small radius — a 1 µF 0805 beside the gate driver was taking 30° on its own, while both MOSFETs together needed 18.

The six boss pads sit on R 26.9–32.2, and between them that band is empty: the heatsink land owns it on the outward face, and on the motor-facing face nothing but the lead pads wants it. So everything without a reason to be somewhere in particular now lives there at R 29.3 — the bootstrap diode (only its capacitor has to be at VB and VS), the DC-link 100 n, both input pulls, and each phase's clamp diode, which ends up beside its own lead pad. The DC-link bulk ceramic moved to its own row at R 26.8, as far out as the land allows.

That held for the utility wedges and not for the phase cells. Routing found that a part in a phase cell's channel can be connected only if its nets are GND or the phase output — the phase-output pour owns B.Cu beneath it, the heatsink land owns F.Cu above it, and the bosses cut the outer ring into six arcs. Five of the six went back into the cell and the cells were widened to 68° to take them. The clamp diode stayed, because its nets are exactly the two that work. §10b has the measurement.

70° cellsall in the channelnow
Phase cell70°60°68°
Utility wedge50°60°52°
Phase group210°180°204°
Bolt spacing70/70/70/50/50/50°60°, even68/68/68/52/52/52°
In the channel, per cell—6 parts1, the clamp diode
Heatsink land286 mm²250 mm²279 mm²

Three consolidations paid for it: the per-driver 1 µF VCC bulk became one 4.7 µF at the power link (three of them on one 0.1 A rail was two too many), the lead pad narrowed from ±8.5° to ±7° — 23 mm², still ample for a 14 AWG lead — and the row packer learned to dodge parts in other rows, because two rows that overlap radially and sit at different angles can collide and a packer that only looks at its own row walks straight into them.

Area is not the constraint. The sister board fits a four-bridge stage, a twelve-pin switched encoder port, isolated CAN and USB-PD into roughly 4400 mm² of courtyard across two sides. This design has four usable sides of 2313 mm² and far less to put on them. That buys the right to keep thermally generous packages instead of chasing the shrinks in the sister project's F-47 — take the small parts where they are also better parts, not merely smaller ones.

Side view of the stack: motor rear face, encoder air gap and standoff, board A, interconnect gap, board B, connectors.
Stack elevation. The standoff under board A is not a free choice — it is whatever the shaft, the magnet holder, the MT6701's working air gap and the SO-8 body add up to, and it lands at 10.75 mm on the placeholder numbers. Change the shaft height in tools/geometry.py once the motor is known and every dimension here follows.

3 · Power stage

Electrical design point
Bus voltage60 V nominal, 12 V minimum (decided 2026-09-22), 80 V silicon throughout, TVS breakdown 71–79 V
Phase current20 A RMS per phase — the figure everything is sized to, and since 2026-09-22 a burst figure rather than a continuous rating (§4) — 28.3 A peak, ±32 A of sense. Draft 0.2 used “20 A” for the RMS figure in §4 and for an instantaneous one in §5; they differ by √2 and the sense chain lost. One number now.
Switching frequency20 kHz nominal (the number every estimate below is quoted at)
TopologyThree independent half-bridges, software dead time, inline phase-current sense
Parts, and why each one differs from the sister board
FunctionPartRationale
MOSFET, 6 ×BSC030N08NS5, PowerPAK SO-8 Unchanged. 80 V, 2.6/3.4 mΩ at VGS = 10 V, 100% avalanche tested, 27k in stock at LCSC. Driven properly this time — see the row below.
Gate driver, 3 ×EG2103, SOP-8 Changed. The sister board's EG3113 is unbuyable — 9 in stock, minimum purchase 25 (their F-40, which blocks its ordering). The obvious like-for-like replacement, EG2131, turns out to be also out of stock. EG2103 is the same EG Micro family and the same logic convention, 2,225 in stock, MOQ 5, $0.19 @100: 600 V, VCC 10–20 V, interlock plus 560 ns internal dead time, HIN active-high with a 200 k pull-down, LIN active-low with a 200 k pull-up. It needs the 12 V rail, and that is the point. What it gives up is drive strength — ±0.3/0.6 A against EG3113's 2/2.5 A — costed in §4.
Gate rail12 V, its own buck on board B Changed. This is the single largest improvement available and it is quantified in the sister project's F-41: driving a 10 V-rated FET from a 5.04 V rail costs ~2 W per bridge of switching loss and ~15% on RDS(on). See §4.
Gate network2.2 Ω + 10 k G–S, PNP booster DNP Changed, because the driver is now the limit rather than the resistor. EG2103 saturates at 0.3 A, and reaching that from a 12 V rail needs Rg ≈ 28 Ω — so below about 28 Ω the resistor buys no speed at all and only adds turn-off delay against the 0.6 A sink. 2.2 Ω is therefore for gate-loop damping, not for edge shaping. Footprints kept for the sister board's PNP turn-off booster (their Q1 + D6/D7), which is the proven way to buy the turn-off edge back if §4's switching term proves binding on the bench.
Current sense, 2 ×INA241A3, TSOT-23-8, 50 V/V Changed. Higher gain lets the shunt shrink. Inline, not low side, so torque is measurable at standstill and at any duty cycle. The package is TSOT-23-8 (the DDF suffix), not the SOT-23-8 draft 0.2 wrote; that is what is placed on board A. A third footprint sits on phase C, DNP — there is no fourth ADC channel for it. And the grade is A3. INA240 numbers its gains A1 = 20, A2 = 50; INA241 numbers them A1 = 10, A2 = 20, A3 = 50. Draft 0.2 carried the INA240 mapping across and asked for a 20 V/V part to do a 50 V/V job — table 4-1 of the INA241A datasheet.
Shunts, 2 ×1.0 mΩ (2 × 2 mΩ 2010) Changed again, and this one is a correction rather than an improvement. Draft 0.2's 1.6 mΩ was chosen to reproduce the sister board's 80 mV/A so firmware constants would carry across. It gives ±20.6 A of full scale, and the design point peaks at 28.3 A: the chain would have clipped on every cycle above 14.6 A RMS. 1.0 mΩ × 50 V/V = 50 mV/A → ±29.0 A worst case, 2.5% over the peak, and costs 0.80 W instead of 1.28 W — the 0.8 mΩ pair this row carried until 2026-09-24 has no stocked 1.6 mΩ 2010 to build it from. Phase C gets the same two footprints as 0 Ω links so all three phases match. See §5.
Phase TVS, 3 ×TPSMF4L64A, SOD-123FL Same 64 V standoff / 400 W as SMAJ64A (their F-09 fix) in a much smaller package — one of the F-47 swaps that is worth taking.
DC-link ceramic6 × 2.2 µF 100 V 1206 + 6 × 100 n New requirement created by putting the bulk on board B. Two of each per cell, flanking the FET pair tangentially inside the commutation loop. §1 now has the measurement that justifies them: 0.39 nH of PCB loop, 1.7 V of overshoot.
Input pulls, 6 ×4.7 k on every HIN and LIN New, and not optional. Two errata meet on this pin. EG2103's LIN is active-low with an internal 200 k pull-up; RP2350's GPIOs leave reset as inputs with a ~60 k pull-down, and erratum E9 can latch a Bank 0 input near 2.1–2.2 V. The divider of those two lands LIN around 1.2 V — above VIL 1.0 and below VIH 2.5, which is the one place a gate driver input must never sit. 4.7 k to GND on both HIN and LIN makes the reset state a property of this board instead of two other people's errata, and ≤9 k is also the documented E9 workaround. To GND on both is the brake-on-reset decision of 2026-09-22 (the box below); until then LIN went to +3V3, for a coast. They sit on the driver's logic side, one via each.
RC snubber, 3 ×2.2 Ω + 470 pF C0G 100 V, 0603, DNP New, footprints only. The simulation's answer to the one corner that fails: if the EG2103's output stage is the strong one, the turn-on ring reaches 73.9 V of Vds; this across the switch node takes it to 62.7 V for 0.034 W per cell (sim P2, Q3). Fitted only if the driver measures strong. On the back under the high-side FET, the resistor's pad on the switch-node pour and the capacitor's on a ground via, their middle node a pour of its own. 0603 rather than the 0805 the simulation proposed, because an 0805 pair does not fit between the sense tap and the shunt; the ratings do not need 0805.

Every changed row above is a sourcing risk until checked. The sister project's F-40 is precisely the failure of not doing this: a design that cannot be ordered because one part has nine in stock. Before any of these reach a schematic symbol, confirm LCSC stock depth and minimum buy for INA241A2 — F-47 verified the A1 in SOT-23-8, not the 50 V/V grade, which is INA241A2IDDFR in TSOT-23-8; INA240A2PWR (C129949) is in stock as a fallback but is TSSOP-8 and only ±80 V common mode against INA241's ±110 V, so it is a different footprint and less margin on a 60 V phase node — 2.0 mΩ 2010 shunts at 1% or better, TPSMF4L64A, LMR38010, and the ripple-rated bulk §8 now needs. F-47 already notes that ISO1044 "is absent from the JLC in-stock listing today" despite being the recommended swap; assume nothing.

Checked 2026-09-07, and the results are why this box exists: EG2103 C480654 — 2,225, MOQ 5, $0.19, but only 0.3/0.6 A drive; FD2103S C5187182 — out of stock, and it was the cheapest hit; EG2131 C193777 — not available; UCC27282DR C2867844 — 1,227, $1.99; NCP5183DR2G C904511 — 2,883, MOQ 1, $0.92, but 4.3 A and no interlock. EG2103 chosen: the interlock and the 560 ns dead time are worth more here than the edge rate. Three of the five first-choice parts were unbuyable or overpriced. The 40 V-class question does not arise here, but no confirmed high-stock 80 V alternative to BSC030N08NS5 was sought — it has 27k in stock and stays.

Fail-safe by construction — the die coasts, and the board now brakes. Draft 0.2 left this open because the sources disagreed. EG2103's own datasheet (V1.0, 2018‑11‑11) settles it, and the interesting part is that it contradicts itself.
  • Its truth table is unambiguous: HIN = LIN = 0 turns the low side on, HIN = LIN = 1 turns the high side on, and any disagreement turns both off. LIN is active-low, as §6 assumed.
  • Its block diagram draws 200 k from an internal +5 V into LIN, and 200 k from HIN to ground.
  • Its parameter table agrees: Iin(L) is specified as −10 µA at Vin = 0 V. Current flowing out of an input held low is a pull-up, and it is a guaranteed number rather than a drawing. Quiescent current is also specified with the inputs floating, which is not how you characterise a part whose floating state turns a MOSFET on.
  • Its prose, in §2, says LIN has a 200 k pull-down — and then finishes the same sentence by claiming floating inputs turn both MOSFETs off, which is only true if it is a pull-up. It is a copy-paste of the HIN clause. EG Micro's website has the corrected wording; LCSC's summary repeats the typo.
Verdict: LIN is pulled up, floating inputs coast, and pulling RUN low is a real hardware stop. Three independent statements in the manufacturer's own document against one self-refuting sentence.

That is the die. The board still needs the six 4.7 k pulls in the parts table, for a different reason: RP2350's reset-state pull-down fights EG2103's 200 k pull-up and parks LIN in the undefined band. The datasheet answer and the erratum answer are not the same answer.

Decided 2026-09-22: a reset brakes. Both pulls go to GND, and HIN = LIN = 0 is the truth table's low-side-on state, so while the CPU is in reset — RUN low, a watchdog, a brown-out, the first milliseconds of boot — every low side conducts and the motor is shorted through its own windings. 4.7 k against the 200 k internal pull-up holds LIN at 0.11 V. Two things follow that are easy to miss. The brake lasts only as long as the 12 V gate rail: GATE_EN comes from a GPIO, which is an input with a pull-down in reset, so board B has to hold the gate rail on by default — a pull-up on the buck's EN that the CPU can pull down — or a reset becomes a brief brake and then a coast. And a brake is not a stop on a joint that is still being driven: it turns the load's energy into winding current, which is also why it does not pump the bus the way a regenerating decel does (question 8).

Two more numbers the datasheet gave up, both of which shape the firmware. Turn-on delay is 780 ns and turn-off delay 220 ns, on both channels. The 560 ns dead time is not a separate circuit bolted on — it is that asymmetry, which is why it holds at 560 ns worst case as well as typical. The consequence is a minimum pulse width: an input pulse shorter than about 560 ns produces no output at all, so at 20 kHz the bottom and top 1.1% of the duty range are unreachable. And VIH is 2.5 V minimum, which a 3.3 V GPIO clears by 0.8 V — enough, but not enough to spend on a series resistor. Nothing goes in the HIN/LIN path.
What the interlock and the 560 ns dead time buy. EG2103 cannot turn both FETs on, whatever the inputs do — the internal lockout forbids it and the dead-time circuit separates the edges. That removes a whole class of firmware bug from a board whose CPU is also running the control loop, and it means the software dead time the sister project's firmware inserts can go to roughly zero, giving back the duty-cycle range it currently spends. At 20 kHz, 560 ns is 1.1% of the period.
What it costs: edge rate, and therefore about 2 W. ±0.3/0.6 A against EG3113's 2/2.5 A. §4 puts the switching term at roughly 1.05 W per bridge instead of 0.3 W — still better than the sister board's as-built 2.0 W, because the 12 V rail roughly halves its 95 ns edges to about 50 ns, but not as good as a strong driver would give. If the bench says that term is binding, the escape is the PNP turn-off booster in the parts table, not a different resistor.

4 · Thermal — what 20 A costs

Decided 2026-09-22: no fins, and 20 A is not a continuous rating. The drive will normally run well below its peak current, and what it can do in bursts will be found on the prototype rather than designed for here. Everything below stays as the arithmetic for that: it is what 20 A RMS continuous would cost, so it is also the scale for how long a burst can last. The board half of the thermal path stays as drawn — the ring land, the plated bosses, the via arrays — because it costs nothing and keeps the option of a heatsink open.

Board A is a sealed sandwich: its inner face looks at the motor, its outer face looks at board B. Neither sees moving air. Everything else in this spec is comfortable; this is not, and committing to 20 A RMS continuous is a decision about the enclosure as much as about the board.

Board A dissipation, computed by geometry.losses() from datasheet maxima — RDS(on) 3.0 mΩ max × 1.58 hot, Qgd 19.5 nC max, Qrr 188 nC max, driver 0.3/0.6 A. The sister board's column is its own figure, kept for comparison.
TermSister board
5 V gate rail, 4 mΩ × 3
This spec
12 V gate rail, 1.0 mΩ × 2
At 20 A RMS per phase — the design point
Conduction, 3 bridges6.6 W5.69 W
Switching, 3 bridges6.0 W3.84 W
Shunts4.8 W (3 phases)0.80 W (2 phases)
Gate charge + driver quiescent0.7 W0.14 W
RP2350A, LDO drop, MT6701, amplifiers0.26 W
Total18.1 W10.72 W
At 10 A RMS per phase
Total6.6 W4.28 W

The switching row is where the driver choice lands. EG2103 sources 0.3 A and sinks 0.6 A, so against a 19.5 nC Qgd the edges are 65 ns on and 32 ns off and no gate resistor below about 28 Ω changes that. Add the complementary body diode's 188 nC of reverse recovery at 60 V and the three bridges come to 3.84 W. A 4 A driver would save roughly 2 W across the board; that 2 W is the price of the hardware interlock and the 560 ns dead time, and it is a deliberate purchase.

The path out, in four resistances

Junction to ambient at the design point, from geometry.thermal(). The first two terms are per-FET at 1.59 W each; the last two carry all 10.72 W.
StepK/WΔTWhat sets it
Junction → drain pad0.91.4 KRthJC max, PG-TDSON-8
Drain pad → inner copper7.612.1 K 16 vias of 0.4 mm in the drain pad, 0.95 mm pitch — the low side; the high side has 20. The FR4 alone would be 354 K/W
Phase cells → perimeter bosses1.010.7 KSpreading in six layers of copper
Bare copper → heatsink0.44.3 K286 mm² of mask-free land through a 0.25 mm gap pad
Heatsink → ambient≤ 6.67≤ 72 K Everything left over, for Tj ≤ 125 °C at 25 °C ambient
A bare stack does not do this, and it is not close. At 15 K/W the board would sit at 201 °C and the junctions at 214 °C. 20 A RMS continuous is not a rating this stack can have on its own; it is a rating the enclosure grants it. The requirement is ≤ 6.67 K/W from the heatsink to ambient, which at 15 W/m²K of natural convection plus radiation means ≥ 100 cm² of external surface. A plain Ø65 × 35 mm aluminium barrel is 71 cm² and falls short; the same barrel with sixteen 6 mm longitudinal fins is about 138 cm² and lands near 4.8 K/W — board 92 °C, junctions 105 °C. That is the design.

So §4 of draft 0.2 asked which of four options to take, and the answer is option 2, taken seriously. The machined aluminium ring is not an optional upgrade that buys headroom — it is a structural member of the thermal design, and the board is now laid out around it:

Options 3 and 4 from draft 0.2 stay declined for the reasons given there: the motor housing is the hotter body in the case this drive exists for, and a finned lid on board B is on the wrong side of an 11 mm air gap. The ring is the only path that leaves board A's copper through metal.

What is still an estimate. The 15 K/W bare-stack figure, the 15 W/m²K convection coefficient and the 1.0 K/W of in-board spreading are all derived, not measured. The via array, the land area and the loss budget are arithmetic on datasheet numbers and placed geometry, and those I would defend. Treat the heatsink requirement as the right order of magnitude and measure it before believing the last 20%.

5 · Sensing

The RP2350A has four ADC inputs, on GPIO26–29. That is the whole analog budget, and it is spent as follows.

ADC allocation
ChannelSignalChain
ADC0 / GPIO26Phase A current1.0 mΩ inline → INA241A3 ×50, 1.65 V referenced → ±29.0 A
ADC1 / GPIO27Phase B currentas above
ADC2 / GPIO28Bus voltage50 k / 5.1 k 0.1% divider on board A, where VBUS already is
ADC3 / GPIO29FET temperature10 k NTC in the phase-cell copper pour
ADC4 (internal)Die temperaturefree, and a usable ambient proxy

Phase C current is reconstructed as Ic = -(Ia + Ib). What that costs is worth stating plainly: no per-phase fault detection, no redundancy, and any offset error in A and B lands doubled on C. For FOC on a wye-connected motor it is standard and sufficient.

A second NTC footprint sits by the motor mount pattern, DNP, with a 0 Ω jumper selecting which thermistor reaches ADC3 — so motor-housing temperature is a build option rather than a respin.

The shunt changed, and the reason is a bug draft 0.2 shipped. That draft set 1.6 mΩ so that 1.6 mΩ × 50 V/V would give exactly the sister board's 80 mV/A and its firmware constants would carry across unchanged. The full scale that buys is ±20.6 A instantaneous. The thermal budget in §4 was written at 20 A RMS, whose peak is 28.3 A. The chain would have clipped on every cycle above 14.6 A RMS, and a current loop cannot be closed on a clipped measurement.

It took two goes. The first correction was 1.0 mΩ for ±32 A, sized against the amplifier's typical output swing. The datasheet's worst case is VS − 0.2 V rather than 0.07 V, and that turns ±32 A into ±29 A — 2.5% over a 28.3 A peak, which is not margin.

Then: 0.8 mΩ (2 × 1.6 mΩ 2010) × 50 V/V = 40 mV/A → ±36.2 A worst case, 28% clear of the peak, and a precision mode (one of each pair desoldered) at exactly the sister board's 80 mV/A.

Now, since 2026-09-24: 1.0 mΩ (2 × 2 mΩ 2010) × 50 V/V = 50 mV/A → ±29.0 A worst case, at 0.80 W. Sourcing found no 1.6 mΩ 2010 at LCSC or JLCPCB — 1 and 2 mΩ, nothing between — so the chain is back at the 2.5% the first correction was turned down for, taken knowingly; the stocked alternative, 2 × 1 mΩ, reads ±58 A at 25 mV/A. The precision mode is now 2 mΩ, 100 mV/A, ±14.5 A, and neither mode is the sister board's scaling. Keep the table in the README and the firmware define pointed at each other; F-14 is what happens otherwise.

Where the sense chain physically is. Both shunts of a pair sit on the motor-facing face at R 25.5, one either side of the cell axis, and the amplifier sits under the gate driver at R 19.4. That radius is not free: each FET drain pad carries sixteen through-board thermal vias which reach R 27.6 on the back, the low-side cluster is the switch node's crossing to that face, and the high-side cluster is VBUS and must not be touched. The shunts are placed tangentially clear of both, and of the perimeter boss pads. tools/placement.py checks all of it, including the cross-layer case a same-side courtyard check cannot see.

6 · Control — RP2350A

Thirty GPIO, of which this design uses twenty-one. QSPI flash, USB, SWD and the crystal all sit on dedicated pins and cost nothing from that budget.

Proposed pin map
GPIONetNote
0, 1RS485_DI, RS485_ROUART0 TX/RX, via the interconnect to board B
2RS485_DETransmit driver enable; firmware- or PIO-timed
3RS485_TERM_ENOptional switched termination — see §7
4, 5, 6ENC_SCK, ENC_CS, ENC_DOSPI0 to the MT6701 in SSI mode. No MOSI: the sensor only talks.
7ENC_OUTMT6701 OUT pin — ABZ or PWM alternate, spare otherwise
8, 9HIN_A, LIN_APWM slice 4, channels A and B
10, 11HIN_B, LIN_BPWM slice 5
12, 13HIN_C, LIN_CPWM slice 6
14GATE_ENThe 12 V rail on board B. Defaults on through a reset, so that a reset brakes (§3); firmware pulls it low to kill the gate drive
15FAULT_nOvercurrent comparator, in — populate/DNP option
16, 17, 18LED_R, LED_G, LED_BPlain RGB LED on board B
19PD_INT_nFUSB302 interrupt
20, 21PD_SDA, PD_SCLI2C0 to the FUSB302 on board B. This was SDA on 19 and SCL on 20, which are I2C1's SCL and I2C0's SDA — not a hardware I²C pair. Fixed 2026-09-22 by rotating the three names on the routed copper; J9 pins 9, 10, 12 are now INT, SDA, SCL. 20–23 are also SPI0 RX, CSn, SCK, TX
22USB_VBUS_DETWas spare. Board B sends it, and nothing on board A was listening
23–25spareThree free, on test points TP1–TP3
26–29ADC0–3§5

Two details in that table are doing real work.

Each half-bridge sits on one PWM slice, and both its inputs carry the same waveform. GPIO 8/9, 10/11 and 12/13 are the A and B channels of slices 4, 5 and 6. Because EG2103's LIN is active-low, the driver does the inversion internally: its truth table has HIN = LIN = 0 turning the low side on and HIN = LIN = 1 turning the high side on. So PWM_CH_CSR.B_INV stays clear and both channels run the same duty. Dead time comes from the driver's own 560 ns, not from offsetting the compare values — and §3 now has the datasheet's truth table confirming that reading. Two consequences for the loop: the driver adds 780 ns of turn-on delay against 220 ns of turn-off, which is the dead time rather than a circuit alongside it; and an input pulse shorter than about 560 ns produces no output, so at 20 kHz the bottom and top 1.1% of the duty range are unreachable.

Then why two GPIO per phase, if both carry the same signal? Because tying HIN and LIN together would make a phase permanently driven — always high or always low, never high-Z. Keeping them separate costs three GPIO, which this design has spare, and buys the ability to put a single phase into high impedance: needed to disable one leg without braking, for trapezoidal commutation with a floating phase for back-EMF sensing, and for any bring-up step that wants one leg out of the way. The driver's interlock means the extra freedom cannot be used to short the bridge.

Get the polarity backwards and nothing dramatic happens, which is the nice consequence of an active-low LIN: B_INV set would drive HIN and LIN oppositely, which the truth table resolves as both-off or a fight the interlock wins — not a shoot-through. Still bring up on a current-limited supply with no motor attached.

The status LED is three GPIO, not a WS2812. The sister project's F-16 is a WS2812B needing VIH = 3.5 V from a 3.3 V GPIO, fixed by dropping the LED supply through a Schottky. With spare GPIO and no 5 V rail on board A, a plain RGB LED is smaller, cheaper and has no failure mode.

7 · Board B — power and I/O

Power tree

SOURCES ──────────────────────────────────────────────────────────────────

  XT30, 12–60 V ──────────────────────────────────────────→ VMOT
                  straight through, so regen can reach the pack

  USB-C VBUS ─┬─ FUSB302BMPX ── I²C ── CPU      PD sink, ≤ 20 V / 5 A
              │
              ├─ LTC4359 + E100N4P0HL1 ────────────────────→ VMOT
              │     ideal diode; blocks VMOT from the USB host.
              │     PD powers the drive instead of a pack, not with one
              │
              └─ Schottky ──────────────────────────────→ 5 V Vlogic
                    pre-contract path: logic only, no gate rail


RAILS ────────────────────────────────────────────────────────────────────

  VMOT ─┬─ SMDJ64A TVS  +  3 × 100 µF / 100 V bulk
        │
        ├─ the three half-bridges, board A
        │
        ├─ LMR38010 ──→ 12 V  Vgate   0.1 A    EG2103 × 3, board A
        │     EN pulled up: on unless GATE_EN pulls it down
        │
        └─ LMR38010 ──→  5 V  Vlogic  0.5 A ──→ SPX3819 ──→ 3.3 V
                                                  RP2350A + MT6701, board A

Two independent bucks off VMOT rather than a cascade. Same part twice, and a fault in the gate rail does not take the CPU down with it. Both are LMR38010 (4.2–80 V, 1 A synchronous), F-47's recommended replacement for the sister board's XL7005A. The 12 V rail is enabled by GATE_EN from the CPU — on by default, with a pull-up the CPU can override, because a reset is meant to brake and a brake needs the gate rail (§3). Both regulate happily from a 20 V VMOT as well as a 60 V one, which is what makes the next section work.

VMOT runs down to 12 V (decided 2026-09-22), and that is the one place the gate rail does not regulate: the LMR38010's maximum duty cycle is 97%, so at a 12 V bus the 12 V rail sits in dropout near 11.5 V. That is still above EG2103's 9.7 V VCC turn-on, the bootstrap reaches about 10.6 V against the 9.6 V VB turn-on, and the FETs see about 11 V of gate against the 10 V their RDS(on) is specified at. Below 12 V none of that holds, which is why 12 V is the floor rather than a nominal.

Three power states, and the drive is safe in the one you bench-test in. USB-PD is a real power source here, not a bench convenience — VBUS feeds VMOT through the ideal diode, so a 20 V contract runs the motor. What the gate rail's 12 V buck cannot do is boost, and that gives the behaviour for free:
SourceVMOT12 V gate railWhat the drive can do
USB, no contract yet5 Vdown Logic, encoder, RS-485 up. Motor cannot energise. Configuration and bring-up.
USB-PD, 20 V contract20 Vup Full control at reduced voltage, capped by PD at 100 W (20 V × 5 A, e-marked cable).
USB-PD, 12 or 15 V contract12–15 Vup (dropout at 12) As above, at lower power. Useful now that VMOT's floor is 12 V.
XT30 battery12–60 VupFull performance at 60 V.

The pre-contract state is not an accident to preserve — it is the direct consequence of a buck that cannot make 12 V from 5 V, and it means bench work on a board with no battery attached is physically incapable of spinning the motor until firmware asks for the voltage.

PD replaces a pack; the two are not used together (decided 2026-09-22). With VMOT down to 12 V a 20 V contract could be the higher source and back-drive a pack on the XT30, and draft 0.6 briefly designed a switch and a detector against that. Taking simultaneous use off the table takes the problem with it, so the PD path stays what it was: one LTC4359 and one FET, an ideal diode that keeps VMOT out of the USB host. What still works: a USB cable for configuration while a pack runs the drive, because the pack is at least 12 V, PD without a contract is 5 V, and the diode never conducts. What the board does not protect against is firmware asking for a contract above the pack's voltage with a pack connected, so firmware should ask for more than 5 V only when the bus reads what USB alone would give it — a guard against accidents, not a mode. The XT30 needs no ideal diode of its own and stays a straight pass-through, so regen still reaches a pack (question 8); reverse polarity is left to the keyed connector, as on the sister board.

Full-duplex RS-485, two ports

Four-wire RS-485, so the master can transmit whenever it likes and only the drives need to arbitrate. Two connectors wired pin-for-pin in parallel: the cable runs in one and out the other, and the bus is a bus, not a repeater chain. No firmware is in the path of a pass-through, so an unpowered or crashed drive does not break the run below it.

Port pinout — both ports identical, straight-through cable
PinNetDirection, from the drive's point of view
1GND—
2, 3DOWN+, DOWN−master transmits, every drive receives
4, 5UP+, UP−one drive transmits at a time, master receives
6GND—

Naming the pairs by direction rather than by A/B is deliberate: a straight cable then wires itself correctly, and there is no crossover to get wrong at 3 a.m.

Two SIT3088ETK (DFN-8, already sourced by the sister project) rather than one full-duplex part: one strapped permanently as a receiver on the DOWN pair, one as a driver with DE on the UP pair. Cheaper than a MAX3491, in a package that is already in the library, and it puts the failure modes somewhere already understood.

Two inherited traps to design out, not rediscover.
  • Termination is per-node, not per-board (their F-19). 120 Ω across each pair behind a solder jumper, open by default. Only the two ends of the chain bridge them. A board that terminates unconditionally breaks every mid-bus installation.
  • Fail-safe bias must be computed against the termination, not guessed (their F-01). Their 10 k/10 k idle bias delivered ±32 mV differential into a 100 Ω termination against a −200…−100 mV receiver threshold. Size the bias for >200 mV differential with the termination present, or use the SIT3088's own fail-safe and prove it on the datasheet.

USB-C

Full USB-PD sink, as on the sister board: FUSB302BMPX negotiating over I²C from the CPU, up to a 20 V / 5 A contract. The sister project's two highest-severity power findings are both PD front-end bugs, and both have adopted fixes that come across as fixes rather than as problems to rediscover:

The 20 V capability is the reason to be careful here rather than a reason to drop PD: it is also what makes the drive useful on a desk with nothing but a laptop charger.

8 · Interconnect

Two connectors, so the 60 V bus never runs beside USB or SWD. The signal count is set mostly by keeping PD on board B: the FUSB302 sits next to the USB-C connector where the CC lines are short, but the CPU that negotiates with it is on board A, so its I²C and interrupt cross the interconnect.

ConnectorCarriesRating
Power link — 2 × (2×5) @ 2.54 mm, stacked radially 10 × VBUS, 10 × GND, ganged 30 A at 3 A/pin, against 23.2 A RMS — see below
Signal link — 2×14 @ 1.27 mm +12 V, +5 V, USB D+/D−, USB_VBUS_DET, PD_SDA, PD_SCL, PD_INT_n, RS485 DI/RO/DE, RS485_TERM_EN, SWCLK, SWDIO, RUN, BOOTSEL, LED × 3, GATE_EN — 20 signals, 8 interleaved grounds. Draft 0.3 said 18 and 10; nobody had counted. USB sits at position 3 with a ground either side. signal
Why it is two 2×5 and not one 2×8. Draft 0.2 sized this link against "a 20 A design point" and got 24 A of capacity, which looked comfortable. It is not the phase current that crosses the link. At 20 A RMS per phase into a 60 V bus the drive delivers about 1.27 kW, so the bus carries 21.2 A of DC; on top of that a three-phase bridge puts 12.4 A RMS of ripple on its DC link, and at 20 kHz the 13 µF of ceramic on board A is a 0.61 Ω path against the bulk's ESR-dominated 0.2 Ω, so three quarters of that ripple goes across the header to reach the bulk. Root-sum-square: 23.2 A RMS against 24 A of rating. Eight hundred milliamps of margin is not a continuous rating.

Two 2×5 headers stacked radially fit the same 50° wedge, mate together, and give 30 A. geometry.interconnect() is the calculation, so changing the design point moves it.

The six perimeter screws do the alignment and carry the mechanical load; the connectors only have to mate. Their mated height and the standoff length are the same dimension and must be ordered as one decision. Note that the screws are now plated and grounded on both boards for §4's sake, which puts six brass standoffs in parallel with the header's ten ground pins — free return path, and a reason the link is less stressed than the arithmetic above assumes.

An alternative worth pricing before committing: make two of the six perimeter standoffs the power path — brass, into generous plated pads on both boards — and drop the header entirely. It is mechanically cleaner and electrically excellent if the joint is sound, and the whole question is whether a screwed joint's contact resistance is trustworthy over thermal cycling. Not the kind of thing to adopt without a bench test — and it now collides with §4, which wants all six of those screws carrying heat and ground, not two of them carrying 60 V.

The bulk on board B is no longer a catalogue item. About 9 A RMS of 20 kHz ripple arrives there. Three 100 µF / 100 V aluminium electrolytics — draft 0.2's budget — are rated for a few hundred milliamps each and would fail early and unspectacularly. The bank has to be chosen for ripple current, not capacitance: roughly four 100 µF / 100 V polymer-hybrid cans at 2–3 A each, or an equivalent low-ESR bank. Capacitance itself is easy — 100 µF holds bus ripple near 3 V at the design point. This is a board B change and it is on the sourcing list.

9 · What this inherits, and what it fixes

The sister project's findings register is the most valuable thing it hands over. These are the rows that apply to a board that has not been built yet.

Their IDWhat it saysWhat this board does
F-40EG3113 unbuyable — 9 in stock, min purchase 25; blocks ordering Designed out — on the third attempt. EG2131 was the obvious swap and is also out of stock; EG2103 (C480654, 2,225, $0.19) keeps the family, the logic convention, the interlock and the dead time.
F-4110 V-rated FETs driven at 5.04 V: ~2 W/bridge switching, ~15% on RDS(on) Designed out. 12 V gate rail, which EG2103 requires anyway (VCC 10–20 V). Halves the sister board's 95 ns edges to ~50 ns; the rest of the gap needs a stronger driver, not a higher rail.
F-16WS2812B VIH 3.5 V vs 3.3 V GPIO Designed out. Plain RGB LED on three GPIO.
F-27, F-39USB-PD front end: 65 V blocker on an 80 V bus; USBLC6 pin 5 on a 20 V rail Inherited as fixes, not as bugs. PD stays — 20 V feeds VMOT. LTC4359 + E100N4P0HL1 per their adopted F-27 disposition; USBLC6 pin 5 to +3V3 per F-39.
F-17E-stop is a bare GPIO; a wedged CPU cannot be stopped Structural. EG2103 floats to both-FETs-off, so RUN low is a real stop. But their disposition says it brakes, and per the EG3113 datasheet it coasts — see below.
F-14Shunt value drifted out of sync between schematic, board, firmware and README Process, not silicon. One table, in the README, referenced from the firmware define.
F-19No on-board bus termination; isolation only worked if the harness cooperated Applied to RS-485: jumpered 120 Ω, open by default.
F-01, F-02, F-03Encoder-port bias, termination and floating-mux failures Mostly moot — no encoder port. The bias/termination arithmetic carries to RS-485.
F-06MT6701 SSI response shifted by transceiver round-trip delay; ~2–4 MHz ceiling Moot. The sensor is on the same board as the CPU; no line drivers, no round trip.
F-47Space-budget study: same-function package swaps and architectural shrinks Cherry-picked. TPSMF4L64A, INA241, LMR38010, 2010 shunts — taken. LMG1205 — declined, it locks the gate rail at 5 V, which is the thing F-41 says to stop doing.
F-40, againThe lesson generalises: three of five first-choice parts here were unbuyable or overpriced when actually checked Every non-inherited part carries its LCSC number and a dated stock figure in §3. Re-check before ordering; these numbers rot.
F-48JLCPCB process: free POFV on 6-layer, 0.10 mm rules, via spacing trade-offs Decide before the first placement run, not after.

Three things are worth handing back. The first is a correction to draft 0.2 of this document rather than to their register.

F-17 is right, and the reason is not in their register. Draft 0.2 read the EG3113 datasheet, found that HIN 内建了一个 200K 下拉电阻,LIN 内建了上拉 5V 高电位,在输入悬空时使上、下功率 MOS 管处于关闭状态 — floating inputs put both MOSFETs off — and concluded that their disposition ("open E-stop loop pulls RUN low → reset → gate-driver pulls brake the motor") described a coast. That was reading their part instead of their board, and it was wrong.

Their board carries R1, R2, R21 and R22: four 10 k pull-downs, on both inputs of two EG3113s, straight to GND (A_PWM_H/A_PWM_L on U1, B_PWM_H/B_PWM_L on U6, pins 2 and 3 in both cases). EG3113's LIN is active-low with an internal 200 k pull-up. 10 k beats 200 k twenty to one, so LIN is held low, which is active, which turns the low-side FET on. Their E-stop brakes, exactly as F-17 says — because of four resistors, not because of the driver.

What is worth handing back is what their register does not say: this is not only the E-stop behaviour, it is the power-up behaviour. Their bridge shorts the motor through its low-side FETs from the moment the gate rail is alive until firmware takes the pins, with no current limit and nothing sequencing it. On a spinning motor that is a braking current set by winding resistance alone. If that is wanted it should be written down as a choice; if it is not, it is four resistors.

This board went the other way at first — 4.7 k up on LIN, so reset coasted — and since 2026-09-22 it brakes like theirs, but as a written-down choice: both pulls to GND, and the gate rail held on through a reset so the brake is real (§3). On power-up the gate rail comes up with the CPU still in reset, so this board too shorts the windings through the low sides until firmware takes the pins. On a motor at rest that is nothing, and it charges the bootstrap capacitors on the way.

RP2350 erratum E9 reaches the gate driver, and that is the second. A Bank 0 GPIO can latch around 2.1–2.2 V, and at reset the pin is an input with a pull-down; against EG Micro's internal 200 k pull-up on LIN the divider lands near 1.2 V, between VIL and VIH. Their four pull-downs happen to defeat both at once. Anyone copying the circuit without them — as draft 0.2 of this spec proposed to — puts a gate driver input in the undefined band. The answer is an external pull in whichever direction you want, sized ≤ 9 k, and stated in the schematic.

F-40 needs a replacement driver regardless. EG2103 is the cheap like-for-like, but for their four-bridge board NCP5183DR2G is probably the better answer: 600 V, 4.3 A both ways against EG3113's 2/2.5 A, matched 120 ns propagation delays where EG3113's are asymmetric — their own F-41 notes its working margin comes mostly from that asymmetry — plus 50 V/ns dV/dt immunity, AEC-Q100, $0.92, MOQ 1, 2,883 in stock. It gives up the interlock and the internal dead time, which their GaN evaluation already argues they do not lean on. This board took EG2103 instead precisely because it is not thermally tighter than they are and can afford to keep the interlock.

F-41's remedy, a 12 V rail, is the last — harder there than here, because thirteen TS5A3159 analog switches sit on their 5 V rail at 5.5 V max. Worth noting it only buys about half the gap on its own: with EG3113's 2/2.5 A the resistor still sets the edge, so the rail and the resistor sweep in F-41 are complementary, not alternatives.

10 · Layout and capture — board A

Board A is placed, captured and routed: 136 parts, 87 nets, 425 pin connections, 78 on the outward face and 58 on the motor-facing face, ten DNP. Every connection is made; DRC finds nothing, ERC finds nothing, and KiCad's own exported netlist matches the one this repo intended — net by net, connection by connection. §10b is the routing.

The schematic is generated from a netlist in Python, not drawn: a component is a reference, a symbol, a value, a footprint and a pin-to-net map, and tools/schematic.py emits four hierarchical sheets from that. The sheets are drawn with labels rather than wires — every pin gets a 2.54 mm stub and its net's name. That is an honest description of what it is: routing wires between 115 parts automatically produces something worse to read than a table. What it buys is that the designators, the footprints and the DNP flags are one source of truth, checked in both directions on every build. gen_boards.py will not write a board if a part is on it and not in the netlist, or in the netlist and not on it, or if their footprints disagree, or if any symbol pin has no net.

Capture is where a design finds out what it forgot. Seven things, in the order the schematic asked for them:
  • The RP2350A was missing everything it cannot run without. No inductor on VREG_LX for the core buck, no cap on DVDD, no filter on ADC_AVDD, no pull-up or cap on RUN, no series resistor on XOUT. Eight parts — and the decoupling moved to the back, directly under the power pins, where the loop is the board thickness rather than a trip around the QFN.
  • The bus divider was wrong by 2.3×. 50 k / 5.1 k puts 5.5 V on a 3.3 V ADC pin at a 60 V bus. It is 112 k / 4.7 k now — 2.42 V at 60 V, 3.18 V at the TVS clamp — and the high leg is two resistors, because a 0603 is rated 50 V working and the bus is 60.
  • FAULT_n floated. The overcurrent comparator is §11's open question, and not fitting it left a CPU input with nothing on it. A 10 k pull-up is the "not fitted" state.
  • USB_VBUS_DET arrived from board B and went nowhere. It is on GPIO22 now.
  • The signal link is 20 signals and 8 grounds, not 18 and 10.
  • The phase TVS were in §3's parts table and not on the board.
  • The MOSFET's symbol and its footprint disagreed. KiCad's own Transistor_FET:BSC030N08NS5 numbers its pads 1–3 source, 4 gate, 5 drain; the sister project's PowerPAK_SO-8_123 numbers them 1/2/3 = S/G/D. Using both would have wired every FET wrong. Board A now takes KiCad's matched pair, which also has a larger drain pad and so an easier via array.
Board A front copper: three phase cells each with two PowerPAK MOSFETs, an SOIC-8 gate driver, DC-link ceramics and a gate network; a QFN-60 wedge; two 2x5 power headers; a 2x14 signal header; three arc-shaped heatsink lands at the perimeter. Board A back copper, mirrored: the MT6701 at the centre, three arc phase pads at the perimeter, six 2010 shunts, three current-sense amplifiers and the filter and pull resistors, with the FET thermal via arrays showing through.
Board A, outward face and motor-facing face (mirrored). The dot grids on the back are the FET drain thermal vias coming through — four by five under each high side, four by four under each low side; the low-side cluster is how the switch node reaches its shunt.

The phase cell

All three are identical, at 60°/180°/300°, so all three commutation loops are the same shape. Radially outward from the centre keepout:

BandR, mmWhat is in it
Gate drive17.2–22.6 EG2103 on the cell axis, turned so it costs the band 5.4 mm of radius instead of 7.4. The SOIC-8 pinout then sorts itself: VCC and the logic inputs land on one side, LO/VS/HO/VB on the other, so the supply parts get one side and the bootstrap the other.
Half-bridge23.1–28.6 Both FETs at the same rotation, 3.75 mm either side of the axis, so the high side's source pads and the low side's drain pad end up adjacent: the switch node is a small island between them and VBUS and GND come out at the two far ends, where the DC link sits. Both are turned so the gate pad, at one end of the source row, sits at R 24.4 facing the driver row across an empty 1.5 mm, rather than at R 27.9 walled in by the switch-node pour and the heatsink land. Nothing else is in this row.
DC-link bulk24.5–29.1 Two 2.2 µF 1206 flanking the pair, as far out as the heatsink land allows — same job, fewer degrees. (A 1210 ran into the perimeter boss once the cell was rebuilt for the fan-out.)
Heatsink land29.2–31.5 Nothing. That is the point — see §4.
Revised 2026-09-22, from the simulation's hand-back. Four changes, one re-route:
  • The low side's barrels all reach B.Cu now. The switch-node pour on the back stopped at R 25.2, so only the inner row of the sixteen under each low-side drain touched it; the other twelve sat in the clearance between it and the phase-output pour. The pour now carries a tab over the whole array (gen_boards.via_tab).
  • The high side has twenty barrels. A fifth row at 0.905 mm, the array moved 0.15 mm off the source side of the pad, and the via pad trimmed to 0.78 mm so it fits. Only the high side can take one: a fifth row under the low side comes 0.14 mm from the motor lead pad, and a fifth column under either comes into the shunts' courtyards.
  • Snubber footprints, DNP, under the high side (§3's parts table).
  • A Ø6 magnet rather than Ø8, as the MT6701 datasheet recommends; Ø8 over-drives the die at the short end of the gap. No board geometry reads the magnet's diameter, so this moved nothing but the drawings.
Re-run on the routed board, the simulation's conduction solver (P3) has the busiest low-side barrel at 2.82 A rms, from 5.16, and the busiest high-side one at 2.10, from 2.42; the switch-node copper loses 0.61 W per cell, from 0.74. At a continuous 20 A RMS both are still over the 2 A a 0.4 mm barrel is given — the current crowds into the barrels nearest the shunts — and both are under it below about 14 A RMS. That fits §4's decision to treat 20 A as a burst.

On the motor-facing face: the amplifier at the cell's centre with its SNSP tap and its filter cap clockwise of it within 0.4 mm of R 18.4, arranged for two arcs rather than for a router; the gate bleeds directly beneath the gate resistors, one via through both pads; the two shunts side by side under the switch-node island between the FETs' via clusters, their SW pads over the switch-node pour and their PHASE pads in the phase-output pour, because nowhere else on the back is clear of the DC-link caps' vias coming through; the arc lead pad at R 28.5–31.3; and the channel at R 29.3, holding the clamp diode.

Second pass, for the router. The first routed board made 69 of 170 connections and the diagnosis blamed the driver ring. Measured, nearly every open connection ended at the RP2350: a 0.4 mm-pitch QFN needs four staggered rows of escape vias, 2.9 mm deep on every side, and the chip had 0.3 mm to the parts beside it with its decoupling on the back under the pins where the vias had to land. The CPU wedge is rebuilt around a clear band — the chip at R 21.15, the IOVDD 100 n on the back in two columns beside it, the crystal at R 29.9, the flash turned so both pad rows face somewhere a via can go, the signal header in to R 25.15 so all twenty signal pads can take an escape via. The regulator and its decoupling are Raspberry Pi's, copied from the RP2350A minimal design (RP-006440) in the chip's own frame to the 0.05 mm: the 3.3 µH 2016 inductor, the VIN, DVDD and AVDD 4.7 µF, 33 Ω into VREG_AVDD, 27 Ω in series with each USB line, five small pours around the regulator pins and a ring of 1V1 copper under the chip joining the four DVDD pins. Doing it found three netlist errors: the bootstrap diode was backwards (Device:D numbers 1 = cathode, and the cathode was on +12V), VREG_AVDD was tied straight to +3V3 where the reference filters it, and USB had no series resistors. The crystal's load caps stay at 27 p where the reference uses 15 p; the value is the crystal's to decide, and it is a different crystal.

How it is kept legal

tools/placement.py holds the floorplan and the rules it has to obey, and gen_boards.py refuses to write a board if any of them fail. Parts pack into rows at a radius; a row proposes a position and then walks it outward until the courtyards genuinely clear, because two rectangles 0.3 mm apart along an arc are also rotated relative to each other and can still have corners inside one another. On top of that it checks: nothing past R 31.5 or inside R 16, nothing outside its own 60° sector, nothing touching a perimeter boss pad, nothing on either arc, and — the one a same-side courtyard check cannot see — no through-hole pad landing on a part on the other face. That last rule is what found the FET thermal vias sitting on the shunts.

Seven things the KiCad file format decided, all found by DRC, ERC or the netlist diff.
  1. Every pad needs its own absolute angle. KiCad does not derive pad orientation from the parent footprint. Leave it out and pad positions rotate while pad shapes do not — on a round board that is every fine-pitch part's copper turned the wrong way. It surfaced as a 1.27 mm-pitch SOIC-8 reporting 0.0000 mm between adjacent pins at 90° and nothing at all at 45°, and it would have been fabricated.
  2. A custom pad needs the angle separately, or its primitive polygon is evaluated unrotated — which put the heatsink lands off the board edge.
  3. Net classes want microvia_*, not uvia_*, and a priority on every class. Get it wrong and KiCad discards the list and silently uses its factory 0.2 mm netclass. The symptom is DRC quoting a clearance rule that appears nowhere in the project file. This one predates the layout work.
  4. The stackup did not add up to 1.6 mm, also pre-existing. It does now, and the F.Cu→In1 prepreg is pinned at 0.1 mm because §1's loop inductance is quoted against it.
  5. A schematic's lib_symbols names units by the bare symbol name. The entry is Device:R, but its units are R_0_1, not Device:R_0_1. Get it wrong and the sheet fails to load — and in a hierarchy that failure is silent: the root still opens, ERC still reports zero violations, and the netlist comes out empty. Nothing but exporting the netlist and counting would have caught it.
  6. A sheet has two different uuids. The one inside the .kicad_sch is the file's; the path a symbol instance quotes is / plus the uuid of the (sheet ...) element in the root, with no root uuid in front. And the root's own sheet entries take (path "/").
  7. Two label stubs on one coordinate weld two nets together, quietly. The first layout pass put +12V and VB_A on the same point. Cells are sized from each symbol's own pin extent now, and the emitter refuses to place a label where a different net already sits.

One check is muted, lib_footprint_mismatch, and only that one: every footprint is embedded verbatim from its library, but KiCad re-derives geometry through the placement angle, so a part at 37.5° mismatches an identical library copy. Verified in isolation before muting.

Three footprints are generated rather than drawn, from the same constants as everything else: the PowerPAK SO-8 with its sixteen-via drain array, the M2.5 thermal boss, and the two arc pads. Change FET_VIA_PITCH or the land radii in geometry.py and the footprints follow.

10b · Routing — board A

Most of this board is not routed and was never going to be. Ground is two solid inner planes; VBUS is a sector of a third and +3V3 is the rest of that layer — the CPU wedge and the whole centre disc; each switch node and phase output is a pour on F.Cu and B.Cu, because 20 A does not go down a track and §4's IPC-2152 figure is about 6 mm of 2 oz copper. What is left is the signals, and those go to freerouting — less the ones it turned out to do badly, which are made first, as copper, and taken away from it.

Where it got to: 1361 track segments, 278 vias, and every one of the 425 pin connections made. DRC finds nothing, ERC finds nothing, KiCad's own exported netlist matches the intended one net for net. 212 of the vias are the tools' — 112 stitched into pads, the rest the fan-out's — and freerouting made the other 66. No foreign copper under the heatsink land, every annular ring legal, no right-angle corners. That is the 2026-09-22 board; the one before it was 1351 segments and 283 vias, and this one routed in freerouting's first round as that one did.

60°, in the channel68°, first pass68°, 2026-09-1168°, this board
Connections beyond a via in the pad170170146143
Left open7410100
DRCcleancleancleanclean
Right-angle corners2000
The last three connections were arithmetic, not routing. For a long time the board stopped at two or three, and they moved around. Freerouting left six and the maze made four of them; the maze run first instead, with only the fan-out in the way, made 49 of the 61 nets and left 12, of which freerouting then closed one; seeding the maze with the six by name and running it first came back with nine. Three routers, three different sets of survivors, the same order of magnitude — the shape of a capacity limit rather than of a search needing more time.

The capacity was the lanes out of the QFN's via field, and it was countable. The escape vias sat in four rows 0.71 mm apart, and a 0.15 mm track passing radially between two rows needs 0.23 mm to clear a via's pad, 0.15 mm of clearance and half its own width on each side: 0.91 mm between row centres, and there was 0.71. No radial lane existed anywhere in the field, so all sixty escaped signals left through the tangential gaps within a row — 1.14 mm between two vias, which holds two tracks — and the last connections were the ones that ran out of them.

Two lines of the fan-out closed it. The outermost row moved out 0.20 mm, making that one gap 0.91 and buying a radial lane the whole way round the chip for 0.2 mm of depth. (A lane between every pair of rows wants 0.95 mm pitch, 3.5 mm of fan-out against today's 2.8, and another 0.6 mm of clear band on every side — the wedge rebuild again, one notch further. It was not needed.) And the pins the planned rows could not place get a second try from the generic escape, which does not mind which row a via lands in: one pin, LED_R, and a QFN pin without a via is a pin no router can ever leave, because the pads are 0.4 mm apart and a track with its clearance needs 0.45. With both, freerouting finished the board in its first round and the maze found nothing left to do.

The maze

The last few connections are not hard because the board is full. They are hard because they are last: the lanes out of the QFN's via field go to whichever nets reach them first, and what is left over cannot be routed by anything unwilling to move what is already there.

tools/finish.py rasterises the board — one bitmap per routable layer (F.Cu, In3, B.Cu; In1 and In4 are ground and In2 the power planes) at 0.05 mm, every foreign shape drawn and then grown by the clearance it wants, 0.15 mm or 0.4 for the 60 V nets, by a distance transform. Then A* from one piece of the net to the other, eight-connected so every step is 45°, with a via as a move between layers wherever a via fits; and the chain of cells that comes back is folded into as few segments as the free space allows. The grid proposes and the exact clearance test disposes: what it rejects is punched out and the route planned again.

Two things the maze must know that KiCad's clearance rules do not cover, both found by DRC after the first run. The board edge — the clearance test is copper against copper and the outline is neither, so the simplifier cut a corner 0.4 mm outside the board. A bare drill — an M3 mounting hole is NPTH, it has no copper, and min_hole_clearance is the only rule that mentions it; seven tracks ran within 0.16 mm of the four mounting holes before the grid was told about drills.

The undo was the part that was wrong. When the maze cannot make a connection it rips up the router's copper near it — whole nets, never the tools' locked copper — makes the connection, and re-makes what it displaced, each of those allowed to rip up in turn. The first version undid a failed attempt by deleting everything that had appeared since and putting back what it had recorded taking off. That is correct until an attempt rips up inside an attempt: the inner one restores its copper as new items, the outer one deletes them as things that were not there before, and they are in nobody's record. The log said four connections made; KiCad said forty-one unconnected. The undo copies the whole board now, and the pass is guarded by KiCad's own count of what is unconnected before and after — if the maze has not improved it, the pass is discarded.

Order matters more than either router. Given the whole board first, with only the fan-out in the way, the maze makes 49 of 61 nets and leaves 12, and freerouting then closes one of those. Given what freerouting leaves — six connections — it makes four. Seeding it with those six by name and running it first came back with nine. It does not matter which nets get the lanes out of the via field; it matters how many lanes there are.

Why the fan-out is a tool and not a pass

A 0.4 mm-pitch QFN cannot take a via per pin in place: 0.2 mm pads, 0.2 mm between them, a 0.46 mm via wanting 0.15 mm around it. Every via row in front of the chip takes lane width from the stubs that still have to pass it. With 0.15 mm stubs, a row of vias every fourth pin leaves 0.84 mm between two of them and the three stubs passing through need 0.75 — 0.09 mm of margin, which is why the fan-out is four rows deep. The stubs are threaded rather than bent: a stub steps 0.06 mm aside for the length of a neighbour's via and comes back, because held aside for a whole row interval two neighbours passing vias on opposite sides met in the middle 0.03 mm too close. It is a small dynamic programme over (waypoint, lateral offset) and finds 51 of 52 vias in five seconds.

What the Specctra round trip got to decide

All of it measured on the way back, not read about:

  1. Freerouting does not treat a Specctra plane as connecting anything — nor as an obstacle. Ground is taken off the table before export: the planes go, the net goes, In1/In4 are declared power layers. And every 20 A pour is a keepout by the time the router sees it, because given as a plane it lays a foreign track straight through and KiCad cuts the pour around it.
  2. A protected via is protected on its pad's layer, by the pad, and nowhere else. The ground vias are removed from the DSN and each leaves a keepout on all four signal layers.
  3. KiCad's SES import takes a via's diameter from the Specctra padstack and its drill from the netclass. A 0.46/0.20 stitching via came back 0.46/0.40, a 0.03 mm annular ring; every drill is rebuilt from the diameter on the way in.
  4. Specctra has no hole-to-hole rule. via_via and via_pin clearance stand in, and a class with a rule of its own does not inherit them from the structure rule — measured as two Analog-class vias 0.48 mm hole to hole — so every class rule carries both.
  5. The router routes to the boundary it is given; the outline is pre-inset by 0.35 mm, exact because it is a circle about the shaft axis.
  6. PAD::GetLayer() answers F.Cu for a pad on a flipped footprint. Only the layer set knows.
  7. KiCad's DRC report names items by uuid, and a footprint embedded verbatim carries the library's pad uuids, so six FETs shared them and the unconnected list named the wrong gate for three runs. Every embedded item has a uuid of its own now.

What freerouting turned out to do

Measured, mostly from its log and some from its bytecode, after four runs in a row stopped at the same thirty connections:

  1. A wire touches a pad only if it ends on the pad's centre, exactly. Freerouting computes that centre itself from the placement rotation, rounded to 0.1 µm; KiCad writes wire ends to the micron. A stub starting on its pad's centre was a separate island to the router, which then queued 119 connections between pads and their own stubs and could insert none of them. route.py rewrites every wire end within 30 µm of a pin or via of its net onto the centre the router will compute.
  2. In 45-degree mode it cannot leave a pad that is not on the grid. Its search tree wraps a tilted pad in its bounding octagon; for the FET gate pads that runs into the halos beside them ("no accessible expansion doors", every gate, every pass), for the flash and the amplifiers it overlaps the neighbours. Free-angle mode routes them and takes twenty-five minutes a pass where 45-degree takes five seconds. So those pads get copper from the tools.
  3. A protected wire is a bad target. A junction into a protected trace cannot be inserted (791 times in one run), and a three-segment protected polyline joined twice loops normalization for ever. So the router is not shown the stubs: a pin with its stub and via leaves the net's pin list, the stub becomes a keepout, and the net's end is the via — a circle, on the grid at any angle. Passes went from thirty-five seconds to five.
  4. Its last pass can undo its earlier ones: a failed attempt does not put the neighbours it ripped up back, and the session comes back with nets that have their vias and no wire. route.py runs rounds, each exporting the board as it stands.
  5. A locked track exports as (type fix) and an unlocked one as (type route) — the only handle on my copper versus the router's across the round trip. A strict round protects both; the rounds after the first are loose, protecting only the tools' copper so the router's own earlier wires are its to move. It does not rescue the last few nets — told it may move them, freerouting reports "board state has not changed since pass #1" and stops — but it makes the rounds safe to repeat. KiCad's session import keeps a locked item and the session carries its own copy of some protected vias, moved up to 25 µm, so the copy goes and the original stays.
  6. A class naming a net that no longer exists makes freerouting read nothing and write an empty session, silently. -inc does nothing in 2.2.4 headless.
Two design rules were wrong before any of this, and routing is what found them.
  • 0.20 mm clearance on the power and analog classes is unbuildable. A 0.65 mm-pitch VSSOP-8 has 0.15 mm between adjacent pins, so every INA241 was a clearance violation no layout could fix. 0.15 mm is JLCPCB's floor for 2 oz outer copper, so nothing is given up.
  • The default via was illegal under the default rules. 0.5 mm on a 0.25 mm drill is a 0.125 mm annular ring against this project's own 0.13 mm minimum. The drill is 0.20 mm now.

45 degrees, and no stair steps

The tracks that come back are measured, not assumed: every segment is bucketed by direction and every corner where a horizontal segment meets a vertical one is counted and then chamfered — both new endpoints lie on the old track and the diagonal lies inside the corner it replaces, so a chamfer only ever removes copper. What is on the board: 360 segments at 45°, 361 orthogonal, 640 at neither, the last being the tools' — pad exits along a rotated pad's axis, the gate and sense arcs, and a fan-out radial to a chip at 270° on a board where nothing is on the grid. No right-angle corners, and 8 tees, which are junctions rather than stair steps.

Four repairs run after the round trip, all deterministic. Degenerate segments: Specctra leaves sub-micron stubs that turn a 45° bend into a right angle with nothing to cut. Hole-to-hole: a few drills come back 20–40 µm too close; a narrower drill moves no geometry, and where that is not enough the via is nudged. Unused vias: an escape the router did not use is copper on one layer, and KiCad is the judge of that — the via goes, then its stub. Pour islands: routing cuts the ground pours into pieces, and any piece without a via of its own gets one.

One more check runs on the way out, and it is not a DRC rule: no foreign copper on F.Cu where the heatsink ring clamps, R 29.2–31.5. That contact is the 20 A rating of §4, the ring is aluminium, and anything but ground under it is a short waiting for the screws.

11 · Open questions

  1. Which motor? Everything mechanical is provisional until this is answered: the encoder air gap, the standoff length, whether the 25 × 19 mm pattern actually bolts to something, and whether the rear face is stationary. Needed: the motor, its rear bolt pattern, and how the magnet is carried on the shaft.
  2. Thermal path — decided: option 2 on the board, and no rating beyond what the prototype shows. The board is laid out for a machined aluminium ring between the boards, clamping the bare copper and bolted through all six perimeter holes. Its outside was question 13, and 2026-09-22 answered it: no fins. The drive runs well below its peak current in normal use, and 20 A RMS is a burst to be measured on the prototype, not a continuous rating (§4).
  3. EG2103's LIN pull direction — settled from the datasheet: it is a pull-up, and a floating input coasts. Truth table, block diagram and the sign of Iin(L) all agree; only one sentence of the PDF's own prose disagrees, and it contradicts itself in the same breath. See §3. The pull resistors got fitted anyway, for the unrelated reason that RP2350's reset-state pull-down parks LIN in the undefined band — and since question 8 was answered they pull LIN down, so the board brakes.
  4. The channel can only hold ground and phase-output parts — decided: the cells were widened to 68° and five parts per cell came back in. Inside a phase cell the channel row on B.Cu is boxed in three ways: the phase-output pour owns B.Cu from R 25.9 to 28.9 across ±11.5 mm, so a channel part's inner pad at R 28.5 sits inside it and cannot trace inward; the heatsink land owns F.Cu from R 29.2 out, and that is bare copper under an aluminium ring, so a foreign-net via there is a short rather than a DRC error; and the six boss pads cut the outer B.Cu ring into six disconnected arcs. What is left is a via corridor about 0.7 mm wide, twice per cell. Neither pour can give the room back — the phase band is already 95 A/mm² at 3.0 mm wide and cannot grow inward past R 25.6 without meeting the switch-node band.

    So C{n}03/C{n}04 went into the DC-link row ahead of the bulk, where a high-frequency capacitor belongs; D{n}01 went beside the bootstrap capacitor it charges; R{n}11/R{n}12 went onto the driver's logic side with the VCC 100 n. D{n}02 stayed, because its nets are exactly GND and the phase output.

    67° is the floor — below it the DC-link row runs into the perimeter boss and the wedges lose their corners — and 68° is set, for a degree of margin. The cost is the even bolt circle, which is what the 60° arrangement was pleasing for; bolts are now at 0/68/136/204/256/308°. Against that, the heatsink land is measured from the cell edge, so it grows from 250 to 279 mm².

  5. Bus input connector — decided 2026-09-22: XT30, as on the sister board. Whether power also chains was not part of the answer, so board B gets one XT30 and no power out; an in/out pair beside the RS-485 ports is still possible, at the cost of board-B perimeter and a real fault-current question.
  6. Does VMOT ever run below 20 V? — decided 2026-09-22: yes, down to 12 V. Two consequences, both in §7. The gate rail runs in dropout at the bottom of the range, near 11.5 V, which EG2103 and the FETs accept and nothing lower would. And a 20 V PD contract could now back-drive a lower-voltage pack — answered the same day by not using the two together: PD replaces a pack. The XT30 stays a straight pass-through.
  7. E-stop. Keep the sister board's expandable loop pulling RUN, now that it actually works? It costs a two-pin connector and it is the only thing that stops a wedged CPU. My inclination is yes.
  8. Brake and regen. What a reset does is decided (2026-09-22): it brakes. LIN and HIN are both pulled to GND, the low sides conduct whenever the CPU is not driving, as on the sister board (§9), and board B holds the gate rail on through a reset so that the brake lasts (§3, §7). Regen is still open. A decelerating load pumps VMOT into the bulk and then into the TVS — the simulation has the bus at the TVS in 0.26 ms at the design-point braking torque, with about 2 J for a part that survives a few — and there is no brake resistor and no active clamp. A brake resistor, an active clamp, much more bulk, or a documented deceleration limit. A pack on the XT30 absorbs some of it, which is why the XT30 has no ideal diode; on USB-PD nothing does, because the PD path's diode blocks it.
  9. Hardware overcurrent. A comparator on the INA241 outputs pulling RUN is about four parts and closes the loop faster than firmware can. Populate, DNP, or leave out? Capture forced half an answer: FAULT_n now has a 10 k pull-up, so the not-fitted state is defined rather than floating. The comparator itself is still a question.
  10. Interconnect: header or standoff? §8. Wants a bench test, not an opinion — and it now competes with §4, which wants all six perimeter screws carrying heat and ground rather than two of them carrying 60 V.
  11. Firmware — decided 2026-09-22: SimpleFOC, as on the sister board, whose firmware, tuning scripts and dashboard all transfer. Its shunt scaling does not transfer: 50 mV/A here, 100 in the precision mode, against its 80. Three changes from the simulation are waiting for it: pace the ADC from the PWM slice's wrap (one conversion per phase at the counter's zero), set dead_zone to zero and leave the dead time to EG2103, and never command a duty the driver's 560 ns minimum pulse cannot reach.
  12. Board B is not captured. Board A is done; board B is still an outline and five empty sheets. Two things are waiting there and both came out of board A's arithmetic: its bulk needs sizing for ~9 A RMS of ripple rather than for capacitance (§8), and the RS-485 fail-safe bias has to be computed against the termination rather than guessed (§7, their F-01).
  13. How big are the fins? — decided 2026-09-22: there are none. §4 had made the enclosure the 20 A rating; the answer is to stop asking for that rating. The drive runs well below its peak current in normal use, and what it can do in bursts will be measured on the prototype. Whether connectors may overhang Ø65 is still open, with the enclosure.