VexRiscv Debug Support
This is implementation of Standard Debug specification in Vexriscv:
- Phase 1A JTAG DTM + DMI Bus
- Phase 1B Core DM (dmcontrol/dmstatus)
- Phase 1C Abstract Register accsess
- Phase 1D Debug CSRs
- Phase 1E Memory access
- Phase 1F Triggers
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
IDCODEregister with manufacturer/part/version fields - Implement
dtmcsregister —version=1,abits,idle,dmistat,dmireset,dmihardreset - Implement
dmishift 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
dmihardresetanddmiresetfor 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 Command | Works? | Reason |
|---|---|---|
| JTAG chain detection | ✅ | IDCODE readable |
target remote :3333 | ❌ | No 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
dmactivestate machine — activation/deactivation with reset behavior - Wire DM hart interface to VexRiscv pipeline (reuse existing
haltIt/resetItsignals) - Implement mutual exclusion on
dmcontrolwrites (only one ofresumereq/hartreset/ackhavereset/setresethaltreq/clrresethaltreqmay 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 Command | Works? | Reason |
|---|---|---|
target remote :3333 | ⚠️ Connects with warnings | OpenOCD finds DM but can't read registers |
continue | ✅ | dmcontrol.resumereq |
Ctrl+C (halt) | ✅ | dmcontrol.haltreq |
monitor reset halt | ✅ | dmcontrol.ndmreset + haltreq |
info registers | ❌ | Needs abstract register access |
stepi | ❌ | Needs 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>=1for RV32,>=2for RV64) - Implement
command(0x17) — Access Register command (cmdtype=0) - Implement abstract command state machine:
idle→busy→complete/error - Implement
cmderrerror 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 Command | Works? | Reason |
|---|---|---|
target remote :3333 | ⚠️ Connects, dcsr/dpc warnings | dcsr CSR not yet on hart |
continue / Ctrl+C | ✅ | halt/resume |
info registers | ✅ | Abstract register access reads x0-x31 + CSRs |
set $a0 = 42 | ✅ | Abstract register write |
stepi / next / step | ❌ | Needs dcsr.step |
break / load / x/... | ❌ | Needs memory access |
backtrace | ❌ | Needs 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 todpc, save privilege todcsr.prv - Implement debug mode exit via
dretinstruction — restore PC fromdpc, privilege fromdcsr.prv - Implement
dcsr.step— single-step (migrate existingstepItlogic to use this CSR) - Implement
dcsr.ebreakm/ebreaks/ebreaku— per-privilege ebreak behavior (replacedisableEbreak) - Migrate
godmodebehavior 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 Command | Works? | Reason |
|---|---|---|
target remote :3333 | ✅ Clean connect | Full DM + dcsr/dpc |
continue / Ctrl+C | ✅ | halt/resume |
info registers | ✅ | Abstract register access + dcsr/dpc |
stepi | ✅ | dcsr.step=1, resumereq, poll halted, dcsr.cause=4 |
set $pc = addr | ✅ | Write dpc via abstract command |
next / step (source-level) | ❌ | Needs memory read for source line mapping |
break / load / x/... / bt | ❌ | Needs 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 forebreak) - Optionally implement
progbuf1(0x21) — allows the access instruction + explicitebreak - Report
progbufsizeinabstractcs - Implement Access Register command with
postexec=1— execute program buffer after register transfer - Implement
impebreakindmstatus— 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 Command | Works? | Reason |
|---|---|---|
target remote :3333 | ✅ | Full connect |
load hello.elf | ✅ | Memory write via progbuf |
break main | ✅ Software BP | Writes ebreak (0x00100073) to memory at breakpoint address |
continue | ✅ | Resume, halts when ebreak hit, dcsr.cause=1 (ebreak) |
stepi / step / next | ✅ | dcsr.step + memory read for source line mapping |
info registers | ✅ | Abstract register access |
x/4x 0x80000000 | ✅ | Memory read via progbuf |
x/s &message | ✅ | Reads "Hello, World!\n" from memory |
print variable | ✅ | Register + memory read |
backtrace | ✅ | Stack memory read (walks frame pointers) |
set *0x80001000 = 0x41 | ✅ | Memory write via progbuf |
hbreak (hardware BP) | ❌ | Needs trigger module |
watch variable | ❌ | Needs 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,datawith 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
mcontrol6withstore=1/load=1+select=1(data) for watchpoints - Implement
dmodebit security — only Debug Mode can write triggers withdmode=1 - Implement
dcsr.cause=2(trigger) on trigger match - Migrate existing
hardwareBreakpointsPC-match logic to use trigger CSRs
GDB/OpenOCD at this phase — everything from Phase 1E, plus:
| GDB Command | Phase 1E | Phase 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 |