Validation Plan¶
Digital-port update, 2026-09-12: Apply the migration acceptance checklist if the new 50-pin port is adopted. It requires observation before firmware with a valid buffer supply. Updated 2026-09-15: there is no manual disable to test — the buffers are permanently enabled, so check instead that the OE pull-downs hold both inputs low across the rail's rise and fall. The 40-pin mappings, component references, analog-tail checks, and high-Z startup tests below remain legacy-board checks; no new hardware has passed them by this documentation edit.
Rev 0 scope: gates that exercise the analog angle output, the pyControl TTL
event lines, or an independent encoder cross-check do not apply. The cross-check
is not deferred — it was deleted on 2026-08-16 and those gates should be struck,
not postponed. Run them when rev 1 adds the circuits. Everything covering
power, the driver, the encoder-to-driver path, homing, and the serial link
applies as written. Note that ENCODER_MISMATCH is retired — there is no
second count to disagree with and there never will be.
InstrumentPort-40 gates apply to Rev 0 as of 2026-08-16, but only for the
eleven live Strands and the three fitted buffer packages U1601/U1701/U1801.
Do not fail a gate on the seven unrouted Strands. Corrected 2026-08-17: the
board carries three SN74LVC125A packages, not five — U_C and U_D do not
exist, so there is nothing to check for DNP. Check instead that no input on the
three fitted packages floats.
No test points exist anywhere on the board (verified 2026-08-17). Any step below that assumes a probe pad currently has nowhere to land; those steps are flagged individually rather than quietly left in place. Either add pads before fabrication or accept that the signal is only reachable at a component pin or through InstrumentPort-40.
Reference designators were annotated on 2026-08-17. Where this document still
uses a pre-placement name, the as-built name is given alongside it: J1501
(InstrumentPort, was J4), J1102 (motor, was J5 then J1101), J1103 (encoder, was J6 then J1102), J1101 (analog BNC, was the J7 placeholder),
J1901 (pyControl RJ-45, was J3), U201 (high-side switch, was U27001),
U1201 (TMC5130A, was U4001). Net names are +3.3V_MCU/+3.3V_SW (not
+3V3_*), HV_RAW (not VAA), and ~{BUS_SW_EN} (not ~{BUS_SW_OE}).
Status: Planning
Date: 2026-08-12
Derived from: mouse-joysticks-pcb/docs/validation-plan.md
This plan adapts the Gates A-F sequence from
orb-weaver-context/context/pcb-development-plan.md to this board.
Animal-facing use is a separate gate requiring scientific, institutional, welfare, operational, and failure-response review. Passing every gate below does not grant that authority.
Pre-Fabrication Checks¶
Power — the 36 V items¶
- The 5 V/3.3 V regulator chain is fed from the protected but unswitched node, not from the high-side switch output. Otherwise the MCU cannot enable its own supply and the board never boots. This is the highest-consequence check on the list.
- A TVS is fitted on
HV_IN(SMBJ40A, or stockedSMF48A). It protects the switch and the buck at hot-plug; it cannot protect the TMC5130A. Reversed 2026-08-17: no TVS is populated. The rationale above is unchanged, so the hot-plug and transient measurements in Gate B are now the only evidence that the unprotected rail survives — treat them as blocking, not informational. - Bulk capacitance is downstream of the high-side switch, so plugging the
terminal block in does not ring against it, and is capped at tens of µF —
the switch dissipates ½CV² = 0.65 mJ per µF charging it, and the datasheet
requires the design not reach thermal shutdown while doing so.
Corrected 2026-08-17: as built,
+36V_SWcarries ~208 µF (C1213+C1214at 100 µF each plus the ceramics), well past "tens of µF". The position is right — it is downstream of the switch — but the cap is not met, so the cold-start and warm-start turn-on checks in Gate B are a real measurement rather than a formality.
Power — the 36 V input block edits¶
The block has never been placed. Corrected 2026-08-17: the block is placed as
U201 / J201 / D201, and the resistors were renumbered in the process. The
R2700x names below are the pre-placement library numbering; the as-built name
follows each one. Every item is a resistor value or a single wire; see the audit in
docs/power.md.
R27001— as builtR501— changed from 10 k to 1 kΩ, soENsees 3.0 V rather than 1.65 V against a 1.5 VVIH. Done, withR50610 kΩ pullingENdown.DIAG_ENtied to+3.3V. Without this theSNSoutput is dead andHV_SNSreads nothing. Reversed 2026-08-17:DIAG_ENis driven fromGP14(HV_DIAG_EN) throughR50210 kΩ, not tied high. Tying it high keeps the TPS1HTC100's OFF-state open-load detection permanently enabled, which puts an internal 150 kΩ pull-up fromVStoVOUTacross the switch and creeps+36V_SWupward while the rail is supposed to be off — see the Gate B check below.DIAG_ENhas an internal pull-down, so withGP14high-Z at reset diagnostics are off and+36V_SWsits at true zero. The consequence stated above still holds and is now firmware's responsibility:HV_SNSand~{HV_FAULT}mean nothing until firmware raisesGP14. Per the datasheet's Table 7-1, current limit and thermal shutdown are unaffected byDIAG_EN— only reporting is gated.~FAULTrouted toGP23with a pull-up to+3.3V_MCU. Moved offGP29on 2026-09-18 so that ADC3 is free forANGLE_OUT_MON; seedocs/interface-map.mdfootnote 11.LATCHtied to+3.3V_MCUthrough a 1 kΩRPROT(R503) for latch-off. Left floating the device auto-retries every 2 ms, which cycles the motor rail at ~500 Hz into a persistent fault. Clearing is by togglingEN, which firmware owns onGP19.R27003(ILIM) — as builtR508— changed from 10 kΩ to 40 kΩ (OW-PASS-SCR0603F40K, stocked), so the limit is ~1.2 A rather than the present 5.3 A typ (4-6.3 A). Confirmed against the identified motor, and able to pass the+36Vinrush. Must never float — floating is a fault state. If more headroom is needed, 2 × 49.9 kΩ in parallel gives the tabulated 25 kΩ / 1.96 A row.R27004(R_SNS) — as builtR509— changed from 1 kΩ to 249 Ω (OW-PASS-CRCW0402249RFKED, stocked), so the fault indication lands at 1.29-1.59 V instead of 3.3-4.4 V, well under the RP2040 ADC'sVDD + 0.3 Vlimit. No divider required. Confirmed 2026-08-17 against the datasheet:ISNSFLTis a 5.2-6.4 mA current source, so a fault reads 1.3-1.6 V against the RP2040'sVPINabsolute maximum ofIOVDD+ 0.5 = 3.8 V, and stays clearly distinct from the ≤0.37 V maximum normal-current reading. The safe upper bound is about 594 Ω; ~470 Ω would use more ADC range if resolution ever matters.DIAG_ENandLATCHeach driven through a 1 kΩRPROTseries resistor (R502,R503), per the datasheet's Table 4-2.R27005— as builtR507— pulls~FAULTUP to+3.3V_MCU, not down toGND. As originally wired it was a pulldown on an open-drain output, which idles low and reads as a permanent fault.HV_INandSNSare hierarchical labels, not local ones. WithoutHV_INthe board cannot feed the buck from the unswitched node or place the TVS; withoutSNSthe current sense cannot reachGP28.ILIMstays local.C27002— as builtC202, 22 nF — is a 100 V part, matchingC27001(as builtC201, 220 nF). 50 V gives only 1.4× margin on a 36 V rail and does not cover the −60 VVOUTexcursion during inductive turn-off. Both confirmed 100 V on 2026-08-17. The same standard is not met byC1213/C1214on+36V_SW, which are 50 V — see the Gate B transient check.- The block's 3.3 V hierarchical label resolves to the board's
+3.3V_MCUnet. - The terminal block's polarity is silkscreened: pin 1
GND, pin 2 positive. - The switch's thermal pad has a via array — it is both the heat path and the ground connection.
Power — driver side¶
- The TMC5130A
VCCis supplied from the external+5Vrail, not left to the internal regulator. AtVM = 36 Vthe internal regulator's 1 W limit is reached at 32 mA. 5VOUTis a decoupled local net, not bridged to the external+5Vrail.- Bulk capacitance sits at the driver's
VSpins, and theVS/GNDloop is small. 36 V leaves 10 V to the 46 V operating limit and the datasheet warns explicitly about ringing from stray inductance. As built that bulk isC1213/C1214, 100 µF 50 V each — 72% of rating on a 36 V rail, with no TVS above them. Acceptable on paper; it is the Gate B transient measurement that decides it. C1209/C1210, the TMCVSceramics, are rated correctly by vendor mapping rather than by requirement.OW-CAP-100N-0402-X5R-16VdeclaresReqMinVoltage = 16Vbut resolves to LCSC C162178, a 100 V part. Move them to a part whose requirement states the rail voltage —OW-PASS-GRM188R72A104KA35D(100 V) is already catalogued — so a future vendor remap cannot silently drop them to 16 V on a 36 V net.- An RC snubber footprint exists across
VS/GNDat the driver, populated or not, so a bad Gate B transient measurement is a populate decision rather than a respin. - Exactly one
PWR_FLAGper net, on input and entry nets only. The 36 V block and theusb2block each carry one; do not add a second to either.
Power — multi-source logic rail¶
- The ORing diodes are at the
TPS73733input, not on+5V. ORing onto+5Vdrops it below the TMC5130A's 4.6 VVCCminimum, and below the rev 1OPA197's 4.5 V minimum. (Corrected 2026-08-26: it does not "truncate the DAC's full-scale output" — that failure mode belonged to theDAC8563plan, where full scale wasVDD-referenced.) +5Vis single-sourced from theTPSM365R6V5.- Diode orientation blocks current back into the USB host and into pyControl.
- The
SW_RAIL_SENSE÷2 divider off+3.3V_SWis fitted onGP26, and firmware refuses to deassert~{BUS_SW_EN}until it reads valid. Firmware refuses to drive the 5 V domain when+5Vis absent. As built the divider isR401/R402, 1 kΩ/1 kΩ, inside themcusheet; the sheet pin namedSW_RAIL_SENSEcarries the raw+3.3V_SWrail in, and the divided node onGP26isSW_RAIL_SENSE_DIV_2. That same 2 kΩ to GND is what holds+3.3V_SWat 0 V when the domain is off, so it is load-bearing twice over: if the divider is ever removed,R1107,R1101,R1102andR1103stop defining their nets. - Series resistors are present on every 3.3 V line driving a 5 V-domain input, as a hardware backstop independent of firmware.
hv_to_5v_tpsm365r6is fed from the switched+36V_SW, soVSA(which thetmc5130block ties to+5V_SW) can never exceed theV_VS+ 0.5 V absolute maximum, andVCCcan never arrive afterVCC_IO. Both orderings hold by construction with no sequencing logic. Recorded as a deliberate deviation: the datasheet saysVSAis "normally tied toVS", and this board does not, to keep 36 V off the analog supply.+3.3V_MCUis a separate always-on rail fed only from USBVBUSor pyControl pin 6, through two ORing diodes, with no tap onHV_IN. The board's inability to boot from 36 V alone is accepted and silkscreened.- The TMC5130A's
VCC_IOis on+3.3V_SW, not on+3.3V_MCU. An always-onVCC_IOwith a switchedVCCis the sequencing order the datasheet forbids. - Two
5v_to_3v3_tps73733instances exist, one per domain, and the two 5 V rails are separate nets that never meet. - Every MCU line reaching the switched domain passes through a
SN74CB3Q3245channel, and the pyControl UART,HV_EN,SW_RAIL_SENSE,HV_SNS,~{HV_FAULT}and theFW_TRIGmarkers do not. Series resistors back up the bus switch on those crossings. - The bus switch is powered from
+3.3V_MCU, so firmware can isolate before droppingHV_EN. Record this as a deliberate divergence from the block's intent: thekicad-libsnote saysrp2040_gpio_isolation_sn74cb3q3245was meant to run from the switched target-domain rail so the part's ownIoffprovides the isolation. As built, all fourVCCpins are on+3.3V_MCUand isolation depends on the MCU holding~{BUS_SW_EN}high, whichR6017.87 kΩ makes the reset default. Also note A side = TMC / switched side, B side = MCU side — the opposite of whatdocs/interface-map.mdsays. Unused B-side pins are tied to GND and the A-side pins are open, so the rev-1 DAC channels are not pre-wired. +3.3V_MCUis never switched. The MCU runs on it and drivesENABLE; switching it off cannot be undone by firmware.R1202— as builtR1202— in thetmc5130block, betweenVCCand5VOUT, is 10 Ω where the datasheet recommends 2.2 or 3.3 Ω. Confirm or change. Signed off 2026-08-17 as benign: as built the 10 Ω sits between+5V_SWand5VOUT, and becauseVCCis supplied externally the internal regulator idles, so the resistor carries almost nothing. The datasheet's sequencing rule —VCCcomes up before or in parallel with5VOUTandVCC_IO— is satisfied. Keep 10 Ω.- Both ORing diodes are populated. The pyControl 5 V branch is no longer optional — it is one of only two sources that power the board.
VBUSis not tied to+5Vanywhere.- USB ESD protection is fitted. The
usb2block contains none — only the receptacle and two 5.1 kΩCCpulldowns. Use the stockedUSBLC6-2P6, placed on the connector side of the 27 Ω series resistors that live in therp2040block. It clampsVBUStoo, which matters now thatVBUSis a power source. - The USB-C receptacle's
SHIELDpins are terminated deliberately, not left unconnected as the block leaves them.
Controller and allocation¶
- The RP2040 GPIO allocation is checked against RP2040 peripheral pin muxing —
UART0,SPI0, andSPI1. PIO0 is unallocated. - Chip selects are plain GPIO, not hardware
CSn. RP2040 hardware chip select does not hold across the TMC5130A's multi-byte datagrams. - Debug console is USB CDC.
UART0is committed to pyControl. - The allocation in
docs/interface-map.mdmatches the final schematic. Corrected 2026-08-17:GP14is no longer spare — it drivesHV_DIAG_ENthroughR502. The remaining spares areGP15,GP16,GP18,GP27— four, not five.GP9is~{BUS_SW_EN}withR6017.87 kΩ pulling it up to+3.3V_MCU, so the bus switch is isolated at reset. RUNandSWCLK/SWDIOare unconnected and NC-flagged, by decision. There is no SWD header in Rev 0 and no reset button.RUN's internal 50-80 kΩ pull-up and the datasheet's permission to leave it make the missing reset an access limitation rather than an electrical defect; both are deferred to a future revision. Gate steps that assume SWD access do not apply to Rev 0.- The
EXT_FAULT_INdecision is made: reserved and routed, or explicitly declined.
Driver¶
- Current sense is 0.15 Ω (
OW-PASS-RLH12FTCR150, 1206, 1 W) on both positions, as builtR1203andR1204. Thetmc5130block ships 0.12 Ω as its default — confirm the placed value, not the block default. This is the one place the board andkicad-libsdisagree, and the board is right (verified 2026-08-17): the library moved to 0.12 Ω in commita8dca31, so re-placing thetmc5130design block will silently revert the sense resistors and put roughly 25% over the intended current. Either fix the library back to 0.15 Ω or never re-place that block. Verify the fitted parts on the physical board, not the schematic value, before first motion. - The schematic and any firmware configuration table record
vsense = 1andIRUN = 28. With 0.15 Ω installed,vsense = 0— the chip's reset default — drives roughly 1.8× the motor's rated current. - Firmware sets and reads back
vsense = 1andIRUN = 28from CHOPCONF and refuses to raiseDRV_ENNunless both are confirmed against the 0.15 Ω shunts, raising anINTERNALfault instead. Read-back, not write-and-hope: the write can be lost with the bus switch isolated and nothing else on the board reports it. - The motor supply envelope is sized from the identified motor.
SWSELis strapped for SPI and brought to a pad. Corrected 2026-08-17:SWSEL(pin 28) is tied to GND, which is SPI — correct — but there is no pad, because the board has no test points at all.SPI_MODE(pin 12) is left no-connect and its internal pull-up selects SPI, which is also correct.DRV_ENNis pulled to the disabled state. As built this isR1103, 10 kΩ from~{TMC_DRV_EN}to+3.3V_SW— the switched rail, deliberately, so it collapses with the domain rather than injecting current into a dead rail. See the Gate B measurement ofU1201pin 29.- The TMC's
DIAG0/DIAG1pull-ups areR1101/R1102, 10 kΩ to+3.3V_SW. Both pins are open-drain at reset and the datasheet requires ≤47 kΩ. - The TMC SPI and
LIMIT_SWtaps into the instrumentation buffers are terminated at 10 kΩ, not 100 kΩ (R1104-R1601). 100 kΩ against the TMC's(tpu)inputs —RPU= 132/166/200 kΩ onCSN/SCK/SDI/SDO— parks the node at ~1.9 V, straddling theSN74LVC125A's 2.0 VVIH. Do not "improve" these to 100 kΩ.
Sensors and outputs¶
- The encoder is identified, and the input path is single-ended or differential
accordingly. Settled 2026-08-17: single-ended 5 V push-pull, received through
quadrature_encoder_input_5v_sn74lvc2g17, which is placed. The chain isJ1103→R801/R802100 kΩ harness-side pull-downs →R803/R8044.7 kΩ series →C802/C803470 pF →U801SN74LVC2G17 dual Schmitt on+3.3V_SW→R805/R806100 kΩ output pull-downs →ENC_A/ENC_B. - The push-pull assumption is in the hardware, not just the prose.
R801/R802are pull-downs, which only work with a push-pull encoder. An open-collector or open-drain encoder will not work without changing them to pull-ups, so confirm the output stage of the chosen part before fabrication. This is one of the two things the encoder decision can still invalidate. - Confirm
J1103's pin order against the chosen encoder's harness: pin 1 =GND, pin 2 = reserved index, no-connect, pin 3 =ENC_A_RAW, pin 4 =+5V_SW, pin 5 =ENC_B_RAW. Note that supply sits between the two channels, so a harness wired to a different convention will put 5 V onto a signal pin. - There is no ESD device anywhere in the encoder path. Other documents in this repo say the encoder input has ESD protection; corrected 2026-08-17 — it does not. Decide whether the cable run justifies adding one.
- Peak encoder channel frequency is measured during the fastest
motor-commanded move (homing or recentering — not the animal's pull, which is
the slow case), and
C802/C803are confirmed. Confirmed 2026-08-17 against the as-built netlist: the fitted filter isR803/R8044.7 kΩ ×C802/C803470 pF, a 71.4 kHz corner (τ = 2.2 µs). Note the two numbers in the sizing table in.agents/PLANS.mdmean different things and are both correct: 71.4 kHz is the −3 dB corner, and 44.9 kHz is the maximum usable channel frequency, about 0.63 × f_c, which is the figure to compare against. Confirm the encoder'sf_ch = CPR × RPM / 60against 44.9 kHz. If chopper hash rides on the line, step the capacitors up using that table — it is computed for the fitted 4.7 kΩ. Both channels must keep the same value so A/B delay stays matched. The node to probe is the block's internalEA_FILT/EB_FILTat theU801inputs, for which no test pad exists — reach it at theU801pin or add a pad. ENC_Nis wired to the TMC5130AENCNand a test pad, and to no GPIO. Corrected 2026-08-17: there is noENC_Nnet and no test pad.ENCNis tied to GND throughR1201(10 kΩ), which satisfies the TMC5130A requirement to pullENCNto GND orVCC_IOwhen unused.J1103pin 2 is reserved for the index channel if the chosen encoder has one, but it is a no-connect today, so adopting an index channel is a schematic change.- The limit switch is identified and the debounce path is reviewed. As built the
path is
J1301→U1301MAX6816 →LIMIT_SW→ TMCREFL(pin 8), withR160110 kΩ to GND defining theU1801buffer input. - ~~A test point exists on the
REFLnet, andGP9drives it through a series resistor weak enough that the real switch always wins.~~ Struck 2026-08-17 —GP9is~{BUS_SW_EN}, the stimulus net was deleted on 2026-08-13, and the board has no test points. Replaced by:REFLis exercised by closing the real switch and observed on InstrumentPortDIG11. - ~~DAC8563
VIHatVDD = 5 Vis checked; a dual-supply level translator is added or explicitly ruled out.~~ Closed 2026-08-26. The part is aDAC80501, whoseVIHis 1.62 V minimum flat acrossVDD= 2.7-5.5 V. No translator. The warning that anSN74LVC125Ais not a valid translator stands for any other 5 V path. - ~~
~LDACis wired for synchronous update.~~ Void 2026-08-26 — the part has noLDACpin. Replaced by a firmware check: synchronous update usesDAC_SYNC_ENplus theTRIGGERregister. - The
GAINregister is written to0x0101before the first DAC data write. Reset is0x0001, which is the 0-5 V range; until that write lands the 3.3 V hardware bound does not hold. This is the single most important firmware ordering check on the analog output. VREFIOmeasures 2.500 V, and the gain fromDAC_VOUTtoAMP_VOUTmeasures 1.32. Both have block-level test pads.- The analog output's fault and reset behavior is decided and documented.
- ~~Whether
AUX_ANALOG_OUTis populated is decided, andJ7is a dual or single BNC to match.~~ Closed 2026-08-27: a single verticalHL-BNC-KHDAis placed asJ1101,AUX_ANALOG_OUTis not populated, andAIN1_P/Nstay no-connect. - The DAC bus terminations are fitted — all three inside the block as of
2026-08-27:
~{DAC_CS}10 kΩ to+3.3V_SW(R2006),DAC_SPI_COPI10 kΩ to GND (R2009),DAC_SPI_SCK10 kΩ to GND (R2010) — and none of them to 5 V. This item used to say the two pull-downs were board-level. Add no board-level copies when theDIG9/DIG10/DIG6taps go in; they would parallel these and halve them. - The block's
+3.3Vport is bound to+3.3V_SW, not+3.3V_MCU. On the always-on rail the~{DAC_CS}pull-up back-powers+5V_SWthrough the DAC's input clamp whenever the switched domain is down. Verified on the netlist 2026-08-27; still measure+5V_SWwithHV_ENlow at Gate B. - The 987 Hz output filter is verified. Measure the corner and the group delay
rather than assuming them: expect −3 dB at about 827 Hz and 266 µs of DC group
delay from
R200120 kΩ /R200213 kΩ /C2003andC200710 nF C0G. Record both as measured numbers, because every latency measured throughAIN0and every latency the researcher sees at the BNC includes the delay. C2003andC2007are C0G, not X7R. Confirm on the assembled board. A class-2 substitution puts a ±15% tempco on the corner and the reported delay, and adds a dielectric-absorption tail that reads as hysteresis in the angle channel.- The chopper-alias check. Sweep the TMC chopper frequency while recording the angle channel. If a periodic artifact moves with it, it is aliasing of the 16-30 kHz chopper into 0-15 kHz past the OneBox's fixed 30.3 kHz sampler, not joystick motion. This is the measurement that says whether 987 Hz was the right corner.
- A fourth
SN74LVC125Apackage is placed for theDIG6/DIG9/DIG10taps.
pyControl and instrument¶
- The RJ-45 — as built
J1901— is wired perdocs/pycontrol-port-pinout.md: pin 1DIO-AtoUART0 RX, pin 4DIO-BtoUART0 TX, pin 2GND. Pins 3, 5, 7 not connected. No Ethernet magnetics. PYC_UART_RX(GP1) has a 10 kΩ pull-up to+3.3V_MCU(R401). All RP2040 GPIOs reset with the internal pull-down enabled, so withoutR401the idle UART line reads low and firmware sees a permanent break. Check the pull-up before blaming the cable at Gate B.R1903on the pyControl 5 V branch is a polyfuse position, not protection. The path isJ1901pin 6 →PYC_CONN_5V→R1903→PYC_5V_FUSED→D1901→+5V_MCU, and the 0.36 Ω part currently fitted is a jumper. Port 5 V is a shared 1.0 A that also feeds the pyControl controller's own logic regulator with no per-port fuse, so a fault here can reset the controller. Populate a 1206 PPTC, 250-500 mA hold, or record the jumper as an accepted risk. There are no test points either side ofR1903, so a tripped PPTC could not be diagnosed in circuit. The on-sheet note still namesR29003, which is not this board's designator — fix the text when the part is decided.- The assigned pyControl port is confirmed UART-capable on the deployed breakout.
- Three event lines have a confirmed physical home, and the breakout has enough BNCs for all boards in the rig, not just this one.
- The InstrumentPort-40 overlay is reviewed against
board-interfacesand not redefined here. J1501's cable orientation is documented before fabrication (J4in earlier drafts) — connector reference, physical pin-1 marker location, contact side, expected cable type and orientation, and whether the mating board sees the logical pin order mirrored. The profile requires this of every board that uses it, and it is an open decision in the profile itself.- The eleven live
DIG*/TRIG*signals are buffered;~OEdefaults to high-Z in hardware —R1601/R1701/R1801, 10 kΩ each to+3.3V_MCU(retired design; those designators now name unrelated parts) — and firmware enables it at startup. The remaining seven Strands are no-connect atJ1501and have no buffer channel behind them. - The pyControl serial Strands are tapped at the board-side nets
PYC_UART_RX/PYC_UART_TX, which are the same nodes as the block'sPYC_DIO_A/PYC_DIO_Bpins, so the block's 100 Ω (R1901/R1902) sits between the buffer input and the cable. The block is unmodified at five boundary pins. - No
SN74LVC125Ainput floats. Corrected 2026-08-17: there are three packages,U1601/U1701/U1801, and noU_C/U_D/U_E. The termination as built is:R1104/R1105/R110610 kΩ to GND onTMC_SPI_SCK/COPI/CIPO;R110710 kΩ to+3.3V_SWon~{TMC_CS}, because a pull-down there would assert chip select;R1601100 kΩ to GND onLIMIT_SW, inside the limit-switch block;R805/R806100 kΩ to GND inside the encoder block onENC_A/ENC_B; andU1801's fourth, spare channel with its input tied straight to GND. Note that~OEdoes not gate the input stage —ΔICCis 500 µA typ / 5000 µA max per partially biased input regardless ofOE, against 40 µA max for the whole device with valid levels — so disabling the buffers is not a substitute for terminating the inputs. ENC_A/ENC_Bare tapped on the driver side of the encoder input filter, so the capture shows what the TMC5130A's pins receive rather than what the cable delivers. As built they are theU801Schmitt outputs.- The instrumentation taps sit on the A / switched side of the bus switch, not
the MCU side.
docs/interface-map.mdsays A is the MCU side; it is wrong and the board is the authority. The consequence is that every tapped TMC signal reads 0 V wheneverHV_ENis low, which is why the terminations above exist. AOUT0/AOUT1are no-connect, and no divided motor rail reaches anyAIN*.AGND(pins 31, 34, 37, 40) is tied toGNDas of 2026-08-17; it was previously an isolated four-pin island.- The documented "buffered static low on
DIG12" is not delivered.U1801's spare channel has its input grounded but its output stops atR1805, andJ1501pin 20 is a no-connect. Do not write a gate step that expects to see it. - ~~Labelled test points exist for the 10 signals not carried by InstrumentPort-40,
including
ENC_NandLIMIT_SW, plusENC_A/ENC_B, which lose their port slots at rev 2 and fall back to the pads.~~ Struck 2026-08-17 — no test points exist anywhere on the board. The rationale stands and the need is real:ENC_A/ENC_Blose their port slots at rev 2 and have no fallback access today. Either add the pads before fabrication or record that those signals become unreachable at rev 2.
Mechanical and process¶
- Board outline and mounting are decided.
mouse-joysticks-mechdescribes the two-joystick assembly; how a per-axis board mounts in it is a decision, not a derivation. - A unit-identifier field is on the silkscreen if the rig will hold two boards.
- Layout zoning separates the motor section and the switching supply from the encoder path and the analog output.
- ERC and DRC pass. ERC recorded clean 2026-08-17: 0 violations across all severities, 142 components, no duplicate reference designators, and every part footprinted. Re-run and re-record after any schematic edit; a clean ERC is only evidence at the commit it was taken on.
- Netclass patterns were extended on 2026-08-17, 108 → 124 patterns, because
146 of 156 nets were falling through to
Defaultand would have routed at 0.2 mm. The 36 V rail, both 5 V rails, both 3.3 V rails, the four motor phases andBRA/BRBno longer do. As assigned:*/HV_RAWand*/HV_IN→PWR_HV_ENTRY1.5 mm;/+36V_SW→PWR_HV_BUS1.2 mm;/motor_encoder_limit/OA*,OB*and*/tmc5130/BRA,BRB→MOTOR_2A0.8 mm;/+5V_*,*/VBUS,*/PYC_CONN_5V,*/PYC_5V_FUSED→PWR_5V0.5 mm;/+3.3V_*→PWR_3V30.3 mm;*/5VOUTand*/tpsm365r6/VCC→PWR_5V_LOCAL0.2 mm;*/tmc5130/VCP,CPO,CPI→BOOTSTRAP0.3 mm. Verified that no net matches more than one netclass. NoteBRA/BRBare not Kelvin sense nets on the TMC5130A — they carry full coil current into the sense resistor, so they are sized with the motor phases.MOTOR_2Aat 0.8 mm was chosen overMOTOR_LOWat 0.3 mm because the motor part is still open; downgrade it once the motor is fixed. DRC has not yet been run against a routed board.
Gate A: Contract Review¶
- Authoritative pyControl connector and electrical sources are cited.
- The exact controller, breakout, hardware revision, port designations, and specification revision are identified.
- BNC count and configurability are established for the whole rig's line count.
- Whether one
RSYNCoutput can drive two boards is answered. - Pin numbering, orientation, cable behavior, power, ground, and signal levels have explicit status.
- Unknowns have owners and do not hide a fabrication risk.
- Controller scope, safe states, timebase ownership, and the cross-board fault policy are documented.
Gate B: Electrical Boundary¶
- Board boots with the motor rail off, and
HV_ENraises it. Confirm before a motor is attached. +36V_SWsits at true 0 V withHV_ENlow. With 36 V applied,GP14(HV_DIAG_EN) low andGP19(HV_EN) low,+36V_SWmust read 0 V and must not creep upward. This is the newest and least obvious defect class on the board, so measure it before anything else on this list. If the rail creeps at roughly 1.2 V/s and the 5 V rail starts hiccupping every few seconds, the TPS1HTC100's OFF-state open-load pull-up — 150 kΩ fromVStoVOUT— is enabled, meaningDIAG_ENis high when it should not be. The arithmetic: ~240 µA into the ~208 µF on+36V_SWcrosses the TPSM365R6's 3.6 V minimumVINin about 3 s, at which point the buck starts, collapses and hiccups, and the switched domain never cleanly powers off. That would break the "HV_ENpower-cycles every resettable chip" property the whole reset architecture rests on, and corruptSW_RAIL_SENSEwith it. Derived from the TPS1HTC100 datasheet on 2026-08-17, not yet bench-verified — this measurement is what converts it.SW_RAIL_SENSEreads zero when the switched domain is off.GP26sees+3.3V_SWthrough theR401/R4021 kΩ/1 kΩ divider, so the ADC node isSW_RAIL_SENSE_DIV_2. Expect ~0 V withHV_ENlow and ~1.65 V with the rail up. This is the guard firmware uses before enabling the bus switch, so it has to be trustworthy in both directions — a reading that is merely low is not the same as zero, and the difference decides whether the isolation sequence is safe.~{TMC_DRV_EN}is high at boot. With the switched domain powered and the bus switch still isolated, measureU1201pin 29: it must be ≥2.31 V, the TMC5130A'sV_INHI= 0.7 ×VCC_IO.R110310 kΩ to+3.3V_SWguarantees ≥2.75 V worst case, even against the RP2040's strongest 50 kΩ reset pull-down passed through the bus switch. If it sits near 1 V the pull-up is missing or on the wrong rail, and the power stage can enable itself. Measure this before a motor is attached.~{TMC_CS}idles high and the other three SPI taps idle low. With the switched domain on and the bus isolated:~{TMC_CS}≈ 3.4 V (deasserted), andTMC_SPI_SCK,TMC_SPI_COPI,TMC_SPI_CIPO≈ 0.28 V. With the domain off, all five nets at 0 V. This confirmsR1104-R1601. A reading near 1.9 V on the SCK/COPI/CIPO group means 100 kΩ was fitted instead of 10 kΩ and the taps straddle theSN74LVC125A's 2.0 VVIH.- Back-power check. With 36 V applied and both USB and pyControl unplugged,
measure
+3.3V_MCUand+3.3V_SW. Both should be 0 V. TheSN74LVC125Ahas noIoffand no partial-power-down support, itsVIabsolute maximum is −0.5 to 6.5 V and is notVCC-referenced, and TI does not specify leakage atVCC= 0 — so this case is unspecified rather than guaranteed by the datasheet. This measurement is what closes it. Anything other than 0 V means a path exists from the switched domain back into the always-on rail, and theHV_ENpower-cycle stops being a power-cycle. - Each power source works alone, and the board behaves sanely in each state:
USB only; pyControl only, which is now a required mode, not an optional
one; 36 V only, which must be confirmed not to boot; and every combination,
including plugging and unplugging each source while the others are live.
Measure
+3.3V_MCUin each case. - Measure pyControl pin 6's available current under the board's real load. This is the item that decides whether pyControl-only operation works, and it is unmeasured today.
- With the switched domain down, confirm no current flows into
+5V_SWor+3.3V_SWthrough any device's input clamps, and thatSW_RAIL_SENSEreads zero. Note that the pulls to+3.3V_SW(R1101,R1102,R1103,R1107) are deliberately on the switched rail so they collapse with it; a pull to+3.3V_MCUin any of those positions would back-power+3.3V_SWto roughly 1.3 V through the TMC's protection diodes, defeat theHV_ENpower-cycle and corruptSW_RAIL_SENSE. If this measurement shows a residual voltage on+3.3V_SW, check those four first. - Exercise the reset sequence with a scope: isolate (
~{BUS_SW_EN}high), dropHV_EN, confirm+3.3V_SWcollapses with no MCU pin driving into it; then raiseHV_EN, confirmSW_RAIL_SENSE, and connect. Getting this order wrong is the failure the architecture exists to prevent, so prove it rather than assuming it. Note that isolation here is not provided by the bus switch'sIoff—U601-U604run from+3.3V_MCUand stay powered — but by the MCU holding~{BUS_SW_EN}high, withR6017.87 kΩ as the reset default. That makes this a firmware-dependent property and therefore worth proving on the scope rather than reasoning about. - No current flows back into the USB host or into pyControl from any other source.
- The analog output reaches a true 3.300 V full scale with the 36 V supply
present. Updated 2026-08-26; this used to say 5.00 V, and the diagnostic
attached to it no longer applies — full scale now comes from the DAC's
internal reference, so a low
+5V_SWdoes not scale the output down. Check the rail directly instead: it must be above theOPA197's 4.5 V minimum. - Rail voltages, sequencing, and ripple are measured with the motor loaded.
- Motor-rail transient overshoot is measured at the TMC5130A
VSpins during motion start, stop, and a deliberate stall. Compare against the 46 V operating and 49 V absolute limits. This is the measurement that justifies or condemns the 36 V choice. It now bounds two things, not one: no TVS is populated, andC1213/C1214are 100 µF 50 V parts on a 36 V rail — 72% of rating — so the same capture is the only evidence that the bulk capacitors' margin survives the transient as well. Record the peak against 50 V, not only against 46 V. - Overshoot on
HV_INis measured while hot-plugging the terminal block, against the switch's 70 V and the buck's 65 V. This is the event the TVS is there for — and no TVS is populated, so this is now an unprotected measurement. Take it with a fast probe and be prepared for the answer to be a populate decision. HV_ENturn-on into the final+36V_SWcapacitance — ~208 µF — completes without tripping the switch's thermal shutdown, from both a cold and a warm start.HV_SNStracks measured load current, and the fault level stays under 3.3 V at the ADC pin when a fault is provoked. RaiseGP14(HV_DIAG_EN) first —HV_SNSand~{HV_FAULT}report nothing untilDIAG_ENis high, so a "dead sense" reading withGP14low is the expected behavior, not a defect. Expect 1.3-1.6 V atGP28on a fault (ISNSFLT5.2-6.4 mA intoR509249 Ω) against ≤0.37 V for the maximum normal-current reading.~{HV_FAULT}asserts on a deliberate overload and the board reportsSUPPLY_OUT_OF_RANGE.- TMC5130A case temperature is measured; the internal regulator is confirmed offloaded.
- Continuity confirmed against real hardware for all eight
J1901RJ-45 pins. The pinout is cited from a schematic, not measured. This is the check that converts it to verified. - UART idle level measured at the RP2040 pin, not only at the connector.
PYC_UART_RXmust idle high onR401's 10 kΩ to+3.3V_MCUwith the cable unplugged. A low idle meansR401is missing or on the wrong rail and every frame will look like a break. - Cable type confirmed (straight-through versus crossover) and the consequence of the wrong one is known.
- Power and ground behavior are safe in every connection order: board first, pyControl first, cable inserted live, cable removed live.
- UART levels, direction, idle state, and protection are measured at 115200 8N1.
- A loopback or echo exchange with the real pyControl board succeeds before any motor is connected.
- Event and sync line levels and edge quality are measured.
- Reset, disconnect, and power-cycle behavior reach a safe state.
- No motor current is present on any pyControl port conductor.
- The analog output reaches its commanded endpoints, measured: 3.300 V at full
scale, and within about 25 mV of ground at zero. Updated 2026-08-26 —
this used to demand "0 V and 5 V". Both numbers were wrong and the first would
have failed a correct board: the
OPA197's output stage sits about 5 mV typical and 25 mV maximum off the negative rail at light load, so a true 0.000 V is not achievable and is not a defect. The valid angle range starts at 50 mV; 0 V is the out-of-band fault level.
Gate C: Protocol Conformance¶
- A host simulator or protocol fixture and the firmware agree on framing and versions.
- Malformed, duplicated, delayed, dropped, and out-of-order input is handled.
- Repeated sequence numbers are re-acknowledged without re-execution.
- An unrecognized verb is rejected, never partially executed.
- Timeouts and faults are observable and recover only through declared paths.
- Capability discovery reports the axis count and the board serial, and rejects incompatible versions.
- Motion verbs are refused outside
ARMED. - Every fault code can be provoked and observed, including the
vsenseself-test path in bit 7. - Two device-class instances on two ports do not collide on event names.
Status 2026-09-03. The corpus requirement is met: two independent
implementations are held to
mouse-joystick-firm/pycontrol/tests/fixtures/protocol_cases.txt — the Python
simulator via pycontrol/tests/test_fixture_corpus.py and the firmware via
test/native/test_protocol/test_protocol.cpp. Covered by that corpus and its
companion unit tests:
| Bullet | Covered |
|---|---|
| Simulator/fixture and firmware agree on framing and versions | yes, both halves reproduce the corpus byte for byte |
| Malformed and duplicated input handled | yes — bad frames, missing and unreadable seq, empty verb, over-length lines, CRLF, partial lines |
Repeated seq re-acknowledged without re-execution |
yes, and restricted to state-changing commands per open question 1 |
| Unrecognized verb rejected, never partially executed | yes |
Motion verbs refused outside ARMED |
yes, including the ordering — ARMED is checked before the range check |
| Capability discovery reports axis count and board serial | yes |
Still not covered, and none of it is a corpus problem:
- Delayed and out-of-order input. No corpus case reorders or delays a frame.
- Every fault code provoked, including the
vsensepath in bit 7. Three of the nine are reachable from the corpus (host_timeout,home_timeout,internal/vsense); the rest need injection hooks the firmware half does not have yet. - Two device-class instances not colliding on event names. Host-side only,
covered by the Python device-class tests, not by the corpus.
What the corpus does NOT tell you, learned the hard way 2026-09-03. A corpus
held by two independent implementations pins protocol behaviour. It says
nothing about hardware preconditions, because one of the two implementations
has no hardware.
status-is-safe-before-anything-is-configuredpassed on the host and hung the core on the board, because the firmware'sSTATUSreached for SPI before the bus existed. Read a green corpus as "the two ends agree about the wire", not as "the firmware is right". Full account indocs/bringup-log.mdunder board 3, stage 4.
First protocol contact on hardware passed 2026-09-03: id over the RJ-45
with the 36 V supply off, on board E4654C159F591D34 through Breakout 1.2 port
1. That covers the "agree on framing and versions" bullet on real hardware for
one command. Everything past id is still unexercised.
- Anything on real hardware. The firmware protocol layer is now wired --
examples/PyControlLinkdrives UART0 on the RJ-45 and the real TMC5130A throughsrc/TmcMotion.hpp-- but it has never been executed. Stage 3 validated the blocking motion inMotionSetup;TmcMotionis a faithful non-blocking rearrangement of the same primitives, and faithful is an argument rather than a measurement. Specifically unproven: the non-blocking homing state machine, the travel limits derived from bench geometry, and every timing above. First run belongs on the bench, supervised. - Two-stage homing over the protocol.
TmcMotionhomes in a single stage, deliberately: two-stage and single-stage repeatability measured the same (2-5 encoder counts) and the slow creep buys datum accuracy, not precision. The cost is a datum that moves with approach velocity — 89 microsteps of offset at 0.200 rev/s against 4.5 at the creep.
Gate D: Bench Operation¶
- Homing is deterministic and bounded, verified over repeated runs.
- Actual coil current is measured, not inferred from IRUN. Expect 0.679 A RMS
at
vsense = 1, IRUN 28. A reading near 1.23 A meansvsenseis 0 and the motor is being driven at ~1.8× its rating — stop immediately. - CHOPCONF read-back confirms
vsense = 1andIRUN = 28before any sustained run, and beforeDRV_ENNis raised at all. The 0.679 A figure is only true against 0.15 Ω shunts, so confirmR1203/R1204on the physical board, not in the schematic: thekicad-libstmc5130block ships 0.12 Ω, and a re-placed block silently reverts them. - Sense-resistor temperature rise is measured during a standstill hold at full current, which is this rig's normal operating condition rather than an edge case.
- Travel, velocity, acceleration, and current limits are verified by attempting to exceed them.
- Encoder angle is verified against an independent angular measurement.
- ~~TMC5130A count and PIO count agree; a deliberate disagreement raises
ENCODER_MISMATCH.~~ Struck 2026-08-16 — there is no PIO count. Replaced by: quadrature edges captured on InstrumentPortDIG0/DIG1agree withX_ENCread over SPI in the same capture. - ~~The synthetic
REFLstimulus onGP9exercises homing without the real switch.~~ Struck —TMC_REF_STIMwas deleted on 2026-08-13 andGP9is now~{BUS_SW_EN}. Replaced by: closing the real switch produces an edge on InstrumentPortDIG11, and the driver reacts in the next SPI poll. - Heartbeat timeout stops motion within a measured time and drops
HV_EN. - Cable removal during motion stops the motor and latches a fault.
- The analog output is verified against the encoder count in the same InstrumentPort capture, including latency. Do not accept a firmware log as evidence here — the DAC bus is on the connector precisely so this can be measured.
- If
EXT_FAULT_INis fitted, asserting it stops motion and reportsEXT_STOP.
Gate E: Native pyControl Operation¶
- An unforked upstream pyControl installation loads the device class through its normal supported mechanism.
- A native task commands the axis and receives status, completion, and fault events.
- Pull events arrive on the correct line and are attributed correctly in the pyControl log.
- Event line timestamps in the pyControl log agree with the board's local timestamps after Rsync alignment, within a stated uncertainty.
- If two boards are deployed: both are loaded, both report distinct serials,
both receive
RSYNC, and cross-joystick event ordering is reconstructed with a stated uncertainty. Record that uncertainty — it is the number that decides whether the two-board form factor is scientifically adequate. - The pyControl I/O actually consumed matches what was budgeted.
- Scientists can use the normal pyControl workflow and native output files.
- Loom, Spiders, and Orb Weaver engineering services are absent from the live path.
Gate F: Reproduction And Reflection¶
- The engineering build and bench validation resolve from an Orb Lock.
- A Reflection identifies inputs, versions, configurations, clocks, evidence, results, and remaining unknowns.
- Calibration values are recorded against the board serial, not against the joystick position.
- Native files remain usable in their normal tools.
- Another engineer can reconstruct the tested system without undocumented personal knowledge.
Instrument-Specific Validation¶
This list is for legacy InstrumentPort-40 hardware. New digital-port acceptance uses the migration checklist, including the opposite hardware-enable default.
- Buffers default to high-Z at power-on, before firmware runs. All three packages
share
~{INSTR_BUF_OE}onGP24, each with its own 10 kΩ pull-up (R1601/R1701/R1801), so the default is set by hardware in triplicate. - Firmware enables buffers at startup and reports buffer state in
ID/STATUS. - The eleven live Strands read defined levels with the switched domain off.
Because the taps sit on the A / switched side of the bus switch, every TMC
signal on the port collapses to 0 V when
HV_ENis low. Confirm that this is what the capture shows rather than a floating trace, which is the same visual and a different fault. DIG12carries nothing. Earlier drafts describe a buffered static low there;U1801's spare output stops atR1805andJ1501pin 20 is a no-connect. Confirm the analyzer channel is unassigned rather than reporting a stuck low.- With buffers enabled, no observable effect on rig behavior: repeat a Gate D motion and timing check with the analyzer attached and detached, and compare.
- The instrument cannot drive any board net. Confirm
AOUT*are unpopulated and the buffers are unidirectional. - A session-length capture is demonstrated end to end.
- The physical cable arrangement survives a realistic rig session.
Evidence Notes¶
Record measured values, not pass/fail marks, in docs/bringup-log.md. A number
with units can be re-examined later; a checkmark cannot.
For timing claims that software logs cannot support on their own, use the InstrumentPort-40 capture path. That is what it is on the board for.