Servos

(top)

Specifically the popular SG90 type. Here is a collection of principles that apply to their use in model railways. Some Arduino-based examples (more to follow). As yet I seem not to have come across the reported digital version of the SG90.

Dave (acE)Mason, Sept 2020.

Ways servos are used on model railways

(top - Servos)

SG90 servos are intended for use in model aircraft. They are also used in robotics. A lot of descriptions, and solutions to buy or build, therefore assume say six servos, with quite short wiring, which must respond as rapidly as possible and simultaneously - quite different from on a model railway.

How do they work?

(top - Servos)

They have an arm that is rotated by an electric motor via a gearbox. It typically can rotate through about 180 degrees. They have 3 wires. However the motor is not simply powered to run in one direction or the other. Two of the wires supply power - nominally 5V and 0v. The third wire carries a digital control signal, either "low" (near 0V) or "high" (near 5V). This control signal most of the time is "low" but calibrated pulses, short periods "high", are sent to move the servo. A single pulse 1.5ms (milliseconds) long will move the arm to the centre of its range of travel.

Inside the servo a potentiometer on the output shaft provides to the servo electronics a feedback Voltage proportional to the arm position. The servo electronics compare this (position/Voltage) to the length of the incoming control pulse and send a short burst of power to the motor which is calibrated to remove the error/difference and put the arm in a position corresponding to the pulse length. In practice a single pulse does not do the job exactly - depending on how hard the motor has to work overcoming friction, springs etc, and other factors such as supply Voltage and temperature.

For that reason pulse are typically sent in a continous stream, one pulse every 20ms. If the pulse length is constant the servo arm may reach the desired position by the third or fourth pulse. In model arcraft and robotics the length of the pulses is more likely being continuously changed providing a moving target for the servo to try to follow. The 20ms interval dates from the early days of Radio Control and is now merely a convention. You can send pulses whenever you like - though results may become unpredictable if the gap between pulses is less than a couple of ms.

Having said that 1.5ms pulses put the servo arm at the central position - say 90degrees - this is how different pulse lengths typically affect the position: 0.5ms = 0degrees, 1.0ms = 45degrees, 1.5ms = 90degrees, 2.0ms = 135degrees, 2.5ms = 180degrees - and all points in between.

How does the Arduino world support servos?

(top - Servos)

There are several options:

Bit-bashing servo control in Arduino.

(top - Servos)
  1. Start with just one servo. Connect its 0V and 5V* to those pins on an Arduino and choose a digital output pin to connect the third wire to, for the control signal. *This will work but later you may want to use a separate 5V source, or at least add a reservoir capacitor, because the servos take a lot of current (around 0.5A) in tiny bursts to move the arm.
  2. In essence, to centralise the servo arm, you just take the output pin high, wait 1.5ms, take it low again. The delay() function that all Arduino beginners encounter takes a parameter in ms (milliseconds) and doesn't accept fractions. So instead use the delayMicroseconds() function, though note that most Arduinos can only count their microseconds (us) to the nearest 4us. Note this sketch only supplies one pulse. If the servo arm is already in the middle it won't move - you can twist it to another position by hand, reset/restart the sketch and the arm will centralise.
    const int servoPin =  19;
    
    byte pin_state;
    long pulse_us = 1500; // range 500 - 2500
    
    void setup() {
        pinMode(servoPin, OUTPUT);
    
        pin_state = 1;
        digitalWrite(servoPin, pin_state);  
           
        delayMicroseconds(pulse_us);
    
        pin_state = 0;
        digitalWrite(servoPin, pin_state);
    }
    
    void loop() {
    
    }

  3. The next step is to send a stream of pulses. Most of the statements are moved into the loop() function. For more interest the pulse length is slowly changed:
    
    const int servoPin =  19;
    const long min_us = 1000;
    const long max_us = 2000;
    
    byte pin_state;
    long pulse_us = 1500; // range 500 - 2500
    long step_us = 4;
    long interval_us = 20000; // range around 5000 upwards
    
    void setup() {
        pinMode(servoPin, OUTPUT);
    }
    
    void loop() {
        if (pulse_us <= min_us or max_us <= pulse_us) {
            step_us = -step_us; // changes direction
        }
        pulse_us += step_us;
    
        pin_state = 1;
        digitalWrite(servoPin, pin_state);     
        
        delayMicroseconds(pulse_us);
    
        pin_state = 0;
        digitalWrite(servoPin, pin_state);
    
        delayMicroseconds(interval_us);
    }
    

  4. The delay() function (whether ms or us) is best avoided because it blocks the microcontroller from doing anything else - like controlling more than one servo. So the next step is to do the timing a better way - note that time intervals are compared, rather than time(of day) itself. This will then work smoothly even when the numbers exceed the limit for a long integer.
    const int servoPin =  19;
    const long min_us = 1000;
    const long max_us = 2000;
    
    byte pin_state;
    long pulse_us = 1500; // range 500 - 2500
    long step_us = 4;
    long interval_us = 20000; // range around 5000 upwards
    long pulse_begin_us;
    long now_us;
    
    void setup() {
        pinMode(servoPin, OUTPUT);
    }
    
    void loop() {
        now_us = micros(); // the time since Reset
        if (pin_state==0) {
            if ((now_us - pulse_begin_us) > interval_us) {
                if (pulse_us <= min_us or max_us <= pulse_us) {
                    step_us = -step_us; // changes direction
                }
                pulse_us += step_us;
    
                pin_state = 1;
                digitalWrite(servoPin, pin_state);         
                pulse_begin_us = now_us;
            }
        } else { // pin_state==1
            if ((now_us - pulse_begin_us) > pulse_us) {
                pin_state = 0;
                digitalWrite(servoPin, pin_state);
            }
        }
    }

  5. Slowly changing the pulse length was just for demonstration. Here is how it can be changed on demand. It will respond to a value in microseconds typed into the serial monitor. This is processed in the interval in pulses in order to not corrupt the pulse duration timing. Also once the target position is reached a limited quantity of pulses is sent (16) rather than a continuous stream. So it will again be possible to twist the servo arm by hand when the servo is inactive. The servo will always jump to its mid-position on Reset.
    const int servoPin =  19;
    const long min_us =  500;
    const long max_us = 2500;
    const long serialBaud = 115200; // set the Serial Monitor to the same rate
    const byte pulses = 16;
    
    byte pin_state;
    long pulse_us = 1500; // range 500 - 2500
    long step_us = 4;
    long interval_us = 20000; // range around 5000 upwards
    long pulse_begin_us;
    long now_us;
    long target_us = 1500;
    byte pulse_counter;
    
    void setup() {
        pinMode(servoPin, OUTPUT);
        Serial.begin(serialBaud); // Serial.begin causes MCU reset within 10ms.
        delay(100);               // allows Serial to settle
    }
    
    void loop() {
        now_us = micros(); // the time since Reset
        if (pin_state==0) {
            if (Serial.available() > 0) {
                String inStr = Serial.readString();
                long new_us = long(inStr.toInt());
                if (new_us < min_us or max_us < new_us) {
                    Serial.println(inStr+" is out of range");
                } else {
                    target_us = new_us; // change position
                    pulse_counter = 0;
                    Serial.println(target_us);
                }          
            }
            if (pulse_us != target_us and pulse_counter < pulses) {                
                if ((now_us - pulse_begin_us) > interval_us) {
                    pin_state = 1;
                    digitalWrite(servoPin, pin_state);         
                    pulse_begin_us = now_us;
                    if (pulse_us == target_us) {
                        pulse_counter++ ;
                    } else if (pulse_us < target_us) {
                        pulse_us = min(target_us, pulse_us + step_us);
                    } else { // if (pulse_us> target_us) {
                        pulse_us = max(target_us, pulse_us - step_us);
                    }    
                 }
            }
        } else { // pin_state==1
            if ((now_us - pulse_begin_us) > pulse_us) {
                pin_state = 0;
                digitalWrite(servoPin, pin_state);
            }
        }
    }

  6. Further stages will be to create OFF and ON positions and toggle between them, to operate several servos, to control the speed of movement, including semaphore "bounce" - with functionality moved from loop() into separate functions.

    The example above uses the Arduino Serial Monitor to allow the user to change values (and to provide feedabck to the user), This is useful for learning and for developing a sketch/program to work the way you want. In operation the required actions could be triggered by the state of another of the Arduino digital input pins.

    Operating several servos can mean simultaneously, as in a model aircraft, or it can mean in groups, as in a 4-gate crossing, or it can just mean the Arduino is connected to 15 points/turnouts but only needs to operate one at a time - others can wait in a short queue. The challenge is to accurately time the end of each pulse.

    Suppose you want to control 4 servos running at the same time - for crossing gates. You will have fine-tuned the start and end angles. At any time each of the 4 servos probably require a different length pulse. One trick is to sort the servos into order, the one with the shortest pulse first, longest last.

    You then start the pulses at, say, 100us intervals (takes 300us if there are 4 servos) and then you have 200us to wait for the time to end the first pulse - if it has the very minimum pulse length of 500us. After that you have at least 100us to wait for the time to end the next servo's pulse - since it was started 100us after the previous servo's pulse and is longer. And so on.

    With 4 servos and a maximum pulse length of 2500us it's all over in 2800us. The microcontroller can be tied up during that period watching for when to end pulses but after that it can be assigned other tasks without compromising the pulse timing accuracy, such as looking for input and sorting the servos for the next round of pulses. In principle this can be extended to hundreds of servos and the limitation is storing all the data in the Arduino.