# Punxa: A Python-Based RISC-V System Simulator for Education

David Castells-Rufas, David Novo, Xavier Martorell

► To cite this version:

David Castells-Rufas, David Novo, Xavier Martorell. Punxa: A Python-Based RISC-V System Simulator for Education. Electronics Letters, 2025, 61 (1), pp.e70300. 10.1049/ell2.70300. lirmm-05406158

HAL Id: lirmm-05406158

https://hal-lirmm.ccsd.cnrs.fr/lirmm-05406158v1

Submitted on 9 Dec 2025

HAL is a multi-disciplinary open access
archive for the deposit and dissemination of sci-
entific research documents, whether they are pub-
lished or not. The documents may come from
teaching and research institutions in France or
abroad, or from public or private research centers.

L'archive ouverte pluridisciplinaire HAL, est
destinée au dépôt et à la diffusion de documents
scientifiques de niveau recherche, publiés ou non,
émanant des établissements d'enseignement et de
recherche français ou étrangers, des laboratoires
publics ou privés.

<figure><img src="image_1.png" /></figure>

Distributed under a Creative Commons Attribution 4.0 International License
# Punxa: a Python-based RISC-V System Simulator for Education

David Castells-Rufas,<fnref n="1" />,<fnref n="3" /> ⚫ David Novo,<fnref n="2" /> ⚫ and Xavier Martorell<fnref n="3" /> ⚫

david.castells@uab.cat

The analysis and verification of the integration of hardware and software components is challenging. Traditional HDL simulators, which are often slow and provide low-level analysis tools, are not always effective for simulating the software of processor-based systems. This work presents an interactive co-simulation framework that enhances students' understanding of computer architecture concepts typically obscured by hardware intricacies. Our framework facilitates a unified environment for iterative design, enabling detailed analysis and generation of reports and traces to identify system performance, bottlenecks, and find implementation errors. By prioritizing analysis features over performance, our framework serves as a valuable tool for educational purposes and comprehensive examination of the interactions between hardware and software.

**Introduction:** Modern integrated circuits (ICs) have grown increasingly complex, integrating multiple processors and numerous intellectual property (IP) blocks. This complexity not only raises production costs but also demands rigorous verification to minimize design errors. A critical challenge lies in ensuring seamless hardware-software interaction, as even blocks functioning in isolation may fail when combined in a complete system. Traditional hardware description language (HDL) simulations often prove too slow for efficient software verification. This limitation makes alternative approaches using higher-level abstractions particularly valuable for various applications including testing software with new hardware, validating cross-compiled code, and understanding system integration challenges. In education, students frequently encounter the effects of architectural concepts without gaining insight into their underlying causes, as hardware implementation details typically remain hidden. Existing RISC-V co-simulation frameworks tend to be overly complex for effective use in undergraduate courses, leaving students unable to properly observe and understand the critical relationships between hardware design decisions and their software consequences. To address these limitations, we have developed an educational framework that combines cycle-accurate simulation with interactive analysis tools. Departing from conventional approaches that emphasize performance optimization, our system prioritizes observability and pedagogical effectiveness. The framework provides real-time execution traces and customizable reports, making architectural concepts concrete and accessible while maintaining flexibility for student experimentation and extension.

**Related work:** Different levels of abstraction are typically used to avoid the simulation of the complete RTL code of a processor-based system using event-driven simulation. These levels vary in timing accuracy, ranging from cycle-accurate simulations to transaction-level and functional-level simulations, and represent different trade-offs between simulation speed and timing accuracy. Some web-based simulators, such as BRISC-V [1] and WebRISC-V [2], are designed primarily for education use. They focus on visualizing the execution of small code snippets and analyzing pipeline behavior. However, these tools lack support for integrating custom hardware components, limiting their use for hardware/software co-design. QEMU [3] offers a robust solution for simulating RISC-V platforms, operating almost at the clock speed of the host system. It provides a flexible environment for running RISC-V binaries using dynamic binary translation to accelerate execution on a host computer. Additionally, QEMU can interface with gdb for step-by-step debugging and analysis. However, despite its use in some co-simulation environments, QEMU is not easily extensible or adaptable for cosim-

ulation, and the complexity of its large codebase can pose challenges for students. Gem5 [4] is another popular simulator used to do computer architecture analysis. It supports RV64GC co-simulation with SystemC (SC) blocks. However, the complexity of the configuration and customization of the framework might be too high for bachelor students.

Spike [5], maintained by RISC-V International, is one of the most widely used RISC-V instruction set simulators (ISS). It provides a fast functional model of the RISC-V architecture, implemented in C++, and supports all standard RISC-V instruction set extensions. However, its utility for hardware integration is limited due to several factors. While some extensions exist for peripheral co-simulation, Spike lacks direct support for simulating peripherals, making it difficult to model hardware interactions such as memory-mapped I/O, interrupts, and custom accelerators. Additionally, Spike does not provide cycle-accurate execution, meaning it cannot be used to analyze hardware timing behavior, cache effects, or bus contention. Another significant limitation is its lack of native support for heterogeneous or multi-core environments with shared peripherals. Integrating Spike with hardware-based platforms, such as FPGA prototypes or RTL simulators, requires additional interfaces or co-simulation frameworks, which are not readily available out of the box. While there are ongoing efforts to use Spike for co-simulation [6, 7], its primary role remains software development and functional validation.

Dromajo [8], designed by Esperanto Technologies for RTL co-simulation, is a fast and accurate RV64GC emulator. It uses functional models to co-simulate RISC-V cores with other hardware blocks, such as memories, peripherals, and interconnects designed in Verilog/SystemVerilog. Dromajo can run unmodified Linux applications by intercepting system calls, similar to Spike. Additionally, it supports checkpointing and restart, allowing users to save and later restore the emulator's state. This feature is particularly beneficial for education, as it enables students to start from a prepared state, saving time on preparatory cycles.

Previous models provide varying degrees of timing accuracy. TLM allows for more abstract and faster simulations compared to Register Transfer Level (RTL) by enabling system components to communicate through channels using transactions. Timing details can be annotated in both the components and the communication channels. One of the first RISC-V ISS based on TLM is described in [9]. Recently, RISC-V VP++ [10] has provided a Virtual Prototype platform based on TLM that can boot Linux and support graphics. TLM is particularly useful for early modeling and can be employed in various scenarios to gain a deeper understanding of the underlying hardware. In our approach, we emphasize scenarios where students need a thorough understanding of cycle-accurate and bit-accurate (CABA) simulation. We leverage cycle-based simulation to create a co-simulation environment for testing and validating hardware aspects of RISC-V 64-bit processors. Table 1 summarizes the most relevant related systems from the state of the art.

<table><caption>Table 1. Simulation platforms for RISC-V processors</caption><thead><tr><th>Tool</th><th>RV64GC</th><th>Speed</th><th>Cosim.</th><th>Checkp.</th><th>Distribution</th></tr></thead><tbody><tr><td>QEmu [3]</td><td>✓</td><td>GHz</td><td>×</td><td>✓</td><td>GitLab</td></tr><tr><td>Gem5 [4]</td><td>✓</td><td>kHz - MHz</td><td>SC</td><td>✓</td><td>GitHub</td></tr><tr><td>SC TLM2 [9]</td><td>×</td><td>MHz</td><td>SC</td><td>×</td><td>GitHub</td></tr><tr><td>Spike [5]</td><td>✓</td><td>MHz</td><td>×</td><td>×</td><td>GitHub</td></tr><tr><td>Dromajo[8]</td><td>✓</td><td>MHz</td><td>Verilog/SV</td><td>✓</td><td>GitHub</td></tr><tr><td>RISC-V VP++ [10]</td><td>✓</td><td>MHz</td><td>SC</td><td>×</td><td>GitHub</td></tr><tr><td>Punxa (this work)</td><td>✓</td><td>kHz</td><td>py4hw</td><td>✓</td><td>Pypi GitHub</td></tr></tbody></table>

Validating software compiled for Linux on a RISC-V processor requires ensuring correct execution of various instruction set extensions, privilege modes, CSRs, interrupts, and interactions with peripherals. However, fully booting Linux in a simulation environment introduces significant overhead, making it impractical for rapid testing of cross-compiled applications. The complexity of simulating the entire OS,

<footnote n="1">¹Department of Microelectronics & Electronic Systems, Universitat Autònoma de Barcelona</footnote>

<footnote n="2">²Laboratoire d'Informatique, de Robotique et de Microélectronique de Montpellier (LIRMM)</footnote>

<footnote n="3">³Barcelona Supercomputing Center (BSC)</footnote>

<footnote n="1">¹ Not natively supported, but some options exist to interface with cycle-accurate and TLM simulators.</footnote>
<figure><img src="image_2.png" /><figcaption>**Fig 1** Logic diagram of the elements of a basic single-cycle RISC-V system. Colored elements represent the most important data that define the state of system.</figcaption></figure>

along with its hardware dependencies, can drastically slow down execution. To address this, simulators can bypass the overhead of executing system calls by intercepting them and directly providing the expected functionality. Modern operating systems like Linux rely on system calls as the primary interface between applications and the OS. Instead of simulating the entire OS, Spike can use a proxy kernel that intercepts ECALL instructions, allowing applications to execute in a simulated environment without the need for a full Linux boot. This approach enables faster validation while still ensuring compatibility with Linux-targeted software. We can follow a similar strategy to speed-up some simulations.

**RISC-V System Models:** Extending from our previous work [11] in which we implemented a system based on a Von Neumann RV64GC processor model with an ideal single-cycle execution, we implemented another RISC-V processor to be used by first-year students to understand the design strategy of dividing a circuit into a simple data path (DP) and a hardwired control unit (CU) that uses several cycles to execute an instruction. We also introduced the support for RV32GC, ending up with 4 different processor models. We use the *py4hw* [12] framework, which provides a cycle-based simulation framework, a library of circuits, and interactive simulation tools. Unlike many Python-based HDL frameworks, *py4hw* offers a clear separation of design styles: structural, combinational, sequential, and algorithmic. For our single-cycle processor, we describe the processor using the algorithmic design style. Python objects maintain the state of the circuit through member variables, such as the processor register files. The algorithmic modeling design style uses Python coroutines (like `cocotb` [13]). In this style, code executed between consecutive `yield` statements considered to occur within the same cycle. The processor functional model performs memory transactions to fetch instructions and access and store data. The internal state of the processor is composed by the register file, the CSRs, and other convenient data structures to make execution smoother. We structure the code in several functions to manage complexity, hide details, make the code more readable and to reuse some functionality, like performing memory transactions. As depicted in Figure 1, besides the processor, a basic system includes other elements (PLIC, the CLINT, an UART, and Memory) that are behaviourally modeled using a sequential design style. As depicted in Figure 2, for the multi-cycle processor, we design the DP using a structural design style and an algorithmic design style for the CU.

We can expect to model systems using large amounts of memory. This is challenging because we could exhaust the host memory if we allocate the amount of memory required by the modeled system. Instead, we design a SparseMemory circuit that models a large memory but allocates subset regions as needed. For instance, even SparseMemory can cover a $2^n$ address space, when a program is loaded it only allocates the memory needed to put the application in memory. One of the elements that is also maintained as information of the processor object is a table of symbols that associates memory addresses with program (or system) symbols. This is useful to display function names instead of memory locations in a number of cases, like when debugging step by step and jumping to an address, or when showing the contents of the stack trace.

<figure><img src="image_3.png" /><figcaption>**Fig 2** Logic diagram of the elements of a RISC-V system based on a multiple-cycle Von Neumann architecture with a hardwired control unit. Colored elements represent the most important data that define the state of system. Note that register files and CSRs are placed in the external memory.</figcaption></figure>

We capture entry and exit of functions by analyzing the execution of JAL, JALR, C.JR, and C.JALR instructions. We support virtual memory by translating virtual memory locations to physical memory locations as specified by the corresponding CSR registers. This requires a page walker, which we implement in software with zero target time. To optimize performance, we cache recent translations in a translation lookaside buffer (TLB), reducing the need for page walking on every memory access. One of our objectives is to have minimal setup requirements, making the system easy to distribute for educational purposes. The code is available at https://github.com/davidcastells/punxa and can be installed via *pypi*, allowing to get a system running in few clicks. It can also be run on Jupyter notebooks and online platforms like Collab and Binder further simplifying access and use.

Having functional models of the hardware elements we want to simulate is a first step to analyze the system's behavior, but these models do not provide stimuli on their own. System testbenches must instantiate specific combinations of the provided elements and deliver the required stimuli to the system. For processors, this typically involves loading an executable file or binary distribution into memory. In our testbenches, we include a function called *prepare* that creates all the necessary hardware instantiations, including the basic peripherals. The *py4hw* framework's interactive simulation workbench allows users to visualize the system's hierarchy, analyze the schematic of each structural module, provide custom visualization of behavioral circuits, and simulate the system by running the clock. However, we generally use a command line interface to interact with the simulation, leveraging Python's scripting capabilities. Typically, we start testbenches interactively by running `Python -i <testbench>`. This approach allows us to easily create simulator commands by implementing Python functions that interact with the hardware model. Listing 1 shows the default commands available for debugging and analyzing the system.

**Extending the System:** Functional processor models are not enough for education. Students need to be able to modify them, combine them, and add analysis functions and custom methods to be able to gain a deep insight of the system elements.

Object oriented languages tend to use inheritance to reuse existing code between semantically related classes. *py4hw* uses class inheritance in many classes. All circuits inherit from *Logic* class. Since our single-cycle RISC-V processor is implemented in a Python class with an algorithmic design style we can create customizations of the processor by simply deriving a class and overriding the required functions that we want to modify. We have used this technique to create a processor that intercepts system calls and uses the host features to resolve them, as Spike does with the proxy-kernel.

A distinctive feature of *py4hw* is its ability to allow designers to create models that can be dynamically modified at runtime. This flexibil-
```python
>>> list_commands()
interactive commands:
    run      - run the system a number of cycles
    step     - run an instruction step
    tbreak  - set a temporal breakpoint
    go       - run until the temporal breakpoint
    regs     - display the registers
    reportCSR - display the content of CSRs
    console  - display the content of the console
    stack    - display the stack
    memoryMap - display the memory map
dump      - dump (binary/ascii) memory
pageTables - displays the page tables
write_trace - exports function call traces
loadProgram - load a program (elf) in memory
checkpoint - save the system state in a file
restore   - restore the system state from a file
```

<caption>Listing 1: Default interactive commands available in the Python console</caption>

```python
class SingleCycleRISCVProxyKernel(SingleCycleRISCV):
    ...

def executeIIns(self):
    op = self.decoded_ins
    if (op == 'ECALL'):
        self.execute_syscall()
    else:
        yield from super().executeIIns()
```

<caption>Listing 2: Using class inheritance to create a processor that intercepts system calls</caption>

ity extends to system creation and even during a simulation session. For example, designers can use functions like `addIn` and `addOut` to modify the circuit interface and instantiate and connect additional subcircuits on the fly. Thus, after instantiating the provided single-cycle processor that basically interfaces with memory mapped devices, the designer can add additional ports, and connect them to new instantiated circuits that provide new hardware features that could be accessed by custom instructions. Being a dynamic language, Python offers advanced introspection features that allow objects to be modified by adding, removing, or changing their methods during runtime. Thus, we actually do not require to use inheritance to override a method of a behavioral model. For quick tests, we can directly inject a new function to an object and use it right away. We have used this technique to modify the standard processor, that has a single-cycle latency in the execution of all ISA instructions, to add different latencies to different complex mathematical operations. Listing 3 depicts how this can be done. The code shows how we substitute the method `execute` of the cpu object by the `new_execute` method that adds some dummy cycles to the execution for the target instructions before calling the previous `execute` function.

**Providing additional convenient features:** The ISS still needs to be augmented with several convenient features. The RISC-V toolchain includes a GCC distribution specifically designed to create bare-metal applications, which do not target a specific operating system like Linux. This compiler is referred to as riscv64-unknown-elf-gcc. Although no operating system is expected, the compiler links with a minimized version of the C runtime library, based on newlib. This library assumes the availability of a set of system calls necessary for basic operations, including standard I/O functions. Consequently, compiling a C/C++ application using this method produces an Executable and Linkable Format (ELF) file, which is intended to be executed on an abstract RISC-V processor that supports the required system calls.

A fundamental aspect of executing a bare-metal ELF file involves loading its contents into the appropriate memory positions. ELF binaries consist of multiple sections designated for code and data and also specify the program's entry point. Additionally, they may include symbols that associate readable names with memory locations. To extract information from ELF files, we use the Python package `pyelftools`. This package allows us to obtain the available sections and their stored byte arrays. We take these byte arrays abd store them in the SparseMemory model. The

```python
def patch_cpu():
    def dummy(self, n):
        for i in range(n):
            yield

def new_execute(self):
    ins = self.decoded_ins
    if (ins in ['MUL', 'MULHU', 'MULW']):
        yield from self.dummy_cycle(3)
    elif (ins in ['FADD.D', 'FSUB.D', 'FMADD.D',
                 'FMSUB.D', 'FMUL.D',
                 'FNMSUB.D']):
        yield from self.dummy_cycle(5)
    elif (ins in ['DIV', 'DIVU', 'DIVUW',
                 'DIVW', 'FDIV', 'FSQRT.D',
                 'REM', 'REMU', 'REMW']):
        yield from self.dummy_cycle(20)
    yield from self.old_execute()
```

```python
import types

cpu.old_execute = cpu.execute
cpu.execute = types.MethodType(new_execute, cpu)
cpu.dummy_cycle = types.MethodType(dummy, cpu)
```

<caption>Listing 3: Using function injection to create a processor model with high latency instructions</caption>

SparseMemory model raises an exception if bytes are written to unallocated positions. To prevent such exceptions, we reallocate the memory ranges of the detected sections in the ELF file before copying the data. We also read symbols from the ELF file if they are available. In this case, it is the CPU model (as shown in Figure 1) the entity that maintains the symbol table. This table is actually implemented as a Python dictionary that associates memory locations with symbolic names. After populating the memory and the symbol table, we assign memory regions for the stack and initialize the processor's global pointer `gp`, the stack pointer `sp`, and the program counter `pc` to the application entry point. The final step before executing the application is to push any external application arguments onto the stack. Additionally, we place the different sections as needed.

Checkpoint and restart offer some advantages for learning and experimentation. It allows students to save the state of a simulation at a particular point in time and later resume execution from that exact state. It facilitates efficient debugging and analysis by enabling learners to repeatedly examine specific segments of program execution without restarting from the beginning. Additionally, different system states could be provided to experiment with different scenarios in a controlled and time-efficient manner. The goal of checkpointing is to make the state of the system persistent on a file that can be later recovered. We identify the most important elements to persist from the system state in Figure 1 by using colored boxes. To checkpoint, we persist the state by simply serializing the different elements of the state in a binary file in a given order. To restart, we just desynchronize the elements stored in the file in the same order. We implemented our own serialization functions, although we could adopt a more standard method (like `pickle`) in the future. To save space in big objects like the SparseMemory content we compress the state using the `zlib` module included in the standard library.

Integrating function tracing in a RISC-V simulator significantly enhances debugging, performance analysis, and understanding of program behavior. It allows developers to identify performance bottlenecks, measure function execution durations, and pinpoint bugs, facilitating code optimization. For students and researchers, tracing provides a clear, visual representation of control flow, aiding in the comprehension of complex interactions within the code. Additionally, it supports verification of hardware-software interactions, serves as detailed execution documentation, and helps diagnose concurrency issues in parallel programming. As explained in Section 3, we identify function enter and function exit by analyzing jump-and-link instructions. This is done to maintain a handy stack information of the processor avoiding to walk the stack memory to recover the stack trace. We take profit of the same identification points to save the timestamps of function enter and exit to capture them and store them for later visualization in trace analysis tools like Vampir [14], Paraver [15], or Perfetto [16]. We chose Perfetto trac-
ing because its visualization tool does not require installation; it can be
directly accessed from a web browser. Perfetto traces are stored as JSON
files compressed with the gzip algorithm. To create the compressed trace
files from the collected events list, we utilize several Python packages
including json, gzip, and shutil.

**Results:** We validate our processor model by running the RISC-V ISA tests, and several bare-metal applications. Some of the most complex instructions of the RISC-V ISA to validate are those dealing with floating point instructions. Besides ISA tests we use the *paranoia* benchmark to test the features of the implementation. With *paranoia* we detected 2 bugs in our initial floating point operations implementation that were not detected by RISC-V ISA tests. The first was related to the results of division by zero, and the second about the precision of the square root for small numbers. Since our goal is to use the ISS for education, in the following subsections we try to illustrate scenarios where it your be used to teach different aspects of Hardware and Software integration.

**Use Case: Understanding application loading and the syscall mechanism:** The first application implemented in any language is the "Hello World" application. Students must learn how the different sections of the ELF are loaded into memory and what system calls are called by such a simple application. The analysis of the assembly code generated by `risc64-unknown-elf-objdump` can reveal some details, but it is much simpler to understand their use if the application is executed in the simulator and the different syscalls are being observed as they are called by the application. Listing 4 illustrates the output of such exercise. It can be observed how the application interact with the standard I/O devices (`stdin`, `stdout`, `stderr`, with file descriptors 0,1,2), writes to `stdout` once, and finally exits. The setup to do a similar exercise with QEMU or Spike would be more complex, requiring booting a Linux Image and excuting `strace` or modifying Spike PK source code.

```c
>>> step(10000)
syscall FSTAT fd:0x1 stat:0x9FD88
syscall BRK ptr:0x0 -> brk:0xC0000
syscall BRK ptr:0xC1000 -> brk:0xC1000
syscall WRITE fd:0x1 buf:0xC0010 count:0xD
syscall CLOSE fd:0x0
syscall CLOSE fd:0x1
syscall CLOSE fd:0x2
syscall EXIT
>>>console()
Hello World!
```

**Listing 4:** Execution of `HelloWorld` application to observe system call invocation. The app first check `stdout`, and extends the heap. Then writes to `stdout`, and closes `stdin`, `stdout`, `stderr` before exiting.

**Use Case: Understanding memory use:** In our teaching experience we often observe that computer science engineering and telecom engineering students struggle to grasp how applications utilize memory and distinguish between different memory regions, such as those used for executable code, global data, heap, and stack. We can use the `ISS` to gather pointers to various program variables located in global scope, local scope (stack), and dynamically allocated (heap).

Additionally, we analyze function locations to collect code addresses during runtime. We selected a method based on implementing a syscall called by `ECALL` instruction and passing the pointer, the size, and the name of the variable to the `ISS`. We encapsulate this process in C macro called `TRACEMEM`. A version of the proxy-kernel `ISS` is patched by injecting a `unknown_syscall` method that collects the information in a Python list, that is later processed to automatically generate the memory layout diagram shown in Figure 3 using the Python package ` matplotlib`.

**Use Case: Understanding the interaction with new Hardware and its impact in performance:** The design of custom hardware elements is often explained as a method to increase performance for specific applications. While this idea is easy to grasp, there is often a difference

```c
int ga = 1;
int gb;
#define SI (sizeof(int))

void f(int fa, int* fb) {
    TRACEMEM(&fa, "fa", SI)
    TRACEMEM(&fb, "fb", SI)
}

void last();

int main(int argc, char* args[])
{
    int la=2;
    int* ph = malloc(SI * 2);
    TRACEMEM(&ga, "ga", SI)
    TRACEMEM(&gb, "gb", SI)
    TRACEMEM(&la, "la", SI)
    TRACEMEM(&ph, "ph", SI)
    TRACEMEM(ph, "h", SI+2)
    f(la, ph);
    TRACEMEM(f, "code",
              (int)last - (int)f)
}

void last(){}
```

<figure><img src="image_4.png" /><figcaption>**Fig 3** Memory Layout (right) obtained by executing the application (left) compiled with standard RISC-V toolchain to teach about the different memory regions (text, init, heap, stack) of C programs.</figcaption></figure>

between the expected gains and the observed ones. Often, students do not have enough information to be able to explain the causes of these differences. The required insight to get a solid explanation requires an analysis of the timing details of the interaction between architectural elements at a fine grain. Or even having a picture of the whole application at different levels of detail. An illustrating example is the acceleration of RGB-to-YCbCr color conversion used in JPEG encoding by using custom instructions (CIs). This step in JPEG is simple enough to be implemented in a single instruction substituting some instructions of the original algorithm. We have used all the techniques presented in Section 3 (class inheritance, circuit modification, and function injection) to build a processor model with different latency instructions and custom instructions to implement the RGB-to-YCbCr color conversion. With the proposed framework, the fine-grain behaviour of CI invocation can be easily analyzed on Jupyter Notebooks using the `wavedrom` package to analyze the system during a number of cycles. Generally, instructors can harness the versatility of executing `punxa` in Jupyter Notebooks to effectively teach the intricacies of HW/SW interfaces and easily create self-guided exercises for students. We implemented the JPEG encoder with a command line flag to instruct whether the new CI should be used or not during the execution. Hence, we can acquire traces from two executions of the same binary, one with CI and one without CI. We combine the traces to be shown in a single Perfetto trace.

<figure><img src="image_5.png" /><figcaption>**Fig 4** Perfetto trace comparing the optimization achieved by using custom instructions in a JPEG Encoder to implement RGB to YCbCr conversion.</figcaption></figure>

The results are shown in Figure 4. It is easy to see and measure the performance gains and understand the impact in the whole application execution time.

**Debugging the Linux Boot:** Booting Linux on a new RISC-V processor presents significant challenges due to the complexity of the privilege specification and the numerous interrelated components involved. We have extended our previous work [11] to facilitate this process, we created a Buildroot distribution and analyzed the Linux boot process in detail. This system consists of an OpenSBI bootloader and a Linux kernel using an `initrd` filesystem. Additionally, we developed our own
device tree (dtb) to describe the hardware configuration of the system. This device tree must be loaded into a specific memory location expected by OpenSBI, as it plays a critical role in ensuring proper initialization and communication between the bootloader, the kernel, and the underlying hardware.

To debug the Linux kernel boot process on our RISC-V system, we extracted symbol information of kernel functions using riscv64-unknown-elf-objdump and imported them into the simulator. By combining these symbols with a Linux cross-reference, we can pinpoint necessary function calls during boot. The ability to checkpoint and restart, which saves the system state, greatly reduce time to repeat experiments. We also implemented a convenient pageTables command to show all the page table hierarchy. Using these tools, we discovered a BUG of our implementation related to the SATP CSR. During the analysis of the Linux boot process, we observed that Linux attempts to determine the supported VM mode by testing invalid modes sequentially (from higher to lower address modes) until it finds one that works. This is done by writing the VM mode to the SATP CSR. Upon revisiting the RISC-V Privileged Specification, we found that the latest version states that the processor should silently ignore invalid writes to the SATP CSR instead of raising an illegal instruction exception. This behavior differs from our initial implementation, which was causing the BUG. We also identified a BUG related to timer interrupt handling. To diagnose the issue, we implemented a Python command (listing 5) to simultaneously monitor the status of key hardware elements, such as the counters in the CLINT and the interrupt lines, alongside the values of relevant CSRs (Control and Status Registers).

```python
>>> reportInterrupts()
Interrupt Status
CLINT
    time: 000000000135E358 < 0000000001388D2E : timecmp
    Interrupt 174550 clks away (0.003491 seconds)
CPU CSRs
    time: 000000000135C440
        MIDELEG      MIP      MIE      MSTATUS
    timer [0]      MTIP [0] --> MTIE [1] --> MIE [0]
                \-> STIP [0] --> STIE [1] --> SIE [0]
sw-i [0]--> MSIP [0] --> MSIE [0] --> MIE [0]
    (sw) --> SSIP [0] --> SSIE [0] --> SIE [0]
ext-M [0]--> MEIP [0] --> MEIE [0] --> MIE [0]
ext-S [0]--> SEIP [0] --> SEIE [0] --> SIE [0]
```

Listing 5: Output of the reportInterrupts command during the Linux boot.

The problem stemmed from the supervisor exception handler not correctly managing timer interrupts. After revisiting the RISC-V Privileged Specification, we explored the requirements for supporting STTC (Supervisor Timer Timer Compare), which necessitates modifications to the device tree (DTB) to inform the kernel and the implementation of support for the stimecmp and stimecmp CSRs. Upon further investigation, we discovered an inconsistency between the time-related CSRs and the memory-mapped values in the CLINT. Once we resolved this discrepancy, the system functioned as expected. Another interesting aspect is the need of an entropy source, which Linux relies on for security purposes. Simulated environments often lack the randomness of real systems. In fact, when booting in Spike, Linux issues warnings about insufficient entropy. To address this issue, we created an script to be used when reaching the function *jent_entropy_init*. It adds a number of random cycles to the time counter of CLINT. This is interpreted by Linux as a good source of entropy.

**Conclusions:** The dynamic nature of Python allows a much easier deployment and customization compared to other languages used in other similar frameworks, effectively lowering infrastructure barriers and enabling students to delve deeper into the analysis of architectural aspects. We have developed a highly flexible framework that is well-suited for teaching various aspects of hardware/software integration in RISC-V systems. We hope that the broader community will benefit from the features this framework offers. In future work, we plan to extend the system to include a 5-stage pipeline processor model and develop tools for analyzing and visualizing details of the pipeline.

**Author contributions:** DC: conceptualization; funding acquisition; investigation; resources; software; writing - original draft. DN: methodology; supervision; validation; writing - review & editing. XM: methodology; supervision; validation; writing - review & editing.

**Acknowledgments:** This project is partly funded by the Catalan Government Research groups 2021-SGR-01623, 2021-SGR-01007, by the Spanish Ministry of Science, Innovation and Universities under grants PID2023-1487170B-C22, PID2019-107255GB-C21, PCI2021-121964, and the F3CAS project (ANR-20-CE25-0010) of the French National Research Agency.

© 2025 The Authors. *Electronics Letters* published by John Wiley & Sons Ltd on behalf of The Institution of Engineering and Technology

This is an open access article under the terms of the Creative Commons Attribution License, which permits use, distribution and reproduction in any medium, provided the original work is properly cited.

Received: DD MMMM YYYY Accepted: DD MMMM YYYY
doi: 10.1049/ell.10001

**Conflict of interest statement:** The authors declare no conflict of interest.

**Data availability statement:** The data that support the findings of this study are openly available at https://github.com/davidcastells/punxa.

## References

1. Agrawal, R., et al.: The brisc-v platform: A practical teaching approach for computer architecture. In: Proceedings of the Workshop on Computer Architecture Education, pp. 1–8. (2019)

2. Mariotti, G., Giorgi, R.: Webrisc-v: A 32/64-bit risc-v pipeline simulation tool. SoftwareX 18, 101105 (2022). doi:10.1016/j.softx.2022.101105

3. Bellard, F.: QEMU, a fast and portable dynamic translator. In: USENIX annual technical conference, FREENIX Track, vol. 41, pp. 10–5555. Califor-nia, USA (2005)

4. Lowe-Power, J. el al.: The gem5 simulator: Version 20.0+. arXiv preprint arXiv:2007.03152 (2020)

5. RISC-V: Spike, a RISC-V ISA simulator. [Online]. https://github.com/riscv/riscv-isa-sim (2021)

6. D. Kim et al.: Debugging RISC-V processors with fpga-accelerated rtl simulation in the fpga cloud. In: Appears in the 2nd Workshop on Computer Architecture Research with RISC-V (CARRV’18), (2018)

7. Sans, J., et al.: Open source verification environment for RISC-V. In: RISC-V Summit Europe, Barcelona, Spain (2023)

8. Kabylkas, N., et al.: Effective processor verification with logic fuzzer enhanced co-simulation. In: MICRO-54: 54th Annual IEEE/ACM International Symposium on Microarchitecture, pp. 667–678. (2021)

9. Montón, M.: A RISC-V SystemC-TLM simulator. In: Workshop on Computer Architecture Research with RISC-V (CARRV 2020), (2020)

10. Schlägl, M., Hazott, C., Große, D.: RISC-V VP++: Next generation open-source virtual prototype. In: Workshop on Open-Source Design Automation, (2024)

11. Castells-Rufas, D., Novo, D., Martorell, X.: An educational tool to analyze the hardware/software integration in risc-v systems. In: 2024 39th Conference on Design of Circuits and Integrated Systems (DCIS), pp. 1–6. (2024)

12. Castells-Rufas, D., Rotger, G., Novo, D.: Streamlining FPGA circuit design and verification with python and py4hw. In: XI Southern Programmable Logic Conference, San Luis, Argentina (2023)

13. Rosser, B.J.: Cocotb: a python-based digital logic verification framework. In: Micro-electronics Section seminar. CERN, Geneva, Switzerland, (2018)

14. Nagel, W.E., et al.: Vampir: Visualization and analysis of mpi resources. Supercomputer 12, 68–80 (1996)

15. Pillet, V., et al.: Paraver: A tool to visualize and analyze parallel code. In: Proceedings of WoTUG-18: transputer and occam developments, vol. 44, pp. 17–31. (1995)

16. Perfetto. accessed: 2024-06-20. https://ui.perfetto.dev/