This lab focuses on the coordination of asynchronous operations, such as interruptions, with the processor. Instead of using syscalls in this assignment, all input and output must be handled using interruption handlers and memory-mapped device access. Display functions use memory-mapped device access managed by an interruption handler. The creation of the interruption handler is part of this assignment.
The SIR model is a simple compartmental model that is used to model infectious diseases. In the SIR model, the population is assigned to compartments labeled S, I, or R (Susceptible, Infectious, or Recovered). Susceptible individuals are not infected but could catch the disease. Infectious individuals are currently infected and may transmit the disease to susceptibles. Recovered individuals have recovered from the disease and can neither transmit nor catch the disease. In this lab, we focus on the SIRD model, an extension of the SIR model adding the compartment D (Deceased). While deceased individuals are defined the same as recovered individuals, they are distinguished as killed by the disease. To learn more about the SIR model view this page.
Write a RISC-V program that will simulate the spread of an infectious disease among a small population using a SIRD model.
This lab uses external interrupts from hardware. The role of four CSRs (Control and Status Registers) are important for the use of interrupts:
ustatus
(User Status Register, CSR#0
) is a 32-bit
register that controls and manages user-level interrupts in the hardware thread
(hart). To enable user-level interrupts set the 0th bit of this register
to 1.uie
(User-Interrupt Enable Register, CSR#4
) is a 32-bit
register that controls the types of interrupts that are enabled using a bitmask.
Bits 4 and 8 are relevant for this lab. The 4th enables user-level timer interrupts.
The 8th bit enables user-level external interrupts. These bits must be set to 1 to
enable interrupts from the timer and the keyboard.utvec
(User Trap-Vector Base-Address Register, CSR#5
)
is a 32-bit register that controls where interrupts are handled. The register
holds the address of the interrupt handler that should be called when an interrupt
or exception occurs.ucause
(User Trap Cause Register, CSR#66
) is a 32-bit
register that controls where interrupts are handled. After an exception or
an interrupt, this register holds the interrupt/exception code to help identify
its cause. An exception code is stored in the first 31 bits of ucause and the
last bit indicates whether or not it was an interrupt or an exception. To
check what type of exception/interrupt occurred refer to the table on the right.These CSRs can be set by using the CSR instructions. For example, to enable
user-level interrupts in ustatus
use "CSR Read/Write Immediate"
instruction: csrrwi zero, 0, 0x1
. Or use pseudo-instructions to read
and write to the CSR registers. For example:
csrr t0, 4   # read from CSR#4 to t0
csrw t0, 6   # write whats in t0 to CSR#6
csrwi 0, 0x4 # write 0x4 to CSR#0
Once an interrupt is raised it must be handled in the interrupt
handler created in this assignment. An interrupt handler is
analogous to a normal function but there are some key
differences. An interrupt can occur at any time, therefore the
handler must guarantee that all registers are restored to their
original values after the handler finishes. Thus, the handler must
save any register that it uses (not just the s
registers) and the handler must restore the original values to
these registers before returning. Also, the instruction
uret
must be used to leave the interrupt handler
instead of the jr ra
instruction that is used to
return from a normal function.
Use the Keyboard and Display MMIO Simulator
, available under the
"Tools" menu in RARS, to interact with the simulation. Display the
simulation in the display section, and input commands in the keyboard
section. Don't forget to click "Connect To Program" after assembling the program
and before running it.
Generally, devices have two registers associated with them, a control, and a data register. The control register relays information about the device's state, and the data register relays data to or from a device. A description of the control and data registers for the keyboard and display can be found in the Memory-Mapped IO section.
A separate keyboard interrupt occurs for every key pressed when the keyboard
interrupts are enabled. Therefore, the user program receives one character at
a time. The solution for this lab has to build a string using these characters
until it receives a newline character (ASCII code 0x0a
), corresponding
to the user pressing enter. At that point, the solution has to parse the string,
perform the appropriate actions, and reset the string for the next input. For more
information about the tool, click the help button in the tool window.
In RISC-V, timing functionality is managed by the timing hardware thread
(hart), maintaining the time asynchronously and allowing the program to raise
an interrupt at a specific time. To do this the core keeps track of the time in
the 64-bit register time
which holds the current time (in milliseconds)
since the program started. To generate a timer interrupt at a specified time,
the value in the register timecmp
must be set. A description of the
time
and timecmp
registers can be found in the
Memory-Mapped IO section. To simulate RISC-V timing functionality
use the Timer Tool
under the "Tools" menu in RARS. Don't forget
to click "Connect To Program" and "Play" after assembling the program and before
running it.
Memory-mapped IO allows interaction with external devices through an interface pretending to be system memory. This mapping allows the processor to communicate with these devices using the load-word and store-word instructions. Here are the memory mappings and descriptions of important I/O registers for this lab:
Register | Memory Address | Description |
---|---|---|
Keyboard control | 0xFFFF0000 |
For keyboard interrupts to be enabled, bit 1 of this register must be set to 1; after the keyboard interrupt occurs, this bit is automatically reset to 0. |
Keyboard data | 0xFFFF0004 |
The ASCII value of the last key pressed is stored here. |
Display control | 0xFFFF0008 |
Bit 0 of this register indicates whether the processor can write to the display. While this bit is 0 the processor cannot write to the display. Thus, the program must wait until this bit is 1. |
Display data | 0xFFFF000C |
When a character is placed into this register, given that the display control
ready bit (bit 0) is 1, that character is drawn onto the display. If the
character is the bell character (ASCII code 0x07 ) the display will
move the cursor and the bits 8-19 and 20-31 correspond to the row and column
respectively. View the image to the right of this table. |
Time | 0xFFFF0018 |
This is a read-only register that holds the time since the program has started in milliseconds. |
Timecmp | 0xFFFF0020 |
When the value in this register is less than or equal to the value in
time a timer interrupt occurs. Writing to this register is
required to set up a timer. |
The task in this lab is to write a RISC-V program that will simulate the spread of an infectious disease among a small population using a SIRD model. The program will read in commands through the keyboard input and update the simulation accordingly.
The simulation is run on a population of ten individuals. Following the SIRD model, each individual is categorized as either susceptible (S), infectious (I), recovered (R), or deceased (D) at any given moment.
Status | Symbol | Description | Mechanics |
---|---|---|---|
Susceptible | S |
Have not been infected yet but may become infected. | Upon contacting an I person, have a 0.5 probability of
becoming I . |
Infectious | I |
Until they recover, they may spread the disease to susceptibles and have a small chance of becoming deceased. | Become R after 14 seconds. There is a 0.01 probability of becoming
D after each second. |
Recovered | R |
Have recovered from the disease and can neither spread nor catch the disease. | No specific mechanics. |
Deceased | D |
Have been killed by the disease and can neither spread nor catch the disease. | Cannot come in contact with other persons. |
The user enters characters in the keyboard section of the
Keyboard and Display MMIO Simulator
. If the user
types q
, then the program should immediately quit
execution. Whenever the user presses enter (\n
), the
program updates the simulation executes all the commands since the
last enter. If the input provided by the user does not follow any
of the below formats, the behavior of the program is
undefined. Undefined behavior means that the response of the
program is up to the programmer's discretion. The program will
not be tested on anything that has undefined behavior.
Let X
and Y
be characters within the range
'0'
-'9'
. The following commands have the described
behavior:
X!
: If X
is susceptible, X
becomes
infectious. Otherwise, the behavior is undefined.X^Y
: X
and Y
contact each other.
If exactly one of X
and Y
is infectious and the other is
susceptible, then the susceptible has a 0.5 probability of becoming infected. If either
X
or Y
is deceased, the behavior is undefined.c
: Reset the state of the simulator making all persons susceptible. Each of the commands above must be followed by
\n
. The behaviour of the program when a sequence of commands such as 3!9!\n
or
4^78!\n
is entered is undefined.
Upon starting, a line for each of the ten individuals in the
population is displayed. Persons are labeled 0
,
1
, ..., 9
. Each line contains the
current status of that individual. At the start of execution, all
persons have the status S
. An infectious person's
status should be displayed as I:yy
where
yy
is the number of seconds until the person
recovers; while infectious the number should count down from
14
to 01
each second. All other statuses
should be displayed using their symbol (S
,
R
, D
).
The annimation above is produced with the following commands:
1!
infects Person 1
.
Person 1
's status is set to I:14
and their timer begins counting down.6!
infects Person 6
.3^6
means Person 3
and
Person 6
come in contact. Person 3
has a 0.5 probability of becoming infected because
Person 3
is susceptible and Person
6
is infectious. Person 3
does not
become infected. 6^8
means that Person 6
and
Person 8
come in contact. As a susceptible,
Person 8
has a 0.5 probability of becoming
infected when contacting an infectious such as Person
6
. Person 8
becomes infected. 0^4
means that Person 0
and
Person 4
come in contact. Unlike the previous
cases, neither has any chance of becoming infected by
contacting each other because both Person 0
and
Person 4
are susceptible. Person 1
becomes deceased. Person 6
recovers from the disease. c
and all
individuals are reset as susceptible. 5!
infects Person 5
. q
which immediately
quits the execution of the program. The execution above was run with the values of 357
, 294
,
127
, and 1000
as X0
, a
,
c
, and m
respectively (these variables are described in the
next section).
A solution to this lab must generate random numbers to simulate random chance when determining if:
To decide if a random event occurs, check if a randomly generated number is less than the probability of the event multiplied by the range of the random number generator (provided that the numbers generated are between 0 and some positive integer). For example, to decide if an event with a 0.4 probability occurs with a generator that generates a random number between 0 and 9, we must check whether a randomly generated number is less than 4.
For this subproblem, implement a Linear Congruential Generator (LCG). An LCG is a simple pseudorandom number generator (PRNG) algorithm defined as the recurrence relation:
Xi = ( aXi-1 + c ) % m
Where X0
, a
, c
, and
m
are constants. X0
is the seed value,
a
is the multiplier, c
is the increment, and
m
is the modulus.
The first randomly generated number is X1
, the
second is X2
, and so on. In general terms,
Xi
is the i-th random number.
With a PRNG, the sequence of generated numbers is always the
same for the same values of X0
, a
,
c
, m
. Therefore, it is easy to know what numbers should be
generated and thus the desired behavior of the program.
Here is a useful tool
for calculating the randomly generated sequence based on values of
X0
, a
, c
, m
that
can be used to check the program when testing. To learn more about linear
congruential generators, view
this page.
The program must generate a pseudorandom number each time a random event may occur. ie. Generate a number when an infectious individual contacts a susceptible and for each infectious person each second.
Write assembly code for the following functions in sird.s
:
sird:
handler:
handlerTerminate
is provided. It should catch
interrupts/exceptions unhandled by the student handler, terminating the
program and providing debugging messages.jal ra, handler
. Instead, the address of the handler should be
stored in the utvec
control status register (CSR #5
)
at the start of the program. When an interrupt is raised,
execution jumps to the instruction address stored in utvec
.random:
a0
: a pseudorandom number, Xi
, between
0 and 999Xi
from
Xi-1
, a
, c
, and
m
. Replace Xi-1
in memory with the newly
generated Xi
.
XiVar
, aVar
, cVar
, and
mVar
from the data section of
common.s
(see the subsection Global Variables in the Constraints section for information on
these variables). Before returning, the generated number
Xi
must be stored in
XiVar
. It should be possible to call
random
multiple times, back-to-back. Therefore
any needed state update to the global variables must be
performed in the function.The optional functions below are suggestions and are not required for grading. They can help with the creation of a modularized solution that is easier to debug through unit testing. These functions may be included in the solution as suggested, or variations of such functions can be used.
getStatus:
(optional)a0
: character between '0'
and '9'
representing an individuala0
: character representing the status of the individual
('S'
, 'I'
, 'R'
, or 'D'
)setStatus:
(optional)a0
: character between '0'
and '9'
representing an individuala1
: character representing the status to set as the individual's
status ('S'
, 'I'
, 'R'
, or
'D'
)a0
: 1
if the individual's status was set successfully,
0
otherwiseupdateTimers:
(optional)printDisplay:
(optional)Write additional functions as needed. Code from the
materials provided in this course can be used in the solution as
long as the source is acknowledged. For example,
displayDemo.s
in Code/Demo/
may be helpful
for printing to the Keyboard and Display MMIO Simulator
display.
Reading or printing syscalls cannot be used in this program. Instead, the program must use the interrupt/poll system to interact with the keyboard and the display.
When executing RARS with your own handler, runtime errors won't be shown in RARS as usual. Thus, a section of the handler that prints the line where an error occurred and the error code is provided. Use the table to the right to identify the error.
In this assignment the main program remains running a forever loop and the state of the display changes when there is an interruption caused either by the input of a command or by a timer. A possible design would be for this forever loop to be constantly reprinting the display. However, such a design could produce a flickering in the screen in certain types of monitors. Therefore a better design is to have flags that indicate when an action must be taken in the forever loop. For instance, the display only need to be reprinted when its content has changed. Therefore the forever loop can be simply checking on specific flags and only take action when action is needed. Flags are variables that can store either 0 or 1. You can use flags to determine what should happen on an iteration of a forever loop. By using different flags for the separate tasks of the program, you can ensure that you only execute tasks when needed.
Here is an example of how you can use a flag to determine when a program prints its output:
changed
) that contains the
value:0
, if the output has not changed since it was last printed
and thus doesn't need to be printed.1
, if the output has changed since it was last printed and
thus should be printed.Using flags in the forever loop of your sird.s
file will also
separate your code into tasks making it easier to understand and debug.
The following lines appear in the .data
section of
common.s
:
This is similar to defining four global variables. These
variables are used in the linear congruential generator and should
contain the values Xi
, a
,
c
, and m
respectively. These variables
must be used in the random
function so that
the solution works with grading scripts. For testing, initialize
the first three values to any value, but keep mVar
as
1000.
When an interrupt is raised, the program is paused and execution is transferred to
the interrupt handler. To ensure that the program can safely resume execution after
returning from the handler, the registers used by the handler must be saved upon
entering the interrupt handler and restored before returning. The registers cannot be
saved using the stack pointer because the stack pointer may be corrupted. Therefore, in
common.s
we have allocated memory labeled iTrapData
where
your handler may save registers. In common.s
we have also placed the
address of iTrapData
into the control status register #64,
uscratch
. You can use uscratch
and the CSR instruction
csrrw
to save and restore all the values of registers used in the
handler.
After returning from the handler, all registers must have the same value as when the
program paused and uscratch
should contain the address of the
iTrapData
. The first instruction executed in the handler and the last
instruction executed before returning from the handler should be
csrrw a0, 0x040, a0
, where a0
is chosen by convention.
Here is some sample code that saves two registers and a0
in the
interrupt trap data:
handler: # swap a0 and uscratch cssrw a0, 0x040, a0 # a0 <- Addr[iTrapData], uscratch <- PROGRAMa0 # save all used registers except a0 sw t0, 0(a0) # save PROGRAMt0 sw s0, 4(a0) # save PROGRAMs0 # save a0 cssr t0, 0x040 # t0 <- PROGRAMa0 sw t0, 8(a0) # save PROGRAMa0 ...
Non-re-entrant handler: It is up to you how you manage the memory allocated for iTrapData
. If you allocate a specific address to save a given register --- for example, register s0
is always saved in Addr[iTrapData]+4
--- then your handler is not re-entrant. You cannot enable interruptions while you are handling an interruption because doing so could cause the fist value of s0
that you had saved to be overwritten.
Re-entrant handler: An elegant solution to create a re-entrant handler is to implement a stack in the memory area reserved for iTrapData
. The solution would have to handle an interrupt stack pointer. It would have to ensure that space is allocated in this stack for a new interruption frame before interruptions are re-enabled. Once space is reserved in the interrupt stack to save the registers that the handler will use, then interrupts can safely be re-enabled. In this case we have a re-entrant handler.
It would be difficult to create a set of tests to determine if a handler is re-entrant. Therefore, in this lab we do not require the implementation of a re-entrant handler. It is acceptable to keep interruptions disabled while an interruption is been processed.
Assignments too short to be adequately judged for code quality will be given a zero.
There is a single file to be submitted for this lab:
sird.s
should contain the code for both the interrupt handler
and the function sird
.
main
label to this file.include "common.s"
Code
folder of the git repository