Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog
RISC-V External Debug Support , task_group_vote-4-g4e0bb0f 2019/03/25

Debugger Implementation

This section details how an external debugger might use the described debug interface to perform some common operations on RISC-V cores using the JTAG DTM described in Section [sec:jtagdtm]. All these examples assume a 32-bit core but it should be easy to adapt the examples to 64- or 128-bit cores.

To keep the examples readable, they all assume that everything succeeds, and that they complete faster than the debugger can perform the next access. This will be the case in a typical JTAG setup. However, the debugger must always check the sticky error status bits after performing a sequence of actions. If it sees any that are set, then it should attempt the same actions again, possibly while adding in some delay, or explicit checks for status bits.

Debug Module Interface Access

To read an arbitrary Debug Module register, select dmi, and scan in a value with opset to 1, and addressset to the desired register address. In Update-DR the operation will start, and in Capture-DR its results will be captured into data. If the operation didn’t complete in time, opwill be 3 and the value in datamust be ignored. The busy condition must be cleared by writing dmiresetin dtmcs, and then the second scan scan must be performed again. This process must be repeated until opreturns 0. In later operations the debugger should allow for more time between Capture-DR and Update-DR.

To write an arbitrary Debug Bus register, select dmi, and scan in a value with opset to 2, and addressand dataset to the desired register address and data respectively. From then on everything happens exactly as with a read, except that a write is performed instead of the read.

It should almost never be necessary to scan IR, avoiding a big part of the inefficiency in typical JTAG use.

Checking for Halted Harts

A user will want to know as quickly as possible when a hart is halted (e.g. due to a breakpoint). To efficiently determine which harts are halted when there are many harts, the debugger uses the haltsum registers. Assuming the maximum number of harts exist, first it checks haltsum3. For each bit set there, it writes hartsel, and checks haltsum2. This process repeats through haltsum1and haltsum0. Depending on how many harts exist, the process should start at one of the lower haltsum registers.

Halting

To halt one or more harts, the debugger selects them, sets haltreq, and then waits for allhaltedto indicate the harts are halted. Then it can clear haltreqto 0, or leave it high to catch a hart that resets while halted.

Running

First, the debugger should restore any registers that it has overwritten. Then it can let the selected harts run by setting resumereq. Once allresumeackis set, the debugger knows the hart has resumed, and it can clear resumereq. Harts might halt very quickly after resuming (e.g. by hitting a software breakpoint) so the debugger cannot use allhalted/anyhaltedto check whether the hart resumed.

Single Step

Using the hardware single step feature is almost the same as regular running. The debugger just sets stepin dcsrbefore letting the hart run. The hart behaves exactly as in the running case, except that interrupts may be disabled (depending on stepie) and it only fetches and executes a single instruction before re-entering Debug Mode.

Accessing Registers

Using Abstract Command

Read s0using abstract command:

image

Write mstatususing abstract command:

image

Using Program Buffer

Abstract commands are used to exchange data with GPRs. Using this mechanism, other registers can be accessed by moving their value into/out of GPRs.

Write mstatususing program buffer:

image

Read f1using program buffer:

image

Reading Memory

Using System Bus Access

With system bus access, addresses are physical system bus addresses.

Read a word from memory using system bus access:

image

Read block of memory using system bus access:

image

Using Program Buffer

Through the Program Buffer, the hart performs the memory accesses. Addresses are physical or virtual (depending on mprvenand other system configuration).

Read a word from memory using program buffer:

image

Read block of memory using program buffer:

image

Using Abstract Memory Access

Abstract memory accesses act as if they are performed by the hart, although the actual implementation may differ.

Read a word from memory using abstract memory access:

image

Read block of memory using abstract memory access:

image

Writing Memory

Using System Bus Access

With system bus access, addresses are physical system bus addresses.

Write a word to memory using system bus access:

image

Write a block of memory using system bus access:

image

Using Program Buffer

Through the Program Buffer, the hart performs the memory accesses. Addresses are physical or virtual (depending on mprvenand other system configuration).

Write a word to memory using program buffer:

image

Write block of memory using program buffer:

image

Using Abstract Memory Access

Abstract memory accesses act as if they are performed by the hart, although the actual implementation may differ.

Write a word to memory using abstract memory access:

image

Write a block of memory using abstract memory access:

image

Triggers

A debugger can use hardware triggers to halt a hart when a certain event occurs. Below are some examples, but as there is no requirement on the number of features of the triggers implemented by a hart, these examples may not be applicable to all implementations. When a debugger wants to set a trigger, it writes the desired configuration, and then reads back to see if that configuration is supported.

Enter Debug Mode just before the instruction at 0x80001234 is executed, to be used as an instruction breakpoint in ROM:

image

Enter Debug Mode right after the value at 0x80007f80 is read:

image

Enter Debug Mode right before a write to an address between 0x80007c80 and 0x80007cef (inclusive):

image

Enter Debug Mode right before a write to an address between 0x81230000 and 0x8123ffff (inclusive):

image

Enter Debug Mode right after a read from an address between 0x86753090 and 0x8675309f or between 0x96753090 and 0x9675309f (inclusive):

image

Handling Exceptions

Generally the debugger can avoid exceptions by being careful with the programs it writes. Sometimes they are unavoidable though, e.g. if the user asks to access memory or a CSR that is not implemented. A typical debugger will not know enough about the platform to know what’s going to happen, and must attempt the access to determine the outcome.

When an exception occurs while executing the Program Buffer, cmderrbecomes set. The debugger can check this field to see whether a program encountered an exception. If there was an exception, it’s left to the debugger to know what must have caused it.

Quick Access

There are a variety of instructions to transfer data between GPRs and the data registers. They are either loads/stores or CSR reads/writes. The specific addresses also vary. This is all specified in hartinfo. The examples here use the pseudo-op transfer dest, src to represent all these options.

Halt the hart for a minimum amount of time to perform a single memory write:

image

This shows an example of setting the mbit in mcontrolto enable a hardware breakpoint in M-mode. Similar quick access instructions could have been used previously to configure the trigger that is being enabled here:

image