CanSat RaspberryPi 2021 Workbook
CanSat RaspberryPi 2021 Workbook
The following four labs have been designed to introduce you to some of the electronics and
programming skills that will be required to undertake the CanSat competition. Whilst these
will be delivered as part of the CPD workshops, these lab scripts have been written in a
standalone fashion to allow you to finish or repeat any of the exercises outside of the
workshop.
The teaching kit comprises of off-the-shelf hardware that is cheap and easy to buy online.
This allows teams to easily replace broken components and to also find support and ideas
from the wealth of online teaching tutorials and technical resources related to Raspberry Pi
Pico and Circuit Python. The kit we use in the labs contains the following items:
Raspberry Pi Pico
The Raspberry Pi Pico combines a small
microcontroller with several hardware
interfaces (UART, SPI, I2C) that have a large
amount of configurability. USB programming
support and a large memory make this an
easy microcontroller to work with and is ideal
for getting started with CanSat
https://uk.farnell.com/raspberry-pi/raspberry-pi-pico
https://shop.pimoroni.com/products/bmp280-breakout-temperature-pressure-altitude-sensor
TMP36 sensor
A new Raspberry Pi Pico will need CircuitPython and the required sensor python libraries
setting up before these labs can be followed.
CircuitPython is a Python environment for controlling small computer systems such as the Pi
Pico. It allows such boards to be programmed in Python by uploading a main python file
(code.py or main.py) that is executed when the Pi Pico is switched on. It contains functions
for direct low-level control of the boards pins and hardware interfaces.
2. The Pi Pico needs to be started into a different mode to install the CircuitPython
environment. To do this, hold down the BOOTSEL switch and plug the USB cable
into the Pi and your PC.
3. The Pi should mount as an USB drive labelled "RPI-RP2", at which point release the
BOOTSEL button.
4. Copy the .uf2 file onto the Pi USB drive. The USB drive will unmount itself and then
re-mount as a new USB drive labelled "CIRCUITPY"
5. If you are using Windows 7 or 8, you will also need to download and install the
"Adafruit drivers" package so that Windows can communicate with the Pi Micro.
https://github.com/adafruit/Adafruit_Windows_Drivers/releases
Install BMP280, RFM9x and Adafruit Boards libraries
CircuitPython provides some built in functionality for managing the Pi Pico, however this can
be extended through the use of third-party libraries. These are libraries produced by
manufacturers, suppliers and the CircuitPython community for the purpose of using extra
devices with the Pi Pico. These libraries reduce the complexity of using external devices by
providing high-level functions to interact with the devices they support.
Libraries are installed into CircuitPython by copying across the ".mpy" file associated with
the library to the "lib" folder found on the USB drive of the Pi Pico.
For the CanSat primary mission we need to install the BMP280 sensor library and the
RFM9x radio library. These are available from the Adafruit CircuitPython library collection
which is available from the following location
https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases:
2. Find adafruit_bmp280.mpy within the lib folder of the adafruit library collection. Copy
this file over to the Pi Pico USB drive (CIRCUITPY).
3. Find adafruit_rfm9x.mpy within the lib folder of the adafruit library collection. Copy
this file over to the Pi Pico USB drive (CIRCUITPY).
5. The contents of your lib folder on your Pi Pico should now look as follows:
There are several programs available to develop code for CircuitPython. Mu Editor
https://codewith.mu/ is simple to use and has built-in support for CircuitPython. When
starting Mu you will be asked for what mode Mu should operate in. Chose CircuitPython at
this screen:
The Mu Environment is fairly self-explanatory. When used in CircuitPython mode, pressing
the Save button or ctrl-S will reset the Pi Pico and so run the code you have just edited. The
serial window will show you the output of any print() commands and allow you to interact
with the Python REPL (as we see in Lab 1).
Clicking on the Serial Window and pressing a key will take you to the REPL.
The first lab will cover running a basic Python3 program that tests that CircuitPython and the
Pi Pico are up and running correctly. It also contains some soldering to build the boards in
your kit. Soldered connections are one of the most reliable ways of connecting parts of the
CanSat electrical design together.
The RFM96W, BMP280 and Raspberry Pi Pico boards will need header pins soldering to
them so that they can be used with the breadboard and jumper wires.
For the RFM96W and BMP280 the boards can be soldered such that the long side of the
header pins are facing the bottom of the boards. The Pi Pico on the other hand has its pin
names on the back and so it may be preferable to solder these pins on backwards if you
intend to use the Pi with a breadboard.
As the Pi pins are in parallel, it is recommend to plug them into a breadboard first to ensure
they are aligned.
https://learn.adafruit.com/adafruit-guide-excellent-soldering/common-problems
General Purpose Input/Output (GPIO)
GPIO pins on the Raspberry Pi allow external voltages to be read from the software and they
also allow external voltages to be set from software. These are digital pins, so the inputs are
interpreted at either a logical "False" or logical "True" depending on the voltage of the signal.
For our 3.3V Raspberry Pi, any voltage under 2.5V is interpreted as "False" and conversely
any voltage over 2.5V is interpreted as true (up to 3.3V). This is similar for output signals. A
"True" output will set the pin's voltage to 3.3V and the "False" output will set the pin's voltage
to 0V.
GPIO pins can be used as either input or output ports and this set by software as we shall
see in this lab. The Pi Pico has 28 GPIO ports as seen in the green boxes in the following
diagram. Many pins are multi-purpose and can also be used for other interfaces (UART, SPI,
I2C), these are represented by the multi-coloured boxes to the side of the green boxes in the
diagram. The following link contains the pinout: https://datasheets.raspberrypi.org/pico/Pico-
R3-A4-Pinout.pdf
https://datasheets.raspberrypi.org/pico/Pico-R3-A4-Pinout.pdf
As this is a GPIO pin, we can control it from the CircuitPython software. To test this will we
use the Read-Evaluate-Print-Loop (REPL) functionality of Python that allows us to write
basic code without saving it to a file. The code has to be entered one line at a time, which
can be tedious but is useful for testing.
6. We can write code directly into this interface. To test the LED we need to use the
GPIO functions. To do this we need to import two Python libraries. The first provides
the GPIO functions, type the following into REPL and press enter:
7. The second library contains the pin names for the Raspberry Pi Pico, type the
following into REPL:
8. Now we can set the LED as a GPIO pin with the following code:
This creates an object called led that we can use to interact with the LED pin
9. Then set the GPIO to output (GPIOs can be either input or output pins)
10. Now we can control the LED from Python. The following line should turn on the LED:
The CircuitPython REPL is handy for testing small amounts of code, but for the CanSat
application the code will need to be written into a file. This file is saved to the Pi Pico and will
be automatically run when your CanSat is powered up.
1. Close the REPL connection by pressing Ctrl-D in the REPL window. This is a useful
command should you enter REPL mode by accident.
2. The main editor window in Mu should have a file called code.py already open. This is
the main file that CircuitPython will run on start-up of the Pi.
4. Click Save (or repress Ctrl-S) and check the Serial window below. You should see
some text saying that your code has been saved onto the Pi and is running. Check
that the LED has turned back on. If so, then this shows you can upload and run code
on your Pi.
5. We can make the LED blink by adding a time delay between switching the LED on
and off and then making this behaviour loop. Add the following code to the end of
your file:
The sleep() function will delay the program by the number of seconds in the
argument (in this case, half a second). The LED is turned off, the program sleeps for
half a second, then the LED is turned back on and the program sleeps again. At this
point the program returns to the top of the while loop checks that True == True (it
always is!) and runs the loop again.
Note the indentation of this code. This is important in Python and shows what code
should be executed as part of this loop.
6. Run the code. You will see some errors regarding the sleep() function. The sleep
function is part of the “time” library, as seen by the “time.” before the function is
called. Therefore, add some code to import the time library in the same way as the
board and digitalio libraries were added.
8. Finally, we can simplify the above code by reading the state of the LED, inverting it
(i.e. True -> False, False -> True) before writing to it back to the LED. Replace your
while loop with the following code:
9. This while loop with the 1 second pause will be used later on in the workshop for
reading the sensors once every second and sending the data over the radio.
CircuitPython allows the Pi to output messages from the Python code that can describe
events and display variable values. This is very useful during the development of the CanSat
code. There are many ways this can be achieved in Python and we will show one method.
The messages are sent using a UART interface that is forwarded over the USB cable. The
Mu editor will display these messages in the Serial window. You could also use a terminal
emulator program (PuTTY, Tera Term for Windows) to connect to the serial port to display
the messages. You can also use the UART interface to connect to other devices such as
GPS receivers and GSM modems.
1. The print() function allows us to send messages over the UART. Add the following line
before your while loop from the previous exercise:
2. Run the code by saving the file and check that you can see the message in the serial
window within Mu.
3. Information messages like this are useful for tracking the progress of the CanSat
program and displaying error messages. However, they can also be used to print out
variable values.
Add the following code after the Hello message:
4. Run the code. Python automatically converts the value of test_value into a message
string.
5. A more useful case is to combine the text message and the value of a variable. The
format() function can be combined with print() to add variable values into placeholders
within the text.
Add and run the following code:
The code within the curly braces { } is a placeholder for a variable. The variable(s)
within the format(....) statement are then substituted in when the code is run. The 'f'
within the { } tells python that this is a floating point (i.e. number has a decimal point)
and the ":.3" in the second example denotes that we want the value displayed to
three places after the decimal point.
6. There is also a format placeholder for integer values, denoted with a ":d" value.
Add and run this code.
7. You should get an error message saying that the format code is not compatible with
the "float" type. The int() function can be used to convert a decimal number to an
integer:
Add the int() call and run the code to check that this resolves the type error.
Universal Asynchronous Receiver-Transmitter (UART)
Despite the name UART is a relatively simple communication interface. It operates in the
same fashion as the GPIO with true/false values represented as 0V and 5V but pulses are
sent across the wire instead of a steady voltage pulses. This allows a numerical value to be
converted to a series of pulses and sent over a single wire:
The Raspberry Pi Pico has two UARTs. These can be connected to many pairs of GP pins
as shown in purple in the Pi Zero pinout: TX is the transmit (i.e. data sent out of the Pi) and
RX as the receive (i.e. data sent to the Pi).
This lab covers communication with the temperature and pressure sensor required to
achieve the CanSat primary mission, using a more complex communication interface: I2C.
I2C allows multiple devices (up to 1008) to be connected to the same I2C interface with just
a pair of wires. It also allows bi-directional communication over these two wires and so is
ideal for communicating with many sensors. An example wiring with three devices would be
as follows:
The software required to communicate with I2C devices can be complex, however most
devices will have a software library provided that will give you functions that make the device
easy to use. For example in this lab we use the provided BMP280 library to hide away the
low-level I2C code.
Before we can read data from the sensor we need to connect it to the Pi. I2C requires us to
connect two data cables and the BMP280 sensor also requires VCC (power) and GND
(ground) connection, thus four cables in total.
1. Connect the 2-6V input pin on the BMP280 to pin 3V3 on the Pi (3.3 volts output) and
connect the GND pin on the BMP280 to a GND pin on the Pi.
2. The I2C SCL and SDA pins need connecting to the Pi's I2C pins. The Pi Pico has two
physical I2C interfaces that can be configured to use several pairs of pins to fit a PCB
design or needs for other interfaces.
For now, we will use I2C1 on pins GP14 and GP15. These are shown in blue in the
diagram below:
And so connect the SDA and SCL BMP280 pins to pins GP14 and GP15 on the Pi.
3. Before we read from the sensor, we need set up the I2C interface by telling python
which Raspberry Pi pins we would like to use for the interface. Add the following line
under the import statements:
4. We now need to create an object that represents the BMP280 sensor using the
Adafruit library. We need to tell the library which I2C interface we wish to use and the
I2C address of the sensor, for the sensor in the kits this is 0x76:
This gives us an object, bmp280_sensor that we can use to access the I2C BMP280
sensor without having to know any of the low-level I2C transactions.
5. We can now add a function to read the temperature from the sensor. The code
will read the temperature from the sensor. Add the following code to the end of your
bmp280.py file to create a function that will read the sensor temperature (note the
spacing before the return statement!):
6. The pressure can be read from the sensor with the following code:
Write a function read_pressure() that can read the pressure from the sensor (it will
look very similar to the read_temperature() function).
7. You have now written your BMP280 library. Return to the code.py file.
8. We will add some code that reads the temperature and pressure. After the line that
toggles the LED within the while True loop, add the following lines to read the
temperature and then print it out:
9. Add some code so that the sensor also measures and prints out the pressure.
10. Save the code and correct any errors. If there is an error concerning the I2C then
check your wiring. Your CanSat should print out the temperature and pressure
readings every 1s.
11. It is also possible to print out a message that can combine the values with text and
set the precision of the decimal point. This uses the format string style of text output:
Add the format string, run the program and observe the difference in output style.
Exercise 2.3 (optional): Interfacing an Analogue Sensor via the ADC
I2C is a digital interface, only two voltage levels are supported (0V and 3.3V) which limits its
use as a sensor input when interfacing directly to any analogue electronic sensors you may
have or developed or procured. The Raspberry Pi Pico contains three Analogue-to-Digital
Convertors (ADC) that can translate an analogue voltage into a number that the Python
program can use. These are located on GP26, GP27 and GP28 as below:
In their default configuration, the Pi ADCs will sample the voltage on the ADC input pin, this
must be within the range of 0V to 3.3V. Once sampled, it converts the voltage to a number
between 0 and 65,535 with a value of 0 representing 0V and a value of 65,535 representing
3.3V.
The kits include a TMP36 analogue temperature sensor. This sensor outputs a voltage
dependant on the ambient temperature the device measures. The output voltage to
temperature relationship for a 3V input is as follows:
Therefore, by connecting the output of the TMP36 to the ADC of the Pi and performing some
transformation of the value read, we can measure the temperature using the TMP36.
1. Connect the TMP36 to the Pi as follows:
a. Pin 1: 3V3
b. Pin 2: ADC0 (GP26)
c. Pin 3: GND
(please note that if Pin 1 and Pin 3 are reversed the TMP36 can get very hot
quite quickly, so double check before powering up the Pi)
The pins on the TMP36 have the following layout (viewed from the bottom of the
device):
2. We can now write the python code to access the TMP36. Create and save a new file
called tmp36.py
4. Now setup the ADC associated with pin GP26 with the following line:
5. To read the sensor via the ADC will we create a function (in the same way as the
BMP280 was used). First, create the read_temperature() function:
6. We then need to read the ADC and convert the value to a voltage by scaling the read
value in the range of 0V to 3.3V:
7. The voltage reading can then be converted to a temperature using the following
information from the data sheet:
As we are using the TMP36 our offset (0.5V) needs to be deducted from the voltage
read by the ADC and the whole result scaled by 100 (as we are working in V whilst
the datasheet is working in mV):
9. We can now call this function from the while loop within the main code.py file. First,
import your TMP36 file as with the other library imports.
11. Run the code and compare the sensor readings between the TMP36 and the
BMP280.
Lab 3: SPI Interface and Radio Communication
This lab builds on the sensing and message sending capabilities we have developed in the
previous labs by adding wireless capabilities to the CanSat, using the SPI interface. This will
fulfil the electrical requirements for the primary mission.
This lab will require two CanSats to operate, one to send data and one to receive data.
Exercise 4.3 builds the CanSat (data transmission) software and Exercise 4.4 builds the
Ground Station (data receive) software. We will have a beacon set up at the front of the
room that will receive all packets. Alternatively, you can pair with someone else; one taking
the CanSat role and the other the Ground Station role.
SCLK: Serial Clock. A stream of 0-1s that the data is aligned to. The SPI clock rate is
related to the speed of this stream, you can slow this down if having data integrity issues.
MISO: Master Input / Slave Output. The data from the peripheral device to the Pi.
MOSI: Master Output / Slave Input. The data from the Pi to the peripheral device.
SS0/CE0: Slave Select / Chip Enable. Enables a peripheral device and means that the
device can output to the MISO pin. One SS/CE pin is needed for each peripheral device.
To use SPI you don't need to be too concerned about the function of these pins as the
device's software library will take care of most of the low-level SPI code for you. However it
is good to be aware of their function when cascading multiple SPI devices together, for
example to connect two devices you will need two SS/CE pins:
The RFM9x LORA module is a long range (upto 2km line-of-sight), low throughput, radio
module and connects to the RaspberryPi via an SPI connection. The SPI signals are present
on the RFM96x as SCK (SCLK), MISO and MOSI.
https://learn.adafruit.com/adafruit-rfm69hcw-and-rfm96-rfm95-rfm98-lora-packet-padio-breakouts/pinouts
The Raspberry Pi Pico has two SPI interfaces and, as with the UART and I2C interfaces, it
can be setup to use a variety of pins for the interface. For this lab we will use the GP2 to
GP7 pins for the radio.
1. Connect the power signals on the RFM9x. You will need two cables to connect the
VIN and GND pins on the RFM9x to the 3V3 and GND pins on the Pi. This module
can cope with both 3.3V and 5V signals, but as the Raspberry Pi's logic pins are 3.3V
we use that voltage for the RFM9x. Pin 36 provides VCC (or it can be chained from
the BMP280’s Vin pin) and there are several GND pins to use.
2. Connect the three SPI signals (SCK, MISO, MOSI) from the RFM9x module to the
Raspberry Pi. GP2 will be used for SCK, GP3 for MOSI (Master-Out, Slave-In, SPI0-
TX on the Pi) and GP4 for MISO (Master-In, Slave-Out, SPI0-RX on the Pi).
3. The RFM9x needs the SPI chip select pin. Connect the CS pin to a GPIO pin so that
we can set this to zero to reset the RFM9x, pin GP6 is used in this example.
4. The RFM9x needs to be reset on start up. Connect the RST pin to a GPIO pin so that
we can set this to zero to reset the RFM9x, pin GP7 is used in this example.
At this point you should have 7 wires (2 power, 3 SPI, CS, reset) connected (and the
BMP280 wires if you have left those connected).
Now that the hardware is connected, we configure the software side of the radio module.
2. First we need to add the required libraries. As with the I2C sensor, we need to add the
board and busio libraries to access the SPI interface. We also need the digitalio
library for the CS and reset pins and finally we also need the RFM9x radio library
provided by Adafruit:
3. Now we setup the SPI interface so that we can communicate to the RFM9x. We map
the SPI signals to the pin numbers based on how they were connected earlier.
4. We need to also set up the CS and reset pins as GPIO digital pins:
5. We can now start up the radio. To do this we can call the RFM9x library functions, this
will give us an object that we can then use to represent the radio:
The rfm9x object that this function returns is what we shall use for accessing the
radio for other parts of the lab.
6. Finally add a message to say that the radio has started up successfully:
7. Go back to code.py and import the radio module you have just created:
8. Run the code and ensure that the “RFM9x Radio Ready” message is printed out. If so
then your wiring and radio module have been set up successfully, if not check your
wiring and the pin assignments in the code.
1. Now that the radio is set up, we can send a test message. Open radio.py and create
the following send() function that calls the RFM9x library function:
2. Add the following code within the while loop in code.py, just before the time.sleep()
call:
3. Run the code. Check that you still get the “Radio Ready” message. If you do not then
something has gone wrong in setting up the radio, so first double check your wire
connections and then your software from the previous exercise. If all has gone well
then the confirmation message should show up in the serial monitor every second to
show your message has been sent.
4. Now check that your radio message has either been received by the shared ground
station or by one of your neighbours who is running their groundstation code.
5. We shall now extend the transmission message to also contain the temperature and
pressure readings. To do this we shall need to include the BMP280 libraries again.
Make sure that your BMP280 library is being imported:
6. Now that the BMP280 is set up we can read the BMP280 sensors in the same fashion
as in the previous lab. Write the code required to read the sensors within the while
True: loop of the last exercise and store them in two variables: room_temp and
room_pressure.
7. We now need to send these values to the radio. Add the following line after the
BMP280 readings (still within the while loop) to send the sensor readings via string
conversions. Replace CANSAT NAME with a name that you can use to identify your
CanSat:
8. Increase the time delay in the loop to 5 seconds to help reduce radio traffic on the
shared groundstation.
9. Run the code and check that the received temperature and pressure values are as
expected.
1. As with the transmit, the radio setup of Exercise 4.2 is enough to allow us to start
receiving data. Each message sent over the radio is dubbed a packet and the receiver
can receive one packet at a time.
Open radio.py and create the following function:
This function will check if the RFM9x has received a packet and if it hasn't it will wait
for 1 second (set by timeout) to see if a packet arrives. It will return the packet data if
one has been received, otherwise it returns the value None.
2. Whilst in radio.py we will add a second function that gets the "Received Signal
Strength Indication (RSSI)" variable which is measured in decibels. The signal
strength is very low and so a typical transmission should have a RSSI of between -40
and -90 dB. Any values lower than this and you should consider upgrading your
transmit or receive antenna (N.B. it's much easier to upgrade the groundstation
antenna to a YAGI rather than the CanSat's!).
Add the following function to radio.py that fetches the RSSI value from the RFM9x:
3. The received packet now needs to be read from within code.py. First remove any
code relating to the bmp280 as the groundstation will not need to use this sensor.
4. Add a variable to count the number of packets received and set it to 0. This needs to
be added before the while True: loop.
5. Within the loop we can add the code to receive the data. First try to read the data:
6. We can then check the result of trying to read the radio and print out the message if a
message was received:
The first print() is for the value of the packet counter and the message. The message
needs converting into a string using the str() function with the ascii parameter.
The second print prints out the RSSI value.
The packet counter is then updated.
7. The sleep() statement can be removed from the end of the while loop as the
try_read() function provides the delay.
8. Run the code and check that you receive packets from either the beacon at the front
of the room or from your neighbour undertaking the transmission exercise.