Tag: ARM Cortex-M

  • Learn SWD Protocol: Key Features and Implementation

    Learn SWD Protocol: Key Features and Implementation

    What’s SWD?

    Serial Wire Debug (SWD) is a two-pin interface and protocol developed by ARM specifically for debugging and programming microcontrollers. It serves as a modern, space-saving alternative to traditional JTAG (Joint Test Action Group) interfaces, making it the standard debug method for ARM Cortex-M processors. Now why am I saying this to you and why does it matter? Well because I recently got into the world of vape hacking! My wife and I have a bunch of old vapes mostly RAZ brand and I have been taking out the batteries for a while to power my ESP32 based projects but I came across stories of people flashing the board that the vapes run on and the vast majority use SWD.

    I found this awesome tutorial from Praetorian where they hacked a nicotine vape I broke open one of my vapes (I have tons more I can work with without breaking later) and found my pins I need to connect to but there was ONNNNNNNE tiny problem. I did not have a SWD probe/debugger but I did have a sh!t ton of ESP32 😂 so I started coding one.

    SWD Protocol

    The Pins

    SWD drops the pin count significantly by utilizing a bi-directional data line. It requires only two mandatory pins for operation:

    • SWDIO (Serial Wire Data Input/Output): A bi-directional line used to transfer commands and data between the host debugger (like a J-Link, ST-Link, or CMSIS-DAP probe) and the target microcontroller.
    • SWCLK (Serial Wire Clock): A clock signal driven by the host debugger to synchronize data transfers.

    In practical implementations, you will also typically route:

    RESET: An optional pin to force a hardware reset on the target during connection or firmware flashing.

    GND: A common ground reference essential for signal integrity.

    VCC / VRef: To sense the target’s operating voltage so the debugger can match logic levels.

    SWO (Serial Wire Output): An optional third pin used for single-wire tracing. It enables real-time logging and profiling without stopping execution or consuming a standard UART peripheral.

    Knowing this I created a C++ Platformio program that consists of three files: main.cpp and swd.cpp and swd.h let’s dive into them one at a time.

    Main.cpp

    #include <Arduino.h>
    #include "swd.h"
    
    static SWD  swd;
    static bool connected = false;
    
    // ── helpers ───────────────────────────────────────────────────────────────────
    
    static bool parseU32(const char *s, uint32_t *out) {
        char *end;
        unsigned long v = strtoul(s, &end, 0);
        if (end == s) return false;
        *out = (uint32_t)v;
        return true;
    }
    
    // ── command handlers ──────────────────────────────────────────────────────────
    
    static void cmdConnect() {
        Serial.println("Connecting...");
        if (!swd.connect()) {
            Serial.println("FAIL — check SWDCLK/SWDIO wiring and target power");
            connected = false;
            return;
        }
        uint32_t id = 0;
        swd.dpRead(DP_IDCODE, &id);
        uint8_t  version  = (id >> 28) & 0xF;
        uint16_t partno   = (id >> 12) & 0xFFFF;
        uint16_t designer = (id >>  1) & 0x7FF;
        Serial.printf("IDCODE   : 0x%08X\n", id);
        Serial.printf("  Version : %u\n", version);
        Serial.printf("  Part    : 0x%03X\n", partno);
        Serial.printf("  Designer: 0x%03X%s\n", designer,
                      (designer == 0x23B) ? "  (ARM Ltd)" : "");
        Serial.println("Connected.");
        connected = true;
    }
    
    static void cmdIDCODE() {
        if (!connected) { Serial.println("Not connected"); return; }
        uint32_t id = 0;
        if (!swd.dpRead(DP_IDCODE, &id)) { Serial.println("FAIL"); return; }
        Serial.printf("IDCODE: 0x%08X\n", id);
    }
    
    static void cmdHalt() {
        if (!connected) { Serial.println("Not connected"); return; }
        if (!swd.halt())    { Serial.println("FAIL: halt"); return; }
        delay(10);
        if (swd.isHalted()) {
            uint32_t pc = 0;
            swd.readCoreReg(15, &pc);
            Serial.printf("Halted. PC = 0x%08X\n", pc);
        } else {
            Serial.println("Halt sent (target may not have stopped yet)");
        }
    }
    
    static void cmdResume() {
        if (!connected) { Serial.println("Not connected"); return; }
        if (!swd.resume()) { Serial.println("FAIL: resume"); return; }
        Serial.println("Running.");
    }
    
    static void cmdStep() {
        if (!connected) { Serial.println("Not connected"); return; }
        if (!swd.isHalted()) { Serial.println("Target must be halted first"); return; }
        if (!swd.step()) { Serial.println("FAIL: step"); return; }
        delay(2);
        uint32_t pc = 0;
        swd.readCoreReg(15, &pc);
        Serial.printf("Step. PC = 0x%08X\n", pc);
    }
    
    static void cmdReset() {
        if (!connected) { Serial.println("Not connected"); return; }
        swd.sysReset();
        delay(150);
        // Re-init DAP after system reset
        connected = swd.connect();
        Serial.println(connected ? "Reset OK. Reconnected." : "FAIL: reconnect after reset");
    }
    
    static void cmdRead(const char *args) {
        if (!connected) { Serial.println("Not connected"); return; }
        uint32_t addr;
        if (!parseU32(args, &addr)) { Serial.println("Usage: read <addr>"); return; }
        uint32_t val = 0;
        if (!swd.memRead32(addr, &val)) { Serial.println("FAIL: read error"); return; }
        Serial.printf("[0x%08X] = 0x%08X\n", addr, val);
    }
    
    static void cmdWrite(const char *args) {
        if (!connected) { Serial.println("Not connected"); return; }
        char a[20] = {}, b[20] = {};
        if (sscanf(args, "%19s %19s", a, b) != 2) {
            Serial.println("Usage: write <addr> <value>"); return;
        }
        uint32_t addr, val;
        if (!parseU32(a, &addr) || !parseU32(b, &val)) {
            Serial.println("Bad arguments"); return;
        }
        if (!swd.memWrite32(addr, val)) { Serial.println("FAIL: write error"); return; }
        Serial.printf("[0x%08X] <= 0x%08X  OK\n", addr, val);
    }
    
    static void cmdDump(const char *args) {
        if (!connected) { Serial.println("Not connected"); return; }
        char a[20] = {}, b[20] = {};
        uint32_t addr, count = 16;
        int n = sscanf(args, "%19s %19s", a, b);
        if (n < 1 || !parseU32(a, &addr)) {
            Serial.println("Usage: dump <addr> [words]"); return;
        }
        if (n >= 2) parseU32(b, &count);
        if (count > 256) count = 256;
    
        for (uint32_t i = 0; i < count; i++) {
            if (i % 4 == 0) Serial.printf("\n0x%08X:", addr + i * 4);
            uint32_t val = 0;
            if (!swd.memRead32(addr + i * 4, &val)) { Serial.println("  ERR"); return; }
            Serial.printf("  %08X", val);
        }
        Serial.println();
    }
    
    static const char *REG_NAMES[] = {
        "R0", "R1", "R2",  "R3",  "R4",  "R5",  "R6", "R7",
        "R8", "R9", "R10", "R11", "R12", "SP",  "LR", "PC", "xPSR"
    };
    
    static void cmdRegs() {
        if (!connected) { Serial.println("Not connected"); return; }
        if (!swd.isHalted()) { Serial.println("Target must be halted. Use 'halt' first."); return; }
        for (int i = 0; i <= 16; i++) {
            uint32_t val = 0;
            if (swd.readCoreReg(i, &val))
                Serial.printf("  %-5s : 0x%08X\n", REG_NAMES[i], val);
            else
                Serial.printf("  %-5s : ERROR\n", REG_NAMES[i]);
        }
    }
    
    static void cmdStatus() {
        if (!connected) { Serial.println("Not connected"); return; }
        uint32_t dhcsr = 0;
        if (!swd.memRead32(DHCSR, &dhcsr)) { Serial.println("FAIL"); return; }
        Serial.printf("DHCSR = 0x%08X — %s\n", dhcsr,
                      (dhcsr & DHCSR_S_HALT) ? "HALTED" : "RUNNING");
    }
    
    static void cmdClear() {
        swd.clearErrors();
        Serial.println("SWD error flags cleared.");
    }
    
    static void cmdResetPin(const char *args) {
        if (!*args) {
            uint8_t p = swd.getResetPin();
            if (p == SWD_NO_PIN) Serial.println("nRESET pin: not set");
            else                 Serial.printf("nRESET pin: GPIO%d\n", p);
            return;
        }
        uint32_t pin;
        if (!parseU32(args, &pin)) { Serial.println("Usage: resetpin <gpio>  (or 'none')"); return; }
        swd.setResetPin((uint8_t)pin);
        Serial.printf("nRESET → GPIO%u\n", pin);
    }
    
    static void cmdNReset() {
        if (swd.getResetPin() == SWD_NO_PIN) {
            Serial.println("No nRESET pin configured. Use: resetpin <gpio>");
            return;
        }
        Serial.println("Asserting nRESET...");
        swd.assertReset();
        delay(50);
        swd.releaseReset();
        delay(150);
        Serial.println("Released. Reconnecting...");
        connected = swd.connect();
        Serial.println(connected ? "Reconnected." : "FAIL: reconnect after reset");
    }
    
    static void cmdConnectHalt() {
        if (swd.getResetPin() == SWD_NO_PIN) {
            Serial.println("Requires nRESET pin. Use: resetpin <gpio>");
            return;
        }
        Serial.println("Connecting under reset...");
        if (!swd.connectHalt()) {
            Serial.println("FAIL — check wiring and target power");
            connected = false;
            return;
        }
        connected = true;
        uint32_t pc = 0;
        swd.readCoreReg(15, &pc);
        Serial.printf("Halted at reset vector. PC = 0x%08X\n", pc);
    }
    
    // GPIO pins exposed on the XIAO ESP32-S3 headers
    static const uint8_t SCAN_PINS[]  = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 43, 44 };
    static const uint8_t SCAN_NPIN    = sizeof(SCAN_PINS);
    
    static void cmdPinset(const char *args) {
        char a[8] = {}, b[8] = {};
        if (sscanf(args, "%7s %7s", a, b) != 2) {
            Serial.println("Usage: pinset <clk_gpio> <io_gpio>");
            Serial.printf("Current: SWDCLK=GPIO%d  SWDIO=GPIO%d\n",
                          swd.getCLKPin(), swd.getIOPin());
            return;
        }
        uint32_t clk, io;
        if (!parseU32(a, &clk) || !parseU32(b, &io)) {
            Serial.println("Bad pin numbers"); return;
        }
        swd.setPins((uint8_t)io, (uint8_t)clk);
        connected = false;
        Serial.printf("Pins set: SWDCLK=GPIO%u  SWDIO=GPIO%u\n", clk, io);
        Serial.println("Run 'connect' to try the new pins.");
    }
    
    // Try every CLK/IO pair from SCAN_PINS until we get a valid IDCODE.
    static void cmdScan() {
        Serial.println("Scanning all GPIO pairs for SWD target...");
        Serial.println("(this takes ~30 s — the vape must be powered)");
        connected = false;
    
        for (uint8_t ci = 0; ci < SCAN_NPIN; ci++) {
            for (uint8_t ii = 0; ii < SCAN_NPIN; ii++) {
                if (ci == ii) continue;
                uint8_t clk = SCAN_PINS[ci];
                uint8_t io  = SCAN_PINS[ii];
                Serial.printf("  CLK=GPIO%-2d  IO=GPIO%-2d ... ", clk, io);
    
                swd.setPins(io, clk);
                uint32_t id = swd.probe();
                if (id) {
                    Serial.printf("FOUND!  IDCODE=0x%08X\n", id);
                    Serial.printf("Run: pinset %d %d\n", clk, io);
                    Serial.println("Then: connect");
                    return;
                }
                Serial.println("--");
            }
        }
        Serial.println("No SWD target found on any pair.");
        Serial.println("Check: target powered? GND shared? correct USB-C pins?");
        // Restore defaults
        swd.setPins(SWD_DEFAULT_IO, SWD_DEFAULT_CLK);
    }
    
    static void printHelp() {
        Serial.println(
            "\nConnection:\n"
            "  connect              line reset → IDCODE → power up → MEM-AP\n"
            "  connecthalt          connect under nRESET, halt at first instruction\n"
            "  scan                 sweep all GPIO pairs to find a live SWD target\n"
            "  pinset <clk> <io>    reassign SWDCLK / SWDIO GPIOs at runtime\n"
            "  resetpin [gpio]      set nRESET GPIO (show current if no arg)\n"
            "\nExecution:\n"
            "  halt                 halt the CPU\n"
            "  resume               resume execution\n"
            "  step                 single-step one instruction (must be halted)\n"
            "  nreset               pulse nRESET low then reconnect\n"
            "  reset                software reset via AIRCR SYSRESETREQ\n"
            "  status               show DHCSR — running or halted\n"
            "\nInspection:\n"
            "  regs                 dump R0-R15, xPSR  (must be halted)\n"
            "  idcode               re-read raw IDCODE\n"
            "  read  <addr>         read one 32-bit word\n"
            "  write <addr> <val>   write one 32-bit word\n"
            "  dump  <addr> [n]     hex dump n words (default 16, max 256)\n"
            "  clear                clear SWD sticky error flags (ABORT)\n"
            "\nDefault pins (XIAO ESP32-S3):\n"
            "  SWDCLK → D8 / GPIO7\n"
            "  SWDIO  → D9 / GPIO8\n"
            "  nRESET → not set    (use 'resetpin <gpio>' to assign)\n"
        );
    }
    
    // ── command dispatcher ────────────────────────────────────────────────────────
    
    static void dispatch(char *line) {
        while (*line == ' ') line++;
        if (!*line) return;
    
        char *cmd = line;
        char *args = line;
        while (*args && *args != ' ') args++;
        if (*args) { *args++ = '\0'; }
        while (*args == ' ') args++;
    
        if      (!strcmp(cmd, "connect")) cmdConnect();
        else if (!strcmp(cmd, "idcode"))  cmdIDCODE();
        else if (!strcmp(cmd, "halt"))    cmdHalt();
        else if (!strcmp(cmd, "resume"))  cmdResume();
        else if (!strcmp(cmd, "step"))    cmdStep();
        else if (!strcmp(cmd, "reset"))   cmdReset();
        else if (!strcmp(cmd, "status"))  cmdStatus();
        else if (!strcmp(cmd, "regs"))    cmdRegs();
        else if (!strcmp(cmd, "read"))    cmdRead(args);
        else if (!strcmp(cmd, "write"))   cmdWrite(args);
        else if (!strcmp(cmd, "dump"))    cmdDump(args);
        else if (!strcmp(cmd, "clear"))        cmdClear();
        else if (!strcmp(cmd, "pinset"))       cmdPinset(args);
        else if (!strcmp(cmd, "scan"))         cmdScan();
        else if (!strcmp(cmd, "resetpin"))     cmdResetPin(args);
        else if (!strcmp(cmd, "nreset"))       cmdNReset();
        else if (!strcmp(cmd, "connecthalt"))  cmdConnectHalt();
        else if (!strcmp(cmd, "help"))         printHelp();
        else Serial.printf("Unknown: '%s'  (type 'help')\n", cmd);
    
        Serial.print("> ");
    }
    
    // ── Arduino entry points ──────────────────────────────────────────────────────
    
    static char   lineBuf[128];
    static uint8_t lineLen = 0;
    
    void setup() {
        Serial.begin(115200);
        // USB CDC: wait for the host to open the port (up to ~5 s), then greet.
        // Without this the banner gets sent before the terminal is ready.
        uint32_t t = millis();
        while (!Serial && (millis() - t < 5000)) delay(10);
        Serial.println("\n=== ESP32-S3 SWD Debugger ===");
        printHelp();
        Serial.print("\n> ");
    }
    
    void loop() {
        while (Serial.available()) {
            char c = (char)Serial.read();
            if (c == '\r') continue;
            if (c == '\n') {
                lineBuf[lineLen] = '\0';
                Serial.println();
                dispatch(lineBuf);
                lineLen = 0;
            } else if (c == '\b' || c == 127) {
                if (lineLen > 0) { lineLen--; Serial.print("\b \b"); }
            } else if (lineLen < sizeof(lineBuf) - 1) {
                lineBuf[lineLen++] = c;
                Serial.print(c);
            }
        }
    }

    1. The Terminal Engine and Dispatcher

    The firmware relies on standard Arduino entry points to drive a non-blocking character parser. Inside setup(), the code initializes the hardware serial interface at 115200 baud and loops until an active USB CDC connection is established, preventing the initial banner from dropping before a terminal viewer opens.

    The main loop() continuously monitors Serial.available(). It streams incoming characters into a raw character buffer (lineBuf), tracks length limits to prevent buffer overflows, and safely handles backspaces (\b or ASCII 127) by dropping the buffer index and echoing terminal erasure commands (\b \b).

    When a newline character (\n) is intercepted, the engine seals the string with a null terminator (\0) and forwards it to the dispatch() routing function. The dispatcher strips leading whitespace, tokenizes the raw text into distinct command and argument pointers by searching for space boundaries, and matches the command string via strcmp to its respective execution handler.

    2. Low-Level SWD Discovery and Connection

    Target acquisition is managed by cmdConnect() and cmdScan(). When the connect command is issued, swd.connect() triggers a low-level line reset sequence (driving SWDIO high for more than 50 clock cycles) to wake up the target’s Debug Port (DP). If successful, it reads the raw 32-bit DP_IDCODE from the target using swd.dpRead().

    The firmware then executes bitwise shifts and masking operations to extract and print the silicon’s metadata:

    • version: Isolated by shifting 28 bits right and applying a 0xF mask.
    • partno: Isolated by shifting 12 bits right and applying a 0xFFFF mask.
    • designer: Isolated by shifting 1 bit right and applying a 0x7FF mask (checking if it matches ARM Ltd’s JEDEC identity code 0x23B).

    If the exact pinout of the target board is unknown, cmdScan() implements a automated hardware prober. It runs nested loops across a predefined array of physical GPIO headers (SCAN_PINS), dynamically reassigning clock (SWDCLK) and data (SWDIO) lines via swd.setPins() before firing a low-overhead swd.probe() request. If a valid IDCODE drops out of the sweep, the firmware breaks execution and outputs the precise pin parameters required to establish a stable link.

    3. Execution Control and Memory Architecture

    Once connected evaluates to true, the firmware can directly mutate target execution and state through the processor’s Debug Halting Control and Status Register (DHCSR). Calling cmdHalt() updates the debug registers to pause the CPU core. Once halted, it calls swd.readCoreReg(15, &pc) to query core register 15—the Program Counter (PC)—revealing exactly where execution froze. Similarly, cmdStep() advances the target by a single assembly instruction before instantly re-reading the updated Program Counter.

    For direct physical memory access, commands like read, write, and dump wrap character inputs into numerical values using parseU32 (a thin wrapper around strtoul). These handlers route transactions directly through the target’s internal bus via the Memory Access Port (MEM-AP).

    • cmdRead() and cmdWrite() invoke swd.memRead32() and swd.memWrite32() respectively to read or write individual 32-bit words at explicit memory addresses.
    • cmdDump() loops through consecutive 32-bit words up to a hard cap of 256, formatting the data into a structured hex grid over the serial console to allow rapid analysis of RAM arrays, flash boundaries, or peripheral registers at runtime.

    swd.h

    #pragma once
    #include <Arduino.h>
    
    // ACK values
    #define SWD_ACK_OK     0x01
    #define SWD_ACK_WAIT   0x02
    #define SWD_ACK_FAULT  0x04
    #define SWD_ACK_ERROR  0xFF  // parity mismatch or comms failure
    
    // DP register addresses (word-aligned, bits [3:2] used in packet)
    #define DP_IDCODE    0x00  // R
    #define DP_ABORT     0x00  // W
    #define DP_CTRLSTAT  0x04
    #define DP_SELECT    0x08
    #define DP_RDBUFF    0x0C
    
    // CTRL/STAT bits
    #define CTRL_CSYSPWRUPACK  (1UL << 31)
    #define CTRL_CSYSPWRUPREQ  (1UL << 30)
    #define CTRL_CDBGPWRUPACK  (1UL << 29)
    #define CTRL_CDBGPWRUPREQ  (1UL << 28)
    #define CTRL_STICKYERR     (1UL << 5)
    #define CTRL_STICKYORUN    (1UL << 1)
    
    // MEM-AP register offsets (bank 0)
    #define AP_CSW  0x00
    #define AP_TAR  0x04
    #define AP_DRW  0x0C
    
    // CSW value: DbgSwEnable off (set by HW), privileged, word, single-increment
    #define AP_CSW_VALUE  0x23000052UL
    
    // Cortex-M debug register addresses
    #define DHCSR  0xE000EDF0UL  // Debug Halting Control & Status
    #define DCRSR  0xE000EDF4UL  // Debug Core Register Selector
    #define DCRDR  0xE000EDF8UL  // Debug Core Register Data
    #define DEMCR  0xE000EDFCUL  // Debug Exception & Monitor Control
    #define AIRCR  0xE000ED0CUL  // Application Interrupt & Reset Control
    
    // DHCSR bits
    #define DHCSR_DBGKEY     0xA05F0000UL
    #define DHCSR_C_DEBUGEN  (1UL << 0)
    #define DHCSR_C_HALT     (1UL << 1)
    #define DHCSR_C_STEP     (1UL << 2)
    #define DHCSR_C_MASKINTS (1UL << 3)
    #define DHCSR_S_REGRDY   (1UL << 16)
    #define DHCSR_S_HALT     (1UL << 17)
    
    // DEMCR bits
    #define DEMCR_TRCENA        (1UL << 24)
    #define DEMCR_VC_CORERESET  (1UL << 0)   // halt on reset vector
    
    // Sentinel meaning "no pin assigned"
    #define SWD_NO_PIN  0xFF
    
    // Default pins for XIAO ESP32-S3
    #define SWD_DEFAULT_CLK  7   // D8 / GPIO7
    #define SWD_DEFAULT_IO   8   // D9 / GPIO8
    
    class SWD {
    public:
        SWD(uint8_t swdio = SWD_DEFAULT_IO, uint8_t swdclk = SWD_DEFAULT_CLK);
    
        // Reconfigure SWD pins at runtime (releases old pins first)
        void setPins(uint8_t swdio, uint8_t swdclk);
        uint8_t getIOPin()  const { return _io;  }
        uint8_t getCLKPin() const { return _clk; }
    
        // Optional nRESET line (open-drain, active-low). Pass SWD_NO_PIN to disable.
        void setResetPin(uint8_t pin);
        uint8_t getResetPin() const { return _rst; }
        void assertReset();   // drive nRESET LOW
        void releaseReset();  // float nRESET (let pull-up take it HIGH)
    
        // Full connection sequence: line reset → IDCODE check → power up → MEM-AP
        bool connect();
    
        // Connect under reset: assert nRESET, init DAP, arm vector-catch, release.
        // Target halts at the very first instruction after reset. Requires nRESET pin.
        bool connectHalt();
    
        // Probe-only connect: returns IDCODE if SWD link is found, 0 on failure.
        // Does NOT power up or set up MEM-AP — use connect() for full init.
        uint32_t probe();
    
        // DP-level access
        bool dpRead(uint8_t addr, uint32_t *out);
        bool dpWrite(uint8_t addr, uint32_t val);
    
        // AP-level access (handles SELECT automatically)
        bool apRead(uint8_t ap, uint8_t reg, uint32_t *out);
        bool apWrite(uint8_t ap, uint8_t reg, uint32_t val);
    
        // Memory access via MEM-AP 0
        bool memRead32(uint32_t addr, uint32_t *out);
        bool memWrite32(uint32_t addr, uint32_t val);
        bool memRead(uint32_t addr, uint32_t *buf, size_t nWords);
    
        // Cortex-M debug operations (target must be connected + powered)
        bool halt();
        bool resume();
        bool step();
        bool sysReset();
        bool isHalted();
    
        // Core register access (target must be halted)
        bool readCoreReg(uint8_t reg, uint32_t *out);
        bool writeCoreReg(uint8_t reg, uint32_t val);
    
        // Clear sticky error flags in DP ABORT
        void clearErrors();
    
    private:
        uint8_t _io, _clk;
        uint8_t _rst;              // nRESET pin, SWD_NO_PIN if unused
        uint8_t _selAP, _selBank;  // cached SELECT state
    
        void ioOut();
        void ioIn();
        void writeBit(uint8_t b);
        uint8_t readBit();
        void clkPulse();
    
        void lineReset();
        void jtagToSWD();
        void idleCycles(uint8_t n);
    
        uint8_t buildReq(bool ap, bool rd, uint8_t reg);
        uint8_t xfer(bool ap, bool rd, uint8_t reg, uint32_t *data);
    
        bool selectAP(uint8_t ap, uint8_t bank);
        bool powerUp();
        bool setupMEMAP();
    };

    1. Protocol Tokens and Register Footprints

    The top half of the header uses standard preprocessor directives (#define) to build a rigid dictionary of the ARM architecture’s hardware requirements. The first block establishes the specific response bytes the target can return during a transaction turnaround, such as SWD_ACK_OK or SWD_ACK_WAIT.

    Following this are the hardcoded addresses for both the Debug Port (DP) and Access Port (AP) registers:

    • DP Registers: DP_IDCODE reads device identification, while DP_SELECT handles routing between different internal Access Ports and register banks.
    • AP Registers: Low-level memory bus operations route through the Memory Access Port using AP_CSW (Control Status Word), AP_TAR (Transfer Address Register), and AP_DRW (Data Read/Write). The specific bitmask AP_CSW_VALUE instructs the target to execute memory accesses as clean 32-bit words with automatic address incrementing turned on.
    • Cortex-M Debug Registers: The final macro block maps the memory locations for the Core Debug registers located at the top of the processor’s system space (the 0xE000EDF0 block). This includes DHCSR, which forces the CPU to halt or single-step, and DEMCR, which allows the host to trap the processor at its very first boot instruction using vector catching (DEMCR_VC_CORERESET).

    2. The Public High-Level API

    The public block of the SWD class exposes a clean interface for application-level code to interrogate a target device without needing to understand raw bit arrangements. The lifecycle begins with initialization, where the constructor assigns the default clock and data pins (configured here for the Seeed Studio XIAO ESP32-S3’s hardware map), while setPins() and setResetPin() manage physical wire reassignments at runtime. Physical reset control is exposed through open-drain abstractions (assertReset and releaseReset) to cleanly float or pull down the target’s physical nRESET line.

    Connection management is divided into three functional paradigms:

    • probe() acts as a fast, low-overhead link verifier that checks for an IDCODE without modifying system state.
    • connect() executes the full operational sequence required to power up the internal debug domains and latch onto the core.
    • connectHalt() implements a specialized sequence that pins the hardware reset line, arms a vector catch, and releases the reset to catch the processor before any firmware can execute.

    Once an active connection is established, developers are provided with high-level functions like halt(), resume(), step(), and register access blocks (readCoreReg/writeCoreReg). These are backed by automated memory methods like memRead32() and memWrite32(), which let you read and write data across the target’s entire internal memory map through a single call.

    3. The Private Bit-Banging Engine

    The private partition contains the raw, low-overhead state tracking and driver primitives needed to translate high-level commands into physical voltage fluctuations on the wire. The integer variables _selAP and _selBank act as a local software cache. By remembering which Access Port and register bank the host is currently talking to, the code avoids sending redundant DP_SELECT write packets over the wire, optimizing data throughput during massive memory dumps.

    The lower methods handle wire-level operations:

    • ioOut() and ioIn() rapidly reconfigure the microcontroller’s shared SWDIO data pin between input and output modes.
    • writeBit(), readBit(), and clkPulse() form the basic clock-synchronized IO foundation.
    • lineReset() and jtagToSWD() output the exact bitstream signatures (such as driving the data line high for 50+ cycles followed by a specific 16-bit switching key) required to reset the wire state machine and kick the target out of JTAG mode into SWD mode.
    • buildReq() and xfer() assemble the mandatory 8-bit request frame (comprising start bits, AP/DP selectors, read/write flags, address bits, and parity), transmit the command, handle the wire turnaround phase, and verify incoming target acknowledgements and data parity bits.

    swd.cpp

    #include "swd.h"
    
    // Half-period in µs → clock frequency ≈ 1/(2*HALF_US) Hz
    // 1 µs → ~500 kHz; safe for most Cortex-M targets
    #define HALF_US 1
    
    SWD::SWD(uint8_t swdio, uint8_t swdclk)
        : _io(swdio), _clk(swdclk), _rst(SWD_NO_PIN),
          _selAP(0xFF), _selBank(0xFF) {}
    
    void SWD::setPins(uint8_t swdio, uint8_t swdclk) {
        pinMode(_io,  INPUT);
        pinMode(_clk, INPUT);
        _io      = swdio;
        _clk     = swdclk;
        _selAP   = 0xFF;
        _selBank = 0xFF;
    }
    
    void SWD::setResetPin(uint8_t pin) {
        if (_rst != SWD_NO_PIN) pinMode(_rst, INPUT);  // release old pin
        _rst = pin;
        if (_rst != SWD_NO_PIN) pinMode(_rst, INPUT);  // start released (hi-Z)
    }
    
    // nRESET is open-drain active-low: drive LOW to assert, hi-Z to release.
    void SWD::assertReset() {
        if (_rst == SWD_NO_PIN) return;
        pinMode(_rst, OUTPUT);
        digitalWrite(_rst, LOW);
    }
    
    void SWD::releaseReset() {
        if (_rst == SWD_NO_PIN) return;
        pinMode(_rst, INPUT);  // let the target's pull-up take it HIGH
    }
    
    // ── pin helpers ──────────────────────────────────────────────────────────────
    
    void SWD::ioOut() { pinMode(_io, OUTPUT); }
    void SWD::ioIn()  { pinMode(_io, INPUT);  }
    
    // Clock starts LOW. Data is driven before clock rises; sampled at rising edge.
    void SWD::writeBit(uint8_t b) {
        digitalWrite(_io, b & 1);
        delayMicroseconds(HALF_US);
        digitalWrite(_clk, HIGH);
        delayMicroseconds(HALF_US);
        digitalWrite(_clk, LOW);
    }
    
    uint8_t SWD::readBit() {
        delayMicroseconds(HALF_US);
        digitalWrite(_clk, HIGH);
        uint8_t b = digitalRead(_io);  // sample at rising edge
        delayMicroseconds(HALF_US);
        digitalWrite(_clk, LOW);
        return b;
    }
    
    void SWD::clkPulse() {
        delayMicroseconds(HALF_US);
        digitalWrite(_clk, HIGH);
        delayMicroseconds(HALF_US);
        digitalWrite(_clk, LOW);
    }
    
    // ── line-level sequences ─────────────────────────────────────────────────────
    
    void SWD::lineReset() {
        ioOut();
        digitalWrite(_io, HIGH);
        for (int i = 0; i < 56; i++) clkPulse();
    }
    
    // Switch from JTAG to SWD: send 0xE79E as 16 bits, LSB first
    void SWD::jtagToSWD() {
        ioOut();
        uint16_t seq = 0xE79E;
        for (int i = 0; i < 16; i++) writeBit((seq >> i) & 1);
    }
    
    void SWD::idleCycles(uint8_t n) {
        ioOut();
        digitalWrite(_io, LOW);
        for (uint8_t i = 0; i < n; i++) clkPulse();
    }
    
    // ── SWD packet transfer ───────────────────────────────────────────────────────
    
    // Build the 8-bit request header (Start|APnDP|RnW|A[2]|A[3]|Parity|Stop|Park)
    uint8_t SWD::buildReq(bool ap, bool rd, uint8_t reg) {
        uint8_t A2  = (reg >> 2) & 1;
        uint8_t A3  = (reg >> 3) & 1;
        uint8_t par = (uint8_t)ap ^ (uint8_t)rd ^ A2 ^ A3;
        return (1        )       // Start
             | ((uint8_t)ap << 1) // APnDP
             | ((uint8_t)rd << 2) // RnW
             | (A2 << 3)          // A[2]
             | (A3 << 4)          // A[3]
             | (par << 5)         // Parity
             | (0  << 6)          // Stop
             | (1  << 7);         // Park
    }
    
    // Single SWD packet. Returns SWD_ACK_* constant.
    // For writes, *data must point to the value to write.
    // For reads, *data is filled with the received value on SWD_ACK_OK.
    uint8_t SWD::xfer(bool ap, bool rd, uint8_t reg, uint32_t *data) {
        // ── request phase (host drives) ──────────────────────────────────────────
        uint8_t req = buildReq(ap, rd, reg);
        ioOut();
        for (int i = 0; i < 8; i++) writeBit((req >> i) & 1);
    
        // ── turnaround: host releases SWDIO ─────────────────────────────────────
        ioIn();
        clkPulse();
    
        // ── ACK (3 bits, target drives) ─────────────────────────────────────────
        uint8_t ack = 0;
        for (int i = 0; i < 3; i++) ack |= (readBit() << i);
    
        if (rd) {
            // ── READ: target continues to drive data ─────────────────────────────
            if (ack == SWD_ACK_OK) {
                uint32_t val = 0;
                uint8_t  par = 0;
                for (int i = 0; i < 32; i++) {
                    uint8_t b = readBit();
                    val |= ((uint32_t)b << i);
                    par ^= b;
                }
                uint8_t rxPar = readBit();
    
                // turnaround: target releases, host takes over
                ioOut();
                clkPulse();
    
                if (data) *data = val;
                return (par == rxPar) ? SWD_ACK_OK : SWD_ACK_ERROR;
            } else {
                // WAIT or FAULT: turnaround then idle
                ioOut();
                clkPulse();
                idleCycles(8);
                return ack;
            }
        } else {
            // ── WRITE: turnaround, then host drives data ──────────────────────────
            ioOut();
            clkPulse();  // turnaround
    
            if (ack == SWD_ACK_OK && data) {
                uint8_t par = 0;
                for (int i = 0; i < 32; i++) {
                    uint8_t b = (*data >> i) & 1;
                    writeBit(b);
                    par ^= b;
                }
                writeBit(par);
            }
            idleCycles(2);  // capture / idle
            return ack;
        }
    }
    
    // ── DP access (with WAIT retry) ───────────────────────────────────────────────
    
    bool SWD::dpRead(uint8_t addr, uint32_t *out) {
        for (int r = 0; r < 8; r++) {
            uint8_t ack = xfer(false, true, addr, out);
            if (ack == SWD_ACK_OK)   return true;
            if (ack != SWD_ACK_WAIT) return false;
        }
        return false;
    }
    
    bool SWD::dpWrite(uint8_t addr, uint32_t val) {
        for (int r = 0; r < 8; r++) {
            uint8_t ack = xfer(false, false, addr, &val);
            if (ack == SWD_ACK_OK)   return true;
            if (ack != SWD_ACK_WAIT) return false;
        }
        return false;
    }
    
    // ── AP SELECT helper ──────────────────────────────────────────────────────────
    
    bool SWD::selectAP(uint8_t ap, uint8_t bank) {
        if (_selAP == ap && _selBank == bank) return true;
        uint32_t sel = ((uint32_t)ap << 24) | ((uint32_t)bank << 4);
        if (!dpWrite(DP_SELECT, sel)) return false;
        _selAP   = ap;
        _selBank = bank;
        return true;
    }
    
    // ── AP access ────────────────────────────────────────────────────────────────
    // AP reads are "posted": the xfer initiates the read and the result comes
    // back via DP RDBUFF (or the next AP read). We always use RDBUFF here.
    
    bool SWD::apRead(uint8_t ap, uint8_t reg, uint32_t *out) {
        uint8_t bank = (reg >> 4) & 0xF;
        if (!selectAP(ap, bank)) return false;
    
        uint32_t dummy = 0;
        for (int r = 0; r < 8; r++) {
            uint8_t ack = xfer(true, true, reg & 0x0F, &dummy);
            if (ack == SWD_ACK_OK)   break;
            if (ack != SWD_ACK_WAIT) return false;
        }
        return dpRead(DP_RDBUFF, out);
    }
    
    bool SWD::apWrite(uint8_t ap, uint8_t reg, uint32_t val) {
        uint8_t bank = (reg >> 4) & 0xF;
        if (!selectAP(ap, bank)) return false;
    
        for (int r = 0; r < 8; r++) {
            uint8_t ack = xfer(true, false, reg & 0x0F, &val);
            if (ack == SWD_ACK_OK)   return true;
            if (ack != SWD_ACK_WAIT) return false;
        }
        return false;
    }
    
    // ── high-level init ───────────────────────────────────────────────────────────
    
    bool SWD::powerUp() {
        if (!dpWrite(DP_CTRLSTAT, CTRL_CSYSPWRUPREQ | CTRL_CDBGPWRUPREQ)) return false;
        for (int i = 0; i < 100; i++) {
            uint32_t stat = 0;
            if (!dpRead(DP_CTRLSTAT, &stat)) return false;
            uint32_t ack_bits = CTRL_CSYSPWRUPACK | CTRL_CDBGPWRUPACK;
            if ((stat & ack_bits) == ack_bits) return true;
            delay(1);
        }
        return false;
    }
    
    bool SWD::setupMEMAP() {
        return apWrite(0, AP_CSW, AP_CSW_VALUE);
    }
    
    // probe(): line reset → JTAG-to-SWD → IDCODE read only.
    // Returns IDCODE on success, 0 on failure. Leaves pins floating afterward.
    uint32_t SWD::probe() {
        pinMode(_clk, OUTPUT);
        pinMode(_io,  OUTPUT);
        digitalWrite(_clk, LOW);
        digitalWrite(_io,  LOW);
        _selAP   = 0xFF;
        _selBank = 0xFF;
    
        lineReset();
        jtagToSWD();
        lineReset();
        idleCycles(2);
    
        uint32_t id = 0;
        if (dpRead(DP_IDCODE, &id) && id != 0 && id != 0xFFFFFFFF) return id;
    
        // Float pins so the next combination can be tried cleanly
        pinMode(_clk, INPUT);
        pinMode(_io,  INPUT);
        return 0;
    }
    
    bool SWD::connect() {
        pinMode(_clk, OUTPUT);
        pinMode(_io,  OUTPUT);
        digitalWrite(_clk, LOW);
        digitalWrite(_io,  LOW);
    
        _selAP   = 0xFF;
        _selBank = 0xFF;
    
        lineReset();
        jtagToSWD();
        lineReset();
        idleCycles(2);
    
        uint32_t id = 0;
        for (int r = 0; r < 3; r++) {
            if (dpRead(DP_IDCODE, &id) && id != 0 && id != 0xFFFFFFFF) goto got_id;
            lineReset();
            idleCycles(2);
        }
        return false;
    
    got_id:
        if (!powerUp())    return false;
        if (!setupMEMAP()) return false;
        return true;
    }
    
    // Assert nRESET, connect the DAP while held in reset, arm vector-catch on the
    // reset vector, then release. The CPU halts at its very first instruction.
    // Returns false if nRESET pin is not configured or connection fails.
    bool SWD::connectHalt() {
        if (_rst == SWD_NO_PIN) return false;
    
        assertReset();
        delay(10);
    
        if (!connect()) {
            releaseReset();
            return false;
        }
    
        // Arm vector-catch so the core halts at the reset vector
        if (!memWrite32(DEMCR, DEMCR_TRCENA | DEMCR_VC_CORERESET)) {
            releaseReset();
            return false;
        }
    
        // Pre-arm the halt flag so debug is enabled before reset exits
        memWrite32(DHCSR, DHCSR_DBGKEY | DHCSR_C_HALT | DHCSR_C_DEBUGEN);
    
        releaseReset();
    
        // Wait for the core to halt (it should be nearly instant)
        for (int i = 0; i < 200; i++) {
            if (isHalted()) return true;
            delay(2);
        }
        return false;
    }
    
    void SWD::clearErrors() {
        dpWrite(DP_ABORT, 0x1EUL);  // clear WDERR, STKERR, STKCMP, ORUNERR
        _selAP   = 0xFF;
        _selBank = 0xFF;
    }
    
    // ── memory access ─────────────────────────────────────────────────────────────
    
    bool SWD::memRead32(uint32_t addr, uint32_t *out) {
        if (!apWrite(0, AP_TAR, addr)) return false;
        return apRead(0, AP_DRW, out);
    }
    
    bool SWD::memWrite32(uint32_t addr, uint32_t val) {
        if (!apWrite(0, AP_TAR, addr)) return false;
        return apWrite(0, AP_DRW, val);
    }
    
    bool SWD::memRead(uint32_t addr, uint32_t *buf, size_t nWords) {
        for (size_t i = 0; i < nWords; i++) {
            if (!memRead32(addr + i * 4, &buf[i])) return false;
        }
        return true;
    }
    
    // ── Cortex-M debug operations ─────────────────────────────────────────────────
    
    bool SWD::halt() {
        return memWrite32(DHCSR, DHCSR_DBGKEY | DHCSR_C_HALT | DHCSR_C_DEBUGEN);
    }
    
    bool SWD::resume() {
        return memWrite32(DHCSR, DHCSR_DBGKEY | DHCSR_C_DEBUGEN);
    }
    
    bool SWD::step() {
        // Mask interrupts so the step doesn't fire an ISR
        return memWrite32(DHCSR, DHCSR_DBGKEY | DHCSR_C_STEP | DHCSR_C_MASKINTS | DHCSR_C_DEBUGEN);
    }
    
    bool SWD::isHalted() {
        uint32_t dhcsr = 0;
        return memRead32(DHCSR, &dhcsr) && (dhcsr & DHCSR_S_HALT);
    }
    
    bool SWD::sysReset() {
        // AIRCR: VECTKEY | SYSRESETREQ
        return memWrite32(AIRCR, 0x05FA0004UL);
    }
    
    bool SWD::readCoreReg(uint8_t reg, uint32_t *out) {
        if (!memWrite32(DCRSR, reg & 0x1F)) return false;
        uint32_t dhcsr = 0;
        for (int i = 0; i < 100; i++) {
            if (!memRead32(DHCSR, &dhcsr)) return false;
            if (dhcsr & DHCSR_S_REGRDY) return memRead32(DCRDR, out);
            delayMicroseconds(10);
        }
        return false;
    }
    
    bool SWD::writeCoreReg(uint8_t reg, uint32_t val) {
        if (!memWrite32(DCRDR, val))                            return false;
        if (!memWrite32(DCRSR, (1UL << 16) | (reg & 0x1F)))    return false;
        uint32_t dhcsr = 0;
        for (int i = 0; i < 100; i++) {
            if (!memRead32(DHCSR, &dhcsr)) return false;
            if (dhcsr & DHCSR_S_REGRDY)   return true;
            delayMicroseconds(10);
        }
        return false;
    }

    This implementation file translates the architectural framework of your header into concrete execution steps, managing everything from exact microsecond line timing up to core halting routines.

    Here is a breakdown of how the source code executes its low-level hardware manipulation.

    1. High-Performance Timing and Bit-Banging Primitives

    At the lowest level, the driver establishes wire communication using manual pin toggling synchronized by the HALF_US macro. Set to 1 microsecond, this creates a clean half-period delay, yielding a stable ~500 kHz clock cycle that is safe for long lines and most ARM Cortex-M targets. The fundamental I/O primitives—writeBit(), readBit(), and clkPulse()—implement standard SPI-like synchronous signaling on top of the shared SWDIO pin.

    Data is strictly driven onto the line before the clock rises, and sampled by the target on the rising edge of SWCLK. To transition between transmitting and receiving without causing a short circuit on the data line, the driver manipulates ioOut() and ioIn(). These call Arduino’s underlying pinMode() to seamlessly flip the SWDIO line between a low-impedance output and a high-impedance input.

    2. Protocol Frame Assembly and Packet Handling

    The transactional heart of the code lies within the xfer() method, which handles a complete, multi-phase SWD packet. It first invokes buildReq() to calculate parity over the target address and read/write flags, formatting an 8-bit command byte wrapped in explicit start, stop, and park bits.

    Once the host streams this byte out, it relinquishes control of the line during a precise one-cycle “turnaround” phase, shifts into input mode, and reads a 3-bit acknowledgement frame (ack) driven back by the target. If the transaction is a read and returns SWD_ACK_OK, the host clocks in 32 data bits followed by a final parity bit, tracking cumulative parity on the fly via an exclusive-OR accumulator (par ^= b).

    For writes, the sequence is inverted: the host clears the turnaround phase, assumes ownership of the data line, and streams out the payload along with its calculated parity before pushing trailing idle cycles to settle the target’s internal state machine. High-level wrappers dpRead() and dpWrite() wrap this machine in a loop that automatically retries transactions up to eight times if the target asserts a SWD_ACK_WAIT response, preventing transient bus collisions from breaking execution.

    3. State Caching and Access Port Topography

    To maximize throughput, the driver uses optimization shielding inside selectAP(). It maintains local copies of the current Access Port (_selAP) and register bank (_selBank) settings. If consecutive commands address the exact same register bank, the driver completely skips writing to the target’s DP_SELECT register, eliminating unnecessary overhead during consecutive transfers.

    This mechanism is critical for apRead() and apWrite(). Because ARM Access Port reads are inherently “posted”—meaning a request initiated on the bus does not return its data until the subsequent transaction—apRead() fires the primary AP request, handles potential wait states, and then executes a follow-up read on the Debug Port’s read buffer (DP_RDBUFF) to extract the staged data.

    4. Advanced Lifecycle and Core Debug Halting

    The complex lifecycles of target discovery and control are finalized in connect(), connectHalt(), and core register manipulators. The standard connect() sequence wakes the target by firing a 56-cycle continuous high signal (lineReset()), sending the mandatory 16-bit JTAG-to-SWD switching key (0xE79E), executing a second line reset, and asserting a power-up request to the internal system and debug power domains via DP_CTRLSTAT.

    The connectHalt() method goes an explicit step further to intercept execution on secured or crashing targets. It asserts a physical hardware reset by driving the target’s nRESET line low, hooks into the alive DAP, and writes to the Core Debug Exception and Monitor Control Register (DEMCR) to arm a vector catch (DEMCR_VC_CORERESET). It then primes the core’s halting state machine in DHCSR, releases the physical reset line, and monitors status flags until the processor halts directly at its first instruction vector before a single line of application firmware can run.

    Finally, readCoreReg() and writeCoreReg() manipulate internal CPU registers (R0-R15) by using a dual-register handshake: the host writes the target register index to the Selector Register (DCRSR), polls DHCSR until the data-ready flag (DHCSR_S_REGRDY) resolves, and then reads or writes the actual payload through the Data Register (DCRDR).

    This was fun!

    After building my swd software I loaded it on my Seeed Studio XIAO ESP32-S3 Sense and immediately got to work on my RAZ board. I was able to probe and dump the firmware successfully. All of my source code is publicly available on Github. Follow me as I go into the new firmware I will put on my vapes. Leave a comment to let me know what you want to see run on the boards!