Stage 1: RP2040 Bring-Up¶
Board: mouse-joystick-pcb rev 0, first assembled article received
2026-09-01. Verified against PCB git tag v0.1. Also run on a v0.3 unit
on 2026-09-09 — see the env table below, which is not the same env.
Program: examples/RP2040SelfTest, env mouse_joystick (MJ_BOARD_REV=0)
for a v0.1/v0.2 board, mouse_joystick_rev1 (MJ_BOARD_REV=1) for v0.3.
Scope: everything on the always-on +3.3V_MCU domain. Nothing else.
Record results in mouse-joystick-pcb/docs/bringup-log.md under the board's
serial, as numbers with units.
Which PCB Revision This Matches¶
The firmware pin map was taken from footprint U501 in the .kicad_pcb at tag
v0.1. Tags v0.1 and v0.2 have byte-identical RP2040 pad-to-net maps, so
the ambiguity between them does not matter. Tag v0.3 differs on exactly
three pins — GP10, GP11, GP13 — which it routes to the angle-output DAC
and v0.1/v0.2 leave unrouted. No connected pin moved.
pcb v1.0 (the board file from commit d5ae4b0, 2026-09-21, unchanged
through the fabrication package and tag v1.0) is a different map, read
off footprint U601 — the RP2040 moved into the combined MCU block and
U501 is now the 36 V switch. Eight pins change role and one leaves; see
src/BoardPins.hpp and the README's pin map. Its first firmware was written
on 2026-10-04 and first ran on unit E4654C159F274534 on 2026-10-05: 20
checks, then stages 2, A3, A6 and the QP build the same day.
Pick the env from the revision the board was fabricated at, not from "is this the newest board".
| PCB | env | MJ_BOARD_REV |
spare pads | stage 1 checks |
|---|---|---|---|---|
v0.1, v0.2 |
mouse_joystick |
0 | 12 | 27 |
v0.3 |
mouse_joystick_rev1 |
1 | 9 | 24 |
v1.0 (d5ae4b0…HEAD) |
mouse_joystick_rev2 |
2 | 2 | 20 (observed 2026-10-05 on E4654C159F274534) |
On v1.0, section [6] checks R607 (bus isolated), R403/R402 (the I2C
pair idles high on the MCU side), R401 (RX idle high) and R507 on GP23
(no 36 V fault), and prints the level of HV_LATCH on GP24 without judging
it, because the switch it belongs to is unpowered in stage 1. Section [8]
adds BRAKE_ISENSE (GP27) and ANGLE_OUT_MON (GP29), both expected at 0 V
with the rail off. The i command does nothing: the Digital-50 buffers have
no enable.
env:mouse_joystick_rev1 is not a future-only env — a v0.3 board needs
it. Build env:mouse_joystick against v0.3 and the spare-pad test drives
internal pull-ups and pull-downs onto the DAC's SPI bus and its chip select.
The bus switch is isolated at reset so it does no damage, but that is luck
rather than intent. The check-count is the tell: a v0.3 board reporting 27
checks was built with the wrong env.
Section [6] prints the designators for whichever revision was built, and they
differ: the ~{HV_FAULT} pull-up is R205 on v0.1/v0.2 and R1905 on
v0.3, and the HV_SNS bottom leg is R207 against R1907. R601 and
R403 are unchanged. The full v0.2→v0.3 mapping is in
mouse-joystick-pcb/docs/bringup-log.md → Board 4.
Reference designators in this document are v0.1 designators, and they are not
stable. Automatic annotation orders by sheet position, so inserting a part
rotates every designator after it. Between v0.1 and HEAD the 36 V input block
moved from U201/R20x to U1901/R190x, and R1901/R1902 changed meaning
entirely — 100 Ω pyControl series resistors at v0.1, 1 kΩ HV_EN/HV_DIAG_EN
series resistors at HEAD. Identify parts by value and net, not by designator.
Watching The Board With The LA5032¶
Worth setting up before you plug in USB, because it is the only diagnostic that works when USB does not. The port differs by revision; the two maps follow.
v1.0: InstrumentPort-Digital-50¶
J1301 is a 50-way 0.5 mm FFC (Hirose FH12-50S) carrying 32 buffered taps,
DIG0–DIG31, through four SN74LV541A on +3.3V_MCU. The port is
read-only and always enabled: there is no buffer enable, so the i
command does nothing on this revision and the heartbeat cannot be silenced.
It mates the la5032-instrument-port-adapter v2.0 through a Molex
Premo-Flex 15020 same-side FFC, and the mapping is straight: CHn observes
DIGn, confirmed on the bench on 2026-10-05 against three known pull
resistors and the boot signature (mouse-joystick-pcb/docs/bringup-log.md,
unit E4654C159F274534; a reversing cable would have shown CHn as
CH31-n).
| Signal | DIG strand = LA5032 channel |
Useful for |
|---|---|---|
FW_TRIG0 |
0 | stage 1 proof of life, heartbeat |
FW_TRIG1 |
1 | stage 1 boot signature, firmware markers |
PYC_UART_TX, PYC_UART_RX |
2, 3 | stage 4 |
TMC_SPI_SCK, ~{TMC_CS}, TMC_SPI_COPI, TMC_SPI_CIPO |
8, 9, 10, 11 | stage 2 |
TMC_DIAG0, TMC_DIAG1, ~{TMC_DRV_EN} |
12, 13, 14 | stage 3 |
LIMIT_SW |
15 | stage 3 |
TQ_SPI_SCK, TQ_CONVST_CS, TQ_SPI_COPI, TQ_SPI_CIPO, TQ_CAL_EN |
16–20 | stage A5 (torque) |
~{BRAKE_FAULT} |
21 | stage A4 (brake) |
DAC_SDA, DAC_SCL |
22, 23 | angle and brake DACs |
HV_EN, HV_DIAG_EN, ~{HV_FAULT}, ~{BUS_SW_EN} |
24, 25, 26, 27 | power sequencing |
ENCA, ENCB |
28, 29 | stage 3 |
PG_5V, PG_12V |
30, 31 | the two bucks, which firmware never sees |
DIG4–DIG7 are tied to ground. The TMC, torque, brake and DAC taps sit on
the peripheral side of the bus switch, so they are live only with the rail
up; the triggers, the UART pair and the power-sequencing lines read with the
36 V input absent. src/BoardPins.hpp carries the same table with its
history; mouse-joystick-pcb/docs/interface-map.md is the source.
Rev 0/1: InstrumentPort-40¶
InstrumentPort-40 is J1501, enabled by firmware through ~{INSTR_BUF_OE}
and silent from reset. Through the la5032-instrument-port-adapter v1.0,
channels 0–17 map straight onto Strands DIG0–DIG15 and TRIG0–TRIG1:
| Signal | J1501 pin |
Strand | LA5032 channel | Useful for |
|---|---|---|---|---|
FW_TRIG0 |
26 | TRIG0 |
16 | stage 1 proof of life |
FW_TRIG1 |
28 | TRIG1 |
17 | stage 1 boot signature |
TMC_SPI_SCK |
5 | DIG2 |
2 | stage 2 |
TMC_SPI_COPI |
7 | DIG3 |
3 | stage 2 |
TMC_SPI_CIPO |
8 | DIG4 |
4 | stage 2 |
~{TMC_CS} |
10 | DIG5 |
5 | stage 2 |
PYC_UART_RX |
13 | DIG7 |
7 | stage 4 |
PYC_UART_TX |
14 | DIG8 |
8 | stage 4 |
ENC_A |
2 | DIG0 |
0 | stage 3 |
ENC_B |
3 | DIG1 |
1 | stage 3 |
LIMIT_SW |
19 | DIG11 |
11 | stage 3 |
Unlisted odd pins from 1 to 31 are GND; connect at least one, more for a fast
or many-channel capture. All eleven taps are buffered by SN74LVC125A packages
running from +3.3V_MCU, so they work with the 36 V input absent — the
buffers are not on the switched domain.
This mapping carried stages 1 to 3 on the rev 0 boards (t burst, I
beacon and the stage 3 encoder captures in mouse-joystick-pcb/docs/bringup-log.md).
Confirm it on a new setup with the boot signature, which is a known pattern on
known pins.
For stage 1 only the two trigger channels matter (0 and 1 on v1.0, 16 and 17 on rev 0/1), plus a ground.
Before You Plug Anything In¶
The 36 V input stays unplugged for all of stage 1. The MCU domain is fed from
USB VBUS or pyControl pin 6 through two ORing diodes and never from the 36 V
chain, so USB alone runs this firmware. Applying 36 V now would gain nothing and
risk an unproven board.
Leave the pyControl RJ45 unplugged too. It is a second power source, and having only one makes BOOTSEL recovery a one-cable operation instead of a two-cable one.
Nothing needs to be connected to the stepper, encoder, or limit switch connectors.
Step 0 — Does The Bootloader Come Up?¶
This is a hardware test, before any firmware exists. Hold the BOOTSEL tactile switch, plug in USB, release.
A mass storage device named RPI-RP2 should appear.
lsblk -o NAME,LABEL,SIZE | grep -i RPI-RP2
If you have the LA5032 on the trigger channels, note that the bootloader drives neither — no edges at this stage is correct.
If it appears, a great deal is already proven: the 3.3 V regulator, the RP2040 itself, the 12 MHz crystal (USB full speed needs 48 MHz within 0.25%, derived from it), the USB connector and its series routing, and the QSPI flash enough to be written.
If it does not appear:
| Symptom | Suspect |
|---|---|
| No USB device at all, host logs nothing | +3.3V_MCU. The chain is USB-C J301 → VBUS → ORing Schottky D301 → +5V_MCU → U701 TPS73733 → +3.3V_MCU. Measure +5V_MCU first: it splits the diode from the regulator. |
| Host logs a device but enumeration fails | crystal, or USB_DP/USB_DM routing. |
| Enumerates but no mass storage | flash U502: QSPI_SS/QSPI_SCLK/QSPI_SD0..3. |
| Never enters BOOTSEL at all | the button circuit: SW501 shorts ~{USB_BOOT} to GND, R505 1 kΩ ties that to QSPI_SS, R504 10 kΩ pulls QSPI_SS up to +3.3V_MCU. Standard circuit, so suspect the switch solder joints. |
Remember there is no SWD header on rev 0, so there is no debugger to fall back on. The next instrument is a meter and a scope.
Step 1 — Flash The Self Test¶
With the board in BOOTSEL:
make upload PORT=/dev/ttyACM0
picotool finds the board in BOOTSEL by USB id, so PORT does not matter for
this first upload.
After this one, you should not need the button again: the running firmware
honours the 1200 baud touch that PlatformIO uses to reboot into the bootloader,
and the self test's B command reboots into BOOTSEL on demand. That matters
more on this board than on a Pico — with both USB and pyControl connected, the
manual BOOTSEL dance needs both cables out, because the MCU domain is ORed from
both.
Step 2 — Proof Of Life That Does Not Need USB¶
The firmware emits a boot signature on the instrument port triggers before
it brings up Serial, and then a 1 Hz heartbeat on FW_TRIG0 forever after.
FW_TRIG1 is LA5032 channel 1 on v1.0 (DIG1) and 17 on rev 0/1 (TRIG1);
FW_TRIG0 is channel 0 (DIG0) and 16 (TRIG0):
FW_TRIG1 (ch 1 | 17): ___|‾|_|‾|_|‾|______________________ 3 pulses, 10 ms half period
FW_TRIG0 (ch 0 | 16): ______________|||||||| _|‾‾‾‾|____|‾ 8 pulses at 1 ms, then 1 Hz
The 10 ms cadence on TRIG1 appears nowhere else in this firmware, so it is
unambiguous. Read the combinations:
TRIG1 signature |
TRIG0 heartbeat |
Conclusion |
|---|---|---|
| present | present | The RP2040 booted from flash and is running loop(). If USB is also silent, the fault is in USB or CDC, not in the MCU, the crystal, or the flash. |
| present | stops | Firmware hung or crashed after setup(). Note when it stopped relative to the serial output. |
| absent | absent | The RP2040 is not reaching your code. Suspect flash contents, boot2, or power. Retry step 0. |
| absent | present | Wiring or channel mapping is wrong — the two are adjacent strands on either port. Check the adapter map above. |
This exists because the board has no LEDs, no test points, and no SWD header on any revision. It is the only independent witness to firmware execution.
On rev 0/1, send i on the serial console to silence the port and the
heartbeat with it. On v1.0 the buffers have no enable and i does nothing.
Step 3 — Read The Serial Output¶
make monitor PORT=/dev/ttyACM0
The test runs once at boot and prints a summary line. The board waits up to 5 s
for the monitor to attach; if you miss it, send r.
What Each Section Means When It Fails¶
[2] reset — after a picotool upload this reports software with
watchdog caused reboot: yes. That is correct, not a fault: picotool reboots
the chip through the watchdog to start the application. A genuine cold start
reads power-on.
[3] clocks — a clk_sys reading far from 133000 kHz means a PLL
misconfiguration, which would be a firmware or core problem rather than a board
one. The measurement is taken against the crystal, so it cannot see an error in
the crystal itself. For that, send k: the board emits one line per second
carrying its own microsecond count, and drift against your host's timestamps is
the crystal's absolute error. 1% shows as 10 ms per second.
[4] qspi flash — the unique id comes from a real 0x4B command over QSPI,
so a plausible answer means the part is present, selected and responding. The
aliasing probe is the interesting one: a flash smaller than 16 MiB wraps its
address space, so a read at +2 MiB would return the same bytes as offset 0 — and
offset 0 holds boot2, which is certainly not erased. ALIASES offset 0 means the
fitted part is smaller than the BOM says, or an address line is broken.
[5] ram — a failure here is an RP2040 fault, which is rare enough to
suspect the test first.
[6] resistor-defined levels — the highest-value section for a first
article, because every check maps to one part:
| Check | Part | If it reads low |
|---|---|---|
GP9 ~{BUS_SW_EN} high |
R601 7.87 kΩ to +3.3V_MCU |
Stop. The GPIO bus is not isolated at reset. Every later stage depends on this being the power-on default; with it low, an MCU pin can back-power the dead switched rail through the TMC5130A's ESD clamps. Find the resistor before going on. |
GP24 ~{INSTR_BUF_OE} high |
R1601/R1701/R1801 10 kΩ |
The instrumentation buffers are enabled at reset instead of silent. Not dangerous, but the port is meant to be passive until asked. |
GP1 PYC_UART_RX high |
R403 10 kΩ |
The pyControl RX line does not idle high. With the cable unplugged this is R403; with it plugged in, an idle UART TX would also hold it high, so test this unplugged. |
GP29 ~{HV_FAULT} high |
R205 10 kΩ to +3.3V_MCU |
Either the pull-up is missing, or the 36 V switch U201 has a latched fault. With 36 V unplugged it should be the former. This resistor was wired as a pull-down in an earlier draft of the design, so if it reads low, check which rail it goes to before suspecting a fault. |
These are captured in the first instruction of setup(), with the RP2040's
internal pulls disabled and before anything drives a pin — which has to be that
early, because the boot signature drives ~{INSTR_BUF_OE}. At reset each pad has
its internal pull-down enabled at ~50–80 kΩ; every pull-up above is 10 kΩ or
less, so the board wins by a wide margin and these levels hold even in the window
before firmware runs.
[7] spare pads — nine GPIOs routed nowhere on any revision, pulled both
ways. A pad that will not move is shorted to a rail or a neighbour; one that
reads wrong in only one direction is an open bond or a bridge. This is an
assembly check, and it is the only one the board's lack of test points does not
block.
[8] adc — with 36 V absent:
SW_RAIL_SENSE_DIV_2should read essentially zero.R401/R402hold+3.3V_SWat a true 0 V when the domain is off, and that is what makes a cross-domain pull-up to that rail a valid LOW. A non-zero reading here means the switched rail is somehow alive, which withHV_ENlow would be a real finding.HV_SNSshould read zero — but it reads zero regardless whileHV_DIAG_ENis low, which stage 1 keeps it. So this confirms the quiet state, not the sense chain. The sense chain is a stage 2 measurement.- The die temperature proves the ADC and
ADC_AVDDwork at all. On this boardADC_AVDDis tied straight to+3.3V_MCUwith no separate reference and no filter, so absolute ADC accuracy is only as good as that rail.
Step 4 — Optional, With Instruments¶
t — 5 kHz trigger burst. 1000 cycles at a 200 µs period on FW_TRIG0
(GP25) with FW_TRIG1 (GP21) at half the rate. Faster than the heartbeat, so it
also shows the pins work at a rate a real timing marker would use. A capture
showing one trigger but not the other separates a pin fault from a buffer fault
(U1301 on v1.0, U1801 on rev 0/1).
u — pyControl UART loopback. Needs DIO-A shorted to DIO-B on the RJ45.
Without a loopback fitted, zero bytes received is the correct result, not a
fault. With one, this tests GP0, GP1, R301, R302, and the connector.
k — clock tick stream. Described above.
Step 5 — Record It¶
Copy the numbers into mouse-joystick-pcb/docs/bringup-log.md under this
board's serial — the flash unique id from section 1. Not pass marks: the whole
point of recording a serial is that two boards in a rig are independently
calibrated and independently suspect.
What Stage 1 Cannot Tell You¶
- Anything about the TMC5130A, the switched rails, the 36 V input block, the motor, the encoder, or the limit switch. All of it is on the far side of the bus switch or the high-side switch.
- Whether
+5V_SWand+3.3V_SWcome up correctly. That is the first thing stage 2 does, andSW_RAIL_SENSEon GP26 is how firmware watches it. - Whether the pyControl link works end to end. Only the loopback and the idle level are in reach.