🧠 FreeRTOS و دو هستهی ESP32: اجرای چند کار همزمان با xTaskCreatePinnedToCore
یکی از تفاوتهای بزرگ ESP32 با آردوینو اونو، دو هسته بودن پردازنده و داشتن یک سیستمعامل کوچک به اسم FreeRTOS است. در آردوینو اونو همهچیز پشت سر هم در یک loop اجرا میشود و اگر یک کار طول بکشد، بقیه منتظر میمانند. در ESP32 میتوانی چند «تسک» بسازی که همزمان اجرا شوند، حتی هر کدام روی یک هسته. در این آموزش با FreeRTOS در ESP32 و تابع xTaskCreatePinnedToCore چند کار را همزمان اجرا میکنیم و یاد میگیریم تسکها چطور بدون خراب کردن کار هم با هم داده رد و بدل کنند.
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));
}
توضیح بخشهای مهم
struct BlinkConfigپین و دورهی چشمک را کنار هم نگه میدارد. یک تابعblinkTaskداریم ولی دو تسک با دو پیکربندی متفاوت از آن میسازیم؛ این همان کاربرد پارامترvoid* paramاست. دقت کنfastوslowسراسریاند؛ اگر محلی بودند، بعد از تمام شدنsetupاز بین میرفتند و تسک به حافظهی خراب اشاره میکرد.for (;;)حلقهی بیپایان تسک است. تابع تسک نباید هیچوقتreturnکند؛ اگر کارش تمام شد، باید باvTaskDelete(NULL)خودش را حذف کند.vTaskDelay(pdMS_TO_TICKS(...))یعنی «این مدت کاری ندارم، پردازنده را به بقیه بده». در ESP32 تابعdelayهم در اصل همین را صدا میزند، ولی در تسکها نوشتنvTaskDelayروشنتر است.xQueueCreate(1, sizeof(int))یک صف یکخانهای میسازد وxQueueOverwriteهمیشه جدیدترین عدد را جایگزین قبلی میکند. صف راه امن رد کردن داده بین تسکهاست، حتی وقتی روی دو هستهی جدا هستند.xQueueReceive(..., pdMS_TO_TICKS(1000))تا یک ثانیه منتظر داده میماند و در این مدت پردازنده را آزاد میگذارد.xSemaphoreCreateMutex()یک قفل میسازد. هر تسکی که میخواهد در سریال بنویسد، اول قفل را میگیرد و بعد پس میدهد؛ اینطوری دو پیام در هم قاطی نمیشوند.xPortGetCoreID()شمارهی هستهای را که کد الان رویش اجرا میشود برمیگرداند. در خروجی میبینی sensorTask روی ۰ و loop روی ۱ است.
کی 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 در میکروپایتون ببین.
اشکالهای رایج
- پیام Task watchdog got triggered و ریاستارت: یک تسک در حلقهای بدون
vTaskDelayگیر کرده و به تسکهای سیستمی، بهخصوص روی هستهی ۰، اجازهی اجرا نمیدهد. در هر حلقه حتی یک میلیثانیه مکث بگذار. - Stack canary watchpoint triggered: پشتهی تسک کم است. عدد ۲۰۴۸ را به ۴۰۹۶ یا بیشتر برسان؛ با
uxTaskGetStackHighWaterMark(NULL)میتوانی ببینی چقدر از پشته خالی مانده است. - عددهای عجیب در متغیر مشترک: دو تسک همزمان یک متغیر را تغییر دادهاند. برای رد کردن داده از صف و برای منابع مشترک مثل سریال یا I2C از Mutex استفاده کن.
- تسک ساخته نمیشود یا برد ریاستارت میشود: روی مدلهای تکهستهای مثل ESP32-C3 و ESP32-S2 هستهی ۱ وجود ندارد؛ آنجا از
xTaskCreateیا هستهی ۰ استفاده کن. - وایفای ناپایدار شده است: یک تسک سنگین با اولویت بالا روی هستهی ۰ گذاشتهای. کارهای سنگین خودت را روی هستهی ۱ بگذار.
برای دیدن بقیهی امکانات این برد، از صفحهی ESP32 شروع کن.
چندکارگی را در تینکرکد فارسی تمرین کن 🧠سؤالهای رایج
برای استفاده از FreeRTOS در ESP32 کتابخانه نصب کنم؟
نه. هستهی آردوینوی ESP32 خودش روی FreeRTOS ساخته شده است و حتی setup و loop داخل یک تسک اجرا میشوند. توابعی مثل xTaskCreatePinnedToCore و vTaskDelay بدون هیچ include اضافه در دسترساند.
loop در کدام هستهی ESP32 اجرا میشود؟
در ESP32 معمولی، setup و loop روی هستهی ۱ اجرا میشوند و وایفای و بلوتوث بیشتر روی هستهی ۰ کار میکنند. با xPortGetCoreID میتوانی شمارهی هسته را خودت چاپ کنی.
آیا FreeRTOS در شبیهساز اجرا میشود؟
نه، تسکهای FreeRTOS در شبیهساز مدل نمیشوند و این کد را باید روی برد واقعی امتحان کنی. ایدهی انجام چند کار همزمان را میتوانی در شبیهساز با millis تمرین کنی؛ نمونهاش در همین آموزش هست.