Raspberry Pi: Programming the GPIO
By egoebelbecker
So far, you’ve unboxed and assembled a UCTronics Robot Car. Then, you set it up as a Wifi client and installed JupyterHub so you could write Python code to control it via a web browser.
Just before the holidays, I dug up my old post about reading a USB game controller and refreshed it, so you can use a gamepad to control the car after I go over how to control the motors.
But first, it’s time to talk about how you’re going to talk to the GPIO pins on your Raspberry Pi.
GPIO Header
Raspberry Pi boards have two rows of pins along one edge. They’re called the GPIO (general-purpose input/output) pins. (But I’ll refer to them collectively as the GPIO to makes things easier.)
You can use GPIO pins for input or output, depending on the device they control. In the Jupyterhub post you used a pin for output to sound the buzzer in the robot’s IO board.
Now, you’ll use a pin for input to read from a sensor.
wiringPi vs. pigpio
In the second post, we used wiringPi to control the car’s buzzer.
Since then, I did some digging around and found that wiringPi isn’t the best choice for a few reasons, not least of which is that the lead developer is no longer supporting it. But, also because there are better options for driving motors and servos.
One of those better options is pigpio.
Like wiringPi, pigpio is a C/C++ library with Python support. But unlike wiringPi, the Python extensions are written and supported by the core library’s author.
Rather than connecting directly to the GPIO, pigpio uses a server process to control the board while the API talks to the daemon. This has the disadvantage of requiring extra software and configuration. But, it has several advantages that we’ll see as we work on getting the robot to move.
Beep beep, pigpio Style
Install pigpio
Let’s use pigpio to operate the buzzer.
First, you need to start the pigpio server process. It’s already installed on the Raspian system supplied with the UCTRONICS robot. If you’re using another system, follow the instructions here to install it.
Next, open a terminal.
If you followed my instructions for installing JupyterHub, you can open a terminal there.

Now, start the pigpio server with sudo pgpiod&

Pgpiod is the name of the server. You need to use sudo, so the process has root access. The ampersand tells the shell to run the process in the background.
Write to an Output Pin
Now we can write some code. Open a notebook and enter this code below.
import time
import pigpio
pi = pigpio.pi()
pi.set_mode(12, pigpio.OUTPUT)
pi.write(12, 1)
time.sleep(.5)
pi.write(12,0)
pi.write(12, 1)
time.sleep(.5)
pi.write(12,0)
When you run this code, the buzzer sounds.
This code is similar to the wiringPi version:
import time
import wiringpi
wiringpi.wiringPiSetup()
wiringpi.digitalWrite(26, 1)
time.sleep(.5)
wiringpi.digitalWrite(26, 0)
wiringpi.digitalWrite(26, 1)
time.sleep(.5)
wiringpi.digitalWrite(26, 0)
But, instead of calling wiringPiSetup()
, pigpio has you create an object:
pi = pigpio.pi()
Then you use that object to write (and read) from your GPIO. Using an object instead of functions makes for cleaner code, in my opinion. It also has other advantages, including making it possible to talk to more than one GPIO at a time. We’ll get to that in a later post.
I also added a line declaring pin 12 as an output pin. I should have included this in the wiringpi demo, too.
But there’s another difference between the two programs. The pin numbers are different, while the code does the same thing. How is that possible?
It’s because wiringPi uses different numbering.
Pigpio uses the native numbering of the GPIO. So, we can use actual pin numbers instead of worrying about mapping to a different system.
Here are the numbers. You can download a complete wiring diagram here.
Pin | Driverboard |
---|---|
21 | Motor Latch |
20 | Motor Clock |
16 | Motor Data |
5 | PWM 1 |
6 | PWM 2 |
13 | PWM 3 |
19 | PWM 4 |
26 | Trigger |
23 | Echo |
17 | Left Sensor |
27 | Middle Sensor |
22 | Right Sensor |
4 | ServoX |
26 | ServoY |
12 | Buzzer |
24 | Infrared |
18 | LED |
We wrote to the GPIO to make the buzzer sound and then wrote again to shut it off.
Read an Input Pin
Now, it’s time to read from the GPIO.
The line following sensors react to changes in light level. They read low when they are over a dark surface and high when they are over a light one.
The GPIO sends events to your application when the sensors go from one state to the other.
The pigpio documentation describes the events this way:
State | Change |
---|---|
0 | change to low |
1 | change to high |
Event-driven Programming
When we use sensors, we’re interested in state changes. You can receive these changes from pigpio as events, rather than reading the pin and waiting for a change. This simplifies your code and makes it easy to read from more than one sensor at a time.
Let’s start with one sensor.
First, you need to decide what you want to happen when the sensor is triggered. Start with a simple function that prints out the state change.
Start a new sheet and put this in a new cell.
def sensor(pin, state, _):
state_string = "down"
if state == 1:
state_string = "up"
print('{} level {}'.format(pin, state_string))
This function accepts three arguments. A pin number, a state indicator that matches the table above, and a third argument that we’re ignoring. It takes the first values and prints out a description of what’s happening.
Run this cell. Nothing will happen, but the function is now available.
Try calling it once to make sure.
Now, let’s set up an event.
Lines 1-4 are the same as the buzzer demo. You’re importing the modules for time and pigpio.
On line 6, you’re telling pigpio that you want to use pin 17 for INPUT, or reading.
Lines 8 & 9 get to the good stuff.
On #8, you told pigpio to activate the Pi’s internal pull-up resistor. This stabilizes the input voltage for that pin and will help you get more accurate readings. This topic is worth its own blog post.
Finally, the last line of code installs the sensor function as a callback. When the sensor state changes, pigpio will call the function with the pin number, the state, and a timestamp. You’re ignoring the timestamp parameter since we don’t need it for now.
Run this cell.
Now, you can test the sensor. I used a piece of white paper with a dark line of ink in the middle to give the sensor a strong contrast. You can even pick up the car and use a finger.
Now, let’s finish up by using the input of the sensor to do something. A buzzer isn’t the best user interface, but it’ll have to do until we learn how to activate the motors. They require more than just writing to a pin to work.
Pigpio allows you to install more than one callback, even for the same pin. At the same time, Jupyterhub manages global data in a way that removing callbacks doesn’t seem to work right. So, restart the kernel before you try to change your callbacks.
Click the restart button and agree to restart the kernel.
Now, write a new callback with a beep function to support it.
def beep(count):
for i in range(count):
pi.write(12, 1)
time.sleep(.07)
pi.write(12,0)
def sensor(pin, state, _):
state_string = "down"
if state == 1:
state_string = "up"
print('{} level {}'.format(pin, state_string))
if pin == 17:
beep(1)
elif pin == 22:
beep(2)
else:
beep(3)
Beep takes a count that tells it how many times to sound the buzzer. A simple for loop writes to it, with a time out that’s reduced to about as short as you can make it and still hear the pause between beeps.
Sensor still prints the pin state, but now it calls beep with a count based on which pin it is called with. That’s right: you can use the same callback for all three pins.
Run this cell.
Now, make a couple of small tweaks to where you install your callbacks.
First, add pin #12 as an OUTPUT pin. We should have done this back when we sounded the buzzer.
Next, set up the other two sensor pins for INPUT.
Then, set the pull-up resistor for the other two pins and install callbacks for them.
import time
import pigpio
pi = pigpio.pi()
pi.set_mode(17, pigpio.INPUT)
pi.set_mode(12, pigpio.OUTPUT)
pi.set_pull_up_down(17, pigpio.PUD_UP)
pi.set_pull_up_down(22, pigpio.PUD_UP)
pi.set_pull_up_down(27, pigpio.PUD_UP)
pi.callback(17, pigpio.RISING_EDGE, sensor)
pi.callback(22, pigpio.RISING_EDGE, sensor)
pi.callback(27, pigpio.RISING_EDGE, sensor)
Now, when you run this and trigger the sensors, you’ll hear the buzzer.
Try different durations for the timer if you want to hear a clearer difference between the sensor, but remember; you’ll need to restart the kernel or Jupyterhub might install the new callback and the old one, and you’ll hear a lot of overlapping beeps.
That’s it for this week. You wrote to a pin, read from three others, and then tied input and output together. In the next post, we’ll drive a motor. (I promise!)
Did you enjoy this post? Did you find it useful? Sign up for my newsletter. There's more content like this coming!