Controlling Positional Servos with Python

Controlling Positional Servos with Python

November 7, 2019 by Todd Beales

In our first installment of this series we discussed the theory of operation for continuous rotation servos that spin continuously in either direction. Positional servos are slightly different. They operate under the assumption that we only want the servo to turn to a certain position and then stop. In fact, the positional servo will prevent us from turning beyond certain limits. Generally they have a 90-180 degree range of motion, but there are many different varieties that can turn to different limits. However, the theory of operation is going to be identical for nearly all positional servos.

Check out the video demo here:

The connection details and basic code will be the same as for continuous rotation servos, so see the previous blog post for that information.

These servos are driven by a stream of pulses, just like the continuous rotation servos we discussed previously. But now the pulse duration is used to control the position of the servo arm, rather than the direction/speed of rotation.

Positional servos operate with the following rules, based on the duration of the pulses sent every 20 ms:

  1. If the pulse is high or “ON” for 1.0 ms during those 20 ms then the Servo will position itself to its standard clockwise position
  2. If the pulse is high or “ON” for 1.5 ms during those 20 ms then the Servo will position itself to its center position (halfway in between standard clockwise and standard counterclockwise)
  3. If the pulse is high or “ON” for 2.0 ms during those 20 ms then the Servo will position itself to its standard counterclockwise position

You can also set anything in between to get an intermediate position. For example: setting the pulse high for 1.2 ms will position the servo at 60% from center in the clockwise direction. See the table below for more values.

Pulse High TimePercent Clockwise from CenterDegree Turn from Center (Typical)
1.0 ms100%90
1.1 ms80%72
1.2 ms60%54
1.3 ms40 %36
1.4 ms20%18
1.5 ms0%0
1.6 ms-20%-18
1.7 ms-40%-36
1.8 ms-60%-54
1.9 ms-80%-72
2.0 ms-100%-90

Note: Many servos will respond to pulse durations outside the limits stated above! Experiment with your servo to see what its full range of motion really is. Try shorter and longer pulses, for example 0.5 ms and 2.5 ms pulse widths. The true p_min and p_max values can become constants in your Python code!

Servo Position Feedback and Jitter

A positional servo is different from a continuous rotation servo in that there is no default OFF position. If you set the continuous servo to 1.5 ms, it will stop rotating and the motor will generally stop completely. The positional servo is different. If you set it to 1.5 ms, it will continuously attempt to move the servo to the zero-degree position. Try as you might to turn the servo-arm after it is set, it will always try to get back to its set position. One example use is setting the position of a ship’s sail. You don’t want the wind to push the sail to a new position once it has been set! You always want it to stay in position regardless of outside forces.

Servos hold their position by using a control loop. If you want to learn more about how that works, check out our Python with Robots curriculum (opens in a new tab) where you build one from scratch in Python!

With low-cost servos the feedback is provided by a small potentiometer, which can sometimes produce a "noisy" signal - making the control circuit think the servo arm has moved a bit and needs to be repositioned... even when it is sitting still. That can cause "jitter" - you'll hear the motor buzzing a bit, and maybe see the servo arm shake back and forth like its had a bit too much coffee!

Python Code Examples:

Code Example 1: Bare Minimum Code

from microbit import *
 
pin0.set_analog_period(20)
 
def main ()
    while True: 
        pin0.write_analog(1023 * 1 / 20)   # position 45 degrees clockwise
        sleep(750)
        pin0.write_analog(1023 * 2 / 20)   # position 45 degrees couterclockwise
 
main()

Code Example 2: An Example with Functions

from microbit import *
 
# 2.0 ms on = 100% counterclockwise position (0 degrees)
# 1.75 ms on = 50% counterclockwise position (45 degrees)
# 1.5 ms on = Center position (90 degrees)
# 1.25 ms on = 50% clockwise position (135 degrees)
# 1.0 ms on = 100% clockwise position (180 degrees)
 
def set_servo_pulse(pin, ms_on)
    # The full-scale analog value for the Microbit is 1023 which equals 20ms
    # To set the amount of "ON" time we will:
    # divide the ms_on by 20 to get a percentage of "ON" time
    # multiply the percentage by the full-scale value of 1023
    pin.write_analog(1023 * ms_on / 20)
 
def init_servo_pin(pin):
    # Set a 20 ms period (50 Hz pulse stream)
    pin.set_analog_period(20)
    # Set at the stopped position (1.5ms)
    set_servo_pulse(pin, 1.5)
 
def main()
init_servo_pin(pin0)  
 
    set_servo_pulse(pin0, 1.0)   # 100% cw position (180 deg)
    sleep(2000)
    set_servo_pulse(pin0, 2.0)   # 100% cw position (0 deg)
    sleep(2000)
    set_servo_pulse(pin0, 1.5)   # 100% cw position (90 deg)
 
main()

Code Example 3: An Expert "Class" Example

from microbit import *
 
class PositionalServo:
    def __init__(self, io_p_in=pin0):
        self.pin = io_pin
        # 20 ms = 50 Hz the RC Servo Standard
        self.pin.set_analog_period(20)
        self.set_degree(0)
 
    def set_ms_pulse(self, ms_on)
        self.pin.write_analog(1023 * ms_on / 20)
 
    def set_degree(self, deg):
        self.degrees = deg
        # convert degrees to ms => 180 = 1.0ms, 0 = 2.0ms
        self.set_ms_pulse(2 + (-self.degrees / 180))
 
    def rotate_clockwise(self):
        new_deg = min(180, self.degrees + 1)
        self.set_degree(new_deg)
 
    def rotate_counterclockwise(self):
        new_deg = max(0, self.degrees - 1)
        self.set_degree(new_deg)
 
servo1 = PositionalServo()
 
while True:
    sleep(20)
 
    if button_b_is_pressed() and not button_a_is_pressed():
        # hold button_b to rotate the servo clockwise
        servo1.rotate_clockwise()
 
    if button_a_is_pressed() and not button_b_is_pressed():
        # hold button_a to rotate the servo counterclockwise
        servo1.rotate_counterclockwise()