0

We all know servos don't move instantly, which is why it is common to put a delay or a millis() loop whenever you issue a servo.write() command to give it time to reach its target before moving on to the next thing.

What irks me though is that this could be inefficient in situations where the last servo position might be close to its target - you might only need a very short delay in those cases, whereas if it has to sweep the full 359 degrees, you might want more time.

I was disappointed that servo.read() is completely useless for this task (why even include it in the library?) as the following code illustrates:

#include <Adafruit_TiCoServo.h>
Adafruit_TiCoServo servo;
const uint8_t PIN_SERVO = 9;

void setup() {
  servo.attach(PIN_SERVO);
  Serial.begin(9600);


  Serial.println("start");
  servo.write(180);
  delay(1000);

  int pos;
  while(pos != 0){
    pos = servo.read();
    Serial.println(pos); // because it only returns what the last write() operation
                         // was sent, it immediately thinks it's at pos 0 and exits.
    servo.write(0);
  }

  Serial.println("Done");
}

So that sucks. It seems that my options are:

  1. just use a long delay that accommodates the worst-case scenario. I hate this.
  2. add a limit switch at the final target position and just read the state of that switch
  3. build some kind of elaborate encoder that you can read an absolute angle from or buy a more expensive servo that can tell you this info

For reference the servo I'm playing with is a hobby servo: MG90S

Specifically my question is to confirm that there are no other options available for the MG90S other than what I've listed above.

3
  • 1
    What exactly are you trying to achieve? Which solution is the best depends on the actual target of your project. If you only want to wait for the servo to finish moving before continuing with your code, how about scaling the delay linearly with the angle difference? You can find the factor by testing it yourself. Also keep in mind, that this is also dependent on the load on the servo axle.
    – chrisl
    Commented Oct 27, 2022 at 16:27
  • @jsotola sure fair enough, but I'll leave it to you to skip the parts you consider offensive and focus on an answer. And to you other comment, fair point - I have attempted to make it more obvious what I'm asking. You know how these questions go - you start to write them and end up writing more of a solution than you expected to. But I still want to know from others whether there are other approaches.
    – Tom Auger
    Commented Oct 27, 2022 at 17:52
  • 1
    I regard servo.read() as a convenience function. I do not need to remember the last target position, I can ask the object, across other functions and methods. No hobby servo I know feeds back the actual position, because they are usually used in radio-controlled models, which have no feedback path. Commented Oct 28, 2022 at 6:25

2 Answers 2

1

in this case there is mostly one simple solution, and there are some better ones.

the problem you faced with the method: First of all, the read() function indeed only returns what you send to it, it mostly is useful for if you want to know what position you send it to in some other part of code(for example can be used to plan what way to go first if you need to go to many points, or if you already are going to the desired position). but since the rc servo has only one signal wire and that wire sends no signals back you don't know what state they are in, rc servo's are mostly for ease of use and not for accuracy and speed, if you need that you can look into a real servo(stepper motor). so yes that function doesn't return the actual position, it returns the desired position.

Solution1: the easy software based method: to do this you can either read the datasheet to read the speed in duration per degree or measure it, and then use that to calculate the time it would take. actually here the Servo.read() function really comes in use, so this is a perfect example of why you would actually sometimes need or want it.

  1. save the duration in miliseconds per degree in a variable.
  2. next just before you write to the servo use Servo.read() to get the current theoretical position and use that combined with the position you want to go to calculate the difference in dergees (for example 100 and 40 results in a difference of 60), then multiply that with the duration per step. and you will have the answer of how long a change would take.

Example1 warning, untested, might contain spelling mistakes and such

    /* tuned delay Sweep
  This example code is in the public domain.
  No warranty

  modified 27 March 2023
  by Daan V.Loon
*/

#include <Servo.h>
#define DELAYPERDEGREE 2 //this is the value for the sg90 servo from the datasheet(0,12s(120ms)per 60degrees.
#define SAFETYMARGIN 110 //this is to add 10% extra time to wait, to make sure it finishes in time even under load. 
//# define is used kind of like a variable that doesn't need to change, purely in code and not in ram
unsigned long waitDuration=0;//variable that shows how long to wait.

Servo myservo; 

unsigned long CalcWait(Servo s,int desiredPosition)//method to calculate and return duration to wait in ms 1000ms is 1 s.
{
  int spos=s.read();
  if(spos>desiredPosition){return (((spos-desiredPosition)*DELAYPERDEGREE)*SAFETYMARGIN)/100;}
  else{ return(((desiredPosition-spos)*DELAYPERDEGREE)*SAFETYMARGIN)/100;}
}//end of method

void ServoAndWait(int pos){
  //insert range limiting code here if needed, warning this method doesn't check if you try to use numbers bigger or smaller than your servo or the servo library can actually handle.
  waitDuration=CalcWait(myservo,pos);//call method to update how long to wait, needs to be done before telling the servo to move.
  myservo.write(pos);//tell servo to rotate
  delay(waitDuration);//blocking wait for servo to complete
}
void setup() {
  myservo.attach(2); 
  ServoAndWait(180);//rotate to 180 degrees and wait until it should be there+10% with the safetymargin of 110
}

void loop() {
    ServoAndWait(0);
    ServoAndWait(180);
    //this should reult in the famous sweep example, however then at the max speed of your servo, this way you can test it.
    //to optimize speed use a safetymargin far over 100(like 200 or such) then reduce it until you see it just stop or just not stop anymore at the edges.
    //also make sure to first test if your servo can actually handle 0 and 180 degrees, for example first test if it moves if you tell it to go from 10 to 0 and from 170 to 180, not all servos reach the full 180 degree range.
    
  }

Example2:(no comments)

/* tuned delay Sweep
  This example code is in the public domain.
  No Warranty

  modified 27 March 2023
  by Daan V.Loon
*/

#include <Servo.h>
#define DELAYPERDEGREE 2
#define SAFETYMARGIN 110
unsigned long waitDuration = 0;
Servo myservo;

unsigned long CalcWait(Servo s, int desiredPosition)
{
  int spos = s.read();
  if (spos > desiredPosition) {
    return (((spos - desiredPosition) * DELAYPERDEGREE) * SAFETYMARGIN) / 100;
  }
  else {
    return (((desiredPosition - spos) * DELAYPERDEGREE) * SAFETYMARGIN) / 100;
  }
}

void ServoAndWait(int pos) {
  waitDuration = CalcWait(myservo, pos);
  myservo.write(pos);
  delay(waitDuration);
}
void setup() {
  myservo.attach(2);
  ServoAndWait(180);
}

void loop() {
  ServoAndWait(0);
  ServoAndWait(180);
}

Example3: Shorter and more stack overflow alien themed for people who just copy paste or find this clearer

/* tuned delay Sweep
  This example code is in the public domain.
  No Warranty

  modified 27 March 2023
  by Daan V.Loon
*/

#include <Servo.h>
#define DELAYPERDEGREE 2
#define SAFETYMARGIN 110

Servo myservo;

unsigned long CalcWait(Servo s, int desiredPosition)
{
  int spos = s.read();
  return (spos>desiredPosition?((spos - desiredPosition) * DELAYPERDEGREE * SAFETYMARGIN) / 100 : ((desiredPosition - spos) * DELAYPERDEGREE * SAFETYMARGIN) / 100);
}

unsigned long waitDuration = 0;
void ServoAndWait(int pos) {
  waitDuration = CalcWait(myservo, pos);
  myservo.write(pos);
  delay(waitDuration);
}

////---code you would see below, headers and functions and variables above---////
void setup() {
  myservo.attach(2);
}

void loop() {
  ServoAndWait(0);
  ServoAndWait(180);
}

These above should be fully functional codes that should work for you, they all work the same. I didn't test it with a servo however, but the logic worked when I tested that. also make sure to change the delay per degree to whatever your datasheet says, in this case the datasheet said 0.12s/60 degrees sg90(very common servo for arduino) so I turned seconds into miliseconds and divided it by 60, so 120/60=2, if you have a much faster servo or need more accurate results for different servos you can use the safety margin to tune, or you can use microseconds in the calculations or such, or just multiply the numbers by 10 and then at the end of the calculations divide it by 10 before returning.

Then here is also the more difficult hardware version to do this you can either try to use the servo's buildin hardware and modify the servo with a 4th wire, either add it to the rotation sensor/potentiometer, or add it to the chip if it has a place where you can read it directly, with this you are on your own however since it is very speciffic for every servo.

a more easy way to do this is to use a compactor(or something else that gives you one digital output(typical rc servos will always keep the motor on,and just very rapidly change direction once they are on position, if it actually stops you might just want to read it like a normal digital signal(perhaps do something to protect against motor spikes), and read the wires both standalone instead of comparing them to each other) to compare the voltages on the wires to the motor in the servo, since when it moves one side has a higher voltage than the other side, when it stops, they either are equal or rapidly change to keep position. so you just check for whenever it changes. or if you know the direction you can be even more accurate and fast. this ofcource requires you opening up the servo.

or you can indeed add something like a rotary encoder, no need for a complex one, since you can just count the stripes and if you suddenly don't get changes anymore the servo is also stopped in case you missed some, or you can use something like a small microphone or vibration meter since the servo when it moves, this however may become inaccurate under higher load. just try to avoid reading it with analog signals, since for what you want to use it for analog signals can be really slow.

also you might want to look into non blocking delays, it won't make the servo's faster, but it allows you to do other things while the servo is moving so you won't have to wait until it is finished.

4
  • Re solution 1: I would expect the servo speed to depend on the load, which would invalidate any computation based on a fixed speed. Commented Mar 28, 2023 at 8:13
  • 1
    Thanks for this detailed answer. The project is long over (I can't remember which workaround I ended up using), but I'll definitely be looking this up the next time I'm playing around with RC servos!
    – Tom Auger
    Commented Mar 28, 2023 at 23:11
  • @EdgarBonet partly yes, depends on the servos partly how much so, but in typical loads where the servo isn't really overloaded it should behave similar to what is stated in the datasheet, when it is overloaded you might want to measure it instead(also mentioned that), but I also speciffically added the SAFETYMARGIN to the code for hat speciffic thing, it allows you to set a safety margin or finetune code so that it works well. most cases where a servo needs to move and all else needs to stop then would be a quite controlled proces, so less expectations for strange behaviour. Commented Mar 29, 2023 at 22:08
  • but indeed when a servo is constantly under different load it might have slightly different speeds, so you will need one of those hardware modifications like the ServoMotorWireReadWires method I added below the code, but software is almost always a tradeoff compared to hardware. but as long as the servo isn't overloaded or close to it it should typically be around the speed in the datasheet, or faster. since often for the speed in the datasheet they take the speed under load(even though that differs per manufacturer and the amount of load as well. reading motor wires instantly shows changes. Commented Mar 29, 2023 at 22:12
-1

Yes, normally servo.read() is pretty useless.

First, check your library with a sketch like this

void setup() {
  servo.attach(PIN_SERVO);
  Serial.begin(9600);

  Serial.println("start");
  servo.write(180);
  delay(1000);
  unsigned long start = millis(); 
  servo.write(0);
  unsigned long duration = millis() - start;
  if (duration <= 1) 
     Serial.println("servo.write() returns immediately");
  else {
     Serial.print("servo.write() takes ");
     Serial.print(duration);
     Serial.println(" ms for a 180° move");
  }
}

If it returns immediately, you have to really measure somehow the actual speed and eventually follow chrisl's comment.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.