مقدمه: Task چیست؟
در RTOS، هر Task وظیفه یک واحد اجرایی مستقل است که کد مخصوص به خود، پشته (Stack)، اولویت (Priority) و زمانبندی اجرا دارد.
برخلاف برنامههای سادهی bare-metal که فقط یک حلقهی while(1) دارند، در FreeRTOS چندین Task بهطور همزمان اجرا میشوند (چندوظیفگی – Multitasking).
هر Task معمولاً مسئول انجام یک کار مشخص است؛ برای مثال:
- خواندن داده از سنسور
- پردازش اطلاعات
- ارسال داده از طریق UART یا SPI
- کنترل LED یا موتور
به این ترتیب، با تقسیم برنامه به چند Task، کد تمیزتر، قابل نگهداریتر و مقیاسپذیرتر میشود. هر Task در واقع مانند یک «ریزبرنامه» درون سیستم عمل میکند که به صورت مستقل از دیگر وظایف اجرا میشود و میتواند در هر لحظه توسط Scheduler متوقف یا از سر گرفته شود.
این استقلال باعث میشود بتوانیم رفتارهای مختلف سیستم را به شکل ماژولار طراحی کنیم و بهجای یک حلقهی پیچیده، هر بخش را در قالب یک وظیفهی جدا توسعه دهیم.
علاوه بر این، هر Task دارای فضای پشته (Stack) مخصوص به خود است تا دادهها و متغیرهای محلی آن از سایر وظایف جدا بماند.
به همین دلیل، Taskها بهطور همزمان و بدون تداخل در دادهها کار میکنند و برنامهنویس میتواند بهراحتی بین وظایف، داده یا سیگنال ارسال کند.
این مفهوم اساس چندوظیفگی واقعی در سیستمهای نهفته را تشکیل میدهد و موجب افزایش کارایی و قابلیت اطمینان سیستم میشود.
ساختار کلی یک Task در FreeRTOS
void vTaskFunction(void *pvParameters)
{
for(;;)
{
// عملیات تسک
vTaskDelay(pdMS_TO_TICKS(1000)); // توقف ۱ ثانیهای
}
}
| ویژگی | توضیح |
| نوع تابع | همیشه void برمیگرداند و یک پارامتر void* میگیرد |
| ساختار | باید شامل یک حلقهی بینهایت باشد |
| خروج از تابع | نباید از تابع بازگردد (در غیر این صورت Scheduler از کار میافتد) |
مثال 3 :
/* USER CODE BEGIN Header_StartDefaultTask */
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
/* USER CODE BEGIN Header_StartLowPriorityLED */
/**
* @brief Function implementing the LowPriorityLED thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartLowPriorityLED */
void StartLowPriorityLED(void *argument)
{
/* USER CODE BEGIN StartLowPriorityLED */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
vTaskDelay(pdMS_TO_TICKS(200));
}
/* USER CODE END StartLowPriorityLED */
}
/* USER CODE BEGIN Header_StartHighPriorityButton */
/**
* @brief Function implementing the HighPriorityBut thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartHighPriorityButton */
void StartHighPriorityButton(void *argument)
{
/* USER CODE BEGIN StartHighPriorityButton */
/* Infinite loop */
for(;;)
{
if (HAL_GPIO_ReadPin(USER_BUTTON_GPIO_Port, USER_BUTTON_Pin) == GPIO_PIN_RESET)
{
uint32_t counter = 0;
while(counter < 500000)
counter++;
}
else
{
vTaskDelay(pdMS_TO_TICKS(500));
}
}
/* USER CODE END StartHighPriorityButton */
}
ایجاد Task – تابع xTaskCreate()
برای ساختن یک Task جدید از تابع زیر استفاده میشود:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode,
const char * const pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask);
| پارامتر | توضیح |
| pvTaskCode | اشارهگر به تابع تسک |
| pcName | نام اختیاری برای دیباگ |
| usStackDepth | اندازه پشته تسک (تعداد کلمات، نه بایت) |
| pvParameters | آرگومان اختیاری برای تسک |
| uxPriority | اولویت تسک |
| pxCreatedTask | اشارهگر برای دریافت شناسه تسک (اختیاری) |
مثال:
xTaskCreate(vLEDTask, "LED", 128, NULL, 2, NULL);
نکته: اگر حافظه کافی برای تسک وجود نداشته باشد، xTaskCreate() مقدار errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY برمیگرداند.
اندازه پشته (Stack Size)
هر Task دارای پشتهی مخصوص خود است. اندازهی آن به میزان استفاده از متغیرهای محلی، فراخوانی توابع، و عمق توابع بازگشتی بستگی دارد.
- در میکروکنترلرهای STM32 معمولاً مقدار 128 تا 512 کلمه (Word) برای تسکهای ساده کافی است.
- اگر از توابع با آرایههای بزرگ یا printf استفاده میکنید، مقدار بزرگتری در نظر بگیرید.
در FreeRTOS هر کلمه (Word) برابر با ۴ بایت است.
اندازهی پشته تأثیر مستقیمی بر پایداری و عملکرد سیستم دارد، زیرا در صورتی که مقدار آن کمتر از حد نیاز تنظیم شود، ممکن است دادههای Taskهای دیگر یا متغیرهای سیستم دچار خرابی (Stack Overflow) شوند.
برای جلوگیری از این مشکل، FreeRTOS امکان بررسی حداقل فضای باقیمانده از پشته را با تابع uxTaskGetStackHighWaterMark() فراهم کرده است تا بتوان مقدار مناسب را برای هر تسک تنظیم کرد.
همچنین هنگام فعال بودن گزینهی configCHECK_FOR_STACK_OVERFLOW در فایل پیکربندی، سیستم بهصورت خودکار وقوع سرریز پشته را تشخیص داده و هشدار میدهد.
بنابراین انتخاب دقیق Stack Size در طراحی تسکها یکی از کلیدیترین مراحل بهینهسازی و افزایش اطمینان در پروژههای RTOS محسوب میشود.
اولویت وظایف (Task Priority)
هر تسک دارای یک اولویت عددی است که Scheduler از آن برای انتخاب تسک بعدی استفاده میکند.
| عدد اولویت | توضیح |
| عدد بزرگتر | اولویت بالاتر |
| عدد کوچکتر | اولویت پایینتر |
بهصورت پیشفرض، محدودهی اولویتها بین 0 تا configMAX_PRIORITIES – 1 است.
اگر دو تسک با اولویت برابر داشته باشید، Scheduler از روش Round-Robin برای تقسیم CPU بین آنها استفاده میکند.
مثال:
xTaskCreate(TaskA, "A", 128, NULL, 3, NULL);
xTaskCreate(TaskB, "B", 128, NULL, 1, NULL);
→ TaskA همیشه زودتر از TaskB اجرا میشود مگر TaskA در حالت Blocked باشد.
در انتخاب اولویتها باید دقت زیادی شود، زیرا تنظیم نادرست میتواند باعث گرسنگی تسکها (Task Starvation) شود؛ یعنی تسکهایی با اولویت پایین هرگز فرصت اجرا پیدا نکنند.
به همین دلیل توصیه میشود وظایف حیاتی (مثل خواندن سنسورهای حساس) در اولویت بالا و کارهای غیرحیاتی (مثل ارسال داده به LCD) در اولویت پایینتر قرار گیرند.
در پروژههای پیچیده میتوان با استفاده از اولویت پویا (Dynamic Priority)، در زمان اجرا اولویت برخی تسکها را تغییر داد تا سیستم واکنشپذیرتر شود.
همچنین FreeRTOS با استفاده از سیاست Priority Inheritance در Mutexها از بروز مشکل Priority Inversion جلوگیری میکند تا تسکهای کماولویت موجب تأخیر در اجرای وظایف حیاتی نشوند.
حالتهای مختلف Task در FreeRTOS
هر Task در یکی از چهار حالت زیر قرار دارد:
| حالت | توضیح |
| Running | تسکی که در حال حاضر CPU را در اختیار دارد |
| Ready | تسکی که آماده اجراست ولی فعلاً CPU ندارد |
| Blocked | منتظر یک رویداد (Delay، Semaphore یا Queue) است |
| Suspended | بهصورت دستی متوقف شده تا دوباره فعال شود |
چرخهی تغییر وضعیت:
+----------+
| Running |
+----------+
^ |
| v
+-----------+ +----------+
| Ready | <-- | Blocked |
+-----------+ +----------+
|
v
+-----------+
| Suspended |
+-----------+
تابع vTaskDelay() و vTaskDelayUntil()
vTaskDelay() :
برای ایجاد تأخیر در تسک بدون توقف کل سیستم.
vTaskDelay(pdMS_TO_TICKS(1000)); // توقف ۱ ثانیه
در این حالت، Task به حالت Blocked میرود و Scheduler CPU را به سایر Taskها میدهد.
vTaskDelayUntil()
مناسب برای تسکهایی با دورهی زمانی ثابت (Periodic Task) است.
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
do_something();
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(100));
}
این روش باعث میشود فاصلهی اجرای Task دقیق و منظم باشد، حتی اگر اجرای تابع طول بکشد.
متوقف کردن و حذف تسکها
vTaskSuspend()
Task را متوقف (Suspend) میکند تا بعداً با vTaskResume() دوباره فعال شود.
vTaskSuspend(xTaskHandle);
vTaskResume(xTaskHandle);
vTaskDelete()
برای حذف کامل Task از سیستم و آزاد کردن حافظهی آن.
vTaskSuspend(xTaskHandle);
vTaskResume(xTaskHandle);
مثال 2:
/* USER CODE END Header_StartGreenLED */
void StartGreenLED(void *argument)
{
/* USER CODE BEGIN StartGreenLED */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
osDelay(200);
}
/* USER CODE END StartGreenLED */
}
/* USER CODE BEGIN Header_StartBlueLED */
/**
* @brief Function implementing the BlueLED thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartBlueLED */
void StartBlueLED(void *argument)
{
/* USER CODE BEGIN StartBlueLED */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(BLUE_LED_GPIO_Port, BLUE_LED_Pin);
osDelay(500);
}
/* USER CODE END StartBlueLED */
}
/* USER CODE BEGIN Header_StartRedLED */
/**
* @brief Function implementing the RedLED thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartRedLED */
void StartRedLED(void *argument)
{
/* USER CODE BEGIN StartRedLED */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
osDelay(100);
}
/* USER CODE END StartRedLED */
}
/* USER CODE BEGIN Header_StartUserButton */
/**
* @brief Function implementing the UserButton thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartUserButton */
void StartUserButton(void *argument)
{
/* USER CODE BEGIN StartUserButton */
/* Infinite loop */
for(;;)
{
if (HAL_GPIO_ReadPin(USER_BUTTON_GPIO_Port, USER_BUTTON_Pin) == GPIO_PIN_RESET)
{
if(RedLEDHandle != NULL)
vTaskSuspend(RedLEDHandle);
}
else
{
if(RedLEDHandle != NULL)
vTaskResume(RedLEDHandle);
}
osDelay(50);
}
/* USER CODE END StartUserButton */
}/* USER CODE BEGIN Header_StartGreenLED */
/**
* @brief Function implementing the GreenLED thread.
* @param argument: Not used
* @retval None
*/
نکته: حذف مکرر و ایجاد Taskهای جدید باعث تکهتکه شدن حافظه (Fragmentation) میشود؛ بهتر است Taskها را تنها یکبار بسازید و فقط Suspend/Resume کنید.
شبیهسازی چندوظیفگی در FreeRTOS
فرض کنید دو Task داریم:
void LED1_Task(void *argument)
{
for(;;)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void LED2_Task(void *argument)
{
for(;;)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_3);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
و در تابع اصلی:
xTaskCreate(LED1_Task, "LED1", 128, NULL, 2, NULL);
xTaskCreate(LED2_Task, "LED2", 128, NULL, 1, NULL);
vTaskStartScheduler();
نتیجه:
- هر دو LED چشمک میزنند.
- Scheduler بهصورت خودکار زمان CPU را بین آنها تقسیم میکند.
- تأخیر در یک Task باعث توقف دیگری نمیشود.
Scheduler و زمانبندی Taskها
فرض پس از اجرای تابع vTaskStartScheduler()، کنترل برنامه به Scheduler واگذار میشود.
از این لحظه، هیچ دستور دیگری در main() اجرا نمیشود و Scheduler تصمیم میگیرد در هر لحظه کدام Task باید اجرا شود.
Scheduler با کمک وقفهی SysTick (هر ۱ میلیثانیه) وضعیت تسکها را بررسی کرده و در صورت نیاز، Context Switch انجام میدهد.
اگر حافظه برای تسک Idle کافی نباشد، Scheduler هرگز شروع نخواهد شد.
Scheduler در واقع همان هستهی زمانبندی FreeRTOS است که تضمین میکند تمام Taskها طبق اولویت و وضعیت خود اجرا شوند.
هر بار که وقفهی SysTick رخ میدهد، Scheduler بررسی میکند کدام Task در حالت Ready است و آیا نیاز به تعویض زمینه (Context Switch) وجود دارد یا خیر.
در این فرآیند، محتوای رجیسترهای Task فعلی در پشته ذخیره و اطلاعات Task بعدی بازیابی میشود تا اجرای آن دقیقاً از نقطهی قبلی ادامه یابد.
اگر حتی یکی از Taskهای حیاتی مانند Idle یا Timer ایجاد نشده باشند، Scheduler قادر به شروع نخواهد بود، زیرا به وجود حداقل یک Task برای حفظ چرخهی CPU نیاز دارد.
به همین دلیل، در طراحی سیستمهای RTOS باید از وجود تسکهای پایه و کافی قبل از فراخوانی vTaskStartScheduler() اطمینان حاصل کرد.
تسک Idle و Hook Functions
هنگامی که هیچ Task فعالی در حالت Ready وجود نداشته باشد، Idle Task اجرا میشود.
این Task همیشه در سیستم وجود دارد و برای انجام کارهای عمومی مثل آزادسازی حافظهی تسکهای حذفشده یا مدیریت توان استفاده میشود.
کاربر میتواند تابع Idle Hook خود را اضافه کند:
void vApplicationIdleHook(void)
{
// اجرای کد هنگام بیکاری CPU
__WFI(); // کاهش مصرف توان
}
نکات بهینهسازی Taskها
✅ از vTaskDelay() بهجای HAL_Delay() استفاده کنید.
✅ تعداد تسکها را تا حد امکان کم نگه دارید.
✅ از Semaphore یا Queue برای همگامسازی استفاده کنید.
✅ اندازهی Stack را در حالت Debug با تابع uxTaskGetStackHighWaterMark() بررسی کنید.
✅ از SystemView یا Tracealyzer برای مشاهدهی رفتار زمانبندی استفاده کنید.
مثال پروژه عملی (CubeMX + FreeRTOS)
مثال 5 :
ساخت دو Task — یکی برای LED و دیگری برای ارسال پیام UART.
/* USER CODE BEGIN Header_StartGreenLED */
/**
* @brief Function implementing the GreenLED thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartGreenLED */
void StartGreenLED(void *argument)
{
/* USER CODE BEGIN StartGreenLED */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
osDelay(200);
}
/* USER CODE END StartGreenLED */
}
/* USER CODE BEGIN Header_StartBlueLED */
/**
* @brief Function implementing the BlueLED thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartBlueLED */
void StartBlueLED(void *argument)
{
/* USER CODE BEGIN StartBlueLED */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(BLUE_LED_GPIO_Port, BLUE_LED_Pin);
osDelay(500);
}
/* USER CODE END StartBlueLED */
}
/* USER CODE BEGIN Header_StartRedLED */
/**
* @brief Function implementing the RedLED thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartRedLED */
void StartRedLED(void *argument)
{
/* USER CODE BEGIN StartRedLED */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
osDelay(1000);
}
/* USER CODE END StartRedLED */
}
/* USER CODE BEGIN Header_StartUartTask */
/**
* @brief Function implementing the UartTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartUartTask */
void StartUartTask(void *argument)
{
/* USER CODE BEGIN StartUartTask */
/* Infinite loop */
for(;;)
{
char msg[] = "FreeRTOS Running\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), 100);
osDelay(1000);
}
/* USER CODE END StartUartTask */
}
و
/* creation of GreenLED */
GreenLEDHandle = osThreadNew(StartGreenLED, NULL, &GreenLED_attributes);
/* creation of BlueLED */
BlueLEDHandle = osThreadNew(StartBlueLED, NULL, &BlueLED_attributes);
/* creation of RedLED */
RedLEDHandle = osThreadNew(StartRedLED, NULL, &RedLED_attributes);
/* creation of UartTask */
UartTaskHandle = osThreadNew(StartUartTask, NULL, &UartTask_attributes);
جمعبندی
*هر Task در FreeRTOS یک فرآیند مستقل با پشته، اولویت و چرخهی اجرای خاص خود است.
Scheduler *زمانبندی اجرای Taskها را با استفاده از SysTick مدیریت میکند.
*حالتهای مختلف Task (Running, Ready, Blocked, Suspended) به RTOS کمک میکنند تا منابع را بهینه تخصیص دهد.
*توابع مهم مدیریت Task شامل:
xTaskCreate(), vTaskDelete(), vTaskDelay(), vTaskSuspend(), vTaskResume(), vTaskDelayUntil()
منابع
UM1722 – Developing Applications on STM32Cube with RTOS – STMicroelectronics
(Mastering the FreeRTOS Kernel – (Richard Barry
(Hands-On RTOS with Microcontrollers – (Brian Amos