Skip to content

JTAG and SWD Protocols

JTAG and SWD are the two wire protocols that connect a debug probe to a target chip. mcjtag abstracts both behind the same set of tools, but understanding the protocol layer helps when things go wrong or when you need raw access via jtag_shift().


JTAG (IEEE 1149.1) is a serial protocol originally designed for boundary scan testing. It uses four wires: TDI (data in), TDO (data out), TMS (mode select), and TCK (clock). An optional fifth wire, TRST, resets the TAP controller.

The Test Access Port controller is a 16-state finite state machine. TMS is sampled on each rising edge of TCK to determine the next state. The key states for debug work:

TMS=1
.-----------.
| |
v |
Test-Logic-Reset --'
|
| TMS=0
v
Run-Test/Idle <-------.
| |
| TMS=1 | TMS=0
v |
Select-DR-Scan |
/ \ |
TMS=1 TMS=0 |
| | |
v v |
Select-IR-Scan Capture-DR |
| | |
TMS=0 | TMS=0| |
v v |
Capture-IR Shift-DR |
| / \ |
TMS=0| TMS=0/ TMS=1\ |
v / v |
Shift-IR ' Exit1-DR |
/ \ | |
TMS=0 TMS=1 TMS=1| |
| | v |
' Exit1-IR Update-DR-'
|
TMS=1|
v
Update-IR ----------'

The two paths that matter most:

  • Shift-DR: Data is shifted through the data register (DR). This is how memory reads, register access, and IDCODE extraction happen.
  • Shift-IR: An instruction is shifted into the instruction register (IR) to select which DR the next Shift-DR operation will access.

Five TMS=1 transitions from any state will return to Test-Logic-Reset. This is how probes recover from unknown states.

JTAG moves data through two serial registers:

  • Instruction Register (IR): Selects the active function. Common IR values on an ARM DAP: 0x0E (IDCODE), 0x0A (DPACC — Debug Port Access), 0x0B (APACC — Access Port Access).
  • Data Register (DR): The payload register whose meaning depends on the current IR value. When IR=IDCODE, the DR is 32 bits containing the chip’s identity. When IR=DPACC, the DR carries debug port read/write transactions.

Data is shifted LSB-first through TDI, and the previous register contents exit through TDO simultaneously. This is why jtag_shift() returns a value_out — you always get back what was in the register before your data arrived.

After reset, the default DR is the 32-bit IDCODE register. Its format:

BitsFieldDescription
31:28VersionSilicon revision
27:12Part NumberChip identifier
11:1Manufacturer IDJEDEC JEP106 code (11 bits)
0FixedAlways 1

Example: an STM32F103 reports IDCODE 0x1BA01477:

  • Version: 0x1
  • Part Number: 0xBA01 — ARM Cortex-M3 DAP
  • Manufacturer: 0x23B — ARM Ltd (JEDEC bank 4, 0x3B)
  • Bit 0: 1

jtag_scan() reads the IDCODE of every TAP on the chain:

  1. The TAP state machine is driven to Test-Logic-Reset (5x TMS=1)
  2. After reset, the IDCODE instruction is loaded by default
  3. The state machine moves to Shift-DR
  4. 32 bits are shifted out of each TAP in the chain
  5. Each IDCODE is parsed and returned as a TAPResult with name, IDCODE, IR length, and enabled status

In a multi-device chain (e.g., an FPGA + CPU on the same board), jtag_scan() enumerates all TAPs. SWD does not support chaining — see below.

jtag_shift() provides direct access to the IR and DR scan operations. It shifts arbitrary bit patterns through the selected register on a named TAP:

# Load the IDCODE instruction into the IR
jtag_shift(tap="stm32f1x.cpu", operation="irscan", value="0x0E")
# Shift out the 32-bit IDCODE from the DR
jtag_shift(tap="stm32f1x.cpu", operation="drscan", value="0x00000000", bits=32)

This is useful for protocols not covered by the higher-level tools — custom boundary scan sequences, vendor-specific debug registers, or TAPs that OpenOCD does not natively support.


SWD (Serial Wire Debug) is a two-wire alternative to JTAG designed by ARM for Cortex-M. It uses SWDIO (bidirectional data) and SWCLK (clock), reducing pin count from 4+ to 2. Most Cortex-M dev boards use SWD by default.

A single SWD transaction has three phases:

Host request (8 bits):

Bit(s)FieldDescription
0StartAlways 1
1APnDP0 = Debug Port, 1 = Access Port
2RnW0 = Write, 1 = Read
3:4Addr[2:3]Register address bits (selects register within DP or AP)
5ParityEven parity over APnDP + RnW + Addr
6StopAlways 0
7ParkAlways 1

Turnaround: The bus direction flips. The probe releases SWDIO and the target drives it.

Target acknowledge (3 bits):

ValueMeaning
001OK — transaction accepted
010WAIT — target busy, retry
100FAULT — error, check status register

Data phase (33 bits): 32 data bits + 1 parity bit. Direction depends on RnW: target drives data on reads, host drives on writes.

The Debug Port Identification Register (DPIDR) is the SWD equivalent of JTAG’s IDCODE. It is read automatically when OpenOCD connects to the target. probe_diagnostics() reports this value as part of its health checks.

The DPIDR uses the same 32-bit format as the JTAG IDCODE (version, part number, manufacturer ID, fixed bit 0).

SWD is point-to-point: one probe, one target. There is no equivalent of JTAG’s scan chain. If you have multiple devices on a board, each needs its own SWD connection or you need to use JTAG.

SWD v2 (ARM ADIv6) introduced multi-drop SWD, which allows multiple targets on a single SWD connection using target selection sequences. Support for this is still uncommon in both probes and OpenOCD.


Both JTAG and SWD converge on the same abstraction: the ARM Debug Interface Architecture (ADI). This defines a two-layer model that is protocol-independent.

The Debug Port is the protocol-facing side. There are two variants:

  • JTAG-DP: Accessed via JTAG IR/DR scans (IR values DPACC and APACC)
  • SWD-DP: Accessed via SWD packet transactions (APnDP=0)

Both expose the same logical registers:

RegisterAddressDescription
DPIDR0x0 (read)Debug Port Identification Register
CTRL/STAT0x4Control and status (power-up, error flags)
SELECT0x8Selects which AP and AP register bank to access
RDBUFF0xC (read)Read buffer for AP read results

The DP is the gateway to all Access Ports. To talk to an AP, the host writes the AP index and register bank to SELECT, then performs AP read/write transactions.

Access Ports sit between the DP and the target’s internal buses. A chip can have multiple APs:

  • AP 0: Usually a MEM-AP connected to the AHB system bus
  • AP 1: Sometimes a second MEM-AP for a different bus (APB, AXI)
  • AP 255: ROM table / CoreSight discovery

Each AP has its own register space. The DP’s SELECT register determines which AP and which register bank within that AP is being addressed.

The Memory Access Port is the most important AP type. It provides access to the target’s memory bus — the same bus the CPU uses. This is what makes read_memory(), write_memory(), read_registers(), and flash_program() possible.

Key MEM-AP registers:

RegisterOffsetDescription
CSW0x00Control/Status Word — sets transfer size (8/16/32 bit), auto-increment mode, and access permissions
TAR0x04Transfer Address Register — the target address for the next read or write
DRW0x0CData Read/Write — writing DRW initiates a write to the address in TAR; reading DRW initiates a read

Protocol-Level Walkthrough: read_memory("0x20000000", 4, 32)

Section titled “Protocol-Level Walkthrough: read_memory("0x20000000", 4, 32)”

Here is what happens on the wire when you call read_memory(address="0x20000000", count=4, width=32):

  1. mcjtag parses the hex address, validates count and width, calls openocd-python’s memory.read_u32(0x20000000, 4)
  2. OpenOCD configures the MEM-AP CSW register for 32-bit transfers with auto-increment
  3. OpenOCD writes 0x20000000 to the TAR register
  4. OpenOCD reads DRW four times — each read returns one 32-bit word and TAR auto-increments by 4
  5. Each of those AP register accesses is a DP transaction:
    • SWD: one SWD write packet to SELECT (to address the AP), then one SWD read/write packet for the AP register
    • JTAG: one IR scan to load APACC, then one DR scan to perform the read/write
  6. The data path from target to host: Target memory bus — AHB interconnect — MEM-AP — DP — probe — USB — OpenOCD — openocd-python — mcjtag

The auto-increment in CSW is what makes sequential reads efficient. Without it, each word would require a separate TAR write.


ARM CoreSight is a debug and trace infrastructure built into Cortex-M (and Cortex-A/R) chips. The DAP is the entry point. Other components are accessible through the MEM-AP at fixed addresses in the system region (0xE0000000+).

mcjtag currently focuses on DAP-level access: memory, registers, flash, and execution control. Trace and breakpoint components are listed here for reference.

ComponentFull NamePurposemcjtag Support
DAPDebug Access PortDP + AP, memory and register accessFull — core functionality
ITMInstrumentation Trace Macrocellprintf-style debug output via SWO pinNot yet
DWTData Watchpoint and TraceHardware watchpoints, cycle counter, PC samplingNot yet
FPBFlash Patch and BreakpointHardware breakpoints (up to 8), flash patchingNot yet
TPIUTrace Port Interface UnitFormats trace output for external captureNot yet
ETMEmbedded Trace MacrocellFull instruction trace (requires trace probe)Not yet

These components live at well-known addresses and can be read with read_memory() today:

# DWT cycle counter control register
read_memory(address="0xE0001000", count=1, width=32)
# FPB control register (breakpoint unit)
read_memory(address="0xE0002000", count=1, width=32)
# ITM stimulus port 0
read_memory(address="0xE0000000", count=1, width=32)

Reading these registers is safe and non-destructive. Writing to them changes debug behavior and should be done with care.


Each mcjtag tool translates to specific protocol operations. This table shows what happens on the wire for each tool call.

mcjtag ToolProtocol OperationWhat Happens on the Wire
jtag_scan()DR scan (IDCODE)Resets TAP state machine, shifts out IDCODE from each TAP in the chain
probe_diagnostics()MEM-AP read of 0xE000ED00Reads CPUID register via DP — MEM-AP — AHB bus
read_memory()MEM-AP readWrites address to TAR, reads DRW (auto-increments for sequential reads)
write_memory()MEM-AP writeWrites address to TAR, writes DRW (auto-increments for sequential writes)
read_registers()CoreSight debug register readsReads via debug register interface at 0xE000EDF0+ (DCRSR/DCRDR)
write_register()CoreSight debug register writeWrites register selector to DCRSR, value to DCRDR at 0xE000EDF0+
target_control(halt)Debug halt requestWrites C_HALT + C_DEBUGEN to DHCSR at 0xE000EDF0 with debug key
target_control(step)Debug step requestWrites C_STEP + C_DEBUGEN to DHCSR
target_control(reset_halt)Reset + haltAsserts reset via AIRCR or nRST pin, catches at reset vector using vector catch
jtag_shift()Raw IR/DR scanShifts arbitrary bit patterns through the selected register on the named TAP
flash_program()MEM-AP writes + flash controllerWrites flash control registers to unlock, erase sectors, program words, and verify
search_memory()Repeated MEM-AP readsReads memory in 4 KB chunks, compares against the search pattern

CPU register reads and writes do not access registers directly. The Cortex-M debug interface uses two registers in the Debug Control Block at 0xE000EDF0:

RegisterAddressPurpose
DHCSR0xE000EDF0Debug Halting Control and Status Register — halt, step, debug enable
DCRSR0xE000EDF4Debug Core Register Selector — which CPU register to read/write
DCRDR0xE000EDF8Debug Core Register Data — the value read from or written to the selected register

To read R0, for example:

  1. Write 0x00000000 to DCRSR (register selector = R0, read mode)
  2. Poll DHCSR until S_REGRDY is set
  3. Read DCRDR — it now contains the value of R0

read_registers() repeats this sequence for each requested register. The target must be halted for this to work.

flash_program() is the most complex wire operation. The general sequence for STM32F1:

  1. Unlock flash by writing the key sequence to FLASH_KEYR (0x40022004)
  2. Set the PG (programming) bit in FLASH_CR (0x40022010)
  3. Write 16-bit half-words to the target flash address via MEM-AP
  4. Poll FLASH_SR (0x4002200C) for BSY flag after each write
  5. Verify by reading back and comparing
  6. Lock flash by setting the LOCK bit in FLASH_CR

OpenOCD handles this internally through chip-specific flash drivers. mcjtag delegates entirely to OpenOCD’s program command, which handles erase, write, and verify as an atomic operation.


ConsiderationJTAGSWD
Pin count4+ (TDI, TDO, TMS, TCK)2 (SWDIO, SWCLK)
Chain scanningYes — multiple devices on one chainNo — point-to-point only
Cortex-M supportYesYes (native)
FPGA supportYesNo
RISC-V supportYesVaries by implementation
ThroughputComparableComparable
Probe supportUniversalARM-specific probes

mcjtag works with both transports. The transport is selected in the OpenOCD configuration file, not in mcjtag itself. See OpenOCD Configurations for shipped configs.

Most Cortex-M work uses SWD because it requires fewer pins and every Cortex-M chip supports it. Use JTAG when you need chain scanning, when working with FPGAs, or when the target board only exposes a JTAG header.