Come sincronizzare due attuatori lineari utilizzando un Arduino

Il movimento sincrono tra più attuatori lineari può essere vitale per il successo di alcune applicazioni dei clienti, una comune è costituita da due attuatori lineari che aprono una botola. Per ottenere ciò si consiglia di utilizzare l'apposito Firgelli scatola di controllo sincrona FA-SYNC-2 e FA-SYNC-4. Tuttavia alcuni fai-da-te e hacker preferiscono la libertà offerta da un microcontrollore come Arduino e preferiscono invece scrivere il proprio programma di controllo sincrono. Questo tutorial ha lo scopo di fornire una panoramica su come ottenere ciò utilizzando il Attuatore lineare serie Optical.

Prefazione

Questo tutorial non è un trattamento rigoroso dei passaggi necessari per ottenere il controllo sincrono con Arduino, piuttosto un'ampia panoramica per aiutarti a scrivere il tuo programma personalizzato. Questo tutorial è avanzato e presuppone che tu abbia già familiarità con l'hardware e il software Arduino e, idealmente, abbia esperienza con segnali di modulazione di larghezza di impulso (PWM), routine di servizio di interruzione (ISR), antirimbalzo di sensori ed encoder motore. L'esempio fornito in questo tutorial è un controller proporzionale primitivo. Molti miglioramenti possono essere implementati nel seguente esempio inclusi, ma non limitati a: implementazione di un anello di controllo PID e scalatura a più di due attuatori lineari. Tieni presente che non abbiamo le risorse per fornire supporto tecnico per le applicazioni Arduino e non eseguiremo il debug, la modifica, il codice o gli schemi elettrici al di fuori di questi tutorial disponibili pubblicamente.

Panoramica Controllo sincrono

Il controllo sincrono si ottiene confrontando la lunghezza di due attuatori lineari e regolando proporzionalmente la velocità; se un attuatore inizia a muoversi più velocemente di un altro, lo rallenteremo. Possiamo leggere la posizione dell'attuatore lineare tramite l'encoder ottico integrato. L'encoder ottico è un piccolo disco di plastica con 10 fori che è collegato al motore CC in modo tale che quando il motore gira anche il disco di plastica lo fa. Un LED a infrarossi è diretto verso il disco di plastica in modo che mentre ruota la luce può essere trasmessa attraverso i fori del disco ottico o bloccata dalla plastica del disco. Un sensore a infrarossi sull'altro lato del disco rileva quando la luce viene trasmessa attraverso il foro ed emette un segnale a onda quadra. Contando il numero di impulsi rilevati dal ricevitore, possiamo calcolare sia l'RPM del motore che la distanza percorsa dall'attuatore lineare. L'attuatore lineare ottico da 35 lb ha 50 (+/- 5) impulsi ottici per pollice di corsa, mentre gli attuatori da 200 lb e 400 lb hanno entrambi 100 (+/- 5) impulsi per pollice. Confrontando l'estensione di ciascun attuatore lineare, siamo in grado di regolare proporzionalmente la velocità dei due attuatori in modo che rimangano sempre alla stessa lunghezza durante l'estensione.

Componenti richiesti

Schema elettrico

Come sincronizzare due attuatori lineari utilizzando un Arduino

Effettuare i collegamenti di cablaggio sopra. Controllare sempre i colori dei fili in uscita dall'attuatore lineare in quanto la convenzione di colorazione può variare rispetto a quanto mostrato nello schema sopra.

    Tutorial veloce

    Se vuoi solo far muovere i tuoi due attuatori lineari in sincrono, segui questi passaggi:

    • Effettuare i collegamenti come mostrato nello schema elettrico.
    • Carica ed esegui il primo programma, di seguito.
    • Copia i due valori emessi da questo programma nella riga 23 del secondo programma, di seguito.
    • Carica ed esegui il secondo programma.
    • Ottimizza il tuo sistema variando la variabile K_p (riga 37, secondo programma). Questo è fatto più facilmente collegando un potenziometro al pin analogico A0 e modificando il codice per leggere il potenziometro e usando la funzione map (): K_p = map (analogRead (A0), 0, 1023, 0, 20000);

    Il resto di questo tutorial esaminerà più in dettaglio alcune delle caratteristiche principali dei programmi. Ancora una volta ribadiamo che questo non è un tutorial esaustivo, piuttosto una panoramica delle cose da considerare quando si crea il proprio programma.

    Panoramica del programma di calibrazione

    Prima di poter ottenere il controllo sincrono, dobbiamo prima calibrare il sistema. Ciò comporta il conteggio del numero di impulsi per ciclo di attuazione poiché, come indicato nelle specifiche del prodotto, esiste una tolleranza di (+/- 5) impulsi per pollice di corsa. Carica ed esegui il programma, di seguito. Questo programma ritrarrà completamente gli attuatori (riga 53) e azzererà la variabile del contatore di impulsi ottici, quindi si estenderà completamente e si ritirerà completamente (riga 63 e 74, rispettivamente). Durante questo ciclo di attivazione il numero di impulsi verrà contato dalla routine di servizio di interruzione (ISR), linea 153 e 166. Una volta completato il ciclo di attivazione, verrà emesso il numero medio di impulsi, linea 88, annotare questi valori per dopo.

    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]--;
        }
      }
    }

    Panoramica del programma sincrono

    Prima di caricare il programma di controllo sincrono, è necessario prima copiare i valori emessi dal programma di calibrazione nella riga 23 e sostituire l'array corrente: {908, 906} con i propri valori. Inoltre, se si utilizza l'attuatore lineare da 35 libbre, sarà necessario modificare il valore della variabile nella riga 29 da 20 millisecondi a 8 millisecondi.

    Dopo essere rientrati completamente una volta (per identificare l'origine) è possibile muovere entrambi gli attuatori lineari in sincrono premendo i tre pulsanti corrispondenti ai comandi di estensione, ritrazione e stop. Gli attuatori rimarranno sincronizzati anche sotto carichi irregolari confrontando i relativi contatori di impulsi e regolando la velocità tra di loro per rimanere sempre sincroni. Tieni presente che il programma corrente implementa un semplice controller proporzionale, la linea 93, in quanto tale è soggetta a overshoot e oscillazioni intorno all'equilibrio. Puoi regolarlo variando la variabile K_p, definita alla riga 37. Questo è fatto più facilmente collegando un potenziometro al pin analogico A0 e modificando il codice per leggere il potenziometro e usando la funzione map (): K_p = map (analogRead (A0), 0, 1023, 0, 20000);

    Per i migliori risultati si consiglia vivamente di rimuovere il controller proporzionale e di implementare un anello di controllo PID; tuttavia questo va oltre lo scopo di questo tutorial introduttivo ed è stato deliberatamente omesso.

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

    Utilizzo degli attuatori Bullet 36 e Bullet 50 in sincrono

    Oltre al nostro attuatore lineare Optical Series, offriamo anche due attuatori lineari con encoder integrati: Bullet 36 Cal. e il Bullet 50 Cal, entrambi dotati di un encoder a effetto Hall in quadratura interna. L'encoder effetto Hall funziona secondo lo stesso principio dell'encoder ottico, tuttavia invece di utilizzare la luce utilizza il magnetismo. Inoltre essendo un encoder in quadratura ha due uscite di segnale, ciascuna sfasata di 90 gradi. Pertanto, è necessario utilizzare una scheda Arduino con 4 o più pin di interrupt (Arduino Uno ne ha solo due) e modificare il codice per elaborare l'input da due segnali per attuatore. Inoltre, la variabile del tempo di antirimbalzo, falsepulseDelay dovrà essere sintonizzata insieme a K_p.

    Suggerimenti per scrivere il proprio programma

    Più di due attuatori lineari

    Quando si utilizzano due o più attuatori lineari, Arduino Uno non funzionerà più poiché ha solo due pin di interruzione disponibili. Dovrai usare una scheda Arduino con il numero approssimativo di pin di interrupt disponibili, maggiori informazioni: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

    In secondo luogo, nell'interesse dell'efficienza, è consigliabile vettorializzare la programmazione utilizzando array e cicli for () per iterare su ciascun attuatore.

    Rimbalzo

    Come per molti sensori è importante essere consapevoli dei segnali rimbalzanti. Come per gli interruttori meccanici, anche gli encoder possono soffrire di rimbalzo. Nell'esempio precedente il processo di debouncing è stato gestito da un semplice ritardo (definito dalla variabile falsepulseDelay), è importante gestirlo in qualsiasi modifica software apportata o con circuiti fisici per filtrare il rumore rimbalzante.

    Movimentazione roll over

    Se si modifica il codice, tenere presente il rollover quando si ha a che fare con la funzione millis(). Sia millis() che l'ultima matriceDebounceTime sono dichiarate variabili lunghe non firmate, il che significa che possono memorizzare valori fino a 4.294.967.295 (32^2-1). Ciò si traduce approssimativamente in un periodo di rollover di 49,7 giorni. Il programma corrente è progettato per gestire il rollover nelle funzioni ISR (interrupt service routine): count_0 & count_1, tuttavia se modifichi questo programma assicurati di gestire correttamente il rollover variabile, altrimenti il tuo programma si blocca dopo ~ 49,7 giorni di utilizzo continuo. Per ulteriori informazioni, fare riferimento a: https://www.norwegiancreations.com/2018/10/arduino-tutorial-avoiding-the-overflow-issue-when-using-millis-and-micros/

     

    Share This Article
    Tags: