Cómo sincronizar dos actuadores lineales usando un Arduino

El movimiento síncrono entre múltiples actuadores lineales puede ser vital para el éxito de algunas aplicaciones de clientes, siendo uno común dos actuadores lineales que abren una trampilla. Para lograr esto, recomendamos utilizar el Firgelli dedicado caja de control síncrono FA-SYNC-2 y FA-SYNC-4. Sin embargo, algunos aficionados al bricolaje y hackers prefieren la libertad que ofrece un microcontrolador como el Arduino y prefieren escribir su propio programa de control sincrónico. Este tutorial tiene como objetivo proporcionar una descripción general sobre cómo lograr esto utilizando el Actuador lineal de la serie óptica.

Prefacio

Este tutorial no es un tratamiento riguroso de los pasos necesarios para lograr el control sincrónico con Arduino, sino una descripción general amplia para ayudarlo a escribir su propio programa personalizado. Este tutorial es avanzado y asume que ya está familiarizado con el hardware y software de Arduino e idealmente tiene experiencia con señales de modulación de ancho de pulso (PWM), rutina de servicio de interrupción (ISR), supresión de rebotes de sensores y codificadores de motor. El ejemplo proporcionado en este tutorial es un controlador proporcional primitivo. Se pueden implementar muchas mejoras en el siguiente ejemplo que incluyen, pero no se limitan a: implementar un bucle de control PID y escalar a más de dos actuadores lineales. Tenga en cuenta que no tenemos los recursos para proporcionar soporte técnico para las aplicaciones Arduino y no depuraremos, editaremos, proporcionaremos códigos o diagramas de cableado fuera de estos tutoriales disponibles públicamente.

Descripción general del control síncrono

El control sincrónico se logra comparando la longitud de dos actuadores lineales y ajustando proporcionalmente la velocidad; si un actuador comienza a moverse más rápido que otro, lo ralentizaremos. Podemos leer la posición del actuador lineal a través del codificador óptico incorporado. El codificador óptico es un pequeño disco de plástico con 10 orificios que está conectado al motor de CC de manera que cuando el motor gira, el disco de plástico también lo hace. Un LED infrarrojo se dirige hacia el disco de plástico para que, a medida que gira, la luz se transmita a través de los orificios del disco óptico o se bloquee por el plástico del disco. Un sensor de infrarrojos en el otro lado del disco detecta cuando la luz se transmite a través del orificio y emite una señal de onda cuadrada. Contando el número de pulsos que detecta el receptor, podemos calcular las RPM del motor y la distancia que ha recorrido el actuador lineal. El actuador lineal óptico de 35 lb tiene 50 (+/- 5) pulsos ópticos por pulgada de recorrido, mientras que los actuadores de 200 lb y 400 lb tienen ambos 100 (+/- 5) pulsos por pulgada. Al comparar cuánto se ha extendido cada actuador lineal, podemos ajustar proporcionalmente la velocidad de los dos actuadores para que siempre permanezcan en la misma longitud mientras se extienden.

Componentes requeridos

Diagrama de cableado

Cómo sincronizar dos actuadores lineales usando un Arduino

Realice las conexiones de cableado anteriores. Siempre verifique los colores de los cables que salen del actuador lineal ya que la convención de colores puede cambiar de lo que se muestra en el diagrama anterior.

    Tutorial rápido

    Si solo desea que sus dos actuadores lineales se muevan en sincronía, simplemente siga estos pasos:

    • Realice las conexiones como se muestra en el diagrama de cableado.
    • Cargue y ejecute el primer programa, a continuación.
    • Copie los dos valores generados por este programa en la línea 23 del segundo programa, a continuación.
    • Cargue y ejecute el segundo programa.
    • Ajuste su sistema variando la variable K_p (línea 37, segundo programa). Esto se hace más fácilmente conectando un potenciómetro al pin analógico A0 y modificando el código para leer el potenciómetro y usando la función map (): K_p = map (analogRead (A0), 0, 1023, 0, 20000);

    El resto de este tutorial repasará con más detalle algunas de las características clave de los programas. Nuevamente reiteramos que este no es un tutorial exhaustivo, sino más bien una descripción general de las cosas a considerar al crear su propio programa.

    Descripción general del programa de calibración

    Antes de que se pueda lograr el control síncrono, primero debemos calibrar el sistema. Esto implica contar el número de pulsos por ciclo de actuación porque, como se indica en las especificaciones del producto, hay una tolerancia de (+/- 5) pulsos por pulgada de recorrido. Cargue y ejecute el programa, a continuación. Este programa retraerá completamente los actuadores (línea 53) y establecerá la variable del contador de pulso óptico en cero, luego se extenderá y retraerá completamente (línea 63 y 74, respectivamente). Durante este ciclo de actuación, el número de pulsos será contado por la rutina de servicio de interrupción (ISR), línea 153 y 166. Una vez que se complete el ciclo de actuación, saldrá el número promedio de pulsos, línea 88, tome nota de estos valores para más adelante.

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

    Descripción general del programa síncrono

    Antes de cargar el programa de control síncrono, primero debe copiar los valores generados por el programa de calibración en la línea 23 y reemplazar la matriz actual: {908, 906} con sus propios valores. Además, si está utilizando el actuador lineal de 35 libras, deberá cambiar el valor de la variable en la línea 29 de 20 milisegundos a 8 milisegundos.

    Después de retraerse completamente una vez (para identificar el origen), puede mover ambos actuadores lineales en sincronía pulsando los tres botones correspondientes a los comandos de extensión, retracción y parada. Los actuadores permanecerán sincronizados incluso bajo cargas desiguales al comparar sus contadores de pulsos relativos y ajustar la velocidad entre ellos para permanecer siempre sincronizados. Tenga en cuenta que el programa actual implementa un controlador proporcional simple, la línea 93, como tal, está sujeta a sobreimpulso y oscilación alrededor del equilibrio. Puede ajustar esto variando la variable K_p, definida en la línea 37. Esto se hace más fácilmente conectando un potenciómetro al pin analógico A0 y modificando el código para leer el potenciómetro y usando la función map (): K_p = map (analogRead (A0), 0, 1023, 0, 20000);

    Para obtener los mejores resultados, recomendamos encarecidamente quitar el controlador proporcional e implementar un lazo de control PID; sin embargo, esto está más allá del alcance de este tutorial introductorio y se ha omitido deliberadamente.

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

    Uso de actuadores Bullet 36 y Bullet 50 en sincronía

    Además de nuestro actuador lineal Optical Series, también ofrecemos dos actuadores lineales con codificadores incorporados: el Bullet 36 Cal. y el Bullet 50 Cal, ambos con un codificador de efecto Hall en cuadratura interno. El codificador de efecto Hall funciona con el mismo principio que el codificador óptico, sin embargo, en lugar de usar luz, utiliza magnetismo. Además, al ser un codificador de cuadratura, tiene dos salidas de señal, cada una desfasada en 90 grados. Como tal, debe usar una placa Arduino con 4 o más pines de interrupción (el Arduino Uno solo tiene dos) y modificar el código para procesar la entrada de dos señales por actuador. Además, la variable de tiempo antirrebote, falsepulseDelay deberá ajustarse junto con K_p.

    Consejos para escribir su propio programa

    Más de dos actuadores lineales

    Cuando se utilizan dos o más actuadores lineales, Arduino Uno ya no funcionará ya que solo tiene dos pines de interrupción disponibles. Deberá utilizar una placa Arduino con el número apropiado de pines de interrupción disponibles, más información: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

    En segundo lugar, en aras de la eficiencia, es aconsejable vectorizar su programación utilizando matrices y bucles for () para iterar sobre cada actuador.

    Rebotando

    Como ocurre con muchos sensores, es importante tener en cuenta las señales que rebotan. Al igual que con los interruptores mecánicos, los codificadores también pueden sufrir rebotes. En el ejemplo anterior, el proceso de eliminación de rebotes se ha manejado mediante un retardo simple (definido por la variable falsepulseDelay), es importante manejar esto en cualquier cambio de software que realice o con circuitos físicos para filtrar el ruido de rebote.

    Manejo de vuelco

    Si modifica el código, tenga en cuenta el rollover cuando trabaje con la función millis (). Tanto millis () como la matriz lastDebounceTime se declaran como variables largas sin firmar, lo que significa que pueden almacenar valores de hasta 4,294,967,295 (32 ^ 2-1). Esto se traduce en aproximadamente un período de renovación de 49,7 días. El programa actual está diseñado para manejar el rollover en las funciones ISR (rutina de servicio de interrupción): count_0 & count_1, sin embargo, si modifica este programa, asegúrese de manejar correctamente el rollover variable, de lo contrario su programa fallará después de ~ 49.7 días de uso continuo. Para obtener más información, consulte: https://www.norwegiancreations.com/2018/10/arduino-tutorial-avoiding-the-overflow-issue-when-using-millis-and-micros/

     

    Share This Article
    Tags: