From 39288a6d59759b1a57ba00b845a01c7973f37c09 Mon Sep 17 00:00:00 2001 From: m8pple Date: Thu, 16 Oct 2014 13:44:26 +0100 Subject: Initial push. --- include/mips.h | 11 ++++ include/mips_core.h | 67 ++++++++++++++++++++ include/mips_cpu.h | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++++ include/mips_mem.h | 155 +++++++++++++++++++++++++++++++++++++++++++++ include/mips_test.h | 150 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 560 insertions(+) create mode 100644 include/mips.h create mode 100644 include/mips_core.h create mode 100644 include/mips_cpu.h create mode 100644 include/mips_mem.h create mode 100644 include/mips_test.h (limited to 'include') diff --git a/include/mips.h b/include/mips.h new file mode 100644 index 0000000..c6306e9 --- /dev/null +++ b/include/mips.h @@ -0,0 +1,11 @@ +/*! \file mips.h + Main include file for all the simulation and test related files. +*/ +#ifndef mips_header +#define mips_header + +#include "mips_mem.h" +#include "mips_cpu.h" +#include "mips_test.h" + +#endif diff --git a/include/mips_core.h b/include/mips_core.h new file mode 100644 index 0000000..0733333 --- /dev/null +++ b/include/mips_core.h @@ -0,0 +1,67 @@ +/*! \file mips_core.h + Establishes central types and definitions used within the simulator and system. +*/ +#ifndef mips_core_header +#define mips_core_header + +/*! \file + The header gives us types of a known width and signedness, like + uint32_t and int16_t. These types can be very useful for + matching the internal types of a processor, and for managing + conversions from signed to unsigned types. The C/C++ standard has certain + (very strict) rules for conversion between types, which are different from + the rules of any particular CPU. In an extremely legalitic interpretation, the + integer encoding is not necessarily twos complement, but we + will mandate that all target architectures use a twos complement + representation. +*/ +#include + +#include +#include + +/* This allows the header to be used from both C and C++, so +programs can be written in either (or both) languages. */ +#ifdef __cplusplus +extern "C"{ +#endif + +/*! This is a list of errors used within the cpu simulator to indicate + various things which could go wrong. Some of those come from within + the implementation of the simulator, while others will arise due to + the execution of the program within the simulator. +*/ +typedef enum _mips_error{ + mips_Success =0, + + //! Error from within the simulator. + ///@{ + mips_ErrorNotImplemented=0x1000, + mips_ErrorInvalidArgument=0x1001, + mips_ErrorInvalidHandle=0x1002, + mips_ErrorFileReadError=0x1003, + mips_ErrorFileWriteError=0x1004, + ///@} + + //! Error or exception from the simulated processor or program. + ///@{ + mips_ExceptionBreak=0x2000, + mips_ExceptionInvalidAddress=0x2001, + mips_ExceptionInvalidAlignment=0x2002, + mips_ExceptionAccessViolation=0x2003, + mips_ExceptionInvalidInstruction=0x2004, + mips_ExceptionArithmeticOverflow=0x2005, + ///@} + + /*! This is an extension point for implementations. Codes + at this number and above are available for the + implementation, but are not generally understood. + They shouldn't be exposed over public APIs. */ + mips_InternalError=0x3000 +}mips_error; + +#ifdef __cplusplus +}; +#endif + +#endif diff --git a/include/mips_cpu.h b/include/mips_cpu.h new file mode 100644 index 0000000..117d869 --- /dev/null +++ b/include/mips_cpu.h @@ -0,0 +1,177 @@ +/*! \file mips_cpu.h + +*/ +#ifndef mips_cpu_header +#define mips_cpu_header + +#include "mips_mem.h" + +#ifdef __cplusplus +extern "C"{ +#endif + +/*! \defgroup mips_cpu CPU API + \addtogroup mips_cpu + @{ +*/ + + +/*! Represents the state of a cpu. + + This another opaque data type, similar to \ref mips_mem_provider. + + \struct mips_cpu_impl +*/ +struct mips_cpu_impl; + +/*! An opaque handle to a mips. + + This represents a handle to a data-type that clients can use, without + knowing how the CPU is implemented. See \ref mips_mem_h for more + commentary. +*/ +typedef struct mips_cpu_impl *mips_cpu_h; + + +/*! Creates and initialises a new CPU instance. + + The CPU should be bound to the given + \ref mips_mem_core "memory space", and have all registers set + to zero. The memory is not owned by the CPU, so it should not + be \ref mips_mem_free "freed" when the CPU is \ref mips_cpu_free "freed". + + \param mem The memory space the processor is connected to; think of it + as the address bus to which the CPU has been wired. +*/ +mips_cpu_h mips_cpu_create(mips_mem_h mem); + +/*! Reset the CPU as if it had just been created, with all registers zerod. + However, it should not modify RAM. Imagine this as asserting the reset + input of the CPU core. +*/ +mips_error mips_cpu_reset(mips_cpu_h state); + +/*! Returns the current value of one of the 32 general purpose MIPS registers */ +mips_error mips_cpu_get_register( + mips_cpu_h state, //!< Valid (non-empty) handle to a CPU + unsigned index, //!< Index from 0 to 31 + uint32_t *value //!< Where to write the value to +); + +/*! Modifies one of the 32 general purpose MIPS registers. */ +mips_error mips_cpu_set_register( + mips_cpu_h state, //!< Valid (non-empty) handle to a CPU + unsigned index, //!< Index from 0 to 31 + uint32_t value //!< New value to write into register file +); + +/*! Sets the program counter for the next instruction to the specified byte address. + + While this sets the value of the PC, it should not cause any kind of + execution to happen. Once you look at branches in detail, you will + see that there is some slight ambiguity about this function. Choose the + only option that makes sense. +*/ +mips_error mips_cpu_set_pc( + mips_cpu_h state, //!< Valid (non-empty) handle to a CPU + uint32_t pc //!< Address of the next instruction to exectute. +); + +/*! Gets the pc for the next instruction. */ +mips_error mips_cpu_get_pc(mips_cpu_h state, uint32_t *pc); + +/*! Advances the processor by one instruction. + + If an exception or error occurs, the CPU and memory state + should be left unchanged. This is so that the user can + inspect what happened and find out what went wrong. So + this should be true: + + uint32_t pc=mips_cpu_get_pc(cpu); + mips_error err=mips_cpu_step(cpu); + if(err!=mips_Success){ + assert(mips_cpu_get_pc(cpu)==pc); + assert(mips_cpu_step(cpu)==err); + } + + Maintaining this property in all cases is actually pretty + difficult, so _try_ to maintain it, but don't worry too + much if under some exceptions it doesn't quite work. +*/ +mips_error mips_cpu_step(mips_cpu_h state); + +/*! Controls printing of diagnostic and debug messages. + + You are encouraged to include diagnostic and debugging + information in your CPU, but you should include a way + to control how much is printed. The default should be + to print nothing, but it is a good idea to have a way + of turning it on and off _without_ recompiling. This function + provides a way for the user to indicate both how much + output they are interested in, and where that output + should go (should it go to stdout, or stderr, or a file?). + + \param state Valid (non-empty) CPU handle. + + \param level What level of output should be printed. There + is no specific format for the output format, the only + requirement is that for level zero there is no output. + + \param dest Where the output should go. This should be + remembered by the CPU simulator, then later passed + to fprintf to provide output. + + \pre It is required that if level>0 then dest!=0, so the + caller will always provide a valid destination if they + have indicated they will require output. + + It is suggested that for level=1 you print out one + line of information per instruction with basic information + like the program counter and the instruction type, and for higher + levels you may want to print the CPU state before each + instruction. Both of these can usually be inserted in + just one place in the processor, and can greatly aid + debugging. + + However, this is completely implementation defined behaviour, + so your simulator does not have to print anything for + any debug level if you don't want to. +*/ +mips_error mips_cpu_set_debug_level(mips_cpu_h state, unsigned level, FILE *dest); + +/*! Free all resources associated with state. + + \param state Either a handle to a valid simulation state, or an empty (NULL) handle. + + It is legal to pass an empty handle to mips_cpu_free. It is illegal + to pass the same non-empty handle to mips_cpu_free twice, and will + result in undefined behaviour (i.e. anything could happen): + + mips_cpu_h cpu=mips_cpu_create(...); + ... + mips_cpu_free(h); // This is fine + ... + mips_cpu_free(h); // BANG! or nothing. Or format the hard disk. + + A better pattern is to always zero the variable after calling free, + in case elsewhere you are not sure if resources have been released yet: + + mips_cpu_h cpu=mips_cpu_create(...); + ... + mips_cpu_free(h); // This is fine + h=0; // Make sure it is now empty + ... + mips_cpu_free(h); // Fine, nothing happens + h=0; // Does nothing here, might could stop other errors +*/ +void mips_cpu_free(mips_cpu_h state); + +/*! + @} +*/ + +#ifdef __cplusplus +}; +#endif + +#endif diff --git a/include/mips_mem.h b/include/mips_mem.h new file mode 100644 index 0000000..97724ff --- /dev/null +++ b/include/mips_mem.h @@ -0,0 +1,155 @@ +/*! \file mips_mem.h + Defines the functions used to interact with simulated memory. + + Note that the notions of "memory/address space" and "RAM" are actually + two related but distinct things (we will explore this more later). + A memory space is some kind of addressable space that the CPU can + read and write to, where addressable locations are identified by + integers. For the moment we will only deal with one address space, + but later on we'll see others. In this API, abstract memory spaces + are accessed using the functions in \ref mips_mem_core, but they + must be intially created using a device specific API from \ref mips_mem_devices. + + RAM is a particular kind of memory device, which maps reads and + writes transactions at particular addresses to corresponding + storage locations. ROM is another kind of memory device that you + saw earlier. It is extremely common for multiple types of memory + device to exist in one address space, but for now we will stick + with the simple idea of having one RAM, which is created using mips_mem_create_ram. +*/ +#ifndef mips_mem_header +#define mips_mem_header + +#include "mips_core.h" + +/* This allows the header to be used from both C and C++, so +programs can be written in either (or both) languages. */ +#ifdef __cplusplus +extern "C"{ +#endif + +/*! \defgroup mips_mem Memory + \addtogroup mips_mem + @{ + + \defgroup mips_mem_core Abstract Memory Interface + \addtogroup mips_mem_core + @{ +*/ + +/*! Represents some sort of memory, but without saying +anything about how it is represented. See \ref mips_mem_h. + +\struct mips_mem_provider +*/ +struct mips_mem_provider; + +/*! An opaque handle to a memory space. + + We can pass this around without knowing who or what provides the + memory. This is an example of an "opaque data type" http://en.wikipedia.org/wiki/Opaque_data_type + and is commonly used in embedded systems and operating + systems. An example you might have come across includes the + FILE data-type used by fopen and fprintf in the C standard + library. + + Because this is a pointer, we can safely give it the + known value of 0 or NULL in order to get a known empty + state. For example: + + mips_mem_h myMem=0; // Declare an empty handle + + if(some_condition) + myMem=get_a_handle(); + + if(myMem) + do_something_with mem(myMem); + + So even without knowing what the data-structure is, we can still + tell whether or not a handle is currently pointing at a + data-structure. +*/ +typedef struct mips_mem_provider *mips_mem_h; + + +/*! Perform a read transaction on the memory + + The implementation is expected to check that the transaction + matches the alignment and block size requirements, and return an + error if they are violated. +*/ +mips_error mips_mem_read( + mips_mem_h mem, //!< Handle to target memory + uint32_t address, //!< Byte address to start transaction at + uint32_t length, //!< Number of bytes to transfer + uint8_t *dataOut //!< Receives the target bytes +); + +/*! Perform a write transaction on the memory + + The implementation is expected to check that the transaction + matches the alignment and block size requirements, and return an + error if they are violated. +*/ +mips_error mips_mem_write( + mips_mem_h mem, //!< Handle to target memory + uint32_t address, //!< Byte address to start transaction at + uint32_t length, //!< Number of bytes to transfer + const uint8_t *dataIn //!< Receives the target bytes +); + + +/*! Release all resources associated with memory. The caller doesn't + really know what is being released (it could be memory, it could + be file handles), and shouldn't care. Calling mips_mem_free on an + empty (zero) handle is legal. Calling mips_mem_free twice on the + same handle is illegal, and the resulting behaviour is undefined + (most likely a crash). + + A pattern that can be useful is: + + mips_mem_h h=0; // Initialise it to empty + ... + h=some_creator_function(...); + ... + use_memory_somehow(h); + ... + if(h){ + mips_mem_free(h); + h=0; // Make sure it is never freed again + } +*/ +void mips_mem_free(mips_mem_h mem); + +/*! @} */ + + +/*! \defgroup mips_mem_devices Concrete Memory Devices + \ingroup mips_mem_devices + @{ +*/ + +/*! Initialise a new RAM of the given size. + + The RAM will expect transactions to be at the granularity + of the blockSize. This means any reads or writes must be aligned + to the correct blockSize, and should consist of an integer number + of blocks. For example, choosing blockSize=4 would result in a RAM + that only supports aligned 32-bit reads and writes. +*/ +mips_mem_h mips_mem_create_ram( + uint32_t cbMem, //!< Total number of bytes of ram + uint32_t blockSize //!< Granularity of transactions supported by RAM +); + +/*! + @} + @} +*/ + + +#ifdef __cplusplus +}; +#endif + +#endif diff --git a/include/mips_test.h b/include/mips_test.h new file mode 100644 index 0000000..f7de203 --- /dev/null +++ b/include/mips_test.h @@ -0,0 +1,150 @@ +/*! \file mips_test.h + Defines the functions used to test a processor. +*/ +#ifndef mips_test_header +#define mips_test_header + +#include "mips_cpu.h" + +/* This allows the header to be used from both C and C++, so +programs can be written in either (or both) languages. */ +#ifdef __cplusplus +extern "C"{ +#endif + +/*! \defgroup mips_test Testing + + This collection of functions is used for defining test suites. + Just like the CPU and memory APIs, it is defined as a set of + functions, leaving freedom to have different implementations and + clients. The broad idea is that of Unit Testing http://en.wikipedia.org/wiki/Unit_testing + so there are lots of small tests which try to check the functionality + of many independent parts, in this case instructions. Usually this + would be followed by Integration Testing http://en.wikipedia.org/wiki/Integration_testing + to check that the parts work correctly together, but that is mostly out + of scope here. + + Integration testing is a huge problem in real-world CPU design and + implementation, as the interaction between many individual components + within a CPU can lead to very subtle bugs, even when the components appear + to be fine. You will be able to test some instructions individually, but + some instructions are impossible to test in isolation (the API is carefully + designed in that regard), and will require a sequence of instructions. + I will also give you larger programs to try running later on, so you'll be + able to see whether your testing of individual instructions results in + a processor that can run real programs with thousands of instructions. + + This particular unit testing API is extremely simple, and has few + of the standard features one would expect from a standard API, + such as JUnit or CppUnit. Typically they support things like nesting + of test suites, can indicate dependencies ("don't run this complex test if + another simpler test already failed"), and support multiple output formats + (GUI dashboards are quite common). Another thing that is missing is + Continuous Integration, whereby the tests are automatically run as + development continues, usually in concert with source control. For + this test framework, it is assumed that every time you run the test suite + all tests will be run, so you'll be able to see if adding new functionality + has broken things that used to work - when new features break + old ones it is a regression (things have got worse), so this is a form + of Regression Testing http://en.wikipedia.org/wiki/Regression_testing. + + The main things the test framework requires you to do are: + + - Indicate when you are starting the test suite with mips_test_begin_suite + + - For each individual test you run, use mips_test_begin_test to record the + what the test is testing, then mips_test_end_test to record if it passed + or failed. + + - Use mips_test_end_suite to say that all tests have ended, so that the + test framework can do things like calculate statistics. + + So an individual test would look something like: + + int testId=mips_test_begin_test("ADD"); + ... + mips_cpu_set_register(cpu, 1, 5); + mips_cpu_set_register(cpu, 2, 5); + ... + // Make the simulator do reg[1]=reg[1]+reg[2] + ... + uint32_t got; + mips_error err=mips_cpu_get_register(cpu, 1, &got); + mips_test_end_test(testId, (err==mips_Success) && (got==10), "Testing 5+5 == 10"); + + While the overall test suite would look like: + + mips_test_begin_suite(); + + testId=mips_test_begin_test(...); + ... + mips_test_end_test(testId, ...); + + testId=mips_test_begin_test(...); + ... + mips_test_end_test(testId, ...); + + mips_test_end_suite(); + + Exactly how you structure it is up to you, the main + requirement is that a given test should actually test + the instruction it says it is - if you say you are testing + MULU, but that instruction never executes within the + scope of the test, then the test doesn't count. You + should aim to test all instructions that you implement. + + As you add tests, you will notice a lot of repetition, + between different tests of one instruction, and the testing + of different instructions. As a programmer, whenever you + see repetition you should thing about automation. This + is a programmatic framework, so how much replicated + functionality can be wrapped up inside a function? + + \addtogroup mips_test + @{ +*/ + +/*! Call once at the beginning of all tests to setup + testing information. +*/ +void mips_test_begin_suite(); + +/*! Used before starting an individual test + \param instruction String identifying which instruction the + test is targetting, for example "add", "lw", etc. + + \retval A unique identifier identifying the test + + You may have some tests which are not associated with any + instruction, in which case use the string "". These + can be useful to establish certain invariants, like "if I set + register 3 to a value, then if I read register 3 it should still + be the same value". +*/ +int mips_test_begin_test(const char *instruction); + +/*! Used to indicate whether an individual test passed or failed. + + \param testId The unique identifier returned from mips_test_begin_test. + + \param passed Flag to indicate if the test succeeded (passed!=0) or failed (passed==0). + + \param msg An optional message to explain what you the test was looking for in + case it failed. Can be NULL if there is nothing useful to print, or you don't want to + write a message. +*/ +void mips_test_end_test(int testId, int passed, const char *msg); + +/*! Call once at the end of all tests to indicate that all tests + have now ended. +*/ +void mips_test_end_suite(); + +/*! @} */ + + +#ifdef __cplusplus +}; +#endif + +#endif -- cgit