آموزش ایجاد یک فرستنده RC با آردوینو
در این مقاله، یاد می گیریم که چگونه یک فرستنده ی RC را ایجاد کنیم. خیلی وقت ها، برای پروژه ای که ایجاد می کنیم، نیاز داریم از کنترل وایرلس استفاده کنیم. به همین خاطر، ما یک رادیو کنترلر(radio controller) چند منظوره ایجاد کرده ایم که می تواند برای هر چیزی مورد استفاده قرار گیرد.
نکته: RC مخفف رادیو کنترلر(radio controller) است.
هشدار: این مقاله صرفاً تئوری است و توسط مترجم تست نشده است؛ و امکان دارد از نظر تامین قطعات یا بورد مدار چاپی(PCB)، یا موجود بودن ورژن های v1 و v2 و v3 از آردوینو پرو مینی با مشکل مواجه شویم.
بررسی اجمالی
حالا ما می توانیم به صورت وایرلس، هر پروژه ی آردوینویی را با برخی تنظیم ها در سمت گیرنده(receiver) کنترل کنیم. همچنین می توانیم از این فرستنده بعنوان فرستنده ی RC تجاری، برای کنترل کردن اسباب بازی های RC، ماشین ها، پهپادها و غیره استفاده کنیم. برای این کار، تنها به یک گیرنده ی ساده ی آردوینو نیاز داریم که سیگنال های مناسب را برای کنترل دستگاه های RC مذکور، تولید می کند.
ما در قالب چند مثال برای کنترل یک ربات ماشینیِ آردوینو، توضیح خواهیم داد که چگونه همه چیز در این ویدئو، کار می کند. برای اطلاعات بیشتر، می توانید به مقاله ی آموزش کنترل موتور براشلس با آردوینو با استفاده از ESC و سروو موتور، مراجعه کنید.
ارتباط رادیویی این کنترلر بر اساس ماژول فرستنده و گیرنده ی NRF24L01 است؛ که اگر با یک آنتن تقویت شود، می تواند حداکثر تا 700 متر در فضای باز، برد داشته باشد. این قطعه 14 کانال دارد، که 6 کانال آن آنالوگ هستند و 8 کانال آن دیجیتال هستند.
این دستگاه از موارد زیر برخوردار است:
- دو جوی استیک(joystick)
- دو پتانسیومتر
- دو کلید فشاری
- دو سوئیچ دو حالته(toggle switches)
- شش دکمه فشاری
- یک سامانه اندازه گیری داخلی(IMU) که شامل یک شتاب سنج و یک ژیروسکوپ است که می تواند برای کنترل برخی چیزها، فقط با حرکت دادن یا کج کردن کنترلر استفاده شود.
نمودار مدار فرستنده ی RC با آردوینو
برای شروع، اجازه دهید به نقشه ی مدار نگاهی بیاندازیم. مغز این کنترلر RC ،یک عدد آردوینوی Pro Mini است که از دو باتری LiPo (لیتیم پلیمر) استفاده می کند که ولتاژی حدود 7.4 ولت را ارائه می دهند. ما می توانیم آنها را مستقیماً به پین RAW از آردوینو Pro Mini متصل کنیم که از یک رگولاتور ولتاژ برخوردار است که ولتاژ مذکور را به 5 ولت کاهش می دهد. توجه کنید که دو نسخه از آردوینوی Pro Mini وجود دارد؛ نسخه ای که ما از آن استفاده می کنیم، با ولتاژ 5 ولت کار می کند و نسخه ی دیگری با ولتاژ 3.3 ولت کار می کند.
از طرف دیگر، ماژول NRF24L01 به شدت به ولتاژ 3.3 ولت نیاز دارد، و توصیه می شود که از یک منبع (برق) اختصاصی استفاده کند. بنابراین ما نیاز داریم از یک رگولاتور ولتاژ 3.3 ولت استفاده کنیم که به باتری ها متصل می شود و برق 7.4 ولت را به 3.3 ولت تبدیل می کند.
همچنین ما نیاز داریم تا از یک خازن بای پَس(decoupling capacitor) درست پس از ماژول مورد نظر استفاده کنیم؛ تا ولتاژ پایدارتر شود؛ بدین ترتیب، ارتباطات رادیویی بسیار پایدارتر خواهد شد. ماژول NRF24L01 با پروتکل SPI ، با آردوینو ارتباط برقرار می کند؛ اما شتاب سنج MPU6050 و ماژول ژیروسکوپ از پروتکل I2C استفاده می کنند.
قطعات مورد نیاز برای این آموزش به شرح زیر هستند:
1. یک عدد ماژول فرستنده و گیرنده ی NRF24L01
2. یک عدد ماژول NRF24L01 + PA + LNA
3. دو عدد پتانسیومتر
4. یک عدد سروو موتور
5. دکمه فشاری؟
6. دو عدد جوی استیک
نکته: به جوی استیک ها، یک بورد اتصال(breakout board) نیز متصل است؛ بنابراین ما نیاز داریم بدنه ی جوی استیک را از آن با هویه جدا کنیم.
7. جوی استیک بدون بورد اتصال(breakout board)
8. آردوینو Pro Mini
نکته: برای بورد های زیر، به نسخه PCB ورژن 2 یا 3 نیاز داریم:
9. آردوینو Pro Mini، ما از بورد زیر استفاده کرده ایم: ؟؟؟
10. رگولاتور ولتاژ HT7333 3.3v، به صورت PCB ورژن 1 و PCB ورژن 2
11. رگولاتور ولتاژ AMS1117 3.3v : به صورت PCB ورژن 3
طراحی بورد مدار چاپی(PCB)
ما در حقیقت مجبور شدیم تا از تمام پین های آنالوگ و دیجیتال بورد آردوینو Pro Mini استفاده کنیم. بنابراین، اکنون اگر ما سعی کنیم همه چیز را با استفاده از سیم های جامپر به هم متصل کنیم، همه چیز آشفته خواهد شد. بنابراین، ما یک بورد مدار چاپی(PCB) با استفاده از نرم افزار طراحی رایگان و آنلاین EasyEDA ایجاد کرده ایم.
در اینجا ارگونومی کنترلر را در نظر گرفته ایم و آن را طوری طراحی کرده ایم که به آسانی با دو دست بتوانیم آن را نگه داریم؛ به طوری که تمام دکمه ها در محدوده ی انگشت ها هستند. ما گوشه های این دسته را گِرد کرده ایم و تعدادی حفره ی 3 میلیمتری در آن ایجاد کرده ایم تا بتوانیم بعداً (به وسیله ی آنها) برد مدار چاپی(PCB) را روی چیز دیگری نصب (سوار) کنیم. ما پین های برنامه نویسی آردوینو Pro Mini را در سمت بالای کنترلر قرار داده ایم؛ از این رو، وقتی که بخواهیم این بورد آردوینو را برنامه نویسی کنیم، به آسانی می توانیم به آنها، دسترسی داشته باشیم.
همچنین ما می توانیم از پین های RX و TX در آردوینو(ی مذکور) برای دکمه های جوی استیک استفاده کنیم. اما این دو سیم نیاز دارند تا وقتی که داریم اسکچِ(یعنی کدهای) خود را در بورد آردوینو آپلود می کنیم، از اتصال به همه چیز قطع(disconnected) شوند. بنابراین این سیم ها، در دو پین قطع شده اند و با استفاده از سیم های جامپر، به راحتی می توانیم آنها را متصل کنیم.
نکته: مطمئن شوید که از ورژن مناسب آردوینو Pro Mini برای تطابق داشتن با برد مدار چاپی(PCB) مربوط به آن استفاده می کنید. در زیر، یک مقایسه ی تصویری، بین سه نسخه ی متفاوت آردوینو Pro Mini، بسته به آردوینو و رگولاتر ولتاژ آن انجام شده است.
برای مشاهده ی این برد مدار چاپی، اینجا کلیک کنید. این لینک، سه نسخه ی مختلف مذکور را در سه تب مختلف، باز می کند؛ و در آن می توانید هرکدام را که خواستید باز کنید. وقتی که ما این طراحی را به پایان رساندیم، فایل های گربر(Gerber file) مورد نیاز برای تولید بورد مدار چاپی(PCB) تولید کردیم. این فایل های گربر را می توانید از لینک های زیر دانلود کنید:
سپس باید این فایل را به سایت های چاپ مدار تحویل دهیم تا آن را برای ما چاپ کنند.
در اینجا به سادگی می توانیم فایل گربر(Gerber) را درگ و درآپ کنیم و وقتی که آپلود شد، برد مدار چاپی(PCB) خود را در مشاهده گر گربر مشاهده کنیم. اگر همه چیز به خوبی پیش برود، آنگاه می توانیم ویژگی هایی(properties) که می خواهیم را برای PCB خود انتخاب کنیم. در این گام، رنگ PCB را مشخص می کنیم؛ که ما رنگ مشکی را برای آن انتخاب کرده ایم. اکنون می توانیم با یک قیمت مناسب، PCB خود را سفارش دهیم.
در تصویر زیر، نتیجه را مشاهده می کنید. کیفیت این برد مدار چاپی(PCB) عالی است و همه چیز همان گونه که طراحی کرده بودیم انجام شده است.
مونتاژ کردن برد مدار چاپی(PCB)
بسیار خب؛ اکنون ما می توانیم قطعات بورد مدار چاپی(PCB) را مونتاژ کنیم. ما در ابتدا به لحیم کردن پین هدرهای آردوینو Pro Mini پرداخته ایم. یک راه ساده و خوب برای انجام این کار، این است که آنها را در یک برد بورد قرار دهیم تا به هنگام لحیم کردن، محکم در جای خود قرار گرفته باشند.
همچنین بورد آردوینو Pro Mini از پین هایی در اطرف خود برخوردار است، اما توجه کنید که موقعیت این پین ها، ممکن است بسته به سازنده ی این بورد، مختلف باشند.
برای بورد ما،(در ورژن 1) در هر طرف، به 5 پین نیاز داریم. اما یک پین GND را خالی رها می کنیم، چونکه ما از ناحیه ی آن، در در زیر برد مدار چاپی(PCB) برای اجرای برخی طرح ها استفاده کرده ایم.
نکته: در این مقاله، از کدام نسخه ی آردوینو پرو مینی استفاده شده است؟ جواب: در لیست قطعات، اطلاعات قطعات توضیح داده شده است.
ما آردوینو پرو مینی(Arduino Pro Mini)، را مستقیماً به برد مدار چاپی(PCB) متصل کرده ایم و طول اضافی هدرها را برش داده ایم. درست در کنار آردوینو پرو مینی، ماژول شتاب سنج MPU6050 و ژیروسکوپ قرار دارند.
سپس رگولاتور ولتاژ 3.3 ولت و یک خازن را در کنار آن لحیم کرده ایم. و یک خازن دیکر نیز در نزدیکی ماژول NRF24L01 لحیم کرده ایم. این ماژول، سه ورژن مختلف دارد و ما می توانیم از هریک از آنها استفاده کنیم.
در ادامه، به پین های برنامه نویسی آردوینوی خود و پین های RX و TX، و پین های منبع تغذیه و کلید پاور(power) پرداخته ایم. سپس برای لحیم کردن پتانسیومترها به برد مدار چاپی، می بایست با استفاده از پین های هدرها، پین های پتانسیومترها را توسعه دهیم. این موضوع در تصویر زیر نشان داده شده است:
یادآور می شویم که ما قبلاً در اینجا، طول اضافی برآمدگی ها(knobs) را برش داده ایم، بنابراین به درستی می توانیم برخی از کلاهک ها(caps) را در (میان) آنها جا کنیم. اما پتانسیومترها را کمی بعد به PCB لحیم خواهیم کرد. سپس ما دو سوئیچ را در بورد قرار داده ایم و آنها را لحیم کر دهیم و دو جوی استیک را نیز در جای خود لحیم کرده ایم.
در نهایت، باید چهار دکمه ی فشاری(push buttons) را لحیم کنیم؛ اما آنها ارتفاع مناسبی ندارند. بنابراین دوباره از هدرها برای توسعه ی پین های دکمه های فشاری استفاده کرده ایم. کار تمام شد! بورد مدار چاپی(PCB) ما آماده است. اکنون می خواهیم برای آن یک کاور ایجاد کنیم. به دلیل اینکه دوست داریم بورد ما ظاهر خوبی داشته باشد، تصمیم گرفته ایم از یک قطعه اکریلیک شفاف برای کاور استفاده کنیم.
در اینجا، ما یک قطعه اکریلیک 4 میلی متر شفاف داریم که یک فویل محافظ دارد و به نظر آبی می رسد. ایده ی استفاده از کاور این است که دو صفحه به شکل PCB داشته باشیم و یکی از آنها بعنوان محافظ در بالای مدار قرار داشته باشد و دیگری در پایین مدار قرار گیرد.
بنابراین ما شکل PCB را علامت گذاری کردیم و با استفاده از اره دستی فلزی، لایه ی اکریلیک را بر اساس آن برش دادیم. سپس با استفاده از یک سوهان، شکل اکریلیک را سوهان زدیم و تنظیم کردیم. این دو کاور، باید با PCB تطابق داشته باشند.
سپس ما مکان هایی که نیاز داریم آنها را برای عبور قطعات باز کنیم را علامت گذاری می کنیم. ما در ابتدا، با استفاده از یک دریل 3 میلی متری، 4 حفره را برای ایمن سازی کاورها در PCB استفاده کرده ایم. برای سوراخ کردن این حفره ها، ما همچنین از خزینه ها(counter sinks) استفاده کرده ایم؛ به طوری که پیچ ها در سوراخ های کاور صاف صافی یا مماس می شوند.
برای باز کردن فضا برای سوئیچ ها و پتانسیومترها، از یک دریل با مته 6 میلی متر استفاده کرده ایم؛ و برای باز کردن فضا برای جوی استیک، از یک مته گازور(forstner bit) 25 میلی متری استفاده کرده ایم. سپس در اینجا هم با استفاده از یک سوهان، حفره ها را سوهان زده ایم.
قبل از متصل کردن کاور، به این نکته توجه کنید که ما پین هدرِ منبع تغذیه را وارونه لحیم کرده ایم(یعنی در پشت بورد آن را لحیم کرده ایم) که در تصویر زیر مشاهده می کنید. به همین خاطر، به این پین ها در پشت بورد، در جایی که باتری قرار دارد، دسترسی خواهیم داشت.
بسیار خب، حالا می توانیم کاور را سر جای خود قرار دهیم. حالا ما شروع به کندن روکش محافظ اکریلیک می کنیم؛ و اقرار می کنیم که این روکش کاملاً راضی کننده بود؛ زیرا حالا اکریلیک ما بسیار تمیز از آب در آمده است. ما اول از همه، دو پتانسیومتر خود را در کاور بالایی قرار داده و محکم می کنیم، و سپس پیچ و مهره های سه میلی متری را تراز می کنیم و حلقه های فاصله 11 میلی متری را در جای خود قرار می دهیم.
سپس ما با استفاده از پیچ و مهره ها، با دقت کاور بالایی را با PCB ادغام کردیم. در اینجا، ما در نهایت پتانسیومتر ها را به PCB لحیم کردیم، زیرا قبلاً ما دقیقاً نمی دانستیم که آنها در چه فاصله ای قرار خواهند گرفت.
سپس ما در کاور پایینی، جا باتری را قرار دادیم و آن را با استفاده از دو پیچ و مهره متصل کردیم. در نهایت، ما مونتاژ کاور را با محکم کردن کاور پشتی به پشت PCB، با استفاده از پیچ و مهره ها، به اتمام رساندیم.
در آخر، می توانیم سیم های باتری را به پین های منبع تغذیه متصل کنیم و دستگیره ها را روی پتانسیومترها قرار داده و محکم کنیم و دستگیره های جوی استیک ها را قرار داده و آنتن ماژول NRF24l01 را متصل کنیم. کار این بخش تمام شد؛ اکنون ما فرستنده ی RC آردوینوی خود را با موفقیت ایجاد کردیم.
تنها چیزی که باقی مانده است، برنامه نویسی بورد آردوینو است. برای برنامه نویسی یک بورد آردوینو پرو مینی(Pro Mini)، به یک رابط USB به سریالِ UART نیاز داریم؛ که می تواند به هدر برنامه نویسیِ قرار گرفته در بالای کنترلر، متصل شود.
سپس در منوی Tools از IDE آردوینو، نیاز داریم از زیر-منوی Board گزینه ی Arduino Pro or Pro Mini را انتخاب کنیم. و از زیر-منوی processor نیز ورژن مناسب پردازنده را انتخاب کنید و از زیر-منوی port، پورت مناسب را انتخاب کرده و از زیر-منوی programmer گزینه ی USBasp را انتخاب کنید.
حالا ما آماده ی این هستیم که کدها را در آردوینو آپلود کنیم.
کدنویسی فرستنده ی RC در آردوینو
اجازه دهید توضیح دهیم که این فرستنده چگونه کار می کند. در ابتدا نیاز داریم برای ارتباطات وایرلس، کتابخانه های SPI و RF24 را(به کدها) اضافه کنیم. همچنین باید کتابخانه ی I2C را برای ماژول شتاب سنج اضافه کنیم. برای دانلود کتابخانه RF24 اینجا کلیک کنید. سپس نیاز داریم ورودی های دیجیتال(digital inputs) و برخی متغیرهای مورد نیاز را برای برنامه ی زیر تعریف کنیم. و آبجکت radio و آدرس ارتباطی(address) را تعریف کنیم.
قطعه کد شماره 1
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <Wire.h>
// تعریف ورودی های دیجیتال
#define jB1 1 // دکمه جوی استیک 1
#define jB2 0 // دکمه جوی استیک 2
#define t1 7 // سوئیچ 1
#define t2 4 // سوئیچ 2
#define b1 8 // دکمه 1
#define b2 9 // دکمه 2
#define b3 2 // دکمه 3
#define b4 3 // دکمه 4
const int MPU = 0x68; // MPU6050 I2C address
float AccX, AccY, AccZ;
float GyroX, GyroY, GyroZ;
float accAngleX, accAngleY, gyroAngleX, gyroAngleY;
float angleX, angleY;
float AccErrorX, AccErrorY, GyroErrorX, GyroErrorY;
float elapsedTime, currentTime, previousTime;
int c = 0;
RF24 radio(5, 6); // nRF24L01 (CE, CSN)
const byte address[6] = "00001"; // Address
سپس، نیاز داریم یک ساختار(structure) تعریف کنیم که می خواهیم در آن، مقادیر 14 ورودی(input values) را برای کنترلر ذخیره کنیم. ماکزیمم سایز این ساختار می تواند 32 بایت باشد؛ زیرا این مقدار، حدِّ بافر ماژول NRF24L01 است. یعنی مقدار داده ای که می تواند به یکباره (توسط این ماژول) ارسال شود.
قطعه کد شماره 2
// Max size of this struct is 32 bytes - NRF24L01 buffer limit
struct Data_Package {
byte j1PotX;
byte j1PotY;
byte j1Button;
byte j2PotX;
byte j2PotY;
byte j2Button;
byte pot1;
byte pot2;
byte tSwitch1;
byte tSwitch2;
byte button1;
byte button2;
byte button3;
byte button4;
};
Data_Package data; //ایجاد یک متغیر با ساختار بالا
در تابع setup نیاز داریم ماژول MPU6050 را مقدار دهی اولیه(initialize) کنیم؛ همچنین می توانیم خطای IMU را محاسبه کنیم. این خطا، مقداری است که بعداً، وقتی که می خواهیم زاویه های صحیحِ ماژول را محاسبه کنیم، مورد استفاده قرار می گیرد.
قطعه کد شماره 3
void initialize_MPU6050() {
Wire.begin(); // مقداردهی اولیه ارتباط
Wire.beginTransmission(MPU); // Start communication with MPU6050 // MPU=0x68
Wire.write(0x6B); // Talk to the register 6B
Wire.write(0x00); // Make reset - place a 0 into the 6B register
Wire.endTransmission(true); //end the transmission
// Configure Accelerometer
Wire.beginTransmission(MPU);
Wire.write(0x1C); //Talk to the ACCEL_CONFIG register
Wire.write(0x10); //Set the register bits as 00010000 (+/- 8g full scale range)
Wire.endTransmission(true);
// Configure Gyro
Wire.beginTransmission(MPU);
Wire.write(0x1B); // Talk to the GYRO_CONFIG register (1B hex)
Wire.write(0x10); // Set the register bits as 00010000 (1000dps full scale)
Wire.endTransmission(true);
}
برای اطلاعات بیشتر، می توانید به مقاله ی چگونه شتاب سنج MEMS و ژیروسکوپ کار می کنند مراجعه کنید. یک آموزش اختصاصی برای ماژول MPU6050 به زودی از این سایت منتشر خواهد شد. اکنون، نیاز داریم ارتباط رادیویی(radio communication) را مقدار دهی اولیه(initialize) کنیم و مقاومت های پول-آپ(pull-up) داخلی را برای تمام ورودی های دیجیتال فعال کنیم؛ و مقادیر پیش فرض اولیه را برای تمام متغیرها تنظیم کنیم.
قطعه کد شماره 4
// Define the radio communication
radio.begin();
radio.openWritingPipe(address);
radio.setAutoAck(false);
radio.setDataRate(RF24_250KBPS);
radio.setPALevel(RF24_PA_LOW);
// Activate the Arduino internal pull-up resistors
pinMode(jB1, INPUT_PULLUP);
pinMode(jB2, INPUT_PULLUP);
pinMode(t1, INPUT_PULLUP);
pinMode(t2, INPUT_PULLUP);
pinMode(b1, INPUT_PULLUP);
pinMode(b2, INPUT_PULLUP);
pinMode(b3, INPUT_PULLUP);
pinMode(b4, INPUT_PULLUP);
در متد loop، شروع به قرائت تمام ورودی های(analog inputs) آنالوگ می کنیم؛ و مقادیر آنها که از 0 تا 1023 هستند، را به مقادیر بایت، که از 0 تا 255 هستند، نگاشت(تبدیل یا map) می کنیم؛ هر ورودی، در یک متغیر خاص data از ساختار(structure) مورد نظر ذخیره می شود.
قطعه کد شماره 5
// Read all analog inputs and map them to one Byte value
data.j1PotX = map(analogRead(A1), 0, 1023, 0, 255); // Convert the analog read value from 0 to 1023 into a BYTE value from 0 to 255
data.j1PotY = map(analogRead(A0), 0, 1023, 0, 255);
data.j2PotX = map(analogRead(A2), 0, 1023, 0, 255);
data.j2PotY = map(analogRead(A3), 0, 1023, 0, 255);
data.pot1 = map(analogRead(A7), 0, 1023, 0, 255);
data.pot2 = map(analogRead(A6), 0, 1023, 0, 255);
باید ذکر کنیم که، چون از مقاومت های پول-آپ(pull-up) استفاده می کنیم، وقتی که دکمه ها فشار داده شوند، قرائت های پین های دیجیتال 0 هستند.
قطعه کد شماره 6
// خواندن تمام ورودی های دیجیتال
data.j1Button = digitalRead(jB1);
data.j2Button = digitalRead(jB2);
data.tSwitch2 = digitalRead(t2);
data.button1 = digitalRead(b1);
data.button2 = digitalRead(b2);
data.button3 = digitalRead(b3);
data.button4 = digitalRead(b4);
بنابراین، با استفاده از تابع radio.write() ما به سادگی، مقادیر را از تمام 14 کانال به گیرنده ارسال می کنیم.
قطعه کد شماره 7
// Send the whole data from the structure to the receiver
// ارسال کل داده ها از ساختار به گیرنده
radio.write(&data, sizeof(Data_Package));
در مواردی که سوئیچ 1 روشن(on) شده باشد، ما از شتاب سنج و ژیروسکوپ، برای کنترل استفاده می کنیم.
قطعه کد شماره 8
if (digitalRead(t1) == 0) {
read_IMU(); // Use MPU6050 instead of Joystick 1 for controling left, right, forward and backward movements
}
بنابراین، ما به جای مقادیر X و Y از جوی استیک 1، از مقادیر زاویه(angle values) که از IMU دریافت می کنیم، استفاده می کنیم؛ به طوری که قبلاً آنها را به طور مناسب، از 90- تا 90+ درجه به مقادیر 0 تا 255 تبدیل کرده ایم.
قطعه کد شماره 9
// Map the angle values from -90deg to +90 deg into values from 0 to 255, like the values we are getting from the Joystick
data.j1PotX = map(angleX, -90, +90, 255, 0);
data.j1PotY = map(angleY, -90, +90, 0, 255);
بنابراین، کدهای فرستنده(transmitter) اینگونه هستند؛
مهمترین چیزها، تعریف ارتباط رادیویی و ارسال داده ها به گیرنده(receiver) هستند. در زیر، کدهای کامل آردوینو برای فرستنده ی RC را مشاهده می کنید:
قطعه کد شماره 10
/*
DIY Arduino based RC Transmitter
by Dejan Nedelkovski, www.HowToMechatronics.com
Library: TMRh20/RF24, https://github.com/tmrh20/RF24/
*/
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <Wire.h>
// Define the digital inputs
#define jB1 1 // Joystick button 1
#define jB2 0 // Joystick button 2
#define t1 7 // Toggle switch 1
#define t2 4 // Toggle switch 1
#define b1 8 // Button 1
#define b2 9 // Button 2
#define b3 2 // Button 3
#define b4 3 // Button 4
const int MPU = 0x68; // MPU6050 I2C address
float AccX, AccY, AccZ;
float GyroX, GyroY, GyroZ;
float accAngleX, accAngleY, gyroAngleX, gyroAngleY;
float angleX, angleY;
float AccErrorX, AccErrorY, GyroErrorX, GyroErrorY;
float elapsedTime, currentTime, previousTime;
int c = 0;
RF24 radio(5, 6); // nRF24L01 (CE, CSN)
const byte address[6] = "00001"; // Address
// Max size of this struct is 32 bytes - NRF24L01 buffer limit
struct Data_Package {
byte j1PotX;
byte j1PotY;
byte j1Button;
byte j2PotX;
byte j2PotY;
byte j2Button;
byte pot1;
byte pot2;
byte tSwitch1;
byte tSwitch2;
byte button1;
byte button2;
byte button3;
byte button4;
};
Data_Package data; //Create a variable with the above structure
void setup() {
Serial.begin(9600);
// Initialize interface to the MPU6050
initialize_MPU6050();
// Call this function if you need to get the IMU error values for your module
//calculate_IMU_error();
// Define the radio communication
radio.begin();
radio.openWritingPipe(address);
radio.setAutoAck(false);
radio.setDataRate(RF24_250KBPS);
radio.setPALevel(RF24_PA_LOW);
// Activate the Arduino internal pull-up resistors
pinMode(jB1, INPUT_PULLUP);
pinMode(jB2, INPUT_PULLUP);
pinMode(t1, INPUT_PULLUP);
pinMode(t2, INPUT_PULLUP);
pinMode(b1, INPUT_PULLUP);
pinMode(b2, INPUT_PULLUP);
pinMode(b3, INPUT_PULLUP);
pinMode(b4, INPUT_PULLUP);
// Set initial default values
data.j1PotX = 127; // Values from 0 to 255. When Joystick is in resting position, the value is in the middle, or 127. We actually map the pot value from 0 to 1023 to 0 to 255 because that's one BYTE value
data.j1PotY = 127;
data.j2PotX = 127;
data.j2PotY = 127;
data.j1Button = 1;
data.j2Button = 1;
data.pot1 = 1;
data.pot2 = 1;
data.tSwitch1 = 1;
data.tSwitch2 = 1;
data.button1 = 1;
data.button2 = 1;
data.button3 = 1;
data.button4 = 1;
}
void loop() {
// Read all analog inputs and map them to one Byte value
data.j1PotX = map(analogRead(A1), 0, 1023, 0, 255); // Convert the analog read value from 0 to 1023 into a BYTE value from 0 to 255
data.j1PotY = map(analogRead(A0), 0, 1023, 0, 255);
data.j2PotX = map(analogRead(A2), 0, 1023, 0, 255);
data.j2PotY = map(analogRead(A3), 0, 1023, 0, 255);
data.pot1 = map(analogRead(A7), 0, 1023, 0, 255);
data.pot2 = map(analogRead(A6), 0, 1023, 0, 255);
// Read all digital inputs
data.j1Button = digitalRead(jB1);
data.j2Button = digitalRead(jB2);
data.tSwitch2 = digitalRead(t2);
data.button1 = digitalRead(b1);
data.button2 = digitalRead(b2);
data.button3 = digitalRead(b3);
data.button4 = digitalRead(b4);
// If toggle switch 1 is switched on
if (digitalRead(t1) == 0) {
read_IMU(); // Use MPU6050 instead of Joystick 1 for controling left, right, forward and backward movements
}
// Send the whole data from the structure to the receiver
radio.write(&data, sizeof(Data_Package));
}
void initialize_MPU6050() {
Wire.begin(); // Initialize comunication
Wire.beginTransmission(MPU); // Start communication with MPU6050 // MPU=0x68
Wire.write(0x6B); // Talk to the register 6B
Wire.write(0x00); // Make reset - place a 0 into the 6B register
Wire.endTransmission(true); //end the transmission
// Configure Accelerometer
Wire.beginTransmission(MPU);
Wire.write(0x1C); //Talk to the ACCEL_CONFIG register
Wire.write(0x10); //Set the register bits as 00010000 (+/- 8g full scale range)
Wire.endTransmission(true);
// Configure Gyro
Wire.beginTransmission(MPU);
Wire.write(0x1B); // Talk to the GYRO_CONFIG register (1B hex)
Wire.write(0x10); // Set the register bits as 00010000 (1000dps full scale)
Wire.endTransmission(true);
}
void calculate_IMU_error() {
// We can call this funtion in the setup section to calculate the accelerometer and gury data error. From here we will get the error values used in the above equations printed on the Serial Monitor.
// Note that we should place the IMU flat in order to get the proper values, so that we then can the correct values
// Read accelerometer values 200 times
while (c < 200) {
Wire.beginTransmission(MPU);
Wire.write(0x3B);
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6, true);
AccX = (Wire.read() << 8 | Wire.read()) / 4096.0 ;
AccY = (Wire.read() << 8 | Wire.read()) / 4096.0 ;
AccZ = (Wire.read() << 8 | Wire.read()) / 4096.0 ;
// Sum all readings
AccErrorX = AccErrorX + ((atan((AccY) / sqrt(pow((AccX), 2) + pow((AccZ), 2))) * 180 / PI));
AccErrorY = AccErrorY + ((atan(-1 * (AccX) / sqrt(pow((AccY), 2) + pow((AccZ), 2))) * 180 / PI));
c++;
}
//Divide the sum by 200 to get the error value
AccErrorX = AccErrorX / 200;
AccErrorY = AccErrorY / 200;
c = 0;
// Read gyro values 200 times
while (c < 200) {
Wire.beginTransmission(MPU);
Wire.write(0x43);
Wire.endTransmission(false);
Wire.requestFrom(MPU, 4, true);
GyroX = Wire.read() << 8 | Wire.read();
GyroY = Wire.read() << 8 | Wire.read();
// Sum all readings
GyroErrorX = GyroErrorX + (GyroX / 32.8);
GyroErrorY = GyroErrorY + (GyroY / 32.8);
c++;
}
//Divide the sum by 200 to get the error value
GyroErrorX = GyroErrorX / 200;
GyroErrorY = GyroErrorY / 200;
// Print the error values on the Serial Monitor
Serial.print("AccErrorX: ");
Serial.println(AccErrorX);
Serial.print("AccErrorY: ");
Serial.println(AccErrorY);
Serial.print("GyroErrorX: ");
Serial.println(GyroErrorX);
Serial.print("GyroErrorY: ");
Serial.println(GyroErrorY);
}
void read_IMU() {
// === Read acceleromter data === //
Wire.beginTransmission(MPU);
Wire.write(0x3B); // Start with register 0x3B (ACCEL_XOUT_H)
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6, true); // Read 6 registers total, each axis value is stored in 2 registers
//For a range of +-8g, we need to divide the raw values by 4096, according to the datasheet
AccX = (Wire.read() << 8 | Wire.read()) / 4096.0; // X-axis value
AccY = (Wire.read() << 8 | Wire.read()) / 4096.0; // Y-axis value
AccZ = (Wire.read() << 8 | Wire.read()) / 4096.0; // Z-axis value
// Calculating angle values using
accAngleX = (atan(AccY / sqrt(pow(AccX, 2) + pow(AccZ, 2))) * 180 / PI) + 1.15; // AccErrorX ~(-1.15) See the calculate_IMU_error()custom function for more details
accAngleY = (atan(-1 * AccX / sqrt(pow(AccY, 2) + pow(AccZ, 2))) * 180 / PI) - 0.52; // AccErrorX ~(0.5)
// === Read gyro data === //
previousTime = currentTime; // Previous time is stored before the actual time read
currentTime = millis(); // Current time actual time read
elapsedTime = (currentTime - previousTime) / 1000; // Divide by 1000 to get seconds
Wire.beginTransmission(MPU);
Wire.write(0x43); // Gyro data first register address 0x43
Wire.endTransmission(false);
Wire.requestFrom(MPU, 4, true); // Read 4 registers total, each axis value is stored in 2 registers
GyroX = (Wire.read() << 8 | Wire.read()) / 32.8; // For a 1000dps range we have to divide first the raw value by 32.8, according to the datasheet
GyroY = (Wire.read() << 8 | Wire.read()) / 32.8;
GyroX = GyroX + 1.85; //// GyroErrorX ~(-1.85)
GyroY = GyroY - 0.15; // GyroErrorY ~(0.15)
// Currently the raw values are in degrees per seconds, deg/s, so we need to multiply by sendonds (s) to get the angle in degrees
gyroAngleX = GyroX * elapsedTime;
gyroAngleY = GyroY * elapsedTime;
// Complementary filter - combine acceleromter and gyro angle values
angleX = 0.98 * (angleX + gyroAngleX) + 0.02 * accAngleX;
angleY = 0.98 * (angleY + gyroAngleY) + 0.02 * accAngleY;
// Map the angle values from -90deg to +90 deg into values from 0 to 255, like the values we are getting from the Joystick
data.j1PotX = map(angleX, -90, +90, 255, 0);
data.j1PotY = map(angleY, -90, +90, 0, 255);
}
کدهای گیرنده(Receiver)
حالا اجازه دهید ببینیم که چگونه می توانیم این داده ها را دریافت کنیم. در زیر، شماتیک یک آردوینوی ساده(نانو) و گیرنده ی NRF24L01 را مشاهده می کنید. البته شما می توانید از هر بورد آردوینوی دیگری نیز استفاده کنید.
و در اینجا مقداری کد ساده برای گیرنده(receiver) وجود دارد؛ گیرنده، همان جایی است که داده ها را دریافت می کنیم و به سادگی آن را در سریال مانیتور(serial monitor) پرینت(print) می کنیم؛ به طوری که بدانیم ارتباطات به درستی کار می کند. دوباره نیاز داریم کتابخانه ی RF24 را اضافه کنیم و آبجکت ها و ساختار(structure) را مانند کدهای فرستنده(transmitter) تعریف کنیم.
در تابع setup، وقتی که ارتباطات رادیویی را تعریف می کنیم، نیاز داریم از تنظیماتی مشابه با تنظیمات فرستنده(transmitter) استفاده کنیم و این ماژول را با استفاده از تابع radio.startListening() بعنوان گیرنده(receiver) تعریف کنیم.
قطعه کد شماره 11
/*
DIY Arduino based RC Transmitter Project
== Receiver Code ==
by Dejan Nedelkovski, www.HowToMechatronics.com
Library: TMRh20/RF24, https://github.com/tmrh20/RF24/
*/
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
RF24 radio(10, 9); // nRF24L01 (CE, CSN)
const byte address[6] = "00001";
unsigned long lastReceiveTime = 0;
unsigned long currentTime = 0;
// Max size of this struct is 32 bytes - NRF24L01 buffer limit
struct Data_Package {
byte j1PotX;
byte j1PotY;
byte j1Button;
byte j2PotX;
byte j2PotY;
byte j2Button;
byte pot1;
byte pot2;
byte tSwitch1;
byte tSwitch2;
byte button1;
byte button2;
byte button3;
byte button4;
};
Data_Package data; //Create a variable with the above structure
void setup() {
Serial.begin(9600);
radio.begin();
radio.openReadingPipe(0, address);
radio.setAutoAck(false);
radio.setDataRate(RF24_250KBPS);
radio.setPALevel(RF24_PA_LOW);
radio.startListening(); // Set the module as receiver
resetData();
}
void loop() {
// Check whether there is data to be received
if (radio.available()) {
radio.read(&data, sizeof(Data_Package)); // Read the whole data and store it into the 'data' structure
lastReceiveTime = millis(); // At this moment we have received the data
}
// Check whether we keep receving data, or we have a connection between the two modules
currentTime = millis();
if ( currentTime - lastReceiveTime > 1000 ) { // If current time is more then 1 second since we have recived the last data, that means we have lost connection
resetData(); // If connection is lost, reset the data. It prevents unwanted behavior, for example if a drone has a throttle up and we lose connection, it can keep flying unless we reset the values
}
// Print the data in the Serial Monitor
Serial.print("j1PotX: ");
Serial.print(data.j1PotX);
Serial.print("; j1PotY: ");
Serial.print(data.j1PotY);
Serial.print("; button1: ");
Serial.print(data.button1);
Serial.print("; j2PotX: ");
Serial.println(data.j2PotX);
}
void resetData() {
// Reset the values when there is no radio connection - Set initial default values
data.j1PotX = 127;
data.j1PotY = 127;
data.j2PotX = 127;
data.j2PotY = 127;
data.j1Button = 1;
data.j2Button = 1;
data.pot1 = 1;
data.pot2 = 1;
data.tSwitch1 = 1;
data.tSwitch2 = 1;
data.button1 = 1;
data.button2 = 1;
data.button3 = 1;
data.button4 = 1;
}
در داخل تابع loop اصلی، در خط 49، با استفاده از تابع available()، بررسی می کنیم که آیا داده ی ورودی(incoming data) داریم یا نه؟ اگر داده ی ورودی داشته باشیم، یعنی ارزش radio.available() برابر با true باشد، ما به سادگی داده ها را قرائت می کنیم و آن را در متغیرهای ساختار(structure) ذخیره می کنیم.
حالا می توانیم داده ها را در سریال مانیتور(serial monitor) پرینت(print) کنیم تا بررسی کنیم که آیا انتقال به درستی کار می کند یا نه. همچنین با استفاده از تابع millis() و یک دستور if ،بررسی می کنیم که آیا دریافت داده ها(receiving data) ادامه می یابد یا اگر داده ای را برای زمانی بیش از 1 ثانیه دریافت نمی کنیم، آنگاه متغیرها را ریست(reset) کرده و مقادیر پیش فرض را برای آنها تنظیم کنیم.
ما از این روش، برای جلوگیری از رفتارهای ناخواسته استفاده می کنیم؛ بعنوان مثال، اگر یک پهپاد را به بالا ببریم، و اتصال قطع شود، این پهپاد به پرواز خود ادامه می دهد، مگر اینکه مقادیر آن را ریست(reset) کنیم. کار تمام شد. اکنون می توانیم این روش دریافت داده ها را برای هر پروژه ی آردوینو انجام دهیم.
بعنوان مثال، در زیر، یک قطعه کد برای کنترل کردن یک ماشین روبات آردوینو مشاهده می کنید. بعنوان یک آپدیت برای این پروژه، ما یک مقاله به نامِ ایجاد یک گیرنده ی RC بر اساس آردوینو ایجاد کرده ایم. دوباره می گوییم که، این مقاله بر اساس بورد آردوینو پرو مینی(Arduino Pro mini) است و چندین اتصال سروو موتور و ESC آماده ی استفاده دارد که در یک بورد مدار چاپی متراکم(PCB) جمع آوری شده اند.
کنترل وایرلس روبات ماشینیِ آردوینو با استفاده از فرستنده ی RC
در اینجا ما نیاز داریم کتابخانه ها را تعریف کنیم؛ ساختار و ارتباطات radio قبلاً توضیح داده شده اند. سپس در تابع loop() اصلی نیاز داریم داده های ورودی را بخوانیم و از هریک از آنها برای هدف خود استفاده کنیم. در این مورد، ما از مقادیر joystick 1 برای راه اندازی ماشین خود استفاده می کنیم.
کنترل روبات مورچه شش پا، با استفاده از فرستنده RC آردوینو
ما به طریق مشابهی، یک روبات مورچه آردوینو را ایجاد کرده ایم که با استفاده از فرستنده RC آردوینو به صورت وایرلس کنترل می شود. برای رفتن به این مقاله اینجا کلیک کنید. ما فقط نیاز داریم داده ها را بخوانیم و بر اساس آن، تابع های مناسب را اجرا کنیم؛ مانند حرکت رو به جلو، حرکت به چپ، حرکت به راست، حمله و غیره.
اسپید کنترل(ESC) و کنترل سروو موتور(Servo Control) با استفاده از فرستنده RC
در آخر، اجازه دهید ببینیم که چگونه می توانیم از فرستنده ی خود برای کنترل دستگاه های RC تجاری استفاده کنیم.
معمولاً برای این دستگاه ها، نیاز داریم سروو موتورها یا موتورهای براشلس آنها را کنترل کنیم. بنابراین پس از دریافت داده ها از فرستنده، برای کنترل سروو موتور، به سادگی از کتابخانه ی سروو ی آردوینو استفاده می کنیم و از مقادیر 0 تا 180 درجه استفاده می کنیم. برای کنترل موتور براشلس با استفاده از ESC، دوباره می توانیم از کتابخانه ی سروو(servo library) برای تولید سیگنال 50Hz PWM برای کنترل ESC استفاده کنیم. ما با تغییر چرخه کار از 1000 تا 2000 میکروثانیه، میزان RPM موتور را از 0 تا مقدار ماکزیمم کنترل می کنیم. در آموزش بعدی، بیشتر در مورد کنترل موتورهای براشلس با استفاده از ESC توضیح خواهیم داد.
نکته: برای ایجاد یک هواپیمای RC آردوینو، این مقاله را مشاهده کنید.
لطفاً توجه کنید که ما در حقیقت نمی توانیم سیستم گیرنده ی استاندارد را با سیستم NRF24L01 2.4GHz مقید کنیم. به جای آن، نیاز داریم گیرنده ی خود را که شامل یک ماژول NRF24L01 است را اصلاح یا ایجاد کنیم. از آنجا می توانیم سیگنال های PWM یا PPM مناسب را برای کنترل دستگاه RC تولید کنیم.
قطعه کد شماره 12
/*
DIY Arduino based RC Transmitter Project
== Receiver Code - ESC and Servo Control ==
by Dejan Nedelkovski, www.HowToMechatronics.com
Library: TMRh20/RF24, https://github.com/tmrh20/RF24/
*/
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <Servo.h>
RF24 radio(10, 9); // nRF24L01 (CE, CSN)
const byte address[6] = "00001";
unsigned long lastReceiveTime = 0;
unsigned long currentTime = 0;
Servo esc; // create servo object to control the ESC
Servo servo1;
Servo servo2;
int escValue, servo1Value, servo2Value;
// Max size of this struct is 32 bytes - NRF24L01 buffer limit
struct Data_Package {
byte j1PotX;
byte j1PotY;
byte j1Button;
byte j2PotX;
byte j2PotY;
byte j2Button;
byte pot1;
byte pot2;
byte tSwitch1;
byte tSwitch2;
byte button1;
byte button2;
byte button3;
byte button4;
};
Data_Package data; //Create a variable with the above structure
void setup() {
Serial.begin(9600);
radio.begin();
radio.openReadingPipe(0, address);
radio.setAutoAck(false);
radio.setDataRate(RF24_250KBPS);
radio.setPALevel(RF24_PA_LOW);
radio.startListening(); // Set the module as receiver
resetData();
esc.attach(9);
servo1.attach(3);
servo2.attach(4);
}
void loop() {
// Check whether we keep receving data, or we have a connection between the two modules
currentTime = millis();
if ( currentTime - lastReceiveTime > 1000 ) { // If current time is more then 1 second since we have recived the last data, that means we have lost connection
resetData(); // If connection is lost, reset the data. It prevents unwanted behavior, for example if a drone jas a throttle up, if we lose connection it can keep flying away if we dont reset the function
}
// Check whether there is data to be received
if (radio.available()) {
radio.read(&data, sizeof(Data_Package)); // Read the whole data and store it into the 'data' structure
lastReceiveTime = millis(); // At this moment we have received the data
}
// Controlling servos
servo1Value = map(data.j2PotX, 0, 255, 0, 180);
servo2Value = map(data.j2PotY, 0, 255, 0, 180);
servo1.write(servo1Value);
servo2.write(servo2Value);
// Controlling brushless motor with ESC
escValue = map(data.pot1, 0, 255, 1000, 2000); // Map the receiving value form 0 to 255 to 0 1000 to 2000, values used for controlling ESCs
esc.writeMicroseconds(escValue); // Send the PWM control singal to the ESC
}
void resetData() {
// Reset the values when there is no radio connection - Set initial default values
data.j1PotX = 127;
data.j1PotY = 127;
data.j2PotX = 127;
data.j2PotY = 127;
data.j1Button = 1;
data.j2Button = 1;
data.pot1 = 1;
data.pot2 = 1;
data.tSwitch1 = 1;
data.tSwitch2 = 1;
data.button1 = 1;
data.button2 = 1;
data.button3 = 1;
data.button4 = 1;
}
به انتهای این مقاله رسیدیم. امیدوارم از این آموزش لذت برده باشید؛ و نکات جدیدی را فرا گرفته باشید.
منابع:
منبع شماره 1: https://howtomechatronics.com
1. سعی کنید نظرات شما مرتبط با مقاله ی مورد نظر باشد، در غیر این صورت پاسخ داده نخواهد شد.
2. سوالات خود را به صورت کوتاه بیان کنید و از پرسیدن چند سوال به طور همزمان خودداری کنید.
3. سوال خود را به طور واضح بیان کنید و از کلمات مبهم استفاده نکنید.