C-তে পয়েন্টার ও মেমরি-ম্যাপড রেজিস্টার
এই পাঠে যা শিখবেন
- পয়েন্টার dereference-এর সংক্ষিপ্ত রিক্যাপ, এমবেডেড প্রেক্ষাপটে
- মেমরি-ম্যাপড রেজিস্টার অ্যাক্সেসের ক্লাসিক C ইডিয়ম কীভাবে গঠিত ও কেন কাজ করে
- কেন
volatile(L09 থেকে) এই ইডিয়মের একটি অবিচ্ছেদ্য অংশ - একটি সত্যিকারের Python অ্যানালগ — dict-based মেমরি অ্যাক্সেস বনাম পেরিফেরাল মেথড, একই অবস্থায় যাচাই
১ · পয়েন্টার রিক্যাপ — একটি সংক্ষিপ্ত দৃষ্টিভঙ্গি
C Programming কোর্সে পয়েন্টারের সাধারণ সিনট্যাক্স ও ব্যবহার
বিস্তারিত কভার করা হয়েছে — এখানে আমরা শুধু সংক্ষেপে মনে করিয়ে দিচ্ছি: একটি পয়েন্টার ভ্যারিয়েবল কোনো ডেটা নিজে
ধারণ করে না, বরং সেই ডেটা কোথায় (কোন মেমরি ঠিকানায়) আছে তা ধারণ করে। *ptr
লিখলে (dereference) কম্পাইলার বলে "এই ঠিকানায় যাও, ওখানে যা আছে তা নাও (বা লিখে দাও)"। এমবেডেড C-তে এই
একই মেকানিজম একটি অসাধারণ কাজে ব্যবহৃত হয় — সরাসরি হার্ডওয়্যার পেরিফেরাল রেজিস্টার অ্যাক্সেস করা, যেহেতু L06
অনুযায়ী এই রেজিস্টারগুলো মূল ঠিকানা-স্পেসেরই একটি অংশ।
২ · ক্লাসিক মেমরি-ম্যাপড রেজিস্টার ইডিয়ম
একটি মাইক্রোকন্ট্রোলারের ডেটাশিটে প্রতিটি পেরিফেরাল রেজিস্টারের একটি নির্দিষ্ট, স্থির ঠিকানা লেখা থাকে (যেমন
একটি GPIO কন্ট্রোল রেজিস্টার হয়তো 0x40020000-এ বসানো)। এমবেডেড C কোডে এই ঠিকানাটিকে একটি
পয়েন্টার-এ রূপান্তর (cast) করে dereference করা হয় — এবং এই পুরো এক্সপ্রেশনটিকে সাধারণত একটি
#define ম্যাক্রোর ভেতরে মুড়ে রাখা হয়, যাতে বাকি কোডে GPIO_REG নামটি ঠিক একটি
সাধারণ ভ্যারিয়েবলের মতোই ব্যবহার করা যায়। মনে রাখবেন — volatile (L09) এখানে অপরিহার্য, কারণ এই
ঠিকানার মান হার্ডওয়্যার দ্বারা প্রোগ্রামের অজান্তে বদলে যেতে পারে।
৩ · ইলাস্ট্রেটিভ C কোড
নিচের C স্নিপেটটি এই ইডিয়ম দেখানোর জন্য — এই সাইটের স্যান্ডবক্সে কোনো C কম্পাইলার নেই বলে এটি সরাসরি চালানো যায় না, শুধুই ব্যাখ্যামূলক।
// ডেটাশিটে উল্লেখিত একটি নির্দিষ্ট, স্থির হার্ডওয়্যার ঠিকানা
#define GPIO_REG_ADDR 0x40020000
// ঠিকানাটিকে volatile uint32_t পয়েন্টারে cast করে dereference (*) করা --
// মেমরি-ম্যাপড রেজিস্টার অ্যাক্সেসের ক্লাসিক ইডিয়ম
#define GPIO_REG (*(volatile uint32_t *)GPIO_REG_ADDR)
void enable_gpio_pin(void) {
GPIO_REG = GPIO_REG | (1 << 5); // bit5 SET করে পিন এনাবল করা
}
uint32_t read_gpio_status(void) {
return GPIO_REG; // dereference করে সরাসরি বর্তমান মান পড়া
}
৪ · সত্যিকারের Python অ্যানালগ — দুই পথ, একই অবস্থা
নিচের কোড সেলে SimulatedMemory ক্লাসটি __getitem__/__setitem__
ব্যবহার করে একটি dict-ভিত্তিক "মেমরি" তৈরি করে — memory[address] লেখা C-এর
*(pointer) dereference-এর সমতুল্য। একই memory অবজেক্ট ও একই ঠিকানার উপর
GpioPeripheral ক্লাসের নিজস্ব মেথডও কাজ করে — কোড সেলটি সরাসরি যাচাই করে দেখায় দুটো ভিন্ন পথ
সত্যিই একই ফলাফলে পৌঁছায় কিনা।
class SimulatedMemory:
"""dict-based 'RAM' -- __getitem__/__setitem__ দিয়ে ঠিকানা দিয়ে অ্যাক্সেস,
C পয়েন্টার dereference (*ptr)-এর সমতুল্য।"""
def __init__(self):
self._store = {}
def __getitem__(self, address):
return self._store.get(address, 0)
def __setitem__(self, address, value):
self._store[address] = value
class GpioPeripheral:
"""পেরিফেরালের নিজস্ব 'অফিসিয়াল' API -- ভেতরে ভেতরে একই memory ও একই ঠিকানা ব্যবহার করে।"""
def __init__(self, mem, address):
self.mem = mem
self.address = address
def enable_pin(self, pin):
current = self.mem[self.address]
self.mem[self.address] = current | (1 << pin)
def read_status(self):
return self.mem[self.address]
GPIO_REG_ADDR = 0x40020000
memory = SimulatedMemory()
gpio = GpioPeripheral(memory, GPIO_REG_ADDR)
print("=== পথ ১: 'পয়েন্টার dereference'-এর সমতুল্য -- সরাসরি ঠিকানা দিয়ে memory[] অ্যাক্সেস ===")
memory[GPIO_REG_ADDR] = memory[GPIO_REG_ADDR] | (1 << 5) # C-এর GPIO_REG |= (1<<5)-এর সমতুল্য
value_via_pointer = memory[GPIO_REG_ADDR]
print(f"'পয়েন্টার' পথে bit5 SET করার পর মান: {format(value_via_pointer, '08b')}")
print()
print("=== পথ ২: পেরিফেরালের নিজস্ব মেথড দিয়ে একই বিট SET করা ===")
memory[GPIO_REG_ADDR] = 0 # রেজিস্টার রিসেট করে আবার শুরু থেকে দেখানো
gpio.enable_pin(5)
value_via_method = gpio.read_status()
print(f"'পেরিফেরাল মেথড' পথে bit5 SET করার পর মান: {format(value_via_method, '08b')}")
print()
print("=== যাচাই: দুটো ভিন্ন API কি সত্যিই একই অন্তর্নিহিত অবস্থায় পৌঁছালো? ===")
print(f"'পয়েন্টার' পথের ফলাফল : {format(value_via_pointer, '08b')}")
print(f"'মেথড' পথের ফলাফল : {format(value_via_method, '08b')}")
print(f"দুটো পথ একই মান দিলো : {value_via_pointer == value_via_method}")
print()
print("=== আরও প্রমাণ: এক পথে লিখে অন্য পথে পড়া ===")
memory[GPIO_REG_ADDR] = 0 # রিসেট
gpio.enable_pin(2) # পথ ২ (মেথড) দিয়ে লেখা
seen_via_pointer = memory[GPIO_REG_ADDR] # পথ ১ (dereference) দিয়ে পড়া -- একই memory, একই ঠিকানা
print(f"মেথড দিয়ে bit2 SET করার পর, সরাসরি memory[GPIO_REG_ADDR] দিয়ে পড়লে: {format(seen_via_pointer, '08b')}")
print("-> নিশ্চিত: দুটো ভিন্ন API আসলে একই memory dict-এর একই ঠিকানাকে প্রতিনিধিত্ব করছে।")
gpio.enable_pin(2) মেথড API দিয়ে লেখা হয়েছে, অথচ সরাসরি
memory[GPIO_REG_ADDR] (dereference-সমতুল্য পথ) দিয়ে পড়লেও সেই একই পরিবর্তন দেখা যাচ্ছে। এটাই
মূল কথা — GpioPeripheral-এর মেথডগুলো জাদু কিছু করে না, তারা ভেতরে ভেতরে ঠিক সেই একই ঠিকানায়
dereference-এর মতোই read-modify-write করে যা C-এর GPIO_REG ম্যাক্রো সরাসরি করত।
একটি মেমরি-ম্যাপড রেজিস্টার আসলে RAM-এর কোনো সাধারণ ভ্যারিয়েবল নয়, বরং একটি ফিক্সড ঠিকানা যেখানে হার্ডওয়্যার নিজে "শুনছে" — পয়েন্টার cast + dereference শুধু C ভাষায় সেই ঠিকানায় পৌঁছানোর একটি সরাসরি উপায়। বাস্তব ফার্মওয়্যারে হাই-লেভেল লাইব্রেরি/HAL (Hardware Abstraction Layer) ফাংশন যা করে, তা নিচের স্তরে গিয়ে এই একই পয়েন্টার dereference-এই পরিণত হয় — এই পাঠের Python অ্যানালগে ঠিক সেই সম্পর্কটাই সরাসরি দেখানো হলো। পরের পাঠে (L12) দেখা যাবে কীভাবে এই ঠিকানাগুলো আসলে একটি লিংকার স্ক্রিপ্টের মাধ্যমে চূড়ান্তভাবে নির্ধারিত হয়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
কোড সেলে SimulatedMemory.__getitem__-এ self._store.get(address, 0)
ব্যবহার করা হয়েছে, সরাসরি self._store[address] নয় কেন?
কারণ প্রথমবার কোনো ঠিকানা পড়ার আগে সেটি _store ডিকশনারিতে নাও থাকতে পারে (যেমন
GpioPeripheral.enable_pin()-এর প্রথম কলে current = self.mem[self.address]
চালানোর সময়, যদি আগে কখনো memory[GPIO_REG_ADDR] = 0 সেট করা না হতো)। .get(address,
0) ব্যবহার করলে না-থাকা ঠিকানার জন্য একটি KeyError-এর বদলে ডিফল্ট মান 0
পাওয়া যায় — বাস্তব হার্ডওয়্যারেও একটি রেজিস্টার সাধারণত রিসেটের পর একটি সুনির্দিষ্ট (প্রায়ই ০) ডিফল্ট
মান নিয়ে শুরু হয়।
প্র ০২
এই লেসনের ম্যাক্রো ইডিয়মে volatile বাদ দিলে কী সমস্যা হতে পারে, L09-এর সাথে সম্পর্ক
কী?
volatile ছাড়া, L09-এ দেখানো ঠিক একই caching বাগ এখানেও ঘটতে পারে — কম্পাইলার
GPIO_REG-কে একটি সাধারণ, স্থির ভ্যারিয়েবল ধরে নিয়ে একবার পড়া মান পুনর্ব্যবহার করতে পারে,
অথচ হার্ডওয়্যার সেই ঠিকানার মান নিজে থেকে বদলে দিতে পারে (যেমন একটি স্ট্যাটাস রেজিস্টার)। তাই মেমরি-ম্যাপড
রেজিস্টার ম্যাক্রোতে পয়েন্টার cast-এর ভেতরে volatile সবসময় অন্তর্ভুক্ত থাকা আবশ্যক — এই
দুই পাঠ আসলে একই সমস্যার দুটো দিক দেখাচ্ছে: পয়েন্টার দিয়ে ঠিকানা নির্দিষ্ট করা (L11), আর সেই ঠিকানার
রিড/রাইট সঠিকভাবে ঘটানো (L09)।
প্র ০৩
GpioPeripheral-এর মতো একটি ক্লাস/HAL ব্যবহার করার সুবিধা কী, যদি এটি শেষ পর্যন্ত সরাসরি
dereference-এর মতোই একই কাজ করে?
দুটো পথ একই অন্তর্নিহিত অবস্থায় পৌঁছালেও, একটি নামযুক্ত মেথড (enable_pin(5)) সরাসরি
GPIO_REG = GPIO_REG | (1 << 5)-এর চেয়ে অনেক বেশি পাঠযোগ্য এবং ভুল-প্রবণতা কম —
বিট-নম্বর মনে রাখার দরকার নেই, ম্যাজিক নাম্বার লুকানো থাকে একটি জায়গায়, এবং টাইপো করলে (ভুল বিট
শিফট) কম্পাইলার-লেভেলেই সহজে ধরা পড়ে যদি ফাংশনটি সঠিকভাবে লেখা থাকে। এটাই একটি HAL (Hardware
Abstraction Layer)-এর মূল উদ্দেশ্য — একই নিচু-স্তরের অ্যাক্সেস, কিন্তু নিরাপদ, পাঠযোগ্য একটি স্তরের
আড়ালে।
অনুশীলন
-
চিন্তা করুন: কোড সেলের একেবারে শেষ ধাপে
gpio.enable_pin(2)কল করার আগে রেজিস্টার0-এ রিসেট করা হয়েছে। যদি রিসেট না করা হতো (আগের ধাপের bit5=1 থেকে যেত), শেষprint-এ কী মান দেখাত বলে আপনার ধারণা?enable_pin()একটি read-modify-write (current | (1 << pin)) — তাই রিসেট না করলে আগের bit5=1 অক্ষত থেকে যেত এবং নতুন bit2 তার সাথে যোগ হতো, ফলাফল হতো00100100(bit5 ও bit2 দুটোই ১), রিসেট করার ফলে যেমন শুধু00000100(শুধু bit2) দেখা যায়। -
পরীক্ষা করুন: কোড সেলে
GpioPeripheral-এ একটি নতুন মেথডdisable_pin(self, pin)যোগ করুন যাself.mem[self.address] &= ~(1 << pin)দিয়ে বিট ক্লিয়ার করে, তারপরgpio.enable_pin(5)-এর পরgpio.disable_pin(5)কল করেgpio.read_status()প্রিন্ট করে দেখুন।enable_pin(5)-এর পর রেজিস্টার00100000হবে, তারপরdisable_pin(5)সেই একই বিট মুছে ফেলবে (L10-এর clear-bit প্যাটার্নের মতোই), ফলেread_status()আবার00000000ফেরত দেবে — নিশ্চিত করবে যেGpioPeripheral-এর মেথডগুলো L10-এ শেখা একই বিটওয়াইজ নীতিই ব্যবহার করে, শুধু একটি পরিষ্কার নামের আড়ালে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ মাইক্রোপ্রসেসর আর্কিটেকচার, এমবেডেড C, GPIO, টাইমার/PWM/ADC, সিরিয়াল প্রোটোকল, RTOS, সেন্সর/অ্যাকচুয়েটর, IoT আর্কিটেকচার, ওয়্যারলেস প্রোটোকল, MQTT/CoAP ও IoT সিকিউরিটি — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- C Programming কোর্স সহোদর কোর্স পয়েন্টারের সাধারণ সিনট্যাক্স ও ব্যবহার সেই কোর্সেই বিস্তারিত কভার করা হয়েছে — এই কোর্স হার্ডওয়্যার-স্পেসিফিক প্রয়োগে গভীরে যায়।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Design and Analysis of Algorithms ও আরও অনেক কোর্স — সব এক জায়গায়।