পাঠ ২৮ · ৫৭-এর মধ্যে · মডিউল ৬
Home / Courses / Computer Architecture & Digital Logic / CPU ডেটাপাথ ও কন্ট্রোল

সিঙ্গেল-সাইকেল ডেটাপাথ ডিজাইন

Single-cycle datapath design
৯ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ডেটাপাথ কী এবং এটি আগের মডিউলগুলোর কোন কোন উপাদান দিয়ে তৈরি
  • সিঙ্গেল-সাইকেল ডিজাইনের সংজ্ঞা এবং এর একটি গুরুত্বপূর্ণ, বাস্তব পারফরম্যান্স সীমাবদ্ধতা
  • Fetch-Decode-Execute-Memory-Writeback — পাঁচটি ধাপ ঠিক কীভাবে হার্ডওয়্যারে ম্যাপ হয়
  • Python দিয়ে একটি সরলীকৃত SingleCycleDatapath বানিয়ে তিন ধরনের ইনস্ট্রাকশন (R-type ADD, I-type ADDI, LOAD) সঠিকভাবে এক্সিকিউট করে দেখানো

১ · ডেটাপাথ — আগের মডিউলগুলোর সমন্বয়

ডেটাপাথDatapathইনস্ট্রাকশন এক্সিকিউট করতে ডেটা যে হার্ডওয়্যার পথ দিয়ে প্রবাহিত হয়। কোনো নতুন উপাদান নয় — এটি এই কোর্সে ইতিমধ্যে শেখা টুকরোগুলোর একটি সমন্বিত সার্কিট। এতে থাকে —

রেজিস্টার ফাইল (L26)
PC, IR সহ সাধারণ-উদ্দেশ্য রেজিস্টারের দ্রুত অ্যারে — Decode ধাপে পড়া হয়, Writeback ধাপে লেখা হয়।
ALU
L08-L09-এর অ্যাডার/সাবট্রাক্টর যুক্তি দিয়ে তৈরি — Execute ধাপে প্রকৃত হিসাব (যোগ, ঠিকানা গণনা) করে।
MUX (L10)
প্রতিটি ধাপে একাধিক সম্ভাব্য ডেটা-উৎসের মধ্যে সঠিকটি বেছে নেয় (যেমন ALU-এর দ্বিতীয় ইনপুট রেজিস্টার হবে না immediate হবে)।
ইনস্ট্রাকশন ও ডেটা মেমরি
যথাক্রমে Fetch ধাপে ইনস্ট্রাকশন এবং LOAD/STORE-এর Memory ধাপে ডেটা সরবরাহ করে।

২ · সিঙ্গেল-সাইকেল ডিজাইন — প্রতিটি ইনস্ট্রাকশন এক ক্লক সাইকেলে

সবচেয়ে সরল ডেটাপাথ কৌশল হলো সিঙ্গেল-সাইকেল ডিজাইন — যেকোনো ইনস্ট্রাকশন, তার প্রকার যাই হোক (সরল ADD হোক বা জটিল LOAD হোক), ঠিক একটি ক্লক সাইকেলে সম্পূর্ণ ফেচ থেকে রাইটব্যাক পর্যন্ত শেষ হয়। এটি বোঝা ও ডিজাইন করা সহজ — কিন্তু একটি গুরুতর, বাস্তব পারফরম্যান্স সমস্যা তৈরি করে।

সিঙ্গেল-সাইকেল ডিজাইনের সীমাবদ্ধতা

যেহেতু প্রতিটি ইনস্ট্রাকশনকে একই ক্লক সাইকেলের মধ্যে শেষ হতে হয়, সেই ক্লক সাইকেলের দৈর্ঘ্য অবশ্যই এতটা দীর্ঘ হতে হবে যাতে সবচেয়ে ধীরতম সম্ভাব্য ইনস্ট্রাকশন (সাধারণত LOAD — যা রেজিস্টার ফাইল পড়া, ALU দিয়ে ঠিকানা গণনা, মেমরি অ্যাক্সেস, এবং রাইটব্যাক — এই চারটি ধাপই ছুঁয়ে যায়) সময়মতো শেষ হতে পারে। ফলে একটি দ্রুত, সরল ADD ইনস্ট্রাকশনও — যেটি আসলে মেমরি স্পর্শই করে না — সেই একই দীর্ঘ সাইকেল অপেক্ষা করতে বাধ্য হয়, স্রেফ কারণ ঘড়ির গতি সবার জন্য এক। এটিই M6/L31-এর মাল্টি-সাইকেল ডিজাইন এবং M7-এর পাইপলাইনিং-এর মূল অনুপ্রেরণা — উভয়েই এই অপচয় কমানোর চেষ্টা করে।

৩ · পাঁচটি ধাপ — বাস্তব হার্ডওয়্যারে ম্যাপ করা

Fetch — PC থেকে ইনস্ট্রাকশন আনা, PC++ Decode — রেজিস্টার ফাইল পড়া (L26) Execute — ALU হিসাব (L08-09) Memory — শুধু LOAD/STORE-এর জন্য Writeback — রেজিস্টার ফাইলে লেখা
প্রতিটি ইনস্ট্রাকশন এই একই পথ ধরে ভ্রমণ করে, তবে ADD/ADDI-এর মতো ইনস্ট্রাকশন Memory ধাপে বাস্তবে কিছুই করে না — তবু ক্লক সাইকেল সেই সময়টুকু "অপেক্ষা" করে।

নিচের কোড সেলে একটি SingleCycleDatapath ক্লাস এই পাঁচটি ধাপ প্রতিটি ইনস্ট্রাকশনের জন্য স্পষ্টভাবে, আলাদা আলাদা করে (একটি অস্বচ্ছ ফাংশনের বদলে) চালায় — R-type ADD, I-type ADDI, এবং LOAD — তিন ধরনের ইনস্ট্রাকশনের জন্য প্রতিটি ধাপের কাজ ও তার ফলাফলে রেজিস্টার/মেমরি স্টেট কীভাবে বদলাল তা প্রিন্ট করা হয়েছে।

Python
# -- একটি টয় সিমুলেটেড ডেটাপাথ: রেজিস্টার ফাইল ও মেমরি Python dict/list, কোনো বাস্তব হার্ডওয়্যার অ্যাক্সেস নয়

class SingleCycleDatapath:
    def __init__(self, registers, data_memory):
        self.registers = registers   # রেজিস্টার ফাইল (L26)
        self.data_memory = data_memory
        self.pc = 0

    def run_instruction(self, instr):
        op = instr['op']
        print(f"\n=== ইনস্ট্রাকশন: {op}  (PC={self.pc}) ===")

        # ----- Fetch -----
        fetched_pc = self.pc
        self.pc += 1
        print(f"[Fetch]     PC={fetched_pc} থেকে ইনস্ট্রাকশন আনা হলো, PC এখন {self.pc}")

        # ----- Decode: রেজিস্টার ফাইল পড়া -----
        rs1_val = self.registers[instr['rs1']]
        rs2_val = self.registers[instr['rs2']] if op == 'ADD' else None
        if op == 'ADD':
            print(f"[Decode]    রেজিস্টার ফাইল পড়া হলো: r{instr['rs1']}={rs1_val}, r{instr['rs2']}={rs2_val}")
        else:
            print(f"[Decode]    রেজিস্টার ফাইল পড়া হলো: r{instr['rs1']}={rs1_val}, immediate={instr['imm']}")

        # ----- Execute: ALU -----
        if op == 'ADD':
            alu_result = rs1_val + rs2_val
            print(f"[Execute]   ALU: {rs1_val} + {rs2_val} = {alu_result}")
        elif op == 'ADDI':
            alu_result = rs1_val + instr['imm']
            print(f"[Execute]   ALU: {rs1_val} + {instr['imm']} = {alu_result}")
        elif op == 'LOAD':
            alu_result = rs1_val + instr['imm']   # ঠিকানা গণনা
            print(f"[Execute]   ALU (ঠিকানা গণনা): {rs1_val} + {instr['imm']} = {alu_result}")

        # ----- Memory: শুধু LOAD-এর জন্য কার্যকর -----
        mem_value = None
        if op == 'LOAD':
            mem_value = self.data_memory.get(alu_result, 0)
            print(f"[Memory]    mem[{alu_result}] পড়া হলো = {mem_value}")
        else:
            print(f"[Memory]    (এই ইনস্ট্রাকশনের মেমরি অ্যাক্সেসের দরকার নেই -- সাইকেল অপেক্ষা করে)")

        # ----- Writeback -----
        result = mem_value if op == 'LOAD' else alu_result
        if instr['rd'] != 0:
            self.registers[instr['rd']] = result
        print(f"[Writeback] r{instr['rd']} = {result}")
        return result


registers = [0, 10, 20, 0, 0, 0, 100, 0]   # r1=10, r2=20, r6=100 (LOAD-এর বেস ঠিকানা)
data_memory = {100: 42}                      # mem[100] = 42

datapath = SingleCycleDatapath(registers, data_memory)

# ইনস্ট্রাকশন ১: R-type ADD  -- r3 = r1 + r2
datapath.run_instruction({'op': 'ADD', 'rd': 3, 'rs1': 1, 'rs2': 2})
# ইনস্ট্রাকশন ২: I-type ADDI -- r4 = r1 + 5
datapath.run_instruction({'op': 'ADDI', 'rd': 4, 'rs1': 1, 'imm': 5})
# ইনস্ট্রাকশন ৩: LOAD -- r5 = mem[r6 + 0]
datapath.run_instruction({'op': 'LOAD', 'rd': 5, 'rs1': 6, 'imm': 0})

print("\n--- চূড়ান্ত রেজিস্টার স্টেট ---")
print(f"r3 (ADD  ফলাফল) = {registers[3]}   প্রত্যাশিত: 10+20=30")
print(f"r4 (ADDI ফলাফল) = {registers[4]}   প্রত্যাশিত: 10+5=15")
print(f"r5 (LOAD ফলাফল) = {registers[5]}   প্রত্যাশিত: mem[100]=42")

assert registers[3] == 30 and registers[4] == 15 and registers[5] == 42
print("\nতিনটি ভিন্ন ইনস্ট্রাকশন টাইপই সঠিক ফলাফল দিয়েছে।")

    
লক্ষ করুন — ADD ও ADDI ইনস্ট্রাকশনেও Memory ধাপটি "চলে" (কোডে দেখা যায়), শুধু কোনো প্রকৃত মেমরি অ্যাক্সেস ঘটে না। বাস্তব হার্ডওয়্যারেও ঠিক একই ঘটনা ঘটে — ক্লক সাইকেলের সেই সময়টুকু "খালি" কাটে, যা সিঙ্গেল-সাইকেল ডিজাইনের অদক্ষতার সরাসরি প্রমাণ।
মূল কথা · Key takeaway

সিঙ্গেল-সাইকেল ডেটাপাথ আগের মডিউলগুলোর সব উপাদান একত্র করে একটি সম্পূর্ণ, কার্যকরী CPU-এর প্রথম ধারণা দেয় — কিন্তু এর "সবাই সমান দীর্ঘ সাইকেল" নিয়ম বাস্তবে অদক্ষ। L29-এ দেখবেন কীভাবে একটি কন্ট্রোল ইউনিট এই একই ডেটাপাথের প্রতিটি MUX ও সিগন্যাল নিয়ন্ত্রণ করে, আর L31-এ দেখবেন কীভাবে এই ধাপগুলোকে ভিন্ন ভিন্ন সাইকেলে ভাগ করে এই অদক্ষতা কমানো যায়।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ ADD ইনস্ট্রাকশন যদি কখনো মেমরি স্পর্শই না করে, তাহলে সিঙ্গেল-সাইকেল ডিজাইনে তাকে কেন মেমরির জন্য বরাদ্দ সময়টুকুও "অপেক্ষা" করতে হয়?

কারণ পুরো ডেটাপাথ একটিমাত্র সাধারণ ক্লক সিগন্যাল দিয়ে চালিত হয় — সব ইনস্ট্রাকশনের জন্য ক্লক সাইকেলের দৈর্ঘ্য একটিই স্থির সংখ্যা, ইনস্ট্রাকশন-নির্দিষ্ট নয়। এই দৈর্ঘ্য নির্ধারিত হয় সবচেয়ে ধীর সম্ভাব্য ইনস্ট্রাকশন (LOAD) অনুযায়ী, তাই দ্রুত ইনস্ট্রাকশনও বাধ্য হয়ে সেই একই দীর্ঘ সময় ধরে "চলমান" থাকে, এমনকি তার আসল কাজ আগেই শেষ হয়ে গেলেও।

প্র ০২ কোড সেলে ADDI-এর Execute ধাপে ALU-এর দ্বিতীয় ইনপুট রেজিস্টার (r2) না হয়ে একটি immediate মান (5) হলো কীভাবে? বাস্তব হার্ডওয়্যারে এটি কোন উপাদান নিয়ন্ত্রণ করে?

বাস্তব হার্ডওয়্যারে এটি একটি MUX (L10) নিয়ন্ত্রণ করে — ALU-এর দ্বিতীয় ইনপুটে দুটো সম্ভাব্য উৎস আসে (রেজিস্টার ফাইলের rs2 আউটপুট, অথবা ইনস্ট্রাকশনের immediate ফিল্ড), আর একটি কন্ট্রোল সিগন্যাল (প্রচলিত নাম ALUSrc) MUX-কে বলে দেয় কোনটি বেছে নিতে হবে। L29-এ ঠিক এই সিগন্যালটিই কীভাবে opcode থেকে স্বয়ংক্রিয়ভাবে তৈরি হয় তা দেখবেন।

প্র ০৩ উপরের সিমুলেটরে if instr['rd'] != 0: চেক করে তবেই রেজিস্টার ফাইলে লেখা হয় কেন?

এটি L26-এর হার্ডওয়্যার্ড-জিরো নিয়মের সরাসরি প্রয়োগ — r0 সবসময় ০ থাকতে হবে, তাই কোনো ইনস্ট্রাকশনের Writeback ধাপ ভুলবশত r0-কে ওভাররাইট করতে পারবে না। বাস্তব হার্ডওয়্যারেও এই সুরক্ষা রেজিস্টার ফাইলের রাইট-পোর্ট সার্কিটেই বেক-ইন করা থাকে (r0-এর রাইট-এনেবল সিগন্যাল হার্ডওয়্যার্ডভাবে বন্ধ রাখা হয়)।

অনুশীলন

  1. চিন্তা করুন: একটি STORE ইনস্ট্রাকশনের জন্য এই পাঁচটি ধাপের কোনগুলো সক্রিয় থাকবে, আর কোনটি (Writeback) কার্যত অকার্যকর থাকবে এবং কেন?

    STORE-এর জন্য Fetch, Decode (দুটো রেজিস্টার পড়া — base ও ডেটা), Execute (ঠিকানা গণনা), এবং Memory (মেমরিতে লেখা) — এই চারটি ধাপ সক্রিয় থাকে। Writeback ধাপ অকার্যকর থাকে, কারণ STORE কোনো রেজিস্টারে ফলাফল লেখে না — এটি শুধু মেমরিতে লেখে। এটি ঠিক LOAD-এর বিপরীত প্যাটার্ন — LOAD মেমরি থেকে পড়ে Writeback করে, STORE রেজিস্টার থেকে পড়ে মেমরিতে লেখে।

  2. পরীক্ষা করুন: কোড সেলে যদি data_memory = {100: 42}-এর বদলে data_memory = {} (খালি) দেওয়া হতো, তাহলে LOAD ইনস্ট্রাকশনের ফলাফল কী হতো এবং কেন?

    self.data_memory.get(alu_result, 0)-এর কারণে ঠিকানা ১০০ মেমরিতে না থাকলে ডিফল্ট মান ০ পড়ত — অর্থাৎ r5 = 0 হতো, কোনো এরর ছাড়াই। এটি এই সরলীকৃত সিমুলেটরের একটি শিক্ষামূলক সরলীকরণ; বাস্তব হার্ডওয়্যারে "না-বরাদ্দ" মেমরি অ্যাক্সেস প্রায়ই একটি হার্ডওয়্যার ফল্ট/এক্সেপশন তৈরি করে (M7/L36-এ এক্সেপশন হ্যান্ডলিং বিস্তারিত আসবে)।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
অ্যাসেম্বলি ল্যাঙ্গুয়েজ বেসিকস