در مجموعه آموزش های سیستم عامل FreeRTOS در برد های ESP32، این قسمت را به یکی از مهم ترین و پرکاربردترین مفاهیم سیستم عامل اختصاص می دهیم. همانطور که در قسمت پیشین دیدید، دو برنامه به صورت همزمان روی برد ESP32 اجرا شد. یکی از این برنامه ها وظیفه خواندن ورودی آنالوگ و دیگری وظیفه کنترل خروجی را بر عهده داشت. اما اگر دو یا چند برنامه قصد داشته باشند تا از یک منبع مشترک استفاده کنند، مسئله بحرانی خواهد شد. در این جا مفهومی به اسم سمافور، مانع ایجاد تداخل خواهد شد. در این آموزش به بحث سمافور semaphore و نحوه پیاده سازی آن در بردهای ESP32 خواهیم پرداخت. بنابراین در ادامه آموزش با مرجع تخصصی سیستم عامل FreeRTOS به زبان فارسی، دیجی اسپارک همراه باشید.
سمافور semaphore تعریف و کاربرد
اگر بخواهیم سمافور semaphore را در یک جمله خلاصه کنیم، خواهیم نوشت: مکانیزم کنترل و جلوگیری از تداخل. در یک یمیکروکنترلر، از یک قابلیت سخت افزاری، تنها یک نمونه وجود دارد. به عنوان مثال می توان به پورت سریال UART، پورت سریال I2C و SPI و …. اشاره نمود. به طور مشابه فرض کنید Task شماره ۱ قصد دارد تا داده ای را با دستور Serial.print روی پورت سریال بنویسد. به طور همزمان، برنامه دیگر با عنوان Task شماره ۲ نیز قصد چنین کاری دارد. از آنجاییکه یکی از Task ها پورت سریال را در اختیار گرفته، در نتیجه Task شماره ۲ نمی تواند به پورت سریال دسترسی داشته باشد.
در اینجا سیستم عامل از یک مکانیزم ویژه و خاص به اسم سمافور استفاده می کند. سمافور یک مکانیزم جالب جهت کنترل از تداخل است. در حقیقت سمافور همانند یک برچسب(Tag) وضعیت یک منبع را مشخص می کند. زمانیکه یک منبع توسط یک Task در حال استفاده باشد، سمافور برچسب مشغول به آن می زند. در این حالت، چنانچه Task دیگری بخواهد از منبع استفاده کند، با توجه به برچسب مشغول بودن، به آن منبع دسترسی پیدا نکرده و تا زمان آزاد شدن آن، منتظر باقی می ماند.
مکانیزم سمافور یک مکانیزم بسیار کاربردی، فوق العاده در زمان اجرای Task ها با اولویت های مختلف است. تصور کنید که یک Task با اولویت بالا در حال استفاده از یک منبع است. از طرف دیگر، یک Task با اولویت پایین تر قصد استفاده از همان منبع را دارد. در این جا جهت مدیریت درخواست، به طوریکه منبع همواره به دست Task با اولویت بالا نباشد و Task با اولویت پایین هم توان استفاده از منبع را داشته باشد، سمافور گزینه بسیار مناسبی است.
تعریف سمافور در ESP32 قدم اول ایجاد شی
در قدم اول برای استفاده از سمافور در سیستم عامل، باید از کلاس کتابخانه آن یک شی تعریف کنیم. بنابراین به صورت Global، تعریف شی را به صورت زیر با یک نام دلخواه، اینجا به اسم xMutex، انتخاب می کنیم. در ادامه به صورت زیر، شی را تعریف خواهیم نمود.
SemaphoreHandle_t xMutex;
در ادامه پس از تعریف شی، می بایست تابع سازنده کلاس کتابخانه را نیز فراخوانی کنیم. به این منظور، در تابع setup، به شیوه زیر عمل می کنیم.
xMutex = xSemaphoreCreateMutex()
در ادامه باید درون Task ها، سمافور تعریف شود. برای تعریف سمافور درون توابع باید به شکل خاصی عمل شود. قسمت بعد مربوط به این موضوع است.
تعریف سمافور در ESP32: قدم دوم توابع سمافور
پس از بررسی سمافور، نوبت به فراخوانی تابع می رسد. زمانیکه وارد یک تابع می شویم، ابتدا باید به سمافور اطلاع دهیم. زمانیکه کار با منبع تمام شد، باید به سمافور پیغام مناسب داده شود. برای اطلاع از استفاده سمافور، از تابع زیر استفاده می کنیم.
xSemaphoreTake( xMutex, portMAX_DELAY );
مطابق تابع فوق، دو ورودی داریم. ورودی نخست که همان شی تعریف شده است. ورودی دوم یک ثابت تعریف شده است. این ثابت یک تاخیر ایجاد می کند. در حقیقت این تاخیر تعیین می کند که منبع به چه مدت زمان در اختیار Task باشد. مقدار این ثابت به صورت هگز و به مقدار ۰xffffffffUL است. این مقدار در دهدهی به صورت ۲ به توان ۳۲ منهای عدد یک است. در ادامه، زمانیکه کار با منبع تمام می شود، باید آن را آزاد کنیم. آزاد کردن آن در سطح سمافور، به صورت زیر انجام می شود.
xSemaphoreGive( xMutex )
برای اینکه موضوع سمافور روشن تر شود، قسمت بعد را به یک مثال اختصاص می دهیم. در این مثال دو Task، یکی با اولویت بیشتر و دیگری با اولویت کمتر تعریف شده است.
SemaphoreHandle_t xMutex; //تعریف سمافور void setup() { Serial.begin(112500); xMutex = xSemaphoreCreateMutex(); //ایجاد یک شی از کلاس سمافور xTaskCreate( lowPriorityTask, //تابع Task "lowPriorityTask", //نام Task ۱۰۰۰, //حجم Task NULL, //ورودی Task ۱, //اولویت Task NULL); //تابع هندل Task delay(500); //Task با اولویت کمتر xTaskCreate( highPriorityTask, "highPriorityTask", ۱۰۰۰, NULL, ۴, NULL); } void loop() { } void lowPriorityTask( void * parameter ) { Serial.println((char *)parameter); for(;;){ Serial.println("lowPriorityTask gains key"); xSemaphoreTake( xMutex, portMAX_DELAY ); delay(2000); //مدت زمان بیشتر برای Task که بتواند منبع سریال را در اختیار داشته باشد Serial.println("lowPriorityTask releases key"); xSemaphoreGive( xMutex ); } vTaskDelete( NULL ); } void highPriorityTask( void * parameter ) { Serial.println((char *)parameter); for(;;){ Serial.println("highPriorityTask gains key"); xSemaphoreTake( xMutex, portMAX_DELAY ); Serial.println("highPriorityTask is running"); Serial.println("highPriorityTask releases key"); xSemaphoreGive( xMutex ); //یک ثانیه تاخیر زیر بعد از پس دادن سمافور است، بنابراین Task با اولویت کمتر ه می تواند به منبع دسترسی داشته باشد. delay(1000); } vTaskDelete( NULL ); }
نواحی بحرانی برنامه ها
پس از بررسی مفهوم سمافور و نقش آن در کنترل روال برنامه ها، نوبت به بررسی یک مفهوم بسیار کلیدی و مهم می رسد. در جریان برنامه نویسی تحت سیستم عامل، گاهی ممکن است بخشی از برنامه به صورت کامل و بدون همزمانی با سایر Task ها، اجرا شود. در این حالت به این قسمت از برنامه اصطلاحا ناحیه بحرانی گفته می شود. زمانیکه که کدهای ناحیه بحرانی اجرا می شود، سایر Task ها باید معلق شوند. به این منظور از یک تابع مخصوص استفاده می کنیم. با فراخوانی این تابع، سایر Task ها معلق می شوند. هرگاه که تابع آزاد سازی فراخوانی شود، اجرای سایر Task ها نیز از سر گرفته می شود. برای معرفی ناحیه بحرانی، از تابع زیر استفاده می شود.
taskENTER_CRITICAL(&mmux)
تابع فوق یک ورودی خاص دارد. این ورودی از نوع خاصی از سمافورهاست. این سمافور کلاس کتابخانه زیر است.
portMUX_INITIALIZER_UNLOCKED
در ادامه، زمانیکه اجرا به پایان می رسد، با دستور زیر می توانیم شرایط بحرانی را خاتمه دهیم.
taskEXIT_CRITICAL(&mmux)
در ادامه با ارائه مثالی، این مورد را بررسی خواهیم کرد. به برنامه زیر دقت کنید. در این برنامه دو Task تعریف شده است. یک Task وظیفه دارد تا از عدد صفر تا ۱۰۰ بشمارد. در این حین Task دیگر معلق شده تا این شمارش به پایان رسد.
#include "esp_task_wdt.h" //فراخوانی کتابخانه واچ داگ int count = 0; portMUX_TYPE mmux = portMUX_INITIALIZER_UNLOCKED; //تعریف شی void setup() { Serial.begin(112500); //تعریف Task xTaskCreate( lowPriorityTask, "lowPriorityTask", ۱۰۰۰۰, NULL, ۱, NULL); delay(500); xTaskCreate( highPriorityTask, "highPriorityTask", ۱۰۰۰۰, NULL, ۴, NULL); } void loop() { } void lowPriorityTask( void * parameter ) { for(;;){ Serial.println("lowPriorityTask lock section"); //در اینجا وارد ناحیه بحرانی می شویم taskENTER_CRITICAL(&mmux); for(count=0; count<1000; count++){ //توجه از آنجاییکه وقفه واچ داگ به صورت پیشفرض از سیستم عامل تغذیه شده اما ما کلیه Task هار معلق کرده ایم، پس به صورت دستی این وقفه را مقدار دهی می کنیم. //وقفه واچ داگ یا به عبارت بهتر تایمر واچ داگ باید داعم در حال اجرا باشد. esp_task_wdt_feed(); } // خروج از ناحیه بحرانی taskEXIT_CRITICAL(&mmux); Serial.println("lowPriorityTask leave section"); } vTaskDelete( NULL ); } void highPriorityTask( void * parameter ) { for(;;){ //پس از اتمام شمارش توسط Task باید در این قسمت عدد نمایش داده شود. Serial.print("highPriorityTask is running and count is "); Serial.println(count); delay(50); } vTaskDelete( NULL ); }
لوازم مورد نیاز
لینک خرید برد ESP32، کلیک کنید
جمع بندی
در مجموعه آموزش های سیستم عامل برای برد ESP32 این قسمت را به یکی از مفاهیم پایه و اساسی سیستم عامل FreeRTOS اختصاص دادیم. در این قسمت به بررسی مفهوم سمافور پرداختیم. به کمک سمافور می توان تداخل بین دو Task حین استفاده از یک منبع مشترک را برطرف نمود. همچنین به کمک این مفهوم می توان یک ناحیه بحرانی تعریف نمود؛ به طوریکه با ورود به ناحیه بحرانی در یک Task، سایر Task ها معلق باقی بمانند.
چنانچه در مراحل راه اندازی و انجام این پروژه با مشکل مواجه شدید، بدون هیچ نگرانی در انتهای همین پست، به صورت ثبت نظر سوالتان را مطرح کنید. من در سریعترین زمان ممکن پاسخ رفع مشکل شما را خواهم داد. همچنین اگر ایرادی در کدها و یا مراحل اجرایی وجود دارند میتوانید از همین طریق اطلاع رسانی کنید.