Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Standard Debug Spec Milestone

Below are milestones planned in the project:

Status snapshot (sim)

AreaStatus
Phase 2A–2C (target-side SWD DTM)✅ Implemented and sim-verified (sbt + LiteX)
Phase 3 step 1 (LiteX + raw-AP smoke)✅ DPIDR + dmstatus over SWD
Phase 3 step 2 (riscv + GDB)✅ Vexriscv fork — disdi/openocd vexriscv-gateway (master + Gerrit 9786 + VexRiscv gateway)
Phase 3 step 2b (demo break / continue)✅ Preload path on the Vexriscv fork; no GDB load in sim
Phase 3 step 3 (Arty + CMSIS-DAP)[ ] Next
Phase 4–5Partial — host sim paths done; hardware / full matrix open

Conformance note: the RISC-V Debug Specification defines only a JTAG DTM. SWD is a custom DTM (explicitly permitted). Claim "RISC-V Debug Specification, with custom DTM" — never an unqualified conformance claim.

Phase 1 adds missing features in DebugModule from the ratified RISC-V Debug Specification.

Phase 2 adds a target-side Arm Serial Wire Debug (SWD) front-end that still drives the same RISC-V DebugBus used by the JTAG DTM. The Arm specification is at ADIv5.0–ADIv5.2 (IHI0031).

Host (SWCLK / SWDIO)
        │
   ┌────▼────┐
   │ SwdPhy  │  Phase 2A — wire protocol (framing, turnaround, line reset)
   └────┬────┘
        │ SwdDpCmd / SwdDpRsp / SwdDpWrite
   ┌────▼────┐
   │  SwdDp  │  Phase 2B — DP registers + ACK policy (OK / WAIT / FAULT)
   └────┬────┘
        │ SwdApCmd / SwdApRsp
   ┌────▼──────────┐
   │ SwdDmiGateway │  Phase 2C — custom DMI AP + SWCLK↔debug CDC
   └────┬──────────┘
        │ DebugBus (same as JTAG DTM)
   ┌────▼────────┐
   │ DebugModule │  Phases 1B–1D
   └─────────────┘

How the SWD protocol works

What SWD is

Serial Wire Debug is a 2-wire, synchronous, packet-based host↔target link:

SignalRole
SWCLKClock (usually driven by the probe/host)
SWDIOBidirectional data (one bit per clock)

There is no separate reset pin. Recovery uses a line reset on SWDIO.

Logically, SWD talks to a Debug Port (DP). The DP either answers for itself (DP registers) or forwards accesses to an Access Port (AP) that reaches the real debug resource. In this project the AP is a custom RISC-V DMI gateway, which drives the existing DebugBus into the Debug Module.

One transaction on the wire

Every SWD access is request → ACK → (optional data).

Packet request (host → target, 8 bits)

After optional idle cycles (SWDIO low), the host sends:

Bit(s)NameMeaning
1StartAlways 1
1APnDP0 = DP register, 1 = AP register
1RnW0 = write, 1 = read
2A[2:3]Register address bits A[3:2] (LSB-first on the wire)
1ParityEven parity over APnDP, RnW, A[2], A[3]
1StopAlways 0
1ParkHost drives 1, then releases the line

All multi-bit fields are LSB first.

Turnaround (Trn)

When drive ownership changes, neither side should drive for a short period (default 1 SWCLK). That prevents bus fight on SWDIO.

ACK (target → host, 3 bits)

Encoding (value)NameWire order (LSB first)
0b001OK1, 0, 0
0b010WAIT0, 1, 0
0b100FAULT0, 0, 1

The numeric value and the bit order on the wire are easy to confuse: OK is value 001, so the first driven bit is 1.

Data phase (only if ACK = OK in this implementation)

DirectionWhenFormat
WriteAfter OK + second turnaround32-bit WDATA + even parity (host → target)
ReadAfter OK, no turnaround (target keeps the line)32-bit RDATA + even parity (target → host)

Errors and special sequences

ConditionTarget behaviour
Protocol error (bad request parity / Stop / Park)Do not drive ACK; stay silent until line reset
WDATA parity failACK already sent; DP sets WDATAERR and drops the write (not a line silence)
WAITBusy (e.g. previous AP access outstanding); host typically retries
FAULTSticky error set; host clears via ABORT
Line resetSWDIO HIGH for ≥ 50 SWCLK, then ≥ 2 idle (LOW)
IdleSWDIO low between frames, or back-to-back Start with zero idle

Clock and pins

Clock domain = SWCLK (probe-driven)

swdio.i  ← host/probe (or pad input)
swdio.o  → pad output value when target drives
swdio.oe → pad output enable
  • Target samples i and updates o/oe on rising SWCLK (OpenOCD bitbang model).

Minimal mental model (one frame)

  1. Host clocks an 8-bit header (Start…Park).
  2. Phy validates it; if bad → silence until line reset.
  3. If good → phy fires cmd and needs rsp.ack for the next few clocks.
  4. Phy drives ACK (value chosen by Phase 2B).
  5. If OK and read → phy streams rdata + parity from the DP.
  6. If OK and write → phy releases the line, samples wdata + parity, fires wr.
  7. If WAIT/FAULT → phy stops after ACK (no data phase here).
  8. Host may retry, clear sticky via ABORT or line-reset.

Debug Transport Module

Phase 1a: JTAG System Integration


Spec Feature Coverage Summary

Spec Feature (Chapter)Vexriscv Debug ImplementationImplementation Details
Ch 6: Debug Transport Module (DTM)
JTAG TAP with IDCODE/dtmcs/dmi✅ ImplementedDebugTransportModuleJtag.scala — standard IR codes, dtmcs with version/abits/idle/dmistat, dmi with op/address/data
DMI bus protocol✅ ImplementedDebugInterfaces.scalaDebugBus with DebugCmd/DebugRsp, DebugBusSlaveFactory
DMI busy/error handling✅ Implementeddmihardreset, dmireset, pending/overrun detection
Cross-clock-domain DMI✅ ImplementedccToggle for JTAG↔debug clock domains
JTAG tunnel support✅ ImplementedJtagTunnel.scala — tunneling through outer TAP

Debug Module

Phase 1B: Core DM Registers


Spec Feature (Chapter)Vexriscv Debug ImplementationImplementation Details
Ch 3: Debug Module (DM)
dmcontrol (0x10)✅ Implementeddmactive, ndmreset, haltreq, resumereq, ackhavereset, hartsello/hi. Missing: hasel, setresethaltreq, hartreset, keepalive
dmstatus (0x11)✅ Implementedversion, authenticated=1, all halted/running/unavail/nonexistent/resumeack/havereset flags, impebreak=1
hartinfo (0x12)✅ Implementeddataaddr=0, datasize=0, dataaccess=0, nscratch=0
abstractcs (0x16)✅ Implementeddatacount, progbufsize, busy, cmderr (all 7 error codes)
command (0x17) — Access Register✅ Implementedcmdtype=0 with full FSM: transfer, write, postexec, aarsize validation, GPR+FPU register access
command (0x17) — Access Memory❌ Not implementedReturns NOT_SUPPORTED
command (0x17) — Quick Access❌ Not implementedReturns NOT_SUPPORTED
abstractauto (0x18)✅ Implementedautoexecdata and autoexecProgbuf for burst access
progbuf0-N (0x20+)✅ ImplementedParameterized progbuf memory, multi-word execution with counter, redo support
data0-N (0x04+)✅ ImplementedMemory-backed, hart writes via fromHarts, host reads async
sbcs (0x38) — System Bus Access✅ Implemented (optional)sbversion=1, sbaccess, sbbusyerror, sbbusy, sbreadonaddr, sbautoincrement, sbreadondata, sberror, 32-bit only
sbaddress0 (0x39)✅ ImplementedRead/write with auto-increment
sbdata0 (0x3c)✅ ImplementedRead/write with bus triggers
sbaddress1-3 / sbdata1-3❌ Not implemented32-bit address/data only
sbcs 8/16/64/128-bit access❌ Not implementedOnly sbaccess32 supported
haltsum0 (0x40)✅ ImplementedPer-hart halted bits, up to 32 harts
haltsum1-3❌ Not implementedOnly haltsum0 exists
authdata (0x30)❌ Not implementedAlways authenticated
confstrptr0-3❌ Not implemented
nextdm (0x1d)❌ Not implemented
dmcs2 (0x32)❌ Not implemented
Hart arrays (hawindowsel/hawindow)❌ Not implemented
custom0-15❌ Not implemented
Multi-hart support✅ ImplementedParameterized p.harts, per-hart buses, hartSel selection

CSR Register

Phase 1C: Debug CSRs (dcsr, dpc)


Spec Feature (Chapter)Vexriscv Debug ImplementationImplementation Details
Ch 4: Core Debug (hart-side CSRs)
Halt/ResumeDebugHartBus + CsrPluginCsrPlugin.scala (line 702+): running flag, DebugHartBus wiring, halt/resume handshake
Single-step✅ ImplementedCsrPlugin.scala (lines 807-845): dcsr.step with full FSM (IDLE→SINGLE→WAIT), timeout/redo handling
dcsr (0x7B0)✅ ImplementedCsrPlugin.scala (lines 792-851): prv, step, nmip, mprven, cause, stoptime, stopcount, stepie, ebreakm/s/u, xdebugver=4
dpc (0x7B1)✅ ImplementedCsrPlugin.scala (line 791): Reg(UInt(32 bits)), read/write via rw(CSR.DPC, dpc)
dscratch0 (0x7B2)❌ Not implemented
dscratch1 (0x7B3)❌ Not implemented
Debug mode entry✅ ImplementedCsrPlugin.scala (lines 1426-1443): saves PC→dpc, sets dcsr.cause (1=ebreak, 3=haltreq, 4=step), saves privilege→dcsr.prv, enters M-mode
Debug mode exit (resume)✅ ImplementedCsrPlugin.scala (lines 1488-1498): jumps to dpc, restores privilege from dcsr.prv, via DebugHartBus.resume
Halt cause reporting✅ Implementeddcsr.cause: 1 (ebreak), 2 (trigger), 3 (haltreq), 4 (step)
dcsr.ebreakm/s/u✅ ImplementedCsrPlugin.scala (lines 1372-1377): per-privilege ebreak→debug detection
dcsr.stoptime✅ ImplementedCsrPlugin.scala (line 866): stoptime output gated by debugMode
dcsr.stopcount✅ ImplementedCsrPlugin.scala (line 1176): mcycle increment gated by !debugMode || !stopcount
dcsr.stepie✅ ImplementedCsrPlugin.scala (line 1315): interrupts cleared when step && !stepie
Interrupt inhibition in debug✅ ImplementedCsrPlugin.scala (line 721): inhibateInterrupts() when debugMode
CSR access protection (0x7Bx)✅ ImplementedCsrPlugin.scala (line 1718): blocks non-debug access to 0x7B0-0x7BF
DebugHartBus wiring✅ ImplementedCsrPlugin.scala (lines 704-789): instruction injection, data CSR, all hartToDm/dmToHart signals
Reset controldmcontrol.ndmresetio.ndmresetBoth implementations provide ndmreset

Triggers

Phase 1D: Trigger Module


Spec Feature (Chapter)Vexriscv Debug ImplementationImplementation Details
Ch 5: Trigger Module (hart-side CSRs)
tselect (0x7A0)✅ ImplementedCsrPlugin.scala (lines 870-875): WARL index, parameterized debugTriggers (default 2)
tinfo (0x7A4)✅ ImplementedCsrPlugin.scala (line 878): reports type 2 (mcontrol) support
tdata1 (0x7A1)⚠️ PartialCsrPlugin.scala (lines 922-942): type=2, dmode, execute, m/s/u, action. Missing: timing, select, sizelo/hi, maskmax, chain, match, load, store, hit
tdata2 (0x7A2)✅ ImplementedCsrPlugin.scala (lines 944-953): 32-bit compare value, PC equality match
tdata3 (0x7A3)❌ Not implemented
tcontrol (0x7A5)❌ Not implementedNo mte/mpte
Trigger type⚠️ mcontrol (type 2) onlyLegacy type 2, not type 6 (mcontrol6). Spec recommends type 6 for new implementations
Match modes⚠️ Equal onlyOnly match=0 (equality). No napot, >=, <, mask modes
Match targets⚠️ Execute address onlyOnly execute bit implemented. No load/store data/address match
Privilege filtering✅ Implementedm, s, u bits with privilegeHit logic (lines 930-934)
dmode security✅ Implementeddmode bit controls debug-only write access (line 925)
action field⚠️ PartialRegister exists but only action=1 (enter debug) used in match logic
Trigger chaining❌ Not implementedNo chain bit
dcsr.cause=2 on trigger✅ ImplementedCsrPlugin.scala (line 892): sets dcsr.cause := 2 on trigger hit
Trigger hit → debug entry✅ ImplementedCsrPlugin.scala (lines 881-897): decodeBreak halts pipeline, enters debug mode
mcontrol6 (type 6)❌ Not implementedOnly legacy type 2 exists
icount (type 3)❌ Not implemented
itrigger/etrigger/tmexttrigger❌ Not implemented
Hardware breakpoints⚠️ Spec-compliant but limitedType 2 mcontrol with execute address match=0 only, privilege filtering, dmode security
mcontext/scontext❌ Not implemented
SoC Integration
DTM→DM→Hart wiring✅ ImplementedDebugModuleFiber.scala — multi-hart binding, clock-domain-safe pipelining
Tilelink SBA bridge✅ ImplementedmakeSysbusTilelink() in DebugModuleFiber

Phase 2A — SWD Protocol State Machine: Implementation & Verification

Code Repository :

Update submodules in pythondata-cpu-vexriscv_smp to below :

  • SpinalHdl - https://github.com/disdi/SpinalHDL/tree/phase2a
  • VexRiscv - https://github.com/disdi/VexRiscv/tree/phase2a
ArtifactPath
RTLEXT/SpinalHDL/lib/src/main/scala/spinal/lib/cpu/riscv/debug/DebugTransportModuleSwd.scala
TestbenchEXT/VexRiscv/src/test/scala/vexriscv/DebugSwdTest.scala
Runcd EXT/VexRiscv && sbt -batch "testOnly vexriscv.DebugSwdTest"

EXT = pythondata-cpu-vexriscv-smp/pythondata_cpu_vexriscv_smp/verilog/ext. EXT/VexRiscv/build.sbt compiles EXT/SpinalHDL from source, so the RTL and testbench build in one sbt project with no LiteX involvement.


1. What is implemented — SwdPhy

SwdPhy is the ARM ADI SW-DP line layer (ADIv6.0 §B4): it speaks the 2-wire protocol and terminates in a decoded-transaction seam. It contains no DP registers (Phase 2B) and no DebugBus bridge (Phase 2C).

1.1 I/O

swdio.i  : in  Bool   -- SWDIO as driven by the probe
swdio.o  : out Bool   -- SWDIO value when the target drives
swdio.oe : out Bool   -- target output enable
dp.cmd   : master Flow(SwdDpCmd)    -- decoded request        (2A -> 2B)
dp.rsp   : slave  Flow(SwdDpRsp)    -- ACK + read data        (2B -> 2A)
dp.wr    : master Flow(SwdDpWrite)  -- write commit           (2A -> 2B)
  • Clock = SWCLK. The component's implicit clock domain is the probe-driven SWCLK. The target samples swdio.i and updates swdio.o/swdio.oe on the rising edge, matching the OpenOCD bitbang model (host sets data while SWCLK is low, samples target data while low).
  • No inout. The tristate split (i/o/oe) is required because the cluster is a Verilog black box in LiteX.

1.2 The 2A↔2B seam (three flows)

A wire-protocol fact shapes the seam: a write's ACK is sent before the 33-bit WDATA phase, so write data cannot ride in the request.

FlowFiredPayload
SwdDpCmdone-cycle pulse on the packet-request park edge, iff parity/stop/park all passapNdp, rnw, addr = A[3:2]
SwdDpRspmust be presented by the DP within the turnaround cycle (an always-ready DP may simply hold it valid)ack (OK=001/WAIT=010/FAULT=100), rdata
SwdDpWriteone-cycle pulse after the 33rd WDATA bitdata, parityOk (false ⇒ WDATAERR material for 2B)

The response is latched into a hold register on first sight (rsp.valid may be combinational off cmd or held continuously); ACK and RDATA are driven from the latched copy for the rest of the frame.

Read path — READ_DATA → RELEASE → IDLE (Fig B4-2)

  • 32 data bits LSB-first from rspHold.rdata, then even parity (xorR).
  • No turnaround between ACK and RDATA (target keeps oe=1).
  • RELEASE: oDrive := False, then IDLE (trailing Trn / release).

Write path — WR_TRN → WRITE_DATA → IDLE (Fig B4-1)

  • WR_TRN: two cycles with oe=0 (cnt 0 then 1) = second turnaround.
  • WRITE_DATA: shift in 32 bits + sample parity; fire dp.wr with data and parityOk.
  • WDATA parity fail → parityOk = false (WDATAERR material for 2B), not protocol error.
  • Return direct WRITE_DATA → IDLE (no RELEASE): host already owns the line. Diagram §1.4.2 also uses WRITE_DATA --> IDLE.

ERROR and line reset

Diagram §1.4.2RTL
ERROR --> ERROR (ignore traffic)ERROR: oDrive := False only; no header parse
ERROR --> IDLE on line resetlineReset.hitRESET_WAIT → first low → IDLE

Line reset is orthogonal (overrides any state): ≥50 consecutive highs on swdio.i while !oDrive; counter frozen/cleared while target drives so ACK/RDATA cannot fake a reset.

RESET_WAIT is an RTL-only gate so the target does not accept Start until the line has gone idle after the reset burst. Diagram §1.4.2 draws a direct ERROR → IDLE; recovery contract is the same.


2. How the testbench works — DebugSwdTest

The bench is the probe

SWCLK is the DUT clock, and the bench owns it: no forkStimulus — every SWCLK cycle is one call to step(bit):

fallingEdge(); set swdio.i = bit;      // host updates while SWCLK low
sample (swdio.o, swdio.oe);            // host samples while SWCLK low
risingEdge();                          // target samples/updates

This reproduces OpenOCD's bitbang_swd_exchange exactly: a host-driven bit is sampled by the target at the rising edge ending its cycle; a target-driven bit read in cycle k is the value the target registered at edge k−1. All multi-bit values are sent/collected LSB first. step returns (o, oe), so every helper can assert drive/release behavior per cycle.

Phase 2B — SW-DP Register File: Implementation & Verification

Code Repository :

Update submodules in pythondata-cpu-vexriscv_smp to below :

  • SpinalHdl - https://github.com/disdi/SpinalHDL/tree/phase2b
  • VexRiscv - https://github.com/disdi/VexRiscv/tree/phase2b
ArtifactPath
RTL (SwdDp, SwdPhyDp, AP seam bundles)EXT/SpinalHDL/lib/src/main/scala/spinal/lib/cpu/riscv/debug/DebugTransportModuleSwd.scala (same file as Phase 2A)
Shared probe driverEXT/VexRiscv/src/test/scala/vexriscv/SwdSimDriver.scala
TestbenchEXT/VexRiscv/src/test/scala/vexriscv/DebugSwdDpTest.scala
Runcd EXT/VexRiscv && sbt -batch "testOnly vexriscv.DebugSwdDpTest" (both phases: "testOnly vexriscv.DebugSwdTest vexriscv.DebugSwdDpTest")

EXT = pythondata-cpu-vexriscv-smp/pythondata_cpu_vexriscv_smp/verilog/ext. Still zero LiteX involvement.


1. What is implemented

1.1 SwdDp — the ADI SW-DP register file

SwdDp sits behind the Phase 2A seam (SwdDpCmd/SwdDpRsp/SwdDpWrite, consumed as slave flows) and exposes a new AP seam toward Phase 2C:

ap.cmd : master Flow(SwdApCmd)   -- rnw, addr = A[3:2], apSel = SELECT[31:24], wdata
ap.rsp : slave  Flow(SwdApRsp)   -- error, data (completion; test-controlled latency)

1.2 DP register map (SWD A[3:2] encoding)

A[3:2]ReadWrite
00DPIDR (parameter; default 0x0BA11AAB placeholder)ABORT — DAPABORT, STKCMPCLR, STKERRCLR, WDERRCLR, ORUNERRCLR
01CTRL/STAT when DPBANKSEL==0; other banks read as zeroCTRL/STAT when DPBANKSEL==0; other banks write-ignored
10RESEND (= last posted result)SELECT — APSEL[31:24], APBANKSEL[7:4], DPBANKSEL[3:0]
11RDBUFF (= last posted result)TARGETSEL (SWD v2) — accepted, ignored

These two diagrams put it back on the wire, making explicit where each step of a DP access is accomplished: the frame shape is Phase 2A (SwdPhy), the register semantics are Phase 2B (SwdDp). Neither phase performs a DP access on its own.

A DP read — DPIDR, A[3:2] = 00

Wire layout — one cell per SWCLK bit period, LSB first within every multi-bit field:

Bits 0–7 host-driven · bits 9–44 target-driven · bits 8 and 45 are turnarounds where neither side drives and SWDIO floats to its mandatory pull-up. The closing turnaround is one bit period on the wire but two SWCLK cycles in RTL (RELEASE), because SwdPhy registers its outputs — see the step-ownership table below.

Phase ownership:

A DP write — SELECT, A[3:2] = 10

Wire layout — note the second turnaround at bit 12, absent from the read:

Bits 0–7 host-driven · bits 9–11 target-driven · bits 13–45 host-driven again · bits 8 and 12 are turnarounds. This layout is the reason the 2A↔2B seam has three flows: the target must commit to the ACK at bits 9–11, thirty-two bit periods before the data it is acknowledging exists on the wire.

Phase ownership:


2. Testbench changes

2.1 Shared driver extraction

The probe-side bit-bang logic (step, header, readAck, transactRead, transactWrite, lineReset, with all embedded protocol assertions) moved from the 1a suite into SwdSimDriver.scala (SwdHostDriver, SwdAckSim). DebugSwdTest delegates to it with its 9 test bodies unchanged — proven by the suites running together (18/18). Step 2 will reuse the same driver against the full transport + DebugModule.

2.2 The stub moves back one layer

Exactly as the step-1b plan prescribes: the always-ready DP stub is gone — the real SwdDp answers within the turnaround by construction (combinational response). The test harness now stubs the AP side:

  • an observer thread records every ap.cmd fire into a queue ((rnw, addr, wdata));
  • apComplete(data, error) delivers a completion on ap.rsp for exactly one SWCLK cycle — completions are test-controlled, which is what makes posted-read ordering and WAIT-while-busy directly testable (the AP simply doesn't respond until the test says so).

Sugar wrappers keep tests readable: dpRead/dpWrite/apRead/apWrite map to driver transactions with APnDP set accordingly.


4. Verification

cd ~/fpga/pythondata-cpu-vexriscv-smp/pythondata_cpu_vexriscv_smp/verilog/ext/VexRiscv
sbt -batch "testOnly vexriscv.DebugSwdTest vexriscv.DebugSwdDpTest"
# expect: Tests: succeeded 18, failed 0, canceled 0, ignored 0, pending 0

Phase 2C — DMI Gateway, Clock Crossing & DebugModule Integration

Code Repository :

  • pythondata-cpu-vexriscv_smp - https://github.com/disdi/pythondata-cpu-vexriscv_smp/tree/phase2c

OR

Update submodules in pythondata-cpu-vexriscv_smp to below :

  • SpinalHdl - https://github.com/disdi/SpinalHDL/tree/phase2c
  • VexRiscv - https://github.com/disdi/VexRiscv/tree/phase2c
ArtifactPath
RTL (SwdDmiGateway, DebugTransportModuleSwd)EXT/SpinalHDL/lib/src/main/scala/spinal/lib/cpu/riscv/debug/DebugTransportModuleSwd.scala (same file as 2A/2B)
Fiber hook (withSwdTransport())EXT/SpinalHDL/lib/src/main/scala/spinal/lib/cpu/riscv/debug/DebugModuleFiber.scala
Testbench (incl. SwdDmTestTop)EXT/VexRiscv/src/test/scala/vexriscv/DebugSwdDmTest.scala
Runcd EXT/VexRiscv && sbt -batch "testOnly vexriscv.DebugSwdDmTest" (all: "testOnly vexriscv.DebugSwdTest vexriscv.DebugSwdDpTest vexriscv.DebugSwdDmTest")

EXT = pythondata-cpu-vexriscv-smp/pythondata_cpu_vexriscv_smp/verilog/ext. Still zero LiteX involvement.


1.Implementation of DMI for SWD

1.1 SwdDmiGateway — the RISC-V DMI gateway AP

Sits behind the 2B AP seam (SwdApCmd/SwdApRsp) and drives a DebugBus master — the same DebugCmd/DebugRsp interface the JTAG DTM produces, which is the whole point of the transport-agnostic design.

The DMI needs a 7-bit address and 32 bits of data. SWD offers, per transaction, one bit of DP/AP selection and two bits of register address. Four AP registers, total.

AP A[3:2]RegisterAccessBehavior
00AP_IDRROidentification constant (default 0x74726976 "triv") — completes locally in 1 SWCLK cycle
01DMI_ADDRRWlatches the 7-bit DMI word address; readable back — local
10DMI_DATARWperforms the DebugBus transaction at DMI_ADDR (RnW from the SWD packet)
11POSTED_READROlast completed DMI read result — local

Writes to RO registers complete OK with no effect. SELECT.APSEL is not decoded (single-AP design). AP reads launch at the request (posted, per 2B); AP writes launch at the WDATA commit carrying the data.

1.2 DebugTransportModuleSwd — the full transport

SWD pins → SwdPhy (2A) → SwdDp (2B) → SwdDmiGateway (2C) → DebugBus. The SWD side elaborates under ClockDomain(swclk, resetKind = BOOT); the DebugBus side under the provided debugCd. This is the SWD counterpart of DebugTransportModuleJtagTap.

DebugModuleFiber.withSwdTransport(dpidr, apIdr) instantiates it alongside withJtagTap() — same one-transport-per-build rule (direct io.ctrl connection, no arbitration).

1.3 End-to-end dmstatus read (the step-2 exit), graphically


2. Testbench — DebugSwdDmTest

  • DUT = SwdDmTestTop: the full transport + a real DebugModule (version 2, 1 hart, progBufSize=2, datacount=1) with a stubbed DebugHartBus — running, never halted, hartToDm/resume.rsp idle.
  • Two genuinely asynchronous clocks: the shared SwdHostDriver bit-bangs SWCLK (bench-owned, via a ClockDomain handle over the pin) while dut.clockDomain free-runs via forkStimulus — DMI completion latency is variable by construction.
  • Host-style retry helpers: apWriteRetry/apReadRetry/rdbuff retry on WAIT with bounded attempts — the same loop a real OpenOCD target would run. dmiRead/dmiWrite compose them into DMI operations.
  • Failure forensics built in: on any unexpected ACK the harness reads CTRL/STAT and includes the sticky flags in the assertion message (ackCheck) — this is what cracked the phantom-completion bug (§4.2). The sim(name, seed = …) hook pins a failing seed for deterministic reproduction.

3. Verification

cd ~/fpga/pythondata-cpu-vexriscv-smp/pythondata_cpu_vexriscv_smp/verilog/ext/VexRiscv
sbt -batch "testOnly vexriscv.DebugSwdDmTest"
# expect: Tests: succeeded 7, failed 0, canceled 0, ignored 0, pending 0
sbt -batch "testOnly vexriscv.DebugSwdTest vexriscv.DebugSwdDpTest vexriscv.DebugSwdDmTest"
# expect: Tests: succeeded 25, failed 0, canceled 0, ignored 0, pending 0

4. Summary

Phases 2A–2C complete the target-side RTL of the SWD plan; the DebugBus now speaks SWD end-to-end in simulation.

System Integration — SWD

Goal is host-visible integration only (LiteX SoC, OpenOCD, GDB). Target-side SWD RTL (SwdPhy / SwdDp / SwdDmiGateway) is Phase 2A–2C and is not re-defined here.


Code repositories

  • LiteX — https://github.com/disdi/litex/tree/swd
  • VexRiscv — https://github.com/disdi/VexRiscv/tree/phase3
  • OpenOCD (Vexriscv fork) — https://github.com/disdi/openocd/tree/vexriscv-gateway

Update submodules in pythondata-cpu-vexriscv_smp to the LiteX / VexRiscv branches above.

OpenOCD (host, not RTL) — two lanes on master:

LaneBuildRole
raw-AP smokeOpenOCD master (stock OK for SWD remote_bitbang)DPIDR + dap apregdmstatus; no GDB
Vexriscv fork (riscv + GDB)disdi/openocd vexriscv-gateway — OpenOCD master + Gerrit 9786 + designer-AP / VexRiscv DTM backendexamine + halt/resume/regs + GDB :3333

Stock master is enough for smoke. Full riscv attach needs the published vexriscv-gateway branch: 9786 (DTM + Mem-AP DMI backend), two fixes to 9786 itself, and a second designer-AP / VexRiscv gateway backend for the Phase 2C DMI_ADDR / DMI_DATA map. No RTL change.

Upstream’s stock riscv target rejects -dap at argument parsing, so Tcl-only dap apreg helpers cannot drive a GDB session — that is why the 9786 + gateway path exists.


Host attach workflows

Side-by-side

TerminalJTAG — full three-terminal ✅SWD — full three-terminal ✅
1 — simlitex_sim … --with-privileged-debug --jtag-tap --with-jtagremote → TCP 44853 (jtagremote)litex_sim … --with-privileged-debug --with-swd-debug --with-swdremote → TCP 44854 (swdremote); add --ram-init=demo.bin for demo debug
2 — OpenOCDStock riscv target + BSCAN tunnel; examines hart; GDB :3333transport select swd + DAP + Vexriscv fork riscv; examines hart; GDB :3333
3 — GDBtarget extended-remote localhost:3333 → halt / regs / loadattach + regs ✅; demo break main / continue / bt ✅ via preload (no GDB load)
CapabilityJTAGSWD
Verilator SoC + official DM✅ (_Swd cluster)
Wire transport in sim✅ JTAG TAP + tunnel✅ SW-DP (DebugTransportModuleSwd)
OpenOCD sees transport✅ TAP 0x10003fff✅ SWD DPIDR 0x0ba11aab
Read dmstatus✅ via riscv / DMI✅ via vexriscv_dmi_read 0x11 / smoke
Examined RISC-V coreXLEN=32, misa=0x40141101
GDB halt / resume / info registers
Break / continue / backtrace✅ (load OK on JTAG)✅ via --ram-init=demo.bin + symbols; GDB load impractical in sim
OpenOCD binarystock masterstock master for raw-AP smoke; Vexriscv fork (disdi/openocd vexriscv-gateway) for riscv / GDB

JTAG — end-to-end workflow

Official stack only (--with-privileged-debug + full JTAG TAP in sim).

Prerequisites

  • litex_sim (LiteX venv)
  • OpenOCD master with standard RISC-V target
  • riscv64-unknown-elf-gdb

Configs

FileRole
openocd_jtag_remote.cfgremote_bitbanglocalhost:44853
riscv_jtag_tunneled.tclTAP irlen 6, ID 0x10003fff, riscv use_bscan_tunnel 6 1

Terminal 1 — sim (keep running)

litex_sim \
  --integrated-main-ram-size=0x10000 \
  --cpu-type=vexriscv_smp \
  --cpu-variant=linux \
  --cpu-count=1 \
  --with-privileged-debug \
  --jtag-tap \
  --with-jtagremote \
  --non-interactive
FlagRole
--integrated-main-ram-size=0x1000064 KiB main RAM for sim (demo load region)
--cpu-type=vexriscv_smp / --cpu-variant=linux / --cpu-count=1SMP Linux-capable cluster, 1 hart
--with-privileged-debugOfficial DebugModule + DTM (_Pd netlist token)
--jtag-tapFull JTAG TAP on cluster (_JtagT); needed so sim has TCK/TMS/TDI/TDO pads
--with-jtagremoteLiteX sim module jtagremote — OpenOCD remote_bitbang on TCP 44853
--non-interactiveKeep sim running (no local control menu); target for OpenOCD/GDB

Wait for Found port 44853 and BIOS prompt litex>. First run may take several minutes (cluster regen + Verilator compile).

Terminal 2 — OpenOCD (after Terminal 1 is up)

openocd -f openocd_jtag_remote.cfg -f riscv_jtag_tunneled.tcl

Success indicators:

Info : JTAG tap: riscv.cpu tap/device found: 0x10003fff
Info : Examined RISC-V core; found 1 harts
Ready for Remote Connections
Info : Listening on port 3333 for gdb connections

Terminal 3 — GDB (after OpenOCD is ready)

riscv64-unknown-elf-gdb demo/demo.elf
set remotetimeout 120
set pagination off
set arch riscv:rv32
target extended-remote localhost:3333
monitor reset halt
x/8i $pc
info registers

One-liner:

riscv64-unknown-elf-gdb -ex "set remotetimeout 120" \
  -ex "target extended-remote localhost:3333" \
  demo/demo.elf

Load demo.elf (linked at 0x40000000) only after halt — this litex_sim invocation does not pass --ram-init=demo.bin, so the image is not preloaded:

monitor reset halt
load demo/demo.elf
break main
continue

If sim is slow and keep_alive() warnings (slow bitbang) are seen, prefer target extended-remote and set remotetimeout 120.


SWD — OpenOCD

LaneOpenOCD buildConfigsGives you
raw APmasteropenocd_swd_remote.cfg + vexriscv_swd.cfgDPIDR + dap apregdmstatus; no GDB
riscv Vexriscv forkdisdi/openocd vexriscv-gateway (master + 9786 + gateway)+ vexriscv_swd_riscv_master.cfgexamine + halt/resume/regs + GDB :3333

Prerequisites (SWD-specific)

Configs

FileRole
openocd_swd_remote.cfgremote_bitbanglocalhost:44854, transport select swd
vexriscv_swd.cfgSW-DP + DAP + vexriscv_dmi_read/write + vexriscv_swd_smokeno riscv target
vexriscv_swd_riscv_master.cfgVexriscv fork: dtm create -type vexriscv-gateway + riscv + gdb-attach halt

SWD Reading dmstatus, all the way down

Terminal 1 — sim (keep running)

GoalExtra flag
Attach / regs / raw-AP smoke(none) — wait for Found port 44854 + BIOS litex>
Debug the demo app (break main / continue / bt)--ram-init=demo.bin — wait for serialboot timeout → Executing booted program at 0x40000000litex-demo-app>
litex_sim \
  --integrated-main-ram-size=0x10000 \
  --cpu-type=vexriscv_smp \
  --cpu-variant=linux \
  --cpu-count=1 \
  --with-privileged-debug \
  --with-swd-debug \
  --with-swdremote \
  --non-interactive
  --ram-init=demo.bin
FlagRole
--with-privileged-debugOfficial DebugModule (required; SWD is official-stack only)
--with-swd-debugCluster SWD transport + _Swd netlist token
--with-swdremoteLiteX sim module swdremote — OpenOCD SWD bitbang on TCP 44854
--ram-init=demo.binPreload demo into main_ram @ 0x40000000 (demo-debug only)

Terminal 2 — raw-AP smoke (stock master; no GDB)

One-shot:

openocd -s tcl \
  -f openocd_swd_remote.cfg \
  -f vexriscv_swd.cfg \
  -c init -c vexriscv_swd_smoke -c shutdown

Verified success:

Info : SWD DPIDR 0x0ba11aab
AP_IDR   = 0x74726976
dmstatus = 0x004c0c82 (version=2 authenticated=1 allrunning=1 allhalted=0)
PASS: SWD -> SW-DP -> DMI gateway -> DebugModule

Interactive Tcl helpers (same two configs, stay open):

vexriscv_swd_smoke
vexriscv_dmi_read 0x11          ;# dmstatus

Terminal 2 — riscv target + GDB server (Vexriscv fork: disdi/openocd vexriscv-gateway)

# Use the openocd binary built from:
#   https://github.com/disdi/openocd/tree/vexriscv-gateway
openocd -s tcl \
  -f openocd_swd_remote.cfg \
  -f vexriscv_swd.cfg \
  -f vexriscv_swd_riscv_master.cfg

Verified examine (real hart, not stub):

Info : SWD DPIDR 0x0ba11aab
Info : [vexriscv.rv] datacount=1 progbufsize=2
Info : [vexriscv.rv] Examined RISC-V core
Info : [vexriscv.rv]  XLEN=32, misa=0x40141101
vexriscv.rv halted due to debug-request.

misa=0x40141101 = RV32 I+M+A+S+U. Check halt/resume by curstate, not only by log lines: resume may print halted due to single-step. while stepping off a breakpoint — that is not a failure.

Terminal 3 — GDB (after the Vexriscv fork is listening on :3333)

Start GDB with the ELF for symbols (attach-only or demo-debug):

riscv64-unknown-elf-gdb demo/demo.elf

Attach and inspect

set remotetimeout 300
set pagination off
set arch riscv:rv32
target extended-remote localhost:3333
info registers
x/6i $pc

vexriscv_swd_riscv_master.cfg sets -event gdb-attach halt, so GDB attaches to an already-halted target — no monitor halt is required.

Debug the demo app (break / continue / bt)

If litex_sim is passed with --ram-init=demo.bin :

set remotetimeout 300
set pagination off
set arch riscv:rv32
target extended-remote localhost:3333
# Image is already in main_ram via --ram-init=demo.bin.

x/8xw 0x40000000        # confirm preload (e.g. 0x0b00006f 0x00000013 ...)
set $pc = 0x40000000    # re-enter demo at _start so main is hit cleanly
break main
continue
bt

Expected: stop at main (typically around 0x4000069c); bt shows #0 main ().


Testing

Phase 4: Verification and Testing

Host-visible sim paths that already pass are tracked under Phase 3 (raw-AP smoke, riscv examine, GDB attach/regs, demo break/continue). This chapter is the broader regression matrix — much of it still open, especially on hardware.


Done in sim (via Phase 3)

  • SWD-DTM → DebugBus → DM path at sbt (Phases 2A–2C) and LiteX Verilator SoC
  • OpenOCD master smoke: DPIDR + dap apregdmstatus
  • OpenOCD riscv examine + halt/resume/register access over SWD (patched builds)
  • GDB attach / registers / memory R/W over SWD
  • GDB break main / continue / backtrace on preloaded demo (no GDB load)

Still open

  • Automated SWD stimuli via custom OpenOCD target on real CMSIS-DAP hardware (Arty)
  • Test dmactive activation/deactivation sequence (formal matrix)
  • Test abstract command error handling (all 7 cmderr codes)
  • Test EBREAK behavior with dcsr.ebreakm / ebreaks / ebreaku combinations
  • Test single-step across privilege mode transitions
  • Test trigger module: each trigger type, chaining, dmode security
  • Test System Bus Access error handling (if implemented)
  • Test authentication mechanism (if implemented)
  • Decide dmstatus.version claim (2 = 0.13 vs 3 = 1.0)
  • Regression suite for all implemented features (CI-friendly)

Documentation

Phase 5: Documentation & Developer Interface


Done

  • Phase 2A–2C architecture chapters in this book (SwdPhy / SwdDp / SwdDmiGateway)
  • Operator how-to for sim three-terminal attach (JTAG and SWD) — Phase 3
  • Document placeholder DPIDR, AP_IDR, and Phase 2C DMI_ADDR / DMI_DATA / POSTED_READ map (see Phase 2C)

Still open

  • Broader user-facing guide beyond sim (FPGA CMSIS-DAP, probe-rs, multi-probe notes)
  • Finalize published identification constants once IDs leave placeholder status
  • Document which optional RISC-V debug-spec features are implemented vs stubbed (dmstatus.version, abstract commands surface, triggers, SBA)
  • Published OpenOCD Vexriscv fork: disdi/openocd vexriscv-gateway (9786 + fixes + gateway; still unmerged upstream)