X أهلا و سهلا بك عزيزي الزائر لأسباب فنية تم نقل المدونة للموقع الجديد يرجى الإنتقال للرابط التالي هوريزون فور إلكترونيكس وشكرا

24‏/06‏/2015

مثال لبناء 4ch RC Receiver متوافق مع بروتوكول WLToys V202

بسم الله الرحمن  الرحيم , و الصلاة و السلام على أشرف الأنبياء و المرسلين نبينا محمد وعلى آله وصحبه أجمعين اللهم لا سهل إلا ماجعلته سهلاً وأنت تجعل الحزن إذا شئت سهلاً.

تكلمت في التدوينة السابقة عن كيفية بناء 4Channel RC Transmitter متوافق مع بروتكول WLToys V202 لكن لم اتطرق لكيفية بناء 4ch RC Receiver متوافق معه و انما اكتفيت بوضع رابط الفيديو لتجربة الـ 4ch RC Receiver
بصراحة تعمدت أن افصل الموضوعين عن بعض لتسهيل مناقشتهم وفهمهم بشكل افضل .


لكن قبل ان ابداء بموضوعنا لهذا اليوم اردت ان أجيب على بعض التساؤلات التي وردتني على الرسائل الخاصة و خصوصا انها تقع في صلب موضوعنا وكانت جميعها تتمحور حول مدى الإرسال وإذا كان بالإمكان زيدة المدى و زيادة عدد قنوات الارسال للـ RC Transmitter

 مدى الإرسال :  

اولا هناك نوعين من هذا المديول الأول يقتصر اسمه على nRF24L01 فقط و الثاني NRF24L01 + PA + LNA
الجدول التالي يوضح أهم الفروقات بينهم :



من الجدول السابق نلاحظ أن المديول NRF24L01 + PA + LNA يتفوق على الأول بقوة الإرسال لإحتوائه على معزز قدرة و مكبر أولي للإستقبال و هوائي خارجي و يقولون ان مدى الاول يصل لحدود 100 متر و الثاني 1000 متر إذا تقيدت بسرعة نقل البيانات 250kbps.  


nRF24L01


nRF24L01 + PA + LNA




ملاحظة هامة لمن يعاني من ضعف أو تقطّع في الإرسال :
في الغالب المشكلة بسبب الضجيج الكهربائي على خط التغذية 3.3V  قم بما يلي لحل هذه المشكلة :
  1.  تحقق من جهد التغذية 3.3V 
  2.  ربط مكثف 100nF على خط التغذية 3.3V بالقرب من المديول
  3. ربط مكثف كيميائي 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
 

مثال لتعديل الدالة 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 و يقع على عاتقك قراءة قيم هذه القنوات وأنت من سيحدد محتوى هذه القنوات هل هي لمفاتيح أم لمداخل تشابهية .

أعلم انني أطلت عليكم في الإجابة على بعض الأسئلة التي وردتني لكن لو لم تكن في صلب الموضوع لما وضعتها هنا
دعونا الأن ننتقل لموضوعنا لهذا اليوم

بناء 4ch RC Receiver متوافق مع بروتوكول WLToys V202

قمت ببناء التصميم على نواة الـ Arduino المتحكم ATmega328 لكن إن لم يتوفر لديك فبإمكانك إستخدام ATmega168 أو حتى ATmega8 فالكود صغير نسبيا لكن الأفضل إستخدام ATmega328 لأنني أنوي في الأيام القادمة تحديث الكود لتحويل الـ
Receiver إلى Flight Controller بعد إضافة 6DOF 3-Axis Accelerometer and Gyroscope إليه إن شاء الله .
الأن دعونا نلقي نظرة سريعة على تصميم الـ 4ch RC Receiver






  1. المتحكم المستخدم ATmega328P و يتوجب عليك حرق محمل الإقلاع Bootloader عليه لتحويله إلى Arduino قبل تركيب المديول nRF24L01 و كنت قد شرحت طريقة حرق محمل الإقلاع Bootloader في تدوينة سابقة .
  2. تعمل القنوات من 1 حتى 4 بنمط PWM 50Hz أما القناة رقم 5 فهي غير مفعلة ضمن البرنامج حاليا. 
  3. دعم ميزة Failsafe الطيران الأمن وهذه الميزة تخولك وضع قيم افتراضية يقوم المستقبل بإخراجها في حال انقطاع الاتصال مع المرسل ويعمل المفتاح اللحظي S1 على تخزين هذه القيم في الذاكرة . 
  4. الـ jumper SJ1 يحدد نظام العمل هل هو V-tail Mixer أو Normal و يستخدك الـ V-tail Mixer في كل من الطائرات Delta wing and Helicopters 
  5. يقوم المأخذ Lamp بقيادة بواعث ضوئية Led في طائرتك و عند تفعيله يومض البواعث بشكل مشابه للطائرات المدنية.
  6. منفذ I2C 3.3V لدعم أجهزة الإستشعار مثل WiiMotionPlus, WiiNunchuk, MEMs gyros, barometers and accelerometers  
  7. المنفذ التسلسلي لتحميل البرامج الثابتة وتطبيقات القياس عن بعد 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();
}
الدالة INIT_SERVO_DRIVER تعمل على ضبط إعدادات المؤقت العداد Timer1 لتوليد فاصل زمني 20ms و في مقاطعة طفحان المؤقت يتم توليد الـ PWM على المخارج بواسطة الكود


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;
}
الدالة check_failsafe_button تتحقق هل المفتاح اللحظي الـ Push Buton S1 تم الضغط عليه أم لا فإن تم الضغط عليه مطولا لمدة 2 ثانية و سيتم إستدعاء الدالة save_failsafe_values و بعدها ستقوم بعمل وميض للباعث الضوئي الأخضر لتعلمك بأنه تم تخزين قيم الـ failsafe  .

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);
      }
    }
  }
}
الدالة subtrim تعمل على تصحيح نسبة الخطاء في عصي التحكم الموجودة في الـ Transmitter (  أظن أنك تقول في خاطرك الأن لماذا أتكلف عناء أرسل هذه القيم و بالإمكان ان تتم  معالجتها في الـ Transmitter ؟.. 
بصراحة لأنني استخدم جهاز التحكم الأساسي من شركة 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 فإن كان الأول فسيتم العمل بالترتيب التالي :
    1.  إستدعاء الدالة subtrim لكل قناة على حدى بإستثناء القناة throttle . 
    2.  إن كانت القناة تحتاج لعكس قيمتها يتم إستدعاء الدالة reverse .
    3.  يتم استتدعاء الدالة mixer لمزج القناتين المحددتين وخرجها يقع ضمن المجال 0..255 .
    4.  يتم تحويل القيمة من 0..255 إلى 1501..4499 بإستدعاء الدالة map .
    5.  وأخيرا يتم تخزين القيمة الجديدة للقناة في Servo_Buffer ليتم اخراجها على هيئة PWM للـ Flight Controller .

        ملاحظة : ميزة الـ V-tail mixer تحتاج قناتين محددتين لتقوم بعمل المزج بينهم و عادتا تكون القناة الأولى هي pitch
        أما القناة الثانية فيتم تحديدها من قبل المستخدم و تكون عادتا roll أو yaw و في اعلى البرنامج ستجد الموجه التالي تستطيع أن تحدد من خلاله القناة التي ترغب بإدخالها على المازج
        #define V_TAIL ROLL // ROLL or YAW
         و إن كان Normal Mode فسيتم العمل بالترتيب التالي :
        1.  إستدعاء الدالة subtrim لكل قناة على حدى بإستثناء القناة throttle
        2.  يتم تحويل القيمة من 0..255 إلى 1501..4499 بإستدعاء الدالة map 
        3.  أخيرا يتم تخزين القيمة الجديدة للقناة في Servo_Buffer ليتم اخراجها على هيئة PWM للـ Flight Controller
          في حال انقضاء 40ms لعدم وصول أي بيانات جديدة يتم استدعاء الدالة load_failsafe_values و يبداء الباعث الضوئي الأخضر بالوميض بشكل بطيء أو بشكل سريع ان تم وصول بيانات جديدة .



           



          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 */
          
           
          و هذا فيديو يوضح تجربة الـ 4ch RC Receiver



          رابط تحميل البرنامج  Protocol_V202_RX_ver1.0

          وهكذا لكل بداية نهاية ، وخير العمل ما حسن آخره وخير الكلام ما قل ودل وبعد هذا الجهد المتواضع أتمنى أن أكون موفقا في شرحي لهذا المشروع موضحا أهم التفاصيل لهذا الموضوع الشائق الممتع ، وفقني الله وإياكم لما فيه صالحنا جميعا .

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

          إرسال تعليق