In the last post (Raspberry Pi Hardware Interactions Part 1 – Button) we began to explore the potential of the Raspberry Pi’s GPIO pins by implementing a simple push button as an input. In this post we will extend those concepts by wiring up a rotary encoder, setting up listeners on each of its input lines, and writing a decoder which will be able to determine the direction our rotary encoder is being turned.
Along the way we will be discussing how our rotary encoder circuit works and how this impacts the decisions we make when writing software that interacts with these components. The most common practical application of this circuit is a volume dial, an example we will finish within this post. As part of creating this volume control program we’ll also take a look at how it can be set up as a system service which is started as the Raspberry Pi starts up; in essense, providing you with a custom volume control dial for your Raspberry Pi.
What You Will Need
If you would like to participate in this demonstration, you will need the following:
- Raspberry Pi (with power supply)
- Rotary Encoder (PEC11-4215F-S24 or similar)
- 10kΩ resistors
We will be using Pin 1 to provide power to our circuit, Pin 6 as the common ground between our rotary encoder and the Raspberry Pi, and Pins 16 and 18 as the input channels from our rotary encoder. Alternatives for these can be found here. If you’re participating and are new to breadboard prototyping, this guide is very helpful.
Figure 1 – Circuit Diagram
How Rotary Encoders Work
A rotary encoder makes use of two core components: a magnetic rotor, and Hall effect sensors. A Hall effect sensor has the ability to vary its output voltage depending on the magnetic field that is present. The magnetic rotor is just a shaft with a circular housing, where a number of magnets are attached.
When the rotary encoder is turned, the magnetic rotor rotates past the Hall effect sensors which causes their respective magnetic fields to vary. Depending on the strength and direction of the magnetic field sensed, either a one or a zero is output onto each of the output lines, provided a specific threshold has been reached. The component is designed in such a way, that the signals it outputs are identical, however, they are out-of-phase by 90 degrees.
Figure 2 – Rotary Encoder Cross Section
Setting up a Rotary Listener
We can create an in-software representation of our rotary encoder by first setting our two pins as inputs and then setting up a listener thread attached to the Channel A pin, in our case Pin 16. This kind of listener setup step is explained in more detail in Part 1 of this blog series: Buttons.
import RPi.GPIO as GPIO GPIO.setmode(GPIO.BOARD) GPIO.setup(16, GPIO.IN) GPIO.setup(18, GPIO.IN) GPIO.add_event_detect(16, GPIO.RISING, callback=handle_pulse_event)
Before we write the function that will handle the input from our rotary encoder we should discuss how this input is seen by the Raspberry Pi. Both of the rotary encoder channels carry a square wave the same as the input line from a button (as discussed earlier). Remember, the signal on these two lines are identical, but are out-of-phase with each other by 90 degrees.
Figure 3 – Phase Shift
The direction of the phase shift maps to the direction the dial was turned. If we know which pulse was first, we know which direction the dial was turned. In the case of our example, we have already attached a listener to Channel A of the encoder. Each time there is a rising edge detected on this pin the direction that the dial is being rotated can be inferred by the state of Channel B. If Channel B is high it means that it must have gone high before Channel A and therefore the dial has been turn counter clockwise. If it is low then, by the same logic, the dial has been turned clockwise. It is important to note that this behaviour is only observable due to the shift in phase between the two signals. The diagram above (Figure 3) illustrates this point.
Now that we understand the signals, we can handle the pulse event due to a rising edge on Channel A. The code snippet below illustrates that if Channel B is high, then call a function that will lower the volume (counterclockwise), inversely, if Channel B is low, then call a function that will raise the volume (clockwise).
def handle_pulse_event(): if GPIO.input(18): lower_volume() else: raise_volume()
Volume Control With Alsaaudio
Python already offers a library for interaction with ALSA Mixer (Advanced Linux Sound Architecture used to configure sound) called alsaaudio. You will need this installed to access the audio mixer, via the Python package manager pip.
$ sudo pip install pyalsaaudio
We create a new object to interact with the ALSA sound controls, and to change the volume we get the current volume, add/subtract a predefined increment value to the volume, while keeping it within the bounds of 0-100.
import alsaaudio INCREMENT = 3 MIN_VOLUME = 0 MAX_VOLUME = 100 mixer = alsaaudio.Mixer(control="PCM", id=0, cardindex=-1, device="default") def raise_volume(): current_volume = mixer.getvolume() if current_volume + INCREMENT > MAX_VOLUME: mixer.setvolume(MAX_VOLUME) else: mixer.setvolume(current_volume + INCREMENT) def lower_volume(): current_volume = mixer.getvolume() if current_volume - INCREMENT < MIN_VOLUME: mixer.setvolume(MIN_VOLUME) else: mixer.setvolume(current_volume - INCREMENT)
The final program put together can be found in this Github repository. The hardware and software behaves as the diagram in Figure 4 illustrates.
Figure 4 – Volume Dial Flow Chart
Hardware Interaction Micro-Service
Once we have our volume control program, we can start it when the Raspberry Pi starts up via a systemd service. To do this:
- Place the Python script in the
/optdirectory of your RaspberryPi
- Create a systemd service unit file (seen below) in the
/etc/systemd/systemdirectory and name it volume-dial
- Enable the service with the command:
$ systemctl enable volume-dial.service
- Start the service with:
$ systemctl start volume-dial.service
[Unit] Description=Volume Dial After=sysinit.target [Service] ExecStart=/usr/bin/python /opt/volume_dial.py [Install] WantedBy=multi-user.target
Now that we know how our rotary encoder works, and how to react to the signals it provides, we can extend our program to do whatever we like when our dial is turned. Try cloning the example code from the example Github repository and changing the “handle_pulse_event” method to something new.
In the following post in this series we’ll learn about outputs, the Serial Peripheral Interface (SPI) and LEDs.