My First Instruction

Slide to adjust verbosity level 

Let’s start from the RV32I description in the (currently) latest version of the RISC-V ISA Specification, which is given in the Chapter 2: RV32I Base Integer Instruction Set. The specification first goes on to describe Integer Computational Instructions (Chapter 2.4), of which the addi instruction is explained first, so let’s start with that one.

Relevant pygears_riscv git commit: pygears_riscv@a7d98ec

Instruction format

The addi instruction has an “Integer Register-Immediate” format, aka the “I-type” format shown below.

../_images/integer-register-immediate-instruction.png

“Integer Register-Immediate” instruction format, aka the “I-type” format, from the RISC-V ISA Specification

Since the instruction encodings have fields that serve different purposes from one another, I’ll represent the instruction with the typing/tuple PyGears type. For the “I-type” instructions, I ended-up with a following definition in PyGears, given in pygears_riscv/riscv/riscv.py:

TInstructionI
TInstructionI = Tuple[{
    'opcode': Uint[7],
    'rd'    : Uint[5],
    'funct3': Uint[3],
    'rs1'   : Uint[5],
    'imm'   : Int[12]
}]

As I said, opcode and funct3 will have unique, specific value for the addi instruction which is specified by ISA. I had to consult Chapter 19: RV32/64G Instruction Set Listings in order to get the correct values for the function ID fields: opcode=0x13 and funct3=0x0.

../_images/addi-instruction-field-value.png

addi instruction format, from RISC-V ISA Specification

Other instruction fields: rd, rs1 and imm, can take arbitrary values, so I can’t fix those in advance. This gives me the following template for the addi instruction:

OPCODE_IMM

OPCODE_IMM = 0x13

FUNCT3_ADDI

FUNCT3_ADDI = 0x0

ADDI
ADDI = TInstructionI({
    'opcode': OPCODE_IMM,
    'rd'    : 0,
    'funct3': FUNCT3_ADDI,
    'rs1'   : 0,
    'imm'   : 0
})

Processor implementation

Without further ado, this single-instruction capable RISC-V processor written in PyGears looks like this:

@gear
def riscv(instruction: TInstructionI, reg_data: Uint['xlen']):

    reg_file_rd_req = instruction['rs1']

    reg_data_signed = reg_data | Int[int(reg_data.dtype)]

    add_res = (reg_data_signed + instruction['imm']) \
        | reg_data.dtype

    reg_file_wr_req = ccat(instruction['rd'], add_res)

    return reg_file_rd_req, reg_file_wr_req

Image below shows the resulting processor structure and connection with its environment.

../_images/riscv_graph_addi.png

Graph of the single-instruction RISC-V processor implementation in PyGears. The gears are drown as octagons and hierarchical modules are drawn as boxes.

The read and write requests are output from the riscv gear by outputting them from the function, and will be connected to the inputs of the register file module in a higher hierarchy level.

Verification environment

For testing the ISA implementation, I’ve envisioned the following test:

  1. Initialize the register file

  2. Send a stream of instructions to the processor

  3. Check the final register values to the reference design

I’ve written an environment that supports these kinds of tests in pygears_riscv/verif/env.py. This is a regular Python function (not a gear) that instantiates the riscv and register_file gears and wires them properly in the following manner.

riscv/images/addi-env-block-diagram.png

Spike interface

I relocated the Spike interface class to pygears_riscv/verif/spike.py and had to make one major change to accomodate for the RISC-V ABI (Application Binary Interface).

I created a wrapper class around my Spike interface inside pygears_riscv/verif/spike_instr_test.py, which automates all the tasks I did manually in the previous blog post, namely: writting the assembly file, running the gcc, and calling Spike interface with the correct parameters. I also added the possibility to easily initialize the register values which will come in handy for thourough verification.

Writing the first test

For the start, I’ll create one simple test as a proof of concept. To make it a bit more serious I’ll use negative numbers as arguments to see whether sign extension works properly too.

def test_addi():
    test_instr = ADDI.replace(imm=-1233, rd=1, rs1=1)

    reg_file_init = {1: -1}

    spike_reg_file_start, spike_reg_file_end = spike_instr_test.run_all(
        [test_instr], outdir='build', reg_file_init=reg_file_init)

    reg_file_mem = riscv_instr_seq_env(
        instr_seq=[test_instr],
        xlen=32,
        reg_file_mem=dict(enumerate(spike_reg_file_start)))

    sim()

    print(f'Resulting value of the register x1: {Int[32](reg_file_mem[1])}')

    for reg_id, reg_value in reg_file_mem.items():
        assert spike_reg_file_end[reg_id] == reg_value

Running the test

For running the tests for the PyGears framework, I’ve been using pytest, so I’ll use it here too.

In order to invoke the test with pytest, you can navigate to the tests/test_instructions folder in your terminal and run the test by invoking:

pytest "test_addi.py::test_addi"

The pytest runner should automatically find the test_addi() test function, run it and print the report:

========================================== test session starts ==========================================
platform linux -- Python 3.6.6, pytest-3.9.3, py-1.7.0, pluggy-0.8.0
rootdir: /tools/home/pygears_riscv/tests, inifile: setup.cfg
collected 1 item

test_addi.py .                                                                                    [100%]

======================================= 1 passed in 3.57 seconds ========================================

Et voila! My RISC-V design is completely aligned with the Spike simulator!

Conclusion

Hey, I have my single-instruction RISC-V processor implemented in PyGears and verified with a simple test. It may seem that much needed to happen in order for the processor to support this one instruction. But most of the effort went into building the verification environment that I think is now really powerfull and I don’t think much additional effort needs to be poured into it, besides adding the data and instruction memory modules. In fact, with only 5 lines of code, the RISC-V implementation decodes the instruction, performs the ALU operation and interfaces the register file, not bad for a 5-liner.

This post turned out longer than I expected, so I left some important topics for later blog posts like: refactoring of the tests, simulation with Cadence or Questa simulators, maximum frequency and core footprint assesment, constrained-random simulation, etc. So stay tuned!

Comments

comments powered by Disqus