This is a post about hello test in piscorv32. I am not going to dig deep into picorv32 itself. Just the firmware.
Where to start Link to heading
As usual, It makes sense to start with Makefile
and work backward.
make -n test
After removing verilog related commands and tests, We have the following commands for the firmware.
/opt/riscv32i/bin/riscv32-unknown-elf-gcc -c -mabi=ilp32 -march=rv32imc -o firmware/start.o firmware/start.S
/opt/riscv32i/bin/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
/opt/riscv32i/bin/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/print.o firmware/print.c
/opt/riscv32i/bin/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/hello.o firmware/hello.c
/opt/riscv32i/bin/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/sieve.o firmware/sieve.c
/opt/riscv32i/bin/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/multest.o firmware/multest.c
/opt/riscv32i/bin/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/stats.o firmware/stats.c
/opt/riscv32i/bin/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 -lgcc
/opt/riscv32i/bin/riscv32-unknown-elf-objcopy -O binary firmware/firmware.elf firmware/firmware.bin
python3 firmware/makehex.py firmware/firmware.bin 32768 > firmware/firmware.hex
Startup assembly Link to heading
Probably the most important part(beside linking):
/opt/riscv32i/bin/riscv32-unknown-elf-gcc -c -mabi=ilp32 -march=rv32imc -o firmware/start.o firmware/start.S
Reset Vector Link to heading
At the start of file start.S
, reseet_vector
label defines two instructions(2 * 4 Bytes), Then jumps to start
reset_vec:
// no more than 16 bytes here !
picorv32_waitirq_insn(zero)
picorv32_maskirq_insn(zero, zero)
j start
The macros eventually expand to .word
(which is 4 bytes). It literally stitch parts of the instruction together.
#define r_type_insn(_f7, _rs2, _rs1, _f3, _rd, _opc) \
.word (((_f7) << 25) | ((_rs2) << 20) | ((_rs1) << 15) | ((_f3) << 12) | ((_rd) << 7) | ((_opc) << 0))
#define picorv32_retirq_insn() \
r_type_insn(0b0000010, 0, 0, 0b000, 0, 0b0001011)
#define picorv32_maskirq_insn(_rd, _rs) \
r_type_insn(0b0000011, 0, regnum_ ## _rs, 0b110, regnum_ ## _rd, 0b0001011)
Start routine Link to heading
start
zeros out GPRs.
start:
/* zero-initialize all registers */
addi x1, zero, 0
addi x2, zero, 0
addi x3, zero, 0
addi x4, zero, 0
...
...
Then initializes the stack pointer to 0x20000
as lui
does the following
LUI (load upper immediate) uses the same opcode as RV32I. LUI places the 20-bit U-immediate into bits 31–12 of register rd and places zero in the lowest 12 bits.
so, sp
will have hex(128*1024) = 0x20000
which is the end of the address space.
Then calls one of the test functions
#ifdef ENABLE_HELLO
/* set stack pointer */
lui sp,(128*1024)>>12
/* call hello C code */
jal ra,hello
#endif
hello example Link to heading
Software Link to heading
For the sake of this post, the important files are hello.c
and print.c
/opt/riscv32i/bin/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/print.o firmware/print.c
/opt/riscv32i/bin/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/hello.o firmware/hello.c
From print.c
void hello(void)
{
print_str("hello world\n");
}
print_str
is a loop to write to address 0x10000000
which should map to something(will see on testbench.v
)
#define OUTPORT 0x10000000
void print_chr(char ch)
{
*((volatile uint32_t*)OUTPORT) = ch;
}
void print_str(const char *p)
{
while (*p != 0)
*((volatile uint32_t*)OUTPORT) = *(p++);
}
And the assembly generated by GCC
00000008 <print_str>:
8: 10000737 lui a4,0x10000
0000000c <.L3>:
c: 00054783 lbu a5,0(a0)
10: e391 bnez a5,14 <.L4>
12: 8082 ret
00000014 <.L4>:
14: 0505 addi a0,a0,1
16: c31c sw a5,0(a4)
18: bfd5 j c <.L3>
Hardware Link to heading
From Hardware side, address 0x10000000
should map to address space allocated for uart but for simulation, testbench.v
just $display
the characters.
if (latched_waddr == 32'h1000_0000) begin
if (verbose) begin
if (32 <= latched_wdata && latched_wdata < 128)
$display("OUT: '%c'", latched_wdata[7:0]);
else
$display("OUT: %3d", latched_wdata);
end else begin
$write("%c", latched_wdata[7:0]);
`ifndef VERILATOR
$fflush();
`endif
end
end else
Linking Link to heading
There are several things to unpack in the linker command
/opt/riscv32i/bin/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 -lgcc
Options Link to heading
-ffreestanding
This one is important as we don’t have libc here. So, we have to tell gcc that. from Stack overflow
A freestanding environment is one in which the standard library may not exist, and program startup may not necessarily be at “main”. The option -ffreestanding directs the compiler to not assume that standard functions have their usual definition.
--nostdlib
from GCC docs
Do not use the standard system startup files or libraries when linking. No startup files and only the libraries you specify are passed to the linker, and options specifying linkage of the system libraries, such as -static-libgcc or -shared-libgcc, are ignored.
Linker script Link to heading
sections.lds
sets mem
to start of 0x0
and lenght of 96k
. Note that sp pointer was init to point to 0x20000
which 128k. Which leaves 32k for stack. Other than that, typical text and data ELF section mapping.
MEMORY {
/* the memory in the testbench is 128k in size;
* set LENGTH=96k and leave at least 32k for stack */
mem : ORIGIN = 0x00000000, LENGTH = 0x00018000
}
SECTIONS {
.memory : {
. = 0x000000;
start*(.text);
*(.text);
*(*);
end = .;
. = ALIGN(4);
} > mem
}
elf to bin Conversion Link to heading
That is straightforward objcopy
to convert elf to binary
/opt/riscv32i/bin/riscv32-unknown-elf-objcopy -O binary firmware/firmware.elf firmware/firmware.bin
bin to hex Conversion Link to heading
The final touch is converting bin file to hex file for $readmemh
python3 firmware/makehex.py firmware/firmware.bin 32768 > firmware/firmware.hex
Snippet from makehex.py
for i in range(nwords):
if i < len(bindata) // 4:
w = bindata[4*i : 4*i+4]
print("%02x%02x%02x%02x" % (w[3], w[2], w[1], w[0]))
else:
print("0")
And $readmemh
in testbench.v
to load the firmware.hex
initial begin
if (!$value$plusargs("firmware=%s", firmware_file))
firmware_file = "firmware/firmware.hex";
$readmemh(firmware_file, mem.memory);
end