Skip to content

Latest commit

 

History

History
250 lines (227 loc) · 11.9 KB

HANDSHAKE.md

File metadata and controls

250 lines (227 loc) · 11.9 KB

Overview

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.

Usage

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.

Handshake Sequence Architecture

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.

CORE_STATUS

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)

TEST_RESULT

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)

WRITE_GPR

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)

WRITE_CSR

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)

Sample Testbench Integration

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.