Single-cycle CPU (Verilog)
Single-cycle CPU (Verilog)
৬-ইন্সট্রাকশনের একটা মিনি-ISA (ADD/SUB/LOAD/STORE/BEQ/JMP) সম্পূর্ণ Verilog-এ — datapath, register file, ALU, instruction/data memory, PC, আর একটা pure combinational control unit — তারপর হাতে-অ্যাসেম্বল করা একটা লুপ-প্রোগ্রাম চালিয়ে testbench দিয়ে যাচাই। এই মডিউলের capstone।
কেন এই প্রজেক্ট
এই পুরো মডিউল জুড়ে আমরা নিচ থেকে উপরে উঠেছি — transistor থেকে gate, gate থেকে adder, adder থেকে ALU, latch থেকে register, state machine থেকে control logic। এই প্রজেক্টে সবগুলো টুকরো একসাথে জোড়া লাগবে একটা জিনিসে — একটা CPU, যেটা সত্যিকারের ইন্সট্রাকশন চালাতে পারে।
গুরুত্বপূর্ণ একটা কথা প্রথমেই স্পষ্ট করে নেওয়া দরকার: এটা একটা শিক্ষামূলক, ইচ্ছাকৃতভাবে ক্ষুদ্র CPU। এখানে pipelining নেই, hazard নেই, cache নেই, বাস্তব কোনো ISA (RISC-V, x86) নেই। প্রতিটা ইন্সট্রাকশন একটাই clock cycle-এ সম্পূর্ণ fetch-decode-execute-writeback হয়ে যায় — এটাই “single-cycle” নামের অর্থ। এই সরলতাটাই আসল শিক্ষার সুযোগ তৈরি করে — datapath-এর প্রতিটা তার কেন সেখানে আছে সেটা সম্পূর্ণভাবে দেখা সম্ভব, যেটা একটা বাস্তব pipelined CPU-তে সম্ভব না। পুরো CPU-এর গভীরতা — pipeline, hazard, branch prediction, cache — Level 3-এর কাজ। এখানে লক্ষ্য শুধু এইটুকু প্রমাণ করা: কয়েকশ গেট, একটা রেজিস্টার ফাইল, আর একটা ছোট decoder দিয়ে সত্যিই “প্রোগ্রাম চালানো” সম্ভব।
Mini-ISA স্পেসিফিকেশন
ইন্সট্রাকশন সেট
৬টা ইন্সট্রাকশন, ৩-বিট opcode:
| opcode | নাম | কাজ | ফরম্যাট |
|---|---|---|---|
000 | ADD | rd = rs1 + rs2 | R-type |
001 | SUB | rd = rs1 − rs2 | R-type |
010 | LOAD | rt = MEM[rs + imm7] | I-type |
011 | STORE | MEM[rs + imm7] = rt | I-type |
100 | BEQ | if (rt == rs) PC = PC + 1 + imm7 | I-type |
101 | JMP | PC = imm13 (absolute) | J-type |
110, 111 | — | সংরক্ষিত — NOP আচরণ (PC কেবল বাড়ে) | — |
রেজিস্টার
৮টা general-purpose রেজিস্টার (R0–R7), প্রতিটা ১৬-বিট, ৩-বিট ঠিকানা। R0 হার্ডওয়্যার্ড শূন্য — একটা সাধারণ RISC-style কনভেনশন যা এই ছোট ISA-তে দুইটা বড় সুবিধা দেয়: (১) ADD R3, R0, R0 দিয়ে যেকোনো রেজিস্টারে শূন্য বসানো যায়, (২) BEQ R2, R0, ... দিয়ে সরাসরি “রেজিস্টার শূন্য কি না” পরীক্ষা করা যায় — কোনো immediate-compare ইন্সট্রাকশন ছাড়াই।
ইন্সট্রাকশন এনকোডিং — ১৬-বিট শব্দ
R-type (ADD, SUB):
বিট: 15 14 13 | 12 11 10 | 9 8 7 | 6 5 4 | 3 2 1 0
ফিল্ড: opcode | rd | rs1 | rs2 | (অব্যবহৃত)
I-type (LOAD, STORE, BEQ):
বিট: 15 14 13 | 12 11 10 | 9 8 7 | 6 5 4 3 2 1 0
ফিল্ড: opcode | rt | rs | imm7 (signed)
J-type (JMP):
বিট: 15 14 13 | 12 11 10 9 8 7 6 5 4 3 2 1 0
ফিল্ড: opcode | imm13 (absolute target)
একটা ইচ্ছাকৃত ডিজাইন-সিদ্ধান্ত লক্ষ্য করুন — R-type-এর rd আর I-type-এর rt একই বিট পজিশনে ([12:10]) বসানো হয়েছে। বাস্তব MIPS-এর মতো ISA-তে এই দুইটা আলাদা পজিশনে থাকে বলে destination-select-এর জন্য একটা আলাদা RegDst মাক্স লাগে। এখানে ফিল্ড দুইটা align করে দেওয়ায় সেই মাক্সটা পুরোপুরি এড়ানো গেছে — write-address সবসময় instr[12:10], কোনো শর্ত ছাড়াই।
কেন control unit-এ FSM লাগে না
Datapath and Control Unit লেসনে multi-cycle control unit একটা finite state machine হিসেবে বানানো হয় — কারণ multi-cycle ডিজাইনে একটা ইন্সট্রাকশন সম্পূর্ণ হতে একাধিক clock cycle লাগে (fetch এক cycle-এ, decode আরেক cycle-এ, execute আরেক cycle-এ…), আর control unit-কে মনে রাখতে হয় “আমি এখন কোন ধাপে আছি” — সেই “কোন ধাপ” তথ্যটাই একটা state।
Single-cycle ডিজাইনে এই সমস্যাটাই নেই। একটা গোটা ইন্সট্রাকশনের fetch, decode, register-read, ALU-execute, memory-access, আর writeback — সবকিছু একটাই clock cycle-এর ভেতর, বিশুদ্ধ কম্বিনেশনাল লজিক দিয়ে, একটার পর একটা wire দিয়ে ঘটে যায়। “আমি কোন ধাপে আছি” প্রশ্নটার কোনো অর্থই নেই, কারণ ধাপ বলে কিছু নেই — পুরো ইন্সট্রাকশনটাই একটা মাত্র ধাপ। তাই control unit এখানে নিছক একটা combinational decoder — opcode-এর ৩ বিট ইনপুট নেয়, আর control সিগন্যালগুলো pure Boolean function হিসেবে আউটপুট দেয়, কোনো ক্লক নেই, কোনো “মেমরি” নেই। এই পার্থক্যটাই — state-থাকা FSM decoder বনাম state-বিহীন combinational decoder — multi-cycle আর single-cycle ডিজাইনের মধ্যেকার সবচেয়ে গুরুত্বপূর্ণ স্থাপত্যগত ফারাক।
Control সিগন্যাল টেবিল
| opcode | RegWrite | MemRead | MemWrite | ALUSrc (imm) | ALUOp | MemToReg | Branch | Jump |
|---|---|---|---|---|---|---|---|---|
| ADD | 1 | 0 | 0 | 0 (রেজিস্টার) | ADD | 0 (ALU) | 0 | 0 |
| SUB | 1 | 0 | 0 | 0 (রেজিস্টার) | SUB | 0 (ALU) | 0 | 0 |
| LOAD | 1 | 1 | 0 | 1 (imm) | ADD | 1 (মেমরি) | 0 | 0 |
| STORE | 0 | 0 | 1 | 1 (imm) | ADD | — | 0 | 0 |
| BEQ | 0 | 0 | 0 | 0 (রেজিস্টার) | SUB | — | 1 | 0 |
| JMP | 0 | 0 | 0 | — | — | — | 0 | 1 |
BEQ-এর সমতা-পরীক্ষা ALU-এর SUB অপারেশন আর তার zero ফ্ল্যাগ পুনর্ব্যবহার করে করা হয় — rs − rt == 0 মানেই rs == rt। এটা 8-bit ALU প্রজেক্টের zero ফ্ল্যাগের ঠিক একই ধারণা, শুধু এখানে ১৬-বিট প্রশস্ত।
ধাপে ধাপে
১. Register file
module regfile (
input clk,
input we,
input [2:0] ra1, ra2, wa,
input [15:0] wd,
output [15:0] rd1, rd2
);
reg [15:0] regs[0:7];
integer i;
assign rd1 = (ra1 == 3'b000) ? 16'b0 : regs[ra1];
assign rd2 = (ra2 == 3'b000) ? 16'b0 : regs[ra2];
always @(posedge clk) begin
if (we && wa != 3'b000)
regs[wa] <= wd;
end
initial begin
for (i = 0; i \< 8; i = i + 1)
regs[i] = 16'b0;
end
endmodule
R0-কে দুইভাবে সুরক্ষিত করা হয়েছে — read port-এ সবসময় 0 ফেরত দেয় (assign rd1 = (ra1==0) ? 0 : ...), আর write port-এ wa != 0 শর্ত দিয়ে লেখা ব্লক করা হয়েছে। দুইটার যেকোনো একটা যথেষ্ট, কিন্তু দুইটা একসাথে রাখাটা defensive ডিজাইনের ভালো অভ্যাস।
২. ALU
এই CPU-এর মিনি-ISA-তে শুধু ADD আর SUB লাগে (LOAD/STORE-এর address গণনা আর BEQ-এর তুলনাও ADD/SUB দিয়েই হয়) — তাই ALU এখানে 8-bit ALU প্রজেক্টের চেয়ে অনেক ছোট:
module cpu_alu (
input [15:0] a, b,
input alu_sub, // 0 = ADD, 1 = SUB
output [15:0] result,
output zero
);
assign result = alu_sub ? (a - b) : (a + b);
assign zero = (result == 16'b0);
endmodule
চ্যালেঞ্জ হিসেবে — 8-bit ALU প্রজেক্টের পূর্ণ ALU (AND/OR/XOR/shift সহ, ১৬-বিটে প্রসারিত) এখানে বসিয়ে নতুন opcode (AND, OR ইত্যাদি) যোগ করা যায়, alu_sub-এর জায়গায় একটা পূর্ণ ৩-বিট alu_op ব্যবহার করে।
৩. Combinational control unit
module control_unit (
input [2:0] opcode,
output reg reg_write,
output reg mem_read,
output reg mem_write,
output reg alu_src_imm, // 1 = ALU-এর দ্বিতীয় ইনপুট immediate, 0 = রেজিস্টার
output reg alu_sub, // 1 = subtract, 0 = add
output reg mem_to_reg, // 1 = writeback ডেটা মেমরি থেকে, 0 = ALU থেকে
output reg branch,
output reg jump
);
localparam ADD = 3'b000;
localparam SUB = 3'b001;
localparam LOAD = 3'b010;
localparam STORE = 3'b011;
localparam BEQ = 3'b100;
localparam JMP = 3'b101;
always @(*) begin
// ডিফল্ট — সব সিগন্যাল 0 (NOP-এর মতো), শুধু প্রযোজ্য opcode-এ বদলায়
reg_write = 1'b0;
mem_read = 1'b0;
mem_write = 1'b0;
alu_src_imm = 1'b0;
alu_sub = 1'b0;
mem_to_reg = 1'b0;
branch = 1'b0;
jump = 1'b0;
case (opcode)
ADD: reg_write = 1'b1;
SUB: begin reg_write = 1'b1; alu_sub = 1'b1; end
LOAD: begin reg_write = 1'b1; mem_read = 1'b1; alu_src_imm = 1'b1; mem_to_reg = 1'b1; end
STORE: begin mem_write = 1'b1; alu_src_imm = 1'b1; end
BEQ: begin alu_sub = 1'b1; branch = 1'b1; end
JMP: jump = 1'b1;
default: ; // 110, 111 — সংরক্ষিত, NOP আচরণ
endcase
end
endmodule
এই মডিউলে কোনো clock ইনপুটই নেই — খেয়াল করুন। always @(*) ব্লক এখানে বিশুদ্ধ কম্বিনেশনাল, ঠিক আগের অংশে যে যুক্তি দেওয়া হয়েছিল তার সরাসরি প্রমাণ।
৪. Instruction memory ও data memory
module imem (
input [7:0] addr, // PC-এর নিচের ৮ বিট — ২৫৬-শব্দের প্রোগ্রাম মেমরি
output [15:0] instr
);
reg [15:0] mem[0:255];
assign instr = mem[addr];
initial $readmemh("program.hex", mem);
endmodule
module dmem (
input clk,
input we, re,
input [7:0] addr,
input [15:0] wdata,
output [15:0] rdata
);
reg [15:0] mem[0:255];
integer i;
assign rdata = re ? mem[addr] : 16'b0;
always @(posedge clk) begin
if (we) mem[addr] <= wdata;
end
initial begin
for (i = 0; i \< 256; i = i + 1)
mem[i] = 16'b0;
$readmemh("data.hex", mem); // শূন্য করার পর কনস্ট্যান্ট ওভারলে করা
end
endmodule
Instruction memory আর data memory ইচ্ছাকৃতভাবে আলাদা মডিউল, আলাদা array (Harvard-style) — একটা single-cycle ডিজাইনে একই cycle-এ ইন্সট্রাকশন fetch এবং data access দুইটাই করতে হতে পারে (যেমন LOAD ইন্সট্রাকশনে); একটামাত্র শেয়ার্ড মেমরি হলে দুইটা read/write port-এর মধ্যে arbitration লজিক লাগত, যা এই শিক্ষামূলক ডিজাইনের সরলতা নষ্ট করত। এই মডিউল দুইটা মাত্র ২৫৬-শব্দ, তাই শুধু addr-এর নিচের ৮ বিট ব্যবহার করা হয়েছে — আমাদের টেস্ট প্রোগ্রাম কখনো ২৫৫-এর বেশি ঠিকানায় যাবে না।
৫. Top-level cpu মডিউল
module cpu (
input clk,
input rst
);
reg [12:0] pc; // JMP-এর পূর্ণ ১৩-বিট imm ধরার জন্য PC ১৩ বিট রাখা হলো
wire [15:0] instr;
wire [2:0] opcode = instr[15:13];
wire [2:0] fld_rd_rt = instr[12:10];
wire [2:0] fld_rs = instr[9:7];
wire [2:0] fld_rs2 = instr[6:4];
wire [6:0] imm7 = instr[6:0];
wire [12:0] imm13 = instr[12:0];
wire [15:0] imm7_ext = {{9{imm7[6]}}, imm7}; // 7 -> 16 বিট sign-extend
// ---- control ----
wire reg_write, mem_read, mem_write, alu_src_imm, alu_sub, mem_to_reg, branch, jump;
control_unit cu (
.opcode(opcode), .reg_write(reg_write), .mem_read(mem_read),
.mem_write(mem_write), .alu_src_imm(alu_src_imm), .alu_sub(alu_sub),
.mem_to_reg(mem_to_reg), .branch(branch), .jump(jump)
);
// ---- register file ----
// R-type-এ দ্বিতীয় read port rs2 (৬:৪) পড়ে; বাকি সব opcode-এ rt (১২:১০) পড়ে
// (STORE-এ "যা মেমরিতে লিখব", BEQ-এ "যার সাথে তুলনা করব")
wire [2:0] read_b_addr = (opcode == 3'b000 || opcode == 3'b001) ? fld_rs2 : fld_rd_rt;
wire [15:0] reg_a, reg_b, write_data;
regfile rf (
.clk(clk), .we(reg_write),
.ra1(fld_rs), .ra2(read_b_addr), .wa(fld_rd_rt),
.wd(write_data), .rd1(reg_a), .rd2(reg_b)
);
// ---- ALU ----
wire [15:0] alu_b = alu_src_imm ? imm7_ext : reg_b;
wire [15:0] alu_result;
wire alu_zero;
cpu_alu alu (
.a(reg_a), .b(alu_b), .alu_sub(alu_sub),
.result(alu_result), .zero(alu_zero)
);
// ---- data memory ----
wire [15:0] dmem_rdata;
dmem dm (
.clk(clk), .we(mem_write), .re(mem_read),
.addr(alu_result[7:0]), .wdata(reg_b), .rdata(dmem_rdata)
);
assign write_data = mem_to_reg ? dmem_rdata : alu_result;
// ---- instruction memory ----
imem im (.addr(pc[7:0]), .instr(instr));
// ---- পরবর্তী PC — তিনটা সম্ভাব্য পথ ----
wire [12:0] pc_plus1 = pc + 13'd1;
wire [12:0] branch_target = pc_plus1 + {{6{imm7[6]}}, imm7}; // 7 -> 13 বিট sign-extend
wire take_branch = branch && alu_zero;
wire [12:0] next_pc = jump ? imm13 : (take_branch ? branch_target : pc_plus1);
always @(posedge clk or posedge rst) begin
if (rst) pc <= 13'b0;
else pc <= next_pc;
end
endmodule
পুরো ডিজাইনে state-ধারণকারী উপাদান মাত্র তিনটা: pc রেজিস্টার, regfile-এর ভেতরের regs[] অ্যারে, আর dmem-এর ভেতরের mem[] অ্যারে — তিনটাই clock edge-এ আপডেট হয়। বাকি সবকিছু — control unit, ALU, register-select মাক্স, next-PC মাক্স — বিশুদ্ধ কম্বিনেশনাল ওয়্যার।
টেস্ট প্রোগ্রাম — লুপ দিয়ে যোগফল
লক্ষ্য: 5 + 4 + 3 + 2 + 1 = 15 গণনা করে data memory-র ঠিকানা 10-এ লিখে রাখা, একটা loop-এ, ছয়টা ইন্সট্রাকশনই ব্যবহার করে।
assembly ঠিকানা উদ্দেশ্য
──────────────────────────────────────────────────────────
LOAD R1, 0(R0) 0 R1 = mem[0] = 1 (কনস্ট্যান্ট ONE)
LOAD R2, 1(R0) 1 R2 = mem[1] = 5 (কাউন্টার i = 5)
ADD R3, R0, R0 2 R3 = 0 (sum = 0)
BEQ R2, R0, 3 3 if (i == 0) goto 7 (LOOP শুরু)
ADD R3, R3, R2 4 sum += i
SUB R2, R2, R1 5 i -= 1
JMP 3 6 goto LOOP
STORE R3, 10(R0) 7 mem[10] = sum (DONE)
JMP 8 8 অসীম self-jump (halt)
R0 সবসময় শূন্য বলে LOAD R1, 0(R0) আসলে “মেমরির absolute address 0 থেকে লোড করো” হয়ে যাচ্ছে — এই ISA-তে কোনো “load-immediate” ইন্সট্রাকশন নেই বলে কনস্ট্যান্ট আনার এটাই একমাত্র উপায়: data memory-তে আগে থেকে বসিয়ে রেখে, R0-এর অফসেট দিয়ে লোড করা।
BEQ R2, R0, 3-এ imm7=3 কীভাবে এলো: BEQ-এর টার্গেট হলো PC+1+imm7। এই ইন্সট্রাকশনটা ঠিকানা 3-এ আছে, তাই PC+1 = 4; টার্গেট চাই ঠিকানা 7 (DONE) — তাই imm7 = 7 − 4 = 3।
হাতে-এনকোড করা মেশিন কোড
| ঠিকানা | ইন্সট্রাকশন | বিট (op|রেজিস্টার/imm) | hex |
|---|---|---|---|
| 0 | LOAD R1,0(R0) | 010 001 000 0000000 | 4400 |
| 1 | LOAD R2,1(R0) | 010 010 000 0000001 | 4801 |
| 2 | ADD R3,R0,R0 | 000 011 000 000 0000 | 0C00 |
| 3 | BEQ R2,R0,3 | 100 010 000 0000011 | 8803 |
| 4 | ADD R3,R3,R2 | 000 011 011 010 0000 | 0DA0 |
| 5 | SUB R2,R2,R1 | 001 010 010 001 0000 | 2910 |
| 6 | JMP 3 | 101 0000000000011 | A003 |
| 7 | STORE R3,10(R0) | 011 011 000 0001010 | 6C0A |
| 8 | JMP 8 | 101 0000000001000 | A008 |
program.hex:
4400
4801
0C00
8803
0DA0
2910
A003
6C0A
A008
data.hex (মেমরি ঠিকানা 0-এ 1, 1-এ 5 বসানো — বাকি সব initial ব্লকে আগেই শূন্য করা হয়েছে):
@0
0001
0005
Testbench দিয়ে যাচাই
`timescale 1ns/1ps
module cpu_tb;
reg clk = 0;
reg rst = 1;
cpu dut (.clk(clk), .rst(rst));
always #5 clk = ~clk; // ১০ns পিরিয়ড ক্লক
initial begin
#12 rst = 0; // অন্তত একটা posedge রিসেট অবস্থায় দেখুক, তারপর ছেড়ে দিই
#300; // ~২৬ সাইকেল দরকার (৫ বার লুপ); মার্জিন সহ ৩০ সাইকেল দিলাম
if (dut.dm.mem[10] == 16'd15)
$display("PASS — mem[10] = %0d (প্রত্যাশা মতোই 15)", dut.dm.mem[10]);
else
$display("FAIL — mem[10] = %0d (প্রত্যাশা ছিল 15)", dut.dm.mem[10]);
$finish;
end
endmodule
dut.dm.mem[10] — এখানে testbench সরাসরি CPU-এর ভেতরের data-memory instance (dm)-এর internal অ্যারেতে hierarchical path দিয়ে উঁকি দিচ্ছে, বাইরের কোনো output port ছাড়াই। এটা Verilog testbench-এ খুবই স্বাভাবিক একটা পদ্ধতি — ভেতরের অবস্থা সরাসরি পরীক্ষা করা, শুধু ঘোষিত ইন্টারফেসের উপর নির্ভর না করে।
চালানো (Icarus Verilog):
$ iverilog -o cpu_sim cpu.v regfile.v cpu_alu.v control_unit.v imem.v dmem.v cpu_tb.v
$ vvp cpu_sim
PASS — mem[10] = 15 (প্রত্যাশা মতোই 15)
(EDA Playground ব্যবহার করলে সবগুলো .v ফাইল আর program.hex/data.hex আপলোড করে Icarus Verilog simulator বেছে নিলেই চলবে।)
#12 rst=0 লাইনটা লক্ষ্য করুন — clock প্রথম posedge-এ পৌঁছায় t=5-এ, তখনো rst=1 (যেহেতু t=12-এ deassert হচ্ছে), তাই সেই প্রথম edge-এই pc নিশ্চিতভাবে শূন্যে রিসেট হয়। #300 অপেক্ষা ২৬-এর বেশি cycle কভার করে — লুপ পাঁচবার ঘোরে (i = 5,4,3,2,1), প্রতিবার ৪টা cycle (BEQ→ADD→SUB→JMP) লাগে, প্লাস সেটআপের ৩ cycle আর শেষে BEQ-taken + STORE-এর ২ cycle — মোট প্রায় ২৫।
নিজেকে চ্যালেঞ্জ করুন
- পূর্ণ ALU — 8-bit ALU প্রজেক্টের AND/OR/XOR/shift যোগ করুন, নতুন opcode আর একটা প্রশস্ত
alu_opদিয়ে - JAL (jump-and-link) — রিটার্ন-ঠিকানা একটা রেজিস্টারে সংরক্ষণ করে ফাংশন-কলের মতো আচরণ যোগ করুন
- BNE (branch-if-not-equal) — control unit-এ নতুন একটা opcode আর
alu_zero-কে invert করার লজিক - Instruction trace — testbench-এ প্রতি cycle-এ PC, opcode, আর write-back মান প্রিন্ট করে একটা সম্পূর্ণ execution trace বানান
- Multi-cycle-এ রূপান্তর — এই একই ISA-কে ইচ্ছাকৃতভাবে multi-cycle-এ পুনর্ডিজাইন করুন (প্রতি ইন্সট্রাকশনে fetch/decode/execute আলাদা cycle-এ), আর এবার control unit-কে datapath-and-control-unit লেসনের FSM পদ্ধতিতে বানান — single-cycle বনাম multi-cycle-এর ট্রেড-অফ (সরল কিন্তু ধীর, বনাম জটিল কিন্তু cycle-সংখ্যা কম) নিজে অনুভব করুন
এটা যেখানে গিয়ে মিশবে
| এখানে যা শিখলেন | পরে কোথায় লাগবে |
|---|---|
| পূর্ণ single-cycle datapath (PC, regfile, ALU, memory ওয়্যারিং) | Level 3 — CPU Architecture-এর পুরো ভিত্তি; CPU Emulator প্রজেক্ট (RISC-V RV32I) |
| Combinational control বনাম FSM control-এর পার্থক্য | Level 3 — multi-cycle ও pipelined control, hazard handling |
| কাস্টম ISA ডিজাইন, বিট-লেভেল ইন্সট্রাকশন এনকোডিং | Level 3 — ISA, RISC vs CISC, instruction encoding/decoding |
| হাতে-অ্যাসেম্বল করা মেশিন কোড | Level 3 (Assembly module) — Toy Assembler প্রজেক্ট |
| এই single-cycle ডিজাইনের সীমাবদ্ধতা (no pipelining) | Level 3 — Pipelining ও hazards, Pipeline Visualizer প্রজেক্ট |
| Hierarchical testbench-ভিত্তিক verification | Level 11 — Advanced Architecture-এর profiling ও correctness methodology |