چه زمان باید از کلمه کلیدی volatile استفاده کرد؟

در این پست قصد دارم در مورد کاربرد کلمه کلیدی volatile در برنامه نویسی میکروکنترلرها به زبان c صحبت کنم. دانستن اینکه volatile چیست و چه زمان باید از آن استفاده کرد، اهمیت بالایی  دارد. طوریکه استفاده نکردن از این عبارت در جای خود، می‌تواند موجب عمل نکردن برنامه شما شود.

شاید این تجربه را داشتید که وقتی بهینه سازی کامپایلر را از حالت کمترین خارج می‌کنید، برنامه دیگر به درستی کار نمی‌کند! اصولاً هم در این حالت بی‌خیال این بهینه سازی می‌شوید و عطایش را به لقایش می‌بخشید. شاید هم ایراد را از کِرَک برنامه بدانید که موجب چنین وضعیتی شده است. اما همیشه هم اینگونه نیست و این اتفاق می‌تواند به این دلیل افتاده باشد که شما در تعریف متغیر خود، از volatile استفاده نکرده‌اید.

کلمه کلیدی volatile در تعریف متغیرهای سراسری -global- به کار می‌رود. این عبارت به کامپایلر اعلام می‌کند که :

“مقدار این متغیر ممکنه خارج از روال عادی برنامه -که تو ازش خبر داری- تغییر کنه، پس حواستو جمع کن و هر وقت هم قرار شد ازش استفاده کنی یک بار مقدارش رو بخون -فرض رو بر این نگیر که مقدارش تغییر نکرده-.”

در این صورت کامپایلر هرجا این متغیر را می‌بیند بر رویش بهینه سازی‌ انجام نمی‌دهد و اینطور در نظر می‌گیرد که، از جایی نامشخص ممکن است مقدارش را تغییر داده باشند. اما این جای نامشخص -برای کامپایلر- چیست؟

در سیستم‌های نهفته 3 امکان برای این جای نامشخص وجود دارد:

1- متغیر اشاره‌ گر به رجیستر یک پریفرال باشد:

فرض کنید متغیر uart_dr اشاره‌گر به آدرس رجیستر data register از پریفرال UART باشد. مقدار این رجیستر هنگامی تغییر می‌کند که در پورت سریال شاهد دریافت دیتایی باشیم. در این صورت در هیچ جای برنامه مشخص نیست که چه زمان جایی که متغیر uart_dr به آن اشاره می‌کند تغییر خواهد کرد.

البته این مورد خیلی جای نگرانی ندارد. چراکه مثلاً برای میکروکنترلرهای ARM، ما اغلب از تعاریف موجود در هدرهای CMSIS برای دسترسی به یک رجیستر استفاده می‌کنیم و خب طبیعیه که در آنجا هم به این موضوع توجه شده. برای مثال به نحوه تعریف GPIO_TypeDef نگاه کنید:

typedef struct
{
  __IO uint32_t MODER;        /*!< GPIO port mode register,                     Address offset: 0x00      */
  __IO uint32_t OTYPER;       /*!< GPIO port output type register,              Address offset: 0x04      */
  __IO uint32_t OSPEEDR;      /*!< GPIO port output speed register,             Address offset: 0x08      */
  __IO uint32_t PUPDR;        /*!< GPIO port pull-up/pull-down register,        Address offset: 0x0C      */
  __IO uint32_t IDR;          /*!< GPIO port input data register,               Address offset: 0x10      */
  __IO uint32_t ODR;          /*!< GPIO port output data register,              Address offset: 0x14      */
  __IO uint32_t BSRR;         /*!< GPIO port bit set/reset register,      Address offset: 0x1A */
  __IO uint32_t LCKR;         /*!< GPIO port configuration lock register,       Address offset: 0x1C      */
  __IO uint32_t AFR[2];       /*!< GPIO alternate function low register,  Address offset: 0x20-0x24 */
  __IO uint32_t BRR;          /*!< GPIO bit reset register,                     Address offset: 0x28      */
} GPIO_TypeDef;

همانطور که می‌بینید پشت هر عضو آن پیشوند IO__ را داریم که درواقع بازتعریف volatile است.

2- مقدار این متغیر در سرویس روتین یک وقفه قرار است تغییر کند:

اول بهتر است تاکید کنم این مورد را با دقت بیشتری بخوانید. چراکه به نظرم متداول‌ترین مورد در بین این 3 وضعیت که احتمالاً گذرتان به آن خواهد خورد، است.

حالتی را در نظر بگیرید که شما تایمری از میکروکنترلر را به گونه‌ای تنظیم کرده‌اید که سر هر یک ثانیه وقفه می‌دهد. درون تابع وقفه متغیری سراسری را پلاس پلاس می‌کنید تا زمان را بر حسب ثانیه برای شما نگه دارد. حال از طرفی در برنامه اصلی شما قرار است هرگاه مقدار این متغیر به 60 رسید آن را صفر کنید و کاری دیگر را انجام دهید. اگر این متغیر را volatile تعریف نکنید ممکن است پس از بهینه سازی برنامه شما به درستی کار نکند.

3- در سیستم‌های Multithread که از دو یا چند thread با این متغیر کار دارند:

هر چند در برنامه ‌هایی که RTOS از اجزا برنامه است، راه‌های دیگری هم برای تبادل داده بین threadها داریم. اما استفاده از یک متغیر سراسری هم یکی از این راه‌هاست که گاهی مورد استفاده قرار می‌گیرد. در اینجا هم با چیزی مشابه حالت وقفه مواجه هستیم. در یک Preemptive RTOS کامپایلر هیچ ایده‌ای ندارد که چه زمان thread بعدی میرسد و اجرا این thread را متوقف می‌کند. به همین دلیل در اینجا هم لازم است متغیرهای سراسری که بین چند thread مورد استفاده قرار می‌گیرند را volatile تعریف کنیم تا بهینه سازی کامپایلر برایمان مشکل ساز نشود.

پی نوشت:

برای نوشتن این مطلب از دو لینک زیر استفاده شده. اگر در خوندن متن انگلیسی مشکلی ندارید، بد نیست نگاهی بهشون بندازید:

How to Use C’s volatile Keyword

Volatile keyword in microcontrollers

 

۹ دیدگاه‌ها

  1. سلام فوق العاده عالی متشکرم

  2. عالی توضیح دادید!

  3. سپاس عالی بود

  4. فوق العاده بود 👌👌👌👌

  5. سلام. خیلی خوب و کامل. ممنونم

  6. Mohammad Karbalaei

    عالی بود. ممنونم.

  7. ممنون.یکی از مشکلاتم حل شد.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *