TCSS 372 Programming Project - Requirements Specification

The SC-2 (Simple Computer - Second Version 2.2)

Author: George Mobus
Date: 1/10/12
Revised: 4/2/ 12

Background

This project is to build an SC-2 simulator similar to the LC-3 simulator you used in 371. A simulator is a program that behaves like a piece of hardware. In this case the program simulates the behavior of a small computer, complete with a CPU, memory module, and several I/O devices. The computer has an instruction set and performs a fetch-decode-execute cycle so that programs writen in the machine language of the computer can be loaded into the memory module and run, demonstrating that the simulator is behaving as specified in the design. This method of writing software simulators is used by chip designers to test their designs before committing to hardware. If there are bugs in the design, the simulator can help find them and allow a cheap way to change a design. Building chips in hardware is expensive as compared with building a software simulation.

Your simulator could allow you to test various aspects of the SC-2 design. A typical test might be to see if there is a performance gain for compiling a C program given the addition of certain instructions or addressing modes. We will not be going that far. The main purpose of this project is to continue to develop your programming expertise by building a significant project.

Overall, this project will be carried out in phases with deliverables at each phase. The first phase will be to build and test the ALU object, register file object, and the memory module object. The second phase will be to build and test the rest of the CPU. This will be done by executing a test suite of machine code and examining the register contents. Finally, you will build I/O module objects (especially for the VID output and the KBD input, also called stdout and stdin), a pseudo-debug monitor, and any remaining pieces to be integrated into the final product. This document will start with phase 1 specifications and requirements.

SC-2 Specifications

Use the SC-2 specification page, which contains details of CPU, memory, and I/O hardware as well as the SC-2 instruction set.

Program: Phase I Specifications

Program Name: phase1_test.c (or phase1_test.java)

Using the object-oriented (ADT) design approach for C, design and implement the SC-2 ALU, a register file, and a memory module.

The program you turn in will be a test-driver to show that you can successfully transfer data, both byte and word, to and from registers/main memory, and from registers to the ALU, ALU operations, and from the ALU to the registers. This program will use implementations in C of the below abstract data types (ADT) for 1) Register, 2) Register File, 3) Main Memory Module, and 4) ALU. Ideally your program will allow a user to designate the data precision, direction (load or store), register, and memory address interactively (e.g. using a simple menu of options). However, it is far more important to get the ADTs right and if necessary, just write a program that hard codes a sufficient demonstration test suite. No loss of credit will happen if you don't get an interactive version done as long as the below demonstration is followed.

The program should:

Class (Abstract Data Type - ADT) Requirements

Use these ADTs for your design (Note: the operations given here are only those needed for data movement; other basic operations will be needed in the full simulation):

ADT - Register16
	Data Type Description
	A register is a 16-bit variable type implemented as an unsigned short (typedef'd ushort)
	Implementation:
		#define uchar unsigned char
		#define ushort unsigned short
		// you can also define byte and word if you prefer, e.g.
		#define byte unsigned char
		#define word unsigned short
		(however, make sure this does not conflict with your compiler's predefined data types)

		typedef Register ushort;
		typedef RegisterPtr * Register;
	Operations
		Basic operations
		Initialize: creates register object and puts a random value from 0 to 65535 into it.
		GetLowByte: returns a uchar from LOB of register
		GetHIByte:  returns a uchar from the HOB of register
		PutLowByte: puts a uchar into the LOB of the register
		PutHIByte:  puts a uchar into the HOB of the register
		SignOf:     returns bit 15 value in a ushort
		Destructor: not needed
		Implementations:
			RegisterPtr r = register_new(void);// allocates space and returns pointer
			uchar register_getLowByte(RegisterPtr r, int * error); // returns LOB if successful
			uchar register_getHIByte(RegisterPtr r, int * error); // returns HOB if successful
			int register_putLowByte(RegisterPtr r, uchar byte); // returns error code
			int register_putHIByte(RegisterPtr r, uchar byte); // returns error code
			uchar register_signOf(RegisterPtr r) // returns 0 or 1, 2 or greater error code
		Composite Operations
		GetRegValue: returns a ushort, the value in the register
		PutRegValue: returns error, puts ushort into register
		Implementations:
			ushort register_getRegValue(RegisterPtr r, int *error); // returns register content
			int register_putRegValue(RegisterPtr r, ushort word); // returns error code, 0 if none

ADT - SC-2 Register File (uses Register)
	Data Type Description
	The register file is a set of sixteen 16-bit registers as described in the SC-2 document
	Implementation in C: an allocated array of RegisterPtr(s)
		#define REGISTER_COUNT 16
		#define $R0 0
			:
			:
		#define $RF 0xF
		typedef RegisterFilePtr RegisterPtr; // to be allocated as an array
	Operations
		Basic Operations
		Initialize: creates a register file of 16 Register objects in an array
		GetByteFrom: returns a uchar from a designated register and LOB or HOB
		PutByteTo:   puts a single byte to either LOB or HOB or register
		Destructor: not needed
		Composite Operations
		GetWordFrom: returns a ushort, the content of the designated register
		PutWordTo: puts the ushort into the designated register
		Implementations:
			RegisterFilePtr registerFile_new(); // allocates space for 16 register
						// pointers and calls register_initialize for each slot
			uchar registerFile_getByteFrom(RegisterFilePtr rf, int reg_num, int which, int * error);
						// returns the byte in register reg_num and HOB or
						// LOB indicated by which, sets error code, 0 if none
			int registerFile_putByteTo(RegisterFilePtr rf, int reg_num, int which, uchar byte);
						// puts the byte into the reg_num and HOB or LOB
						// indicated by which. returns error code
			ushort registerFile_getRegValue(RegisterFilePtr rf, int reg_num, int * error);
						// returns register content; sets error code
			int registerFile_putRegValue(RegisterFilePtr rf, int reg_num, ushort value);
						// puts the value into the designated reg_num register
						// returns an error code

ADT - Memory Module
	Data Type Description
	The memory module simulates the SC-2 memory model starting at location 0x3000 (user memory) to
	0xFFFF. The memory is byte-addressable.
	Implementation in C: an allocated array of uchar. Note that array index 0 has to be translated
	as 0x3000 in address references.
		typedef MemoryModulePtr * uchar; // note we can skip an ADT for a memory slot!
	Operations
		Basic Operations
		Initialize: allocates space for memory of uchar(s), fills memory with random values between 0 and 0xFF (note upper case hex characters)
		FetchByteFrom: returns a single uchar from location indicated
		StoreByteTo: puts a uchar into the memory location indicated
		Destructor: not needed
		Composite Operations
		FetchWordFrom: returns a ushort (word) from the memory location indicated
		StoreWordTo: puts a ushort into consecutive memory locations starting with address given. Preserve big endian format.
		Implementation:
		MemoryModulePtr memory_new(); // creates user memory space and fills with random
							// numbers
		uchar memory_fetchByteFrom(MemoryModulePtr mm, ushort address, int * error);
							// returns a byte form address, sets error code
		int memory_storeByteTo(MemoryModulePtr mm, ushort address, uchar byte); // stores byte
							// at address, sets error code
		ushort memory_fetchWordFrom(MemoryModulePtr mm, ushort address, int * error); // returns
							// the contents of two consecutive locations
							// starting at address. sets error code
		ushort memory_storeWordTo(MemoryModulePtr mm, ushort address, ushort word, int * error);
							// stores the contents at two consecutive locations
							// starting at address. returns error code
#Revision 4/18/12
ADT - ALU
	Data Type Description
	The ALU implements all of the ALU instructions
		#define ALU_OPs, e.g. ADD	1, SUB	2, etc.
		typedef ALU struct
			Register A, B, R, R2 // R2 is needed for multiply and divide operations
			Register * SW	// SW (status word) is external to ALU and a pointer to it needs to
			be set at initialization
	Operations:
		alu_Initialize		// allocate ALU object and initialize registers
		alu_setSWReference	// allows a CPU object to set the pointer to the SW for the ALU to use
		alu_clearFlags		// ALU clears all condition code flags in the SW at start of operation
		alu_setFlags		// ALU sets any of the condition codes as appropriate after operation

		int error alu_setRegister(ALUPtr theALU, int regno, Register value) //
		// sets the register number (A=0, B=1) to value in the ALU object pointed to by theALU

		Register alu_getResult(ALUPtr theAlu, int regno, int *error)
		// returns a register value from the R (0) or R2 (1) registers and sets the contents of
		// error to zero (no error) or an error code if there is an error

		All ALU operations
		Implementation (example)
			int error = alu_[OP](ALUPtr alu);// where OP stands for any operation
			// e.g.ADD, SUB, etc.
			// sets the flag bits in SW->register
			// these operations assume that both A & B registers have been appropriately
			// set previous to the operation call
			// each call will store the result in the R register for later retrieval (R2 also if
			// a multiply or divide operation

		The ALU is implemented as a large switch statement

Note that there is no explicit hardware controller ADT. You can implement all control as a simple function for testing purposes. Here you can simply transfer a register value to the ALU register(s) and signal the ALU which operation to perform. In Phase II you will tighten this up through implementing the controller as a fetch-decode-execute cycle in the CPU object.

As you can see in these ADTs the kinds of errors have not been made explicit. You should design global error codes that you can use to catch problems. For example, passing a NULL pointer into a function should be detected and prevent the operation. Return or pass via the parameter a code representing this type of error (e.g. -1). When you write your test code you should explicitly test various error conditions (see below).

You may (and should for now) have one header (.h) file for all three ADT definitions and one code file (.c) for all three implementations. Note the use of prefixes to designate which ADT any functions belong to. Use comment blocks to indicate the ADT definitions and implementations in both files.

The .c file should have a main() test driver that tests each implementation. Start with the Register. The test program should call each of the functions testing various values and error conditions. Once you have tested Register, go on to implement RegisterFile and test it completely (you can comment out the Register test code but do not remove it).

Implement the MemoryModule and test it in the same fashion. Then the ALU. Once you have tested every function of the four ADTs write a test whereby bytes and words are transfered between the register file and main memory and ALU. E.g. a function that transfers a word from an arbitrary $Rs to some memory location, say above 0x8000. Do the same for reverse transfers and for single bytes as well. Then show the transfers between registers and the ALU, operations, and then transfer the result back to the register file and show it.

NOTE: If you write this simulator in Java, the ADTs above are guides for your classes. The major difference will be that you will use the try-catch-throw synatax for error handling in Java.

Grade Components

Component and weight

Note that grading standards will change for future programs to weigh more heavily on running.

Turn In

Be absolutely certain that every source file has a header comment containing team name, team members, and description of the file. You should zip your files (.zip, not some non-Windows format!) together. The zip file should be named <team_name>_phase1.zip. Do not forget the team name in the filename. Failure to follow these instructions will cost some points. I should get only one .c file, one .h file and one readme.txt file all zipped in a single submission (Java submissions will include separate .java files for each class). Only one team member should submit the zip file to the Moodle site. This is why it is important to have all team member names in every file.

The readme file should contain the following information:

Due Date

Thr., April 19, 23:55.


Program: Phase II Specifications

In this second phase you will build the CPU, including the controller for program execution. You will also build a minimal debug monitor simulator that will allow the display of registers, memory, and a menu of command options on the screen. This piece will be the largest part of the whole project but since you have many of the components already this should mostly be a matter of implementation and integration.

In this phase you need only implement the load and store, ALU (already done), and just the branch and JSR/RET instructions. Do not worry about the trap, reti, or I/O just yet. And only implement the halt and nop miscellaneous instructions. In phase 3 you will implement I/O devices and the I/O instructions.

Design the CPU as an ADT/Class using the following skeleton:

ADT - CPU
	Data Type Description:
	Using the ADTs from Phase I.
		RegisterFilePtr registerFile;	// internal register file
		Register SW, PC, IR, MDR, MAR;	// data path registers
		ALUPtr alu;			// internal ALU
		MemoryModulePtr mainMemory;	// external memory - this simulates the bus!
		int macroState;		// one or more integers contain the control state (e.g. fetch,
						// etc)
		int microState;		// needed for switch inside of switch!
	Operations:
		initialize or cpu_new()
		run
	State dependent
		fetch instruction
		decode instruction
		fetch operands (if needed)
		execute instruction
		write memory (if needed)

		[NOTE: the LC-2200 only uses a fetch-decode-execute cycle in which fetch operands, write
		results, and execute are all in one macro state (see Chapter 3). The LC-3, used the above
		set of states explicitly. In this simulator we will stick to the above set of states and
		the reason will become clear when we get to pipeline designs in Chapter 5. The LC-2200
		micro-states will be broken up into the five 'stages' or states given above. Engineering
		students should particularly pay attention to the these states as they will eventually
		implement a pipeline simulation as well!]


		appropriate getters and setters
Even though it might seem that designing the CPU as an ADT/Class is overkill (and there are good arguments for that) there are two reasons I want you to go to this extent. First it is more programming experience with more complex class objects. Second you should think ahead about the possibility that you could simulate multiple core computers with this approach.

You will also need a minimal debug monitor data structure.

ADT - DebugMonitor
	Data Type Description:
	Contains debug state and breakpoint addresses. Note you need not implement the latter in this phase
		int     debugState;		// used to inform the CPU
		ushort	breakPoints[MAXBREAKPOINTS];
		ushort	currentPCValue;
		ushort	memDumpStart;

	Operations:
		Initialize		// new a debug monitor and initialize the above variables
		LoadProgram		// open a text file containing a loadable program and read that text into
						// main memory starting at the load address given in the first line of the
						// file (use the same format as the LC-3 load file)
		RunProgram		// sets cpu->PC to 0x3000, start of user program; sets debugState to RUN
		StepProgram		// sets cpu->PC to 0x3000, sets debugState to STEP
		ShowRegisters	// refreshes register contents; returns to cpu_run()
		ShowMemory		// gets start address to dump and shows 16 words of memory in following format:
						// address	byte  byte
						// x3400    2A    01
						//           ^     ^
						//           |     +-- byte at x3401
						//           +-------- byte at x3400
		ShowAll			// refreshes all screen variables

		appropriate getters and setters

In this phase you only need to implement the bare essentials for you to run and debug your program (I mean this program not the machine program (see below). You should be thinking about the various debug facilities afforded by the LC-3 simulator to see what kinds of things you will implement in phase 3.

The main driver of this program will initialize the memory module and the cpu and simulate the debug monitor. The latter can be done by call backs to monitor code from inside the cpu_run(CPUPtr). Your code might look something like this:


int main(int argc, char* argv[]) {
	// main driver for computer simulation
	CPUPtr cpu = cpu_new();	// allocate CPU
	//initialize memory and any other variables needed such as debug monitor state
         :
         :
	cpu_setState(cpu, FETCH);
	cpu_run(cpu);					// starts the cpu
									// cpu stops when it encounters a HALT instruction
}

// In the CPU class implementation file

	int cpu_run(CPUPtr cpu) {

		int monitorState;
		for(;;) {
			monitorState = debug_getState();	// call debug monitor for update
			// a state machine is simulated with an infinite loop (stopped when the state is set
			// to STOP!) and a switch statement based on the state value. If micro-states are needed
			// they will be interpreted within one of the macro-state calls. See Chapter 3 of text
			switch macroState {
				case FETCH: cpu_fetch(cpu); break;
				case DECODE: cpu_decode(cpu); break;
				case FETCHOPERANDS: cpu_fetchOperands(cpu); break;
				// etc.
			}
			// the cleanest way (I think) to handle debug monitor services is to have the following
			// cases and callbacks. The cpu checks the monitor before a cycle to see what it should do
			switch monitorState {
				case STEP: getchar(); break;	// get user keypress to continue;
				case BREAK: debug_serviceBreak(cpu_getPC(cpu)); break;	// call back to monitor
				// etc.
			}
		}

	}

	int cpu_fetch(CPUPtr cpu) {

		// get instruction pointed at by cpu->PC, load it into cpu->IR
		cpu->macroState = DECODE;
		return NOERROR;
	}

// In the debug monitor file

	int debug_getState() {return debugState;}
	// etc.


Be sure to break your code into functions as much as possible so that if you need to make changes you can. Also, don't do the same work in each state function call. Use helper functions liberally.

For teams that feel very confident and want to master the screen formatting for their display and debug monitor, try to incorporate ncurses (the cursor and ouput control library for C). This library provides you with ways to format your output into nice text-based windows on the screen. Extra credit (up to 15%) if you succeed in producing a nicely formatted screen output. [Java programs can earn extra credit for GUI interface for the monitor.]

Once you have a working simulation you can build a simple test SC-2 program by simply writing the opcodes and address references in a hex file format (like the LC-3 hex file). This “program” doesn't have to accomplish anything; it just tests each instruction. Name it <teamName>_test.hex. When you test branch instructions you will need to make provisions for setting flags and branching to some address that has an unconditional branch back to the next instruction after the test.

Then hand assemble another simple program to the hex format. Here is an example simple program:

; simple.asm
; written for the SC-2 computer

				.ORIG		x3000
				LDA		$R1, OPERAND1		; use $R1 as a refernce address
				LDW		$R2, $R1			;
				ADI		$R1, #2
				LDW		$R3, $R1
				SUB		$R2, $R3
				ADI		$R1, #2
				STW		$R2, $R1
				HALT
OPERAND1			.DW		x0100
OPERAND2			.DW		x00AB
RESULT				.RW		1
				.END

Note that .DW is the pseudo-operation to ‘Define Word’, meaning to allocate one word (two bytes) of memory and store the value in that location. There is another pseudo-op called .DB for defining a single byte, e.g. .DB 'H' would set the value at the current location to the ASCII value for H. .RW reserves the number of words indicated and fills them with zeros. There will be additional pseudo-ops when we work on the language. As you can see this is very similar to the LC-3 language. Take special note that the first instruction takes up two words. Your hex file should look something like this:
3000
1080
3014
2109
 :
 :
 :
 :
 :
D000
0100
00AB
0000
The first line is the load start address. Your loader function (debug monitor simulator) should load the next word starting at 0x3000 in main memory. Note that if I counted correctly the third word (3014) should be the address of the 0x0100 (third from bottom).

The above program can be modified to use $R1 as a base address and, say, $R4 as the add register (LDI, $R4, #2). Or try to use the index register $RC and the LDW $R2& that will automatically increment after each load. Also think about a base-relative mode using an immediate operand. Can it be fit into the load and store codes? If you do try some other modifications send all of your .asm source files with your submission so I can see what you tried out. We won't be able to try out very elaborate programs until we have an assembler (in testing).

The program should:

Grade Components

Component and weight

Turn In

Basically the same as the requirements for Phase I turnin. You should zip your files (.zip, not some non-Windows format!) together. Include only source code (.c, .h .asm and .hex) files and a readme.txt file all zipped in a single submission. The readme file should contain the following information:

Due Date

Thr., May 10, 23:55.


Program: Phase III Specifications

Background

In this last phase of the project you will complete the simulation of the SC-2 computer by adding in the input/output devices (keyboard and video monitor) along with the IN/OUT instructions. Engineering teams will also implement a pipelined version of the CPU (see below).

The simulation will be complete with the implementation of the video (VID) output and the keyboard (KBD) input. These instructions are only allowed when the machine is in supervior (executive or kernel) mode. For our simulation purposes we assume that this is the case. Technincally the machine should switch between supervisor and user modes. The system would start up in supervisor mode with bit SW[B] set to 1, and then if an LDOS instruction is executed, the modes are switched whenever the PC or MAR values point to addresses inside or outside the OS boundary. If the PC or MAR is pointing to memory below the boundary then the machine must be in supervior mode, otherwise a segment fault (exception) is generated. Bit SW[9] is set to 1 to indicate that the machine should switch modes. Execution of a TRAP instruction will cause the system to switch into supervisor mode if it was in user mode. This is how a user program can access OS services (next quarter!) This information is supplies for background purposes. The CSS students will not need to worry about these issues. The CES students should.

Specification

The VID output device can be mapped to a specific region of the stdout screen. If you are using ncurses this can be anywhere there is room to print 80 character lines that don't interefere with the monitor output. If you are just printing regular lines you will need to use the bottom of the screen for your VID output and your monitor output will get scrolled upward with each line of output. We will not be doing anything fancy with this, just an assembly language version of helloWorld.

The keyboard can be handled the same. I will give you a simple keystroke capture subroutine that you can embedded in a loop that waits for the sequence and puts the characters typed into a memory buffer. Again, this will not be elaborate (unless you make it so!) The basic objective will be that the program can print a prompt to VID and wait for keyboard input to continue.

Implement any other instructions that you did not get to in Phase II. This includes Load/Store operations using the various addressing modes, ALU ops not yet implemented, flow of control ops not yet implemented, and supervisor mode instructions.

Write an assembly program that will prompt the user to enter their first name (string). After the user enters their name and hits ENTER, the program prompts with a question such as: “Print your name ? ” and waits for the response. If the user inputs a 'Y'/'y', the program prints the name and terminates, otherwise it just terminates. This program should exercise the JSR and BRx instructions. Since this is a more complex assembly program (and since we don't yet have an assembler to generate the object file) teams may work together and share their assembly code and object code for this program. As a class we will pick one representative and then when I test your simulator I will use that one object file as the test case.

Engineer Students (and aggressive CSS teams)

In addition to the above requirements the engineering teams should now implement a pipelined design for the SC-2. If you used the fetch-decode-execute states (five stages) as given above, then you should not have difficulty doing this. In this exercise you are to do the actual design work following these general specifications. This requirement is to help fulfill an ABET accreditation learning objective so is mandatory for engineering students and optional for CSS students.

Specification

Given the five stages (states) already implemented for the fetch-execute cycle:

  1. FETCH - gets the instruction from memory and updates the PC
  2. DECODE - determines the opcode and the addresses of the operands
  3. FETCH OPERANDS - moves operands from register file to ALU as needed
  4. EXECUTE - calls ALU operations as needed
  5. MEMORY - stores or loads MDR as needed
  6. WRITE BACK - moves data from/to the MDR to the target register as needed

Design the cycle loop such that as many as five instructions could be in process at any given time. Use the concept of a stage buffer as shown in the book to guide your design. Consider how each stage buffer holds the state information needed to pass on to the next stage. Could you devise a data structure to be incorporated in your processing loop that would emulate this behavior? Note that you should not need to change your basic operation code, just the internals of the cycle control.

Use bubble propagation and stall feedback signals to handle pipeline hazzards. Consider RAW hazzards and control hazzards (branches and jsr) only. You can implicitly use a strategy of branch not taken and then generate signals if the branch (or conditional JSR) is taken to stall the pipeline.

Instrument your simulation by keeping a count of the number of instructions retired and the number of cycles executed. At every 100rth cycle compute the average number of CPI and display it on the screen (round to one decimal point).

I will work with the engineering teams to flesh this requirement out better. If a CSS team wishes to tackle this addition, there will be extra points (up to 15%). However, you must let me know in advance of your intention and tell me how you plan to accomplish it.

Turnin and grading criteria will be the same as before. The grade you receive on this phase will be a weighted part of the final grade for the project.

Due Date

May 24, 23:55.

In the last week of classes we will schedule demonstrations of the final products. Each team will give a running demonstration of their simulator in class, explaining how it works, its features, what problems they encountered in design/implementation and how they overcame them, and what the most important things they learned from this project (e.g. coding practices OK, but especially what they learned about architecture). The demonstration will be graded, 20% of the final phase.


Total Grade for Project

As you may have noted from the syllabus, the total project is worth 450 points, weighted as: 100, 150, and 200 respectively for each phase. The heavier weighting on later phases gives you a chance to recover from any problems you might have had on the earlier phase(s). In theory there should be less actual work on later phases as far as the total programming is concerned. However you should be giving more effort to testing in the later phases so the work load toward the end will be greater than at the beginning. Plan accordingly.