4. Chapter Analog & PWM

In previous chapters, we learned that a Push Button Switch has two states: Pressed (ON) and Released (OFF), and an LED has a Light ON and OFF state. Is there a middle or intermediated state? We will next learn how to create an intermediate output state to achieve a partially bright (dim) LED.

First, let us learn how to control the brightness of an LED.

4.1. Project Breathing LED

We describe this project as a Breathing Light. This means that an LED that is OFF will then turn ON gradually and then gradually turn OFF like “breathing”. Okay, so how do we control the brightness of an LED to create a Breathing Light? We will use PWM to achieve this goal.

4.1.1. Component List

  1. Raspberry Pi (with 40 GPIO) x1

  2. GPIO Extension Board & Ribbon Cable x1

  3. Breadboard x1

Jumper Wires x2

jumper-wire

LED x1

LED

Resistor 220Ω x 1

res-220R-hori

4.1.2. Component Knowledge

4.1.2.1. Analog & Digital

An Analog Signal is a continuous signal in both time and value. On the contrary, a Digital Signal or discrete-time signal is a time series consisting of a sequence of quantities . Most signals in life are analog signals. A familiar example of an Analog Signal would be how the temperature throughout the day is continuously changing and could not suddenly change instantaneously from 0℃ to 10℃. However, Digital Signals can instantaneously change in value. This change is expressed in numbers as 1 and 0 (the basis of binary code). Their differences can more easily be seen when compared when graphed as below.

../../../_images/analog-digital.png

Note that the Analog signals are curved waves and the Digital signals are “Square Waves”.

In practical applications, we often use binary as the digital signal, that is a series of 0’s and 1’s. Since a binary signal only has two values (0 or 1) it has great stability and reliability. Lastly, both analog and digital signals can be converted into the other.

4.1.2.2. PWM

PWM, Pulse-Width Modulation, is a very effective method for using digital signals to control analog circuits. Digital processors cannot directly output analog signals. PWM technology makes it very convenient to achieve this conversion (translation of digital to analog signals).

PWM technology uses digital pins to send certain frequencies of square waves, that is, the output of high levels and low levels, which alternately last for a while. The total time for each set of high levels and low levels is generally fixed, which is called the period (Note: the reciprocal of the period is frequency). The time of high level outputs are generally called “pulse width”, and the duty cycle is the percentage of the ratio of pulse duration, or pulse width (PW) to the total period (T) of the waveform. The longer the output of high levels last, the longer the duty cycle and the higher the corresponding voltage in the analog signal will be. The following figures show how the analog signal voltages vary between 0V-5V (high level is 5V) corresponding to the pulse width 0%-100%:

../../../_images/duty-cycle.png

The longer the PWM duty cycle is, the higher the output power will be. Now that we understand this relationship, we can use PWM to control the brightness of an LED or the speed of DC motor and so on.

It is evident, from the above, that PWM is not actually analog but the effective value of voltage is equivalent to the corresponding analog value. Therefore, by using PWM, we can control the output power of to an LED and control other devices and modules to achieve multiple effects and actions.

In RPi, GPIO18 pin has the ability to output to hardware via PWM with a 10-bit accuracy. This means that 100% of the pulse width can be divided into 210=1024 equal parts.

The wiringPi library of C provides both a hardware PWM and a software PWM method.

The hardware PWM only needs to be configured, does not require CPU resources and is more precise in time control. The software PWM requires the CPU to work continuously by using code to output high level and low level. This part of the code is carried out by multi-threading, and the accuracy is relatively not high enough.

In order to keep the results running consistently, we will use PWM.

4.1.3. Circuit

Schematic diagram

java_pwm_Sc

Hardware connection. If you need any support,please feel free to contact us via:

support@freenove.com

java_pwm_Fr

4.1.4. Sketch

In this chapter, we will learn how to make the LED to present a breathing effect.

4.1.4.1. Sketch_04_BreathingLED

First, enter where the project is located:

$ cd ~/Freenove_Kit/Pi4j/Sketches/Sketch_04_BreathingLED
../../../_images/java_breathing.png

You can enter the command to run the code.

$ jbang BreathingLED.java
../../../_images/java_breathing_run.png

When the code is running, you can see the LED lights up from dim to bright and then from bright to dim, and the process repeats.

../../../_images/java_pwm_Fr.png

Press Ctrl-C to exit the code.

../../../_images/java_breathing_code.png

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

$ geany BreathingLED.java

Click the icon to run the code.

../../../_images/java_breathing_runcode.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 java.util.HashMap;  
 13import java.util.Map;  
 14
 15class PWMController implements Runnable {  
 16    private DigitalOutput pwm;  
 17    private int pwmFrequency;
 18    private double pwmDutyCycle;
 19    private boolean running = true;  
 20    private long period;  
 21    private long highTime;  
 22    private long lowTime;  
 23
 24    public PWMController(DigitalOutput pwm) {  
 25        this.pwm = pwm;  
 26        this.pwmFrequency = 1000;
 27        this.pwmDutyCycle = 0.5;
 28        this.period = (int) (1000000 / pwmFrequency);  
 29        this.highTime = (int) (period * pwmDutyCycle);  
 30        this.lowTime = (int) (period - highTime);  
 31    }  
 32
 33    @Override  
 34    public void run() {  
 35        while (running) {    
 36            if(highTime!=0){
 37                pwm.high();  
 38                delayUs(highTime); 
 39            }           
 40            if(lowTime!=0){
 41                pwm.low();  
 42                delayUs(lowTime);  
 43            }
 44        }  
 45    }  
 46
 47    public void setPwmFrequency(int frequency) {  
 48        if(frequency!=0){
 49            this.pwmFrequency = frequency;
 50            this.period = (int) (1000000 / pwmFrequency);  
 51            this.highTime = (int) (period * pwmDutyCycle);  
 52            this.lowTime = (int) (period - highTime); 
 53        }
 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
 80public class BreathingLED {  
 81    private static int   LED_PIN = 17;
 82    private static final Context pi4j = Pi4J.newAutoContext();  
 83    private static final Map<Integer, PWMController> pwmControllers = new HashMap<>();  
 84
 85    public static void setPwmConfig(int pin) throws Exception {  
 86        DigitalOutput led = pi4j.dout().create(pin);  
 87        PWMController pwmController = new PWMController(led);  
 88        Thread pwmThread = new Thread(pwmController, "PWM LED Controller " + pin);  
 89        pwmControllers.put(pin, pwmController);  
 90        pwmThread.start();  
 91        Runtime.getRuntime().addShutdownHook(new Thread(() -> {  
 92            pwmController.requestStop();  
 93            try {  
 94                pwmThread.join();  
 95            } catch (InterruptedException e) {  
 96                Thread.currentThread().interrupt();  
 97            }  
 98        }));  
 99    } 
100
101    public static void main(String[] args) throws Exception {  
102        setPwmConfig(LED_PIN);  
103        PWMController BreathingLed = pwmControllers.get(LED_PIN);  
104        try {  
105                while (true) {  
106                    double dutyCycle = 0.01;  
107                    while (dutyCycle < 1) {  
108                        BreathingLed.setPwmDutyCycle(dutyCycle);  
109                        dutyCycle += 0.01;  
110                        Thread.sleep(10);
111                    }  
112                    while (dutyCycle > 0) {  
113                        BreathingLed.setPwmDutyCycle(dutyCycle);  
114                        dutyCycle -= 0.01;  
115                        Thread.sleep(10);  
116                    }  
117                }  
118            }  
119        finally {  
120            for (PWMController controller : pwmControllers.values()) {  
121                controller.requestStop();  
122            }  
123            pi4j.shutdown();  
124        }  
125    }  
126}

Use JBange to run the script and automatically process the declared dependencies.

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

Import Pi4J library, context management, digital output interface, HashMap class, and Map interface.

1import com.pi4j.Pi4J;  
2import com.pi4j.context.Context;  
3import com.pi4j.io.gpio.digital.DigitalOutput;  
4import java.util.HashMap;  
5import java.util.Map;  

Pi4j only has 4 hardware PWM pins, and the use of these pins is greatly limited, so we use software PWM to control the LED. Although software PWM is not as precise as hardware PWM, it can be applied to any GPIO.

To implement software PWM, we have written a PWMController class to control the GPIO output of PWM. Since PWMController implements the Runnable interface, it can be used to create a Thread object, passing an instance of PWMController as the target to the Thread constructor, and then starting this thread. In this way, the run method of PWMController will be executed in a new thread, allowing it to perform PWM control tasks concurrently.

1class PWMController implements Runnable {
2    ......
3}

Define variables to configure PWM’s parameters.

1private DigitalOutput pwm;  
2private int pwmFrequency;
3private double pwmDutyCycle;
4private boolean running = true;  
5private long period;  
6private long highTime;  
7private long lowTime;  

Constructor, to initialize various parameters of the PWM controller.

1public PWMController(DigitalOutput pwm) {  
2    this.pwm = pwm;  
3    this.pwmFrequency = 1000;
4    this.pwmDutyCycle = 0.5;
5    this.period = (int) (1000000 / pwmFrequency);  
6    this.highTime = (int) (period * pwmDutyCycle);  
7    this.lowTime = (int) (period - highTime);  
8}  

This is a simple delay method in microsecond.

1private void delayUs(long us) {  
2    long startTime = System.nanoTime();  
3    long endTime = startTime + (us * 1000);  
4    while (System.nanoTime() < endTime) {  
5    }  
6}  

The functions to set PWM frenqunency.

 1public void setPwmFrequency(int frequency) {  
 2    if(frequency!=0){
 3        this.pwmFrequency = frequency;
 4        this.period = (int) (1000000 / pwmFrequency);  
 5        this.highTime = (int) (period * pwmDutyCycle);  
 6        this.lowTime = (int) (period - highTime); 
 7    }
 8    else{
 9        this.pwmFrequency = 0;
10        this.period = (int) (1000);  
11        this.highTime = (int) (0);  
12        this.lowTime = (int) (period - highTime); 
13    }
14}  

The functions to set PWM duty cycle.

1public void setPwmDutyCycle(double dutyCycle) {  
2    this.pwmDutyCycle = dutyCycle;
3    this.highTime = (int) (period * pwmDutyCycle);  
4    this.lowTime = (int) (period - highTime); 
5} 

@Override means that the run method overrides the method in the parent class or implements the interface.

This method allows the pin to output PWM signals.

 1@Override  
 2public void run() {  
 3    while (running) {    
 4        if(highTime!=0){
 5            pwm.high();  
 6            delayUs(highTime); 
 7        }           
 8        if(lowTime!=0){
 9            pwm.low();  
10            delayUs(lowTime);  
11        }
12    }  
13}  

Request to stop the PWM controller. The PWMController class is used in the thread. When running is false, exit the thread.

1public void requestStop() {  
2    running = false;  
3}  

Define the pins that control PWM and create an integer Map variable pwmControllers to store the mapping of PWMController objects.

1private static int   LED_PIN = 17;
2private static final Context pi4j = Pi4J.newAutoContext();  
3private static final Map<Integer, PWMController> pwmControllers = new HashMap<>();  

The PWM configuration function only needs to fill in the GPIO number in the parameter. The code will automatically apply for a PWMController object and create a corresponding thread to run it. At the same time, the PWMController is stored in pwmControllers and then the thread is started.

1public static void setPwmConfig(int pin) throws Exception {
2    DigitalOutput led = pi4j.dout().create(pin);
3    PWMController pwmController = new PWMController(led);
4    Thread pwmThread = new Thread(pwmController, "PWM LED Controller " + pin);
5    pwmControllers.put(pin, pwmController);
6    pwmThread.start();
7    ......
8}

Add JVM shutdown hook to ensure PWM controller is stopped on JVM shutdown.

1    Runtime.getRuntime().addShutdownHook(new Thread(() -> {  
2        pwmController.requestStop();  
3        try {  
4            pwmThread.join();  
5        } catch (InterruptedException e) {  
6            Thread.currentThread().interrupt();  
7        }  
8    }));  
9} 

The main program, which is used to test the effect of making the LED breathe.

1public static void main(String[] args) throws Exception {
2    ......
3}

Configure PWM and associate it with pin.

1setPwmConfig(LED_PIN);

Obtain PWM controller corresponding to the pin.

1PWMController BreathingLed = pwmControllers.get(LED_PIN);

The duty cycle value is changed every 10 milliseconds, so that the LED cycles from dark to bright and then from bright to dark.

 1try {  
 2        while (true) {  
 3            double dutyCycle = 0.01;  
 4            while (dutyCycle < 1) {  
 5                BreathingLed.setPwmDutyCycle(dutyCycle);  
 6                dutyCycle += 0.01;  
 7                Thread.sleep(10);
 8            }  
 9            while (dutyCycle > 0) {  
10                BreathingLed.setPwmDutyCycle(dutyCycle);  
11                dutyCycle -= 0.01;  
12                Thread.sleep(10);  
13            }  
14        }  
15    }  

When exiting the main loop, first close all PWM controller threads and then close the pi4j context.

1finally {  
2    for (PWMController controller : pwmControllers.values()) {  
3        controller.requestStop();  
4    }  
5    pi4j.shutdown();  
6}