এমবেডেড সিস্টেমে ইন্টারাপ্ট — প্রায়োরিটি ও লেটেন্সি
এই পাঠে যা শিখবেন
- ইন্টারাপ্ট প্রায়োরিটি লেভেল ও নেস্টেড (প্রি-এম্পটিভ) ইন্টারাপ্ট কীভাবে কাজ করে
- ইন্টারাপ্ট লেটেন্সি ঠিক কী, এবং বাস্তবে এটি কেন কখনোই শূন্য হতে পারে না
- একটি সত্যিকারের, চলমান সিমুলেশন — একাধিক IRQ বিভিন্ন সময়ে এসে পৌঁছালে ডিসপ্যাচার কীভাবে সেগুলো প্রায়োরিটি অনুযায়ী সার্ভিস করে, FIFO অনুযায়ী নয়
- এই পাঠ পরবর্তী পুরো M7 মডিউলের (RTOS, সেমাফোর, প্রায়োরিটি ইনভার্সন) ভিত্তি তৈরি করে
১ · রিক্যাপ — ইন্টারাপ্ট আসলে কী
Computer Architecture & Digital Logic কোর্সে সাধারণ ইন্টারাপ্ট হ্যান্ডলিং প্রক্রিয়া — CPU কীভাবে চলমান ইনস্ট্রাকশন শেষ করে, বর্তমান স্টেট সেভ করে, আর ইন্টারাপ্ট ভেক্টর টেবিল থেকে হ্যান্ডলারের ঠিকানা খুঁজে বের করে — বিস্তারিত কভার করা হয়েছে। এই পাঠ সেই ভিত্তি ধরে নিয়ে সরাসরি এমবেডেড সিস্টেমের বিশেষ দিকে যায়: একাধিক IRQ একই সময়ে পেন্ডিং থাকলে কোনটা আগে, আর লেটেন্সি বাস্তবে কতটা গুরুত্বপূর্ণ।
সংক্ষেপে — একটি ইন্টারাপ্ট রিকোয়েস্ট (IRQ) হলো হার্ডওয়্যারের (একটি টাইমার ওভারফ্লো, একটি UART বাইট রিসিভ, একটি GPIO পিনের স্টেট পরিবর্তন) পক্ষ থেকে CPU-কে পাঠানো একটি সংকেত — "এখনই তোমার মূল কাজ থামিয়ে আমাকে সার্ভিস করো"। CPU তখন একটি ছোট ফাংশন — ইন্টারাপ্ট সার্ভিস রুটিন (ISR) — চালায়, তারপর মূল কাজে ফিরে যায়। একটি বাস্তব মাইক্রোকন্ট্রোলারে একই সময়ে একাধিক ভিন্ন উৎস থেকে IRQ পেন্ডিং থাকতে পারে — টাইমার, UART, ADC, GPIO — সবাই একসাথে CPU-এর দৃষ্টি আকর্ষণের চেষ্টা করতে পারে।
২ · প্রায়োরিটি লেভেল ও নেস্টেড ইন্টারাপ্ট
যদি একসাথে একাধিক IRQ পেন্ডিং থাকে, CPU কোনটা আগে সার্ভিস করবে তা ঠিক করতে প্রতিটি ইন্টারাপ্ট উৎসকে একটি প্রায়োরিটি লেভেল দেওয়া হয় (কনফিগারযোগ্য রেজিস্টারের মাধ্যমে)। নিয়ম সহজ — সবচেয়ে বেশি প্রায়োরিটির পেন্ডিং IRQ সবসময় আগে সার্ভিস পায়, এটা কখন এসেছে তা বিবেচ্য নয়।
এখান থেকেই আসে নেস্টেড ইন্টারাপ্টNested Interruptএকটি নিম্ন-প্রায়োরিটি ISR চলাকালীন একটি উচ্চ-প্রায়োরিটি IRQ এলে সেটি চলমান ISR-কে প্রি-এম্পট করে নিজে আগে চলে। — যদি CPU ইতিমধ্যে একটি নিম্ন-প্রায়োরিটি ISR চালাচ্ছে, আর ঠিক তখনই একটি উচ্চ-প্রায়োরিটি IRQ আসে, তাহলে সেই চলমান ISR-কে প্রি-এম্পট করে (নিজের স্টেট সেভ করে থেমে) উচ্চ-প্রায়োরিটি ISR আগে চলে, তারপর আবার নিম্ন-প্রায়োরিটি ISR-এ ফিরে আসা হয় — অনেকটা একটি ফাংশন কলের ভেতরে আরেকটি ফাংশন কল "নেস্ট" করার মতো। প্রতিটি মাইক্রোকন্ট্রোলার ফ্যামিলির নিজস্ব ইন্টারাপ্ট কন্ট্রোলার এই প্রি-এম্পশন লজিক হার্ডওয়্যারেই বাস্তবায়ন করে (যেমন ARM Cortex-M-এর NVIC)।
৩ · ইন্টারাপ্ট লেটেন্সি
ইন্টারাপ্ট লেটেন্সিInterrupt Latencyহার্ডওয়্যার ইভেন্ট ঘটার মুহূর্ত থেকে ISR-এর প্রথম ইনস্ট্রাকশন সত্যিই চলা শুরু হওয়া পর্যন্ত সময়। হলো হার্ডওয়্যার ইভেন্ট ঘটার মুহূর্ত (t=0) থেকে ISR-এর প্রথম ইনস্ট্রাকশন সত্যিই চলা শুরু হওয়া পর্যন্ত সময় — বাস্তবে এটি কখনোই শূন্য নয়। এর প্রধান উপাদানগুলো:
$$T_{latency} = T_{finish\_current\_instr} + T_{context\_save} + T_{vector\_fetch}$$অর্থাৎ — CPU-কে প্রথমে বর্তমান চলমান ইনস্ট্রাকশনটি শেষ করতে হয় (তাৎক্ষণিকভাবে মাঝপথে থামতে পারে না), তারপর বর্তমান রেজিস্টার/স্টেট স্ট্যাকে সেভ করতে হয় (যাতে ISR শেষে ঠিক আগের জায়গা থেকে চালিয়ে যাওয়া যায়), তারপর ইন্টারাপ্ট ভেক্টর টেবিল থেকে সঠিক ISR-এর ঠিকানা ফেচ করতে হয় — এই তিনটি ধাপ শেষ হওয়ার পরই ISR-এর আসল কোড চলা শুরু হয়। যদি ইতিমধ্যে একটি সমান বা বেশি প্রায়োরিটির ISR চলমান থাকে, নতুন IRQ-কে সেটি শেষ হওয়া পর্যন্ত (বা প্রি-এম্পট করতে না পারা পর্যন্ত) আরও অপেক্ষা করতে হয় — এই অতিরিক্ত অপেক্ষাও লেটেন্সির অংশ। এই কারণেই হার্ড রিয়েল-টাইম সিস্টেমে (M7-এর পরের পাঠে বিস্তারিত) সর্বোচ্চ সম্ভাব্য লেটেন্সি একটি নির্দিষ্ট সীমার মধ্যে থাকা নিশ্চিত করা জরুরি।
৪ · একটি সত্যিকারের সিমুলেশন — প্রায়োরিটি-চালিত ডিসপ্যাচার
নিচের কোডে পাঁচটি IRQ বিভিন্ন সিমুলেটেড টিকে বিভিন্ন প্রায়োরিটি নিয়ে আসছে (প্রায়োরিটি ১ = সবচেয়ে জরুরি, সংখ্যা যত বাড়বে প্রায়োরিটি তত কমবে)। ডিসপ্যাচার প্রতি টিকে পেন্ডিং তালিকা থেকে সবচেয়ে বেশি প্রায়োরিটিরটি বেছে নেয় — প্রয়োজনে চলমান একটি ISR-কেও প্রি-এম্পট করে। ট্রেসটি লক্ষ করুন — এটি সত্যিই গণনা করে দেখাচ্ছে যে সার্ভিসের ক্রম FIFO নয়, প্রায়োরিটি-চালিত।
class InterruptRequest:
def __init__(self, name, priority, arrival_tick, service_ticks):
self.name = name
self.priority = priority # ১ = সর্বোচ্চ প্রায়োরিটি, সংখ্যা বাড়লে প্রায়োরিটি কমে
self.arrival_tick = arrival_tick
self.service_ticks = service_ticks
self.remaining = service_ticks
self.first_start_tick = None
self.completion_tick = None
irqs = [
InterruptRequest("UART_RX", priority=3, arrival_tick=0, service_ticks=2),
InterruptRequest("TIMER_OVF", priority=2, arrival_tick=1, service_ticks=1),
InterruptRequest("ADC_DONE", priority=4, arrival_tick=1, service_ticks=2),
InterruptRequest("EXT_BUTTON", priority=1, arrival_tick=3, service_ticks=1),
InterruptRequest("SPI_DONE", priority=3, arrival_tick=4, service_ticks=1),
]
TOTAL_TICKS = 9
arrival_by_tick = {}
for irq in irqs:
arrival_by_tick.setdefault(irq.arrival_tick, []).append(irq)
pending = []
current = None
trace = []
for tick in range(TOTAL_TICKS):
for irq in arrival_by_tick.get(tick, []):
pending.append(irq)
trace.append(f"tick {tick}: {irq.name} (priority={irq.priority}) এসে পৌঁছাল")
if pending:
pending.sort(key=lambda r: (r.priority, r.arrival_tick))
best = pending[0]
if current is None or best.priority < current.priority:
if current is not None:
pending.append(current)
pending.sort(key=lambda r: (r.priority, r.arrival_tick))
trace.append(f"tick {tick}: {current.name} প্রি-এম্পটেড হলো (উচ্চ-প্রায়োরিটি {best.name} এসেছে)")
current = pending.pop(0)
if current.first_start_tick is None:
current.first_start_tick = tick
if current is not None:
current.remaining -= 1
trace.append(f"tick {tick}: {current.name} সার্ভিস হচ্ছে (priority={current.priority}, বাকি={current.remaining})")
if current.remaining == 0:
current.completion_tick = tick
trace.append(f"tick {tick}: {current.name} সম্পন্ন হলো")
current = None
else:
trace.append(f"tick {tick}: CPU আইডল")
for line in trace:
print(line)
print()
print(f"{'IRQ':<12}{'priority':<10}{'arrival':<9}{'start':<8}{'complete':<10}{'latency':<9}")
for irq in irqs:
latency = irq.first_start_tick - irq.arrival_tick
print(f"{irq.name:<12}{irq.priority:<10}{irq.arrival_tick:<9}{irq.first_start_tick:<8}{irq.completion_tick:<10}{latency:<9}")
completion_order = sorted(irqs, key=lambda r: r.completion_tick)
print("\nসম্পন্ন হওয়ার প্রকৃত ক্রম:", [r.name for r in completion_order])
ADC_DONE এসেছে tick ১-এ (সবার আগে, EXT_BUTTON ও
SPI_DONE-এর অনেক আগে), কিন্তু এটি সম্পন্ন হয় সবার শেষে (tick ৬-এ) — কারণ এর প্রায়োরিটি
সবচেয়ে কম (৪)। উল্টোদিকে EXT_BUTTON tick ৩-এ এসেই তাৎক্ষণিকভাবে সার্ভিস পায় কারণ এর প্রায়োরিটি
সবচেয়ে বেশি (১)। আর UART_RX tick ০-এ চালু হয়েও tick ১-এ উচ্চ-প্রায়োরিটি
TIMER_OVF-এর কাছে প্রি-এম্পটেড হয়, পরে আবার চালিয়ে গিয়ে সম্পন্ন হয় — এটাই নেস্টেড ইন্টারাপ্টের
বাস্তব প্রমাণ। এই "প্রায়োরিটি-চালিত, FIFO নয়" আচরণটাই RTOS টাস্ক শিডিউলিং-এর (L29) মূল ভিত্তি।
ইন্টারাপ্ট সার্ভিসিং প্রায়োরিটি-চালিত — সবচেয়ে বেশি প্রায়োরিটির পেন্ডিং IRQ সবসময় আগে চলে, প্রয়োজনে নিম্ন-প্রায়োরিটি ISR-কে প্রি-এম্পট করে (নেস্টেড ইন্টারাপ্ট)। ইন্টারাপ্ট লেটেন্সি — ইভেন্ট থেকে ISR শুরু হওয়া পর্যন্ত সময় — বাস্তবে কখনোই শূন্য নয়, এবং কম প্রায়োরিটির IRQ-এর জন্য এটি অনেক বেশি হতে পারে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ উপরের সিমুলেশনে যদি সব IRQ-এর প্রায়োরিটি সমান হতো, তাহলে সার্ভিসের ক্রম কেমন হতো বলে আপনার মনে হয়?
তাহলে প্রায়োরিটি দিয়ে আর তফাত করা যেত না, তাই কোডের pending.sort(key=lambda r: (r.priority,
r.arrival_tick)) লাইনে দ্বিতীয় ক্রাইটেরিয়া হিসেবে arrival_tick কাজ করত — অর্থাৎ
সমান প্রায়োরিটির মধ্যে যেটা আগে এসেছে সেটাই আগে সার্ভিস পেত (একটি FIFO আচরণে ফিরে যেত)। এটাই দেখায় যে
প্রায়োরিটি না থাকলে (বা সব সমান হলে) FIFO-ই স্বাভাবিক ডিফল্ট আচরণ।
প্র ০২ বাস্তব হার্ডওয়্যারে ইন্টারাপ্ট লেটেন্সি কখনো ঠিক শূন্য হতে পারে না কেন?
কারণ CPU একটি চলমান ইনস্ট্রাকশনের মাঝপথে হুট করে থেমে যেতে পারে না (এতে ডেটা অসামঞ্জস্যপূর্ণ থেকে যেতে পারে) — তাকে অন্তত বর্তমান ইনস্ট্রাকশনটি শেষ করতে হয়, তারপর স্টেট সেভ করতে হয়, তারপর ISR-এর ঠিকানা খুঁজে বের করতে হয়। এই তিনটি ধাপই প্রকৃত সময় নেয় — তাই সূত্র অনুযায়ী $T_{latency}$ কখনো ঠিক শূন্য হতে পারে না, শুধু কমবেশি হতে পারে।
প্র ০৩
উপরের কোডে EXT_BUTTON-এর latency ০ কেন, অথচ ADC_DONE-এর latency ৪?
EXT_BUTTON সবচেয়ে বেশি প্রায়োরিটি (১) নিয়ে tick ৩-এ আসে, ঠিক সেই মুহূর্তে CPU ফাঁকা ছিল
(UART_RX tick ২-তেই শেষ হয়ে গিয়েছিল) — তাই তাৎক্ষণিকভাবে সার্ভিস শুরু হয়, latency ০।
কিন্তু ADC_DONE সবচেয়ে কম প্রায়োরিটি (৪) নিয়ে tick ১-এ আসে, আর এরপর একের পর এক বেশি
প্রায়োরিটির IRQ (TIMER_OVF, EXT_BUTTON, SPI_DONE) তার আগে
সার্ভিস পেয়ে যায় — ফলে এটি tick ৫ পর্যন্ত অপেক্ষা করে, latency = ৫-১ = ৪ টিক।
অনুশীলন
-
চিন্তা করুন: উপরের কোড সেলে
ADC_DONE-এরpriorityযদি ৪-এর বদলে ১ করা হয় (আরEXT_BUTTON-এর সাথে টাই হয়), সম্পন্ন হওয়ার ক্রম কীভাবে বদলাবে বলে মনে করেন?তখন
ADC_DONEওEXT_BUTTONদুটোই সর্বোচ্চ প্রায়োরিটি (১) পাবে, আর সাজানোর দ্বিতীয় ক্রাইটেরিয়াarrival_tickঅনুযায়ীADC_DONE(tick ১)EXT_BUTTON-এর (tick ৩) চেয়ে আগে পেন্ডিং তালিকায় থাকবে — ফলেADC_DONEঅনেক আগে সার্ভিস পাবে, বর্তমান আউটপুটের চেয়ে সম্পূর্ণ ভিন্ন একটি ক্রম তৈরি হবে। -
পরীক্ষা করুন: কোড সেলে একটি ষষ্ঠ IRQ যোগ করুন —
InterruptRequest("WATCHDOG_WARN", priority=1, arrival_tick=6, service_ticks=1)— এবং Run চেপে দেখুন এটি কি অন্য সব চলমান/পেন্ডিং IRQ-এর আগে সার্ভিস পায় কিনা।হ্যাঁ — tick ৬-এ যখন এটি আসবে, তখন এর প্রায়োরিটি (১) ঐ মুহূর্তে চলমান বা পেন্ডিং যেকোনো IRQ-এর চেয়ে বেশি বা সমান হবে, তাই ডিসপ্যাচার এটিকেই পরের টিকে বেছে নেবে (প্রয়োজনে চলমান কিছু প্রি-এম্পট করেও) — বাস্তবেও একটি ওয়াচডগ-সম্পর্কিত ইন্টারাপ্ট সাধারণত সর্বোচ্চ প্রায়োরিটিতেই রাখা হয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ L28 রিয়েল-টাইম সিস্টেম — হার্ড বনাম সফট রিয়েল-টাইম — মিসড ডেডলাইনের প্রকৃত পরিণতি কী হতে পারে তা এই পাঠের ভিত্তির উপর দাঁড়িয়েই আলোচনা করা হবে।
- Computer Architecture & Digital Logic কোর্স সহোদর কোর্স সাধারণ ইন্টারাপ্ট হ্যান্ডলিং মেকানিজম ও ইন্টারাপ্ট ভেক্টর টেবিলের গভীর আলোচনা সেই কোর্সেই আছে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ RTOS টাস্ক শিডিউলিং, সেমাফোর/মিউটেক্স, প্রায়োরিটি ইনভার্সন — বাকি M7 মডিউল আসছে এরপরই।