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