مقدمه

در سیستم‌های چندوظیفه‌ای (Multitasking) وظایف مختلف به‌صورت هم‌زمان در حال اجرا هستند و اغلب نیاز دارند با یکدیگر داده مبادله کنند یا برای استفاده از منابع مشترک هماهنگ شوند.
اگر این هماهنگی به درستی انجام نشود، ممکن است سیستم دچار مشکلاتی مانند تداخل داده (Data Corruption) یا بن‌بست (Deadlock) شود.

برای حل این چالش، FreeRTOS مجموعه‌ای از ابزارها را برای ارتباط و همگام‌سازی ارائه می‌دهد:

نوعهدف اصلی
Queueانتقال داده بین Taskها یا از ISR به Task
Semaphoreهمگام‌سازی و کنترل دسترسی به منابع
Mutexجلوگیری از دسترسی هم‌زمان (Critical Section)
Event Groupارسال اعلان‌های چندگانه به چند Task
Task Notificationجایگزین سبک و سریع برای Queue یا Semaphore

Queue – صف پیام در FreeRTOS

Queue یکی از مهم‌ترین مکانیزم‌های FreeRTOS است که امکان ارسال و دریافت داده بین تسک‌ها (یا بین ISR و Task) را فراهم می‌کند.
هر Queue مانند یک بافر FIFO (اول وارد، اول خارج) عمل می‌کند.

ساخت Queue:

QueueHandle_t xQueue = xQueueCreate(5, sizeof(uint32_t));

در این مثال، صفی با ۵ خانه ایجاد شده که هر خانه قادر به نگهداری یک عدد ۳۲ بیتی است.

ارسال و دریافت داده:

uint32_t value = 100;
xQueueSend(xQueue, &value, portMAX_DELAY); // ارسال داده
xQueueReceive(xQueue, &value, portMAX_DELAY); // دریافت داده

اگر صف پر باشد، xQueueSend تا زمانی که فضا خالی شود یا زمان انتظار تمام شود، بلوکه می‌ماند.
اگر صف خالی باشد، xQueueReceive نیز تا ورود داده جدید منتظر می‌ماند.

در واقع Queue مانند یک کانال ارتباطی امن بین وظایف عمل می‌کند که داده‌ها را به‌صورت منظم و بدون تداخل منتقل می‌کند.
هر Task می‌تواند داده‌ای را در صف قرار دهد و Task دیگر در زمان مناسب آن را دریافت کند، بدون اینکه نیازی به دسترسی مستقیم به حافظه‌ی مشترک باشد.
این ویژگی باعث می‌شود مشکلاتی مثل Race Condition یا از بین رفتن داده‌ها به‌طور کامل برطرف شوند.
علاوه بر این، Queueها می‌توانند میان Taskها، یا حتی بین وقفه‌ها (ISR) و Taskها، برای انتقال سریع و مطمئن اطلاعات مورد استفاده قرار گیرند.
در نتیجه، Queue یکی از ستون‌های اصلی برای ایجاد ساختارهای چندوظیفه‌ای پایدار و قابل اعتماد در FreeRTOS محسوب می‌شود.

توابع مهم Queue:

تابعکاربرد
xQueueCreate()ایجاد صف
xQueueSend()ارسال داده به صف
xQueueReceive()دریافت داده از صف
xQueueSendFromISR()ارسال از داخل ISR
xQueuePeek()مشاهده‌ی داده بدون حذف از صف
uxQueueMessagesWaiting()شمارش خانه‌های پر در صف

مثال: ارتباط بین دو Task از طریق Queue

QueueHandle_t sensorQueue;

void vSensorTask(void *pvParameters)
{
  uint16_t sensorValue = 0;
  for(;;)
  {
    sensorValue = readSensor();
    xQueueSend(sensorQueue, &sensorValue, portMAX_DELAY);
    vTaskDelay(pdMS_TO_TICKS(100));
  }
}

void vDisplayTask(void *pvParameters)
{
  uint16_t value;
  for(;;)
  {
    if(xQueueReceive(sensorQueue, &value, portMAX_DELAY))
      displayValue(value);
  }
}

نتیجه:
vSensorTask داده را در صف قرار می‌دهد و vDisplayTask همان داده را از صف خوانده و روی LCD نمایش می‌دهد.

Semaphore-همگام‌سازی وظایف

Queue یکی از مهم‌ترین مکانیزم‌های FreeRTOS است که امکان ارسال و دریافت داده بین تسک‌ها (یا بین ISR و Task) را فراهم می‌کند.

نوعکاربرد
Binary Semaphoreارسال سیگنال بین دو بخش از سیستم (مثلاً ISR → Task)
Counting Semaphoreشمارنده برای کنترل منابع متعدد (مثل چند پورت سریال)

Binary Semaphore Example:

SemaphoreHandle_t xSemaphore = NULL;

void ISR_ButtonPress(void)
{
   BaseType_t xHigherPriorityTaskWoken = pdFALSE;
   xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
   portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void vButtonTask(void *pvParameters)
{
   for(;;)
   {
      if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
         HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
   }
}

عملکرد:
هنگامی که دکمه فشرده می‌شود، ISR پرچم Semaphore را می‌دهد (GiveFromISR) و Task مربوطه بلافاصله بیدار می‌شود و LED را تغییر وضعیت می‌دهد.

Counting Semaphore Example:

SemaphoreHandle_t xCountSemaphore = xSemaphoreCreateCounting(3, 3);

در این مثال، حداکثر ۳ تسک می‌توانند هم‌زمان به یک منبع خاص دسترسی پیدا کنند.

سمافور در اصل از تئوری سیستم‌عامل‌های کلاسیک گرفته شده و در RTOS به‌صورت یک مکانیزم بسیار سبک و سریع پیاده‌سازی شده است.
عملکرد آن بر پایه‌ی «گرفتن» (Take) و «دادن» (Give) یک مجوز است؛ یعنی اگر منبعی آزاد باشد، Task می‌تواند آن را بگیرد و پس از پایان کار، آن را آزاد کند تا دیگر وظایف نیز بتوانند از آن استفاده کنند.
در واقع، سمافور جلوی اجرای هم‌زمان چند Task بر روی یک منبع مشترک را می‌گیرد و به این ترتیب از بروز خطاهای هم‌زمانی جلوگیری می‌کند.
همچنین می‌توان از آن برای ارسال سیگنال یا اعلان وقوع یک رویداد مانند اتمام تبدیل ADC یا فشرده شدن کلید استفاده کرد.
به همین دلیل، سمافورها در FreeRTOS نه تنها ابزار همگام‌سازی، بلکه یک روش ارتباطی ساده و مؤثر بین اجزای مختلف سیستم نیز هستند.

Mutex  قفل متقابل برای منابع مشترک

Mutex (Mutual Exclusion) نوع خاصی از Binary Semaphore است که برای محافظت از منابع مشترک (Shared Resources) استفاده می‌شود.

مثلاً وقتی دو Task قصد دارند از یک پورت UART استفاده کنند، Mutex تضمین می‌کند که در هر لحظه فقط یکی از آن‌ها به منبع دسترسی داشته باشد.

مثال:

SemaphoreHandle_t xUartMutex;

void vTask1(void *pvParameters)
{
   for(;;)
   {
      if(xSemaphoreTake(xUartMutex, portMAX_DELAY) == pdTRUE)
      {
         printf("Task 1 Writing...\n");
         HAL_UART_Transmit(&huart2, "Hello", 5, 100);
         xSemaphoreGive(xUartMutex);
      }
      vTaskDelay(500);
   }
}

اگر Task2 نیز از همان Mutex استفاده کند، تا پایان کار Task1 صبر خواهد کرد.
این سازوکار از درهم‌ریختگی داده‌ها جلوگیری می‌کند.
Mutex برخلاف Semaphore معمولی، مفهوم «مالکیت» دارد؛ یعنی فقط تسکی که Mutex را گرفته است، می‌تواند آن را آزاد کند.

این ویژگی از سوءاستفاده یا آزاد شدن تصادفی Mutex توسط تسک‌های دیگر جلوگیری می‌کند و پایداری سیستم را افزایش می‌دهد.

در FreeRTOS، Mutex‌ها برای محافظت از منابع حیاتی مثل درگاه‌های ارتباطی (UART, SPI)، فایل‌سیستم‌ها و حافظه‌های اشتراکی به کار می‌روند.

همچنین Mutex از سیاست Priority Inheritance پشتیبانی می‌کند تا هنگام رقابت تسک‌ها با اولویت‌های مختلف، از بروز مشکل Priority Inversion جلوگیری شود.

به طور کلی، استفاده‌ی درست از Mutex باعث می‌شود چندوظیفگی در سیستم بدون تداخل داده و با عملکرد کاملاً قابل پیش‌بینی انجام شود.

Priority Inversion و Priority Inheritance

مشکل Priority Inversion:   

وقتی یک Task با اولویت پایین Mutex را در اختیار دارد و Task با اولویت بالا منتظر آزاد شدن آن است، سیستم دچار قفل اولویت (Priority Inversion) می‌شود.

به دلیل اجرای Taskهایی با اولویت متوسط، تسک حیاتی ممکن است برای مدت طولانی منتظر بماند.

راه‌حل: Priority Inheritance

در FreeRTOS، زمانی که Task با اولویت بالا منتظر Mutex است، Task صاحب Mutex به‌طور موقت اولویت خود را افزایش می‌دهد تا زودتر Mutex را آزاد کند.
این سازوکار به‌طور خودکار در FreeRTOS برای Mutexها فعال است و نیازی به تنظیم دستی ندارد.

Event Groups  اعلان هم‌زمان رویدادها

 Event Group ساختاری است که چندین رویداد (Flag) را در قالب یک متغیر ۳۲ بیتی ذخیره می‌کند.هر بیت از Event Group نشان‌دهنده‌ی یک وضعیت خاص است.

کاربردها:

  • هماهنگی چند Task
  • تشخیص وضعیت چند منبع
  • ارسال هم‌زمان چند اعلان

مثال:

EventGroupHandle_t xEventGroup;

#define BIT_UART  (1 << 0)
#define BIT_ADC   (1 << 1)

void vUartTask(void *pvParameters)
{
    for(;;)
    {
        // انجام کار UART
        xEventGroupSetBits(xEventGroup, BIT_UART);
    }
}

void vMonitorTask(void *pvParameters)
{
    EventBits_t uxBits;
    for(;;)
    {
        uxBits = xEventGroupWaitBits(xEventGroup,
                                     BIT_UART | BIT_ADC,
                                     pdTRUE,
                                     pdTRUE,
                                     portMAX_DELAY);
        if((uxBits & (BIT_UART | BIT_ADC)) == (BIT_UART | BIT_ADC))
            printf("Both events done!\n");
    }
}

عملکرد:
MonitorTask منتظر می‌ماند تا هر دو تسک (UART و ADC) کار خود را تمام کنند و سپس پیامی چاپ می‌کند.

به کمک Event Group می‌توان چندین وظیفه را طوری هماهنگ کرد که منتظر وقوع هم‌زمان چند رویداد باشند؛ مثلاً یک Task فقط زمانی اجرا شود که هم داده‌ی سنسور آماده است و هم ارتباط UART برقرار شده باشد.
هر Task می‌تواند یک یا چند بیت از این گروه را تنظیم (Set) یا پاک (Clear) کند تا سایر وظایف از وضعیت جدید آگاه شوند.
این روش باعث می‌شود به جای استفاده از چند Semaphore جداگانه، تمام رویدادهای مرتبط در قالب یک ساختار منسجم و ساده مدیریت شوند و کد برنامه خواناتر و قابل نگهداری‌تر گردد.

Task Notifications  ارتباط سبک و سریع

 از نسخه‌ی 10 FreeRTOS به بعد، Task Notification به‌عنوان جایگزین سبک‌تر برای Queue و Semaphore معرفی شده است.
هر Task یک «notification slot» مخصوص به خود دارد که می‌تواند یک مقدار (عدد یا flag) از دیگر Taskها یا ISR دریافت کند.

ارسال Notification:

xTaskNotifyGive(xTaskHandle);

دریافت Notification:

ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

این روش بسیار سریع‌تر از Queue است، زیرا نیازی به حافظه‌ی پویا ندارد و فقط یک متغیر داخلی در ساختار Task را تغییر می‌دهد.

مقایسه روش‌های ارتباطی در FreeRTOS

مکانیزمهدف اصلیحجم دادهمناسب برای ISRسرعت
Queueانتقال داده واقعیزیادبلهمتوسط
Semaphoreهمگام‌سازی سادهبدون دادهبلهزیاد
Mutexمحافظت از منبع مشترکبدون دادهخیرزیاد
Event Groupاعلان چندگانه۳۲ بیتخیرزیاد
Task Notificationاعلان ساده یا عددی۱ عددبلهبسیار زیاد ✅

.

نکات طراحی و بهینه‌سازی

✅ برای تبادل داده‌های حجیم از Queue استفاده کنید.
✅ برای هماهنگی ساده (مثلاً بیدار کردن Task از ISR) از Binary Semaphore یا Notification استفاده کنید.
✅ در استفاده از Mutex دقت کنید تا از بن‌بست جلوگیری شود.
✅ همیشه از xQueueSendFromISR() یا xSemaphoreGiveFromISR() در ISR استفاده کنید تا Scheduler به درستی بیدار شود.
✅ از Event Group برای کنترل چند منبع هم‌زمان استفاده کنید تا ساختار برنامه تمیز و قابل‌درک باقی بماند.

مثال پروژه‌ی عملی

 هدف:

ساخت پروژه‌ای با سه تسک:

  1. خواندن داده از ADC
  2. ارسال داده به UART
  3. کنترل LED در زمان خاص

پیاده‌سازی:

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * File Name          : freertos.c
  * Description        : Code for freertos applications
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */

/* USER CODE END Variables */
/* Definitions for defaultTask */
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
  .name = "defaultTask",
  .stack_size = 150 * 4,
  .priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for MonitorTask */
osThreadId_t MonitorTaskHandle;
const osThreadAttr_t MonitorTask_attributes = {
  .name = "MonitorTask",
  .stack_size = 150 * 4,
  .priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for AdcTask */
osThreadId_t AdcTaskHandle;
const osThreadAttr_t AdcTask_attributes = {
  .name = "AdcTask",
  .stack_size = 150 * 4,
  .priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for UartTask */
osThreadId_t UartTaskHandle;
const osThreadAttr_t UartTask_attributes = {
  .name = "UartTask",
  .stack_size = 150 * 4,
  .priority = (osPriority_t) osPriorityLow,
};
/* Definitions for xAdcQueue */
osMessageQueueId_t xAdcQueueHandle;
const osMessageQueueAttr_t xAdcQueue_attributes = {
  .name = "xAdcQueue"
};
/* Definitions for xUartMutex */
osMutexId_t xUartMutexHandle;
const osMutexAttr_t xUartMutex_attributes = {
  .name = "xUartMutex"
};
/* Definitions for xEventGroup */
osEventFlagsId_t xEventGroupHandle;
const osEventFlagsAttr_t xEventGroup_attributes = {
  .name = "xEventGroup"
};

/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */

/* USER CODE END FunctionPrototypes */

void StartDefaultTask(void *argument);
void StartMonitorTask(void *argument);
void StartAdcTask(void *argument);
void StartUartTask(void *argument);

void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */

/**
  * @brief  FreeRTOS initialization
  * @param  None
  * @retval None
  */
void MX_FREERTOS_Init(void) {
  /* USER CODE BEGIN Init */

  /* USER CODE END Init */
  /* Create the mutex(es) */
  /* creation of xUartMutex */
  xUartMutexHandle = osMutexNew(&xUartMutex_attributes);

  /* USER CODE BEGIN RTOS_MUTEX */
  /* add mutexes, ... */
  /* USER CODE END RTOS_MUTEX */

  /* USER CODE BEGIN RTOS_SEMAPHORES */
  /* add semaphores, ... */
  /* USER CODE END RTOS_SEMAPHORES */

  /* USER CODE BEGIN RTOS_TIMERS */
  /* start timers, add new ones, ... */
  /* USER CODE END RTOS_TIMERS */

  /* Create the queue(s) */
  /* creation of xAdcQueue */
  xAdcQueueHandle = osMessageQueueNew (16, sizeof(uint16_t), &xAdcQueue_attributes);

  /* USER CODE BEGIN RTOS_QUEUES */
  /* add queues, ... */
  /* USER CODE END RTOS_QUEUES */

  /* Create the thread(s) */
  /* creation of defaultTask */
  defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);

  /* creation of MonitorTask */
  MonitorTaskHandle = osThreadNew(StartMonitorTask, NULL, &MonitorTask_attributes);

  /* creation of AdcTask */
  AdcTaskHandle = osThreadNew(StartAdcTask, NULL, &AdcTask_attributes);

  /* creation of UartTask */
  UartTaskHandle = osThreadNew(StartUartTask, NULL, &UartTask_attributes);

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
  /* USER CODE END RTOS_THREADS */

  /* creation of xEventGroup */
  xEventGroupHandle = osEventFlagsNew(&xEventGroup_attributes);

  /* USER CODE BEGIN RTOS_EVENTS */
  /* add events, ... */
  /* USER CODE END RTOS_EVENTS */

}

/* 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_StartMonitorTask */
/**
* @brief Function implementing the MonitorTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartMonitorTask */
void StartMonitorTask(void *argument)
{
  /* USER CODE BEGIN StartMonitorTask */
     uint32_t bits;
  /* Infinite loop */
  for(;;)
  {
   
    bits = osEventFlagsWait(xEventGroupHandle,
                            (1 << 0) | (1 << 1),
                            osFlagsWaitAll | osFlagsNoClear,
                            osWaitForever);

    if(bits == ((1 << 0) | (1 << 1)))
    {
      HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin); 
            HAL_GPIO_TogglePin(BLUE_LED_GPIO_Port, BLUE_LED_Pin); 
            HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
      osEventFlagsClear(xEventGroupHandle, (1 << 0) | (1 << 1));
    }
  }
  /* USER CODE END StartMonitorTask */
}

/* USER CODE BEGIN Header_StartAdcTask */
/**
* @brief Function implementing the AdcTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartAdcTask */
void StartAdcTask(void *argument)
{
  /* USER CODE BEGIN StartAdcTask */
    uint16_t value;
    HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
  /* Infinite loop */
  for(;;)
  {

        HAL_ADC_Start(&hadc1);

       
        if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
        {
            value = (uint16_t)HAL_ADC_GetValue(&hadc1);
        }
        else
        {
            value = 0xFFFF; 
        }

        HAL_ADC_Stop(&hadc1);

        
        osMessageQueuePut(xAdcQueueHandle, &value, 0, 0);

       
        osEventFlagsSet(xEventGroupHandle, (1U << 0));

        osDelay(100);  
  }
  /* USER CODE END StartAdcTask */
}

/* 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 */
     uint16_t value;
  /* Infinite loop */
  for(;;)
  {
     
    if(osMessageQueueGet(xAdcQueueHandle, &value, NULL, osWaitForever) == osOK)
    {
      
      osMutexAcquire(xUartMutexHandle, osWaitForever);

//      printf("ADC Value: %u\r\n", value); 
//      HAL_UART_Transmit(&huart2, (uint8_t*)&value, sizeof(value), 100);
 
      send_adc(value);
      osMutexRelease(xUartMutexHandle);

    
      osEventFlagsSet(xEventGroupHandle, (1 << 1));
    }
  }
  /* USER CODE END StartUartTask */
}

/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */

/* USER CODE END Application */

نتیجه:

  • داده از ADC خوانده شده و از طریق Queue به UART فرستاده می‌شود.
  • با اجرای کامل دو عملیات، LED چشمک می‌زند.
  • سیستم بدون تداخل یا بن‌بست کار می‌کند.

جمع‌بندی

*همگام‌سازی وظایف در RTOS برای جلوگیری از تداخل، حفظ یکپارچگی داده‌ها و استفاده‌ی درست از منابع ضروری است.
 FreeRTOS *ابزارهای مختلفی برای این هدف ارائه می‌دهد: Queue، Semaphore، Mutex، Event Group، Task Notification.
*انتخاب ابزار مناسب بستگی به نوع ارتباط (ارسال داده یا اعلان وضعیت) دارد.
* با درک درست از این مکانیزم‌ها، می‌توان سیستم‌های چندوظیفه‌ای بسیار قابل اطمینان و پایدار طراحی کرد.

منابع

Mastering the FreeRTOS Kernel – Richard Barry
Hands-On RTOS with Microcontrollers – Brian Amos
FreeRTOS Reference Manual
 Hands-On RTOS with Developing Applications on STM32Cube with RTOS

با نظرات خود به تیم جبرا در بهبود کیفیت کمک کنید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

سبد خرید
پیمایش به بالا