riscv: Heavy bus refactor.
It now doesn't assume literally every device would map something to memory. This should also fix some API orthogonality issues (ergo the CPU being treated specially)
This commit is contained in:
parent
a85a8ddc97
commit
8eaf05a8ac
|
@ -9,6 +9,8 @@ add_subdirectory(projects/lucore)
|
|||
# RISC-V emulator library
|
||||
add_subdirectory(projects/riscv)
|
||||
|
||||
add_subdirectory(projects/riscv_test_harness)
|
||||
|
||||
# Garry's Mod native bindings to RISC-V emulator
|
||||
# Also lua device stuff
|
||||
add_subdirectory(projects/lcpu)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
//! OptionalRef - std::optional<T&> for C++20
|
||||
#pragma once
|
||||
|
||||
#include <lucore/Assert.hpp>
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ project(riscv_emu
|
|||
|
||||
add_library(riscv
|
||||
src/Bus.cpp
|
||||
src/CPU.cpp
|
||||
src/Devices/RamDevice.cpp
|
||||
)
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#include <lucore/OptionalRef.hpp>
|
||||
#pragma once
|
||||
|
||||
#include <lucore/Assert.hpp>
|
||||
#include <riscv/Types.hpp>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
@ -7,51 +9,94 @@ namespace riscv {
|
|||
|
||||
struct CPU;
|
||||
|
||||
/// An address/memory bus. No virtual address translation is implemented;
|
||||
/// all addresses/devices are placed in physical addresses.
|
||||
/// An bus that is part of a [System].
|
||||
/// Note that no virtual address translation is implemented at all.
|
||||
/// All addresses/devices of devices in memory are placed in physical addresses.
|
||||
struct Bus {
|
||||
/// Interface all memory bus devices use.
|
||||
struct Device {
|
||||
Device() = default;
|
||||
enum class BasicType {
|
||||
Device, // do not upcast.
|
||||
PlainMemory, // upcast to MemoryDevice is safe.
|
||||
Mmio // upcast to MmioDevice is safe.
|
||||
};
|
||||
|
||||
// Devices have no need to be copied or moved.
|
||||
Device() = default;
|
||||
Device(const Device&) = delete;
|
||||
Device(Device&&) = delete;
|
||||
|
||||
virtual ~Device() = default;
|
||||
|
||||
/// How many bytes does this device occupy of address space? This should
|
||||
/// effectively be a constant, and should probably not change during CPU execution.
|
||||
virtual AddressT Size() const = 0;
|
||||
virtual BasicType Type() const { return BasicType::Device; }
|
||||
|
||||
/// Used to allow bus devices to know when they are attached to a memory bus,
|
||||
/// and ultimately, an instance of a System
|
||||
virtual void Attached(Bus* memoryBus, AddressT baseAddress) = 0;
|
||||
|
||||
/// Does this device require a clock "signal"?
|
||||
/// Does this device clock?
|
||||
virtual bool Clocked() const { return false; }
|
||||
|
||||
/// This function is called to give clocked devices
|
||||
/// the ability to... well, clock!
|
||||
/// This function is called by the bus to clock devices.
|
||||
virtual void Clock() {}
|
||||
|
||||
// TODO(feat): default implementations of Peek* and Poke* should exist
|
||||
// and trap the CPU (similarly to what happens if a unmapped bus read occurs).
|
||||
template <class T>
|
||||
constexpr bool IsA() {
|
||||
if(std::is_same_v<T, Bus::MemoryDevice*>) {
|
||||
return this->Type() == BasicType::PlainMemory;
|
||||
} else if(std::is_same_v<T, Bus::MmioDevice*>) {
|
||||
return this->Type() == BasicType::Mmio;
|
||||
}
|
||||
|
||||
// Invalid types should do this.
|
||||
return false;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
constexpr T Upcast() {
|
||||
LUCORE_ASSERT(IsA<T>(), "Upcast failure: this is not a T");
|
||||
return static_cast<T>(this);
|
||||
}
|
||||
};
|
||||
|
||||
/// Interface plain memory bus devices use.
|
||||
struct MemoryDevice : Device {
|
||||
virtual ~MemoryDevice() = default;
|
||||
|
||||
virtual BasicType Type() const override { return BasicType::PlainMemory; }
|
||||
|
||||
virtual AddressT Base() const = 0;
|
||||
/// How many bytes does this device occupy of address space? This should
|
||||
/// effectively be a constant, and should not change during CPU execution.
|
||||
virtual AddressT Size() const = 0;
|
||||
|
||||
/// Peek() -> reads a value from this device.
|
||||
virtual u8 PeekByte(AddressT offset) = 0;
|
||||
virtual u16 PeekShort(AddressT offset) = 0;
|
||||
virtual u32 PeekWord(AddressT offset) = 0;
|
||||
virtual u8 PeekByte(AddressT address) = 0;
|
||||
virtual u16 PeekShort(AddressT address) = 0;
|
||||
virtual u32 PeekWord(AddressT address) = 0;
|
||||
|
||||
/// Poke() -> Writes a value to this device's space in memory
|
||||
virtual void PokeByte(AddressT offset, u8 value) = 0;
|
||||
virtual void PokeShort(AddressT offset, u16 value) = 0;
|
||||
virtual void PokeWord(AddressT offset, u32 value) = 0;
|
||||
virtual void PokeByte(AddressT address, u8 value) = 0;
|
||||
virtual void PokeShort(AddressT address, u16 value) = 0;
|
||||
virtual void PokeWord(AddressT address, u32 value) = 0;
|
||||
};
|
||||
|
||||
/// A device in the MMIO region.
|
||||
/// (0x10000000-0x12000000)
|
||||
struct MmioDevice : Device {
|
||||
virtual ~MmioDevice() = default;
|
||||
|
||||
virtual BasicType Type() const override { return BasicType::Mmio; }
|
||||
|
||||
virtual AddressT Base() const;
|
||||
|
||||
/// How many bytes does this device occupy of address space? This should
|
||||
/// effectively be a constant, and should not change during CPU execution.
|
||||
virtual AddressT Size() const = 0;
|
||||
|
||||
virtual u32 Peek(AddressT address) = 0;
|
||||
virtual void Poke(AddressT address, u32 value) = 0;
|
||||
};
|
||||
|
||||
Bus(CPU* cpu);
|
||||
~Bus();
|
||||
|
||||
CPU* GetCPU() { return attachedCpu; }
|
||||
|
||||
/// Attach a device to the bus.
|
||||
///
|
||||
/// Note that once this function is called (and the device is successfully added),
|
||||
|
@ -61,8 +106,9 @@ namespace riscv {
|
|||
/// This function returns true if the device was able to be put on the bus.
|
||||
/// This function returns false in the following error cases:
|
||||
/// - [device] is a null pointer
|
||||
/// - The provided base address overlaps a already attached device in some way
|
||||
bool AttachDevice(AddressT baseAddress, Device* device);
|
||||
/// - if [device] is a memory device (and thus reserves address space), adding it would
|
||||
/// end up shadowing another previously-added device.
|
||||
bool AttachDevice(Device* device);
|
||||
|
||||
/// Clock all clocked devices mapped onto the bus..
|
||||
void Clock();
|
||||
|
@ -76,12 +122,19 @@ namespace riscv {
|
|||
void PokeWord(AddressT address, u32 value);
|
||||
|
||||
private:
|
||||
lucore::OptionalRef<Device> FindDeviceForAddress(AddressT address) const;
|
||||
Bus::Device* FindDeviceForAddress(AddressT address) const;
|
||||
|
||||
// TODO: The CPU needs not be a separate member or be treated specially, it too can be a Bus::Device now
|
||||
// In fact that would probably be really clean and elegant for calling Step() properly
|
||||
|
||||
CPU* attachedCpu {};
|
||||
|
||||
// TODO: if this ends up being a hotpath replace with ankerl::unordered_dense
|
||||
std::unordered_map<AddressT, Device*> mapped_devices;
|
||||
/// All devices attached to the bus
|
||||
std::vector<Device*> devices;
|
||||
|
||||
// TODO: if these end up being a hotpath replace with ankerl::unordered_dense
|
||||
std::unordered_map<AddressT, MemoryDevice*> mapped_devices;
|
||||
std::unordered_map<AddressT, MmioDevice*> mmio_devices;
|
||||
};
|
||||
|
||||
} // namespace riscv
|
||||
|
|
|
@ -34,11 +34,18 @@ namespace riscv {
|
|||
u32 extraflags;
|
||||
};
|
||||
|
||||
CPU(Bus* bus);
|
||||
|
||||
State& GetState() { return state; }
|
||||
|
||||
|
||||
// TODO: Handlers for CSR read/write
|
||||
|
||||
u32 Step(u32 elapsedMicroseconds, u32 instCount);
|
||||
|
||||
private:
|
||||
State state;
|
||||
Bus bus;
|
||||
Bus* bus;
|
||||
};
|
||||
|
||||
} // namespace riscv
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
namespace riscv::devices {
|
||||
|
||||
/// A block of RAM which can be used by the CPU.
|
||||
struct RamDevice : public Bus::Device {
|
||||
RamDevice(AddressT size);
|
||||
struct RamDevice : public Bus::MemoryDevice {
|
||||
RamDevice(AddressT base, AddressT size);
|
||||
virtual ~RamDevice();
|
||||
|
||||
// Implementation of Device interface
|
||||
|
||||
AddressT Base() const override;
|
||||
AddressT Size() const override;
|
||||
|
||||
void Attached(Bus* bus, AddressT base) override;
|
||||
|
||||
u8 PeekByte(AddressT address) override;
|
||||
u16 PeekShort(AddressT address) override;
|
||||
|
@ -25,12 +25,10 @@ namespace riscv::devices {
|
|||
/// helper used for implementing Peek/Poke API
|
||||
template <class T>
|
||||
constexpr usize AddressToIndex(AddressT address) {
|
||||
return ((address - baseAddress) % memorySize) / sizeof(T);
|
||||
return ((address - memoryBase) % memorySize) / sizeof(T);
|
||||
}
|
||||
|
||||
// remember what we were attached to via "signal"
|
||||
Bus* attachedBus {};
|
||||
AddressT baseAddress {};
|
||||
AddressT memoryBase {};
|
||||
|
||||
u8* memory {};
|
||||
usize memorySize {};
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
#include <riscv/Bus.hpp>
|
||||
#include <riscv/Devices/RamDevice.hpp>
|
||||
|
||||
namespace riscv {
|
||||
|
||||
// fwd decls
|
||||
struct CPU;
|
||||
|
||||
/// a system.
|
||||
struct System {
|
||||
|
||||
/// Create
|
||||
static System* WithMemory(AddressT ramSize);
|
||||
|
||||
void AddDevice(Bus::MmioDevice* device);
|
||||
|
||||
/// returns false if the cpu broke execution
|
||||
bool Step();
|
||||
|
||||
CPU* GetCPU();
|
||||
Bus* GetBus();
|
||||
|
||||
private:
|
||||
|
||||
/// How many Cycle() calls will the bus get
|
||||
/// (also decides ipsRate)
|
||||
u32 cycleRate;
|
||||
|
||||
/// How many instructions will the CPU execute each step
|
||||
u32 ipsRate;
|
||||
|
||||
CPU* cpu;
|
||||
Bus* bus;
|
||||
devices::RamDevice* ram;
|
||||
}
|
||||
|
||||
} // namespace riscv
|
|
@ -1,88 +1,117 @@
|
|||
#include <algorithm>
|
||||
#include <riscv/Bus.hpp>
|
||||
|
||||
#include "riscv/Types.hpp"
|
||||
|
||||
namespace riscv {
|
||||
|
||||
Bus::Bus(CPU* cpu)
|
||||
: attachedCpu(cpu) {
|
||||
|
||||
Bus::Bus(CPU* cpu) : attachedCpu(cpu) {
|
||||
}
|
||||
|
||||
Bus::~Bus() {
|
||||
// Free all devices
|
||||
for(auto& pair : mapped_devices)
|
||||
delete pair.second;
|
||||
for(auto device: devices)
|
||||
delete device;
|
||||
}
|
||||
|
||||
bool Bus::AttachDevice(AddressT baseAddress, Device* device) {
|
||||
bool Bus::AttachDevice(Device* device) {
|
||||
if(!device)
|
||||
return false;
|
||||
|
||||
// Refuse to overlap a device at its base address..
|
||||
if(FindDeviceForAddress(baseAddress))
|
||||
return false;
|
||||
// ... or have the end overlap the start of another device.
|
||||
else if(FindDeviceForAddress(baseAddress + device->Size()))
|
||||
return false;
|
||||
if(device->IsA<MemoryDevice*>()) {
|
||||
auto* upcasted = device->Upcast<MemoryDevice*>();
|
||||
|
||||
// Refuse to overlap a device at its base address..
|
||||
if(FindDeviceForAddress(upcasted->Base()))
|
||||
return false;
|
||||
// ... or have the end overlap the start of another device.
|
||||
else if(FindDeviceForAddress(upcasted->Base() + upcasted->Size()))
|
||||
return false;
|
||||
|
||||
mapped_devices[upcasted->Base()] = upcasted;
|
||||
} else if(device->IsA<MmioDevice*>()) {
|
||||
auto* upcasted = device->Upcast<MmioDevice*>();
|
||||
|
||||
// Refuse to overlap a device at its base address..
|
||||
if(FindDeviceForAddress(upcasted->Base()))
|
||||
return false;
|
||||
// ... or have the end overlap the start of another device.
|
||||
else if(FindDeviceForAddress(upcasted->Base() + upcasted->Size()))
|
||||
return false;
|
||||
|
||||
mmio_devices[upcasted->Base()] = upcasted;
|
||||
}
|
||||
|
||||
devices.push_back(device);
|
||||
|
||||
mapped_devices[baseAddress] = device;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Bus::Clock() {
|
||||
for(auto& device : mapped_devices) {
|
||||
if(device.second->Clocked())
|
||||
device.second->Clock();
|
||||
for(auto device : devices) {
|
||||
if(device->Clocked())
|
||||
device->Clock();
|
||||
}
|
||||
}
|
||||
|
||||
u8 Bus::PeekByte(AddressT address) {
|
||||
if(auto opt = FindDeviceForAddress(address); opt)
|
||||
return opt->PeekByte(address);
|
||||
if(auto dev = FindDeviceForAddress(address); dev)
|
||||
return dev->Upcast<MemoryDevice*>()->PeekByte(address);
|
||||
return -1;
|
||||
}
|
||||
|
||||
u16 Bus::PeekShort(AddressT address) {
|
||||
if(auto opt = FindDeviceForAddress(address); opt)
|
||||
return opt->PeekShort(address);
|
||||
if(auto dev = FindDeviceForAddress(address); dev)
|
||||
return dev->Upcast<MemoryDevice*>()->PeekShort(address);
|
||||
return -1;
|
||||
}
|
||||
|
||||
u32 Bus::PeekWord(AddressT address) {
|
||||
if(auto opt = FindDeviceForAddress(address); opt)
|
||||
return opt->PeekWord(address);
|
||||
if(auto dev = FindDeviceForAddress(address); dev) {
|
||||
if(dev->IsA<MmioDevice*>())
|
||||
return dev->Upcast<MmioDevice*>()->Peek(address);
|
||||
else
|
||||
return dev->Upcast<MemoryDevice*>()->PeekWord(address);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Bus::PokeByte(AddressT address, u8 value) {
|
||||
if(auto opt = FindDeviceForAddress(address); opt)
|
||||
return opt->PokeByte(address, value);
|
||||
if(auto dev = FindDeviceForAddress(address); dev)
|
||||
return dev->Upcast<MemoryDevice*>()->PokeByte(address, value);
|
||||
}
|
||||
|
||||
void Bus::PokeShort(AddressT address, u16 value) {
|
||||
if(auto opt = FindDeviceForAddress(address); opt)
|
||||
return opt->PokeShort(address, value);
|
||||
if(auto dev = FindDeviceForAddress(address); dev)
|
||||
return dev->Upcast<MemoryDevice*>()->PokeShort(address, value);
|
||||
}
|
||||
|
||||
void Bus::PokeWord(AddressT address, u32 value) {
|
||||
if(auto opt = FindDeviceForAddress(address); opt)
|
||||
return opt->PokeWord(address, value);
|
||||
if(auto dev = FindDeviceForAddress(address); dev) {
|
||||
if(dev->IsA<MmioDevice*>())
|
||||
dev->Upcast<MmioDevice*>()->Poke(address, value);
|
||||
else
|
||||
dev->Upcast<MemoryDevice*>()->PokeWord(address, value);
|
||||
}
|
||||
}
|
||||
|
||||
lucore::OptionalRef<Bus::Device> Bus::FindDeviceForAddress(AddressT address) const {
|
||||
auto it = std::find_if(mapped_devices.begin(), mapped_devices.end(), [&](const auto& pair) {
|
||||
return
|
||||
// We can shorcut region checking if the requested addess matches base address.
|
||||
pair.first == address ||
|
||||
// If it doesn't we really can't, though.
|
||||
(address >= pair.first && address < pair.first + pair.second->Size());
|
||||
});
|
||||
Bus::Device* Bus::FindDeviceForAddress(AddressT address) const {
|
||||
auto try_find_device = [&](auto container, AddressT address) {
|
||||
return std::find_if(container.begin(), container.end(), [&](const auto& pair) {
|
||||
return
|
||||
// We can shorcut region checking if the requested addess matches base address.
|
||||
pair.first == address ||
|
||||
// If it doesn't we really can't, though.
|
||||
(address >= pair.first && address < pair.first + pair.second->Size());
|
||||
});
|
||||
};
|
||||
|
||||
// No device was found at this address
|
||||
if(it == mapped_devices.end())
|
||||
return lucore::NullRef;
|
||||
else
|
||||
return *it->second;
|
||||
if(auto it = try_find_device(mapped_devices, address); it != mapped_devices.end())
|
||||
return static_cast<Bus::Device*>(it->second);
|
||||
else if(auto it = try_find_device(mmio_devices, address); it != mmio_devices.end())
|
||||
return static_cast<Bus::Device*>(it->second);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace riscv
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
namespace riscv::devices {
|
||||
|
||||
RamDevice::RamDevice(AddressT size) : Bus::Device(), memorySize(size) {
|
||||
RamDevice::RamDevice(AddressT base, AddressT size) : memoryBase(base), memorySize(size) {
|
||||
memory = new u8[size];
|
||||
LUCORE_CHECK(memory, "Could not allocate buffer for memory device with size 0x{:08x}.",
|
||||
size);
|
||||
|
@ -15,15 +15,12 @@ namespace riscv::devices {
|
|||
delete[] memory;
|
||||
}
|
||||
|
||||
AddressT RamDevice::Size() const {
|
||||
return memorySize;
|
||||
AddressT RamDevice::Base() const {
|
||||
return memoryBase;
|
||||
}
|
||||
|
||||
// Implementation of Device interface
|
||||
|
||||
void RamDevice::Attached(Bus* bus, AddressT base) {
|
||||
attachedBus = bus;
|
||||
baseAddress = base;
|
||||
AddressT RamDevice::Size() const {
|
||||
return memorySize;
|
||||
}
|
||||
|
||||
u8 RamDevice::PeekByte(AddressT address) {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
add_executable(rvtest
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(rvtest PUBLIC
|
||||
riscv::riscv
|
||||
)
|
|
@ -0,0 +1,32 @@
|
|||
#include <riscv/Bus.hpp>
|
||||
#include <riscv/CPU.hpp>
|
||||
#include <riscv/Devices/RamDevice.hpp>
|
||||
|
||||
#include "riscv/Types.hpp"
|
||||
|
||||
/// simple 16550 UART implementation
|
||||
struct SimpleUartDevice : public riscv::Bus::MmioDevice {
|
||||
constexpr static riscv::AddressT BASE_ADDRESS = 0x10000000;
|
||||
|
||||
riscv::AddressT Base() const override { return BASE_ADDRESS; }
|
||||
|
||||
riscv::AddressT Size() const override { return 5; }
|
||||
|
||||
// TODO: emulate properly
|
||||
u32 Peek(riscv::AddressT address) override {
|
||||
switch(address) {
|
||||
case BASE_ADDRESS:
|
||||
break;
|
||||
case BASE_ADDRESS + 5:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Poke(riscv::AddressT address, u32 value) override {
|
||||
if(address == BASE_ADDRESS) { // write to data buffer
|
||||
printf("%c\n", value);
|
||||
}
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue