Emulator

Let’s start this post with some truism. Developing a software/hardware/firmware is a never ending research and learning process. But what does it really mean? It means that I had spent shit amout of time searching through internet and as a result I decided I need to write a RISC V CPU emulator.

But why?

Writing and debugging verilog code while still not knowing what is going on can be frustrating. So in order to spare my hair I decided to gain a deep understanding of RISC V architecture.

Do I have it now? Yes, but You do not know what you do not know.

Source code is available on github. The main.cc in under 850 lines of C++ code. I am sure that there is still some bugs, I have tested only ADDI, ADD and SUB commands. The question that comes up to me is: If I do understand how it works, is it worth to spend more time on testing emulator which will never be used by anyone? The naive anwser is NO. If You do not know what you do not know is true, then there is something to acknowledge. The second argument is trivial: finish what you have started.

After this long intro it is time for a little sample! Code below is written in risc v assembly. What it does is: load 1 into t0 register, load 2 into t1 register, add t0 to t1 and store result in t2

.text
  .global _start

_start:
  addi t0, x0, 1
  addi t1, x0, 2
  add t2, t0, t1

After compilation with RISC V Toolchain we got: � ��b. Because it says nothing meaningful, let’s use hexdump magic!

00100293
00200313
006283b3

Each line is one instruction. Those instructions are later decoded and executed by cpu. Example of decoding may looks like:

000000000001 00000 000    00101 0010011
imm          rs1   funct3 rd    opcode
  • imm - 12 bit signed value -> 1
  • rs1 - source register address -> x0 value is always zero.
  • funct3 - 000 means that it is ADDI.
  • rd - destination resiter addres -> 5 (t0 is alias for x5).
  • opcode - tells what type of instruction it is (R-type, I-Type, S-type, U-Type).

After running this program registers look like:

x0: 0x0
x1: 0x0
x2: 0x0
x3: 0x0
x4: 0x0
x5: 0x1   <-- t0 = 1
x6: 0x2   <-- t1 = 2
x7: 0x3   <-- t2 = 3 (1 + 2 = 3)
x8: 0x0
x9: 0x0
xa: 0x0
xb: 0x0
xc: 0x0
xd: 0x0
xe: 0x0
xf: 0x0
x10: 0x0
x11: 0x0
x12: 0x0
x13: 0x0
x14: 0x0
x15: 0x0
x16: 0x0
x17: 0x0
x18: 0x0
x19: 0x0
x1a: 0x0
x1b: 0x0
x1c: 0x0
x1d: 0x0
x1e: 0x0
x1f: 0x0

Is there anything that differs from hardware implementation? Yes, risc v pipelining is poorly implemented.

What is next? Finish tests and move to verilog.