Table of Contents |
---|
Introduction |
Design Specifications |
Simulation and Testing |
Building on lab 4
, we quickly implemented the single cycle version
. The main challenge was to get all of the base instruction set working properly, and the control unit
was arguably the most significant part of the single cycle
CPU, with most time being spent on debugging it.
Our design references the following schematic from the recommended textbook:
There weren't any specific modifications or modules added on to the schematic above. There were however several specific design choices that were made:
We noticed that while the loading of the instruction memory
was offset in the bash scripts while loading, the data memory
had to be offset correctly in SystemVerilog code.
Memory map | Explanation |
---|---|
![]() |
The memory map shows that the data memory goes from 0x01000 to 0x1FFFF , which means we need |
// Define the data array
// Each bit is 1 byte (8 bits) wide, with 2^17 bytes memory locations
logic [MEM_WIDTH-1:0] array [2**17-1:0];
initial begin
$display("Loading data into data memory...");
$readmemh("../rtl/data.hex", array, 17'h10000, 17'h1FFFF);
end
always_ff @* begin
// Needs to be addressed in multiples of 4
// 17 bits of addressing
RD = {
array[{A[16:2], 2'b11}],
array[{A[16:2], 2'b10}],
array[{A[16:2], 2'b01}],
array[{A[16:2], 2'b00}]
};
end
AddrMode
was added to efficiently implement byte addressing for lb
and sb
instructions, which would also prove to be really useful in running the reference program pdf.s
.
// Read and write operations
always_ff @(posedge clk) begin
if (WE && AddrMode == 3'b01x) begin // Write only least significant byte (8 bits)
array[A] <= WD[7:0];
end
else if (WE) begin // Write whole word
array[{A[16:2], 2'b00}] <= WD[7:0];
array[{A[16:2], 2'b01}] <= WD[15:8];
array[{A[16:2], 2'b10}] <= WD[23:16];
array[{A[16:2], 2'b11}] <= WD[31:24];
end
end
Extra note: There is a bug in data memory
in the tag v0.2.0
which was not discovered until a debugging session later on when working on pipelining
. This is because for byte addressing
, we wrote the
Emphasis must also be placed on the control unit
, which took the most time to debug for single cycle
.
For reusability and readability concerns, def.sv
was created to define all the OPCODE
, PCCODE
, etc.
Control Unit | Instruction List |
---|---|
![]() |
![]() |
While we swiftly implemented most of the basic instruction set, JALR
was particularly hard to implement. We had troubles with the ret
function not working as expected, and took some time to debug. The trouble can be seen in def.sv
, where JALR
gets its own PC CODE
:
`define PC_NEXT 3'b000
`define PC_ALWAYS_BRANCH 3'b001
`define PC_JALR 3'b010
`define PC_INV_COND_BRANCH 3'b100
`define PC_COND_BRANCH 3'b101
(A snippet of def.sv
)
For single cycle
, we wrote unit testbenches to ensure that all modules' behaviour are accurate and working, and to isolate errors to specific modules for easier debugging. This includes the:
Component | Testbench Link |
---|---|
ALU | alu_tb.cpp |
Control unit | control_unit_tb.cpp |
Data memory | data_mem_tb.cpp |
Instruction memory | instr_mem_tb.cpp |
MUX | mux_tb.cpp |
PC | program_counter_tb.cpp |
The speciality of these testbenches is that it uses industry standard GTests
,
as stated in the testing.md
. A snippet of one of the tests from
the control unit testbench
:
TEST_F(ControlunitTestbench, MemWriteTest)
{
// MemWrite = 1: all store instructions
// MemWrite = 0: else
top->instr = OPCODE_S;
top->eval();
EXPECT_EQ(top->MemWrite, 1) << "Opcode = OPCODE_S";
for (int opcode : {
OPCODE_I1, OPCODE_I2, OPCODE_I3, OPCODE_I4,
OPCODE_U1, OPCODE_U2, OPCODE_J, OPCODE_R, OPCODE_J
}) {
// Make sure MemWrite pulls DOWN instead of leave hanging
top->instr = OPCODE_S;
top->eval();
top->instr = opcode;
top->eval();
EXPECT_EQ(top->MemWrite, 0) << "Opcode: " << std::bitset<7>(opcode);
}
}
This allows us to check for the expected behaviour of each control / data path signal in each module. The testbench provides feedback in the terminal in the following format:
which definition can be found in the doit.sh
.
The bash scripts compile.sh
and
doit.sh
help compile and assemble
C
and asm
tests in the testbench, run the
tests, and creates disassembly texts
for debugging purposes.
Every team member participated in writing these testbenches to ensure everyone gains experience of DevOps in hardware / firmware development.
It is highly encouraged for the reader to take a look at
the testing.md
to understand how the entire testbench is used.