استفاده از Mutex در FreeRTOS در میکروکنترلرهای STM32

0

استفاده از Mutex در FreeRTOS در میکروکنترلرهای STM32

در این بخش به معرفی چالش­ها و مشکلاتی که در محیط­های 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 شده است.

مثال شرایط رقابتی در محیط¬های Multithread
مثال شرایط رقابتی در محیط¬های Multithread

برای جلوگیری از ایجاد شرایط رقابتی در سیستم عامل FreeRTOS سه مکانیزم در نظر گرفته شده است.

  1. روش اول مشخص کردن قسمت­های حساس برنامه با استفاده از ماکروهای taskENTER_CRITICAL و taskEXIT_CRITICAL. با فراخوانی ماکروی taskENTER_CRITICAL کلیه وقفه­های سیستم را غیرفعال می­شود. ماکروی taskEXIT_CRITICAL نیز وقفه ها را مجددا فعال می­کند. بدین ترتیب این روش مانع دسترسی سایر تسک­ها و وقفه­های سخت افزاری و نرم افزاری سیستم به قسمت­های حساس نرم افزار می­شود. در کد زیر نحوه استفاده از این ماکروها نشان داده شده است.

taskENTER_CRITICAL();

sensetiveOperation();

taskEXIT_CRITICAL();

  1. روش دوم برای جلوگیری از شرایط رقابتی غیرفعال کردن Scheduler است. بدین ترتیب قبل از انجام قمست حساس برنامه Scheduler غیرفعال شده و پس از اتمام عملیات موردنظر مجددا Scheduler فعال می­شود. برای غیرفعال نمودن Scheduler از تابع vTaskSuspendAll و برای فعال کردن آن از xTaskResumeAll استفاده می­شود. در کد زیر نحوه استفاده از این روش نشان داده شده است.

vTaskSuspendAll();

sensetiveOperation();

xTaskResumeAll();

  1. روش سوم برای حل مسئله شرایط رقابتی استفاده از Mutex است. Mutex نوع خاصی از Binary Semaphore است که برای کنترل دسترسی به منابع استفاده می­شود. Mutex همانند یک Token عمل می­کند. بطوریکه فقط تسکی که Token را داشته باشد امکان دسترسی به منبع مشترک را دارد. هر تسک هنگامی که بخواهد از منبع مشترک استفاده کند ابتدا باید درخواست Mutex ­دهد. در صورتیکه Mutex در اختیار تسک دیگری نباشد، Mutex را بدست می­آورد و بعد از آن می­تواند به منبع مشترک دسترسی داشته باشد. هنگامی­که تسک دیگر نیازی به منبع مشترک نداشته باشد باید Mutex را رها کند تا تسک­های دیگر بتوانند از منبع مشترک استفاده کنند.

Deadlock

در محیطی که تسک­ها برای در اختیار گرفتن منابع مشترک با هم رقابت می­کنند، ممکن است شرایطی پیش آید که تسک­ها راه استفاده از منابع را بر یکدیگر سد کنند. به حالتی که در آن مجموعه ای از تسک­ها که منتظر آزاد شدن منابعی باشند که در اختیار همین تسک­ها باشد، Deadlock گفته می­شود. Deadlock هنگامی روی می­دهد چند تسک از چندین منبع مشترک ولی با ترتیب های متفاوت استفاده می­کنند. Deadlock در شکل 2 نشان داده شده است. در این شکل فرض کنید تسک 1 منبع Obj1 را در اختیار دارد و به Obj2 نیازمند است. همینطور تسک 2 منبع Obj2 را در اختیار دارد و میخواهد به شی Obj1 دسترسی داشته باشد. بدین ترتیب این دو تسک تا بینهایت منتظر یکدیگر می­مانند.

رخ دادن Deadlock بین دو Thread
رخ دادن Deadlock بین دو Thread

بعنوان مثال دیگر شکل زیر را در نظر بگیرید. همانطور که مشاهده می­کنید هیچ از خودروها امکان جلو رفتن ندارند و بصورت غیر مستقیم مانع حرکت خودروهای دیگر شده اند.

Deadlock در زندگی روزمره
Deadlock در زندگی روزمره

برای حل مسئله 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 گفته می­شود.

نمایش Priority Inversion
نمایش 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 باز شود. در این پنجره نام آن و مکانیزم اختصاص حافظه آن را مشخص کنید. در شکل زیر نحوه تعریف نشان داده شده است.

نحوه تعریف Mutex در نرم افزار STM32CubeMX
نحوه تعریف Mutex در نرم افزار STM32CubeMX

در شکل زیر تسک­­های تعریف شده نشان داده شده است.

تسکهای تعریف مثال اول در نرم افزار STM32CubeMX
تسکهای تعریف مثال اول در نرم افزار STM32CubeMX

برای ارتباط با ماژول MPU9265  از گذرگاه SPI1 میکروکنتر استفاده شده است. نحوه تنظیم آن در شکل زیر نشان داده شده است. از پایه PC.4  بعنوان پایه Chip Select استفاده شده است.

نحوه تنظیم واحد SPI1 برای خواندن داده از ماژول MPU9265
نحوه تنظیم واحد SPI1 برای خواندن داده از ماژول MPU9265
نحوه تنظیم واحد UART
نحوه تنظیم واحد UART
نحوه تنظیم کلاک در میکروکنترلر STM32CubeMX
نحوه تنظیم کلاک در میکروکنترلر STM32CubeMX

بعد از انجام تنظیمات فوق به سربرگ Project Manager رفته و نام و مسیر ذخیره پروژه را تعیین کنید و در نهایت بر روی دکمه GENERATE CODE کلیک کنید.

نرم افزار

در این کد زیر نحوه خواندن داده از سنسور و چگونگی استفاده از Mutex نشان داده شده است. هر تسک داده مورد نظر خود را می­خواند و سپس به کامپیوتر ارسال می­کند.

/* USER CODE END Header_Func_A */
void Func_A(void const * argument)
{
  /* USER CODE BEGIN 5 */
  uint8_t TxDataTaskA[5];
  uint8_t RxDataTaskA[5];
  uint16_t ACCEL_XOUT=0;
  TxDataTaskA[0] = 59 | 0x80;
  /* Infinite loop */
  for(;;)
  {
    osMutexWait(MPU_9250_MutexHandle, osWaitForever);
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_RESET);
    HAL_Delay(1);
    HAL_SPI_TransmitReceive(&hspi1, TxDataTaskA, RxDataTaskA, 3, 100);
    HAL_Delay(1);
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_SET);
    ACCEL_XOUT = ((uint16_t)RxDataTaskA[1] << 8) + (uint16_t)RxDataTaskA[2];
    sprintf(strDataToPC, "X axis = %4d\n", ACCEL_XOUT);
    HAL_UART_Transmit(&huart3, strDataToPC, strlen(strDataToPC), 100);
    osMutexRelease(MPU_9250_MutexHandle);
    
    osDelay(100);
  }
  /* USER CODE END 5 */ 
}

/* USER CODE END Header_Func_B */
void Func_B(void const * argument)
{
  /* USER CODE BEGIN Func_B */
  uint8_t TxDataTaskB[5];
  uint8_t RxDataTaskB[5];
  uint16_t ACCEL_YOUT=0;
  
  TxDataTaskB[0] = 61 | 0x80;
  /* Infinite loop */
  for(;;)
  {
    osMutexWait(MPU_9250_MutexHandle, osWaitForever);
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_RESET);
    HAL_Delay(1);
    HAL_SPI_TransmitReceive(&hspi1, TxDataTaskB, RxDataTaskB, 3, 100);
    HAL_Delay(1);
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_SET);
    ACCEL_YOUT = ((uint16_t)RxDataTaskB[1] << 8) + (uint16_t)RxDataTaskB[2];
    sprintf(strDataToPC, "Y axis = %4d\n", ACCEL_YOUT);
    HAL_UART_Transmit(&huart3, strDataToPC, strlen(strDataToPC), 100);
    
    osMutexRelease(MPU_9250_MutexHandle);
    osDelay(100);
  }
  /* USER CODE END Func_B */
}

/* USER CODE BEGIN Header_Func_C */

/* USER CODE END Header_Func_C */
void Func_C(void const * argument)
{
  /* USER CODE BEGIN Func_C */
  uint8_t TxDataTaskC[5];
  uint8_t RxDataTaskC[5];
  uint16_t ACCEL_ZOUT=0;
  TxDataTaskC[0] = 63 | 0x80;
  /* Infinite loop */
  for(;;)
  {
    osMutexWait(MPU_9250_MutexHandle, osWaitForever);
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_RESET);
    HAL_Delay(1);
    HAL_SPI_TransmitReceive(&hspi1, TxDataTaskC, RxDataTaskC, 3, 100);
    HAL_Delay(1);
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_SET);
    ACCEL_ZOUT = ((uint16_t)RxDataTaskC[1] << 8) + (uint16_t)RxDataTaskC[2];
    sprintf(strDataToPC, "Z axis = %4d\n", ACCEL_ZOUT);
    HAL_UART_Transmit(&huart3, strDataToPC, strlen(strDataToPC), 100);
    osMutexRelease(MPU_9250_MutexHandle);
    osDelay(100);
  }
  /* USER CODE END Func_C */
 
داده دریافت شده از سنسور MPU9265
داده دریافت شده از سنسور MPU9265
Choose your Reaction!
دیدگاه خود را بنویسید

آدرس ایمیل شما منتشر نخواهد شد.

redronic.com