Find the JTAG and SWD pins on any board, using a €11 STM32 Nucleo.
Open-source firmware that turns an STM32G431RB Nucleo-64 into a JTAG / SWD
pinout discoverer in the same spirit as Joe Grand's JTAGulator. Wire the
mystery pads from a target PCB to a row of pins on the Nucleo, run
SCAN ALL in a serial terminal, and the firmware brute-forces every
JTAG / SWD pin combination until something replies with a valid IDCODE
or DPIDR.
git clone https://github.com/<you>/jtag-finder
cd jtag-finder
make -C firmware && make -C firmware flash # build + st-flash
miniterm /dev/ttyACM0 115200 # any serial terminalThen in the terminal:
> PINS 4
OK pins=4
> SCAN ALL
JTAG TCK=PB0 TMS=PB1 TDI=PB2 TDO=PB3 IDCODE=0x4BA00477 MFG=ARM
JTAG_DONE hits=1
SWD SWCLK=PB0 SWDIO=PB1 DPIDR=0x2BA01477 MFG=ARM
SWD_DONE hits=1
That's the whole workflow. Wire 4-8 pads to PB0..PB7 on the Nucleo, set
PINS to the number of pads, run SCAN, read the hits.
Joe Grand's JTAGulator pioneered this — a small standalone box that brute- forces every plausible JTAG / SWD pin assignment until a device responds. It costs ~$199, has closed firmware, and is the right answer if you need 24 channels and UART discovery.
jtag-finder is the cheap, open-source, 8-channel, JTAG+SWD-only
alternative for hobbyists, students, CTF players, and quick PCB
investigations.
| Feature | JTAGulator | jtag-finder |
|---|---|---|
| Channels | 24 | 8 |
| JTAG IDCODE scan | yes | yes |
| SWD DPIDR scan | yes | yes |
| UART pin discovery | yes | no (planned) |
| Adjustable target voltage | yes (1.4-5.5 V) | no (3.3 V only) |
| Firmware | closed | MIT, ~5.9 KB |
| Hardware cost | ~$199 | ~€11 Nucleo + jumper wires |
If you need any of the JTAGulator-only rows above, buy the JTAGulator. If you don't, build this in 20 minutes.
- STM32G431RB Nucleo-64 board (USB Type-C connector).
- A handful of female-to-female jumper wires (or grabber leads) to reach the target's pads.
- A common GND between the Nucleo and the target. USB ground usually suffices.
- A 3.3 V tolerant target. This firmware does not level-shift.
- A serial terminal on the host (miniterm, picocom, PuTTY, screen).
Toolchain to build:
arm-none-eabi-gcc(13.x tested)arm-none-eabi-objcopy,arm-none-eabi-nm,arm-none-eabi-readelfstlink-tools(st-flashbinary)- GNU Make
- Python 3.10+ with
pyserialfor the test suite
All commands end with \r; replies are line-based ASCII.
| Command | Description |
|---|---|
V |
Print firmware version |
PINS <n> |
Set the number of active scan pins (1-8) |
SCAN JTAG |
Try every (TCK, TMS, TDI, TDO) permutation |
SCAN SWD |
Try every (SWCLK, SWDIO) ordered pair |
SCAN ALL |
JTAG first, then SWD |
TRY JTAG a b c d |
Try a single tuple (debugging) |
TRY SWD c d |
Try a single pair (debugging) |
VALIDATE IDCODE 0x... |
Run the matcher on a hex value, no GPIO involved |
VALIDATE DPIDR 0x... |
Same for DPIDR |
FAKE START [0xhex] |
Turn on the internal fake TAP (self-test) |
FAKE STOP |
Turn it off |
A match looks like this:
JTAG TCK=PB0 TMS=PB1 TDI=PB2 TDO=PB3 IDCODE=0x4BA00477 MFG=ARM
SWD SWCLK=PB0 SWDIO=PB1 DPIDR=0x2BA01477 MFG=ARM
MFG=… is decoded from the JEP106 7-bit manufacturer code in the
IDCODE / DPIDR field. Thirteen vendors are recognised (ARM, ST, NXP,
Atmel, Xilinx, Cypress, Intel, Microchip, TI, ON Semi, Silicon Labs,
Maxim, Toshiba). Floating-pin readings that happen to have bit 0 set
but land on an unknown JEDEC byte get rejected — that's the cheapest
false-positive filter.
Default scan pins are PB0..PB3 (PINS 4). Extend to PB0..PB7 with
PINS 8 for 1680 JTAG permutations (~30 s scan).
STM32G431RB Nucleo-64
CN7 (left) CN10 (right)
┌─────────────┐ ┌─────────────┐
pin 8 │ GND │ pin 9 │ GND │
pin 11│ (PB7) │ pin 11│ PB7 │ <- scan #7
pin 13│ (PB6) │ pin 13│ PB6 │ <- scan #6
pin 15│ PB3 │ scan #3 │ ... │
pin 17│ (PB5) │ pin 22│ PB2 │ <- scan #2
pin 34│ PB0 │ scan #0 │ pin 24 PB1 │ <- scan #1
└─────────────┘ └─────────────┘
For the fake-target self-test (no external target required), four wires loop the scanner back to the on-board fake TAP:
PB0 ──── PA8 TCK
PB1 ──── PA9 TMS
PB2 ──── PA10 TDI
PA11 ──── PB3 TDO (PA11 drives, PB3 reads)
For each ordered tuple (TCK, TMS, TDI, TDO):
- Drive
TMS=1for 5+ TCK cycles to force Test-Logic-Reset. - Walk
TMSthrough Run-Test/Idle → Select-DR → Capture-DR → Shift-DR. - Clock 32 bits out of
TDO. After reset, the IDCODE register is loaded into DR by default (per IEEE 1149.1). - Validate: bit 0 must be
1; bits [11:1] must decode to a known JEP106 manufacturer.
For each ordered (SWCLK, SWDIO) pair:
≥50SWCLK pulses withSWDIO=1→ line reset.- Send the JTAG-to-SWD switching sequence
0xE79E(LSB first). - Another
≥50SWCLK reset. - Issue an SWD read of DP register 0 (DPIDR): start=1, APnDP=0, RnW=1,
addr=00, parity=1, stop=0, park=1 → request byte
0xA5. - Sample 3-bit ACK; expect
0b001(OK). - Read 32 data bits + 1 parity bit, validate.
EXTI line 8 fires on every rising edge of PA8. The ISR samples PA9 (TMS), advances a 16-state TAP state machine, and in Shift-DR shifts the configured IDCODE out on PA11. With four jumper wires, the same Nucleo acts as both scanner and target.
The whole thing is ~5.9 KB and reads register-for-register from RM0440. Bit-banged GPIO is fast enough — the protocol-level limit is target response time, not host CPU speed.
- 3.3 V only. Targets running below ~2.5 V may not register a logic high; targets running above 3.6 V will damage the Nucleo.
- Single-tap chains only. Multi-device JTAG chains aren't decoded.
- No UART discovery. Other tools (JTAGulator, glasgow) do that better.
- No ARM or RISC-V boundary scan beyond IDCODE. Once you have the
pinout, hand off to
openocd. - Bit-banged. ~10 kHz TCK, ~50 kbps SWD. Plenty for discovery; not enough for live debugging.
70 host-side integration tests across 8 suites, run against real
hardware (Nucleo on /dev/ttyACM0):
python3 host/run_all.py| Suite | # | Coverage |
|---|---|---|
test_build.py |
6 | Binary size, ELF entry, SP, no libc |
test_cmd.py |
14 | Every command + every error path |
test_idcode.py |
6 | IDCODE/DPIDR matcher against real chips |
test_validate_extensive.py |
10 | 13 JEDEC vendors + hex edge cases + 50× stress |
test_pins.py |
11 | PINS boundaries 0-9, TRY duplicates / out-of-range |
test_adversarial.py |
14 | Prefix isolation, RX burst, multi-cmd batch |
test_stability.py |
6 | 100×V, 50×VALIDATE, 20×FAKE, 10×SCAN+V |
test_self_loopback.py |
3 | End-to-end fake-TAP self-test (needs 4 jumpers) |
Master run completes in ~5 minutes. Each suite resets the board so order doesn't matter.
- UART pin scan (autobaud + framing detection).
- SWD AP scan after DPIDR — AP IDR, MEM-AP CSW, base addr, ROM table.
- JSON output for tooling integration (
openocdcfg generation). - Target Vcc sensing via ADC before driving pins.
- More JEDEC manufacturers in the matcher.
PRs welcome. Run host/run_all.py before opening one — it must finish
70/70.
- Joe Grand / Grand Idea Studio for JTAGulator, which set the bar for this category of tool.
- OpenOCD / sigrok / glasgow for openly documenting the protocols this firmware speaks.
MIT — see LICENSE.