This project presents the design, verification, synthesis and optimization of a custom 8-bit single-cycle CPU -- fully implemented in Verilog. The CPU supports 8-opcode custom ISA (ALU, LIM, MOV, SIN, RIN, BJP, CLL, RET) with 16-condition branch evaluation unit. The processor has an 8-operation ALU and a dedicated link register for subroutine call/return operations. The functional verification was conducted using Icarus Verilog and GTKwave through various directed and random testbenches. Synthesis was carried out using generic, library-independent logic synthesis. In an early design, critical path is dictated by CLL operations, primarily due to Program Counter unit having a local logic depth of 35. The Program Counter was subsequently optimized, reducing its local logic level to 9 (a 75% reduction). Sequential Equivalence Checking (SEC) was performed for the optimized Program Counter unit using Yosys SAT solver. This optimization process reduced the overall CPU's critical path from 57 to 45 logic levels. The current critical path of the CPU is dictated by ALU operations with a 21% reduction.
| Opcode [15:12] | Instruction | Bit Layout [11:0] | Description |
|---|---|---|---|
0000 |
ALU | ALU_sel[11:9], destination[8:6], src_1[5:3], src_2[2:0] |
Performs ALU operation according to ALU_sel[11:9]. |
0001 |
LIM | destination[11:9], X[8], immediate_val[7:0] |
Load Immediate: Loads 8-bit value to pointed register. |
0010 |
MOV | destination[11:9], src[8:6], XXXXXX[5:0] |
Move: Copies data from source to destination. |
0011 |
SIN | src_reg[11:9], address_reg[8:6], XXXXXX[5:0] |
Store Indirect: Stores source data to RAM address pointed by destination register. |
0100 |
RIN | destination[11:9], address_reg[8:6], XXXXXX[5:0] |
Read Indirect: Loads data to destination from RAM address pointed by source register. |
0101 |
BJP | condition[11:8], address[7:0] |
Branch Jump: Jumps to address based on condition (see table below). |
1000 |
CLL | XXXX[11:8], address[7:0] |
Call (Jump + Link): Jumps to pointed address and saves next address in Register 7 (Q7). |
1001 |
RET | XXXXXXXXXXXX[11:0] |
Return: Returns from CALL using the address from Register 7 (Q7). |
Note:
Xrepresents ignored/don't care bits.
Note:
Qrepresents register.
ALU_sel |
Operation | Description |
|---|---|---|
000 |
ADD | Addition |
001 |
SUB | Subtraction |
010 |
SIR | Shift Right (Logical) |
011 |
SIL | Shift Left (Logical) |
100 |
AND | Bitwise AND |
101 |
XOR | Bitwise XOR |
110 |
OR | Bitwise OR |
111 |
ASR | Arithmetic Shift Right |
Condition |
Description |
|---|---|
0000 |
Jump if Equal (==) |
0001 |
Jump if Not Equal (!=) |
0010 |
Jump if Lesser Than (< signed) |
0011 |
Jump if Greater or Equal (>= signed) |
0100 |
Jump if Greater Than (> signed) |
0101 |
Jump if Lesser or Equal (<= signed) |
0110 |
Jump if Lesser Than (< unsigned) |
0111 |
Jump if Greater or Equal (>= unsigned) |
1000 |
Jump if Greater Than (> unsigned) |
1001 |
Jump if Lesser or Equal (<= unsigned) |
1010 |
Jump if overflow |
1011 |
Jump if no overflow |
1100 |
Jump if positive |
1101 |
Jump if negative |
1110 |
Always Jump (No condition) |
1111 |
Never Jump (No operation) |
Note: There is no independent unconditinonal jump (JMP) command since there is Always Jump (1110) in BJP Command
CPU's ALU provides 8 operation. This set is sufficient for basic computation. Moreover, more advanced algorithms can be constructed with the help of CLL and RET commands/instructions.
A 4 bit of branch select provides 16 conditions. Even though CPU has an 8-bit architecture, it is capable of making decisions through relatively complex scenarios.
ISA shows potential for the development of a more complex CPU architectures. CPU can make decisions, call functions and return, evaluate conditions and branch jump.
Register 7 (Q7) is reserved for CLL and RET instructions. Next instruction address before CALL (PC + 1) is loaded into Register 7.
For visualization, the datapath of CPU is shown in below schematic:
Results below are obtained via Icarus Verilog and GTKwave. In order to simulate the CPU in your local device, you can run these terminal commands.
Note: Since the CPU fetches instructions from the ROM, update ROM_opcode.txt according to your intended simulation. The machine code blocks for each simulation are ready to execute once the "//" comment indicators are removed.
# For branch jump simulation:
iverilog -o CPU_sim_BJP.vvp src/*.v tb/BJP.v
vvp CPU_sim_BJP.vvp
gtkwave BJP.vcd# For ALU, LIM, SIN and RIN simulation;
iverilog -o CPU_sim_ALSR.vvp src/*.v tb/ALU_LIM_SIN_RIN.v
vvp CPU_sim_ALSR.vvp
gtkwave ALU_LIM_SIN_RIN.vcd# For CLL and RET simulation:
iverilog -o CPU_sim_CLLRET.vvp src/*.v tb/CLL_RET.v
vvp CPU_sim_CLLRET.vvp
gtkwave CLL_RET.vcd| Clock Cycle | Assembly Code | RTL | Comment |
|---|---|---|---|
#0 |
LIM Q0 15 |
Q0 <- d15 |
Load 15 into Register 0 |
#1 |
LIM Q1 1 |
Q1 <- d1 |
Load 1 into Register 1 |
#2 |
ADD Q3, Q0 Q1 |
Q3 <- Q0 + Q1 |
Add Q0 and Q1, store result in Q3 |
#3 |
LIM Q6 8 |
Q6 <- d8 |
Load 8 into Register 6 |
#4 |
SIN Q3, Q6 |
MEM[Q6] <- Q3 |
Store value of Q3 into memory address at Q6 |
#5 |
RIN Q2, Q6 |
Q2 <- MEM[Q6] |
Read value from memory address at Q6 into Q2 |
Simulating testbench file tb/ALU_LIM_SIN_RIN.v:
This simulation was conducted in order to verify ALU, LIM, SIN and RIN instructions. As highlighted in Figure 2; CPU loaded immediate value (LIM Q0 15), performed ALU operations (ADD Q3, Q0 Q1), stored/read indirect by using register values as address pointers (SIN Q3, Q6 - RIN Q2, Q6).
| Clock Cycle | Assembly Code | RTL | Comment |
|---|---|---|---|
#0 |
LIM Q0 24 |
Q0 <- d24 |
Load 24 into Register 0 |
#1 |
LIM Q1 24 |
Q1 <- d24 |
Load 24 into Register 1 |
#2 |
SUB Q2, Q1 Q0 |
Q2 <- Q1 - Q0 |
Subtract Q0 from Q1 and load into Q2 |
#3 |
BEQ 8 |
If Equal PC <- 8 |
Evaluate equality and branch jump to 8th address |
#4 |
LIM Q0 8 |
Q0 <- 8 |
Trap instruction |
#5 |
LIM Q1 8 |
Q1 <- 8 |
Trap instruction |
#6 |
LIM Q2 8 |
Q2 <- 8 |
Trap instruction |
#7 |
LIM Q3 8 |
Q3 <- 8 |
Trap instruction |
#8 |
ADD Q2, Q0 Q1 |
Q2 <- Q0 + Q1 |
CPU successfully branch jumped and executed 8th instruction |
Simulating testbench file tb/BJP.v:
This test flow was focused on Branch Jump operation of the CPU. As highlighted in Figure 3, CPU evaluated the BEQ (Jump if equal) condition at clock cycle = #3 and successfully branched to PC = 8. There are trap instructions between clock cycle #4-#7 which the CPU should not execute. The CPU did not execute trap instructions and successfully carried on instruction #8 (ADD Q2, Q0 Q1). If the CPU failed to properly simulate the test flow, it could be detected by examining Registers 1-3.
| Clock Cycle | Assembly Code | RTL | Comment |
|---|---|---|---|
#0 |
LIM Q0 8 |
Q0 <- d8 |
Load 8 into Register 0 |
#1 |
LIM Q1 2 |
Q1 <- d2 |
Load 2 into Register 1 |
#2 |
CLL 4 |
Q7 <- 3, PC <- 4 |
Call subroutine at address 4 |
#3 |
LIM Q2 16 |
Q2 <- d16 |
(Skipped by CLL) |
#4 |
ADD Q5, Q0 Q1 |
Q5 <- Q0 + Q1 |
Subroutine execution: Add Q0, Q1 and write Q5 |
#5 |
RET |
PC <- Q7(3) |
CPU successfully returned from subroutine |
Simulating testbench file tb/CLL_RET.v
The ability of executing subroutine without any hardware complication of CPU was verified. As highlighted in Figure 4, the CPU called a subroutine in clock cycle #2 and jumped to 4th instruction address. It is crucial to show that return address (PC + 1) was stored in Register 7 (Q7) since it is dedicated to CLL/RET instructions. The CPU successfully executed the subroutine at 4th instruction address and returned to PC = 3 by reading Register 7 (Q7).
Verification of CALL and RETURN operations.
The CPU is structurally verified using below Yosys commands
# Reading all source verilog code in /src file
read_verilog *.v
# Declaring the top module
hierarchy -check -top CPU
# Removing hierarchal boundaries between top/sub modules
flatten
# Synthesize physical units from behavioral and memory code blocks
proc
opt -full
memory
memory_map
opt -full
# Synthesize physical units from logical and arithmetic code blocks
techmap
# Optimize and clean unused wires-pins before verification
opt -full
clean
# Verify the CPU by checking unused drivers, pins and wires
check
# Verify the CPU by checking strongly connected components (logic-combinational loops)
scc -expect 0The design is structurally verified as shown in below terminal lines. Yosys detected 0 structural problems and combinational loops. Since the CPU has a single-cycle architecture, a combinational loop would have devastating synthesis consequences. By flattening the design, all units / submodules and datapath are checked, thereby verifying the entire structure.
18. Executing CHECK pass (checking for obvious problems).
Checking module CPU...
Found and reported 0 problems.
19. Executing SCC pass (detecting logic loops).
Found 0 SCCs in module CPU.
Found and expected 0 SCCs.Since the PC module was redesigned, previous and current modules should be formally verified via submodule level equivalence checking. Below Yosys test flow was used:
# Read, synthesize and rename the new PC design as "gold"
read_verilog PC.v
hierarchy -check -top PC3
flatten
proc
design -stash gold
# Read, synthesize and rename the old PC design as "gate"
read_verilog PC_1.v
hierarchy -check -top PC_1
flatten
proc
design -stash gate
# Assign gold and gate modules
design -reset
design -copy-from gold -as gold PC3
design -copy-from gate -as gate PC_1
# Show active modules for debugging
ls
# Connect gate and gold module into equivalence miter
miter -equiv -make_assert -flatten gold gate the_miter
hierarchy -top the_miter
# "async2sync" is needed for SAT to work on D-FF's
async2sync
opt
clean
# Start the verification by clearing all D-FFs
# If "-set-init-zero" is not used, PC's D-FFs are set to random values before SAT solver, causing failure of verification!
sat -verify -prove-asserts -show-inputs -show-outputs -seq 10 -set-init-zero the_miterThe Yosys test flow's result obtained as below:
Thus, the redesigned PC unit is formally verified. Bounded Model Checking (BMC) approach was used due to the unit has D-FF's. A state space of 10 clock cycles is checked and it is proven that two modules are mathematically identical.
Longest Topological Path of all submodules is determined using below Yosys commands:
# Read the module that yosys is going to synthesis
read_verilog src/"ModuleFileName".v
# Check and declare the top module
hierarchy -check -top "ModuleName"
# Translate behavioral code blocks and abstract memory arrays into physical flip-flops
proc
memory
opt -full
# Flatten all modules included in top module, removing top/sub module boundaries.
flatten
opt -full
# Map abstract arithmetic/logical codes into physical cells
techmap
opt -full
# Synthesize the module by only using primitive gate cells. Transfer $MUX or $ADD cells into AND, OR, XOR...
abc -g gates
opt -full
# Remove unused pins, wires and cells
clean
# Show implementation statistics
stat
# Compute the Longest Topological Path
ltpLongest Topological Path (Critical Path) of all modules are shown below table:
| Module Name | Longest Topological Path (LTP) |
|---|---|
| ALU (Arith. Logic Unit) | 19 |
| CU (Control Unit) | 10 |
| PC (Program Counter) | 9 |
| RF (Register File) | 7 |
| SR (Status Register) | 1 |
| RAM (Data Memory) | 15 |
| ROM (Instruction Mem.) | 3 |
These LTP values show the gate-level depth (Logic Level) of all modules. With this information, the critical path of CPU can be determined without using an external library - solely depends on generic synthesis.
The critical path of CPU is decided by ALU instructions with LTP of 45.
Note: Because there is no external library given to Yosys, the ALU is synthesized using an 8-bit RCA (Ripple Carry Adder). An 8-bit RCA alone has 16 logic level. Thus, the ALU has the longest LTP and determines the critical path.
For visualization, the signal propagation through ALU operations is shown in below schematic:
The critical path of the CPU is worst case scenario that signal must propagate through one clock cycle. The signal must arrive the input of a Flip-Flop before next clock cycle begins. Thus, the maximum clock frequency which the CPU can operate is determined by the critical path. In this case the signal starts to propagate from Program Counter and its final destination is D-FFs of Register File. Since the signal also must propagate through Flip-Flops, the setup time of D-FF (
Note: The calculated result represents the technology-independent gate level depth -- plus setup time of the destination D-FF. Therefore, current timing analysis does not indicate any absolute timing value. In order to obtain quantitive critical timing value, an external cell library should be used.
| Primitive | Count | Description |
|---|---|---|
$_OR_ |
405 | 2-Input OR Gate |
$_AND_ |
346 | 2-Input AND Gate |
$_NOT_ |
101 | NOT Gate |
$_DFFE_PP0P_ |
68 | D-FF w/Enable & Posedge rst / RF + SR |
$_MUX_ |
54 | 2-to-1 Multiplexer |
$_XOR_ |
39 | 2-Input XOR Gate |
$_DFF_PP0_ |
8 | Standard D-FF / PC's D-FFs |
| Total Comb. Cell | 1021 | Combinational Cells |
| Total Seq. Cell | 76 | Sequential Cells |
| Total Cells | 1097 | Synthesized Logic Cells |
Despite the fact that implementation statistics contains primitive gates, there are $MUX cells in statistics counted as gate. This statistic is documented after technology mapping (techmap) -- before generic gate synhesis (ABC). It is expected that ABC will optimize the CPU and synthesize $MUX cells as primitive gates.
Note: It is crucial to note that Table 8 metrics contains data which not including RAM and ROM modules -- blackbox. Each one of the RAM and ROM units contains 256x8 D-FFs and extensive Store/Read selection logic. Including RAM and ROM into statistics would inflate the data.
| Metric | Value |
|---|---|
| Logic Levels (LEV) | 33 |
| AIG Nodes (AND) | 759 |
| Latches (LAT) | 0 |
These metrics are acquired using abc -script command in Yosys. Script file contains two lines:
strash
print_statsIt is confirmed that no latches were synthesized. Latches are asynchronous -- level sensitive storage elements. No latches should be inferred in this synchronous design. Unintended latch inference would indicate an issue in the RTL source code.. Furthermore, LEV metric indicates the longest combinational path the CPU contains.
Note: AND-Inverter Graph (AIG) metric should not be interpreted as absolute primitive gate statistic, since ABC does not count inverter logics as separate primitives.
Note:
LEVmetric is not useful in critical path computations. Although it indicates the longest combinational path in the CPU, the critical path is an instruction-dictated register-to-register path. Therefore, the longest logic level reported byLEVmetric should not be interpreted as critical timing statistic. Finally, it is safe to say thatLEVis only measure of structural complexity and input-to-output combinational logic depth.
| Primitive / Cell Type | Count | Hardware Description |
|---|---|---|
$_NAND_ |
340 | 2-Input NAND Gate |
$_AND_ |
271 | 2-Input AND Gate |
$_DFFE_PP0P_ |
68 | D-FF w/Enable & Posedge rst / RF + SR |
$_OR_ |
47 | 2-Input OR Gate |
$_ORNOT_ |
28 | 2-Input OR-NOT Gate |
$_ANDNOT_ |
17 | 2-Input AND-NOT Gate |
$_XNOR_ |
14 | 2-Input Equivalence Gate |
$_DFF_PP0_ |
8 | Standard D-FF |
$_NOR_ |
8 | 2-Input NOR Gate |
$_XOR_ |
7 | 2-Input XOR Gate |
$_NOT_ |
1 | NOT Gate |
| Total Comb. Cells | 741 | Primitive Gates |
| Total Seq. Cells | 76 | Sequential Cells |
| Total Cells | 817 | Synthesized Logic |
This results concludes the synthesis stage. The technology independent generic gate level count is shown in Table 10. Total of 741 primitive gates and 76 sequential cells were synthesized.
In the first iteration of design, the Program Counter unit had 35 LTP (Longest-Topological-Path). This is a result of below source code:
...
PC <= PC + 1;
...Since, there was no external library given to Yosys, a basic incrementation of 8-bit counter was synthesized with a ripple-carry adder (RCA) logic. The RCA logic is depends on serial implementation of FAs (Full-Adder). The Program Counter determines MSB (Most-Significant Bit) of next clock cycle after the result of other 7-bit is concluded. The LTP of 45 is a result of cascade implemented FAs. Therefore it is decided that a fast-incrementer should be included in Program Counter RTL source code. Instead of RCA logic, the carry-lookahead adder logic is used to build incrementer module. Since one of the addend is always 8'd1/8'b00000001, the complex RCA logic is simplified as shown below:
...
wire c0 = PrevPC[0];
wire c1 = &PrevPC[1:0];
wire c2 = &PrevPC[2:0];
wire c3 = &PrevPC[3:0];
wire c4 = &PrevPC[4:0];
wire c5 = &PrevPC[5:0];
wire c6 = &PrevPC[6:0];
assign PCload[0] = ~PrevPC[0];
assign PCload[1] = PrevPC[1] ^ c0;
assign PCload[2] = PrevPC[2] ^ c1;
assign PCload[3] = PrevPC[3] ^ c2;
assign PCload[4] = PrevPC[4] ^ c3;
assign PCload[5] = PrevPC[5] ^ c4;
assign PCload[6] = PrevPC[6] ^ c5;
assign PCload[7] = PrevPC[7] ^ c6;
...Optimized PC unit has LTP of 9 -- 75% reduction from LTP = 35. If PC module has 35 logic level delay, the critical path of the CPU would be dictated by CLL instruction with a LTP of 57. In CLL instructions, next PC value should be computed and written in RF's Register 7 (Q7) -- which is dedicated to CLL instructions. Therefore, it was crucial to optimize the Program Counter in order to shorten length of the critical path. Current critical path is 45 logic-depth -- a 21% reduction from LTP = 57.



