RisalDash RisalDash

دليل · FreeRTOS · ثنائي النواة

انقل العمل إلى النواة الثانية في ESP32 عبر FreeRTOS

لدى ESP32 الكلاسيكي نواتان، لكنّ حلقة Arduino loop() لا تعمل إلا على إحداهما. وما إن تقع قراءة مستشعر بطيئة أو تحليل بيانات في loop() حتى تتباطأ صفحة الويب ويتلعثم Wi-Fi. الحل موجود على الرقاقة أصلًا: FreeRTOS. أنشئ مهمّة، وثبّتها على النواة الأخرى — فتعملان جنبًا إلى جنب.

FreeRTOSثنائي النواةxTaskCreateESP32تعدد المهام
مخطّط نواتَي ESP32: حزمة WiFi على النواة 0 ومهمّتك على النواة 1 وبينهما طابور
النواة 0 للراديو؛ والنواة 1 كلّها لك — تحدّثا عبر طابور.

هذه الخطوة التالية بعد millis() والمقاطعات: تلك تمنع حلقة واحدة من الحجب؛ أما FreeRTOS فيتيح تشغيل عدة حلقات في آنٍ واحد — على نوى عتادية حقيقية.

أنت تستخدمه أصلًا

تعمل نواة Arduino لـ ESP32 فوق FreeRTOS. فـ setup()/loop() عندك هي نفسها مهمّة (loopTask) مثبّتة على النواة 1. وحزمة Wi-Fi/Bluetooth تشغّل مهامّها، أغلبها على النواة 0. فـ«إضافة مهمّة» ليست غريبة — أنت فقط تنشئ واحدة أخرى مما يفعله النظام أصلًا.

مهمّتك الخاصة

المهمّة دالّة بحلقة لا نهائية for(;;) لا تعود أبدًا. تُنشئها مرة، ويشغّلها المجدوِل إلى الأبد، مقسّمًا وقت المعالج بينها وبين كل شيء آخر. القاعدة التي يتعثّر بها الجميع: داخل المهمّة استخدم vTaskDelay() لا delay() — فـ vTaskDelay يعيد المعالج للمهامّ الأخرى أثناء الانتظار.

ثبّتها على النواة الثانية

استخدم xTaskCreatePinnedToCore() وأعطها النواة 0 — فيبقى العمل الثقيل بعيدًا عن النواة 1 حيث يعيش loop() وخادم الويب:

void sensorTask(void *pv) {
  for (;;) {
    float t = readTemp();              // your slow work
    Serial.printf("temp: %.1f\n", t);
    vTaskDelay(pdMS_TO_TICKS(1000));   // yield — never delay() in a task
  }
}

void setup() {
  Serial.begin(115200);
  xTaskCreatePinnedToCore(
    sensorTask,   // function to run
    "sensor",     // name (for debugging)
    4096,         // stack in bytes — too small = crash/reboot
    NULL,         // parameter
    1,            // priority (higher = more urgent)
    NULL,         // task handle (optional)
    0);           // core 0 — leave core 1 for loop() + Wi-Fi
}

void loop() {
  // core 1: keep the web server / UI / Wi-Fi responsive
}

الوسائط السبع هي: الدالّة، اسم، حجم المكدّس، معامل، الأولوية، مقبض اختياري، والنواة (0 أو 1). اثنان منها يعضّان المبتدئين: مكدّس صغير جدًّا فتنهار المهمّة بمجرد لمسها ذاكرة؛ والأولوية نسبية — مهمّة عالية الأولوية لا تتنازل ستُجوّع كل ما دونها.

تمرير البيانات بين النواتين — طابور

الفخّ الآن هو مشاركة البيانات. متغيّر عامّ عادي يُكتب على النواة 0 ويُقرأ على النواة 1 قد «يتمزّق» أو يُقرأ قديمًا — volatile وحده لا يكفي لأيّ شيء أكبر من كلمة واحدة محاذاة. الطريقة النظيفة طابور: مهمّة تضع القيم وأخرى تستقبلها، ويتولّى FreeRTOS القفل:

QueueHandle_t q;

void producer(void *pv) {
  for (;;) {
    float v = readTemp();
    xQueueSend(q, &v, 0);              // hand the value across — no shared globals
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

void setup() {
  q = xQueueCreate(8, sizeof(float));  // room for 8 floats
  xTaskCreatePinnedToCore(producer, "prod", 4096, NULL, 1, NULL, 0);
}

void loop() {
  float v;
  while (xQueueReceive(q, &v, 0) == pdTRUE)   // core 1 drains it safely
    Serial.println(v);
}

الطوابير مثالية لتقسيم مُنتِج/مُستهلِك — مهمّة مستشعر تقيس ومهمّة واجهة تعرض. أما بنية مشتركة تلمسها من الجهتين فاحمِها بـ mutex (xSemaphoreCreateMutex()). القاعدة: مرّر نسخًا عبر طابور، ولا تشارك المتغيّرات العامة.

مطبّات تُعيد تشغيل اللوحة

⚠ أيّ رقائق ESP ثنائية النواة فعلًا

النمط الذي يفيد في كل مشروع تقريبًا: أبقِ loop() للشبكة والواجهة، وادفع العمل الحاجب — مستشعر أو ملف أو تحليل — إلى مهمّة مثبّتة تردّ عبر طابور. فتكفّ اللوحة عن التلعثم، ويبقى لوح القيادة سلسًا بينما تؤدّي المستشعرات عملها.

لست متأكدًا هل للوحتك نواة ثانية — أو أيّ الأطراف متاحة لمهمّة المستشعر؟