RudolV by bobbl
RISC-V processor for real-time systems. 32 bit in-order pipeline with 5 stages.
- Multiple target FPGAs
- Predictable timing model
- Security extension
- CSR extension interface
- Simulation with Icarus Verilog or Verilator
- Dhrystone: Dhrystone MIPS/MHz: 0.736 ... 1.815 (depending on Dhrystone implementation)
- CoreMark: 1.295 per MHz
Structure of Repository
|scripts/||Scripts for synthesis and simulation|
|scripts/sim/||Simulation with Icarus Verilog or Verilator|
|scripts/icestorm/||Synthesis with ICEStorm for Lattice FPGAs|
|scripts/libero/||Synthesis with Libero for Microsemi FPGAs|
|scripts/quartus/||Synthesis with Quartus for Intel FPGAs|
|scripts/vivado/||Synthesis with Vivado for Xilinx FPGAs|
|src/||CSR extensions and FPGA-specific verilog code|
|sw/||Software that can be executed on RudolV|
|sw/compliance/||RISC-V compliance tests|
|sw/coremark/||EEMBC CoreMark benchmark|
|sw/dhrystone/||Multiple implementations of the Dhrystone benchmark|
|sw/uart/||UART driver and bootloader|
|FPGA||Cyclone IV||iCE40 UP5K||IGLOO2 M2GL025||Kinex-7|
|Board||DE2-115||UltraPlus MDP||Future Creative||Genesys-2|
- 64 KiByte RAM (Microsemi only 56 KiByte)
- UART with 115200 Bit/s
- 32 bit timer and interrupt
- Bootloader waits for an executable image via UART
For FPGA support change to the manufacturer-specific directory and run the make script
cd scripts/TOOLNAME ./make.sh
to display the make targets. They differ from FPGA to FPGA, but all have in common, that the first argument must be the board name. To do the synthesis, use
./make.sh BOARD synth
On some boards, a separate place and route pass is necessary:
./make.sh BOARD pnr
Programming the device is done with
./make.sh BOARD prog
Programming (and UART communication) requires some access rights that can be
granted by udev rules. The udev rules for all supported boards can be found in
Simply copy this file to
After programming or pushing the reset button, RudolV waits for the binary application image to be transfered via the UART. The format is simple: first send the length of the image (in bytes) as decimal ASCII string. Followed by a blank (0x20) and then the binary data. The following script can be used:
sw/bootloader/send_elf.sh /dev/ttyUSB0 IMAGE.elf
Use a terminal emulator like picocom at 115200 baud to communicate with the program:
picocom -b 115200 --imap lfcrlf /dev/ttyUSB0
The architecture avoids speculative components to provide a predictable timing as required by hard real-time systems.
Data hazards are avoided by operand forwarding, therefore most instructions are executed in one cycle. Since the memory is single ported, memory accesses take two cycles. The jump target is computed in the execute stage, resulting in a two cycle latency. There is no dynamic branch prediction but subsequent instructions are only killed in the case of a taken branch. This behaviour can be considered as a static always not taken prediction.
|RV32M||MUL, MULH, MULHSU, MULHU, DIV, DIVU, REM, REMU||34|
|unconditional jump||JAL, JALR||3|
|taken branch||BEQ, BNE, BLT, BGE, BLTU, BGEU||3|
|CSR access||CSRRW, CSRRS, CSRRC||2|
|memory access||LB, LH, LW, LBU, LHU, SB, SH, SW||2|
|not taken branch||BEQ, BNE, BLT, BGE, BLTU, BGEU||1|
|barrel shifter||SLL, SRL, SRA||1|
|arithmetic||ADD, SUB, SLT, SLTU, XOR, OR, AND, LUI, AUIPC||1|
The objective of the contest was to thwart 5 particular attacks of RIPE, the Runtime Intrusion Prevention Evaluator. RudolV can detect all these 5 attacks and responds with an exception. No compiler modifications are necessary.
Grubby attack detection
RudolV monitors if a memory location was written as a whole 32 bit word or if narrow byte accesses modified it. Memory locations that were not written as a whole are called grubby. If an instruction is fetched from such a grubby memory location, an exception (14) is thrown. If the value from a grubby memory location is used as function pointer or return address, exception 10 is throw.
Each memory word requires an additional grubby bit. The bit is cleared, if a
sw instruction writes the word. The other store instructions (
sb) set the grubby bit. If a word with grubby bit set is fetched, execution
continues at the address
mcause is set to 14.
The detection of an indirect jump to an address from a grubby memory location is
a bit more complex. There is no single instruction to jump to the address in a
memory location. At least two instructions are involved instead. First,
reads the target address from memory to a register and then
jalr jumps to the
address in the register. Therefore, all registers also need a grubby bit. It is
written by a load and tested by a jump.
The hardware costs are not very high. The logic is simple (only a few dozend LUT4s) and most FPGAs provide block RAMs with parity bits that are 36 bits wide, not 32 bits. These parity bits can be used as grubby bits. Only the Lattice FPGAs do not provide a parity bit.
If available on the board, a switch (DE2-115, Genesys-2) or a button (IGLOO2) can disable the grubby detection.
All buffer overflows that write the memory byte by byte can be detected with the
grubby mechanism. RIPE tests 850 different stack overflow attacks, subdivided by
five dimensions. Four dimensions (location, target code pointer, overflow technique
and attack code) are irrelevat for the grubby detection, the only dimension that
has an influence is the abused function. Grubby detection only works for functions
that write the memory byte by byte. Fortunately, 9 of the 10 functions use
to write to memory (at least in the glibc implementation). Only
on a word basis and is resistant to the grubby detection.
memcpy() by a byte by byte implementation is not an option, because
memcpy() may be used for legal copies of function pointers that may lead to false
positives. So far, no other false positives are known, as long as no dirty hacks
are used in the source code.
Comparison with Pulserain Rattlesnake
The grubby detection is very similar to the Dirty Address Trace (DAT) technique used by Pulserain Rattlesnake. It also needs an additional bit per memory word and register. The dirty bit is set, if more than 8 consecutive writes to memory are detected.
DAT also covers word by word like
memcpy() attacks, but it is vulnerable to attacks
with short consecutive writes and contexts switches during a write sequence.
I may be biased, but in my opinion, the DAT and grubby detection rate are roughly
comparable. However, the hardware costs of the grubby detection are lower.
CSR Extension Interface
Extra CSRs can be added with the follwing interface:
input clk input read input [2:0] modify input [31:0] wdata input [11:0] addr output [31:0] rdata output valid
addr is valid one cycle earlier that the other signals. Thus, address decoding
is separated from the actual read or write action.
In the following cycle,
read is set if the CSR value should be read. If a valid
register was selected by
addr in the previous cycle,
valid should be set to
rdata to the value of the register. Both signals are only asserted for
one cycle, otherwise both are cleared to 0.
modify defines, how the current CSR value should be combined with
form the new value.
This interface can also be used to connect external peripherals. Its advantage over a memory mapped interface is less impact on the critcal path and thus a potentially higher clock rate.
Examples of CSR extensions can be found in src/csr.v:
|verilog module||no||CSRs / description|
|CsrUartBitbang||07C0||Minimal UART interface for software bitbanging|
|CsrUartChar||0BC0||Read and write bytes via UART|
|CsrPinsIn||0FC1||Read external pins (e.g. buttons or switches)|
|CsrPinsOut||0BC1||Write external pins (e.g. LEDs)|
|CsrTimerAdd||0BC2||Simple timer that asserts the timer interupt|
For simulation use the make script in scripts/sim/:
cd scripts/sim ./make.sh
will display the usage of the script.
Icarus Verilog and Verilator are supported and any .elf file can be executed with optional debug output and .vcd generation.
RISC-V Compliance Tests
Meanwhile the RISC-V compliance tests include most of the older riscv-tests, but some tests for multiplication and division are missing. Therefore these missing tests and some RudolV-specific tests were added. They can be found under sw/tests/src/.
Build the compliance tests:
make -C sw/compliance/
Build the remaining riscv-tests and rudolv-tests:
make -C sw/tests/
Run all tests with Icarus:
cd scripts/sim ./make.sh icarus tests
Simulate only a single test with debug output:
cd scripts/sim ./make.sh icarus debug ../../sw/tests/build/rudolv_irq.elf
RudolV supports Zephyr with an out-of-tree board description. This means that a plain, unpatched installation of Zephyr can be used to build an .elf file for RudolV. To install Zephyr follow the instructions on https://docs.zephyrproject.org/latest/getting_started/index.html, particularly topics 2 (dependencies) and 5 (SDK). Topics 3 (source code) and 4 (python dependencies) are integrated in RudolV's make script:
cd sw/zephyr ./make.sh zephyr
Technically, a complete Zephyr port consists of board, DeviceTree, SoC and driver definitions. However, as of Zephyr 2.2.0, out-of-tree SoC definitions and drivers do not yet work properly. Therefore, all RudolV customisations are packed into the board definition at sw/zephyr/board/.
For example, tu build the hello_word sample, type
cd sw/zephyr west build -b rudolv ./zephyrproject/zephyr/samples/hello_world -- -DBOARD_ROOT=.
or use the make script:
./make.sh elf ./zephyrproject/zephyr/samples/hello_world
Dhrystone is widely used, but it depends to such an extend on the compiler settings and libc implementation that it is hardly comparable. Here are 3 different implementations from repositories of other RISC-V cores. Build them with
cd sw/dhrystone ./make.sh all
And send the images in
sw/dhrystone/build/ to the bootloader. Or run it with
cd scripts/icarus ./make.sh run ../../sw/dhrystone/build/riscv.elf ./make.sh run ../../sw/dhrystone/build/picorv32.elf ./make.sh run ../../sw/dhrystone/build/scr1.elf
|Dhrystone source||reference core||Fmax||cycles/iteration||RudolV c/i||DMIPS/MHz||RudolV D/M|
EEMBC CoreMark Benchmark
Run one iteration of EEMBC CoreMark with Icarus Verilog:
cd sw/coremark/ ./make.sh icarus
The results can be found in
sw/coremark/coremark/run1.log. Since this is a
simulation, the error message can be ignored and the computed iterations/sec
corresponds to CoreMark/MHz.
To get the CoreMark of an FPGA implementation, build an ELF image with UART output that executes 1000 iterations and send it to the bootloader:
cd sw/coremark/ ./make.sh elf 1000 ../bootloader/send_elf.sh /dev/ttyUSB1 build/coremark_iter_1000.elf
CoreMark requires the benchmark to run for at least 10 seconds. If RudolV is too fast, build an ELF file image with more iterations and send this one to the bootloader.
The CoreMark/MHz of RudolV is 1.295 and 0.892 without bitwise multiplier and divider.
Licensed under the ISC licence (similar to the MIT/Expat license).
Copyright (c) 2018-2019 Jörg Mische <firstname.lastname@example.org> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.