Linear Actuator Position Control

So there are a wide variety of linear actuators from the automotive hot rod industry that are relatively cheap and powerful made by Autoloc. Here's the manual for one of their series:

http://www.truckshop.com/files/instructions/autoloc/LA.pdf

These can go from a couple inches to a foot or more and move several hundred pounds depending on model. There isn't any PWM control of them, however. But they do have built in hall effect sensors that appear to be a fairly high resolution. Biggest drawback is that there probably isn't a great way to control speed, they are what they are. But for my application, the speed is fine.

What I want is to connect one to an Arduino Leonardo and a motor shield. I'll use the relay circuit shown in the Autoloc manual and simply control the relays with the motor outputs (and that part is really already done anyway because we currently have this setup via manual control just like the manual). I've already tested connecting the hall effect sensor to an interrupt pin and using that to blink LED13 when the state changes (running the actuator manually with switches for now, NOT with the Arduino). From this, I can tell the hall effect sensor is pretty high resolution, but how high I'm not entirely sure. I thought maybe it would be something like 10 pulses per inch of movement, but it's a good bit higher than that. I don't think it's so high that we have to worry about the Arduino being fast enough to handle it, mind you, but the resolution is pretty good.

So what I want to do is end up with a serial interface to the actuator. My thought is that when the Arduino boots up the first thing it does is retract until the pulses stop for X amount of time (the actuator seems to have internal limit, as it certainly doesn't seem to mind you holding the button currently to retract with it fully retracted). That'll get it "home."

Then it extends until it stops extending for X amount of time and counts how many pulses it got. Now we have "distance." Then it retracts completely again to home.

Then it goes "ready." That probably means an output to the serial port saying "ready" or similar. Then it goes into command mode, where it waits for a command to move to a position. I'm thinking just an 8 bit position is fine, so basically you send it some number from 0-255 with a carriage return to terminate the command. It does the math and starts extending or retracting until it gets to the right position (based on the number of pulses). It then sends another message that the new position is complete and it's ready for another command.

So it needs some bounds checking, I think, since I doubt these pulses are going to be 100% perfect the same count from one end to the other every time, so it knows to just stop at the end and start the position count from scratch after no pulses for X amount of time.

This seems like it might be a pretty generic motion control kind of problem, but I've searched and haven't found anything that seemed terribly close. What I know is that while I know a little bit of programming in C, I'm NOT really a programmer. I'm pretty good at modifying things that are somewhat close, but when I look at things like how to use timers on the Arduino, my eyes start to glaze over a bit.

So anyone got any pointers to anything close? Or do I need to just start digging in with Nick's sample frequency counter code? That seemed to be the closest thing to the hard parts of this, but is a long way from everything.

--Donnie

Hmmmm. Not really sure... but my reading of the document is that there are "Hall" (Magnetic) switches in the EXTended and RETracted positions?? The Yellow "Pulses" connection??

What do you have for test gear? Multimeter?? Oscilloscope to look at those Yellow-Lead pulses would be great.

What is an online source for these?? Cost?? Look interesting! OK, got to autoloc.com Cool. I could get dangerous there. Thanks..

I looked at the data sheet, I would suspect the hall effect sensors may be for the internal limit switches. I also suspect the red, black, and yellow wires are connected to an internal pot. You can check for a pot by measuring the resistance across the red and black wires to see if the resistance is something like 5k or 10k. If there is a resistance like this, then measure the resistance across the black and yellow wires as the actuator moves full stroke. If the resistance changes from ~0 to the resistance measured across the red and black wires, the actuator has an internal position pot (a good thing!). Below are the firgelli actuators with internal pots.

I also suspect the red, black, and yellow wires are connected to an internal pot.

I think the KAT may be right. I saw a reference to a "Pot" for the "Controller" they want you to buy to run the actuator.

Quote
I also suspect the red, black, and yellow wires are connected to an internal pot.
I think the KAT may be right. I saw a reference to a "Pot" for the "Controller" they want you to buy to run the actuator.

After reading the data sheet, I don't agree. There is a section that says "Hall", lists the red wire as Vcc+ and lists the yellow wire as pulses. That is not consistent with a pot.. So, I do think there is a Hall sensor that counts motor rotations or some fraction thereof. However, they give no information about the resolution, so positioning would require calibration (and it would not be absolute).

You can buy much cheaper linear actuators with absolute potentiometer position readouts at Pololu.

I believe it's pulses as it moves. That's how I read the (admittedly bad) data sheet, and jives with my observation. What I did was plopped this sketch into an Arduino:

int pin = 13;
volatile int state = LOW;

void setup()
{
  Serial.begin(9600);
  attachInterrupt(1, talk, CHANGE);
}

void loop()
{
  digitalWrite(pin, state);
}

void talk()
{
  state = !state;
}

Then I ran the actuator using manual control. I saw pulses on LED13 as it moved. They were fairly fast, but did seem consistent. I do realize that the pulses won't be perfectly accurate, but for this application that's fine. And that's nice that there are cheaper options, but I only need to build this one item and I already have a couple of these actuators from another project.

My next plan was to write a sketch that uses a couple buttons to let me move the actuator using the Arduino and count pulses between movements and output the result each time I stop moving. Should be relatively straightforward to do that, I just got caught up in a different project using XBee (where I SWEAR I somehow got two bad XBee modules or I killed them both in some strange way because two new modules are working like they should).

--Donnie

Oh, and I'll add this. I think I outlined a system with too much logic on the Arduino (not that it can't be done, just that it doesn't need to be). I think it should probably be a bit more simple. Something like:

Power up, await command.

Initialize command does:
. Full retract
. Full extend, counting pulses
. Full retract, counting pulses
. Compare and maybe average the pulse count, return count or return error if difference is more than some threshold

Extend X pulses:
. bounds check to make sure command can be completed, return error if not (or maybe just fully extend and return fully extended state)
. extend until pulse count reached
. return amount extended

Retract X pulses:
. same as above the other direction

Fully extend (why not?):
. obvious

Fully retract:
. obvious

I think that simplifies the code greatly. I think the only part I don't feel comfortable with is using a timer to interrupt the "action" and return an error or know when we're fully extended or retracted. But I think even that part I can probably figure out. So unless someone knows of code out there to do this (which is seeming unlikely), I'll start working on it in the next couple days and just post my progress as I go.

Anyway, the above will let the real control software (which will be on a connected PC of some kind) know the "distance" in pulses in the initialize phase and it can have some calibration from there (like you input the distance the thing can travel physically) and just do the math on that end for how much it wants the thing to raise and lower.

To fill in the last gap, what this is going to do is raise and lower the front end of a bicycle trainer to simulate climbing. It's actually quite common when spinning indoor on a "real" bike that's attached to a rear wheel trainer to elevate the front end to simulate climbing during intervals because you do change slightly the way you engage muscles as the bike tilts under your body. And there are computer control programs that can change trainer resistance on the fly (TrainerRoad.com, for one, and Golden Cheetah for an open source solution), so I want to add the ability to let it also change the incline of the bike if you so choose.

--Donnie

Donnie
Check out the post of a problem I was having with code for position control of a linear actuator using reed switches as feedback. The code is all there for pulse counting , de-bounce and positioning; though make sure you check the solution to make sure you fix the problem I was having. You may find it helpful as there are some similar items covered. The difference being I am using relays for direction control vs your motor shield.
http://forum.arduino.cc/index.php?topic=236470.0
Let me know If I can help. MarkT helped me out with my problem, I am more than willing to help you out if I can.
Mike W.

Bringing this back from the dead. Using IDE 1.0.6.

Finally got around to working on this more. Fortunately I came back and checked the thread and found the post above. So I've massaged that code a little and here's what I have:

//
// AutoLoc Linear Actuator Control Software
//
// AutoLoc linear actuators use a DC motor and simple polarity reversing to extend
// and retract.  They also feature a hall effect sensor that sends pulses as it moves.
// 
// http://www.carid.com/images/autoloc/vertical-doors/pdf/la24-installation-instructions.pdf
// 
// This code taken from:
// http://forum.arduino.cc/index.php?topic=236470.0
// which originally took the code from here:
// http://learn.robotgeek.com/demo-code/123-arduino-linear-actuator-tutorial-preset-position-button-control.html
//
// 


const int relay1 = 7;     // extend relay output pin connection
const int relay2 = 4;     // retract relay output pin connection
const int reedSwitch = 2; // interupt pin connection - See attachInterrupt() docs for your board

int counter = 0;
int counterState = 0;        // current state of the button
int lastButtonState = 0;     // previous state of the button

long lastDebounce0 = 0;
long debounceDelay = 10;     // Ignore bounces under 10ms

int CurrentPosition = 0; 
int goalPosition = 0;

boolean Extending = false;
boolean Retracting = false;

int incomingByte;      // a variable to read incoming serial data into

void actuatorExtend(){
      digitalWrite(relay1, HIGH);  // Always turn one off and then the other on
      digitalWrite(relay2, LOW);   // so that there are never two relays on (BIG!)
}

void actuatorRetract(){
      digitalWrite(relay2, HIGH);  // Always turn one off and then the other on
      digitalWrite(relay1, LOW);   // so that there are never two relays on (BIG!)
}

void actuatorStop() {
    digitalWrite(relay2, HIGH);
    digitalWrite(relay1, HIGH);
}

void setup() {
  // initialize serial communication:
  Serial.begin(9600);
  
  pinMode(reedSwitch, INPUT);        // reedSwitch is an input

  attachInterrupt(1, trigger0, RISING);  // See attachInterrupt() doc for YOUR BOARD
                                         // Leanardo needs "1" for pin 2

  // Never see the output below on the Serial Monitor
  Serial.println("Repetition counter");
  Serial.print("Start");
  Serial.print("\t");
  Serial.println("End");
  
  // Set both output pins
  pinMode(relay1, OUTPUT);
  pinMode(relay2, OUTPUT);
  
  // Initialize both relays to off (make sure whatever relay setup you use defaults
  // to OFF since there may be some delay between power-up and this code executing!
  digitalWrite(relay2, HIGH);
  digitalWrite(relay1, HIGH);
}


void loop() {
  
if (Extending == true && CurrentPosition > goalPosition ) {    
      //we have reached our goal, shut the relays off
      actuatorStop();
      Extending = false;
      Serial.println("IDLE");  
}
  
if (Retracting == true && CurrentPosition < goalPosition) {
       //we have reached our goal, shut the relay off
       actuatorStop();
       Retracting = false;
       Serial.println("DELI");  
}

// see if there's incoming serial data: 
if (Serial.available() > 0) {   
    // read the oldest byte in the serial buffer:
    incomingByte = Serial.read();

    if (incomingByte == 'A') {
      goalPosition = 5;
      if (goalPosition > CurrentPosition) {
        Retracting = false;
        Extending = true;
        actuatorExtend ();
        Serial.println("AExtending"); 
      }     
      
      else if (goalPosition < CurrentPosition) {
        Retracting = true;
        Extending = false;
        actuatorRetract();
        Serial.println("ARetracting");  
      }
    }
                  
    if (incomingByte == 'B') { 
      goalPosition = 2; 
      if (CurrentPosition < goalPosition ) {
        Retracting = false;
        Extending = true;
        actuatorExtend ();
        Serial.println("BExtending");   
      }  
      else if (goalPosition < CurrentPosition) {
        Retracting = true;
        Extending = false;
        actuatorRetract();
        Serial.println("BRetracting");            
       } 
     }
  
     if (incomingByte == 'S') {
       actuatorStop();
       Serial.println("Stop"); 
     }
  }
}

// Once we start a relay, we should start getting pulses from the sensor as the
// actuator moves. When we get a RISING edge and interrupt will be generated and
// we will jump to the following function.
void trigger0() {
    if ( (millis() - lastDebounce0) > debounceDelay && Extending == true) {
      counter++;
      Serial.print(counter);
      Serial.println(" : ");
      CurrentPosition = counter;
      lastDebounce0 = millis();
    }
            
    if ( (millis() - lastDebounce0) > debounceDelay && Retracting == true) {
      counter--;
      Serial.println(counter);   
      Serial.print(" : ");
      CurrentPosition = counter;
      lastDebounce0 = millis();
    }       
}

This is really just to move it between two spots, 5 pulses from power up and 3 pulses from power up. I grabbed some arbitrary numbers.

It seems to sort of work, but quickly (like after anywhere from 2 to 5 movements) something goes haywire. It's like it gets stuck in an interrupt. Hitting the Reset button on the Leonardo doesn't even fix it. I have to power cycle the board and then it will work a little bit again. Now, the first version of this code was for a linear actuator that was potentiometer controlled (so it didn't need interrupts). Then the poster above modified it to use interrupts and debounce his reed switch (and he ran it on a Mega, so I had to change some interrupt stuff for the Leonardo, but maybe I missed something?). I haven't bothered to try to understand the debounce code fully, but I'm also not sure I need it with a Hall Effect sensor. Thoughts on that part?

Also, anything obvious that would cause this thing to get stuck? It usually will extend and retract a couple times and ultimately an attempt to go one way or the other causes it to just start extending with no limit (and I can't stop it with an 'S' command, I have to remove power).

Anything obvious here?

--Donnie

Is this strictly a SW issue ?

raschemmel:
Is this strictly a SW issue ?

Unsure, but it seems likely. The hardware is incredibly simple and it almost works.

--Donnie

Define "almost". (do you mean with or without the Hall effect circuit ?)

raschemmel:
Define "almost". (do you mean with or without the Hall effect circuit ?)

With the hall effect circuit.

At power up, I can open the serial monitor and send an A. It'll extend 5 pulses and stop and I get the expected output on the monitor. I can send a B and it'll retract a couple pulses and stop. But the next A might work or it might just start extending. Often if it starts extending, I can use an 'S' to stop it, but only once. The next 'B' usually results in it starting to retract and nothing short of a power-off will stop it and I get no more serial output as it does it.

But that's not 100% repeatable. Sometimes I can do a couple A-B cycles before it goes haywire.

--Donnie

What is the hardware equivilent of the Hall circuit when not using a uC ? How is the Hall circuit used in that case ? If you can post a schematic of an application of the actuator without a uC perhaps that would provide some insight into the problem.

raschemmel:
What is the hardware equivilent of the Hall circuit when not using a uC ? How is the Hall circuit used in that case ? If you can post a schematic of an application of the actuator without a uC perhaps that would provide some insight into the problem.

The hall effect sensor just provides pulses as the actuator moves. Same as the reed switch in the post above by pjp62mw.

Without a sensor, you can only turn it on and off one direction or the other with no clue how far it actually moves.

With one you can measure how far it moves per pulse and get relatively accurate movement. There's no way to know where it starts or finishes at power-up, so ultimately I'll add a calibration where it will retract until no pulses come, then extend until no pulses come, and count how many pulses it gets end-to-end. Then it'll move to whatever "home" position I ultimately want in the final application.

--Donnie

Understood but have you been able to find a circuit to add to the Hall sensor that converts the signal to 5V logic pulses or do you already have that added. SW is not my forte but it certainly sounds like your problem is your pulse counting code. First of all , although I am not really a SW person, the thing that jumps out at me is your whole program revolves around serial and you are only running at 9600. Serial communication has a high timing overhead. I would immediately change your speed to 115200 bps. Also, as a diagnostic test, I would comment out EVERYTHING except the pulse counting code and the serial output of the pulse count and drive the actuator using the rocker switch (disconnect it from the motor shield). Then press the rocker switch and watch the pulse count to see if it is vastly different and more accurate. If you can drive it back and forth manually and see accurate pulse counts without the software hanging then that would tell you that it is indeed a SW issue , specifically related to timing issues. Troubleshooting is all about isolating one system or code block from another by eliminating everything except one and running each block with all the others commented out. Similarly, electronics troubleshooting is about disconnect all the circuits but one and testing them all independently. Right now you have too many things going on. You need to isolate the problem by eliminating everything but one item at a time. Works for me. I'm an electronics tech.

The AutoLoc actuator has a 5V, GND, and "pulse" wire. So yeah, it's giving me logic pulses.

But you're right, the next step is to isolate that code and see what's going on. I do still have manual control of the actuator in my prototype, so that's pretty easy to do. Tomorrow...

--Donnie

Try changing the serial baud rate to 115200 first.

So this is now figured out. Wasn't baud rate or anything so simple, it was having things in the Interrupt Service Routine (ISR) that shouldn't have been. Namely, things like sending things out via serial.print() and millis() that can't be in there because they use interrupts as well (should have read the attachInterrupt() documentation more closely, because that's spelled out quite clearly). In my defense, I took what was supposedly working code snippets from other projects that did those things.

Anyway, I looked at my wave form output from my hall effect sensor with an oscilloscope and it does not appear to need debouncing, which removed any need for the millis() stuff that was inside the ISR. So that's gone. Then I simply removed the serial output during the ISR, which I also didn't need but was in there for debugging purposes (yeah, debugging code adding the bug). Now it works. Here's the revised code:

//
// AutoLoc Linear Actuator Control Software
//
// AutoLoc linear actuators use a DC motor and simple polarity reversing to extend
// and retract.  They also feature a hall effect sensor that sends pulses as it moves.
// 
// http://www.carid.com/images/autoloc/vertical-doors/pdf/la24-installation-instructions.pdf
// 
// This code taken from:
// http://forum.arduino.cc/index.php?topic=236470.0
// which originally took the code from here:
// http://learn.robotgeek.com/demo-code/123-arduino-linear-actuator-tutorial-preset-position-button-control.html
//
// 


const int relay1 = 7;     // extend relay output pin connection
const int relay2 = 4;     // retract relay output pin connection
const int reedSwitch = 2; // interupt pin connection - See attachInterrupt() docs for your board

volatile int counter = 0;
int counterState = 0;        // current state of the button
int lastButtonState = 0;     // previous state of the button

long lastDebounce0 = 0;
long debounceDelay = 10;     // Ignore bounces under 10ms

volatile int CurrentPosition = 0; 
int goalPosition = 0;

boolean Extending = false;
boolean Retracting = false;

int incomingByte;      // a variable to read incoming serial data into

void actuatorExtend(){
      digitalWrite(relay1, HIGH);  // Always turn one off and then the other on
      digitalWrite(relay2, LOW);   // so that there are never two relays on (BIG!)
}

void actuatorRetract(){
      digitalWrite(relay2, HIGH);  // Always turn one off and then the other on
      digitalWrite(relay1, LOW);   // so that there are never two relays on (BIG!)
}

void actuatorStop() {
    digitalWrite(relay2, HIGH);
    digitalWrite(relay1, HIGH);
}

void setup() {
  // initialize serial communication:
  Serial.begin(9600);
  
  pinMode(reedSwitch, INPUT);        // reedSwitch is an input

  attachInterrupt(1, trigger0, RISING);  // See attachInterrupt() doc for YOUR BOARD
                                         // Leanardo needs "1" for pin 2

  // Never see the output below on the Serial Monitor
  Serial.println("Repetition counter");
  Serial.print("Start");
  Serial.print("\t");
  Serial.println("End");
  
  // Set both output pins
  pinMode(relay1, OUTPUT);
  pinMode(relay2, OUTPUT);
  
  // Initialize both relays to off (make sure whatever relay setup you use defaults
  // to OFF since there may be some delay between power-up and this code executing!
  digitalWrite(relay2, HIGH);
  digitalWrite(relay1, HIGH);
}


void loop() {
  
if (Extending == true && CurrentPosition > goalPosition ) {    
      //we have reached our goal, shut the relays off
      actuatorStop();
      Extending = false;
      Serial.println("IDLE");  
}
  
if (Retracting == true && CurrentPosition < goalPosition) {
       //we have reached our goal, shut the relay off
       actuatorStop();
       Retracting = false;
       Serial.println("DELI");  
}

// see if there's incoming serial data: 
if (Serial.available() > 0) {   
    // read the oldest byte in the serial buffer:
    incomingByte = Serial.read();

    if (incomingByte == 'A') {
      goalPosition = 50;
      if (goalPosition > CurrentPosition) {
        Retracting = false;
        Extending = true;
        actuatorExtend ();
        Serial.println("AExtending"); 
      }     
      
      else if (goalPosition < CurrentPosition) {
        Retracting = true;
        Extending = false;
        actuatorRetract();
        Serial.println("ARetracting");  
      }
    }
                  
    if (incomingByte == 'B') { 
      goalPosition = 1; 
      if (CurrentPosition < goalPosition ) {
        Retracting = false;
        Extending = true;
        actuatorExtend ();
        Serial.println("BExtending");   
      }  
      else if (goalPosition < CurrentPosition) {
        Retracting = true;
        Extending = false;
        actuatorRetract();
        Serial.println("BRetracting");            
       } 
     }
  
     if (incomingByte == 'S') {
       actuatorStop();
       Serial.println("Stop"); 
     }
  }
}

// Once we start a relay, we should start getting pulses from the sensor as the
// actuator moves. When we get a RISING edge and interrupt will be generated and
// we will jump to the following function.
void trigger0() {
      if ( Extending == true) {
      counter++;
      CurrentPosition = counter;
    }
            
      if ( Retracting == true ) {
      counter--;
      CurrentPosition = counter;
    }       
}

There's still work to turn this into the full control code I want, but it moves the actuator back and forth between the specified positions just fine. Onward and upward...

(And I ultimately solved this by pairing down my code to just what was needed to listen to the hall effect sensor and when that didn't work I posted in the sensors forum here and someone pointed out the serial printing wouldn't work. Already posted there that this is fixed and how it was done, too.)

--Donnie