aboutsummaryrefslogtreecommitdiffstats
path: root/include
diff options
context:
space:
mode:
Diffstat (limited to 'include')
-rw-r--r--include/mips.h11
-rw-r--r--include/mips_core.h67
-rw-r--r--include/mips_cpu.h177
-rw-r--r--include/mips_mem.h155
-rw-r--r--include/mips_test.h150
5 files changed, 560 insertions, 0 deletions
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 <stdint.h> 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 <stdint.h>
+
+#include <stdio.h>
+#include <stdlib.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
+
+/*! 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 "<internal>". 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