در این بخش به معرفی چالشها و مشکلاتی که در محیطهای Multithread ممکن است روی دهد پرداخته شده است. سپس راهکارهای متناسب با هر کدام بیان شده است. در نهایت نحوه پیاده سازی و استفاده از آن در میکروکنترلرهای STM32 با استفاده از یک مثال عملی بیان شده است.
فهرست مطالب
سخت افزار مورد نیاز
سنسور MPU9265 و هر یک از میکروکنترلرهای 32 بیتی شرکت ST. در این پروژه از بورد STM32F4DISCOVERY استفاده شده است.
نرم افزار مورد نیاز
- stm32cubemx
- یک نرم افزار برای کامپایل و پروگرم کردن میکروکنترلر. در این پروژه از بورد IAR استفاده شده است.
شرایط رقابتی
یکی از چالشهایی که در محیطهای Multithread روی میدهد شرایط رقابتی است. شرایط رقابتی هنگامی روی میدهد که دو یا چند Thread بطور همزمان از یک منبع مشترک استفاده کنند. منبع مشترک میتواند یک متغیر ساده و یا یک بافر داده و یا حتی رجیسترهای پردازنده و واحدهای داخلی آن باشد. با توجه به اینکه Scheduler در هر لحظه میتواند تسک در حال اجرا را متوقف و تسک دیگری را جایگزین آن کند، ترتیب اجرای تسکها قابل پیش بینی نیست و مقدار داده یا منبع مشترک به الگوریتم اجرای Scheduler وابسته میشود.
برای درک این موضوع به کد ساده زیر توجه کنید.
myVariable++;
مقدار اولیه myVariable برابر 17 است. برای انجام این دستور ابتدا باید مقدار فعلی myVariable از حافظه خوانده شود و سپس به آن یک واحد اضافه شود و مقدار نهایی آن در حافظه نوشته شود. حال شرایطی را درنظر بگیرید که دو Thread به نامهای A و B بطور همزمان کد فوق را اجرا نمایند. فرض کنید ابتدا Thread A مقدار myVariable را میخواند. سپس مقدار آن را یک واحد افزایش داده میدهد. قبل از اینکه Thread A مقدار جدید آن را در حافظه بنویسد، Thread B مقدار متغیر myVariable را که 17 است از حافظه خوانده و یک واحد به آن اضافه میکند و مقدار جدید آن را که برابر 18 است را در حافظه مینویسد. همانطور که ملاحظه میکنید با اینکه هر دو Thread مقدار متغیر را یک واحد اضافه کردهاند ولی مقدار نهایی آن 18 شده است.
برای جلوگیری از ایجاد شرایط رقابتی در سیستم عامل FreeRTOS سه مکانیزم در نظر گرفته شده است.
- روش اول مشخص کردن قسمتهای حساس برنامه با استفاده از ماکروهای taskENTER_CRITICAL و taskEXIT_CRITICAL. با فراخوانی ماکروی taskENTER_CRITICAL کلیه وقفههای سیستم را غیرفعال میشود. ماکروی taskEXIT_CRITICAL نیز وقفه ها را مجددا فعال میکند. بدین ترتیب این روش مانع دسترسی سایر تسکها و وقفههای سخت افزاری و نرم افزاری سیستم به قسمتهای حساس نرم افزار میشود. در کد زیر نحوه استفاده از این ماکروها نشان داده شده است.
taskENTER_CRITICAL();
sensetiveOperation();
taskEXIT_CRITICAL();
- روش دوم برای جلوگیری از شرایط رقابتی غیرفعال کردن Scheduler است. بدین ترتیب قبل از انجام قمست حساس برنامه Scheduler غیرفعال شده و پس از اتمام عملیات موردنظر مجددا Scheduler فعال میشود. برای غیرفعال نمودن Scheduler از تابع vTaskSuspendAll و برای فعال کردن آن از xTaskResumeAll استفاده میشود. در کد زیر نحوه استفاده از این روش نشان داده شده است.
vTaskSuspendAll();
sensetiveOperation();
xTaskResumeAll();
- روش سوم برای حل مسئله شرایط رقابتی استفاده از Mutex است. Mutex نوع خاصی از Binary Semaphore است که برای کنترل دسترسی به منابع استفاده میشود. Mutex همانند یک Token عمل میکند. بطوریکه فقط تسکی که Token را داشته باشد امکان دسترسی به منبع مشترک را دارد. هر تسک هنگامی که بخواهد از منبع مشترک استفاده کند ابتدا باید درخواست Mutex دهد. در صورتیکه Mutex در اختیار تسک دیگری نباشد، Mutex را بدست میآورد و بعد از آن میتواند به منبع مشترک دسترسی داشته باشد. هنگامیکه تسک دیگر نیازی به منبع مشترک نداشته باشد باید Mutex را رها کند تا تسکهای دیگر بتوانند از منبع مشترک استفاده کنند.
Deadlock
در محیطی که تسکها برای در اختیار گرفتن منابع مشترک با هم رقابت میکنند، ممکن است شرایطی پیش آید که تسکها راه استفاده از منابع را بر یکدیگر سد کنند. به حالتی که در آن مجموعه ای از تسکها که منتظر آزاد شدن منابعی باشند که در اختیار همین تسکها باشد، Deadlock گفته میشود. Deadlock هنگامی روی میدهد چند تسک از چندین منبع مشترک ولی با ترتیب های متفاوت استفاده میکنند. Deadlock در شکل 2 نشان داده شده است. در این شکل فرض کنید تسک 1 منبع Obj1 را در اختیار دارد و به Obj2 نیازمند است. همینطور تسک 2 منبع Obj2 را در اختیار دارد و میخواهد به شی Obj1 دسترسی داشته باشد. بدین ترتیب این دو تسک تا بینهایت منتظر یکدیگر میمانند.
بعنوان مثال دیگر شکل زیر را در نظر بگیرید. همانطور که مشاهده میکنید هیچ از خودروها امکان جلو رفتن ندارند و بصورت غیر مستقیم مانع حرکت خودروهای دیگر شده اند.
برای حل مسئله Deadlock راهکارهای گوناگونی وجود دارد که مطمئنترین و ساده ترین روش آن جلوگیری از ایجاد آن در هنگام طراحی نرم افزار است. در سیستم های Embedded نیز بعلت سادگی نرم افزار و مسلط بودن کامل طراح بر آن، حل این مشکل بر عهده طراح قرار داده شده است.
Recursive Mutex
مسئله Deadlock میتواند حتی در یک تسک نیز روی دهد. هنگامی که یک تسک بخواهد یک Mutex را که قبلا گرفته است را مجددا بگیرد Deadlock روی میدهد. بعنوان مثال فرض کنید که پس از گرفتن یک Mutex تابعی فراخوانی شود که در آن مجددا آن Mutex درخواست شود.
در FreeRTOS برای حل این مشکل Recursive Mutex در نظر گرفته شده است. در این حالت با استفاده از یک کانتر تعداد دفعاتی که تسک Mutex را گرفته باشد شمرده میشود. هر بار که Mutex گرفته شود، کانتر یک واحد زیاد میشود و هر بار که رها شود یک واحد از آن کاسته میشود. هنگامیکه کانتر به صفر برسد Mutex رها میشود.
Priority Inversion
Priority Inversion هنگامی روی میدهد یک تسک با اولویت پایین بر تسک با اولویت بالاتر مقدم شود. برای درک این موضوع یک سیستم با سه تسک به نامهای A، B و C در نظر بگیرید که اولویت A از B و اولویت B از C بالاتر است. تسک A و C از یک منبع مشترک استفاده میکنند و برای جلوگیری از ایجاد شرایط رقابتی از Mutex استفاده شده است. فرض کنید ابتدا تسک C منبع مشترک را در ختیار دارد و Mutex را Lock کرده است. در این لحظه تسک A اجرا شده و تسک C را Preempt میکند. تسک A برای استفاده از منبع مشترک درخواست Mutex را میدهد. تا زمانیکه تسک C، Mutex را رهاسازی نکند، تسک A باید منتظر بماند لذا تسک A به حالت کاری Block میرود. در این لحظه تسک B نیز در حالت ready قرار دارد. Scheduler از بین تسکهای B و C تسک B را که اولویت بالاتری دارد انتخاب میکند. بدین تسک A باید منتظر تسک B که اولویت پایین تری نسبت به آن دارد، بماند. به این حالت Priority Inversion گفته میشود.
در سیستم عامل FreeRTOS برای حل این مشکل راهکار Priority Inheritance در نظر گرفته شده است. در این روش اولویت تسکی که Mutex را گرفته است را تا اولویت بالاترین تسکی که از آن منبع مشترک استفاده میکند، موقتا افزایش داده میشود. لذا در شکل 4 هنگامیکه تسک A منتظر تسک C است، تسک C اولویت تسک A را به ارث میبرد و مانع از اجرا شدن تسک B میشود.
نحوه تعریف Mutex در نرم افزار STM32CubeMX
برای تعریف Mutex میتوان مستقیما از توابع FreeRTOS استفاده نمود و یا بصورت گرافیکی با نرم افزار STM32CubeMX انجام داد. برای تعریف Mutex در قسمت FreeRTOS سربرگ Mutexes را انتخاب نمایید. سپس در دکمه Add کلیک کنید پنجره New Mutex باز شود. در این پنجره نام دلخواه خود را وارد کرده و سپس بر روی OK کلیک کنید. نوع اختصاص آن میتواند بصورت Static و یا Dynamic باشد. در صورتیکه گزینه Dynamic انتخاب شود، از حافظه Heap برای تعریف Mutex استفاده میشود و اگر گزینه Static انتخاب شود از حافظه Stack برای این منظور استفاده میشود.
برای تعریف Recursive Mutex ابتدا باید از سر برگ Config parameters قسمت Kernel setting گزینه USE_RECURSIVE_MUTEXES فعال نمود. سپس در سربرگ Mutexes بخش Recursive Mutexes میتوان تعریف نمود.
در این مثال آموزشی نحوه خواندن داده از ماژول MPU9265 که یک سنسور نه محوره MEM است با استفاده از FreeRTOS با گذرگاه SPI بیان شده است. برای این منظور سه تسک به نامهای task_A، Task_B و Task_C با اولویتهای یکسان برای خواندن مولفه شتاب هر یک از محورهای مختصات تعریف شده است. هر تسک داده مربوط به یک محور مختصات را خوانده و با گذرگاه UART به کامپیوتر ارسال میکند. با توجه به اینکه هر سه تسک بطور همزمان از واحد SPI و UART استفاده میکنند ممکن است Race Condition روی دهد. لذا یک Mutex به نام MPU_9250_Mutex تعریف شده است.
برای تعریف Mutex در نرم افزار STM32CubeMX به سربرگ Mutexs رفته و بر روی دکمه Add کلیک کنید تا پنجره New Mutex باز شود. در این پنجره نام آن و مکانیزم اختصاص حافظه آن را مشخص کنید. در شکل زیر نحوه تعریف نشان داده شده است.
در شکل زیر تسکهای تعریف شده نشان داده شده است.
برای ارتباط با ماژول MPU9265 از گذرگاه SPI1 میکروکنتر استفاده شده است. نحوه تنظیم آن در شکل زیر نشان داده شده است. از پایه PC.4 بعنوان پایه Chip Select استفاده شده است.
بعد از انجام تنظیمات فوق به سربرگ Project Manager رفته و نام و مسیر ذخیره پروژه را تعیین کنید و در نهایت بر روی دکمه GENERATE CODE کلیک کنید.
نرم افزار
در این کد زیر نحوه خواندن داده از سنسور و چگونگی استفاده از Mutex نشان داده شده است. هر تسک داده مورد نظر خود را میخواند و سپس به کامپیوتر ارسال میکند.
درود بر شما
واقعا مطلب مفیدی بود.
تشکر