Initial commit

This commit is contained in:
2025-05-27 14:39:37 +02:00
commit 2132a915a5
7 changed files with 786 additions and 0 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
# Top-most EditorConfig file
root = true
# Use same style for all files
[*]
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
.dub
docs.json
__dummy.html
docs/
/riscd
riscd.so
riscd.dylib
riscd.dll
riscd.a
riscd.lib
riscd-test-*
*.exe
*.pdb
*.o
*.obj
*.lst

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# RISC-D
A simple RISC-V emulator written in D.

6
dub.sdl Normal file
View File

@ -0,0 +1,6 @@
name "riscd"
description "A simple RISC-V emulator"
authors "Tomas"
copyright "Copyright © 2025, Tomas"
license "MIT"
targetType "staticLibrary"

493
source/riscd/cpu.d Normal file
View File

@ -0,0 +1,493 @@
module riscd.cpu;
import riscd.memory;
import std.traits;
private enum OPCODE
{
// Referenced from:
// The RISC-V Instruction Set Manual Volume I: Unprivileged Architecture
// Chapter 34. RV32/64G Instruction Set Listings
// 00
LOAD = 0b0000011,
LOAD_FP = 0b0000111,
CUSTOM0 = 0b0001011,
MISCMEM = 0b0001111,
OPIMM = 0b0010011,
AUIPC = 0b0010111,
OPIMM32 = 0b0011011,
// 01
STORE = 0b0100011,
STORE_FP = 0b0100111,
CUSTOM1 = 0b0101011,
AMO = 0b0101111,
OP = 0b0110011,
LUI = 0b0110111,
OP32 = 0b0111011,
// 10
MADD = 0b1000011,
MSUB = 0b1000111,
NMSUB = 0b1001011,
NMADD = 0b1001111,
OP_FP = 0b1010011,
OP_V = 0b1010111,
CUSTOM2 = 0b1011011,
// 11
BRANCH = 0b1100011,
JALR = 0b1100111,
_RESERVED = 0b1101011,
JAL = 0b1101111,
SYSTEM = 0b1110011,
OP_VE = 0b1110111,
CUSTOM3 = 0b1111011,
}
enum Cause
{
MISALIGNED_FETCH = 0x00,
FETCH_ACCESS = 0x01,
ILLEGAL_INSTRUCTION = 0x02,
BREAKPOINT = 0x03,
MISALIGNED_LOAD = 0x04,
LOAD_ACCESS = 0x05,
MISALIGNED_STORE = 0x06,
STORE_ACCESS = 0x07,
USER_ECALL = 0x08,
SUPERVISOR_ECALL = 0x09,
VIRTUAL_SUPERVISOR_ECALL = 0x0A,
MACHINE_ECALL = 0x0B,
FETCH_PAGE_FAULT = 0x0C,
LOAD_PAGE_FAULT = 0x0D,
STORE_PAGE_FAULT = 0x0F,
DOUBLE_TRAP = 0x10,
SOFTWARE_CHECK_FAULT = 0x12,
HARDWARE_ERROR_FAULT = 0x13,
FETCH_GUEST_PAGE_FAULT = 0x14,
LOAD_GUEST_PAGE_FAULT = 0x15,
VIRTUAL_INSTRUCTION = 0x16,
STORE_GUEST_PAGE_FAULT = 0x17,
}
private T signExtend(T)(T num, uint bits) @safe @nogc pure nothrow
{
alias U = Unsigned!T;
U one = 1;
bool ngt = (num & (one << (bits-1))) != 0;
if (ngt)
return (U.max >> bits << bits) | num;
else
return (U.max >> (U.sizeof*8 - bits)) & num;
}
static assert(signExtend!int(0b0110, 4) == 6);
static assert(signExtend!int(0b1010, 4) == -6);
class CPU
{
@safe:
public:
alias WORD = uint;
enum XLEN = WORD.sizeof;
this(Memory memory)
{
this.memory = memory;
}
void tick()
{
static assert(WORD.sizeof == 4, "Only 32 bit architecture is supported");
// Zero register
x[0] = 0;
// Jumped internal flag
jumped = false;
// Common variables
bool mems; // Memory read/write status
// Fetch instruction
WORD inst = fetch(pc);
// Ensure instruction is 32bit
if (!((inst & 0b11) == 0b11 && (inst & 0b11111) != 0b11111))
{
// It is not!
exception(Cause.ILLEGAL_INSTRUCTION);
}
if (pc & 3)
exception(Cause.MISALIGNED_FETCH);
// Decode instruction
ubyte opcode = inst & 0x7F;
ubyte rd = (inst >> 7) & 0x1F;
ubyte funct3 = (inst >> 12) & 0x07;
ubyte funct7 = (inst >> 25) & 0x7F;
ubyte rs1 = (inst >> 15) & 0x1F;
ubyte rs2 = (inst >> 20) & 0x1F;
WORD i_imm = (inst >> 20) & 0xFFF;
WORD u_imm = inst & 0xFFFFF000;
WORD s_imm;
s_imm |= (inst >> 7) & 0x0001F; // [4:0]
s_imm |= (inst >> 20) & 0x00FE0; // [11:5]
WORD b_imm;
b_imm |= (inst >> 7) & 0x0001E; // [4:1]
b_imm |= (inst >> 20) & 0x007E0; // [10:5]
b_imm |= (inst << 4) & 0x00800; // [11]
b_imm |= (inst >> 19) & 0x01000; // [12]
WORD j_imm;
j_imm |= (inst >> 20) & 0x0007FE; // [10:1]
j_imm |= (inst >> 9) & 0x000800; // [11]
j_imm |= inst & 0x0FF000; // [19:12]
j_imm |= (inst >> 11) & 0x100000; // [20]
WORD i_immex = signExtend!WORD(i_imm, 12);
WORD s_immex = signExtend!WORD(s_imm, 12);
WORD b_immex = signExtend!WORD(b_imm, 12);
WORD u_immex = signExtend!WORD(u_imm, 20);
WORD j_immex = signExtend!WORD(j_imm, 20);
// Debug logging
debug
{
import std.stdio;
writeln();
writefln(
"\x1B[35m%06X\x1B[0m: \x1B[96m%s\x1B[0m, funct7: \x1B[33m%d\x1B[0m, funct3: \x1B[33m%d\x1B[0m " ~
"rs2: \x1B[36m%s\x1B[0m, rs1: \x1B[36m%s\x1B[0m, rd: \x1B[91m%d\x1B[0m",
pc,
cast(OPCODE)opcode,
funct7,
funct3,
rs2,
rs1,
rd,
);
auto binstr = (WORD num, int bits){
import std.format;
string img = "";
foreach (c; format("%0*b", bits, num))
if (c=='1')
img ~= "\x1B[32m1";
else
img ~= "\x1B[31m0";
return img~"\x1B[0m";
};
writefln("I-IMM : %s | \x1B[94m%d\x1B[0m", binstr(i_imm&0xFFF, 12), cast(int)i_immex);
writefln("S-IMM : %s | \x1B[94m%d\x1B[0m", binstr(s_imm&0xFFF, 12), cast(int)s_immex);
writefln("B-IMM : %s | \x1B[94m%d\x1B[0m", binstr(b_imm&0xFFF, 12), cast(int)b_immex);
writefln("U-IMM : %s | \x1B[94m%d\x1B[0m >> %d", binstr(u_imm&0xFFFFF, 20), cast(int)u_immex, cast(int)u_immex >> 12);
writefln("J-IMM : %s | \x1B[94m%d\x1B[0m", binstr(j_imm&0xFFFFF, 20), cast(int)j_immex);
}
// Do instruction
with (OPCODE) switch (opcode)
{
case OPIMM:
switch (funct3)
{
case 0: // ADDI
x[rd] = x[rs1] + i_immex;
break;
case 1: // SLLI
x[rd] = x[rs1] << (i_imm & 0x1F);
break;
case 2: // SLTI
x[rd] = cast(Signed!WORD)x[rs1] < cast(Signed!WORD)i_immex;
break;
case 3: // SLTU
x[rd] = x[rs1] < i_immex;
break;
case 4: // XORI
x[rd] = x[rs1] ^ i_immex;
break;
case 5: // SRLI/SRAI
if ((i_imm & 0x400) == 0) // SRLI
x[rd] = x[rs1] >> (i_imm & 0x1F);
else // SRAI
x[rd] = cast(Signed!WORD)x[rs1] >> (i_imm & 0x1F);
break;
case 6: // ORI
x[rd] = x[rs1] | i_immex;
break;
case 7: // ANDI
x[rd] = x[rs1] & i_immex;
break;
default:
exception(Cause.ILLEGAL_INSTRUCTION);
}
break;
case OP:
switch (funct3)
{
case 0: // ADD
if (funct7 == 0)
x[rd] = x[rs1] + x[rs2];
else if (funct7 == 32)
x[rd] = x[rs1] - x[rs2];
else
goto default;
break;
case 1: // SLL
if (funct7 == 0)
x[rd] = x[rs1] << (x[rs2] & 0x1F);
else
goto default;
break;
case 2: // SLT
if (funct7 == 0)
x[rd] = cast(Signed!WORD)x[rs1] < cast(Signed!WORD)x[rs2];
else
goto default;
break;
case 3: // SLTU
if (funct7 == 0)
x[rd] = x[rs1] < x[rs2];
else
goto default;
break;
case 4: // XOR
if (funct7 == 0)
x[rd] = x[rs1] ^ x[rs2];
else
goto default;
break;
case 5: // SRL/SRA
if (funct7 == 0) // SRL
x[rd] = x[rs1] >> (x[rs2] & 0x1F);
else if (funct7 == 32) // SRA
x[rd] = cast(Signed!WORD)x[rs1] >> (x[rs2] & 0x1F);
else
goto default;
break;
case 6: // OR
if (funct7 == 0)
x[rd] = x[rs1] | x[rs2];
else
goto default;
break;
case 7: // AND
if (funct7 == 0)
x[rd] = x[rs1] & x[rs2];
else
goto default;
break;
default:
exception(Cause.ILLEGAL_INSTRUCTION);
}
break;
case LUI:
x[rd] = u_imm >> 12;
break;
case AUIPC:
pc += u_imm >> 12;
x[rd] = pc;
break;
case JAL:
x[rd] = pc + 4;
jump(pc + cast(Signed!WORD)j_immex);
break;
case JALR:
if (funct3)
exception(Cause.ILLEGAL_INSTRUCTION);
x[rd] = pc + 4;
jump((x[rs1] + cast(Signed!WORD)i_immex) << 1 >> 1);
break;
case BRANCH:
switch (funct3)
{
case 0: // BEQ
if (x[rs1] == x[rs2])
jump(pc + cast(Signed!WORD)b_immex);
break;
case 1: // BNE
if (x[rs1] != x[rs2])
jump(pc + cast(Signed!WORD)b_immex);
break;
case 4: // BLT
if (cast(Signed!WORD)x[rs1] < cast(Signed!WORD)x[rs2])
jump(pc + cast(Signed!WORD)b_immex);
break;
case 5: // BGE
if (cast(Signed!WORD)x[rs1] > cast(Signed!WORD)x[rs2])
jump(pc + cast(Signed!WORD)b_immex);
break;
case 6: // BLTU
if (x[rs1] < x[rs2])
jump(pc + cast(Signed!WORD)b_immex);
break;
case 7: // BGEU
if (x[rs1] > x[rs2])
jump(pc + cast(Signed!WORD)b_immex);
break;
default:
exception(Cause.ILLEGAL_INSTRUCTION);
break;
}
break;
case LOAD:
switch (funct3)
{
case 0: // LB
x[rd] = load!byte(x[rs1] + cast(Signed!WORD)i_immex);
break;
case 1: // LH
x[rd] = load!short(x[rs1] + cast(Signed!WORD)i_immex);
break;
case 2: // LW
x[rd] = load!int(x[rs1] + cast(Signed!WORD)i_immex);
break;
case 4: // LBU
x[rd] = load!ubyte(x[rs1] + cast(Signed!WORD)i_immex);
break;
case 5: // LHU
x[rd] = load!ushort(x[rs1] + cast(Signed!WORD)i_immex);
break;
default:
exception(Cause.ILLEGAL_INSTRUCTION);
break;
}
break;
case STORE:
switch (funct3)
{
case 0: // SB
store!ubyte(x[rs1] + s_immex, cast(ubyte)x[rs2]);
break;
case 1: // SH
store!ushort(x[rs1] + s_immex, cast(ushort)x[rs2]);
break;
case 2: // SW
store!uint(x[rs1] + s_immex, cast(uint)x[rs2]);
break;
default:
exception(Cause.ILLEGAL_INSTRUCTION);
break;
}
break;
case MISCMEM:
switch (funct3)
{
case 0: // FENCE
if (rs1 != 0 || rd != 0)
exception(Cause.ILLEGAL_INSTRUCTION);
if (i_imm ^ 0x0FF) // && i_imm ^ 0x8FF)
exception(Cause.ILLEGAL_INSTRUCTION);
fence();
break;
default:
exception(Cause.ILLEGAL_INSTRUCTION);
}
break;
case SYSTEM:
switch (funct3)
{
case 0: // PRIV
if (rs1 != 0 || rd != 0)
exception(Cause.ILLEGAL_INSTRUCTION);
switch (i_imm)
{
case 0: // ECALL
exception(Cause.MACHINE_ECALL);
break;
case 1: // EBREAK
exception(Cause.BREAKPOINT);
break;
default:
exception(Cause.ILLEGAL_INSTRUCTION);
}
break;
default:
exception(Cause.ILLEGAL_INSTRUCTION);
break;
}
break;
default:
exception(Cause.ILLEGAL_INSTRUCTION);
break;
}
// Increment program counter
if (!jumped)
pc += XLEN;
}
void exception(Cause cause)
{
import std.conv;
throw new Exception(cause.to!string);
}
void interrupt(Cause cause)
{
import std.conv;
throw new Exception(cause.to!string);
}
void fence()
{
}
WORD[32] x;
WORD pc;
Memory memory;
private:
void jump(WORD addr)
{
pc = addr;
jumped = true;
}
WORD fetch(WORD addr)
{
WORD value;
if (!memory.get!WORD(addr, value))
exception(Cause.FETCH_ACCESS);
return value;
}
T load(T)(WORD addr)
{
T value;
if (!memory.get!T(addr, value))
exception(Cause.LOAD_ACCESS);
return value;
}
T store(T)(WORD addr, T value)
{
if (!memory.set!T(addr, value))
exception(Cause.STORE_ACCESS);
return value;
}
bool jumped = false;
}

254
source/riscd/memory.d Normal file
View File

@ -0,0 +1,254 @@
module riscd.memory;
import std.format;
import std.file : readFile = read;
import std.traits;
interface MemoryDevice
{
@safe:
bool get(size_t addr, ref ubyte value);
bool set(size_t addr, ubyte value);
}
class RAM : MemoryDevice
{
@safe:
public:
this(size_t size)
{
data.length = size;
}
this(void[] data, bool copy = true) @system
{
if (copy)
this.data = cast(ubyte[]) data.dup;
else
this.data = cast(ubyte[]) data;
}
static RAM loadFile(string filename) @system
{
return new RAM(readFile(filename), false);
}
bool get(size_t addr, ref ubyte value) @nogc nothrow pure
{
if (data.length <= addr)
return false;
value = data[addr];
return true;
}
bool set(size_t addr, ubyte value) @nogc nothrow pure
{
if (data.length <= addr)
return false;
data[addr] = value;
return true;
}
protected:
ubyte[] data;
}
class ROM : MemoryDevice
{
@safe:
public:
this(const(void)[] data, bool copy = true)
{
if (copy)
this.data = cast(const(ubyte)[]) data.idup;
else
this.data = cast(const(ubyte)[]) data;
}
static ROM loadFile(string filename)
{
return new ROM(readFile(filename), false);
}
bool get(size_t addr, ref ubyte value) @nogc nothrow pure
{
if (data.length <= addr)
return false;
value = data[addr];
return true;
}
bool set(size_t addr, ubyte value) @nogc nothrow pure
{
return false;
}
protected:
const(ubyte)[] data;
}
class MMIO : MemoryDevice
{
@safe:
public:
this()
{
}
void map(MemoryDevice device, size_t origin, size_t length)
{
// Check for colliding mappings
foreach (mapping; mappings)
if (
// Is mapping.origin inside new mapping?
(mapping.origin > origin && mapping.origin < origin + length)
||
// Is origin inside existing mapping?
(origin > mapping.origin && origin < mapping.origin + mapping.length)
)
// We collide, throw
throw new Exception(
format("Trying to map %p over existing %p", origin, mapping.origin)
);
// No collisons? Cool, craete a new mapping
mappings ~= MemoryMapping(device, origin, length);
}
void unmap(size_t origin)
{
size_t index = findMapping(origin);
// Could not find the mapping
if (index == -1)
throw new Exception(format("Nothing mapped at %p", origin));
if (mappings.length != 1)
mappings[index] = mappings[$];
mappings.length -= 1;
}
bool get(size_t addr, ref ubyte value)
{
size_t index = findMapping(addr);
if (index == -1)
return false;
return mappings[index].device.get( addr - mappings[index].origin, value);
}
bool set(size_t addr, ubyte value)
{
size_t index = findMapping(addr);
if (index == -1)
return false;
return mappings[index].device.set( addr - mappings[index].origin, value);
}
protected:
struct MemoryMapping
{
MemoryDevice device;
size_t origin, length;
}
size_t findMapping(size_t origin) @nogc pure nothrow
{
// Is there only one mapping, and it's the correct one?
if (mappings.length == 1 && mappings[0].origin == origin)
return 0;
// Look for the mapping
for (size_t i = 0; i < mappings.length; i++)
if (mappings[i].origin == origin)
return i;
// Could not find the mapping
return -1;
}
MemoryMapping[] mappings;
}
// TODO: Add endianess support
class Memory
{
@safe:
public:
this(MemoryDevice device)
{
this.device = device;
}
bool get(T)(size_t addr, ref T value) @trusted
if (isBasicType!T)
{
return get(addr, (&value)[0..1]);
}
bool set(T)(size_t addr, T value) @trusted
if (isBasicType!T)
{
return set(addr, (&value)[0..1]);
}
bool get(T)(size_t addr, ref T value) @system
if (!isBasicType!T)
{
return get(addr, (&value)[0..1]);
}
bool set(T)(size_t addr, T value) @system
if (!isBasicType!T)
{
return set(addr, (&value)[0..1]);
}
bool get(size_t addr, void[] buffer) @system
{
ubyte[] buff = cast(ubyte[])buffer;
for (size_t i = 0; i < buff.length; i++)
if (!get(addr+i, buff[i]))
return false;
return true;
}
bool set(size_t addr, const(void)[] buffer) @system
{
const(ubyte)[] buff = cast(const(ubyte)[])buffer;
for (size_t i = 0; i < buff.length; i++)
if (!set(addr+i, buff[i]))
return false;
return true;
}
bool get(size_t addr, ref ubyte value)
{
return device.get(addr, value);
}
bool set(size_t addr, ubyte value)
{
return device.set(addr, value);
}
protected:
MemoryDevice device;
}

4
source/riscd/package.d Normal file
View File

@ -0,0 +1,4 @@
module riscd;
public import riscd.cpu;
public import riscd.memory;