From 8eaf05a8ac23aa59ec931ea06f220459624ae64e Mon Sep 17 00:00:00 2001 From: modeco80 Date: Fri, 21 Jul 2023 06:32:04 -0400 Subject: [PATCH] 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) --- native/CMakeLists.txt | 2 + .../lucore/include/lucore/OptionalRef.hpp | 1 + native/projects/riscv/CMakeLists.txt | 1 + native/projects/riscv/include/riscv/Bus.hpp | 111 +++++++++++++----- native/projects/riscv/include/riscv/CPU.hpp | 9 +- .../riscv/include/riscv/Devices/RamDevice.hpp | 14 +-- .../projects/riscv/include/riscv/System.hpp | 37 ++++++ native/projects/riscv/src/Bus.cpp | 111 +++++++++++------- native/projects/riscv/src/CPU.cpp | 0 .../projects/riscv/src/Devices/RamDevice.cpp | 13 +- .../riscv_test_harness/CMakeLists.txt | 7 ++ native/projects/riscv_test_harness/main.cpp | 32 +++++ 12 files changed, 251 insertions(+), 87 deletions(-) create mode 100644 native/projects/riscv/include/riscv/System.hpp create mode 100644 native/projects/riscv/src/CPU.cpp create mode 100644 native/projects/riscv_test_harness/CMakeLists.txt create mode 100644 native/projects/riscv_test_harness/main.cpp diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index a934e74..d09dcf9 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -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) diff --git a/native/projects/lucore/include/lucore/OptionalRef.hpp b/native/projects/lucore/include/lucore/OptionalRef.hpp index d8fa4d9..9b62005 100644 --- a/native/projects/lucore/include/lucore/OptionalRef.hpp +++ b/native/projects/lucore/include/lucore/OptionalRef.hpp @@ -1,4 +1,5 @@ //! OptionalRef - std::optional for C++20 +#pragma once #include diff --git a/native/projects/riscv/CMakeLists.txt b/native/projects/riscv/CMakeLists.txt index 12d2b19..9dda00c 100644 --- a/native/projects/riscv/CMakeLists.txt +++ b/native/projects/riscv/CMakeLists.txt @@ -7,6 +7,7 @@ project(riscv_emu add_library(riscv src/Bus.cpp + src/CPU.cpp src/Devices/RamDevice.cpp ) diff --git a/native/projects/riscv/include/riscv/Bus.hpp b/native/projects/riscv/include/riscv/Bus.hpp index ca3738d..5f0080b 100644 --- a/native/projects/riscv/include/riscv/Bus.hpp +++ b/native/projects/riscv/include/riscv/Bus.hpp @@ -1,4 +1,6 @@ -#include +#pragma once + +#include #include #include #include @@ -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 { + enum class BasicType { + Device, // do not upcast. + PlainMemory, // upcast to MemoryDevice is safe. + Mmio // upcast to MmioDevice is safe. + }; + Device() = default; - - // Devices have no need to be copied or moved. 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 + constexpr bool IsA() { + if(std::is_same_v) { + return this->Type() == BasicType::PlainMemory; + } else if(std::is_same_v) { + return this->Type() == BasicType::Mmio; + } + + // Invalid types should do this. + return false; + } + + template + constexpr T Upcast() { + LUCORE_ASSERT(IsA(), "Upcast failure: this is not a T"); + return static_cast(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 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 mapped_devices; + /// All devices attached to the bus + std::vector devices; + + // TODO: if these end up being a hotpath replace with ankerl::unordered_dense + std::unordered_map mapped_devices; + std::unordered_map mmio_devices; }; } // namespace riscv diff --git a/native/projects/riscv/include/riscv/CPU.hpp b/native/projects/riscv/include/riscv/CPU.hpp index 3bbcf46..062ec7a 100644 --- a/native/projects/riscv/include/riscv/CPU.hpp +++ b/native/projects/riscv/include/riscv/CPU.hpp @@ -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 diff --git a/native/projects/riscv/include/riscv/Devices/RamDevice.hpp b/native/projects/riscv/include/riscv/Devices/RamDevice.hpp index a9ff34a..38dec96 100644 --- a/native/projects/riscv/include/riscv/Devices/RamDevice.hpp +++ b/native/projects/riscv/include/riscv/Devices/RamDevice.hpp @@ -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 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 {}; diff --git a/native/projects/riscv/include/riscv/System.hpp b/native/projects/riscv/include/riscv/System.hpp new file mode 100644 index 0000000..e20eca9 --- /dev/null +++ b/native/projects/riscv/include/riscv/System.hpp @@ -0,0 +1,37 @@ +#include +#include + +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 diff --git a/native/projects/riscv/src/Bus.cpp b/native/projects/riscv/src/Bus.cpp index 9626652..454878f 100644 --- a/native/projects/riscv/src/Bus.cpp +++ b/native/projects/riscv/src/Bus.cpp @@ -1,88 +1,117 @@ #include #include +#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()) { + auto* upcasted = device->Upcast(); + + // 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()) { + auto* upcasted = device->Upcast(); + + // 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()->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()->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()) + return dev->Upcast()->Peek(address); + else + return dev->Upcast()->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()->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()->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()) + dev->Upcast()->Poke(address, value); + else + dev->Upcast()->PokeWord(address, value); + } } - lucore::OptionalRef 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(it->second); + else if(auto it = try_find_device(mmio_devices, address); it != mmio_devices.end()) + return static_cast(it->second); + + return nullptr; } } // namespace riscv diff --git a/native/projects/riscv/src/CPU.cpp b/native/projects/riscv/src/CPU.cpp new file mode 100644 index 0000000..e69de29 diff --git a/native/projects/riscv/src/Devices/RamDevice.cpp b/native/projects/riscv/src/Devices/RamDevice.cpp index a749be2..fe7dcdc 100644 --- a/native/projects/riscv/src/Devices/RamDevice.cpp +++ b/native/projects/riscv/src/Devices/RamDevice.cpp @@ -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) { diff --git a/native/projects/riscv_test_harness/CMakeLists.txt b/native/projects/riscv_test_harness/CMakeLists.txt new file mode 100644 index 0000000..205aacd --- /dev/null +++ b/native/projects/riscv_test_harness/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(rvtest + main.cpp +) + +target_link_libraries(rvtest PUBLIC + riscv::riscv +) diff --git a/native/projects/riscv_test_harness/main.cpp b/native/projects/riscv_test_harness/main.cpp new file mode 100644 index 0000000..e5736c0 --- /dev/null +++ b/native/projects/riscv_test_harness/main.cpp @@ -0,0 +1,32 @@ +#include +#include +#include + +#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); + } + } +};