freertos
فهرست مطالب
- ۱.۱. چرا RTOS؟ مقایسه با روش حلقه بینهایت (Super Loop)
- ۱.۲. معرفی FreeRTOS: تاریخچه، مزایا و کاربردهای صنعتی
- ۱.۳. آشنایی با سختافزار: معرفی بردهای سری STM32F3xx
- ۱.۴. نصب و راهاندازی ابزارها: STM32CubeIDE و درایورهای ST-LINK
- ۲.۱. ایجاد پروژه با STM32CubeMX: تنظیمات اولیه
- ۲.۲. پیکربندی سیستم: فعالسازی Serial Wire Debug (SWD)
- ۲.۳. پیکربندی کلاک (Clock) و منبع زمان (Timebase Source)
- ۲.۴. فعالسازی و تنظیمات حیاتی FreeRTOS
- ۲.۵. تولید و بررسی ساختار کد
- ۳.۱. تعریف وظیفه و چرخه حیات آن
- ۳.۲. ایجاد وظایف به صورت استاتیک و داینامیک
- پروژه عملی ۱: چشمک زدن سه LED با سه نرخ زمانی متفاوت
- پروژه عملی ۲: کنترل یک وظیفه (Suspend/Resume) توسط وظیفهای دیگر
- ۴.۱. الگوریتم زمانبندی مبتنی بر اولویت
- ۴.۲. توابع تاخیر: vTaskDelay در مقابل vTaskDelayUntil
- پروژه عملی ۳: کنترل سرعت چشمک زدن LED توسط وظیفهای با اولویت بالاتر
- پروژه عملی ۴: تولید موج مربعی دقیق با `vTaskDelayUntil`
- ۵.۱. ارسال و دریافت داده بین وظایف
- پروژه عملی ۵: ارسال دادههای سنسور از یک وظیفه به وظیفه دیگر برای کنترل PWM
- پروژه عملی ۶: ارسال ساختار داده (struct) از طریق صف
- ۶.۱. سمافور باینری در مقابل سمافور شمارشی
- پروژه عملی ۷: همگامسازی یک وظیفه با وقفه کلید فشاری (سمافور باینری)
- پروژه عملی ۸: مدیریت منابع محدود (سمافور شمارشی)
- ۷.۱. حفاظت از منابع مشترک و مشکل Race Condition
- ۷.۲. میوتکس استاندارد در مقابل میوتکس بازگشتی (Recursive)
- ۷.۳. بخشهای بحرانی (taskENTER_CRITICAL): سریعترین روش حفاظت
- ۷.۴. نمایش و حل مشکل وارونگی اولویت (Priority Inversion)
- پروژه عملی ۹: حفاظت از پورت سریال (UART) با میوتکس
- پروژه عملی ۱۰: نمایش عملکرد میوتکس بازگشتی
- ۸.۱. معرفی سیگنالها: روشی سبک و سریع برای همگامسازی
- پروژه عملی ۱۱: جایگزین کردن سمافور وقفه کلید با یک سیگنال مستقیم
- پروژه عملی ۱۲: ارسال مقدار عددی از یک وقفه ADC به یک وظیفه از طریق سیگنال
- فصل ۹: گروههای رویداد (Event Groups)
- فصل ۱۰: تایمرهای نرمافزاری (Software Timers)
- فصل ۱۱: مدیریت حافظه و وقفهها
- فصل ۱۲: ابزارهای اشکالزدایی
- فصل ۱۳: بهینهسازی و نکات نهایی
۱.۱. چرا RTOS؟ مقایسه با روش حلقه بینهایت (Super Loop)
اگر پیش از این برای میکروکنترلرها برنامهنویسی کرده باشید، به احتمال زیاد با ساختار حلقه بینهایت (Super Loop) آشنا هستید. این روش، سادهترین و مستقیمترین راه برای نوشتن یک برنامه برای یک سیستم نهفته است. ساختار آن به شکل زیر است:
void main(void) {
// 1. مقداردهی اولیه سختافزار و پریفرالها
initialize_hardware();
initialize_peripherals();
// 2. ورود به حلقه بینهایت
while (1) {
// 3. اجرای وظایف به صورت متوالی
task_A();
task_B();
task_C();
// ... و غیره
}
}این رویکرد برای کاربردهای ساده مانند خواندن یک سنسور و روشن کردن یک LED کاملاً کارآمد است. اما با افزایش پیچیدگی سیستم، معایب آن به سرعت آشکار میشود:
- عدم پاسخگویی (Unresponsiveness): اگر `task_A` یک وظیفه زمانبر باشد (مثلاً منتظر دریافت داده از یک ارتباط کند باشد)، اجرای `task_B` و `task_C` به تأخیر میافتد. این موضوع در سیستمهایی که نیاز به پاسخ سریع به رویدادها دارند، یک مشکل جدی است.
- مدیریت زمان پیچیده: پیادهسازی وظایفی که باید در بازههای زمانی دقیق و متفاوت اجرا شوند (مثلاً یکی هر ۱۰ میلیثانیه و دیگری هر ۱ ثانیه) بسیار دشوار است و اغلب به استفاده از فلگها و شمارندههای متعدد در حلقهی اصلی منجر میشود که کد را پیچیده و غیرقابل نگهداری میکند.
- کد اسپاگتی (Spaghetti Code): با افزایش تعداد وظایف و وابستگیهای بین آنها، حلقهی `main` به سرعت به کدی طولانی، تو در تو و غیرقابل فهم تبدیل میشود.
راهحل: سیستمعامل بیدرنگ (RTOS)
یک RTOS این مشکلات را با معرفی مفهوم چندوظیفهای (Multitasking) و زمانبندی (Scheduling) حل میکند. به جای اجرای متوالی وظایف، هر وظیفه به عنوان یک ریسه (Thread) یا وظیفه (Task) مستقل در نظر گرفته میشود. یک بخش مرکزی در RTOS به نام زمانبند (Scheduler) مسئولیت مدیریت اجرا و جابجایی بین این وظایف را بر عهده دارد.
مهمترین مزایای استفاده از RTOS عبارتند از:
- مدیریت اولویت: شما میتوانید برای وظایف حیاتیتر، اولویت بالاتری تعیین کنید. زمانبند تضمین میکند که وظیفه با بالاترین اولویت که آماده اجراست، همیشه در حال اجرا باشد. این ویژگی برای سیستمهای بیدرنگ که باید به رویدادهای خاصی در یک محدودیت زمانی پاسخ دهند، ضروری است.
- سازماندهی کد: هر وظیفه منطق و عملکرد مستقلی دارد و در تابع مربوط به خود پیادهسازی میشود. این کار باعث ماژولار شدن، خوانایی و نگهداری آسانتر کد میشود.
- مدیریت منابع مشترک: RTOS ابزارهایی مانند میوتکس (Mutex) و سمافور (Semaphore) برای مدیریت دسترسی همزمان چند وظیفه به منابع مشترک (مانند پورت سریال یا یک متغیر سراسری) فراهم میکند و از بروز مشکلاتی مانند Race Condition جلوگیری میکند.
- سادگی در مدیریت زمان: دیگر نیازی به محاسبات پیچیده زمانی در حلقه اصلی نیست. RTOS توابع تأخیر دقیقی مانند `vTaskDelay` را فراهم میکند که وظیفه را برای مدت مشخصی مسدود (Block) کرده و به وظایف دیگر اجازه اجرا میدهد.
۱.۲. معرفی FreeRTOS: تاریخچه، مزایا و کاربردهای صنعتی
FreeRTOS یک سیستمعامل بیدرنگ متنباز، محبوب و بسیار کوچک برای میکروکنترلرها است. این RTOS توسط Richard Barry در سال ۲۰۰۳ توسعه داده شد و به دلیل سادگی، پایداری و ردپای حافظه (Memory Footprint) بسیار کم، به سرعت به یکی از استانداردهای صنعتی تبدیل شد. در سال ۲۰۱۷، شرکت آمازون وب سرویسز (AWS) مدیریت این پروژه را بر عهده گرفت و آن را به عنوان بخشی از اکوسیستم اینترنت اشیاء (IoT) خود توسعه داد.
مزایای کلیدی FreeRTOS:
- متنباز و رایگان: FreeRTOS تحت لایسنس MIT منتشر میشود که استفاده از آن را در پروژههای تجاری و شخصی بدون هیچ محدودیتی ممکن میسازد.
- بسیار کوچک و کارآمد: هسته FreeRTOS تنها از چند فایل C تشکیل شده و میتواند در میکروکنترلرهایی با حافظه RAM و Flash بسیار محدود (حتی چند کیلوبایت) اجرا شود.
- قابلیت حمل بالا (Highly Portable): کد FreeRTOS به گونهای نوشته شده که وابستگی کمی به معماری سختافزار دارد و به راحتی برای دهها خانواده مختلف از میکروکنترلرها پورت شده است.
- پشتیبانی گسترده: جامعه کاربری بزرگ و فعال و پشتیبانی شرکتهای بزرگی مانند STMicroelectronics و AWS، منابع آموزشی، مثالها و راهکارهای فراوانی را برای آن فراهم کرده است.
- پایداری اثبات شده: FreeRTOS سالهاست که در میلیونها محصول صنعتی و تجاری، از گجتهای پوشیدنی و لوازم خانگی هوشمند گرفته تا تجهیزات پزشکی و سیستمهای کنترل صنعتی، با موفقیت به کار گرفته شده است.
۱.۳. آشنایی با سختافزار: معرفی بردهای سری STM32F3xx
اگرچه FreeRTOS بر روی طیف وسیعی از میکروکنترلرها قابل اجراست، خانواده STM32 از شرکت STMicroelectronics به دلیل قدرت، تنوع و پشتیبانی عالی، یکی از بهترین گزینهها برای یادگیری و ساخت پروژههای حرفهای است. در این کتاب، ما از بردهای توسعه سری STM32F3xx، به خصوص برد STM32F3 Discovery، استفاده خواهیم کرد.
خانواده STM32F3 برای کاربردهای سیگنال ترکیبی (Mixed-Signal) و کنترلی طراحی شده و ویژگیهای منحصربهفردی دارد که آن را برای پروژههای RTOS ایدهآل میسازد:
- هسته قدرتمند Cortex-M4F: این هسته علاوه بر دستورالعملهای پردازش سیگنال دیجیتال (DSP)، دارای واحد ممیز شناور (FPU) است که محاسبات ریاضی سنگین را سرعت میبخشد.
- پریفرالهای آنالوگ پیشرفته: این سری شامل مبدلهای آنالوگ به دیجیتال (ADC) و دیجیتال به آنالوگ (DAC) بسیار سریع، مقایسهگرها (Comparators) و تقویتکنندههای عملیاتی (Op-Amps) داخلی است.
- حافظه CCM (Core Coupled Memory): بخشی از حافظه SRAM با سرعت بسیار بالا که مستقیماً به هسته پردازنده متصل است و میتوان از آن برای اجرای کدهای بسیار حیاتی با تأخیر صفر استفاده کرد.
- تایمرهای پیشرفته: این میکروکنترلرها دارای تایمرهایی با وضوح بالا برای تولید دقیق سیگنالهای PWM و کاربردهای کنترل موتور هستند.
برد STM32F3 Discovery یک گزینه ارزانقیمت و فوقالعاده است که علاوه بر میکروکنترلر STM32F303، شامل یک مجموعه کامل ۹ محوره از سنسورهای حرکتی (شتابسنج، ژیروسکوپ و مغناطیسسنج) و تعدادی LED برای شروع سریع پروژههاست.
۱.۴. نصب و راهاندازی ابزارها: STM32CubeIDE و درایورهای ST-LINK
برای برنامهنویسی و اشکالزدایی پروژهها، به یک محیط توسعه یکپارچه (IDE) نیاز داریم. در حالی که ابزارهای مختلفی وجود دارد، ما از STM32CubeIDE استفاده خواهیم کرد. این نرمافزار رسمی و رایگان از شرکت ST، تمام ابزارهای لازم را در یک بسته واحد جمعآوری کرده است.
مراحل نصب:
- دانلود STM32CubeIDE: به وبسایت رسمی STMicroelectronics مراجعه کرده و آخرین نسخه STM32CubeIDE را متناسب با سیستمعامل خود (Windows, macOS, Linux) دانلود کنید.
- نصب نرمافزار: مراحل نصب را مطابق دستورالعملها دنبال کنید. این فرآیند ساده و سرراست است.
- درایور ST-LINK: بردهای Discovery و Nucleo دارای یک دیباگر/پروگرامر داخلی به نام ST-LINK هستند. این رابط از طریق USB به کامپیوتر شما متصل میشود. معمولاً درایورهای آن به همراه STM32CubeIDE نصب میشوند.
- بهروزرسانی Firmware ST-LINK: این یک مرحله بسیار مهم است! اغلب بردهای توسعه با یک Firmware قدیمی برای ST-LINK عرضه میشوند. این موضوع میتواند باعث بروز مشکلات در هنگام دیباگ کردن شود. STM32CubeIDE معمولاً پس از اولین اتصال، به شما پیشنهاد بهروزرسانی Firmware را میدهد. حتماً این کار را انجام دهید.
با انجام این مراحل، محیط کاری شما برای شروع فصل بعدی و ساخت اولین پروژه FreeRTOS آماده است.
۲.۱. ایجاد پروژه با STM32CubeMX: تنظیمات اولیه
STM32CubeIDE ابزار همهکاره ماست. فرآیند ساخت پروژه را با این ابزار شروع میکنیم.
- اجرای STM32CubeIDE: نرمافزار را باز کنید.
- ایجاد پروژه جدید: از منوی `File`، گزینه `New > STM32 Project` را انتخاب کنید. با این کار، پنجره **Target Selector** باز میشود که به شما اجازه میدهد میکروکنترلر یا برد مورد نظر خود را انتخاب کنید.
- انتخاب برد: برای سادگی کار، به تب **Board Selector** بروید. در این قسمت، لیست تمام بردهای توسعه رسمی ST قرار دارد. در بخش `Commercial Part Number`، نام برد خود را تایپ کنید (مثلاً `STM32F3-Discovery`). با انتخاب برد، CubeMX به صورت خودکار بسیاری از تنظیمات اولیه پینها را برای شما انجام میدهد (مثلاً پینهای مربوط به دکمه کاربر یا LEDها). پس از انتخاب برد، روی دکمه `Next` کلیک کنید.
- نامگذاری پروژه: در پنجره بعدی، یک نام برای پروژه خود انتخاب کنید (مثلاً `MyFirstRTOSProject`). سایر تنظیمات را به صورت پیشفرض رها کرده و روی `Finish` کلیک کنید.
- مقداردهی اولیه پریفرالها: نرمافزار از شما سوالی مبنی بر مقداردهی اولیه تمام پریفرالها با حالت پیشفرضشان میپرسد (`Initialize all peripherals with their default Mode?`). روی `Yes` کلیک کنید.
اکنون STM32CubeIDE پروژه را ایجاد کرده و شما را به نمای گرافیکی **CubeMX** میبرد که در آن میتوانید میکروکنترلر و پریفرالهای آن را به صورت بصری مشاهده و پیکربندی کنید.
۲.۲. پیکربندی سیستم: فعالسازی Serial Wire Debug (SWD)
اولین و مهمترین قدم پس از ایجاد پروژه، فعال کردن رابط اشکالزدایی (Debug) است. بدون این رابط، امکان پروگرام کردن و اجرای قدم به قدم کد روی سختافزار وجود نخواهد داشت.
- در نمای گرافیکی CubeMX، در پنل سمت چپ زیر دستهبندی `System Core`، روی `SYS` کلیک کنید.
- در پنل `Mode` که در مرکز صفحه نمایان میشود، بخش `Debug` را پیدا کنید.
- از منوی کشویی، گزینه **`Serial Wire`** را انتخاب کنید.
با این کار، دو پین **PA13 (SWDIO)** و **PA14 (SWCLK)** به رنگ سبز درآمده و برای دیباگ رزرو میشوند. این رابط استاندارد دیباگ در پردازندههای Cortex-M است و تنها با دو سیم، امکانات کاملی برای اشکالزدایی فراهم میکند.
۲.۳. پیکربندی کلاک (Clock) و منبع زمان (Timebase Source)
قلب تپنده میکروکنترلر، سیستم کلاک آن است. پیکربندی صحیح آن برای عملکرد پایدار و بهینه سیستم ضروری است.
تنظیم کلاک اصلی
- به تب **Clock Configuration** در بالای صفحه بروید. در اینجا شما یک دیاگرام گرافیکی از تمام منابع کلاک و تقسیمکنندههای فرکانس را مشاهده میکنید.
- در بخش `Input frequency`، فرکانس کریستال خارجی برد خود (HSE) را وارد کنید. برای اکثر بردهای Discovery و Nucleo این مقدار **8MHz** است.
- در بخش `PLL Source Mux`، گزینه **HSE** را انتخاب کنید تا از کریستال خارجی به عنوان منبع اصلی PLL استفاده شود.
- در بخش `System Clock Mux`، گزینه **PLLCLK** را انتخاب کنید.
- در کادر `HCLK (MHz)`، بالاترین فرکانس ممکن برای میکروکنترلر خود را وارد کنید (مثلاً **72** برای STM32F303). CubeMX به صورت خودکار مقادیر تقسیمکنندهها و ضریب PLL را محاسبه و تنظیم میکند.
تنظیم منبع زمان (Timebase Source)
این بخش برای کار با FreeRTOS **بسیار حیاتی** است. به صورت پیشفرض، کتابخانه HAL از تایمر داخلی هسته به نام **SysTick** برای ایجاد تأخیرها و زمانبندیهای خود استفاده میکند. اما FreeRTOS نیز برای زمانبندی وظایف خود، به کنترل کامل SysTick نیاز دارد. برای جلوگیری از تداخل، بهترین کار این است که منبع زمانی HAL را به یک تایمر سختافزاری دیگر منتقل کنیم.
- به نمای `Pinout & Configuration` بازگردید.
- در بخش `System Core > SYS`، به قسمت `Timebase Source` بروید.
- از منوی کشویی، یک تایمر پایهای مانند **TIM6** یا **TIM7** را انتخاب کنید. این تایمرها ساده هستند و برای کاربردهای دیگر کمتر مورد استفاده قرار میگیرند، لذا انتخاب مناسبی برای این کار هستند.
۲.۴. فعالسازی و تنظیمات حیاتی FreeRTOS
حالا زمان اضافه کردن سیستمعامل به پروژه است.
- در پنل سمت چپ، زیر دستهبندی `Middleware`، روی **FREERTOS** کلیک کنید.
- در پنل `Mode`، از منوی کشویی `Interface`، گزینه **CMSIS_V2** را انتخاب کنید. این کار FreeRTOS را فعال کرده و از لایه سازگاری مدرن CMSIS استفاده میکند.
- به تب **Configuration** بروید. در اینجا پارامترهای مهم هسته FreeRTOS را تنظیم میکنیم.
- Kernel Settings: مقدار `TICK_RATE_HZ` را روی **1000** تنظیم کنید. این به معنای آن است که تیک سیستمعامل هر ۱ میلیثانیه یکبار اتفاق میافتد که یک استاندارد رایج است.
- Memory Management: بخش `Memory management scheme` را روی **heap_4** تنظیم کنید. این مدل حافظه، یکی از انعطافپذیرترین مدلهاست که به شما اجازه میدهد حافظه را به صورت پویا تخصیص داده و آزاد کنید و همچنین از تکهتکه شدن حافظه جلوگیری میکند.
- Heap Size: مقدار `TOTAL_HEAP_SIZE` را حداقل روی **4096** (۴ کیلوبایت) تنظیم کنید. این فضا برای ایجاد وظایف، صفها و دیگر اشیاء RTOS استفاده میشود. کمبود این حافظه یکی از دلایل رایج کرش کردن برنامههای RTOS است.
۲.۵. تولید و بررسی ساختار کد
پس از انجام تمام پیکربندیها، زمان آن رسیده که CubeMX کدهای لازم را برای ما تولید کند.
- در گوشه بالا-راست صفحه، روی آیکون چرخدنده (Generate Code) کلیک کنید.
- صبر کنید تا فرآیند تولید کد به پایان برسد. STM32CubeIDE به صورت خودکار کتابخانههای FreeRTOS و کدهای راهاندازی پریفرالها را به پروژه شما اضافه میکند.
- پس از اتمام، به نمای **Project Explorer** بروید. ساختار پروژه خود را بررسی کنید. پوشههای `Core` (شامل `main.c`) و `Middlewares/Third_Party/FreeRTOS` (شامل سورسکدهای سیستمعامل) ایجاد شدهاند.
- فایل `main.c` را باز کنید. به کامنتهای `/* USER CODE BEGIN ... */` و `/* USER CODE END ... */` توجه کنید. این یک قانون طلایی است: همیشه کدهای خود را فقط بین این بلاکها بنویسید. اگر کدی خارج از این بلاکها بنویسید، با هر بار تولید مجدد کد توسط CubeMX، کدهای شما حذف خواهند شد.
تبریک! شما با موفقیت اولین پروژه STM32 خود را با FreeRTOS پیکربندی کردید. پروژه شما اکنون آماده است تا در فصل بعدی، اولین وظایف (Tasks) خود را در آن ایجاد کرده و قدرت واقعی یک سیستمعامل بیدرنگ را مشاهده کنید.
۳.۱. تعریف وظیفه و چرخه حیات آن
در FreeRTOS، یک وظیفه (که گاهی ریسه یا Thread نیز نامیده میشود) در عمل یک تابع C است که هرگز به پایان نمیرسد (معمولاً شامل یک حلقه `while(1)` است). [cite_start]هر وظیفه پشته (Stack) مخصوص به خود را دارد که متغیرهای محلی و وضعیت اجرایی آن را ذخیره میکند[cite: 7752, 7746]. این استقلال به زمانبند (Scheduler) اجازه میدهد تا اجرای یک وظیفه را متوقف کرده، وضعیت آن را در پشتهاش ذخیره کند و اجرای وظیفهای دیگر را از سر بگیرد. این جابجایی سریع بین وظایف، توهم اجرای همزمان را روی یک هسته پردازنده ایجاد میکند.
[cite_start]هر وظیفه در هر لحظه در یکی از چهار وضعیت اصلی قرار دارد[cite: 7758, 4438]. درک این وضعیتها و انتقال بین آنها برای طراحی یک سیستم پایدار ضروری است:
- آماده (Ready): وظیفه آماده اجراست و منتظر است تا زمانبند، CPU را به آن اختصاص دهد. ممکن است چندین وظیفه همزمان در این وضعیت باشند. [cite_start]زمانبند همیشه وظیفهای با بالاترین اولویت را از این لیست برای اجرا انتخاب میکند[cite: 7760].
- در حال اجرا (Running): در هر لحظه، تنها یک وظیفه میتواند روی یک هسته CPU در حال اجرا باشد. [cite_start]این وظیفه در وضعیت Running قرار دارد[cite: 7760]. [cite_start]یک وظیفه در حال اجرا ممکن است توسط وظیفهای با اولویت بالاتر کنار گذاشته شود (preempted) و به وضعیت Ready بازگردد[cite: 7761].
- مسدود (Blocked): وظیفه در این وضعیت منتظر یک رویداد خارجی است. این رویداد میتواند سپری شدن زمان مشخصی (با فراخوانی `vTaskDelay`) یا در دسترس قرار گرفتن یک منبع (مانند داده در یک صف یا آزاد شدن یک سمافور) باشد. [cite_start]وظیفهای که در وضعیت Blocked قرار دارد، هیچ زمانی از CPU را مصرف نمیکند و به وظایف با اولویت پایینتر اجازه اجرا میدهد[cite: 7761].
- معلق (Suspended): وظیفه به صورت دستی توسط فراخوانی تابع `vTaskSuspend()` متوقف شده و از دسترس زمانبند خارج میشود. [cite_start]این وظیفه تا زمانی که به صورت صریح با `vTaskResume()` فراخوانی نشود، در همین وضعیت باقی میماند و هیچ رویدادی آن را به وضعیت Ready باز نمیگرداند[cite: 7763].
۳.۲. ایجاد وظایف به صورت استاتیک و داینامیک
FreeRTOS دو روش برای ایجاد وظایف ارائه میدهد که هر کدام مزایا و کاربردهای خاص خود را دارند.
ایجاد وظیفه به صورت داینامیک (Dynamic Task Creation)
این روش متداولترین راه برای ایجاد وظیفه است. [cite_start]در این حالت، حافظه مورد نیاز برای پشته و بلوک کنترل وظیفه (TCB - Task Control Block) از حافظه **Heap** که در فصل قبل پیکربندی کردیم، تخصیص داده میشود[cite: 7767]. تابع اصلی برای این کار `xTaskCreate()` است.
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode,
const char * pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void * pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * pvCreatedTask
);pvTaskCode: اشارهگری به تابعی که کد وظیفه را پیادهسازی میکند.pcName: یک نام متنی برای وظیفه که برای اشکالزدایی مفید است.usStackDepth: اندازه پشته وظیفه بر حسب **کلمه (Word)**. انتخاب مقدار صحیح برای این پارامتر بسیار مهم است؛ [cite_start]پشته کوچک باعث سرریز (Stack Overflow) و کرش سیستم میشود[cite: 7768].pvParameters: یک اشارهگر `void*` که میتوان از آن برای ارسال پارامتر به وظیفه هنگام ایجاد استفاده کرد.uxPriority: اولویت وظیفه (عدد بزرگتر به معنای اولویت بالاتر است).pvCreatedTask: یک اشارهگر اختیاری برای دریافت یک **Handle** یا شناسه برای وظیفه ایجاد شده. این شناسه برای کنترل وظیفه (مانند suspend یا resume کردن) از وظایف دیگر ضروری است.
ایجاد وظیفه به صورت استاتیک (Static Task Creation)
در این روش، شما به عنوان برنامهنویس، حافظه مورد نیاز برای پشته و TCB را خودتان به صورت یک آرایه سراسری یا استاتیک تعریف میکنید و آدرس آن را به تابع `xTaskCreateStatic()` میدهید. مزیت اصلی این روش این است که کاملاً از حافظه Heap بینیاز است و ریسک خطاهای مربوط به عدم موفقیت در تخصیص حافظه را از بین میبرد. این روش برای سیستمهای ایمنی-بحرانی (safety-critical) بسیار مناسب است.
پروژه عملی ۱: چشمک زدن سه LED با سه نرخ زمانی متفاوت
این پروژه "Hello, World!" دنیای RTOS است. ما سه وظیفه مجزا ایجاد میکنیم که هر کدام یک LED را با فرکانس متفاوتی کنترل میکنند. این پروژه به خوبی نشان میدهد که چگونه وظایف بدون مسدود کردن یکدیگر اجرا میشوند.
ابتدا مطمئن شوید که در CubeMX سه پین را به عنوان خروجی برای سه LED پیکربندی کردهاید. سپس کد زیر را در فایل `main.c` و در بلاک `/* USER CODE BEGIN 2 */` تا `/* USER CODE END 2 */` برای تعریف توابع وظیفه و در بلاک `/* USER CODE BEGIN 4 */` برای ایجاد آنها قرار دهید.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void BlinkTask_Red(void *argument)
{
for(;;)
{
HAL_GPIO_TogglePin(LED_RED_GPIO_Port, LED_RED_Pin);
vTaskDelay(pdMS_TO_TICKS(250)); // تاخیر 250 میلیثانیه
}
}
void BlinkTask_Green(void *argument)
{
for(;;)
{
HAL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
vTaskDelay(pdMS_TO_TICKS(500)); // تاخیر 500 میلیثانیه
}
}
void BlinkTask_Blue(void *argument)
{
for(;;)
{
HAL_GPIO_TogglePin(LED_BLUE_GPIO_Port, LED_BLUE_Pin);
vTaskDelay(pdMS_TO_TICKS(1000)); // تاخیر 1000 میلیثانیه
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
xTaskCreate(BlinkTask_Red, "Blink_Red", 128, NULL, 1, NULL);
xTaskCreate(BlinkTask_Green, "Blink_Green", 128, NULL, 1, NULL);
xTaskCreate(BlinkTask_Blue, "Blink_Blue", 128, NULL, 1, NULL);
/* USER CODE END 2 */
پس از کامپایل و اجرای این کد، خواهید دید که هر سه LED به صورت کاملاً مستقل و با نرخهای زمانی متفاوت چشمک میزنند. تابع `vTaskDelay()` وظیفه را به وضعیت Blocked میبرد و به زمانبند اجازه میدهد تا وظایف دیگر را اجرا کند. ماکروی `pdMS_TO_TICKS()` نیز به راحتی زمان میلیثانیه را به تعداد تیکهای مورد نیاز سیستمعامل تبدیل میکند.
پروژه عملی ۲: کنترل یک وظیفه (Suspend/Resume) توسط وظیفهای دیگر
در این پروژه یاد میگیریم که چگونه از یک وظیفه برای کنترل اجرای وظیفهای دیگر استفاده کنیم. ما یک وظیفه برای چشمک زدن یک LED و وظیفهای دیگر برای نظارت بر دکمه کاربر ایجاد میکنیم. با هر بار فشرده شدن دکمه، وظیفه چشمکزن متوقف (Suspend) یا از سر گرفته (Resume) میشود.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
TaskHandle_t blinkTaskHandle = NULL; // متغیر برای ذخیره شناسه وظیفه
void BlinkerTask(void *argument)
{
for(;;)
{
HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin); // LD2 همان LED سبز است
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void ControlTask(void *argument)
{
for(;;)
{
// منتظر فشرده شدن دکمه کاربر (B1)
if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_RESET)
{
// Debounce: کمی تاخیر برای جلوگیری از خطاهای ناشی از لرزش کلید
vTaskDelay(pdMS_TO_TICKS(50));
eTaskState taskState = eTaskGetState(blinkTaskHandle);
if (taskState != eSuspended)
{
// وظیفه را معلق کن
vTaskSuspend(blinkTaskHandle);
}
else
{
// وظیفه را از سر بگیر
vTaskResume(blinkTaskHandle);
}
// منتظر بمان تا کاربر دست خود را از روی دکمه بردارد
while(HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_RESET)
{
vTaskDelay(pdMS_TO_TICKS(50));
}
}
vTaskDelay(pdMS_TO_TICKS(10)); // هر 10 میلیثانیه دکمه را چک کن
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
// ایجاد وظیفه چشمکزن و ذخیره شناسه آن
xTaskCreate(BlinkerTask, "Blinker", 128, NULL, 1, &blinkTaskHandle);
// ایجاد وظیفه کنترل
xTaskCreate(ControlTask, "Controller", 128, NULL, 2, NULL);
/* USER CODE END 2 */
در این مثال، هنگام ایجاد `BlinkerTask`، شناسه یا Handle آن را در متغیر `blinkTaskHandle` ذخیره کردیم. سپس در `ControlTask` از این شناسه برای فراخوانی توابع `vTaskSuspend` و `vTaskResume` استفاده کردیم. این پروژه به خوبی قدرت کنترل بین وظایف در FreeRTOS را به نمایش میگذارد.
۴.۱. الگوریتم زمانبندی مبتنی بر اولویت
زمانبند FreeRTOS از یک الگوریتم **پیشخریدنی (Pre-emptive) و مبتنی بر اولویت (Priority-Based)** استفاده میکند. بیایید این دو مفهوم را باز کنیم:
- [cite_start]مبتنی بر اولویت: قانون اصلی زمانبند بسیار ساده است: "همیشه وظیفهای با بالاترین اولویت که در وضعیت آماده (Ready) قرار دارد، اجرا میشود." [cite: 7755] شما هنگام ایجاد هر وظیفه، یک عدد به عنوان اولویت به آن اختصاص میدهید (از 0 به عنوان پایینترین اولویت تا `configMAX_PRIORITIES - 1` به عنوان بالاترین). زمانبند لیست وظایف آماده را بررسی کرده و CPU را به وظیفهای با بالاترین اولویت در آن لیست میدهد.
- [cite_start]پیشخریدنی: "پیشخرید" به این معناست که زمانبند میتواند اجرای یک وظیفه را به صورت اجباری متوقف کند تا وظیفهای با اولویت بالاتر را اجرا کند. [cite: 7756] فرض کنید `TaskA` با اولویت 1 در حال اجراست. ناگهان یک رویداد رخ میدهد (مثلاً دادهای از شبکه میرسد) و `TaskB` که اولویت 2 دارد، از وضعیت مسدود (Blocked) به وضعیت آماده (Ready) منتقل میشود. در این لحظه، زمانبند فوراً اجرای `TaskA` را متوقف کرده (آن را pre-empt میکند)، `TaskB` را به وضعیت در حال اجرا (Running) میبرد و پس از اتمام کار `TaskB` و بازگشت آن به وضعیت مسدود، اجرای `TaskA` را از همان جایی که متوقف شده بود، ادامه میدهد.
یک حالت خاص زمانی است که چند وظیفه با **اولویت یکسان** در وضعیت آماده باشند. در این حالت، اگر قابلیت **Time Slicing** (که به صورت پیشفرض فعال است) روشن باشد، زمانبند به صورت **نوبتگردشی (Round-Robin)** به هر کدام از این وظایف، یک برش زمانی (Time Slice) از CPU را اختصاص میدهد. طول هر برش زمانی توسط ثابت `configTICK_RATE_HZ` مشخص میشود.
۴.۲. توابع تاخیر: vTaskDelay در مقابل vTaskDelayUntil
ایجاد تاخیر یکی از رایجترین عملیات در سیستمهای نهفته است. FreeRTOS دو تابع اصلی برای این کار ارائه میدهد که عملکرد متفاوتی دارند و انتخاب صحیح بین آنها برای پیادهسازی وظایف دورهای (Periodic) حیاتی است.
`vTaskDelay()` - تاخیر نسبی
[cite_start]این تابع، وظیفه فراخواننده را برای تعداد مشخصی از "تیکهای" سیستمعامل به وضعیت **مسدود (Blocked)** میبرد. [cite: 7783] این تاخیر، یک تاخیر **نسبی** است؛ یعنی از لحظهای که تابع فراخوانی میشود، شمارش آغاز میگردد.
// وظیفه به مدت 100 تیک سیستمعامل مسدود میشود
vTaskDelay(100);
مشکل اصلی `vTaskDelay` برای کارهای دورهای، **ایجاد خطای انباشتی (Cumulative Error) یا لغزش (Drift)** است. فرض کنید میخواهید یک LED را هر 100 میلیثانیه یکبار تغییر وضعیت دهید:
for(;;) {
// کد مربوط به تغییر وضعیت LED (مثلاً 2 میلیثانیه طول میکشد)
Toggle_LED();
// تاخیر 100 میلیثانیه
vTaskDelay(pdMS_TO_TICKS(100));
}
در این حالت، دوره تناوب واقعی حلقه `100ms + 2ms` خواهد بود. این 2 میلیثانیه اضافی در هر بار اجرای حلقه انباشته شده و باعث میشود فرکانس اجرای وظیفه به مرور زمان از مقدار دقیق خود فاصله بگیرد.
`vTaskDelayUntil()` - تاخیر مطلق
این تابع برای حل مشکل خطای انباشتی و پیادهسازی وظایف دورهای **دقیق** طراحی شده است. `vTaskDelayUntil` وظیفه را تا یک **زمان مطلق** در آینده مسدود میکند، نه برای یک مدت زمان نسبی.
TickType_t xLastWakeTime;
const TickType_t xFrequency = pdMS_TO_TICKS(100); // دوره تناوب 100 میلیثانیه
// مقداردهی اولیه قبل از ورود به حلقه
xLastWakeTime = xTaskGetTickCount();
for(;;) {
// کد مربوط به تغییر وضعیت LED (مثلاً 2 میلیثانیه طول میکشد)
Toggle_LED();
// وظیفه تا زمان (xLastWakeTime + xFrequency) مسدود میشود
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
در هر بار فراخوانی، این تابع به طور خودکار زمان اجرای کد (`Toggle_LED`) را محاسبه کرده و وظیفه را فقط به اندازه زمان باقیمانده تا رسیدن به دوره تناوب 100 میلیثانیهای بعدی مسدود میکند. این کار باعث میشود دوره تناوب حلقه همیشه **دقیقاً** 100 میلیثانیه باقی بماند.
پروژه عملی ۳: کنترل سرعت چشمک زدن LED توسط وظیفهای با اولویت بالاتر
این پروژه به خوبی قدرت پیشخریدی (Pre-emption) را نشان میدهد. ما یک وظیفه با اولویت پایین برای چشمک زدن یک LED و یک وظیفه با اولویت بالا برای تغییر سرعت آن ایجاد میکنیم.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
// یک متغیر سراسری برای کنترل زمان تاخیر
volatile uint32_t blinkDelay = 1000; // شروع با تاخیر 1 ثانیه
void BlinkerTask(void *argument)
{
for(;;)
{
HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
// از متغیر سراسری برای تاخیر استفاده کن
vTaskDelay(pdMS_TO_TICKS(blinkDelay));
}
}
void ControlTask(void *argument)
{
for(;;)
{
// هر 5 ثانیه یکبار اجرا شو
vTaskDelay(pdMS_TO_TICKS(5000));
// سرعت چشمک زدن را تغییر بده
if (blinkDelay == 1000) {
blinkDelay = 100; // سرعت را زیاد کن
} else {
blinkDelay = 1000; // سرعت را کم کن
}
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
// وظیفه چشمکزن با اولویت 1 (پایین)
xTaskCreate(BlinkerTask, "Blinker", 128, NULL, 1, NULL);
// وظیفه کنترل با اولویت 2 (بالا)
xTaskCreate(ControlTask, "Controller", 128, NULL, 2, NULL);
/* USER CODE END 2 */
پس از اجرا، LED با دوره تناوب 1 ثانیه چشمک میزند. بعد از 5 ثانیه، `ControlTask` (که اولویت بالاتری دارد) از وضعیت مسدود خارج شده و اجرای `BlinkerTask` را پیشخرید میکند. سپس `blinkDelay` را به 100 تغییر داده و دوباره به خواب میرود. بلافاصله پس از آن، `BlinkerTask` با سرعت جدید و بالاتر به کار خود ادامه میدهد.
پروژه عملی ۴: تولید موج مربعی دقیق با `vTaskDelayUntil`
در این پروژه، یک موج مربعی با فرکانس ثابت و دقیق تولید میکنیم. برای مشاهده نتیجه این پروژه، بهتر است از یک اسیلوسکوپ استفاده کنید تا پایداری فرکانس را به چشم ببینید.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
#define SQUARE_WAVE_HALF_PERIOD_MS 10 // دوره تناوب کل 20 میلیثانیه (50 هرتز)
void SquareWaveTask(void *argument)
{
// متغیر برای ذخیره زمان بیدار شدن قبلی
TickType_t xLastWakeTime;
const TickType_t xFrequency = pdMS_TO_TICKS(SQUARE_WAVE_HALF_PERIOD_MS);
// مقداردهی اولیه با زمان فعلی
xLastWakeTime = xTaskGetTickCount();
for(;;)
{
// پین را تغییر وضعیت بده
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // فرض میکنیم پین PA5 برای خروجی است
// تا نیم-پریود بعدی صبر کن
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
xTaskCreate(SquareWaveTask, "SquareWave", 128, NULL, 3, NULL);
/* USER CODE END 2 */
اگر خروجی این پین را با اسیلوسکوپ مشاهده کنید، خواهید دید که یک موج مربعی با فرکانس بسیار دقیق 50 هرتز تولید میشود. `vTaskDelayUntil` تضمین میکند که فاصله زمانی بین هر تغییر وضعیت پین دقیقاً 10 میلیثانیه است و هیچ خطای زمانی انباشته نمیشود.
۵.۱. ارسال و دریافت داده بین وظایف
یک صف در FreeRTOS مجموعهای از اسلاتهای حافظه با اندازه ثابت است که میتواند دادهها را در خود نگه دارد. این مکانیزم چندین ویژگی کلیدی دارد که آن را برای سیستمهای بیدرنگ ایدهآل میسازد:
- امنیت در محیط چندوظیفهای (Thread-Safe): تمام عملیات روی صفها به صورت ذاتی ایمن هستند. شما نیازی به استفاده از میوتکس یا سایر مکانیزمهای حفاظتی برای دسترسی به صف از وظایف مختلف ندارید. FreeRTOS این کار را در پسزمینه برای شما انجام میدهد.
- ساختار FIFO (First-In, First-Out): دادهها به همان ترتیبی که به صف ارسال میشوند، از آن دریافت میگردند. اولین دادهای که وارد میشود، اولین دادهای است که خارج خواهد شد.
- قابلیت مسدودسازی (Blocking): یک وظیفه میتواند برای دریافت داده از یک صف خالی، یا ارسال داده به یک صف پر، برای مدت زمان مشخصی **مسدود** شود. این ویژگی باعث میشود CPU به جای انتظار بیهوده، به وظایف دیگر اختصاص یابد.
- کپی کردن داده (Data Copying): برخلاف بسیاری از سیستمها که از اشارهگر استفاده میکنند، FreeRTOS دادهها را به داخل صف **کپی** میکند (Pass-by-Copy). این یعنی وظیفه فرستنده میتواند بلافاصله پس از ارسال، متغیری که داده را در آن نگه داشته بود، تغییر دهد یا از آن مجدداً استفاده کند، بدون آنکه نگران تغییر دادههای داخل صف باشد.
توابع اصلی API صفها
برای کار با صفها، از چند تابع کلیدی استفاده میکنیم:
- `xQueueCreate()`: برای ایجاد یک صف جدید.
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);uxQueueLength: حداکثر تعداد آیتمهایی که صف میتواند در خود نگه دارد.uxItemSize: اندازه هر آیتم (بر حسب بایت). برای ارسال یک عدد صحیح ۳۲ بیتی، این مقدار `sizeof(uint32_t)` خواهد بود.
- `xQueueSend()`: برای ارسال یک آیتم به انتهای صف.
BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait);pvItemToQueue: اشارهگری به متغیری که داده ارسالی در آن قرار دارد.xTicksToWait: حداکثر زمانی که وظیفه باید در حالت مسدود منتظر بماند تا در صف فضای خالی ایجاد شود. اگر `portMAX_DELAY` قرار داده شود، وظیفه تا ابد منتظر خواهد ماند.
- `xQueueReceive()`: برای دریافت یک آیتم از ابتدای صف.
BaseType_t xQueueReceive(QueueHandle_t xQueue, void * pvBuffer, TickType_t xTicksToWait);pvBuffer: اشارهگری به متغیری که داده دریافتی در آن کپی خواهد شد.xTicksToWait: حداکثر زمانی که وظیفه باید در حالت مسدود منتظر بماند تا دادهای در صف قرار گیرد.
پروژه عملی ۵: ارسال دادههای سنسور از یک وظیفه به وظیفه دیگر برای کنترل PWM
این پروژه یک الگوی طراحی بسیار رایج در سیستمهای نهفته است: یک وظیفه مسئول خواندن داده از یک سنسور است و وظیفه دیگر مسئول پردازش آن داده و کنترل یک خروجی. این جداسازی، کد را بسیار ماژولار و قابل نگهداری میکند.
**هدف:** خواندن مقدار یک پتانسیومتر با ADC در یک وظیفه و ارسال آن از طریق صف به وظیفه دیگر برای کنترل روشنایی یک LED با سیگنال PWM.
**پیکربندی CubeMX:**
- یک کانال ADC را فعال کنید (مثلاً `IN1` روی پین `PA0`).
- یک تایمر را در حالت PWM Generation فعال کنید (مثلاً `TIM2_CH1` روی پین `PA5`).
- FreeRTOS را فعال نگه دارید.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
QueueHandle_t adcQueue;
void SensorReadTask(void *argument)
{
uint32_t adcValue;
for(;;)
{
// شروع تبدیل ADC و انتظار برای اتمام آن
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
adcValue = HAL_ADC_GetValue(&hadc1);
// ارسال مقدار خوانده شده به صف
xQueueSend(adcQueue, &adcValue, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(100)); // هر 100 میلیثانیه یکبار بخوان
}
}
void PWMControlTask(void *argument)
{
uint32_t receivedAdcValue;
for(;;)
{
// منتظر دریافت داده از صف بمان
if (xQueueReceive(adcQueue, &receivedAdcValue, portMAX_DELAY) == pdPASS)
{
// مقدار Duty Cycle تایمر را بر اساس مقدار ADC تنظیم کن
// مقدار ADC 12 بیتی است (0-4095). تایمر را با پریود 4095 تنظیم کنید.
TIM2->CCR1 = receivedAdcValue;
}
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() قبل از حلقه while(1) ...
/* USER CODE BEGIN 2 */
// ایجاد صف برای نگهداری یک مقدار از نوع uint32_t
adcQueue = xQueueCreate(1, sizeof(uint32_t));
// فعال کردن PWM
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
// ایجاد وظایف
xTaskCreate(SensorReadTask, "SensorRead", 256, NULL, 1, NULL);
xTaskCreate(PWMControlTask, "PWMControl", 256, NULL, 2, NULL); // اولویت بالاتر
/* USER CODE END 2 */
پس از اجرای کد، با چرخاندن پتانسیومتر، روشنایی LED متصل به خروجی PWM تغییر خواهد کرد. `SensorReadTask` داده را تولید و بدون اطلاع از مصرفکننده، آن را در صف قرار میدهد. `PWMControlTask` نیز بدون اطلاع از منبع داده، منتظر آن میماند و کار خود را انجام میدهد. این **جداسازی (Decoupling)**، قدرت واقعی RTOS است.
پروژه عملی ۶: ارسال ساختار داده (struct) از طریق صف
قدرت صفها تنها به ارسال مقادیر عددی محدود نمیشود. شما میتوانید هر نوع دادهای، از جمله ساختارهای پیچیده (structs)، را از طریق صف ارسال کنید. این قابلیت برای ارسال بستههای داده منسجم بسیار کاربردی است.
**هدف:** ایجاد یک ساختار داده برای نگهداری اطلاعات چند سنسور (شبیهسازی شده) و ارسال آن بین دو وظیفه.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
// تعریف ساختار داده
typedef struct {
uint16_t temperature;
uint16_t humidity;
uint32_t pressure;
} SensorData_t;
QueueHandle_t sensorDataQueue;
void DataSourceTask(void *argument)
{
SensorData_t currentData;
for(;;)
{
// شبیهسازی خواندن داده از سنسورها
currentData.temperature = (rand() % 20) + 15; // دمای بین 15 تا 35
currentData.humidity = (rand() % 30) + 50; // رطوبت بین 50 تا 80
currentData.pressure = (rand() % 100) + 980; // فشار بین 980 تا 1080
// ارسال کل ساختار به صف
xQueueSend(sensorDataQueue, ¤tData, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(2000)); // هر 2 ثانیه یکبار
}
}
void DataProcessingTask(void *argument)
{
SensorData_t receivedData;
for(;;)
{
// منتظر دریافت ساختار داده از صف
if (xQueueReceive(sensorDataQueue, &receivedData, portMAX_DELAY) == pdPASS)
{
// در اینجا میتوانید دادهها را پردازش کنید
// برای مثال، ما آنها را چاپ میکنیم (نیاز به پیادهسازی پورت سریال دارد)
printf("Temp: %d C, Humidity: %d %%, Pressure: %ld hPa\r\n",
receivedData.temperature,
receivedData.humidity,
receivedData.pressure);
}
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
// ایجاد صف با اندازه کافی برای نگهداری 5 ساختار داده
sensorDataQueue = xQueueCreate(5, sizeof(SensorData_t));
// ایجاد وظایف
xTaskCreate(DataSourceTask, "DataSource", 256, NULL, 1, NULL);
xTaskCreate(DataProcessingTask, "DataProcess", 256, NULL, 1, NULL);
/* USER CODE END 2 */
نکته کلیدی در این پروژه، نحوه ایجاد صف است: `sizeof(SensorData_t)`. ما به FreeRTOS میگوییم که هر آیتم در این صف، به اندازه یک ساختار `SensorData_t` است. FreeRTOS به صورت خودکار کپی کردن این حجم از داده را مدیریت میکند و شما یک راه تمیز و کارآمد برای ارسال بستههای داده بین وظایف در اختیار دارید.
۶.۱. سمافور باینری در مقابل سمافور شمارشی
FreeRTOS دو نوع اصلی سمافور را پشتیبانی میکند که هر کدام برای سناریوهای متفاوتی طراحی شدهاند.
سمافور باینری (Binary Semaphore)
یک سمافور باینری را میتوان مانند یک پرچم یا یک "توکن" در نظر گرفت که تنها دو حالت دارد: **موجود (Available)** یا **ناموجود (Unavailable)**. این نوع سمافور برای همگامسازی یک وظیفه با یک رویداد، بهخصوص یک وقفه (Interrupt)، ایدهآل است.
- عملکرد: یک وظیفه تلاش میکند سمافور را "بگیرد" (Take). اگر سمافور موجود باشد، وظیفه آن را گرفته و به کار خود ادامه میدهد. اگر موجود نباشد، وظیفه به وضعیت مسدود (Blocked) میرود تا زمانی که وظیفه یا وقفه دیگری سمافور را "بدهد" (Give).
- کاربرد اصلی: پردازش معوق وقفه (Deferred Interrupt Processing). به جای انجام کارهای سنگین و زمانبر در داخل روتین سرویس وقفه (ISR)، روتین وقفه فقط سمافور را آزاد (Give) میکند و به سرعت خارج میشود. سپس یک وظیفه که منتظر آن سمافور بوده، از حالت مسدود خارج شده و پردازش اصلی را در محیط امن خود انجام میدهد.
- توابع API:
- `xSemaphoreCreateBinary()`: یک سمافور باینری ایجاد میکند.
- `xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait)`: برای گرفتن سمافور.
- `xSemaphoreGive(SemaphoreHandle_t xSemaphore)`: برای دادن (آزاد کردن) سمافور از یک وظیفه.
- `xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken)`: نسخه امن برای استفاده در داخل روتینهای وقفه.
سمافور شمارشی (Counting Semaphore)
یک سمافور شمارشی، برخلاف نوع باینری، میتواند چندین توکن را مدیریت کند. هنگام ایجاد آن، شما یک مقدار حداکثر و یک مقدار اولیه برای شمارنده آن مشخص میکنید.
- عملکرد: هر بار که یک وظیفه سمافور را "میگیرد" (Take)، شمارنده یکی کم میشود (تا زمانی که بزرگتر از صفر باشد). هر بار که سمافور "داده میشود" (Give)، شمارنده یکی زیاد میشود (تا سقف مقدار حداکثر). اگر یک وظیفه تلاش کند سمافوری را بگیرد که شمارنده آن صفر است، مسدود میشود.
- کاربرد اصلی: مدیریت دسترسی به مجموعهای از **منابع محدود و یکسان**. فرض کنید ۳ کانال ارتباطی یکسان دارید و ۵ وظیفه میخواهند از آنها استفاده کنند. با یک سمافور شمارشی با شمارنده ۳، میتوانید تضمین کنید که در هر لحظه حداکثر ۳ وظیفه به این کانالها دسترسی دارند.
- توابع API:
- `xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount)`: یک سمافور شمارشی ایجاد میکند.
- توابع `xSemaphoreTake` و `xSemaphoreGive` برای این نوع سمافور نیز کاربرد دارند.
پروژه عملی ۷: همگامسازی یک وظیفه با وقفه کلید فشاری (سمافور باینری)
این پروژه، کلاسیکترین کاربرد یک سمافور باینری را به نمایش میگذارد. ما یک وقفه خارجی برای کلید فشاری کاربر (B1) فعال میکنیم. روتین وقفه فقط سمافور را آزاد میکند و یک وظیفه مجزا که منتظر این سمافور است، یک LED را تغییر وضعیت میدهد.
**پیکربندی CubeMX:**
- پین مربوط به دکمه کاربر (معمولاً `PC13`) را به عنوان ورودی وقفه خارجی (`GPIO_EXTI13`) تنظیم کنید.
- در تب `System Core > NVIC`، تیک مربوط به وقفه `EXTI line[15:10]` را فعال کنید.
- یک پین را برای LED به عنوان خروجی تنظیم کنید.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
SemaphoreHandle_t buttonSemaphore;
void ButtonHandlerTask(void *argument)
{
for(;;)
{
// منتظر گرفتن سمافور بمان (تا ابد)
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY) == pdTRUE)
{
// اگر سمافور با موفقیت گرفته شد، LED را تغییر وضعیت بده
HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
}
}
}
// این تابع Callback توسط HAL پس از وقوع وقفه خارجی فراخوانی میشود
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == B1_Pin) // اگر وقفه از پین دکمه بود
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// سمافور را از داخل ISR آزاد کن
xSemaphoreGiveFromISR(buttonSemaphore, &xHigherPriorityTaskWoken);
// اگر وظیفهای با اولویت بالاتر بیدار شد، فوراً به آن سوئیچ کن
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
// ایجاد سمافور باینری
buttonSemaphore = xSemaphoreCreateBinary();
// ایجاد وظیفه
xTaskCreate(ButtonHandlerTask, "BtnHandler", 128, NULL, 2, NULL);
/* USER CODE END 2 */
با اجرای این کد، هر بار که دکمه را فشار میدهید، LED تغییر وضعیت میدهد. روتین وقفه (`HAL_GPIO_EXTI_Callback`) بسیار کوتاه و سریع باقی میماند و کار اصلی (تغییر وضعیت LED) به یک وظیفه امن واگذار میشود.
پروژه عملی ۸: مدیریت منابع محدود (سمافور شمارشی)
در این پروژه، یک سناریو را شبیهسازی میکنیم که در آن چندین وظیفه میخواهند به تعداد محدودی از یک منبع مشترک (مثلاً ۳ "اسلات پردازشی") دسترسی پیدا کنند.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
#define PROCESSING_SLOTS 3 // تعداد منابع موجود
SemaphoreHandle_t processingSlotsSemaphore;
// تابع مشترک برای همه وظایف کارگر
void WorkerTask(void *argument)
{
char taskName[20];
// نام وظیفه را از پارامتر ورودی بگیر
sprintf(taskName, "Task-%ld", (uint32_t)argument);
for(;;)
{
// تلاش برای گرفتن یک اسلات پردازشی
if (xSemaphoreTake(processingSlotsSemaphore, portMAX_DELAY) == pdTRUE)
{
// موفقیت! اسلات گرفته شد
printf("%s is using a processing slot.\r\n", taskName);
// شبیهسازی انجام کار
vTaskDelay(pdMS_TO_TICKS( (rand() % 1000) + 500 ));
printf("%s is releasing the slot.\r\n", taskName);
// آزاد کردن اسلات
xSemaphoreGive(processingSlotsSemaphore);
}
// کمی صبر کن قبل از تلاش مجدد
vTaskDelay(pdMS_TO_TICKS( (rand() % 500) ));
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
// ایجاد سمافور شمارشی با 3 توکن
processingSlotsSemaphore = xSemaphoreCreateCounting(PROCESSING_SLOTS, PROCESSING_SLOTS);
// ایجاد 5 وظیفه کارگر
for(long i = 0; i < 5; i++)
{
char taskName[20];
sprintf(taskName, "Worker%ld", i);
xTaskCreate(WorkerTask, taskName, 128, (void*)i, 1, NULL);
}
/* USER CODE END 2 */
برای دیدن خروجی این پروژه، باید پورت سریال (UART) را فعال کرده و تابع `printf` را به آن متصل کنید. پس از اجرا، در خروجی سریال مشاهده خواهید کرد که در هر لحظه، حداکثر ۳ وظیفه پیام "is using a processing slot" را چاپ میکنند و بقیه منتظر میمانند. این پروژه به وضوح نشان میدهد که چگونه یک سمافور شمارشی میتواند دسترسی به منابع را مدیریت کند.
۷.۱. حفاظت از منابع مشترک و مشکل Race Condition
یک Race Condition زمانی رخ میدهد که خروجی یک سیستم به ترتیب زمانبندی غیرقابل پیشبینی عملیاتها بستگی داشته باشد. فرض کنید دو وظیفه میخواهند یک شمارنده سراسری را افزایش دهند:
volatile uint32_t g_sharedCounter = 0;
void Task1(void* arg) {
for(;;) { g_sharedCounter++; vTaskDelay(10); }
}
void Task2(void* arg) {
for(;;) { g_sharedCounter++; vTaskDelay(10); }
}
شاید در نگاه اول به نظر برسد که `g_sharedCounter++` یک عملیات یکپارچه (Atomic) است، اما در سطح ماشین اینطور نیست. این دستور به سه مرحله مجزا تجزیه میشود:
- خواندن (Read): مقدار فعلی `g_sharedCounter` از حافظه RAM به یک رجیستر CPU خوانده میشود.
- افزایش (Modify): مقدار داخل رجیستر CPU یکی زیاد میشود.
- نوشتن (Write): مقدار جدید از رجیستر CPU به حافظه RAM بازنویسی میشود.
برای حل این مشکل، باید اطمینان حاصل کنیم که این سه مرحله به صورت یک عملیات **یکپارچه (Atomic)** و بدون وقفه انجام میشوند. به این بخش از کد که به منبع مشترک دسترسی دارد، **بخش بحرانی (Critical Section)** گفته میشود و ما باید از آن حفاظت کنیم.
۷.۲. میوتکس استاندارد در مقابل میوتکس بازگشتی (Recursive)
**میوتکس (Mutex)** که مخفف **Mutual Exclusion** است، یکی از اصلیترین ابزارها برای حفاظت از بخشهای بحرانی است. میوتکس مانند یک "کلید" برای دسترسی به یک منبع است. در هر لحظه، تنها یک وظیفه میتواند این کلید را در اختیار داشته باشد.
میوتکس استاندارد
یک وظیفه قبل از ورود به بخش بحرانی، باید میوتکس را "بگیرد" (Take). اگر میوتکس آزاد باشد، وظیفه آن را گرفته و وارد بخش بحرانی میشود. اگر وظیفه دیگری از قبل آن را گرفته باشد، وظیفه جدید به وضعیت مسدود (Blocked) میرود تا زمانی که میوتکس "داده شود" (Give) یا آزاد گردد.
نکته بسیار مهم این است که یک وظیفه نمیتواند میوتکس استانداردی را که **خودش** در اختیار دارد، دوباره بگیرد. این کار منجر به **قفلشدگی یا Deadlock** میشود، زیرا وظیفه برای گرفتن کلیدی که در دستان خودش است تا ابد منتظر خواهد ماند.
میوتکس بازگشتی (Recursive Mutex)
این نوع میوتکس برای حل مشکل Deadlock در شرایط خاص طراحی شده است. یک میوتکس بازگشتی به وظیفه مالک خود اجازه میدهد تا آن را به صورت تودرتو و چندین بار بگیرد. این میوتکس تنها زمانی آزاد میشود که به همان تعداد دفعاتی که گرفته شده، داده شود. این ویژگی در توابع بازگشتی که همگی به یک منبع مشترک دسترسی دارند، بسیار مفید است.
۷.۳. بخشهای بحرانی (taskENTER_CRITICAL): سریعترین روش حفاظت
FreeRTOS یک روش سریع و مستقیم نیز برای حفاظت از کد ارائه میدهد که با استفاده از دو ماکروی `taskENTER_CRITICAL()` و `taskEXIT_CRITICAL()` پیادهسازی میشود.
taskENTER_CRITICAL();
// این بخش از کد کاملاً اتمیک اجرا میشود
// هیچ وقفهای (با اولویت مشخص) و هیچ جابجایی وظیفهای رخ نمیدهد
g_sharedCounter++;
taskEXIT_CRITICAL();
این روش با **غیرفعال کردن موقت وقفهها** کار میکند. از آنجایی که زمانبند برای جابجایی بین وظایف به وقفه تیک سیستم نیاز دارد، با غیرفعال کردن وقفهها، هیچ context switch رخ نخواهد داد.
مزیت: این سریعترین روش ممکن است زیرا هیچ سربار مربوط به مدیریت سمافور یا میوتکس را ندارد.
عیب بزرگ: غیرفعال کردن وقفهها، زمان پاسخدهی سیستم به رویدادهای خارجی را افزایش میدهد (System Latency). بنابراین، بخشهای بحرانی باید **بسیار بسیار کوتاه** نگه داشته شوند.
۷.۴. نمایش و حل مشکل وارونگی اولویت (Priority Inversion)
این یکی از خطرناکترین مشکلات در سیستمهای بیدرنگ است. میوتکسها یک مکانیزم داخلی برای حل این مشکل دارند که به آن **وراثت اولویت (Priority Inheritance)** گفته میشود.
- `Task_Low` (اولویت ۱)
- `Task_Medium` (اولویت ۲)
- `Task_High` (اولویت ۳)
۲. `Task_High` که به همان منبع نیاز دارد، اجرا شده و تلاش میکند میوتکس را بگیرد، اما چون میوتکس در اختیار `Task_Low` است، مسدود میشود.
۳. اکنون `Task_Medium` (که هیچ نیازی به آن منبع ندارد) آماده اجرا میشود. از آنجایی که اولویت آن از `Task_Low` بالاتر است، اجرای `Task_Low` را پیشخرید میکند و شروع به اجرا میکند.
نتیجه فاجعهبار است: `Task_High` (مهمترین وظیفه سیستم) منتظر `Task_Low` است، اما `Task_Low` هرگز اجرا نمیشود زیرا `Task_Medium` دائماً در حال اجرای آن است! در عمل، یک وظیفه با اولویت متوسط، اجرای یک وظیفه با اولویت بالا را مختل کرده است.
راهحل (وراثت اولویت): وقتی از میوتکسهای FreeRTOS استفاده میکنید، به محض اینکه `Task_High` برای گرفتن میوتکس مسدود میشود، زمانبند به طور موقت اولویت `Task_Low` (مالک میوتکس) را به اندازه اولویت `Task_High` **افزایش میدهد**. این کار باعث میشود `Task_Low` دیگر توسط `Task_Medium` پیشخرید نشود، کار خود را با منبع مشترک به سرعت تمام کند، میوتکس را آزاد کرده و به اولویت اصلی خود بازگردد. سپس `Task_High` بلافاصله میوتکس را گرفته و اجرا میشود.
پروژه عملی ۹: حفاظت از پورت سریال (UART) با میوتکس
در این پروژه، دو وظیفه سعی میکنند به صورت همزمان روی پورت سریال پیام چاپ کنند. ما با استفاده از میوتکس، از درهمریختگی پیامها جلوگیری میکنیم.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
SemaphoreHandle_t uartMutex;
// برای استفاده از printf، باید syscalls را پیادهسازی کنید
// یا از یک تابع سفارشی برای ارسال رشته استفاده نمایید.
void Print(const char* msg)
{
// قبل از چاپ، میوتکس را بگیر
if (xSemaphoreTake(uartMutex, portMAX_DELAY) == pdTRUE)
{
HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
// پس از اتمام چاپ، میوتکس را آزاد کن
xSemaphoreGive(uartMutex);
}
}
void PrinterTask1(void *argument)
{
for(;;)
{
Print("########## Printed by Task 1 ##########\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void PrinterTask2(void *argument)
{
for(;;)
{
Print("---------- Printed by Task 2 ----------\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
// ایجاد میوتکس
uartMutex = xSemaphoreCreateMutex();
xTaskCreate(PrinterTask1, "Printer1", 128, NULL, 1, NULL);
xTaskCreate(PrinterTask2, "Printer2", 128, NULL, 1, NULL);
/* USER CODE END 2 */
اگر این پروژه را بدون میوتکس اجرا کنید، خروجیها درهم و ناقص خواهند بود. اما با وجود میوتکس، هر پیام به صورت کامل و یکپارچه چاپ میشود.
پروژه عملی ۱۰: نمایش عملکرد میوتکس بازگشتی
در این پروژه، تابعی بازگشتی داریم که در هر بار فراخوانی، نیاز به گرفتن یک قفل دارد.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
SemaphoreHandle_t recursiveMutex;
void recursiveFunction(int count)
{
// اگر به انتهای بازگشت رسیدیم، خارج شو
if (count <= 0) {
return;
}
// میوتکس بازگشتی را بگیر
if (xSemaphoreTakeRecursive(recursiveMutex, portMAX_DELAY) == pdTRUE)
{
printf("Recursive lock taken. Count: %d\r\n", count);
// تابع را دوباره فراخوانی کن
recursiveFunction(count - 1);
printf("Recursive lock released. Count: %d\r\n", count);
// میوتکس را آزاد کن
xSemaphoreGiveRecursive(recursiveMutex);
}
}
void TestTask(void *argument)
{
printf("Starting recursive test...\r\n");
recursiveFunction(5);
printf("Recursive test finished.\r\n");
// وظیفه پس از اتمام کارش حذف میشود
vTaskDelete(NULL);
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
// ایجاد میوتکس بازگشتی
recursiveMutex = xSemaphoreCreateRecursiveMutex();
xTaskCreate(TestTask, "Test", 256, NULL, 1, NULL);
/* USER CODE END 2 */
خروجی این کد نشان میدهد که وظیفه توانسته ۵ بار متوالی میوتکس را بگیرد و سپس ۵ بار آن را آزاد کند. اگر در این مثال از یک میوتکس استاندارد (`xSemaphoreCreateMutex`) استفاده میکردید، برنامه در اولین فراخوانی بازگشتی دچار Deadlock میشد.
۸.۱. معرفی سیگنالها: روشی سبک و سریع برای همگامسازی
هر وظیفهای که در FreeRTOS ایجاد میشود، به صورت داخلی یک **مقدار سیگنال ۳۲ بیتی (Notification Value)** و یک **وضعیت سیگنال (Notification State)** دارد. این ویژگیها بخشی از ساختار کنترلی وظیفه (TCB) هستند و حافظه اضافی مصرف نمیکنند. سیگنالها میتوانند جایگزین بسیار بهینهای برای سمافورهای باینری و شمارشی، و حتی صفهایی با یک آیتم شوند.
مزایای سیگنالهای وظیفه:
- سرعت بسیار بالا: ارسال یک سیگنال به یک وظیفه، عملیاتی مستقیم و سریع است که تنها شامل آپدیت مقدار سیگنال در TCB آن وظیفه میشود. این کار به مراتب سریعتر از ارسال داده به یک صف است که نیازمند کپی کردن داده و مدیریت یک شیء مجزاست.
- مصرف حافظه صفر: از آنجایی که برای استفاده از سیگنالها نیاز به ایجاد یک شیء جدید (مانند صف یا سمافور) نیست، هیچ حافظه RAM اضافی مصرف نمیشود. این ویژگی در سیستمهای با حافظه محدود بسیار ارزشمند است.
توابع اصلی API سیگنالها
API سیگنالها بسیار انعطافپذیر است و میتواند رفتار سمافورها یا صفها را شبیهسازی کند.
- `xTaskNotifyGive()` / `vTaskNotifyGiveFromISR()`: این تابع مانند `xSemaphoreGive` عمل میکند. با هر بار فراخوانی، مقدار سیگنال وظیفه مقصد را یکی افزایش میدهد. این تابع برای شبیهسازی سمافورها ایدهآل است.
- `ulTaskNotifyTake()`: این تابع مانند `xSemaphoreTake` برای سمافورها عمل میکند. وظیفه را تا زمانی که مقدار سیگنال آن بزرگتر از صفر شود، مسدود میکند. پس از دریافت سیگنال، میتواند مقدار آن را صفر کرده یا یکی از آن کم کند.
- `xTaskNotify()` / `xTaskNotifyFromISR()`: این تابع قدرتمندترین ابزار برای ارسال سیگنال است. به شما اجازه میدهد مقدار سیگنال وظیفه را به صورت مستقیم تنظیم کنید، بیتهای خاصی از آن را set یا clear کنید، یا مقدار جدیدی را به آن بفرستید.
- `xTaskNotifyWait()`: این تابع به وظیفه اجازه میدهد تا با انعطافپذیری بالا منتظر دریافت سیگنال بماند و کنترل کاملی روی نحوه خواندن و پاک کردن بیتهای مقدار سیگنال داشته باشد.
پروژه عملی ۱۱: جایگزین کردن سمافور وقفه کلید با یک سیگنال مستقیم
در این پروژه، کد پروژه عملی ۷ (همگامسازی با وقفه کلید) را بازنویسی میکنیم، اما این بار به جای استفاده از سمافور باینری، از مکانیزم سبک و سریع `xTaskNotifyGive` استفاده خواهیم کرد.
**پیکربندی CubeMX:** دقیقاً مانند پروژه ۷، یک پین را به عنوان ورودی وقفه خارجی (`GPIO_EXTI`) و یک پین را برای LED به عنوان خروجی تنظیم کنید.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
TaskHandle_t buttonHandlerTaskHandle; // شناسه وظیفه برای ارسال سیگنال
void ButtonHandlerTask(void *argument)
{
for(;;)
{
// منتظر دریافت سیگنال بمان (مانند گرفتن سمافور)
// پارامتر اول (pdTRUE) باعث میشود شمارنده سیگنال پس از دریافت صفر شود
// پارامتر دوم زمان انتظار است
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// اگر سیگنال دریافت شد، LED را تغییر وضعیت بده
HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
}
}
// تابع Callback وقفه خارجی
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == B1_Pin)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// به جای دادن سمافور، به صورت مستقیم به وظیفه سیگنال بده
vTaskNotifyGiveFromISR(buttonHandlerTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
// دیگر نیازی به ایجاد سمافور نیست!
// فقط وظیفه را ایجاد کرده و شناسه آن را ذخیره کن
xTaskCreate(ButtonHandlerTask, "BtnHandler", 128, NULL, 2, &buttonHandlerTaskHandle);
/* USER CODE END 2 */
همانطور که میبینید، کد نهایی سادهتر و بهینهتر است. ما دیگر نیازی به تعریف و ایجاد یک شیء سمافور نداریم و حافظه کمتری مصرف کردهایم. عملکرد برنامه دقیقاً مشابه قبل خواهد بود، اما در سطح هسته، عملیات همگامسازی با سرعت بیشتری انجام میشود.
پروژه عملی ۱۲: ارسال مقدار عددی از یک وقفه ADC به یک وظیفه از طریق سیگنال
این پروژه قدرت واقعی سیگنالها را نشان میدهد. ما میتوانیم از مقدار ۳۲ بیتی سیگنال برای ارسال مستقیم یک داده کوچک (مانند نتیجه ADC یا یک شمارنده) استفاده کنیم و از ایجاد یک صف برای این کار ساده اجتناب کنیم.
**هدف:** خواندن مقدار ADC در روتین وقفه و ارسال مستقیم مقدار دیجیتال به یک وظیفه از طریق سیگنال.
**پیکربندی CubeMX:** یک کانال ADC را فعال کنید و وقفه `End of Conversion` آن را در `NVIC` فعال نمایید.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
TaskHandle_t adcProcessingTaskHandle;
void ADC_ProcessingTask(void *argument)
{
uint32_t receivedAdcValue;
for(;;)
{
// منتظر دریافت سیگنال بمان
// پارامتر اول: بیتهایی که قبل از خروج از تابع باید در مقدار سیگنال صفر شوند (اینجا صفر)
// پارامتر دوم: بیتهایی که پس از خروج از تابع باید در مقدار سیگنال صفر شوند (اینجا همه بیتها)
// پارامتر سوم: برای دریافت مقدار سیگنال
// پارامتر چهارم: زمان انتظار
if (xTaskNotifyWait(0x00, 0xFFFFFFFF, &receivedAdcValue, portMAX_DELAY) == pdPASS)
{
// مقدار ADC دریافت شد
printf("ADC Value: %lu\r\n", receivedAdcValue);
}
}
}
// تابع Callback وقفه اتمام تبدیل ADC
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
// مقدار دیجیتال را بخوان
uint32_t adcValue = HAL_ADC_GetValue(hadc);
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// مقدار ADC را به عنوان مقدار سیگنال به وظیفه ارسال کن
xTaskNotifyFromISR(adcProcessingTaskHandle,
adcValue,
eSetValueWithOverwrite, // عمل: مقدار قبلی را با این مقدار جدید جایگزین کن
&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/* USER CODE END 0 */
// ... در داخل تابع main() ...
/* USER CODE BEGIN 2 */
// ایجاد وظیفه و ذخیره شناسه آن
xTaskCreate(ADC_ProcessingTask, "ADC_Process", 256, NULL, 2, &adcProcessingTaskHandle);
// شروع تبدیل ADC در حالت وقفه
HAL_ADC_Start_IT(&hadc1);
/* USER CODE END 2 */
در این کد، روتین وقفه ADC پس از هر بار تبدیل، مقدار دیجیتال را مستقیماً در مقدار سیگنال وظیفه `ADC_ProcessingTask` مینویسد. این وظیفه که با `xTaskNotifyWait` منتظر است، بیدار شده و مقدار را دریافت و پردازش میکند. این روش برای ارسال دادههای کوچک، بهینهترین راه ممکن در FreeRTOS است.
فصل ۹: گروههای رویداد (Event Groups)
تاکنون ابزارهای همگامسازی را بررسی کردیم که یک وظیفه را منتظر یک رویداد واحد نگه میداشتند (یک آیتم در صف یا یک توکن از سمافور). اما در سیستمهای پیچیده، گاهی یک وظیفه باید منتظر وقوع ترکیبی از چندین رویداد مختلف بماند. **گروههای رویداد (Event Groups)** ابزار قدرتمند FreeRTOS برای مدیریت چنین سناریوهای پیچیدهای هستند. یک گروه رویداد مجموعهای از "فلگ" یا بیتهای رویداد است که یک وظیفه میتواند منتظر یک ترکیب منطقی خاص از آنها بماند.
۹.۱. همگامسازی بر اساس چندین رویداد مختلف (AND/OR)
هر گروه رویداد یک متغیر (معمولاً ۲۴ بیتی) را مدیریت میکند که هر بیت آن میتواند نمایانگر یک رویداد مجزا در سیستم باشد. برای مثال، بیت 0 میتواند نمایانگر "اتصال به شبکه برقرار شد"، بیت 1 "داده از سنسور آماده است" و بیت 2 "دکمه فشرده شد" باشد.
قدرت اصلی این مکانیزم در تابع `xEventGroupWaitBits()` نهفته است. این تابع به یک وظیفه اجازه میدهد تا اجرای خود را متوقف کرده و منتظر یک الگوی بیتی خاص شود. مهمترین پارامترهای این تابع عبارتند از:
uxBitsToWaitFor: ماسک بیتی که مشخص میکند وظیفه منتظر کدام رویداد(ها) است.xWaitForAllBits: اگر `pdTRUE` باشد، وظیفه منتظر میماند تا تمام بیتهای مشخص شده `set` شوند (عملیات منطقی AND). اگر `pdFALSE` باشد، به محض `set` شدن هر یک از بیتهای مشخص شده، وظیفه از حالت مسدود خارج میشود (عملیات منطقی OR).xClearOnExit: اگر `pdTRUE` باشد، بیتهایی که باعث خروج وظیفه از حالت انتظار شدهاند، به صورت خودکار پاک (صفر) میشوند.
برای `set` کردن بیتها نیز از توابع `xEventGroupSetBits()` (در وظایف) و `xEventGroupSetBitsFromISR()` (در وقفهها) استفاده میشود.
پروژه عملی ۱۳: فعالسازی وظیفه پس از وقوع همزمان دو رویداد مجزا (AND)
هدف: یک وظیفه اصلی منتظر میماند تا هم اتصال به شبکه برقرار شود و هم تنظیمات از حافظه خوانده شود، و تنها پس از وقوع هر دو رویداد، کار اصلی خود را شروع میکند.
/* USER CODE BEGIN 0 */
EventGroupHandle_t appEventGroup;
const int NETWORK_CONNECTED_BIT = (1 << 0); // Bit 0
const int CONFIG_LOADED_BIT = (1 << 1); // Bit 1
void NetworkTask(void* arg) {
printf("NetworkTask: Connecting to network...\r\n");
vTaskDelay(pdMS_TO_TICKS(2000)); // شبیهسازی اتصال
printf("NetworkTask: Connected.\r\n");
xEventGroupSetBits(appEventGroup, NETWORK_CONNECTED_BIT);
vTaskDelete(NULL);
}
void ConfigTask(void* arg) {
printf("ConfigTask: Loading configuration...\r\n");
vTaskDelay(pdMS_TO_TICKS(3000)); // شبیهسازی خواندن تنظیمات
printf("ConfigTask: Config loaded.\r\n");
xEventGroupSetBits(appEventGroup, CONFIG_LOADED_BIT);
vTaskDelete(NULL);
}
void MainAppTask(void* arg) {
printf("MainAppTask: Waiting for system to be ready...\r\n");
EventBits_t bits = xEventGroupWaitBits(
appEventGroup,
NETWORK_CONNECTED_BIT | CONFIG_LOADED_BIT,
pdTRUE, // Clear bits on exit
pdTRUE, // Wait for ALL bits
portMAX_DELAY);
if ((bits & (NETWORK_CONNECTED_BIT | CONFIG_LOADED_BIT)) == (NETWORK_CONNECTED_BIT | CONFIG_LOADED_BIT)) {
printf("MainAppTask: System is ready! Starting main application.\r\n");
}
vTaskDelete(NULL);
}
/* USER CODE END 0 */
// در تابع main
/* USER CODE BEGIN 2 */
appEventGroup = xEventGroupCreate();
xTaskCreate(NetworkTask, "NetTask", 256, NULL, 1, NULL);
xTaskCreate(ConfigTask, "CfgTask", 256, NULL, 1, NULL);
xTaskCreate(MainAppTask, "MainApp", 256, NULL, 2, NULL);
/* USER CODE END 2 */
پروژه عملی ۱۴: فعالسازی وظیفه پس از وقوع هر یک از چند رویداد ممکن (OR)
هدف: یک وظیفه پردازشگر دستور، منتظر دستور از دو منبع مختلف (پورت سریال یا کلید فشاری) میماند و به هر کدام که زودتر رخ دهد، پاسخ میدهد.
/* USER CODE BEGIN 0 */
EventGroupHandle_t commandEventGroup;
const int UART_COMMAND_BIT = (1 << 0);
const int BUTTON_PRESS_BIT = (1 << 1);
// وقفه کلید فشاری
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == B1_Pin) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(commandEventGroup, BUTTON_PRESS_BIT, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
// وظیفهای برای خواندن از UART (برای سادگی شبیهسازی شده)
void UartReaderTask(void* arg) {
for(;;) {
vTaskDelay(pdMS_TO_TICKS(5000)); // هر 5 ثانیه یک دستور از سریال میآید
printf("UartReaderTask: New command from UART.\r\n");
xEventGroupSetBits(commandEventGroup, UART_COMMAND_BIT);
}
}
void CommandHandlerTask(void* arg) {
for(;;) {
printf("CommandHandler: Waiting for a command...\r\n");
EventBits_t bits = xEventGroupWaitBits(
commandEventGroup,
UART_COMMAND_BIT | BUTTON_PRESS_BIT,
pdTRUE, // Clear bits on exit
pdFALSE, // Wait for ANY bit
portMAX_DELAY);
if ((bits & UART_COMMAND_BIT) != 0) {
printf("CommandHandler: Processing UART command.\r\n");
}
if ((bits & BUTTON_PRESS_BIT) != 0) {
printf("CommandHandler: Processing Button press.\r\n");
}
}
}
/* USER CODE END 0 */
// در تابع main
/* USER CODE BEGIN 2 */
commandEventGroup = xEventGroupCreate();
xTaskCreate(UartReaderTask, "UartReader", 256, NULL, 1, NULL);
xTaskCreate(CommandHandlerTask, "CmdHandler", 256, NULL, 2, NULL);
/* USER CODE END 2 */
فصل ۱۰: تایمرهای نرمافزاری (Software Timers)
در بسیاری از برنامهها نیاز به اجرای یک قطعه کد پس از یک بازه زمانی مشخص یا به صورت دورهای داریم. استفاده از یک تایمر سختافزاری برای هر کدام از این کارها، به سرعت منابع میکروکنترلر را به اتمام میرساند. FreeRTOS برای حل این مشکل، **تایمرهای نرمافزاری** را ارائه میدهد. این تایمرها به صورت بسیار بهینه و با استفاده از یک وظیفه سیستمی واحد (`Tmr Svc`) مدیریت میشوند و به شما اجازه میدهند تا تعداد زیادی رویداد زمانبندی شده را بدون هدر دادن منابع سختافزاری پیادهسازی کنید.
۱۰.۱. تایمرهای یکباره (One-shot) در مقابل دورهای (Auto-reload)
تمام تایمرهای نرمافزاری با یک تابع Callback همراه هستند که پس از اتمام زمان تایمر، اجرا میشود. این تایمرها به دو دسته اصلی تقسیم میشوند:
- تایمر یکباره (One-shot): این تایمر پس از شروع، تنها **یک بار** پس از سپری شدن دوره زمانی مشخص شده، تابع Callback خود را اجرا کرده و سپس متوقف میشود. این نوع تایمر برای پیادهسازی Timeoutها یا انجام یک کار با تاخیر بسیار مناسب است.
- تایمر دورهای (Auto-reload): این تایمر پس از هر بار اتمام دوره زمانی، به صورت **خودکار** مجدداً راهاندازی میشود و تابع Callback خود را به صورت دورهای و با یک فرکانس ثابت اجرا میکند. این تایمر برای انجام کارهای تکراری مانند نمونهبرداری از سنسورها در فواصل زمانی طولانی کاربرد دارد.
تابع اصلی برای ایجاد تایمر `xTimerCreate()` است که یکی از پارامترهای آن (`uxAutoReload`) نوع تایمر را مشخص میکند.
پروژه عملی ۱۵: ایجاد یک سیستم هشدار watchdog با تایمر یکباره
هدف: شبیهسازی یک Watchdog نرمافزاری. یک وظیفه اصلی باید به صورت دورهای یک تایمر را "reset" کند (به اصطلاح آن را petting کند). اگر وظیفه اصلی به هر دلیلی هنگ کند، تایمر منقضی شده و تابع Callback آن یک پیام خطا را چاپ میکند.
/* USER CODE BEGIN 0 */
TimerHandle_t watchdogTimer;
void WatchdogCallback(TimerHandle_t xTimer) {
printf("FATAL ERROR: Main task seems to be stuck!\r\n");
// در یک سیستم واقعی، اینجا میتوان میکروکنترلر را ریست کرد
}
void MainProcessingTask(void* arg) {
for(;;) {
printf("Main task is running, petting the watchdog...\r\n");
// تایمر را ریست کن تا دوباره از اول شمارش کند
xTimerReset(watchdogTimer, portMAX_DELAY);
// شبیهسازی انجام کار
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* USER CODE END 0 */
// در تابع main
/* USER CODE BEGIN 2 */
// ایجاد یک تایمر یکباره با دوره 2 ثانیه
watchdogTimer = xTimerCreate(
"Watchdog",
pdMS_TO_TICKS(2000),
pdFALSE, // uxAutoReload = pdFALSE -> One-shot timer
(void*)0,
WatchdogCallback);
xTaskCreate(MainProcessingTask, "MainTask", 256, NULL, 1, NULL);
// تایمر را برای اولین بار فعال کن
xTimerStart(watchdogTimer, portMAX_DELAY);
/* USER CODE END 2 */
تا زمانی که `MainProcessingTask` به درستی کار کند و هر ثانیه تایمر را ریست نماید، پیام خطا هرگز چاپ نمیشود. اما اگر این وظیفه متوقف شود، پس از ۲ ثانیه، تابع Callback اجرا خواهد شد.
پروژه عملی ۱۶: مدیریت چندین تایمر دورهای با یک تابع Callback و Timer ID
هدف: به جای نوشتن یک تابع Callback مجزا برای هر تایمر، از یک تابع مشترک استفاده کرده و با استفاده از شناسه تایمر (Timer ID)، تشخیص میدهیم کدام تایمر منقضی شده است.
/* USER CODE BEGIN 0 */
#define TIMER_ID_1 1
#define TIMER_ID_2 2
void MultiTimerCallback(TimerHandle_t xTimer) {
uint32_t timerId = (uint32_t)pvTimerGetTimerID(xTimer);
if (timerId == TIMER_ID_1) {
printf("Timer 1 (500ms) expired.\r\n");
} else if (timerId == TIMER_ID_2) {
printf("Timer 2 (1200ms) expired.\r\n");
}
}
/* USER CODE END 0 */
// در تابع main
/* USER CODE BEGIN 2 */
TimerHandle_t timer1, timer2;
timer1 = xTimerCreate("Timer1", pdMS_TO_TICKS(500), pdTRUE, (void*)TIMER_ID_1, MultiTimerCallback);
timer2 = xTimerCreate("Timer2", pdMS_TO_TICKS(1200), pdTRUE, (void*)TIMER_ID_2, MultiTimerCallback);
if (timer1 != NULL && timer2 != NULL) {
xTimerStart(timer1, 0);
xTimerStart(timer2, 0);
}
/* USER CODE END 2 */
این روش مدیریتی کد را بسیار تمیزتر و بهینهتر میکند، بهخصوص زمانی که تعداد زیادی تایمر نرمافزاری در سیستم خود دارید.
فصل ۱۱: مدیریت حافظه و وقفهها
دو موضوع حیاتی که پایداری و کارایی هر سیستم نهفتهای به آن بستگی دارد، مدیریت صحیح حافظه و طراحی امن روتینهای وقفه است. FreeRTOS ابزارها و قوانینی را برای هر دو مورد ارائه میدهد. در این فصل، مدلهای مختلف مدیریت حافظه Heap را بررسی کرده و اصول طراحی روتینهای وقفه امن (ISR) را یاد میگیریم.
۱۱.۱. بررسی مدلهای مختلف حافظه Heap
FreeRTOS چندین مدل برای مدیریت حافظه Heap ارائه میدهد که در فایلهای `heap_x.c` پیادهسازی شدهاند. شما با قرار دادن یکی از این فایلها در پروژه خود، مدل مورد نظر را انتخاب میکنید. (این کار در CubeMX به صورت خودکار انجام میشود).
- `heap_1.c`: سادهترین مدل. فقط تخصیص حافظه را پشتیبانی میکند و قابلیت آزاد کردن (`free`) را ندارد. برای سیستمهایی که تمام اشیاء RTOS را در ابتدا ایجاد کرده و دیگر تغییر نمیدهند، مناسب است.
- `heap_2.c`: نسخه قدیمی که تخصیص و آزادسازی را پشتیبانی میکند اما مستعد تکهتکه شدن (Fragmentation) حافظه است. استفاده از آن دیگر توصیه نمیشود.
- `heap_3.c`: یک پوشش (wrapper) ساده برای توابع `malloc` و `free` کتابخانه استاندارد C است. این مدل معمولاً Thread-safe نیست و استفاده از آن نیازمند دقت بالاست.
- `heap_4.c`: بهترین و پرکاربردترین گزینه برای عموم کاربردها. این مدل تخصیص و آزادسازی حافظه را به صورت بهینه انجام میدهد و بلاکهای حافظه آزاد شده مجاور را با هم ادغام میکند تا از تکهتکه شدن جلوگیری کند.
- `heap_5.c`: مشابه `heap_4` است با این تفاوت که به شما اجازه میدهد تا Heap را در چندین ناحیه حافظه غیرمجاور تعریف کنید.
۱۱.۲. طراحی روتینهای وقفه امن (ISR) و توابع `...FromISR`
روتینهای وقفه (ISR) در یک زمینه اجرایی خاص و متفاوت از وظایف اجرا میشوند. دو قانون طلایی برای نوشتن ISR در FreeRTOS وجود دارد:
- ISRها باید تا حد ممکن کوتاه و سریع باشند. انجام پردازشهای سنگین یا تاخیر در داخل یک ISR، کل سیستم را مختل میکند.
- هرگز نباید از توابع API استاندارد FreeRTOS در داخل ISR استفاده کرد. توابعی مانند `xQueueSend` یا `vTaskDelay` ممکن است باعث مسدود شدن شوند که در زمینه وقفه مجاز نیست.
برای حل این مشکل، FreeRTOS برای بسیاری از توابع خود، یک نسخه جایگزین و امن برای استفاده در ISR ارائه میدهد که پسوند **`FromISR`** دارند (مانند `xQueueSendFromISR` یا `xSemaphoreGiveFromISR`). این توابع تضمین میکنند که هرگز مسدود نمیشوند.
این توابع معمولاً یک پارامتر اضافی به نام `pxHigherPriorityTaskWoken` دارند. اگر فراخوانی تابع `...FromISR` باعث شود یک وظیفه با اولویت بالاتر از وظیفه در حال اجرا، آماده اجرا شود، این متغیر `pdTRUE` خواهد شد. در این صورت، ISR باید قبل از خروج، ماکروی `portYIELD_FROM_ISR()` را فراخوانی کند تا یک جابجایی وظیفه فوری (Context Switch) درخواست شود.
پروژه عملی ۱۷: نظارت بر میزان حافظه Heap باقیمانده
هدف: ایجاد یک وظیفه برای نظارت بر میزان حافظه Heap آزاد، تا از خطاهای ناشی از کمبود حافظه جلوگیری کنیم.
/* USER CODE BEGIN 0 */
void MemoryMonitorTask(void* arg) {
for(;;) {
size_t freeHeap = xPortGetFreeHeapSize();
printf("Free Heap Size: %u bytes\r\n", freeHeap);
vTaskDelay(pdMS_TO_TICKS(5000)); // هر 5 ثانیه گزارش بده
}
}
// وظیفهای که حافظه مصرف میکند
void ConsumerTask(void* arg) {
for(;;) {
// هر بار که دکمه فشرده میشود، 512 بایت حافظه بگیر
if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_RESET) {
uint8_t* buffer = pvPortMalloc(512);
if (buffer != NULL) {
printf("Successfully allocated 512 bytes.\r\n");
// در یک برنامه واقعی، پس از استفاده باید حافظه را free کرد
// vPortFree(buffer);
} else {
printf("Failed to allocate memory!\r\n");
}
vTaskDelay(pdMS_TO_TICKS(200)); // Debounce
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
/* USER CODE END 0 */
// در تابع main
/* USER CODE BEGIN 2 */
xTaskCreate(MemoryMonitorTask, "MemMon", 128, NULL, 1, NULL);
xTaskCreate(ConsumerTask, "Consumer", 128, NULL, 1, NULL);
/* USER CODE END 2 */
با هر بار فشردن دکمه، خواهید دید که مقدار حافظه آزاد گزارش شده توسط `MemoryMonitorTask` کاهش مییابد.
پروژه عملی ۱۸: واگذاری پردازش سنگین از وقفه (Deferred Interrupt Processing)
هدف: این پروژه الگوی طراحی صحیح برای مدیریت وقفهها را به صورت عملی نشان میدهد. ما پردازش دادههای دریافتی از UART را از ISR به یک وظیفه مجزا منتقل میکنیم.
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
QueueHandle_t uartRxQueue;
void UartProcessingTask(void* arg) {
uint8_t receivedChar;
for(;;) {
// منتظر دریافت کاراکتر از صف بمان
if (xQueueReceive(uartRxQueue, &receivedChar, portMAX_DELAY) == pdPASS) {
// شبیهسازی پردازش سنگین
printf("Processing received character: %c\r\n", receivedChar);
HAL_Delay(50); // هرگز از HAL_Delay در ISR استفاده نکنید!
}
}
}
// این Callback پس از دریافت هر بایت از UART فراخوانی میشود
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// فرض کنید داده در یک بافر یک بایتی (uartRxBuffer) دریافت شده
extern uint8_t uartRxBuffer;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// کاراکتر را به صف بفرست
xQueueSendFromISR(uartRxQueue, &uartRxBuffer, &xHigherPriorityTaskWoken);
// آمادهسازی برای دریافت بایت بعدی
HAL_UART_Receive_IT(&huart2, &uartRxBuffer, 1);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/* USER CODE END 0 */
// در تابع main
/* USER CODE BEGIN 2 */
extern uint8_t uartRxBuffer;
uartRxQueue = xQueueCreate(16, sizeof(uint8_t)); // یک صف برای 16 کاراکتر
xTaskCreate(UartProcessingTask, "UartProc", 256, NULL, 2, NULL);
// فعال کردن وقفه دریافت UART برای اولین بار
HAL_UART_Receive_IT(&huart2, &uartRxBuffer, 1);
/* USER CODE END 2 */
در این الگو، ISR (`HAL_UART_RxCpltCallback`) تنها کاراکتر را در صف قرار میدهد و به سرعت خارج میشود. وظیفه `UartProcessingTask` پردازش اصلی و زمانبر را انجام میدهد. این روش، پاسخدهی و پایداری کل سیستم را تضمین میکند.
فصل ۱۲: ابزارهای اشکالزدایی
نوشتن کد تنها نیمی از مسیر است؛ نیم دیگر، یافتن و رفع خطاهاست. اشکالزدایی در سیستمهای چندوظیفهای میتواند چالشبرانگیز باشد، زیرا خطاها ممکن است به زمانبندی و تعامل بین وظایف وابسته باشند. خوشبختانه، FreeRTOS ابزارهای داخلی قدرتمندی برای کمک به این فرآیند ارائه میدهد. در این فصل، با ماکروی `configASSERT`، هوکهای نرمافزاری و روشهای تشخیص خطاهای رایج مانند سرریز پشته و تحلیل بار پردازنده آشنا میشویم.
۱۲.۱. استفاده از configASSERT و هوکهای نرمافزاری
configASSERT
ماکروی `configASSERT(x)` مانند تابع `assert` در کتابخانه استاندارد C عمل میکند. شما این ماکرو را در فایل `FreeRTOSConfig.h` تعریف میکنید. اگر در حین اجرای برنامه، شرط `x` نادرست (false) باشد، این ماکرو فراخوانی شده و میتواند یک تابع خطا را اجرا کند یا برنامه را در یک حلقه بینهایت متوقف سازد تا شما با دیباگر علت را بررسی کنید.
استفاده از `configASSERT` در طول فرآیند توسعه برای شناسایی خطاهای زیر بسیار حیاتی است:
- **خطاهای پیکربندی:** مثلاً ایجاد یک وظیفه با اولویتی بالاتر از `configMAX_PRIORITIES`.
- **خطاهای استفاده از API:** مثلاً ارسال پارامتر `NULL` به یک تابع که انتظار آن را ندارد.
// در فایل FreeRTOSConfig.h
#define configASSERT( x ) if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }
هوکهای نرمافزاری (Software Hooks)
هوکها توابعی هستند که شما پیادهسازی میکنید، اما هسته FreeRTOS آنها را در شرایط خاصی فراخوانی میکند. دو هوک بسیار مهم برای اشکالزدایی عبارتند از:
- `vApplicationStackOverflowHook` (هوک سرریز پشته): اگر قابلیت تشخیص سرریز پشته فعال باشد، هرگاه یک وظیفه از فضای پشته اختصاص داده شده خود فراتر رود، این هوک فراخوانی میشود.
- `vApplicationMallocFailedHook` (هوک خطای تخصیص حافظه): هرگاه تابع `pvPortMalloc` (که برای ایجاد وظایف، صفها و... استفاده میشود) نتواند حافظه مورد نیاز را از Heap تخصیص دهد، این هوک فراخوانی میشود.
پروژه عملی ۱۹: شبیهسازی و تشخیص خطای سرریز پشته (Stack Overflow)
هدف: مشاهده عملکرد مکانیزم تشخیص سرریز پشته در عمل.
پیکربندی:
- در فایل `FreeRTOSConfig.h`، مقدار `configCHECK_FOR_STACK_OVERFLOW` را برابر `2` قرار دهید. این بهترین و دقیقترین روش تشخیص است.
- تابع هوک `vApplicationStackOverflowHook` را در `main.c` پیادهسازی کنید.
/* USER CODE BEGIN 0 */
// پیادهسازی هوک سرریز پشته
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
printf("FATAL: Stack overflow detected in task: %s\r\n", pcTaskName);
// در یک سیستم واقعی، اینجا باید سیستم را به یک حالت امن برد یا ریست کرد
while(1);
}
void OverflowTask(void* arg) {
// یک آرایه بزرگ روی پشته محلی تعریف کن تا باعث سرریز شود
char largeArray[200];
// از آرایه استفاده کن تا کامپایلر آن را بهینهسازی نکند
memset(largeArray, 0xAA, sizeof(largeArray));
printf("Task is running with large array...\r\n");
for(;;) {
vTaskDelay(1000);
}
}
/* USER CODE END 0 */
// در تابع main
/* USER CODE BEGIN 2 */
// یک وظیفه با پشته بسیار کوچک ایجاد کن (مثلاً 64 کلمه = 256 بایت)
xTaskCreate(OverflowTask, "Overflow", 64, NULL, 1, NULL);
/* USER CODE END 2 */
بلافاصله پس از شروع اجرای `OverflowTask`، به دلیل تعریف آرایه بزرگ، سرریز پشته رخ داده و تابع `vApplicationStackOverflowHook` فراخوانی میشود و پیام خطا را در پورت سریال چاپ میکند.
پروژه عملی ۲۰: تحلیل بار پردازنده هر وظیفه با `vTaskGetRunTimeStats`
هدف: اندازهگیری دقیق میزان استفاده هر وظیفه از CPU. این اطلاعات برای بهینهسازی و یافتن گلوگاههای عملکردی سیستم ضروری است.
پیکربندی:
- در `FreeRTOSConfig.h`، مقدار `configGENERATE_RUN_TIME_STATS` را برابر `1` قرار دهید.
- شما باید یک تایمر سختافزاری با فرکانس بالا (حداقل ۱۰ برابر فرکانس تیک سیستم) را به عنوان منبع شمارنده زمان اجرا معرفی کنید. این کار با تعریف دو ماکرو انجام میشود. در اینجا از `TIM6` استفاده میکنیم:
// در فایل FreeRTOSConfig.h
#define configGENERATE_RUN_TIME_STATS 1
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() (TIM6->CR1 = TIM_CR1_CEN) // فقط تایمر را فعال کن
#define portGET_RUN_TIME_COUNTER_VALUE() (TIM6->CNT) // مقدار شمارنده تایمر را برگردان
// در فایل main.c و قبل از main()
void configureTimerForRunTimeStats() {
// فعال کردن کلاک تایمر TIM6
__HAL_RCC_TIM6_CLK_ENABLE();
// تنظیم Prescaler برای داشتن فرکانس بالا، مثلا 1MHz
TIM6->PSC = (HAL_RCC_GetPCLK1Freq() / 1000000) - 1;
TIM6->ARR = 0xFFFF; // شمارش تا حداکثر مقدار
TIM6->CR1 = TIM_CR1_CEN; // فعال کردن تایمر
}
/* USER CODE BEGIN 0 */
void HeavyWorkTask(void* arg) {
for(;;) {
// شبیهسازی کار سنگین
for(volatile int i = 0; i < 50000; i++);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void MonitorTask(void* arg) {
char statsBuffer[512];
for(;;) {
vTaskDelay(pdMS_TO_TICKS(5000));
printf("----------- Task Stats -----------\r\n");
vTaskGetRunTimeStats(statsBuffer);
printf("%s\r\n", statsBuffer);
printf("----------------------------------\r\n");
}
}
/* USER CODE END 0 */
// در تابع main
/* USER CODE BEGIN 2 */
configureTimerForRunTimeStats(); // فراخوانی تابع راهاندازی تایمر
xTaskCreate(HeavyWorkTask, "HeavyWork", 128, NULL, 1, NULL);
xTaskCreate(MonitorTask, "Monitor", 256, NULL, 2, NULL);
/* USER CODE END 2 */
هر ۵ ثانیه، جدولی در خروجی سریال چاپ میشود که نام هر وظیفه، زمان اجرای مطلق آن و درصد استفاده از CPU را نمایش میدهد.
فصل ۱۳: بهینهسازی و نکات نهایی
در فصل پایانی این کتاب، به دو موضوع کلیدی میپردازیم: بهینهسازی مصرف انرژی که برای دستگاههای مبتنی بر باتری حیاتی است، و درک تفاوت بین سیستمهای بیدرنگ سخت و نرم که دیدگاه شما را نسبت به طراحی سیستمهای قابل اعتماد شکل میدهد. در نهایت، با یک ابزار حرفهای برای تحلیل گرافیکی و زنده سیستم آشنا میشویم.
۱۳.۱. بهینهسازی مصرف انرژی با Idle Task Hook و حالت Tickless
هوک وظیفه بیکار (Idle Task Hook)
وظیفه بیکار (`Idle Task`) پایینترین اولویت را در سیستم دارد و تنها زمانی اجرا میشود که هیچ وظیفه دیگری در وضعیت آماده (Ready) نباشد. با پیادهسازی هوک `vApplicationIdleHook`، میتوانید کدی را مشخص کنید که در این زمانهای بیکاری اجرا شود. رایجترین کاربرد این هوک، قرار دادن میکروکنترلر در حالت **خواب (Sleep Mode)** برای کاهش چشمگیر مصرف انرژی است.
حالت بدون تیک (Tickless Idle Mode)
حتی زمانی که سیستم در حالت خواب است، وقفه دورهای `SysTick` همچنان میکروکنترلر را بیدار میکند تا تیک سیستم را افزایش دهد. این بیداریهای مکرر، مصرف انرژی را بالا میبرد. با فعال کردن `configUSE_TICKLESS_IDLE` در `FreeRTOSConfig.h`، این رفتار تغییر میکند. اگر FreeRTOS تشخیص دهد که سیستم برای مدت طولانی بیکار خواهد بود، وقفه `SysTick` را موقتاً متوقف کرده و با استفاده از یک تایمر کممصرف، یک وقفه را برای زمانی در آینده تنظیم میکند که اولین وظیفه باید از حالت مسدود خارج شود. این کار به میکروکنترلر اجازه میدهد تا برای مدتهای طولانیتری در حالت خواب عمیق باقی بماند.
۱۳.۲. نکات طراحی برای سیستمهای بیدرنگ سخت (Hard) و نرم (Soft)
درک نوع سیستم بیدرنگی که طراحی میکنید، بسیار مهم است.
- سیستم بیدرنگ نرم (Soft Real-Time): سیستمی است که از دست دادن یک مهلت زمانی (Deadline) مطلوب نیست، اما فاجعهبار هم نیست و تنها باعث کاهش کیفیت خدمات میشود. مثال: پخش آنلاین ویدئو یا یک رابط کاربری.
- سیستم بیدرنگ سخت (Hard Real-Time): سیستمی است که از دست دادن حتی یک مهلت زمانی به منزله شکست کامل سیستم است. نتیجه یک محاسبه تنها زمانی صحیح است که به موقع تحویل داده شود. مثال: سیستم ترمز ضد قفل خودرو (ABS)، سیستم کنترل پرواز یا یک ضربانساز قلب.
FreeRTOS به دلیل ماهیت قابل پیشبینی زمانبند خود، میتواند در بسیاری از سیستمهای بیدرنگ سخت استفاده شود، اما این مسئولیت بر عهده طراح سیستم است که با تحلیل دقیق، تضمین کند که هیچ مهلت زمانی از دست نخواهد رفت.
پروژه عملی ۲۱: پیادهسازی حالت Sleep با Idle Task Hook
هدف: کاهش مصرف انرژی با قرار دادن MCU در حالت خواب در زمانهای بیکاری.
پیکربندی: در `FreeRTOSConfig.h`، مقدار `configUSE_IDLE_HOOK` را برابر `1` قرار دهید.
/* USER CODE BEGIN 0 */
// پیادهسازی هوک وظیفه بیکار
void vApplicationIdleHook(void) {
// قرار دادن میکروکنترلر در حالت خواب کم مصرف
// وقفه بعدی (مثلا از یک دکمه یا تایمر) آن را بیدار خواهد کرد
HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
}
void BlinkerTask(void* arg) {
for(;;) {
HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
// وظیفه به مدت 2 ثانیه مسدود میشود
// در این مدت، وظیفه بیکار اجرا شده و هوک آن فراخوانی میشود
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
/* USER CODE END 0 */
// در تابع main
/* USER CODE BEGIN 2 */
xTaskCreate(BlinkerTask, "Blinker", 128, NULL, 1, NULL);
/* USER CODE END 2 */
در طول ۲ ثانیهای که `BlinkerTask` در حالت مسدود است، سیستم به حالت خواب رفته و مصرف انرژی به شدت کاهش مییابد.
پروژه عملی ۲۲: تحلیل گرافیکی و زنده سیستم با ابزار SEGGER SystemView
SEGGER SystemView یک ابزار فوقالعاده برای تحلیل و بصریسازی رفتار سیستمعامل بیدرنگ شماست. این ابزار به شما یک نمودار زمانی دقیق از اجرای وظایف، وقفهها، و تعامل با اشیاء RTOS (صف، سمافور و...) را به صورت زنده نمایش میدهد.
مراحل راهاندازی:
- نصب SystemView روی کامپیوتر: نرمافزار را از وبسایت رسمی SEGGER دانلود و نصب کنید.
- اضافه کردن سورسهای SystemView به پروژه: فایلهای مربوط به سمت میکروکنترلر (Target) را به پروژه STM32CubeIDE خود اضافه کنید.
- پیکربندی: ماکروهای لازم برای اتصال FreeRTOS به SystemView را در `FreeRTOSConfig.h` تعریف کنید.
پس از راهاندازی، با اجرای هر کدام از پروژههای چندوظیفهای قبلی و اجرای نرمافزار SystemView روی کامپیوتر، میتوانید به صورت زنده مشاهده کنید که:
- کدام وظیفه در هر لحظه در حال اجراست.
- چه زمانی جابجایی وظیفه (Context Switch) رخ میدهد.
- چه زمانی یک وظیفه برای یک سمافور یا صف مسدود میشود.
- چه زمانی یک وقفه رخ میدهد و چه مدت طول میکشد.
این ابزار دید بینظیری از عملکرد داخلی سیستم به شما میدهد و برای اشکالزدایی مشکلات پیچیده زمانی، ابزاری بیرقیب است.
Comments
Post a Comment