





Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
A project for enee 646: digital computer design, where students will develop an assembler and a simple verilog cpu. The project involves writing assembly code, understanding different instruction formats, and creating an assembler that translates assembly language into machine code. The document also includes a description of the cpu architecture and the format of the assembly code.
Typology: Study Guides, Projects, Research
1 / 9
This page cannot be seen from the preview
Don't miss anything!






This assignment has two primary goals: to teach you the rudiments of the Verilog hardware description language and to show you how software assembles programs into machine-level code. The project has two parts. For the first part, you will write a C-language program that reads in an assembly-language pro- gram and produces the corresponding machine code. For the second part, you will write a Verilog-lan- guage behavioral simulator for arbitrary machine code. In writing an assembler, you will learn about file I/O and how CPUs interpret numbers as instructions. When writing your Verilog program, you will learn about non-blocking assignments and concurrency. Non-blocking assignments are specific to the Verilog language; concurrency is a powerful concept that shows up at all levels of processor design. The proces- sor model will be a simple sequential implementation—on every cycle, you will execute an instruction and update the program counter accordingly.
Before we talk about the C-language assembler and Verilog-language CPU implementations, we have to talk about the instruction-set architecture (ISA) that you are working with. This section describes the ISA of the 16-bit Ridiculously Simple Computer (RiSC-16), a teaching ISA based on the Little Computer (LC-896) developed by Peter Chen at the University of Michigan. The RiSC-16 is an 8-register, 16-bit computer and has a total of 8 16-bit instructions. The instructions are illustrated in the figure below.
Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
000 reg A reg B 0 reg C
3 bits 3 bits 3 bits 4 bits 3 bits ADD:
101 reg A reg B signed immediate (-64 to 63)
3 bits 3 bits 3 bits 7 bits LW:
110 reg A reg B signed immediate (-64 to 63)
3 bits 3 bits 3 bits 7 bits BNE:
111
3 bits JALR: Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
001 reg A reg B signed immediate (-64 to 63)
3 bits 3 bits 3 bits 7 bits ADDI:
011 reg A immediate (0 to 0x3FF)
3 bits 3 bits 10 bits LUI:
010 reg A reg B 0 reg C
3 bits 3 bits 3 bits 4 bits 3 bits NAND:
100 reg A reg B signed immediate (-64 to 63)
3 bits 3 bits 3 bits 7 bits SW:
reg A reg B
3 bits 3 bits 0
7 bits
ENEE 646: Digital Computer Design, Fall 2006 Assigned: Thursday, Aug 31; Due: Tuesday, Sep 19
There are 3 machine-code instruction formats: RRR-type, RRI-type, and RI-type.
All addresses are shortword-addresses (i.e. address 0 corresponds to the first two bytes of main memory, address 1 corresponds to the second two bytes of main memory, etc.). Like the MIPS instruction-set architecture, by hardware convention, register 0 will always contain the value 0. The machine enforces this: reads to register 0 always return 0, irrespective of what has been written there. The RiSC-16 is very simple, but it is general enough to solve complex problems. The following table describes the different instruction operations.
Note that the 8 basic instructions of the RiSC-16 architecture form a complete ISA that can perform arbi- trary computation. For example:
- Moving constant values into registers. The number 0 can be moved into any register in one cycle (add rX r0 r0). Any number between -64 and 63 can be placed into a register in one operation using the ADDI instruction (addi rX r0 number). Moreover, any 16-bit number can be moved into a register in two operations ( lui + addi ). - Subtracting numbers. Subtracting is simply adding the negative value. Any number can be made negative in two instructions by flipping its bits and adding 1. Bit-flipping can be done by NAND- ing the value with itself; adding 1 is done with the ADDI instruction. Therefore, subtraction is a three-instruction process. Note that without an extra register, it is a destructive process.
Mnemonic Name and Format Opcode(binary) Assembly Format Action
add AddRRR-type 000 add rA, rB, rC Add contents ofstore result in regA^ regB.^ with^ regC ,
addi Add ImmediateRRI-type 001 addi rA, rB, imm Add contents ofstore result in regA^ regB.^ with^ imm ,
nand NandRRR-type 010 nand rA, rB, rC Nand contents ofstore results in regA^ regB.^ with^ regC ,
lui Load Upper ImmediateRI-type 011 lui rA, imm Place the top 10 bits of the 16-bitof regA , setting the bottom 6 bits of^ imm regA^ into the top 10 bits to zero.
sw Store WordRRI-type 100 sw rA, rB, imm Store value fromformed by adding^ regA imm^ into memory. Memory address iswith contents of regB.
lw Load WordRRI-type 101 lw rA, rB, imm Load value from memory intoformed by adding imm with contents of^ regA. Memory address is regB.
bne Branch if Not EqualRRI-type 110 bne rA, rB, imm
If the contents of regA and regB are not the same, branch to the address PC+1+ imm , where PC is the address of the bne instruction.
jalr Jump And Link RegisterRRI-type 111 jalr rA, rB
Branch to the address in regB. Store PC+1 into regA , where PC is the address of the jalr instruction.
reg A unsigned immediate (0 to 1023)
3 bits 10 bits
opcode reg A reg B 0 reg C
3 bits 3 bits 3 bits 4 bits 3 bits RRR-type:
opcode reg A reg B signed immediate (-64 to 63)
3 bits 3 bits 3 bits 7 bits RRI-type:
opcode
3 bits RI-type: Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
The following paragraphs describe these pseudo-instructions in more detail:
- The nop pseudo-instruction means “do not do anything this cycle” and is replaced by the instruc- tion add 0,0,0 (which clearly does nothing). - The halt pseudo-instruction means “stop executing instructions and print current machine state” and is replaced by jalr 0, 0 with a non-zero immediate field having the value 113 decimal (chosen for historical reasons). Therefore the halt instruction is the hexadecimal number 0xE071. - The .fill directive tells the assembler to put a number into the place where the instruction would normally be stored. The .fill directive uses one field, which can be either a numeric value (in deci- mal, hexadecimal, or octal) or a symbolic address (i.e. a label). For example, “.fill 32” puts the value 32 where the instruction would normally be stored; “.fill 0x10” also puts the value 32 (deci- mal) where the instruction would normally be stored. Using .fill with a symbolic address will store the address of the label. In the example below, the line “.fill start” will store the value 2, because the label “start” refers to address 2.
When labels are used as part of instructions (e.g. the immediate values for lw , sw , .fill , lui , bne , and even addi instructions), they refer to the value of the label (the address at which the label occurs), and they are interpreted slightly differently depending on the instruction in which they are found:
- For lw , sw , or .fill instructions, the assembler should compute the immediate value to be equal to the address of the label. Therefore if a label appears in an immediate field, it should be interpreted as the address at which the label is found. In the case of lw or sw , this could be used with a zero base register to refer to the label, or could be used with a non-zero base register to index into an array starting at the label. - Labels are slightly different for bne instructions: the assembler should not use the label’s address directly but instead should determine the numeric immediate value needed to branch to that label. The bne description shows the program counter being updated by the immediate value (PC <- label), but the address represented by the label does not go directly into the instruction. The instruction format specifies that the immediate field must contain a PC-relative value. Therefore, if a label is used in the assembly program, the assembler must calculate the PC-relative value that will accomplish the same as a jump to the address of that label. This amounts to solving the fol- lowing equation for offset : label = PC + 1 + offset.
The assembler makes two passes over the assembly-language program. In the first pass, it calculates the address for every symbolic label. You may assume that the first instruction is located at address 0. In the second pass, the assembler generates a machine-level instruction in ASCII hexadecimal for each line of assembly language. For example, the following is an assembly-language program that counts down from 5, stopping when it hits 0.
lw 1,0,count # load reg1 with 5 (uses symbolic address) lw 2,1,2 # load reg2 with -1 (uses numeric address) start: add 1,1,2 # decrement reg1 (could have been “addi 1, 1, -1”) bne 0,1,start # go back to the beginning of the loop unless reg1== done: halt # end of program nop count: .fill 5 neg1: .fill - startAddr: .fill start # will contain the address of start (2)
And here is the corresponding machine-level program (note the absence of “0x” characters):
a a 0482 c0fe e 0000 0005 ffff 0002
Be sure you understand how the above assembly-language program got translated to this machine-code file.
3.1 Running Your Assembler
You must write your program so that it is run as follows (assuming your program name is “assemble”).
assemble assembly-code-file machine-code-file
Note that the format for running the command must use command-line arguments for the file names (rather than standard input and standard output). The first argument is the file name where the assembly- language program is stored, and the second argument is the file name where the output (the machine- code) is written. Your program should only store the list of hexadecimal numbers in the machine-code file, one instruction per line—any other format will render your machine-code file ungradable. Each num- ber can have ‘0x’ in front or not, as you wish. Any other output that you want the program to generate (e.g. debugging output) can be printed to stdout or stderr.
3.2 Error Checking
Your assembler should catch errors in the assembly language program, as well as errors that occur because the user ran your program incorrectly (e.g. with only 1 argument instead of 2 arguments). For example, it should detect the use of undefined labels, duplicate labels, missing arguments to opcodes (e.g. only giving two fields to lw ), immediate values that are out of range, unrecognized opcodes, etc.
3.3 Code Fragment for Assembler
The focus of this class is machine organization, not C programming skills. To help you, here is a fragment of the C program for the assembler. This shows how to specify command-line arguments to the program (via argc and argv ), how to parse the assembly-language file, etc. This fragment is provided strictly to help you, though it may take a bit for you to understand and use the file. You may also choose to not use this fragment.
/* Assembler code fragment for RiSC */ #include <stdio.h> #include <string.h> #define MAXLINELENGTH 1000 char * readAndParse(FILE *inFilePtr, char *lineString, char **labelPtr, char **opcodePtr, char **arg0Ptr, char **arg1Ptr, char *arg2Ptr) { / read and parse a line note that lineString must point to allocated memory, so that *labelPtr, *opcodePtr, and *argXPtr won’t be pointing to readAndParse’s memory note also that *labelPtr, *opcodePtr, and *argXPtr point to memory locations in lineString. When lineString changes, so will *labelPtr, *opcodePtr, and *argXPtr. function returns NULL if at end-of-file */ char *statusString, *firsttoken; statusString = fgets(lineString, MAXLINELENGTH, inFilePtr); if (statusString != NULL) { firsttoken = strtok(lineString, " \t\n"); if (firsttoken == NULL || firsttoken[0] == ’#’) { return readAndParse(inFilePtr, lineString, labelPtr, opcodePtr, arg0Ptr, arg1Ptr, arg2Ptr); } else if (firsttoken[strlen(firsttoken) - 1] == ’:’) { *labelPtr = firsttoken; *opcodePtr = strtok(NULL, " \n"); firsttoken[strlen(firsttoken) - 1] = ’\0’; } else { *labelPtr = NULL; *opcodePtr = firsttoken; } *arg0Ptr = strtok(NULL, ", \t\n"); *arg1Ptr = strtok(NULL, ", \t\n"); *arg2Ptr = strtok(NULL, ", \t\n"); } return(statusString); }
= 01001 (binary), which is 9 decimal.
= 11011 (binary), which is 27 decimal.
Use these operations to create and manipulate machine-code. E.g. to look at bit 3 of the variable a , you might do: (a>>3) & 0x1. To look at bits 15-13 of a 16-bit word (for instance, the opcode of each instruc- tion), you could do: (a>>13) & 0x7. To put a 6 into bits 5-3 and a 3 into bits 2-1, you could do the follow- ing: (6<<3) | (3<<1). If you’re not sure what an operation is doing, print some intermediate results to help you debug.
Beware, however, that printf expects you to tell it when printing out non-word data sizes.
Printing 16-bit numbers from within C-language programs is done by attaching an ‘h’ to the print-format string (‘h’ stands for half-word). For example, the following code prints out short integers correctly.
int num = 0x983475; /* larger than a 16-bit quantity */ short hword; hword = num & 0xffff; printf(“short int: 0x%04hx, %hd \n”, hword, hword);
The corresponding output:
short int: 0x3475, 13429
The first number printed is the value of hword as a hexadecimal number (the ‘04’ tells the printf function to pad the number on the left with zeroes if necessary, up to a total length of 4 digits). The second number printed is the value of hword as a decimal number. If you leave off the ‘h,’ or instead use ‘l,’ the value printed might not reflect the actual value of the number (heavily dependent on the compiler).
Because the immediate value is a 7-bit 2’s complement number, it only holds values ranging from -64 to
The second half of the project is to create in Verilog a CPU model of the RiSC-16 instruction set. As men- tioned, the model is to be single-cycle sequential (non-pipelined) execution. This means that during every cycle, the CPU will execute a single instruction and will not move to the next instruction until the present instruction has been completed and the program counter redirected to a new instruction (the next instruc- tion). This is the simplest form of processor model, so it should require very little code to implement. My solution, which is not particularly efficient, adds a mere 50 lines to the skeleton code shown below. You should be able to develop your processor model well within a week.
Clearly, the main point of this exercise is not to investigate advanced architecture concepts but to teach you the rudiments of the Verilog modeling language. Future projects will explore more advanced archi- tecture concepts like pipelines and precise interrupts.
You have been given a skeleton Verilog file that looks like this:
// // RiSC-16 skeleton // define ADD 3’ddefine ADDI 3’d define NAND 3’ddefine LUI 3’d define SW 3’ddefine LW 3’d define BNE 3’ddefine JALR 3’d define EXTEND 3’ddefine INSTRUCTION_OP 15:13 // opcode define INSTRUCTION_RA 12:10 // rAdefine INSTRUCTION_RB 9:7 // rB define INSTRUCTION_RC 2:0 // rCdefine INSTRUCTION_IM 6:0 // immediate (7-bit, to be sign-extended) define INSTRUCTION_LI 9:0 // large immediate (10-bit, to be 0-extended)define INSTRUCTION_SB 6 // immediate’s sign bit define ZERO 16’ddefine HALTINSTRUCTION { EXTEND, 3’d0, 3’d0, 3’d7, 4’d1 } module RiSC (clk); input clk; reg [15:0] rf[0:7]; reg [15:0] pc; reg [15:0] m[0:65535]; initial begin pc = 0; rf[0] =ZERO; rf[1] = ZERO; rf[2] =ZERO; rf[3] = ZERO; rf[4] =ZERO; rf[5] = ZERO; rf[6] =ZERO; rf[7] = ZERO; for (j=0; j<65536; j=j+1) m[j] = 0; end always @(negedge clk) begin rf[0] <=ZERO; end always @(posedge clk) begin // put your code here end endmodule
The file contains a number of definitions that will be helpful. For instance, the top group of definitions are the various instruction opcodes. The second group are fields of the instruction, such that the following statement:
instr[ `INSTRUCTION_OP ];
would yield the opcode of the instruction contained in instr.
The HALTINSTRUCTION definition allows you to decide when to halt; when you encounter an instruc- tion that matches this value, you can either $stop (which exits to the simulator debugger level) or $finish (which exits to the UNIX shell).
The RiSC module contains the definition of the CPU core. This is the module that you will implement. So far it contains only the registers, program counter, and memory. These are all initialized to hold the 16-bit value zero by the “initial” block. This block executes before all others at the time of the simulator start- up. The “always” block sets register zero to the value 0 on the negative edge of the clock. All processor