احتمالا برای شما هم پیشآمده است که در تنظیمات کامپایلر واژه Newlib را مشاهده کرده باشید که میتوان از آن استفاده کرد، اما واقعا این Newlib چیست و چرا در پروژه های امبدد نقش مهمی را ایفا میکند، استفاده از آن چه مزیت هایی را دارد و چه قابلیت هایی را در اختیار ما میگذارد.
هنگام برنامه نویسی برای یک پلتفرم سختافزاری آخرین چیزی که قصد انجام آن را دارید، کار با روتینهای I/O (ورودی/خروجی)، بررسی stringها و جزئیات خستهکننده و تکراری است که هیچ ارتباطی به روند اصلی پروژه ندارند، در سیستمهای بزرگ، اینجا دقیقا زمانی است که، کتابخانه استاندارد C نقش مهمی بازی میکند.
شاید برای شما مفید باشد: آموزش الکترونیک از 0 تا 100
برای پلتفرمهای کوچک امبدد مثل میکروکنترلرها، محدودیتهای حافظه و قدرت پردازشی باعث میشود که فضای کافی برای stdlib استاندارد وجود نداشته باشد؛ به همین دلیل Newlib نقش مهمی را در توسعه برنامه های امبدد ایفا میکند، Newlib مزایای پرتابل بودن یک کتابخانه استاندارد را برای میکروکنترلر فراهم میکند. فرقی ندارد از C، C++ یا MicroPython برای برنامهنویسی MCU استفاده کنید، درهرصورت، Newlib نقش مهمی ایفا میکند. با این حال، دقیقاً چگونه با سخت افزار یکپارچه میشود، و چگونه فراخوانی های سیستمی syscalls (برای مثال. مدیریت فایل و ورودی/خروجی) اجرا شده است؟
ابزار های STUBBY
کتابخانه استاندارد C تعدادی هدر (header) فراهم میکند تا فانکشنهای موجود را پوشش دهند. با ایجاد هر revision در استاندارد C، هدرهای جدیدی اضافه میشوند تا فانکشنهای اضافی موجود نیز پوشش داده شوند. از رایجترین هدرهای مورداستفاده عبارتاند از:
<stdio.h>
<string.h>
<stdlib.h>
<math.h>
<time.h>
در اینجا میتوان حدس زد که هر یک از این هدرها در پیچیدگی کدها برای پورت کردن به یک پلتفرم جدید، بهویژه در مورد یک پلتفرم امبدد بدون سیستمعامل (OS)، با یکدیگر متفاوت هستند. بدون سیستمعامل، دسترسی به فانکشنهای خاص مانند ورودی و خروجی متن استاندارد یا ساعت و تقویم سیستم وجود ندارد. این موضوع باعث میشود که ما به stub functions در Newlib رویآوریم.
هدر <string.h> را می توان یکی از مطمئن ترین هدر ها دانست؛ چرا که استرینگ های C-style و عملکردهای آنها به عملیات حافظه مربوط میشوند که برای این موضوع نیازی به فراخوانهای سیستمی (syscalls) خاصی نیست. این هدر با هدر <stdio.h> بسیار متفاوت است؛ چرا که هدر <stdio.h> شامل فانکشنهای دسترسی به فایل و عملیات و همچنین، فانکشنهای خروجی یا ورودی است.
بدون برخی از کدهای پایهای که پیاده سازی libc روی مواردی از جمله ترمینال یا حافظه انجام میدهند. هیچ اتفاقی برای فانکشنهای ورودی/خروجی نمیافتد، اگرچه عملکرد حساس پیش فرضی برای فانکشن هایی از جمله printf() یا fopen() وجود ندارد.
اگر بخواهیم از printf() یا دیگر فانکشنهای خروجی متن استفاده کنیم، مستندات Newlib به ما میگوید که باید یک فانکشن سراسری int _write (int handle، char* داده، int size) پیادهسازی کنیم.
همانطور که از نام Stub مشخص است، کتابخانه Newlib با پیادهسازی stub خود، هیچ کاری برای اجرای فانکشنهای خروجی متن انجام نمیهد؛ بنابراین چه کاری باید انجام شود تا فانکشنی مثل printf() اجرا شود؟ در اینجا مهمترین چیزی که باید به آن دقت کرد این است که این موضوع به پیادهسازی و نوع پروژه بستگی دارد. معمولا در برنامههای امبدد اغلب از خروجی با فرمت متن برای خروجی دیباگ و اطلاعات مشابه استفاده میشود؛ در این صورت، برای مثال،ارسال خروجی با استفاده از پروتکل سریال (USART) انتخاب بسیار مناسبی میتواند باشد.
در چارچوب Nodate، به کد استارتآپ (start-up) اجازه داده میشود تا یک پرفرال جانبی خاص، پروتکل سریال را برای ارسال خروجی انتخاب کند؛ زیرا میتوان اجرای stub فانکشن را در ماژول IO مشاهده کرد:
1 2 3 4 5 6 7 8 9 10 11 12 13 | bool stdout_active = false; USART_devices IO::usart; int _write(int handle, char* data, int size) { if (!stdout_active) { return 0; } int count = size; while (count-- > 0) { USART::sendUart(IO::usart, *data); data++; } return size; } |
در قسمت بالا آرایهای (array) از کاراکترها به همراه طول آن به فانکشن مروبطه ارسال شده است که این آرایه را به بر روی سریال دلخواهی ارسال میکنیم. از انجایی که اغلب پرفرالهای سریال به شکل بایت به بایت داده ها را ارسال و دریافت میکنند در این مثال نیز آرایه به کمک حلقه روی سریال ارسال خواهد شد.
ازآنجاییکه تارگت USART میتواند در هر پلتفرم تغییر کند، این امکان برای توسعهدهنده وجود دارد که تارگت خروجی، را بهصورت داینامیک هم در زمان راهاندازی (start-up) و هم در طول زمان اجرا (runtime) تنظیم کند.
1 2 3 4 5 6 | bool IO::setStdOutTarget(USART_devices device) { IO::usart = device; stdout_active = true; return true; } |
نکته مهم در مورد پیادهسازیهای stub این است که برای یافتن overrides از پیوند C-style استفاده میشود. ازآنجاییکه در زبانهایی مانند C++ نام mangling به طور پیشفرض اعمال میشود، مطمئن شوید که یک بلوک ” extern “C” { } ” در اطراف پیادهسازی کامل یا یک اعلان فوروارد (forward declaration) در پیادهسازی stub اعمال میشود.
اهمیت زمان بندی
برای اینکه فانکشن مربوط به زمان همانطور که در هدر <time.h> تعریف شده است کار کند، باید یک time base یا حداقل شمارنده (counter) وجود داشته باشد که بتوان این اطلاعات را از آن به دست آورد. بهعنوانمثال، استفاده از یک شمارنده (counter) سیستمی که حاوی تعداد میلیثانیههای سپری شده از لحظه کارکرد سیستم است برای این کار کافی نیست. بهطورکلی time() به تعدادی ثانیه از Unix Epoch time نیاز دارد که Unix Epoch time یک مقیاس اندازهگیری زمان بهصورت آنی (Point in Time) است.
برای پیادهسازی اساسی ” int _times (struct tms* buf) ” ، فراخوان سیستمی از RTC استفاده میکند. در اینجا استفاده از سیستم بلادرنگ نسبت به استفاده از سیستم معمولی یک مزیت محسوب میشود؛ زیرا RTC را میتوان در حالت low-power قرارداد. در این صورت، هنگامی که سیستم در حالت sleep یا حتی خاموش قرار دارد، باز هم امکان مشاهده نتایج زمانبندی دقیق وجود دارد.
در Nodate، این فانکشن در clock.cpp برای STM32 پیادهسازی شده است که RTC را درصورتیکه قبلاً راهاندازی نشده باشد، فعال میکند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int _times(struct tms* buf) { #if defined RTC_TR_SU if (!rtc_pwr_enabled) { if (!Rtc::enableRTC()) { return -1; } rtc_pwr_enabled = true; } // Fill tms struct from RTC registers. // struct tms { // clock_t tms_utime; /* user time */ // clock_t tms_stime; /* system time */ // clock_t tms_cutime; /* user time of children */ // clock_t tms_cstime; /* system time of children */ // }; uint32_t tTR = RTC->TR; uint32_t ticks = (uint8_t) RTC_Bcd2ToByte(tTR & (RTC_TR_ST | RTC_TR_SU)); ticks = ticks * SystemCoreClock; |
در MCU های مبتنی بر STM32 Cortex-M (بهاستثنای STM32F1)، رجیسترهای RTC حاوی شمارش زمان در قالب BCD (اعشاری با کد دودویی) هستند که برای سازگاری با هر کدی، نیاز است که آنها به کد باینری تبدیل شوند که این کار با استفاده از فانکشن _times() انجام میشود.
1 2 3 4 5 | uint8_t RTC_Bcd2ToByte(uint8_t Value) { uint32_t tmp = 0U; tmp = ((uint8_t)(Value & (uint8_t)0xF0) >> (uint8_t)0x4) * 10; return (tmp + (Value & (uint8_t)0x0F)); } |
NEWLIB برای میکروکنترلرها
از نظر فنی دو نسخه از Newlib وجود دارد: یکی کتابخانه regular و full-fat، دیگری نسخه نانو و low-fat است که توسط ARM برای MCUهای Cortex-M در سال 2013 راهاندازی شد. یک نقطهضعف بزرگ نسخه regular این است که فضای نسبتاً زیادی را اشغال میکند. برای مثال، در مورد MCUهای کوچکتر با حافظه فلش محدود، حتی اگر یک “Hello World” ساده در برابر آن کامپایل شود، ممکن است NEWLIB فضای بسیار زیادی را اشغال کند.
برای یک پلتفرم MCU مانند STM32 یا SAM هنگام کامپایل کردن با GCC، به کامپایلر میتوان دستور داد تا با افزودن فایل مشخصات (specs file) برای استفاده از دستور ”linker ، ” –specs=nano.specs با Newlib-nano لینک کند. بهطورکلی این فایل مشخصات (specs file) تضمین میکند که پروژه با کتابخانه Newlib-nano مرتبط است و از فایلهای هدر مناسب استفاده میکند.
تفاوت اندازه بین نسخه regular (معمولی) Newlib و نسخه نانو آن کاملاً چشمگیر است. برای پروژهای که یک MCU با حافظه کم نظیر Cortex-M0، استفاده از Newlib معمولی غیرممکن است.
بهعنوانمثال، در پروژه STM32F030F4 با فلش 16 کیلوبایت و 4 کیلوبایت SRAM استفاده از Newlib معمولی ممکن نیست. بهعلاوه، با استفاده از Newlib-nano، سایز پروژههای اولیه ارائه شده توسط Nodate (بهعنوانمثال Blinky، Pushy) تنها حدود 2 کیلوبایت است؛ بنابراین بهراحتی در Flash و RAM جا می شوند. پیادهسازی همراه با Nodate برای دریافت پشتیبانی کامل از printf() حتی در MCUهای کوچک Cortex-M0 نیز قابلاستفاده است.
پروژه ها را زیاد شلوغ نکنید!
هنگام توسعه میکروکنترلرهای resource-restricted، به معنای واقعی کلمه هر بایت مهم است. اکثر MCUها سیستمهای تکهستهای هستند. همچنین، آنها در هنگام استفاده از سیستمهای چندهستهای (مانند STM32H7)، به پشتیبانی multi-threaded (چندرشتهای) نیاز ندارند. کد reentrant به ورودیهایی مانند impure_data و مشابه آن، پیوند داده میشود. این کد اغلب به دلیل فانکشن خاصی که در کد پروژه استفاده میشود به آن پیوند داده میشود.
آنالیز مستقیم فایل نقشه یا استفاده از ابزاری مانند MapViewer (فقط برای ویندوز) میتواند به ردیابی dependencies (وابستگیها) کمک کند. برای جلوگیری از استفادة نسخه reentrant کنترلکننده خروج شما میتوانید پرچم -fno-use-cxa-atexit را به پرچمهای کامپایل GCC اضافه کنید.
جمعبندی
تمام مواردی که در این مقاله به آنها اشاره کردیم شامل اصول اولیه برای استفاده از کتابخانه NewLib میباشد.
مدیریت فرایند موضوع دیگری است که توسط getpid()، fork() و فانکشنهای دیگر stub انجام میشود. بهطورکلی Newlib بسیار انعطافپذیر است و میتواند بهراحتی با هر پلتفرمی سازگار شود؛ به همین دلیل، Newlib میتواند هم با MCUهای تکهستهای Cortex-M و هم با سیستمهای چندهستهای بزرگ مانند کنسولهای بازی کار کند و حتی تغییری هم در آن ایجاد نشود.
منبع: HACKADAY
جالب بود