Foundationপ্রথম নীতি থেকে
LEVEL 2রিসার্চ-লেভেল~১৮ ঘণ্টাVerilog

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নামকাজফরম্যাট
000ADDrd = rs1 + rs2R-type
001SUBrd = rs1 − rs2R-type
010LOADrt = MEM[rs + imm7]I-type
011STOREMEM[rs + imm7] = rtI-type
100BEQif (rt == rs) PC = PC + 1 + imm7I-type
101JMPPC = imm13 (absolute)J-type
110, 111সংরক্ষিত — NOP আচরণ (PC কেবল বাড়ে)

রেজিস্টার

৮টা general-purpose রেজিস্টার (R0R7), প্রতিটা ১৬-বিট, ৩-বিট ঠিকানা। 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 সিগন্যাল টেবিল

opcodeRegWriteMemReadMemWriteALUSrc (imm)ALUOpMemToRegBranchJump
ADD1000 (রেজিস্টার)ADD0 (ALU)00
SUB1000 (রেজিস্টার)SUB0 (ALU)00
LOAD1101 (imm)ADD1 (মেমরি)00
STORE0011 (imm)ADD00
BEQ0000 (রেজিস্টার)SUB10
JMP00001

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
0LOAD R1,0(R0)010 001 000 00000004400
1LOAD R2,1(R0)010 010 000 00000014801
2ADD R3,R0,R0000 011 000 000 00000C00
3BEQ R2,R0,3100 010 000 00000118803
4ADD R3,R3,R2000 011 011 010 00000DA0
5SUB R2,R2,R1001 010 010 001 00002910
6JMP 3101 0000000000011A003
7STORE R3,10(R0)011 011 000 00010106C0A
8JMP 8101 0000000001000A008

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 — মোট প্রায় ২৫।

নিজেকে চ্যালেঞ্জ করুন

  1. পূর্ণ ALU — 8-bit ALU প্রজেক্টের AND/OR/XOR/shift যোগ করুন, নতুন opcode আর একটা প্রশস্ত alu_op দিয়ে
  2. JAL (jump-and-link) — রিটার্ন-ঠিকানা একটা রেজিস্টারে সংরক্ষণ করে ফাংশন-কলের মতো আচরণ যোগ করুন
  3. BNE (branch-if-not-equal) — control unit-এ নতুন একটা opcode আর alu_zero-কে invert করার লজিক
  4. Instruction trace — testbench-এ প্রতি cycle-এ PC, opcode, আর write-back মান প্রিন্ট করে একটা সম্পূর্ণ execution trace বানান
  5. 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-ভিত্তিক verificationLevel 11 — Advanced Architecture-এর profiling ও correctness methodology