riscv: IT WORKS

finally. once i'm sure it's 100% working i can probably like, develop this addon finally
This commit is contained in:
Lily Tsuru 2023-07-24 00:01:39 -04:00
parent 38e7fc4646
commit 878990a921
15 changed files with 395 additions and 74 deletions

View File

@ -9,3 +9,7 @@ This is based off [cnlohr/mini-rv32ima](https://github.com/cnlohr/mini-rv32ima),
- Moved *ALL* device and MMIO code to seperate interfaces - Moved *ALL* device and MMIO code to seperate interfaces
Depends on lucore. Depends on lucore.
# Usage
TBD (if this is moved to another repo). See the riscv_test_harness project.

View File

@ -6,6 +6,7 @@ namespace riscv {
/// The CPU core. /// The CPU core.
struct CPU : Bus::Device { struct CPU : Bus::Device {
BasicType Type() const override { return BasicType::Cpu; }
bool Clocked() const override { return true; } bool Clocked() const override { return true; }
void Clock() override; void Clock() override;
@ -15,6 +16,17 @@ namespace riscv {
void TimerInterrupt(); void TimerInterrupt();
constexpr CPU() {
Reset();
}
constexpr void Reset() {
// Initalize some state. We're cool like that :)
pc = 0x80000000;
gpr[10] = 0x0; // HART id
extraflags |= 3; // Start in Machine mode
}
// TODO: Handlers for CSR read/write (if we need it?) // TODO: Handlers for CSR read/write (if we need it?)
/// CPU state /// CPU state

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <riscv/Types.hpp> #include <riscv/Types.hpp>
#include <string_view>
namespace riscv { namespace riscv {
@ -25,41 +26,78 @@ namespace riscv {
}; };
enum class Gpr : u8 { enum class Gpr : u8 {
X0, // zero Zero,
X1, Ra,
X2, Sp,
X3, Gp,
X4, Tp,
X5, T0,
X6, T1,
X7, T2,
X8, S0, // also `fp`
X9, S1,
X10, A0,
X11, A1,
X12, A2,
X13, A3,
X14, A4,
X15, A5,
X16, A6,
X17, A7,
X18, S2,
X19, S3,
X20, S4,
X21, S5,
X22, S6,
X23, S7,
X24, S8,
X25, S9,
X26, S10,
X27, S11,
X28, T3,
X29, T4,
X30, T5,
X31, T6
PC
}; };
constexpr std::string_view RegName(Gpr gpr) {
std::string_view table[] = {
"zero",
"ra",
"sp",
"gp",
"tp",
"t0",
"t1",
"t2",
"s0",
"s1",
"a0",
"a1",
"a2",
"a3",
"a4",
"a5",
"a6",
"a7",
"s2",
"s3",
"s4",
"s5",
"s6",
"s7",
"s8",
"s9",
"s10",
"s11",
"t3",
"t4",
"t5",
"t6"
};
return table[static_cast<usize>(gpr)];
}
/// Container for GPRs which can be Statically Typed or not depending on use case. /// Container for GPRs which can be Statically Typed or not depending on use case.
/// Pretty cool, huh? /// Pretty cool, huh?
struct GeneralPurposeRegisters { struct GeneralPurposeRegisters {

View File

@ -14,6 +14,9 @@ namespace riscv::devices {
Address Base() const override; Address Base() const override;
Address Size() const override; Address Size() const override;
u8* Raw() const {
return memory;
}
u8 PeekByte(Address address) override; u8 PeekByte(Address address) override;
u16 PeekShort(Address address) override; u16 PeekShort(Address address) override;

View File

@ -11,6 +11,7 @@ namespace riscv::devices {
SysconDevice(System* system); SysconDevice(System* system);
Address Base() const override { return BASE_ADDRESS; }
Address Size() const override { return sizeof(u32); } // I think this is right? Address Size() const override { return sizeof(u32); } // I think this is right?
u32 Peek(Address address) override; u32 Peek(Address address) override;

View File

@ -1,8 +1,8 @@
#include <riscv/Bus.hpp> #include <riscv/Bus.hpp>
#include <riscv/CPU.hpp> #include <riscv/CPU.hpp>
#include <riscv/Devices/ClntDevice.hpp>
#include <riscv/Devices/RamDevice.hpp> #include <riscv/Devices/RamDevice.hpp>
#include <riscv/Devices/SysconDevice.hpp> #include <riscv/Devices/SysconDevice.hpp>
#include <riscv/Devices/ClntDevice.hpp>
namespace riscv { namespace riscv {
@ -10,25 +10,13 @@ namespace riscv {
struct System { struct System {
/// Create a basic system with the basic periphials created. /// Create a basic system with the basic periphials created.
/// All other periphials should be managed by the creator of this System /// All other periphials should be managed by the creator of this System
static System* WithMemory(Address ramSize); static System* Create(Address ramSize);
~System(); ~System();
void AddDeviceToBus(Bus::Device* device); void Step();
/// returns false if the cpu broke execution // TODO: callbacks for SYSCON PowerOff and Reboot.
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;
Bus* bus; Bus* bus;
@ -37,6 +25,9 @@ namespace riscv {
devices::RamDevice* ram; devices::RamDevice* ram;
devices::SysconDevice* syscon; devices::SysconDevice* syscon;
devices::ClntDevice* clnt; devices::ClntDevice* clnt;
private:
System() = default;
}; };
} // namespace riscv } // namespace riscv

View File

@ -14,6 +14,8 @@ namespace riscv {
if(!device) if(!device)
return false; return false;
device->Attached(this);
if(device->IsA<CPU*>()) { if(device->IsA<CPU*>()) {
// Return early to avoid putting the CPU pointer inside the devices vector. // Return early to avoid putting the CPU pointer inside the devices vector.
// We do not actually own the CPU. // We do not actually own the CPU.
@ -119,7 +121,7 @@ namespace riscv {
} }
Bus::Device* Bus::FindDeviceForAddress(Address address) const { Bus::Device* Bus::FindDeviceForAddress(Address address) const {
auto try_find_device = [&](auto container, Address address) { auto try_find_device = [&](const auto& container, Address address) {
return std::find_if(container.begin(), container.end(), [&](const auto& pair) { return std::find_if(container.begin(), container.end(), [&](const auto& pair) {
return return
// We can shorcut region checking if the requested addess matches base address. // We can shorcut region checking if the requested addess matches base address.

View File

@ -3,11 +3,12 @@
#include <riscv/Bus.hpp> #include <riscv/Bus.hpp>
#include <riscv/CPU.hpp> #include <riscv/CPU.hpp>
#include "riscv/CPUTypes.hpp" #include <lucore/Logger.hpp>
namespace riscv { namespace riscv {
constexpr static Address RamImageOffset = 0x80000000; // Not needed
//constexpr static Address RamImageOffset = 0x80000000;
void CPU::Clock() { void CPU::Clock() {
// do the thing // do the thing
@ -47,31 +48,35 @@ namespace riscv {
u32 rdid = 0; u32 rdid = 0;
u32 rval = 0; u32 rval = 0;
u32 pc = this->pc;
u32 cycle = this->cyclel; u32 cycle = this->cyclel;
if(interruptsInFlight()) { if(interruptsInFlight()) {
Trap(0x80000007); Trap(0x80000007);
} else { } else {
for(u32 iInst = 0; iInst < instCount; ++iInst) { for(u32 iInst = 0; iInst < instCount; ++iInst) {
auto ofs_pc = pc - RamImageOffset; rdid = 0; // force it to gpr 0 (zero), which is not writable
cycle++; cycle++;
if(ofs_pc & 3) { //lucore::LogInfo("[CPU] pc @ 0x{:08x}", pc);
if((pc & 3)) {
lucore::LogWarning("[CPU] misaligned jump target.. 0x{:08x}", pc);
Trap(TrapCode::InstructionAddressMisaligned); Trap(TrapCode::InstructionAddressMisaligned);
break; break;
} else { } else {
auto ir = bus->PeekWord(ofs_pc); auto ir = bus->PeekWord(pc);
if(trapped) { if(trapped) {
// Overwrite the trap that the bus generated. This might not really work out // Overwrite the trap that the bus generated. This might not really work out
// in practice but should at least kind-of replicate behaviour (our address // in practice but should at least kind-of replicate behaviour (our address
// space is emulated using the [Bus] class, so there is no heap write issue // space is emulated using the [Bus] class, so there is no heap write issue
// that could be caused by leaving it unbound). // that could be caused by leaving it unbound).
Trap(TrapCode::InstructionAccessFault); Trap(TrapCode::InstructionAccessFault);
rval = ofs_pc + RamImageOffset; rval = pc;
break; break;
} }
//lucore::LogInfo("[CPU] fetch 0x{:08x} 0x{:08x}", pc, ir);
rdid = (ir >> 7) & 0x1f; rdid = (ir >> 7) & 0x1f;
// Do the thing! // Do the thing!
@ -91,6 +96,7 @@ namespace riscv {
if(reladdy & 0x00100000) if(reladdy & 0x00100000)
reladdy |= 0xffe00000; reladdy |= 0xffe00000;
rval = pc + 4; rval = pc + 4;
//lucore::LogInfo("j/al 0x{:08x}", pc + reladdy);
pc = pc + reladdy - 4; pc = pc + reladdy - 4;
break; break;
} }
@ -100,6 +106,7 @@ namespace riscv {
i32 imm_se = imm | ((imm & 0x800) ? 0xfffff000 : 0); i32 imm_se = imm | ((imm & 0x800) ? 0xfffff000 : 0);
rval = pc + 4; rval = pc + 4;
pc = ((gpr[((ir >> 15) & 0x1f)] + imm_se) & ~1) - 4; pc = ((gpr[((ir >> 15) & 0x1f)] + imm_se) & ~1) - 4;
//lucore::LogInfo("jalr {}, 0x{:08x}", RegName(static_cast<Gpr>(((ir >> 15) & 0x1f))), ((gpr[((ir >> 15) & 0x1f)] + imm_se) & ~1) - 4);
break; break;
} }
@ -153,8 +160,6 @@ namespace riscv {
i32 imm_se = imm | ((imm & 0x800) ? 0xfffff000 : 0); i32 imm_se = imm | ((imm & 0x800) ? 0xfffff000 : 0);
u32 rsval = rs1 + imm_se; u32 rsval = rs1 + imm_se;
rsval -= RamImageOffset;
switch((ir >> 12) & 0x7) { switch((ir >> 12) & 0x7) {
// LB, LH, LW, LBU, LHU // LB, LH, LW, LBU, LHU
case 0: case 0:
@ -178,7 +183,7 @@ namespace riscv {
} }
if(trapped) { if(trapped) {
rval = rsval + RamImageOffset; rval = rsval;// + RamImageOffset;
} }
break; break;
} }
@ -188,7 +193,7 @@ namespace riscv {
u32 addy = ((ir >> 7) & 0x1f) | ((ir & 0xfe000000) >> 20); u32 addy = ((ir >> 7) & 0x1f) | ((ir & 0xfe000000) >> 20);
if(addy & 0x800) if(addy & 0x800)
addy |= 0xfffff000; addy |= 0xfffff000;
addy += rs1 - RamImageOffset; addy += rs1;
rdid = 0; rdid = 0;
switch((ir >> 12) & 0x7) { switch((ir >> 12) & 0x7) {
@ -200,6 +205,7 @@ namespace riscv {
bus->PokeShort(addy, rs2); bus->PokeShort(addy, rs2);
break; break;
case 2: case 2:
//lucore::LogInfo("storeWord(0x{:08x}, 0x{:08x})", addy, rs2);
bus->PokeWord(addy, rs2); bus->PokeWord(addy, rs2);
break; break;
default: default:
@ -207,7 +213,7 @@ namespace riscv {
} }
if(trapped) { if(trapped) {
rval = addy + RamImageOffset; rval = addy;// + RamImageOffset;
} }
break; break;
} }
@ -457,13 +463,13 @@ namespace riscv {
// rs1 -= MINIRV32_RAM_IMAGE_OFFSET; // rs1 -= MINIRV32_RAM_IMAGE_OFFSET;
rs1 -= RamImageOffset; //rs1 -= RamImageOffset;
// We don't implement load/store from UART or CLNT with RV32A here. // We don't implement load/store from UART or CLNT with RV32A here.
rval = bus->PeekWord(rs1); rval = bus->PeekWord(rs1);
if(trapped) { if(trapped) {
rval = rs1 + RamImageOffset; rval = rs1;// + RamImageOffset;
break; break;
} }
@ -523,6 +529,7 @@ namespace riscv {
break; break;
if(rdid) { if(rdid) {
//lucore::LogInfo("writing register {} -> 0x{:08x}", RegName(static_cast<Gpr>(rdid)), rval);
gpr[rdid] = rval; gpr[rdid] = rval;
} }

View File

@ -0,0 +1,36 @@
#include <riscv/System.hpp>
namespace riscv {
System* System::Create(Address ramSize) {
auto* system = new System;
system->bus = new Bus();
system->cpu = new CPU();
system->ram = new devices::RamDevice(0x80000000, ramSize);
system->clnt = new devices::ClntDevice();
system->syscon = new devices::SysconDevice(system);
system->cpu->Reset();
system->bus->AttachDevice(system->cpu);
system->bus->AttachDevice(system->clnt);
system->bus->AttachDevice(system->syscon);
system->bus->AttachDevice(system->ram);
return system;
}
System::~System() {
delete cpu;
delete bus; // the rest of the device pointers will be deleted by the bus.
}
void System::Step() {
// Clock the bus, it'll do everything else.
bus->Clock();
// Later: handling for invalid cases!
}
} // namespace riscv

View File

@ -1,5 +1,10 @@
//! A test harness for testing the riscv library. //! A test harness for testing if the riscv library actually works.
#include <riscv/System.hpp> #include <riscv/System.hpp>
#include <lucore/StdoutSink.hpp>
#include <cstdio> // I know, I know, but this is a test program. Yell later :)
#include <lucore/Assert.hpp>
#include <lucore/Logger.hpp>
/// simple 16550 UART implementation /// simple 16550 UART implementation
struct SimpleUartDevice : public riscv::Bus::MmioDevice { struct SimpleUartDevice : public riscv::Bus::MmioDevice {
@ -7,29 +12,55 @@ struct SimpleUartDevice : public riscv::Bus::MmioDevice {
riscv::Address Base() const override { return BASE_ADDRESS; } riscv::Address Base() const override { return BASE_ADDRESS; }
riscv::Address Size() const override { return 5; } riscv::Address Size() const override { return 12; } // for now
// TODO: emulate properly
u32 Peek(riscv::Address address) override { u32 Peek(riscv::Address address) override {
switch(address) { switch(address) {
case BASE_ADDRESS: case BASE_ADDRESS:
break; return 0x60; // active, but no keyboard input
case BASE_ADDRESS + 5: case BASE_ADDRESS + 5:
break; return '\0';
} }
return 0; return 0;
} }
void Poke(riscv::Address address, u32 value) override { void Poke(riscv::Address address, u32 value) override {
if(address == BASE_ADDRESS) { // write to data buffer if(address == BASE_ADDRESS) {
printf("%c\n", value); char c = value & 0x000000ff;
//lucore::LogInfo("[UART] got data buffer poke of char {:02x} @ pc 0x{:08x}", c, bus->GetCPU()->pc);
//printf("%c", c);
fputc(c, stderr);
} }
} }
}; };
int main() { int main(int argc, char** argv) {
auto system = riscv::System::WithMemory(128 * 1024); lucore::LoggerAttachStdout();
system->AddDeviceToBus(new SimpleUartDevice);
LUCORE_CHECK(argc == 2, "this test harness expects one argument (the file to load into riscv memory and execute). got {} arguments", argc);
// 128 KB of ram. Won't be enough to boot linux but should be good enough to test most baremetal apps
auto system = riscv::System::Create(128 * 1024);
// Attach our UART device
system->bus->AttachDevice(new SimpleUartDevice);
auto fp = std::fopen(argv[1], "rb");
LUCORE_CHECK(fp, "could not open file \"{}\"", argv[1]);
fseek(fp, 0, SEEK_END);
auto len = ftell(fp);
fseek(fp, 0, SEEK_SET);
fread(system->ram->Raw(), 1, len, fp);
fclose(fp);
// Do the thing now!
while(true) {
system->Step();
}
delete system;
return 0; return 0;
} }

View File

@ -0,0 +1,51 @@
PROJECT = test
# where your rv32 toolchain is
TCPATH = /home/lily/bin/riscv/bin
PREFIX = $(TCPATH)/riscv32-unknown-elf
CC = $(PREFIX)-gcc
CXX = $(PREFIX)-g++
CCFLAGS = -ffreestanding -fno-stack-protector
CCFLAGS += -static -static-libgcc -fdata-sections -ffunction-sections
CCFLAGS += -g -Os -march=rv32ima -mabi=ilp32
CXXFLAGS = $(CCFLAGS) -std=c++20 -fno-exceptions -fno-rtti
LDFLAGS = -T binary.ld -nostdlib -Wl,--gc-sections
OBJS := start.o main.o
.PHONY: all test clean
all: $(PROJECT).bin $(PROJECT).debug.txt
test: $(PROJECT).bin $(PROJECT).debug.txt
../../../../build/projects/riscv_test_harness/rvtest $<
clean:
rm $(PROJECT).elf $(PROJECT).bin $(PROJECT).debug.txt $(OBJS)
$(PROJECT).elf: $(OBJS)
$(CC) $(CCFLAGS) $(LDFLAGS) -o $@ $(OBJS)
$(PROJECT).bin : $(PROJECT).elf
$(PREFIX)-objcopy $^ -O binary $@
$(PROJECT).debug.txt : $(PROJECT).elf
$(PREFIX)-objdump -t $^ > $@
$(PREFIX)-objdump -S $^ >> $@
# Compile rules
%.o: %.cpp
$(CXX) -c $(CXXFLAGS) $< -o $@
%.o: %.c
$(CC) -c $(CCFLAGS) $< -o $@
%.o: %.S
$(CC) -x assembler-with-cpp -march=rv32ima -mabi=ilp32 -c $< -o $@

View File

@ -0,0 +1,5 @@
# what
This is a simple bare-metal riscv program that is able to run on the test harness that does some testing.
It is barebones, I know.

View File

@ -0,0 +1,73 @@
__heap_size = 0x1000;
__stack_size = 0x1000;
ENTRY(_start)
SECTIONS
{
. = 0x80000000;
.text : ALIGN(16) {
__TEXT_BEGIN__ = .;
*(.initial_jump)
*(.entry.text)
*(.init.literal)
*(.init)
*(.text)
*(.literal .text .literal.* .text.* .stub)
*(.out_jump.literal.*)
*(.out_jump.*)
__TEXT_END__ = .;
}
/* If we're on a newer compiler */
/DISCARD/ :
{
*(.interp)
*(.dynsym)
*(.dynstr)
*(.header)
} : phdr
.data : ALIGN(16) {
__DATA_BEGIN__ = .;
*(.rodata)
*(.rodata.*)
*(.gnu.linkonce.r.*)
*(.rodata1)
*(.dynsbss)
*(.gnu.linkonce.sb.*)
*(.scommon)
*(.gnu.linkonce.sb2.*)
*(.sbss)
*(.sbss.*)
*(.sbss2)
*(.sbss2.*)
*(.dynbss)
*(.data)
*(.data.*)
*(.got)
*(.got.*)
__DATA_END__ = .;
}
.bss : ALIGN( 16 ) {
__BSS_BEGIN__ = .;
*(.bss) /* Tricky: BSS needs to be allocated but not sent. GCC Will not populate these for calculating data size */
*(.bss.*)
__BSS_END__ = .;
}
.heap : ALIGN( 16 ) {
_sheap = .;
. = . + __heap_size;
_eheap = .;
}
.stack : ALIGN( 16 ) {
_estack = .;
. = . + __stack_size;
_sstack = .;
}
}

View File

@ -0,0 +1,54 @@
// a simple test program
#include <stdint.h>
uint32_t strlen(const char* str) {
if(!str)
return 0;
const char* c = str;
while(*c++)
;
return c - str;
}
#define UART_BASE 0x10000000
#define UART_DATA *(volatile uint32_t*)UART_BASE
#define UART_STATUS UART_DATA
void putc(char c) {
UART_DATA = (uint32_t)c;
}
__attribute__((noinline)) void puts(const char* str) {
const uint32_t length = strlen(str);
for(uint32_t i = 0; i < length; ++i)
putc(str[i]);
}
static uint32_t value = 0;
static uint16_t shortvalue = 0;
static uint8_t bytevalue = 0;
#define COUNTER_TEST(var, max) \
for(int i = 0; i < max; ++i) { \
puts(#var " is (before modification): "); \
putc("0123456789"[var]); \
putc('\n'); \
\
var = i; \
puts(#var " is (after modification): "); \
putc("0123456789"[var]); \
putc('\n'); \
}
void main() {
puts("hello world I guess\n");
#if 1
COUNTER_TEST(value, 9);
COUNTER_TEST(shortvalue, 9);
COUNTER_TEST(bytevalue, 9);
#endif
// loop forever
for(;;);
}

View File

@ -0,0 +1,13 @@
# Simple bare-metal RISCV startup code.
.section .initial_jump
.global _start
.extern main
.align 4
_start:
la sp, _sstack # set up C stack
addi sp,sp,-16 # ...
sw ra,12(sp) # ...
jal ra, main # jump to C code!