Skip to main content

A tiny RISC-V instruction set simulator

Project description

tinyRV

A RISC-V instruction decoder and instruction set simulator in less than 200 lines of python.

  • Uses official RISC-V specs to decode every specified RISC-V instruction.
  • Emulates at least the base ISAs and is easily extendable.
  • RV32IZicsr_Zifencei and RV64IZicsr_Zifencei compliance validated using RISCOF (see Testing below).

Getting Started

pip install tinyrv

Print all RISC-V instructions in a binary:

from tinyrv import rvprint
rvprint('firmware.bin', xlen=32)  # xlen just for output formatting

Outputs for firmware.bin from picorv32:

00000000: custom0    raw=0x0800400b                # INVALID
00000004: custom0    raw=0x0600600b                # INVALID
00000008: jal        zero, 000003e0                # rv_i
0000000c: addi       zero, zero, 0                 # rv_i
00000010: custom0    raw=0x0200a10b                # INVALID
00000014: custom0    raw=0x0201218b                # INVALID
00000018: lui        ra, 00000000                  # rv_i
0000001c: addi       ra, ra, 352                   # rv_i
00000020: custom0    raw=0x0000410b                # INVALID
00000024: sw         sp, 0(ra)                     # rv_i
00000028: custom0    raw=0x0001410b                # INVALID
0000002c: sw         sp, 4(ra)                     # rv_i
00000030: custom0    raw=0x0001c10b                # INVALID
00000034: sw         sp, 8(ra)                     # rv_i
00000038: sw         gp, 12(ra)                    # rv_i
0000003c: sw         tp, 16(ra)                    # rv_i
...

picorv32 uses some custom instructions for IRQ handling.

Decode instructions from data:

from tinyrv import rvdecoder
for op in rvdecoder(0xf2410113, 0xde0ec086, 0x2013b7):
    print(op)

Outputs four instructions (the second word contains actually two 16-bit compressed instructions):

addi       sp, sp, -220
c.swsp     ra, uimm8sp_s=64
c.swsp     gp, uimm8sp_s=60
lui        t2, 0x201000

Each decoded instruction comes with a lot of metadata and parsed arguments:

op = next(rvdecoder(0xf2410113))
print(hex(op.data), op.name, op.extension, op.encoding, op.variable_fields, op.valid())
print(op.args, op.rd, op.rs1, op.imm12)
print(op.arg_str())
0xf2410113 addi ['rv_i'] -----------------000-----0010011 ['rd', 'rs1', 'imm12'] True
{'rd': 2, 'rs1': 2, 'imm12': -220} 2 2 -220
sp, sp, -220

Simulate a binary:

from tinyrv import rvmem, rvsim
mem = rvmem(xlen=32)  # xlen just for output formatting
mem.load('firmware.bin', base=0)
rv = rvsim(mem, xlen=32)  # xlen affects overflows, sign extensions
print(rv)  # print registers
print()
rv.step(10)  # simulate up to 10 instructions

Outputs:

x00(ro)=00000000  x08(fp)=00000000  x16(a6)=00000000  x24(s8)=00000000
x01(ra)=00000000  x09(s1)=00000000  x17(a7)=00000000  x25(s9)=00000000
x02(sp)=00000000  x10(a0)=00000000  x18(s2)=00000000  x26(10)=00000000
x03(gp)=00000000  x11(a1)=00000000  x19(s3)=00000000  x27(11)=00000000
x04(tp)=00000000  x12(a2)=00000000  x20(s4)=00000000  x28(t3)=00000000
x05(t0)=00000000  x13(a3)=00000000  x21(s5)=00000000  x29(t4)=00000000
x06(t1)=00000000  x14(a4)=00000000  x22(s6)=00000000  x30(t5)=00000000
x07(t2)=00000000  x15(a5)=00000000  x23(s7)=00000000  x31(t6)=00000000

00000000: custom-0   raw=0x0800400b                # UNKNOWN  # halted: unimplemented op

Simulation halts at the first instruction that is not implemented. Just set the pc and carry on:

rv.pc = 8
rv.step(50)
00000008: jal        zero, .+984                   #  

000003e0: addi       ra, zero, 0                   # ra = 00000000 
000003e4: addi       sp, zero, 0                   # sp = 00000000 
(... boring initialization stuff skipped ...)
00000454: addi       t5, zero, 0                   # t5 = 00000000 
00000458: addi       t6, zero, 0                   # t6 = 00000000 
0000045c: lui        sp, 0x20000                   # sp = 00020000 
00000460: jal        ra, .+1916                    # ra = 00000464 

00000bdc: lui        a0, 0xc000                    # a0 = 0000c000 
00000be0: addi       a0, a0, 1948                  # a0 = 0000c79c 
00000be4: jal        zero, .-220                   #  

00000b08: lui        a4, 0x10000000                # a4 = 10000000 
00000b0c: lbu        a5, 0(a0)                     # a5 = 00000068 mem[0000c79c] -> 68
00000b10: bne        a5, zero, .+8                 #  

00000b18: addi       a0, a0, 1                     # a0 = 0000c79d 
00000b1c: sw         a5, 0(a4)                     #  mem[10000000] <- 00000068
00000b20: jal        zero, .-20                    #  

00000b0c: lbu        a5, 0(a0)                     # a5 = 00000065 mem[0000c79d] -> 65
00000b10: bne        a5, zero, .+8                 #

Each jump, taken branch produces a newline, right-hand side has register changes and memory transactions. rvmem is paged. Memory is allocated on demand and persists. Now let's get past this loop by setting a breakpoint:

rv.step(1000, bpts={0xb14})
rv.step(10)
...
00000b0c: lbu        a5, 0(a0)                     # a5 = 0000000a mem[0000c7a7] -> 0a
00000b10: bne        a5, zero, .+8                 #  

00000b18: addi       a0, a0, 1                     # a0 = 0000c7a8 
00000b1c: sw         a5, 0(a4)                     #  mem[10000000] <- 0000000a
00000b20: jal        zero, .-20                    #  

00000b0c: lbu        a5, 0(a0)                     # a5 = 00000000 mem[0000c7a8] -> 00
00000b10: bne        a5, zero, .+8                 #  
00000b14: jalr       zero, 0(ra)                   #  
00000464: addi       ra, zero, 1000                # ra = 000003e8 
00000468: custom-0   raw=0x0a00e00b                # UNKNOWN  # halted: unimplemented op

Dev Setup

All code is in tinyrv.py. No dependencies, except pyyaml. However, tinyRV needs to load opcode specs from riscv-opcodes. Do this:

git clone https://github.com/riscv/riscv-opcodes.git tinyrv_opcodes
cd tinyrv_opcodes; make

The necessary opcode specs are also bundled in the PyPI package. If there is a tinyrv_opcodes in the current directory, tinyRV will try to use those instead of the packaged ones.

Testing

Need python (duh!) and riscv-gnu-toolchain. Tested on MacOS.

Install the RISC-V compatibility framework RISCOF:

pip3 install setuptools wheel
git clone https://github.com/riscv/riscof.git
cd riscof
pip3 install -e .

Install the Sail ISA specification language:

brew install opam zlib z3 pkg-config
opam init
opam switch create ocaml-base-compiler
opam install sail
eval $(opam config env)

Install the RISCV Sail Model:

git clone https://github.com/riscv/sail-riscv.git
cd sail-riscv
ARCH=RV32 make c_emulator/riscv_sim_RV32
ARCH=RV64 make c_emulator/riscv_sim_RV64
# copy / link c_emulator/riscv_sim_RV{32,64} into $PATH location

Optionally, install Spike RISC-V ISA Simulator:

git clone https://github.com/riscv-software-src/riscv-isa-sim.git
cd riscv-isa-sim
mkdir build
cd build
../configure --prefix=/path/to/install  # /path/to/install/bin must be in $PATH
make
make install
spike  # test

Then, run the tests:

cd tests
riscof --verbose info arch-test --clone
riscof run --config=config.ini --suite=riscv-arch-test/riscv-test-suite/ --env=riscv-arch-test/riscv-test-suite/env

Related

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tinyrv-0.0.4.tar.gz (31.6 kB view hashes)

Uploaded Source

Built Distribution

tinyrv-0.0.4-py3-none-any.whl (33.5 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page