Computer Design A computer aided design and VLSI approach Paul J. Drongowski Chapter 3 - ISA modeling. This chapter discusses the computer-aided description and modeling of instruction set architectures. We will use the SP.1 as an example. A complete, unambiguous of the ISA is an essential element in computer design. The ISA specification is a reference guide for the functional behavior of machines within the ISA family. Although the family members will have different levels of performance and price tags, they must all adhere to the ISA to guarantee software compatibility. Thus, the ISA specification not only drives the design process, but it is an essential element in the verification and testing of products. We will write our ISA specification using the C programming language. C is our choice as it is readily available on mainframes and personal computers and it allows the convenient manipulation of binary data items at the bit level (when required.) The semantics or meaning of the ISA specification, therefore, will draw upon the semantics of the C programming language. CAD tool builders have proposed and constructed special purpose languages for hardware description. (This topic will be taken up in a later chapter.) By writing the SP.1 ISA in C, we will obtain an executable specification for the SP.1. This approach has several advantages. The C program permits us to simulate the operation of the SP.1 to evaluate its features, performance and programmability. Since the investment in the ISA simulator is much less than the implementation of a full prototype, the ISA can be evaluated and modified inexpensively. It is difficult to make changes to a machine once many thousand of person hours and dollars have been spent on development. The characteristics of VLSI technology (limited pin out, poor ability to probe the state of an active chip) are not conducive to testing and debugging. A software simulation of the ISA and other aspects of the machine design are easily instrumented by inserting trace statements into the simulation program. The simulator becomes a testbed for SP.1 application programs. Programs may be tested and debugged before they are run on the actual SP.1 hardware. In a real, large scale development effort, operating system and compiler implementation may proceed in parallel with hardware design and construction of the first machine prototype. Diagnostic programs which exercise the instructions and features of the SP.1 can be developed on the simulator. Complete and accurate diagnostic programs greatly assist hardware debugging post-production quality assurance tests. An executable ISA specification is more than a guide for design and test -- it is a useful tool in its own right for the development of the complete computer system. Section 1 - Declarations. Like most modern programming languages, C requires macros and variables to be declared before they are used. The declarations in the SP.1 simulation program include symbolic constants for operation code values, assembly macros, utility variables, and finally, the simple variables and arrays that are used to represent the SP.1 registers and program memory. Figure 1 is the header for the SP.1 simulation program. An ISA specification is like any other program and deserves the same respect. In a large development project with many engineers, the ISA should be a carefully controlled document. Changes to the ISA should only be made with the approval of a central authority who will vouch for the overall validity of the ISA. This will prevent the ISA from becoming a "moving target" with chaos and bad design as the overall result. /* * SP.1 architectural simulation program. */ /* * Author: Paul J. Drongowski * Address: Computer Engineering and Science * Case Western Reserve University * Cleveland, Ohio 44106 * Version: 1.1 * Date: 25 July 1986 * Changed: 28 August 1987 * * Copyright (c) 1987 Paul J. Drongowski */ #include Figure 1 - Program header. The program header identifies the module as the SP.1 architectural simulation program, its author, version number, initial development date and date of the last change. In this case, the program was initially developed on 25 July 1986 with the last change occurring on 27 August 1987. An historical list of changes showing the date, nature and reason for the modification is helpful for tracking and possibly undoing changes to the program. The header in Figure 1 also includes the C language standard input/output package. /************** * SP opcodes * **************/ #define OP_NOOP 0x0 #define OP_GET 0x1 #define OP_PUT 0x2 #define OP_ADD 0x3 #define OP_AND 0x4 #define OP_SHR 0x5 #define OP_SHL 0x6 #define OP_IMM 0x7 #define OP_DEC 0x8 #define OP_SWAP 0x9 #define OP_EXC 0xA #define OP_BRZ 0xB #define OP_BR 0xC #define OP_CALL 0xD #define OP_RET 0xE #define OP_HALT 0xF Figure 2 - Operation code definitions. In Figure 2 we have defined symbolic constants for each of the SP.1 operation codes. It is good programming practice to define symbolic constants for global values which may change at some later time. If we decided to rearrange the operation code values, for example, only the definitions of the symbolic constants need to be changed. /******************* * Assembly macros * *******************/ #define NOOP (OP_NOOP << 4) #define GET (OP_GET << 4) #define PUT (OP_PUT << 4) #define ADD (OP_ADD << 4) #define AND(V) (OP_AND << 4) , V #define SHR (OP_SHR << 4) #define SHL (OP_SHL << 4) #define IMMCR(V) (OP_IMM << 4) | 1, V #define IMMAC(V) (OP_IMM << 4) | 2, V #define IMMMD(V) (OP_IMM << 4) | 4, V #define DEC (OP_DEC << 4) #define SWAP (OP_SWAP << 4) #define EXC(C) (OP_EXC << 4) | C #define BRZ(A) (OP_BRZ << 4) , A #define BR(A) (OP_BR << 4) , A #define CALL(A) (OP_CALL << 4) , A #define RET (OP_RET << 4) #define HALT(C) (OP_HALT << 4) | C Figure 3 - Assembly code macros. An assembler and linking loader are required for large scale program at the native machine level. Since we do not want to develop a complete assembler for the SP.1 (that is another book), we will use the macro definition capability of the C preprocessor to define a mini-assembly language. Figure 3 shows a set of macro definitions for the SP.1. When each macro is called, it will produce a one or two (constant) bytes for the corresponding SP.1 instruction. For simple one byte instructions like NOOP, GET or RET, the appropriate operation code is placed into the high order 4 bits of the instruction byte. The exception code is placed into the low order four bits of the EXC and HALT instructions. For the two byte instructions (AND, BRZ, etc.), the value of the argument to the macro will be placed into the second constant byte. Three different macros are defined for the Immediate instruction, one each for loading the CR, AC and MD registers. We will return to the assembly macros when we discuss the declaration of the SP.1 memory. /***************************** * Trace strings (literals.) * *****************************/ char *Instr[] = { "Noop", "Get", "Put", "Add", "And", "Shr", "Shl", "Imm", "Dec", "Swap", "Exc", "Brz", "Br", "Call", "Ret", "Halt" }; Figure 4 - Trace strings. Figure 4 contains the declaration of a vector of character pointers. This vector will be used during simulation to display the name of the instruction which is currently being executed by the SP.1 simulator. /************************ * SP machine registers * ************************/ char IP, /* Instruction pointer */ Z, /* Zero flag */ Run, /* Run flag */ CR, /* Counting register */ AC, /* Accumulator register */ MD, /* Multiply/divide temporary register */ SR; /* Subroutine return address */ /************************* * SP instruction memory * *************************/ char M[256] = { IMMCR(2), /* 0,1 Perform loop twice */ CALL(10), /* 2,3 Call get, swap and put subroutine */ DEC, /* 4 Decrement count */ BRZ(9), /* 5,6 Go around next instruction if zero */ BR(2), /* 7,8 Branch to top of loop */ HALT(0), /* 9 */ GET, /* 10 Subroutine entry address */ SWAP, /* 11 */ PUT, /* 12 */ RET /* 13 */ }; Figure 5 - SP.1 registers and memory. The declaration of the SP.1 registers and memory appear in Figure 5. AC, MD, IP, CR, and SR registers are represented by character variables by the same names. C language character variables are eight bit quantities and are sufficient to represent the full range of SP.1 register values. For other ISA's, integer or long integer variables may be required depending upon the size of the register to be modeled. Two flags are defined, Z and Run. The Z flag is part of the SP.1 ISA while the Run flag is introduced for program control purposes only. The Run flag may or may not appear in an implementation of the SP.1. When we examine the declaration of the program memory, M, we discover how the assembly macros are used. M is declared to be a vector of 256 eight bit bytes. The vector must be initialized to a valid SP.1 program such that the simulator will have some meaningful work to do. It is here that the assembly macros go to work. The initialization part of the memory declaration consists of a series of macro calls. The macros will be expanded by the C preprocessor into SP.1 machine code which will be loaded into the simulated memory by the C compiler. Standard C syntax can be used for comments and program constants as shown. Unlike a true assembler, we cannot have labels on our SP.1 instruction macros. All addresses, therefore, must be computed manually and given as numeric constants. This will make coding more difficult especially if the target addresses of branch and subroutine call instructions change. The problem may be slightly alleviated by artificially inserting NOOP instructions into the program. If more space is needed due to a program modification, the NOOP's can be replaced by more useful instructions. We have also put the load address(es) of each instruction in the program comments to help identify the branch and call destinations. This is a small price to pay for such a cheap assembler. The ten instruction program in Figure 5 has a counted loop that will execute the subroutine at location 10 twice. The subroutine reads a value from the input port, swaps the contents of the AC and MD registers, and sends the new value of the AC to the Output port. Section 2 - Fetch and execute loop. The main program for the SP.1 architectural simulation is shown in Figure 6. The program will first print a greeting to the user terminal and then initializes the register and flag variables. The Run flag is set to true so that the fetch-execute while loop will be performed at least once. The simulation will terminate when the Run flag becomes false (zero.) Before terminating, the program will print a farewell message. The body of the while is where the simulation activity really takes place. The "printf" at the head of the loop will display the hexadecimal value of the SP.1 instruction to be executed and its symbolic name as selected from the vector "Instr." Note that the instruction is picked from the program memory with the instruction pointer by indexing the vector M with the variable IP. The operation code is isolated by shifting the operation field four places to the right and then masking out everything but the low order four bits of the result. The simulation of an ISA typically involves a lot of shifting and masking. The operation code is used as the selection value for the switch statement. The instruction is thus "decoded" and one of sixteen functions is chosen for execution. Each function simulates the effect of executing a particular SP.1 instruction. After the switch statement, the hexadecimal values of the AC, CR, MD, and Z variables will be displayed. The calls to "printf" in the loop will provide a visible trace of the execution of the SP.1 instructions by the simulator. It is the job of the designer to examine this trace, find bugs, and correct them in either the SP.1 application program or the ISA. /*************************** * Main fetch/execute loop * ***************************/ main() { printf("SP simulator (v1)\n\n"); Run = 1; /* Set run flag true */ IP = 0; /* Begin execution at location zero */ AC = 0; /* Clear registers and flags */ CR = 0; MD = 0; Z = 0; while(Run) { printf("\n%2x %s\n", (unsigned) IP & 0xFF, Instr[(M[IP] >> 4) & 0xF]); switch( (M[IP] >> 4) & 0xF ) /* Decode instruction */ { case OP_NOOP: Noop(); break; case OP_GET: Get(); break; case OP_PUT: Put(); break; case OP_ADD: Add(); break; case OP_AND: And(); break; case OP_SHR: Shr(); break; case OP_SHL: Shl(); break; case OP_IMM: Imm(); break; case OP_DEC: Dec(); break; case OP_SWAP: Swap(); break; case OP_EXC: Exc(); break; case OP_BRZ: Brz(); break; case OP_BR: Br(); break; case OP_CALL: Call(); break; case OP_RET: Ret(); break; case OP_HALT: Halt(); break; } printf(" AC: %2x CR: %2x\n", (unsigned) AC & 0xFF, (unsigned) CR & 0xFF); printf(" MD: %2x Z: %s\n", (unsigned) MD & 0xFF, (Z ? "True" : "False")); } printf("Leaving the simulator.\n\n"); } Figure 6 - Main program. Section 3 - Instructions. Figures 7 through 12 contain C language for a few of the SP.1 instructions. (The rest of the functions are left as an exercise.) Noop() /* * No-operation. This instruction is used for timing * waits. */ { IP = IP + 1; } Figure 7 - Noop instruction. Add() /* * Add the MD register to the accumulator. */ { AC = AC + MD; IP = IP + 1; } Figure 8 - Add instruction. Please note that the fetch and execute loop (Figure 6) does not advance the instruction pointer. This job is left to the individual instruction interpretation procedures and makes the handling of multiple byte instructions easier to handle. Further, it encourages a style of specification in which all instruction side effects (i.e., changes to storage elements) are isolated to the individual instruction procedures. One can think of each procedure as a "this state to next state" transformation where the names of all storage elements to be modified appear on the left hand side of an assignment and the expressions on the right side of the assignment define the new values to be assumed by the storage elements. A trivial example is the NOOP instruction in Figure 7. The procedure simply advances the instruction pointer by one memory location. No other registers are affected. The ADD instruction (Figure 8) both advances the instruction pointer by one and loads the AC register with the sum of the old contents of AC and MD. Get() /* * Read a byte from the 8-bit signal input port. */ { static int Value; printf(" Input hex value: "); scanf("%x", &Value); AC = Value; IP = IP + 1; } Figure 9 - Get instruction. Put() /* * Write a byte to the 8-bit signal output port. */ { printf(" Output hex value: %2x\n", (unsigned) AC & 0xFF); IP = IP + 1; } Figure 10 - Put instruction. Figures 9 and 10 show the procedures for the Get and Put instructions. Because the communication portion of the SP.1 was left unspecified, Get and Put use the standard I/O "scanf" and "printf" functions to read and write an eight byte value to the user's terminal. Get prints a prompt, reads an eight bit value with "scanf," stores the value in AC, and advances IP by one. The procedure Put prints the eight bit value in AC as a hexadecimal value and then adds one to IP. As written here, Get and Put are sufficient for simple ISA debugging and evaluation. And() /* * Apply a mask to the accumulator register. The * byte following the instruction byte contains * the mask. Set the Z flag appropriately. */ { Z = (AC & M[IP + 1]) == 0 ; IP = IP + 2; } Figure 11 - And instruction. The function And in Figure 11 forms the bit-wise logical AND between AC and the second byte in the instruction. If the result is zero, the Z flag will be set to one, otherwise it will be set false. The instruction pointer is advanced by two since And is a two byte instruction. Halt() /* * Terminate execution. */ { printf("Halt instruction executed.\n"); printf(" Output exception value: %1x\n", (unsigned) M[IP] & 0xF); Run = 0; IP = 0; } Figure 12 - Halt instruction. The Halt function prints a message and the four bit exception code in the low order four bits of the instruction byte. The function sets the global Run flag to false (zero) so that the while loop in the main program will terminate. It also clears the instruction pointer. Section 4 - Instrumentation. The instrumentation in the example above is minimal. It will display an execution trace of the program in M. External port values are read and written one byte at a time. In fact, the user must enter incoming data bytes by hand from the keyboard. This is sufficient for simple debugging. The instrumentation may be expanded in several ways making the SP.1 simulator a more useful design and analysis tool. A simple command interpreter could be added to the main fetch-execute loop. The interpreter would accept commands from the user terminal and perform functions like examining and changing register values, setting execution breakpoints (locations and conditions to temporarily stop the simulation for debugging), and single step operation (advancing the simulation one instruction set at a time.) These features would make application program and ISA testing more interactive. The handling of bulky application programs and I/O values is clumsy. An external assembler could be used to produce SP.1 object modules which would be loaded upon request by the command interpreter. The interpreter could prompt the user for the name of the file containing the object code and then load it byte by byte into M. All the services of an assembler would be available to the programmer. Further, new or different programs could be loaded and tested without completely recompiling the simulation program. I/O values should also be stored in disk files since a signal processing engine like the SP.1 must operate on long streams of data. Actual signal data (binary encoded speech, for example) may be "played" through the simulation at high speed. Comprehensive system tests often involve the use of large amounts of input data. By storing input data in files, testing becomes convenient. If the output produced by an application or diagnostic program is saved (assuming it is correct of course), it can be compared with later runs to determine if new errors have been introduced into the program or ISA. This kind of testing is called "regression testing." Unix and other operating systems provide a utility that compares two files and displays any differences. A comparison utility further automates the process and reduces both drudgery and mistakes. Performance oriented instrumentation may also be added to an ISA simulation. Information about instruction frequency (mix) and memory access statistics may be collected. This data may indicate critical areas in the ISA which must be optimized in the machine implementation. If an instruction has a high execution frequency, the detailed design can be biased toward the fastest possible evaluation of that instruction. Copyright (c) 1987-2013 Paul J. Drongowski