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 :)