Servo Hold Position with joystick input

Hi all,

I’m working on a project that uses an Arduino and joystick to control a trolling motor on a canoe using a heavy duty servo (DS5160). So far everything is working great, I’ve even incorporated VarSpeedServo to slow down the turn speed so it does not knock me into the water when executing a turn at full throttle :wink:

Currently the code below uses a zero setting which it automatically returns to when the joystick is released. In theory it should work well, letting go of the joystick goes back to the straight position. The problem lies when I want to take an extended turn. The joystick needs to be pushed in that direction 10 - 20 times so it does not go back to zero and does not go beyond the desired turning radius.

I found a servo demo here that should move the servo as needed and also hold the position when the joystick returns to center. Unfortunately it is beyond my understanding how to incorporate this into my currently working code.

Any assistance in getting the code below to have the servo stay in place would be tremendously appreciated.

/*
Original code from: https://create.arduino.cc/projecthub/RiddledExistence/controlling-a-servo-motor-with-thumb-joystick-46a4d3
Wiring: https://d.pr/i/ANwkU8
*/

#include <VarSpeedServo.h>
#define SERVO_PIN 9
#define GROUND_JOY_PIN A3            //joystick ground pin will connect to Arduino analog pin A3
#define VOUT_JOY_PIN A2              //joystick +5 V pin will connect to Arduino analog pin A2
#define XJOY_PIN A1                  //X axis reading from joystick will go into analog pin A1
VarSpeedServo myservo ;

//define joystick pins (Analog)
int joyX = 0;

//variable to read the values from the analog pins
int joyVal;

int SPEED1 = 15; //operational setting 8

void setup()
{
 Serial.begin(9600);
 pinMode(VOUT_JOY_PIN, OUTPUT) ;    //pin A3 shall be used as output
 pinMode(GROUND_JOY_PIN, OUTPUT) ;  //pin A2 shall be used as output
 digitalWrite(VOUT_JOY_PIN, HIGH) ; //set pin A3 to high (+5V)
 digitalWrite(GROUND_JOY_PIN,LOW) ; //set pin A3 to low (ground)
 myservo.attach(9);
}
 
void loop()
{
  
 
   //read the value of joystick (between 0-1023)
  joyVal = analogRead(joyX);
  joyVal = map (joyVal, 0, 1023, 0, 180); //servo value between 0-180
  myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
  delay(15);

    
}

Also, here is a screencap of the wiring diagram. Important note that this is the setup I use for testing with a micro servo (SG90) and that when I use the heavy duty servo I use a an external power source to bring in the 7.4V it requires.

So ... how did you want to signal to the arduino that the turn is complete?

Maybe what you need is an LED that indicates that a turn is being held. You move the joystick, hold it, and the LED comes on. It then ignores a return to center - you have to tap the joystick the other way to "unstick" the rudder.

Ok, fine. The problem being that now your sketch has to deal with time, had to deal with sloppiness in the joystick reading - it's not going to be precisely 512 when the joystick is at center, and has to deal with the fact that when you release the joystick and the spring returns it to center, there will be a couple of other readings as it returns. Oh, and when you hold the joystick in a turn, the reading will not be a constant - it will waver a little.

I suggest a couple of things:

Sample the joystick value at a known rate, not "as fast as the arduino will go". 10 times a second should be more than enough for a trolling canoe. Check millis() to see if 100ms has elapsed since the last time you sampled the joystick.

Keep that last - say - 5 seconds of readings in a circular bufffer (50 readings). That is - you have to 'hold' for 5 seconds before the rudder 'locks'. Maybe that's too short? You could reduce the rate or increase the buffer size to take more samples.

At this point, we can introduce some logic.

Your sketch at any moment is in one of 3 states:

1 - normal operation. (follow the rudder unless the joystick gets held)

If the joystick is at least some distance away from center, and all of the readings in the buffer are close enough to the joystick's current position, then engage lock and turn on the 'steering lock' led - the sketch is now in 'lock engaged' state.
Otherwise, the rudder follows the joystick.

2 - Lock engaged (allow the joystick to return to center. Permit turn adjustments. Unstick when it is tapped the other way.)

If the joystick is at least some distance away from the center position in the opposite direction of the current lock, set the rudder to zero (VarSpeedServo will make this softer). The sketch is now in 'lock disengaging' state.
If the joystick is at least some distance away from center in the direction of the current lock, and the past (say) .5 seconds of readings are all close enough to the current reading, set the rudder to follow the joystick and continue on in a "lock engaged" state. (I assume the spring returns the joystick to center much faster than .5 seconds)
Otherwise, we are retaining the lock.

3 - Lock disengaging (allow the joystick to return to center after being tapped the other way ... but as a safety measure "unstick" afer half a second regardless)

If the joystick is close enough to center, or if it is nonzero in the direction on the previously held lock, or if it has been half a second since the start of lock disengaging, set the rudder according to the joystick, turn off the steering lock LED, and set the sketch state to 'normal operation'
Otherwise, we are still waiting for the joystick to return to zero - stay in 'lock disengaging' state.

You keep the current state in a global variable outside the loop. I'd suggest making a distinction between joystick reading and rudder position - store the readings in the circular buffer, not the mapped rudder position.

To find out what values are "close enough to center" and "close enough to the joystick's current position", you will need to print out the values you are reading from the joystick and experiment until you get the 'feel' you want.

Thanks for taking the time to put together all of those scenarios. I agree that finding the exact center that moves the canoe completely straight is almost impossible to find. Especially with external factors like wind, tides, and even load balance on the canoe itself.

My thought was that I could find a much simpler solution that in essence moves the motor to the right when the joystick moves right. If the joystick is released, and goes back to center, the motor should stay in the last right most position it received.

If the joystick is then moved in the left direction, the motor should start turning to the left until the joystick is released and returns to center. The position of the motor should not return to center but stay in the last position before the joystick moved to center.

This way if the canoe is to stay on an extended turn, just pointing the motor correctly and letting go of the joystick will accomplish that. It the canoe needs to go straight, the joystick is used to point the motor in the desired direction and when released will stay at that bearing.

In case anyone is curious here are a few pictures of the canoe project Arduino and servo setup:

stevetested:
If the joystick is then moved in the left direction, the motor should start turning to the left until the joystick is released and returns to center. The position of the motor should not return to center but stay in the last position before the joystick moved to center.

The problem is that when you release the joystick, it passes through all the other positions on the way to the center.

How about this:

// when the rudder is outside the 'noturn' range, then we are definitely turning and want to lock
// when the rudder is inside the 'center' range, the joystick is released and is floating near the center
// if we are locked, then the joystick being moved in the opposite direction beyond the center range will break the lock.

const int MIN_NOTURN = 80;
const int MIN_CENTER = 85;
const int CENTER = 90;
const int MAX_CENTER = 95;
const int MAX_NOTURN = 100;

boolean holding = false;
int rudder= CENTER;

void loop()
{
   //read the value of joystick (between 0-1023)
  joyVal = analogRead(joyX);
  joyVal = map (joyVal, 0, 1023, 0, 180); //servo value between 0-180

  if(holding) {
    if(rudder < CENTER && joyval < rudder) {
      // turn harder
      rudder = joyval;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else if(rudder > CENTER && joyval > rudder) {
      // turn harder
      rudder = joyval;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else if(rudder < CENTER && joyval > MAX_CENTER) {
      // break the lock
      holding = false;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else if(rudder > CENTER && joyval < MIN_CENTER) {
      // break the lock
      holding = false;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else {
      // do nothing - maintain the lock
    }
  }
  else {
    if(joyVal < MIN_NOTURN) {
      holding = true;
      rudder = joyval;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else if(joyVal < MAX_NOTURN) {
      // rudder follows the joystick
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else {
      // hold the turn to this bearing
      holding = true;
      rudder = joyval;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
  }
  delay(15);
}

Amazing! Thanks again for your effort with this.

The movement is now really close to ideal. I did notice that once I tap a few times to the right to get to the correct bearing, it stays at that spot. That part is perfect.

If I go a little past the needed angle, I can’t go back just a little bit. Any click in the opposite direction goes back to full zero position. Ideally, I’ll be able to provide little adjustments all of the time to go in the desired direction. With that in mind, I don’t think it is even necessary for the code to ever need to go back to zero.

The motor position at zero tends to not go straight anyway especially with all of the external factors pushing/pulling the canoe in different directions.

I’ll include the full code I’m using here just in case anyone else is following along:

/*
https://create.arduino.cc/projecthub/RiddledExistence/controlling-a-servo-motor-with-thumb-joystick-46a4d3
https://d.pr/i/ANwkU8
*/

#include <VarSpeedServo.h>
#define SERVO_PIN 9
#define GROUND_JOY_PIN A3            //joystick ground pin will connect to Arduino analog pin A3
#define VOUT_JOY_PIN A2              //joystick +5 V pin will connect to Arduino analog pin A2
#define XJOY_PIN A1                  //X axis reading from joystick will go into analog pin A1
VarSpeedServo myservo ;

//define joystick pins (Analog)
int joyX = 0;

//variable to read the values from the analog pins
int joyVal;

int SPEED1 = 38; //original setting 8

// https://forum.arduino.cc/index.php?msg=4753117 H/T to PaulMurrayCbr
// when the rudder is outside the 'noturn' range, then we are definitely turning and want to lock
// when the rudder is inside the 'center' range, the joystick is released and is floating near the center
// if we are locked, then the joystick being moved in the opposite direction beyond the center range will break the lock.

const int MIN_NOTURN = 80;
const int MIN_CENTER = 85;
const int CENTER = 90;
const int MAX_CENTER = 95;
const int MAX_NOTURN = 100;


boolean holding = false;
int rudder= CENTER;


void setup()
{
 Serial.begin(9600);
 pinMode(VOUT_JOY_PIN, OUTPUT) ;    //pin A3 shall be used as output
 pinMode(GROUND_JOY_PIN, OUTPUT) ;  //pin A2 shall be used as output
 digitalWrite(VOUT_JOY_PIN, HIGH) ; //set pin A3 to high (+5V)
 digitalWrite(GROUND_JOY_PIN,LOW) ; //set pin A3 to low (ground)
 myservo.attach(9);
}
 
void loop()
{
   //read the value of joystick (between 0-1023)
  joyVal = analogRead(joyX);
  joyVal = map (joyVal, 0, 1023, 0, 180); //servo value between 0-180

  if(holding) {
    if(rudder < CENTER && joyVal < rudder) {
      // turn harder
      rudder = joyVal;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else if(rudder > CENTER && joyVal > rudder) {
      // turn harder
      rudder = joyVal;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else if(rudder < CENTER && joyVal > MAX_CENTER) {
      // break the lock
      holding = false;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else if(rudder > CENTER && joyVal < MIN_CENTER) {
      // break the lock
      holding = false;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else {
      // do nothing - maintain the lock
    }
  }
  else {
    if(joyVal < MIN_NOTURN) {
      holding = true;
      rudder = joyVal;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else if(joyVal < MAX_NOTURN) {
      // rudder follows the joystick
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
    else {
      // hold the turn to this bearing
      holding = true;
      rudder = joyVal;
      myservo.write(joyVal,SPEED1); //set the servo position according to the joystick value
    }
  }
  delay(15);
  
}

Do you have a joystick with a button on top? That would make this simpler and a bit more precise.

You could also tighten up that center range - the only point of it is to handle the little bit of slop when the joystick returns to center. I just picked those numbers out of the air.

I tried all variations of the center range settings and no matter what the setting was, any movement in the opposite position of the current rudder setting always goes back to center.

I’m wondering if there is a way to limit any return movement by X if the joystick is no longer held and returns to zero. This way it will never auto-reset to center.