Starting from:

$30

Laboratory Exercise 2 An Enhanced Processor

Laboratory Exercise 2
An Enhanced Processor
In Laboratory Exercise 1 we described a simple processor. In Part I of that exercise the processor itself was
designed, and in Part II the processor was connected to an external counter and a memory unit. This exercise
describes subsequent parts of the processor design. Note that the numbering of figures and tables in this exercise
are continued from those in Parts I and II of the preceding lab exercise.
In this exercise we will extend the capability of the processor so that the external counter is no longer needed, and
so that the processor has the ability to perform read and write operations using memory or other devices. We will
add four new instructions to the processor, as displayed in Table 3. The ld (load) instruction loads data into register
rX from the external memory address specified in register rY. The st (store) instruction stores the data contained in
register rX into the memory address found in rY. Finally, the instructions mvnz (move if not zero) performs a mv
operation only under the condition that the current contents of register G (the adder/subtracter output) is not equal
to 0, and the mvnc instruction performs a mv operation only if the most-recently executed add (or sub) instruction
did not produce a carry-out (or borrow).
Operation Function performed
ld rX, [rY] rX ← [rY]
st rX, [rY] [rY] ← rX
mvnz rX, rY if G != 0, rX ← rY
mvnc rX, rY if no carry-out, rX ← rY
Table 3: New instructions performed in the processor.
A schematic of the enhanced processor is given in Figure 12. All general-purpose registers are now 16-bits wide,
whereas in Laboratory Exercise 1 they were nine-bits wide. In the figure registers r0 to r6 are the same as in
Figure 1 of Exercise 1, but register r7 has been changed to a counter. This counter is used to provide the addresses
in the memory from which the processor’s instructions are read; in the preceding lab exercise, a counter external
to the processor was used for this purpose. We will refer to r7 as the processor’s program counter (pc), because
this terminology is common for real processors available in the industry. When the processor is reset, pc is set
to address 0. At the start of each instruction (in time step T0) the contents of pc is used as an address to read an
instruction from the memory. The instruction is stored into IR and the pc is automatically incremented to point to
the next instruction (in the case of mvi the pc provides the address of the immediate data and is then incremented
again).
The processor’s control unit increments pc by using the incr_pc signal, which is just an enable on this counter. It
is also possible to directly load an address into pc (r7) by having the processor execute a mv or mvi instruction
in which the destination register is specified as r7. In this case the control unit uses the signal r7in to perform a
parallel load of the counter. Thus, the processor can execute instructions at any address in the memory, as opposed
to only being able to execute instructions that are stored in successive addresses. Similarly, the current contents
of pc, which always has the address of the next instruction to be executed, can be copied into another register by
using a mv instruction.
1
AddSub
Ain
Gin
Run
9
DIN
r0in
Multiplexers
r7in
Bus
Clock
16
r0
Counter
IRin
Addsub
IR
9
A
G
ADDR DOUT
ADDRin
ADDR
DOUT
W W_D
Resetn
(r7)
incr_pc
DOUTin
L
E 16 16
16
16
16
16
16
Select
Run
Control FSM
Figure 12: An enhanced version of the processor.
An example of code that uses the pc register to implement a loop is shown below, where the text after the // on each
line is just a comment. The instruction mv r5,r7 places into r5 the address in memory of the instruction sub r2,r1.
Then, the instruction mvnz r7,r5 causes the sub instruction to be executed repeatedly until r2 becomes 0. This type
of loop could be used in a larger program as a way of creating a delay.
mvi r1, #1
mvi r2, #10000 // delay value
mv r5, r7 // save address of next instruction
sub r2, r1 // decrement delay count
mvnz r7, r5 // loop until delay count gets to 0
Figure 12 shows two registers in the processor that are used for data transfers. The ADDR register is used to send
addresses to an external device, such as a memory module, and the DOUT register is used by the processor to
provide data that can be stored outside the processor. One use of the ADDR register is for reading, or fetching,
instructions from memory; when the processor wants to fetch an instruction, the contents of pc (r7) are transferred
across the bus and loaded into ADDR. This address is provided to the memory. In addition to fetching instructions,
the processor can read data at any address by using the ADDR register. Both data and instructions are read into the
processor on the DIN input port. The processor can write data for storage at an external address by placing this
address into the ADDR register, placing the data to be stored into its DOUT register, and asserting the output of
the W (write) flip-flop to 1.
2
Figure 13 illustrates how the enhanced processor can be connected to memory and other devices. The memory unit
in the figure supports both read and write operations and therefore has both address and data inputs, as well as a
write enable input. The memory also has a clock input, because the address, data, and write enable inputs must be
loaded into the memory on an active clock edge. This type of memory unit is usually called a synchronous static
random access memory (synchronous SRAM). Figure 13 also includes a 9-bit register that can be used to store data
from the processor; this register might be connected to a set of LEDs to allow display of data on your DE-series
board. To allow the processor to select either the memory unit or register when performing a write operation, the
circuit shows NOR gates that perform address decoding: if the upper address lines are A15 . . . A12 = 0, then the
memory module will be written. Figure 13 shows n lower address lines connected to the memory; for this exercise
a memory with 256 words is probably sufficient, which implies that n = 8 and the memory address port is driven
by A7 . . . A0. For addresses in which A15 . . . A12 = 1, the data written by the processor is loaded into the register
whose outputs are called LEDs in Figure 13.
Resetn
Clock
Memory
addr
q
Processor
16
DIN
ADDR
Resetn
RunDone
Run
DOUT
wr_en
data
n
W
9
E
D Q LEDs
16 16
9
4
A15
A12
A14
A13
4
A15
A12
A14
A13
Figure 13: Connecting the enhanced processor to a memory unit and output register.
Part III
Figure 14 gives Verilog code for a top-level file that you can use for this part of the exercise. This file implements
the circuit illustrated in Figure 13, plus an additional input port that is connected to switches SW8 . . . SW0. This
input port can be read by the processor at addresses in which A15 . . . A12 = 3. Switch SW9 is not a part of the
switch port, because it is dedicated for use as the processor’s Run input.
The code in Figure 14 is provided on the course website, along with a few other source-code files: flipflop.v,
inst_mem.v, inst_mem.mif, and proc.v. The inst_mem.v source-code file was created by using the Quartus IP
Catalog to instantiate a RAM: 1-PORT memory module. The memory has one 16-bit wide read/write data port
and is 256 words deep.
The proc.v file provides partially-completed Verilog code for the enhanced processor. This code implements
register r7 as a program counter, as discussed above, and includes a number of changes that are needed to support
the new ld, st, mvnz, and mvnc instructions. In this part you are to augment the provided Verilog code to complete
the implementation of the ld and st instructions. You do not need to work on the mvnz or mvnc instructions for this
part.
3
module part3 (KEY, SW, CLOCK_50, LEDR);
input [0:0] KEY;
input [9:0] SW;
input CLOCK_50;
output [9:0] LEDR;
wire [15:0] DOUT, ADDR;
wire Done;
reg [15:0] DIN;
wire W, Sync, Run;
wire inst_mem_cs, SW_cs, LED_reg_cs;
wire [15:0] inst_mem_q;
wire [8:0] LED_reg, SW_reg; // LED[9] and SW[9] are used for Run
// synchronize the Run input
flipflop U1 (SW[9], KEY[0], CLOCK_50, Sync);
flipflop U2 (Sync, KEY[0], CLOCK_50, Run);
proc U3 (DIN, KEY[0], CLOCK_50, Run, DOUT, ADDR, W, Done);
assign inst_mem_cs = (ADDR[15:12] == 4’h0);
assign LED_reg_cs = (ADDR[15:12] == 4’h1);
assign SW_cs = (ADDR[15:12] == 4’h3);
inst_mem U4 (ADDR[7:0], CLOCK_50, DOUT, inst_mem_cs & W, inst_mem_q);
always @ (*)
if (inst_mem_cs == 1’b1)
DIN = inst_mem_q;
else if (SW_cs == 1’b1)
DIN = {7’b0000000, SW_reg};
else
DIN = 16’bxxxxxxxxxxxxxxxx;
regn #(.n(9)) U5 (DOUT[8:0], LED_reg_cs & W, CLOCK_50, LED_reg);
assign LEDR[8:0] = LED_reg;
assign LEDR[9] = Run;
regn #(.n(9)) U7 (SW[8:0], 1’b1, CLOCK_50, SW_reg); // SW[9] is used for Run
endmodule
Figure 14: Verilog code for the top-level file.
Perform the following:
1. Augment the code in proc.v so that the enhanced processor fully implements the ld and st instructions.
Test your Verilog code by using the ModelSim simulator. Include in the project the top-level file from
Figure 14 and the other related source-code files. Sample setup files for ModelSim are provided on the
course website, along with the other files for this exercise. The memory module inst_mem will be initialized
with the contents of the file inst_mem.mif. This file represents the assembly-language program shown in
Figure 15, which tests the ld and st instructions by reading the values of the SW switches and writing these
values to the LEDs, in an endless loop. Make sure that your testbench sets the Run switch, SW9, so that
your processor will execute instructions. Observe the signals inside your processor, toggle the other SW
switches, and observe the LED signals in the top-level module.
The file inst_mem.mif was created by assembling the program in Figure 15 using the Assembler sbasm.py.
This Assembler is written in Python and is available on the course website. To use this Assembler you have
to first install Python (version 2.7) on your computer. The Assembler includes a README file that explains
4
its use. The sbasm.py assembler allows you to define symbolic constants using a .define directive, and it
supports the use of labels for referring to lines of code, such as MAIN in the figure.
.define LEDR_ADDRESS 0x1000
.define SW_ADDRESS 0x3000
// Read SW switches and display on LEDs
mvi r3, #LEDR_ADDRESS // point to LED port
mvi r4, #SW_ADDRESS // point to SW port
MAIN: ld r0, [r4] // read SW values
st r0, [r3] // light up LEDs
mvi r7, #MAIN
Figure 15: Assembly-language program that uses ld and st instructions.
2. Once your simulation results are correct, create a new Quartus project for your Verilog code. Compile your
code using the Quartus Prime software, and download the resulting circuit into the DE1-SoC board. Toggle
the SW switches and observe the LEDs to test your circuit.
Part IV
In this part you are to create a new Verilog module to be instantiated in your top-level module from Part III. The
new module will function as an output port called seg7_scroll. It will allow your processor to write data to each
of the six 7-segment displays on a DE-series board. Include six write-only seven-bit registers in the seg7_scroll
module, one for each display. Each register should directly drive the segment lights for one seven-segment display,
so that the processor can write characters onto the displays.
Create the necessary address decoding to allow the processor to write to the registers in the seg7_scroll module
at addresses in which A15 . . . A12 = 2. Address 0x2000 should select the register that controls display HEX0,
0x2001 should select the register for HEX1, and so on. If your processor writes 0 to address 0x2000, then the
seg7_scroll module should turn off all of the segment-lights in the HEX0 display; writing 0x7f should turn on
all of the lights in this display. You will need to add ports to your top-level design for the six 7-segment displays.
Pin assignments for these ports, which are called HEX0[6:0], HEX1[6:0], . . ., HEX6[6:0], are included in the
Quartus settings files provided on the course website. Segment 0 is on the top of a seven-segment display, and
then segments 1 to 5 are assigned in a clockwise fashion, with segment 6 being in the middle of the display. Thus,
the Verilog assignment statement
HEX0[6:0] = 7’b01101101;
would display the digit 5 on HEX0. Perform the following:
1. You may want to first simulate/debug your code with ModelSim. Then, create a Quartus project for this
part of the exercise and write the Verilog code for the seg7_scroll module. Modify your top-level module to
connect the processor to the seg7_scroll module, and to connect this module to the HEX display pins.
2. To test your circuit you can use the inst_mem.mif file provided for this part of the exercise on the course
website, which corresponds to the assembly-language program shown in Figure 16. This program works as
follows: it reads the SW switch port and lights up a seven-segment display corresponding to the value read.
For example, if the SW switches were set to 0, then the digit 0 would be shown on HEX0. If the switches
were set to 1, then the digit 1 would be displayed on HEX1, and so on, up to the digit 5 which would be
shown on HEX5.
5
.define LED_ADDRESS 0x1000
.define HEX_ADDRESS 0x2000
.define SW_ADDRESS 0x3000
// This program shows a decimal digit on the HEX displays
mv r5, r7 // return address for subroutine
mvi r7, #BLANK // call subroutine to clear HEX displays
MAIN: mvi r2, #HEX_ADDRESS // point to HEX port
mvi r3, #DATA // used to get 7-segment display patterns
mvi r4, #SW_ADDRESS // point to SW port
ld r0, [r4] // read switches
mvi r4, #LED_ADDRESS // point to LED port
st r0, [r4] // light up LEDs
add r2, r0 // point to correct HEX display
add r3, r0 // point to correct 7-segment pattern
ld r0, [r3] // load the 7-segment pattern
st r0, [r2] // light up HEX display
mvi r7, #MAIN
// Subroutine BLANK
// This subroutine clears all of the HEX displays
// input: none
// returns: nothing
BLANK:
mvi r0, #0 // used for clearing
mvi r1, #1 // used to add/sub 1
mvi r2, #HEX_ADDRESS // point to HEX displays
st r0, [r2] // clear HEX0
add r2, r1
st r0, [r2] // clear HEX1
add r2, r1
st r0, [r2] // clear HEX2
add r2, r1
st r0, [r2] // clear HEX3
add r2, r1
st r0, [r2] // clear HEX4
add r2, r1
st r0, [r2] // clear HEX5
add r5, r1
add r5, r1
mv r7, r5 // return from subroutine
DATA: .word 0b00111111 // ’0’
.word 0b00000110 // ’1’
.word 0b01011011 // ’2’
.word 0b01001111 // ’3’
.word 0b01100110 // ’4’
.word 0b01101101 // ’5’
Figure 16: Assembly-language program that tests the seven-segment displays.
6
Part V
In this part you are to enhance your processor so that it implements the mvnz and mvnc instructions. To do this, you
should create two condition-code flags in your processor. One flag, z, should be set to 1 when the adder/subtracter
unit in the processor generates a result of zero; otherwise z should be 0. The other flag, c, should be set to 1 when
the adder/subtracter unit produces a carry-out of 1; otherwise c should be 0. Thus, c should be 1 when an add
instruction generates a carry-out, or when a sub instruction requires a borrow for the most-significant bit. The
mvnz and mvnc instructions should perform a mv operation when z = 0 and c = 0, respectively.
The mv instruction should also affect the z flag, based on whether the result produced by the instruction is zero.
For example, if register r0 contains 0, then mv r1,r0 should set the z flag to 1, because the instruction produces the
result r1 = 0. The mv instruction may have an undefined effect on the c flag.
Perform the following:
1. Enhance your processor so that it implements the condition-code flags z and c, and supports the mvnz
and mvnc instructions. Also, modify your processor’s mv instruction so that it sets, or clears, the z flag
appropriately. You may want to use ModelSim to test/debug your code, and then create a new Quartus
project to implement this part of the exercise in the DE1-SoC board.
2. To test your processor, you can use the inst_mem.mif file that is provided for this part of the exercise on the
course website. This file corresponds to the assembly-language program displayed in Figure 17. It provides
code that tests for the correct operation of instructions supported by the enhanced processor. If all of the
tests pass, then the program shows the word PASSEd on the seven-segment displays. It also shows the
binary value 1000 on the LEDs, representing eight successful tests. If any test fails, then the program
shows the word FAILEd on the seven-segment displays and places on the LEDs the address in the memory
of the instruction that caused the failure. If a failure occurs, then the offending instruction can be identified
by cross-referencing the LED pattern with the addresses in the file inst_mem.mif.
.define LED_ADDRESS 0x1000
.define HEX_ADDRESS 0x2000
// This code tests various instructions
mvi r0, #0
mvi r1, #1
mvi r2, #0 // used to count number of successful tests
mvi r5, #FAIL
mvi r6, #T1 // save address of next instruction
add r0, r0 // set the z flag
// test mvnz
T1: mvnz r7, r5 // should not take the branch!
add r2, r1 // incr success count
mvi r4, #S1
mvi r6, #T2 // save address of next instruction
// test mv’s effect on z flag
mv r3, r1 // reset the z flag
mvnz r7, r4 // should take the branch!
T2: mvi r7, #FAIL
S1: add r2, r1 // incr success count
mvi r4, #S2
mvi r6, #T3 // save address of next instruction
Figure 17: Assembly-language program that tests various instructions. (Part a)
7
add r3, r1 // reset the z flag
mvnz r7, r4 // should take the branch!
T3: mvi r7, #FAIL
S2: add r2, r1 // incr success count
mvi r6, #T4
mv r0, r0 // set the z flag
// test mv’s effect on z flag
T4: mvnz r7, r5 // should not take the branch!
add r2, r1 // incr success count
mvi r6, #T5 // save address of next instruction
mvi r3, #0xffff
add r3, r1 // set the c flag
// test mvnc
T5: mvnc r7, r5 // should not take the branch!
add r2, r1 // incr success count
mvi r4, #S3
mvi r6, #T6
add r3, r0 // clear carry flag
// test mvnc
mvnc r7, r4 // should take the branch!
T6: mvi r7, #FAIL
S3: add r2, r1 // count the success
// finally, test ld and st from/to memory
mvi r6, #T7 // save address of next instruction
mvi r4, #_LDTEST
ld r4, [r4]
mvi r3, #0xA5A5
sub r3, r4
T7: mvnz r7, r5 // should not take the branch!
add r2, r1 // incr success count
mvi r6, #T8 // save address of next instruction
mvi r3, #0xA5A5
mvi r4, #_STTEST
st r3, [r4]
ld r4, [r4]
sub r3, r4
T8: mvnz r7, r5 // should not take the branch!
add r2, r1 // incr success count
mvi r7, #PASS
// Show the result on the HEX displays
FAIL: mvi r3, #LED_ADDRESS
st r6, [r3] // show address of failed test on LEDs
mvi r5, #_FAIL
mvi r7, #PRINT
PASS: mvi r3, #LED_ADDRESS
st r2, [r3] // show success count on LEDs
mvi r5, #_PASS
Figure 17: Assembly-language program that tests various instructions. (Part b)
8
PRINT: mvi r1, #1
mvi r4, #HEX_ADDRESS // address of HEX0
// We would normally use a loop counting down from 6
// with mvnz to display the six letters. But in this
// testing code we can’t assume that mvnz even works!
ld r3, [r5] // get letter
st r3, [r4] // send to HEX display
add r5, r1 // ++increment character pointer
add r4, r1 // point to next HEX display
ld r3, [r5] // get letter
st r3, [r4] // send to HEX display
add r5, r1 // ++increment character pointer
add r4, r1 // point to next HEX display
ld r3, [r5] // get letter
st r3, [r4] // send to HEX display
add r5, r1 // ++increment character pointer
add r4, r1 // point to next HEX display
ld r3, [r5] // get letter
st r3, [r4] // send to HEX display
add r5, r1 // ++increment character pointer
add r4, r1 // point to next HEX display
ld r3, [r5] // get letter
st r3, [r4] // send to HEX display
add r5, r1 // ++increment character pointer
add r4, r1 // point to next HEX display
ld r3, [r5] // get letter
st r3, [r4] // send to HEX display
add r5, r1 // ++increment character pointer
add r4, r1 // point to next HEX display
HERE: mvi r7, #HERE
_PASS: .word 0b0000000001011110 // d
.word 0b0000000001111001 // E
.word 0b0000000001101101 // S
.word 0b0000000001101101 // S
.word 0b0000000001110111 // A
.word 0b0000000001110011 // P
_FAIL: .word 0b0000000001011110 // d
.word 0b0000000001111001 // E
.word 0b0000000000111000 // L
.word 0b0000000000110000 // I
.word 0b0000000001110111 // A
.word 0b0000000001110001 // F
_LDTEST: .word 0xA5A5
_STTEST: .word 0x5A5A
Figure 17: Assembly-language program that tests various instructions. (Part c)
Part VI
Write an assembly-language program that displays a binary counter on the LED port. Initialize the counter to 0,
and then increment the counter by one in an endless loop. You should be able to control the speed at which the
counter is incremented by using nested delay loops, along with the SW switches. If the SW switches are set to
their maximum value, 0b111111111, then the delay loops should cause the counter to increment slowly enough
so that each change in the counter can be visually observed on the LEDs. Lowering the value of the SW switches
should make the counter increment more quickly up to some maximum.
9
Assemble your program by using the sbasm.py Assembler (or assemble it by hand if you prefer!). Save the output
produced by sbasm.py in a new file named inst_mem.mif. Make sure that the new MIF file is stored in the folder
that holds your Quartus project for Part V. Then use the Quartus command Processing > Update Memory
Initialization File, to include your new inst_mem.mif file in your Quartus project. Next, select the
Quartus command Processing > Start > Start Assembler to produce a new programming bitstream
for your DE-series board. Finally, use the Quartus Programmer to download the new bitstream onto your board.
If the Run signal is asserted, then your processor will execute your the program.
Part VII
Augment your assembly-language program from Part VI so that counter values are displayed on the seven-segment
display port rather than on the LED port. You should display the counter values as decimal numbers from 0 to
65535. The speed of counting should be controllable using the SW switches in the same way as for Part VI. As
part of your solution you may want to make use of the code shown in Figure 18. This code provides a subroutine
that divides the number in register r0 by 10, returning the quotient in r2 and the remainder in r0. Dividing by 10 is
a useful operation when performing binary-to-decimal conversion.
// subroutine DIV10
// This subroutine divides the number in r0 by 10
// The algorithm subtracts 10 from r0 until r0 < 10, and keeps count in r2
// input: r0
// returns: quotient Q in r2, remainder R in r0
DIV10:
mvi r1, #1
sub r6, r1 // save registers that are modified
st r3, [r6]
sub r6, r1
st r4, [r6] // end of register saving
mvi r2, #0 // init Q
mvi r3, RETDIV // for branching
DLOOP: mvi r4, #9 // check if r0 is < 10 yet
sub r4, r0
mvnc r7, r3 // if so, then return
INC: add r2, r1 // but if not, then increment Q
mvi r4, #10
sub r0, r4 // r0 -= 10
mvi r7, DLOOP // continue loop
RETDIV:
ld r4, [r6] // restore saved regs
add r6, r1
ld r3, [r6] // restore the return address
add r6, r1
add r5, r1 // adjust the return address by 2
add r5, r1
mv r7, r5 // return results
Figure 18: A subroutine that divides by 10
As described for Part VI, assemble your program with sbasm.py, update your MIF file in the Quartus software,
generate a new bitstream file by using the Quartus Assembler, and then download the new bitstream onto your
DE-series board to run your code.
10

More products