استفاده از استاندارد DO-178B/C بسیاری از مسائل و مشکلات رایج مربوط به توسعه نرم‌افزارهای بلادرنگ را از بین می‌برد، اما هنوز مسائلی وجود دارد که در صنعت نرم‌افزارهای تعبیه‌شده بلادرنگ رایج است و همچنان دنیای اویونیک را آزار می‌دهد. در این گزارش اشتباهات متعددی که توسط مهندسان نرم‌افزارهای Embedded در اویونیک و رایج در استاندارد DO-178B/C به وجود می‌آید، مطرح می‌شود. همچنین برای اینگونه مسائل راه‌حل‌هایی ارائه شده است.

1- استفاده از متغیرهای سراسری

متغیرهای سراسری اغلب توسط مهندسان نرم‌افزار منسوخ شده‌اند به این دلیل که این متغیرها معیار کپسوله‌سازی[1] در طراحی شی‌گرا را نقض می‌کنند و حفظ و نگهداری نرم‌افزار را سخت‌تر می‌کنند. درحالیکه این دلایل در توسعه نرم‌افزار‌های بلادرنگ در نظر گرفته می‌شود، بنابراین اجتناب از استفاده از متغیرهای سراسری در سیستم‌های بلادرنگ حائز اهمیت است.

در سیستم‌عامل‌های بلادرنگ فرآیندها معمولا به صورت نخ[2] اجرا می‌شوند. در این شرایط متغیر سراسری بین همه فرآیند‌ها به اشتراک گذاشته می‌شود. بنابراین دو فرآیند که از ماژول مشابه با یک متغیر سراسری تعریف شده استفاده می‌کنند، مقدار یکسانی به اشتراک خواهند گذاشت. چنین درگیری‌هایی عملکرد سیستم را مختل می‌کند، از این‌رو مشکل متغیر سراسری مساله‌ای فراتر از حفظ و نگهداری نرم‌افزار است.

2- استفاده نامناسب یا عدم استفاده از وقفه‌ها

وقفه‌ها شاید بزرگترین دلیل معکوس کردن انحراف اولویت در سیستم‌های بلادرنگ باشند. همین دلیل باعث می‌شود سیستم تمام الزامات زمان‌بندی خود را برآورده نکند. دلیل این تاخیر این است که وقفه‌ها از هر عملیاتی پیشی گرفته و برنامه‌ریزی نشده‌ عمل می‌کنند. اگربرنامه‌نویس رویدادی را به طور منظم برنامه‌ریزی کند، ممکن است رفتار ناخواسته‌ای رخ دهد. یک سیستم بلادرنگ ایده‌آل بدون وقفه است. بسیاری از برنامه‌نویسان 80 تا 90 درصد کد برنامه را درون بخش وقفه‌ها قرار می‌دهند. پردازش کامل درخواست‌های I/O و حلقه‌های دوره‌ای متداول‌ترین مواردی هستند که در بحث مدیریت‌ وقفه قرار می‌گیرد. برنامه‌نویسان ادعا می‌کنند که یک مدیر وقفه سربار کمتری روی سیستم‌عامل دارد، بنابراین سیستم بهتر کار می‌کند. با اینکه رسیدگی به وقفه نسبت به روش تعویض زمینه[3] (به فرایند ذخیره کردن و بازیابی وضعیت یک پردازش گفته می‌شود، به‌طوری‌که اجرای آن پردازش بتواند بعداً از همان نقطه ادامه یابد) دارای سربار کمتری است اما لزوما به دلایل مختلف بهتر عمل نمی‌کند.

  • وقفه‌ها اولویت بالایی دارند و می‌توانند باعث معکوس کردن اولویت شوند.
  • وقفه‌ها محدوده زمانی الگوریتم برنامه‌ریزی بلادرنگ را کاهش می‌دهند، بنابراین هرگونه صرفه‌جویی در سربار در مقایسه با روش تعویض زمینه بی‌‌اثر خواهد بود.
  • وقفه‌ها برای اشکال‌زدایی و آنالیز دشوار هستند، به این دلیل که تعداد کمی از برنامه‌های اشکال‌یاب‌ اجازه تنظیم نقطه انفضال‌ را درون متن وقفه می‌دهند.

3- اطلاعات پیکربندی در اعلان‌های #define

برنامه‌نویسان embedded به طور مستمر از اعلان‌های #define در کد‌های خود استفاده می‌کنند. از این اعلان برای تعیین آدرس‌های رجیستر، بازه آرایه‌ها و ثابت‌های پیکربندی استفاده می‌شود. اگرچه این عمل رایجی است اما امری نامطلوب محسوب می‌شود؛ زیرا استفاده مجدد از نرم‌افزار را در سایر برنامه‌های مشابه بسیار دشوار می‌کند.

مشکل از آنجاست که یک اعلان #define در همه جای کد منبع گسترش پیدا می‌کند. بنابراین ممکن است مقدار تعریف شده در بیش از 20 مکان مختلف در کد نمایش داده شود. از این‌رو اگر این مقدار در کد شی تغییر کند، اشاره به یک مکان واحد برای ایجاد تغییر آن آسان نیست.

4- تشخیص و رسیدگی به خطا یک چاره‌اندیشی موقت است که از طریق آزمون و خطا اجرا می‌شود

تشخیص و رسیدگی به خطا به ندرت در یک قالب معنادار و معقول در طراحی نرم‌افزار گنجانده می‌شود. طراحی نرم‌افزار عمدتا روی عملکرد عادی تمرکز دارد و هر گونه استثناء و رسیدگی به خطا بعد از وقوع توسط برنامه‌نویس در نظر گرفته می‌شود. از این‌رو برنامه‌نویس عملیات تشخیص خطا را در هر جای نرم‌افزار اعمال می‌کند. این در حالیست که در بسیاری از مواقع نیازی به آن نیست و حضور آن بر عملکرد و زمان‌بندی تاثیر می‌گذارد.

تشخیص خطا باید در طراحی سیستم، درست مانند هر حالت دیگر گنجانده شود. بنابراین اگر برنامه‌ای توسط یک ماشین حالت متناهی ساخته شود، هر مورد استثناء و خطا می‌تواند به عنوان یک ورودی که موجب عمل و انتقال به حالت جدید می‌شود، در نظر گرفته شود.

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

5- استفاده از روش انتقال پیام (message passing) به عنوان ارتباط بین فرآیندهای اصلی

زمانیکه نرم‌افزاری به صورت بلوک‌های عملکردی[4] توسعه پیدا می‌کند؛ اولین هدف، پیاده‌سازی ورودی‌ها و خروجی‌ها به صورت پیام است. این در محیط‌های غیر بلادرنگ مانند شبکه‌های توزیع شده و زیرسیستم‌های اصلی که سطح‌های مختلفی از نیازمندی‌های بلادرنگ وجود دارد، به خوبی کار می‌کند. با این حال در سیستم‌های بلادرنگ استفاده از این روش به عنوان خط ارتباطی بین نخ‌های یک فرایند، مشکل ساز است. در روش انتقال پیام به جای خواندن و نوشتن در یک حافظه‌ی مشترک، فرآیند‌ها به یکدیگر پیغام می فرستند و داده‌های مورد نظر خود را منتقل می‌کنند. در این حالت به یک پروتکل مشخص و توابع مشخصی برای فرستادن و گرفتن پیغام در فرآیند‌ها نیازمندیم. همچنین فرآیند‌ها باید دارای شناسه‌ی مشخص(ID) باشند. بنابراین هنگام استفاده از روش انتقال پیام در سیستم بلادرنگ، سه مساله اصلی به وجود می‌آید.

  • روش انتقال پیام نیاز به همگام‌سازی دارد، بنابراین یک منبع اصلی غیرقابل پیش‌بینی برای زمان‌بندی بلادرنگ نیاز است.
  • در سیستم‌هایی که ارتباط متقابل دو جهته بین فرآیندها یا هر نوع حلقه بازخورد وجود دارد، امکان بروز بن‌بست[5] (به حالتی گفته می‌شود دو یا چند کار پردازشی منتظر پایان کار یکدیگر هستند) وجود دارد.
  • در مقایسه با حافظه مشترک، روش انتقال پیام به اطلاعات سربار بیشتری نیاز دارد.

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

6- وابستگی‌های بسیار زیاد بین ماژول‌ها

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

نمودار گراف وابستگی، یک ابزار ارزشمند مهندسی نرم‌افزار است. با چنین نموداری به راحتی می‌توان قسمت‌هایی از نرم‌افزار که مجددا قابل استفاده هستند را تشخیص داد. همچنین می‌توان یک استراتژی برای تست تدریجی ماژول‌‌ها و توسعه یک روش برای محدود کردن انتشار خطا در کل سیستم ارائه کرد.

هر چرخه در گراف (وابستگی حلقه‌ای)، توانایی استفاده مجدد از ماژو‌های نرم‌افزاری را کاهش می‌دهد. برای چنین مواردی عملیات تست فقط می‌تواند برای مجموعه‌ای ترکیب شده از ماژول‌های وابسته انجام شود و ایزوله کردن خطاها برای یک ماژول واحد، دشوارتر است. بنابراین اگر گراف دارای چرخه‌های بسیاری باشد یا اینکه یک چرخه بزرگ وجود داشته باشد که ماژول در پایین‌ترین سطح از گراف وابسته به ماژول بالا باشند، دیگر یک ماژول واحد قابل استفاده مجدد نیست.

برای استفاده بهتر از گراف‌های وابستگی به منظور تجزیه و تحلیل قابلیت استفاده مجدد و حفظ قابلیت نگهداری از نرم‌افزار، باید کدی نوشته شود که گراف آسان‌تری تولید کند. این بدان معناست که همه‌ی اعلان‌های خارجی برای متغیرهای تعریف شده در توابع یک ماژول xxx باید در فایل جداگانه xxx.h تعریف شود. در ماژول yyy به سادگی این فایل‌ها را از طریق #include دنبال می‌کنیم تا وابستگی ماژول‌ها مشخص شود. اگر این قوانین پیروی نشود و اعلان‌های خارجی به جای استفاده از #include مناسب در yyy.c تعبیه شود، گراف وابستگی نادرست خواهد شد و تلاش برای استفاده مجدد از کد که به نظر می‌رسد مستقل از ماژول‌های دیگر است دشوار خواهد بود.

7- یک سوپرلوپ بزرگ (حلقه بزرگ)

زمانیکه نرم‌افزار بلادرنگ تنها به صورت یک حلقه بزرگ طراحی شود؛ ما انعطاف‌پذیری لازم برای تغییر زمان اجرای بخش‌های مختلف کد به طور مستقل را نداریم. در واقع تعداد کمی از سیستم‌های بلادرنگ نیاز به انجام هر کاری در نرخ مشابه دارند. بنابراین یکی از روش‌ها برای کاهش مصرف CPUایی که دچار سربار شده است، کاهش نرخ اجرای بخش‌هایی از برنامه است که از لحاظ بحرانی بودن اهمیت کمتری دارند. این روش تنها در صورتی امکان‌پذیر است که ویژگی‌های چند وظیفه‌ای RTOS مورد استفاده قرار گیرد یا اینکه کد بر اساس مجری (اجرایی) بلادرنگ توسعه پیدا کند.

8- عدم بررسی ویژگی‌های سخت‌افزار پیش از شروع طراحی نرم‌افزار

چه مدت طول می‌کشد تا دو عدد هشت بیتی با هم جمع شوند؟ در مورد دو عدد 16 بیتی یا 32 بیتی چطور؟ اضافه کردن عدد 8 بیتی به یک عدد شناور چطور؟ یک طراح نرم‌افزاری که نتواند بر اساس پردازنده‌های مورد هدف به این پرسش‌ها پاسخ دهد برای طراحی و کدنویسی نرم‌افزار بلادرنگ آماده نیست. به عنوان نمونه برای پردازنده Z180 بیش از 250 درصد زمان بیشتر برای جمع کردن یک عدد شناور با یک بایت نسبت به جمع دو عدد شناور با هم نیاز است.

از سوی دیگر برخی پردازنده‌ها برای انجام محاسبات ریاضی خاص مانند توابع مثلثاتی بهینه‌سازی شده‌اند. اطلاع از ویژگی‌های پردازنده به طراح کمک می‌کند تا مناسب‌ترین روش‌های محاسباتی را برای اجرای عملیات‌های سیستم انتخاب کند.

در هنگام پیاده‌سازی کد برای سیستم‌های بلادرنگ، توجه به مفهوم زمان‌بندی هر خط کد بسیار مهم است. درک قابلیت‌ها و محدودیت‌های پردازنده هدف و طراحی مجدد برنامه‌ای که بیش از حد از دستورالعمل‌های کند استفاده می‌کند، بسیار اهمیت دارد. برای مثال برای Z180 انجام هر عملیاتی در نوع شناور بهتر از داشتن تنها چند متغیر شناور و تعداد زیادی متغیر دیگر است.

9- پیاده‌سازی تاخیر با حلقه خالی

نرم‌فزارهای بلادرنگ اغلب برای اطمینان از اینکه داده‌ها روی پورت‌های ورودی/ خروجی ارسال یا دریافت شده، از تاخیر استفاده می‌کنند. این تاخیرها اغلب با قرار دادن حلقه‌های خالی یا حالت بدون عملیاتی (NOP) پیاده‌سازی می‌شوند. اگر کد روی پردازنده‌های متفاوت یا حتی پردازنده‌های مشابه با سرعت متفاوت (برای مثال CPU 25 مگاهرتز در مقابل 33 مگاهرتز) اجرا شود، ممکن است کد روی پردازنده سریعتر زودتر اجرا شود. این دقیقا مساله‌ای است که باید از آن اجتناب کرد زیرا به نوعی مشکل زمان‌بندی را منجر می‌شود و ردیابی و حل آن دشوار است. در عوض می‌توان از یک مکانیزم مبتنی بر تایمر استفاده کرد. با اینکه برخی از RTOSهای تایید شده DO-178B این توابع را ارئه می‌دهند، اما می‌توانند به راحتی ساخته شوند.

در ادامه دو روش برای ساخت یک تابع تاخیر سفارشی آورده شده است. بیشتر تایمرهای شمارش معکوس به نرم‌افزار اجازه می‌دهند یک رجیستر را برای دستیابی به مقدار شمارش معکوس فعلی بخواند. یک متغیر سیستم می‌تواند نرخ تایمر را در واحدهایی همچون میکروثانیه در هر تیک ذخیره کند. فرض کنید مقدار هر تیک تایمر 2 میکروثانیه و میزان تاخیر مورد نیاز 10 میکروثانیه است. تابع تاخیر برای 5 تیک تایمر تنظیم می‌شود. با فرض اینکه یک پردازنده با سرعت متفاوت استفاده شود، تیک‌های تایمر هنوز همان باقی می‌ماند. همچنین اگر فرکانس تایمر تغییر کند، متغیر سیستم تغییر کرده و در نتیجه تعداد تیک‌های حالت انتظار تغییر خواهد کرد، اما زمان تاخیر همان 10 میکروثانیه است.

اگر تایمر از خواندن مقادیر شمارش معکوس پشتیبانی نکند، جایگزین مناسب دیگر می‌تواند محاسبه سرعت پردازنده در مقداردهی اولیه سیستم باشد. با اجرای یک حلقه خالی می‌توان سرعت پردازنده و مدت زمان اجرای حلقه را محاسبه کرد. سپس این مقدار به صورت پویا تعداد تکرار حلقه برای انجام زمان تاخیر تعیین شده را مشخص می‌کند.

10- دستورهای if-then-else و case طولانی

دیدن این دستورها در کدهای نرم‌افزارهای embedded غیر معمول نیست. اما نوشتن این دستورات از 3 دیدگاه مشکل‌ساز است.

  • چنین عبارت‌هایی برای اشکال‌زدایی بسیار دشوار است زیرا انتهای کد به مسیرهای مختلفی متصل می‌شود و اگر عبارت‌ها تودرتو باشد، بسیار پیچیده‌تر می‌شود.
  • دشواری تست برای کدهای ساخت‌یافته[6] و تصمیم‌گیری‌ها به طور نمایی با تعداد شاخه‌های برنامه افزایش می‌یابد، بنابراین باید تعداد شاخه‌ها به حداقل برسد.
  • تفاوت بین زمان اجرای بهترین حالت و بدترین حالت بعد از اجرای این دستورات، قابل توجه است. این منجر به استفاده نامناسب از CPU یا احتمال ایجاد خطای زمان‌بندی می‌شود.

روش‌های محاسباتی اغلب می‌توانند پاسخی معادل ارائه کنند. اجرای جبر بولی، پیاده‌سازی یک ماشین حالت متناهی به عنوان یک جدول پرش (جدول شاخه‌ای)، یا استفاده از جداول ارجاعی[7] جایگزین‌هایی هستند که می‌توانند یک عبارت if-else با 100 خط را به کمتر از 10 خط کد کاهش دهند.

اشتباهات فوق در صورتیکه جلوگیری یا اصلاح شود، می‌تواند هفته‌ها یا ماه‌ها در نیروی انسانی صرفه‌جویی کند. این منجر به افزایش کیفیت و استحکام برنامه می‌شود و امکان استفاده مجدد از نرم‌افزار را برای برنامه‌های مشابه در آینده فراهم می‌کند.

[1] encapsulation

[2] threads

[3] Context Switching

[4] functional blocks

[5] deadlock

[6] structured code

[7] lookup table