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.

../_images/Overview.drawio.svg

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 when tdo is 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)

../_images/jtag_tap_controller_state_diagram.svg

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 the Debug Module (DM) over JTAG. In the “External Debug Support” standard, the specification for a DTM with a JTAG interface is defined. This is based on the definition of a TAP in 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-bus and to reset the DMI-bus.

DMI

0x11

This register is used to access the DM via the DMI-bus.

The DMI-bus is used to read or write to registers inside the DM.

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 function jtag_get_output_pin(). To set the JTAG reset pin call the function jtag_reset(). The type jtag_input_pins_t is a struct holding the tck, tms, tdi signals as booleans.

The DTM is the master in the DMI-bus and drives all the operations happening. The callback functions for write, read or reset the DMI-bus must be provided when a c_dtm instance 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-bus into 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 DM has registers accessed by the DTM called Debug registers and normal or system registers. The c_soft_dm module derives from c_soft_peripheral so 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 DM if he is running, halted, executing commands.

DEBUG_HANDLER_START

0x3c

Start of the debug handler program.

The registers accessed by the DTM are implemented and described in the c_debug_regs module which is part of this module.

Halting a hart can be done in two ways.

  • The hart reads an EBREAK instruction

  • haltrequst from the host. If a hart is halted it starts executing the debug handler program. The haltrequst from 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_BUFFERx registers or send and abstract commands. The abstract command holds the instruction in a more abstract kind. It has the be processed to one or multiple assembler commands. They are then saved in the ABSTRACT_COMMANDx registers. After the commands are updated in the registers there is the run_commandsreq bit set in the HARTCONTROL register which tells the dabug handler to executed the new commands.

To resume the execution of the main program there is the resumereq bit in the HARTCONTROL register that tells the debug handler to resume the main program.

The HARTSTATUS register 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_dm module is fixed set to zero (0x00000000) following to the External Debug Support standard.

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 invokeing set_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_regs module is a submodule of the c_soft_dm module and implements the necessary registers exposed to the host. The registers are implemented according to the RISC-V External Debug Support specification. The registers are accessed by the DMI-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, resumed and how many harts are available.

DMSTATUS

0x11

Debug Module Status. Indicates if the hart is currently halted, running and have reset.

ABSTRACTCS

0x16

Abstract Control Status. Indicates if the hart is busy or an error occured related to abstract commands.

COMMAND

0x17

The abstract command is 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 DATAx and PROGRAM_BUFFERx registers are mirrored to the registers accessed by the hart.

More complex registers like COMMAND and ABSTRACTCS are 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_command module is a submodule of the c_debug_regs module. It implements the command register exposed to the host according to the RISC-V External Debug Support specification. The register is accessed by the DMI-bus.

Note: The module is not synthesizable and is only used for simulation purposes.

group c_debug_reg_abstractcs

The c_debug_reg_abstractcs module is a submodule of the c_debug_regs module. It implements the abstractcs register exposed to the host according to the RISC-V External Debug Support specification. The register is accessed by the DMI-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.

../_images/debug_handler_flow_diagram.drawio.svg

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.

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