RisalDash RisalDash

دليل · المقاطعات · التوقيت

تخلَّ عن delay() — المقاطعات وmillis() على Arduino وESP32

أول ما يتعلّمه الجميع هو delay(). وثاني ما يصطدم به الجميع هو جداره: أثناء تنفيذ delay(1000) لا تفعل اللوحة شيئًا — لا تقرأ زرًّا ولا تحدّث شاشة ولا تُبقي Wi-Fi حيًّا. إليك المخرج في خطوتين: توقيت غير حاجب بـ millis()، والتقاط الأحداث لحظة وقوعها بـ المقاطعات.

المقاطعاتdelay vs millisإزالة الارتدادESP32Arduino

الخطوة 1 — millis() بدل delay()

معظم لحظات «أحتاج تأخيرًا» تعني في الحقيقة «افعل هذا كل فترة، لكن أبقِ كل شيء آخر يعمل». لهذا وُجد millis() — يعيد عدد الملّي ثانية منذ الإقلاع، فتتحقّق من مقدار الوقت المنقضي بدل التجميد:

unsigned long prev = 0;
const unsigned long period = 1000;   // ms

void loop() {
  unsigned long now = millis();
  if (now - prev >= period) {
    prev = now;
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));  // blink
  }
  // ...everything else keeps running here, no freeze
}

الحلقة لا تتوقف أبدًا: تنظر إلى الساعة في كل دورة فقط. هذا النمط الواحد يحلّ محلّ 90% من استدعاءات delay()، وهو ما يتيح للوحة أن تُومض ليدًا وتقرأ مستشعرًا وتخدم صفحة في آنٍ واحد. لكن للاستقصاء فجوة — بين تحقّقين قد تفوتك حادثة سريعة. هنا تأتي المقاطعات.

الخطوة 2 — المقاطعات: لا تستقصِ، بل تلقَّ إشعارًا

المقاطعة هي «نقرة على الكتف» عتادية: عندما يتغيّر طرف، يترك المعالج ما بيده ويشغّل دالة صغيرة (ISR — روتين خدمة المقاطعة) ثم يعود تمامًا حيث توقّف. تلجأ إليها حين لا يجوز أن تفوتك حادثة ولا تقدر على استقصائها:

التوصيل: زرّ يُسحب إلى الأرض

أنظف زرّ لا يحتاج قطعًا خارجية: رِجْل إلى GPIO والأخرى إلى GND، وتُفعّل مقاومة الرفع الداخلية للرقاقة بـ INPUT_PULLUP. يبقى الطرف HIGH في السكون ويُقرأ LOW عند الضغط — لذا التحفيز على FALLING.

تصدير من BoardLab: زرّ موصّل بين طرف GPIO في ESP32 (طرف مقاطعة) وGND باستخدام مقاومة الرفع الداخلية
زرّ بين GPIO وGND — INPUT_PULLUP يعني «دون مقاومة خارجية». مبنيّ في BoardLab.

على ESP32 يصلح أيّ GPIO تقريبًا كمقاطعة؛ أما على Uno الكلاسيكي فـ D2 وD3 فقط. لُفّ الطرف دائمًا بـ digitalPinToInterrupt() — فهو يربط الطرف بخطّ المقاطعة الصحيح. ومَكمَن على ESP32: الأطراف المخصّصة للدخل فقط GPIO34–39 لا تملك مقاومات رفع داخلية، فالزرّ هناك يحتاج فعلًا مقاومة خارجية ~10 كΩ إلى 3V3.

الخطوة 3 — إزالة الارتداد، وإلا حُسبت ضغطة واحدة خمسًا

لا يصنع الزرّ الميكانيكي حافّة نظيفة واحدة — بل ترتدّ التلامسات، متذبذبة بين HIGH/LOW لبضع ملّي ثانية. دون معالجة يُطلَق ISR عدة مرات للضغطة الواحدة. الحل: تجاهُل أيّ حافّة تصل بعد سابقتها بسرعة مفرطة:

const byte BUTTON = 19;               // your interrupt pin
volatile uint32_t presses = 0;
volatile uint32_t lastUs  = 0;

void IRAM_ATTR onPress() {             // ESP32: the ISR must live in IRAM
  uint32_t now = micros();
  if (now - lastUs > 25000) {          // ignore bounces within 25 ms
    presses++;
    lastUs = now;
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON, INPUT_PULLUP);       // button to GND, no external resistor
  attachInterrupt(digitalPinToInterrupt(BUTTON), onPress, FALLING);
}

void loop() {
  static uint32_t shown = 0;
  if (presses != shown) {              // do the slow work OUT here, not in the ISR
    shown = presses;
    Serial.printf("presses: %u\n", shown);
  }
}

حارس micros() يبتلع الارتدادات ضمن 25 ملّي ثانية مع بقائه مستجيبًا للضغطات الحقيقية. تفضّل الحلّ العتادي؟ مكثّف 100 نانوفاراد عبر الزر (أو مرشّح RC) يُنعّم الحافّة — لكن الحارس البرمجي مجاني وكافٍ لكل شيء تقريبًا.

قواعد ISR التي تعضّ فعلًا

معًا: millis() يقتل delay() اليومي، والمقاطعات تلتقط الأحداث التي لا يجوز أن تفوتك. وحين يصير كلاهما ذاكرة عضلية، تكفّ اللوحة عن التلعثم وتبدأ بفعل عدة أشياء دفعة واحدة — وعلى ESP32 يمكنك المضيّ أبعد ووضع العمل على النواة الثانية.

توصّل زرًّا أو مستشعرًا بالطرف المناسب؟ تحقّق أولًا أنه متاح وقادر على المقاطعة.