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 :

Phase 1 adds missing features in DebugModule from 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

Status: implemented and sim-verified.

Tasks :

  • Define the internal 2A↔2B transaction seam first: SwdDpCmd/SwdDpRsp/SwdDpWrite flows so 2A is verifiable standalone against a stubbed DP
  • Implement 2-wire physical interface on the target: swclk input (driven by probe) + swdio as i/o/oe (tristate/IOBUF at SoC level)
  • Implement packet request parser — 8-bit frame: Start(1), APnDP(1), RnW(1), A2:3, Parity(1), Stop(1), Park(1)
  • Implement ACK response generator — 3-bit ACK[2:0]: OK(0b001), WAIT(0b010), FAULT(0b100) (ADIv6.0 Table B4-1). Transmitted LSB-first like all SWD data, so OK appears on the wire as 1,0,0 — do not confuse wire order with the register value
  • Implement turnaround period management — direction change on SWDIO between host-driven and target-driven phases (Sec B4.1.3); fixed at 1 cycle (DLCR.TURNROUND unsupported)
  • Implement WDATA phase — 33-bit host→target: WDATA[0:31] + parity (for write operations after OK ACK)
  • Implement RDATA phase — 33-bit target→host: RDATA[0:31] + parity (for read operations after OK ACK)
  • Implement even parity checker/generator — separate parity on packet request (4 bits: APnDP, RnW, A[2:3]) and data transfer (32 bits) (Sec B4.1.6)
  • Implement LSB-first bit ordering for all data values (Sec B4.1.5)
  • Accept idle cycles between transactions (Sec B4.1.4); back-to-back with zero idle also works
  • Implement line reset detection — 50+ clock cycles with SWDIO HIGH, followed by 2+ idle cycles (Sec B4.3.3)
  • Start with SWD protocol version 1 (point-to-point); version 2 multi-drop support is optional; no ORUNDETECT

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.

1.3 Frame timing (rising-edge numbering)

Request bits are sampled at edges e1..e8 (start, APnDP, RnW, A2, A3, parity, stop, park — LSB first). cmd fires at e8.

Read, ACK=OK (one turnaround each way, target keeps the line between ACK and RDATA):

e1..e8   host request          (target oe=0)
e9       turnaround            -> target drives ACK[0] at e9
e9..e11  ACK[0..2]
e12..e43 RDATA[0..31]          (from rsp.rdata, LSB first)
e44      even parity of RDATA
e45      release (oe -> 0)     turnaround back to host

Write, ACK=OK (two turnarounds around the ACK — the bug the tests caught: sampling one edge early captures the turnaround as data bit 0):

e1..e8   host request
e9       turnaround            -> target drives ACK[0]
e9..e11  ACK[0..2]
e12      release (oe -> 0)     \  second turnaround: host takes the line,
e13      turnaround bit period /  drives its first WDATA bit after it
e14..e45 WDATA[0..31]          sampled LSB first into a shift register
e46      parity bit            -> dp.wr fires with data + parityOk verdict

WAIT/FAULT (either direction): the data phase is skipped — after ACK[2] the target releases immediately and returns to idle. (No ORUNDETECT support, so this is unconditional.)

1.4 FSM

States: IDLE → HEADER → ACK → {READ_DATA | WR_TRN → WRITE_DATA | RELEASE} → IDLE, plus ERROR and RESET_WAIT (power-up init is RESET_WAIT, so the target stays quiet until the line is seen low once).

  • Protocol error (request parity, stop≠0, park≠1): enter ERROR — the target stops driving and ignores all traffic until line reset (ADIv6.0 §B4.2.5).
  • Line reset detector (independent of the FSM): a saturating counter of consecutive swdio.i == 1 samples while the target is not driving; at 50 it forces RESET_WAIT from any state (including ERROR), which falls to IDLE on the first low (idle) cycle. The counter is frozen/cleared while oe=1 so target-driven phases can't fake a reset.

1.4.1 Target state machine for one frame (implementation-oriented)

A practical encoding of Section B4.1–B4.2 from the specification for a single transaction on the target.

This section has two layers:

  1. §1.4.2 abstract diagram — phase names aligned with ADI figures (includes TRN_H2T and optional OVERRUN_DATA).
  2. §1.4.3–§1.4.5 Phase 2A SwdPhy — how that diagram maps onto the RTL enum and transitions in DebugTransportModuleSwd.scala (case class SwdPhy).

1.4.2 Abstract one-frame FSM (spec-oriented)


1.4.3 State inventory — diagram §1.4.2 ↔ RTL

Diagram §1.4.2RTL EStateRole
IDLEIDLEWait for Start; between frames (oDrive := False)
HEADERHEADERLSB-first shift-in after Start; validate at park
TRN_H2T(no named state)Folded into first cycle of ACK
ACKACKDrive ACK[0..2] from ackNow / rspHold (cnt 0..2)
READ_DATAREAD_DATARDATA[0:31] + even parity; keep oe=1 after ACK
WR_TRNWR_TRNPost-ACK turnaround before write (oe=0, 2 cycles)
WRITE_DATAWRITE_DATASample WDATA + parity → dp.wr
RELEASERELEASERelease after target-driven phase → IDLE
ERRORERRORProtocol error: do not drive; ignore headers
OVERRUN_DATAabsentORUNDETECT out of scope (WAIT/FAULT always skip data)
(recovery)RESET_WAITAfter line-reset hit; leave on first low → IDLE

1.4.4 Transition mapping (diagram → RTL)

IDLE → HEADER — Start bit = 1

DiagramRTL
Start sampled 1when(dio) { state := HEADER; cnt := 0 } in IDLE

Start is not stored in hdr; it only opens the header window. Remaining bits are shifted in HEADER.

HEADER → ERROR or valid exit

On park edge (cnt === 6), hdr holds APnDP…Stop and dio is Park:

CheckRTLSpec
Even parity of APnDP, RnW, A2, A3(apNdp ^ rnw ^ a2 ^ a3) === hdr(4)B4.1.6 / B4.2.5
Stop = 0stopOk = !hdr(5)B4.2.5
Park = 1dio on that edgeB4.2.5
  • Fail → ERROR (silent until line reset).
  • Pass → cmdValid pulse (SwdDpCmd: apNdp, rnw, addr) and state := ACK
    (“cmd accepted” on the diagram’s park edge).

Where is diagram TRN_H2T?

There is no TRN_H2T state. Comment in RTL: “next cycle is the turnaround” after park, then ACK both:

  1. owns the host→target turnaround cycle (from the host’s view), and
  2. drives the three ACK bits.
EdgeDiagram nameRTL
Parkend of HEADERHEADER → schedule ACK, fire cmd
Next rising edgeTRN_H2T / first ACK driveFirst ACK cycle: oe=1, ACK[0]
Next two edgesrest of ACKACK[1], ACK[2] (cnt 1, 2)

So diagram TRN_H2T + ACK collapse into RTL ACK with cnt = 0,1,2. Same wire timing as Fig B4-1/B4-2; one fewer named state.

ACK → … after three ACK bits (cnt === 2)

Diagram edgeRTLMatch?
OK ∧ read → READ_DATAackNow === OK && cmdPayload.rnwREAD_DATAExact
OK ∧ write → WR_TRNackNow === OK && !rnwWR_TRNExact
≠OK, no overrun → RELEASEelse → RELEASEExact
≠OK ∧ ORUNDETECT → OVERRUN_DATAneverBy design (scope)

ACK value comes from the 2B seam, not sticky logic inside 2A:

  • io.dp.rsp → latched in rspHold when valid
  • ackNow = Mux(rsp.valid, rsp.ack, rspHold.ack)
  • Encoded LSB-first on the wire: OK=001, WAIT=010, FAULT=100

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.


1.4.5 Side-by-side graph

Diagram §1.4.2 (abstract)                 SwdPhy RTL
───────────────────────                 ──────────

[*] → IDLE                              RESET_WAIT → IDLE  (after first low)
        │                                      │
        │ Start=1                              │ dio=1
        ▼                                      ▼
     HEADER ──bad──► ERROR                  HEADER ──bad──► ERROR
        │                                      │
        │ good park                            │ good park (+ cmd pulse)
        ▼                                      ▼
    TRN_H2T  ─────────────────────────────►  (implicit; first ACK cycle)
        │                                      │
        ▼                                      ▼
       ACK ──OK∧R──► READ_DATA ──► RELEASE    ACK ──OK∧R──► READ_DATA ──► RELEASE
        │                 │                     │                 │
        │ OK∧W            └────────► IDLE       │ OK∧W            └────────► IDLE
        ▼                                      ▼
     WR_TRN ──► WRITE_DATA ──► IDLE         WR_TRN ──► WRITE_DATA ──► IDLE
        │                                      │
        │ ≠OK (no overrun)                     │ ≠OK
        ▼                                      ▼
     RELEASE ──► IDLE                        RELEASE ──► IDLE
        │
        │ ≠OK ∧ ORUNDETECT
        ▼
   OVERRUN_DATA ──► IDLE                   (not implemented)

  ERROR ──line reset──► IDLE               ERROR ──line reset──► RESET_WAIT ──low──► IDLE
                                           (line reset also from any other state)

2. How the testbench works — DebugSwdTest

2.1 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.

2.2 Always-ready stub DP

The DP behind the seam is a stub programmed by two vars (stubAck, stubRdata) and observed through two queues (cmds — decoded requests, wrs — write commits with the parity verdict). A forked thread runs every sampling:

  • holds rsp.valid true continuously with the programmed ack/rdata, and
  • records cmd/wr pulses into the queues.

The always-ready shape is deliberate: the ADI contract requires the DP to answer within the turnaround cycle. An earlier stub that reacted to cmd.valid from a sim thread arrived one clock late nondeterministically (SpinalSim thread-scheduling race) and produced flaky all-zero ACKs — the always-valid response is both race-free and the honest model of the combinational 2B register file.

2.3 Transaction helpers (assertions built in)

  • header(...) — sends the 8-bit request; can inject flipParity / badStop / badPark; asserts the target never drives during the request.
  • readAck() — collects ACK[0..2]; asserts oe on all three bits.
  • transactRead(...) — request → trn → ACK; on OK collects 33 target bits asserting oe throughout, recomputes and checks even parity, then asserts release on the trailing turnaround. On WAIT/FAULT asserts the very next cycle is undriven (data phase skipped).
  • transactWrite(...) — request → trn → ACK; on OK asserts release for the second turnaround, then drives 33 bits (optionally with flipDataParity). On WAIT/FAULT same skip assertion as reads.
  • lineReset() — 52 high cycles + 2 idle; idle(n) / ones(n) primitives.

Each test runs in a fresh sim (compiled.doSim) with reset applied, the stub forked, and 4 idle cycles before the body.


3. What each test verifies

#TestVerifies
1write reaches stub bit-exactFull OK write frame: decoded cmd fields (apNdp=0, rnw=0, A[3:2]=2) reach the stub; 0xCAFE1234 arrives in the commit bit-exact with parityOk=true. Proves WDATA sampling starts after the second turnaround (this test caught the off-by-one that received data << 1).
2read returns stub data bit-exact with parityFull OK read frame: 0x12345678 returned LSB-first with correct even parity; cmd decoded as (apNdp=1, rnw=1, addr=1); target drives ACK+33 data bits and releases on the trailing turnaround.
3DPIDR smoke readThe §6.6 step 1 exit smoke test: DP read at A[3:2]=00 returns the stub's DPIDR constant — the exact transaction OpenOCD issues first after line reset.
4header parity error silences target until line resetRequest-parity error → ERROR: target undriven for the error frame and for a subsequent well-formed request; no cmd ever reaches the DP; full line reset restores normal operation (verified by a clean read after).
5stop bit error silences target until line resetSame protocol-error contract triggered via stop≠0; recovery verified with a clean write.
6WAIT and FAULT skip the data phaseACK=WAIT then FAULT for both read and write: correct 3-bit ACK serialization (LSB first), line released immediately after ACK[2] (no data phase), no write commits, and a following OK read works with no reset needed. Also checks all 4 requests still reached the DP (cmds.size == 4).
7back-to-back transactions without idle cyclesA new request may start on the cycle right after the previous frame ends (write→read with zero idle); both transactions complete bit-exact.
8write data parity error is flagged and recoverableCorrupted WDATA parity → commit fires with parityOk=false (WDATAERR material for 2B), data still delivered; not a line-level protocol error — the next read succeeds without line reset.
949 high cycles are not a line reset, 50 areThe reset threshold exactly: from ERROR, 49 highs + idle leaves the target silent; 50 highs + idle restores it. (The bench breaks the run of 1s after the error header so the park bit can't pre-count toward the 50.)

Embedded in every test via the helpers: turnaround positions (oe window), LSB-first ordering, and the request-phase no-drive rule.


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

Status: implemented and sim-verified (Jul 2026)

Tasks:

  • Implement DPIDR register (DP read, A[3:2]=0b00) — read-only device ID with manufacturer, version, min/revision fields
  • Implement CTRL/STAT register (DP read/write, A[3:2]=0b01, SELECT.DPBANKSEL=0x0) — sticky error flags: STICKYERR, STICKYCMP, STICKYORUN, WDATAERR; power control: CDBGPWRUPREQ/ACK, CSYSPWRUPREQ/ACK; ORUNDETECT enable
  • Implement SELECT register (DP write, A[3:2]=0b10) — DPBANKSEL (4 bits) + ADDR (AP address selection)
  • Implement RDBUFF register (DP read, A[3:2]=0b11) — read buffer for previous AP read result
  • Implement ABORT register (DP write, A[3:2]=0b00) — DAPABORT, STKCMPCLR, STKERRCLR, WDERRCLR, ORUNERRCLR
  • Implement sticky error handling — FAULT response when any sticky flag is set; errors cleared only via ABORT register (Sec B1.2)
  • Implement WAIT response logic — issued when AP/DP access is outstanding or AP read result not yet available (Sec B4.2.3)
  • Consume SwdDpWrite.parityOk as WDATAERR sticky material (line-layer protocol error / line reset already handled in Phase 2A)

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)

SwdPhyDp assembles SwdPhy + SwdDp and is the step-1b DUT: SWD pins on one side, AP seam on the other.

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

CTRL/STAT implemented bits: ORUNDETECT[0] (stored only), STICKYORUN[1], STICKYCMP[4], STICKYERR[5], WDATAERR[7], CDBGPWRUPREQ/ACK[28/29], CSYSPWRUPREQ/ACK[30/31] — the ACKs mirror the REQs. All other bits read as zero.

The DPIDR default has a non-ARM DESIGNER field ([11:1] = 0x555, bit0 = 1, DPv1) — a deliberate placeholder to be replaced per the identification rules before any host-tool integration.

1.3 ACK policy (the WAIT/FAULT generation deferred from 2A)

Decided combinationally off the registered cmd pulse, so the answer lands within the turnaround as the 2A contract requires:

ConditionACKScope
any sticky flag setFAULTAP accesses and RDBUFF reads only (ADIv5.2 §B4.2.4) — DPIDR/CTRL·STAT/SELECT/ABORT/RESEND keep answering OK so the host can diagnose and clear
AP transaction outstandingWAITAP accesses and RDBUFF reads — DP accesses (e.g. CTRL/STAT polling) still OK
otherwiseOKthe access itself only happens on OK

FAULT takes precedence over WAIT.

1.4 Posted AP reads, write commit, stickies

  • AP read (ACK=OK): answered immediately from rdBuffer (posted semantics — the previous result), while the new access is launched on ap.cmd. Completion loads rdBuffer; RDBUFF/RESEND then return it without launching anything.
  • AP write (ACK=OK): launched only at the WDATA commit (SwdDpWrite with parityOk), carrying the data. Busy until completion.
  • STICKYERR: set by a completion with error=1 (the completion's data is discarded).
  • WDATAERR: set when the WDATA phase fails parity — the commit is dropped (DP register writes and AP writes alike). Not a line-level protocol error; no line reset needed.
  • DAPABORT: clears busy and marks the in-flight completion to be discarded when it eventually arrives, so a stale response cannot corrupt rdBuffer.
  • DP writes commit at the WDATA phase too: the DP remembers the last OK-acked write target (last.pendingWrite) from the request; WAIT/FAULT frames produce no commit (2A skips their data phase), so a gated write can never take effect.

1.5 Graphical model — spec intent vs RTL (side-by-side, as in Phase2A.md §1.4)

Phase 2B is not a per-frame FSM like SwdPhy — it is a register file plus an access decision made once per request and a small amount of persistent state (stickies, busy, read buffer, pending-write). The diagrams therefore show the decision/dataflow rather than wire states.

1.5.1 Abstract model (ADIv5.2 §B4.2 behavior)

The most 2B-specific behavior — the posted AP read — as a sequence:

1.5.2 SwdDp RTL — the same flow in signals

Source: spinal.lib.cpu.riscv.debug.SwdDp (three concurrent paths: request, WDATA commit, AP completion — no FSM, one decision per cmd pulse).

1.5.3 Element inventory — abstract ↔ RTL

Abstract (§1.5.1)RTL constructNotes
sticky flagsstickyOrun/stickyCmp/stickyErr/wdataErr regsonly stickyErr/wdataErr hardware-set (scope)
"AP outstanding"apBusy regset at launch, cleared at completion / DAPABORT
read bufferrdBuffer regserves AP-read RDATA, RDBUFF and RESEND
ACK decisioncombinational mux off the cmd pulseFAULT > WAIT > OK; lands within the turnaround
"access not performed"launches/commits gated on ack == OKapRdFire, last.pendingWrite
write accepted, data laterlast.pendingWrite/isApReg/addrReg regsthe request↔commit link across the WDATA phase
AP write launchapWrFire at wr.validcarries wr.data; not at the request
completion → bufferapWasRead gatewrite completions can't clobber rdBuffer
DAPABORTapDiscard flagRTL-only: in-flight completion dropped safely

1.5.4 Side-by-side graph

Abstract (ADIv5.2 B4.2)                        SwdDp RTL
───────────────────────                        ─────────
request ──sticky?(AP/RDBUFF)──► FAULT          cmd.valid ──anySticky && gated──► ack=FAULT
   │ no                                             │
   ├────busy?(AP/RDBUFF)──────► WAIT                ├──apBusy && gated──────────► ack=WAIT
   │ no                                             │
   ▼                                                ▼
  OK ──DP read──► DPIDR/CTRL·STAT/                ack=OK ── !apNdp && rnw ──► dpReadData mux
   │              RESEND/RDBUFF                     │
   ├──AP read──► previous result                    ├── apRdFire ──► rdata=rdBuffer,
   │             + launch new read                  │                ap.cmd, apBusy:=1
   │                                                │
   └──write───► ACK now, data later                 └── last.pendingWrite := True
                      │                                            │
             WDATA parity ok?                            wr.valid: parityOk?
              no  → WDATAERR, drop                        no  → wdataErr := True
              yes → ABORT / register /                    yes → ABORT/CTRL·STAT/SELECT
                    AP write launch                             or apWrFire ──► ap.cmd

completion ── error ──► STICKYERR              ap.rsp.valid ── error ──► stickyErr := 1
           └─ data  ──► read buffer                         └─ apWasRead ──► rdBuffer := data
                                               (apDiscard: DAPABORT'd completion dropped)

1.5.5 Match / difference summary

AspectMatch?
ACK precedence FAULT > WAIT > OKYes
Sticky FAULT scoped to AP + RDBUFF; other DP accesses keep workingYes (ADIv5.2 §B4.2.4)
Posted AP read: previous result now, new result to bufferYes — spec leaves first-read data implementation-defined; RTL returns rdBuffer
Write ACKed at request, committed after WDATA parityYes (last.pendingWrite)
WDATAERR on data-phase parity fail, commit dropped, no line reset neededYes
RESENDAlias of rdBuffer — same value as RDBUFF, no relaunch
ORUNDETECT / STICKYORUN / STICKYCMP hardware behaviorNo — stored/clearable only (deliberate scope)
Power-up REQ→ACK handshakeMirrored, no power controller modeled
apDiscard (DAPABORT in-flight drop), apWasRead gateRTL-only robustness details

One-sentence takeaway: §1.5.1 is the ADI access contract; SwdDp implements it as one combinational decision per request plus three concurrent signal paths (request / commit / completion), with last.* bridging the ACK-before-data gap that defines SWD writes.


1.6 Scope limits (deliberate)

ORUNDETECT is stored but overrun detection is not implemented — STICKYORUN and STICKYCMP are never set by hardware, only ABORT-clearable · CTRL/STAT banks other than 0 (DLCR etc.) are RAZ/WI · TARGETSEL is v2 and ignored · power-up ACKs mirror REQs rather than modeling a power controller.


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.


3. What each test verifies

#TestVerifies
1DPIDR read returns the configured valueThe step-1b headline: a real DPIDR from the register file (no stub), bit-exact against the elaboration parameter.
2CTRL/STAT power-up requests mirror ACKsCTRL/STAT reads as 0 fresh; writing CDBGPWRUPREQ+CSYSPWRUPREQ (0x50000000) reads back 0xF0000000 — REQs stored, ACKs mirrored. Also proves the DP write path commits at the WDATA phase.
3SELECT DPBANKSEL banks CTRL/STATWith DPBANKSEL=1 the CTRL/STAT address reads as zero (unimplemented bank, still ACK=OK); switching back to bank 0 restores the real register.
4AP read is posted; RDBUFF returns the completed resultADI posted-read semantics end-to-end: first AP read returns the stale buffer while launching the access; RDBUFF returns the completion; a second AP read returns the previous result while posting the next.
5WAIT while an AP access is outstanding; DP accesses stay OKWAIT generation (deferred from 2A): AP read and RDBUFF get WAIT while busy; CTRL/STAT polling still answers OK — the exact behavior a host retry loop relies on.
6AP completion error sets STICKYERR; FAULT until ABORT clearsThe sticky-error contract: FAULT on AP + RDBUFF, DPIDR/CTRL·STAT unaffected, STICKYERR visible in CTRL/STAT, cleared only via ABORT.STKERRCLR, then normal operation resumes.
7Write data parity error sets WDATAERR and drops the commitA DP write with corrupted WDATA parity is ACKed OK (ACK precedes data) but never commits — the power bits stay clear while WDATAERR sets; FAULT gating applies; ABORT.WDERRCLR clears.
8AP write commits after the data phase and holds busyThe AP write launches only at the WDATA commit with bit-exact data, holds WAIT for subsequent AP accesses until completion, and the write completion does not clobber rdBuffer.
9RESEND returns the same value as RDBUFFThe DP-read A[3:2]=10 decode: both RESEND and RDBUFF return the posted result without launching a new AP access.

Everything from Phase 2A (turnaround positions, LSB-first, parity, line reset, protocol-error silence) is still enforced on every transaction by the shared driver's embedded assertions — now exercised against the real DP.


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

Status: implemented and sim-verified.

Tasks:

  • Map AP read/write operations to DebugBus.cmd (same DebugCmd interface used by JTAG DTM)
  • Implement AP address decoding — translate SELECT.ADDR + A[3:2] to DMI address for DebugBus.cmd.address
  • Implement posted AP reads — first AP read returns UNKNOWN data; result available on next AP read or RDBUFF read (Sec B4.2.2)
  • Wire DebugBus.rsp to RDATA output and ACK generation
  • Handle cross-clock-domain between SWD clock (SWCLK) and debug clock domain (reuse ccToggle pattern from JTAG DTM)
  • Implement WAIT response when DebugBus.cmd is not ready (DM busy)
  • Implement FAULT response when DebugBus.rsp.error is set

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. What is implemented

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.

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) — crosses domains
11POSTED_READROlast completed DMI read result (RDBUFF semantics) — 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 Clock crossing — the JTAG DTM pattern, reused

The gateway is an Area with the same two-domain shape as DebugTransportModuleJtag:

  • cmd (SWCLK → debug): dmiCmd.ccToggle(...).toStream.m2sPipe(crossClockData = true, holdPayload = true)bus.cmd. Stream backpressure from a busy DM simply extends the in-flight window — the DP's WAIT logic (2B) covers it.
  • rsp (debug → SWCLK): bus.rsp.ccToggle(..., withOutputBufferedReset = false) — the flag matters: the SWCLK domain is BOOT-reset (a 2-wire interface has no reset pin; line reset is the protocol-level reset), and a buffered reset cannot be synthesized into a BOOT domain (SpinalHDL asserts). Pop-side synchronizers boot-init instead.
  • Solicited-completion gating (dmiPending here, apBusy in 2B): only a response to an in-flight access completes anything.

The DM responds on DebugBus.rsp for both reads and writes (DebugBusSlaveFactory stages every cmd.fire), so AP writes complete on the crossed response — no self-acknowledgment needed.

1.3 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.4 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. What each test verifies

#TestVerifies
1DPIDR reads through the assembled transportThe full stack elaborates and runs: BOOT-domain phy, async clocks, fiber-style wiring — and the plain DP path still works.
2AP IDR via posted readGateway local completion path + 2B posted-read plumbing end-to-end (AP_IDR lands in RDBUFF).
3DMI_ADDR write and readbackThe address latch: AP write commits (WDATA path through the gateway), AP read returns it.
4dmstatus read over SWD — step 2 exit criterionThe headline: DMI_ADDR←0x11, DMI_DATA read crosses to the DM and back; RDBUFF poll returns dmstatus with version=2 and authenticated=1.
5dmcontrol dmactive write and readbackFull DMI write path into a real DM register (resets 0, reads back 1) — write launch at commit, crossed completion, subsequent read.
6abstractauto write and readbackA second, WARL-masked DM register round-trip (0x18, bits [0]/[17:16]) including clearing. (data0/progbuf cannot round-trip with a stubbed hart — see §4.3.)
7POSTED_READ returns the last DMI read resultThe gateway's lastRead alias agrees with RDBUFF after a completed DMI read.

WAIT responses are exercised implicitly on nearly every test by the retry helpers (the async completion latency makes WAIT occur naturally); FAULT-on-rsp.error was unit-tested in the 1b suite (the real DM's factory never raises error).


4. 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

5. 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).

StepContentStatus
1Cluster / core.py / swdremote / litex_sim / Makefile + OpenOCD smoke✅ Completed
2Custom OpenOCD option A (riscv glue) + GDB; finalize IDs if host hard-codes them🟡 DAP/DMI helpers done; GDB open

Note:

  • Support for official RISC-V DM only (withPrivilegedDebug).
  • One Debug transport per build — no JTAG-DTM + SWD-DTM on the same DebugBus without arbitration. Do not set JTAG_SIM=1 and SWD_SIM=1 together.

Code Repository :

  • Litex - https://github.com/disdi/litex/tree/swd
  • VexRiscv - https://github.com/disdi/VexRiscv/tree/phase3

(Update submodules in pythondata-cpu-vexriscv_smp to above branch)


SWD specific tasks

Step 1 (Already done)

  • Create DebugTransportModuleSwd with io.swclk (input, probe-driven) + io.swdio as i/o/oe — Phase 2A–2C
  • Add withSwdTransport() to DebugModuleFiber (mutually exclusive with withJtagTap() per build) — Phase 2C
  • LiteX SMP cluster: swd param, --swd CLI, three-wire debugPort_swclk / swdio_{i,o,oe}, noTap guarded !jtagTap && !swd — Phase 3
  • LiteX sim: --with-swd-debug, _Swd netlist token, add_swd(), swdremote on TCP 44854, SWD_SIM=1 Makefile targets — Phase 3
  • OpenOCD smoke on sim: DPIDR 0x0ba11aab + dap apregdmstatus 0x004c0c82
  • OpenOCD cfgs: openocd_swd_remote.cfg + vexriscv_swd.cfg (DAP + vexriscv_dmi_read/write + smoke)
  • Cluster asserts: reject swd && jtagTap; require privilegedDebug when swd

Step 2 — host tooling + GDB (open)

  • [~] Custom OpenOCD target: DAP/DMI procs done; riscv target glue + GDB still open
    • Exit: halt / resume / register read of the sim CPU over SWD (analogous to JTAG three-terminal)
    • Files: vexriscv_swd.cfg (extend), optional interface cfgs for hardware
    • Requires real hart in LiteX sim (not Phase2C stub DebugHartBus)
  • Finalize DPIDR / AP_IDR before host scripts hard-code them
    • Rules + placeholders owned by Phase 2C (DPIDR=0x0BA11AAB, AP_IDR=0x74726976)
    • Must not look like ARM Cortex SW-DP (0x2ba01477 / DESIGNER 0x23B)
  • GDB workflow on sim: target extended-remote :3333, set remotetimeout 120, monitor reset halt, info registers

JTAG — End to End Workflow

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

Prerequisites

  • litex_sim
  • OpenOCD
  • 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 -ex "set remotetimeout 120" \
  -ex "set remote disable-packet-optimization 1" \
  -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

SWD — what is possible now

step 1 landed LiteX + swdremote + OpenOCD smoke. In Terminal 3, NO GDB yet because vexriscv_swd.cfg deliberately does not target create riscv (stock OpenOCD has no RISC-V-over-SWD path).

Prerequisites (SWD-specific)

  • litex_sim
  • OpenOCD with SWD remote_bitbang
  • GDB not usable on this path until step 2

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

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 \
  --with-swd-debug \
  --with-swdremote \
  --non-interactive
FlagRole
--integrated-main-ram-size=0x1000064 KiB main RAM for sim
--cpu-type=vexriscv_smp / --cpu-variant=linux / --cpu-count=1SMP Linux-capable cluster, 1 hart
--with-privileged-debugOfficial DebugModule (required; SWD is official-stack only)
--with-swd-debugCluster SWD transport + _Swd netlist token; forces privileged debug on LiteX side
--with-swdremoteLiteX sim module swdremote — OpenOCD remote_bitbang SWD on TCP 44854
--non-interactiveKeep sim running as the target for OpenOCD

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

Do NOT pass --jtag-tap / --with-jtagremote here.

Terminal 2 — OpenOCD smoke (after Terminal 1 is up)

One-shot (connect, vexriscv_swd_smoke, exit):

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

Terminal 3 — GDB

Not supported yet (step 2).
No :3333 riscv GDB server from the SWD configs above.

when Step 2 is done — same shape as JTAG:

TerminalIntended SWD flow (not done)
1litex_sim … --with-privileged-debug --with-swd-debug --with-swdremote --non-interactive
2OpenOCD + SWD + riscv examine → listen :3333
3riscv64-unknown-elf-gdbtarget extended-remote localhost:3333 → halt / regs / load

Host attach workflows (sim only)

Side-by-side

TerminalJTAG — full three-terminal ✅SWD — partial 🟡
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)
2 — OpenOCDStock riscv target + BSCAN tunnel; examines hart; GDB server :3333transport select swd + DAP; DPIDR / dmstatus via dap apreg; no riscv target yet
3 — GDBtarget extended-remote localhost:3333 → halt / regs / loadNot available until step 2 (riscv glue + GDB)
CapabilityJTAG nowSWD now
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 core; found 1 harts❌ step 2
GDB halt / resume / info registers❌ step 2
load demo.elf / break / continue❌ step 2
OpenOCD binaryremote_bitbang SWD chars

Testing

Phase 4: Verification and Testing


Tasks

  • Build simulation testbenches for SWD-DTM → DebugBus → DM → CPU path (bit-level SWD + DMI gateway)
  • Send automated SWD stimuli via custom OpenOCD target (Phase 2D) on real CMSIS-DAP hardware
  • Test dmactive activation/deactivation sequence
  • 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)
  • Regression tests for all implemented features

Documentation

Phase 5: Documentation & Developer Interface


Tasks

  • Write internal architecture documentation (SW-DP / DMI gateway AP / DebugBus / DM)
  • Write user-facing guide: official JTAG (riscv-openocd + riscv-target) vs SWD (custom target)
  • Document DPIDR, AP_IDR, and DMI_ADDR register map
  • Document which optional RISC-V debug spec features are implemented