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

08‏/06‏/2015

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


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

كنت قد عرضت تصميم لـ 8Ch RC Transmitter and Receiver في تدوينة سابقة بإستخدام المديول RFM22B و دخله عبارة عن إشارة PPM ما يضطرك لشراء جهاز التحكم أو بنائه , و قد وصلتني بعض الرسائل من متابعين المدونة تفيد بأنهم لا يستطيعون شراء المديول بسبب القوانين الجمركية في بلادنا العربية .
و وعدتهم بأن اقوم بعمل تصميم جديد يعتمد على المديول NRF24L01 وهو متوفر بكثرة في الدول العربية .  
بعدها هممت ببناء مستقبل للبروتوكول V202 لوجود الـ RC Transmitter لدي و بعد نجاحه فكرت بناء RC Transmitter ليكون المشروع متكامل ,  
الفيديو التالي اختبار للمستقبل الخاص ببروتوكول V202 


 
4ch rc receiver

4ch rc receiver

مقدمة : 

لا شك انك سمعت عن أو شاهدت طائرات Micro Quadcopter و شركة WLToys من الشركات الرائدة في صناعة هذا النوع من الطائرات باللإضافة إلى الـ Helicopter أيضا والجدير بالذكر ان جميع اجهزة التحكم لهذه الطائرات ذات الرقم V202 تعمل بنفس البروتوكول

في موضوعنا لهذا اليوم سنتكلم عن طريقة عمل هذا البروتوكول بالتفصيل الممل لتكون لديكم ارضية جيدة عن كيفية بناء و تصميم أجهزة الإرسال و سنقدم مثال بسيط على بناء مرسل RC باربع قنوات متوافق مع بروتوكول V202و في تدوينة لاحقة سأقوم بعمل تصميم لـ 8Ch RC Transmitter and Receiver بمواصفات عالية إن شاء الله .
 

1- ما المقصود بكلمة بروتوكول Protocol :

البروتوكول هو عبارة عن لغة التفاهم بين أجهزة الإلكترونية. حيث أنه إذا كان هناك اختلاف في نوع البروتوكول المستخدم بين الأجهزة فإنه لن يتم التخاطب ونقل البيانات فيما بينها وذلك لأن الأوامر الصادرة من كلا الجهازين لن يتم التعرف عليها من قبل الطرف الآخر. ويمكننا تشبيه البروتوكول باللغة عند الإنسان فلو مثلاً شخص يتكلم العربية ويريد أن يتكلم مع شخص آخر و لكن هذا الشخص لا يعرف العربية و إنما يعرف لغة أخرى غيرها و ليكن مثلاً اللغة الإنجليزية . في هذه الحالة عندما يتحدث أحد 
هاذين الطرفين لن يتم فهمه من قبل الطرف الآخر وذلك لإختلاف عنصر التوحيد بين اللغتين. أما إذا كان كلا الطرفين يتحدثون اللغة نفسها فسيسهل على كل طرف فهم حديث الآخر. كذ لك هو الحال في أنظمة الإلكترونية.

2- مقدمة عن بروتوكول V202 :

من اين أتت تسمية البروتوكول بـ V202 : شركة WLToys تعتمد هذا الجهاز في جميع طائراتها ذات البادئة V2 و هي طائرات من النوع Micro Quadcopter  وهذه قائمة بأرقام الموديلات المصنعة من قبل شركة WLToys
V202/V212/V222/V232/V242/V252/V262/V272/V282/V292
كما تلاحظ فإن الفارق الوحيد بين أرقام الموديلات هو الرقم الأوسط لذالك وقع اختياري على التسمية V202
و طبعا هناك اختلاف بين الموديلات مثلا حجم الطائرة و مع أو بدون كمرة .

3- كيف يعمل بروتوكول V202 :

ما اريد ان تعلموه جيدا عن هذا البروتوكول انه :
  • لكل جهاز تحكم رقم فريد ID مؤلف من 3 بايت تحدده الشركة المصنعة .
  • يعمل على التردد 2.4GHz ISM Frequency Range و لضمان عدم التداخل مع اجهزة اخرى يختار 16 قناة للعمل عليهم بناء على رقم الـ ID المخصص له .
  • دعم ميزة Auto ID في الـ Receiver وهذا يعني ان جميع الطائرات بإمكانها العمل على نفس الجهاز .
  •  بعد ارسال حزمة البيانات يقوم بالقفز إلى التردد التالي من القنوات المحددة له وتسمى هذه التقنية بـ FHSS
  • حجم البايتات المرسلة في الحزمة الواحدة هو 16 بايت .
  • يتم ترتيب حزمة البيانات بحسب الجدول التالي بحيث كل حقل من index يمثل 1 بايت :


15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0
Index
Sum
Flags
empty
TX id
Trims
Regular packet
Struct
var
bits
3
2
1
0
2
1
0
roll
pitch
yaw
roll
pitch
yaw
throttle
Data
?
?
0x00
0..0xFFFFFF
-64 .. 64
-127..127
0..255
Value

 

4- تشغيل الجهاز

قبل الخوض في الأكواد البرمجية للبروتوكول دعونا نلقي نظرة سريعة على الإجراءات الواجب التقيد بها عند تشغيل الجهاز :
  1. قبل تشغيل الجهاز يجب إنزال عصى throttle للاسفل و بعدها تقوم بتشغيل الجهاز .
  2. بعد تشغيل الجهاز ارفع عصى throttle للأخير لعمل binding  بمعنى أخر موالفة بين الجهاز و الطائرة .
  3. ستسمع صوت Beep يعلمك بانه تم الإتصال بين الجهاز و الطائرة وأصبحت الطائرة جاهزة للطيران .

أظن أنه أصبح لدينا فكرة أولية عن ما يجري وراء الكواليس و بإمكاننا الأن البدء بكتابة الكود من لحظة تشغيل الجهاز إلى عملية  bind

سنقوم حاليا بالبدء بتصميم بسيط ل  4Ch Transmitter حتى يسهل عليكم فهمه و بعدها سننتقل لمستوى أعلى إن شاء الله

4Ch Transmitter circuit

ملاحظة : لمن يريد معرفة طريقة التعامل برمجيا مع المديول NRF24L01 و فهمها بشكل صحيح فأنصحه بمشاهدة سلسلة الدروس هذه وهي باللغى العربية  Wireless Communication Tutorial Series

الأن ننتقل إلى كود المرسل
أولا حمل الكود من هــــنـــا

/************************************************************
 ** V202 protocol 4ch Transmitter
 ** By : Andullah Jalloul (2015-06)
 ** Website: http://horizon4electronic.blogspot.com
 ************************************************************/

#include
#include "nRF24L01.h"
#include "V202.h"

#define THROTTLE_AI_Pin A0
#define YAW_AI_Pin      A1
#define ROLL_AI_Pin     A2
#define PITCH_AI_Pin    A3


#define CE_PIN  9
#define CSN_PIN 10

enum {
  THROTTLE,
  YAW,
  ROLL,
  PITCH
};

enum {
  FLAG_CAMERA = 0x01,
  FLAG_VIDEO  = 0x02,
  FLAG_FLIP   = 0x04,
  FLAG_UNK9   = 0x08,
  FLAG_LED    = 0x10,
  FLAG_UNK10  = 0x20,
  FLAG_BIND   = 0xC0
};

const int joy_pin[] = { THROTTLE_AI_Pin, YAW_AI_Pin, ROLL_AI_Pin, PITCH_AI_Pin };

uint8_t txid[3] = { 0x00, 0x00, 0x00 };


nRF24 radio(CE_PIN, CSN_PIN);
V202_TX tx(radio);

uint8_t throttle, flags;
int8_t yaw , pitch, roll;
int counter = 0;
int direction = 1;
bool bind = true;
bool calibrated = false;

struct {
  int iVal;
  int iMin;
  int iMax;
}
joyLimit[4];
/************************************************************/

void calibrate() {
  for(byte n = 0; n < 4; n++) {
    joyLimit[n].iMin = 150;
    joyLimit[n].iMax = 600;
  }
}
/************************************************************/

void initInput()
{
  pinMode(THROTTLE_AI_Pin, INPUT);
  pinMode(YAW_AI_Pin, INPUT);
  pinMode(ROLL_AI_Pin, INPUT);
  pinMode(PITCH_AI_Pin, INPUT);
  calibrate();
}
/************************************************************/

bool readInput() {
  bool changed = false;
  long a;

  for(byte n = 0; n < 4; n++) {
    a = analogRead(joy_pin[n]);
    if (a < joyLimit[n].iMin) joyLimit[n].iMin = a;
    if (a > joyLimit[n].iMax) joyLimit[n].iMax = a;
    a = (a-joyLimit[n].iMin)*255/(joyLimit[n].iMax-joyLimit[n].iMin);
    if (a != joyLimit[n].iVal) {
      changed = true;
      joyLimit[n].iVal = a;
    }
  }

  return changed;
}
/************************************************************/

void setup()
{
  initInput();
  readInput();
  Serial.begin(115200);
  tx.setTXId(txid); 
  tx.begin();


  Serial.write("Reading status\n");
  uint8_t res = radio.read_register(STATUS);
  Serial.write("Result: ");
  Serial.print(res);
  Serial.write("\n");
}
/************************************************************/

void loop()
{
  bool changed = readInput();
  //  if (changed) {
  Serial.write("sticks: ");
  for(byte n = 0; n < 4; n++) {
    Serial.print(joyLimit[n].iVal);
    Serial.write(" ");
  }
  Serial.write("\n");
  //  }
  if (bind) {
    throttle = joyLimit[THROTTLE].iVal;
    flags = FLAG_BIND;

    if (direction > 0) {
      if (throttle >= 255) direction = -1;
    }
    else {
      if (throttle == 0) {
        direction = 1;
        counter = 0;
        bind = false;
        flags = 0;
        Serial.write("Bound\n");
        for(byte n = 0; n < 4; n++) {
          Serial.write("min " );
          Serial.write(n);
          Serial.write(":");
          Serial.write(joyLimit[n].iMin);
          Serial.write(" max ");
          Serial.write(n);
          Serial.write(":");
          Serial.write(joyLimit[n].iMax);
          Serial.write("\n");
        }

      }
    }
  }
  else {
    throttle = joyLimit[THROTTLE].iVal;
    yaw = joyLimit[YAW].iVal < 0x80 ? 0x7f - joyLimit[YAW].iVal : joyLimit[YAW].iVal;
    roll = joyLimit[ROLL].iVal < 0x80 ? 0x7f - joyLimit[ROLL].iVal : joyLimit[ROLL].iVal ;
    pitch = joyLimit[PITCH].iVal < 0x80 ? 0x7f - joyLimit[PITCH].iVal : joyLimit[PITCH].iVal;

    // Blinking LED lights
    counter += direction;
    if (direction > 0) {
      if (counter > 255) {
        direction = -1;
        counter = 255;
        flags = FLAG_LED;
      }
    }
    else {
      if (counter < 0) {
        direction = 1;
        counter = 0;
        flags = 0x00;
      }
    }

  }
  tx.command(throttle, yaw, pitch, roll, flags);
  delay(4);
}

/************************************************************/



شرح الكود :

قراءة قيم Joystick

Joystick

كلنا يعلم أن عصي التحكم Joystick تتكون من two potentiometers على كل من المحورين XY و لكل محور حد علوي و اخر سفلي و مهما كانت جودة الـ Joystick عالية فإنه من النادر ان تتوافق قيم الحد الأدنا و الأعلى لأي منهم مع Joystick اخرى بسبب نسبة الخطاء الموجودة في الـ potentiometer و حدود الحركة الميكانيكية .
تعمل الدالة calibrate على تحديد القيم الإفتراضية العالية و المنخفضة لعصي التحكم ثم تعمل الدالة readInput على قراءة القيم الحالية لموضع العصي و إعادة تحديد القيم العليا و المنخفضة لكل محور على حدى و بعدها تقوم بحساب القيمة الموافقة لموقع العصى مع الأخذ بالحسبان القيمة العليا و المنخفضة و بذالك نحصل على قيم مستقرة عندما تكون العصى في المنتصف .
مما ذكر نفهم انه يتوجب علينا بعد كل تشغيل للجهاز ان نقوم بعمل calibration بتحريك العصي في جميع الإتجاهات ليقوم الكونترول بضبط القيم العليا و الدنيا.
ملاحظة :  في الأجهزة المتقدمة تتم هذه العملية مرة واحدة و يتم تخزين هذه القيم في الذاكرة EEPROM و عند اقلاع الجهاز يقوم بجلب هذه القيم من الذاكرة و بالتالي يغنيك عن عمل calibration في كل مرة تشغل فيها الجهاز ,لا تقلق ففي المقال القادم سيتم حل هذه المشكلة .

عملية binding

في بداية الكود قمت بتعريف المتحول bind و اسندت له القيمة true حتى أجبر المستخدم على عمل Binding عند تشغيل الجهاز و عرفت المتحول direction و اسندت القيمة 1 إليه لتحديد اتجاه عصى التحكم throttle و سيبقى الجهاز يرسل الأمر Bind حتى يتم رفع العصى throttle للأعلى و من ثم للأسفل كما هو واضح في الكود التالي :

if (bind) {
    throttle = joyLimit[THROTTLE].iVal;
    flags = FLAG_BIND;

    if (direction > 0) {
      if (throttle >= 255) direction = -1;
    }
    else {
      if (throttle == 0) {
        direction = 1;
        counter = 0;
        bind = false;
        flags = 0;
        Serial.write("Bound\n");
        for(byte n = 0; n < 4; n++) {
          Serial.write("min " );
          Serial.write(n);
          Serial.write(":");
          Serial.write(joyLimit[n].iMin);
          Serial.write(" max ");
          Serial.write(n);
          Serial.write(":");
          Serial.write(joyLimit[n].iMax);
          Serial.write("\n");
        }

      }
    }
  }



إرسال البيانات :

بعد الإنتهاء من عمل Bind يبداء الجهاز بإسناد قيم عصي التحكم للمتحولات حسب الجدول السابق ذكره :

 throttle = joyLimit[THROTTLE].iVal;
yaw = joyLimit[YAW].iVal < 0x80 ? 0x7f - joyLimit[YAW].iVal : joyLimit[YAW].iVal; 
roll = joyLimit[ROLL].iVal < 0x80 ? 0x7f - joyLimit[ROLL].iVal : joyLimit[ROLL].iVal;
pitch = joyLimit[PITCH].iVal < 0x80 ? 0x7f - joyLimit[PITCH].iVal : joyLimit[PITCH].iVal;

و بعدها يقوم بتفعيل أو إلغاء تفعيل العلم FLAG_LED لتشغيل و اطفاء الأضواء في الطائرة كل 1 ثانية تقريبا و هذا الأمر للإختبار فقط .

    // Blinking LED lights
    counter += direction;
    if (direction > 0) {
      if (counter > 255) {
        direction = -1;
        counter = 255;
        flags = FLAG_LED;
      }
    }
    else {
      if (counter < 0) {
        direction = 1;
        counter = 0;
        flags = 0x00;
      }
    }

و اخيرا يرسل القيم إلى المستقبل
tx.command(throttle, yaw, pitch, roll, flags);

شرح مختصر لمكتبة V202

كيف تتم عملية frequency hopping وكيف يستطيع المرسل تحديد القنوات التي سيعمل عليها بناء على رقم الـ ID المخصص له :
في البداية يتوجب عليك تحديد رقم خاص للجهاز مؤلف من 3 بايت وستجد المصفوفة المخصصة له في بداية الكود

uint8_t txid[3] = { 0x00, 0x00, 0x00 };
تستطيع وضع أي قيمة بشرط أن لا تتجاوز 255 لكل بايت , وسيتم استدعاء الدالة setTXId لتقوم هي بدورها في حساب أرقام القنوات التي سيعمل عليها المرسل و ستضعهم في المصفوفة rf_channels و التي سيتم استخدامها لحقا في كل عملية ارسال للبيانات .

كود حساب القنوات المعمول بها بناء على الـ ID


// This is frequency hopping table for V202 protocol
// The table is the first 4 rows of 32 frequency hopping
// patterns, all other rows are derived from the first 4.
// For some reason the protocol avoids channels, dividing
// by 16 and replaces them by subtracting 3 from the channel
// number in this case.
// The pattern is defined by 5 least significant bits of
// sum of 3 bytes comprising TX id
uint8_t freq_hopping[][16] = {
 { 0x27, 0x1B, 0x39, 0x28, 0x24, 0x22, 0x2E, 0x36,
   0x19, 0x21, 0x29, 0x14, 0x1E, 0x12, 0x2D, 0x18 }, //  00
 { 0x2E, 0x33, 0x25, 0x38, 0x19, 0x12, 0x18, 0x16,
   0x2A, 0x1C, 0x1F, 0x37, 0x2F, 0x23, 0x34, 0x10 }, //  01
 { 0x11, 0x1A, 0x35, 0x24, 0x28, 0x18, 0x25, 0x2A,
   0x32, 0x2C, 0x14, 0x27, 0x36, 0x34, 0x1C, 0x17 }, //  02
 { 0x22, 0x27, 0x17, 0x39, 0x34, 0x28, 0x2B, 0x1D,
   0x18, 0x2A, 0x21, 0x38, 0x10, 0x26, 0x20, 0x1F }  //  03
};

void V202_TX::setTXId(uint8_t txid_[3])
{
  uint8_t sum;
  txid[0] = txid_[0];
  txid[1] = txid_[1];
  txid[2] = txid_[2];
  sum = txid[0] + txid[1] + txid[2];
  // Base row is defined by lowest 2 bits
  uint8_t (&fh_row)[16] = freq_hopping[sum & 0x03];
  // Higher 3 bits define increment to corresponding row
  uint8_t increment = (sum & 0x1e) >> 2;
  for (int i = 0; i < 16; ++i) {
    uint8_t val = fh_row[i] + increment;
    // Strange avoidance of channels divisible by 16
    rf_channels[i] = (val & 0x0f) ? val : val - 3;
  }
}


 الدالة tx.begin تقوم بتهيئة مسجلات المديول nRF24L01 و قد أشرت في البداية إلى كورس باللغة العربية للتعامل مع هذا المديول .

ارسال البيانات يتم بإستدعاء الدالة
void V202_TX::command(uint8_t throttle, int8_t yaw, int8_t pitch, int8_t roll, uint8_t flags)

في حال كان المتحول flags يحمل القيمة 0xC0 فإن الدالة command ستنفذ العملية binding و لن يتم ارسال اي قيم أخرى

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;
  } 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
  }
  // TX id
  buf[7] = txid[0];
  buf[8] = txid[1];
  buf[9] = txid[2];
  // empty
  buf[10] = 0x00;
  buf[11] = 0x00;
  buf[12] = 0x00;
  buf[13] = 0x00;
  //
  buf[14] = flags;
  uint8_t sum = 0;
  uint8_t i;
  for (i = 0; i < 15;  ++i) sum += buf[i];
  buf[15] = sum;

في هذا الكود تتم عملية frequency hopping قبل ارسال البيانات و لكن يتم القفز إلى القناة التالية بعد  كل عمليتي ارسال للبيانات كما هو واضح في الكود التالي 

  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);



  و أخيرا اتمنى من الله ان يكون هذا المقال ببساطته نافعا لكم و انتظروا المقال القادم لبناء 4ch rc receiver لهذا المشروع
 

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

إرسال تعليق