حتما برای شما هم پیشآمده که در پروژهای بهدلایل زیادی ازقبیل کمبود حافظه فلش، کمبود حافظه رم، سرعت پایین و… مجبور شدهاید میکروکنترلر را عوضکنید و زحمت برنامهنویسی مجدد را به جان بخرید در این موارد معمولا تمام کاسهکوزهها سر میکروکنترلر شکسته میشود و محدودیتهای آن مورد سرزنش و شماتت قرار می گیرد! درصورتیکه همیشه اینچنین نیست. در اغلب اوقات با تغییرات جزئی در مدار یا سبک برنامهنویسی بهراحتی میتوان مشکلات پیشرو را حلکرد؛ متاسفانه خیلیاز دوستان و همکارانگرامی به این موضوع اشراف ندارند و به اشتباه میکروکنترلر را مقصر میدانند؛ بهعنوان مثال خیلیاز دوستان، میکروکنترلر AVR را یک میکروکنترلر صنعتی نمیدانند ، درعوض میکروکنترلر PIC یا ARM را صنعتی میدانند، برای شخصی که تجربه کافی درخصوص طراحی مدارات دیجیتال و صنعتی داشتهباشد، این استدلال نه تنها بی پایه و اساس است بلکه خنده دار هم خواهد بود؛ محدودیت ها و باید و نباید های هر پردازنده ای در منوال آن ذکر شدهاست، با رعایت این نکات و البته طراحی صحیح هر میکروکنترلری را می توان در هرجایی که موردنیاز باشد استفاده کرد، البته منکر این مهم نیستم که برخی میکروکنترلرها برای مقاصد خاصی طراحی و تولید میشوند. چه مدارات صنعتی که بر پایه همین میکروکنترلر AVR طراحی شدهاند؛ مهم نکاتیاست که باید در طراحی لحاظ کرد. در این مقاله قصد داریم نشاندهیم که چقدر مدل برنامهنویسی میتواند در عملکرد یک میکروکنترلر دخیل باشد و در خیلی از هزینهها صرفهجویی کنید پس با ما همراه باشد.
معرفی میکروکنترلر و نحوه آزمایش سبک برنامهنویسی
در این تست از میکروکنترلر ATMEGA328 در فرکانس ۱۶مگاهرتز (برد آردوینو البته از GCC برای کامپایل کد استفاده خواهیمکرد نه ابزار آردوینو) استفادهخواهیمکرد و با اتصال یک عدد LCD رنگی بهصورت سریال راهاندازی میشود قصدداریم سرعت رسم فریمهای تصویر را اندازهگیری کنیم. برای اینکار مدت زمان رسم ۱۰فریم از تصویر را اندازهگیری میکنیم و نمایش میدهیم. LCD مذکور از انتقال ۹بیت سریال برای رسم تصویر استفاده میکند و واحد SPI موجود در میکروکنترلر نهایتا درحالت ۸بیتی کار میکند و نمیتوانیم از آن استفادهکنیم پس این قسمت از برنامه را مجبوریم که با کدنویسی پیادهسازی کنیم. در ادامه بررسی خواهیمکرد که مدلهای برنامهنویسی چه تاثیری در سرعت اجرای آن خواهند داشت.
برنامهاول: برنامهای که همه می نویسند
برای نوشتن برنامهای که 9بیت را بهصورت سریال انتقالدهد یکیاز مرسومترین راهها استفادهاز حلقه for میباشد. در اولین قدم ما نیز برنامه را به روش رایج مینویسیم:
void LcdSend(uint16_t data) { cs_clr(); for(uint16_t i=0;i<9;i++) { if(data & 1<<(8-i)) sda_set(); else sda_clr(); clk_set(); clk_clr(); } cs_set(); }
همانطورکه می بینید ابتدا پایه CS را صفر میکنیم، بعد بااستفادهاز حلقه For 9 بیت داده را انتقال میدهیم و سپس پایه CS را یک میکنیم! مسالهای که برای ما حائز اهمیت است، سرعت اجرای این کد است، فرضکنید قصدداریم تصویر متحرکی را روی صفحه نمایش دهیم، اگر سرعت رفرششدن صفحه از حدی بیشتر شود، اصلا امکان چنین کاری نیست، با اجرای این کد خروجی زیر را خواهیمداشت.
همانطورکه در تصویر مشخص است، اجرای ده فریم از تصویر (یعنی دهبار پرکردن صفحه) زمانی حدود ۲۰۴۶۵ میلیثانیه نیاز داشتهاست، بهعبارتی هربار پرکردن صفحه دوثانیه طول میکشد. در این مرحله عدهای ناامید میشوند و تقصیر را به گردن میکروکنترلر میاندازند و سراغ میکروکترلر قویتری میروند! اگر دید درستی به سختافزار و نرمافزار داشتهباشیم بهسادگی میشه حدسزد که مشکل از کجاست و چطور میشه حلش کرد! در این مثال بگذارید اول بررسی کنیم، برای پرکردن صفحه از رنگی خاص چقدر زمان CPU صرف میشه، صفحه نمایشگر ما ۱۶۰ در ۱۲۰ است یعنی۱۹۲۰۰ پیکسل باید دیتا دریافت کنند، برای پرکردن هر پیکسل نیازه که دوبار این تابع قراخوانی بشه، میشه درواقع ۳۸۴۰۰بار باید این تابع رو فراخوانی کنیم! حالا چطور میتونیم برنامه رو بهینهتر کنیم، داخل for رو نگاه کنید! باتوجهبه اینکه دستورات درون for با هربار اجرای تابع ۹ مرتبه اجرا میشوند ، برای پرکردن صفحه لازمه ۳۴۵۶۰۰بار اجرا شوند. یعنی صرفهجویی در یک سیکل ماشین درون روتین حلقه، به تعداد ۳۴۵۶۰۰سیکل ما را سریعتر میکند، باتوجهه فرکانس کاری ۱۶مگ، هر سیکل صرفهجویی، معادل ۲۱.۶ میلیثانیه خواهد بود! پس اصلا دستکم نگیریدش! اما چطور میشه برنامه رو بهینهتر نوشت؟
برنامهدوم: برنامهای که بعداز تفکر مینویسید
بعداز کمی فکرکردن درخصوص اینکه چطور میتونید برنامه رو بهینه کنید، با مقداری هوش– برنامه رو اینچنین خواهید نوشت:
void LcdSend(uint16_t data) { static uint16_t ShiftBit[] = {0x100,0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01}; cs_clr(); for(uint16_t i=0;i<9;i++) { if(data & ShiftBit[i])) sda_set(); else sda_clr(); clk_set(); clk_clr(); } cs_set(); }
اما چه تغییری کرد برنامه؟ درواقع فرایند مقایسه را بهینه کردیم، در برنامهی قبل عملیات مقایسه بهصورتزیر بود:
if(data & 1<<(8-i))
در خط فوق برای هر بیت، اول ۸ را از متغیر i کم میکردیم، بعد عدد ۱ به تعداد حاصل شیفت میدادیم به سمت چپ و بعد با مقدار data اند میکردیم و براساس آن خروجی را تنظیم میکردیم! این فرایندها را برای پرکردن صفحه ۳۴۵۶۰۰بار تکرار میکردیم! اگر بتوانید این فرایند را حذف کنیم، قطعا تعداد سیکل خیلی زیادی صرفهجویی میکنیم. اما چطور؟ خیلی ساده است باتعریف یک جدول مقایسه (lookup table)!
static uint16_t ShiftBit[] = {0x100,0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01};
یعنی حاصل عبارتی را میکروکنترلر باید یکبار تفریق و یکبار شیفت را برایش انجام میداد بهصورت دستی محاسبهکنیم و در یک آرایه ذخیرهکنیم، و در شرط مقدار موردنظر را از آن استخراج کنیم، اما ببینم اینکار تا چهاندازه اثربخش خواهد بود؟
بله، همین کار ساده سرعت اجرای برنامه را تقریبا ۲برابر افزایش داد؛ یعنی برای رسم هر فریم نیاز به ۱ثانیه زمان بیشتر نخواهیدداشت! اما آیا باز میشود کد را بهینهتر نوشت؟
برنامهسوم: برنامهای که با خلاقیت و شناخت سختافزار مینویسید
در برنامهی قبلی بااستفادهاز یک تکنیکساده سرعت اجرای برنامه ۲برابر شد، اما آیا راهدیگری هست که باز هم بشود برنامه را بهینهکرد؟اینجاست که دیدشما نسبتبه سختافزار و آشناییبا معماری پردازنده به کمک شما میآید، بیایید نگاهی به کد اسمبلی تولیدشده بیندازیم.
اگر دقتکنید در خط ۶۲۴ و ۶۲۵ دو دستور اسمبلی داریم با نامهای LD ، کار این دستورها لودکردن مقداریاست که Z به اون اشاره میکنه (انتخاب اینکدس موردنظر از آرایه تعریفشده ShiftBit) مطابق اونچه داخل دیتاشیت AVR موجوده هرکدام از این دستورات به ۲سیکل ماشین نیاز داره، دستور brne که هم برای پیادهسازی if استفادهشده هم برای پیادهسازی for، اجراش نیاز به ۱یا ۲(درصورتیکه پرش انجام بشه) سیکل ماشین داره، خود پیادهسازی حلقه هم، نیازبه cpc و cpi داره که هرکدوم یک سیکل ماشین رو مصرف میکنن! با توصیفاتبالا مشخصمیشه که حذف حلقه و انتخاب از ایندکس آرایه خیلی در سیکلهای ماشین مصرفشده صرفهجویی خواهد کرد، پس برنامه را به شکلزیر تغییر میدهیم:
void LcdSend(uint16_t data) { static uint16_t ShiftBit[] = {0x100,0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01}; cs_clr(); if(data & ShiftBit[0])) sda_set(); else sda_clr();clk_set();clk_clr(); if(data & ShiftBit[1])) sda_set(); else sda_clr();clk_set();clk_clr(); if(data & ShiftBit[2])) sda_set(); else sda_clr();clk_set();clk_clr(); if(data & ShiftBit[3])) sda_set(); else sda_clr();clk_set();clk_clr(); if(data & ShiftBit[4])) sda_set(); else sda_clr();clk_set();clk_clr(); if(data & ShiftBit[5])) sda_set(); else sda_clr();clk_set();clk_clr(); if(data & ShiftBit[6])) sda_set(); else sda_clr();clk_set();clk_clr(); if(data & ShiftBit[7])) sda_set(); else sda_clr();clk_set();clk_clr(); if(data & ShiftBit[8])) sda_set(); else sda_clr();clk_set();clk_clr(); cs_set(); }
بله یا یک روشساده و حذف حلقه؛ برنامه به شکلفوق درخواهد آمد!
همانطورکه میبینید با این تغییر سرعت اجرا باز دوبرابر شد یعنی رسم هر فریم تصویر تقریبا ۵۵۰میلیثانیه زمان خواهد برد و این یعنی چهاربرابر سریعتر از برنامه اول! میبینید که بهسادگی و فقط با تغییر در مدل کدنویسی میتوان نتیجه خیلی بهتری را از سختافزار موردنظر گرفت، تاجاییکه دیگر لازمبه تغییر میکروکنترلر نباشد!
مهمنیست از چه میکروکنترلری استفاده میکنید؛ ARM یا AVR مهم این است که چقدر در کار با آن تجربه و تبحر دارید!
چالش برنامه نویسی آخر
خوب تا اینجا دیدیم که چقدر نحوه برنامهنویسی میتونه توی نتیحه خروجی(حداقل در این موردخاص سرعت) تاثیرگذار باشه! بهعنوان چالش برای شما، همین برنامه را با همین ساختار باز بهینه کردم ، ولی اینبار شما حدسبزنید چطور میشه اینکار رو انجام داد؟
البته بااستفادهاز تکنیکهای دیگر برنامهنویسی همچنان میشه سرعت را زیادتر کرد.
منبع: سیسوگ