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

VexRiscv Debug Support

This is implementation of Standard Debug specification in Vexriscv:

Debug Transport Module

Phase 1A: DMI Bus + JTAG DTM (Foundation Layer)

Dependency: None — this is the transport, can be tested standalone Deliverable: Standard JTAG TAP that can read/write DMI addresses


Tasks:

  • Implement JTAG TAP state machine with standard IR codes (BYPASS 0x1f, IDCODE 0x01, dtmcs, dmi)
  • Implement IDCODE register with manufacturer/part/version fields
  • Implement dtmcs register — version=1, abits, idle, dmistat, dmireset, dmihardreset
  • Implement dmi shift register — op (2-bit), address (abits-wide), data (32-bit)
  • Implement DMI request/response handshake with busy detection (dmistat=3)
  • Create DMI bus interface (SpinalHDL Bundle) — internal bus between DTM and DM
  • Implement dmihardreset and dmireset for error recovery

GDB/OpenOCD at this phase:

OpenOCD: JTAG scan detects IDCODE ✅
OpenOCD: Reads dtmcs, gets abits/version ✅
OpenOCD: Writes dmcontrol.dmactive=1 via DMI... but no DM exists ❌
OpenOCD: "Error: no debug module found" ❌
GDB: Cannot connect
GDB CommandWorks?Reason
JTAG chain detectionIDCODE readable
target remote :3333No DM to respond to DMI reads
Everything else

Practical use: Validates JTAG connectivity and DMI bus. OpenOCD can scan the chain and see the IDCODE. Raw DMI read/write can be tested via OpenOCD scripts.


Debug Module

Phase 1B: Core DM Registers (Minimal Control Plane)

Dependency: Phase 1A (DMI bus exists) Deliverable: Can halt/resume a hart through standard dmcontrol/dmstatus


Tasks:

  • Create DebugModule.scala — DMI address decoder, register file
  • Implement dmcontrol (0x10) — dmactive, haltreq, resumereq, ndmreset, hartsello (single hart = hart 0)
  • Implement dmstatus (0x11) — version=3 (1.0), authenticated=1 (no auth), anyhalted/allhalted/anyrunning/allrunning
  • Implement dmactive state machine — activation/deactivation with reset behavior
  • Wire DM hart interface to VexRiscv pipeline (reuse existing haltIt/resetIt signals)
  • Implement mutual exclusion on dmcontrol writes (only one of resumereq/hartreset/ackhavereset/setresethaltreq/clrresethaltreq may be 1)

GDB/OpenOCD at this phase:

OpenOCD: Connects, reads dmstatus.version=3 ✅
OpenOCD: Writes haltreq=1, hart halts ✅
OpenOCD: Polls dmstatus.allhalted=1 ✅
OpenOCD: Reads abstractcs → not implemented, gets 0 ❌
OpenOCD: Tries abstract register read of dcsr → fails ❌
OpenOCD: "Error: failed to read dcsr"
GDB: Connects with warnings, can't read registers
GDB CommandWorks?Reason
target remote :3333⚠️ Connects with warningsOpenOCD finds DM but can't read registers
continuedmcontrol.resumereq
Ctrl+C (halt)dmcontrol.haltreq
monitor reset haltdmcontrol.ndmreset + haltreq
info registersNeeds abstract register access
stepiNeeds dcsr.step
break / load / x/...Needs memory access

Practical use: CPU can be halted and resumed. CPU can be also reset it. Verify the hello world program is running (UART output appears when resumed, stops when halted). Cannot inspect or modify anything.


Register

Phase 1C: Abstract Register Access

Dependency: Phase 1B (DM registers, halt/resume working) Deliverable: Can read/write GPRs and CSRs through abstract commands


Tasks:

  • Implement abstractcs (0x16) — datacount (min 1 for RV32), progbufsize, busy, cmderr
  • Implement data0 (0x04) — at minimum 1 data register (datacount>=1 for RV32, >=2 for RV64)
  • Implement command (0x17) — Access Register command (cmdtype=0)
  • Implement abstract command state machine: idlebusycomplete/error
  • Implement cmderr error codes: busy(1), not supported(2), exception(3), halt/resume(4)
  • Implement register number decoding: GPRs (0x1000-0x101f), CSRs (0x0000-0x0fff)
  • Wire abstract register access to hart — either via instruction injection (reuse existing injection port) or direct register file access

GDB/OpenOCD at this phase:

OpenOCD: Reads abstractcs, gets datacount=1 ✅
OpenOCD: Abstract command reads x1-x31 ✅
OpenOCD: Abstract command reads mstatus, misa ✅
OpenOCD: Abstract command reads dcsr → CSR doesn't exist yet, cmderr=2 ⚠️
OpenOCD: Identifies hart as rv32i ✅
GDB: Connects, reads registers, but dcsr/dpc warnings
GDB CommandWorks?Reason
target remote :3333⚠️ Connects, dcsr/dpc warningsdcsr CSR not yet on hart
continue / Ctrl+Chalt/resume
info registersAbstract register access reads x0-x31 + CSRs
set $a0 = 42Abstract register write
stepi / next / stepNeeds dcsr.step
break / load / x/...Needs memory access
backtraceNeeds stack memory read

Practical use: Halt the running hello world and inspect all CPU registers. See what function it's in (from PC), stack pointer value, arguments in a0-a7. Can modify registers. Cannot see memory, set breakpoints, or single-step.


CSR Register

Phase 1D: Debug CSRs (dcsr, dpc)

Dependency: Phase 1C (can read/write CSRs via abstract commands) Deliverable: Proper halt cause reporting, single-step via dcsr.step, PC via dpc

Tasks:

  • Implement dcsr (0x7B0) as actual hart CSR — xdebugver=4, cause (5 values), step, ebreakm/ebreaks/ebreaku, prv, stepie, stopcount, stoptime, mprven, nmip
  • Implement dpc (0x7B1) as actual hart CSR — captures PC on debug entry
  • Implement dscratch0 (0x7B2) — debug scratch register for debugger use
  • Implement debug mode entry: set dcsr.cause, save PC to dpc, save privilege to dcsr.prv
  • Implement debug mode exit via dret instruction — restore PC from dpc, privilege from dcsr.prv
  • Implement dcsr.step — single-step (migrate existing stepIt logic to use this CSR)
  • Implement dcsr.ebreakm/ebreaks/ebreaku — per-privilege ebreak behavior (replace disableEbreak)
  • Migrate godmode behavior to proper debug mode privilege rules (Sec 4.1)

GDB/OpenOCD at this phase:

OpenOCD: Reads dcsr ✅ xdebugver=4, cause=3 (haltreq)
OpenOCD: Reads dpc ✅ exact halt PC
OpenOCD: "Examined RISC-V core; found 1 hart, rv32i" ✅ CLEAN CONNECT
GDB: Full register display with proper PC, single-step works
GDB CommandWorks?Reason
target remote :3333✅ Clean connectFull DM + dcsr/dpc
continue / Ctrl+Chalt/resume
info registersAbstract register access + dcsr/dpc
stepidcsr.step=1, resumereq, poll halted, dcsr.cause=4
set $pc = addrWrite dpc via abstract command
next / step (source-level)Needs memory read for source line mapping
break / load / x/... / btNeeds memory access

Practical use: Halt hello world, see exactly where it stopped (PC with source line if ELF has debug info), read all registers, and single-step instruction by instruction. Watch the program counter advance through the code. Cannot see memory, set breakpoints, or do source-level stepping.

Memory

Phase 1E: Memory Access (Program Buffer)

Dependency: Phase 1C (abstract command framework working) Deliverable: Debugger can read/write target memory. This is the "fully functional hello world debugger" milestone.

Approach: Program Buffer is recommended for VexRiscv since the instruction injection port already exists and can be reused. OpenOCD's RISC-V driver knows how to synthesize memory read/write sequences using lw/sw instructions in the program buffer.


Tasks:

  • Implement progbuf0 (0x20) — minimum 1 word (2 is better: one for the access instruction, one for ebreak)
  • Optionally implement progbuf1 (0x21) — allows the access instruction + explicit ebreak
  • Report progbufsize in abstractcs
  • Implement Access Register command with postexec=1 — execute program buffer after register transfer
  • Implement impebreak in dmstatus — implicit ebreak after last progbuf word (saves a progbuf slot)
  • Wire program buffer execution to existing instruction injection port
  • Handle cmderr=3 (exception) if progbuf instruction causes a fault

How OpenOCD uses progbuf for memory access:

Memory Read (e.g., read 32-bit word at address A):
  1. Write "lw s0, 0(s0)" into progbuf0
  2. Write address A into data0
  3. Execute: Access Register command (regno=s0, write=1, transfer=1, postexec=1)
     → DM writes A into s0, then executes progbuf → "lw s0, 0(s0)" loads mem[A] into s0
  4. Read: Access Register command (regno=s0, write=0, transfer=1)
     → DM reads s0 into data0
  5. Read data0 → contains mem[A]

Memory Write (e.g., write value V to address A):
  1. Write "sw s1, 0(s0)" into progbuf0
  2. Write address A to s0, value V to s1 (via abstract register write)
  3. Execute postexec → stores V to mem[A]


GDB/OpenOCD at this phase:

OpenOCD: Sees progbufsize >= 1 in abstractcs ✅
OpenOCD: Memory read/write via progbuf lw/sw sequences ✅
GDB: Full debugging capability for hello world
GDB CommandWorks?Reason
target remote :3333Full connect
load hello.elfMemory write via progbuf
break main✅ Software BPWrites ebreak (0x00100073) to memory at breakpoint address
continueResume, halts when ebreak hit, dcsr.cause=1 (ebreak)
stepi / step / nextdcsr.step + memory read for source line mapping
info registersAbstract register access
x/4x 0x80000000Memory read via progbuf
x/s &messageReads "Hello, World!\n" from memory
print variableRegister + memory read
backtraceStack memory read (walks frame pointers)
set *0x80001000 = 0x41Memory write via progbuf
hbreak (hardware BP)Needs trigger module
watch variableNeeds trigger module data watchpoints

Typical hello world debug session at this phase:

$ riscv32-unknown-elf-gdb hello.elf
(gdb) target remote :3333
Remote debugging using :3333
0x80000080 in _start ()

(gdb) load
Loading section .text, size 0x1234 lma 0x80000000
Loading section .data, size 0x100 lma 0x80002000
Start address 0x80000000, load size 4916

(gdb) break main
Breakpoint 1 at 0x80000124: file hello.c, line 3.

(gdb) continue
Breakpoint 1, main () at hello.c:3
3    const char *message = "Hello, World!\n";

(gdb) next
4    uart_puts(message);

(gdb) print message
$1 = 0x80001000 "Hello, World!\n"

(gdb) info registers
ra  0x800000e4   sp  0x80010000   gp  0x80002800
a0  0x80001000   a1  0x00000000   ...

(gdb) backtrace
#0  main () at hello.c:4
#1  0x800000a0 in _start ()

(gdb) x/4i $pc
0x80000128: lui   a0,0x80001
0x8000012c: addi  a0,a0,0
0x80000130: jal   ra,0x80000200 <uart_puts>
0x80000134: j     0x80000134

(gdb) continue
Continuing.    ← "Hello, World!" appears on UART


Trigger

Dependency: Phase 1D (dcsr exists for cause=2 trigger reporting) Deliverable: Hardware breakpoints and data watchpoints through standard trigger CSRs


Tasks:

  • Implement tselect (0x7A0, Sec 5.7.1) — WARL trigger index selection (support N triggers, parameterized)
  • Implement tdata1 (0x7A1, Sec 5.7.2) — type, dmode, data with type multiplexing
  • Implement tdata2 (0x7A2, Sec 5.7.3) — compare value (address or data)
  • Implement tinfo (0x7A4, Sec 5.7.5) — version=1, info (supported type bitmask)
  • Implement mcontrol6 (type 6, Sec 5.7.12) — start with: execute=1, match=0 (equal), action=1 (enter debug mode), select=0 (address)
  • Extend mcontrol6 with store=1/load=1 + select=1 (data) for watchpoints
  • Implement dmode bit security — only Debug Mode can write triggers with dmode=1
  • Implement dcsr.cause=2 (trigger) on trigger match
  • Migrate existing hardwareBreakpoints PC-match logic to use trigger CSRs

GDB/OpenOCD at this phase — everything from Phase 1E, plus:

GDB CommandPhase 1EPhase 1F
hbreak *0x80000100✅ Hardware breakpoint, no memory modification
hbreak main✅ Works on ROM/flash code
watch counter✅ Data watchpoint (fires on write to &counter)
rwatch buffer✅ Read watchpoint (fires on read from &buffer)
break on flash/ROM✅ Hardware BP doesn't modify memory