Skip to main content

PicoRV32 RISC-V Core

· 4 min read

We implement a RISC-V core in Verik based on the PicoRV32 project. PicoRV32 is a CPU core that supports the RISC-V RV32IMC instruction set. On top of the base RV32I instruction set, it implements the M-extension for integer multiplication and division and the C-extension for compressed instructions. It is configurable with an optional interrupt controller, single or two-cycle ALU, and single or dual-port register file.

For the most part, Verik source code is very similar to SystemVerilog. Here we have part of the decoder for compressed instructions. In SystemVerilog, the width and signedness of an expression may be context dependent. Expressions may be implicitly extended, truncated, or cast between signed and unsigned. To better enforce type safety, Verik does not permit implicit casts of width or signedness. Here we see that expressions are explicitly extended and truncated with ext and tru. This improves readability and reduces the likelihood of bugs.

when {
!mem_rdata_latched[11] && !mem_rdata_latched[12] -> { // C.SRLI, C.SRAI
is_alu_reg_imm = true
decoded_rd = (u(8) + mem_rdata_latched[9, 7]).ext()
decoded_rs1 = (u(8) + mem_rdata_latched[9, 7]).ext()
decoded_rs2 = cat(mem_rdata_latched[12], mem_rdata_latched[6, 2]).tru()
}
mem_rdata_latched[11, 10] == u(0b10) -> { // C.ANDI
is_alu_reg_imm = true
decoded_rd = (u(8) + mem_rdata_latched[9, 7]).ext()
decoded_rs1 = (u(8) + mem_rdata_latched[9, 7]).ext()
}
mem_rdata_latched[12, 10] == u(0b011) -> { // C.SUB, C.XOR, C.OR, C.AND
is_alu_reg_imm = true
decoded_rd = (u(8) + mem_rdata_latched[9, 7]).ext()
decoded_rs1 = (u(8) + mem_rdata_latched[9, 7]).ext()
decoded_rs2 = (u(8) + mem_rdata_latched[4, 2]).ext()
}
}

Aside from the synthesizable aspects of SystemVerilog, Verik also supports verification features such as coverage and assertions. Here we perform assertions on signals to the memory to check that the appropriate control signals are being asserted or deasserted.

if (resetn && !trap) {
if (mem_do_prefetch || mem_do_rinst || mem_do_rdata)
assert(!mem_do_wdata)
if (mem_do_prefetch || mem_do_rinst)
assert(!mem_do_rdata)
if (mem_do_rdata)
assert(!mem_do_prefetch && !mem_do_rinst)
if (mem_do_wdata)
assert(!(mem_do_prefetch || mem_do_rinst || mem_do_rdata))
if (mem_state == u("2'd2") || mem_state == u("2'd3"))
assert(mem_valid || mem_do_prefetch)
}

The PicoRV32 core is parameterized. We declare optionally instantiated modules with the optional function. Here we instantiate a fast multiplier based on the compile time constant ENABLE_FAST_MUL.

@Make
val pcpi_fast_mul = optional(ENABLE_FAST_MUL) {
RV32PcpiFastMul(
clk = clk,
resetn = resetn,
pcpi_valid = pcpi_valid,
pcpi_insn = pcpi_insn,
pcpi_rs1 = pcpi_rs1,
pcpi_rs2 = pcpi_rs2,
pcpi_wr = pcpi_mul_wr,
pcpi_rd = pcpi_mul_rd,
pcpi_wait = pcpi_mul_wait,
pcpi_ready = pcpi_mul_ready
)
}

To demonstrate the design, we execute the following assembly sequence. It repeatedly loads, increments, and stores a value stored in memory.

<start>:
3fc00093 li x1, 1020
0000a023 sw x0, 0(x1)
<loop>:
0000a103 lw x2, 0(x1)
00110113 addi x2, x2, 1
0020a023 sw x2, 0(x1)
ff5ff06f j <loop>

A dump of the simulated waveform is shown below. We can see that it is accessing the expected instructions and data from memory.

riscv-wave

The source code for this project can be found here. The reference PicoRV32 implementation can be found here.