شاید تا به حال برای شما هم پیش آمده باشد که یک کتابخانه خیلی خوب و قدرتمند پیدا کرده باشید، اما آن را برای آردوینو و با زبان آن ++C نوشته شده باشند. در حالی که شما برنامه نویس میکروکنترلر AVR با زبان C هستید. در این مقاله کاربردی، سعی میکنیم تا بهصورت عملی کدهای کتابخانهای را از آردوینو به AVR تبدیل کنیم.
با این آموزش همراه باشید.
قبل از اینکه دست به کار بشید و کتابخانه آردوینو رو تبدیل کنید، اول چند چیز مهم رو باید بررسی کنید.
- آیا جستجو کرده اید که کسی قبل از شما این کتابخانه را برای AVR نوشته است یا نه؟!
- آیا به جز این، کتابخانه تمیز تر، بهتر و با امکانات بیشتری نیز برای این کار نوشته شده است؟
- آیا کتابخانه مورد نظر آن قدر ارزش دارد که بر روی آن وقت بگذارید و آن را تبدیل کنید؟ آیا خودتان نمیتوانید آن را از اول بنویسید؟
- آیا قبل از تبدیل، آن را بر روی آردوینو تست کرده ام تا از صحت عملکرد آن مطمئن شوم؟!
- و…
چند نکته مهم!
هرچند نوشتن مجدد کتابخانهای که قبلاً توسط دیگران نوشته شده، ممکن است کار بسیار وقت گیری باشد، اما خیلی اوقات اگر کدی را خودتان از اول بنویسید، بعداً در عیب یابی و پشتیبانی آن بسیار راحتتر خواهید بود. همین طور هیچ وقت نمیتوان برای تبدیل کدها از یک زبان یا حالتی به حالت دیگر یک الگوی ثابت ارائه داد. چون باید دقیقاً هدف و منظور برنامه نویس را از کدهایی که نوشته است بدانید. شاید برنامه نویس با توجه به محدودیتها و یا امکاناتی دستگاه مورد نظرش داشته است، برنامهای را توسعه داده باشد، و یا با چندین بار اجرا و تست گرفتن، به کد مورد نظر رسیده باشد، در حالی که همان کدها بر روی دستگاه شما باید به نحو دیگری پیاده سازی شوند.
پس اگر کدی را که برای زبان و دستگاه مورد نظر شما نوشته شده را پیدا نکردید، اما همان کتابخانه را برای دستگاه دیگری پیدا کردید، تا حد امکان سعی کنید به جای تبدیل، از آنها الگو گرفته و خودتان آن را بازنویسی کنید.
این نکته را نیز در ذهن داشته باشید که شما میتوانید به کمک چارچوب برنامه نویسی (Framework) آردوینو برای بسیاری از میکروکنترلر های AVR و ARM نیز برنامه نویسی کنید و حتماً هم نیازی به تبدیل کدها نخواهید داشت. توصیه میکنم مقاله “آماده سازی محیط VSCODE برای برنامه نویسی AVR” را از دست ندهید.
شما به عنوان کسی که میخواهد کتابخانه ++C آردوینو را به C تبدیل کند، باید با ساختار (Structure) و قواعد نوشتاری (Syntax) هر دو زبان به اندازه کافی آشنا باشید.
تعدادی از آموزش های مرتبط:
آموزش میکروبلیز و زبان برنامهنویسی C
ویرایشگر حرفه ای ویژوال استودیو کد با طعم آردوینو
تفاوت های زبان C با ++C
خوب مسلماً بررسی کامل ساختار هر دو زبان بحث طولانی دارد و در یک مطلب نمیگنجد. اما شما باید به تفاوتهای این دو زبان نیز توجه داشته باشید. برای مثال، زبان CPP آبجکتیو است. به همین خاطر متغیرها و کلاسهایی با طول متغیر مثل String و… را میتوان تعریف کرد. در حالی که در زبان C، مدیریت حافظه معمولاً بهصورت استاتیک است و شما باید طول آرایهها، رشتههای متنی و دیگر خواص داینامیک CPP را تعیین کنید.
برخی از مهمترین امکاناتی که در زبان برنامه نویسی ++C اضافه شدهاند:
- کلاس – class
- قالب یا تمپلیت – template
- توابع مجازی – virtual function
- operator overloading
- مدیریت خطاها – Exception Handling
- ارث بری و ارث بری چندگانه – multiple inheritances
- افزایش قابلیت های Type Checking
- اختصاص حافظه داینامیک – Dynamic memory
همان طور که میدانید، زبان ++C بر پایهی C است که گرامر آن توسعه یافته است و شیء گرایی و امکانات دیگر به آن اضافه شده است. یعنی کدهای C در محیط ++C نیز اجرا خواهند شد.
تبدیل کتابخانه آردوینو به AVR یک مبحث بسیار طولانی است و میتواند حالتهای بسیار زیادی هم داشته باشد. اما خوب! نگران نباشید. با یه مثال خیلی ساده شروع میکنیم. بزن بریم!
بررسی کتابخانه هدف – مورس
به عنوان نمونه، میخواهیم یک کتابخانه ساده مثل Morse آردوینو را برای AVR تبدیل کنیم. این کتابخانه شامل یک کلاس با نام Morse است و دو عضو با نامهای dot و dash دارد. با صدا زدن این توابع، بر روی پین مورد نظر کدهای مورس را ایجاد میکند. ما ابتدا این کلاس را از حالت شیء گرا خارج کرده و در خود آردوینو توابع جدید را تست میکنیم، سپس آن را برای AVR باز نویسی میکنیم.
بررسی فایل ها
کتابخانه مورس و تمام فایلهای مورد نیاز در این آموزش، در انتها قابل دانلود هستند. همانطور که میدانید هر کتابخانه چه در زبان C و چه در زبان ++C یک فایل سرآیند (Header: هدر) و یک فایل منبع (Source) میباشد. پسوند فایل منبع در زبان سی پلاس پلاس، cpp. و در زبان سی، c. میباشد. پسوند فایل سرآیند در هر دو زبان معمولا h. است. اما گاهی اوقات در زبان CPP، فایل هدر ممکن است بدون پسوند و یا با پسوند hpp. نیز ظاهر شود.
فایل Morse.h
/* Morse.h - Library for flashing Morse code. Created by David A. Mellis, November 2, 2007. Released into the public domain. */ #ifndef Morse_h #define Morse_h #include "Arduino.h" class Morse { public: Morse(int pin); void dot(); void dash(); private: int _pin; }; #endif
فایل Morse.cpp
/* Morse.cpp - Library for flashing Morse code. Created by David A. Mellis, November 2, 2007. Released into the public domain. */ #include "Arduino.h" #include "Morse.h" Morse::Morse(int pin) { pinMode(pin, OUTPUT); _pin = pin; } void Morse::dot() { digitalWrite(_pin, HIGH); delay(250); digitalWrite(_pin, LOW); delay(250); } void Morse::dash() { digitalWrite(_pin, HIGH); delay(1000); digitalWrite(_pin, LOW); delay(250); }
نحوه استفاده از کتابخانه در آردوینو
برای استفاده از این کتابخانه در برنامه اصلی، ابتدا باید فایل سرآیند آن را به برنامه اضافه کرده و سپس یک شیء از روی آن بسازیم:
#include "Morse.h" Morse morse(13);
با توجه به وجود متد سازنده (structure) در کلاس، هنگام ساختن شی جدید حتماً باید پارامترهای خواسته شده را نیز به ورودی متد بدهید. در اینجا برای ساخت کتابخانه، از ما شماره پینی که میخواهیم کد مورس روی آن اجرا شود را میخواهد. به همین خاطر عدد 13 را در ورودی تابع بالا وارد میکنیم.
حال، در هر جای برنامه برای صدا زدن متد مورد نظر از شیء ساخته شده، به شکل زیر عمل میکنیم:
morse.dot(); morse.dash();
به همین ترتیب برنامه اصلی (main) ما به شکل زیر نوشته میشود.
#include "morse.h" Morse morse(13); void setup() { } void loop() { morse.dot(); morse.dot(); morse.dot(); morse.dash(); morse.dash(); morse.dash(); morse.dot(); morse.dot(); morse.dot(); delay(3000); }
مرحله اول -خارج کردن متد ها ازکلاس
تغییرات فایل منبع
ابتدا از کل پروژه یک کپی بگیرید و تغییرات را روی پروژه اصلی انجام ندهید! از فایل سورس برای تبدیل به زبان C شروع میکنیم. فایل را برای ویرایش باز کنید. از آن جایی که توابع، دیگر وابسته به کلاس نیستند، میبایست نام کلاس (در اینجا::Mors) از ابتدای متدها حذف شوند. اما بهتر است به جای حذف، تنها عبارت :: را در نام تمام توابع با _ جایگزین کنید و نام متد جدید را نیز به حروف کوچک تغییر دهید. زیرا با حذف نام کلاس از ابتدای برخی توابع، ممکن است تشابه اسمی میان نام جدید و نام توابع دیگر کلاس ها بوجود آید.
برای مثال:
void Morse::dot() { digitalWrite(_pin, HIGH); delay(250); digitalWrite(_pin, LOW); delay(250); }
به شکل زیر اصلاح میشود:
void morse_dot() { digitalWrite(_pin, HIGH); delay(250); digitalWrite(_pin, LOW); delay(250); }
این کار به شما کمک میکند تا بعداً بدانید تابع استفاده شده در برنامه، مربوط به کدام کتابخانه است. برای مثال، اگر به توابع کتابخانه lcd در کدویژن نیز دقت کنید، همین فرمت را دارند:
lcd_clear(); lcd_ready() ...
متد سازنده کلاس
متد سازنده یا همان Constructor وظیفه مقدار دهی اولیه اشیاء را هنگام ساخته شدن دارد. این متد هم نام کلاس خودش میباشد و از لحاظ سینتکس، مانند یک متد محسوب میشود. اما هیچگونه مقدار برگشتی مشخصی ندارد. یعنی هنگام ساختن آن هیچ مقدار برگشتی (حتی void) را برای آن تعریف نمیکنیم. همهی کلاسها نیز لزوماً متد سازنده ندارند.
از آنجایی که متد سازنده، هم نام خود کلاس است، در مرحله قبل نام آن به morse_morse تبدیل میشود. چون در زبان C کلاسی وجود ندارد، متد سازندهای هم وجود ندارد. اما کتابخانه برای شروع کار خود، نیاز به مقدار دهی و تنظیمات اولیه دارد. بنابراین نام متد سازنده (که به شکل morse_morse در آمده است) را اصلاح میکنیم و یکی از نامهای begin یا init را بسته به نوع کاربرد، به جای morse دوم آن قرار میدهیم. همچنین نوع خروجی تابع را نیز مشخص میکنیم.
void morse_init(int pin) { pinMode(pin, OUTPUT); _pin = pin; }
انتقال متغیر ها
در فایل هدر زبان C، نباید هیچ متغیری تعریف شود. بنابراین تمام متغیرهایی که درون فایل هدر CPP تعریف شدهاند را حذف و به فایل سورس منتقل کنید. در این مثال ;int _pin را جابجا میکنیم.
در نهایت فایل سورس ما به شکل زیر اصلاح میشود:
/* Morse.c - Library for flashing Morse code. Created by David A. Mellis, November 2, 2007. Editted by Digi boy (blogfa.belec.ir) Released into the public domain. */ #include "Arduino.h" #include "Morse.h" int _pin; void morse_init(int pin) { pinMode(pin, OUTPUT); _pin = pin; } void morse_dot() { digitalWrite(_pin, HIGH); delay(250); digitalWrite(_pin, LOW); delay(250); } void morse_dash() { digitalWrite(_pin, HIGH); delay(1000); digitalWrite(_pin, LOW); delay(250); }
نکته: اگر درون خود توابع نیز، توابع دیگری از داخل کلاس صدا زده شدهاند، نام آنها را مطابق نام جدیدشان اصلاح کنید.
تغییرات فایل سرآیند
قاعدتاً چون ما در زبان C چیزی به اسم کلاس و شیء نداریم، پس عبارت } class Morse را به همراه کروشه باز و بسته ;{ از ابتدا و انتهای فایل هدر پاک میکنیم. همچنین توابع عمومی و خصوصی معنایی ندارند. پس عبارات :public و :private را نیز حذف میکنیم. توجه داشته باشید که هدف برنامه نویس از خصوصی کردن توابع، این است که شما مستقیماً آن را در برنامه صدا نزنید! پس حواستان به این موضوع نیز باشد. اگر کتابخانه شما دارای متد خصوصی است، نیازی نیست امضای آن تابع را در فایل هدر وارد کنید. با این کار توابع شما تنها درون فایل سورس قابل استفاده هستند.
سپس تغییراتی که در مرحله قبل روی فایل سورس دادیم، بر روی فایل سرآیند نیز اعمال میکنیم. در فایل سورس، ما یک متد شروع کننده ساختیم، به ابتدای تمامی توابع _morse اضافه کردیم و همه آنها را به حروف کوچک تغییر دادیم. همچنین تمام متغیرها را نیز به فایل سورس منتقل کردهایم.
نتیجه:
/* Morse.h - Library for flashing Morse code. Created by David A. Mellis, November 2, 2007. Editted by Digi boy (blogfa.belec.ir) Released into the public domain. */ #ifndef Morse_h #define Morse_h #include "Arduino.h" void morse_init(int pin); void morse_dot(); void morse_dash(); #endif
حال برای استفاده از توابع در برنامه اصلی، دیگر نیازی نیست تا یک شیء از روی آن بسازیم، در عوض باید کلاس را راه اندازی اولیه کنیم:
morse_init(13);
برنامه اصلی، بدون شیء گرایی به شکل زیر در میآید:
#include "Morse.h" void setup() { morse_init(13); } void loop() { morse_dot(); morse_dot(); morse_dot(); morse_dash(); morse_dash(); morse_dash(); morse_dot(); morse_dot(); morse_dot(); delay(۳۰۰۰); }
خوب! تا اینجا توانستیم متدها را از داخل کلاس خارج کنیم. همیشه اول کتابخانه آردوینو را از حالت شیء گرایی خارج کنید و درون خود آردوینو توابع تبدیل شده را تست کنید، تا قبل از تبدیل آنها به AVR، از صحت عملکرد کدهای جدید اطمینان حاصل کنید.
مرحله دوم – تبدیل توابع سطح بالای آردوینو به AVR
همان طور که می دانید، آردوینو یک زبان مالتی پلت فرم است که به کمک آن میتوانید برای انواع بردها و میکروکنترلر ها (مثل AVR, ARM, ESP8266 و…) برنامه بنویسید. کدی که شما مینویسید، تقریباً برای همهی بردها یکسان است. برای مثال، پینها خیلی راحت و با یک شماره در دسترس شما هستند و یا استفاده از یک تابع، میتوانید پورت سریال را پیکربندی کنید. در حالی که هر برد و میکروکنترلری، رجیستر های مخصوص خود را دارد. در واقع، آردوینو هنگام کامپایل کردن به طور خودکار مطابق با نوع بردی که انتخاب کردهاید، رجیستر های آن را قرار میدهد.
شما برای تبدیل کدهای سطح بالای آردوینو به رجیستر های AVR در زبان C، دو راه دارید، که راه دوم کمی منطقی تر است:
- مطالعه کردن سورس آردوینو و استخراج رجیستر ها برای برد مورد نظر
- درک کردن عملی که تابع آردوینو انجام میدهد (بدون نیاز به خواندن سورس) و بازنویسی آن برای AVR
بازنویسی فایل منبع
اگر شما برنامه نویس AVR باشید، خیلی راحت میتوانید منظور آردوینو را از توابع مختلف را متوجه شده و به راحتی آن را برای یک تراشه خاص (مثلاً ATmega32) باز نویسی کنید. برای مثال، برای تغییر جهت ورودی پینها در آردوینو از تابع pinMode استفاده میشود که معادل همان DDRx در AVR است. همچنین برای صفر و یک کردن یک پایه نیز، در آردوینو از digitalWrite استفاده میشود که در AVR معادل همان رجیستر PORTx میباشد. برای اینکه این رجیستر ها را تنها یک بار در برنامه تعریف کنیم و در جاهای دیگری از برنامه از آنها استفاده کنیم، از دستورات پیش پردازنده define استفاده میکنیم. برای صفر و یک کردن یک بیت از یک بایت (رجیستر مورد نظر) نیز از عملیات بیتی کمک میگیریم. همچنین توابع تأخیر در فایل سرآیند util/delay.h وجود دارند، اما کمی در فرم نوشتاری فرق دارند. (در این آموزش از کامپایلر GCC استفاده شده است. در کدویژن نیز کدها کمی تفاوت دارند.)
بنابراین فایل سورس ما برای AVR به شکل زیر تبدیل می شود:
/* Morse.c - Library for flashing Morse code. Created by David A. Mellis, November 2, 2007. Editted by Digi boy (blogfa.belec.ir) Released into the public domain. */ #include <avr/io.h> #include <util/delay.h> #include "morse.h" #define _PORTx PORTB #define _DDRx DDRB uint8_t mask = 0b00000001; void morse_init() { _DDRx |= mask; } void morse_dot() { _PORTx |= mask; //High _delay_ms(250); _PORTx &= ~ mask; //Low _delay_ms(250); } void morse_dash() { _PORTx |= mask; //High _delay_ms(1000); _PORTx &= ~ mask; //Low _delay_ms(250); }
ترفند کمکی
اگر دقت کنید، بعضی از توابع تنها در نوشتار اختلاف کوچکی دارند. برای مثال تابع delay در آردوینو، معادل همان _delay_ms در کامپایلر GCC یا delay_ms در کامپایلر کدویژن است. شما میتوانید پس از اضافه کردن کتابخانه مورد نظر، بدون اینکه تغییری در کد ایجاد کنید، به کمک دستورات پیش پردازنده به کامپایلر بفهمانید که تابع صدا زده شده معادل چیست!
#define delay(x) _delay_ms(x)
بازنویسی فایل هدر
در این پروژه، فایل هدر کمترین تغییر را دارد. تنها فایل سرآیند arduino.h از ابتدای خطوط حذف شده است، همچنین تابع init نیز دیگر به ورودی احتیاج ندارد. بنابراین امضای آن تغییر میکند.
/* Morse.h - Library for flashing Morse code. Created by David A. Mellis, November 2, 2007. Editted by Digi boy (blogfa.belec.ir) Released into the public domain. */ #ifndef Morse_h #define Morse_h void morse_init(); void morse_dot(); void morse_dash(); #endif
برنامه اصلی در AVR
تابع Loop در واقع همان while(1) در AVR است و تابع setup نیز، همان قسمت ابتدایی تابع main تا قبل از while(1) است.
بنابراین فرم کلی یک برنامه در AVR بهصورت زیر است:
#include <avr/io.h> int main() { //setup while (1) { //Loop } return 0; }
برنامه اصلی مورس ما که در فایل main.c قرار دارد نیز، به شکل زیر در میآید:
#include <avr/io.h> #include <util/delay.h> #include "morse.h" int main() { morse_init(); while(1) { morse_dot(); morse_dot(); morse_dot(); morse_dash(); morse_dash(); morse_dash(); morse_dot(); morse_dot(); morse_dot(); _delay_ms(3000); } }