পাঠ ০৮ · ৫৭-এর মধ্যে · মডিউল ২
Home / Courses / Microprocessors, Embedded Systems & IoT / আর্কিটেকচার তুলনা

মাইক্রোকন্ট্রোলারে ভন নিউম্যান বনাম হার্ভার্ড আর্কিটেকচার

Von Neumann vs Harvard architecture in microcontrollers
৯ মিনিট পড়া মধ্যম · Intermediate Python সিমুলেশনসহ সম্পূর্ণ বাংলায়

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

  • ভন নিউম্যান ও হার্ভার্ড আর্কিটেকচারের মৌলিক গঠনগত পার্থক্য
  • কেন বেশিরভাগ মাইক্রোকন্ট্রোলার হার্ভার্ড আর্কিটেকচার বেছে নেয়
  • সমান্তরাল ফেচ+ডেটা-অ্যাক্সেসের বাস্তব পারফরম্যান্স সুবিধা
  • একটি সত্যিকারের, চলমান সিমুলেশন — একই প্রোগ্রামের জন্য দুই আর্কিটেকচারের প্রকৃত টিক-গণনার পার্থক্য

১ · দুটি ভিন্ন মেমরি-সংগঠন দর্শন

L06-এ আমরা দেখেছি কীভাবে Flash, SRAM ও পেরিফেরাল রেজিস্টার একটি একক অ্যাড্রেস স্পেসে ম্যাপ থাকে — কিন্তু "একক অ্যাড্রেস স্পেস" আর "একক ফিজিক্যাল বাস" এক জিনিস নয়। CPU কীভাবে প্রোগ্রাম কোড ও ডেটার সাথে সংযুক্ত থাকে তার উপর ভিত্তি করে দুটি মৌলিক আর্কিটেকচারাল দর্শন আছে —

  • ভন নিউম্যানVon Neumann Architectureএকটি একক, শেয়ার্ড মেমরি ও বাসে প্রোগ্রাম কোড ও ডেটা দুটোই থাকে -- একবারে একটি অ্যাক্সেসই সম্ভব। — প্রোগ্রাম কোড ও ডেটা একই মেমরিতে, একই বাসের মাধ্যমে CPU-র সাথে সংযুক্ত। সরল ডিজাইন, কিন্তু একবারে হয় একটি ইনস্ট্রাকশন ফেচ করা যায় নয়তো একটি ডেটা অ্যাক্সেস করা যায় — দুটো একসাথে নয় (একে "ভন নিউম্যান বটলনেক" বলা হয়)।
  • হার্ভার্ডHarvard Architectureপ্রোগ্রাম কোড ও ডেটার জন্য সম্পূর্ণ আলাদা মেমরি ও বাস -- একই সাথে ফেচ ও ডেটা-অ্যাক্সেস সম্ভব। — প্রোগ্রাম কোড ও ডেটার জন্য সম্পূর্ণ আলাদা মেমরি এবং আলাদা বাস। এর ফলে একই সাথে (একই টিকে) একটি ইনস্ট্রাকশন ফেচ করা যায় এবং একটি ডেটা রিড/রাইট করা যায় — বেশিরভাগ AVR, PIC ও অনেক ARM Cortex-M চিপ এই ধরন অনুসরণ করে, কারণ পূর্বাভাসযোগ্য টাইমিং ও দ্রুততর এক্সিকিউশন এমবেডেড কাজে গুরুত্বপূর্ণ।
ভন নিউম্যান CPU একক বাস মেমরি (কোড + ডেটা একসাথে) হার্ভার্ড CPU কোড বাস প্রোগ্রাম মেমরি (কোড) ডেটা বাস ডেটা মেমরি (SRAM)
ভন নিউম্যানে একটি একক বাস দিয়ে ফেচ ও ডেটা-অ্যাক্সেস পালাক্রমে হয়; হার্ভার্ডে দুটো আলাদা বাস দিয়ে দুটোই একই সাথে ঘটতে পারে — নিচের কোড সেল এই পার্থক্যটি প্রকৃত টিক-গণনায় দেখাচ্ছে।

২ · একটি সত্যিকারের সিমুলেশন — টিক-গণনার পার্থক্য

নিচে একই ৪-ইনস্ট্রাকশনের প্রোগ্রাম দুটো ভিন্ন CPU মডেলে চালানো হয়েছে। HarvardCpu-তে প্রোগ্রাম মেমরি ও ডেটা মেমরি সম্পূর্ণ আলাদা ডিকশনারি — তাই একই টিকে ফেচ ও ডেটা-অ্যাক্সেস দুটোই ঘটে। VonNeumannCpu-তে একটিই শেয়ার্ড ডিকশনারি — তাই ফেচ ও ডেটা-অ্যাক্সেসকে আলাদা টিকে সিরিয়ালাইজ (একের পর এক) করতে হয়।

Python
class HarvardCpu:
    # হার্ভার্ড আর্কিটেকচার: প্রোগ্রাম মেমরি ও ডেটা মেমরি সম্পূর্ণ আলাদা বাসে, আলাদা মেমরিতে
    def __init__(self, program, initial_data=None):
        self.program_memory = dict(enumerate(program))  # আলাদা কোড মেমরি
        self.data_memory = dict(initial_data or {})      # আলাদা ডেটা মেমরি
        self.ticks_used = 0
        self.bus_access_log = []

    def run(self):
        pc = 0
        while pc in self.program_memory:
            self.ticks_used += 1
            instr = self.program_memory[pc]
            # হার্ভার্ডে ফেচ ও ডেটা-অ্যাক্সেস একই টিকে, দুটো ভিন্ন বাসে ঘটতে পারে
            self.bus_access_log.append((self.ticks_used, "code-bus", "fetch", pc))
            if instr[0] == "LOAD":
                value = self.data_memory.get(instr[1], 0)
                self.bus_access_log.append((self.ticks_used, "data-bus", "read", instr[1], value))
            elif instr[0] == "STORE":
                self.data_memory[instr[1]] = instr[2]
                self.bus_access_log.append((self.ticks_used, "data-bus", "write", instr[1], instr[2]))
            pc += 1
        return self.ticks_used


class VonNeumannCpu:
    # ভন নিউম্যান আর্কিটেকচার: কোড ও ডেটা একই মেমরি, একই বাসে -- একবারে একটি অ্যাক্সেসই সম্ভব
    def __init__(self, program, initial_data=None):
        self.memory = dict(enumerate(program))  # কোড মেমরির নিচু ঠিকানায় (0, 1, 2, ...)
        self.data_base = 1000                   # ডেটা একই মেমরিতে, ভিন্ন ঠিকানা রেঞ্জে
        for addr, value in (initial_data or {}).items():
            self.memory[self.data_base + addr] = value
        self.ticks_used = 0
        self.bus_access_log = []

    def run(self):
        pc = 0
        while pc in self.memory and isinstance(self.memory[pc], tuple):
            instr = self.memory[pc]
            self.ticks_used += 1  # ফেচের জন্য একটি টিক
            self.bus_access_log.append((self.ticks_used, "shared-bus", "fetch", pc))
            if instr[0] == "LOAD":
                addr = self.data_base + instr[1]
                self.ticks_used += 1  # একই বাস -- ডেটা রিডের জন্য আলাদা, দ্বিতীয় টিক লাগে
                value = self.memory.get(addr, 0)
                self.bus_access_log.append((self.ticks_used, "shared-bus", "read", addr, value))
            elif instr[0] == "STORE":
                addr = self.data_base + instr[1]
                self.ticks_used += 1  # একই বাস -- ডেটা রাইটের জন্য আলাদা, দ্বিতীয় টিক লাগে
                self.memory[addr] = instr[2]
                self.bus_access_log.append((self.ticks_used, "shared-bus", "write", addr, instr[2]))
            pc += 1
        return self.ticks_used


# একই প্রোগ্রাম: ঠিকানা 0 থেকে লোড, ঠিকানা 1-এ স্টোর, ঠিকানা 1 থেকে লোড, ঠিকানা 2-এ স্টোর
program = [
    ("LOAD", 0),
    ("STORE", 1, 42),
    ("LOAD", 1),
    ("STORE", 2, 99),
]
initial_data = {0: 7}

harvard = HarvardCpu(program, initial_data)
h_ticks = harvard.run()
print(f"হার্ভার্ড আর্কিটেকচার -- মোট টিক লাগলো: {h_ticks}")
for entry in harvard.bus_access_log:
    print("  ", entry)

von = VonNeumannCpu(program, initial_data)
v_ticks = von.run()
print(f"\nভন নিউম্যান আর্কিটেকচার -- মোট টিক লাগলো: {v_ticks}")
for entry in von.bus_access_log:
    print("  ", entry)

print(f"\nএকই প্রোগ্রামের জন্য ভন নিউম্যান হার্ভার্ডের তুলনায় {v_ticks - h_ticks} বেশি টিক ব্যবহার করলো "
      f"({v_ticks / h_ticks:.1f}x ধীর) -- কারণ একই বাসে ফেচ ও ডেটা-অ্যাক্সেস একসাথে করা যায় না।")

    
লক্ষ্য করুন — প্রোগ্রামের ৪টি ইনস্ট্রাকশনের প্রতিটিই একটি ডেটা-অ্যাক্সেস করে (LOAD বা STORE), তাই হার্ভার্ড মডেলে ঠিক ৪ টিক লাগে (প্রতি ইনস্ট্রাকশনে ফেচ+ডেটা-অ্যাক্সেস একসাথে), কিন্তু ভন নিউম্যান মডেলে প্রতিটি ইনস্ট্রাকশনের জন্য ফেচ ও ডেটা-অ্যাক্সেস আলাদা টিকে করতে হওয়ায় মোট ৮ টিক লাগে — ঠিক দ্বিগুণ। এটি কোনো কাকতালীয় ব্যাপার নয়; যেকোনো প্রোগ্রাম যেখানে প্রতিটি ইনস্ট্রাকশনই একটি ডেটা-অ্যাক্সেস করে, সেখানে এই ২x পার্থক্য সাধারণভাবে প্রত্যাশিত।
মূল কথা · Key takeaway

হার্ভার্ড আর্কিটেকচারের দ্বৈত বাস অতিরিক্ত হার্ডওয়্যার জটিলতার বিনিময়ে সমান্তরাল ফেচ+ডেটা-অ্যাক্সেস দেয় — একটি বাস্তব, পরিমাপযোগ্য পারফরম্যান্স সুবিধা যা রিসোর্স-সীমিত মাইক্রোকন্ট্রোলারে গুরুত্বপূর্ণ, যেখানে প্রতিটি টিক বাঁচানো মানে দ্রুততর রেসপন্স ও কম পাওয়ার খরচ।

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

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

প্র ০১ যদি প্রোগ্রামের সব ইনস্ট্রাকশনই শুধু গাণিতিক অপারেশন হতো (কোনো LOAD/STORE ছাড়াই), তাহলে হার্ভার্ড ও ভন নিউম্যানের টিক সংখ্যায় পার্থক্য থাকত কি?

না, তেমন কোনো ইনস্ট্রাকশনের জন্য দুটো মডেলই সমান টিক নিত (প্রতি ইনস্ট্রাকশনে শুধু একটি ফেচ, কোনো আলাদা ডেটা-অ্যাক্সেস নেই)। হার্ভার্ডের সুবিধা তখনই প্রকাশ পায় যখন কোনো ইনস্ট্রাকশন মেমরি থেকে ডেটা রিড/রাইট করে — তখনই ফেচ ও ডেটা-অ্যাক্সেস একই সাথে (হার্ভার্ড) নাকি আলাদা টিকে (ভন নিউম্যান) ঘটবে তা গুরুত্বপূর্ণ হয়ে ওঠে।

প্র ০২ উপরের কোড সেলে VonNeumannCpu-তে ডেটাকে data_base = 1000 ঠিকানা থেকে শুরু করে রাখা হয়েছে কেন, প্রোগ্রামের ইনস্ট্রাকশনের একই ঠিকানা রেঞ্জে না রেখে?

কারণ ভন নিউম্যান মডেলেও কোড ও ডেটা একই ফিজিক্যাল মেমরিতে থাকলেও তাদের ঠিকানা ওভারল্যাপ করা যায় না — প্রোগ্রাম ইনস্ট্রাকশন ঠিকানা 0-এ শুরু হচ্ছে, তাই ডেটাকে অনেক দূরের একটি ঠিকানা রেঞ্জে (1000+) রাখা নিশ্চিত করে দুটো একে অপরের সাথে সংঘর্ষ (collision) না করে — ঠিক যেভাবে বাস্তব ভন নিউম্যান-স্টাইল মেমরি ম্যাপেও কোড ও ডেটা সেগমেন্ট আলাদা ঠিকানা রেঞ্জে রাখা হয়, শুধু একই বাসে অ্যাক্সেস করা হয়।

প্র ০৩ হার্ভার্ড আর্কিটেকচারের এই পারফরম্যান্স সুবিধা থাকা সত্ত্বেও কেন সব CPU (যেমন ডেস্কটপ/সার্ভার প্রসেসর) হার্ভার্ড ব্যবহার করে না?

দুটো সম্পূর্ণ আলাদা মেমরি সিস্টেম ও বাস তৈরি করা বেশি হার্ডওয়্যার (বেশি পিন, বেশি ট্রানজিস্টর, বেশি বোর্ড জটিলতা) দাবি করে, এবং প্রোগ্রাম কোড ও ডেটার মধ্যে ফিক্সড বিভাজন থাকায় নমনীয়তা কমে যায় (যেমন স্ব-পরিবর্তনশীল কোড বা একটি সাধারণ OS-এর ডাইনামিক মেমরি ব্যবস্থাপনা কঠিন হয়ে পড়ে)। ডেস্কটপ/ সার্ভার CPU-তে ক্যাশ মেমরির মাধ্যমে (Computer Architecture কোর্সে বিস্তারিত) একটি "মডিফায়েড হার্ভার্ড" পদ্ধতি ব্যবহার করে আংশিক সুবিধা পাওয়া যায়, মূল মেমরি ভন নিউম্যান-স্টাইল রেখেই।

অনুশীলন

  1. চিন্তা করুন: যদি প্রোগ্রামে ৬টি ইনস্ট্রাকশন থাকত এবং প্রতিটিই একটি করে LOAD/STORE করত, তাহলে হার্ভার্ড ও ভন নিউম্যান মডেলে মোট টিক সংখ্যা কত হতো বলে আপনার ধারণা?

    হার্ভার্ডে প্রতি ইনস্ট্রাকশনে ১ টিক (ফেচ+ডেটা-অ্যাক্সেস একসাথে) — মোট $6$ টিক। ভন নিউম্যানে প্রতি ইনস্ট্রাকশনে ২ টিক (ফেচ ও ডেটা-অ্যাক্সেস আলাদা) — মোট $12$ টিক। পার্থক্য এখানেও ঠিক দ্বিগুণ, উপরের ৪-ইনস্ট্রাকশনের উদাহরণের প্যাটার্নের সাথে সামঞ্জস্যপূর্ণ।

  2. পরীক্ষা করুন: উপরের কোড সেলে program লিস্টে একটি নতুন এন্ট্রি যোগ করুন — ("LOAD", 2) — এবং Run চেপে দেখুন h_ticks, v_ticks ও তাদের অনুপাত কীভাবে বদলায়।

    প্রোগ্রামে এখন ৫টি ইনস্ট্রাকশন, প্রতিটিই একটি ডেটা-অ্যাক্সেস করে, তাই h_ticks ৪ থেকে ৫ হবে এবং v_ticks ৮ থেকে ১০ হবে — অনুপাত এখনও ঠিক $10/5 = 2.0$x-ই থাকবে, কারণ প্রতিটি নতুন LOAD/STORE ইনস্ট্রাকশন উভয় মডেলে সমানুপাতিকভাবে টিক যোগ করে।

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

আগের পাঠ
ক্লক সিস্টেম ও রিসেট