Neyko's devlog

PicoRV32 instruction test (iverilog, QEMU)

📅 2026-09-14

Goals:

  • build and simulate YosysHQ picorv32 instruction test
  • try to run same test using QEMU

Required files and dependencies:

  • git build-essential iverilog curl texinfo bison flex libgmp-dev libmpfr-dev libmpc-dev zlib1g-dev libexpat1 libexpat1-dev libncursesw5-dev libncurses5-dev
  • qemu ( qemu-system-riscv32 )
  • riscv-gnu-toolchain ( for gdb, from previous posts )

Instructions

  • clone picorv32 repo, install dependencies
$ git clone https://github.com/YosysHQ/picorv32.git
$ sudo apt install build-essential iverilog curl texinfo bison flex libgmp-dev libmpfr-dev libmpc-dev zlib1g-dev libexpat1 libexpat1-dev libncursesw5-dev libncurses5-dev
$ sudo apt install qemu-system-riscv32
  • in the Makefile, set RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX to the place where you want to install the rv32 toolchain. ( Yes we built our own before, but lets follow this Makefile's flow )
RISCV_GNU_TOOLCHAIN_INSTALL_PREFIX = /home/user/picorv32/opt/riscv32
  • make rv toolcain
$ make build-tools
  • with tools ready, lets build and test. its a little verbose, so will skip parts of it with (...)
$ make test
  
  //compiles our testbench
  iverilog -o testbench.vvp -DCOMPRESSED_ISA testbench.v picorv32.v
  chmod -x testbench.vvp

  //generates all *.o files
  riscv32-unknown-elf-gcc -c -mabi=ilp32 -march=rv32imc \
    -o firmware/start.o firmware/start.S
  riscv32-unknown-elf-gcc -c -mabi=ilp32 -march=rv32ic -Os --std=c99 -Werror -Wall -Wextra -Wshadow -Wundef -Wpointer-arith -Wcast-qual -Wcast-align -Wwrite-strings -Wredundant-decls -Wstrict-prototypes -Wmissing-prototypes -pedantic  -ffreestanding -nostdlib \
    -o firmware/irq.o firmware/irq.c

  //many intermediate steps
  (...)

  //generates .elf
  riscv32-unknown-elf-gcc \
    -Os -mabi=ilp32 -march=rv32imc -ffreestanding -nostdlib -o firmware/firmware.elf \
    -Wl,--build-id=none,-Bstatic,-T,firmware/sections.lds,-Map,firmware/firmware.map,--strip-debug \
    firmware/start.o firmware/irq.o 
    firmware/print.o firmware/hello.o firmware/sieve.o 
    firmware/multest.o firmware/stats.o tests/addi.o 
    tests/add.o tests/andi.o tests/and.o
    tests/auipc.o tests/beq.o tests/bge.o 
    tests/bgeu.o tests/blt.o tests/bltu.o 
    tests/bne.o tests/div.o tests/divu.o tests/jalr.o 
    tests/jal.o tests/j.o tests/lb.o tests/lbu.o 
    tests/lh.o tests/lhu.o tests/lui.o tests/lw.o 
    tests/mulh.o tests/mulhsu.o tests/mulhu.o tests/mul.o 
    tests/ori.o tests/or.o tests/rem.o tests/remu.o 
    tests/sb.o tests/sh.o tests/simple.o tests/slli.o 
    tests/sll.o tests/slti.o tests/slt.o tests/srai.o 
    tests/sra.o tests/srli.o tests/srl.o tests/sub.o 
    tests/sw.o tests/xori.o tests/xor.o -lgcc

    chmod -x firmware/firmware.elf

  //we need the raw bin for sim
    riscv32-unknown-elf-objcopy \
      -O binary firmware/firmware.elf firmware/firmware.bin

    chmod -x firmware/firmware.bin

  //generate an hex to be loaded via $readmemh() on picorv testbench.v
    python3 firmware/makehex.py firmware/firmware.bin 32768 > firmware/firmware.hex

  //run simulation
    vvp -N testbench.vvp
      hello world
      lui..OK
      auipc..OK
      j..OK
      jal..OK
      jalr..OK
      (...)
       1st prime is 2.
       2nd prime is 3.
       3rd prime is 5.
       4th prime is 7.
       (...)
      Cycle counter ......... 459615
      Instruction counter ... 100408
      CPI: 4.57
      DONE
      ------------------------------------------------------------
      EBREAK instruction at 0x0000076C
      pc  0000076F    x8  00000000    x16 F98C5E4E    x24 00000000
      x1  0000073C    x9  00000000    x17 1B639DFB    x25 00000000
      x2  00020000    x10 20000000    x18 00000000    x26 00000000
      x3  DEADBEEF    x11 075BCD15    x19 00003AC4    x27 00000000
      x4  DEADBEEF    x12 0000004F    x20 00000000    x28 38BAA671
      x5  000010C0    x13 0000004E    x21 00000000    x29 38BAA670
      x6  1B639DFB    x14 00000045    x22 00000000    x30 00000000
      x7  00000000    x15 0000000A    x23 00000000    x31 00000000
      ------------------------------------------------------------
      Number of fast external IRQs counted: 57
      Number of slow external IRQs counted: 7
      Number of timer IRQs counted: 22
      TRAP after 501212 clock cycles
      ALL TESTS PASSED.
      testbench.v:266: $finish called at 5013220000 (1ps)
  • lets inspect "make test" iverilog step in detail. its a brief inspection, so will skip large chunks of code with (...)
  iverilog -o testbench.vvp -DCOMPRESSED_ISA testbench.v picorv32.v
  //-o filename: Place  output in the file filename. If no output file name is specified,
  //            iverilog uses the default name a.out.
  //-Dmacro: Defines macro macro with the string `1' as its definition. This form  is
  //         normally only used to trigger ifdef conditionals in the Verilog source.
  • testbench.v. contains the following module declarations:
    • testbench : Simulation top. Generates signals and contains SoC top
    • picorv32_wrapper : SoC top. Contains the processor and its memory
    • picorv32_axi : processor. defined in picorv32.v
    • axi4_memory : an axi memory, used by the processor
  `timescale 1 ns / 1 ps
`ifndef VERILATOR //this is iverilog, so we use a verilog testbench

module testbench #(
    parameter AXI_TEST = 0,
    parameter VERBOSE = 0
);
    reg clk = 1;
    reg resetn = 0; //start sim with resetn asserted
    wire trap;

    always #5 clk = ~clk; //generate system clk

    initial begin  //wait 100 clks, then deassert resetn
        repeat (100) @(posedge clk); 
        resetn <= 1;
    end

    initial begin
        if ($test$plusargs("vcd")) begin //if vcd was passed as flag, dump waveform
            $dumpfile("testbench.vcd");
            $dumpvars(0, testbench);
        end
        repeat (1000000) @(posedge clk); //run for 1e6 clks
        $display("TIMEOUT");
        $finish; //quit simulation
    end

    wire trace_valid;
    wire [35:0] trace_data;
    integer trace_file;

    initial begin
        if ($test$plusargs("trace")) begin //if trace was passed as flag, dump trace
            trace_file = $fopen("testbench.trace", "w");
            repeat (10) @(posedge clk);
            while (!trap) begin
                @(posedge clk);
                if (trace_valid)
                    $fwrite(trace_file, "%x\n", trace_data);
            end
            $fclose(trace_file);
            $display("Finished writing testbench.trace.");
        end
    end

    picorv32_wrapper #( //SoC TOP instantiation
        .AXI_TEST (AXI_TEST),
        .VERBOSE  (VERBOSE)
    ) top (
        .clk(clk),
        .resetn(resetn),
    //(...)
    );
endmodule
`endif    

//========================================================

module picorv32_wrapper #( //SoC TOP declaration. contains the processor and its peripherials

    parameter AXI_TEST = 0,
    parameter VERBOSE = 0
) (
    input clk,
    input resetn,
  //(...)
);

//(...)

    axi4_memory #( //SoC Memory instantiation, compatible with axi
        .AXI_TEST (AXI_TEST),
        .VERBOSE  (VERBOSE)
    ) mem (
        .clk             (clk             ),
        .mem_axi_awvalid (mem_axi_awvalid ),
        .mem_axi_awready (mem_axi_awready ),
    //(...)
    );

    picorv32_axi #( //picosoc processor with axi interface
    //(...)
    ) uut (
        .clk            (clk            ),
        .resetn         (resetn         ),
        .trap           (trap           ),
        .mem_axi_awvalid(mem_axi_awvalid),
        .mem_axi_awready(mem_axi_awready),
    //(...)
    );

  `ifdef RISCV_FORMAL
      picorv32_rvfimon rvfi_monitor ( //monitor for formal verification
          .clock          (clk           ),
          .reset          (!resetn       ),
      //(...)
      );
  `endif

  //-- THIS IS HOW OUR FIRMWARE IS LOADED INTO SOC MEMORY --

    reg [1023:0] firmware_file;
    initial begin //read firmware.hex, copy to mem.memory using verilog's $readmemh
        if (!$value$plusargs("firmware=%s", firmware_file))
            firmware_file = "firmware/firmware.hex";
        $readmemh(firmware_file, mem.memory);
    end

endmodule

//========================================================
  
module axi4_memory #( //SoC Memory declaration.
    parameter AXI_TEST = 0,
    parameter VERBOSE = 0
) (
  //(...)
endmodule

picorv32.v

/*
 *  PicoRV32 -- A Small RISC-V (RV32I) Processor Core
 *
 *  Copyright (C) 2015  Claire Xenia Wolf <claire@yosyshq.com>
 //(...)
 *
 */

//(...)

//picorv32 core
module picorv32  #() (...);
  //some functionalities are included as modules instances
  picorv32_pcpi_fast_mul pcpi_mul (...);
  picorv32_pcpi_mul pcpi_mul (...);
  picorv32_pcpi_div pcpi_div (...);
  picorv32_regs cpuregs (...);
endmodule

//modules instantiated in picorv32 core
module picorv32_regs  #() (...);
module picorv32_pcpi_mul #() (...);
module picorv32_pcpi_fast_mul #() (...);
module picorv32_pcpi_div #() (...);

//picorv32_axi = picorv32 core + axi_adapter
module picorv32_axi  #() (...);
  picorv32_axi_adapter axi_adapter (...);
  picorv32 picorv32_core (...);

//axi adapter to native picorv32 memory interface
module picorv32_axi_adapter #() (...);

//picorv32 core with wishbone bus
module picorv32_wb  #() (...);
  picorv32 picorv32_core (...);

endmodule
  • ok now we have a little more insight on picorv32's hierarchy, and we were able to run our fw on our verilog testbench, but how about we run it on qemu too?
$ qemu-system-riscv32 -M virt -bios none -kernel firmware/firmware.elf -nographic -S -s
  qemu-system-riscv32: Some ROM regions are overlapping
  These ROM regions might have been loaded by direct user request or 
  by default.
  They could be BIOS/firmware images, a guest kernel, initrd or some 
  other file loaded into guest memory.
  Check whether you intended to load all this guest code, and whether
   it has been built to load to the correct addresses.

The following two regions overlap (in the memory address space):
  firmware/firmware.elf ELF program header segment 0 
  (addresses 0x0000000000000000 - 0x000000000000c894)
  mrom.reset 
  (addresses 0x0000000000001000 - 0x0000000000001028)
  • since qemu virt uses 0x1000, we cannot use this elf as-is, but it might be possible to run it if we change it a little. lets get info about the original elf.
  $ riscv32-unknown-elf-readelf -lw firmware/firmware.elf 
  Elf file type is EXEC (Executable file)
  Entry point 0x0
  There is 1 program header, starting at offset 52
  
  Program Headers:
    Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg
   Align
    LOAD           0x001000 0x00000000 0x00000000 0x0c894 0x0c894 RWE
   0x1000
  
   Section to Segment mapping:
    Segment Sections...
     00     .memory 
  • so since the start address is 0x0, it overlaps qemu section mrom.reset. Also, qemu guest ram starts at 0x80000000. so lets do the following modifications:
//make reset_vec on start.S global
//start.S:26
    .section .text.reset
    .global reset_vec
    .type reset_vec, @function

//comment custom instructions
//start.S:43
    //picorv32_waitirq_insn(zero)
    //picorv32_maskirq_insn(zero, zero)

//move origin to qemus guest ram addr start
//sections.lds:13
    mem : ORIGIN = 0x80000000, LENGTH = 128M /*qemu*/
  • and recompile
  $ rm firmware/*.o firmware/*.elf
  $ make firmware/firmware.elf 
  • lets check the elf. virt addr was moved to 0x80000000,
  $ riscv32-unknown-elf-readelf -lw firmware/firmware.elf

  Elf file type is EXEC (Executable file)
  Entry point 0x0
  There is 1 program header, 
  starting at offset 52
  
  Program Headers:
    Type           Offset   VirtAddr   PhysAddr   FileSi
  z MemSiz  Flg Align
    LOAD           0x001000 0x80000000 0x80000000 0x0c8b
  4 0x0c8b4 RWE 0x1000
  
   Section to Segment mapping:
    Segment Sections...
     00     .memory 
  • run in qemu. it prints hello world, but stops after that
  $ qemu-system-riscv32 -M virt -bios none -kernel firmware/firmware.elf -nographic
  hello world
  QEMU: Terminated
  • lets debug it then. run on two separate terminals:
  $ qemu-system-riscv32 -M virt -bios none -kernel firmware/firmware.elf -nographic -S -s
  $ riscv32-unknown-elf-gdb firmware/firmware.elf
  • seems like gdb wasnt built in my picorv32 build-tools, so i will use the one i built for previous posts
  riscv32-unknown-elf-gdb firmware/firmware.elf

  //connect
  (gdb) target remote tcp::1234

  //print program counter on each step
  (gdb) display/i $pc

  //step each instruction
  (gdb) si

  //then add some breakpoints (e.g. start, print_str)
  //or just keep pressing enter

  //while stepping, we can see each "hello world" character being printed
  //shortly after that we find something odd

  (gdb) 
  0x80000504 in start ()
  => 0x80000504 <start+132>:      li      ra,1000
  (gdb) 
  0x80000508 in start ()
  => 0x80000508 <start+136>:      .insn   4, 0x0a00e00b
  (gdb) 
  0x00000000 in ?? ()
  => 0x0: <error: Cannot access memory at address 0x0>
  (gdb) 
  • we found another insn. Since many of this fw instruction tests use insn, modifying all of them is not practical. It's better to make a fw designed to be multi targeted. so lets stop here, and maybe try that on following posts :)