برنامهنویسی حرفهای تاثیر خیلیزیادی در راندمان سختافزار دارد، قبلا در مقالهای تحتعنوان “میکروکنترلر مقصر نیست مقصر برنامه نویسی است” بررسیکردیم که چقدر برنامهنویسی میتواند تاثیر بسزایی در راندامان و بازدهی سختافزار داشتهباشد، با روشنشدن این مسالهی مهم، نکتهای که باید به آن توجهداشت، بهبود سطح برنامهنویسی است. یکیاز مسائلی که به شخصه فکر میکنم نقطه ضعف طراحهای الکترونیک و البته برنامهنویسهای سیستمهای نهفته (embedded systems)است عدم تسلط کافی به مقوله برنامهنویسی است؛ برای بررسی بیشتر این مساله با ما همراه باشید.
اهمیت برنامه نویسی برای مهندسین الکترونیک
امروزه بیشتر مدارات الکترونیکی از میکروکنترلرها و پردازندهها استفاده میکنند که نیازمند برنامهنویسی برای عملکرد دلخواه هستند، مقولهای که در دانشگاهها به آن پرداخته نمیشود آموزش صحیح برنامهنویسی برای مهندسین الکترونیک است، مهندسین الکترونیک دیدخوبی نسبتبه سختافزار و عملکرد آن دارند اما آیا واقعا فکر میکنید گذراندن یک درس دوواحدی “برنامه نویسی” برای یادگرفتن مهارت برنامهنویسی کافی است؟ ممکناست فکرکنید، که کار برنامهنویسی سیستمهای میکروکنترلر را می شود به مهندسین کامپیوتر واگذار است ،اما واقعا اینطور نیست؛ سیستم های میکروکنترلری دارای پیچیدگیهایی است که درک آن برای یک مهندس کامپیوتر سخت و دشوار است (البته استثنا همیشه وجود دارد) از طرفی مهندسین کامپیوتر با محدودیتهای سختافزاری آشنایی لازم را ندارد و این خود بزرگترین چالش برای آنها خواهد بود. فکرکنید یک مهندس کامپیوتر بخواهد برنامهای بنویسد که کلا از ۵۱۲بایت RAM استفادهکند. پس بهترین گزینه برای پرکردن خلاء برنامهنویسی سختافزار یا همان میکروکنترلر، مهندسین الکترونیک هستند بهشرط آنکه مهارت برنامهنویسی خود را بهبود ببخشند و از روشهای حرفهای برای برنامهنویسی استفادهکنند.
چالش برنامه نویسی این پست
امروزه که میکروکنترلرهای ARM رواج پیدا کردهاند باعث تغییرات شگرفی در طراحی سختافزار شدهاست، پردازنده ۳۲بیتی که مقدار RAM و FLASH قابلیتوجهی دارد و سرعتبالایی را کنار توانمصرفی کم ارائه میکند، شما ممکناست بهیاد نداشتهباشید که طراحی میکروکنترلری بااستفادهاز Z80 یا 8086 چقدر دشوار و پیچیده بود ازطرفی برنامهنویسی به زبان اسمبلی برای محاسبات ریاضی برروی اعداد ۳۲بیتی یک کابوس تمامعیار بود و یا محاسبات اعشاری و ممیز شناور کار هرکسی نبود اما امروزه بهلطف تکنولوژی تمام این کابوسهای تلخ تبدیلبه یک رویای شیرین شدهاست. با این همه، تکنولوژی نمیتواند برخی مسائل را حل کند، برنامهنویسی نیز یکیاز این مسائل است. برای چالش این پست فرض میکنیم که یک متغیر ۳۲بیتی داریم!(باتوجهبه وجود میکروکنترلرهای ۳۲بیتی ARM) و قصد داریم تعداد بیت های ۱ را در این متغییر شمارش کنیم. برای روشنشدن مساله به جدولزیر توجهکنید:
0x80000001 = 1000 0000 0000 0000 0000 0000 0000 0001 -> 2 Bit Set 0x00000001 = 0000 0000 0000 0000 0000 0000 0000 0001 -> 1 Bit Set 0xF0000F00 = 1111 0000 0000 0000 0000 1111 0000 0000 -> 8 Bit Set 0xA0000500 = 1010 0000 0000 0000 0000 0101 0000 0000 -> 4 Bit Set
درواقع ما نیازیبه برنامهای داریم که اگر عدد0xA0000500 را در ورودی دریافتکرد، عدد۴ را در خروجی نمایشدهد. ممکناست نوشتن چنین برنامهای کار سادهای باشد ولی روشهای مختلفی که میشود این برنامه را نوشت بررسی کنیم.
برنامهای که همه مینویسند:
int CountingBitsSet(int data) { int sum=0; for(int i=0;i<sizeof(int)*8;i++) { if((data&(1<<i))!=0) sum++; } return sum; }
قطعا سادهترین برنامهای که میشه نوشت برنامه بالاست ولی برنامهی بالا خیلی کند عمل خواهد کرد؛ شاید برای برنامهنویس کامپیوتر که قراره برنامه برروی یک پردازنده چند گیگاهرتزی چند هستهای اجرا بشه؛ شاید زیاد اهمیت نداشتهباشه این مساله ولی برای اجرا روی یک پردازنده Cortex-m که فرکانس چند مگاهرتزی داره مساله بازدهی خیلی مهمه! بگذارید اول نحوه عملکرد برنامه رو توضیحبدیم، بعد خواهیمگفت چرا بهلحاظ پرفومنسی جالب نیست، در این روش، ما به اندازه تعداد بیتهای متغیر حلقه ایجاد کردهایم و تکتک بیتها رو بهلحاظ ۱ بودن بررسی میکنیم درصورتیکه بیت اول یک بود یک واحد به متغیر sum اضافه میکنیم، بعد بیتدوم، بعد سوم و همینطور تا آخرین بیت. اما چرا میگیم این برنامه بهلحاظ پرفومنسی بهینه نیست اولین مساله وجود حلقه است(که ظاهرا اجنتابناپذیره) دوم محاسباتی که توی حلقه انجام میشه همونطورکه میبینید عملیات مقایسهای داریم، شیفت بیتی داریم عملیات منطقی(AND) و جمع داریم یعنی برای هربار اجرای حلقه کلی محاسبه نیازه که انجام بشه! اما چطور میشه برنامه رو بهینه کرد؟ با مطالعه پست “میکروکنترلر مقصر نیست مقصر برنامه نویسی است” میتونید ایده بگیرد.
برنامه ای که بعداز فکرکردن مینویسید:
int CountingBitsSet(int data) { int sum=0; for(sum=0;data;data>>=1) { sum += data & 1; } return sum; }
برنامه فوق باز بهتر از برنامه قبلی شد، چراکه یک عملیات شیفت بیتی و یک عملیات مقایسهای رو حذف کردیم؛ درواقع عملیات شیفت و مقایسه رو بهداخل حلقه منتقل کردیم به اینصورت که مقایسه با خود حلقه انجام بشه و کل متغییر به سمت چپ درون عملگر حلقه شیفت پیدا کنه، با این صرفهجوییها راندمان برنامه بهتر شد ولی هنوز اون مطلوبی که باید باشه نیست؛ آیا هنوز ایدهای برای بهترشدن برنامه دارید؟
برنامه ای که مهندس سخت افزار مسلط به میکروکنترلر مینویسه:
#define GetBB_Adr(VarAddr) ( (__IO uint32_t *) (SRAM_BB_BASE | ( ((uint32_t)VarAddr - SRAM_BASE) << 5) )) int CountingBitsSet(int data) { int sum=0; volatile uint32_t *RamBB = GetBB_Adr(&data); for(int i=0;i<sizeof(int)*8;i++) { sum += RamBB[i]; } return sum; }
این جاییست که دید سختافزاری و آشنایی با سختافزار به کمک شما میآید و برنامه رو بهتر میکنه! همونطورکه میبینید، حلقه که به قوت خودش باقیاست ولی عملیات شیفت و And منطقی حذفشده که باعث افزایش سرعت میشه! اما واقعا در برنامه چه اتفاقی میافته؟ چطور سختافزار به کمکما میآید؟ سری میکروکنترلرهای Cortex-m قابلیتیدارند تحت عنوان Bit-Banding؛ این قابلیت به میکروکنترلر اجازهمیده که بهصورت بیتی به حافظه SRAM دسترسی داشتهباشد، یعنی یک بیت از RAM رو بخونیم یا بنویسیم! از اونجاییکه خانواده Cortex-m مخصوص میکروکنترلرها توسعه پیداکرده، این قابلیت که کمک فراوانی بهسادگی برنامهنویسی میکنه بهش اضافهشده، اما اینکار چطورممکنه؟ برای درکبهتر بهتصور زیر دقتکنید.
همانطورکه در تصویرفوق مشاهده میکنید هربیت از حافظه Sram روی یک آدرس دیگه مپ شده که با خوندن یا نوشتن اون آدرس از حافظه میشه به مقدار اون بیت دسترسی داشت.
برنامهای که حرفهایها مینویسند
تااینجا چندروش ساده رو بررسی کردم و تاجایممکن اون روش رو با دانش برنامهنویسی و دید سختافزاری بهینهکردیم، اما آیا فکر میکنید بازهم میشه بهتر این برنامه رو نوشت؟ قطعا جواب مثبت هست اما چطور؟ برای دید بهتر به برنامهی زیر دقتکنید:
int CountingBitsSet(int data) { static const unsigned char BitsSetTable256[256] = { #define B2(n) n, n+1, n+1, n+2 #define B4(n) B2(n), B2(n+1), B2(n+1), B2(n+2) #define B6(n) B4(n), B4(n+1), B4(n+1), B4(n+2) B6(0), B6(1), B6(1), B6(2) }; unsigned char * p = (unsigned char *) &data; int sum = BitsSetTable256[p[0]] + BitsSetTable256[p[1]] + BitsSetTable256[p[2]] + BitsSetTable256[p[3]]; return sum; }
بله در برنامه فوق حلقه For حذفشدهاست و بهجای آن از جدول استفاده کردهایم، این کار باعث افزایش چشمگیر سرعت اجرای برنامه خواهد شد، درواقع جدول BitsSetTable256 شامل تعداد بیتهای یک اعداد ۰ تا ۲۵۵ هست یعنی یک بایت.
static const unsigned char BitsSetTable256[256] = { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4, 1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5, 1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5, 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, 1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5, 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, 3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, 1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5, 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, 3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, 2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6, 3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, 3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7, 4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8, }
اینکه چطور با چهارخط #define چنین جدولی را ایجاد کردیم یک چالش باشد برایشما که جوابش رو پیداکنید. هر ۳۲بیت متشکلاز ۴بایت است که اگر مجموع بیتهای یک هر بایت را هم جمعکنیم حاصل مجموع بیتهای متغیر خواهدبود. این اتفاقیاست که در ادامه کد افتادهاست.
چالش انتهایی
int CountingBitsSet(int data) { data = data - ((data >> 1) & 0x55555555); data = (data & 0x33333333) + ((data >> 2) & 0x33333333); int sum = ((data + (data >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; return sum; }
خوب بهسادگی برنامه فوق هم میشه این کار رو انجام داد!اما این برنامه دقیقا چطور کار میکنه؟ این روشی نیست که همه بخوان ازش استفادهکنند؛ نوشتنش که هیچ، حتی درک اینکه چطور این برنامه کار میکنه هم کار هرکسی نیست! آیا کسی میتونه بگه چطور اینبرنامه کار میکنه؟
منبع: سیسوگ