آموزش آردوینو

آموزش بهینه سازی کد آردوینو (کاهش حجم و زمان)

در سیستم های مبتنی بر میکروکنترلر که منابع و حافظه محدود هستند، توسعه دهندگان زمان و هزینه زیادی را صرف میکنند تا کد بهینه ای داشته باشند. هدف اصلی این کار بهبود عملکرد کلی سیستم است. مقاله ای که در اینجا ارائه می کنیم، تکنیک های مختلفی را برای بهینه سازی کد آردوینو بررسی می کند. ما این تکنیک ها را به تکنیک های کلی و تکنیک های پیشرفته دسته بندی می کنیم. در مورد هر روش توضیح کلی می دهیم و سپس به چند نمونه نگاه می کنیم.

در زمینه برنامه نویسی آردوینو، بهینه سازی روشی برای بهبود کد برای تولید یک برنامه کارآمد است. هدف اصلی این است که جنبه های خاصی از برنامه مانند اندازه برنامه، مصرف حافظه، زمان اجرا، توان عملیاتی و مصرف انرژی را بهبود بخشد. در حالت ایده آل، یک برنامه بهینه شده دارای فرمت کوچکتر است، حافظه کمتری مصرف می کند، زمان اجرای کمتری دارد و انرژی کمتری مصرف می کند. اینها ملاحظات طراحی مهمی هستند که باید هنگام نوشتن کد برای برنامه در سیستم های تعبیه شده با محدودیت منابع در نظر گرفته شوند. اما مهم ترین پارامتر بهینه سازی کد این است که خروجی آن باید با خروجی کد بهینه نشده یکسان باشد.

ملاحظات در بهینه سازی کد

با این حال، چند فاکتور وجود دارد که توسعه‌دهندگان باید قبل از بهینه‌سازی کدشان آن‌ها را در نظر بگیرند. یکی از بزرگترین چالش ها این است که بدانید چه چیزی و چه زمانی باید بهینه شود. به عنوان مثال، اگر مشکل توسعه یک برنامه آردوینو برای نظارت بر سطح آب در یک مخزن باشد، بهینه سازی غیر ضروری است زیرا به احتمال زیاد از محدودیت های آردوینو گذر نمی کنید. علاوه بر این، فرآیند بهینه سازی کد هزینه و زمان زیادی نیاز دارد. بنابراین، شما باید کدی را که واقعاً به بهینه سازی نیاز دارد، بهینه کنید.

با این حال، اگر راه حل های دیگری مانند استفاده از یک آردوینو بزرگتر امکان پذیر نیست، می توانید بهینه سازی کد را به عنوان یک گزینه برای پروژه خود در نظر بگیرید. در این مقاله، نویسنده بر روی تکنیک های بهینه سازی کد در آردوینو Uno محبوب تمرکز خواهد کرد. روش هایی که در اینجا به آن می پردازیم را می توان برای سایر بردهای آردوینو نیز اعمال کرد.

چرا بهینه سازی کد مهم است

در مقاله قبلی یاد گرفتیم که هنگام اجرای برنامه آردوینو مقداری داده ایجاد می شود. این داده ها مربوط به فراخوانی تابع و روال وقفه است. هنگامی که طرح یا داده های تولید شده به فضای بیشتری نسبت به اندازه تعیین شده نیاز دارند، ممکن است مشکلات ایجاد شوند. هنگامی که این اتفاق می افتد، برنامه آردوینو ممکن است به روش های مختلف شکست بخورد. از این رو، نیاز به بهینه سازی کد و اجتناب از موقعیتی است که در نمودار (b) زیر نشان داده شده است.

یک سلول حافظه در آردوینو

همانطور که قبلا در این مقاله اشاره کردیم، بهینه سازی کد، کد سریع تری تولید می کند. این نتیجه نه تنها برای انجام محاسبات بلکه برای اجرای عملیات ورودی-خروجی نیز ضروری است. به عنوان مثال، این عملیات ممکن است شامل برقراری ارتباط با حسگرهای خارجی یا صرفاً به روز رسانی وضعیت پین های آنالوگ و دیجیتال باشد. برای برنامه‌هایی که نیاز به کنترل دقیق‌تری دارند یا برنامه‌هایی که به حلقه‌های بازخورد سریع‌تر نیاز دارند، مانند سیستم‌های کنترل خودکار، بهینه سازی کد ضروری است.

به طور خلاصه، مزایای بهینه سازی کد آردوینو شما عبارتند از:

  • کد سریعتر اجرا می شود
  • خوانایی کد بهتر
  • نگهداری کد
  • مصرف حافظه کمتر

تکنیک های بهینه سازی کد در آردوینو

نمودار زیر تکنیک های مختلفی را نشان می دهد که توسعه دهندگان برای بهینه سازی کد خود در آردوینو استفاده می کنند. برخی از تکنیک ها مستقل از زبان هستند و برخی برای کار کردن فقط روی آردوینو طراحی شده اند.

تکنیک های بهینه سازی کد در آردوینو

حذف کد غیر ضروری

کد غیر ضروری به هر تابع، کتابخانه یا متغیرهای استفاده نشده ای اشاره دارد که ممکن است در کدخود گنجانده باشید. با وجود اینکه از آنها در طرح استفاده نمی شوند، هنوز مقداری از فضای حافظه را اشغال می کنند. بنابراین، لازم است چنین کدهای مرده ای را در طرح خود حذف کنید تا مقداری حافظه آزاد شود. بررسی متغیرهای استفاده نشده در طرح شما ساده است. به عنوان مثال.

void setup() {
pinMode(LED_BUILTIN, OUTPUT);
int myPin = 10;
}

void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
delay(500);
}

به کد بالا دقت کنید، متغیر عدد صحیح int myPin ایجاد شده اما استفاده نشده است. این کار باعث ایجاد یک پیام هشدار در آردوینو IDE میشود. شما میتوانید این خط را حذف کنید.

C:\Users\Dell\Documents\Arduino\sketch_may28a\sketch_may28a.ino: In function 'void setup()': 
C:\Users\Dell\Documents\Arduino\sketch_may28a\sketch_may28a.ino:7:7: warning: unused variable 'myPin' [-Wunused-variable] 
int myPin = 10; 
^~~~~

استفاده از انواع داده های کوچکتر

در زمینه برنامه نویسی کامپیوتر، متغیر راهی برای نامگذاری و ذخیره یک مقدار در حافظه برای استفاده بعدی است. این شامل یک نوع، یک نام و به صورت اختیاری، یک مقدار اولیه است. در ابتدا، ما باید انواع داده ها را برای متغیرها در کد آردوینو درک کنیم.

نوع داده اندازه (bits) مقدار
bool 8 1 or 0
char 8 -128 تا 127
unsigned char, byte 8 0 تا 255
short, int 16 -327768 تا 32767
unsigned int, word 16 0 تا 65535
long 32 -2147483648 تا 2147483648
unsigned long 32 0 تا 4294967295
float, double 32 1.175e-38 تا 3.402e38

همانطور که در جدول بالا می بینیم، انواع داده های مختلف اندازه های متفاوتی دارند. بنابراین، همیشه باید از نوع داده ای استفاده کرد که کمترین دامنه را برای قرار دادن داده ها داشته باشد. همچنین داشتن یک ایده تقریبی از دامنه داده های خود مفید است زیرا استفاده از انواع داده های گسترده تر منجر به بالا رفتن حجم کد می شود.

استفاده از توابع به جای تکرار کد

ما می‌توانیم این اصل را با استفاده از مثالی از سیگنال کد SOS مورس نشان دهیم. سه نقطه نشان دهنده S است و O با سه خط تیره نشان داده می شود. بنابراین، SOS در کد مورس 3 نقطه، 3 خط تیره و 3 نقطه خواهد بود. در پیاده سازی ما، هر نقطه 250 میلی ثانیه و هر خط تیره 1000 میلی ثانیه طول دارد. این یک مثال عالی است زیرا کد مورس حاوی کدهایی است که در فواصل زمانی منظم تکرار می شوند.

همچنین اگر در مورد این مطلب سوالی داشتید در انتهای صفحه در قسمت نظرات بپرسید
void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
  digitalWrite(LED_BUILTIN, HIGH);
  delay(250);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250); 
  digitalWrite(LED_BUILTIN, HIGH);
  delay(250);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(250);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(250);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(250);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(250);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
  delay(3000);
}

این را می توان دوباره نوشت:

void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
  dot(); dot(); dot();
  dash(); dash(); dash();
  dot(); dot(); dot();
  delay(3000);
}

void dot()
{
  digitalWrite(LED_BUILTIN, HIGH);
  delay(250);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
}

void dash()
{
  digitalWrite(LED_BUILTIN, HIGH);
  delay(1000);
  digitalWrite(LED_BUILTIN, LOW);
  delay(250);
}

استفاده از توابع در این کد منجر به صرفه جویی در حدود 200 بایت در حافظه می شود. این عمدتا به این دلیل است که یک نمونه از تابع در حافظه ایجاد می شود. و هنگامی که تابع دوباره فراخوانی می شود، CPU دوباره کد را از محل خود در حافظه بارگذاری می کند، بدون اینکه نیازی به ایجاد مجدد متغیرها باشد.

استفاده از متغیر Local به جای متغیر Global

برای اینکه بتوانیم مزایای استفاده از متغیرهای محلی را درک کنیم، باید بدانیم که آردوینو چگونه با متغیرهای محلی Local و سراسری Global رفتار می کند. اما در ابتدا باید بدانیم اگر یک متغیر اگر قبل از توابع setup() و loop() ایجاد شده باشد، Global Variable نامیده می شود. در حالی که Local Variable متغیری است که می توانیم آن را حفظ کرده و درون یک تابع فراخوانی کنیم. پیشنهاد میکنم مقاله متغیر در آردوینو را بخوانید.

یک متغیر سراسری با هر تابعی در طرح قابل دسترسی است، در حالی که یک متغیر محلی فقط در آن تابع که تعریف شده است قابل مشاهده است. اثرات این متغیرها بر عملکرد برنامه خارج از محدوده این مقاله است.

HEAP در مقابل STACK

متغیرهای سراسری به heap تخصیص داده می شوند، در حالی که متغیرهای محلی به stack تخصیص داده می شوند. تفاوت بین این دو مکان در سرعت دسترسی یا به طور کلی تعداد دستورالعمل های ماشین مورد نیاز برای تکمیل یک کار است. به دلیل معماری های متفاوت این دو سیستم، stack زمان دسترسی بالاتری نسبت به heap دارد. در نتیجه، دسترسی به متغیرهای محلی سریعتر از متغیرهای سراسری است.

از آنجایی که متغیرهای سراسری قبل از هر تابعی اعلان می شوند، به این معنی است که آنها فقط یک بار تخصیص داده می شوند. این مقدار برای کل مدت برنامه شما حفظ می شود، در حالی که متغیرهای محلی زمانی تخصیص می یابند که برنامه شما یک تابع خاص را اجرا می کند. سپس زمانی که برنامه اجرای تابع را کامل کرد، این مقدار تخصیص داده می شود. این بدان معنی است که اندازه متغیرهای سراسری در سراسر برنامه شما ثابت می ماند، در حالی که ظرفیت متغیرهای محلی متفاوت است.

می توانیم از متغیرهای محلی برای بهینه سازی کد آردوینو استفاده کنیم زیرا سرعت دسترسی و اندازه حافظه مستقیماً بر عملکرد کلی آردوینو تأثیر می گذارد. امیدواریم در آینده تأثیر تخصیص حافظه پویا بر عملکرد کد آردوینو را بررسی کنیم.

F() STRINGS

چاپ تعداد زیادی رشته بر روی مانیتور سریال یا صفحه نمایش LCD، مقدار زیادی رم مصرف می کند. برای صرفه جویی در RAM گرانبها، چنین رشته هایی را می توان در حافظه فلش ذخیره کرد. برای رسیدن به این هدف، آردوینو از ماکرو F() استفاده می کند. این راه حل ساده و در عین حال قدرتمند، کامپایلر را مجبور می کند تا رشته محصور شده را در PROGMEM قرار دهد. در اینجا یک مثال است آورده ایم.

در حالت طبیعی ما کد را به این شکل مینویسیم: Serial.print("Optimizing Code");

ما می توانیم این کد را به صورت روبرو قرار دهیم: Serial.print(F("Optimizing Code"));

گنجاندن این ماکرو برای این دو کلمه می تواند تا 16 حافظه بایت ذخیره کند. با این حال، ماکرو F() فقط روی حروف رشته ای کار می کند.

انتقال داده های ثابت به PROGMEM

به طور کلی، آردوینو متغیرها را در SRAM ذخیره می کند. همانطور که قبلاً متوجه شدیم، اندازه این متغیرها می تواند در طول اجرای برنامه تغییر کند. برای جلوگیری از تمام شدن حافظه رم، باید داده هایی را که به این بلوک حافظه می رود کنترل کنیم. برای رسیدن به این هدف از کلمه کلیدی PROGMEM برای ذخیره داده ها در حافظه برنامه به جای RAM استفاده می کنیم. این کار به ویژه برای داده هایی که هرگز تغییر نمی کنند، مانند ثابت ها مفید است. اما اشکال این است که کمی کندتر است. اما ویژگی بزرگتر این است که ما RAM را ذخیره می کنیم. در اینجا نمونه ای از پیاده سازی PROGMEM آورده شده است.

قبل از اجرای PROGMEM:

const int16_t chars[] = {200, 101, 521, 24, 892, 3012, 100};

پس از اجرای PROGMEM:

const int16_t chars[] PROGMEM = {200, 101, 521, 24, 892, 3012, 100};

برای خواندن متغیر از حافظه برنامه از کد زیر استفاده می کنیم:

void ReadData() {
  unsigned int displayInt;
  
  for (byte k = 0; k < 7; k++) {
    displayChars = pgm_read_word_near(chars + k);
    Serial.println(displayChars);
  }
  Serial.println();
}

حلقه for در کد بالا فرض می کند که اندازه داده های متغیر خود را می دانید. با این حال، اگر این اطلاعات در دسترس نیست، می توانید حلقه for را با کد زیر جایگزین کنید:

for (byte k = 0; k < (sizeof(chars) / sizeof(chars[0])); k++) { 
displayInt = pgm_read_word_near(chars + k); 
Serial.println(displayInt); 
}

و مقادیر زیر را بدست می آوریم:

خواندن مقادیر از حافظه FLASH PROGMEM آردوینو

استفاده از RESERVE() برای رشته ها

روش دیگر برای بهینه سازی کد آردوینو، مدیریت حافظه رشته هایی است که اندازه آنها افزایش می یابد. برای انجام این کار، از تابع reserve() برای پیش‌تخصیص حافظه برای چنین رشته‌هایی استفاده می‌کنیم. اگر ما این را کنترل نکنیم، ممکن است خطاهایی که مربوط به تکه تکه شدن حافظه است رخ دهد که منجر به کاهش عملکرد می شود. در اینجا کد پیاده سازی تابع reserve() آمده است.

String string;
string.reserve(50);

برای استفاده صحیح از آن، متغیری از نوع String را اعلام می کنیم و سپس تعداد بایت های موجود در حافظه را برای ذخیره این رشته ارسال می کنیم.

تکنیک های پیشرفته

حالا برخی از تکنیک های پیشرفته را بررسی میکنیم.

دستکاری مستقیم پورت

ما می‌توانیم آردوینو را با استفاده از C خالص برنامه‌ریزی کنیم، زیرا نرم‌افزار مبتنی بر کامپایلر avr-gcc و سخت‌افزار مبتنی بر میکروکنترلرهای Atmel AVR است. هر پین روی میکروکنترلر از بیت رجیستر های روبرو تشکیل شده است: PINxn، DDxn، و PORTxn

با دستکاری مستقیم پورت آردوینو می توان دو پیشرفت اصلی را ایجاد کرد. اولا سرعت کنترل مستقیم پورت منجر به کنترل ورودی/خروجی بسیار سریع‌تر می‌شود و در نتیجه چند میکروثانیه صرفه‌جویی می‌شود. ثانیاً، کنترل مستقیم پورت میزان حافظه مورد استفاده کد شما را کاهش می دهد. به عنوان مثال، دو قطعه کد زیر یک برنامه چشمک زن ساده LED را پیاده سازی می کنند. اولی از توابع Arduino DigitalWrite() استفاده می کند، در حالی که دومی از کنترل مستقیم پورت در C استفاده می کند.

void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);
}
void setup() {
DDRB |= (1<<PD5);
}

void loop(){
PORTB |= (1<<PD5);
_delay_ms(1000);
PORTB &= !(1<<PD5);
_delay_ms(1000);
}

اندازه برنامه دوم 488 بایت است، در مقایسه با برنامه digitalWrite() که حدود 924 بایت حافظه مصرف می کند. با این حال، بحث های زیادی در مورد اینکه آیا دستکاری پورت مستقیم در C خالص به عنوان روشی برای بهینه سازی کد برای آردوینو محسوب می شود، وجود دارد. نرم افزار آردوینو برای ساده سازی فرآیند برنامه نویسی میکروکنترلر نوشته شده است. کتابخانه ها باعث میشوند با این زبان C سطح پایین کار نکنیم و از توابع آماده استفاده کنیم. بنابراین، بازگشت به C خالص می تواند به عنوان نقض هدف استفاده از آردوینو در نظر گرفته شود. نویسنده فقط این تکنیک را برای نشان دادن جنبه های حافظه C خالص در مقایسه با سبک آردوینو ذکر کرده است.

حذف بوت لودر

پلتفرم آردوینو فرآیند انتقال کد شما به میکروکنترلر را ساده می کند. این فرآیند با استفاده از سیستم عامل به جای پروگرامر خارجی استفاده میشود. این سیستم عامل بوت لودر نامیده می شود و حدود 2000 بایت حافظه فلش نیاز دارد. وقتی همه گزینه های بهینه سازی تمام شد، می توانید بوت لودر را نیز دور بزنید. شما می توانید از آردوینو به عنوان ISP استفاده کنید یا از یک پروگرامر خارجی برای آپلود کد خود بدون استفاده از بوت لودر استفاده کنید. هنگامی که میکروکنترلر آردوینو به این صورت برنامه ریزی می شود، فضای بیشتری برای کد شما باقی می گذارد.

امیدواریم این مقاله به شما ایده هایی در مورد نحوه بهینه سازی کد آردوینو داده باشد! اگر سوالی دارید یا می خواهید چیزی اضافه کنید، در زیر نظر خود را بنویسید.

5/5 - (2 امتیاز)

برای دریافت مطالب جدید کانال تلگرام یا پیج اینستاگرام آیرنکس را دنبال کنید.
تصویر از محمد رحیمی

محمد رحیمی

محمد رحیمی هستم. سعی میکنم در آیرنکس مطالب مفید قرار بدهم. سوالات مربوط به این مطلب را در قسمت نظرات همین مطلب اعلام کنید. سعی میکنم در اسرع وقت به نظرات شما پاسخ بدهم.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد.