Skip to main content
Blog

Defining RISC-V CPUs in Renode simulation with custom instructions and extensions

By July 31, 2024No Comments

By: Antmicro

The openness and customizability of the RISC-V ISA has encouraged its use across a variety of scenarios, such as supporting cores in larger systems, standalone embedded MCUs and even many-core server AI processing solutions. With first-class RISC-V support and advanced co-simulation capabilities, Renode, Antmicro’s open source simulator, is helping silicon, firmware and software teams be more productive in all of those scenarios.

In this blog article, Antmicro discusses how to simulate complex RISC-V CPUs in Renode, which provides broad coverage for the customization capabilities of the various RISC-V instruction sets in use across the industry today. Renode, like the RISC-V ISA, is extremely modular, allowing users to pick specific architecture variants for their particular use cases, and define arbitrarily complex heterogeneous multi-core CPU clusters including ones that utilize different RISC-V instruction sets or mix architectures (such as RISC-V + Arm). 

A RISC-V based board alongside a configuration file for Renode Defining-RISC-V -CPUs

To demonstrate Renode’s support for RISC-V, Antmicro also reviews the description of the popular HiFive Unmatched. They also present various methods of implementing custom instructions and CSRs, ranging from Python one-liners to direct-from-RTL definitions for co-simulation alongside other Renode capabilities introduced and used by Antmicro for helping customers develop custom SoCs as part of their commercial services.

Defining a RISC-V CPU in Renode

In Renode, RISC-V CPUs can be defined in a [REPL (REnode PLatform) file which lets you pick a specific ISA variant for your CPU core and customize it further according to your needs. 

Below, you can learn how to define a RISC-V CPU following the example of the popular, multi-core RISC-V platform from SiFive, HiFive Unmatched and the FU740 SoC.

Since the RISC-V spec allows for different bit widths, we need to decide whether the CPU core will be 32 or 64-bit when defining the CPU core in the REPL file. For the purpose of this example, we will define a 64-bit CPU core, but users can replace all 64s with 32s without requiring any additional changes to the configuration to get a 32-bit core instead. 

First, add the following line to the REPL file created for our first CPU core, named `s7`:

```

s7: CPU.RiscV64 @ sysbus

```

At this point we have to make a second decision – the ISA extensions that our CPU core should support. Here, we’ll keep things rather simple and add support for integer multiplication and division, atomic operations, as well as compressed instructions. In addition, our core will support the control and status register (CSR) instructions and the `FENCE.I` instruction:

```

s7: CPU.RiscV64 @ sysbus

  cpuType: "rv64imac_zicsr_zifencei"

```

Technically, this is all we need to have a functional RISC-V CPU in Renode!

The FU740 platform file defines a few more details:

```

s7: CPU.RiscV64 @ sysbus

    cpuType: "rv64imac_zicsr_zifencei"

    hartId: 0

    privilegeArchitecture: PrivilegeArchitecture.Priv1_10

    timeProvider: clint

```

The `hartId` parameter specifies the ID of this particular hart (useful for multi-core setups). The privileged architecture specification version can be provided as well: by default Renode uses `Priv1_11`, but `Priv1_09` and `Priv1_10` are also available (and `Priv1_12` is coming soon!).

The last parameter, `timeProvider`, specifies the source of time for the `MTIME` register – here we use the CLINT interrupt controller which is a safe bet for many standard RISC-V platforms.

To find out about more CPU configuration setups covering the selection of RISC-V extensions, check out the documentation.

Multi-core and heterogenous Renode setups

Renode is not limited to simulating a single core. Multi-core setups are also possible, including heterogeneous ones that encompass the emerging blend of Arm CPUs with RISC-V coprocessors. Mixing AMP and SMP architectures is easy to perform within Renode – all you need to do is add more CPU definitions to your REPL file. For example, if your platform consists of a dual-core ARM chip and a RISC-V chip, your REPL could contain:

```

cpu_arm0: CPU.ARMv7A @ sysbus

    cpuType: "cortex-a9"

    genericInterruptController: gic

cpu_arm1: CPU.ARMv7A @ sysbus

    cpuType: "cortex-a9"

    genericInterruptController: gic

cpu_rv: CPU.RiscV32 @ sysbus

    cpuType: "rv32imac_zicsr_zifencei"

    privilegeArchitecture: PrivilegeArchitecture.Priv1_10

    timeProvider: clint

```

HiFive Unmatched, which has four identical SMP RISC-V cores, defines them as follows:

```

u74_1: CPU.RiscV64 @ sysbus

    cpuType: "rv64gc_zicsr_zifencei"

    hartId: 1

    privilegeArchitecture: PrivilegeArchitecture.Priv1_10

    timeProvider: clint

u74_2: [...]

u74_3: [...]

u74_4: CPU.RiscV64 @ sysbus

    cpuType: "rv64gc_zicsr_zifencei"

    hartId: 4

    privilegeArchitecture: PrivilegeArchitecture.Priv1_10

    timeProvider: clint

```

You will notice that U74 cores are very similar to S7 cores – they only differ in the `hartId` value and the instruction set: they declare the “g” set, which is an alias to “imafd” – F and D are responsible for enabling, respectively, single and double precision floating point operations. 

Naturally, a complete platform is more than just a set of CPU cores – but Renode comes with support for a range of peripherals, allowing you to run the exact same software you’d typically run on hardware.

You can explore a demo that runs a Zephyr sample with SD card support included as part of the Zephyr filesystem, accessible in Renode by using: 

```

i @scripts/single-node/hifive_unmatched_sdcard.resc

```

You can also visit the Renopedia page of the HiFive Unmatched board to find more software examples of common Zephyr and U-Boot-based demos, with execution traces and UART output recordings, like this one created for the Dining Philosophers demo below:

The Dining Philosophers demo

Custom instructions, extensions, CSRs and co-simulation in Renode

In Renode, CPUs are not limited to those that use the base RISC-V ISA or those that take advantage of ratified extended instruction sets. In fact, CPUs can be extended to use non-standard instructions by using Python or C# hooks, or by even modifying the base ISA itself, e.g. to mimic existing hardware bugs or vendor-specific changes. Users might also choose to create their own custom extension, such as for interacting with purpose-built hardware or for assisting with enhancing performance of their specific computation-heavy workloads. overcoming various performance bottlenecks. 

An example of how a custom extension was created and implemented in RISC-V is the Andes AndeStar V5, also supported in Renode. In fact, in Renode, you can easily define an entire group of instructions / custom CSRs and combine them into a single named extension that can be used in `cpuType`, making it even easier to use and remove custom extensions – in this case, to use the Andes AndeStar V5 extension, you can define it as follows:

```

    cpuType: "rv32imac_xandes"

``` 

Users can also create their own scripts for creating a custom extension, as shown below on an example of a custom “add” implementation that adds values stored in two registers (encoded in the instruction opcode) and stores the result in its internal state (for example to be further used by another instruction):

```

    sysbus cpu InstallCustomInstructionHandlerFromString "1011001110001111bbbb11111000aaaa"

    """

        src_reg_a = instruction & 0xF

        src_reg_b = (instruction >> 12) & 0xF

        res = cpu.GetRegisterUnsafe(src_reg_a).RawValue + cpu.GetRegisterUnsafe(src_reg_b).RawValue

        state['res'] = res 

    """

```

The “1011001110001111bbbb11111000aaaa” part of the command indicates the pattern the opcode must have to be interpreted as our custom instruction. The “a” and “b” parts indicate where opcode arguments are encoded.

If you don’t want to inline your Python instruction implementation, you can also load it from a file using `sysbus cpu InstallCustomInstructionHandlerFromFile “1011…” @path/to/implementation.py`.

A very similar approach can be used to register custom CSR registers. Registers can be read or written to, so their implementation needs some conditional logic. A very trivial CSR that prints out a message when it’s accessed can look like this:

```

sysbus.cpu RegisterCSRHandlerFromString 0xF0D

"""

if request.isRead:

    cpu.DebugLog('CSR read!')

elif request.isWrite:

    cpu.DebugLog('CSR written: {}!'.format(hex(request.value)))

"""

```

0xF0D is the number of the CSR (it can even override the existing CSRs, if needed), and the implementation should be self-explanatory. Here you can also use the `RegisterCSRHandlerFromFile` option to keep your implementation in a dedicated Python script.

More information about the API of custom instructions and custom CSRs can be found in the Renode documentation. 

The approaches presented above allow you to program custom instructions’ behavior in C# and Python, but it’s also possible to leverage Renode co-simulation capabilities to easier support pre-silicon or soft-core use cases. These allow users to connect their HDL implementations directly, without the need to rewrite your Renode models every time you change the logic. A great example of this approach, aimed specifically at custom edge AI accelerators, is Renode’s support for Custom Function Units (CFUs), which can be used within an FPGA that can be reprogrammed according to the current task being performed. In Renode, it is possible to install your custom CFUs and attach them to simulated RISC-V CPUs. 

To develop your own CFU, it is possible to design them in the CFU Playground by Google, which allows you to build your own ML accelerator in Verilog. To illustrate this, Renode provides a sample CFU accelerating the MobileNetv2 convolutional neural network that can be used in cooperation with a RISC-V CPU.

To learn more about Custom Function Units support, visit the Renode documentation.

Commercial, large-scale and pre-launch support of RISC-V devices and software

Renode’s significant use case is in demonstrated large-scale projects where the emphasis is on the reliability and repeatability of various test scenarios that utilize a range of commercially available RISC-V based boards, for instance in the case of their ports in Zephyr RTOS, which Antmicro helps maintain.

At the same time, it is also used to support various hardware devices before commercial launch, such as was the case with Microsemi’s PolarFire SoC and the Icicle Kit platform, also available in Renodepedia. Enabling pre-silicon software development in cooperation with Microchip lets their customers work on their software before the final tapeout process, allowing them to plan ahead without requiring the physical hardware in their hands.

Renode also plays an integral role in projects like Google’s Open Se Cura which provides a low-power secure embedded platform for ambient ML applications. Renode is at the heart of the simulation-driven co-development methodology adopted by Open Se Cura, providing a well-tested development and simulation platform for this reference design for practical real life AI-based solutions that utilize RISC-V co-processors and accelerators.

The European Union’s TRISTAN project, focusing on open and reusable IP and tooling for RISC-V software and hardware development, uses Renode to support these efforts with specific ISA extensions, prototyping, co-simulation and other user-facing features, like code coverage reporting

With its modularity, Renode enables project partners to use a unified workflow in very diverse scenarios, e.g. on multiple hardware platforms, providing precise information about the actually executed fragments of code to generate a report with the number of executions for each line. With comprehensive code coverage, Renode allows users to examine all of the executed CPU instructions during testing, offering a method for linking a particular execution trace to an individual (or multiple) line of code.

Quickly testing and reiterating RISC-V CPU configurations using Renode 

With Renode’s configurability, Antmicro provides all the building blocks necessary to support both basic RISC-V MCU-style CPUs like Microchip’s Mi-V, configurable and secure OpenTitan-based platforms, as well as powerful, high-performance multi-core clusters like the SiFive P550.

By using Renode, developers can cut down on development time by quickly testing and iterating RISC-V CPU configurations for already existing software, incorporating them in automated testing processes. To discover more about how Renode can enhance the development of your next RISC-V based project, including supporting complex multi-architecture and multi-core scenarios, get in touch with Antmicro at contact@antmicro.com.