While the RISCV-DV
Instruction Generator provides a debug_rom
section as
well as various interrupt and exception handlers, from a verification
standpoint this is not enough to help ensure that a core's internal state is being
updated correctly upon receiving external stimulus such as interrupts or debug
requests, such as the values of any relevant CSRs.
To help with this issue, the instruction generator also provides a mechanism by
which to communicate information to a RTL simulation environment by means of a
handshaking protocol.
Every handshake produced by the instruction generator is just a small segment of
RISC-V assembly code, that end in one or more sw
instructions to a specified memory
address signature_addr
.
This signature_addr
is completely customizable, and
can be specified through a plusarg
runtime option to the generator.
There is also an enable bit require_signature_addr
that must be set through
another plusarg
argument to enable these handshake code segments to be
generated in the main random assembly program.
An RTL simulation environment that utilizes
this handshaking mechanism should provide a basic set of tasks to monitor this
signature_addr
for any writes, as this will indicate that the core under test is
executing a particular handshake assembly sequence and is transmitting some
information to the testbench for analysis.
As a result, this signature_addr
acts as sort of memory-mapped address that the testbench will monitor, and as
such, case should be taken when setting this address to ensure that the generator's
program randomization will not somehow create a sequence of random load/store
instructions that access the same signature_addr
.
A suggested value for this signature_addr
is the value 0x8ffffffc
.
More details, and an example, as to how to interface the testbench with this
handshake mechanism will be provided below.
The function gen_signature_handshake(...)
contained in
src/riscv_asm_program_gen.sv
is used to actually generate the handshaking code and push it into the specified
instruction queue. Its usage can be seen repeatedly throughout the program
generation in various places, such as trap handlers and the debug ROM, where it
is important to send information to a testbench for further verification.
The signature_type_t
, core_status_t
, and test_result_t
enums specified as
input values to this function are defined in
src/riscv_signature_pkg.sv.
Note that all of these definitions are within a standalone package, this is so
that an RTL simulation environment can also import this package to gain access
to these enums.
The signature_type_t
enum is by far the most important enum value, as
this specifies what kind of handshake will be generated.
There are currently 4 defined values of signature_type
, each corresponding
to a different handshake type that will be generated; each will be explained below.
Note that two GPRs must be used to temporarily hold the store address and the
actual data to store to this address; the generator randomizes these two GPRs
for every generated program, but for the purposes of this document, x1
and
x2
will be used, and 0x8ffffffc
will be used as the example signature_addr
.
When the signature_type
argument is specified as CORE_STATUS
, a single word
of data will be written to the signature_addr
. As the actual signature_type
value is 8 bits wide, as specified in the riscv_signature_pkg
, this generated
data word will contain the CORE_STATUS
value in its bottom 8 bits, and will
contain the specified value of core_status_t
in the upper 24 bits. This
signature handshake is intended to convey basic information about the core's
execution state to an RTL simulation environment; a handshake containing a
core_status
of IN_DEBUG_MODE
is added to the debug ROM to indicate to a
testbench that the core has jumped into Debug Mode and is executing the debug
ROM, a handshake containing a core_status
of ILLEGAL_INSTR_EXCEPTION
is
added to the illegal instruction exception handler code created by the generator
to indicate to a testbench that the core has trapped to and is executing the
proper handler after encountering an illegal instruction, and so on for the rest
of the defined core_status_t
enum values.
Note that when generating these specific handshakes, it is only necessary to
specify the parameters instr
, signature_type
, and core_status
. For
example, to generate this handshake to signal status IN_MACHINE_MODE
to the
testbench, the call to the function looks like this:
gen_signature_handshake(.instr(instr_string_queue),
.signature_type(CORE_STATUS),
.core_status(IN_MACHINE_MODE));
The sequence of assembly code generated by this call looks like the following:
# First, load the signature address into a GPR
li x2, 0x8ffffffc
# Load the intended core_status_t enum value into
# a second GPR
li x1, 0x2
# Left-shift the core_status value by 8 bits
# to make room for the signature_type
slli x1, x1, 8
# Load the intended signature_type_t enum value into
# the bottom 8 bits of the data word
addi x1, x1, 0x0
# Store the data word to memory at the location of the signature_addr
sw x1, 0(x2)
As before, when signature_type
is set to TEST_RESULT
a single word of data
will be written to the signature address, and the value TEST_RESULT
will be
placed in the bottom 8 bits. The upper 24 bits will then contain a value of type
test_result_t
, either TEST_PASS
or TEST_FAIL
, to indicate to the testbench
the exit status of the test. As the ISS co-simulation flow provides a robust
end-of-test correctness check, the only time that this signature handshake is
used is in the riscv_csr_test
. Since this test is generated with a Python
script and is entirely self-checking, we must send an exit status of TEST_PASS
or TEST_FAIL
to the testbench to indicate to either throw an error or end the
test correctly.
Note that when generating these handshakes, the only arguments that need to be
specified are instr
, signature_type
, and test_result
. For example, to
generate a handshake to communicate TEST_PASS
to a testbench, the function
call would look like this:
gen_signature_handshake(.instr(instr_string_queue),
.signature_type(TEST_RESULT),
.test_result(TEST_PASS));
The sequence of generated assembly code with this function call would look like the following:
# Load the signature address into a GPR
li x2 0x8ffffffc
# Load the intended test_result_t enum value
li x1, 0x0
# Left-shift the test_result value by 8 bits
slli x1, x1, 8
# Load the intended signature_type_t enum value into
# the bottom 8 bits of the data word
addi x1, x1, 0x1
# Store this formatted word to memory at the signature address
sw x1, 0(x2)
When a signature_type
of WRITE_GPR
is passed to the
gen_signature_handshake(...)
function, one data word will initially be written
to the signature address, containing the signature_type
of WRITE_GPR
in the
lower 8 bits. After this, the value held by each of the 32 RISC-V general
purpose registers from x0
to x31
will be written to the signature address
with sw
instructions.
For this particular handshake, the only function arguments that need to be
specified are instr
and signature_type
. A function call to generate this
particular handshake would look like the following:
gen_signature_handshake(.instr(instr_string_queue),
.signature_type(WRITE_GPR));
The generated assembly sequence would look like this:
# Load the signature address into a GPR
li x2, 0x8ffffffc
# Load the value of WRITE_GPR into a second GPR
li x1, 0x2
# Store this word to memory at the signature address
sw x1, 0(x2)
# Iterate through all 32 GPRs and write each one to
# memory at the signature address
sw x0, 0(x2)
sw x1, 0(x2)
sw x2, 0(x2)
sw x3, 0(x2)
...
sw x30, 0(x2)
sw x31, 0(x2)
When gen_signature_handshake(...)
is called with WRITE_CSR
as the
signature_type
argument, we will generate a first sw
instruction that writes a
data word to the signature_addr
that contains the value WRITE_CSR
in the
bottom 8 bits, and the address of the desired CSR in the upper 24 bits, to
indicate to the testbench which CSR will be written.
This first generated sw
instruction is then followed by a second one, which
writes the actual data contained in the specified CSR to the signature address.
Note the only function arguments that have to be specified to generate this
handshake are instr
, signature_type
, and csr
. As an example, to generate a
handshake that writes the value of the mie
CSR to the RTL simulation
environment, the function call would look like this:
gen_signature_handshake(.instr(instr_string_queue),
.signature_type(WRITE_CSR),
.csr(MIE));
The sequence of assembly generated by this call would look like the following:
# Load the signature address into a GPR
li x2, 0x8ffffffc
# Load the address of MIE into the second GPR
li x1, 0x304
# Left-shift the CSR address by 8 bits
slli x1, x1, 8
# Load the WRITE_CSR signature_type value into
# the bottom 8 bits of the data word.
# At this point, the data word is 0x00030403
addi x1, x1, 0x3
# Store this formatted word to memory at the signature address
sw x1, 0(x2)
# Read the actual CSR value into the second GPR
csrr x1, 0x304
# Write the value held by the CSR into memory at the signature address
sw x1, 0(x2)
Everything previously outlined has been relating to how this handshake
generation is implemented from the perspective of the RISCV-DV
instruction
generator, but some work must be done in the RTL simulation environment to
actually interface with and use these handshakes to improve verification with
this mechanism.
This handshaking mechanism has been put to use for verification of the Ibex
RISC-V core, in collaboration with LowRISC. To
interface with the handshaking code produced in the generator, this testbench
makes heavy use of the task wait_for_mem_txn(...)
found in
core_ibex_base_test.sv.
This task polls the Ibex core's data memory interface for any writes to the
chosen signature address (0x8ffffffc
), and then based on the value of
signature_type
encoded by the generated handshake code, this task takes
appropriate action and stores the relevant data into a queue instantiated in the
base test class.
For example upon detecting a transaction written to the
signature address that has a signature_type
of WRITE_CSR
, it right-shifts
the collected data word by 8 to obtain the CSR address, which is then stored to
the local queue. However, since for WRITE_CSR
signatures there is a second
data word that gets written to memory at the signature address, the task waits
for the second write containing the CSR data to arrive, and then stores that
into the queue as well. After this task completes, it is now possible to pop
the stored data off of the queue for analysis anywhere else in the test class,
in this case examining the values of various CSR fields.
Additionally, the Ibex testbench provides a fairly basic API of some tasks
wrapping wait_for_mem_txn(...)
for frequently used functionalities in various
test classes. This API is also found in
core_ibex_base_test.sv.
Examples of use-cases for these API functions can be found throughout the
library of tests written for the Ibex core, found at
core_ibex_test_lib.sv, as these are heavily used to verify the core's response to external debug and interrupt stimulus.