RaspberryPi:ProgrammingInPython

From UCT EE Wiki
Jump to navigation Jump to search

Overview[edit]

Most of the content in the guide comes from the documentation, available online here.

Python, while not as powerful as C or C++, is quickly becoming a common choice for embedded systems developers due to its ease of use and rapid development times. See this IEEE article. For information on programming in C or C++, go to RaspberryPi:ProgrammingInC.

This chapter serves as a short guide for programming in Python on the RPi. You will also find some templates for techniques such as debouncing, or making use of the Raspberry Pi’s multicore architecture by implementing threading.

The RPI.GPIO Library[edit]

The RPi.GPIO library is the common Python library used on the Raspberry Pi. Documentation for the library can be found here:
https://sourceforge.net/p/raspberry-gpio-python/wiki/Home/

It is included in the environment variables by default, so to use it, you can simply just import it into your Python script:

import RPi.GPIO as GPIO

You need to specify which board mode you’re using. For more information on board modes, see Section RaspberryPi:Overview#GPIO_Pins. Board modes are specified as follows:

GPIO.setmode(GPIO.BOARD)
# or
GPIO.setmode(GPIO.BCM)

You also need to perform “cleanup” on GPIOs when your application exists. By performing cleanup and resetting all pins to their default modes, you can prevent possible damage to your Raspberry Pi. The cleanup function is called as follows:

GPIO.cleanup()

If your program is meant to run indefinitely, and only close upon an exception, you can wrap it in a try catch, as follows:

if __name__ == "__main__":
    # Make sure the GPIO is stopped correctly
    try:
        while True:
            main()
    except KeyboardInterrupt:
        print("Exiting gracefully")
        GPIO.cleanup()
    except e:
        print("Some other error occurred: {}".format(e.message)})
        GPIO.cleanup()

Basic IO[edit]

Read the documentation, available here.

Digital Logic[edit]

Digital output on the Raspberry Pi is accomplished by writing values to channels. A channel is a pin, which is numbered in the way you’ve configured. Basic configuration of a pin for output is as follows:

GPIO.output(<channel>, <Logic>)

Your logic (high, 3.3V, or low, 0V), can be specified as follows:

  • For 3.3V (high) output:

    GPIO.output(<channel>, GPIO.HIGH)
    GPIO.output(<channel>, 1)
    GPIO.output(<channel>, True)
    
  • For 0V (low) output:

    GPIO.output(<channel>, GPIO.LOW)
    GPIO.output(<channel>, 0)
    GPIO.output(<channel>, False)
    

Channels can also be specified in lists, for example:

LEDs = (11,12)
GPIO.output(LEDs, GPIO.HIGH) # Will turn all channels HIGH
GPIO.output(LEDs, (GPIO.HIGH, GPIO.LOW)) # Will the first channel HIGH, and the second LOW

Note that you can also read the state of the pin/channel that is set as an output by using the input() function. For example, if you wanted to toggle pin 12, you could do something as follows:

GPIO.output(12, not GPIO.input(12))

Analog[edit]

The Raspberry Pi does not have any analog input pins. You will need to use something like the MCP3008 to read analog voltages.

Inputs[edit]

Digital Read[edit]

To read the value of a digital pin, you can use the input() function:

if GPIO.input(12):
    print("Pin 12 HIGH")
else:
    print("Pin 12 LOW")

Pull Up/Pull Down Resistors[edit]

Using pull up and pull down resistors is essential when working with digital logic. Thankfully, the Raspberry Pi has internal pull up and pull down resistors. To make use of these, initialize the pin/channel as follows:

GPIO.setup(<channel>, GPIO.IN, pull_up_down=GPIO.PUD_UP)
# or
GPIO.setup(<channel>, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

Interrupts[edit]

Very often it is useful to set up an interrupt, for example to trigger an event when a button is pressed. Callback functions are executed on a different thread. If you have multiple callbacks, know that they will be executed sequentially as only one thread exists for interrupts. Interrupts can be implemented as follows:

# The bounce time is given in milliseconds.
# If your pin is set to use pull down resistors
# Connect a button between <channel> and 3.3V
GPIO.add_event_detect(<channel>, GPIO.RISING, callback=callback_method(), bouncetime=200)
# If your pin is set to use pull up resistors
# Connect a button between <channel> and GND
GPIO.add_event_detect(<channel>, GPIO.FALLING, callback=callback_method(), bouncetime=200)

Other functions[edit]

The RPi.GPIO Library offers other functions for interrupts the first is the wait_for_edge() function, which is designed to block execution until an edge is detected. You can detect edges of GPIO.RISING, GPIO.FALLING, or GPIO.BOTH. The timeout is given in milliseconds:

# wait for up to 5 seconds for a rising edge
edge = GPIO.wait_for_edge(<channel>, GPIO_RISING, timeout=5000)
if edge is None:
    print('Timeout occurred')
else:
    print('Edge detected on pin <CHANNEL>')

Another function is the event_detected() function. From the docs: “The event_detected() function is designed to be used in a loop with other things, but unlike polling it is not going to miss the change in state of an input while the CPU is busy working on other things. This could be useful when using something like Pygame or PyQt where there is a main loop listening and responding to GUI events in a timely basis.”

GPIO.add_event_detect(channel, GPIO.RISING)  # add rising edge detection on a channel
if GPIO.event_detected(channel):
    print('Button pressed')

Communication Protocols[edit]

The RPi.GPIO library has no native support for communication protocols. Very often, specific Python libraries are provided on a per-device use case. However, this can become tedious and cause your code to become bloated. See the sections below for relevant libraries for using I2C and SPI directly.

I2C[edit]

Ensure you enable I2C in raspi-config.

To use I2C in Python, you can use the smbus library. To do this, we need to install and configure smbus. This is usually done by default, but instructions are included for posterity.

$ sudo apt-get install i2c-tools
$ sudo apt-get install python-smbus
$ sudo adduser <username> i2c
$ sudo reboot

Once you connect a device, you can run $gpio i2cdetect to determine if a device is detected on the I2C bus. In the example below, there is an electronic compass (GY-271) connected to the Pi.

An i2cdetect example

To initialize an I2C device, do the following:

import smbus
i2cdevice = smbus.SMBus(1) # 1 indicates /dev/i2c-1
address = 0x1e #whatever the device is for your i2c device

To read a byte from the I2C device:

result = i2cdevice.read_byte_data(address, <register>)

To write a byte to the I2C device:

i2cdevice.write_byte_data(address, <register>, <value>)

Methods Available[edit]

smbus has multiple methods. The documentation for these is quite minimal, but brief descriptions are available here.

Note: If you're working with EEPROM (specifically 24C32), you may need to read the answer to the questions given here.

SPI[edit]

Ensure you enable SPI in raspi-config.

You can use SPI in Python by using the spidev library. Documentation is available here. Basic usage is as follows:

import spidev

#Bus is 0 or 1, depending on which SPI bus you've connected to
bus = 0
#Device is the chip select pin. Set to 0 or 1, depending on the connections
device = 1

spi = spidev.SpiDev() #Enable SPI
spi.open(bus, device) #Open connection to a specific bus and device (CS pin)

# Set settings (SPI speed and mode)
spi.max_speed_hz = 500000
spi.mode = 0

to_send = [0x01, 0x02, 0x03] #define what to send
spi.xfer(to_send)

# Close the SPI connection
close()

Settings[edit]

The following settings are configurable in the spidev library, and can be set as follows:

spi = spidev.SpiDev() #Enable SPI
spi.<setting> = <value>

List of settings:

  • bits_per_word
  • cs_high
  • loop

Set the “SPI_LOOP” flag to enable loopback mode

  • no_cs

Set the “SPI_NO_CS” flag to disable use of the chip select (although the driver may still own the CS pin)

  • lsbfirst
  • max_speed_hz
  • mode

SPI mode as two bit pattern of clock polarity and phase [CPOL/CPHA], min: 0b00 = 0, max: 0b11 = 3

  • threewire

SI/SO signals shared

Methods[edit]

The following methods are available in the spidev library

  • open(bus, device)
    Connects to the specified SPI device, opening /dev/spidev<bus>.<device>
  • readbytes(n)
    Read n bytes from SPI device.
  • writebytes(list of values)
    Writes a list of values to SPI device.
  • writebytes2(list of values)
    Similar to ‘writebytes‘ but accepts arbitrary large lists. If list size exceeds buffer size (which is read from /sys/module/spidev/parameters/bufsiz), data will be split into smaller chunks and sent in multiple operations. Also, writebytes2 understands buffer protocol so it can accept numpy byte arrays for example without need to convert them with tolist() first. This offers much better performance where you need to transfer frames to SPI-connected displays for instance.
  • xfer(list of values[, speed_hz, delay_usec, bits_per_word])
    Performs an SPI transaction. Chip-select should be released and reactivated between blocks. Delay specifies the delay in usec between blocks.
  • xfer2(list of values[, speed_hz, delay_usec, bits_per_word])
    Performs an SPI transaction. Chip-select should be held active between blocks.
  • xfer3(list of values[, speed_hz, delay_usec, bits_per_word])
    Similar to xfer2 but accepts arbitrary large lists. If list size exceeds buffer size (which is read from /sys/module/spidev/parameters/bufsiz), data will be split into smaller chunks and sent in multiple operations.
  • close()
    Disconnects from the SPI device.

Using Threads[edit]

This guide serves as the basis for the text below. This manual does not teach everything there is to know about threading in Python, but will give you the basics to be able to utilize threads for simple tasks that may be of use to you in the practicals. It’s strongly recommended you read through that text if you have not yet been exposed to threading concepts.

This text does not cover issues and precautions when working with threads, such as mutex’s and locks, data races, producer/consumer concerns and the likes. However, these are knowledge areas of critical importance, and it is strongly suggested that the reader tries to make an effort to understand these concerns before writing threaded code.

The Threading Library[edit]

The Python library threading library offers all the functionality one would expect. Import it in to your code in the standard way:

import threading

There are 4 basic things you need to do when creating a thread:

  1. Initialize it
  2. Start it
  3. Let is execute
  4. “Shut it down” by joining

Basic Thread Usage[edit]

For example, if you want to create a separate thread to fetch a sensor value five times, you could do it as follows:

 1 import threading
 2 import time
 3 import RPi.GPIO as GPIO
 4 
 5 def setup():
 6     #Contains all code for initialisation of RPi.GPIO
 7 
 8 def fetch_sensor_vals(sensor_pin):
 9     for i in range(5):
10         GPIO.input(sensor_pin)
11         time.sleep(2)
12     
13 if __name__ == "__main__":
14     setup()
15     # Create a thread to call the function and pass "12" in as sensor pin
16     x = threading.Thread(target=fetch_sensor_vals, args=(12,)) 
17     print("Starting thread")
18     x.start()
19     print("Waiting for the thread to finish")
20     x.join()
21     print("Reading finished")

Timed Thread Usage[edit]

Often in embedded systems we want a specific task to run every X time units. There is an option in the Python threading library. It works as follows:

 1 import threading
 2 import datetime
 3 
 4 def print_time_thread():
 5     """
 6     This function prints the time to the screen every five seconds
 7     """
 8     thread = threading.Timer(5.0, print_time_thread)
 9     thread.daemon = True  # Daemon threads exit when the program does
10     thread.start()
11     print(datetime.datetime.now())
12     
13 
14 if __name__ == "__main__":
15     print_time_thread() # call it once to start the thread
16     
17     # Tell our program to run indefinitely
18     while True:
19         pass

You will see that in this code example, we have also set the daemon flag of the thread to be be True. A daemon thread will shut down immediately when the program exits. In essence, the daemon thread will run indefinitely in the background until your application exits, and you do not need to worry about calling join().<ref>It’s important to note that calling t.join() will wait for a thread to finish executing, even if it is a daemon. </ref>