🧠 FreeRTOS و دو هسته‌ی ESP32: اجرای چند کار هم‌زمان با xTaskCreatePinnedToCore

✍️ تیم ووکوی فارسی ·

یکی از تفاوت‌های بزرگ ESP32 با آردوینو اونو، دو هسته بودن پردازنده و داشتن یک سیستم‌عامل کوچک به اسم FreeRTOS است. در آردوینو اونو همه‌چیز پشت سر هم در یک loop اجرا می‌شود و اگر یک کار طول بکشد، بقیه منتظر می‌مانند. در ESP32 می‌توانی چند «تسک» بسازی که هم‌زمان اجرا شوند، حتی هر کدام روی یک هسته. در این آموزش با FreeRTOS در ESP32 و تابع xTaskCreatePinnedToCore چند کار را هم‌زمان اجرا می‌کنیم و یاد می‌گیریم تسک‌ها چطور بدون خراب کردن کار هم با هم داده رد و بدل کنند.

💡 تسک‌های FreeRTOS در شبیه‌ساز مدل نمی‌شوند، پس کد اصلی این آموزش برای برد واقعی است. همان ایده را با millis در شبیه‌ساز هم تمرین می‌کنیم.

FreeRTOS به زبان ساده

RTOS یعنی سیستم‌عامل بلادرنگ. قلبش یک زمان‌بند (Scheduler) است که هر یک میلی‌ثانیه نگاه می‌کند کدام تسک آماده‌ی اجراست و با اولویت بالاتر کدام است. هر تسک یک تابع با حلقه‌ی بی‌پایان است، با پشته (Stack) و اولویت خودش. وقتی یک تسک می‌گوید «۵۰۰ میلی‌ثانیه کاری ندارم»، زمان‌بند پردازنده را به تسک دیگری می‌دهد. نتیجه این است که چند کار، بدون این‌که برای هم صبر کنند، جلو می‌روند.

ESP32 معمولی دو هسته دارد: هسته‌ی ۰ که پشته‌ی وای‌فای و بلوتوث بیشتر روی آن کار می‌کند، و هسته‌ی ۱ که setup و loop آردوینو روی آن اجرا می‌شوند. خود loop هم در واقع یک تسک به اسم loopTask با اولویت ۱ است. مقایسه‌ی کامل‌تر این برد با اونو را در ESP32 یا آردوینو اونو ببین.

پارامترهای xTaskCreatePinnedToCore

پارامترمثالمعنی
تابع تسکblinkTaskتابعی با ورودی void* که هیچ‌وقت برنمی‌گردد
نام"blinkFast"فقط برای عیب‌یابی
اندازه‌ی پشته2048در ESP32 بر حسب بایت
پارامتر&fastیک اشاره‌گر که به تابع تسک داده می‌شود
اولویت1عدد بزرگ‌تر یعنی مهم‌تر
دستگیرهNULLبرای متوقف یا حذف کردن تسک در آینده
هسته0 یا 1تسک روی کدام هسته اجرا شود

چه می‌سازیم؟

چهار کار هم‌زمان: دو LED با دو سرعت متفاوت چشمک می‌زنند (یک تابع، دو تسک، با پارامترهای متفاوت)، یک تسک روی هسته‌ی ۰ هر ۱۰۰ میلی‌ثانیه پتانسیومتر را می‌خواند، و loop روی هسته‌ی ۱ آخرین مقدار را از یک صف برمی‌دارد و چاپ می‌کند. سریال مانیتور با یک Mutex محافظت می‌شود تا دو تسک وسط جمله‌ی هم نپرند.

قطعه‌ها و سیم‌کشی

قطعهاتصال
ESP32—
LED اول + مقاومت ۲۲۰ اهمGPIO18 → مقاومت → پایه‌ی بلند LED، پایه‌ی کوتاه به GND
LED دوم + مقاومت ۲۲۰ اهمGPIO19 به همان شکل
پتانسیومتردو پایه‌ی کناری به 3V3 و GND، پایه‌ی وسط به GPIO34

GPIO34 روی ADC1 است و با روشن بودن وای‌فای هم درست کار می‌کند. جزئیات خواندن آنالوگ را در analogRead در ESP32 ببین.

کد کامل

// Two cores, four jobs: FreeRTOS tasks on a real ESP32.
// LEDs on GPIO18 and GPIO19 (220 ohm each), potentiometer on GPIO34.

struct BlinkConfig {
  int pin;
  int periodMs;
};

BlinkConfig fast = {18, 200};
BlinkConfig slow = {19, 1000};

QueueHandle_t potQueue;          // carries readings from core 0 to core 1
SemaphoreHandle_t serialLock;    // only one task at a time on Serial

void safePrintln(const String& text) {
  if (xSemaphoreTake(serialLock, portMAX_DELAY) == pdTRUE) {
    Serial.println(text);
    xSemaphoreGive(serialLock);
  }
}

void blinkTask(void* param) {
  BlinkConfig* cfg = (BlinkConfig*) param;
  bool on = false;
  pinMode(cfg->pin, OUTPUT);
  for (;;) {                     // a task never returns
    on = !on;
    digitalWrite(cfg->pin, on ? HIGH : LOW);
    vTaskDelay(pdMS_TO_TICKS(cfg->periodMs / 2));
  }
}

void sensorTask(void* param) {
  safePrintln("sensorTask runs on core " + String(xPortGetCoreID()));
  for (;;) {
    int raw = analogRead(34);
    xQueueOverwrite(potQueue, &raw);   // keep only the newest value
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

void setup() {
  Serial.begin(115200);
  delay(300);
  potQueue = xQueueCreate(1, sizeof(int));
  serialLock = xSemaphoreCreateMutex();

  xTaskCreatePinnedToCore(blinkTask, "blinkFast", 2048, &fast, 1, NULL, 1);
  xTaskCreatePinnedToCore(blinkTask, "blinkSlow", 2048, &slow, 1, NULL, 1);
  xTaskCreatePinnedToCore(sensorTask, "sensor", 4096, NULL, 1, NULL, 0);

  safePrintln("setup() runs on core " + String(xPortGetCoreID()));
}

void loop() {
  int value;
  if (xQueueReceive(potQueue, &value, pdMS_TO_TICKS(1000)) == pdTRUE) {
    safePrintln("pot = " + String(value) + "  (loop on core " + String(xPortGetCoreID()) + ")");
  }
  vTaskDelay(pdMS_TO_TICKS(500));
}

توضیح بخش‌های مهم

کی FreeRTOS لازم است و کی millis کافی است؟

برای چشمک زدن چند LED و خواندن چند سنسور، روش millis ساده‌تر و کافی است. FreeRTOS وقتی ارزشش را نشان می‌دهد که یک کار مسدودکننده داری که نمی‌توانی تکه‌تکه‌اش کنی؛ مثلاً یک درخواست اینترنتی که دو ثانیه طول می‌کشد و نمی‌خواهی در این دو ثانیه نمایشگر یا موتور از کار بیفتد. آن‌وقت کار شبکه را در یک تسک جدا می‌گذاری و بقیه‌ی برنامه روان می‌ماند. مثال‌های وب‌سرور ESP32 را هم می‌شود با یک تسک جدا برای سنسورها ترکیب کرد.

تمرین در شبیه‌ساز: همان سه کار با millis

در شبیه‌ساز تسک‌های FreeRTOS اجرا نمی‌شوند، ولی ایده‌ی «چند کار هم‌زمان بدون منتظر ماندن» را می‌شود دقیقاً تمرین کرد. این کد همان دو LED و خواندن پتانسیومتر را با چند ساعت millis در یک loop انجام می‌دهد و روی آردوینو اونو (رایگان) و ESP32 کار می‌کند:

// The same jobs without FreeRTOS: one loop, several millis() clocks.
// Runs in the simulator on Arduino Uno (free) or ESP32.
const int LED1 = 4;
const int LED2 = 5;
const int POT = A0;

unsigned long t1 = 0, t2 = 0, t3 = 0;
bool s1 = false, s2 = false;

void setup() {
  Serial.begin(115200);
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
}

void loop() {
  unsigned long now = millis();
  if (now - t1 >= 100) { t1 = now; s1 = !s1; digitalWrite(LED1, s1 ? HIGH : LOW); }
  if (now - t2 >= 500) { t2 = now; s2 = !s2; digitalWrite(LED2, s2 ? HIGH : LOW); }
  if (now - t3 >= 500) {
    t3 = now;
    Serial.print("pot = ");
    Serial.println(analogRead(POT));
  }
}

نسخه‌ی آماده و کامل‌ترش در تینکرکد فارسی پروژه‌ی چند کار هم‌زمان است و درس چندکارگی قدم‌به‌قدم توضیحش می‌دهد. مقاله‌ی millis یا delay هم فرق این دو را خوب باز کرده است. این‌ها روی آردوینو اونو رایگان‌اند؛ اجرای برد ESP32 در شبیه‌ساز اشتراک Plus می‌خواهد. اگر با میکروپایتون کار می‌کنی، همین ایده را در Timer در میکروپایتون ببین.

اشکال‌های رایج

برای دیدن بقیه‌ی امکانات این برد، از صفحه‌ی ESP32 شروع کن.

چندکارگی را در تینکرکد فارسی تمرین کن 🧠

سؤال‌های رایج

برای استفاده از FreeRTOS در ESP32 کتابخانه نصب کنم؟

نه. هسته‌ی آردوینوی ESP32 خودش روی FreeRTOS ساخته شده است و حتی setup و loop داخل یک تسک اجرا می‌شوند. توابعی مثل xTaskCreatePinnedToCore و vTaskDelay بدون هیچ include اضافه در دسترس‌اند.

loop در کدام هسته‌ی ESP32 اجرا می‌شود؟

در ESP32 معمولی، setup و loop روی هسته‌ی ۱ اجرا می‌شوند و وای‌فای و بلوتوث بیشتر روی هسته‌ی ۰ کار می‌کنند. با xPortGetCoreID می‌توانی شماره‌ی هسته را خودت چاپ کنی.

آیا FreeRTOS در شبیه‌ساز اجرا می‌شود؟

نه، تسک‌های FreeRTOS در شبیه‌ساز مدل نمی‌شوند و این کد را باید روی برد واقعی امتحان کنی. ایده‌ی انجام چند کار هم‌زمان را می‌توانی در شبیه‌ساز با millis تمرین کنی؛ نمونه‌اش در همین آموزش هست.

مقاله‌های مرتبط

📡

ESP32 به وای‌فای وصل نمی‌شود؟ عیب‌یابی قدم به قدم با یک کد تشخیص

🛠️

خطای آپلود ESP32: حل Failed to connect و Timed out waiting for packet header

💡

اولین پروژه‌ی ESP32 در شبیه‌ساز: چشمک‌زن دو LED، بدون خرید برد

🌗

PWM با میکروپایتون روی رزبری پای پیکو: LED نفس‌کش با duty_u16 و freq

همه‌ی آموزش‌های ESP32 و اینترنت اشیا ← · همه‌ی مقاله‌ها