Chapter 13 Servo

Previously, we learned how to control the speed and rotational direction of a DC Motor. In this chapter, we will learn about Servos which are a rotary actuator type motor that can be controlled rotate to specific angles.

Project Sweep

First, we need to learn how to make a Servo rotate.

Component knowledge

Servo

Servo is a compact package which consists of a DC Motor, a set of reduction gears to provide torque, a sensor and control circuit board. Most Servos only have a 180-degree range of motion via their “horn”. Servos can output higher torque than a simple DC Motor alone and they are widely used to control motion in model cars, model airplanes, robots, etc. Servos have three wire leads which usually terminate to a male or female 3-pin plug. Two leads are for electric power: Positive (2-VCC, Red wire), Negative (3-GND, Brown wire), and the signal line (1-Signal, Orange wire) as represented in the Servo provided in your Kit.

../../../_images/Chapter13_00.png

We will use a 50Hz PWM signal with a duty cycle in a certain range to drive the Servo. The lasting time 0.5ms-2.5ms of PWM single cycle high level corresponds to the Servo angle 0 degrees - 180 degree linearly. Part of the corresponding values are as follows:

Note

the lasting time of high level corresponding to the servo angle is absolute instead of accumulating. For example, the high level time lasting for 0.5ms correspond to the 0 degree of the servo. If the high level time lasts for another 1ms, the servo rotates to 45 degrees.

High level time

Servo angle

0.5ms

0 degree

1ms

45 degree

1.5ms

90 degree

2ms

135 degree

2.5ms

180 degree

When you change the Servo signal value, the Servo will rotate to the designated angle.

Component List

Freenove Projects Board for Raspberry Pi

Chapter01_04

Raspberry Pi

Chapter01_05

GPIO Ribbon Cable

Chapter01_06

Jumper Wire

Chapter05_02

Servo

Chapter13_01

Circuit

Schematic diagram

Chapter13_02

Hardware connection:

Chapter13_03

Note

If you have any concerns, please send an email to: support@freenove.com

Sketch

In this chapter, we will learn how to control the servo to rotate at the range of 0 to 180 degrees.

Sketch_13_1_Sweep

First, enter where the project is located:

$ cd ~/Freenove_Kit/Pi4j/Sketches/Sketch_13_1_Sweep
../../../_images/Chapter13_06.png

Enter the command to run the code.

$ jbang Sweep.java
../../../_images/Chapter13_07.png

When the code is running, you can see the servo rotate between 0 to 180 degrees.

../../../_images/Chapter13_08.png

Meanwhile, the messages are printed on the terminal.

../../../_images/Chapter13_09.png

Press Ctrl+C to exit the code.

../../../_images/Chapter13_10.png

You can run the following command to open the code with Geany to view and edit it.

$ geany Sweep.java

Click the icon to run the code.

../../../_images/Chapter13_11.png

If the code fails to run, please check Geany Configuration.

The following is program code:

  1///usr/bin/env jbang "$0" "$@" ; exit $?
  2
  3//DEPS org.slf4j:slf4j-api:2.0.12
  4//DEPS org.slf4j:slf4j-simple:2.0.12
  5//DEPS com.pi4j:pi4j-core:2.6.0
  6//DEPS com.pi4j:pi4j-plugin-raspberrypi:2.6.0
  7//DEPS com.pi4j:pi4j-plugin-gpiod:2.6.0
  8
  9import com.pi4j.Pi4J;
 10import com.pi4j.context.Context;
 11import com.pi4j.io.gpio.digital.DigitalOutput;
 12import com.pi4j.util.Console;
 13import java.util.HashMap;
 14import java.util.Map;
 15
 16class PWMController implements Runnable {
 17    private DigitalOutput pwm;
 18    private int pwmFrequency;
 19    private double pwmDutyCycle;
 20    private boolean running = true;
 21    private long period;
 22    private long highTime;
 23    private long lowTime;
 24
 25    public PWMController(DigitalOutput pwm) {
 26        this.pwm = pwm;
 27        this.pwmFrequency = 1000;
 28        this.pwmDutyCycle = 0.5;
 29        this.period = (int) (1000000 / pwmFrequency);
 30        this.highTime = (int) (period * pwmDutyCycle);
 31        this.lowTime = (int) (period - highTime);
 32    }
 33
 34    @Override
 35    public void run() {
 36        while (running) {
 37            if (highTime != 0) {
 38                pwm.high();
 39                delayUs(highTime);
 40            }
 41            if (lowTime != 0) {
 42                pwm.low();
 43                delayUs(lowTime);
 44            }
 45        }
 46    }
 47
 48    public void setPwmFrequency(int frequency) {
 49        if (frequency != 0) {
 50            this.pwmFrequency = frequency;
 51            this.period = (int) (1000000 / pwmFrequency);
 52            this.highTime = (int) (period * pwmDutyCycle);
 53            this.lowTime = (int) (period - highTime);
 54        } else {
 55            this.pwmFrequency = 0;
 56            this.period = (int) (1000);
 57            this.highTime = (int) (0);
 58            this.lowTime = (int) (period - highTime);
 59        }
 60    }
 61
 62    public void setPwmDutyCycle(double dutyCycle) {
 63        this.pwmDutyCycle = dutyCycle;
 64        this.highTime = (int) (period * pwmDutyCycle);
 65        this.lowTime = (int) (period - highTime);
 66    }
 67
 68    private void delayUs(long us) {
 69        long startTime = System.nanoTime();
 70        long endTime = startTime + (us * 1000);
 71        while (System.nanoTime() < endTime) {
 72        }
 73    }
 74
 75    public void requestStop() {
 76        running = false;
 77    }
 78}
 79
 80class Servo {
 81    private final int pin;
 82    private final PWMController pwmController;
 83    private final Context pi4j;
 84
 85    public Servo(int pin) throws Exception {
 86        this.pin = pin;
 87        this.pi4j = Pi4J.newAutoContext();
 88        DigitalOutput pwm = pi4j.dout().create(pin);
 89        this.pwmController = new PWMController(pwm);
 90
 91        Thread pwmThread = new Thread(pwmController, "PWM Controller for Servo on PIN " + pin);
 92        pwmThread.start();
 93
 94        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
 95            pwmController.requestStop();
 96            try {
 97                pwmThread.join();
 98            } catch (InterruptedException e) {
 99                Thread.currentThread().interrupt();
100            }
101        }));
102    }
103
104    public void setFrequency(int frequency) {
105        pwmController.setPwmFrequency(frequency);
106    }
107
108    public void setDutyCycle(double dutyCycle) {
109        pwmController.setPwmDutyCycle(dutyCycle);
110    }
111
112    public void setAngle(int angle) {
113        if (angle > 180 || angle < 0) {
114            return;
115        }
116        double dutyCycle = (((double) angle / 180.0 * 2) + 0.5) / 20;
117        pwmController.setPwmDutyCycle(dutyCycle);
118    }
119
120    public void shutdown() {
121        pwmController.requestStop();
122        pi4j.shutdown();
123    }
124}
125
126public class Sweep {
127    private static int SERVO_PIN = 18;
128    private static final Map<Integer, Servo> servoMap = new HashMap<>();
129
130    public static void myPrintln(String format, Object... args) {
131        Console console = new Console();
132        console.println(String.format("\u001B[32m" + format + "\u001B[0m", args));
133    }
134
135    public static void main(String[] args) throws Exception {
136        servoMap.put(SERVO_PIN, new Servo(SERVO_PIN));
137        Servo servo = servoMap.get(SERVO_PIN);
138        servo.setFrequency(50);
139        servo.setDutyCycle(0.075);
140
141        myPrintln("Start sweeping ...");
142        Thread.sleep(3000);
143
144        try {
145            while (true) {
146                for (int i = 0; i < 180; i++) {
147                    servo.setAngle(i);
148                    myPrintln("The Angle is %d", i);
149                    Thread.sleep(20);
150                }
151                for (int i = 180; i > 0; i--) {
152                    servo.setAngle(i);
153                    myPrintln("The Angle is %d", i);
154                    Thread.sleep(20);
155                }
156            }
157        } finally {
158            if (servo != null) {
159                servo.shutdown();
160            }
161        }
162    }
163}

Servo constructor, initializes the servo control pins, and adds a JVM shutdown hook to ensure that the PWM controller and Pi4J context are properly closed when the program exits.

 1public Servo(int pin) throws Exception {
 2    this.pin = pin;
 3    this.pi4j = Pi4J.newAutoContext();
 4    DigitalOutput pwm = pi4j.dout().create(pin);
 5    this.pwmController = new PWMController(pwm);
 6
 7    Thread pwmThread = new Thread(pwmController, "PWM Controller for Servo on PIN " + pin);
 8    pwmThread.start();
 9
10    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
11        pwmController.requestStop();
12        try {
13            pwmThread.join();
14        } catch (InterruptedException e) {
15            Thread.currentThread().interrupt();
16        }
17    }));
18}

Set the PWM duty cycle according to the angle to control the servo to rotate to the specified position.

1public void setAngle(int angle) {
2    if (angle > 180 || angle < 0) {
3        return;
4    }
5    double dutyCycle = (((double) angle / 180.0 * 2) + 0.5) / 20;
6    pwmController.setPwmDutyCycle(dutyCycle);
7}

Initialize the servo controller and store it in servoMap.

1servoMap.put(SERVO_PIN, new Servo(SERVO_PIN));

Get the servo controller from servoMap.

1Servo servo = servoMap.get(SERVO_PIN);

The signal period of the servo is 20ms. According to the formula f=1/T, the frequency of the servo is set to 50Hz.

To control the servo to rotate to 0 degrees, a 1.5ms high level is required. 1.5ms/20ms=0.075, so the duty cycle of the servo is 0.075. Similarly, if you want the servo to rotate to 180 degrees, a 2.5ms high level is required, 2.5ms/20ms=0.375. You only need to modify the duty cycle value to control the rotation of the servo.

1servo.setFrequency(50);
2servo.setDutyCycle(0.075);

The main code controls the servo to rotate between 0-180 degrees and prints prompt information on the terminal interface.

 1while (true) {
 2    for (int i = 0; i < 180; i++) {
 3        servo.setAngle(i);
 4        myPrintln("The Angle is %d", i);
 5        Thread.sleep(20);
 6    }
 7    for (int i = 180; i > 0; i--) {
 8        servo.setAngle(i);
 9        myPrintln("The Angle is %d", i);
10        Thread.sleep(20);
11    }
12}

Project Knob

In this project, we will learn how to control the servo with a potentiometer.

Component List

Freenove Projects Board for Raspberry Pi

Chapter01_04

Raspberry Pi

Chapter01_05

GPIO Ribbon Cable

Chapter01_06

Jumper Wire

Chapter05_02

Servo

Chapter13_01

Circuit

Schematic diagram

Chapter13_12

Hardware connection:

Chapter13_03

Sketch

In this project, we will control the servo to rotate to a designated position by rotating the potentiometer.

Sketch_13_2_Knob

First, enter where the project is located:

cd ~/Freenove_Kit/Pi4j/Sketches/Sketch_13_2_Knob
../../../_images/Chapter13_13.png

Enter the command to run the code.

jbang Knob.java
../../../_images/Chapter13_14.png

When the code is running, rotating the potentiometer and you can see the servo angle change.

../../../_images/Chapter13_03.png

On the terminal, you can see messages printed.

../../../_images/Chapter13_15.png

Press Ctrl+C to exit the program.

../../../_images/Chapter13_16.png

You can run the following command to open the code with Geany to view and edit it.

geany Knob.java

Click the icon to run the code.

../../../_images/Chapter13_17.png

If the code fails to run, please check Geany Configuration.

The following is program code:

  1///usr/bin/env jbang "$0" "$@" ; exit $?
  2
  3//DEPS org.slf4j:slf4j-api:2.0.12
  4//DEPS org.slf4j:slf4j-simple:2.0.12
  5//DEPS com.pi4j:pi4j-core:2.6.0
  6//DEPS com.pi4j:pi4j-plugin-raspberrypi:2.6.0
  7//DEPS com.pi4j:pi4j-plugin-gpiod:2.6.0
  8//DEPS com.pi4j:pi4j-plugin-linuxfs:2.6.0  
  9
 10import com.pi4j.Pi4J;  
 11import com.pi4j.context.Context;  
 12import com.pi4j.io.gpio.digital.DigitalOutput;  
 13import com.pi4j.util.Console;
 14import com.pi4j.io.i2c.I2C;  
 15import com.pi4j.io.i2c.I2CConfig;  
 16import com.pi4j.io.i2c.I2CProvider;  
 17import java.util.HashMap;  
 18import java.util.Map;  
 19
 20class PWMController implements Runnable {  
 21    private DigitalOutput pwm;  
 22	private int pwmFrequency;
 23	private double pwmDutyCycle;
 24    private boolean running = true;  
 25	private long period;  
 26	private long highTime;  
 27	private long lowTime;  
 28
 29    public PWMController(DigitalOutput pwm) {  
 30        this.pwm = pwm;  
 31		this.pwmFrequency = 1000;
 32		this.pwmDutyCycle = 0.5;
 33		this.period = (int) (1000000 / pwmFrequency);  
 34		this.highTime = (int) (period * pwmDutyCycle);  
 35		this.lowTime = (int) (period - highTime);  
 36    }  
 37
 38    @Override  
 39    public void run() {  
 40        while (running) {    
 41            if(highTime!=0){
 42				pwm.high();  
 43				delayUs(highTime); 
 44			}			
 45			if(lowTime!=0){
 46				pwm.low();  
 47				delayUs(lowTime);  
 48			}
 49        }  
 50    }  
 51
 52	public void setPwmFrequency(int frequency) {  
 53        if(frequency!=0){
 54			this.pwmFrequency = frequency;
 55			this.period = (int) (1000000 / pwmFrequency);  
 56			this.highTime = (int) (period * pwmDutyCycle);  
 57			this.lowTime = (int) (period - highTime); 
 58		}
 59		else{
 60			this.pwmFrequency = 0;
 61			this.period = (int) (1000);  
 62			this.highTime = (int) (0);  
 63			this.lowTime = (int) (period - highTime); 
 64		}
 65    }  
 66
 67	public void setPwmDutyCycle(double dutyCycle) {  
 68        this.pwmDutyCycle = dutyCycle;
 69		this.highTime = (int) (period * pwmDutyCycle);  
 70		this.lowTime = (int) (period - highTime); 
 71    } 
 72
 73    private void delayUs(long us) {  
 74        long startTime = System.nanoTime();  
 75        long endTime = startTime + (us * 1000);  
 76        while (System.nanoTime() < endTime) {  
 77        }  
 78    }  
 79
 80    public void requestStop() {  
 81        running = false;  
 82    }  
 83}
 84
 85class Servo {  
 86    private final int pin;  
 87    private final PWMController pwmController;  
 88    private final Context pi4j;  
 89
 90    public Servo(int pin) throws Exception {  
 91        this.pin = pin;  
 92        this.pi4j = Pi4J.newAutoContext();  
 93        DigitalOutput pwm = pi4j.dout().create(pin);  
 94        this.pwmController = new PWMController(pwm);  
 95
 96        Thread pwmThread = new Thread(pwmController, "PWM Controller for Servo on PIN " + pin);  
 97        pwmThread.start();  
 98
 99        Runtime.getRuntime().addShutdownHook(new Thread(() -> {  
100            pwmController.requestStop();  
101            try {  
102                pwmThread.join();  
103            } catch (InterruptedException e) {  
104                Thread.currentThread().interrupt();  
105            }  
106        }));  
107    }  
108
109    public void setFrequency(int frequency) {  
110        pwmController.setPwmFrequency(frequency);  
111    }  
112
113    public void setDutyCycle(double dutyCycle) {  
114        pwmController.setPwmDutyCycle(dutyCycle);  
115    }  
116
117	public void setAngle(int angle){
118		if(angle >180 || angle < 0){
119			return;
120		}
121		double dutyCycle = (((double)angle/180.0*2)+0.5)/20;
122		pwmController.setPwmDutyCycle(dutyCycle);
123	}
124
125    public void shutdown() {  
126        pwmController.requestStop();  
127        pi4j.shutdown();  
128    }  
129}
130
131class ADCDevice {  
132    private final I2C adcChip;  
133    private final int adcChipAddr;  
134
135    public ADCDevice(Context pi4j, I2CProvider provider, int adcChipAddr) throws Exception {  
136        this.adcChipAddr = adcChipAddr;  
137        I2CConfig i2cConfig = I2C.newConfigBuilder(pi4j).id("ADCDevice").bus(1).device(adcChipAddr).build();  
138        this.adcChip = provider.create(i2cConfig);  
139    }  
140
141    public boolean detectI2C() throws Exception {  
142        try {  
143            adcChip.write(0);  
144            byte[] data = new byte[1];  
145            int bytesRead = adcChip.read(data, 0, 1);  
146            return bytesRead == 1;  
147        } catch (Exception e) {  
148            return false;  
149        }  
150    }  
151
152    public int analogRead(int chn) {  
153        byte command = (byte) (0x84 | (((chn << 2 | chn >> 1) & 0x07) << 4));  
154        adcChip.write(command);  
155        byte[] data = new byte[1];  
156        int bytesRead = adcChip.read(data, 0, 1);  
157        if (bytesRead == 1) {  
158            int adcValue = data[0] & 0xFF;  
159            return adcValue;  
160        } else {  
161            return -1;  
162        }  
163    }  
164}
165
166public class Knob {  
167	private static int   SERVO_PIN = 18;
168    private static final Map<Integer, Servo> servoMap = new HashMap<>();  
169
170	public static void myPrintln(String format, Object... args) {    
171		Console console = new Console();  
172		console.println(String.format("\u001B[32m" + format + "\u001B[0m", args));   
173	}
174
175    public static void main(String[] args) throws Exception {  
176		Context pi4j = Pi4J.newAutoContext();  
177		I2CProvider i2CProvider = pi4j.provider("linuxfs-i2c"); 
178
179		servoMap.put(SERVO_PIN, new Servo(SERVO_PIN));
180
181		Servo servo = servoMap.get(SERVO_PIN);
182		servo.setFrequency(50); 
183		servo.setDutyCycle(0.075); 
184
185		myPrintln("Start sweeping ...");
186		Thread.sleep(1000);
187		try {  
188            int ADC_CHIP_ADDR = 0x48;  
189            ADCDevice adcDevice = new ADCDevice(pi4j, i2CProvider, ADC_CHIP_ADDR);  
190
191            if (adcDevice.detectI2C()) {  
192                int ADC_CHANNEL = 2;  
193                while (true) {  
194                    int adcValue = adcDevice.analogRead(ADC_CHANNEL);  
195					int angle = (int)((double)adcValue/255.0*180);
196                    if (adcValue != -1) {  
197						myPrintln("ADC Channel %d Value is:%d, angle value is %d", ADC_CHANNEL, adcValue, angle); 
198						servo.setAngle(angle);
199                    } else {  
200                        myPrintln("Failed to read data from ADC.");  
201                    }  
202                    Thread.sleep(100);  
203                }  
204            } else {  
205                myPrintln("ADS7830 device not detected at address 0x" + Integer.toHexString(ADC_CHIP_ADDR));  
206            }  
207        }
208		finally {  
209            if (servo != null) {  
210				servo.shutdown();  
211			} 
212        } 
213    }  
214}

The ADC value at the potentiometer is obtained every 100 milliseconds and is converted into an angle value, which is provided to the servo angle control function, and a prompt message is printed on the terminal interface.

 1if (adcDevice.detectI2C()) {
 2    int ADC_CHANNEL = 2;
 3    while (true) {
 4        int adcValue = adcDevice.analogRead(ADC_CHANNEL);
 5        int angle = (int)((double)adcValue/255.0*180);
 6        if (adcValue != -1) {
 7            myPrintln("ADC Channel %d Value is:%d, angle value is %d", ADC_CHANNEL, adcValue, angle);
 8            servo.setAngle(angle);
 9        } else {
10            myPrintln("Failed to read data from ADC.");
11        }
12        Thread.sleep(100);
13    }
14}