اشکان انوشه
اشکان انوشه
خواندن ۵ دقیقه·۶ سال پیش

نکات طراحی نرم افزار برای امبددکارها

قبل از هر چیزی منظورم از واژه امبددکارها بیشتر فریمور دولوپرها ( Firmware Developers) یا به عبارت دیگر میکرو کار ها و مخصوصا ARM کارها هست. البته میتواند برای طرفداران رزبری و آردوینو نیز مفید باشد.

نکات ذکر شده بیشتر خلاصه ای از کتاب Reusable Firmware Development اثر آقای Jacob Beningo و همچنین تجربه شخصی خودم هست.

حتما شما هم وقتی شروع کردید به یادگیری یک میکروکنترلر با کشمکش های عجیبی رو به رو شدید که هر کسی میکرویی که خودش کار میکنه خیلی خفنه ! یکی میگه STM32 یکی دیگه از NXP میگه و ... ! بحث من یک ذره کلی تره. چه میشود اگر یک برنامه بنویسیم و با حداقل تغییر در میکرو های متفاوت استفاده بشود ؟

اگر شما با سخت افزار و دنیای رجیستری پشت میکروکنترلر درگیر شده باشید متوجه میشوید که همه ی میکرو کنترلر ها در بسیاری از موارد مشابه هم هستند و عمده ترین تفاوت آنها در نام گذاری رجیسترها و شاخ و برگ دادن به پریفرال ها هست. مثلا پریفرال ( Peripheral ) ارتباطی UART را در نظر بگیرید. ویژگی های اصلی پروتکل همه جا بصورت استاندارد ثابت است همینطور در راه اندازی اکثر میکروها به اینصورت عمل میکنیم

  • - صبر میکنیم که فلگ ( Flag ) خاصی فعال بشه.(نشان دهنده خالی بودن بافر ارسال است)
  • رجیستر مربوط به بافر ارسال را با دیتایی که قرار هست ارسال شود مقداردهی میکنیم
  • صبر میکنیم که فلگ خاصی فعال بشود.( ارسال انجام شده و بافر آماده دیتای جدید است)

کلیت ماجرا ثابت هست.خب پس اگه همشون در باطن یک کار و انجام میدن پس میشه فریموری نوشت که در میکرو های مختلف استفاده کرد ؟ جواب تقریبا بلللله هست !

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

حالا نکات طلایی طراحی نرم افزار

  • ماژولار برنامه بنویسید !
    به جای اینکه یک فایل main.c به اندازه ۶۰۰۰ خط کد درست کنید کد هایتان را عاقلانه و در فایل های مختلف دسته بندی کنید. حتما برای هر ماژول یک فایل h و یک فایل c در نظر بگیرید. در اولی فقط اینترفیس و در دومی پیاده سازی و کامنت گذاری کنید.
  • از زبان C استفاده کنید !!
    راجع به خوب و بد بودن این زبان اصلا حرف نمیزنم چون که خودتون میدونید چقدر خوبه ! مخصوصا از استاندارد C99 به بعد !
  • نیم نگاهی به استاندارد های کد نویسی C مثل MISRA داشته باشید.
  • از کتابخونه stdint.h استفاده کنید !
    به منظور صراحت و دقت بیشتر از قالب های int16_t ، uint16_t ، uint32_t و ... به جای int , short int و ... استفاده کنید.
  • از بیت فیلدها ( ‌Bit Field ) استفاده نکنییید !!
    بیت فیلد ها امکان دسترسی به یک بیت از یک دیتا تایپ را میدهند( مثلا یک بیت از بایت) اما پیاده سازی این دسترسی برای کامپایلر دردسر دارد ! و اغلب با کاهش کارایی و افزایش حجم برنامه همراه است.
  • از یک استاندارد برای نام گذاری متغیرها ، توابع و ماکروها پیروی کنید.
    این استاندارد میتواند حاصل تجربه خودتان باشد اما می بایست نام های با مسما و به یادماندنی باشند.
  • در ماژول ها بیشتر از ۱۰ الی ۱۵ تابع نداشته باشید.
    اگر هم داشتید تلاش کنید به دو ماژول تقسیم اش کنید.
  • از معماری ساده ی لایه ای استفاده کنید.
    به عنوان مثال معماری فریمور زیر را زیر ببینید.
  • تا جای امکان از متد non-blocking استفاده کنید !
    به عنوان مثال اگر میخواهید رشته ای از LOG را به پورت UART میکرو منتقل کنید، با در نظر گرفتن نرخ انتقال پایین این پروتکل بهتر است از اینتراپت و یا DMA برای ارسال و دریافت استفاده شود و میکرو زمان اش را با انتظار برای تک تک کاراکتر ها هدر ندهد.
  • تفاوت کلیدواژه های extern و static را در زبان C بخاطر بیاورید.
    بصورت پیشفرض تمام متغیر ها و توابع بصورت extern هستند. یعنی قابلیت دسترسی global دارند و اغلب برای اطلاع دادن به کامپایلر که متغیر مورد نظر در فایل دیگری تعریف شده است آن را بصورت extern در فایل جدید تعریف می کنیم. با استفاده از کلیدواژه static برای توابع ، دسترسی آن هارا به فایل محدود میکنیم.
  • اهمیت ویژه کلیدواژه volatile را بخاطر داشته باشیم.
    بیشتر از هر جای دیگری در میکروکنترلر ها این کلیدواژه را میبینیم ! شاید برایتان پیش آمده باشد که برنامه بدرستی کار نکند و با غیر فعال کردن بهینه سازی کامپایلر مشکل تان برطرف شود. علت چیست ؟ به کد زیر از دید کامپایلر بنگرید.
#define GPIOA_BASE 0x40020000 uint32_t * GPIOA = ((uint32_t *) GPIOA_BASE); while(*GPIOA & 0x01 == 1);

همانطور که شاید حدس زده باشید در صورتی که کامپایلر کد بالا را بهینه سازی کند، فقط یکبار مقدار رجیستر GPIOA را می خواند چراکه اشاره گر GPIOA در برنامه هیچ تغییری نکرده پس کامپایلر مقادیر داخل while را ثابت در نظر میگیرد. اما مشکل کجاست ؟ مشکل اینجاست که کامپایلر از امکان تغییر محتوای حافظه ای که GPIOA به آن اشاره می کند بی خبر است. این محتوا ممکن است فلگی (Flag) باشد که توسط سخت افزار تغییر کند ! پس نباید ثابت در نظر گرفته شود.

راه حل چیست ؟ فقط کافیست با اضافه کردن واژه volatile به کامپایلر بگوییم که خانه حافظه GPIOA امکان تغییر توسط سخت افزار را دارد.

uint32_t volatile * GPIOA = ((uint32_t *) GPIOA_BASE);

توجه داشته باشید که حتما کلید واژه volatile سمت چپ ستاره ی اشاره گر باشد.( این نکات را در مقاله دیگری مفصل توضیح خواهم داد.)

  • استفاده از کلمه const تا جای امکان
    این واژه بیشتر برای برنامه نویس است که نمایانگر این است که برنامه نویس نباید در توابع این متغیر را تغییر دهد !
  • داکیومنتینگ و کامنتینگ !!
    ترجیحا با Doxygen و graphviz.
    در کامنت نویسی همیشه چرای کد را توضیح دهید نه چگونگی را.
    ابتدا کامنت کد را بنویسید سپس خود کد !
  • در طراحی HAL موارد زیر را به ترتیب انجام دهید.
    ۱. دیتاشیت قطعه و رجیستر مپ آن پریفرال را بصورت مروری مطالعه کنید.
    ۲. ویژگی های ضروری و اضافی آن پریفرال را از هم سوا کرده و یادداشت کنید.
    ۳. اینترفیس ویژگی های ضروری پریفرال را طراحی کنید.
    ۴. کامنت گذاری
    ۵. پیاده سازی توابع برای آن میکروکنترلر خاص
    ۶. تست !
    ۷. تکرار عملیات بالا برای پریفرال بعدی.
  • نکات طراحی HAL
    * در ماژول اصلی فقط ویژگی های اصلی را پیاده کنید.
    * از نوشتن کد همه منظوره برای همه میکرو ها پرهیز کنید !( بخاطر پیچیدگی و باگ های احتمالی خطرناک.)
    * پیاده سازی توابع دسترسی مستقیم به رجیسترها.
    * از شخص دیگری بخواهید کدتان را مطالعه کند.
    * از یک شیوه نام گذاری استاندارد پیروی کنید.
    * در توابع initialize حتما از یک پارامتر استفاده کنید.

منابع:

Reusable Software Development , Jacob Beningo.


مهندس دیجیتالی !
شاید از این پست‌ها خوشتان بیاید