در قسمت قبل آموزش FreeRTOS در STM32 با کامپایلر gcc آشنا شدیم، با دستورات آن کار کردیم و دیدیم که برای کامپایل کردن یک برنامه ساده چه سوئیچهایی از این ابزار را باید به کار گرفت. این دستورات طولانی آن هم برای یک برنامه ساده بسیار زمانبر است و اصلاً منطقی نیست که برای رفع باگهای برنامه و بهروزرسانی برنامه هر بار آنها را تکرار کنیم. در این قسمت به آموزش ابزار GNU Make می پردازیم.
همچنین لازم به ذکر است که تا الان ما در محیط سیستمعامل لینوکس و برای آن برنامهنویسی میکردیم، اما در قسمتهای آتی میخواهیم برای میکروکنترلرها برنامهنویسی کنیم و دستوراتی که بهمنظور کامپایل برنامه برای میکروکنترلرها به کار گرفته میشود، نسبت به این چیزی که هم اکنون با آن روبهرو هستیم، بسیار طولانیتر هستند.
1 2 3 4 5 6 7 8 9 | arm-none-eabi-gcc "../Drivers/STM32F1XX HAL Driver/Src/stm32f1xx_hal.c" -mcpu=cortex-m3 -std=gnull -g3 -DUSE_HAL_DRIVER -DSTM32F103xx -DDEBUG -c -I../Core/Inc -I../Drivers/STM32F1xx_HAL_Driver/Inc -I../Drivers/ STM32F1XX_HAL_Driver/Inc/Legacy -I../Drivers/CMSIS/Device/ST/STM32F1xx/Include -I../Drivers/CMSIS/Include -O0 -ffunction-sections -fdata-sections -Wall -fstack-usage -MMD -MP -MF"Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal.d" -MT"Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal.o" --specs-nano.specs -mfpu=fpv5-sp-d16 -mfloat-abi=hard -mthumb -o "Drivers/STM32F1xx_HAL_Driver/Src /stm32f1xx_hal.o |
عباراتی که در باکس بالا میبینید، بخشی از دستوراتی هستند که برای کامپایل یک برنامه ساده تا یک برنامه پیچیده باید به کار گرفت. اینجاست که باید گفت گل بود به سبزه نیز آراسته شد. خب واضحه که تکرار اجرای چنین دستوراتی چقدر زمانبر و خستهکننده است. این مشکل، چالشی بود که سالها پیش در پروژه گنو مطرح شد و بهمنظور تسهیل کامپایل برنامههای بزرگ، نرمافزاری نوشته شد که در واقع خودکار انجامشدن کامپایل یک برنامه را مدیریت میکرد. در این قسمت ما نیز با مطرحکردن این چالش در چند سطر قبل، قصد داریم در ادامه شما را با این ابزار بسیار کارآمد پروژه گنو آشنا کنیم.
شاید برای شما مفید باشد: پروژه الکترونیک از ساده تا پیشرفته و صنعتی
چالش اول: دستورات کامپایل یک برنامه از ساده تا پیچیده بسیار طولانی هستند و تکرار آنها بهمنظور بهروزرسانی یا رفع باگهای برنامه اصلاً منطقی نیست. چگونه میتوان این فرایند را یکبار نوشت و پس از آن بهصورت خودکار انجام شود؟ |
برای پاسخ به این چالش به قسمت دوم – بخش اول از “پیشنیازهای نوشتن یک سیستمعامل بلادرنگ برای STM32” خوش اومدید.
چالش اول: ابزار Make
برای توسعه برنامههای نوشته شده، Make یک ابزار کنترل فرایند کامپایل است که به طور خودکار برنامهها و کتابخانههای اجرایی را از سورس اصلی با خواندن فایلهایی به نام Makefiles که نحوه استخراج برنامه هدف را مشخص میکند، کامپایل میکند.Make میتواند برای مدیریت هر پروژهای استفاده شود که در آن برخی از فایلها باید به طور خودکار از سایر فایلها بهروز شوند، هر زمان که فایلهای دیگر علاوه بر کامپایل برنامهها تغییر کنند. |
Make در واقع باعث میشود که فرایند برنامهنویسی ما از این:
به این تبدیل شود.
بنابراین، پس از نصب make، برای استفاده از این ابزار ابتدا میبایست یک فایل text ایجاد کنیم و سپس در داخل آن فایل با استفاده از فرمتی از پیش تعریف شده، دستوراتی که میخواهیم بهصورت اتوماتیک انجام شوند را قرار دهیم. سپس با استفاده از دستور make در داخل ترمینال و دادن نام آن فایل متنی، Make فایل متنی را میخواند و پس از پردازش فایل آن را اجرا میکند. بریم فایل متنی را ایجاد کنیم؛ اما داخل این فایل متنی باید با چه فرمتی دستوراتمون رو بنویسیم؟ برای پاسخ به این سؤال میخوایم یک پروژه ساده بسازیم و سپس با Make فرایند کامپایل برناممون را مدیریت کنیم. برنامه ما بهصورت زیر هست:
1 2 3 4 5 | //main.c Source Code #include <stdio.h> int main(){ printf("Hello World!\n"); } |
بریم سراغ یادآوری کامپایل با کامپایلر gcc:
1 2 | gcc -c main.c -o main.o gcc main.o -o output |
حالا باید این دستورات رو در داخل فایل متنی قرار بدیم تا Make هنگام پردازش آن فایل، بتواند آن را بخواند و کامپایل را مدیریت کند. اما فرمتی که از آن صحبت میکردیم، بهصورت زیر است:
خب همونطور که در تصویر بالا میبینید ساختار قرارگرفتن دستورات در داخل فایل متنی به این صورت است؛ اما هر کدام از قسمتها به چه معناست؟
- Target: در واقع نام و آدرس دایرکتوری فایل نهایی است که در نتیجه اجرای دستور یا دستورات قرار است ساخته شود.
- Prerequisite(s): در فرایند کامپایل برای ساخت خروجی یک یا چند فایل موردنیاز است. برای مثال برای اینکه فایل output ساخته شود، فایل main.o نیاز است.پس فایل main.o پیشنیاز فایل output میباشد.در واقع در این قسمت اون فایلهایی که Target برای ساختهشدن به آنها نیاز دارد باید قرار گیرند.
- Recipe: در این بخش دستور یا دستورات ترمینالی مرتبط قرار میگیرد.
Rule: به ترکیب موارد بالا یک Rule گفته می شود. Ruleها جزء اصلی یک Makefile هستند.
خب فک کنم متوجه یک نکتهای هم شدید و اون ترتیب نوشتنه. ترتیب نوشتن دستورات بهصورت تصویر بالا است. یعنی ابتدا ما نهاییترین چیزی که میخوایم ایجاد شود را بهعنوان Target مینویسیم و به ترتیب پیشنیازهای آن و دستورات مرتبط با آن را مینویسیم و اگر آن پیشنیازها هم نیاز به کامپایل داشته باشد، در ادامه یک Rule جدید مینویسیم که حالا در آن Rule، آن پیشنیازها بهعنوان Target قرار میگیرند و برای آنها نیز پیشنیازها و دستورات مربوطه را مینویسیم و به همین صورت تا آخر ادامه میدهیم.
شاید برای شما مفید باشد: آموزش آلتیوم دیزاینر از 0 تا 100
پس از انجام مراحل بالا میتوانیم با هر نامی که خواستیم فایلمون رو ذخیره میکنیم یا از یک سری نامهای خاص استفاده کنیم که در ادامه توضیح داده میشود. حالا با استفاده از دستور make بهصورت زیر میتونیم مدیریت کامپایل را با خیالی آسوده به Make بسپاریم.
1 | make -f [Name of makefile] |
در ادامه باید گفت که دستور make به بعضی از اسامی حساس است، یعنی درصورتیکه نام Makefile رو به یکی از اسامی makefile, Makefile, GNUmakefile تغییر بدیم، خود دستور متوجه Makefile میشود و آن را میخواند، بنابراین نیازی به دادن نام فایل به دستور make نیست و استفاده از این دستور مانند حالت زیر بسیار کوتاه میشود:
1 | make |
این بود نحوه استفاده از Make در سادهترین حالت ممکن. حالا برای اینکه بتونیم بهتر و بیشتر با این ابزار کار کنیم باید بدونیم دقیقاً چطور یک Makefile را میخواند و چطور آن را اجرا میکند؟
Make چگونه یک فایل را پردازش و اجرا میکند؟
در ابتدا باید گفت که این ابزار Makefile را میخواند و سپس تمامی فرایندهای این فایل را به یک Dependecy chart تبدیل میکند.حالا این چیزی که گفتم اصلا چی هست؟
Dependency chart Dependency chart به صورت خیلی ساده یک نمودار است که در آن با توجه به وابستگی فایل ها به یک دیگر فایل ها اولویت بندی و به یکدیگر مرتبط میشوند. |
ساخت خود Dependency chart هم دو مرحله داره:
- ابتدا Target اولین Rule خوانده شده و بهعنوان ریشه(root) یا هدف پیشفرض(default goal) این نمودار شناخته میشود.
- سپس پیشنیازهای این Target بهعنوان زیرگروه آن شناخته میشوند و آنقدر این روند ادامه مییابد تا نمودار خاتمه یابد.
برای مثال میخوایم Dependency chart پروژه ساده خودمان رو رسم کنیم:
پس از ایجاد Dependency chart حالا نوبت میرسد به اجرای آن. در این مرحله، ابزار Make اجرای دستورات را از نقطهای شروع میکند که آن نقطه هیچ وابستگی به چیزی نداشته باشد که اگر بخواهیم محل اون در نمودار را مشخص کنیم، پایینترین نقطه از نمودار میشود؛ بنابراین روند اجرای نمودار از پایینبهبالا است، درصورتیکه ترتیب نوشتن دستورات در Makefile از بالابهپایین است. حالا میخوایم عملکرد دستور make رو در مثال قبل بررسی کنیم:
ابتدا ابزار Make پس از اجرای دستور فایل را میخواند و پس از پردازش آن را به Dependency chart تبدیل میکند.حالا نوبت میرسد به اجرا. فایل output به main.o وابستگی دارد. و ازآنجاکه main.o خود دارای وابستگی است،بنابراین نقطه شروع اجرا نمیتواند اینجا باشد. پس میریم سراغ Rule بعدی.فایل main.o به main.c وابستگی دارد و ازآنجاکه فایل main.c به چیزی وابستگی ندارد، اینجا همان نقطه شروع اجراست.
ممکنه سؤال پیش میاد که اگر مجدد دستور make را اجرا کنیم چه اتفاقی میافتد؟
باید بگم شما با پیغام زیر مواجه میشید:
علتش اینه که Make یک Rule را فقط در صورت برقرار بودن یکی از دو شرط زیر اجرا میکند:
- در صورتی که Target وجود نداشته باشد، مانند اولین باری که دستور make اجرا میشود.
- در صورتی که یکی یا هر چندتا از پیش نیاز های Target تغییر کرده باشند.در این صورت فایل Target ساخته شده نسبت به پیش نیاز های خود قدیمی تر است و در این صورت است که Make آن Rule را اجرا میکند.
بریم سراغ یک چالش دیگر. شروع این چالش با اضافهکردن یک قابلیت به Makefile است.خیلی از وقتها ما نیاز داریم فایلهای ساخته شده توسط Make را پاک کنیم تا در هنگام اجرای مجدد، مطمئن شویم که فایلها بروز شده است. به این کار clean کردن نیز گفته میشود. برای پاککردن میتوانیم از یک دستور ترمینالی استفاده کنیم.
1 | rm -f *.o output |
حالا سوئیچ f- اینجا چی می گه؟ این سوئیچ کاربرد پاککردن بهصورت force را دارد؛ اما در اینجا به این منظور استفاده شده تا اگر فایلی برای پاککردن وجود نداشت یا فایل موردنظر توسط نرمافزاری باز نگه داشته شده بود و ما از clean استفاده کردیم، اروری به ما بازگشت داده نشود و فایل درهرصورت پاک شود.
در ادامه برای اضافهکردن این دستور به Makefile یک نکتهای وجود داره و آن هم این هست که clean در واقع Target ای است که هیچ فایلی در نتیجه اجرای دستورات آن ایجاد نمیشود و همچنین هیچ پیش نیازی ندارد. پس هیچکدام از شرایط اجرا توسط Make برای این Rule برقرار نیست.به چنین Target هایی، PHONY Target گفته میشود.
PHONY Target PHONY Target به Targetهایی گفته میشود که هیچکدام از شرایط اجرا توسط Make را ندارند.پس برای اجرای آنها باید چه کرد؟ |
برای اینکه به Make بفهمانیم که این Target یک PHONY Target است، از دستور زیر در Makefile استفاده میکنیم:
1 | .PHONY : clean ... |
و در نهایت Makefile بهصورت زیر میشود:
1 2 3 4 5 6 7 | output : main.o gcc main.o -o output main.o : main.c gcc -c main.c -o main.o clean : rm *.o output .PHONY : clean |
شاید براتون سؤال پیش بیاد که Make فقط Ruleهایی که داخل Dependency chart باشد را اجرا میکند،حالا چطور میخواهد این Rule را اجرا کند؟ در پاسخ باید گفت که باید نام آن Target که میخوایم اجرا کنیم را به دستور make بدهیم، در این صورت make از آن Target یک Dependency chart میسازد و آن را اجرا میکند. در نتیجه داریم:
1 | make clean |
بریم سراغ یک جمعبندی تا اینجای ماجرا: تا اینجا یاد گرفتیم که یک Makefile بسازیم و دستورات کامپایلمون رو بر اساس یک ساختار خاص تحت عنوان Rule در داخل آن بنویسیم و سپس تونستیم PHONY Target بسازیم که با استفاده از اون میتوانیم هر دستور ترمینالی رو در Makefile به کار ببریم. همچنین روش کار Make رو فهمیدیم که درک بهتری از کار با آن به ما میدهد. حالا قراره یک چالش دیگر مطرح کنم و سپس با پاسخ به آن یک نکتهٔ بسیار مهم رو در استایل کامپایل بگم.
چالش دوم: بی دقتی در کدنویسی
چالش دوم: برنامه زیر را داریم. بر اساس Makefile آن، وجود فایلهای کتابخانه بهعنوان پیشنیاز Ruleها چه ضرورتی دارد؟ آیا میتوان فایل آنها را از پیشنیاز Ruleها حذف کرد؟ |
Makefile مثال:
1 2 3 4 5 6 7 8 9 10 11 12 13 | ##makefile output : main.o sum.o gcc sum.o main.o -o output sum.o : sum.c header.h gcc -c sum.c -o sum.o main.o : main.c header.h gcc -c main.c -o main.o clean : rm -f *.o output .PHONY : clean |
بر اساس Makefile بریم سراغ کشیدن Dependency chart:
پاسخ چالش:
- اگر ما header.h را از Rule که Target آن sum.o است حذف کنیم،با احتساب اینکه در کتابخانه تعریف prototype تابع sum قرار گرفته، اگر تغییری در این کتابخانه بدهیم، باید فایل sum.c نیز تغییر کند و ازآنجاکه header.h جزئی از پیشنیازهای این Rule نیست،بنابراین در Make مجدد این Rule اجرا نخواهد شد.پس به دلیل اینکه ممکن است از روی بیدقتی تغییری اعمال کنیم و حواسمان نباشد که آن تغییر را در فایل سورس کتابخانه هم اعمال کنیم، باید فایل کتابخانه را بهعنوان پیشنیاز این Target قرار دهیم تا در مواقع بیدقتی به ما ارور بازگرداند. این خیلی مهم است.
- حال اگر فایل کتابخانه را از Rule که Target آن main.o هست حذف کنیم، در این صورت اگر محتوای فایل کتابخانه را تغییر دهیم، قاعدتاً فرمت توابع استفاده شده در main.c نیز باید تغییر کند و اگر ما از روی بیدقتی آن را تغییر ندهیم، چون فایل کتابخانه بهعنوان پیشنیاز این Rule نمیباشد،در Make مجدد این Rule اجرا نخواهد شد و در نتیجه اروری به ما بازگشت نمیدهد و بعد از اجرای برنامه خواهیم دید که ایدلغافل اصلاً تغییرات ما کامپایل نشده. پس همواره دقت داشته باشید که فایلهای کتابخانه نوشته شده توسط ما باید بهعنوان پیشنیاز Rule قرار گیرد، به دلیل بیدقتیهای برنامهنویسی.
قبل از اینکه این چالش هم به پایان رسد، یک نکته خیلی ریز رو میخوام بگم، آن هم این هست که برای کامنت کردن یک خط در Makefile میتوانید از # در ابتدای آن خط استفاده کنید. فکر کنم خیلی ریز بود.
چالش سوم: Pattern Rule
چالش سوم: اگر ما بهجای سه یا چهارتا سورس فایل، صد تا سورس فایل داشته باشیم. حالا باید برای هر کدام از آنها مانند Makefile چالش دوم یک Rule بنویسیم که سورس فایل را به یک آبجکت فایل تبدیل کند. خب این کار هم بسیار زمانبر است و هم احتمال خطا در آن بسیار زیاد! پس چهکار کنیم؟ |
در این بخش با مفهومی به نام Pattern Rule آشنا میشویم. از اینجا به بعد هر گاه کلمه Pattern رو در Makefile شنیدید،یعنی ما یک الگوی مشخص برای انجام کاری داریم. برای مثال در Makefile چالش دوم ما دو تا Rule داریم که Target هر دو یک آبجکت فایل است و پیشنیاز آنها یک سورس فایل و یک فایل کتابخانه یکسان است. در چنین شرایطی ما میتوانیم بهجای تکتک نوشتن آنها، در یک Rule کل این کار را انجام دهیم. مانند زیر:
1 2 3 | %.o : %.c header.h gcc -c $< -I./header -o $@ |
در این Ruleها برای مشخصکردن یک الگوی خاص از “%” استفاده میکنیم. هر چیزی که بعدازاین علامت بیاید، همان الگوی یکسانی است که Make در بین فایلها به دنبال آن میگردد؛ بنابراین مثلاً:
- c.%: یعنی فایلهایی که پسوند c. دارند.
- o.%: یعنی فایلهایی که پسوند o. دارند.
اینجا دو تا علامت عجیب غریب هم هست. یکی >$ و دیگری @$. اینها یعنی چی؟ به این علائم Automatic Variable گفته میشود و اما متغیرهای خودکار یعنی چه؟
خب وقتی ما از Pattern Rule استفاده میکنیم، هر بار که Recipe میخواهد اجرا شود،باید نام سورس فایل در قسمت مشخصی از دستور gcc قرار گیرد.بنابراین ما نمیتوانیم یک نام خاص به Recipe بدهیم.به همین دلیل است که از یک ویژگی خاص Make به نام Automatic Variable استفاده میکنیم.این متغیرها هر بار متناسب با Pattern Rule یک Target و پیشنیاز در دستور gcc قرار میدهند تا تمامی سورس فایلها به آبجکت فایل کامپایل شود. هر کدام از این متغیرها یک معنای خاصی دارند. برای مثال:
- ^$ : یعنی تمامی Prereqها
- >$ : یعنی اولین Prereq
- @$ : یعنی فایل Target
- …
و اما برای اطلاعات بیشتر در مورد Pattern Rule میتوانید از اینجا مطالعه کنید.
حالا اگر بخواهیم یکم پروژه رو تمیزتر بنویسیم. مثلاً تمامی سورس فایل هامون رو در یک دایرکتوری، هدر فایلها رو هم همینطور و… قرار دهیم در این صورت برای اینکه Make بتواند این فایلها را پیدا کند، دوراه وجود دارد:
1-آدرسدهی مستقیم در Pattern Rule:
1 2 3 | %.o : src/%.c header/header.h gcc -c $< -I./header -o $@ |
2-استفاده از vpath : vpath در واقع همان اول کار یک آدرس از محلی که مثلاً سورس فایلها قرار گرفتهاند به Make میدهد تا با موفقیت کار را به اتمام رساند.
شاید برای شما مفید باشد: آموزش رزبری پای از مقدماتی تا پیشرفته
1 2 | vpath %.c src vpath %.h header |
این قسمت ادامه دارد…
در پایان این بخش سعی کردم شما را با Make آشنا کنم و کمی هم با آن کارکرده باشیم. در بخشهای آتی سعی میکنیم Makefileهای حرفهایتر و منعطفتری بنویسیم.
بحث جذابی رو شروع کردید .
سپاس.
عالی
سپاس فراوان