মাইক্রোকন্ট্রোলারে ভন নিউম্যান বনাম হার্ভার্ড আর্কিটেকচার
এই পাঠে যা শিখবেন
- ভন নিউম্যান ও হার্ভার্ড আর্কিটেকচারের মৌলিক গঠনগত পার্থক্য
- কেন বেশিরভাগ মাইক্রোকন্ট্রোলার হার্ভার্ড আর্কিটেকচার বেছে নেয়
- সমান্তরাল ফেচ+ডেটা-অ্যাক্সেসের বাস্তব পারফরম্যান্স সুবিধা
- একটি সত্যিকারের, চলমান সিমুলেশন — একই প্রোগ্রামের জন্য দুই আর্কিটেকচারের প্রকৃত টিক-গণনার পার্থক্য
১ · দুটি ভিন্ন মেমরি-সংগঠন দর্শন
L06-এ আমরা দেখেছি কীভাবে Flash, SRAM ও পেরিফেরাল রেজিস্টার একটি একক অ্যাড্রেস স্পেসে ম্যাপ থাকে — কিন্তু "একক অ্যাড্রেস স্পেস" আর "একক ফিজিক্যাল বাস" এক জিনিস নয়। CPU কীভাবে প্রোগ্রাম কোড ও ডেটার সাথে সংযুক্ত থাকে তার উপর ভিত্তি করে দুটি মৌলিক আর্কিটেকচারাল দর্শন আছে —
- ভন নিউম্যানVon Neumann Architectureএকটি একক, শেয়ার্ড মেমরি ও বাসে প্রোগ্রাম কোড ও ডেটা দুটোই থাকে -- একবারে একটি অ্যাক্সেসই সম্ভব। — প্রোগ্রাম কোড ও ডেটা একই মেমরিতে, একই বাসের মাধ্যমে CPU-র সাথে সংযুক্ত। সরল ডিজাইন, কিন্তু একবারে হয় একটি ইনস্ট্রাকশন ফেচ করা যায় নয়তো একটি ডেটা অ্যাক্সেস করা যায় — দুটো একসাথে নয় (একে "ভন নিউম্যান বটলনেক" বলা হয়)।
- হার্ভার্ডHarvard Architectureপ্রোগ্রাম কোড ও ডেটার জন্য সম্পূর্ণ আলাদা মেমরি ও বাস -- একই সাথে ফেচ ও ডেটা-অ্যাক্সেস সম্ভব। — প্রোগ্রাম কোড ও ডেটার জন্য সম্পূর্ণ আলাদা মেমরি এবং আলাদা বাস। এর ফলে একই সাথে (একই টিকে) একটি ইনস্ট্রাকশন ফেচ করা যায় এবং একটি ডেটা রিড/রাইট করা যায় — বেশিরভাগ AVR, PIC ও অনেক ARM Cortex-M চিপ এই ধরন অনুসরণ করে, কারণ পূর্বাভাসযোগ্য টাইমিং ও দ্রুততর এক্সিকিউশন এমবেডেড কাজে গুরুত্বপূর্ণ।
২ · একটি সত্যিকারের সিমুলেশন — টিক-গণনার পার্থক্য
নিচে একই ৪-ইনস্ট্রাকশনের প্রোগ্রাম দুটো ভিন্ন CPU মডেলে চালানো হয়েছে। HarvardCpu-তে
প্রোগ্রাম মেমরি ও ডেটা মেমরি সম্পূর্ণ আলাদা ডিকশনারি — তাই একই টিকে ফেচ ও ডেটা-অ্যাক্সেস দুটোই ঘটে।
VonNeumannCpu-তে একটিই শেয়ার্ড ডিকশনারি — তাই ফেচ ও ডেটা-অ্যাক্সেসকে আলাদা টিকে
সিরিয়ালাইজ (একের পর এক) করতে হয়।
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 ছাড়াই), তাহলে হার্ভার্ড ও ভন নিউম্যানের টিক সংখ্যায় পার্থক্য থাকত কি?
না, তেমন কোনো ইনস্ট্রাকশনের জন্য দুটো মডেলই সমান টিক নিত (প্রতি ইনস্ট্রাকশনে শুধু একটি ফেচ, কোনো আলাদা ডেটা-অ্যাক্সেস নেই)। হার্ভার্ডের সুবিধা তখনই প্রকাশ পায় যখন কোনো ইনস্ট্রাকশন মেমরি থেকে ডেটা রিড/রাইট করে — তখনই ফেচ ও ডেটা-অ্যাক্সেস একই সাথে (হার্ভার্ড) নাকি আলাদা টিকে (ভন নিউম্যান) ঘটবে তা গুরুত্বপূর্ণ হয়ে ওঠে।
প্র ০২
উপরের কোড সেলে VonNeumannCpu-তে ডেটাকে data_base = 1000 ঠিকানা
থেকে শুরু করে রাখা হয়েছে কেন, প্রোগ্রামের ইনস্ট্রাকশনের একই ঠিকানা রেঞ্জে না রেখে?
কারণ ভন নিউম্যান মডেলেও কোড ও ডেটা একই ফিজিক্যাল মেমরিতে থাকলেও তাদের ঠিকানা ওভারল্যাপ করা যায়
না — প্রোগ্রাম ইনস্ট্রাকশন ঠিকানা 0-এ শুরু হচ্ছে, তাই ডেটাকে অনেক দূরের একটি ঠিকানা
রেঞ্জে (1000+) রাখা নিশ্চিত করে দুটো একে অপরের সাথে সংঘর্ষ (collision) না করে —
ঠিক যেভাবে বাস্তব ভন নিউম্যান-স্টাইল মেমরি ম্যাপেও কোড ও ডেটা সেগমেন্ট আলাদা ঠিকানা রেঞ্জে রাখা
হয়, শুধু একই বাসে অ্যাক্সেস করা হয়।
প্র ০৩ হার্ভার্ড আর্কিটেকচারের এই পারফরম্যান্স সুবিধা থাকা সত্ত্বেও কেন সব CPU (যেমন ডেস্কটপ/সার্ভার প্রসেসর) হার্ভার্ড ব্যবহার করে না?
দুটো সম্পূর্ণ আলাদা মেমরি সিস্টেম ও বাস তৈরি করা বেশি হার্ডওয়্যার (বেশি পিন, বেশি ট্রানজিস্টর, বেশি বোর্ড জটিলতা) দাবি করে, এবং প্রোগ্রাম কোড ও ডেটার মধ্যে ফিক্সড বিভাজন থাকায় নমনীয়তা কমে যায় (যেমন স্ব-পরিবর্তনশীল কোড বা একটি সাধারণ OS-এর ডাইনামিক মেমরি ব্যবস্থাপনা কঠিন হয়ে পড়ে)। ডেস্কটপ/ সার্ভার CPU-তে ক্যাশ মেমরির মাধ্যমে (Computer Architecture কোর্সে বিস্তারিত) একটি "মডিফায়েড হার্ভার্ড" পদ্ধতি ব্যবহার করে আংশিক সুবিধা পাওয়া যায়, মূল মেমরি ভন নিউম্যান-স্টাইল রেখেই।
অনুশীলন
-
চিন্তা করুন: যদি প্রোগ্রামে ৬টি ইনস্ট্রাকশন থাকত এবং প্রতিটিই একটি করে LOAD/STORE
করত, তাহলে হার্ভার্ড ও ভন নিউম্যান মডেলে মোট টিক সংখ্যা কত হতো বলে আপনার ধারণা?
হার্ভার্ডে প্রতি ইনস্ট্রাকশনে ১ টিক (ফেচ+ডেটা-অ্যাক্সেস একসাথে) — মোট $6$ টিক। ভন নিউম্যানে প্রতি ইনস্ট্রাকশনে ২ টিক (ফেচ ও ডেটা-অ্যাক্সেস আলাদা) — মোট $12$ টিক। পার্থক্য এখানেও ঠিক দ্বিগুণ, উপরের ৪-ইনস্ট্রাকশনের উদাহরণের প্যাটার্নের সাথে সামঞ্জস্যপূর্ণ।
-
পরীক্ষা করুন: উপরের কোড সেলে
programলিস্টে একটি নতুন এন্ট্রি যোগ করুন —("LOAD", 2)— এবং Run চেপে দেখুনh_ticks,v_ticksও তাদের অনুপাত কীভাবে বদলায়।প্রোগ্রামে এখন ৫টি ইনস্ট্রাকশন, প্রতিটিই একটি ডেটা-অ্যাক্সেস করে, তাই
h_ticks৪ থেকে ৫ হবে এবংv_ticks৮ থেকে ১০ হবে — অনুপাত এখনও ঠিক $10/5 = 2.0$x-ই থাকবে, কারণ প্রতিটি নতুন LOAD/STORE ইনস্ট্রাকশন উভয় মডেলে সমানুপাতিকভাবে টিক যোগ করে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ মাইক্রোপ্রসেসর আর্কিটেকচার, এমবেডেড C, GPIO, টাইমার/PWM/ADC, সিরিয়াল প্রোটোকল, RTOS, সেন্সর/অ্যাকচুয়েটর, IoT আর্কিটেকচার, ওয়্যারলেস প্রোটোকল, MQTT/CoAP ও IoT সিকিউরিটি — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- L09 · এমবেডেড C ফান্ডামেন্টাল ও volatile পরবর্তী মডিউল M2-এর আর্কিটেকচার ভিত্তি শেষে M3-এ সরাসরি এমবেডেড C প্রোগ্রামিং-এ প্রবেশ।
- Computer Architecture & Digital Logic কোর্স সহোদর কোর্স ক্যাশ মেমরি ও "মডিফায়েড হার্ভার্ড" ডিজাইনের গভীর ভিত্তি সেই কোর্সেই তৈরি হয়েছে।