Practical 5 Single Cycle Processor
Objectives
This section is not a list of tasks for you to do. It is a list of skills you will have or things you will know after you complete the practical.
Following completion of this practical you should be able to:
- Instantiate and combine modules in verilog.
- Read and interpret simulated waveforms.
- Implement and test a limited instruction set single-cycle processor (R, I, and S types) in ModelSim.
- Use waveform diagrams and verilog test benches to debug a processor implementation.
- Discuss clocking strategies for a set of sequential logic that must happen in a specific order.
Guidelines
- Because you will be iteratively adding functionality to one processor module, we strongly recommend that you periodically add and commit your progress to git as a backup.
Time estimate This practical will take approximately 5-8 hours per student, depending on your familiarity with Verilog and the single-cycle RISC-V architecture covered in class.
Preliminary Tasks
Follow this sequence of instructions to complete the practical.
Obtain your RISC-V-single-cycle-processor
git repo
First, get your team name from your professor (it is probably on moodle or CATME). Once you have been assigned team names, you can create your repository via github classroom.
When you follow the link to get your repo you need to enter your team name exactly as your professor provided it.
Only the first person in your group to set up the repo needs to do this, later people can select the correct team from the list of teams.
Get your repo at this link. Don't click that link without reading the sentences above.
Create your own ModelSim Project (.mpf
) file.
Because ModelSim uses absolute pathing in its settings, each .mpf
project is intended to be used on one single machine. This means you cannot easily run ModelSim using another team member’s .mpf
file.
Instead, each team member should have their own unique .mpf
file (e.g., username.mpf
).
With unique project files, each team member can contribute towards the practical without annoying path issues.
You each project file will reference the same verilog source files (*.v
), so you will not maintain copies of the verilog files.
Have each team member do the following:
- Create their own ModelSim Project
.mpf
file, - Check the
.mpf
file contains the two linesmsgmode=both
anddisplaymsgmode=both
, - Add, commit, and push the
.mpf
file to the git repository. (Tip: do a pull before you add/commit/push to avoid merges)
Build a basic RISC-V Processor
Repository Overview
Before diving in, let’s take a quick overview of the files in your repository:
vunit.v
: the testing suite, this should be familiar from Practical 4,Processor.v
: the main processor module, this module will instantiate other Verilog Modules (mostly done in the provided skeleton code):Register.v
as the PC,DP_Memory.v
as the instruction and main memory,RegFile.v
to be imported from your Practical 4 as the register file,ImmGen.v
to be imported form your Practical 4 as the immediate generator,Control.v
as the control unit,ALUCtl.v
as the ALU control,ALU.v
as the ALU,- a few additional modules such as muxes and others as you see fit.
- A few testbenches:
tb_Control.v
: testbench to verify the validity of your control unit before integrating it into Processor.v,tb_Procoessor.v
: testbench to verify the validity of your processor running R-type instructions; this will read a text file to load as its instruction memory.
1 Implement R-type Instructions
You will begin by implementing a processor that only implements R-Type RISC-V instructions.
Trace R-Types on the datapath
On the first page of your practical worksheet, there's a datapath drawing.
- (Q) Using the RTL from class, trace the wires used for the
add
instruction. - Label each traced wire with a name. For example, consider the wire coming out of the right side of Instruction Memory; you could label this wire
inst
to be consistent with the RTL. - As you trace through blocks of logic (register file, muxes, etc) circle the names of any control bits necessary for the
add
instruction.
Implement Control
We've provided you with the start of a verilog control unit, Control.v
and a test bench tb_Control.v
. It doesn't currently do much, but has all the inputs and outputs you'll need.
Review your traced datapath and make note of what each of the 7 control signals should be for add
(and other R-type instructions).
For ALUOp, this is a 2-bit signal.
Review the comments documented in ALUCtl.v
to determine what ALUOp should be for R types.
- Open up your repo in VS Code (or your favorite text editor)
- Edit
Control.v
- In the
always @(opcode)
block of the control unit, look for the case block forR_OPCODE
. Between thebegin
andend
, add value assignments for all the control signals you will need to set to makeadd
happen.- You can see Figure 4.26 in your textbook (should be page 281) to verify which signals and which values matter for "R-format" instructions.
- HINT:
ALUOp
is a two-bit bus, unlike in the texbook table that treats each bit separately; do not set each bit separately, instead assign a two-bit binary value. - Again, reference the comments in
ALUCtl.v
to determine what the two-bitALUOp
should be. - Note: you should also include every "write" control signal and explicitly disable them (set to zero) for things you don't want to be written (for example,
memWrite
).
Test Control
Once you've coded control for R-types, you need to make sure it emits the right signals!
-
In VS Code, open the
tb_Control.v
file and review the test we provide for R-type instructions.- Note that it calls the
test_control
task we created with the expected control signals. This is similar to how you will create additional tests later. - While you do not need to edit that file yet, read and try to understand what it currently does.
- Note that it calls the
-
Open ModelSim, and your uniquely-named project file.
-
Add all the verilog files in the repo to your ModelSim project.
-
Compile all the verilog files, fixing any errors in
Control.v
andtb_Control.v
. Don't worry about errors in the other modules for now. -
Start simulation for the
tb_Control
module andrun -all
. The tests should pass; if they don't, check your work. -
You could add another test, but since all R-format instructions have the same opcode, there's no need.
-
When you get control working, do a git pull, add, commit, and push to save your work.
-
While this test may seem redundant since you just implemented the exact same thing in
Control.v
, it is still very important to do this check to catch errors:- Have one team member implement Control.v, and another team member implement
tb_Control.v
. This way you have redundancy built into this verification process. - As you implement the other instruction types and eventually other logic modules, the team member implementing these modules will be fatigued and be prone to mistakes.
- Having a testbench to check the validity of a module before integrating it into the larger
Processor.v
module will help prevent elusive bugs. Even if you do get a bug, you can run these smaller testbenches again to pinpoint where the bug is.
- Have one team member implement Control.v, and another team member implement
Implement the Datapath for R-types
In this part you will use your control unit and design a datapath around it to execute R-type instructions from memory.
- Examine the
Processor.v
file. We're providing you with a processor module that has a few component instances in it, but they're not hooked up. Compare this set of instances with the components you traced on the worksheet. - Add new instances of any missing components you will need to make your processor execute only R-types. Just make them, don't connect them to anything.
Here are some hints and suggestions:
- We are providing you with working implementations of
ALU.v
andALUCtl.v
. They should already be in your git repository. - We've also included copies of the
DP_Memory.v
, andRegister.v
from Practical 4. - There are lots of suggestions and tips in the comments in the files provided, make use of these.
- Instruction and data memory will be the same component. We're using both halves of the
DP_Memory
module. See the comments inProcessor.v
for details. - Don't make multiplexers. You can make them with raw verilog later.
- There's no register file in your git repo so you will have to make your own!
- SUGGESTION: Since you created and tested this in the previous practical, one member of your team should copy their implementation into your repo and use them for this practical.
- Don't forget to add this file to your ModelSim project after you've made it.
- We are providing you with working implementations of
- Create wires to connect the components. Create an instance of each wire you labeled on the practical worksheet's datapath; use the same name you wrote on the worksheet.
- Some of your wires will need to be 32 bits:
wire [31:0] myWideWire;
- Put the wire declarations inside the
Processor
module before you declare the other major components. This will ensure they're ready to use by any component that needs it.
- Some of your wires will need to be 32 bits:
- Attach the wires to your component's input or output pins
- This is as simple as writing the wire name between the parentheses next to the input or output where you want to attach it. In this example, the output of instance
A
is connected to the input of instanceB
:wire [4:0] myWire; Thingy A( .InputPin1(), .OutputPin(myWire) // <-- attach one end here ); Thingy B( .InputPin1(myWire), // <-- attach the other end here .OutputPin() );
- STRONG SUGGESTION: Because you are not implementing immediates, memory, or branch instructions yet, ignore the three muxes used by the ALU (from ImmGen) branches (From the adder) and data memory (output of memory) for now. Assume the wire goes straight through the mux. For example, you might have a wire
B
that goes directly from the register file into the second input on the ALU. You can add these muxes later.
- This is as simple as writing the wire name between the parentheses next to the input or output where you want to attach it. In this example, the output of instance
Tip: name and create wires when connecting components
This is so important, we're saying it twice.
For ease of wiring, we recommend defining wires as you named them when tracing your datapath diagram. For example, consider the instruction:
This could be a 32-bit wire inst
connected to Mem and Control:
wire [31:0] inst;
DP_Memory Mem (
// instruction memory ports
.addr_a(), // instruction address (TODO)
.we_a(0), // no writing to instructions, so we’ll ground this
.data_a(32’b0), // no writing to instructions, so we’ll ground this
.clk_a () , // clock for reading instructions (TODO)
.q_a(inst) , // q_a output is always an instruction
...
);
Control controlUnit (
.opcode( inst[6:0] ), // these are the opcode bits of the instruction
.ALUSrc() ,
...
) ;
A note about Memory and Addresses
When you hook up memory you will need to adjust the address.
RISC-V uses byte addressing, but the provided memory modules use word addressing. Because our words are 4 bytes long, every byte address is 4 times too big. For example, the second instruction in memory will be at byte address 0x0004
but word address 0x0001
.
We can simply shift the byte address right by 2 to divide by 4 and convert it to a word number.
Hint: you do not need a dedicated bit shifter, nor an assign statement to achieve this. You can strategically wire a subset of PC_output[xx:yy]
to addr_a
. If this is confusing, consult your instructor.
Additionally, the provided memory only has *10 bit (word) addresses (meaning 2^12 byte addresses), this is because of the limitations on the amount of memory on the FPGA we are simulating.
Feed in the least significant ten bits of your address (after left shifting) to account for this (we're essentially cutting off the top of memory when we do this). As you debug keep in mind that if you look at the values in memory the addresses will be 4 times smaller than the address you would "expect".
-
Connect the
reset
input onProcessor
to all components that have a reset (most likely the PC, and RegFile). When you "reset" the processor, you want to clear out all the registers and make sure the PC goes back to the beginning of your code. -
We recommend you connect components from left-to-right in your datapath diagram, checking each step to see if it worked before moving on:
- Make the PC increment every clock cycle.
To increment the PC by 4 every clock cycle, we can skip defining a separate adder module and simply define a new wire
newPC
and assign its value to bePC_output + 4
. Note thatPC_output
is not defined for you. - Connect PC and memory to read instructions
Use the waveform to see if the right instructions are coming out of memory. You can look at the
memory-R.txt
file to see the instructions in memory. - Connect the instruction bus (output of memory) to the register file and control unit. Use the waveform to see if the right registers get read.
- Connect the register file's outputs to the ALU
- Connect the ALU output back to the register file
- Connect control outputs to the register file and ALU
It is ok to start this list, then move to the next section ("Test with a few R-Types") to test your progress as you finish implementing the R-types.
- Make the PC increment every clock cycle.
To increment the PC by 4 every clock cycle, we can skip defining a separate adder module and simply define a new wire
-
Be sure your
Processor
compiles in ModelSim before moving on. Make sure you've added all the files used in your Processor or the tests to the ModelSim project.
Test with a few R-types
-
Examine
memory-R.txt
. This is the first set of instructions you will test on your processor. It has anadd
and asub
that have been assembled and put into memory in this order. -
In the
tb_Processor_R.v
file, you can see a start at a test bench for your processor. It loadsmemory-R.txt
into memory, then resets your processor, then allows the clock to cycle and checks the changes to the register file after each cycle.- Modify the test to properly inspect the contents of your register file. Follow the instructions in the code to tell the test bench how to look at your regsters' values.
-
In ModelSim, run the
tb_Processor_R
tests!-
If your implementation is not complete, it's ok, the tests will still do useful things.
Tip: How to test a partial implementation
To run a test with a partially-working implementation, compile in ModelSim and then simulate
tb_Processor
. (Make sure the compile succeeds!)Populate the waveform with values that are important to check. Remember, you can add values from individual module components onto the waveform via sim → tb Processor → UUT → component name.
This is what your waveform may look like after connecting the PC, Control, Memory and Register File.
Consider saving your waveform file (
tb_Processor_R_waves.do
) as you test. -
If you think your implementation is complete but the tests don't pass, consider building a more detailed waveform with control and all your labeled wires to inspect what it's doing.
-
If you build a waveform, be sure to save it! We recommend calling it something similar to the test bench where it is useful (
tb_Processor_R_waves.do
for example).
-
-
Once the instructions in the R-type tests are succeeding, assemble a few more R-type instructions and add them to the memory file, then add tests to the verilog test bench.
- HINT: you can use your assembler from practicals 1/2, or RARS to quickly assemble instructions.
-
Once you've got R-types working, save your progress in git!
-
Be sure to add, commit, and push only the verilog files, changed memory file, and any waveform
.do
file you edited or created. -
SUGGESTION: Have each member of your group create their own
Practical5.mpf
project file in Model Sim. Don't commit these to git. This will save you some time. -
(Q) Take a screenshot of your waveform and annotate it, labelling which portions of the waveform are running which tests. You may need to add an additional temporary wire to your testbench to read the
result
output from the ALU. Be sure to submit this annotated waveform for your Practical Worksheet.Below is a sample of an annotated waveform:
2 Implement I, Memory, and SB type instructions
From here, repeat the same steps for I, S, and SB types. Note that for I-types, skip jalr
; we will do that on the next practical. In addition, lw
(an I-type) should be implemented with sw
(the S-type).
For each of the remaining instructions for this practical (I-Types, memory instructions, and SB types):
- Review the RTL from class
- (Q) Trace this type's execution on the practical worksheet datapath and label newly used wires.
- Expand your
Control.v
andControl_tb.v
to test control for the type. - Expand
Processor.v
to implement the instruction type. - Add a task to
tb_Processor.v
to substantially test the new instruction type. For each type, there is a memory file in thetests/
folder that you can use for testing, but you'll need to add contents to those files - (Q) Take a screenshot of modelsim's waveform showing the test execution and annotate it, labeling which portions of the waveform are running which tests. Put this on the worksheet
- Commit your progress to git.
The following sections contain hints, requirements, and tips for each type.
I-TYPES (not lw
or sw
)
Datapath:
- You'll need to add an Immediate Generator (you made one in Practical 4).
- There's a mux controlling the second input of the ALU. Now you need to implement that mux! There are many ways to do this.
Tip: Ways to create a Mux
Here are three different ways to create a mux:
Method 1: conditional connection
First, you can "conditionally connect" a wire to the input. For example, this code connects wire A to the input when "ACONTROL" is 1, and otherwise it connects B:
wire [4:0] A;
wire [4:0] B;
Thingy UNIT(
.InputPin1( ACONTROL ? A : B ),
.OutputPin()
);
Method 2: input and output wires for the mux
Another way is to create a third wire from the output of the mux, lets call it "choice", then conditionally assign that wire:
wire [4:0] A;
wire [4:0] B;
wire [4:0] choice;
assign choice = ACONTROL ? A : B;
Thingy UNIT(
.InputPin1( choice ),
.OutputPin()
);
Method 3: using an always block and if statement
A final way is to use an always
block to recompute choice
when the inputs change. Note that choice
is a reg
type here:
wire [4:0] A;
wire [4:0] B;
reg [4:0] choice;
always @(A,B,ACONTROL) begin
if (ACONTROL === 1) choice
- Don't forget to update any components you hooked up for R-types that may need new connections (for example, the
Read 2
port on the regitster file).
Testbench:
-
Add at least five tests for I-types.
-
Make sure you test positive, negative, and zero values for these instructions.
-
Recall that you wrote an assembler in Practicals 1/2 you can use that to create the .txt file you need here. Note that you can use the
--mode
option in your assembler to create hexidecimal output, for example:python assembler.py my_I_type_tests.asm --mode hex
You can also edit the.vscode/settings.json
file to change the output mode and input file name if you want to run the script through VScode. -
Be sure the R-type tests still work with your updated
Processor.v
. Re-run your R-type task (tb_Processor_R
) after you get the I-type tests working.
MEMORY TYPES (lw
and sw
)
You may have noticed that now there is a critical series of more than two clocked things that must happen in sequence during one clock cycle:
- (a) Read instruction from memory
- (b) Load data from memory
- (c) Put in register file
The processor cannot put the value into the register file until it has been loaded from memory. And it cannot do any of this until it has read the instruction (a). The clock only has two edges, rising and falling edge, so we need a strategy to handle this!
If your register file has async reads, this makes it much easier; we can write the data into the register file while the next instruction is getting read from memory. Both (a) and (c) can happen on the rising edge, and the memory read (b) can happen between the other two.
-
(Q) On the practical worksheet, complete the timing diagram near the end of the worksheet. This asks you to idincate what happens for the
add
andlw
instructions that execute in sequence.Label the clock signal edges in the worksheet with the instruction steps above each clock edge where they should happen. Multiple steps from one instruction or steps from both instructions may need to happen simultaneously. The first step for each instruction is given for you.
-
(Q) Answer the ChatGPT timing question on the practical worksheet, on the page after the timing diagram question.
Datapath:
-
Instead of adding a new memory component for data memory, use the "B" half of the
DP_Memory
block that is already in your code.- The "B" half should operate on the falling edge of the clock so it happens after the instruction is loaded (at the rising edge).
Tip: sense the opposite clock edge
You can't (or don't want to) change the definition in a module to sense the opposite edge of the clock.
You can choose the edge when you connect a clock to a component: you can wire CLK or ∼CLK into the clock ports for posedge and negedge respectively.
If you don't want to change a module, you can fool it by inverting the clock ( use
~CLK
) when you connect it to the clock (e.g.,clk_b
) input. This will make the negative edge "look like" a positive edge to the module. -
You'll need to put a mux between the output of the ALU and the input of the register file. For this mux, DO NOT use the ternary operator (
q ? a : b
). You will want to grow the mux later, so it's best to do it with an always block:// this wire connects to the output of the mux and the write data port on the reg file. reg [31:0] aluOutputOrMemData; always @(A,B,MemtoReg) begin if (MemtoReg === 1) aluOutputOrMemData <= A; else aluOutputOrMemData <= B; end
-
It is OK to completely ignore the
MemRead
control signal. Our memory is always reading.
Testbench:
DP_Memory
is word-addressed, soram[50]
is equivalent to byte-address 200. You can access this with the instruction such aslw t0, 200(x0)
.
Tip: manually pre-loading data into memory
When you write tests for lw and sw, you may need to manually set values in memory. You can do so with the same approach as how the provided testbench manually sets register values when a task begins.
For instance, if I want to set the 50th word in memory to the value 1729, then I can write UUT.Mem.ram[50] = 1729;
in tb_Processor
.
Refer to Practical 4’s tb_Memory for a testbench that reads what gets written into memory:
// wait for clock to rise
@( posedge CLK );
#1; // wait a bit more for propagation
VU.SET_TEST_NAME("Single Port Memory WRITE Test");
// COOL TIP: we can reach into the sp_ram to read 'ram' values !!!
VU.ASSERT(sp_ram.ram[0] === sp_data) ;
VU.SET_TEST_NAME("Single Port Memory WRITE Test");
VU.ASSERT_INT_EQUAL(sp_q, sp_data);
If you want to pre-load memory before a test, you can reach in right before you let the clock cycle.
- Be careful not to overwrite instructions when you are manually setting memory values. Instructions start at 0, so if you set
UUT.Mem.ram[2]
you will be overwriting the third instruction you load in.
SB TYPES (beq
ONLY, FIRST)
Datapath:
-
For the mux, implement it like the one for memory. Use an always block to sometimes assign the input to the PC to be PC+4 and sometimes to be the computed branch target.
-
To implement
PC+offset
, you can use an assign statement as you did to calculatePC+4
. You do not need a dedicated adder module. -
Implement the control for the PCSrc mux. Recall that the branch signal from the control unit does not directly control this mux; instead it is branch
&
-ed with the zero signal from the ALU (already implemented for you).
Testbench:
-
Your test bench can inspect the value of the PC by "digging into" the unit you are testing, just like you did to read/set Memory values. Do this to see if branches worked.
Tip: "digging into" components from the tests
Assumming your test bench has an instance of
Processor
calledUUT
and the processor has an instance ofRegister
calledPC
, you can do this:Processor UUT(.CLK(CLK), .reset(reset)); // ... your tasks go here initial begin //... setup code here... // Look at the PC's output to see if it is correct VU.ASSERT_INT_EQUAL(UUT.PC.q, 32'h00004444);
You can inspect all your
wire
instances inside theProcessor
module in a similar fashion. -
Be wary of the clock timing for branching. Remember you cannot update PC the exact same moment you want to read an instruction.
-
Because of this timing, you are likely to find that your tests don't pass initially. Take a careful look at when and how PC changes. Investigate your waveform and look at the timing diagram you made for
lw
/sw
on the worksheet.You will likely need to change the timing for branches to work. Make sure after you make any changes that the old tests you wrote still work (don't assume they do, actually run them). If you want another hint, expand this after trying to work through it yourself for a bit:
Hint
You need to move only one component to work on the negative clock edge.
SB TYPES (All Remaining)
Once you’ve ensured your processor supports beq, you can now expand out to
the remaining branch variations (bne
, bge
, blt
). Here, instead of and-ing the control unit's branch with ALU’s zero, you need the full branch detection unit.
The decision table for the branch detection unit from lecture is replicated below:
Instruction | zero | result[31] | Branch? |
---|---|---|---|
beq | 0 | x | no |
1 | x | yes | |
bne | 0 | x | yes |
1 | x | no | |
bge | 0 | 0 | yes |
0 | 1 | no | |
1 | x | yes | |
blt | 0 | 0 | no |
0 | 1 | yes | |
1 | x | no |
Datapath:
When deciding where to put this Branch Control unit, you can either:
- Implement this as an always block directly in Processor.v directly next to the PCSrc mux for quick referencing.
- Implement a separate Branch_Detect.v module that handles all this logic in a separate file. This allows for better modularity and separation of code:
module Branch_Detect ( input wire branch, // will come from control unit input wire [2:0] func3, // will be from instruction [ 14:12 ] input wire zero, // will be from ALU module input wire neg, // will be from ALU_result [ 31 ] output reg BRANCH ! // control the mux, definitely rename this ); always @( branch , func3 , zero , neg ) begin // logic goes in here as conditional statements end endmodule
- HINT: use a case statement to make a decision differently based on the type of comparison you want to do.
- IMPORTANT: Be sure to have a
default
case in the case statement in case an unexpected funct3 value shows up.
- IMPORTANT: Be sure to have a
Testbench:
-
You may implement an optional
tb_branch.v
to fully verify your branch detection logic unit. This will allow you to be absolutely certain your branch detection is flawless before integrating it into your processor. -
In your main
tb_Processor
, test branches by running ASSERTs to check your PC’s value (UUT.PC.q()
output port) with the expected branch target PC value.- Be wary of the clock timing: you want to ASSERT after PC is updated at posedge / negedge, not at the moment of branch determination.
Working Ahead
You can read ahead to Practical 6 and start planning how to support U types, UJ types, and plan your own instruction.
Submission and Grading
Functional Requirements
At the end of the practical you should have done these things:
- Implement the
Control
module and comprehensively test it withtb_Control
, - Integrate the
ImmGen
andRegFile
modules from Practical 4 - Add to
Processor.v
to support:- R-type / Standard I-type / lw and sw / SB-type
- Create individual
memory-X.txt
files that contain the instructions for each testbench task in tb_Processor:- memory-R / memory-I / memory-lw / memory-sw / memory-SB
- Implement testbench tasks in
tb_Processor
using thememory-X.txt
files:- test_R_types / test_I_types / test_lw / test_sw / test_SB_types
- Completed and submitted the Practical Worksheet
Git Requirements
Remember, Do not add and commit every single file ModelSim creates. Only add, commit, and push .v, .do, and .mpf files.
In addition to the list below, you should regularly commit and push whenever you fix a bug, work to a stopping point, or make any incremental updates. At minimum, you must have at least 5 commits in your repo for this practical:
- Git commit 1: upon completion and tested R types.
- Git commit 2: upon completion and tested I types.
- Git commit 3: upon completion and tested lw.
- Git commit 4: upon completion and tested sw.
- Git commit 5: upon completion and tested SB types.
Commit and push via git either using VSCode’s built-in source control or with the git bash terminal. Be sure to copy your final commit ID number for the final question on the worksheet. This ID number can be found on the commit history tab on your Github repository.
Worksheet Requirement
All the practicals for CSSE232 have these general requirements:
General Requirements for all Practicals
- The solution fits the need
- Aspects of performance are discussed
- The solution is tested for correctness
- The submission shows iteration and documentation
Some practicals will hit some of these requirements more than others. But you should always be thinking about them.
(Q) Complete the practical worksheet. Specifically, complete the question on page 9 about how you tested your work and the last few questions that ask you to reflect on the efficiency of this design and your implementation experiences.
Final Checklist
- Verify that your code compiles and your tests pass (or at least run).
- Verify your verilog code is committed and the commits are pushed to github.
- Submit your completed worksheet to gradescope.
Grading Breakdown
Practical 5 Rubric items | Possible Points | Weight |
---|---|---|
Worksheet | 76 | 50% |
Code | 80 | 50% |
Total out of | 100% |