آموزش تنظیم وقفه Interrupts در برد ESP32

در بسیاری از پروژهها، نیاز است که ESP32 در حین اجرای برنامه اصلی خود، بهصورت مداوم رخدادهای خاصی را نیز پایش کند. یکی از روشهای مؤثر و پراستفاده برای این منظور، استفاده از وقفهها (Interrupts) است.
انواع وقفه در ESP32
ESP32 به ازای هر هسته پردازشی، حداکثر 32 اسلات وقفه در اختیار قرار میدهد. هر وقفه دارای یک سطح اولویت مشخص بوده و در دو دسته اصلی تقسیمبندی میشود:
- وقفه سختافزاری (Hardware Interrupt): در پاسخ به یک رویداد خارجی رخ میدهد.
مثال: وقفه پایه GPIO هنگام فشرده شدن کلید، یا وقفه لمسی (Touch) هنگام تماس سطح لمسی. - وقفه نرمافزاری (Software Interrupt): در پاسخ به یک دستور یا رویداد داخلی نرمافزاری رخ میدهد.
مثال: وقفه تایمر یا وقفه تایمر واچداگ هنگام پایان زمان.وقفه GPIO در ESP32
در ESP32 میتوان یک تابع سرویسدهی وقفه (Interrupt Service Routine یا ISR) تعریف کرد تا در زمان تغییر سطح منطقی پین GPIO فراخوانی شود.
تمامی پایههای GPIO در ESP32 قابلیت پیکربندی برای دریافت وقفه را دارند.
اتصال وقفه به پایه GPIO
در محیط Arduino IDE، برای تنظیم وقفه بر روی یک پایه خاص، از تابع attachInterrupt() استفاده میشود:
attachInterrupt(GPIOPin, ISR, Mode);
ورودیهای تابع:
GPIOPin: شماره پایه GPIO که باید برای وقفه پایش شود.ISR: نام تابعی که هنگام وقوع وقفه اجرا خواهد شد.Mode: شرایطی که در آن وقفه باید فعال شود.
مقادیر مجاز برای Mode:
| مقدار | توضیح |
|---|---|
LOW | فعال شدن وقفه هنگام پایین بودن سطح پین |
HIGH | فعال شدن هنگام بالا بودن سطح پین |
CHANGE | فعال شدن هنگام تغییر سطح پین از بالا به پایین یا بالعکس |
FALLING | فعال شدن هنگام تغییر از HIGH به LOW |
RISING | فعال شدن هنگام تغییر از LOW به HIGH |
حذف وقفه از یک پین
در صورتی که دیگر نیازی به پایش پایه نداشته باشید، میتوانید با تابع detachInterrupt() وقفه را غیرفعال کنید:
detachInterrupt(GPIOPin);
تابع سرویسدهی وقفه (ISR)
تابع ISR، همان تابعی است که در هنگام وقوع وقفه اجرا میشود. ساختار آن به شکل زیر است:
void IRAM_ATTR ISR() {
// کد مربوط به پاسخ به وقفه
}
نکات مهم در تعریف ISR در ESP32:
- تابع نباید هیچ ورودی یا خروجی داشته باشد.
- باید تا حد امکان کوتاه و سریع نوشته شود؛ چرا که در زمان اجرای آن، عملکرد عادی برنامه متوقف میشود.
- باید دارای صفت
IRAM_ATTRباشد تا کد آن در حافظه RAM داخلی ESP32 ذخیره شود.
IRAM_ATTR چیست؟
هنگامی که قطعهای از کد را با IRAM_ATTR علامتگذاری میکنیم، کامپایلر آن را در حافظه RAM داخلی (IRAM) ذخیره میکند. در غیر این صورت، کد در حافظه Flash ذخیره میشود که بسیار کندتر از RAM است.
از آنجا که وقفهها باید در کوتاهترین زمان ممکن پاسخ داده شوند، بارگذاری آنها از حافظه Flash میتواند باعث تاخیر و اختلال عملکرد شود. به همین دلیل، طبق مستندات رسمی ESP32، تابع ISR باید در IRAM قرار گیرد.
پروژه نمونه وقفه در ESP32
بحث تئوری کافی است! بیایید به یک مثال عملی نگاه کنیم.
بیایید یک دکمه فشاری را به GPIO#18 (D18) برد ESP32 وصل کنیم. برای این پین نیازی به هیچ pullup ندارید زیرا پین را به صورت داخلی pull up خواهیم کرد.

کد نمونه: وقفه ساده با ESP32
کد زیر استفاده از وقفهها و روش صحیح نوشتن یک روال سرویس وقفه را نشان میدهد.
این برنامه GPIO#18 (D18) را برای لبه FALLING زیر نظر میگیرد. به عبارت دیگر، به دنبال تغییر ولتاژ از منطق HIGH به منطق LOW است که هنگام فشار دادن دکمه رخ میدهد. وقتی این اتفاق میافتد، تابع isr فراخوانی میشود. کد درون این تابع تعداد دفعات فشار دادن دکمه را میشمارد.
struct Button {
const uint8_t PIN;
uint32_t numberKeyPresses;
bool pressed;
};
Button button1 = {18, 0, false};
void IRAM_ATTR isr() {
button1.numberKeyPresses++;
button1.pressed = true;
}
void setup() {
Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);
}
void loop() {
if (button1.pressed) {
Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
}
پس از آپلود کد، دکمه EN را روی ESP32 فشار دهید و مانیتور سریال را با نرخ انتقال داده 115200 باز کنید. با فشار دادن دکمه، خروجی زیر را دریافت خواهید کرد.

در ابتدای کد، ساختاری به نام Button ایجاد میکنیم. این ساختار سه عضو دارد:
- شماره پین
- تعداد فشرده شدن کلید
- وضعیت فشرده شده
یک ساختار struct مجموعهای از متغیرهایی با انواع مختلف (اما از نظر منطقی مرتبط با یکدیگر) تحت یک نام واحد است.
struct Button {
const uint8_t PIN;
uint32_t numberKeyPresses;
bool pressed;
};
سپس یک نمونه از ساختار Button ایجاد میکنیم و شماره پین را 18، تعداد فشرده شدن کلید را 0 و وضعیت پیشفرض فشرده شده را false مقداردهی اولیه میکنیم.
Button button1 = {18, 0, false};
کد زیر یک روال سرویس وقفه است. همانطور که قبلاً ذکر شد، ISR در ESP32 باید دارای ویژگی IRAM_ATTR باشد.
در ISR، ما به سادگی شمارنده KeyPresses را 1 واحد افزایش میدهیم و حالت فشرده شدن دکمه را روی True تنظیم میکنیم.
void IRAM_ATTR isr() {
button1.numberKeyPresses += 1;
button1.pressed = true;
}
در بخش Setup، ابتدا ارتباط سریال با کامپیوتر را مقداردهی اولیه میکنیم و سپس pullup داخلی را برای پین GPIO D18 فعال میکنیم.
در مرحله بعد، به ESP32 میگوییم که پین D18 را رصد کند و وقتی پین از HIGH به LOW یعنی لبه FALLING میرود، روال سرویس وقفه isr را فراخوانی کند.
Serial.begin(115200); pinMode(button1.PIN, INPUT_PULLUP); attachInterrupt(button1.PIN, isr, FALLING);
در بخش Loop کد، ما به سادگی بررسی میکنیم که آیا دکمه فشرده شده است یا خیر و سپس تعداد دفعاتی که کلید تاکنون فشرده شده است را چاپ میکنیم و حالت فشرده شدن دکمه را روی false تنظیم میکنیم تا بتوانیم به دریافت وقفهها ادامه دهیم.
if (button1.pressed) {
Serial.printf("Button 1 has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
دیبانس دکمه
یک مشکل رایج در مورد وقفهها این است که اغلب برای یک رویداد چندین بار فعال میشوند. اگر به خروجی سریال مثال بالا نگاه کنید، متوجه خواهید شد که حتی اگر فقط یک بار دکمه را فشار دهید، شمارنده چندین بار افزایش مییابد.

برای فهمیدن دلیل این اتفاق، باید به سیگنال نگاهی بیندازید. اگر هنگام فشار دادن دکمه، ولتاژ پین روی تحلیلگر سیگنال را کنترل کنید، سیگنالی مانند این دریافت خواهید کرد:

ممکن است احساس کنید که تماس بلافاصله برقرار میشود، اما در واقع قطعات مکانیکی درون دکمه چندین بار قبل از اینکه در یک حالت خاص قرار بگیرند، با هم تماس پیدا میکنند. این باعث میشود چندین وقفه فعال شوند.
این یک پدیده کاملاً مکانیکی است که به عنوان “بانس سوئیچ” شناخته میشود، مانند انداختن یک توپ – قبل از اینکه در نهایت روی زمین فرود بیاید، چندین بار بالا و پایین میرود.
زمان تثبیت سیگنال بسیار سریع است و برای ما تقریباً آنی به نظر میرسد، اما برای ESP32 این یک دوره زمانی طولانی است. میتواند چندین دستورالعمل را در آن دوره زمانی اجرا کند.
فرآیند حذف پرش سوئیچ “دیبانس” نامیده میشود. دو راه برای دستیابی به این هدف وجود دارد.
- از طریق سختافزار: با اضافه کردن یک فیلتر RC مناسب برای هموار کردن انتقال.
- از طریق نرمافزار: با نادیده گرفتن موقت وقفههای بیشتر برای مدت کوتاهی پس از فعال شدن اولین وقفه.
کد مثال: دیبانس دکمه وقفه ESP32
در اینجا کد بالا بازنویسی شده است تا نحوه دیبانس یک وقفه به صورت برنامهنویسی شده را نشان دهد. در این کد، ما اجازه میدهیم که ISR فقط یک بار با هر بار فشار دادن دکمه اجرا شود، به جای اینکه چندین بار اجرا شود.
struct Button {
const uint8_t PIN;
uint32_t numberKeyPresses;
bool pressed;
};
Button button1 = {18, 0, false};
//variables to keep track of the timing of recent interrupts
unsigned long button_time = 0;
unsigned long last_button_time = 0;
void IRAM_ATTR isr() {
button_time = millis();
if (button_time - last_button_time > 250)
{
button1.numberKeyPresses++;
button1.pressed = true;
last_button_time = button_time;
}
}
void setup() {
Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);
}
void loop() {
if (button1.pressed) {
Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
}
بیایید دوباره به خروجی سریال هنگام فشردن دکمه نگاه کنیم. توجه داشته باشید که ISR فقط یک بار برای هر بار فشردن دکمه فراخوانی میشود.

این راه حل به این دلیل کار میکند که هر بار که ISR اجرا میشود، زمان فعلی برگردانده شده توسط تابع millis() را با آخرین زمانی که ISR فراخوانی شده است مقایسه میکند.
اگر در محدوده 250 میلیثانیه باشد، ESP32 وقفه را نادیده میگیرد و بلافاصله به کاری که انجام میداد برمیگردد. در غیر این صورت، کد داخل عبارت if را اجرا میکند و شمارنده را افزایش میدهد و متغیر last_button_time را بهروزرسانی میکند، بنابراین تابع مقدار جدیدی برای مقایسه با زمانی که در آینده فعال میشود، دارد.








