مقدمه
در سیستمهای بیدرنگ، زمان نقش حیاتی دارد. بسیاری از فرآیندها باید در دورههای زمانی دقیق یا در زمان خاصی انجام شوند؛ مثل:
- روشن و خاموش کردن LED در بازههای معین،
- خواندن سنسور هر ۱۰۰ میلیثانیه،
- ارسال داده در هر ۱ ثانیه،
- یا بررسی Timeout برای ارتباطها.
در برنامههای سادهی bare-metal معمولاً از Delay مثل ()HAL_Delay استفاده میکنیم،
اما در RTOS، چون چندین Task همزمان فعال هستند، توقف CPU باعث توقف کل سیستم میشود.
به همین دلیل از تایمرهای نرمافزاری (Software Timers) استفاده میکنیم تا عملیات زمانبندی را بدون بلاک کردن تسکها انجام دهیم
Tick Timer در واقع «ضربان قلب» سیستم FreeRTOS است و تمام زمانبندیها به آن وابستهاند.
هر بار که وقفهی Tick اتفاق میافتد، RTOS زمانبندی تسکها، محاسبهی تاخیرها (vTaskDelay) و اجرای تایمرهای نرمافزاری را بهروزرسانی میکند.
اگر نرخ Tick خیلی پایین باشد، دقت زمانی سیستم کاهش مییابد؛ و اگر خیلی بالا تنظیم شود، وقفههای زیاد باعث افزایش بار پردازنده میشود.
بنابراین انتخاب مقدار مناسب برای configTICK_RATE_HZ (معمولاً بین 1000 تا 2000 هرتز) بسیار مهم است تا هم دقت زمانی مناسب حفظ شود و هم عملکرد CPU مختل نشود.
در سیستمهای کممصرف نیز میتوان از حالت Tickless Idle استفاده کرد تا در زمان بیکاری CPU، وقفههای Tick غیرفعال شده و مصرف انرژی کاهش یابد..
Tick Timer قلب زمانبندی در RTOS
تمام مکانیزمهای زمانی در FreeRTOS بر پایهی Tick Interrupt کار میکنند.
Tick Timer یک وقفهی سختافزاری (معمولاً SysTick در ARM Cortex-M) است که در بازههای زمانی ثابت (مثلاً هر ۱ میلیثانیه) فعال میشود.
در هر Tick:
- شمارندهی سیستم (xTickCount) افزایش مییابد.
- Scheduler بررسی میکند آیا Taskی باید از حالت Blocked به Ready تغییر وضعیت دهد یا نه.
- اگر تایمر نرمافزاری فعال باشد، زمان باقیماندهاش کم میشود.
پارامتر مربوطه در FreeRTOSConfig.h:
#define configTICK_RATE_HZ (1000) // یعنی هر 1ms یک tick
بنابراین اگر ۱۰۰۰ تنظیم شده باشد، هر ۱۰۰۰ tick معادل ۱ ثانیه است.
مفهوم Delay در FreeRTOS
vTaskDelay():
یک تسک را برای مدت زمان مشخص متوقف میکند (در حالت Blocked قرار میدهد).
در این حالت CPU آزاد میشود و سایر Taskها میتوانند اجرا شوند.
مثال:
for(;;)
{
toggleLed();
vTaskDelay(pdMS_TO_TICKS(500)); // تأخیر 500ms
}- تسک پس از گذشت ۵۰۰ میلیثانیه به حالت Ready برمیگردد.
- تابع pdMS_TO_TICKS() مدتزمان میلیثانیهای را به تعداد tick تبدیل میکند.
vTaskDelayUntil()
این تابع برای تسکهایی مناسب است که باید در فواصل زمانی دقیق و ثابت (Periodic Task) اجرا شوند،
صرفنظر از اینکه اجرای تابع قبلی چقدر طول کشیده است.
مثال:
TickType_t xLastWakeTime = xTaskGetTickCount();
for(;;)
{
readSensor();
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(100));
}این کد تضمین میکند که اجرای تابع readSensor() دقیقاً هر ۱۰۰ میلیثانیه تکرار شود،
حتی اگر اجرای آن چند میلیثانیه طول بکشد.
تایمر نرمافزاری (Software Timer)
تایمر نرمافزاری یک شیء RTOS است که به شما اجازه میدهد یک تابع (Callback) را بعد از گذشت مدتزمان مشخص بدون نیاز به اجرای دائمی یک Task.
اجراکنید.
در واقع تایمر نرمافزاری مانند یک تسک پنهان در پسزمینه عمل میکند که وظیفهاش اجرای خودکار یک تابع خاص در زمان مشخص است.
این مکانیزم بهویژه در مواقعی مفید است که بخواهیم عملیاتی را در بازههای زمانی منظم انجام دهیم، اما نمیخواهیم برای آن یک Task جداگانه با حلقهی بینهایت بسازیم.
همچنین تایمرهای نرمافزاری کاملاً مستقل از اولویت تسکها کار میکنند و اجرای آنها توسط Timer Daemon Task کنترل میشود.
با استفاده از این قابلیت میتوان کارهایی مثل تایماوت (Timeout)، بررسی سلامت سیستم، یا ارسال دورهای داده را بدون اشغال CPU و حافظهی اضافی پیادهسازی کرد.
| ویژگی | Delay (vTaskDelay) | Software Timer |
| سطح اجرا | داخل Task | مستقل از Task |
| بلاککردن CPU | بله (Task در حالت Blocked میرود) | خیر |
| فراخوانی تابع خاص | توسط Task | توسط Callback |
| دقت زمانی | وابسته به Scheduler | وابسته به Tick |
| کاربرد | زمانبندی تسکها | انجام خودکار یک کار در زمان مشخص |
ساختار تایمر نرمافزاری
هر تایمر شامل این اجزا است:
- Callback Function: تابعی که در زمان فعال شدن تایمر اجرا میشود.
- Period: مدت زمان بین هر اجرا (برحسب Tick).
- Mode: حالت اجرای یکبار (One-shot) یا تکراری (Auto-reload).
- Timer ID: شناسهی عددی برای شناسایی تایمر.
ایجاد و راهاندازی تایمر در FreeRTOS
ایجاد تایمر:
TimerHandle_t xTimer = xTimerCreate(
"LED_Timer", // نام تایمر (اختیاری)
pdMS_TO_TICKS(1000), // پریود: ۱ ثانیه
pdTRUE, // pdTRUE = تکراری (Auto-reload)
(void*)0, // Timer ID (اختیاری)
vTimerCallback // تابع callback
);شروع تایمر
xTimerStart(xTimer, 0);تابع Callback
void vTimerCallback(TimerHandle_t xTimer)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}نتیجه: LED هر ۱ ثانیه یکبار چشمک میزند، بدون نیاز به تسک جداگانه.
1-7 توقف، ریست و حذف تایمر
| تابع | کاربرد |
| xTimerStop(xTimer, 0) | توقف تایمر |
| xTimerReset(xTimer, 0) | شروع مجدد تایمر از ابتدا |
| xTimerChangePeriod(xTimer, pdMS_TO_TICKS(500), 0) | تغییر پریود در زمان اجرا |
| xTimerDelete(xTimer, 0) | حذف تایمر و آزادسازی حافظه |
1-8 انواع تایمر در FreeRTOS
| نوع تایمر | توضیح |
| One-shot Timer | فقط یکبار بعد از زمان مشخص اجرا میشود. پس از Callback، متوقف میشود. |
| Auto-reload Timer | پس از هر بار اجرا، خودکار دوباره شروع میشود. برای کارهای دورهای استفاده میشود. |
// تایمر یکبار مصرف
xTimerCreate("Single", pdMS_TO_TICKS(2000), pdFALSE, 0, vOnceTimerCallback);
// تایمر تکرارشونده
xTimerCreate("Repeat", pdMS_TO_TICKS(1000), pdTRUE, 0, vRepeatTimerCallback);
تایمر Daemon Task (سرویس مرکزی تایمرها)
FreeRTOS برای مدیریت تمام تایمرهای نرمافزاری از یک Task داخلی به نام Timer Service (یا Daemon Task) استفاده میکند.این تسک در زمان راهاندازی Scheduler بهصورت خودکار ایجاد میشود.
تمام تایمرها دستورات خود را (Start, Stop, Reset) در یک صف فرمان تایمر (Timer Command Queue) قرار میدهند و Daemon Task این دستورات را در زمان مناسب اجرا میکند.به همین دلیل تایمرها همیشه در پسزمینه اجرا میشوند و هیچ تسکی را بلاک نمیکنند.
این تسک در واقع بهصورت مداوم صف فرمان تایمرها را بررسی میکند و هر زمان که زمان اجرای یک تایمر فرا برسد، تابع Callback مربوط به آن را فراخوانی میکند.
تمام دستورات مربوط به تایمرها (مثل شروع، توقف یا تغییر پریود) بهصورت غیرهمزمان در این صف قرار میگیرند تا ترتیب اجرا حفظ شود و تعارضی بین Taskها بهوجود نیاید.
بهاینترتیب، اجرای تایمرها در سیستم کاملاً منسجم، ایمن و مستقل از سایر تسکها انجام میشود.
در صورت افزایش تعداد تایمرها یا بار پردازشی بالا، میتوان اولویت Daemon Task را در فایل تنظیمات FreeRTOS تغییر داد تا پاسخگویی به تایمرها سریعتر انجام شود.
نمونهی عملی: چشمک زدن LED با تایمر
گام ۱ – تعریف تایمر:
TimerHandle_t xLedTimer;گام ۲ – ساخت تایمر در MX_FREERTOS_Init():
xLedTimer = xTimerCreate("LEDTimer", pdMS_TO_TICKS(500), pdTRUE, 0, vLedTimerCallback);گام ۳ – تابع Callback:
void vLedTimerCallback(TimerHandle_t xTimer)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // LED
}گام ۴ – شروع تایمر در تسک اولیه:
xTimerStart(xLedTimer, 0);حالا LED هر ۵۰۰ میلیثانیه چشمک میزند،
بدون نیاز به هیچ ()vTaskDelay و بدون بلاک کردن CPU.
مدیریت چند تایمر همزمان
میتوان چندین تایمر مختلف ساخت و هرکدام را با Callback مخصوص خود اجرا کرد.
FreeRTOS آنها را در یک صف مشترک مدیریت میکند.
مثلاً:
xTimer1 = xTimerCreate("T1", pdMS_TO_TICKS(1000), pdTRUE, 0, vCallback1);
xTimer2 = xTimerCreate("T2", pdMS_TO_TICKS(2000), pdTRUE, 0, vCallback2);
xTimerStart(xTimer1, 0);
xTimerStart(xTimer2, 0);تایمر اول هر ثانیه اجرا میشود و دومی هر دو ثانیه — کاملاً مستقل از هم.
نکات مهم در استفاده از تایمرها
- تابع Callback نباید از APIهای بلاککننده مثل vTaskDelay() یا xQueueReceive() استفاده کند،چون در داخل Daemon Task اجرا میشود.
- Callback باید سریع تمام شود؛ اجرای طولانی باعث تأخیر در سایر تایمرها میشود.
- بهتر است کارهای سنگین را در Callback فقط علامتگذاری کرده و اجرای اصلی را در Task انجامدهید.
مثلاً:
void vTimerCallback(TimerHandle_t xTimer)
{
xSemaphoreGiveFromISR(xTaskSemaphore, NULL);
}تایمرها به دقت tick وابستهاند. اگر دقت بالاتری میخواهی (مثلاً زیر ۱ میلیثانیه)، باید از تایمر سختافزاری (TIMx) استفاده کنی
تایمرها در CubeMX
در محیط STM32CubeMX → Middleware → FreeRTOS → Timers and Semaphores
میتوان تایمرها را با نام، دوره، حالت و تابع Callback بهصورت گرافیکی تعریف کرد.
xLedTimerHandle = osTimerNew(vLedTimerCallback, osTimerPeriodic, NULL, &xLedTimer_attributes);
osTimerStart(xLedTimerHandle, 500);که معادل است با:
xTimerCreate("LED", pdMS_TO_TICKS(500), pdTRUE, 0, vLedTimerCallback);
xTimerStart(xLedTimer, 0);مثال
با این کد، سه تا سناریو یکجا فعال میشن: LED هر ۱ ثانیه چشمک میزنه، بعد از ۵ ثانیه خاموش میشه، و دو پیام با دورههای متفاوت از UART می رسد.
این مثال ۴ تا تایمر نرمافزاری درست میکنه:
- تایمر Periodic برای چشمکزدن LED هر ۱ ثانیه
- تایمر One-shot که ۵ ثانیه بعد از شروع، LED رو خاموش میکنه
- دو تایمر Periodic با دورههای متفاوت که هر کدوم یک پیام روی UART میفرستند
تعریف هندل تایمرها و پروتوتایپ Callbackها:
// Timer handles
osTimerId_t ledToggleTimerHandle;
osTimerId_t ledOffTimerHandle;
osTimerId_t msgFastTimerHandle;
osTimerId_t msgSlowTimerHandle;
// Callbacks
static void ledToggle_cb(void *argument);
static void ledOff_cb(void *argument);
static void msgFast_cb(void *argument);
static void msgSlow_cb(void *argument);ساخت و استارت تایمرها:
void MX_FREERTOS_Init(void)
{
// ... بقیه ساخت Mutex/Queue/Task های تو
// 1) LED toggle هر 1000ms (Periodic)
ledToggleTimerHandle = osTimerNew(ledToggle_cb, osTimerPeriodic, NULL, NULL);
osTimerStart(ledToggleTimerHandle, 1000);
// 2) LED one-shot بعد از 5000ms خاموش شود
ledOffTimerHandle = osTimerNew(ledOff_cb, osTimerOnce, NULL, NULL);
osTimerStart(ledOffTimerHandle, 5000);
// 3) پیام سریع (مثلاً هر 700ms) و پیام کند (مثلاً هر 2000ms)
msgFastTimerHandle = osTimerNew(msgFast_cb, osTimerPeriodic, NULL, NULL);
osTimerStart(msgFastTimerHandle, 700);
msgSlowTimerHandle = osTimerNew(msgSlow_cb, osTimerPeriodic, NULL, NULL);
osTimerStart(msgSlowTimerHandle, 2000);
}نکته: پارامتر دوم osTimerPeriodic یعنی Auto-reload و osTimerOnce یعنی One-shot.
کالبکها:
این کالبکها در Timer Daemon Task اجرا میشن؛ بهتره خیلی کوتاه باشن.
برای سادگی همینجا از HAL_UART_Transmit استفاده میکنیم (پیامهای خیلی کوتاه). اگر پروژه شلوغ شد، بهتره فقط «سیگنال» داده و ارسال UART را به یک Task. سپرد.
static void ledToggle_cb(void *argument)
{
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
static void ledOff_cb(void *argument)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
}
static void msgFast_cb(void *argument)
{
const char msg[] = "FAST timer: 700ms\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)msg, sizeof(msg)-1, 50);
}
static void msgSlow_cb(void *argument)
{
const char msg[] = "SLOW timer: 2000ms\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)msg, sizeof(msg)-1, 50);
}
جمعبندی
میتوان چندین تایمر مختلف ساخت و هرکدام را با Callback مخصوص خود اجرا کرد.
FreeRTOS آنها را در یک صف مشترک مدیریت میکند.
- تمام عملیات زمانی FreeRTOS بر پایهی Tick Interrupt است.
- از
vTaskDelay()برای کنترل فاصلهی اجرای تسکها استفاده میشود. - Software Timer برای اجرای خودکار توابع در زمان مشخص به کار میرود.
- تایمرها در پسزمینه اجرا میشوند و Taskها را بلاک نمیکنند.
با ترکیب Timer و Event Group میتوان سیستمهای کنترلی دقیق و غیرمسدود ساخت.
منابع
Mastering the FreeRTOS Kernel – Richard Barry
Hands-On RTOS with Microcontrollers – Brian Amos
UM1722 – Developing Applications on STM32Cube with RTOS
FreeRTOS Reference Manual – Chapter 5 (Software Timer API)