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 Fundamentals
Section titled “JTAG Fundamentals”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.
TAP State Machine
Section titled “TAP State Machine”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.
Scan Chains: IR and DR
Section titled “Scan Chains: IR and DR”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.
IDCODE Register Format
Section titled “IDCODE Register Format”After reset, the default DR is the 32-bit IDCODE register. Its format:
| Bits | Field | Description |
|---|---|---|
| 31:28 | Version | Silicon revision |
| 27:12 | Part Number | Chip identifier |
| 11:1 | Manufacturer ID | JEDEC JEP106 code (11 bits) |
| 0 | Fixed | Always 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
How jtag_scan() Works
Section titled “How jtag_scan() Works”jtag_scan() reads the IDCODE of every TAP on the chain:
- The TAP state machine is driven to Test-Logic-Reset (5x TMS=1)
- After reset, the IDCODE instruction is loaded by default
- The state machine moves to Shift-DR
- 32 bits are shifted out of each TAP in the chain
- Each IDCODE is parsed and returned as a
TAPResultwith 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.
Raw Access with jtag_shift()
Section titled “Raw Access with jtag_shift()”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 IRjtag_shift(tap="stm32f1x.cpu", operation="irscan", value="0x0E")
# Shift out the 32-bit IDCODE from the DRjtag_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 Fundamentals
Section titled “SWD Fundamentals”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.
Packet Structure
Section titled “Packet Structure”A single SWD transaction has three phases:
Host request (8 bits):
| Bit(s) | Field | Description |
|---|---|---|
| 0 | Start | Always 1 |
| 1 | APnDP | 0 = Debug Port, 1 = Access Port |
| 2 | RnW | 0 = Write, 1 = Read |
| 3:4 | Addr[2:3] | Register address bits (selects register within DP or AP) |
| 5 | Parity | Even parity over APnDP + RnW + Addr |
| 6 | Stop | Always 0 |
| 7 | Park | Always 1 |
Turnaround: The bus direction flips. The probe releases SWDIO and the target drives it.
Target acknowledge (3 bits):
| Value | Meaning |
|---|---|
001 | OK — transaction accepted |
010 | WAIT — target busy, retry |
100 | FAULT — 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.
DPIDR — SWD’s IDCODE
Section titled “DPIDR — SWD’s IDCODE”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).
No Daisy-Chaining
Section titled “No Daisy-Chaining”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.
Debug Port and Access Port Architecture
Section titled “Debug Port and Access Port Architecture”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.
Debug Port (DP)
Section titled “Debug Port (DP)”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:
| Register | Address | Description |
|---|---|---|
| DPIDR | 0x0 (read) | Debug Port Identification Register |
| CTRL/STAT | 0x4 | Control and status (power-up, error flags) |
| SELECT | 0x8 | Selects which AP and AP register bank to access |
| RDBUFF | 0xC (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 Port (AP)
Section titled “Access Port (AP)”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.
MEM-AP
Section titled “MEM-AP”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:
| Register | Offset | Description |
|---|---|---|
| CSW | 0x00 | Control/Status Word — sets transfer size (8/16/32 bit), auto-increment mode, and access permissions |
| TAR | 0x04 | Transfer Address Register — the target address for the next read or write |
| DRW | 0x0C | Data 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):
- mcjtag parses the hex address, validates count and width, calls
openocd-python’smemory.read_u32(0x20000000, 4) - OpenOCD configures the MEM-AP CSW register for 32-bit transfers with auto-increment
- OpenOCD writes
0x20000000to the TAR register - OpenOCD reads DRW four times — each read returns one 32-bit word and TAR auto-increments by 4
- 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
- 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.
CoreSight Component Table
Section titled “CoreSight Component Table”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.
| Component | Full Name | Purpose | mcjtag Support |
|---|---|---|---|
| DAP | Debug Access Port | DP + AP, memory and register access | Full — core functionality |
| ITM | Instrumentation Trace Macrocell | printf-style debug output via SWO pin | Not yet |
| DWT | Data Watchpoint and Trace | Hardware watchpoints, cycle counter, PC sampling | Not yet |
| FPB | Flash Patch and Breakpoint | Hardware breakpoints (up to 8), flash patching | Not yet |
| TPIU | Trace Port Interface Unit | Formats trace output for external capture | Not yet |
| ETM | Embedded Trace Macrocell | Full 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 registerread_memory(address="0xE0001000", count=1, width=32)
# FPB control register (breakpoint unit)read_memory(address="0xE0002000", count=1, width=32)
# ITM stimulus port 0read_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.
How mcjtag Maps to Protocols
Section titled “How mcjtag Maps to Protocols”Each mcjtag tool translates to specific protocol operations. This table shows what happens on the wire for each tool call.
| mcjtag Tool | Protocol Operation | What 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 0xE000ED00 | Reads CPUID register via DP — MEM-AP — AHB bus |
read_memory() | MEM-AP read | Writes address to TAR, reads DRW (auto-increments for sequential reads) |
write_memory() | MEM-AP write | Writes address to TAR, writes DRW (auto-increments for sequential writes) |
read_registers() | CoreSight debug register reads | Reads via debug register interface at 0xE000EDF0+ (DCRSR/DCRDR) |
write_register() | CoreSight debug register write | Writes register selector to DCRSR, value to DCRDR at 0xE000EDF0+ |
target_control(halt) | Debug halt request | Writes C_HALT + C_DEBUGEN to DHCSR at 0xE000EDF0 with debug key |
target_control(step) | Debug step request | Writes C_STEP + C_DEBUGEN to DHCSR |
target_control(reset_halt) | Reset + halt | Asserts reset via AIRCR or nRST pin, catches at reset vector using vector catch |
jtag_shift() | Raw IR/DR scan | Shifts arbitrary bit patterns through the selected register on the named TAP |
flash_program() | MEM-AP writes + flash controller | Writes flash control registers to unlock, erase sectors, program words, and verify |
search_memory() | Repeated MEM-AP reads | Reads memory in 4 KB chunks, compares against the search pattern |
Register Access Detail
Section titled “Register Access Detail”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:
| Register | Address | Purpose |
|---|---|---|
| DHCSR | 0xE000EDF0 | Debug Halting Control and Status Register — halt, step, debug enable |
| DCRSR | 0xE000EDF4 | Debug Core Register Selector — which CPU register to read/write |
| DCRDR | 0xE000EDF8 | Debug Core Register Data — the value read from or written to the selected register |
To read R0, for example:
- Write
0x00000000to DCRSR (register selector = R0, read mode) - Poll DHCSR until S_REGRDY is set
- 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 Programming Detail
Section titled “Flash Programming Detail”flash_program() is the most complex wire operation. The general sequence for STM32F1:
- Unlock flash by writing the key sequence to FLASH_KEYR (
0x40022004) - Set the PG (programming) bit in FLASH_CR (
0x40022010) - Write 16-bit half-words to the target flash address via MEM-AP
- Poll FLASH_SR (
0x4002200C) for BSY flag after each write - Verify by reading back and comparing
- 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.
JTAG vs SWD: When to Use Which
Section titled “JTAG vs SWD: When to Use Which”| Consideration | JTAG | SWD |
|---|---|---|
| Pin count | 4+ (TDI, TDO, TMS, TCK) | 2 (SWDIO, SWCLK) |
| Chain scanning | Yes — multiple devices on one chain | No — point-to-point only |
| Cortex-M support | Yes | Yes (native) |
| FPGA support | Yes | No |
| RISC-V support | Yes | Varies by implementation |
| Throughput | Comparable | Comparable |
| Probe support | Universal | ARM-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.