
وقفه سختافزاری در ESP32 چیست؟ آموزش Interrupt و خواندن ورودیهای سریع
تصور کنید با ESP32 در حال ساخت یک شمارنده پالس، انکودر چرخشی، دورسنج موتور یا سیستم اندازهگیری سرعت هستید. در این پروژهها ممکن است سیگنال ورودی با سرعت زیادی تغییر کند؛ در حالی که برنامه اصلی شما همزمان مشغول خواندن سنسورها، ارتباط Wi-Fi یا پردازش دادههاست.
اگر برنامه برای تشخیص هر تغییر وضعیت ورودی مجبور باشد دائماً پایه GPIO را بررسی کند، ممکن است بعضی از تغییرات سریع را از دست بدهد. یکی از راهکارهای مناسب برای چنین شرایطی، استفاده از وقفه سختافزاری (Hardware Interrupt) است.
وقفه به میکروکنترلر اجازه میدهد در صورت وقوع یک رویداد مشخص، اجرای عادی برنامه را موقتاً متوقف کند و یک تابع مخصوص را اجرا کند. پس از پایان این تابع، برنامه میتواند به مسیر اجرای قبلی خود بازگردد.
در این مقاله با نحوه استفاده از وقفههای GPIO در ESP32 و Arduino IDE آشنا میشویم، یک شمارنده پالس میسازیم و محدودیتهایی را بررسی میکنیم که در پروژههای واقعی اهمیت دارند.
وقفه سختافزاری چیست؟
در برنامهنویسی میکروکنترلر، معمولاً برنامه اصلی در یک حلقه تکرارشونده اجرا میشود. در Arduino این حلقه با تابع loop() شناخته میشود.
برای مثال، اگر بخواهیم وضعیت یک ورودی دیجیتال را بررسی کنیم، میتوانیم از روش زیر استفاده کنیم:
|
1 2 3 4 5 |
void loop() { if (digitalRead(27) == HIGH) { // انجام عملیات موردنظر } } |
در این روش، برنامه مرتباً وضعیت GPIO شماره 27 را بررسی میکند. این روش Polling نام دارد. اما اگر وضعیت ورودی در فاصله زمانی کوتاهی تغییر کند و برنامه در همان لحظه مشغول اجرای بخش دیگری باشد، ممکن است تغییر وضعیت دیده نشود. در روش وقفه، بهجای بررسی مداوم ورودی، یک تابع به رویداد مشخصی متصل میشود.
برای نمونه، میتوانیم به ESP32 بگوییم:
«هر زمان GPIO شماره 27 از LOW به HIGH تغییر کرد، تابع مشخصی را اجرا کن.» این تابع که با نام ISR یا Interrupt Service Routine شناخته میشود، وظیفه رسیدگی به رویداد وقفه را بر عهده دارد.
تفاوت Interrupt و Polling
| ویژگی | Polling | Interrupt |
|---|---|---|
| روش تشخیص رویداد | بررسی مداوم ورودی | واکنش به رویداد مشخص |
| درگیری برنامه اصلی | ممکن است زیاد باشد | معمولاً کمتر است |
| تشخیص تغییرات سریع | وابسته به سرعت حلقه و زمان بررسی | مناسبتر برای رویدادهای سریع |
| پیچیدگی برنامه | معمولاً کمتر | نیازمند رعایت محدودیتهای ISR |
| کاربرد نمونه | کلیدهای ساده و ورودیهای کمسرعت | انکودر، شمارنده پالس و رویدادهای زمانحساس |
البته وقفه همیشه انتخاب بهتری نیست. برای برخی ورودیها، Polling سادهتر و قابل پیشبینیتر است. انتخاب روش مناسب به سرعت ورودی، میزان پردازش و نیازهای پروژه بستگی دارد.
چه زمانی از وقفه در ESP32 استفاده کنیم؟
وقفه برای پروژههایی مناسب است که در آنها وقوع یک رویداد اهمیت دارد و نمیخواهیم برنامه اصلی دائماً در انتظار آن بماند.
چند کاربرد متداول عبارتاند از:
-
شمارش پالس خروجی سنسورهای سرعت
-
دریافت تغییر وضعیت انکودرهای چرخشی
-
تشخیص لبه سیگنالهای دیجیتال
-
تشخیص رویدادهای خارجی
-
ثبت زمان وقوع یک پالس
-
تشخیص فشردن کلید در برخی کاربردها
برای مثال، در یک دورسنج موتور ممکن است سنسور در هر دور چند پالس تولید کند. با شمارش تعداد پالسها در یک بازه زمانی مشخص، میتوان سرعت چرخش موتور را محاسبه کرد.
اما باید توجه داشت که برای اندازهگیری دقیق فرکانسهای بالا، تنها استفاده از وقفه GPIO کافی نیست. در برخی کاربردها بهتر است از واحدهای سختافزاری مخصوص شمارش یا اندازهگیری پالس استفاده شود.
حالتهای مختلف وقفه GPIO در ESP32
در Arduino-ESP32 تابع attachInterrupt() برای اتصال یک تابع وقفه به پایه GPIO استفاده میشود.
ساختار کلی این تابع به شکل زیر است:
|
1 |
attachInterrupt(pin, ISR, mode); |
پارامترهای آن عبارتاند از:
-
pin: شماره پایه GPIO -
ISR: تابعی که هنگام وقوع وقفه اجرا میشود -
mode: نوع رویدادی که باعث فعال شدن وقفه خواهد شد
در مستندات Arduino-ESP32، حالتهای مختلفی برای وقفه GPIO معرفی شدهاند.
| حالت | عملکرد |
|---|---|
RISING |
تشخیص تغییر از LOW به HIGH |
FALLING |
تشخیص تغییر از HIGH به LOW |
CHANGE |
تشخیص هر دو لبه |
ONLOW |
فعال بودن وقفه هنگام LOW بودن ورودی |
ONHIGH |
فعال بودن وقفه هنگام HIGH بودن ورودی |
برای شروع، معمولاً سه حالت RISING، FALLING و CHANGE کاربرد بیشتری دارند.
تفاوت RISING، FALLING و CHANGE
فرض کنید ورودی GPIO یک سیگنال مربعی دریافت میکند. اگر فقط بخواهیم تعداد پالسهای مثبت را بشماریم، استفاده از RISING مناسب است؛ زیرا هر پالس تنها یک بار در لبه بالارونده شمارش میشود.
اگر بخواهیم هنگام پایان پالس یا پایین آمدن سیگنال واکنش نشان دهیم، از FALLING استفاده میکنیم. حالت CHANGE هر دو لبه را تشخیص میدهد. بنابراین اگر برای شمارش پالس از این حالت استفاده کنیم، ممکن است هر پالس دو بار شمرده شود؛ یک بار هنگام بالا رفتن و بار دیگر هنگام پایین آمدن. این موضوع در اندازهگیری سرعت و فرکانس اهمیت دارد.
راهاندازی وقفه در ESP32 با Arduino IDE
برای اجرای مثالهای این مقاله به موارد زیر نیاز داریم:
-
یک برد ESP32
-
Arduino IDE
-
نصب بودن بسته بردهای Arduino-ESP32
-
یک منبع سیگنال دیجیتال یا مدار تولید پالس
-
سیم اتصال به GPIO
در مثالهای این مقاله، GPIO شماره 27 بهعنوان ورودی انتخاب شده است. این شماره صرفاً برای نمونه است و باید با توجه به مدل دقیق برد و محدودیتهای پایههای آن انتخاب شود.
مثال اول: تشخیص فشردن کلید با وقفه
در این مثال، یک کلید را بین GPIO شماره 27 و GND قرار میدهیم. با استفاده از مقاومت Pull-up داخلی، وضعیت عادی ورودی HIGH خواهد بود و با فشردن کلید به LOW تغییر میکند.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#define BUTTON_PIN 27 volatile bool buttonPressed = false; void IRAM_ATTR buttonISR() { buttonPressed = true; } void setup() { Serial.begin(115200); pinMode(BUTTON_PIN, INPUT_PULLUP); attachInterrupt( digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING ); } void loop() { if (buttonPressed) { buttonPressed = false; Serial.println("Button Pressed"); } } |
در این برنامه، تابع buttonISR() هنگام فشردن کلید اجرا میشود. نکته مهم این است که داخل تابع وقفه فقط مقدار یک متغیر تغییر میکند. عملیات چاپ در سریال مانیتور در حلقه اصلی انجام میشود. این روش باعث میشود ISR کوتاه باقی بماند و عملیات زمانبر در خارج از آن اجرا شود.
توجه: این مثال برای آموزش ساختار وقفه است. کلید مکانیکی ممکن است هنگام فشردن یا رها شدن چند تغییر وضعیت سریع ایجاد کند. بنابراین در یک پروژه واقعی باید برای حذف پرش کنتاکت (Debouncing) نیز راهکار مناسبی در نظر گرفت.
مثال دوم: شمارش پالس ورودی با ESP32
یکی از کاربردهای مهم وقفه، شمارش پالسهایی است که از سنسور یا مدار خارجی دریافت میشوند. فرض کنید یک سنسور دیجیتال به GPIO شماره 27 متصل است و در هر بار تشخیص یک رویداد، یک پالس مثبت تولید میکند. میخواهیم تعداد پالسهای دریافتشده را بشماریم و هر یک ثانیه یک بار نتیجه را در Serial Monitor نمایش دهیم.
در این مثال، برای محافظت از متغیر مشترک میان ISR و برنامه اصلی، از Critical Section استفاده میکنیم.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#define PULSE_PIN 27 volatile uint32_t pulseCount = 0; portMUX_TYPE pulseMux = portMUX_INITIALIZER_UNLOCKED; void IRAM_ATTR pulseISR() { portENTER_CRITICAL_ISR(&pulseMux); pulseCount++; portEXIT_CRITICAL_ISR(&pulseMux); } void setup() { Serial.begin(115200); pinMode(PULSE_PIN, INPUT); attachInterrupt( digitalPinToInterrupt(PULSE_PIN), pulseISR, RISING ); } void loop() { static uint32_t lastPrintTime = 0; if (millis() - lastPrintTime >= 1000) { lastPrintTime = millis(); uint32_t countSnapshot; portENTER_CRITICAL(&pulseMux); countSnapshot = pulseCount; pulseCount = 0; portEXIT_CRITICAL(&pulseMux); Serial.print("Pulses per second: "); Serial.println(countSnapshot); } } |
بررسی عملکرد برنامه
در این برنامه، چند نکته مهم وجود دارد.
۱. متغیر شمارنده
متغیر pulseCount تعداد پالسهای دریافتشده را نگه میدارد. از آنجا که این متغیر هم در ISR و هم در برنامه اصلی استفاده میشود، با volatile تعریف شده است.
۲. تابع ISR تابع pulseISR() با هر لبه بالارونده ورودی اجرا میشود و مقدار شمارنده را افزایش میدهد.
۳. Critical Section برای جلوگیری از دسترسی همزمان ناسازگار به متغیر شمارنده، در این مثال از portENTER_CRITICAL_ISR() و portEXIT_CRITICAL_ISR() در ISR استفاده شده است. در برنامه اصلی نیز از نسخه معمولی این توابع استفاده میکنیم.
۴. محاسبه تعداد پالسها در هر ثانیه در حلقه اصلی، با استفاده از millis() یک بازه زمانی تقریباً یکثانیهای ایجاد میکنیم. پس از پایان بازه، مقدار شمارنده خوانده میشود و برای بازه بعدی صفر میشود. در نتیجه، مقدار چاپشده تقریباً تعداد پالسهای دریافتشده در هر ثانیه را نشان میدهد.
این روش برای آموزش شمارش پالس مناسب است، اما برای اندازهگیری دقیق فرکانس یا سرعت در کاربردهای حساس، باید خطای زمانبندی حلقه اصلی و مدت اجرای برنامه نیز در نظر گرفته شود.
چرا نباید داخل ISR از Serial.print() استفاده کنیم؟
یکی از اشتباهات رایج در برنامهنویسی وقفه این است که تمام عملیات موردنیاز را مستقیماً داخل تابع ISR انجام دهیم.
برای مثال، کد زیر انتخاب مناسبی نیست:
|
1 2 3 |
void IRAM_ATTR pulseISR() { Serial.println("Pulse detected"); } |
توابع مربوط به ارتباط سریال ممکن است زمانبر باشند. همچنین در بعضی شرایط، استفاده از این توابع در ISR میتواند باعث بروز مشکلاتی مانند از دست رفتن داده، تأخیر در رسیدگی به رویدادهای دیگر یا رفتار غیرقابل پیشبینی شود. در ESP32، محدودیتهای ISR بهویژه هنگام عملیات حافظه فلش اهمیت دارند. برخی وقفهها ممکن است هنگام غیرفعال بودن Cache به تأخیر بیفتند؛ مگر اینکه الزامات اجرای IRAM-safe رعایت شده باشند.
بهتر است در تابع وقفه از انجام موارد زیر خودداری کنیم:
-
استفاده از
Serial.print()وSerial.println() -
اجرای حلقههای طولانی
-
استفاده از
delay() -
انجام محاسبات سنگین
-
فراخوانی توابعی که ممکن است مسدود شوند
-
تخصیص حافظه پویا
-
اجرای عملیات ارتباطی مانند ارسال Wi-Fi یا درخواست HTTP
بهجای این کارها، بهتر است ISR فقط یک رویداد را ثبت کند؛ مثلاً یک شمارنده را افزایش دهد یا یک Flag را فعال کند. سپس برنامه اصلی یا یک Task در FreeRTOS، پردازش اصلی را انجام دهد.
مدیریت متغیرهای مشترک بین ISR و برنامه اصلی
یکی از نکات مهم در برنامهنویسی وقفه، مدیریت صحیح متغیرهایی است که هم در تابع وقفه و هم در برنامه اصلی استفاده میشوند.
کاربرد volatile
فرض کنید متغیری به شکل زیر تعریف شده است:
|
1 |
volatile bool eventDetected = false; |
کلمه کلیدی volatile به کامپایلر میگوید که مقدار این متغیر ممکن است خارج از جریان معمول اجرای برنامه تغییر کند. بنابراین کامپایلر نباید فرض کند که مقدار متغیر همیشه ثابت باقی میماند.
اما یک نکته مهم وجود دارد:
volatile بهتنهایی دسترسی اتمیک یا ایمن به متغیر را تضمین نمیکند. برای مثال، اگر ISR یک متغیر شمارنده را تغییر دهد و برنامه اصلی همزمان آن را بخواند یا صفر کند، ممکن است بین این عملیات تداخل ایجاد شود.
برای متغیرهای ساده، روش مناسب به نوع داده و معماری بستگی دارد. در ESP32، برای متغیرهای مشترک حساستر میتوان از Critical Section، Mutex در Taskها یا سازوکارهای مناسب FreeRTOS استفاده کرد.
همچنین باید توجه داشت که توابعی مانند Mutex معمولی برای استفاده مستقیم داخل ISR مناسب نیستند. در چنین شرایطی باید از API مخصوص ISR استفاده شود.
محدودیتهای وقفه در ESP32 و مفهوم IRAM
در ESP32، برنامه معمولاً از حافظه فلش اجرا میشود و Cache برای دسترسی سریعتر به کد و دادهها مورد استفاده قرار میگیرد. هنگام برخی عملیات فلش، مانند پاک کردن یا نوشتن اطلاعات، Cache ممکن است غیرفعال شود.
اگر تابع وقفه یا دادههایی که به آن نیاز دارد در حافظهای قرار گرفته باشند که در آن لحظه قابل دسترسی نیست، اجرای وقفه ممکن است به تأخیر بیفتد یا با مشکل مواجه شود. برای کاربردهای حساس به زمان، Espressif سازوکار IRAM-safe را ارائه میکند. در این حالت، کد موردنیاز ISR باید در IRAM و دادههای موردنیاز آن در حافظه مناسب داخلی مانند DRAM قرار داشته باشند. همچنین توابعی که ISR فراخوانی میکند نیز باید شرایط لازم را رعایت کنند.
آیا استفاده از IRAM_ATTR کافی است؟
خیر. در مثالهای ساده Arduino-ESP32 ممکن است از IRAM_ATTR برای قرار دادن تابع در IRAM استفاده کنیم:
|
1 2 3 |
void IRAM_ATTR pulseISR() { pulseCount++; } |
اما این کار بهتنهایی تضمین نمیکند که تمام کدها و دادههای مورد استفاده ISR در حافظه مناسب قرار گرفته باشند. برای مثال، اگر تابع وقفه یک تابع دیگر را فراخوانی کند که در Flash قرار دارد، همچنان ممکن است مشکل ایجاد شود. بنابراین برای کاربردهای زمانحساس، باید محل قرارگیری تمام کدها و دادههای موردنیاز ISR بررسی شود.
برای پروژههای معمولی Arduino، بهتر است ابتدا ISR را کوتاه و ساده نگه داریم و تنها در صورت نیاز واقعی به تضمین تأخیر، سراغ بررسی تخصصی IRAM-safe برویم.
عیبیابی وقفههای GPIO در ESP32
اگر برنامه شما با وجود استفاده از وقفه، پالسها را بهدرستی ثبت نمیکند، موارد زیر را بررسی کنید.
۱. انتخاب نادرست پایه GPIO
تمام پایههای ESP32 شرایط یکسانی ندارند. بعضی پایهها محدودیتهایی در کاربرد دارند و برخی در زمان راهاندازی یا عملکردهای دیگر برد استفاده میشوند.
بنابراین پیش از انتخاب پایه ورودی، باید مشخصات مدل دقیق ESP32 را بررسی کنید. برای مثال، GPIO شماره 27 در بسیاری از بردهای مبتنی بر ESP32 کلاسیک گزینه مناسبی برای ورودی دیجیتال است؛ اما این انتخاب را نمیتوان بدون بررسی به تمام اعضای خانواده ESP32 تعمیم داد.
۲. انتخاب اشتباه حالت وقفه
اگر حالت وقفه را FALLING انتخاب کرده باشید، تغییر LOW به HIGH باعث اجرای ISR نمیشود. برای شمارش لبههای بالارونده، از RISING استفاده کنید. برای شمارش هر دو لبه، CHANGE مناسب است؛ اما باید در محاسبات خود تعداد لبههای هر پالس را در نظر بگیرید.
۳. نویز و پرش کنتاکت
در ورودیهایی مانند کلید مکانیکی، ممکن است هنگام تغییر وضعیت چندین لبه ناخواسته ایجاد شود. در این شرایط، شمارنده ممکن است بیش از یک بار افزایش پیدا کند.
راهکارهای کاهش این مشکل شامل موارد زیر هستند:
-
استفاده از Pull-up یا Pull-down مناسب
-
طراحی صحیح مسیر ورودی
-
استفاده از فیلتر سختافزاری در صورت نیاز
-
پیادهسازی Debouncing نرمافزاری
-
استفاده از فیلترهای زمانی متناسب با نوع سیگنال
نکته مهم این است که فیلتر نرمافزاری باید با مشخصات واقعی سیگنال سازگار باشد. اگر مدت فیلتر بیش از حد طولانی انتخاب شود، ممکن است پالسهای معتبر نیز حذف شوند.
۴. فرکانس بالای ورودی
وقفه GPIO برای بسیاری از کاربردها مناسب است؛ اما اگر فرکانس ورودی بسیار بالا باشد، ممکن است محدودیتهایی مانند مدت اجرای ISR، تأخیر سیستم و تعداد رویدادهای همزمان باعث از دست رفتن پالسها شوند. برای چنین کاربردهایی، بهتر است امکانات سختافزاری ESP32 نیز بررسی شوند.
بر اساس مدل دقیق تراشه و نیاز پروژه، امکاناتی مانند PCNT برای شمارش پالس یا RMT برای دریافت و اندازهگیری سیگنالهای دیجیتال میتوانند گزینههای مناسبتری باشند. البته انتخاب این واحدها به نوع سیگنال، فرکانس و دقت موردنیاز وابسته است.
۵. تداخل با Wi-Fi و عملیات فلش
اگر پروژه شما از Wi-Fi استفاده میکند، باید توجه داشته باشید که عملیات فلش و محدودیتهای Cache میتوانند بر زمانبندی برخی وقفهها اثر بگذارند.
در کاربردهایی که از دست رفتن حتی یک رویداد قابل قبول نیست، بررسی IRAM-safe و انتخاب روش مناسب دریافت سیگنال اهمیت بیشتری پیدا میکند.
جمعبندی
وقفه سختافزاری یکی از ابزارهای مهم در برنامهنویسی ESP32 است که امکان واکنش به رویدادهای GPIO را بدون نیاز به بررسی مداوم ورودی فراهم میکند.
با استفاده از attachInterrupt() میتوان یک تابع را به رویدادهایی مانند لبه بالارونده، لبه پایینرونده یا تغییر وضعیت ورودی متصل کرد.
برای استفاده صحیح از وقفه، چند نکته اساسی را باید در نظر داشت:
-
ISR باید کوتاه و سریع باشد.
-
عملیات زمانبر باید به برنامه اصلی یا Task منتقل شوند.
-
متغیرهای مشترک باید بهدرستی مدیریت شوند.
-
volatileبهتنهایی برای مدیریت همزمانی کافی نیست. -
نویز و پرش کنتاکت باید متناسب با کاربرد فیلتر شوند.
-
برای فرکانسهای بالا، امکانات سختافزاری شمارش و اندازهگیری سیگنال نیز باید بررسی شوند.
اگر قصد دارید از وقفه برای شمارش پالس یا اندازهگیری سرعت استفاده کنید، پیشنهاد میشود ابتدا یک مدار ساده با ورودی دیجیتال و یک مولد پالس بسازید و عملکرد شمارنده را در فرکانسهای مختلف بررسی کنید. سپس در صورت نیاز، روش اندازهگیری را به واحدهای سختافزاری مناسبتر ارتقا دهید.



