بسم الله الرحمن الرحيم , و الصلاة و السلام على أشرف الأنبياء و المرسلين نبينا محمد وعلى آله وصحبه أجمعين اللهم لا سهل إلا ماجعلته سهلاً وأنت تجعل الحزن إذا شئت سهلاً.
تكلمت في التدوينة السابقة عن كيفية بناء 4Channel RC Transmitter متوافق مع بروتكول WLToys V202 لكن لم اتطرق لكيفية بناء 4ch RC Receiver متوافق معه و انما اكتفيت بوضع رابط الفيديو لتجربة الـ 4ch RC Receiver
بصراحة تعمدت أن افصل الموضوعين عن بعض لتسهيل مناقشتهم وفهمهم بشكل افضل .
لكن قبل ان ابداء بموضوعنا لهذا اليوم اردت ان أجيب على بعض التساؤلات التي وردتني على الرسائل الخاصة و خصوصا انها تقع في صلب موضوعنا وكانت جميعها تتمحور حول مدى الإرسال وإذا كان بالإمكان زيدة المدى و زيادة عدد قنوات الارسال للـ RC Transmitter
مدى الإرسال :
اولا هناك نوعين من هذا المديول الأول يقتصر اسمه على nRF24L01 فقط و الثاني NRF24L01 + PA + LNA
الجدول التالي يوضح أهم الفروقات بينهم :
من الجدول السابق نلاحظ أن المديول NRF24L01 + PA + LNA يتفوق على الأول بقوة الإرسال لإحتوائه على معزز قدرة و مكبر أولي للإستقبال و هوائي خارجي و يقولون ان مدى الاول يصل لحدود 100 متر و الثاني 1000 متر إذا تقيدت بسرعة نقل البيانات 250kbps.
من الجدول السابق نلاحظ أن المديول NRF24L01 + PA + LNA يتفوق على الأول بقوة الإرسال لإحتوائه على معزز قدرة و مكبر أولي للإستقبال و هوائي خارجي و يقولون ان مدى الاول يصل لحدود 100 متر و الثاني 1000 متر إذا تقيدت بسرعة نقل البيانات 250kbps.
![]() |
| nRF24L01 |
![]() |
| nRF24L01 + PA + LNA |
ملاحظة هامة لمن يعاني من ضعف أو تقطّع في الإرسال :
في الغالب المشكلة بسبب الضجيج الكهربائي على خط التغذية 3.3V قم بما يلي لحل هذه المشكلة :
- تحقق من جهد التغذية 3.3V
- ربط مكثف 100nF على خط التغذية 3.3V بالقرب من المديول
- ربط مكثف كيميائي 1uF to 10uF على خط التغذية 3.3V بالقرب من المديول
زيادة المدى للمديول nRF24L01
عند تجربتي للمديول ذو المدى 100m لم احصل على أكثر من 30m مع أنني تقيدت بسرعة نقل البيانات 250kbps وكانت نتيجة الإختبار هذه غير متوقعة فخطرت لي فكرة تركيب external antenna وكان لدي هوائي ذو ربح 2Db فقمت بإلغاء Pcb antenna المدمج في المديول و ربط external antenna و أعدت الإختبار بدون أي تغيير في الكود فحصلت على 50m إضافية ليكون إجمالي المسافة 80m تقريبا .
طريقة تركيب الهوائي على المديول :
الصور التالية توضح طريقة تركيب الهوائي ( لا يشترط ان تستخدم نفس الهوئي فأنا إستخدمته لتوفره لدي بإمكانك استخدام هوائي الراوتر فهو يعمل بنطاق 2.4Ghz )
![]() |
| الهوائي المستخدم FrSky Receiver Antenna |
الصورة التالية توضح طريقة وصل سلك الهوائي بالمديول بعد قطع السلكين المؤشر عليهم بالدوائر الصفراء
![]() |
| توضح طريقة تركيب سلك الهوائي |
![]() |
| الشكل النهائي بعد تركيب سلك الهوائي |
مما سبق ذكره استطيع أن اقول لكم انه في حال كنت تريد ان تحصل على مدى يصل لـ 800M فكل ما عليك فعله ان تستخدم المديول nRF24L01 + PA + LNA في وحدة الإرسال أما وحدة الإستقبال فبالإمكان ان تستخدم فيها المديول nRF24L01 و إذا أردت زيادة هذا المدى فتستطيع ان تدمج هوائي خارجي لوحدة الإستقبال و ستحصل على ما يقارب 1100m .
زيادة عدد قنوات الارسال للـ RC Transmitter
بالنسبة لزيادة عدد القنوات الأمر بسيط جدا كل ما عليك فعله هو تعديل مدخلات الدالة V202_TX::command
لما يتناسب مع متطلباتك وبإمكانك معالجة قيم Trim في المرسل بدل أن ترسلها للمستقبل ليعالجها و بالتالي توفر 3 بايت تستطيع استخدامهم كقنوات إضافيه أو تستطيع استخدام البايتات الفراغة وعددها 4
لما يتناسب مع متطلباتك وبإمكانك معالجة قيم Trim في المرسل بدل أن ترسلها للمستقبل ليعالجها و بالتالي توفر 3 بايت تستطيع استخدامهم كقنوات إضافيه أو تستطيع استخدام البايتات الفراغة وعددها 4
مثال لتعديل الدالة Command لإرسال 8 قنوات
void V202_TX::command(uint8_t throttle, int8_t yaw, int8_t pitch, int8_t roll, int8_t aux1, int8_t aux2, int8_t aux3, int8_t aux4, uint8_t flags) { uint8_t buf[16]; if (flags == 0xc0) { // binding buf[0] = 0x00; buf[1] = 0x00; buf[2] = 0x00; buf[3] = 0x00; buf[4] = 0x00; buf[5] = 0x00; buf[6] = 0x00; buf[10] = 0x00; buf[11] = 0x00; buf[12] = 0x00; buf[13] = 0x00; } else { // regular packet buf[0] = throttle; buf[1] = (uint8_t) yaw; buf[2] = (uint8_t) pitch; buf[3] = (uint8_t) roll; // Trims, middle is 0x40 buf[4] = 0x40; // yaw buf[5] = 0x40; // pitch buf[6] = 0x40; // roll // Add 4 channels buf[10] = (uint8_t) aux1; // [-127 .. 127] buf[11] = (uint8_t) aux2; buf[12] = (uint8_t) aux3; buf[13] = (uint8_t) aux4; } // TX id buf[7] = txid[0]; buf[8] = txid[1]; buf[9] = txid[2]; buf[14] = flags; uint8_t sum = 0; uint8_t i; for (i = 0; i < 15; ++i) sum += buf[i]; buf[15] = sum; if (packet_sent) { boolean report_done = false; while (!(radio.read_register(STATUS) & _BV(TX_DS))) ; radio.write_register(STATUS, _BV(TX_DS)); } packet_sent = true; uint8_t rf_ch = rf_channels[rf_ch_num >> 1]; rf_ch_num++; if (rf_ch_num >= 32) rf_ch_num = 0; radio.write_register(RF_CH, rf_ch); radio.flush_tx(); radio.write_payload(buf, 16); delayMicroseconds(15); }
الأن عند إستدعاء الدالة V202_TX::command يتوجب عليك اسناد قيم القنوات الجديدة إليها والتي تقع ضمن المجال من -127 حتى 127 و يقع على عاتقك قراءة قيم هذه القنوات وأنت من سيحدد محتوى هذه القنوات هل هي لمفاتيح أم لمداخل تشابهية .
أعلم انني أطلت عليكم في الإجابة على بعض الأسئلة التي وردتني لكن لو لم تكن في صلب الموضوع لما وضعتها هنا
دعونا الأن ننتقل لموضوعنا لهذا اليوم
Receiver إلى Flight Controller بعد إضافة 6DOF 3-Axis Accelerometer and Gyroscope إليه إن شاء الله .
الأن دعونا نلقي نظرة سريعة على تصميم الـ 4ch RC Receiver
الدالة INIT_SERVO_DRIVER تعمل على ضبط إعدادات المؤقت العداد Timer1 لتوليد فاصل زمني 20ms و في مقاطعة طفحان المؤقت يتم توليد الـ PWM على المخارج بواسطة الكود
الدالة check_failsafe_button تتحقق هل المفتاح اللحظي الـ Push Buton S1 تم الضغط عليه أم لا فإن تم الضغط عليه مطولا لمدة 2 ثانية و سيتم إستدعاء الدالة save_failsafe_values و بعدها ستقوم بعمل وميض للباعث الضوئي الأخضر لتعلمك بأنه تم تخزين قيم الـ failsafe .
الدالة subtrim تعمل على تصحيح نسبة الخطاء في عصي التحكم الموجودة في الـ Transmitter ( أظن أنك تقول في خاطرك الأن لماذا أتكلف عناء أرسل هذه القيم و بالإمكان ان تتم معالجتها في الـ Transmitter ؟..
بصراحة لأنني استخدم جهاز التحكم الأساسي من شركة WLToys و بروتوكول نقل البيانات يفرض علي أن أقوم بمعالجة subtrim في الـ Receiver لعدم معالجتها في الـ Transmitter ).
الدالة reverse تعمل على عكس القيمة المدخلة إليها أما الدالة mixer فتقوم بعمل مزج بين قناتين وكلا الدالتين يتم استخدامهم في اجرائية V-tail Mixer في حال تفعيل العمل بأسلوب V-tail Mixer
بعد أن تعرفنا على الدوال الأساسية في برنامجنا وعلى وظيفة كل دالة سيسهل علينا فهم ما يجري في الحلقة الرئيسية Loop
في البداية نتحقق إن كانت هناك بيانات جديدة قد وصلتنا من الـ Transmitter
في حال وصول البيانات يتم التحقق من اسلوب العمل هل هو V-tail Mode أم Normal Mode فإن كان الأول فسيتم العمل بالترتيب التالي :
أعلم انني أطلت عليكم في الإجابة على بعض الأسئلة التي وردتني لكن لو لم تكن في صلب الموضوع لما وضعتها هنا
دعونا الأن ننتقل لموضوعنا لهذا اليوم
بناء 4ch RC Receiver متوافق مع بروتوكول WLToys V202
قمت ببناء التصميم على نواة الـ Arduino المتحكم ATmega328 لكن إن لم يتوفر لديك فبإمكانك إستخدام ATmega168 أو حتى ATmega8 فالكود صغير نسبيا لكن الأفضل إستخدام ATmega328 لأنني أنوي في الأيام القادمة تحديث الكود لتحويل الـReceiver إلى Flight Controller بعد إضافة 6DOF 3-Axis Accelerometer and Gyroscope إليه إن شاء الله .
الأن دعونا نلقي نظرة سريعة على تصميم الـ 4ch RC Receiver
- المتحكم المستخدم ATmega328P و يتوجب عليك حرق محمل الإقلاع Bootloader عليه لتحويله إلى Arduino قبل تركيب المديول nRF24L01 و كنت قد شرحت طريقة حرق محمل الإقلاع Bootloader في تدوينة سابقة .
- تعمل القنوات من 1 حتى 4 بنمط PWM 50Hz أما القناة رقم 5 فهي غير مفعلة ضمن البرنامج حاليا.
- دعم ميزة Failsafe الطيران الأمن وهذه الميزة تخولك وضع قيم افتراضية يقوم المستقبل بإخراجها في حال انقطاع الاتصال مع المرسل ويعمل المفتاح اللحظي S1 على تخزين هذه القيم في الذاكرة .
- الـ jumper SJ1 يحدد نظام العمل هل هو V-tail Mixer أو Normal و يستخدك الـ V-tail Mixer في كل من الطائرات Delta wing and Helicopters
- يقوم المأخذ Lamp بقيادة بواعث ضوئية Led في طائرتك و عند تفعيله يومض البواعث بشكل مشابه للطائرات المدنية.
- منفذ I2C 3.3V لدعم أجهزة الإستشعار مثل WiiMotionPlus, WiiNunchuk, MEMs gyros, barometers and accelerometers
- المنفذ التسلسلي لتحميل البرامج الثابتة وتطبيقات القياس عن بعد Telemetry
![]() |
| 3D Pcb 4ch Receiver Ver 1.1 |
شرح الكود
في البداية تمت تهيئة المداخل و المخارجvoid setup() { // SS pin must be set as output to set SPI to master ! pinMode(SS, OUTPUT); pinMode(GREEN_LED_pin, OUTPUT); pinMode(Servo1_OUT, OUTPUT); //Servo1 pinMode(Servo2_OUT, OUTPUT); //Servo2 pinMode(Servo3_OUT, OUTPUT); //Servo3 pinMode(Servo4_OUT, OUTPUT); //Servo4 pinMode(Servo5_OUT, OUTPUT); //Servo5 pinMode(Lamp_OUT, OUTPUT); //Lamp pinMode(MODE_pin, INPUT_PULLUP); pinMode(FS_BTN, INPUT_PULLUP); // Set CS pin to D9 and CE pin to D10 wireless.setPins(CS_pin, CE_pin); protocol.init(&wireless); #if DEBUG Serial.begin(115200); Serial.println("Start"); #endif INIT_SERVO_DRIVER(); }
void INIT_SERVO_DRIVER(void) { TCCR1B = 0x00; //stop timer TCNT1H = 0x00; //setup TCNT1L = 0x00; ICR1 = 40000; // used for TOP, makes for 50 hz TCCR1A = 0x02; TCCR1B = 0x1A; //start timer with 1/8 prescaler for 0.5us PPM resolution #ifdef TIMSK1 TIMSK1 = (1<<TOIE1); #else TIMSK = (1<<TOIE1); // for ATmega8 #endif } //**************************************************** ISR(TIMER1_OVF_vect) { unsigned int us; Servo_Ports_LOW; Servo_Number++; // jump to next servo if (Servo_Number>4) // back to the first servo { total_ppm_time = 0; // clear the total servo ppm time Servo_Number = 0; } if (Servo_Number == 4) // Check the servo number. //Servos accepting 50hz ppm signal, this is why we are waiting for 20ms before second signal brust. us = 40000 - total_ppm_time; //wait for total 20ms loop. waiting time = 20.000us - total servo times else us = Servo_Buffer[Servo_Number]; total_ppm_time += us; // calculate total servo signal times. switch (Servo_Number) { case 0: Servo1_OUT_HIGH; break; case 1: Servo2_OUT_HIGH; break; case 2: Servo3_OUT_HIGH; break; case 3: Servo4_OUT_HIGH; break; } TCNT1 = 40000 - us; }
void check_failsafe_button(void) { unsigned long loop_time; if (digitalRead(FS_BTN)==0) { // Check the button Green_LED_OFF; time = millis(); //set the current time loop_time = time; // wait for button reelase if it is already pressed. while ((digitalRead(FS_BTN)==0) && (loop_time < time + 2000)) { loop_time = millis(); } if (digitalRead(FS_BTN)==0) { save_failsafe_values(); for(uint8_t i = 0; i < 4; i++) { Green_LED_ON; delay(100); Green_LED_OFF; delay(100); } } } }
بصراحة لأنني استخدم جهاز التحكم الأساسي من شركة WLToys و بروتوكول نقل البيانات يفرض علي أن أقوم بمعالجة subtrim في الـ Receiver لعدم معالجتها في الـ Transmitter ).
uint8_t subtrim(int8_t Val, int8_t Trim) {
int ret = Val;
ret += Trim;
if(ret < -127) ret = -127;
else if( ret > 127) ret = 127;
ret = map(ret, -127, 127, 0, 255);
return lowByte(ret);
}
الدالة reverse تعمل على عكس القيمة المدخلة إليها أما الدالة mixer فتقوم بعمل مزج بين قناتين وكلا الدالتين يتم استخدامهم في اجرائية V-tail Mixer في حال تفعيل العمل بأسلوب V-tail Mixer
uint8_t reverse(uint8_t val) {
int ret = ((int)val - 127) * -1 + 127;
if(ret < 0)ret = 0;
else if(ret > 255) ret = 255;
return lowByte(ret);
}
//****************************************************
uint8_t mixer(uint8_t input1, uint8_t input2) {
int out = ((input1 - 127) + (input2 - 127)) + 127;
if(out < 0) out = 0;
//else if(out > 255) out = 255;
return lowByte(out);
}
بعد أن تعرفنا على الدوال الأساسية في برنامجنا وعلى وظيفة كل دالة سيسهل علينا فهم ما يجري في الحلقة الرئيسية Loop
في البداية نتحقق إن كانت هناك بيانات جديدة قد وصلتنا من الـ Transmitter
في حال وصول البيانات يتم التحقق من اسلوب العمل هل هو V-tail Mode أم Normal Mode فإن كان الأول فسيتم العمل بالترتيب التالي :
- إستدعاء الدالة subtrim لكل قناة على حدى بإستثناء القناة throttle .
- إن كانت القناة تحتاج لعكس قيمتها يتم إستدعاء الدالة reverse .
- يتم استتدعاء الدالة mixer لمزج القناتين المحددتين وخرجها يقع ضمن المجال 0..255 .
- يتم تحويل القيمة من 0..255 إلى 1501..4499 بإستدعاء الدالة map .
- وأخيرا يتم تخزين القيمة الجديدة للقناة في Servo_Buffer ليتم اخراجها على هيئة PWM للـ Flight Controller .
ملاحظة : ميزة الـ V-tail mixer تحتاج قناتين محددتين لتقوم بعمل المزج بينهم و عادتا تكون القناة الأولى هي pitch
أما القناة الثانية فيتم تحديدها من قبل المستخدم و تكون عادتا roll أو yaw و في اعلى البرنامج ستجد الموجه التالي تستطيع أن تحدد من خلاله القناة التي ترغب بإدخالها على المازج
و إن كان Normal Mode فسيتم العمل بالترتيب التالي :#define V_TAIL ROLL // ROLL or YAW
- إستدعاء الدالة subtrim لكل قناة على حدى بإستثناء القناة throttle
- يتم تحويل القيمة من 0..255 إلى 1501..4499 بإستدعاء الدالة map
- أخيرا يتم تخزين القيمة الجديدة للقناة في Servo_Buffer ليتم اخراجها على هيئة PWM للـ Flight Controller
void loop() {
time = millis();
uint8_t value = protocol.run(&rxValues);
if(value == BIND_IN_PROGRESS) {
if(!bind_in_progress) {
bind_in_progress = true;
#if DEBUG==1
Serial.println("Bind in progress");
#endif
}
}
else if(value == BOUND_NEW_VALUES) {
last_pack_time = time; // record last package time
failsafeMode = false;
if (digitalRead(MODE_pin) == 0) { // check V-Tail Mixer Mode
// do the v-tail mixing
// input 1 : elevator(pitch)
// input 2 : aileron(roll) or rudder(yaw)
#if V_TAIL == ROLL
Servo_Buffer[AILERON] = map(mixer(
subtrim(rxValues.pitch, rxValues.trim_pitch),
subtrim(rxValues.roll, rxValues.trim_roll)), 0, 255, 1501, 4499);
Servo_Buffer[ELEVATOR] = map(mixer(reverse(
subtrim(rxValues.pitch, rxValues.trim_pitch)),
subtrim(rxValues.roll, rxValues.trim_roll)), 0, 255, 1501, 4499);
Servo_Buffer[RUDDER] = map(subtrim(rxValues.yaw, rxValues.trim_yaw), 0, 255, 1501, 4499);
#else
Servo_Buffer[RUDDER] = map(mixer(
subtrim(rxValues.pitch, rxValues.trim_pitch),
subtrim(rxValues.yaw, rxValues.trim_yaw)), 0, 255, 1501, 4499);
Servo_Buffer[ELEVATOR] = map(mixer(reverse(
subtrim(rxValues.pitch, rxValues.trim_pitch)),
subtrim(rxValues.yaw, rxValues.trim_yaw)), 0, 255, 1501, 4499);
Servo_Buffer[AILERON] = map(subtrim(rxValues.roll, rxValues.trim_roll), 0, 255, 1501, 4499);
#endif
}
else {
Servo_Buffer[AILERON] = map(subtrim(rxValues.roll, rxValues.trim_roll), 0, 255, 1501, 4499);
Servo_Buffer[ELEVATOR] = map(subtrim(rxValues.pitch, rxValues.trim_pitch), 0, 255, 1501, 4499);
Servo_Buffer[RUDDER] = map(subtrim(rxValues.yaw, rxValues.trim_yaw), 0, 255, 1501, 4499);
}
Servo_Buffer[THROTTLE] = map(rxValues.throttle, 0, 255, 1501, 4499);
Servo_Buffer[AUX1] = rxValues.flags;
check_failsafe_button();
#if DEBUG==1
Serial.print("ail : ");
Serial.print(Servo_Buffer[AILERON]);
Serial.print("\t ele : ");
Serial.print(Servo_Buffer[ELEVATOR]);
Serial.print("\t thr : ");
Serial.print(Servo_Buffer[THROTTLE]);
Serial.print("\t rud : ");
Serial.println(Servo_Buffer[RUDDER]);
#endif
}
else if(time-last_pack_time > 40 && !failsafeMode) {
failsafeMode = true;
last_pack_time = time;
load_failsafe_values(); // Load Failsafe positions from EEPROM.
}
if(failsafeMode) {
if(time - last_led_time > 200) {
last_led_time = time;
Green_LED_TOG;
}
} else {
if(time - last_led_time > 50) {
last_led_time = time;
Green_LED_TOG;
}
}
if(Servo_Buffer[AUX1] & FLAG_LED) {
if(time > last_flasher_time) {
last_flasher_time = time + ledFlasher[ledFlasherIndex];
if(++ledFlasherIndex == sizeof(ledFlasher)/2) ledFlasherIndex = 0;
ledFlasherState ^= true;
if(ledFlasherState) Lamp_OUT_HIGH;
else Lamp_OUT_LOW;
}
}
else {
Lamp_OUT_LOW;
ledFlasherState = false;
ledFlasherIndex = 0;
}
} /* END MAIN LOOP */
وهكذا لكل بداية نهاية ، وخير العمل ما حسن آخره وخير الكلام ما قل ودل وبعد هذا الجهد المتواضع أتمنى أن أكون موفقا في شرحي لهذا المشروع موضحا أهم التفاصيل لهذا الموضوع الشائق الممتع ، وفقني الله وإياكم لما فيه صالحنا جميعا .










ليست هناك تعليقات:
إرسال تعليق