برنامه نویسی حرفه ای میکروکنترلر در واقع مجموعه مقالاتی است که در آن سعی میکنیم ترفندهای کاربردی برنامه نویسی را با مثال های عملی اندازهگیری کنیم و پرفومنس برنامه را بالا ببریم ، پیش از این دو مقاله دیگر با این مضمون منتشر کردهایم، مقاله اول تحت عنوان میکروکنترلر مقصر نیست مقصر برنامه نویسی است به اهمیت سبک برنامه نویسی و تاثیر آن در خروجی نهایی سیستم میپرداخت ، و در مقاله دوم که با عنوان برنامه نویسی میکروکنترلر را به صورت حرفه ای بیاموزیم منتشر شد، به اهمیت یادگیری برنامه نویسی حرفه ای برای میکروکنترلرها پرداختیم و با یک مثال عملی تفاوت برنامه نوشته شده توسط یک متخصص صرفا کامپیوتر و یک متخصص آشنا به سخت افزار را بررسی کردیم و نشان دادیم که آشنایی با سخت افزار و تسلط کامل به آن تا چه اندازه میتواند باعث تغییرات چشمگیر در عملکرد برنامه شود.
در این مقاله قصد داریم که با طرح یک مثال عملی و کاربردی به تاثیر آشنایی با مفهوم حافظه جهت بهبود برنامه بپردازیم ، پس برای حرفه ای شدن با سیسوگ همراه باشد.
برنامه نویسی حرفه ای میکروکنترلر و طرح مساله
فرض کنید که 4 بایت دادهای موجود است که برای ذخیره سازی یا انتقال نیاز است که این 4 بایت را درون یک متغیر 4 بایتی (uint32_t) ذخیره کنیم ؛ اتفاقا این تبدیل کاربرد زیادی در برنامه نویسی میکروکنترلر دارد و زیاد مورد استفاده قرار می گیرد ، اما واقعا چند روش برای نوشتن چنین برنامهای وجود دارد ، در ادامه روش های مختلف را با هم بررسی میکنیم.
این کار را به شیوه های مختلفی می توان انجام داد که در این مقاله بررسی خواهیم کرد که سبکهای متفاوت با رویکرد های مختلف چه تاثیری در نتیجه نهایی خواهد داشت. در این آموزش از میکروکنترلر AVR با فرکانس 16 مگاهرتز و کامپایلر GCC با اپتیمایز حجم (-Os) استفاده خواهیم کرد.
برنامه ای که همه مینویسند
برنامه از این قراره که 4 بایت رو به یک متغییر 32 بیتی تبدیل کنیم، برای سنجش سرعت هم در نظر گرفتیم که با هر 256 سیکل اجرای تبدیل پایهای از میکروکنترلر تغییر وضعیت دهد ، هرچه فرکانس ایجاد شده بالاتر باشد در نتیجه برنامه ای بهتر نوشته شده است. فرکانس ایجاد شده را هم با استفاده از لاجیک آنالایزر اندازه گیری میکنیم.
اما چطور این کار را انجام دهیم ؛ تعداد زیادی از برنامه نویس ها به زبان آدم ها این کار را انجام میدهند نه زبان ماشین! اما منظور از زبان آدم ها چیست ؛ بهتر است گریزی بزنم به دوران ابتدایی و ریاضیات آن دوره و مفاهیم یکان و دهگان و صدگان و هزارگان و…. را زنده کنیم ، فرض کنید میخواهیم عدد 123 را ایجاد کنیم ، عدد 1 باید در جایگاه صدگان قرار گیرد آن را در عدد صد ضرب می کنیم و عدد 2 که باید در جایگاه دهگان قرار گیرد را در 10 ضرب میکنیم و عدد 3 را هم که در جایگاه یکان قرار دارد قائدتا در یک ضرب میکنیم ، و مجموع حاصل سه عملیات را با هم جمع میکنیم که میشود 123 ! حالا به جای عدد ده و صد و هزار … از معادل کامپیوتری آن استفاده میشود یعنی 256 و 4096 و 65536 و… که برنامه بر همین پایه و اصول نوشته می شود.
1 2 3 4 | counter = (uint32_t)Byte[0] * 0x1; counter += (uint32_t)Byte[1] * 0x100; counter += (uint32_t)Byte[2] * 0x10000; counter += (uint32_t)Byte[3] * 0x1000000; |
همانطور که می بینید بایت های 0 تا 3 در اعداد 0x1 تا 0x1000000 ضرب شده اند و حاصل هر محاسبه با مقدار نهایی جمع شده است. برنامه نهایی به شکل زیر خواهد بود
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | int main(void) { DDRB |= (1<<4) | (1<<3); uint32_t counter = 0; uint8_t Byte[4] = {0}; while(1) { Byte[0]+=1; Byte[1]+=2; Byte[2]+=3; Byte[3]+=4; counter = (uint32_t)Byte[0] * 0x1; counter += (uint32_t)Byte[1] * 0x100; counter += (uint32_t)Byte[2] * 0x10000; counter += (uint32_t)Byte[3] * 0x1000000; if(counter == 0x00) { PORTB ^= (1<<3); } } } |
و نتیجه خروجی به شکل زیر خواهد بود
همانطور که در تصویر مشاهده میکنید این فرایند با فرکانس کاری 16 مگاهرتزی میکروکنترلر تنها 638 بار در ثانیه اجرا می شود!
برنامه را به زبان ماشین نزدیک کنیم
افردای که کمی با تجربهتر باشند و دید درستی نسبت به پردازش های صورت گرفته درون پردازنده داشته باشند آن هم بر روی یک پردازنده 8 بیتی که ضرب اعداد 32 بیتی برایش کار دشواری است ، سعی میکنند که برنامه را منطقیتر و نزدک تر به زبان ماشین بنویسند و قسمت تبدیل را به شکل زیر باز نویسی میکنند
1 2 3 4 | counter = (uint32_t)Byte[0]; counter |= (uint32_t)Byte[1]<<8; counter |= (uint32_t)Byte[2]<<16; counter |= (uint32_t)Byte[3]<<24; |
و نتیجه خروجی به شکل زیر خواهد بود
همانطور که در عکس مشخص است با چند تغییر ساده سرعت اجرای برنامه حدود 3 برابر بهتر شد ، و هر 1735 تبدیل در یک ثانیه انجام می شود. اما تغییرات چه بوده که اینچنین در خروجی تاثیر گذاشته است ؟ ؛ اولین قدم حذف عملیات ضرب است و جایگزینی آن با عملیات شیف در غالب پردازندهها عملیات منطقی از عملیات ریاضی سریعتر انجام میشود و سیکل ماشین کمتری نیاز دارد به طبع جایگزینی عملیات جمع با Or منطقی هم تاثیر بسزایی در فرایند داشته است.
اما سوالی که مطرح می شود این است که آیا باز هم بهتر می شود این کار را انجام داد ؟
برنامه نویس حرفه ای به زبان مسلط است
قائدتا هر زبان برنامه نویسی دارای نکات و ریزه کاری هایی است که گاهی خیلی به فرایند برنامه نویسی و بهبود برنامه کمک میکند ، زبان c هم از این قائده جدا نیست. به عنوان نمونه union یکی از قابلیت هایی است که حدس میزنم بیشتر افراد حتی فراموشش کرده اند ! یا اصلا کاربرد آن را درست نمی داند و همین باعث میشود که برنامه ها گاهی کند و سنگین نوشته شوند.
در این مثال دقیقا union به بهبود برنامه کمک میکند، قبل از این که نمونه کد را ارائه کنیم بگذارید یک یادآوری در خصوص unionها داشته باشیم ؛ union ها دقیقا مشابه با Struct ها تعریف میشوند. تنها تفاوتشون اینه که به جای واژه struct باید از واژه union استفاده کرد. union ساختاریه که برای استفاده بهینه از حافظه ساخته شده و به شما اجازه میده که چند تا متغیر رو داخل یک بلاک از حافظه ذخیره کنید. برای روشن تر شدن قضیه به عکس زیر دقت کنید.
همانطور که در عکس فوق می بینید ، union تعریف شده 4 بایت از حافظه رو اشغال کرده و هر کدام از اعضای ch به یکی از این بایت ها اشاره می کنند ؛ یعنی دقیقا همون چیزی که برای تبدیل لازم داریم . هر تغییر در [2]ch باعث میشه بیت های 16 تا 23 از متغیر val هم که یک متغییر 32 بیتی است نیز تغییر کند ؛ با استفاده از همین قابلیت برنامه رو بازنویسی میکنیم
1 2 3 4 5 6 7 8 9 10 11 12 | union uconv { uint8_t B[4]; uint32_t W; }; union uconv conv; conv.B[0] = Byte[0]; conv.B[1] = Byte[1]; conv.B[2] = Byte[2]; conv.B[3] = Byte[3]; counter = conv.W; |
و اما نتیجه
همانطور که در تصویر مشاهده می کنید استفاده از union تاثیر مثبت در اجرای برنامه داشته است و سرعت آن را بهبود بخشیده است و تا 1952 تبدیل در ثانیه بالا برده است.
ممکه با خودتون بگید این همه دردسر برای یه بهبود جزیی ! ، باید اضافه کنم که همین بهبود های جزیی تفاوت یک برنامه نویس حرفه ای با برنامه نویس معمولی است.
چالش آخر برنامه نویسی حرفه ای میکروکنترلر
با همه وجود توضیحات و راه کارهایی که ارائه کردیم ، باز هم میشود برنامه را بهبود بخشید و سرعت آن را نزدیک به دوبرابر بهتر کرد ، اما فکر می کنید چطور این امکان وجود دارد ؟ ، با استفاده از چه روشی چنین بهبودی امکان پذیر است ؟
برای این که زیاد به بیراهه کشیده نشوید تکنیک مورد استفاده در خصوص نحوه دسترسی به حافظه است.
همانطور که ملاحظه میکنید دانش و تجربه کاری در خصوص سخت افزار و برنامه نویسی می تواند کمک شایانی به بهبود برنامه نویسی حرفه ای میکروکنترلر بکند. برای آموزش های بیشتر با سیسوگ همراه باشید.
ممنون از این مقاله کاربردی
خیلی خوشحالم که این محتوا مفید بوده براتون
باعث افتخاره سیسوگه
راحترین راه و پر سرعت ترین راه آدرس پوینتر
مچکر مفید بود
عالی بود مچکر
خواهش میکنم 🙂
سلام و ممنون بابت پست خوبتون
به نظرم قضیه little endian و big endian بودن سیستم هم در جواب سوالات بالا میتونه تاثیر داشته باشه.
مثلا در سیستمی که little endian باشه روش سوم و روش چهارم(استفاده از پوینتر)، جوابش با ۲ روش اول متفاوت هست. اگه اشتباه میکنم تصحیح بفرمایید.
سلام دوست عزیز
فکر نمیکنم مشکلی پیش بیاره این مساله چون وقتی که دارید روی یکی از این حاالات برنامه رو کامپایل میکنید کل حافظه لیتل یا بیگ میشه ! و همین امر باعث میشه که مشکلی پیش نیاد.
با سلام خدمت زئوس عزیز شاید یکی از دلایل اینکه افرادی مثل شما در زمینه برنامه نویسی موفقند چون برنامه رو به ۸۰۵۱ و اسمبلی شروع کردند و همین باعث شد وقتی که برنامه کامپایلر شد کد اسمبلی ایجاد شده در کامپایلر رو باز می کنند تا تفاوت کدهای تولیدی با
دستورات مختلف رو بررسی کنند که تعداد کد برای اون کار خاص به حداقل برسه و برای نمونه همین کد ساعتها در اینترنت میچرخه تا بهترین کد رو پیدا کنه
مطمینا یه آشنایی به دستورات اسمبلی یک میکرو به برنامه نویس دید خیلی بهتری رو بده
و مطمینا همیشه مشکل از برنامه نویس هست چون کمودور ۶۴ فرکانسش فقط ۹۰۰ کیلوهرتز بود
سلام و درود دوست عزیز
بله درسته ؛ البته درصد زیادی هم کنجکاوی هم هست و البته علاقه ؛
فکر میکنم تفاوت یک برنامه نویس ساده و یک برنامه نویس حرفه ای آشنایی به همین ریزه کاری ها باشه!
آخ کمودور 64 ؛ چه ساعت هایی که ننشستیم و براش برنامه ننوشتیم.
سلام این روش آخر که گفتین تو چند سیکل اجرا میشه؟؟
uint32_t* ptr =(uint32_t*) (&Byte[0]);
counter = *ptr;
خیلی بستگی پیدا میکنه به کامپایلر و معماری پردازنده ؛ فکر میکنم حدود یک سیکل یا دو سیکل ماشین احتمالا
برای جواب دقیق تر می تونید خروجی ASM کامپایلر رو بررسی کنید.
سلام لطفا درباره روش آخر بیشتر رانمایی می کنید
سلام دوست عزیز
کامنت پیمان ریز همین پست رو بخونید ، روش آخر رو درست گفتند
سلام ممنون از توضیحاتتون
خب بی زحمت نمیخاین اخرین قسمتو هم کامل توضیح بدین تا متوجه بشیم چطور سرعت اینقدر بیشتر شد؟؟
خواهش میکنم دوست عزیز
وقتی میگیم چالش یعنی شما به موضوع فکر کنید ، خوشبخانه یکی از دوستان به جواب اشاره کرد که کاملا صحیح هم هست
کامنت های همین پست رو بخونید.
متشکرم مهندس…اما هنوز مونده تا من بتونم مثله یه حرفه ایی کد بزنم ((:
خواهش میکنم
هیچ کس از اول آدم باتجربه ای نبوده ، کار کردن و ممارست برای یادگیری کمک میکنه که آدم بتونه توی هر زیمنه ای پشرفت کنه
قطعا روزهایی بوده که من هم خیلی چیزا برام سوال بوده
پس سوال کنید ؛ کار کنید ؛ و پیشرفت کنید ؛ ما خوشحال میشیم که همران ما حرفه ای باشند و ما رو به چالش بکشند
سلام، در روش اولی که نوشتید ضرایب باید 0xFF و 0xFF00 و … باشند.
روشی که به نظر من میاد استفاده از اشاره گرهاست.
uint32_t* ptr =(uint32_t*) (&Byte[0]);
نه درست نوشتم ضرایب باید مثلا 0xFF+1 باشه – اونم به خاطر 8 بیت بودن هر پک هستش
بله کاملا درسته ؛ اما میتونید بگید چرا سرعتش اینقدر تفاوت داره ؟
تفاوت آیا به این دلیل نیست که در روش پوینتر ما تنها یکبار ادرس را در جای دیگر ریختیم. اما در روش قبل ۴ بار باید یک بایت را در جای دیگر میریختیم.
بله دقیقا درسته 🙂
با سلام خدمت نویسنده بزرگوار
آموزش خیلی جالبی هست . بخصوص برای من که تازه کارم و خیلی خیلی کم تجربه.از زحماتتون خیلی سپاسگزارم.
موفق باشید.
خواهش میکنم دوست عزیز
خوشحالم که این دسته مقالات مورد توجه شما قرار گرفته
سلام
اگر امکانش هست روش حرفهای کهربا دسترسی به حافظه برای این برنامه هست را هم توضیح دهید.
ممنون از سایت و کانال خوبتان
دوستان روش حرفه رو گفتند – کامنت های همین پست رو بخونید