In the previous tutorial, we discussed TTK menus, layout management in Tk GUI toolkit, and mouse and keyboard events in Tkinter/TTK. We also discussed multi-threading in Python and created our own class of threading package so that we can implement our Raspberry Pi codes exactly as if they are running on a microcontroller. We also modified the threading class so that we could kill threads whenever required. Now, it’s time to start with electronics on Raspberry Pi.
The very first recipe that we are going to implement on Raspberry Pi is an LED driver. Building an LED driver is the ‘Hello World’ of embedded systems. Interestingly, our Raspberry Pi based GUI will control LED drivers. We are implementing our Raspberry Pi recipes through GUI apps, so we could demonstrate how flexible and powerful it is controlling embedded electronics from a single-board computer in comparison to a microcontroller or microcontroller board.
Raspberry Pi vs. microcontrollers
Any embedded controller can interface and communicate with external electronics in five possible ways
1) Digital Output – The controller may output a digital HIGH or LOW signal to the other device. The voltage levels of the digital signal may be CMOS or TTL logic compatible.
2) Digital Input – The controller may read a digital LOW or HIGH signal from the other device. Again the voltage levels can be CMOS or TTL logic compatible.
3) Analog Output – The controller may output an analog signal to the other device. Generally, this analog signal is not analog. Instead, it is Pulse Width Modulated (PWM) signal that approximates to analog voltage levels.
4) Analog Input – The controller may have built-in Analog-to-Digital (ADC) channels to sense analog voltage levels and convert them into a digital reading.
5) Serial Communication – The controller may be capable of communicating with other devices via different serial communication protocols like UART/USART (for peer-to-peer communication), I2C/TWI (for half-duplex master-slave serial data communication with multiple devices), or SPI (for full-duplex serial data communication with multiple devices). Many other serial communication protocols may be peer-to-peer, half-duplex, or full-duplex.
Most of the microcontrollers are equipped with all the five ways of data and signal communication and may have additional hardware features like Timers/Counters, hardware interrupts, and RTC (Real-time Clock). Raspberry Pi is capable of four of these ways of signal communication but analog input. Raspberry Pi can perform digital output, digital input, analog output, and communicate via common serial communication protocols, including USART, I2C, and SPI. However, it does not have a built-in analog-to-digital (ADC) channels. For analog to digital conversion, Raspberry Pi needs to be interfaced with external ADC that may communicate digital readings to RPi via serial communication. RPi also has a system timer and an ARM timer that can be used as a typical timer/counter. All the GPIO (General-Purpose Input/Output) pins of Raspberry Pi can be configured as an interrupt source.
Apart from these controllers-specific features, Raspberry Pi also has USB ports, Ethernet, Wi-Fi, Bluetooth, on-board GPU, HDMI port(s), analog video, and audio output, CSI port, and DSI port. It comes with a RAM up to 4GB and a MicroSD card up to 64 GB can be used for booting it. All of this, combined with the ability of Raspberry Pi to run an operating system where apps can be programmed in any high-level language, makes the Raspberry Pi a mighty and resourceful embedded controller. With these resources, Raspberry Pi (a single-board computer) can be used for very advanced embedded applications that may never run on a microcontroller.
Raspberry Pi GPIO
One of the most powerful features of Raspberry Pi is its GPIO pins arranged as 40-pin (or 26-pin in models prior to Pi 1 Model B+) expansion header on the top of the board. This GPIO row makes the RPi able to directly interface with electronics and make it capable of acting as an embedded controller. Typical desktop computers do not have such GPIO headers, so they cannot operate as embedded computers. The only way typical desktop systems can be used as embedded controllers/computers is using USB-to-GPIO modules or Bluetooth GPIO modules or Wi-Fi GPIO modules.
Their board numbers generally refer to the GPIO pins. These are pin numbers on the P1 header of the RPi that remains the same for all the Raspberry Pi models. The counting of the pins start from the edge of the board as shown in the following pinout diagram of Raspberry Pi.
Not all pins are the same. Some of these pins are reserved while some have additional use. The following figure shows the reserved pins of the GPIO header.
Out of the 40 pins, two pins are connected to the Pi’s 5V rail and are reserved to provide a consistent +5.0V DC supply; two pins are connected to the Pi’s 3.3V rail and are reserved to provide a consistent +3.3V DC supply; eight pins are connected to Pi’s ground and are reserved to provide common ground; two pins are reserved for I2C communication with HATS EEPROM; 26 pins are left for digital input/output. These 26 pins available for digital input/output are 3V3 pins, i.e., their outputs can be set to 3.3V, and inputs are 3.3V tolerable. The pins can be configured to use built-in pull-up or pull-down resistors except for GPIO2 (Board Pin Number 3) and GPIO3 (Board Pin Number 5), which have fixed pull-up resistors. The GPIO pins output 3.3 V for logical HIGH and 0V for logical LOW. They read 3.3V as logical HIGH and 0V as logical LOW.
All the 26 GPIO pins can generate software PWM signals while hardware PWM signals are available only at GPIO12 (Board Pin Number 32), GPIO13 (Board Pin Number 33), GPIO18 (Board Pin Number 12) and GPIO19 (Board Pin Number 35). For serial communication (USART), GPIO14 (Board Pin Number 8) is used as a serial transmitter (Tx), and GPIO15 (Board Pin Number 10) is used as a serial receiver (Rx).
For I2C communication with Pi HATS (Hardware Attached on Top) EEPROM, GPIO0 (Board Pin Number 27) is reserved for data and GPIO1 (Board Pin Number 28) is reserved for the clock signal. For I2C communication with external devices, GPIO2 (Board Pin Number 3) is used for data, and GPIO3 (Board Pin Number 5) is used for clock signals.
For SPI communication, there are two channels. SPI0 uses GPIO10 (Board Pin Number 19) for MOSI, GPIO9 (Board Pin Number 21) for MISO, GPIO11 (Board Pin Number 23) for SCLK, GPIO8 (Board Pin Number 24) for CE0 and GPIO7 (Board Pin Number 26) for CE1. SPI1 uses GPIO20 (Board Pin Number 38) for MOSI, GPIO19 (Board Pin Number 35) for MISO, GPIO21 (Board Pin Number 40) for SCLK, GPIO18 (Board Pin Number 12) for CE0, GPIO17 (Board Pin Number 11) for CE1 and GPIO16 (Board Pin Number 36) for CE2.
Using Raspberry Pi GPIO from Python
For controlling Raspberry Pi’s GPIO pins from Python, you need first to import RPi.GPIO module as follows:
import RPi.GPIO as GPIO
The module reference IO pins in two ways – Board numbers and BCM numbers. The board numbers are the pin numbers on the P1 header of the Raspberry Pi. These numbers remain the same for all Raspberry Pi models. The BCM numbers refer to the channel numbers on the Broadcom SoC. Different RPi models may have channel numbers wired to RPi board numbers differently. So, if you use BCM numbers, you will need to work with the schematic of the particular board. If BCM numbers are used, it may be possible that your script may work on one model but may break on the other. The numbering system of the pins can be set using the setmode() method of the GPIO module. The following are valid examples of setting a numbering system of IO pins in the Python script.
To detect the numbering system of IO pins, getmode() method of GPIO module can be called as follows:
mode = GPIO.getmode()
The method returns GPIO.BOARD, GPIO.BCM or NONE (if numbering system is not set) as a string. It is recommended to use a board numbering system so that your Python script could run on all models without breaking.
It may be possible that more than one script be run in a session. In such a case, if one script is already using an IO pin and another script is run, which also uses the same IO pin, a warning is generated, prompting the user that the channel is already in use. To ignore such warnings and continue using the IO pin from the current script, the warnings can be set False using setwarnings() method of the GPIO module as follows:
This must be done even before setting the numbering system of pins. For using pins as digital input or output, they need to be configured using the setup() method of the GPIO module. A pin can be configured as digital output as follows:
Here, the channel refers to pin number referenced using either board or BCM number. An initial value can also be set to output by the pin while configuring it as digital output as follows:
GPIO.setup(channel, GPIO.OUT, initial=GPIO.HIGH)
To set the output from a pin, output() method of GPIO module is used as follows:
Here, the state can be 0 or GPIO.LOW or False for logical LOW, and 1 or GPIO.HIGH or True for logical HIGH. It is possible to output digital signal on multiple pins at the same time by using a list of channels as an argument as follows:
gpio_list = [38,40] # also works with tuples
GPIO.output(gpio_list, GPIO.LOW) # sets all to GPIO.LOW
GPIO.output(gpio_list, (GPIO.HIGH, GPIO.LOW)) # sets first HIGH and second LOW
A pin can be configured as digital input as follows:
GPIO.setup(channel, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(channel, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
To read digital input, input() method of GPIO module is used as follows:
The state of a pin configured as digital input can be read using a conditional block-like as follows:
At the end of the script, any resources used by it must be cleaned. This includes the IO pins. If the script is terminated without cleaning, the pins remain in use by the Raspberry Pi, and if other script tries to access those pins, a warning is generated. It is even much important to cleanup if pins have been used as input without pull-up or pull-down, as such pins can get damaged by shorting. The channels can be cleaned using the cleanup() method of the GPIO module. The method also clears the numbering system in use.
Light Emitting Diodes (LEDs) are like signal diodes that can emit visible, infrared or laser light in forward-bias condition. These are used for decorative purposes or as indicators of mutually exclusive conditions like if a device is ON or OFF, an option/feature is selected or not selected, or if the device is working or not. In certain situations, LEDs are blinked to show that an action/event is in process.
LEDs are two-terminal devices that can be switched ON or OFF in a circuit. An LED is switched ON in forward-bias condition when it starts glowing. It is switched OFF in reverse bias condition when it does not glow. An LED driver is a circuit that controls the switching of an LED. The LEDs are current-controlled devices and require just 12 to 30 mA forward current for their operation. The GPIO pins of any microcontroller or processor can typically source up to 40 mA current for the output of logical HIGH. They can sink the same current for the input of logical HIGH. So, LEDs can be directly interfaced and controlled by IO pins of CMOS/TTL chips, including the Raspberry Pi. To learn more about LEDs and how they are interfaced in a circuit, check out the following Arduino tutorial:
Arduino Compatible Coding 04 – Interfacing and Driving LED by Digital Output from Arduino
Raspberry Pi based GUI-controlled LED driver
Now, when we know how to use Raspberry Pi’s GPIO pins for digital IO and how LEDs are interfaced in a circuit (from Arduino Tutorial), let us now build an LED driver on Raspberry Pi.
1) Raspberry Pi 3/4 Model B x1
2) LED x1
3) 330 Ohms Resistor x1
4) Breadboard x1
5) Male-to-Male Jumper Wires
Connect GPIO21 (Board Pin Number 40) of Raspberry Pi with the anode of the LED. Connect the cathode of the LED with a series resistor of 330 Ohms and ground the other terminal of the resistor. The DC supply voltage and ground can be given to the circuit from pin number 2 and pin number 6 of the Pi board, respectively.
We are controlling the switching operation of the LED from GUI. We start from the GUI loaded with menus, developed in the previous tutorial.
To control LEDs, we write an app that is loaded on clicking the ‘LEDs’ menu button in the ‘Displays’ menu. The app runs through the execution of a single function LED_driver(). So, we assign the LED_driver() function to the command argument of add_command() method for ‘displays_menu’ replacing the donothing() function as follows:
The following modules are imported at the beginning of the script.
from tkinter import *
from tkinter import ttk
import RPi.GPIO as GPIO
As we will be running the app as a separate thread so that the GUI does freeze in running the microcontroller-like infinite loop, we declared a global variable to reference this thread. We also need global variables to reference IO pins used in the script. As a single IO pin is used in the script, a variable ‘gpio_pin’ is declared.
thread_LED_driver = None
gpio_pin = 40
Inside the LED_driver() function, our customized threading class is defined. The infinite While loop in the body of the modified run() method of the threading class, let us run the code in an infinite loop, like if it is running on a microcontroller. The user-defined methods stop() and stopped() are written to virtually kill the thread. The threading module does not provide any method to kill threads. So, we are killing threads by this little coding trick.
def __init__(self, *args, **kwargs):
super(MyThread, self).__init__(*args, **kwargs)
self._stop = threading.Event()
Inside the LED_driver() function, we first need to write the code that loads the GUI to control the LED.
LED_def = Frame(main_ui)
label_LED_gpio = Label(LED_def, text = “Select GPIO”)
label_LED_gpio.grid(row = 1, column = 1)
combo_LED_gpio = ttk.Combobox(LED_def, values=[“GPIO 02 (03)”, “GPIO 03 (05)”, “GPIO 04 (07)”, “GPIO 05 (29)”, “GPIO 06 (31)”, “GPIO 07 (26)”, “GPIO 08 (24)”,
“GPIO 09 (21)”, “GPIO 10 (19)”, “GPIO 11 (23)”, “GPIO 12 (32)”, “GPIO 13 (33)”, “GPIO 14 (08)”, “GPIO 15 (10)”
“GPIO 16 (36)”, “GPIO 17 (11)”, “GPIO 18 (12)”, “GPIO 19 (35)”, “GPIO 20 (38)”, “GPIO 21 (40)”, “GPIO 22 (15)”
“GPIO 23 (16)”, “GPIO 24 (18)”, “GPIO 25 (22)”, “GPIO 26 (37)”, “GPIO 27 (13)”,])
combo_LED_gpio.grid(row = 2, column = 1)
label_LED_conf = Label(LED_def, text = “LED Configuration”)
label_LED_conf.grid(row = 1, column = 2)
combo_LED_config = ttk.Combobox(LED_def, values=[“Source Mode”, “Sink Mode”])
combo_LED_config.grid(row = 2, column = 2)
label_LED_op = Label(LED_def, text = “Operation”)
label_LED_op.grid(row = 1, column = 3)
combo_LED_op = ttk.Combobox(LED_def, values=[“Switch ON”, “Switch OFF”, “Blink”])
combo_LED_op.grid(row = 2, column = 3)
label_LED_duration = Label(LED_def, text = “Blink Duration (Milliseconds)”)
label_LED_duration.grid(row = 1, column = 4)
entry_LED_duration = Entry(LED_def)
entry_LED_duration[‘state’] = DISABLED
entry_LED_duration.grid(row = 2, column = 4)
button_LED_signal = Button(LED_def, text = “Generate Signal”, command = thread_LED_signal)
button_LED_signal.grid(row = 3, column = 2, padx = 5, pady = 5)
button_LED_shutdown = Button(LED_def, text = “Shutdown Signal”, command = shutdown_LED_signal)
button_LED_shutdown.grid(row = 3, column = 3, padx = 5, pady = 5)
The GUI is loaded in a frame ‘LED_def’ below the menus. It contains a combobox to select the GPIO pin where the LED may be interfaced, a combobox to select whether the pin should act as current source or current sink to light up the LED, a combobox to select the desired LED operation (switch ON, switch OFF, or Blink) and an Entry to load value of blink interval in Milliseconds. There are label widgets on the top of each combobox and the entry widget to indicate their purpose. The entry is disabled by default and its state is changed to normal when ‘Blink’ operation is selected. This is done using <<ComboboxSelected>> virtual event of the Combobox widget. This event is bound to the combobox using bind() method of Tkinter/TTK and active_blink() function is executed on the occurrence of the event. In the active_blink() function, current index of the combobox is read and if it is 2 (corresponding to blink operation), the state of entry widget is set to normal.
x = combo_LED_op.current()
if x == 2:
entry_LED_duration[‘state’] = NORMAL
The GUI includes two buttons – one to start generating signal that drive the LED and the other to stop the signal. The button, pressing which signal is generated, is bind to thread_LED_signal() function. The button, pressing which the signal is stopped, is bind to shutdown_LED_signal() function. Inside the thread_LED_signal() function, first the global variable declared to reference the thread is defined, then generate_LED_signal() function is assigned target callable of the thread and finally the thread is started by calling start() method of the threading module.
thread_LED_driver = MyThread(target = generate_LED_signal)
In the generate_LED_signal() function, first the current values of the comboboxes are read using get() method of the widgets. The value of selected GPIO pin is decoded to an integer number that corresponds to the board pin numbers of the Raspberry Pi. The value entered by user for blink interval is validated to be an integer, otherwise 500 Milliseconds is set as default blink interval. The GPIO warning is set to False and the board numbering system is selected for the Raspberry Pi. The selected GPIO pin is set as output.
Next the conditions are matched for the selected LED operations with different cases for selected LED driving method and accordingly selected GPIO pin is set to logical HIGH or logical LOW to accomplish the selected operation under selected LED driving method. For blinking LED, simply the state of the selected IO pin is toggled by a delay entered by the user. To provide delay, sleep() method of Time module is used. The method accepts delay in seconds.
gpio_pin = combo_LED_gpio.get()
LED_config = combo_LED_config.get()
LED_op = combo_LED_op.get()
if gpio_pin == “GPIO 02 (03)”:
gpio_pin = 3
elif gpio_pin == “GPIO 03 (05)”:
gpio_pin = 5
elif gpio_pin == “GPIO 04 (07)”:
gpio_pin = 7
elif gpio_pin == “GPIO 14 (08)”:
gpio_pin = 8
elif gpio_pin == “GPIO 15 (10)”:
gpio_pin = 10
elif gpio_pin == “GPIO 18 (12)”:
gpio_pin = 12
elif gpio_pin == “GPIO 17 (11)”:
gpio_pin = 11
elif gpio_pin == “GPIO 27 (13)”:
gpio_pin = 13
elif gpio_pin == “GPIO 22 (15)”:
gpio_pin = 15
elif gpio_pin == “GPIO 23 (16)”:
gpio_pin = 16
elif gpio_pin == “GPIO 24 (18)”:
gpio_pin = 18
elif gpio_pin == “GPIO 10 (19)”:
gpio_pin = 19
elif gpio_pin == “GPIO 09 (21)”:
gpio_pin = 21
elif gpio_pin == “GPIO 11 (23)”:
gpio_pin = 23
elif gpio_pin == “GPIO 25 (22)”:
gpio_pin = 22
elif gpio_pin == “GPIO 08 (24)”:
gpio_pin = 24
elif gpio_pin == “GPIO 07 (26)”:
gpio_pin = 26
elif gpio_pin == “GPIO 05 (29)”:
gpio_pin = 29
elif gpio_pin == “GPIO 06 (31)”:
gpio_pin = 31
elif gpio_pin == “GPIO 12 (32)”:
gpio_pin = 32
elif gpio_pin == “GPIO 13 (33)”:
gpio_pin = 33
elif gpio_pin == “GPIO 19 (35)”:
gpio_pin = 35
elif gpio_pin == “GPIO 16 (36)”:
gpio_pin = 36
elif gpio_pin == “GPIO 26 (37)”:
gpio_pin = 37
elif gpio_pin == “GPIO 20 (38)”:
gpio_pin = 38
elif gpio_pin == “GPIO 21 (40)”:
gpio_pin = 40
gpio_pin = 40
LED_duration = entry_LED_duration.get()
LED_duration = int(LED_duration)
LED_duration = 500
if LED_op == “Switch ON”:
if LED_config == “Source Mode”:
elif LED_config == “Sink Mode”:
elif LED_op == “Switch OFF”:
if LED_config == “Source Mode”:
elif LED_config == “Sink Mode”:
elif LED_op == “Blink”:
On pressing the shutdown signal button, the thread must be closed. So, in shutdown_LED_signal() function, the global variable declared to reference the thread is defined, then the thread is closed using stop() method of our modified threading class. Finally, the cleanup of GPIO pins is done as it must be done while terminating the signal.
All the functions and GUI body statements are contained within the LED_driver() function. This single-function loads the GUI and implements an embedded LED driver.
Working the project
The LED is prototyped on the breadboard such that it is forward biased when the current is sourced to it. The GUI app can control the LED irrespective of how it is interfaced with the Raspberry Pi. The GUI app takes a hint from the user that how the LED is interfaced with the Raspberry Pi, and the app is programmed to set the output of IO pins accordingly to switch ON, OFF, or blink the LED.
You can now realize the strength of using a single-board computer in comparison to a microcontroller. On Raspberry Pi, we have controlled and driven the LED such that it may be interfaced any way; we can dynamically drive it from our GUI app.
Such flexibility is generally not spared in a microcontroller-based application. A microcontroller would need interfacing buttons and engage digital input to implement such functionality. It would have required to interface a numeric keypad to set blink duration if a similar application might have been developed on a microcontroller. A separate display device (SSD or LCD) would have been required to indicate the selected blink duration. That involves a lot of hardware. On Raspberry Pi, we have just used a single IO pin and managed to implement all other functionalities from our GUI app.
Tkinter/TTK limitation as GUI tool
Tkinter/TTK allows creating only static GUIs. If it would have supported dynamic GUI, we could have added options to interface more LEDs on the fly and then could have controlled multiple LEDs at the same time using variable arrays.
Tkinter/TTK does not support dynamic interfaces. But, we could have preloaded GUI with options for all GPIO pins of the Raspberry Pi. Then, we could have populated these options by a GUI button by calling the grid() method for those options when user prompts to add driver for a new LED.
In the next tutorial, we will discuss digital input on Raspberry Pi and create a counter application by interfacing a button. Later on, we will expand the same application to a numeric keypad for Raspberry Pi by employing a multiplexing technique.