Do you allow this site to use Cookies?

I have decided to try out a few i2c sensors with the Raspberry Pi Pico and see what the difference is to how I use them with a Raspberry Pi 4.

I usually write Raspberry Pi Programs in Python so I was interested to find out what the differences programming a Raspberry Pi Pico RS2040 Micro controller with MicroPython. 

So instead of trying some of the beginners guides for Leds and motion sensors I jumped straight into I2C sensor boards. The first issue was the python libraries for MicroPython, there was none available for the devices I was trying. Well the ones I came across where for other microcontroller boards and I was not making any progress beyound confirming I had a connection to the sensor board. 

I then looked into Circuit Python by adafruits. The i2c sensors I have are by pololu, sparkfun or Pimoroni and as Adafruits use the same sensors in their addons they had libraries for the same sensors,  which are also compatible. So switching from the recommended language of  Micropython to Adafruits Circuit Python seemed the way to go to save a bunch of time not rummaging around in datasheets. The Pico can only have one or the other setup so you need to commit to which setup you need. As Circuit Python is based on Micropython I will still adapt to Micropython in the future if I need to, hopefully.

i2c devices on a raspberry pi pico

 

As Circuit Python is designed for Micro Controllers with low memory the libraries are more basic than the ones use for Python on desktop computers. So common libraries like numpy, matplotlib, scipy and Pillow are not available though there is a ulab which is like a cut down version of numpy.

To install Circuit Python you download the adafruits .uf2 firmware file from circuitpython.org/downloads . Connect the Pico to a computer with the bootsel button pressed to put it in USB mode. Drop the .utf file into the Pico's drive. Then disconnect the Pico. When you reconnect it CircuitPython will be ready.  Then using Thonny or Mu python editors, you can access the Pico and run your code. When you are done, disconnect the Pico from the computers USB, then when it next has power your code will run.

The main concept is your program is called code.py, this will run automatically when the micro controller has power. Any additional libraries are manually copied to the root or lib folder on the PIco. As soon as you save the file it will run, not always useful when you are developing a program having to stop it before saving it again. Your program can also be called code.txt, main.txt and main.py but code.py is the standard used name. Micropython uses main.py as standard. To run a script with a different name you will need to add a reference to code.py or main.py to import and run the myscript,py file. If you are using Thonny then you can run the myscript.py from the tool bar but you have to stop code.py from running first. This is only useful for testing purposes. If there is a short break with no programs running then code.py will start running again which can be a bit of pain while testing new scripts.

Adafruits supply the libraries and example files for many devices and sensors so you should be able to find the code to get you started.

Connecting I2C devices to the Raspberry Pi PIco

Pico GPIO I2C SPI

The Raspberry Pi Pico has 2 x I2C peripherals, these can be accessed across 6 sets of GPIO pins per peripheral. This means you can easily connect 12 devices without needing any daisy chaining unlike the Raspberry Pi main boards that only have 1 set available as standard.

 For i2c to work you will need the library folders of adafruit_bus_device and adafruit_register in the lib or root folder on the Pico.

The libraries can be downloaded to your computer from circuitpython.org/libraries and then transferred to the Pico.

For your i2c program you will need to import the libraries board and busio. Your code will use the line i2c = busio.I2C(board.SDA, board.SCL) to create a I2C object but this doesn't work on a PIco.

For the Pico you will need to replace the SDA and SCL with the Pin numbers. These are different for each micro controller so you can use some programs from the Adafruit website to view the GPIO pin names, the i2c SDA and SCL pins.

  • board.GP0
  • board.GP1
  • board.GP2
  • board.GP3
  • board.GP4
  • board.GP5
  • board.GP6
  • board.GP7
  • board.GP8
  • board.GP9
  • board.GP10
  • board.GP11
  • board.GP12
  • board.GP13
  • board.GP14
  • board.GP15
  • board.GP16
  • board.GP17
  • board.GP18
  • board.GP19
  • board.GP20
  • board.GP21
  • board.GP22
  • board.GP25 board.LED
  • board.A0 board.GP26 board.GP26_A0
  • board.A1 board.GP27 board.GP27_A1
  • board.A2 board.GP28 board.GP28_A2

or 

  • SCL pin: board.GP1 SDA pin: board.GP0
  • SCL pin: board.GP3 SDA pin: board.GP2
  • SCL pin: board.GP5 SDA pin: board.GP4
  • SCL pin: board.GP7 SDA pin: board.GP6
  • SCL pin: board.GP9 SDA pin: board.GP8
  • SCL pin: board.GP11 SDA pin: board.GP10
  • SCL pin: board.GP13 SDA pin: board.GP12
  • SCL pin: board.GP15 SDA pin: board.GP14
  • SCL pin: board.GP21 SDA pin: board.GP20
  • SCL pin: board.GP27_A1 SDA pin: board.GP26_A0

The code for these list are at learn.adafruit.com/circuitpython-essentials/circuitpython-pins-and-modules and learn.adafruit.com/circuitpython-essentials/circuitpython-i2c

So depending on what pins you use the code would be for example i2c = busio.I2C(board.GP1, board.GP0)
if you are using the first two GPIO pins on the left of the Pico board.

The next line will be assigning the i2c object to the device library. Adafruits will have their sensors address coded into the library. Often the sensor will be the same on different manufactures boards but you may need to change this.

In this example adafruits library uses address 0x75 but I needed 0x76 so you can make the change at this point.


i2c = busio.I2C(board.GP1, board.GP0)
sensor = adafruit_bme680.Adafruit_BME680_I2C(i2c,address=0x76)

That's the basics to for an i2c connection for the Raspberry Pi Pico using Circuit Python.

I2C Sensor tryout on the Raspberry Pi Pico

I will go through how to setup a few i2c device with Circuit python on the Pico. These will be for 

  • VL53L0X - Time of Flight distance sensor
  • TSL2561 - Ambient Light Sensor
  • BME680 - Air Quality, Pressure, Humidity and Temperature sensor
  • AS7282 - 6 Channel Spectral Sensor

These programs will all use a SPI connected 240 x 240 lcd display to show the results.

For all of these sensors you will need the board and busio libraries. The time library is useful for sleep and you will also need the libraries to run the SPI LCD display. terminalio, displayio, adafruit_st7789, adafruit_display_text. As the standard font is small and blocky these scripts will also use a free font Sans-SerifPro-Bold so the adafruit_bitmap_font library will also be needed. These are avilable in the download at the base of this article.

Libraries Required:

  • board
  • busio
  • terminalio
  • displayio
  • adafruit_st7789 - to use the display in the examples
  • adafruit_display_text
  • adafruit_bitmap_font
  • adafruit_vl53l0x - to use the vl53l0x ToF sensor
  • adafruit_tsl2561 - to use the Ambient light sensor
  • adafruit_bme680 - to use the bm3680 Temperature and Air Quality sensor
  • adafruit_as726x - to use the 6 Channel Spectral Sensor
  • also the Sans-SerifPro-Bold fonts in size 20 & 40 

From the Adafruit Libraries that you downloaded to your computer,  copy the above libraries to the lib folder on the CIRCUITPY drive of the Pico.

Pico Lib Folder

Time of Flight Distance Measuring Laser VL53L0X

VL03L0X ToF Raspberry Pi Pico

The first sensor will a be the VL03L0X ToF distance measuring laser. This is simple to use and has one  adjustable setting for accuracy. 

The sensor needs 4 connections. A 3v supply, Ground, SDA and SCL i2c connections.  For this program I have used the last two pins on the left side from the i2c 1 connections. GP14 (SDA) and GP15 (SCL). The 3v supply comes from the 5th pin on the right (3v3 Out)

For this script start by importing all the required libraries.

 


import time
import board
import busio
import adafruit_vl53l0x #ToF sensor
from adafruit_bitmap_font import bitmap_font

import terminalio
import displayio 
from adafruit_display_text import label
from adafruit_st7789 import ST7789 #display

next is a variable to allow an offset if the sensor is mounted in a case to minus the depth of the case from the measured distance.

distoffset = 0 #case offset in mm

This section sets up the SPi device for connection to the display and the 240 x 240 pixel display.

#display setup
displayio.release_displays()
spi = busio.SPI(board.GP18, board.GP19)
tft_cs = board.GP17
tft_dc = board.GP16

display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs)
display = ST7789(display_bus, width=240, height=240, rowstart=80, rotation=180)

 Now the i2c setup is made with the SDA wire as GP15 and the SCL at GP14


i2c = busio.I2C(board.GP15, board.GP14)
vl53 = adafruit_vl53l0x.VL53L0X(i2c, address=0x29)

The VL53L0X has an i2c address of 29 which has also been set in case it doesn't match what is in the library for the VL53L0X board.

Next a few functions will be setup. The first one is to set the measuring accuracy of the sensor. This takes 1 argument of the time allowed to measure which is in milliseconds. 33 is the default accuracy. More accurate results will be acheived with a setting of 200 or higher depending on how quick you need the results.


def vl53accuracy(self):
    """Change Distance Accuracy Setting"""
    #give time in ms ie 33 for default
    vl53.measurement_timing_budget = self * 1000 

 The next function gets the latest measurement from the sensor. This particular sensor can measure up to 2 meters in ideal conditions but in general use it is about 1.2 meters. If there is no object in range the results will be over 8000. This function will return "Out of Range" when this happens otherwise give a string of the measurement minus any offset defined at the top of the script.


def vl53dist():
    a = vl53.range
    if a > 5000:
        return "Out of Range"
    else:
        return vl53.range - distoffset

 The next function does the work by getting the latest results and then drawing the results on the screen just using text.


def dispvl53(acc, cali):
    global distoffset
    if type(acc) == int:
        if acc > 0:
            vl53accuracy(acc)
    else:
        vl53accuracy(33)
    if type(cali) == int:
        distoffset = cali
    font20 = bitmap_font.load_font("/SerifPro-Bold-20.pcf")
    font40 = bitmap_font.load_font("/SerifPro-Bold-40.pcf")
    text_group = displayio.Group(max_size=10, scale=1, x=5, y=10)
    text=str(vl53dist()) #get reading
    dist = label.Label(font40, text=text, color=0xFFFF00, x=0, y=120)
    text_group.append(dist)  # Subgroup for text scaling
    title = label.Label(font40, text="Distance\nmm:", color=0xFFAA00, x=5, y=15)
    text_group.append(title)
    display.show(text_group)

 The first few lines check that the entries for acc & cali are numbers. 

Then the 2 sizes of fonts are loaded ready for use.

text_group is used to zone an area of the display ready to have text added.

Label then takes some text and positions it in the group zone

The display group works like a list so you can append new elements as long as it doesn't exceed the max_size of elements. In this case it is 10.

Once all the elements are in place then use display.show to show the image.

The next section is used when the script is run rather than called from another script.


def main():
    while True:
        dispvl53(33,0) #accurracy, offset
        time.sleep(0.3) #screen update

if __name__ == "__main__":
    main()    

 If you save this file as code.py and save it to the Pico's root folder it will automatically run when the Pico has power.

TSL2561 - Ambient Light Sensor

TSL2561 AmbientLight RaspberryPi Pico

The TSL2561 Ambient Light Sensor is used to detect light levels for both Visible and Infrared light. The Lux level can be calculated from the results. 

You will need the adafruit_tsl2561 library in the lib folder on the Pico to use this sensor. This sensor board by SparkFun uses i2c address 0x39.

This program has two stages. A function to get the results from the sensor and one to create the display. The display this time is made up from both text and graphics.
First the libraries need to be imported and the display initialised.


import time
import board
import busio
import adafruit_tsl2561
import adafruit_imageload
from adafruit_bitmap_font import bitmap_font
#display
import terminalio
import displayio
from adafruit_display_text import label
from adafruit_st7789 import ST7789
#display setup
displayio.release_displays()
spi = busio.SPI(board.GP18, board.GP19)
tft_cs = board.GP17
tft_dc = board.GP16
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs)
display = ST7789(display_bus, width=240, height=240, rowstart=80, rotation=180)

Next the i2c device is initialised with the sensor connected to SDA on GP2 and SCL on GP3


# Create the I2C bus
i2c = busio.I2C(board.GP3, board.GP2)
# Create the TSL2561 instance, passing in the I2C bus
tsl = adafruit_tsl2561.TSL2561(i2c, address=0x39)

Next a function to collect the sensor readings and the libraries Lux calculated results.


def lightreadings(g,i): #gain, intergration
    tsl.enabled = True #enable sensor
    time.sleep(1)
    # Set gain 0=1x, 1=16x
    if 0 <= g <= 1: #check if g is 0 or 1
        tsl.gain = g
    else:
        tsl.gain = 0 #default
    # Set integration time (0=13.7ms, 1=101ms, 2=402ms)
    if 0 <= i <= 2: #check if i is between 0 and 2
        tsl.integration_time = i
    else:
        tsl.integration_time = 0 #default
    #readings
    visable = tsl.broadband
    infrared = tsl.infrared
    # Get computed lux value (tsl.lux can return None or a float)
    lux = tsl.lux
    if lux == None:
        lux = 0
    else:
        lux = "{:.0f}".format(lux)
    # Disble the light sensor (to save power)
    tsl.enabled = False
    return visable,infrared,lux

This function has a some settings that can be changed. 

Gain can be set for 0 in normal light or 1 which is 16x gain for low light conditions.

Integration is the the exposure time. This has 3 standard settings. The default is 13.7 ms with option 0 or 1 for 101ms or 2 for 402ms,

The TS2561 sensor also has a manual exposure setting but this is not a feature of this script.

The readings for the Visible Light, Infrared light are taken and the lux is calculated. The results are returned to the calling command.


def luxdisplay():
    results = lightreadings(0,0) #get readings with a gain of 0 and integration of 0
    #Display design
    title = ["Visable Light","Infrared","Lux Level"]
    lux_group = displayio.Group(max_size=8, scale=1, x=0, y=0)

    #Text results
    font20 = bitmap_font.load_font("/SerifPro-Bold-20.pcf")
    font40 = bitmap_font.load_font("/SerifPro-Bold-40.pcf")
    luxtext = ""
    ypos = 50

    #Text layout for results
    for i in range(len(results)): #get each of the 3 results from the 'results' list
        luxtext = str(results[i]) 
        text_area = label.Label(font40, text=luxtext, color=0x00FF00)
        text_area.x = 80 #results shown 80 pixels to the right
        text_area.y = ypos + (i*80) #each result shown 80 pixels apart
        lux_group.append(text_area)  #build text to display layer 

    #Text layout for Titles
    ypos = 10
    for i in range(len(title)):
        luxtext = str(title[i])
        text_area = label.Label(font20, text=luxtext, color=0x00FFFF)
        text_area.x = 80
        text_area.y = ypos + (i*80)
        lux_group.append(text_area)  #build Titles to display layer

    #Icons layout from bitmap image
    bitmap, palette = adafruit_imageload.load("/luxicons3.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette)
    icon1 = displayio.TileGrid(bitmap, pixel_shader=palette, height=5, width=1, tile_width=40, tile_height=40, x=20, y=40)

    #display a 5 elment image. Set the 3 icons the rest uses position zero, a blank tile of the luxicons3.bmp
    icon1[0] = 1
    icon1[2] = 2
    icon1[4] = 3
    lux_group.append(icon1) #build Icons to display layer
    display.show(lux_group)

 This function gets the readings from the lightreadings() function and then builds the display from the 3 results of visible, infrared and lux. Then the second set of text containing the Titles are placed 80 pixels apart in the next three layers. The images for the icons is taken from a single bmp image file. This is layed out in so each icon is in a 40 x 40 pixel square with the first block being blank. A tileGrid is created which is 1 block wide wide and 5 heigh. Then blocks from the bmp image are assigned to the TileGrid blocks to craete an image fro display. 

By default all the TileGrid block are assigned the first block (0) from the bmp image. So are blank. Then block 1, the sun, is assigned to block 1 of the TileGrid with the line icon1[0] = 1

then the IR block and candle are assigned to TileGrid position 2 & 4 to complete the icons on the display.

luxicons3 example

 source bmp icon file

 BME680 Temperature, Air Quality, Pressure & Humidity  Sensor

BME680 Air Quality Sensor RaspberryPi Pico

 The BME680 sensor is simple to use giving 5 readings. These are for Temperature, Humidity %, Air Pressure and Air quality by detecting volatile organic compounds (VOC). The Air pressure results can be used to calculate the Altitude. 

The Air Quality sensing need to be heated slightly to work and takes 20 - 30 minutes before the reading can be used. In turn this will also effect the Temperature reading as the devices is heated. So you will need to set two offsets, one for Temperature and one to set the sea level air pressure reading at your location. 

The readings can then be recorded and adjusted before the display is updated.

You will need to copy the adafruit_bme680 libray to the Pico's lib folder. To start the program, set up the Libraries and display settings.


import board
import adafruit_bme680
import busio
from adafruit_bitmap_font import bitmap_font

import terminalio
import displayio
from adafruit_display_text import label
from adafruit_st7789 import ST7789
from time import sleep

# Release any resources currently in use for the displays
displayio.release_displays()

spi = busio.SPI(board.GP18, board.GP19)
tft_cs = board.GP17
tft_dc = board.GP16

display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs)
display = ST7789(display_bus, width=240, height=240, rowstart=80, rotation=180)

 In this example the i2c connections will be SDA = GP14 and SCL = GP15


# Create library object using our Bus I2C port
i2c = busio.I2C(board.GP15, board.GP14)
#Pimoroni Sensor at 0x76
sensor = adafruit_bme680.Adafruit_BME680_I2C(i2c, address=0x76)

 The sensor I am using is from Pimoroni and has an address of 76. If you have the adafruit version then the address entry is not required. 

The next section is for the sea level pressure reading at your location. The Temperature offset will need to be calibrated with another temperature device. 


# change this to match the location's pressure (hPa) at sea level
sensor.sea_level_pressure = 1013.25

# You will usually have to add an offset to account for the temperature of
# the sensor. This is usually around 5 degrees but varies by use. Use a
# separate temperature sensor to calibrate this one.
temperature_offset = -5

The next stage is a function that collects each of the readings from the bme680 sensor.


def bme680read():
        t = round(sensor.temperature + temperature_offset,1)
        g = sensor.gas
        h = round(sensor.relative_humidity,1)
        p = round(sensor.pressure,1)
        a = round(sensor.altitude,1)
        return t, g, h, p, a

 


def readings():
    bmetype = ['Tem:', 'Gas:', 'Hum:', 'Pre:', 'Alt:']
    results = bme680read()
    font40 = bitmap_font.load_font("/SerifPro-Bold-40.pcf")
    # setup display group for text results
    text_group = displayio.Group(max_size=6, scale=1, x=2, y=2)
    text = ""
    ypos = 8
    #Using the bmetype list for headings each result is displayed
    #50 pixels apart using orange text.
    for i in range(len(results)): 
        text = bmetype[i] +" " + str(results[i])+"\n"
        text_area = label.Label(font40, text=text, color=0xFF9900)
        text_area.x = 5
        text_area.y = ypos + (i*50)
        text_group.append(text_area)
    display.show(text_group)

Then finally the loop to run it all.


while True:
    readings()
    sleep(1)

AS7262 6-channel Spectral Sensor

AS7262 6 channel Spectral Sensor RaspberryPi Pico

 The AS7262 6 Channel Spectral Sensor give 6 readings for the intensity of the light wavelengths for the specific wave lengths it monitors. The maximum reading for each channel is 16000.

So these results can simply shown as text or a more useful bar graph.

This program example displays the text reading for each colour with the text in the colour it represents. Optionally it can display a simple bar graph with each bar in the colour it represents.

For this program you will need the adafruit_as726x library in the Pico's lib folder.

As usual the first stage is to import the libraries and setup the display.


import board
import terminalio
import displayio
import busio
from adafruit_as726x import AS726x_I2C
from adafruit_display_text import label
from adafruit_st7789 import ST7789
from adafruit_display_shapes.rect import Rect
from adafruit_bitmap_font import bitmap_font
from time import sleep

# Release any resources currently in use for the displays
displayio.release_displays()
#Setup diplay
spi = busio.SPI(board.GP18, board.GP19)
tft_cs = board.GP17
tft_dc = board.GP16
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs)
display = ST7789(display_bus, width=240, height=240, rowstart=80,  rotation=180)

Then setup the i2c connection using GP2 for SDA and GP3 for SCL.


# for I2C use:
i2c = busio.I2C(board.GP3, board.GP2)
sensor = AS726x_I2C(i2c)

The colours for the text and bar graph can be read from a list in hex format, with the corresponding names for the text display.


palette = [0xFF0000,0XFF9900,0xFFFF00,0x00FF00,0x0000FF,0xFF00FF]
colourname = ["Red","Org","Yel","Gre","Blu","Vio"]

Next are some functions. These are to turn the inbuilt led on and off. Spectrum() gets the current readings from the sensor and returns them as a tuple.


def ledon():
    sensor.driver_led = 1
def ledoff():
    sensor.driver_led = 0
    
def spectrum():
    sensor.conversion_mode = sensor.MODE_2
    # Wait for data to be ready
    while not sensor.data_ready:
            sleep(0.1)
    #get readings
    vi = sensor.violet
    bl = sensor.blue
    gr = sensor.green
    ye = sensor.yellow
    og = sensor.orange
    rd = sensor.red
    return rd, og, ye, gr, bl, vi

For the Text output the readings function is used. For this the readings are collected from the spectrum function. Then each result in the tuple is added to the display group with it's corresponding colour from the palette list. These are positioned 40 pixels vertically apart multiplied by the tuple position to give even spacing.


def readings():
    #Display Results as text
    sp = spectrum()
    font40 = bitmap_font.load_font("/SerifPro-Bold-40.pcf")
    text_group = displayio.Group(max_size=8, scale=1, x=2, y=2)
    text = ""
    ypos = 10
    for i in range(6):
        text = colourname[i] +" " + str(sp[i])+"\n"
        text_area = label.Label(font40, text=text, color=palette[i], x=5)
        text_area.y = ypos + (i*40)
        text_group.append(text_area)
    display.show(text_group)

The bar graph display is made from two functions. specgraph() scales the sensor readings to be represented as a percentage of 200. 200 will be set for the colour with the highest reading. All others colours will be a percentage of the highest reading.

The bar graph function uses the shapes library to draw rectangles on the display. As the display coordinates start with 0 at the top the screen the bar graph is upside down. So at the Y position the height of the bar is subtracted from the display height of 240 to make it look as if the graph is drawn from the bottom up. It is in fact from the top down.


def specgraph(self):
    bar = []
    m = float(max(self))
    total = 200 #max pixel height of bar graph
    for i in range(len(self)):
        bar.append(round((self[i]/m)*total))
    return bar

def bargraph():
    #Display Results as a Bargarph
    disppx = 240
    sp = spectrum() #get spectral readings
    bscale = specgraph(sp) #scale results to a 200px bar graph
    bargrp = displayio.Group(max_size=10) #set display group
    for i in range(len(bscale)): #plot graph
        bargrp.append(Rect(40*i, disppx - bscale[i], 30, bscale[i], fill=palette[i])) #x,y,w,h,fill
    display.show(bargrp)

The next part is the main part of the program. This displays ten text results and then the ten Bar graph readings.


def main():
    while True:
        ledon()
        for i in range(10):
            readings()
            sleep(1)
        for i in range(10):
            bargraph()
            sleep(1)
        ledoff()

if __name__ == "__main__":
    main()

 Example Files:

The font files used in these examples and the full code for each example is available for download here

  • SerifPro-Bold-40.pcf
  • SerifPro-Bold-20.pcf
  • AS7282_spectral_display.py
  • tsl2561_lux.py
  • bme680-display.py
  • vl53l0x_tof_disp.py

The example files have been modified from the code in the adafruits examples file to add display output and functions.

 


Add comment
Any issues with comments then please email me. see contact me link at bottom of the page