Debugging
Author: Johannes Hofmann 2025
For developing larger and more complex software, a debugger is an essential tool. It allows developers to analyze and control program execution with features such as:
Starting and stopping execution
Setting breakpoints
Reading and writing memory
Reading and writing registers
One widely used debugger is the GNU Debugger (GDB). When debugging embedded
hardware, OpenOCD is required to facilitate communication between the hardware
and GDB.
The debug hardware follows the RISC-V External Debug Support specification, ensuring compatibility with GDB for seamless debugging.
In figure Overview of the debug hardware you can see the debug infrastructure architecture.
Fig. 47 Overview of the debug hardware
Soft Debugger
Remote BitBang
- group c_remote_bitbang
This file contains the definition of the c_remote_bitbang class. For simulation ONLY.
This module implements remote bitbang with a socket server that allows a client to connect and send or recieve data following the remote bitbang specification:
- Author
Johannes Hofmann
Command
Event
B
LED on (Not supported)
b
LED off (Not supported)
r
JTAG reset
0
tck = 0, tms = 0, tdi = 0
1
tck = 0, tms = 0, tdi = 1
2
tck = 0, tms = 1, tdi = 0
3
tck = 0, tms = 1, tdi = 1
4
tck = 1, tms = 0, tdi = 0
5
tck = 1, tms = 0, tdi = 1
6
tck = 1, tms = 1, tdi = 0
7
tck = 1, tms = 1, tdi = 1
R
Read tdo
Q
Quit (Close socket connection)
Commands that are not listed are ignored.
The JTAG interface is connected through callback functions that respond to specific client commands:
void callback_jtag_set_inputs(bool tck, bool tms, bool tdi): Invoked when a new JTAG input pin command is received.void callback_jtag_get_output(): Invoked when the current state whentdois requested.void callback_jtag_trigger_reset(): Invoked when a JTAG reset command is received.
Note: The module is not synthesizable and is only used for simulation purposes.
Functions
-
c_remote_bitbang::c_remote_bitbang(std::function<void(bool, bool, bool)> callback_jtag_set_inputs, std::function<bool(void)> callback_jtag_get_output, std::function<void(void)> callback_jtag_trigger_reset, uint16_t port = PN_CFG_DEBUG_OPENOCD_PORT)
Constructor.
- Parameters:
callback_jtag_set_inputs – Callback function called when new JTAG write input command recieved.
callback_jtag_get_output – Callback function called when new JTAG read output command recieved.
callback_jtag_trigger_reset – Callback function called when JTAG trigger reset command recieved.
port – Port to which will be listen (openocd have to connect to it).
-
c_remote_bitbang::~c_remote_bitbang()
Destructor.
-
void c_remote_bitbang::process()
Performs all update functions. Note: Should be called inside a while loop or similar.
-
bool c_remote_bitbang::connected()
Checks if another socket is connected.
- Returns:
True if another socket has connected else false.
Debug Module Interface (DMI)
The DMI is the interface between the Debug Transport Module (DTM) and the
Debug Module (DM). For the software debugger its three functions that the DM (DMI slave)
has to implement which are called by the DTM (DMI master):
void dmi_write(uint8_t adr, uint32_t data): Called when a register write operation is performed.uint32_t dmi_read(uint8_t data): Called when a register read operation is performed.void dmi_reset(): Called when a reset operation is performed.
Debug Transport Module (DTM)
Fig. 48 Overview TAP-Machine.
- group c_dtm
This file contains the definition of the c_dtm class. For simulation ONLY.
The
Debug Transport Module (DTM)provide access to theDebug Module (DM)over JTAG. In the “External Debug Support” standard, the specification for aDTMwith a JTAG interface is defined. This is based on the definition of aTAPin the JTAG standard, which enables access to custom-defined JTAG registers. There are four registers implemented to accomplish this.- Author
Johannes Hofmann
Name
Address
Description
BYPASS
0x00
This register has 1-bit length and has no effect on. Used for bypass data. (JTAG standard)
IDCODE
0x01
This register is read-only. It holds the value
0xdeadbeef. Used for identification purposes. (JTAG standard)DTMCS
0x10
This register is used to control the
DMI-busand to reset theDMI-bus.DMI
0x11
This register is used to access the
DMvia theDMI-bus.The
DMI-busis used to read or write to registers inside theDM.To set the state of the JTAG input pin (tck, tms, tdi) call the functon
jtag_set_input_pins(jtag_input_pins_t). The get the state of the JTAG output pin tdo call the functionjtag_get_output_pin(). To set the JTAG reset pin call the functionjtag_reset(). The typejtag_input_pins_tis a struct holding the tck, tms, tdi signals as booleans.The
DTMis the master in theDMI-busand drives all the operations happening. The callback functions for write, read or reset theDMI-busmust be provided when ac_dtminstance is created to the constructor.Note: The module is not synthesizable and is only used for simulation purposes.
Functions
-
c_dtm::c_dtm(std::function<void(uint8_t, uint32_t)> callback_dmi_write, std::function<uint32_t(uint8_t)> callback_dmi_read, std::function<void(void)> callback_dmi_reset)
Constructor.
- Parameters:
callback_dmi_write – Callback function called when a write is performed on dmi bus.
callback_dmi_read – Callback function called when a read is performed on dmi bus.
callback_dmi_reset – Callback function called when a reset is performed on dmi bus.
-
c_dtm::~c_dtm()
Destructor.
-
void c_dtm::jtag_set_input_pins(jtag_input_pins_t jtag_input_pins)
Set JTAG input pins.
- Parameters:
jtag_input_pins – JTAG input pins states.
-
bool c_dtm::jtag_get_output_pin() const
Get output JTAG tdo.
- Returns:
State of JTAG tdo pin.
-
size_t c_dtm::get_instruction_reg_width() const
Get the width of the instruction register.
- Returns:
Width of the instruction register.
-
size_t c_dtm::get_data_reg_width(e_data_reg_address data_reg) const
Get the width of the given data register.
- Parameters:
data_reg – Given data register.
- Returns:
Width of the given data register.
Debug Module (DM)
- group c_soft_dm
This file contains the definition of the c_soft_dm class. For simulation ONLY.
The module translates the instructions it receives from the
DMI-businto commands. It must be able to handle the following requests:Provide the debugger with information about the hardware implementation.
Halt and start hart.
Indicate if the hart is currently halted.
Read and write “General Purpose Registers” (GPR).
Enable debugging from the very first assembler instruction.
- Author
Johannes Hofmann
The
DMhas registers accessed by theDTMcalledDebug registersand normal or system registers. Thec_soft_dmmodule derives fromc_soft_peripheralso the system registers can be accessed by the processor. The registers exposed to the system are:Name
Address
Description
DATA0
0x00
For data exchange between the host and the processor. Accessed by both the hart and the host.
DATA1
0x04
For data exchange between the host and the processor. Accessed by both the hart and the host.
PROGRAM_BUFFER0
0x08
Holds an assembler command coming from the host. Accessed by both the hart and the host.
PROGRAM_BUFFER1
0x0c
Holds an assembler command coming from the host. Accessed by both the hart and the host.
PROGRAM_BUFFER2
0x10
Holds an assembler command coming from the host. Accessed by both the hart and the host.
ABSTRACT_COMMAND0
0x14
Holds a command coming from the host and is translated into an assembler command.
ABSTRACT_COMMAND1
0x18
Holds a command coming from the host and is translated into an assembler command.
ABSTRACT_COMMAND2
0x1c
Holds a command coming from the host and is translated into an assembler command.
ABSTRACT_COMMAND3
0x20
Holds a command coming from the host and is translated into an assembler command.
ABSTRACT_COMMAND4
0x24
Holds a command coming from the host and is translated into an assembler command.
ABSTRACT_COMMAND5
0x28
Holds a command coming from the host and is translated into an assembler command.
ABSTRACT_COMMAND6
0x2c
Holds a command coming from the host and is translated into an assembler command.
ABSTRACT_COMMAND7
0x30
Holds a command coming from the host and is translated into an assembler command.
HARTCONTROL
0x34
Control register that tells the processor if commands should be executed or if the debug handler should be exited.
HARTSTATUS
0x38
Status register where the processor tells the
DMif he isrunning,halted, executingcommands.DEBUG_HANDLER_START
0x3c
Start of the debug handler program.
The registers accessed by the
DTMare implemented and described in thec_debug_regsmodule which is part of this module.Halting a hart can be done in two ways.
The hart reads an
EBREAKinstructionhaltrequstfrom the host. If a hart is halted it starts executing thedebug handlerprogram. Thehaltrequstfrom the host is an actual signal going into the hart to halt it.
If the host has a request to run some custom commands it will either write the assembler comamnds directly into the
PRORGAM_BUFFERxregisters or send andabstract commands. Theabstract commandholds the instruction in a more abstract kind. It has the be processed to one or multiple assembler commands. They are then saved in theABSTRACT_COMMANDxregisters. After the commands are updated in the registers there is therun_commandsreqbit set in theHARTCONTROLregister which tells thedabug handlerto executed the new commands.To resume the execution of the main program there is the
resumereqbit in theHARTCONTROLregister that tells thedebug handlerto resume the main program.The
HARTSTATUSregister is used as a feedback so the host can be sure that the hart is in the requested state.The base address of the
c_soft_dmmodule is fixed set tozero(0x00000000) following to theExternal Debug Supportstandard.The module has one output signals,
signal_debug_haltrequest. There is one callback function as parameters to the constructor which are invoked, if the state of the signal is changed. Additionaly, the module has one input signal,signal_debug_haltrequest_ack. The state of the signal is set by invokeingset_signal_debug_haltrequest_ack.Note: The module is not synthesizable and is only used for simulation purposes.
Debug registers
- group c_debug_regs
The
c_debug_regsmodule is a submodule of thec_soft_dmmodule and implements the necessary registers exposed to the host. The registers are implemented according to theRISC-V External Debug Supportspecification. The registers are accessed by theDMI-bus.Currently there are following registers implemented:
Name
Address
Description
DATA0
0x04
For data exchange between the host and the hart. Accessed by both the hart and the host.
DATA1
0x05
For data exchange between the host and the hart. Accessed by both the hart and the host.
DMCONTROL
0x10
Debug Module Control. Indicates if the hart should be
halted,resumedand how many harts are available.DMSTATUS
0x11
Debug Module Status. Indicates if the hart is currently
halted,runningandhave reset.ABSTRACTCS
0x16
Abstract Control Status. Indicates if the hart is busy or an error occured related to
abstract commands.COMMAND
0x17
The
abstract commandis written in here.ABSTRACTAUTO
0x18
(Under construction).
PROGRAM_BUFFER0
0x20
Holds an assembler command coming from the host. Accessed by both the hart and the host.
PROGRAM_BUFFER1
0x21
Holds an assembler command coming from the host. Accessed by both the hart and the host.
PROGRAM_BUFFER2
0x22
Holds an assembler command coming from the host. Accessed by both the hart and the host.
The
DATAxandPROGRAM_BUFFERxregisters are mirrored to the registers accessed by the hart.More complex registers like
COMMANDandABSTRACTCSare implemented in their own module.Note: The module is not synthesizable and is only used for simulation purposes.
- group c_debug_reg_command
The
c_debug_reg_commandmodule is a submodule of thec_debug_regsmodule. It implements thecommandregister exposed to the host according to theRISC-V External Debug Supportspecification. The register is accessed by theDMI-bus.Note: The module is not synthesizable and is only used for simulation purposes.
- group c_debug_reg_abstractcs
The
c_debug_reg_abstractcsmodule is a submodule of thec_debug_regsmodule. It implements theabstractcsregister exposed to the host according to theRISC-V External Debug Supportspecification. The register is accessed by theDMI-bus.Note: The module is not synthesizable and is only used for simulation purposes.
Functions
-
c_soft_dm::c_soft_dm(std::function<void(bool)> callback_signal_debug_haltrequest)
Constructor.
- Parameters:
callback_signal_debug_haltrequest – Callback function called when a debug hatlrequest is perfomed or done.
-
c_soft_dm::~c_soft_dm()
Destructor.
-
const char *c_soft_dm::get_info() override
Short info text about this module.
- Returns:
Info text about this module.
-
bool c_soft_dm::is_addressed(uint64_t adr) override
Returns true if adr is in range of the module.
- Parameters:
adr – Address to check.
- Returns:
True if adr is in range of this module else false.
-
uint32_t c_soft_dm::read32(uint64_t adr) override
Read register word.
- Parameters:
adr – Address of register.
- Returns:
Register data.
-
void c_soft_dm::write32(uint64_t adr, uint32_t data) override
Write register word.
- Parameters:
adr – Address of register.
data – Data to be written.
-
void c_soft_dm::dmi_write(uint8_t address, uint32_t data)
Write operation from dmi bus.
- Parameters:
address – Address of the module/register.
data – Data to write.
-
uint32_t c_soft_dm::dmi_read(uint8_t address)
Read operation from dmi bus.
- Parameters:
address – Address of the module/register.
- Returns:
Data to read.
-
void c_soft_dm::dmi_reset()
Reset from dmi bus.
Soft Debugger Wrapper
This module serves as a wrapper for all debugger_soft modules.
It instantiates a remote_bitbang, a c_dtm and a c_soft_dm.
The c_soft_peripheral interface is forwarded to the c_soft_dm.
Note: The module is not synthesizable and is only used for simulation purposes.
Debug Handler
The debug handler is a routine executed by the processor when entering debug mode.
Its purpose is to process commands received from the host system and resume
the main program if requested. Written in RISC-V assembly, it operates in
four stages: _entry, _loop, _run_cmd, and _resume. Execution begins
at _entry, then moves to _loop, where it waits for a resume request or a
run command request. If either is set, the handler jumps to the corresponding
stage. The _run_cmd stage concludes by setting the program counter to the
first abstract command register of the DM.
According to the External Debug Support standard, the last command from the
host, whether an abstract command or progbuf is an EBREAK instruction.
As a result, the debug handler is executed again after the last command.
Note
Currently there is no exception handling implemented. The debug handler needs to be extended when the piconut supports exception handling.
The Debug Handler program behaves like described in the diagram below.
Fig. 49 Debug Handler program flow.
Hardware Debugger
Under construction
Usage
For the soft debugger an example system can be found at systems/demo_debugger_soft.
For the synthesizeable debugger an example system can be found at systems/demo_debugger.
Execute a system that instantiates a debugger. For example the demo_debugger_soft
$ cd systems/demo_debugger_soft
$ DEBUG=1 make TECHS=sim sim
Or program a FPGA with the hardware debugger example system
$ cd systems/demo_debugger
$ DEBUG=1 make TECHS=syn program
To start a debug session you can either use GDB in terminal mode or use the
VSCode interface.
Terminal
To attach GDB to the current running simulation type:
$ ./tools/bin/pn-gdb --sim <path-to-target-app>
Or attach GDB to the current system running on a FPGA type:
$ ./tools/bin/pn-gdb --board <path-to-target-app>
Note
The target application must be build with DEBUG=1 option to ensure debug symbols are included.
This attaches GDB to the running program. If you want to debug the program from
the first assembler command you need to restart the application. In the GDB console type:
b _start
j _start
VSCode
Add the necessary configurations to the VSCodes configuration files.
The debugger configuration in the launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "(GDB) PicoNut Software Debugger",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/sw/application/animation/animation.rv32",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"preLaunchTask": "start-openocd-sim",
"postDebugTask": "stop-openocd",
"serverStarted": "Listening on port 3333",
"environment": [],
"externalConsole": true,
"MIMode": "gdb",
"miDebuggerPath": "riscv64-unknown-elf-gdb",
"miDebuggerServerAddress": "localhost:3333",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set hex format for gdb",
"text": "set output-radix 16"
},
{
"description": "Set available registers",
"text": "set tdesc filename ${workspaceFolder}/tools/etc/gdb_piconut.xml"
}
]
}
]
}
The tasks to start or stop OpenOCD when a debug session is started or stopped
in the tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "start-openocd-sim",
"type": "shell",
"command": "openocd -f ${workspaceFolder}/tools/etc/openocd-sim.cfg",
"problemMatcher": [],
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared"
}
},
{
"label": "stop-openocd",
"type": "shell",
"command": "pkill -f openocd",
"problemMatcher": [],
"presentation": {
"reveal": "never",
"panel": "shared"
}
}
]
}
To start the debug session go to the Run and Debug menu an select the
(GDB) PicoNut Software Debugger. After clicking run ignore the warning popup
saying that OpenOCD hasn’t terminated yet and click Debug Anyway.