Compare commits

...

2 Commits

Author SHA1 Message Date
Lily Tsuru 7af85f5601 riscv: misc code cleanup (now I can actually focus on stuff!) 2023-07-24 00:20:40 -04:00
Lily Tsuru 878990a921 riscv: IT WORKS
finally. once i'm sure it's 100% working i can probably like, develop this addon finally
2023-07-24 00:01:39 -04:00
15 changed files with 398 additions and 100 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
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.
struct CPU : Bus::Device {
BasicType Type() const override { return BasicType::Cpu; }
bool Clocked() const override { return true; }
void Clock() override;
@ -15,6 +16,17 @@ namespace riscv {
void TimerInterrupt();
constexpr CPU() {
Reset();
}
constexpr void Reset() {
// Initalize some state. We're cool like that :)
pc = 0x80000000;
gpr[Gpr::A0] = 0x0; // HART id
extraflags |= 3; // Start in Machine mode
}
// TODO: Handlers for CSR read/write (if we need it?)
/// CPU state
@ -45,9 +57,7 @@ namespace riscv {
/// Set by [CPU::Trap] for the trap code.
u32 trapCode { 0 };
u32 Step(u32 instCount);
// todo: counters for chrono/inst count.
void Step(u32 instCount);
};
} // namespace riscv

View File

@ -1,5 +1,6 @@
#pragma once
#include <riscv/Types.hpp>
#include <string_view>
namespace riscv {
@ -25,41 +26,78 @@ namespace riscv {
};
enum class Gpr : u8 {
X0, // zero
X1,
X2,
X3,
X4,
X5,
X6,
X7,
X8,
X9,
X10,
X11,
X12,
X13,
X14,
X15,
X16,
X17,
X18,
X19,
X20,
X21,
X22,
X23,
X24,
X25,
X26,
X27,
X28,
X29,
X30,
X31,
PC
Zero,
Ra,
Sp,
Gp,
Tp,
T0,
T1,
T2,
S0, // also `fp`
S1,
A0,
A1,
A2,
A3,
A4,
A5,
A6,
A7,
S2,
S3,
S4,
S5,
S6,
S7,
S8,
S9,
S10,
S11,
T3,
T4,
T5,
T6
};
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.
/// Pretty cool, huh?
struct GeneralPurposeRegisters {

View File

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

View File

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

View File

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

View File

@ -14,6 +14,8 @@ namespace riscv {
if(!device)
return false;
device->Attached(this);
if(device->IsA<CPU*>()) {
// Return early to avoid putting the CPU pointer inside the devices vector.
// We do not actually own the CPU.
@ -119,7 +121,7 @@ namespace riscv {
}
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
// We can shorcut region checking if the requested addess matches base address.

View File

@ -1,16 +1,12 @@
//! Portions of this code are copyright 2022 Charles Lohr (CNLohr).
//! Portions of this code are copyright 2022 Charles Lohr (CNLohr),
//! from [mini-rv32ima](https://github.com/cnlohr/mini-rv32ima).
#include <riscv/Bus.hpp>
#include <riscv/CPU.hpp>
#include "riscv/CPUTypes.hpp"
namespace riscv {
constexpr static Address RamImageOffset = 0x80000000;
void CPU::Clock() {
// do the thing
Step(1024);
}
@ -36,39 +32,38 @@ namespace riscv {
// trapCode = 0x80000007;
}
u32 CPU::Step(u32 instCount) {
void CPU::Step(u32 instCount) {
auto interruptsInFlight = [&]() {
return (mip & (1 << 7) /*|| mip & (1 << 11)*/) && (mie & (1 << 7) /*|| mie & (1 << 11)*/) && (mstatus & 0x8 /*mie*/);
};
// Don't run if waiting for an interrupt
if(extraflags & 4)
return 1;
return;
u32 rdid = 0;
u32 rval = 0;
u32 pc = this->pc;
u32 cycle = this->cyclel;
if(interruptsInFlight()) {
Trap(0x80000007);
} else {
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++;
if(ofs_pc & 3) {
if((pc & 3)) {
Trap(TrapCode::InstructionAddressMisaligned);
break;
} else {
auto ir = bus->PeekWord(ofs_pc);
auto ir = bus->PeekWord(pc);
if(trapped) {
// 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
// space is emulated using the [Bus] class, so there is no heap write issue
// that could be caused by leaving it unbound).
Trap(TrapCode::InstructionAccessFault);
rval = ofs_pc + RamImageOffset;
rval = pc;
break;
}
@ -133,12 +128,12 @@ namespace riscv {
break;
case 6: // BLTU
if((uint32_t)rs1 < (uint32_t)rs2)
if((u32)rs1 < (u32)rs2)
pc = immm4;
break;
case 7: // BGEU
if((uint32_t)rs1 >= (uint32_t)rs2)
if((u32)rs1 >= (u32)rs2)
pc = immm4;
break;
default:
@ -153,8 +148,6 @@ namespace riscv {
i32 imm_se = imm | ((imm & 0x800) ? 0xfffff000 : 0);
u32 rsval = rs1 + imm_se;
rsval -= RamImageOffset;
switch((ir >> 12) & 0x7) {
// LB, LH, LW, LBU, LHU
case 0:
@ -178,7 +171,7 @@ namespace riscv {
}
if(trapped) {
rval = rsval + RamImageOffset;
rval = rsval; // + RamImageOffset;
}
break;
}
@ -188,7 +181,7 @@ namespace riscv {
u32 addy = ((ir >> 7) & 0x1f) | ((ir & 0xfe000000) >> 20);
if(addy & 0x800)
addy |= 0xfffff000;
addy += rs1 - RamImageOffset;
addy += rs1;
rdid = 0;
switch((ir >> 12) & 0x7) {
@ -200,6 +193,7 @@ namespace riscv {
bus->PokeShort(addy, rs2);
break;
case 2:
// lucore::LogInfo("storeWord(0x{:08x}, 0x{:08x})", addy, rs2);
bus->PokeWord(addy, rs2);
break;
default:
@ -207,7 +201,7 @@ namespace riscv {
}
if(trapped) {
rval = addy + RamImageOffset;
rval = addy; // + RamImageOffset;
}
break;
}
@ -417,7 +411,7 @@ namespace riscv {
mstatus |= 8; // Enable interrupts
extraflags |= 4; // Set inernal WFI bit
this->pc = pc + 4;
return 1;
return;
} else if(((csrno & 0xff) == 0x02)) { // MRET
// https://raw.githubusercontent.com/riscv/virtual-memory/main/specs/663-Svpbmt.pdf
// Table 7.6. MRET then in mstatus/mstatush sets MPV=0, MPP=0,
@ -455,15 +449,9 @@ namespace riscv {
u32 rs2 = gpr[(ir >> 20) & 0x1f];
u32 irmid = (ir >> 27) & 0x1f;
// rs1 -= MINIRV32_RAM_IMAGE_OFFSET;
rs1 -= RamImageOffset;
// We don't implement load/store from UART or CLNT with RV32A here.
rval = bus->PeekWord(rs1);
if(trapped) {
rval = rs1 + RamImageOffset;
rval = rs1; // + RamImageOffset;
break;
}
@ -522,10 +510,8 @@ namespace riscv {
if(trapped)
break;
if(rdid) {
if(rdid)
gpr[rdid] = rval;
}
pc += 4;
}
}
@ -543,13 +529,11 @@ namespace riscv {
else
mtval = pc;
}
mepc = pc; // TRICKY: The kernel advances mepc automatically.
// CSR( mstatus ) & 8 = MIE, & 0x80 = MPIE
// On an interrupt, the system moves current MIE into MPIE
mepc = pc; // Interrupt handler will advance mepc
mstatus = (mstatus & 0x08) << 4 | ((extraflags)&3) << 11;
pc = (mtvec - 4);
// If trapping, always enter machine mode.
// Always enter machine mode when trapping.
extraflags |= 3;
// Reset trap flags
@ -561,8 +545,6 @@ namespace riscv {
if(cyclel > cycle)
cycleh++;
cyclel = cycle;
pc = pc;
return 0;
}
} // namespace riscv

View File

@ -0,0 +1,39 @@
#include <riscv/System.hpp>
namespace riscv {
System* System::Create(Address ramSize) {
auto* system = new System;
// create all the devices we require.
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);
// techinically this is done on construction but lets be hard about it
system->cpu->Reset();
// attach everything into the bus
if(!system->bus->AttachDevice(system->cpu)) return nullptr;
if(!system->bus->AttachDevice(system->clnt)) return nullptr;
if(!system->bus->AttachDevice(system->syscon)) return nullptr;
if(!system->bus->AttachDevice(system->ram)) return nullptr;
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 <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
struct SimpleUartDevice : public riscv::Bus::MmioDevice {
@ -7,29 +12,56 @@ struct SimpleUartDevice : public riscv::Bus::MmioDevice {
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 {
switch(address) {
case BASE_ADDRESS:
break;
return 0x60; // active, but no keyboard input
case BASE_ADDRESS + 5:
break;
return '\0';
}
return 0;
}
void Poke(riscv::Address address, u32 value) override {
if(address == BASE_ADDRESS) { // write to data buffer
printf("%c\n", value);
if(address == BASE_ADDRESS) {
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() {
auto system = riscv::System::WithMemory(128 * 1024);
system->AddDeviceToBus(new SimpleUartDevice);
int main(int argc, char** argv) {
lucore::LoggerAttachStdout();
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);
LUCORE_CHECK(system, "could not create system for some reason.");
// 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;
}

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!