نحوه همگام سازی دو محرک خطی با استفاده از آردوینو

حرکت همزمان بین محرک های خطی mutliple می تواند برای موفقیت برخی از برنامه های مشتری حیاتی باشد ، یکی از آنها متشکل از دو محرک خطی است که یک درب ورودی را باز می کند. برای دستیابی به این هدف ما توصیه می کنیم از Firgelli اختصاصی استفاده کنید جعبه کنترل همزمان FA-SYNC-2 و FA-SYNC-4. با این حال برخی از DIY ها و هکرها آزادی یک میکروکنترلر مانند آردوینو را ترجیح می دهند و ترجیح می دهند برنامه کنترل همزمان خود را بنویسند. این آموزش به منظور ارائه یک نمای کلی در مورد چگونگی دستیابی به این هدف با استفاده از محرک خطی سری نوری.

پیش گفتار

این آموزش یک روش دقیق در مورد مراحل لازم برای دستیابی به کنترل همزمان با Arduino نیست ، بلکه یک نمای کلی برای کمک به شما در نوشتن برنامه سفارشی خود است. این آموزش پیشرفته است و فرض می کند شما از قبل با سخت افزار ، نرم افزار آردوینو آشنا هستید و در حالت ایده آل تجربه سیگنال های تعدیل عرض پالس (PWM) ، روال سرویس قطع (ISR) ، رفع اشکال سنسورها و رمزگذارهای موتور را دارید. مثالی که در این آموزش ارائه شده است یک کنترل کننده متناسب ابتدایی است. بسیاری از پیشرفت ها را می توان در مثال زیر پیاده سازی کرد ، از جمله ، محدود به این نیست: اجرای حلقه کنترل PID و مقیاس بندی به بیش از دو محرک خطی. لطفاً توجه داشته باشید که ما منابعی برای پشتیبانی فنی برنامه های آردوینو نداریم و نمودارهای سیم کشی ، ویرایش ، ارائه کد یا سیم کشی را خارج از این آموزشهای عمومی که ارائه می دهیم ، برطرف نمی کنیم.

بررسی اجمالی کنترل همزمان

کنترل همزمان با مقایسه طول دو محرک خطی و تنظیم متناسب سرعت انجام می شود. اگر یک محرک سریعتر از دستگاه دیگر شروع به حرکت کند ، سرعت آن را کم خواهیم کرد. ما می توانیم موقعیت محرک خطی را از طریق رمزگذار نوری داخلی بخوانیم. رمزگذار نوری یک دیسک پلاستیکی کوچک است که دارای 10 سوراخ در آن است و به موتور DC متصل است به گونه ای که هنگام چرخش موتور دیسک پلاستیک نیز کار می کند. یک LED مادون قرمز به سمت دیسک پلاستیکی هدایت می شود تا هنگام چرخش نور ، از طریق سوراخ های دیسک نوری منتقل شود یا توسط پلاستیک دیسک مسدود شود. یک سنسور مادون قرمز در سمت دیگر دیسک هنگام انتقال نور از سوراخ را تشخیص می دهد و یک سیگنال موج مربعی را خارج می کند. با شمارش تعداد پالس های گیرنده ، ما می توانیم RPM موتور و مسافتی را که محرک خطی طی کرده است محاسبه کنیم. محرک خطی نوری 35 پوندی دارای 50 پالس نوری در هر اینچ سفر است در حالی که محرک های 200 پوندی و 400 پوندی هر دو دارای 100 پالس در هر اینچ هستند. با مقایسه اینکه هر محرک خطی تا کجا امتداد داشته است ، می توانیم سرعت دو محرک را متناسب تنظیم کنیم تا هنگام کشش همیشه در همان طول بمانند.

قطعات مورد نیاز

نمودار سیم کشی

نحوه همگام سازی دو محرک خطی با استفاده از آردوینو

اتصالات سیم کشی فوق را ایجاد کنید. همیشه رنگهای سیم را که از محرک خطی خارج می شوند بررسی کنید زیرا ممکن است رنگ آمیزی از آنچه در نمودار بالا نشان داده شده تغییر کند.

    آموزش سریع

    اگر فقط می خواهید دو محرک خطی خود را به صورت همزمان حرکت دهید ، به سادگی این مراحل را دنبال کنید:

    • اتصالات را همانطور که در نمودار سیم کشی نشان داده شده است ایجاد کنید.
    • اولین برنامه را در زیر بارگذاری و اجرا کنید.
    • دو مقدار خروجی توسط این برنامه را در خط 23 برنامه دوم ، در زیر کپی کنید.
    • برنامه دوم را بارگذاری و اجرا کنید.
    • با تغییر دادن متغیر K_p (خط 37 ، برنامه دوم) سیستم خود را دقیق تنظیم کنید. این کار به راحتی با اتصال پتانسیومتر به پین ​​آنالوگ A0 و تغییر کد برای خواندن پتانسیومتر و استفاده از تابع map () انجام می شود: K_p = map (analogRead (A0)، 0، 1023، 0، 20000)؛

    بقیه این آموزش با جزئیات بیشتر به برخی از ویژگیهای اصلی برنامه ها می پردازد. باز هم تکرار می کنیم که این یک آموزش جامع نیست ، بلکه یک نمای کلی از مواردی است که باید هنگام ایجاد برنامه خود در نظر بگیرید.

    بررسی اجمالی برنامه کالیبراسیون

    قبل از دستیابی به کنترل همزمان ، ابتدا باید سیستم را کالیبره کنیم. این شامل شمارش تعداد پالس ها در هر چرخه تحریک است زیرا همانطور که در مشخصات محصول ذکر شده تحمل (+/- 5) پالس در هر اینچ سفر وجود دارد. برنامه را در زیر بارگذاری و اجرا کنید. این برنامه محرک ها را به طور کامل جمع می کند (خط 53) و متغیر شمارنده پالس نوری را روی صفر قرار می دهد و سپس کاملا کشیده می شود و کاملاً جمع می شود (به ترتیب خط 63 و 74). در طول این چرخه تحریک تعداد پالس ها توسط روال سرویس قطع (ISR) ، خط 153 و 166 محاسبه می شوند. پس از اتمام چرخه تحریک ، تعداد متوسط ​​پالس ها خارج می شوند ، خط 88 ، این مقادیر را برای بعد یادداشت کنید.

    https://gist.github.com/Will-Firgelli/89978da2585a747ef5ff988b2fa53904

    COPY
    /* Written by Firgelli Automations
     * Limited or no support: we do not have the resources for Arduino code support
     * This code exists in the public domain
     * 
     * Program requires two (or more) of our supported linear actuators:
     * FA-OS-35-12-XX
     * FA-OS-240-12-XX
     * FA-OS-400-12-XX
     * Products available for purchase at https://www.firgelliauto.com/collections/linear-actuators/products/optical-sensor-actuators
     */
    
    #include <elapsedMillis.h>
    elapsedMillis timeElapsed;
    
    #define numberOfActuators 2 
    int RPWM[numberOfActuators]={6, 11}; //PWM signal right side
    int LPWM[numberOfActuators]={5,10}; 
    int opticalPins[numberOfActuators]={2,3}; //connect optical pins to interrupt pins on Arduino. More information: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
    volatile long lastDebounceTime_0=0; //timer for when interrupt was triggered
    volatile long lastDebounceTime_1=0;
    
    int Speed = 255; //choose any speed in the range [0, 255]
    
    #define falsepulseDelay 20 //noise pulse time, if too high, ISR will miss pulses. 
    volatile int counter[numberOfActuators]={}; 
    volatile int prevCounter[numberOfActuators]={}; 
    int Direction; //-1 = retracting
                   // 0 = stopped
                   // 1 = extending
    
    int extensionCount[numberOfActuators] = {}; 
    int retractionCount[numberOfActuators] = {}; 
    int pulseTotal[numberOfActuators]={}; //stores number of pulses in one full extension/actuation
    
    void setup(){
      for(int i=0; i<numberOfActuators; i++){
        pinMode(RPWM[i],OUTPUT);
        pinMode(LPWM[i], OUTPUT);
        pinMode(opticalPins[i], INPUT_PULLUP);
        counter[i]=0; //initialize variables as array of zeros
        prevCounter[i]=0;
        extensionCount[i] = 0;
        retractionCount[i] = 0;
        pulseTotal[i] = 0;
      }
      attachInterrupt(digitalPinToInterrupt(opticalPins[0]), count_0, RISING);
      attachInterrupt(digitalPinToInterrupt(opticalPins[1]), count_1, RISING); 
    
      Serial.begin(9600);
      Serial.println("Initializing calibration");
      Serial.println("Actuator retracting...");
      Direction = -1;
      moveTillLimit(Direction, 255); 
      Serial.println("Actuator fully retracted");
      delay(1000);
    
      for(int i=0; i<numberOfActuators; i++){
        Serial.print("\t\t\t\tActuator ");
        Serial.print(i);
      }
    
      Direction = 1;
      moveTillLimit(Direction, 255); //extend fully and count pulses
      Serial.print("\nExtension Count:");
      for(int i=0; i<numberOfActuators; i++){
      extensionCount[i]=counter[i];
      Serial.print("\t\t"); 
      Serial.print(extensionCount[i]);
      Serial.print("\t\t\t"); 
      }
      delay(1000);
    
      Direction = -1;
      moveTillLimit(Direction, 255); //retract fully and count pulses
      Serial.print("\nRetraction Count:");
      for(int i=0; i<numberOfActuators; i++){
      retractionCount[i]=counter[i];
      Serial.print("\t\t"); 
      Serial.print(abs(retractionCount[i]));
      Serial.print("\t\t\t"); 
      }
      Serial.print("\n");
    
      for(int i=0; i<numberOfActuators; i++){
        Serial.print("\nActuator ");
        Serial.print(i);
        Serial.print(" average pulses: ");
        pulseTotal[i]=(extensionCount[i]+abs(retractionCount[i]))/2; //takes the average of measurements
        Serial.print(pulseTotal[i]); 
      } 
      Serial.println("\n\nEnter these values in the synchronous control progam.");
    }
    
    void loop() { 
    }
    
    void moveTillLimit(int Direction, int Speed){
      //this function moves the actuator to one of its limits
      for(int i = 0; i < numberOfActuators; i++){
        counter[i] = 0; //reset counter variables
        prevCounter[i] = 0;
      } 
      do {
        for(int i = 0; i < numberOfActuators; i++) {
          prevCounter[i] = counter[i];
        }
        timeElapsed = 0;
        while(timeElapsed < 200){ //keep moving until counter remains the same for a short duration of time
          for(int i = 0; i < numberOfActuators; i++) {
            driveActuator(i, Direction, Speed);
          }
        }
      } while(compareCounter(prevCounter, counter)); //loop until all counts remain the same
    }
    
    bool compareCounter(volatile int prevCounter[], volatile int counter[]){
      //compares two arrays and returns false when every element of one array is the same as its corresponding indexed element in the other array
      bool areUnequal = true;
      for(int i = 0; i < numberOfActuators; i++){
        if(prevCounter[i] == counter[i]){
          areUnequal = false;
        } 
        else{ //if even one pair of elements are unequal the entire function returns true
          areUnequal = true;
          break;
        }
      }
      return areUnequal;
    }
    
    void driveActuator(int Actuator, int Direction, int Speed){
      int rightPWM=RPWM[Actuator];
      int leftPWM=LPWM[Actuator];
    
      switch(Direction){
      case 1: //extension
        analogWrite(rightPWM, Speed);
        analogWrite(leftPWM, 0);
      break;
    
      case 0: //stopping
        analogWrite(rightPWM, 0);
        analogWrite(leftPWM, 0);
      break;
    
      case -1: //retraction
        analogWrite(rightPWM, 0);
        analogWrite(leftPWM, Speed);
      break;
      }
    }
    
    void count_0(){
      //This interrupt function increments a counter corresponding to changes in the optical pin status
      if ((millis() - lastDebounceTime_0) > falsepulseDelay) { //reduce noise by debouncing IR signal 
        lastDebounceTime_0 = millis();
        if(Direction==1){
          counter[0]++;
        }
        if(Direction==-1){
          counter[0]--;
        }
      }
    }
    
    void count_1(){
      if ((millis() - lastDebounceTime_1) > falsepulseDelay) { 
        lastDebounceTime_1 = millis();
        if(Direction==1){
          counter[1]++;
        }
        if(Direction==-1){
          counter[1]--;
        }
      }
    }

    بررسی اجمالی برنامه همزمان

    قبل از بارگذاری برنامه کنترل همزمان ، ابتدا باید مقادیر خروجی توسط برنامه کالیبراسیون را در خط 23 کپی کرده و آرایه فعلی را جایگزین کنید: {908 ، 906} با مقادیر خاص خود. بعلاوه ، اگر از محرک خطی 35lb استفاده می کنید باید مقدار متغیر موجود در خط 29 را از 20 میلی ثانیه به 8 میلی ثانیه تغییر دهید.

    پس از یک بار جمع شدن کامل (برای شناسایی مبدا) می توانید با فشار دادن سه دکمه مربوط به دستورات پسوند ، جمع و توقف ، هر دو محرک خطی را به طور همزمان حرکت دهید. محرک ها با مقایسه شمارنده های پالس نسبی خود و تنظیم سرعت بین آنها ، همزمان تحت بارهای ناهموار در حالت همزمان باقی می مانند تا همیشه در حالت همزمان باقی بمانند. به شما توصیه می شود که برنامه فعلی یک کنترل کننده متناسب ساده را اجرا می کند ، خط 93 ، به همین دلیل در معرض تعویض و تعادل قرار دارد. می توانید با تغییر دادن متغیر K_p ، که در خط 37 تعریف شده است ، این تنظیم را انجام دهید. این کار به راحتی با اتصال پتانسیومتر به پین ​​آنالوگ A0 و تغییر کد برای خواندن پتانسیومتر و استفاده از تابع map () انجام می شود: K_p = map (analogRead (A0)، 0، 1023، 0، 20000)؛

    برای بهترین نتیجه ، ما قویاً پیشنهاد می کنیم کنترل کننده تناسبی را حذف کرده و یک حلقه کنترل PID را پیاده سازی کنید. اما این از حوصله این آموزش مقدماتی خارج است و به عمد حذف شده است.

    https://gist.github.com/Will-Firgelli/44a14a4f3cac3209164efe8abe3285b6

    COPY
    /* Written by Firgelli Automations
     * Limited or no support: we do not have the resources for Arduino code support
     * This code exists in the public domain
     * 
     */
    
    #include <elapsedMillis.h>
    elapsedMillis timeElapsed;
    
    #define numberOfActuators 2       
    int downPin = 7;
    int stopPin = 8;
    int upPin = 9;        
    int RPWM[numberOfActuators]={6, 11};                                  //PWM signal right side
    int LPWM[numberOfActuators]={5,10};        
    int opticalPins[numberOfActuators]={2,3};                             //connect optical pins to interrupt pins on Arduino. More information: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
    volatile unsigned long lastDebounceTime[numberOfActuators]={0,0};     //timer for when interrupt is triggered
    int pulseTotal[numberOfActuators]={908, 906};                         //values found experimentally by first running two-optical-actuators-sync-calibration.ino
    
    int desiredSpeed=255;                            
    int adjustedSpeed;
    int Speed[numberOfActuators]={};    
    
    #define falsepulseDelay 20                                            //noise pulse time, if too high, ISR will miss pulses. If using 35lb actuator, set to 8ms                          
    volatile int counter[numberOfActuators]={};   
    volatile int prevCounter[numberOfActuators]={};     
    volatile float normalizedPulseCount[numberOfActuators]={};
    int Direction;                                                        //-1 = retracting
                                                                          // 0 = stopped
                                                                          // 1 = extending
    float error;
    int K_p=12000;                                                        //optimized experimentally. adjust this to fine tune your system
    int laggingIndex, leadingIndex;                                       //index of the slowest/fastest actuator
    
    void setup(){ 
      pinMode(stopPin, INPUT_PULLUP);
      pinMode(downPin, INPUT_PULLUP);
      pinMode(upPin, INPUT_PULLUP);
      for(int i=0; i<numberOfActuators; i++){
        pinMode(RPWM[i],OUTPUT); 
        pinMode(LPWM[i], OUTPUT);
        pinMode(opticalPins[i], INPUT_PULLUP);
        Speed[i]=desiredSpeed;
      }
      attachInterrupt(digitalPinToInterrupt(opticalPins[0]), count_0, RISING);
      attachInterrupt(digitalPinToInterrupt(opticalPins[1]), count_1, RISING);  
      Serial.begin(9600);
      Serial.println("Calibrating the origin");
      Serial.println("Actuator retracting...");
      Direction = -1;
      moveTillLimit(Direction, 255);  
      for(int i=0; i<numberOfActuators; i++){
        counter[i]=0;                                                     //reset variables 
        prevCounter[i]=0;
        normalizedPulseCount[i] = 0;
      }
      delay(1000);
      Serial.println("Actuator fully retracted");
    } 
    
    void loop() {  
      checkButtons();
    
      if(Direction==1){                                                   //based on direction of motion identify the leading and lagging actuator by comparing pulse counts
        if(normalizedPulseCount[0] < normalizedPulseCount[1]){
          laggingIndex = 0; 
          leadingIndex = 1;
        }
        else{
          laggingIndex = 1;
          leadingIndex = 0;
        }
      }
      else if(Direction==-1){
        if(normalizedPulseCount[0] > normalizedPulseCount[1]){
          laggingIndex = 0;
          leadingIndex = 1;
        }
        else{
          laggingIndex = 1;
          leadingIndex = 0;
        }
      }
      
      error=abs(normalizedPulseCount[laggingIndex]-normalizedPulseCount[leadingIndex]);
      if(Direction!=0){       
        adjustedSpeed=desiredSpeed-int(error*K_p);                 
        Speed[leadingIndex]=constrain(adjustedSpeed, 0, 255);               //slow down fastest actuator
        Speed[laggingIndex]=desiredSpeed;
      }
      for(int i=0; i<numberOfActuators; i++){
        Serial.print("  ");
        Serial.print(Speed[i]);
        Serial.print("  ");
        Serial.print(normalizedPulseCount[i]*1000);
        driveActuator(i, Direction, Speed[i]);
      }
      Serial.println();
    }
    
    void checkButtons(){
      //latching buttons: direction remains the same when let go
      if(digitalRead(upPin)==LOW){ Direction=1; }                           //check if extension button is pressed
      if(digitalRead(downPin)==LOW){ Direction=-1; }  
      if(digitalRead(stopPin)==LOW){ Direction=0; }
    }
    
    void moveTillLimit(int Direction, int Speed){
      //function moves the actuator to one of its limits
      for(int i = 0; i < numberOfActuators; i++){
        counter[i] = 0;                                                     //reset counter variables
        prevCounter[i] = 0;
      }  
      do {
        for(int i = 0; i < numberOfActuators; i++) {
          prevCounter[i] = counter[i];
        }
        timeElapsed = 0;
        while(timeElapsed < 200){                                           //keep moving until counter remains the same for a short duration of time
          for(int i = 0; i < numberOfActuators; i++) {
            driveActuator(i, Direction, Speed);
          }
        }
      } while(compareCounter(prevCounter, counter));                        //loop until all counters remain the same
    }
    
    bool compareCounter(volatile int prevCounter[], volatile int counter[]){
      //compares two arrays and returns false when every element of one array is the same as its corresponding indexed element in the other array
      bool areUnequal = true;
      for(int i = 0; i < numberOfActuators; i++){
        if(prevCounter[i] == counter[i]){
          areUnequal = false;
        } 
        else{                                                               //if even one pair of elements are unequal the entire function returns true
          areUnequal = true;
          break;
        }
      }
      return areUnequal;
    }
    
    void driveActuator(int Actuator, int Direction, int Speed){
      int rightPWM=RPWM[Actuator];
      int leftPWM=LPWM[Actuator];
      switch(Direction){
        case 1:       //extension
          analogWrite(rightPWM, Speed);
          analogWrite(leftPWM, 0);
          break;  
        case 0:       //stopping
          analogWrite(rightPWM, 0);
          analogWrite(leftPWM, 0);
          break;
        case -1:      //retraction
          analogWrite(rightPWM, 0);
          analogWrite(leftPWM, Speed);
          break;
      }
    }
    
    void count_0(){
      //This interrupt function increments a counter corresponding to changes in the optical pin status
      if ((millis() - lastDebounceTime[0]) > falsepulseDelay) {             //reduce noise by debouncing IR signal with a delay
        lastDebounceTime[0] = millis();
        if(Direction==1){
          counter[0]++;
        }
        if(Direction==-1){
          counter[0]--;
        }
        normalizedPulseCount[0]=float(counter[0])/float(pulseTotal[0]);
      }
    }
    
    void count_1(){
      if ((millis() - lastDebounceTime[1]) > falsepulseDelay) {   
        lastDebounceTime[1] = millis();
        if(Direction==1){
          counter[1]++;
        }
        if(Direction==-1){
          counter[1]--;
        }
        normalizedPulseCount[1]=float(counter[1])/float(pulseTotal[1]);
      } 
    }

    استفاده از محرکهای Bullet 36 و Bullet 50 بصورت همزمان

    علاوه بر محرک خطی سری Optical ، ما همچنین دو محرک خطی با رمزگذار داخلی را ارائه می دهیم: Bullet 36 Cal. و Bullet 50 Cal که هر دو دارای رمزگذار داخلی Hall Effect هستند. رمزگذار Hall Effect بر همان اصل رمزگذار نوری کار می کند اما به جای استفاده از نور از مغناطیس استفاده می کند. بعلاوه اینکه یک رمزگذار درجه حرارت است ، دارای دو خروجی سیگنال است که هر کدام 90 درجه از فاز خارج می شوند. به همین ترتیب شما باید از یک برد آردوینو با 4 یا بیشتر پین وقفه استفاده کنید (Arduino Uno فقط دو عدد دارد) و کد را برای پردازش ورودی از دو سیگنال در هر محرک تغییر دهید. علاوه بر این ، متغیر زمان debounse ، falsepulseDelay باید همراه با K_p تنظیم شود.

    نکاتی برای نوشتن برنامه خود

    بیش از دو محرک خطی

    هنگام استفاده از دو یا چند محرک خطی ، Arduino Uno دیگر کار نمی کند زیرا فقط دو پایه قطع دارد. شما باید از یک صفحه آردوینو با تعداد نامعلوم پین های وقفه در دسترس استفاده کنید ، اطلاعات بیشتر: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

    در مرحله دوم به منظور بهره وری توصیه می شود برنامه نویسی خود را با استفاده از آرایه ها و برای حلقه های () تکرار در هر محرک برنامه نویسی کنید.

    برکنار کردن

    همانطور که با بسیاری از سنسورها مهم است که شناخت سیگنال های تندرست. همانطور که با سوئیچ های مکانیکی, encoders نیز می تواند از تندرست رنج می برند. در مثال بالا فرایند debouncing شده است با تاخیر ساده (تعریف شده توسط متغیر falsepulseDelay) ، مهم است که رسیدگی به این در هر گونه تغییرات نرم افزار شما را و یا با مدار فیزیکی برای فیلتر کردن نویز تندرست.

    دست زدن به رول بیش از

    اگر شما تغییر کد آگاه باشید از rollover در هنگام برخورد با میلیس() تابع. هر دو میلی () و آرایه lastDebounceTime به عنوان متغیرهای طولانی امضا نشده اعلام می شوند به این معنی که می توانند مقادیر را تا ۴٬۲۹۴٬۹۶۷٬۲۹۵ (۳۲^۲-۱) ذخیره کنند. این ترجمه به تقریبا یک دوره rollover از 49.7 روز. برنامه فعلی طراحی شده است برای رسیدگی به rollover در ISR (وقفه روال خدمات) توابع : count_0 & count_1 ، با این حال اگر شما تغییر این برنامه مطمئن شوید که به درستی رسیدگی به rollover متغیر ، دیگر برنامه خود را پس از ~ 49.7 روز استفاده مداوم سقوط خواهد کرد. برای کسب اطلاعات بیشتر به این افراد اشاره کنید: https://www.norwegiancreations.com/2018/10/arduino-tutorial-avoiding-the-overflow-issue-when-using-millis-and-micros/

     

    Share This Article
    Tags: