Potentiometer not rotating servo as expected

Hello Arduino community members. I'm trying to create an electronic gear shifter for Shimano bicycle hub, as my custom trike frame flexes too much for conventional shifter (can be used for other electric bikes, or just bikes, want to make it open source). I'm using Arduino Uno, 30kg 270 degree servo I had kicking around and a 10k potentiometer. With the help of Chat GPT I have managed to produce this code, however I'm a bit confused about the starting behavior of the servo (or potentiometer, to be precise). I assumed if I start the "if" statement with pot value more than 0 and the pot is at its leftmost position nothing will happen at first and when I turn the pot it will move into Position 1 (which I calculated to be 25 degrees). However when I upload the sketch servo moves to its starting position (500 usec according to the datasheet), which is as expected. But when I turn the pot a bit, it moves a bit less than 25 degrees, and stays there, even if I turn the pot back entirely. After that the other steps seem to be producing correct rotation, however it will not go back to its starting position (500 usec) when the pot is at 0 (or between 50 and 100). I imagine that its my lack of understanding how the potentiometer functions, I wonder if someone can suggest what can be done to fix this issue in the code.

#include <Servo.h>

Servo myServo;            // create a servo object
int potPin = A0;          // analog pin connected to the potentiometer
int potValue = 0;         // variable to store the potentiometer value
int targetMicroseconds = 0;  // target position in microseconds based on potentiometer
int currentMicroseconds = 500;  // initial position in microseconds 

void setup() {
  myServo.attach(9);        // attaches the servo on pin 9
  myServo.writeMicroseconds(currentMicroseconds);  // set initial position
}

void loop() {
  potValue = analogRead(potPin);   // read the potentiometer value (0-1023)

  // Determine the target microseconds based on potentiometer range
  if (potValue >= 50 && potValue <= 100) {
    targetMicroseconds = 685;
 
  } else if (potValue >= 101 && potValue <= 200) {
    targetMicroseconds = 870;
  } else if (potValue >= 201 && potValue <= 300) {
    targetMicroseconds = 1055;
  } else if (potValue >= 301 && potValue <= 400) {
    targetMicroseconds = 1240;
  } else if (potValue >= 401 && potValue <= 500) {
    targetMicroseconds = 1477;
  } else if (potValue >= 501 && potValue <= 600) {
    targetMicroseconds = 1662;
  } else if (potValue >= 601 && potValue <= 700) {
    targetMicroseconds = 1847;
  } else if (potValue >= 701 && potValue <= 800) {
    targetMicroseconds = 2032;
  } else if (potValue >= 801 && potValue <= 900) {
    targetMicroseconds = 2217;
  } else if (potValue >= 901 && potValue <= 1000) {
    targetMicroseconds = 2439;
  }

  // Gently rotate towards target microseconds at quarter speed
  if (currentMicroseconds < targetMicroseconds) {
    currentMicroseconds++;
    myServo.writeMicroseconds(currentMicroseconds);
    delay(1);   // delay to slow down rotation speed
  } 
  else if (currentMicroseconds > targetMicroseconds) {
    currentMicroseconds--;
    myServo.writeMicroseconds(currentMicroseconds);
    delay(1);   // delay to slow down rotation speed
  }
}

Try printing potValue and see if it is what you think it is.

How are you powering the servo?

1 Like

wow, its so cool! (serial plotter window) I'm powering it from the 5v pin on a Raspberry pi 3B+. According to that serial plotter running Analog Read Serial sketch my pot is outputting full range from 0 to 1023. I assume you are suggesting I somehow print out the execution of my main sketch, I need to try and figure out how to do that and I'll report the result. I have managed to add another "if" condition, making the first one 0-100 for pot and servo at starting position (500) and adjusted the rest of the code, so now it returns to zero, but my first (now second) step still produces an angle close to 15 degrees, instead of 25. Its 185 usec long (685 minus 500), at 7.4 usec per degree it should be moving it 25 degrees, as the next step does.

the code now looks like this

 #include <Servo.h>

Servo myServo;            // create a servo object
int potPin = A0;          // analog pin connected to the potentiometer
int potValue = 0;         // variable to store the potentiometer value
int targetMicroseconds = 0;  // target position in microseconds based on potentiometer
int currentMicroseconds = 500;  // initial position in microseconds 

void setup() {
  myServo.attach(9);        // attaches the servo on pin 9
  myServo.writeMicroseconds(currentMicroseconds);  // set initial position
}

void loop() {
  potValue = analogRead(potPin);   // read the potentiometer value (0-1023)

  // Determine the target microseconds based on potentiometer range
  if (potValue >= 0 && potValue <= 100) {
    targetMicroseconds = 500;
  } else if (potValue >= 101 && potValue <= 150) {
    targetMicroseconds = 685;
  } else if (potValue >= 151 && potValue <= 200) {
    targetMicroseconds = 870;
  } else if (potValue >= 201 && potValue <= 300) {
    targetMicroseconds = 1055;
  } else if (potValue >= 301 && potValue <= 400) {
    targetMicroseconds = 1240;
  } else if (potValue >= 401 && potValue <= 500) {
    targetMicroseconds = 1477;
  } else if (potValue >= 501 && potValue <= 600) {
    targetMicroseconds = 1662;
  } else if (potValue >= 601 && potValue <= 700) {
    targetMicroseconds = 1847;
  } else if (potValue >= 701 && potValue <= 800) {
    targetMicroseconds = 2032;
  } else if (potValue >= 801 && potValue <= 900) {
    targetMicroseconds = 2217;
  } else if (potValue >= 901 && potValue <= 1000) {
    targetMicroseconds = 2439;
  }

  // Gently rotate towards target microseconds at quarter speed
  if (currentMicroseconds < targetMicroseconds) {
    currentMicroseconds++;
    myServo.writeMicroseconds(currentMicroseconds);
    delay(1);   // delay to slow down rotation speed
  } 
  else if (currentMicroseconds > targetMicroseconds) {
    currentMicroseconds--;
    myServo.writeMicroseconds(currentMicroseconds);
    delay(1);   // delay to slow down rotation speed
  }
}

That may be a problem, especially once the servo is under load. The servo needs a power supply capable of giving enough current.

There is variance in these different servos. It would be much more useful to set up a simple sketch to move the servo to different positions and determine experimentally what microseconds value you need.

The servo library can also write directly to an angle. Is there a particular reason you're avoiding that?

Changing the written value by 1 microsecond every millisecond will end up taking a long time to move the servo.

1 Like

I assumed almost 2 amps from Rpi will be enough for testing without load, as for the real world use I was planning to power it from the bicycle 36v battery through a step down converter. Thank you for the suggestions, I'll start experimenting on the angles. As for the servo angle library - that is what I have tried initially with very poor results, so I've moved to microseconds. I'll probably setup a decent power supply as well and will report back.

Maybe you did it wrong.

Probably (definitely) ) Here is that code I used anyway, I'll have something to eat and start testing the angles from a simple sketch, the decent power supply will have to wait till tomorrow as I don't have it at home.

#include <Servo.h>

Servo myServo;      // create a servo object
int potPin = A0;    // analog pin connected to the potentiometer
int potValue = 0;   // variable to store the potentiometer value
int targetAngle = 0;   // target angle based on potentiometer value
int currentAngle = 0;  // current angle of the servo

void setup() {
  myServo.attach(9);    // attaches the servo on pin 9
  currentAngle = 0;     // initialize the current angle to 0
  myServo.write(currentAngle);  // set initial position
}

void loop() {
  potValue = analogRead(potPin);   // read the potentiometer value (0-1023)

  // Determine the target angle based on potentiometer range
  if (potValue >= 5 && potValue <= 100) {
    targetAngle = 25;
  } else if (potValue >= 101 && potValue <= 200) {
    targetAngle = 50;
  } else if (potValue >= 201 && potValue <= 300) {
    targetAngle = 75;
  } else if (potValue >= 301 && potValue <= 400) {
    targetAngle = 100;
  } else if (potValue >= 401 && potValue <= 500) {
    targetAngle = 132;
  } else if (potValue >= 501 && potValue <= 600) {
    targetAngle = 157;
  } else if (potValue >= 601 && potValue <= 700) {
    targetAngle = 182;
  } else if (potValue >= 701 && potValue <= 800) {
    targetAngle = 207;
  } else if (potValue >= 801 && potValue <= 900) {
    targetAngle = 232;
  } else if (potValue >= 901 && potValue <= 1000) {
    targetAngle = 262;
  }

  // Gently rotate towards target angle at quarter speed
  if (currentAngle < targetAngle) {
    currentAngle++;
    myServo.write(currentAngle);
    delay(5);   // small delay to slow down rotation speed
  } 
  else if (currentAngle > targetAngle) {
    currentAngle--;
    myServo.write(currentAngle);
    delay(5);   // small delay to slow down rotation speed
  }
}

If your reading is less than 5 or greater than 1000, the angle will be unchanged form whatever it was set to when one if the if tests passed.

Not a problem, maybe, as usually you would be moving slow and it would just stay at the max or minimum.

You can make the code a bit shorter

if (potValue <= 100) {
    targetAngle = 25;
  } else if (potValue <= 200) {
    targetAngle = 50;
  } else if (potValue <= 300) {
    targetAngle = 75;
  } else if (potValue <= 400) {
    targetAngle = 100;
  } else if (potValue <= 500) {
    targetAngle = 132;
  } else if (potValue <= 600) {
    targetAngle = 157;
  } else if (potValue <= 700) {
    targetAngle = 182;
  } else if (potValue <= 800) {
    targetAngle = 207;
  } else if (potValue <= 900) {
    targetAngle = 232;
  } else {
    targetAngle = 262;
  }

which is the same except I've added 0 to 5 to the first range tested, and the last range goes out to 1023.

You might like a switch/case statement here instead, using ranges makes things very clear and easy, viz:

  switch (potValue) {
  case 5 ... 100 :
    targetAngle = 25;
    break;

  case 101 ... 200:
     targetAngle = 50;
     break;

  case 201 ... 300:
    targetAngle = 75;
    break;

// 

  case 901 ... 1000:
    targetAngle = 262;
  }

where I've left out due to laziness writing the other cases.

a7

1 Like

What about map? Shorter still !

Something like this, providing the relationship is fairly linear:

potValue = constrain(potValue, 5 , 1000);
targetAngle = map(potValue, 5 , 1000, 25, 262);

I've added constrain in there in case you want to keep potValue to some valid range.
And map also has the same effect as constrain, on targetAngle in this case.
If the relationship is a bit more non-linear, but not too complex you can use multimap.

1 Like

Thank you, I like switch better, definitely easier to read and write. As for the full range of the pot, you are right, not letting the board know what to do in undeclared zones is sloppy, I should correct that.

I think that maps the pot to the angles evenly, doesn't it? I should have specified in the original post that not all servo move increments are the same, for the reason that some gears require 5mm pull to switch, while several are non-standard, like 6.4mm, that is why I've used ranges like that. Should have included that in the specs, my bad.

Ok, I have figured some things out, like for example why the servo angle library didn't do what I wanted. Its because the servo is 270 degrees and the library thinks its 180 (or the servo's firmware is the culprit). So if I rotate the servo 180 in the code, it moves 270. Dividing the angle seems to work, although I've read the suggestions that microseconds are the answer here. Anyway I'm still not getting the expected range when using microseconds, so probably should switch to the proper power supply and then start calibrating. Protractor isn't giving good accuracy for that so me thinking 3d printing some kind of rig with calipers, will be updating the thread as I move along. I need like 1-2 degrees accuracy to be confident that it will not finish off my gearbox when working live.

Yes, map uses a linear relationship between input and output values.
multimap may work if you can approximate a curve with straight-line segments.

Since you seem to have discreet steps, probably best to stick with the switch/case method.

++1 for using microseconds to control your 270 servo

1 Like

I hope you are doing this kind of experiment with the simplest possible code, just a sketch that you can change the endpoints on a sweep or something. Easier, nothing else to worry about.

You may prefer to change the settings that the library uses, so you can actually talk about real angles and hit them directly rather than tinker at the higher level.

In any case it's just calibration and with inexpensive servos needs be done.

a7

1 Like

@robtillaart has a multimap library available in the IDE.

yep, its still there

1 Like

Ok, its seems the issue of the inadequate first step has been solved. I've looked into Servo.h library setup file (something I should have probably have done at the start) and while my servo's full range was 500-2500 the library limited min and max values as follows:

#define MIN_PULSE_WIDTH       544     // the shortest pulse sent to a servo
#define MAX_PULSE_WIDTH      2400     // the longest pulse sent to a servo

hence my fists step was closer to 20 degrees instead of the 25 (in the code I have initially submitted).
I have changed those values in the Servo.h file and that problem has been remedied. I know it can be mapped during servo.attach, but I have decided to change it at the source. I also decided to stick with microseconds instead of angles for the time being, as the only solution I see for now is to map usecs to angles in the sketch (couldn't see where to change it in the setup file), so I'm sticking to microseconds. Many thanks to everyone who chipped in, I have certainly learned a lot from all your answers, and as my long-term goal is to learn Arduino programming - those answers provided a lot of useful information, both on how to improve the code as well as different approaches to solving problems. Here is the code I've ended up with:

#include <Servo.h>

Servo myServo;            // create a servo object
int potPin = A0;          // analog pin connected to the potentiometer
int potValue = 0;         // variable to store the potentiometer value
int targetMicroseconds = 0;  // target position in microseconds based on potentiometer
int currentMicroseconds = 500;  // initial position in microseconds (usually midpoint for most servos)

void setup() {
  myServo.attach(9);        // attaches the servo on pin 9
  myServo.writeMicroseconds(currentMicroseconds);  // set initial position
}

void loop() {
  potValue = analogRead(potPin);   // read the potentiometer value (0-1023)

  // Determine the target microseconds based on potentiometer range
  if (potValue >= 0 && potValue <= 10) {
    targetMicroseconds = 500;
  } else if (potValue >= 11 && potValue <= 100) {
    targetMicroseconds = 685;
  } else if (potValue >= 101 && potValue <= 200) {
    targetMicroseconds = 870;
  } else if (potValue >= 201 && potValue <= 300) {
    targetMicroseconds = 1055;
  } else if (potValue >= 301 && potValue <= 400) {
    targetMicroseconds = 1240;
  } else if (potValue >= 401 && potValue <= 500) {
    targetMicroseconds = 1477;
  } else if (potValue >= 501 && potValue <= 600) {
    targetMicroseconds = 1662;
  } else if (potValue >= 601 && potValue <= 700) {
    targetMicroseconds = 1847;
  } else if (potValue >= 701 && potValue <= 800) {
    targetMicroseconds = 2032;
  } else if (potValue >= 801 && potValue <= 900) {
    targetMicroseconds = 2217;
  } else if (potValue >= 901 && potValue <= 1023) {
    targetMicroseconds = 2439;
  }

  // Gently rotate towards target microseconds at quarter speed
  if (currentMicroseconds < targetMicroseconds) {
    currentMicroseconds++;
    myServo.writeMicroseconds(currentMicroseconds);
    delay(1);   // delay to slow down rotation speed
  } 
  else if (currentMicroseconds > targetMicroseconds) {
    currentMicroseconds--;
    myServo.writeMicroseconds(currentMicroseconds);
    delay(1);   // delay to slow down rotation speed
  }
}