برنامه نویسی حرفه ای تاثیر خیلی زیادی در راندمان سخت افزار دارد، قبلا در مقاله ای تحت عنوان “میکروکنترلر مقصر نیست مقصر برنامه نویسی است” بررسی کردیم که چقدر برنامه نویسی می تواند تاثیر بسزایی در راندامان و بازدهی سخت افزار داشته باشد، با روشن شدن این مساله مهم ، نکته ای که باید به آن توجه داشت ، بهبود سطح برنامه نویسی است. یکی از مسائلی که به شخصه فکر می کنم نقطه ضعف طراح های الکترونیک و البته برنامه نویس های سیستم های نهفته (embedded systems) است عدم تسلط کافی به مقوله برنامه نویسی است؛ برای بررسی بیشتر این مساله با سیسوگ همراه باشید.
اهمیت برنامه نویسی برای مهندسین الکترونیک
امروزه بیشتر مدارات الکترونیکی از میکروکنترلرها و پردازنده ها استفاده می کنند که نیازمند برنامه نویسی برای عملکرد دلخواه هستند، مقوله ای که در دانشگاه ها به آن پرداخته نمی شود آموزش صحیح برنامه نویسی برای مهندسین الکترونیک است ، مهندسین الکترونیک دید خوبی نسبت به سخت افزار و عملگرد آن دارند اما آیا واقعا فکر می کنید گذراندن یک درس دو واحدی “برنامه نویسی” برای یاد گرفتن مهارت برنامه نویسی کافی است ؟
ممکن است فکر کنید، که کار برنامه نویسی سیستم های میکروکنترلر را می شود به مهندسین کامپیوتر واگذار است ،اما واقعا اینطور نیست؛ سیستم های میکروکنترلری دارای پیچیدگی هایی است که درک آن برای یک مهندس کامپیوتر سخت و دشوار است (البته استثنا همیشه وجود دارد) از طرفی مهندسین کامپیوتر با محدودیت های سخت افزاری آشنایی لازم را ندارد و این خود بزرگترین چالش برای آنها خواهد بود. فکر کنید یک مهندس کامپیوتر بخواهد برنامه ای بنویسد که کلا از 512 بایت RAM استفاده کند.
پس بهترین گزینه برای پر کردن خلاء برنامه نویسی سخت افزار یا همان میکروکنترلر، مهندسین الکترونیک هستند به شرط آنکه مهارت برنامه نویسی خود را بهبود ببخشند و از روش های حرفه ای برای برنامه نویسی استفاده کنند ، ما در سیسوگ سعی خواهیم کرد با مطرح کردن چالش هایی ، مهارت برنامه نویسی دوستان عزیز را محک بزنیم.
چالش برنامه نویسی این پست
امروزه که میکروکنترلر های ARM رواج پیدا کردهاند باعث تغییرات شگرفی در طراحی سخت افزار شده است ، پردازنده 32 بیتی که مقدار RAM و FLASH قابلی توجهی دارد و سرعت بالایی را کنار توان مصرفی کم ارائه می کند ، شما ممکن است به یاد نداشته باشید که طراحی میکروکنترلری با استفاده از Z80 یا 8086 چقدر دشوار و پیچیده بود از طرفی برنامه نویسی به زبان اسمبلی برای محاسبات ریاضی بر روی اعداد 32 بیتی یک کابوس تمام عیار بود و یا محاسبات اعشاری و ممیز شناور کار هر کسی نبود اما امروزه به لطف تکنولوژی تمام این کابوس های تلخ تبدیل به یک رویای شیرین شده است. با این همه، تکنولوژی نمی تواند برخی مسائل را حل کند ، برنامه نویسی نیز یکی از این مسائل است. برای چالش این پست فرض می کنیم که یک متغیر 32 بیتی داریم!(با توجه به وجود میکروکنترلرهای 32 بیتی ARM) و قصد داریم تعداد بیت های 1 را در این متغییر شمارش کنیم. برای روشن شدن مساله به جدول زیر توجه کنید
1 2 3 4 | 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 را در ورودی دریافت کرد ، عدد 4 را در خروجی نمایش دهد. ممکن است نوشتن چنین برنامه ای کار ساده ای باشد ولی روش های مختلفی که می شود این برنامه را نوشت بررسی کنیم.
برنامه ای که همه می نویسند
1 2 3 4 5 6 7 8 9 10 11 12 | int CountingBitsSet(int data) { int sum=0; for(int i=0;i<sizeof(int)*8;i++) { if((data&(1<<i))!=0) sum++; } return sum; } |
بگذارید اول نحوه عملکرد برنامه رو توضیح بدیم ، بعد خواهیم گفت چرا به لحاظ پرفومنسی جالب نیست ، در این روش ، ما به اندازه تعداد بیت های متغیر حلقه ایجاد کرده ایم و تک تک بیت ها رو به لحاظ یک بودن بررسی میکنیم در صورتی که بیت اول یک بود یک واحد به متغیر sum اضافه میکنیم ، بعد بیت دوم ، بعد سوم و همینطور تا آخرین بیت.
اما چرا میگیم این برنامه به لحاظ پرفومنسی بهینه نیست اولین مساله وجود حلقه است (که ظاهرا اجنتاب ناپذیره) دوم محاسباتی که توی حلقه انجام میشه همونطور که می بینید عملیات مقایسه ای داریم ، شیفت بیتی داریم عملیات منطقی(AND) و جمع داریم یعنی برای هر بار اجرای حلقه کلی محاسبه نیازه که انجام بشه ! اما چطور میشه برنامه رو بهینه کرد ؟ با مطالعه پست “میکروکنترلر مقصر نیست مقصر برنامه نویسی است” میتونید ایده بگیرد.
برنامه ای که بعد از فکر کردن می نویسید
1 2 3 4 5 6 7 8 9 10 11 | int CountingBitsSet(int data) { int sum=0; for(sum=0;data;data>>=1) { sum += data & 1; } return sum; } |
برنامه ای که مهندس سخت افزار مسلط به میکروکنترلر مینویسه
1 2 3 4 5 6 7 8 9 10 11 12 | #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; } |
سری میکروکنترلر های Cortex-m قابلیتی دارند تحت عنوان Bit-Banding ؛ این قابلیت به میکروکنترلر اجازه میده که به صورت بیتی به حافظه SRAM دسترسی داشته باشد ، یعنی یک بیت از RAM رو بخونیم یا بنویسیم! از اونجایی که خانواده Cortex-m مخصوص میکروکنترلر ها توسعه پیدا کرده ، این قابلیت که کمک فراوانی به سادگی برنامه نویسی میکنه بهش اضافه شده ، اما این کار چطور ممکنه ؟ برای درک بهتر به تصور زیر دقت کنید.
همانطور که در تصویر فوق مشاهده می کنید هر بیت از حافظه Sram روی یک آدرس دیگه مپ شده که با خوندن یا نوشتن اون آدرس از حافظه میشه به مقدار اون بیت دسترسی داشت.
برنامه ای که حرفه ای ها می نویسند
تا اینجا چند روش ساده رو بررسی کردم و تا جای ممکن اون روش رو با دانش برنامه نویسی و دید سخت افزاری بهینه کردیم ، اما آیا فکر می کنید باز هم میشه بهتر این برنامه رو نوشت ؟ قطعا جواب مثبت هست اما چطور ؟ برای دید بهتر به برنامه زیر دقت کنید
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 شامل تعداد بیت های یک اعداد 0 تا 255 هست یعنی یک بایت.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 چنین جدولی را ایجاد کردیم یک چالش باشد برای شما که جوابش رو پیدا کنید. هر 32 بیت متشکل از 4 بایت است که اگر مجموع بیت های یک هر بایت را هم جمع کنیم حاصل مجموع بیت های متغیر خواهد بود. این اتفاقی است که در ادامه کد افتاده است.
چالش انتهایی
1 2 3 4 5 6 7 | 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; } |
این روشی نیست که همه بخوان ازش استفاده کنند؛ نوشتنش که هیچ ، حتی درک این که چطور این برنامه کار میکنه هم کار هر کسی نیست !
آیا کسی میتونه بگه چطور این برنامه کار میکنه ؟
ممنون از این مقاله کاربردی
سلام. ممنون ازین مطلب عالی.
بنظرتون اگه بخوایم از لحاظ سرعت اجرا برسی کنیم کدی که از جدول استفاده میکنه سریع تره یا کدی که تو چالش انتهایی نوشته شده؟
قطعا جدول سریعتره – چون فقط به اندازه یک دسترسی به حافظه زمان میبره 🙂
احساس پوچی کردم ای بابا :)))
چرا احساس پوچی دوست عزیز !
تا وقتی میشه چیزای جدید یاد گرفت، جای بسیار زیادی برای امیدواری هست
سلام. ممنون از سایت خوبتون. یه سوال داشتم. من برنامه نویسی avr كار كردم و از سخت افزار هم تا حدودی سر در میارم. سال بعد دانشكاه میرم. خواستم ببینم arm یاد بگیرم بهاره یا ای وی ار رو ادامه بدم. و این كه شركت های بزرك مثلا تسلا یا اسپیس ایكس و… كه پیچیده ترین كد نویسی ها رو برای سخت افزار انجام میدن از چه تراشه هایی استفاده میكنن؟ آیا تراشه های arm به اندازه كافی قدرت دارن؟ اكر كسی بخواد برنامه نویسی در اون حد بیشرفته رو یاد بگیره باید چیكار كنه؟
سلام دوست من – خودتون محدود به نوع پردازنده نکن
هر میکروکنترلری برای هدفی ساخته شده و اینطور نیست مثلا با یادگیری یک مدل پردازنده همه کارهایی که میخوای بکنی رو بتونی پوشش بدی – اگه بخوام بهت توصیه ای داشته باشم میگم اصول رو یاد بگیر از بیس یاد بگیر
مثلا پروتکل uart یه استاندارده که همه جا به همون شکل هندل میشه – حالا از یه میکرو به میکروی دیگه فقط چند تا رجیستر تغییر میکنه به همین سادگی !!!
شرکت های بزرگ بسته به کاری که میخوان انجام بدن سخت افزار رو انتخاب می کنند.
http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
بله متشکر
مطالب لینکی که بالا دادم فوق العادست برای همین Counting bits set اومده ۷ روش روگفته
کلی چالش دیگه هم داره
—سوال —-
منبع کامل آموزش دقیق C با رویکرد embedded چیزی معرفی می کنید؟ مثلا همین volatile رو خیلی جاها توضیح نداده
چالش های دیگه اش هم جالبه 🙂
— جواب —
نه متاسفانه ؛ فکر نمیکنم زبان سی فرقی بین امبدد و خیر امبدد داشته باشه ؛ شناخت از سیستم های امبدد هست که کمک میکنه برنامه شما خاص اون سخت افزار باشه یا نباشه
فکر کنم دوستمون آقا محمد منظورشون از منبع همین لینک بوده ؛ ممنون که پست کردید این لینک رو ؛ واقعا جالبه ؛ توضیحات خیلی خوبی هم داره
مشاهده اش رو پیشنهاد میکنم به تمام دوستان
سلام ممنون از سایت خوبتون. اما کاش منابع رو هم ذکر میکردید. تا الان که توی سایت میگشتم هیچ پستی منبع نداره.
وقتی خودتون اصرار دارید مطالب با منبع ذکر بشن پس بهتره خودتون هم منبع رو ذکر کنید?
سلام دوست گرامی
تمام مطالبی که ترجمه هستند یا از منبعی بازنشر شده باشند دارای منبع هستند ِ به عنوان نمونه تمام آموزش های STM8 یا آموزش ها و رفرنس Arduino و… همه دارای منبع هستند ِ اگر میبینید مطلبی دارای منبع نیست ِیعنی از اول نوشته شده توسط نویسنده نه این که از جایی کپی شده باشد که نیاز به ذکر منبع داشته باشد
فکر میکنم همه سایت رو ندیدید !!!
(unsigned char *) &data; این یعنی چی
سلام ؛
&data در واقع آدرس حافظه ای که متغییر data توی اون ذخیره شده رو بر میگردونه ؛ خوب حالا این حافظه از چه نوعی هست ؟ خوب معلومه دیگه static const unsigned char ؛ وقتی بخوایم اونو تو یه اشارهگر از نوع unsigned char بریزیم کامپایلر خطا میده ! برای این که با این خطا مواجه نشیم با این شکل نوشتاری ((unsigned char *) &data;) به کامپایلر میگیم داداش تو فکر کن متغییر ما از نوع unsigned char* هستش 🙂
با سلام و خسته نباشید .
اون کدی که با چند خط دیفاین جول رو تولید میکنه داره از توابع بازگشتی استفاده میکنه و این خطB6(0), B6(1), B6(1), B6(2) درواقع کل جدول رو تولید میکنه .به این صورت کهB2(n) مربوط به اعداد 2بیتی است .و B2(0) میشه 0,1,1,2
اگر برای اعداد 3 بیتی هم بخواهیم محاسبه کنیم 0,1,1,2,1,2,2,3 اگر کمی دقت کنیم دنباله مربوط به 3 بیتی میشهB2(0),B2(1) یا به طور کلی B3(n)=B2(n),B2(n+1)
به طور خلاصه هر بار که یک بیت به طول عدد اضافه میشه دنباله جدید عبارت است از دنباله مربوط به حالت قبلی که جملاتش 1 واحد اضافه شده و در ادامه دنباله قبلی اومده . به طور مثال برای 4 بیت میشه B3(0),B3(1) که اگر به جای b3 معادل اون بر اساس b2 رو بنویسیم میشه
B2(0),B2(1),B2(1),B2(2) که در حالت کلی B4(n)=B2(n),B2(n+1),B2(n+1),B2(n+2) که همون فرمول جدول هست منتها برای صرفه جویی حالتهای B3(n) و B5 و B7 حذف شدن . و فرمولها مستقیم بر اساس 2 مرحله قبل نوشته شدن .
ولی کد آخری فهمیدنش واقعا مشکل هست!!!
بسیار عالی ^_^ ؛ بله همیطوره که میگید
در مورد تابع آخر وقتی بیتی خودتون تجزیه تحلیلش کنید خیلی راحت تر میشه درکش 🙂
برای منم اولش سخت بود و بی معنی