Standard Debug Spec Milestone
Below are milestones planned in the project :
- Phase 1A JTAG DTM + DMI Bus
- Phase 1B Core DM (dmcontrol/dmstatus)
- Phase 1C Debug CSRs
- Phase 1D Triggers
- Phase 2A SWD protocol state machine (
SwdPhy) - Phase 2B SW-DP register file (
SwdDp) - Phase 2C DMI gateway + clock crossing
- Phase 3 Integration
- Phase 4 Testing
- Phase 5 Documentation
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:
| Signal | Role |
|---|---|
| SWCLK | Clock (usually driven by the probe/host) |
| SWDIO | Bidirectional 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) | Name | Meaning |
|---|---|---|
| 1 | Start | Always 1 |
| 1 | APnDP | 0 = DP register, 1 = AP register |
| 1 | RnW | 0 = write, 1 = read |
| 2 | A[2:3] | Register address bits A[3:2] (LSB-first on the wire) |
| 1 | Parity | Even parity over APnDP, RnW, A[2], A[3] |
| 1 | Stop | Always 0 |
| 1 | Park | Host 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) | Name | Wire order (LSB first) |
|---|---|---|
0b001 | OK | 1, 0, 0 |
0b010 | WAIT | 0, 1, 0 |
0b100 | FAULT | 0, 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)
| Direction | When | Format |
|---|---|---|
| Write | After OK + second turnaround | 32-bit WDATA + even parity (host → target) |
| Read | After OK, no turnaround (target keeps the line) | 32-bit RDATA + even parity (target → host) |
Errors and special sequences
| Condition | Target behaviour |
|---|---|
| Protocol error (bad request parity / Stop / Park) | Do not drive ACK; stay silent until line reset |
| WDATA parity fail | ACK already sent; DP sets WDATAERR and drops the write (not a line silence) |
| WAIT | Busy (e.g. previous AP access outstanding); host typically retries |
| FAULT | Sticky error set; host clears via ABORT |
| Line reset | SWDIO HIGH for ≥ 50 SWCLK, then ≥ 2 idle (LOW) |
| Idle | SWDIO 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
iand updateso/oeon rising SWCLK (OpenOCD bitbang model).
Minimal mental model (one frame)
- Host clocks an 8-bit header (Start…Park).
- Phy validates it; if bad → silence until line reset.
- If good → phy fires cmd and needs rsp.ack for the next few clocks.
- Phy drives ACK (value chosen by Phase 2B).
- If OK and read → phy streams rdata + parity from the DP.
- If OK and write → phy releases the line, samples wdata + parity, fires wr.
- If WAIT/FAULT → phy stops after ACK (no data phase here).
- 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 Implementation | Implementation Details |
|---|---|---|
| Ch 6: Debug Transport Module (DTM) | ||
| JTAG TAP with IDCODE/dtmcs/dmi | ✅ Implemented | DebugTransportModuleJtag.scala — standard IR codes, dtmcs with version/abits/idle/dmistat, dmi with op/address/data |
| DMI bus protocol | ✅ Implemented | DebugInterfaces.scala — DebugBus with DebugCmd/DebugRsp, DebugBusSlaveFactory |
| DMI busy/error handling | ✅ Implemented | dmihardreset, dmireset, pending/overrun detection |
| Cross-clock-domain DMI | ✅ Implemented | ccToggle for JTAG↔debug clock domains |
| JTAG tunnel support | ✅ Implemented | JtagTunnel.scala — tunneling through outer TAP |
Debug Module
Phase 1B: Core DM Registers
| Spec Feature (Chapter) | Vexriscv Debug Implementation | Implementation Details |
|---|---|---|
| Ch 3: Debug Module (DM) | ||
dmcontrol (0x10) | ✅ Implemented | dmactive, ndmreset, haltreq, resumereq, ackhavereset, hartsello/hi. Missing: hasel, setresethaltreq, hartreset, keepalive |
dmstatus (0x11) | ✅ Implemented | version, authenticated=1, all halted/running/unavail/nonexistent/resumeack/havereset flags, impebreak=1 |
hartinfo (0x12) | ✅ Implemented | dataaddr=0, datasize=0, dataaccess=0, nscratch=0 |
abstractcs (0x16) | ✅ Implemented | datacount, progbufsize, busy, cmderr (all 7 error codes) |
command (0x17) — Access Register | ✅ Implemented | cmdtype=0 with full FSM: transfer, write, postexec, aarsize validation, GPR+FPU register access |
command (0x17) — Access Memory | ❌ Not implemented | Returns NOT_SUPPORTED |
command (0x17) — Quick Access | ❌ Not implemented | Returns NOT_SUPPORTED |
abstractauto (0x18) | ✅ Implemented | autoexecdata and autoexecProgbuf for burst access |
progbuf0-N (0x20+) | ✅ Implemented | Parameterized progbuf memory, multi-word execution with counter, redo support |
data0-N (0x04+) | ✅ Implemented | Memory-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) | ✅ Implemented | Read/write with auto-increment |
sbdata0 (0x3c) | ✅ Implemented | Read/write with bus triggers |
sbaddress1-3 / sbdata1-3 | ❌ Not implemented | 32-bit address/data only |
sbcs 8/16/64/128-bit access | ❌ Not implemented | Only sbaccess32 supported |
haltsum0 (0x40) | ✅ Implemented | Per-hart halted bits, up to 32 harts |
haltsum1-3 | ❌ Not implemented | Only haltsum0 exists |
authdata (0x30) | ❌ Not implemented | Always 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 | ✅ Implemented | Parameterized p.harts, per-hart buses, hartSel selection |
CSR Register
Phase 1C: Debug CSRs (dcsr, dpc)
| Spec Feature (Chapter) | Vexriscv Debug Implementation | Implementation Details |
|---|---|---|
| Ch 4: Core Debug (hart-side CSRs) | ||
| Halt/Resume | ✅ DebugHartBus + CsrPlugin | CsrPlugin.scala (line 702+): running flag, DebugHartBus wiring, halt/resume handshake |
| Single-step | ✅ Implemented | CsrPlugin.scala (lines 807-845): dcsr.step with full FSM (IDLE→SINGLE→WAIT), timeout/redo handling |
dcsr (0x7B0) | ✅ Implemented | CsrPlugin.scala (lines 792-851): prv, step, nmip, mprven, cause, stoptime, stopcount, stepie, ebreakm/s/u, xdebugver=4 |
dpc (0x7B1) | ✅ Implemented | CsrPlugin.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 | ✅ Implemented | CsrPlugin.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) | ✅ Implemented | CsrPlugin.scala (lines 1488-1498): jumps to dpc, restores privilege from dcsr.prv, via DebugHartBus.resume |
| Halt cause reporting | ✅ Implemented | dcsr.cause: 1 (ebreak), 2 (trigger), 3 (haltreq), 4 (step) |
dcsr.ebreakm/s/u | ✅ Implemented | CsrPlugin.scala (lines 1372-1377): per-privilege ebreak→debug detection |
dcsr.stoptime | ✅ Implemented | CsrPlugin.scala (line 866): stoptime output gated by debugMode |
dcsr.stopcount | ✅ Implemented | CsrPlugin.scala (line 1176): mcycle increment gated by !debugMode || !stopcount |
dcsr.stepie | ✅ Implemented | CsrPlugin.scala (line 1315): interrupts cleared when step && !stepie |
| Interrupt inhibition in debug | ✅ Implemented | CsrPlugin.scala (line 721): inhibateInterrupts() when debugMode |
| CSR access protection (0x7Bx) | ✅ Implemented | CsrPlugin.scala (line 1718): blocks non-debug access to 0x7B0-0x7BF |
DebugHartBus wiring | ✅ Implemented | CsrPlugin.scala (lines 704-789): instruction injection, data CSR, all hartToDm/dmToHart signals |
| Reset control | ✅ dmcontrol.ndmreset → io.ndmreset | Both implementations provide ndmreset |
Triggers
Phase 1D: Trigger Module
| Spec Feature (Chapter) | Vexriscv Debug Implementation | Implementation Details |
|---|---|---|
| Ch 5: Trigger Module (hart-side CSRs) | ||
tselect (0x7A0) | ✅ Implemented | CsrPlugin.scala (lines 870-875): WARL index, parameterized debugTriggers (default 2) |
tinfo (0x7A4) | ✅ Implemented | CsrPlugin.scala (line 878): reports type 2 (mcontrol) support |
tdata1 (0x7A1) | ⚠️ Partial | CsrPlugin.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) | ✅ Implemented | CsrPlugin.scala (lines 944-953): 32-bit compare value, PC equality match |
tdata3 (0x7A3) | ❌ Not implemented | — |
tcontrol (0x7A5) | ❌ Not implemented | No mte/mpte |
| Trigger type | ⚠️ mcontrol (type 2) only | Legacy type 2, not type 6 (mcontrol6). Spec recommends type 6 for new implementations |
| Match modes | ⚠️ Equal only | Only match=0 (equality). No napot, >=, <, mask modes |
| Match targets | ⚠️ Execute address only | Only execute bit implemented. No load/store data/address match |
| Privilege filtering | ✅ Implemented | m, s, u bits with privilegeHit logic (lines 930-934) |
dmode security | ✅ Implemented | dmode bit controls debug-only write access (line 925) |
action field | ⚠️ Partial | Register exists but only action=1 (enter debug) used in match logic |
| Trigger chaining | ❌ Not implemented | No chain bit |
dcsr.cause=2 on trigger | ✅ Implemented | CsrPlugin.scala (line 892): sets dcsr.cause := 2 on trigger hit |
| Trigger hit → debug entry | ✅ Implemented | CsrPlugin.scala (lines 881-897): decodeBreak halts pipeline, enters debug mode |
mcontrol6 (type 6) | ❌ Not implemented | Only legacy type 2 exists |
icount (type 3) | ❌ Not implemented | — |
itrigger/etrigger/tmexttrigger | ❌ Not implemented | — |
| Hardware breakpoints | ⚠️ Spec-compliant but limited | Type 2 mcontrol with execute address match=0 only, privilege filtering, dmode security |
mcontext/scontext | ❌ Not implemented | — |
| SoC Integration | ||
| DTM→DM→Hart wiring | ✅ Implemented | DebugModuleFiber.scala — multi-hart binding, clock-domain-safe pipelining |
| Tilelink SBA bridge | ✅ Implemented | makeSysbusTilelink() 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/SwdDpWriteflows so 2A is verifiable standalone against a stubbed DP - Implement 2-wire physical interface on the target:
swclkinput (driven by probe) +swdioasi/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.TURNROUNDunsupported) - 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
| Artifact | Path |
|---|---|
| RTL | EXT/SpinalHDL/lib/src/main/scala/spinal/lib/cpu/riscv/debug/DebugTransportModuleSwd.scala |
| Testbench | EXT/VexRiscv/src/test/scala/vexriscv/DebugSwdTest.scala |
| Run | cd 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.iand updatesswdio.o/swdio.oeon 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.
| Flow | Fired | Payload |
|---|---|---|
SwdDpCmd | one-cycle pulse on the packet-request park edge, iff parity/stop/park all pass | apNdp, rnw, addr = A[3:2] |
SwdDpRsp | must 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 |
SwdDpWrite | one-cycle pulse after the 33rd WDATA bit | data, 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 == 1samples while the target is not driving; at 50 it forcesRESET_WAITfrom any state (includingERROR), which falls toIDLEon the first low (idle) cycle. The counter is frozen/cleared whileoe=1so 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.4.2 abstract diagram — phase names aligned with ADI figures (includes
TRN_H2Tand optionalOVERRUN_DATA). - §1.4.3–§1.4.5 Phase 2A
SwdPhy— how that diagram maps onto the RTL enum and transitions inDebugTransportModuleSwd.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.2 | RTL EState | Role |
|---|---|---|
IDLE | IDLE | Wait for Start; between frames (oDrive := False) |
HEADER | HEADER | LSB-first shift-in after Start; validate at park |
TRN_H2T | (no named state) | Folded into first cycle of ACK |
ACK | ACK | Drive ACK[0..2] from ackNow / rspHold (cnt 0..2) |
READ_DATA | READ_DATA | RDATA[0:31] + even parity; keep oe=1 after ACK |
WR_TRN | WR_TRN | Post-ACK turnaround before write (oe=0, 2 cycles) |
WRITE_DATA | WRITE_DATA | Sample WDATA + parity → dp.wr |
RELEASE | RELEASE | Release after target-driven phase → IDLE |
ERROR | ERROR | Protocol error: do not drive; ignore headers |
OVERRUN_DATA | absent | ORUNDETECT out of scope (WAIT/FAULT always skip data) |
| (recovery) | RESET_WAIT | After line-reset hit; leave on first low → IDLE |
1.4.4 Transition mapping (diagram → RTL)
IDLE → HEADER — Start bit = 1
| Diagram | RTL |
|---|---|
| Start sampled 1 | when(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:
| Check | RTL | Spec |
|---|---|---|
| Even parity of APnDP, RnW, A2, A3 | (apNdp ^ rnw ^ a2 ^ a3) === hdr(4) | B4.1.6 / B4.2.5 |
| Stop = 0 | stopOk = !hdr(5) | B4.2.5 |
| Park = 1 | dio on that edge | B4.2.5 |
- Fail →
ERROR(silent until line reset). - Pass →
cmdValidpulse (SwdDpCmd:apNdp,rnw,addr) andstate := 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:
- owns the host→target turnaround cycle (from the host’s view), and
- drives the three ACK bits.
| Edge | Diagram name | RTL |
|---|---|---|
| Park | end of HEADER | HEADER → schedule ACK, fire cmd |
| Next rising edge | TRN_H2T / first ACK drive | First ACK cycle: oe=1, ACK[0] |
| Next two edges | rest of ACK | ACK[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 edge | RTL | Match? |
|---|---|---|
OK ∧ read → READ_DATA | ackNow === OK && cmdPayload.rnw → READ_DATA | Exact |
OK ∧ write → WR_TRN | ackNow === OK && !rnw → WR_TRN | Exact |
≠OK, no overrun → RELEASE | else → RELEASE | Exact |
≠OK ∧ ORUNDETECT → OVERRUN_DATA | never | By design (scope) |
ACK value comes from the 2B seam, not sticky logic inside 2A:
io.dp.rsp→ latched inrspHoldwhenvalidackNow = 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, thenIDLE(trailing Trn / release).
Write path — WR_TRN → WRITE_DATA → IDLE (Fig B4-1)
WR_TRN: two cycles withoe=0(cnt0 then 1) = second turnaround.WRITE_DATA: shift in 32 bits + sample parity; firedp.wrwithdataandparityOk.- WDATA parity fail →
parityOk = false(WDATAERR material for 2B), not protocol error. - Return direct
WRITE_DATA → IDLE(noRELEASE): host already owns the line. Diagram §1.4.2 also usesWRITE_DATA --> IDLE.
ERROR and line reset
| Diagram §1.4.2 | RTL |
|---|---|
ERROR --> ERROR (ignore traffic) | ERROR: oDrive := False only; no header parse |
ERROR --> IDLE on line reset | lineReset.hit → RESET_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.validtrue continuously with the programmed ack/rdata, and - records
cmd/wrpulses 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 injectflipParity/badStop/badPark; asserts the target never drives during the request.readAck()— collects ACK[0..2]; assertsoeon all three bits.transactRead(...)— request → trn → ACK; on OK collects 33 target bits assertingoethroughout, 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 withflipDataParity). 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
| # | Test | Verifies |
|---|---|---|
| 1 | write reaches stub bit-exact | Full 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). |
| 2 | read returns stub data bit-exact with parity | Full 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. |
| 3 | DPIDR smoke read | The §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. |
| 4 | header parity error silences target until line reset | Request-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). |
| 5 | stop bit error silences target until line reset | Same protocol-error contract triggered via stop≠0; recovery verified with a clean write. |
| 6 | WAIT and FAULT skip the data phase | ACK=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). |
| 7 | back-to-back transactions without idle cycles | A new request may start on the cycle right after the previous frame ends (write→read with zero idle); both transactions complete bit-exact. |
| 8 | write data parity error is flagged and recoverable | Corrupted 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. |
| 9 | 49 high cycles are not a line reset, 50 are | The 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
DPIDRregister (DP read, A[3:2]=0b00) — read-only device ID with manufacturer, version, min/revision fields - Implement
CTRL/STATregister (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
SELECTregister (DP write, A[3:2]=0b10) — DPBANKSEL (4 bits) + ADDR (AP address selection) - Implement
RDBUFFregister (DP read, A[3:2]=0b11) — read buffer for previous AP read result - Implement
ABORTregister (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.parityOkas 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
| Artifact | Path |
|---|---|
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 driver | EXT/VexRiscv/src/test/scala/vexriscv/SwdSimDriver.scala |
| Testbench | EXT/VexRiscv/src/test/scala/vexriscv/DebugSwdDpTest.scala |
| Run | cd 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] | Read | Write |
|---|---|---|
00 | DPIDR (parameter; default 0x0BA11AAB placeholder) | ABORT — DAPABORT, STKCMPCLR, STKERRCLR, WDERRCLR, ORUNERRCLR |
01 | CTRL/STAT when DPBANKSEL==0; other banks read as zero | CTRL/STAT when DPBANKSEL==0; other banks write-ignored |
10 | RESEND (= last posted result) | SELECT — APSEL[31:24], APBANKSEL[7:4], DPBANKSEL[3:0] |
11 | RDBUFF (= 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:
| Condition | ACK | Scope |
|---|---|---|
| any sticky flag set | FAULT | AP 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 outstanding | WAIT | AP accesses and RDBUFF reads — DP accesses (e.g. CTRL/STAT polling) still OK |
| otherwise | OK | the 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 onap.cmd. Completion loadsrdBuffer;RDBUFF/RESENDthen return it without launching anything. - AP write (ACK=OK): launched only at the WDATA commit (
SwdDpWritewithparityOk), 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 construct | Notes |
|---|---|---|
| sticky flags | stickyOrun/stickyCmp/stickyErr/wdataErr regs | only stickyErr/wdataErr hardware-set (scope) |
| "AP outstanding" | apBusy reg | set at launch, cleared at completion / DAPABORT |
| read buffer | rdBuffer reg | serves AP-read RDATA, RDBUFF and RESEND |
| ACK decision | combinational mux off the cmd pulse | FAULT > WAIT > OK; lands within the turnaround |
| "access not performed" | launches/commits gated on ack == OK | apRdFire, last.pendingWrite |
| write accepted, data later | last.pendingWrite/isApReg/addrReg regs | the request↔commit link across the WDATA phase |
| AP write launch | apWrFire at wr.valid | carries wr.data; not at the request |
| completion → buffer | apWasRead gate | write completions can't clobber rdBuffer |
| DAPABORT | apDiscard flag | RTL-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
| Aspect | Match? |
|---|---|
| ACK precedence FAULT > WAIT > OK | Yes |
| Sticky FAULT scoped to AP + RDBUFF; other DP accesses keep working | Yes (ADIv5.2 §B4.2.4) |
| Posted AP read: previous result now, new result to buffer | Yes — spec leaves first-read data implementation-defined; RTL returns rdBuffer |
| Write ACKed at request, committed after WDATA parity | Yes (last.pendingWrite) |
| WDATAERR on data-phase parity fail, commit dropped, no line reset needed | Yes |
| RESEND | Alias of rdBuffer — same value as RDBUFF, no relaunch |
| ORUNDETECT / STICKYORUN / STICKYCMP hardware behavior | No — stored/clearable only (deliberate scope) |
| Power-up REQ→ACK handshake | Mirrored, no power controller modeled |
apDiscard (DAPABORT in-flight drop), apWasRead gate | RTL-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.cmdfire into a queue ((rnw, addr, wdata)); apComplete(data, error)delivers a completion onap.rspfor 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
| # | Test | Verifies |
|---|---|---|
| 1 | DPIDR read returns the configured value | The step-1b headline: a real DPIDR from the register file (no stub), bit-exact against the elaboration parameter. |
| 2 | CTRL/STAT power-up requests mirror ACKs | CTRL/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. |
| 3 | SELECT DPBANKSEL banks CTRL/STAT | With DPBANKSEL=1 the CTRL/STAT address reads as zero (unimplemented bank, still ACK=OK); switching back to bank 0 restores the real register. |
| 4 | AP read is posted; RDBUFF returns the completed result | ADI 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. |
| 5 | WAIT while an AP access is outstanding; DP accesses stay OK | WAIT 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. |
| 6 | AP completion error sets STICKYERR; FAULT until ABORT clears | The 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. |
| 7 | Write data parity error sets WDATAERR and drops the commit | A 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. |
| 8 | AP write commits after the data phase and holds busy | The 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. |
| 9 | RESEND returns the same value as RDBUFF | The 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(sameDebugCmdinterface used by JTAG DTM) - Implement AP address decoding — translate
SELECT.ADDR+A[3:2]to DMI address forDebugBus.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.rspto RDATA output and ACK generation - Handle cross-clock-domain between SWD clock (SWCLK) and debug clock domain (reuse
ccTogglepattern from JTAG DTM) - Implement WAIT response when
DebugBus.cmdis not ready (DM busy) - Implement FAULT response when
DebugBus.rsp.erroris 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
| Artifact | Path |
|---|---|
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 |
| Run | cd 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] | Register | Access | Behavior |
|---|---|---|---|
00 | AP_IDR | RO | identification constant (default 0x74726976 "triv") — completes locally in 1 SWCLK cycle |
01 | DMI_ADDR | RW | latches the 7-bit DMI word address; readable back — local |
10 | DMI_DATA | RW | performs the DebugBus transaction at DMI_ADDR (RnW from the SWD packet) — crosses domains |
11 | POSTED_READ | RO | last 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 (
dmiPendinghere,apBusyin 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 realDebugModule(version 2, 1 hart,progBufSize=2,datacount=1) with a stubbedDebugHartBus— running, never halted,hartToDm/resume.rspidle. - Two genuinely asynchronous clocks: the shared
SwdHostDriverbit-bangs SWCLK (bench-owned, via aClockDomainhandle over the pin) whiledut.clockDomainfree-runs viaforkStimulus— DMI completion latency is variable by construction. - Host-style retry helpers:
apWriteRetry/apReadRetry/rdbuffretry on WAIT with bounded attempts — the same loop a real OpenOCD target would run.dmiRead/dmiWritecompose 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). Thesim(name, seed = …)hook pins a failing seed for deterministic reproduction.
3. What each test verifies
| # | Test | Verifies |
|---|---|---|
| 1 | DPIDR reads through the assembled transport | The full stack elaborates and runs: BOOT-domain phy, async clocks, fiber-style wiring — and the plain DP path still works. |
| 2 | AP IDR via posted read | Gateway local completion path + 2B posted-read plumbing end-to-end (AP_IDR lands in RDBUFF). |
| 3 | DMI_ADDR write and readback | The address latch: AP write commits (WDATA path through the gateway), AP read returns it. |
| 4 | dmstatus read over SWD — step 2 exit criterion | The headline: DMI_ADDR←0x11, DMI_DATA read crosses to the DM and back; RDBUFF poll returns dmstatus with version=2 and authenticated=1. |
| 5 | dmcontrol dmactive write and readback | Full DMI write path into a real DM register (resets 0, reads back 1) — write launch at commit, crossed completion, subsequent read. |
| 6 | abstractauto write and readback | A 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.) |
| 7 | POSTED_READ returns the last DMI read result | The 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).
| Step | Content | Status |
|---|---|---|
| 1 | Cluster / core.py / swdremote / litex_sim / Makefile + OpenOCD smoke | ✅ Completed |
| 2 | Custom 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
DebugBuswithout arbitration. Do not setJTAG_SIM=1andSWD_SIM=1together.
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
DebugTransportModuleSwdwithio.swclk(input, probe-driven) +io.swdioasi/o/oe— Phase 2A–2C - Add
withSwdTransport()toDebugModuleFiber(mutually exclusive withwithJtagTap()per build) — Phase 2C - LiteX SMP cluster:
swdparam,--swdCLI, three-wiredebugPort_swclk/swdio_{i,o,oe},noTapguarded!jtagTap && !swd— Phase 3 - LiteX sim:
--with-swd-debug,_Swdnetlist token,add_swd(),swdremoteon TCP 44854,SWD_SIM=1Makefile targets — Phase 3 - OpenOCD smoke on sim: DPIDR
0x0ba11aab+dap apreg→dmstatus0x004c0c82 - OpenOCD cfgs:
openocd_swd_remote.cfg+vexriscv_swd.cfg(DAP +vexriscv_dmi_read/write+ smoke) - Cluster asserts: reject
swd && jtagTap; requireprivilegedDebugwhenswd
Step 2 — host tooling + GDB (open)
- [~] Custom OpenOCD target: DAP/DMI procs done;
riscvtarget 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_IDRbefore 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/ DESIGNER0x23B)
- Rules + placeholders owned by Phase 2C (
- 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_simOpenOCDriscv64-unknown-elf-gdb
Configs
| File | Role |
|---|---|
openocd_jtag_remote.cfg | remote_bitbang → localhost:44853 |
riscv_jtag_tunneled.tcl | TAP 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
| Flag | Role |
|---|---|
--integrated-main-ram-size=0x10000 | 64 KiB main RAM for sim (demo load region) |
--cpu-type=vexriscv_smp / --cpu-variant=linux / --cpu-count=1 | SMP Linux-capable cluster, 1 hart |
--with-privileged-debug | Official DebugModule + DTM (_Pd netlist token) |
--jtag-tap | Full JTAG TAP on cluster (_JtagT); needed so sim has TCK/TMS/TDI/TDO pads |
--with-jtagremote | LiteX sim module jtagremote — OpenOCD remote_bitbang on TCP 44853 |
--non-interactive | Keep 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
| File | Role |
|---|---|
openocd_swd_remote.cfg | remote_bitbang → localhost:44854, transport select swd |
vexriscv_swd.cfg | SW-DP + DAP + vexriscv_dmi_read/write + vexriscv_swd_smoke — no 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
| Flag | Role |
|---|---|
--integrated-main-ram-size=0x10000 | 64 KiB main RAM for sim |
--cpu-type=vexriscv_smp / --cpu-variant=linux / --cpu-count=1 | SMP Linux-capable cluster, 1 hart |
--with-privileged-debug | Official DebugModule (required; SWD is official-stack only) |
--with-swd-debug | Cluster SWD transport + _Swd netlist token; forces privileged debug on LiteX side |
--with-swdremote | LiteX sim module swdremote — OpenOCD remote_bitbang SWD on TCP 44854 |
--non-interactive | Keep 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:
| Terminal | Intended SWD flow (not done) |
|---|---|
| 1 | litex_sim … --with-privileged-debug --with-swd-debug --with-swdremote --non-interactive |
| 2 | OpenOCD + SWD + riscv examine → listen :3333 |
| 3 | riscv64-unknown-elf-gdb → target extended-remote localhost:3333 → halt / regs / load |
Host attach workflows (sim only)
Side-by-side
| Terminal | JTAG — full three-terminal ✅ | SWD — partial 🟡 |
|---|---|---|
| 1 — sim | litex_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 — OpenOCD | Stock riscv target + BSCAN tunnel; examines hart; GDB server :3333 | transport select swd + DAP; DPIDR / dmstatus via dap apreg; no riscv target yet |
| 3 — GDB | target extended-remote localhost:3333 → halt / regs / load | ❌ Not available until step 2 (riscv glue + GDB) |
| Capability | JTAG now | SWD 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 binary | ✅ | remote_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
dmactiveactivation/deactivation sequence - Test abstract command error handling (all 7
cmderrcodes) - Test EBREAK behavior with
dcsr.ebreakm/ebreaks/ebreakucombinations - Test single-step across privilege mode transitions
- Test trigger module: each trigger type, chaining,
dmodesecurity - 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, andDMI_ADDRregister map - Document which optional RISC-V debug spec features are implemented