So... my CPU emulator/simulator is now functional!
At this point, it can simulate a 5-stage pipelined RISC-V core. The variant can be selected at init time, it currently supports RV32I/E, RV64I/E, and the M extension (RV32/RV64). The C extension is not quite finished, and then I intend on implementing most other standard extensions as well.
I have tested it with a few RISC-V test programs. After the first few very simple, hand-crafted assembly tests, I have written some start-up code and a linker script, so I can test C code. Current tests are emulating an environment with a 256KB instruction memory and 1MB data memory. The startup code: sets up the stack pointer, initializes data, zero-fills unitialized globals, calls 'main' and then I put an 'ebreak' instruction right after that. I've set up a stop condition in my simulator to stop execution on 'ebreak', so that's a simple setup to stop automatically once 'main' has returned.
Example code that simulates correctly (stripping out include/comments/...):
static char gst_szString[256];
static size_t StrLen(const char *szString)
{
size_t nLength;
if (szString == NULL)
return 0;
for (nLength = 0; *szString != '\0'; szString++)
nLength++;
return nLength;
}
static char * StrReverse(char *szStringDest, const char *szStringSrc)
{
size_t nLength, i, j;
if ((szStringDest == NULL) || (szStringSrc == NULL))
return NULL;
nLength = StrLen(szStringSrc);
for (i = 0, j = nLength - 1; i < nLength; i++, j--)
szStringDest[i] = szStringSrc[j];
szStringDest[nLength] = '\0';
return szStringDest;
}
int main(void)
{
(void) StrReverse(gst_szString, "This is a test string!");
return 0;
}
So, this basically stores a reversed version of some constant string in a global variable. The simulator dumps data memory content in a file once it's done, so I can check that it correctly stored "!gnirts tset a si sihT".
The simulator CLI outputs this for the above at this point (yes as you can see it runs on Windows here, but it's pure C99 code, so it builds and runs fine on any Posix-compliant platform):
CPUEmu: CPUEmu-RISCV (0.1.1) on CPUEmu (0.1.0)
CPU Variant: RV32IM
Binary file '..\..\Tests\RISCV_Code\GCC-Build\Test2.bin' loaded.
Running simulation...
Simulation completed in 0.000015 s.
Clock Counter = 664
Instruction Counter = 442
CPI = 1.502
STOP: Stop on Instruction (Num = 1)
Registers:
x0 0x00000000
x1 0x00000058
x2 0x10100000
x3 0x00000000
x4 0x00000000
x5 0x00000000
x6 0x00000000
x7 0x00000000
x8 0x00000000
x9 0x00000000
x10 0x00000000
x11 0x10000016
x12 0x00000054
x13 0x10000016
x14 0x000000C7
x15 0x10000016
x16 0x00000000
x17 0x00000000
x18 0x00000000
x19 0x00000000
x20 0x00000000
x21 0x00000000
x22 0x00000000
x23 0x00000000
x24 0x00000000
x25 0x00000000
x26 0x00000000
x27 0x00000000
x28 0x00000000
x29 0x00000000
x30 0x00000000
x31 0x00000000
pc 0x0000005C
(About half of all executed instructions are in the startup code, as it zero-fills the 256-byte string global.)
The CPI at about 1.5 is not extraordinary but it's not too bad for a first pipeline version. I think branches are the culprit here, as I haven't implemented branch prediction so far, so it's just wasting 2 cycles each time a branch is taken, and this small test code is almost entirely made up of loops.
So now I'm going to write many tests for it. (I'm also going to test Bruce's benchmark shortly.) If anyone has any example code to share that I could test, that'd be cool as well.