Skip to content

Commit

Permalink
Rework SystemBus component and create notion of address map
Browse files Browse the repository at this point in the history
  • Loading branch information
matiasilva committed Jan 27, 2025
1 parent f62d0af commit f449c9b
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 50 deletions.
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,32 @@
`pyrv` is an instruction set simulator (ISS) for the RISC-V ISA.

An ISS provides a non-cycle accurate functional model of a CPU core. `pyrv`
models RISC-V harts, meeting particular RISC-V ISA versions. There is an
[official simulator](https://github.com/riscv-software-src/riscv-isa-sim) called
Spike from RISC-V International, which is much more feature-rich and should be
used for any official work.
models RISC-V hardware threads (harts) of a particular RISC-V ISA version.

The aim of `pyrv` is to model a resource-constrained bare metal environment,
with simulated on-board flash memory and SRAM. This means, for example, that
`ecall`s are not supported and instead the processor must interact with the host
through the simulator. For OS-level RISC-V work, use the official RISC-V ISA
simulator [Spike](https://github.com/riscv-software-src/riscv-isa-sim) from
RISC-V International.

## Features

- RV32I Base Integer ISA support, v2.1
- `.elf` and assembly support
- flexible types make future ISA support easy
- little endian
- C runtime support
- loads native ELF files or binary files
- optimizes performance with numpy integration
- flexible and reusable types simplifies future ISA support
- mix-and-match peripherals to build a custom Hart

## Getting started

`pyrv` exposes a CLI, which is available once the package is installed. You can
install `pyrv` system-wide or in a virtual environment by running
`pip install .` in a cloned version of this repository.

A simple demo:

## Development

Dependencies are managed with [uv](https://docs.astral.sh/uv/). You can consult
Expand All @@ -28,8 +40,7 @@ To run the tests:
uv run pytest
```

> [!WARNING]
> You need a working set of the GNU Compiler Toolchain for RISC-V,
> [!WARNING] You need a working set of the GNU Compiler Toolchain for RISC-V,
> built specifically for the ISA you are targeting. It doesn't matter if you
> specify `-march`; if your compiler isn't built for it, it won't work!
Expand Down
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ version = "0.1.0"
description = "RV32I instruction set simulator"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"numpy>=2.2.2",
"pyelftools>=0.31",
"pytest>=8.3.4",
]
dependencies = ["numpy>=2.2.2", "pyelftools>=0.31", "pytest>=8.3.4"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project.scripts]
pyrv = "pyrv.core:main"

[tool.ruff.lint]
select = [
# pycodestyle
Expand Down
10 changes: 7 additions & 3 deletions pyrv/harts.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from pathlib import Path

from elftools.elf.constants import P_FLAGS
from elftools.elf.elffile import ELFFile

from pyrv.adapters import check_elf
from pyrv.helpers import MutableRegister
from pyrv.instructions import decode_instr
from pyrv.models import (
DataMemory,
InstructionMemory,
RegisterFile,
SimControl,
SystemBus,
)
from pyrv.instructions import decode_instr
from pyrv.adapters import check_elf
from pathlib import Path


class Hart:
Expand Down Expand Up @@ -60,3 +62,5 @@ def load(self, elf_path: Path | str):
for seg in elf_file.iter_segments("PT_LOAD"):
if seg["p_flags"] & P_FLAGS.PF_X:
self.instruction_memory.load(seg.data())
else:
self.data_memory.load(seg.data())
170 changes: 137 additions & 33 deletions pyrv/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import TYPE_CHECKING

import numpy
import numpy.typing as npt

from pyrv.helpers import (
AccessFaultException,
Expand Down Expand Up @@ -79,50 +80,80 @@ def __getattr__(self, attr: str) -> Register:
return self[attr]


type Address = int | Register
class Peripheral:
pass


class InstructionMemory:
def __init__(self):
self.SIZE = 2 * 1024
"""The size of the memory in kiB"""
self._contents = numpy.zeros(self.SIZE * 1024, numpy.uint8)
class Memory(Peripheral):
"""Generic Memory class, implementing a few basic methods"""

def _check_addr(self, addr: int, n: int) -> None:
if addr > self._contents.size:
raise AccessFaultException
def __init__(self, size: int, width: int = 1):
"""
Initializes the Memory class with size and width args.
def read(self, addr: int, n: int) -> numpy.ndarray:
"""Read `n` words from the memory starting at address `addr`"""
self._check_addr(addr, 4)
return self._contents[addr : addr + 4]
Args:
size: the size of the memory in KiB
width: the width of the memory in multiples of bytes
"""
self._size = size
"""The size of the memory in KiB"""

match width:
case 4:
dtype = numpy.uint32
case 1:
dtype = numpy.uint8
case _:
raise ValueError("Unsupported memory size")
self._contents: npt.NDArray = numpy.zeros(self._size * 1024, dtype)
"""Internal container for our memory"""

def _read(self, addr: int, n: int) -> npt.NDArray:
"""Read `n` bytes from the memory, starting at address `addr`"""
return self._contents[addr : addr + n]

def load(self, data: bytes, offset=0) -> None:
self._contents[: len(data)] = memoryview(data)
def _write_bytes(self, addr: int, data: bytes) -> None:
"""Write `data` bytes to memory, starting at address `addr`"""
self._contents[addr : addr + len(data)] = memoryview(data)

def _write(self, addr: int, data: int, n: int) -> None:
"""Write `n` bytes from `data` into memory, starting at address `addr`"""
self._write_bytes(addr, data.to_bytes(n, byteorder="little"))

class DataMemory:
def __init__(self):
self.SIZE = 6 * 1024
"""The size of the memory in kiB"""
self._contents = numpy.zeros(self.SIZE * 1024, numpy.uint8)
# AHB-like intf
def read_byte(self, addr: int):
return self._read(addr, 1)

def _check_addr(self, addr: int, n: int) -> None:
if addr > self._contents.size:
raise AccessFaultException
def read_halfword(self, addr: int):
return self._read(addr, 2)

def write(self, addr: int, data: int, n: int) -> None:
"""Write `n` bytes of `data` to the memory starting at address `addr`"""
self._check_addr(addr, n)
self._contents[addr : addr + n] = data
def read_word(self, addr: int):
return self._read(addr, 4)

def read(self, addr: int, n: int) -> numpy.ndarray:
"""Read `n` bytes from the memory starting at address `addr`"""
self._check_addr(addr, n)
return self._contents[addr : addr + n]
def read_burst(self, addr: int, burst_size: int, beat_count: int):
return self._read(addr, burst_size * beat_count)

def write_byte(self, addr: int, data: int):
return self._write(addr, data, 1)

def write_halfword(self, addr: int, data: int):
return self._write(addr, data, 2)

def write_word(self, addr: int, data: int):
return self._write(addr, data, 4)


class InstructionMemory(Memory):
def __init__(self):
super().__init__(2 * 1024, 4)

class SimControl:

class DataMemory(Memory):
def __init__(self):
super().__init__(6 * 1024, 4)


class SimControl(Peripheral):
"""Controls interaction with the simulator, like stopping the simulation"""

def __init__(self) -> None:
Expand All @@ -135,6 +166,22 @@ def write(self, addr: int, data: int, n: int):
pass


class AddressRange:
def __init__(self, start: int, size: int):
self.start = start
self.end = start + size - 1
self.size = size

def contains(self, addr: int, n_bytes: int = 1) -> bool:
"""Check if an address + n_bytes falls within this AddressRange"""
access_end = addr + n_bytes - 1
return self.start <= addr and access_end <= self.end

def overlaps(self, other: "AddressRange") -> bool:
"""Check if this range overlaps with another range"""
return not (self.end < other.start or other.end < self.start)


class SystemBus:
"""
Dispatches load/store instructions
Expand All @@ -151,13 +198,16 @@ def _check_addr(self, addr: int, n: int):
raise AddressMisalignedException
if addr % n != 0:
raise AddressMisalignedException
# check addr beyond range
if addr > self._contents.size:
raise AccessFaultException

def write(self, addr: int, data: int, n: int):
pass

def read(self, addr: int, n: int) -> int:
"""Read `n` bytes from the system bus"""
port = self.addr2port(addr)
port = self.get_port(addr, n)
self._check_addr(addr, n)
return port.read(addr, n)

Expand All @@ -176,3 +226,57 @@ def addr2port(self, addr: int) -> InstructionMemory | DataMemory | SimControl:
return self._hart.sim_control
else:
raise AccessFaultException

def add_peripheral(self, name: str, start_addr: int, size: int, peripheral=None):
"""
Add a peripheral to the address map.
Args:
name: Identifier for the peripheral
start_addr: Base address
size: Size in bytes
peripheral: Optional reference to peripheral object
"""
new_range = AddressRange(start_addr, size)

# Check for overlaps with existing peripherals
for existing_name, (existing_range, _) in self.peripherals.items():
if new_range.overlaps(existing_range):
raise ValueError(
f"Address range 0x{start_addr:x}-0x{start_addr + size - 1:x} "
f"overlaps with existing peripheral {existing_name}"
)

self.peripherals[name] = (new_range, peripheral)

def check_access(self, addr: int, n_bytes: int) -> Peripheral:
"""
Check if an access to address + n_bytes is valid.
Validity checks:
- n_bytes must be a power of 2
- alignment of `addr` on an `n_bytes` boundary
- addr + n_bytes is contained in the address map
Returns:
A valid peripheral if an address is valid, else None
"""
for name, (addr_range, peripheral) in self.peripherals.items():
if addr_range.contains(addr, n_bytes):
return True, name, peripheral

return False, None, None

def get_peripheral(self, addr: int, n_bytes: int = 1) -> Peripheral:
"""
Get the peripheral at address `addr` in the system address map.
The access occurs at address `addr` and has an extent of `n_bytes`. If this
access is invalid, then no peripheral is returned.
Returns:
A peripheral object if the access is valid, else None
"""
peripheral = self.check_access(addr, n_bytes)
if not peripheral:
raise AccessFaultException(f"No peripheral at address 0x{addr:x}")
return peripheral

0 comments on commit f449c9b

Please # to comment.