CanSat RaspberryPi 2024-5 Workbook
CanSat RaspberryPi 2024-5 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.
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://www.adafruit.com/product/3072
BMP280 Barometric Pressure/Temperature/Altitude Sensor
https://shop.pimoroni.com/products/bmp280-breakout-temperature-pressure-altitude-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 board’s 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
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:
Mu Editor with CircuitPython
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:
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 recommended to plug them into a breadboard first to
ensure they are aligned.
https://learn.adafruit.com/adafruit-guide-excellent-soldering/common-problems
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
5. Click on this window and press enter. You should be prompted with the Python REPL
interface “>>>”:
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. The LED
may not be on, this is because your program has finished running and Pi has
switched the LED back off. For the rest of these labs we shall use infinite loops to
stop your code from finishing.
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.
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.
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. 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 see the difference in output style.
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:
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 comment out the
radio.send() code as these are not required for the ground station operation.
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.
Lab 4: Internal Storage
The Pi Pico has a moderate amount of internal storage (2MB). Some of this is taken up by
your code and Circuit Python, the rest can be used to store data on your Pi. This can be
used as a useful back-up of your primary and possibly secondary mission data should your
radio communication link fail.
The Pi protects this storage and we will need to override this protection to allow your code to
write data to files. However, you will still need the ability to update your code which requires
this protection to be restored. Therefore, we shall use a jumper cable to provide a
“programming mode” which controls the protection. In your final CanSat design you could
replace this with a small switch (such as a DIP switch) or a jumper header.
Enabling file writing has to be done before the code.py file is run by the Pi. There is a special
file that you can create called boot.py that is run first before handing control over to code.py.
3. The internal storage is managed by a dedicated library, add the import for this library:
4. Now the GPIO pin can set up as an input. GP28 will be used. A pull-up shall be
enabled so that when the jumper is disconnected GP28 will be read as “True”:
5. The final step is to then read the value of GP28 and set the read-only status of the
internal storage depending on the presence of the jumper cable:
6. Save boot.py
7. Check operation of the write protection by disconnecting USB cable from the Pi and
fitting a jumper cable between GP28 and GND. This will disable the write protection
(GP28 = False). Now plug the Pi back into the USB and try to save code.py. Mu
should give you the following error:
This indicates that control of the internal storage has been given over to the Pi (rather
than the USB) and Mu cannot write to the Pi.
8. Unplug the Pi and disconnect the GP28 jumper. Plug the Pi back into the USB and
try to save code.py again. This time it should succeed and shows that your
“programming mode” jumper cable is working well.
If you make a mistake updating boot.py then it is possible to lock yourself out of the internal
storage and you won’t be able to update boot.py to rectify the mistake! If this happens you
can start the Pi in “Safe Mode” where boot.py and code.py are not run and therefore the
drive is unlocked.
1. Disconnect the Pi from the USB and add a jumper between the RUN pin and GND:
2. Plug the Pi into the USB. The Pi is held in reset and will not do anything when
plugged in.
3. Quickly unplug the jumper from one pin and then plug it back in. You have about 1s
to perform this action.
4. Unplug the jumper again and leave it unplugged. The Pi should restart into safe
mode. You can confirm via the serial monitor in Mu:
5. Update boot.py to correct the mistake. Make sure to save boot.py and then
disconnect/reconnect the Pi from the USB to power-cycle the Pi and exit safe mode.
Now that the internal storage has been setup, code.py can be modified to store the primary
mission data to a file.
1. Import the storage library into code.py as per the previous exercise
2. code.py needs to check if file writing is enabled by the jumper. This can be done by a
call to the storage library that gets the status of the file storage. Assign this to a
variable so that this only needs to be done once at the start of cope.py:
Note the use of f strings as an alternative to the .format() method of printing the
values of variables. Both approaches are valid, a f-string is more compact.
3. There are a few options for writing the data to the file. The safest option is to open
the file, write the data and then immediately close the file. This offers the most
protection against data corruption should the CanSat suddenly be powered off.
Add the following lines after radio.send() to write your primary mission data to a file:
The “a” option to open() will append the data written to the end of a file, retaining any
existing information. “w” would cause the file to be overwritten. You may want to add
an open/write/close sequence at the start of code.py to clear the file before the
results data is added in.
4. Save code.py and unplug the USB from the Pi. Connect the programming mode
jumper to enable writing to the internal storage and the reconnect the USB to the Pi.
5. This should result in a file called “results.txt” being created on the Pi with your
primary mission results within:
Windows can be unreliable with reading from this file whilst the Pi is writing to it. If
you see missing data then you may need to unplug the USB from the Pi, disconnect
the programming jumper and then reconnect the USB. The results.txt file should then
contain all of the expected data.
If you see the following error then it means the write protection has not been read
correctly, check boot.py and the code for write_en.
Lab 5 Analogue Sensing
GPIO 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 pin ADC_VREF is the reference voltage that the ADC compares the input voltage to,
likewise AGND is the ground pin of the ADC circuitry inside the PI. For cleaner analogue
readings it is important that these pins are used as part of the measurement.
2. We shall first setup the ADC. Import the required libraries to access the ADC pins:
3. Now setup the ADC associated with pin GP26 with the following line:
6. Import your mf52.py library by adding the following to the top of the file:
7. Add the function call to mf52.read_temperature() within your while loop to call the
function once a second.
8. Run your code, note the values that are printed out with nothing connected to the
ADC. Is this what you would expect?
9. Modify read_temperature() to also print the ADC voltage. This can be done by
multiplying the adc_raw value by the ADC_VREF value (3.3 V) and dividing by the
maximum raw reading of the ADC (65,536).
10. Check that your displayed voltages are always between 0V and 3.3V
11. Read the ADC with 10k resistor between ADC0 and 3.3V
Using the breadboard, plug the 10k resistor between the ADC0 and 3.3V pins of the
Pi. Rerun your code and check the voltages measured.
12. Read the ADC with 10k resistor between ADC0 and ADC_VREF
Move the 10k resistor so that it is now between ADC0 and ADC_VEF. Note the
voltages measured and the difference with 3.3V. Hold your finger on the resistor to
heat it up slightly and see if there is a change in voltage.
13. Read the ADC with the thermistor also connected between ADC0 and AGND
Connect the thermistor between ADC0 and AGND. You have made the following
potential divider circuit:
𝑅2
𝑉𝑜𝑢𝑡 = . 𝑉𝑖𝑛
𝑅1 + 𝑅2
ADC_VREF (3.3V)
10k
ADC0
10k @ 25°C
When the resistance in the top half is
AGND (0V)
equal to the resistance in the bottom
half, the divider at point ADC0 will be half the input voltage (3.3 V). Check that this is
represented in your ADC voltage readings.
100
80
Temperature (degress C)
60
y = -23.78ln(x) + 84.816
40
20
-20
-40
-60
0 50 100 150 200
Resistance (kOhms)
First, we need to convert the ADC value to resistance. This can be done by
rearranging the potential divider equation for R2:
Now use this value with the regression formula above to approximate the ADC
temperature. You will need to diving the therm_r value by 1000 as the regression
formula is in kOhms.
will give you the ln() function (remember to import the math library).
Print out the temperature calculated and return the temperature value to end the
function:
The TMP36 is a different kind of 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.
The pins on the TMP36 have the following layout (viewed from the bottom of the
device):
TMP36 Datasheet Rev H.
2. Set up ADC1 in the same way as you set up ADC0. Make a new function called
read_temperature_tmp36()
3. In this function read ADC1 and convert the value to a voltage by scaling the read
value in the range of 0V to 3.3V:
4. 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):
6. We can now call this function from the while loop within the main code.py file.
7. Run the code and compare the sensor readings between the TMP36 and the
thermistor.
Appendix A: Complete CanSat Solution
code.py
bmp280.py
radio.py
boot.py
mf52.py
tmp36.py