CS233 Lab 9 Handout
“The cheapest, fastest and most reliable components of a computer system are those that aren’t there.”
– Gordon Bell
Learning Objectives
1. Understanding memory-mapped I/O in hardware
2. Understanding interrupt handling in hardware
3. Understanding the functioning of coprocessor 0
Work that needs to be handed in (via SVN)
First deadline
1. timer.v: The Verilog implementation of the timer circuit.
Second deadline
1. cp0.v: The Verilog implementation of coprocessor 0.
2. machine.v: The single-cycle machine combined with the timer and coprocessor 0 circuits.
Guidelines
• Please read the entirety of the handout carefully and make sure you understand the circuits
before implementing them. It’ll make your life a lot easier and also help you debug and test
your circuits.
• Look over the provided Verilog files. You don’t need to understand the syntax used (since it’s
deliberately goofy), but you should be aware of what modules we provide, since you’ll need to
use some of them in your implementation. In particular, we’ve provided flip flops and registers
with synchronous reset inputs, which will be used in our implementation.
Equality circuit
For our timer circuit, we’ll need to compare addresses against known values. We’ll use the following
symbol to indicate this, where A and B can be any number of bits wide and eq is 1 if A and B are
equal. In Verilog, you could simply write A == B.
=
A
B
eq
1
CS233 Lab 9 Handout
Buses and tri-state buffers
As discussed in lecture, all hardware devices use the memory bus to communicate, which means all
devices need to output to the data bus. So far, we’ve been using muxes to select between multiple
sources for the same output line, but a more scalable (and realistic) solution is the tri-state buffer.
A tri-state buffer has a data input d, a control input c, and an output o. If c is 1, the input is passed
to the output, i.e., o = d. If c is 0, however, the output is high impedance, which is essentially
equivalent to it being disconnected. You can think of a tri-state buffer as a switch, where c = 0
means the switch is off and c = 1 means the switch is on. In Verilog, a high impedance value is
represented as z, or as a yellow line in gtkwave.
d
c
o
c d o
0 X z
1 0 0
1 1 1
Figure 1. A tri-state buffer and its truth table
A high impedance wire can be connected to other wires without affecting them. Tri-state buffers
therefore allow multiple outputs to be connected to the same bus directly. Only one tri-state should
be enabled at a time, and the output of the enabled tri-state becomes the output of the entire bus.
For example, here’s how to implement a 2-to-1 mux using tri-state buffers (and larger muxes are in
fact usually made from tri-state buffers):
A
B
~control
control out
Figure 2. A 2-to-1 mux built from tri-state buffers
We’ve provided a parametrized tri-state buffer in modules.v. We’ve also modified our data memory
such that when its read input is 0, its data output is high impedance (using a tri-state buffer).
You’ll use this to connect the data memory and the timer device to the same data bus.
2
CS233 Lab 9 Handout
The timer circuit [30 points]
The timer is a hardware device which allows the processor to read the number of clock cycles that
have elapsed, as well as request an interrupt at a certain cycle.
As with any hardware device, the processor communicates with the timer using memory-mapped
I/O. It can load from address 0xffff001c to get the current number of cycles, and it can store a
value to this address to request an interrupt at that cycle. It can also store any value to address
0xffff006c to acknowledge the timer interrupt.
Here’s what our timer circuit will look like:
reset
enable
Q[31:0]
D[31:0]
Cycle counter
add
32
1
ALU
32
3
1
32
reset
enable
Q[31:0]
D[31:0]
Interrupt cycle
MemRead
MemWrite
address
data
=
clock
reset
=
0xffff001c 32
reset
TimerWrite
reset
clock
clock
=
0xffff006c 32
32
AND
AND
TimerRead
TimerRead
TimerWrite
AND Acknowledge
OR
reset
enable
Q
D
Interrupt line
clock
OR
Acknowledge
reset
1
32
clock
reset
TimerInterrupt
cycle
TimerAddress
It takes as input the memory address and data, and whether memory is being read or written,
as well as the clock and reset signals. The cycle output is the current cycle if the timer was
being read, otherwise high impedance. The TimerAddress output is 1 when the address is a
timer memory-mapped I/O address (so that data memory reads and writes can be disabled). The
TimerInterrupt output is 1 when a timer interrupt is being raised.
The cycle counter register keeps track of the number of cycles. The interrupt cycle register stores
which cycle to interrupt the processor on, and when the current cycle matches the interrupt cycle, a
timer interrupt is raised by enabling the interrupt line flip flop. The interrupt line stays asserted
3
CS233 Lab 9 Handout
until the interrupt is acknowledged or the machine is reset.
Make sure you understand how this circuit works, and then write the Verilog for it in timer.v.
We’ve provided a basic test bench in timer_tb.v, which you should augment with your own test
cases. You can use make timer to compile and run the test bench.
4
CS233 Lab 9 Handout
Coprocessor 0 [35 points]
Coprocessor 0 handles interrupts and exceptions. In this lab, we only have to handle the timer
interrupt, and we don’t deal with exceptions. Our coprocessor will therefore be quite simple, but
it’ll still capture all the important ideas underlying a real coprocessor 0 implementation.
Status register
The status register (coprocessor 0 register 12) controls which interrupts are enabled and whether
any are enabled at all. We’re concerned with the following bits of the status register:
Each bit in the interrupt mask corresponds to a disabling (if it’s 0) or enabling (if it’s 1) a particular
type of interrupt; for us, bit 15 corresponds to the timer interrupt. The interrupt enable bit controls
whether any interrupts are taken. These bits can be set by user code using the mtc0 instruction.
The exception level bit is 1 whenever an interrupt or exception is currently being handled. This
along with all the other status register bits (which we’ll just force to 0 in our implementation)
cannot be set by user code; an mtc0 to the status register should not modify these bits.
To enforce this, we’ll actually have one register to hold the bits which can be set by the user, and
another flip flop to hold the exception level bit. We’ll combine the two to get our final status register
value:
5
CS233 Lab 9 Handout
0 status_register[16]
0 status_register[17]
0 status_register[18]
0 status_register[19]
0 status_register[20]
0 status_register[21]
0 status_register[22]
0 status_register[23]
0 status_register[24]
0 status_register[25]
0 status_register[26]
0 status_register[27]
0 status_register[28]
0 status_register[29]
0 status_register[30]
0 status_register[31] user_status[15]
user_status[14]
user_status[13]
user_status[12]
user_status[11]
user_status[10]
user_status[9]
user_status[8]
status_register[15]
status_register[14]
status_register[13]
status_register[12]
status_register[11]
status_register[10]
status_register[9]
status_register[8]
0 status_register[2]
0 status_register[3]
0 status_register[4]
0 status_register[5]
0 status_register[6]
0 status_register[7]
exception_level
user_status[0]
status_register[1]
status_register[0]
Cause register
The cause register (coprocessor 0 register 13) tells us which interrupt or exception occurred:
The cause register isn’t implemented as an actual register. Instead, each of the pending interrupt
bits is connected to the corresponding interrupt line; for us, bit 15 will be connected to the timer
interrupt line. The exception code would be encoded from the incoming exception lines, but since
we don’t have any exceptions, we’ll just hardcode the exception code bits to 0:
6
CS233 Lab 9 Handout
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0 cause_register[31] TimerInterrupt cause_register[15]
0
0
0
0
0
0
cause_register[30]
cause_register[29]
cause_register[28]
cause_register[27]
cause_register[26]
cause_register[25]
cause_register[24]
cause_register[23]
cause_register[22]
cause_register[21]
cause_register[20]
cause_register[19]
cause_register[18]
cause_register[17]
cause_register[16]
0
0
0
0
0
0
0
0
0
cause_register[14]
cause_register[13]
cause_register[12]
cause_register[11]
cause_register[10]
cause_register[9]
cause_register[8]
cause_register[7]
cause_register[6]
cause_register[5]
cause_register[4]
cause_register[3]
cause_register[2]
cause_register[1]
cause_register[0]
EPC register
The exception program counter register (coprocessor 0 register 14) stores the address the interrupt
handler should return to. Whenever an interrupt is taken, this register is updated with what the next
PC would have been normally, so that execution can resume there after the interrupt is serviced.
Reading and writing coprocessor 0 registers
The mtc0 instruction is used to write coprocessor 0 registers. As usual, we’ll use a decoder to
enable the register being written. Since the actual coprocessor has 32 registers, we’ll use a 5-to-32
decoder, although only bits 12 and 14 of the decoder output will get used (since we can only write
to the status and EPC registers).
The mfc0 instruction is used to read coprocessor 0 registers. We’ll use a 32-to-1 mux to select
which register value to output. Inputs 12, 13 and 14 will be connected to the status, cause and
EPC registers, respectively; the rest will be connected to 0s (since we don’t implement any other
registers).
Complete coprocessor 0 circuit
We can put all our components together to get a complete coprocessor 0 circuit:
7
CS233 Lab 9 Handout
reset
enable
Q[29:0]
D[29:0]
EPC register
12
14
reset
clock
MTC0
clock
reset
wr_data
regnum
rd_data
TakenInterrupt
TimerInterrupt
next_pc
ERET
AND
cause_register[15]
status_register[15]
status_register[0]
AND
status_register[1]
AND
OR
TakenInterrupt
reset
enable
Q[31:0]
D[31:0]
User status
reset
clock
32
12
13
14
status_register
32
cause_register
32
30
0
1
30
reset
enable
Q
D
Exception level
clock
1
OR
TakenInterrupt
reset
5
user_status
exception_level
32
cause_register[15]
30
5
clock
reset
TakenInterrupt
EPC
NOT
32
2’b0
32
wr_data[31:2]
It takes as input what coprocessor register number is being read or written, what data is being
written to it, whther the current instruction is an mtc0 or an eret, what the regular next PC
would have been, whether a timer interrupt is being raised (from our timer circuit), and the clock
and reset signals. It outputs whether an interrupt should be taken, the value of the coprocesor
register being read, and the value of the EPC register.
The EPC register can be written both by user code (using the mtc0 instruction) and by the
coprocessor itself (when an interrupt is taken), hence enabling it under both circumstances and
using a mux to select which value to write.
We take an interrupt when the timer interrupt is enabled and we have a pending timer interrupt,
the interrupt enable bit is set, and we’re not already handling an interrupt. In an actual coprocessor,
we would have to check for all other interrupts being enabled and pending too.
The eret instruction is used to return from an exception. The exception level bit should be set to
0 upon the execution of this instruction, hence resetting the exception level flip flop when an eret
instruction is seen.
Make sure you understand how this circuit works, and then write the Verilog for it in cp0.v. We’ve
provided a basic test bench in cp0_tb.v, which you should augment with your own test cases.
You can use make cp0 to compile and run the test bench.
8
CS233 Lab 9 Handout
Putting it all together [35 points]
Now that we have our timer and coprocessor 0 circuits, we can combine them with the given
single-cycle datapath (figure included below for reference) to support I/O and interrupts in our
machine. The required additions are described in textual and via a circuit diagram.
Our provided decoder (in modules.v) already supports the mfc0, mtc0 and eret instructions,
and outputs control signals for each of them.
You’ll connect the timer circuit’s address and data inputs to the same sources as the data memory’s
address and write data inputs, respectively, the MemRead and MemWrite inputs to the corresponding
control signals, and the clock and reset inputs to the corresponding signals. The TimerAddress
output being 1 should force the data memory’s read and write inputs to be 0 (through ANDing
with the NotIO signal), and the cycle output should be connected directly to the same bus as the
data memory’s read data output (because of the internal tri-state buffers in both).
Coprocessor 0’s TimerInterrupt input should be connected to the TimerInterrupt output from the
timer circuit. The MTC0 and ERET inputs should be connected to the corresponding decoder
outputs, and clock and reset connected to the corresponding signals. The regnum input should be
connected to rd and the c0’s wr_data intput to R[rt], since those are the registers used by the
mfc0 and mtc0 registers. The processor’s next_pc input should be connected to the corresponding
bus (to be precise, what that bus represents in the unmodified datapath). You’ll need to write the
c0’s rd_data output to the register file on an mfc0 instruction; the provided decoder already takes
care of the control signals, so you just need to adjust the register file’s write data input accordingly.
Finally, you’ll need to adjust control flow to account for interrupts and interrupt returns. When
the TakenInterrupt output from coprocessor 0 is 1, the PC should be set to 0x80000180 (the MIPS
interrupt handler address), and on an eret instruction, the PC should be set to the EPC output
from coprocessor 0.
Make all these changes to the single cycle datapath in machine.v, and then compile and run
your circuit using make machine. You can modify test.s to change the assembly code being
executed.
9
CS233 Lab 9 Handout
0
1
Addr
Instruction
memory
Instr
Address
Write
data
Data
memory
Read
data 1
0
Extend
ALUSrc
Result
ALU
Instr [15 – 0]
RegDst
Read
register 1
Read
register 2
Write
register
Write data
Read
data 2
Read
data 1
Registers
Rd
0
1
4
P
C
Add
1
0
PCSrc
Add
Shift
left 2
Rt
Rs
opcode
funct Decoder
MemToReg
control
signals
PCSrc AND
BEQ
Write enable
PC
next_PC
PC_plus4
PC_target
RegWrite
imm
rd1_data
rd2_data
ALUOp
alu_out_data
zero
negative
MemRead
MemWrite
load_data
wr_data
inst
Figure 3. Single-cycle datapath as given.
0
1
Addr
Instruction
memory
Instr
Address
Write
data
Data
memory
Read
data 1
0Extend
ALUSrc
Result
ALU
Instr [15 – 0]
RegDst
Read
register 1
Read
register 2
Write
register
Write data
Read
data 2
Read
data 1
Registers
Rd
0
1
4
P
C
Add
1
0
PCSrc
Add
Shift
left 2
Rt
Rs
opcode
funct Decoder
MemToReg
control
signals
(including
mfc0,mtc0,
eret)
PCSrcAND
BEQ
Write enable
PC
PC_plus4
PC_target
RegWrite
imm
rd1_data
rd2_data
ALUOp
alu_out_data
zero
negative
MemRead
MemWrite
load_
data
wr_data
inst
t:address
t:data, c0:wr_data
AND
AND
NotIO
NOT NotIOTimerAddress
cycle
0
1
1 0Taken
Interrupt
0x80000180
c0:rd_data
mfc0
1 0ERET
EPC
CP0
regnum,
c0:wr_data,
next_PC,
TimerInterrupt,
mtc0, eret,
clock, reset
next_PC
c0:rd_data,
EPC,
TakenInterrupt
Timer
t:address,
t:data,
MemRead,
MemWrite,
clock, reset
TimerInterrupt,
TimerAddress,
cycle
Figure 4. Single-cycle datapath extended so that it can be connected to the timer and co-processor 0 modules.
10