ما را در شبکههای اجتماعی دنبال کنید:
10 اشتباه معمول مهندسان نرمافزار Embedded در اویونیک
استفاده از استاندارد DO-178B/C بسیاری از مسائل و مشکلات رایج مربوط به توسعه نرمافزارهای بلادرنگ را از بین میبرد، اما هنوز مسائلی وجود دارد که در صنعت نرمافزارهای تعبیهشده بلادرنگ رایج است و همچنان دنیای اویونیک را آزار میدهد. در این گزارش اشتباهات متعددی که توسط مهندسان نرمافزارهای Embedded در اویونیک و رایج در استاندارد DO-178B/C به وجود میآید، مطرح میشود. همچنین برای اینگونه مسائل راهحلهایی ارائه شده است.
1- استفاده از متغیرهای سراسری
متغیرهای سراسری اغلب توسط مهندسان نرمافزار منسوخ شدهاند به این دلیل که این متغیرها معیار کپسولهسازی در سیستمعاملهای بلادرنگ فرآیندها معمولا به صورت نخ[2] اجرا میشوند. در این شرایط متغیر سراسری بین همه فرآیندها به اشتراک گذاشته میشود. بنابراین دو فرآیند که از ماژول مشابه با یک متغیر سراسری تعریف شده استفاده میکنند، مقدار یکسانی به اشتراک خواهند گذاشت. چنین درگیریهایی عملکرد سیستم را مختل میکند، از اینرو مشکل متغیر سراسری مسالهای فراتر از حفظ و نگهداری نرمافزار است. وقفهها شاید بزرگترین دلیل معکوس کردن انحراف اولویت در سیستمهای بلادرنگ باشند. همین دلیل باعث میشود سیستم تمام الزامات زمانبندی خود را برآورده نکند. دلیل این تاخیر این است که وقفهها از هر عملیاتی پیشی گرفته و برنامهریزی نشده عمل میکنند. اگربرنامهنویس رویدادی را به طور منظم برنامهریزی کند، ممکن است رفتار ناخواستهای رخ دهد. یک سیستم بلادرنگ ایدهآل بدون وقفه است. بسیاری از برنامهنویسان 80 تا 90 درصد کد برنامه را درون بخش وقفهها قرار میدهند. پردازش کامل درخواستهای I/O و حلقههای دورهای متداولترین مواردی هستند که در بحث مدیریت وقفه قرار میگیرد. برنامهنویسان ادعا میکنند که یک مدیر وقفه سربار کمتری روی سیستمعامل دارد، بنابراین سیستم بهتر کار میکند. با اینکه رسیدگی به وقفه نسبت به روش تعویض زمینه[3] (به فرایند ذخیره کردن و بازیابی وضعیت یک پردازش گفته میشود، بهطوریکه اجرای آن پردازش بتواند بعداً از همان نقطه ادامه یابد) دارای سربار کمتری است اما لزوما به دلایل مختلف بهتر عمل نمیکند. برنامهنویسان embedded به طور مستمر از اعلانهای #define در کدهای خود استفاده میکنند. از این اعلان برای تعیین آدرسهای رجیستر، بازه آرایهها و ثابتهای پیکربندی استفاده میشود. اگرچه این عمل رایجی است اما امری نامطلوب محسوب میشود؛ زیرا استفاده مجدد از نرمافزار را در سایر برنامههای مشابه بسیار دشوار میکند. مشکل از آنجاست که یک اعلان #define در همه جای کد منبع گسترش پیدا میکند. بنابراین ممکن است مقدار تعریف شده در بیش از 20 مکان مختلف در کد نمایش داده شود. از اینرو اگر این مقدار در کد شی تغییر کند، اشاره به یک مکان واحد برای ایجاد تغییر آن آسان نیست. تشخیص و رسیدگی به خطا به ندرت در یک قالب معنادار و معقول در طراحی نرمافزار گنجانده میشود. طراحی نرمافزار عمدتا روی عملکرد عادی تمرکز دارد و هر گونه استثناء و رسیدگی به خطا بعد از وقوع توسط برنامهنویس در نظر گرفته میشود. از اینرو برنامهنویس عملیات تشخیص خطا را در هر جای نرمافزار اعمال میکند. این در حالیست که در بسیاری از مواقع نیازی به آن نیست و حضور آن بر عملکرد و زمانبندی تاثیر میگذارد. تشخیص خطا باید در طراحی سیستم، درست مانند هر حالت دیگر گنجانده شود. بنابراین اگر برنامهای توسط یک ماشین حالت متناهی ساخته شود، هر مورد استثناء و خطا میتواند به عنوان یک ورودی که موجب عمل و انتقال به حالت جدید میشود، در نظر گرفته شود. اضافه کردن یک سیستم کدگذاری شده باینری ثبت خطا با قابلیت تعیین زمان وقوع و امکان پردازش آفلاین میتواند این نوع تشخیص و مدیریت خطا را با بهینهسازی مناسب و حداقل سربار اجرایی کند. زمانیکه نرمافزاری به صورت بلوکهای عملکردی[4] توسعه پیدا میکند؛ اولین هدف، پیادهسازی ورودیها و خروجیها به صورت پیام است. این در محیطهای غیر بلادرنگ مانند شبکههای توزیع شده و زیرسیستمهای اصلی که سطحهای مختلفی از نیازمندیهای بلادرنگ وجود دارد، به خوبی کار میکند. با این حال در سیستمهای بلادرنگ استفاده از این روش به عنوان خط ارتباطی بین نخهای یک فرایند، مشکل ساز است. در روش انتقال پیام به جای خواندن و نوشتن در یک حافظهی مشترک، فرآیندها به یکدیگر پیغام می فرستند و دادههای مورد نظر خود را منتقل میکنند. در این حالت به یک پروتکل مشخص و توابع مشخصی برای فرستادن و گرفتن پیغام در فرآیندها نیازمندیم. همچنین فرآیندها باید دارای شناسهی مشخص(ID) باشند. بنابراین هنگام استفاده از روش انتقال پیام در سیستم بلادرنگ، سه مساله اصلی به وجود میآید. با توسعه تئوری اتوماسیون پورت به طور رسمی ثابت شد که سیستم کنترل پایدار و قابل اطمینان میتواند تنها با خواندن آخرین اطلاعات ایجاد شود. با ایجاد کپیهای محلی از حافظه مشترک، میتوان از دسترسی متقابل منحصر به فرد هر فرآیند به اطلاعات مورد نیاز اطمینان حاصل کرد. در طراحی یک نرمافزار خوب وابستگی بین ماژولها میتواند به صورت یک درخت نشان داده شود. یک نمودار وابستگی شامل گرهها و فلشهای ارتباطی است که در آن هر گره نماینده یک ماژول (همچون یک فایل کد منبع) بوده و هر فلش وابستگی بین گرهها یا دیگر ماژولها را نشان میدهد. ماژولهایی که روی پایینترین ردیف از نمودار قرار دارند، به هیچ یک از ماژولهای نرمافزاری وابسته نیستند. برای حداکثر استفاده مجدد از نرمافزارها، فلشها همیشه باید به سمت پایین بوده و از ارتباط به سمت بالا و دوطرفه جلوگیری کرد. نمودار گراف وابستگی، یک ابزار ارزشمند مهندسی نرمافزار است. با چنین نموداری به راحتی میتوان قسمتهایی از نرمافزار که مجددا قابل استفاده هستند را تشخیص داد. همچنین میتوان یک استراتژی برای تست تدریجی ماژولها و توسعه یک روش برای محدود کردن انتشار خطا در کل سیستم ارائه کرد. هر چرخه در گراف (وابستگی حلقهای)، توانایی استفاده مجدد از ماژوهای نرمافزاری را کاهش میدهد. برای چنین مواردی عملیات تست فقط میتواند برای مجموعهای ترکیب شده از ماژولهای وابسته انجام شود و ایزوله کردن خطاها برای یک ماژول واحد، دشوارتر است. بنابراین اگر گراف دارای چرخههای بسیاری باشد یا اینکه یک چرخه بزرگ وجود داشته باشد که ماژول در پایینترین سطح از گراف وابسته به ماژول بالا باشند، دیگر یک ماژول واحد قابل استفاده مجدد نیست. برای استفاده بهتر از گرافهای وابستگی به منظور تجزیه و تحلیل قابلیت استفاده مجدد و حفظ قابلیت نگهداری از نرمافزار، باید کدی نوشته شود که گراف آسانتری تولید کند. این بدان معناست که همهی اعلانهای خارجی برای متغیرهای تعریف شده در توابع یک ماژول xxx باید در فایل جداگانه xxx.h تعریف شود. در ماژول yyy به سادگی این فایلها را از طریق #include دنبال میکنیم تا وابستگی ماژولها مشخص شود. اگر این قوانین پیروی نشود و اعلانهای خارجی به جای استفاده از #include مناسب در yyy.c تعبیه شود، گراف وابستگی نادرست خواهد شد و تلاش برای استفاده مجدد از کد که به نظر میرسد مستقل از ماژولهای دیگر است دشوار خواهد بود. زمانیکه نرمافزار بلادرنگ تنها به صورت یک حلقه بزرگ طراحی شود؛ ما انعطافپذیری لازم برای تغییر زمان اجرای بخشهای مختلف کد به طور مستقل را نداریم. در واقع تعداد کمی از سیستمهای بلادرنگ نیاز به انجام هر کاری در نرخ مشابه دارند. بنابراین یکی از روشها برای کاهش مصرف CPUایی که دچار سربار شده است، کاهش نرخ اجرای بخشهایی از برنامه است که از لحاظ بحرانی بودن اهمیت کمتری دارند. این روش تنها در صورتی امکانپذیر است که ویژگیهای چند وظیفهای RTOS مورد استفاده قرار گیرد یا اینکه کد بر اساس مجری (اجرایی) بلادرنگ توسعه پیدا کند. چه مدت طول میکشد تا دو عدد هشت بیتی با هم جمع شوند؟ در مورد دو عدد 16 بیتی یا 32 بیتی چطور؟ اضافه کردن عدد 8 بیتی به یک عدد شناور چطور؟ یک طراح نرمافزاری که نتواند بر اساس پردازندههای مورد هدف به این پرسشها پاسخ دهد برای طراحی و کدنویسی نرمافزار بلادرنگ آماده نیست. به عنوان نمونه برای پردازنده Z180 بیش از 250 درصد زمان بیشتر برای جمع کردن یک عدد شناور با یک بایت نسبت به جمع دو عدد شناور با هم نیاز است. از سوی دیگر برخی پردازندهها برای انجام محاسبات ریاضی خاص مانند توابع مثلثاتی بهینهسازی شدهاند. اطلاع از ویژگیهای پردازنده به طراح کمک میکند تا مناسبترین روشهای محاسباتی را برای اجرای عملیاتهای سیستم انتخاب کند. در هنگام پیادهسازی کد برای سیستمهای بلادرنگ، توجه به مفهوم زمانبندی هر خط کد بسیار مهم است. درک قابلیتها و محدودیتهای پردازنده هدف و طراحی مجدد برنامهای که بیش از حد از دستورالعملهای کند استفاده میکند، بسیار اهمیت دارد. برای مثال برای Z180 انجام هر عملیاتی در نوع شناور بهتر از داشتن تنها چند متغیر شناور و تعداد زیادی متغیر دیگر است. نرمفزارهای بلادرنگ اغلب برای اطمینان از اینکه دادهها روی پورتهای ورودی/ خروجی ارسال یا دریافت شده، از تاخیر استفاده میکنند. این تاخیرها اغلب با قرار دادن حلقههای خالی یا حالت بدون عملیاتی (NOP) پیادهسازی میشوند. اگر کد روی پردازندههای متفاوت یا حتی پردازندههای مشابه با سرعت متفاوت (برای مثال CPU 25 مگاهرتز در مقابل 33 مگاهرتز) اجرا شود، ممکن است کد روی پردازنده سریعتر زودتر اجرا شود. این دقیقا مسالهای است که باید از آن اجتناب کرد زیرا به نوعی مشکل زمانبندی را منجر میشود و ردیابی و حل آن دشوار است. در عوض میتوان از یک مکانیزم مبتنی بر تایمر استفاده کرد. با اینکه برخی از RTOSهای تایید شده DO-178B این توابع را ارئه میدهند، اما میتوانند به راحتی ساخته شوند. در ادامه دو روش برای ساخت یک تابع تاخیر سفارشی آورده شده است. بیشتر تایمرهای شمارش معکوس به نرمافزار اجازه میدهند یک رجیستر را برای دستیابی به مقدار شمارش معکوس فعلی بخواند. یک متغیر سیستم میتواند نرخ تایمر را در واحدهایی همچون میکروثانیه در هر تیک ذخیره کند. فرض کنید مقدار هر تیک تایمر 2 میکروثانیه و میزان تاخیر مورد نیاز 10 میکروثانیه است. تابع تاخیر برای 5 تیک تایمر تنظیم میشود. با فرض اینکه یک پردازنده با سرعت متفاوت استفاده شود، تیکهای تایمر هنوز همان باقی میماند. همچنین اگر فرکانس تایمر تغییر کند، متغیر سیستم تغییر کرده و در نتیجه تعداد تیکهای حالت انتظار تغییر خواهد کرد، اما زمان تاخیر همان 10 میکروثانیه است. اگر تایمر از خواندن مقادیر شمارش معکوس پشتیبانی نکند، جایگزین مناسب دیگر میتواند محاسبه سرعت پردازنده در مقداردهی اولیه سیستم باشد. با اجرای یک حلقه خالی میتوان سرعت پردازنده و مدت زمان اجرای حلقه را محاسبه کرد. سپس این مقدار به صورت پویا تعداد تکرار حلقه برای انجام زمان تاخیر تعیین شده را مشخص میکند. دیدن این دستورها در کدهای نرمافزارهای embedded غیر معمول نیست. اما نوشتن این دستورات از 3 دیدگاه مشکلساز است. روشهای محاسباتی اغلب میتوانند پاسخی معادل ارائه کنند. اجرای جبر بولی، پیادهسازی یک ماشین حالت متناهی به عنوان یک جدول پرش (جدول شاخهای)، یا استفاده از جداول ارجاعی[7] جایگزینهایی هستند که میتوانند یک عبارت if-else با 100 خط را به کمتر از 10 خط کد کاهش دهند. اشتباهات فوق در صورتیکه جلوگیری یا اصلاح شود، میتواند هفتهها یا ماهها در نیروی انسانی صرفهجویی کند. این منجر به افزایش کیفیت و استحکام برنامه میشود و امکان استفاده مجدد از نرمافزار را برای برنامههای مشابه در آینده فراهم میکند. [1] encapsulation [2] threads [3] Context Switching [4] functional blocks [5] deadlock [6] structured code [7] lookup table2- استفاده نامناسب یا عدم استفاده از وقفهها
3- اطلاعات پیکربندی در اعلانهای #define
4- تشخیص و رسیدگی به خطا یک چارهاندیشی موقت است که از طریق آزمون و خطا اجرا میشود
5- استفاده از روش انتقال پیام (message passing) به عنوان ارتباط بین فرآیندهای اصلی
6- وابستگیهای بسیار زیاد بین ماژولها
7- یک سوپرلوپ بزرگ (حلقه بزرگ)
8- عدم بررسی ویژگیهای سختافزار پیش از شروع طراحی نرمافزار
9- پیادهسازی تاخیر با حلقه خالی
10- دستورهای if-then-else و case طولانی