SWD and JTAG Debugging for ARM: Probes, Wiring, OpenOCD and GDB
Programming a chip through a serial bootloader is enough to get started, but it gives you no view inside the running program. A hardware debugger lets you stop the CPU at any line, step through code, read and change variables and registers, and see exactly which instruction caused a fault. ARM chips provide this through JTAG or Serial Wire Debug (SWD). This tutorial explains both interfaces, how to wire them, which probes to use, and how to debug from the command line with OpenOCD and GDB.
JTAG Versus SWD
JTAG (IEEE 1149.1) was originally designed for boundary-scan testing of circuit boards and was later used for CPU debugging. It needs four or five signals. ARM7 and ARM9 chips such as the LPC2148 debug only through JTAG. SWD is an ARM-defined two-wire protocol introduced with the CoreSight debug architecture. Almost all Cortex-M chips support SWD, and many support both, sharing the same pins.
| Signal | JTAG | SWD | Function |
|---|---|---|---|
| Clock | TCK | SWCLK | Clock driven by the probe |
| Data / mode | TMS | SWDIO | Bidirectional data in SWD, mode select in JTAG |
| Data in | TDI | Not used | Serial data into the chip |
| Data out | TDO | SWO (optional) | Serial data out in JTAG; trace output in SWD |
| Test reset | nTRST (optional) | Not used | Resets the debug logic |
| System reset | nRESET | nRESET | Lets the probe reset the target |
| Reference | VTref | VTref | Target I/O voltage, used by the probe for level shifting |
SWD saves pins and is the default for new designs. JTAG is still needed for ARM7 parts, and for daisy-chaining several devices on one board for boundary-scan testing.
Connectors and Probes
Two connectors are common. The older 20-pin 0.1 inch ARM JTAG header is found on LPC2148 boards and works with ULINK2 and J-Link. The 10-pin 0.05 inch Cortex Debug connector is used on most modern Cortex-M boards.
10-pin Cortex Debug connector (0.05 inch pitch) VTref 1 2 SWDIO / TMS GND 3 4 SWCLK / TCK GND 5 6 SWO / TDO KEY 7 8 NC / TDI GNDDet 9 10 nRESET
| Probe | Interfaces | Notes |
|---|---|---|
| CMSIS-DAP / DAPLink | SWD, JTAG | Open standard, cheap, supported by OpenOCD, pyOCD and Keil |
| SEGGER J-Link | SWD, JTAG | Fast, supports ARM7 and Cortex-M, own GDB server software |
| Keil ULINK2 | SWD, JTAG | Integrates with Keil uVision, common in college labs |
| ST-LINK | SWD | Built into ST boards; officially intended for ST devices |
Debugging with OpenOCD and GDB
OpenOCD talks to the probe and opens a GDB server on TCP port 3333. GDB from the GNU Arm toolchain connects to it. The example uses a CMSIS-DAP probe and an LPC1768; OpenOCD ships configuration files for many probes and targets in its interface and target folders.
Terminal 1: openocd -f interface/cmsis-dap.cfg -c "transport select swd" -f target/lpc17xx.cfg Terminal 2: arm-none-eabi-gdb app.elf (gdb) target extended-remote localhost:3333 (gdb) monitor reset halt (gdb) load # program flash (gdb) break main (gdb) continue (gdb) next # step over one line (gdb) step # step into a function (gdb) print ms_ticks (gdb) info registers (gdb) watch error_count # stop when the variable changes (gdb) x/8xw 0x2007C000 # dump 8 words of memory (gdb) backtrace
Build your program with -g so that GDB has line and variable information, and use -Og or -O0 while debugging; higher optimisation reorders code and removes variables, which makes stepping confusing. Hardware breakpoints are limited, typically between two and eight depending on the core, so GDB may refuse to add more breakpoints in flash.
Built-in Debug Helpers: DWT and ITM
Cortex-M3, M4 and M7 contain a Data Watchpoint and Trace unit (DWT) with a 32-bit cycle counter, and an Instrumentation Trace Macrocell (ITM) that can send printf-style text through the SWO pin with almost no CPU cost. The code below measures how many cycles a function takes and sends text over ITM channel 0.
#include "LPC17xx.h"
#include <stdio.h>
static void cycle_counter_init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; /* enable DWT and ITM */
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; /* start counting */
}
static void itm_print(const char *s)
{
while (*s)
ITM_SendChar((uint32_t)*s++); /* no-op if the debugger has not enabled ITM */
}
static volatile uint32_t sink;
static void work(void)
{
for (uint32_t i = 0; i < 1000u; i++)
sink += i * i;
}
int main(void)
{
char msg[48];
uint32_t start, cycles;
SystemCoreClockUpdate();
cycle_counter_init();
while (1) {
start = DWT->CYCCNT;
work();
cycles = DWT->CYCCNT - start; /* unsigned subtraction handles wrap */
snprintf(msg, sizeof msg, "work: %lu cycles, %lu us\n",
(unsigned long)cycles,
(unsigned long)(cycles / (SystemCoreClock / 1000000u)));
itm_print(msg);
for (volatile uint32_t d = 0; d < 1000000u; d++)
;
}
}
To see ITM output, enable SWO trace in your debugger with the correct core clock frequency, for example in Keil under Debug Settings, Trace, or with OpenOCD's tpiu or swo configuration commands. The DWT counter works without SWO; just read cycles in the watch window. Cortex-M0 and M0+ do not have the DWT cycle counter or ITM.
Troubleshooting Connection Problems
- Probe reports no target or wrong voltage: VTref is not connected or the board is unpowered.
- Connects once, then fails after flashing new code: the program reconfigured the SWD pins as GPIO or entered deep sleep. Connect under reset (hold nRESET while attaching) or use the chip's ISP mode to erase it.
- Random communication errors: wires are too long or the SWD clock is too fast; lower the adapter speed, for example adapter speed 1000 in OpenOCD.
- Breakpoints are skipped: code was built with optimisation and no debug information, or the ELF file in GDB does not match the code in flash.
- LPC2148 with JTAG does not connect: the JTAG pins on Port 1 must be enabled for debugging; check the board's debug jumper and that the program does not reassign P1.26 to P1.31.
Summary
JTAG uses TCK, TMS, TDI and TDO and is required for ARM7 parts like the LPC2148, while SWD needs only SWDIO and SWCLK and is standard on Cortex-M. Connect VTref, GND, the signal lines and nRESET to a probe such as CMSIS-DAP or J-Link, run OpenOCD as a GDB server, and debug with breakpoints, stepping and watchpoints. On Cortex-M3 and above, the DWT cycle counter and ITM add precise timing and low-cost text output.