© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
C. BellBeginning MicroPython with the Raspberry Pi Picohttps://doi.org/10.1007/978-1-4842-8135-2_4

4. Low-Level Hardware Support

Charles Bell1  
(1)
Warsaw, VA, USA
 

The previous chapters have given us a foundation of what is possible when programming the Pico in MicroPython. However, there is far more about the Pico than what has been presented in the previous chapters. In fact, there are many layers to the Pico hardware support including libraries that contain helpful constructs and classes you will need in order to work with the hardware connected to your Pico.

While we’ve had a quick look at how to work with the Pico including a presentation on several forms of Python projects and a tutorial in programming in Python, we are only just beginning to learn what is possible with the Pico. It is now time to learn more about the available hardware-related software libraries.

In this chapter, we will look at the MicroPython libraries available for you to use in your projects and have a brief look at the low-level hardware support in MicroPython for the Pico. Finally, we will also revisit working with breakout boards to demonstrate some of the libraries and hardware protocols and techniques discussed in previous chapters.

Before we jump into looking at the Pico hardware and supporting software, let’s take a more detailed look at the GPIO header and pins.

The Pico GPIO Header

We learned a bit about the general-purpose input/output (GPIO) header and the pins included in the last chapter. The GPIO pins are arranged in a very specific layout that isn’t completely linear. More specifically, the GPIO pin number does not map directly to the physical pin number.

Where people can go wrong is not knowing (or not verifying) the GPIO header layout, which can lead to the wrong pins being used for electronics and can result in unexpected behavior, things not working at all, or even damaged components. Thus, like all successful endeavors, you need to consult a map before you begin. Figure 4-1 shows a drawing that illustrates the GPIO pins available on the Pico.
Figure 4-1

Pico GPIO pins (courtesy of raspberrypi.org)

Here, we see there are pins labeled (starting from closest to the board) by a physical number, logical GPIO name/number, then any low-level interfaces or mechanisms supported. Some pins can be programmed to operate in different modes or for different hardware features. For example, look at physical pins 31 and 32. Here, we see the pins can act as analog pins (indicated with ADC) as well as an I2C interface (more on that later). Also notice there are a number of pins marked as ground (GND), and those related to power located on physical pins 36, 37, 39, and 40.

Now, let’s take a look at the core MicroPython software libraries available that provide advanced capabilities we can exploit in our projects.

Note

This chapter contains only a subset of the much larger documentation found at https://datasheets.raspberrypi.org/pico/pico-datasheet.pdf.

MicroPython Libraries

The libraries available in MicroPython mirror those in Python. In fact, the libraries in the firmware (sometimes called the application programming interface or API or firmware API) comprise a great deal of the same libraries in Python.

There are some notable exceptions for standard libraries where there is an equivalent library in MicroPython, but it has been renamed to distinguish it from the Python library. In this case, the library has either been reduced in scope by removing the less frequently used features or modified in some ways to fit the MicroPython platform – all to save space (memory).

There are also libraries that are specific to MicroPython and the hardware that provide functionality that may or may not be in some general Python releases. These libraries are designed to make working with the microcontroller and hardware easier.

Thus, there are three types of libraries in the firmware: those that are standard and mostly the same as those in Python, those that are specific to MicroPython, and those specific to the hardware. There is another type of library sometimes called user-supplied or simply custom libraries. These are libraries (APIs) we create ourselves that we can deploy to our board and thereby make functionality available to all our projects. We will see an overview of all types of libraries in this section.

Rather than simply paraphrase or (gasp) copy the existing documentation, we will see overviews of the libraries in the form of quick reference tables you can use to become familiar with what is available. We will also see some code snippets designed to help you learn how to work with some of the more common libraries.

Let’s begin with a look at those libraries in MicroPython that are “standard” Python libraries.

Built-In and Standard Libraries

MicroPython is a specialized and trimmed-down version of Python we can use on our PC. It contains much of the same libraries as Python, but with some differences. We call these libraries “built-in,” but it is more correct to name them “standard” libraries since these libraries are the same as those in Python.

They have the same classes with the same functions as those in Python. So, you can write a script on your PC and execute it there and then execute the same script unaltered on your MicroPython board. Nice! As you can surmise, this helps greatly when developing a complex project.

In this section, we will explore the standard Python libraries beginning with a short overview of what is available followed by details on how to use some of the more common libraries.

Tip

See https://datasheets.raspberrypi.org/pico/raspberry-pi-pico-python-sdk.pdf for complete documentation of the built-in libraries for MicroPython on the Pico. You can also check out the overview at https://docs.micropython.org/en/latest/rp2/quickref.html.

Overview

The standard libraries in MicroPython contain objects that you can use to perform mathematical functions, operate on programming structures, work with transportable documents (a document store) through JSON, interact with the operating system and other system functions, and even perform calculations on time.

Table 4-1 contains a list of the current standard MicroPython libraries. The first column is the name we use in our import statement, the second is a short description, and the third contains a link to the online documentation with abbreviated links for brevity.

Note

All links start with https://docs.micropython.org/en/latest/.

Table 4-1

Standard Python Libraries in MicroPython

Library

Description

Documentation

cmath

Mathematical functions for complex numbers

library/cmath.html

gc

Control the garbage collector

library/gc.html

math

Mathematical functions

library/math.html

uarray

Arrays of numeric data

library/uarray.html

uasyncio

Asynchronous I/O scheduler

library/uasyncio.html

ubinascii

Binary/ASCII conversions

library/ubinascii.html

ucollections

Collection and container types

library/ucollections.html

uerrno

System error codes

library/uerrno.html

uhashlib

Hashing algorithms

library/uhashlib.html

uio

Input/output streams

library/uio.html

ujson

JSON encoding and decoding

library/ujson.html

uos

Basic “operating system” services

library/uos.html

ure

Simple regular expressions

library/ure.html

uselect

Wait for events on a set of streams

library/uselect.html

ustruct

Pack and unpack primitive data types

library/ustruct.html

usys

System-specific functions

library/usys.html

utime

Time-related functions

library/utime.html

uzlib

zlib decompression

library/uzlib.html

_thread

Multithreading support

library/_thread.html

Note

The MicroPython standard library includes additional libraries not currently part of the Pico firmware. For example, the uheapq, ussl, and usocket libraries are not currently included, but may be added in later releases.

As you can see, there are many libraries that begin with u to signify they are special versions of the Python equivalent libraries. That is, if you need access to the original Python version – if it exists – you can still access it by using the original name (without the u prefix). In this case, MicroPython will attempt to find the module by the original name and, if not there, default to the MicroPython version. For example, if we wanted to use the original io library, we could use import io. However, if there is no module named io on the platform, MicroPython will use the MicroPython version named uio.

Next, we will look at some of the more commonly used standard libraries and see some code examples for each. But first, there are two categories of standard functions we should discuss.

Interactive Help For Libraries
A little-known function named help() can be, well, very helpful when learning about the libraries in MicroPython. You can use this function in a REPL session to get information about a library. The following shows an excerpt of the output for the uos library:
>>> help(uos)
object <module 'uos'> is of type module
  __name__ -- uos
  uname -- <function>
  urandom -- <function>
  chdir -- <function>
  getcwd -- <function>
  listdir -- <function>
  mkdir -- <function>
  remove -- <function>
  rename -- <function>
  rmdir -- <function>
  stat -- <function>
  statvfs -- <function>
  ilistdir -- <function>
  mount -- <function>
  umount -- <function>
  VfsFat -- <class 'VfsFat'>
  VfsLfs2 -- <class 'VfsLfs2'>
>>>

Notice we see the names of all the functions and, if present, constants. This can be a real help when learning the libraries and what they contain. Try it!

Now let’s look at examples of some of the more commonly used standard libraries. What follows is just a sampling of what you can do with each of the libraries. See the online documentation for a full description of all the capabilities.

sys

The sys library provides access to the execution system such as constants, variables, command-line options, streams (stdout, stdin, stderr), and more. Most of the features of the library are constants or lists. The streams can be accessed directly, but typically we use the print() function, which sends data to the stdout stream by default. The following shows the most commonly used functions in this library, and Listing 4-1 contains a demonstration of the sys library:
  • sys.argv: List of arguments passed to the script from the command line

  • sys.exit(r): Exit the program returning the value r to the caller

  • sys.modules: List of modules loaded (imported)

  • sys.path: List of paths to search for modules – can be modified

  • sys.platform: Display the platform information such as Linux, MicroPython, etc.

  • sys.stderr: Standard error stream

  • sys.stdin: Standard input stream

  • sys.stdout: Standard output stream

  • sys.version: The version of Python currently executing

# Beginning MicroPython - Chapter 4: Listing 4-1
# Example use of the sys library
import sys
print("Modules loaded: " , sys.modules)
sys.path.append("/my_libs")
print("Path: ", sys.path)
sys.stdout.write("Platform: ")
sys.stdout.write(sys.platform)
sys.stdout.write(" ")
sys.stdout.write("Version: ")
sys.stdout.write(sys.version)
sys.stdout.write(" ")
sys.exit(1)
Listing 4-1

Demonstration of the sys Library Features

Notice we start with the import statement, and after that, we can print the constants and variables in the sys library using the print() function. We also see how to append a path to our search path with the sys.path.append() function. This is very helpful if we create our own directories on the flash memory drive to place our code. Without this addition, the import statement will fail unless the code module is in the lib directory.

At the end of the example, we see how to use the stdout stream to write things to the screen. Note that you must provide the carriage return (newline) command to advance the output to a new line ( ). The print() function takes care of that for us. The following shows the output of running this script on the Pico:
Modules loaded:  {'rp2': <module 'rp2' from 'rp2.py'>}
Path:  ['', '/lib', '/my_libs']
Platform: rp2
Version: 3.4.0

Notice the addition of the my_libs folder. We add this so that we could import modules from that directory. If you place your modules in a subfolder, and don’t include the subfolder in the import statement, you must add the folder to the system path.

uio

The uio library contains additional functions to work with streams and stream-like objects. There is a single function named uio.open() that you can use to open files (but most people use the built-in function named open()) as well as classes for byte and string streams. In fact, the classes have similar file functions such as read(), write(), seek(), flush(), close(), as well as a getvalue() function, which returns the contents of the stream buffer that contains the data. Listing 4-2 shows a demonstration of the uio library.
# Beginning MicroPython - Chapter 4: Listing 4-2
# Example use of the uio library
# Note: change uio to io to run this on your PC!
import uio
try:
    fio_out = uio.open('data.bin', 'wb')
    fio_out.write(b"x5Fx9ExAEx09x3Ex96x68x65x6Cx6Cx6F")
    fio_out.write(b"x00")
    fio_out.close()
except Exception as err:
    print("ERROR (writing):", err)
# Read the binary file and print out the results in hex and char.
try:
    fio_in = uio.open('data.bin', 'rb')
    print("Raw,Dec,Hex from file:")
    byte_val = fio_in.read(1)  # read a byte
    while byte_val:
        print(byte_val, ",", ord(byte_val), hex(ord(byte_val)))
        byte_val = fio_in.read(1)  # read a byte
    fio_in.close()
except Exception as err:
    print("ERROR (reading):", err)
Listing 4-2

Demonstration of the uio Library Features

In this example, we first open a new file for writing and write an array of bytes to the file. The technique used is passing the hex values for each byte to the write() function. When you read data from sensors, they are typically in binary form (a byte or string of bytes). You signify a byte with the escape x as shown.

After writing the data to the file, we then read the file one byte at a time by passing 1 to the read() function. We then print the values read in their raw form (the value returned from the read(1) call) as a decimal value and a hex value. The bytes written contain a secret word (one obscured by using hex values) – can you see it?

This is like how you would use the normal built-in functions, which we saw in the last chapter. The following shows the output when run on the Pico:
Raw,Dec,Hex from file:
b'_' , 95 0x5f
b'x9e' , 158 0x9e
b'xae' , 174 0xae
b' ' , 9 0x9
b'>' , 62 0x3e
b'x96' , 150 0x96
b'h' , 104 0x68
b'e' , 101 0x65
b'l' , 108 0x6c
b'l' , 108 0x6c
b'o' , 111 0x6f
b'x00' , 0 0x0
If you’re curious what the file looks like, you can use a utility like hexdump to print the contents as shown in the following. Can you see the hidden message?
$ hexdump -C data.bin
00000000  5f 9e ae 09 3e 96 68 65  6c 6c 6f 00              |_...>.hello.|
0000000c

ujson

The ujson library is one of those libraries you are likely to use frequently when working with data in an IoT project. It provides encoding and decoding of JavaScript Object Notation (JSON) documents. This is because many of the IoT services available either require or can process JSON documents. Thus, you should consider getting into the habit of formatting your data in JSON to make it easier to integrate with other systems. The library implements the following functions that you can use to work with JSON documents:
  • ujson.dumps(obj): Return a string decoded from a JSON object

  • ujson.loads(str): Parse the JSON string and return a JSON object. Will raise an error if not formatted correctly

  • ujson.load(fp): Parse the contents of a file pointer (a file string containing a JSON document). Will raise an error if not formatted correctly

Recall we saw a brief example of JSON documents in the last chapter. That example was written exclusively for the PC, but a small change makes it possible to run it on the Pico. Let’s look at a similar example. Listing 4-3 shows an example of using the ujson library.
# Beginning MicroPython - Chapter 4: Listing 4-3
# Example use of the ujson library
# Note: change ujson to json to run it on your PC!
import ujson
# Prepare a list of JSON documents for pets by converting JSON to a dictionary
vehicles = []
vehicles.append(ujson.loads('{"make":"Chevrolet", "year":2015, "model":"Silverado", "color":"Pull me over red", "type":"pickup"}'))
vehicles.append(ujson.loads('{"make":"Yamaha", "year":2009, "model":"R1", "color":"Blue/Silver", "type":"motorcycle"}'))
vehicles.append(ujson.loads('{"make":"SeaDoo", "year":1997, "model":"Speedster", "color":"White", "type":"boat"}'))
vehicles.append(ujson.loads('{"make":"TaoJen", "year":2013, "model":"Sicily", "color":"Black", "type":"Scooter"}'))
# Now, write these entries to a file. Note: overwrites the file
json_file = open("my_vehicles.json", "w")
for vehicle in vehicles:
    json_file.write(ujson.dumps(vehicle))
    json_file.write(" ")
json_file.close()
# Now, let's read the list of vehicles and print out their data
my_vehicles = []
json_file = open("my_vehicles.json", "r")
for vehicle in json_file.readlines():
    parsed_json = ujson.loads(vehicle)
    my_vehicles.append(parsed_json)
json_file.close()
# Finally, print a summary of the vehicles
print("Year Make Model Color")
for vehicle in my_vehicles:
    print(vehicle['year'],vehicle['make'],vehicle['model'],vehicle['color'])
Listing 4-3

Demonstration of the ujson Library Features

The following shows the output of the script running on the Pico:
Year Make Model Color
2015 Chevrolet Silverado Pull me over red
2009 Yamaha R1 Blue/Silver
1997 SeaDoo Speedster White
2013 TaoJen Sicily Black

uos

The uos library implements a set of functions for working with the base operating system. Some of the functions may be familiar if you have written programs for your PC. Most functions allow you to work with file and directory operations. The following lists several of the more commonly used functions:
  • uos.chdir(path): Change the current directory

  • uos.getcwd(): Return the current working directory

  • uos.listdir([dir]): List the current directory if dir is missing or list the directory specified

  • uos.mkdir(path): Create a new directory

  • uos.remove(path): Delete a file

  • uos.rmdir(path): Delete a directory

  • uos.rename(old_path, new_path): Rename a file

  • uos.stat(path): Get the status of a file or directory

In this example, we see how to change the working directory so that we can simplify our import statements. We also see how to create a new directory, rename it, create a file in the new directory, list the directory, and finally clean up (delete) the changes. Listing 4-4 shows the example for working with the uos library functions.
# Beginning MicroPython - Chapter 4
# Example use of the uos library
# Note: change uos to os to run it on your PC!
import sys
import uos
# Create a function to display files in directory
def show_files():
    files = uos.listdir()
    sys.stdout.write(" Show Files Output: ")
    sys.stdout.write(" name size ")
    for file in files:
        stats = uos.stat(file)
        # Print a directory with a "d" prefix and the size
        is_dir = True
        if stats[0] > 16384:
            is_dir = False
        if is_dir:
            sys.stdout.write("d ")
        else:
            sys.stdout.write(" ")
        sys.stdout.write(file)
        if not is_dir:
            sys.stdout.write(" ")
            sys.stdout.write(str(stats[6]))
        sys.stdout.write(" ")
# List the current directory
show_files()
# Create a directory
uos.mkdir("test")
show_files()
Listing 4-4

Demonstration of the uos Library Features

While this example is a little long, it shows some interesting tricks. Notice we created a function to print out the directory list rather than printing out the list of files returned. We also checked the status of the file to determine if the file was a directory or not, and if it is, we printed a d to signal the name refers to a directory. We also used the stdout stream to control formatting with tabs ( ) and newline ( ) characters.

Now let’s see the output. The following shows the output when run on the Pico. Note: If you run this a second time, be sure to delete the new directory created.
Show Files Output:
     name     size
     data.bin     12
     example1.py     632
     example2.py     946
     example3.py     939
     fibonacci.py     2259
     hello_pico.py     25
     listing_04_01.py     380
     listing_04_02.py     794
     listing_04_03.py     1411
     listing_04_04.py     907
     my_data.json     268
     my_vehicles.json     377
     roman.py     621
     roman_numerals.py     1839
Show Files Output:
     name     size
     data.bin     12
     example1.py     632
     example2.py     946
     example3.py     939
     fibonacci.py     2259
     hello_pico.py     25
     listing_04_01.py     380
     listing_04_02.py     794
     listing_04_03.py     1411
     listing_04_04.py     907
     my_data.json     268
     my_vehicles.json     377
     roman.py     621
     roman_numerals.py     1839

There are also built-in functions that are not part of any specific library, and there are exceptions that allow us to capture error conditions. Let’s look at those before we dive into some of the more commonly used standard libraries.

Built-In Functions and Classes

Python comes with many built-in functions – functions you can call directly from your script without importing them. There are many classes that you can use to define variables, work with data, and more. They’re objects so you can use them to contain data and perform operations (functions) on the data. We’ve seen a few of these in the examples so far.

Let us see some of the major built-in functions and classes. Table 4-2 includes a short description of each. You should look through this list and explore the links for those you find interesting and refer to the list when developing your projects so that you can use the most appropriate function or class. You may be surprised how much is “built-in.”
Table 4-2

MicroPython Built-In Functions and Classes

Name

Description

abs(x)

Return the absolute value of a number

all(iterable)

Return True if all elements of the iterable are true (or if the iterable is empty)

any(iterable)

Return True if any element of the iterable is true

bin(x)

Convert an integer number to a binary string

class bool([x])

Return a Boolean value, i.e., one of True or False

class bytearray([source[, encoding[, errors]]])

Return a new array of bytes

class bytes([source[, encoding[, errors]]])

Return a new “bytes” object, which is an immutable sequence of integers in the range 0 <= x < 256

callable(object)

Return True if the object argument appears callable, False if not

chr(i)

Return the string representing a character whose Unicode code point is the integer i

classmethod(function)

Return a class method for a function

class complex([real[, imag]])

Return a complex number with the value real + imag*1j or convert a string or number to a complex number

delattr(obj, name)

This is a relative of setattr(). The arguments are an object and a string. The string must be the name of one of the object’s attributes

class dict()

Create a new dictionary

dir([object])

Without arguments, return the list of names in the current local scope. With an argument, attempt to return a list of valid attributes for that object

divmod(a,b)

Take two (noncomplex) numbers as arguments and return a pair of numbers consisting of their quotient and remainder when using integer division

enumerate(iterable, start=0)

Return an enumerate object. The iterable must be a sequence, an iterator, or some other object which supports iteration

eval(expression, globals=None, locals=None)

Evaluate an expression using globals and locals as dictionaries in a local namespace

exec(object[, globals[, locals]])

Execute a set of Python statements or object using globals and locals as dictionaries in a local namespace

filter(function, iterable)

Construct an iterator from those elements of the iterable for which the function returns true

class float([x])

Return a floating-point number constructed from a number or string

class frozenset([iterable])

Return a new frozenset object, optionally with elements taken from the iterable

getattr(object, name[, default])

Return the value of the named attribute of the object. The name must be a string

globals()

Return a dictionary representing the current global symbol table

hasattr(object, name)

The arguments are an object and a string. The result is True if the string is the name of one of the object’s attributes, False if not

hash(object)

Return the hash value of the object (if it has one). Hash values are integers

hex(x)

Convert an integer number to a lowercase hexadecimal string prefixed with “0x”

id(object)

Return the “identity” of an object

input([prompt])

If the prompt argument is present, it is written to standard output without a trailing newline. The function then reads a line from input, converts it to a string (stripping a trailing newline), and returns that

class int(x)

Return an integer object constructed from a number or string x, or return 0 if no arguments are given

isinstance(object, classinfo)

Return true if the object argument is an instance of the classinfo argument or of a (direct, indirect, or virtual) subclass thereof

issubclass(class, classinfo)

Return true if the class is a subclass (direct, indirect, or virtual) of classinfo

iter(object[, sentinel])

Return an iterator object

len(s)

Return the length (the number of items) of an object

class list([iterable])

List sequence

locals()

Update and return a dictionary representing the current local symbol table

map(function, iterable, ...)

Return an iterator that applies a function to every item of the iterable, yielding the results

max([iterable|arg*])

Return the largest item in an iterable or the largest of two or more arguments

class memoryview(obj)

Return a “memory view” object created from the given argument

min([iterable|arg*])

Return the smallest item in an iterable or the smallest of two or more arguments

next(iterator[, default])

Retrieve the next item from the iterator by calling its __next__() method

class objectO

Return a new featureless object. The object is a base for all classes

oct(x)

Convert an integer number to an octal string

open(file, mode="r", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

Open a file and return a corresponding file object. Use close() to close the file

ord(c)

Given a string representing one Unicode character, return an integer representing the Unicode code point of that character

pow(x, y[, z])

Return x to the power y; if z is present, return x to the power y, modulo z (computed more efficiently than pow(x, y) % z)

print(*objects, sep=' ', end=" ", file=sys.stdout, flush=False)

Print objects to the text stream file, separated by sep and followed by end. sep, end, file, and flush, if present, must be given as keyword arguments

class property(fget=None, fset=None, fdel=None, doc=None)

Return a property attribute

range([stop|[start, stop[, step]]])

Range sequence

repr(object)

Return a string containing a printable representation of an object

reversed(seq)

Return a reverse iterator

round(number[, ndigits])

Return a number rounded to ndigits precision after the decimal point

class set([iterable])

Return a new set object, optionally with elements taken from the iterable

setattr(object, name, value)

This is the counterpart of getattr(). The arguments are an object, a string, and an arbitrary value

class slice(start, stop[, step])

Return a slice object representing the set of indices specified by range(start, stop, step)

sorted(iterable[, key][, reverse])

Return a new sorted list from the items in the iterable

staticmethod(function)

Return a static method for a function

class str(object)

Return a str version of an object

sum(iterable[, start])

Sum the start and the items of an iterable from left to right and return the total

super([type[, object-or-type]])

Return a proxy object that delegates function calls to a parent or sibling class of a type

class tuple([iterable])

Tuple sequence

type(object)

Return the type of an object

zip(*iterables)

Make an iterator that aggregates elements from each of the iterables

Now let’s talk about a topic we haven’t talked a lot about – exceptions. Exceptions are part of the built-in module for Python and can be a very important programming technique you will want to use. Perhaps not right away, but eventually you will appreciate the power and convenience of using exceptions in your code.

Exceptions

There is also a powerful mechanism we can use in Python (and MicroPython) to help manage or capture events when errors occur and execute code for a specific error. This construct is called exceptions, and the exceptions (errors) we can capture are called exception classes. It uses a special syntax called the try statement (also called a clause since it requires at least one other clause to form a valid statement) to help us capture errors as they are generated.

Exceptions can be generated anywhere in code with the raise() function. That is, if something goes wrong, a programmer can “raise” a specific, named exception, and the try statement can be used to capture it via an except or else clause. Table 4-3 shows the list of exception classes available in MicroPython along with a short description of when (how) the exception could be raised.
Table 4-3

MicroPython Exception Classes

Exception Class

Description of Use

AssertionError

An assert() statement fails

AttributeError

An attribute reference fails

Exception

Base exception class

ImportError

One or more modules failed to import

IndexError

Subscript is out of range

KeyboardInterrupt

Keyboard CTRL+C was issued or simulated

KeyError

Key mapping in the dictionary is not present in the list of keys

MemoryError

Out of memory condition

NameError

A local or global name (variable, function, etc.) is not found

NotImplementedError

An abstract function has been encountered (it is incomplete)

OSError

Any system-related error from the operating system

RuntimeError

Possibly fatal error encountered on execution

StopIteration

An iterator's next function signaled no more values in an iterable object

SyntaxError

Code syntax error encountered

SystemExit

The sys.exit() function was called or simulated

TypeError

A function or operation is applied to an inappropriate type (like type mismatch)

ValueError

The right type but wrong value found (like out of bounds)

ZeroDivisionError

Mathematical function results in division by zero

The syntax for the try statement is shown as follows. Each part of the construct is called a clause:
try_stmt  ::=  try1_stmt | try2_stmt
try1_stmt ::=  "try" ":" code block
               ("except" [expression ["as" identifier]] ":" code block)+
               ["else" ":" code block]
               ["finally" ":" code block]
try2_stmt ::=  "try" ":" code block
               "finally" ":" code block

Notice there are four clauses: try, except, else, and finally. The try clause is where we put our code (code block) – one or more lines of code that will be included in the exception capture. There can be only one try, else, and finally, but you can have any number of except clauses naming an exception class.

In fact, the except and else go together such that if an exception is detected running any of the lines of code in the try clause, it will search the except clauses, and if and only if no except clause is met, it will execute the else clause. The finally clause is used to execute after all exceptions are processed and executed.

Notice also that there are two versions of the statement: one that contains one or more except and optionally an else and finally, and another that has only the try and finally clauses.

Let’s look at one of the ways we can use the statement to capture errors in our code. Suppose you are reading data from a batch of sensors and the libraries (modules) for those sensors raise ValueError if the value read is out of range or invalid. It may also be the case that you don’t want the data from any other sensors if one or more fail. So, we can use code like the following to “try” to read each of the sensors and, if there is a ValueError, issue a warning and keep going or, if some other error is encountered, flag it as an error during the read. Note that typically we would not stop the program at that point; rather, we would normally log it and keep going. Study the following until you’re convinced exceptions are cool:
values = []
print("Start sensor read.")
try:
    values.append(read_sensor(pin11))
    values.append(read_sensor(pin12))
    values.append(read_sensor(pin13))
    values.append(read_sensor(pin17))
    values.append(read_sensor(pin18))
except ValueError as err:
    print("WARNING: One or more sensors valued to read a correct value.", err)
except:
    print("ERROR: fatal error reading sensors.")
finally:
    print("Sensor read complete.")
Another way we can use exceptions is when we want to import a module (library) but we’re not sure if it is present. For example, suppose there was a module named piano.py that has a function named keys() that you want to import, but the module may or may not be on the system. In this case, we may have other code we can use instead creating our own version of keys(). To test if the module can be imported, we can place our import inside a try block as shown in the following. We can then detect if the import fails and take appropriate steps:
# Try to import the keys() function from piano. If not present,
# use a simulated version of the keys() function.
try:
    from piano import keys
except ImportError as err:
    print("WARNING:", err)
    def keys():
        return(['A','B','C','D','E','F','G'])
print("Keys:", keys())

If we added code like this and the module were not present, not only can we respond with a warning message, but we can also define our own function to use if the module isn’t present.

Finally, you can raise any exception you want including creating your own exceptions. Creating custom exceptions is an advanced topic, but let’s see how we can raise exceptions since we may want to do that if we write our own custom libraries. Suppose you have a block of code that is reading values, but it is possible that a value may be out of range. That is, too large for an integer, too small for the valid range of values expected, etc. You can simply raise the ValueError passing in your custom error message as follows with the raise statement and a valid exception class declaration:
raise ValueError("ERROR: the value read from the sensor ({0}) is not in range.".format(val_read))
You can then use the try statement to capture this condition since you know it is possible and work your code around it. For example, if you were reading data, you could elect to skip the read and move on – continue the loop. However, if this exception were to be encountered when running your code and there were no try statements, you could get an error like the following, which, even though is fatal, is still informative:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: ERROR: the value read from the sensor (-12) is not in range.

You can use similar techniques as shown here to make your MicroPython code more robust and tolerant of errors. Better still, you can write your code to anticipate errors and react to them in a graceful, controlled manner.

MicroPython-Specific Libraries

There are also libraries that are built expressly for the MicroPython system. These are libraries designed to help facilitate using MicroPython on the hardware and are specific to the MicroPython implementation of Python. Let’s look at a few of the more common MicroPython libraries and see some code examples for each. What follows is just a sampling of what you can do with each of the libraries. See the online documentation for a full description of all the capabilities.

machine

The machine library contains functions related to the hardware providing an abstraction layer that you can write code to interact with the hardware. Thus, this library is the main library you will use to access features like timers, communication protocols, CPUs, and more. Since this functionality is communicating directly with the hardware, you should take care when experimenting to avoid changing or even potentially damaging the performance or configuration of your board. For example, using the library incorrectly could lead to lockups, reboots, or crashes.

Caution

Take care when working with the low-level machine library to avoid changing or even potentially damaging the performance or configuration of your Pico.

Since the machine library is a low-level hardware abstraction, we will not cover it in depth in this chapter. Rather, we will see more of the hardware features in the next chapter. In the meantime, let’s explore another interesting gem of MicroPython knowledge by showing you how to discover what a library contains through the help function. For example, Listing 4-5 shows an excerpt of what is reported through the REPL console when we issue the statement help(machine) on the Pico. While it doesn’t replace a detailed explanation or even a complete example, it can be useful when encountering a library for the first time.
>>> help(machine)
>>> object <module 'umachine'> is of type module
  __name__ -- umachine
  unique_id -- <function>
  soft_reset -- <function>
  reset -- <function>
  reset_cause -- <function>
  bootloader -- <function>
  freq -- <function>
  idle -- <function>
  lightsleep -- <function>
  deepsleep -- <function>
  disable_irq -- <function>
  enable_irq -- <function>
  time_pulse_us -- <function>
  mem8 -- <8-bit memory>
  mem16 -- <16-bit memory>
  mem32 -- <32-bit memory>
  ADC -- <class 'ADC'>
  I2C -- <class 'I2C'>
  SoftI2C -- <class 'SoftI2C'>
  Pin -- <class 'Pin'>
  PWM -- <class 'PWM'>
  RTC -- <class 'RTC'>
  Signal -- <class 'Signal'>
  SPI -- <class 'SPI'>
  SoftSPI -- <class 'SoftSPI'>
  Timer -- <class 'Timer'>
  UART -- <class 'UART'>
  WDT -- <class 'WDT'>
  PWRON_RESET -- 1
  WDT_RESET -- 3
Listing 4-5

The machine Library Help

Notice there is a lot of information there! What this gives us most is the list of classes we can use to interact with the hardware. Here, we see there are classes for UART, SPI, I2C, PWM, and more.

Custom Libraries

Building your own custom libraries may seem like a daunting task, but it isn’t. What is possibly a bit of a challenge is figuring out what you want the library to do and making the library abstract (enough) to be used by any script. The rules and best practices for programming come into play here such as data abstraction, API immutability, etc.

In this section, we will look at how to organize our code modules into a library (package) that we can deploy (copy) to our Pico and use in all our programs. This example, while trivial, is a complete example that you can use as a template should you decide to make your own custom libraries.

For this example, we will create a library with two modules: one that contains code to perform value conversions for a sensor and another that contains helper functions for our projects – general functions that we want to reuse. We will name the library my_helper. It will contain two code modules: sensor_convert.py and helper_functions.py. Recall we will also need an __init__.py file to help MicroPython import the functions correctly, but we will get back to that in a moment. Let’s look at the first code module.

We will place the files in a directory named my_helper (same as the library name). This is typical convention, but you can put whatever name you want, but you must remember it since we will use that name when importing the library in our code.

There are two ways to go about creating the files. You can create them on your PC and then upload them to the Pico, or you can create them on the Pico directly using Thonny. We will use the Thonny method.

First, connect your Pico to your PC and then open Thonny. Make sure it connects to the Pico. Then, we will create a new folder named my_helper on the Pico. You can do this by right-clicking the Pico section of the file viewer in Thonny and choose New directory…. Once you have the folder created, double-click it and then create a new file named __init__.py and save it on the Pico. Create the other files the same way: sensor_convert.py and helper_functions.py. Once created, you should see the directory and three files as shown in Figure 4-2.
Figure 4-2

New directory and files (Thonny)

Now let’s look at the code. The first module is named helper_functions.py and contains a helper function for formatting a time data structure to print the time in a more pleasing format. Listing 4-6 shows the complete code for the module.
#
# Beginning MicroPython - Chapter 4
#
# Example module for the my_helper library
# This module contains helper functions for general use.
#
# Format the time (epoch) for a better view
def format_time(tm_data):
    # Use a special shortcut to unpack tuple: *tm_data
    return "{0}-{1:0>2}-{2:0>2} {3:0>2}:{4:0>2}:{5:0>2}".format(*tm_data)
Listing 4-6

The helper_functions.py Module

The second code module is named sensor_convert.py and contains functions that are helpful in converting sensor raw values into a string for qualitative comparisons. For example, the function get_moisture_level() returns a string based on the threshold of the raw value.

The data sheet for the sensor will define such values, and you should use those in your code until and unless you can calibrate the sensor. In this case, if the value is less than the lower bound, the soil is dry, and if greater than the upper bound, the soil is wet. Listing 4-7 shows the complete code for the module.
#
# Beginning MicroPython - Chapter 4
#
# Example module for the my_helper library
# This function converts values read from the sensor to a
# string for use in qualifying the moisture level read.
# Constants - adjust to "tune" your sensor
_UPPER_BOUND = 400
_LOWER_BOUND = 250
def get_moisture_level(raw_value):
    if raw_value <= _LOWER_BOUND:
        return("dry")
    elif raw_value >= _UPPER_BOUND:
        return("wet")
    return("ok")
Listing 4-7

The sensor_convert.py Module

Now let’s go over the __init__.py file. This is a very mysterious file that developers often get very confused about. If you do not include one in your library directory, you should import what you want to use manually. That is, with something like import my_helper.helper_functions. But with the file, you can do your imports at one time allowing a simple import my_helper statement, which will import all the files. Let’s look at the __init__.py file. The following shows the contents of the file:
# Metadata
__name__ = "Chuck's Python Helper Library"
__all__ = ['format_time', 'get_moisture_level']
# Library-level imports
from my_helper.helper_functions import format_time
from my_helper.sensor_convert import get_moisture_level
Notice on the first line we use a special constant to set the name of the library. The next constant limits what will be imported by the * (all) option for the import statement. Since it lists all the methods, it’s just an exercise but a good habit to use especially if your library and modules contain many internal functions that you do not want to make usable to others. The last two lines show the import statements used to import the functions from the modules making them available to anyone who imports the library. The following shows a short example of how to do that along with how to use an alias. Here, we use myh as the alias for my_helper:
>>> import my_helper as myh
>>> myh.get_moisture_level(375)
'ok'
>>> myh.get_moisture_level(35)
'dry'
>>> myh.get_moisture_level(535)
'wet'
In case you’re wondering, the help function works on this custom library too!
>>> help(myh)
object <module 'Chuck's Python Helper Library' from 'my_helper/__init__.py'> is of type module
  __path__ -- my_helper
  get_moisture_level -- <function get_moisture_level at 0x2001e610>
  __name__ -- Chuck's Python Helper Library
  __file__ -- my_helper/__init__.py
  format_time -- <function format_time at 0x2001e580>
  helper_functions -- <module 'my_helper.helper_functions' from 'my_helper/helper_functions.py'>
  __all__ -- ['format_time', 'get_moisture_level']
  sensor_convert -- <module 'my_helper.sensor_convert' from 'my_helper/sensor_convert.py'>

Once you have started experimenting with MicroPython and have completed several projects, you may start to build up a set of functions that you reuse from time to time. These are perfect candidates to place into a library. It is perfectly fine if the functions are not part of a larger class or object. So long as you organize them into modules of like functionality, you may not need to worry about making them classes. On the other hand, if data is involved or the set of functions works on a set of data, you should consider making that set of functions a class for easier use and better quality code.

Low-Level Libraries

While the MicroPython firmware at the most basic of functionality is the same from board to board for all the general Python languages supported and many of the built-in functions, some of the libraries in the MicroPython firmware have a few minor differences from one board to another.

In some cases, there are more libraries or classes available than others or perhaps the classes are organized differently, but most implement the same core libraries in one form or another. The same cannot be said to be true at the lower-level hardware abstraction layers. This is simply because one board vendor may implement different hardware than others. In some cases, the board has features that are not present on other boards. For example, some boards support networking, but the Pico (currently) does not. To keep things brief, we will explore the board-specific libraries for the Pico.

Tip

You can see the differences in the low-level library support for other boards at https://docs.micropython.org/en/latest/library/index.html, clicking the links for the other boards listed such as the pyboard, ESP8266, and WiPy. The Pico libraries are listed under the RP2040 section at https://docs.micropython.org/en/latest/library/rp2.html.

The low-level libraries for the Pico (also described as RP2040-specific libraries) are encapsulated in a single library named rp2. This library contains a number of classes and functions for performing programmable input/output tasks (PIO), accessing the filesystem (flash drive) directly, or working with a state machine. The classes are defined as follows:
  • Flash: Built-in flash storage

  • PIO: Advanced PIO

  • StateMachine: Support for the RP2040’s programmable I/O interface

The PIO and StateMachine classes provide the ability to add additional interfaces or protocols such as additional serial communication support. This is not likely something most Pico projects will require, but it is there should you find you need one more interface than what is provided by the Pico hardware.

With these classes, you can create your own custom low-level hardware access mechanisms. For example, if you have a sensor that needs a specific timing to read data faster than the existing hardware and software library support or a device requires a specific sequence of commands or responses, you can use these classes to essentially use software to form the hardware interface. These special code segments are loaded and run in a special processing core allowing up to eight processes to run. You can find a complete guide to using PIO in Chapter 3 of the RP2040 data sheet book (https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf).

Similarly, the Flash class provides the ability to work directly with the flash filesystem. This may be handy if you want to do some low-level data storage, but in general you are encouraged to use the existing higher-level MicroPython libraries for reading and writing files.

Should you wish to explore these classes in greater detail, or you want to learn more about PIO support, you can find example code at https://github.com/raspberrypi/pico-micropython-examples/tree/master/pio.

Working with Low-Level Hardware

Working with the low-level hardware (some would just say, “hardware” or “device”) is where all the action and indeed the focus (and relative difficulty) of using MicroPython takes place. MicroPython and the breakout board vendors have done an excellent job of making things easier for us, but there is room for improvement in the explanations.

That is, the documentation online is a bit terse when it comes to offering examples of using the low-level hardware. Part of this is because the examples often require additional, specific hardware and software. For example, to work with the I2C interface, you will need an I2C capable breakout board as well as a software library (or drive) to “talk” to the board. Thus, the online examples provide only the most basic of examples and explanations.

Except for the onboard sensors, most low-level communication will be through I2C, one-wire, analog, or digital pins, or using even SPI interfaces. The I2C and SPI interfaces are those where you will likely encounter the most difficulty working with hardware. This is because each device (breakout board) you use will require a very specific protocol. That is, the device may require a special sequence to trigger the sensor or features of the device that differs from other breakout boards. Thus, working with I2C or SPI (and some other) type devices can be a challenge to figure out exactly how to “talk” to them.

Drivers and Libraries to the Rescue!

Fortunately, there are a small but growing number of people making classes and sets of functions to help us work with those devices. These are called libraries or more commonly drivers and come in the form of one or more code modules that you can download, copy to your board, and import the functionality into your program. The developers of the drivers have done all the heavy lifting for you, making it very easy to use the device.

Thus, for most just starting out with MicroPython wanting to work with certain sensors, devices, breakout boards, etc., you should limit what you plan to use to those that you can find a driver that works with it. So, how do you find a driver for your device? There are several places to look.

First and foremost, you should look to the forums and documentation on MicroPython. In this case, don’t limit yourself to only those forums that cater to your board of choice. Rather, look at all of them! Chances are you can find a library that you can adapt with only minor modifications. Most of them can be used with very little or even no effort beyond downloading it and copying it to the board. The following lists the top set of forums and documentation you should frequent when looking for drivers:
There are also a number of documents you can download and read offline. The following are some of the more important Pico documents:

Second, use your favorite Internet search engine and search for examples of the hardware. Use the name of the hardware device and “MicroPython” in your search. If the device is new, you may not find any hits on the search terms. Be sure to explore other search terms too.

Once you find a driver, the fun begins! You should download the driver and copy it to your board for testing. Be sure to follow the example that comes with the driver to avoid using the driver in an unexpected way.

This calls to mind one important thing you should consider when deciding if you want to use the driver. If the driver is documented well and has examples – especially if the example is written for the Pico – you should feel safe using it. However, if the driver isn’t documented at all or there is no or little sample code or it is written for a specific board, you may not want to use it. There is a good chance it is half-baked, old, a work in progress, or just poorly coded. Not all those that share can share and communicate well.

We will see several examples of libraries as we work through the example projects in this book. As you will see, not all are as simple as downloading and using.

One skill we will need going forward is understanding breakout boards and how to use them. Let’s look at how to communicate with breakout boards using the I2C and SPI protocols.

Using Breakout Boards

Breakout boards are one of the key elements hobbyists and enthusiasts will use in creating a MicroPython (or any microcontroller based) IoT solution. This is because breakout boards are small circuit boards that contain all the components needed to support a function such as a sensor, network interface, or even a display. Breakout boards also support one of several communication protocols that require only a few pins to be wired making them very easy to use. In general, they save the developer a lot of time trying to figure out how to design circuits to support a sensor or chip.

There are two methods for working with breakout boards: finding a driver you can use or building your own driver. Building your own driver is not recommended for those new to MicroPython and I2C or SPI. It is much easier to take the time to search for a driver that you can use (or adapt) than to try to write one yourself. This is because you must be able to obtain, read, and understand how the breakout board communicates (understand its protocol). Each board will communicate differently based on the sensor or devices supported. That is, a driver for a BMP180 sensor will not look or necessarily work the same as one for a BME280 sensor. You must be very specific when locating and using a driver.

Searching for a driver can be a tedious endeavor, which requires some patience and perhaps several searches on the forums using different search terms such as “micropython BME280”. Once you find a driver, you can tell quickly whether it is a viable option by looking at the example included. As mentioned before, if there is no example or the example doesn’t resemble anything you’ve seen in this book or in the online documentation, don’t use it.

Let’s look at two examples of breakout boards: one that uses the I2C protocol and another that uses the SPI protocol. We will follow a pattern of explaining the examples that is used throughout the book to introduce the project, present the required components, show you how to set up the hardware (connect everything together), write the code, and finally execute it.

THE VALUE OF ONLINE EXAMPLES

If you want to use a breakout board in your IoT project, be sure to spend some time not only in the forums but also looking at various blogs and tutorials such as those on hackaday.com, learn.sparkfun.com, or learn.adafruit.com. The best blogs and tutorials are those that explain not only how to write the code but also what the breakout board does and how to use it. These online references are few, but the ones from these three sites are among the very best. Also, look at some of the videos on the topic too. Some of those are worth the time to watch – especially if they’re from the nice folks at Adafruit or SparkFun.

Inter-integrated Circuit (I2C)

The I2C protocol is perhaps the most common protocol that you will find on breakout boards. We’ve encountered this term a few times in previous chapters, and thus we only know it is a communication protocol. So, what is it?

What Is I2C?

I2C is a fast digital protocol using two wires (plus power and ground) to read data from circuits (or devices). The protocol is designed to allow the use of multiple devices (slaves) with a single master (the MicroPython board). Thus, each I2C breakout board will have its own address or identity that you will use in the driver to connect to and communicate with the device.

Tip

See https://learn.sparkfun.com/tutorials/i2c for an in-depth discussion of I2C.

Overview

Let’s look at an example of how to use an I2C breakout board. In this example, we want to use an RGB sensor from Adafruit (www.adafruit.com/product/1334) to read the color of objects. Yes, you can make your Pico see in color!

What the code will present is four values read from the sensor. We will see the values for the red, green, and blue spectrum as well as the clear light value. The combination of the red, green, and blue values defines the color. You can use a color picker control from one of several websites like www.rapidtables.com/web/color/RGB_Color.html to show you the color. This RGB sensor isn’t going to give you a 100% color match, but you may be surprised how well it can distinguish colors. Let’s get started.

Required Components

Don’t worry if you do not have or do not want to purchase the Adafruit RGB sensor breakout board (although it is not expensive). This example is provided as a tutorial for working with I2C breakout boards. We will use another I2C breakout board in one of the example projects later in the book. Figure 4-3 shows the Adafruit RGB sensor. Note that this sensor comes without the header soldered, so you will need to solder a header on the breakout board before you can use it with your Pico.
Figure 4-3

Adafruit RGB sensor (courtesy of adafruit.com)

Set Up the Hardware

Wiring the breakout board is also very easy since we need only power, ground, SCL, and SDA connections. SCL is the clock signal, and SDA is the data signal. These pins are labeled on your Pico (or in the documentation) as well as the breakout board. When you connect your breakout board, make sure the power requirements match. That is, some breakout boards can take 5V, but many are limited to 3 or 3.3V. Check the vendor’s website if you have any doubts.

We need only to connect the 3V, ground, SDA, SCL, and LED pins. The LED pin is used to turn on the bright LED on the breakout board to signal it is ready to read. We will leave it on for ten seconds so that there is time to read the color value and then display it. We will then wait another five seconds to take the next reading.

But to get this to work, we will need to connect the breakout board to the Pico. If you ordered a Pico with headers or you soldered your own headers to the Pico, we can use what is called a breadboard to host the Pico and use wires called jumper wires to connect the Pico GPIO pins to the pins on the breakout board.

Note

We will discuss breadboards and their use in more detail in the next chapter.

Once you place your Pico on a breadboard, you can use (5) male-to-female jumper wires to connect to the breakout board. Figure 4-4 shows the connections you need to make.
Figure 4-4

Wiring the RGB sensor

The connections we will use are shown in Table 4-4, which shows the pin for the Pico in the first three columns depicting the description, physical pin, and GPIO number with the pin on the breakout board in the last column. Recall, physical pins are numbered 1–20 on the left of the USB connector starting at the top and 21–40 on the right starting from the bottom.
Table 4-4

Connections for the RGB Sensor

Pico

Physical Pin

GPIO Number

RGB Sensor

Function

Pin Label

OUT

20

GP15

LED

I2C SDA

11

GP8

SDA

I2C SDC

12

GP9

SCL

3V3

37

N/A

3V3

GND

38

N/A

GND

Write the Code

Once you have the hardware connected, set it aside. We need to download the driver and copy it to the board before we can experiment further. You can find the driver for download on GitHub at https://github.com/adafruit/micropython-adafruit-tcs34725. This is a fully working, tested driver that demonstrates how easy it is to use an I2C breakout board.

Note

This library has been abandoned by Adafruit in an effort to focus on their version of MicroPython named CircuitPython. But don’t worry. The library still works very well. We just are not likely to see any updates to the code.

So, how do we find the address of our I2C breakout board? Recall the I2C bus requires each device to have a unique address. The I2C firmware uses this address to know which device it is communicating with, and the device itself will only recognize messages for that specific address.

We check the documentation, or we can check the code for the library. If you open the library you downloaded, you can read through it and look in the initialization code (or constructor) to see what address the library is using. In this case, we find the address in the library is 0x29 as shown in the following, but since the address is a parameter, you can override it if you have another breakout board for the same RGB sensor that is at a different address. This means you can use more than one RGB sensor with the same driver!
class TCS34725:
    def __init__(self, i2c, address=0x29):

To download the driver, you first navigate to https://github.com/adafruit/micropython-adafruit-tcs34725 and then click the Download button and then the Download Zip button. Once the file has been downloaded, unzip it. In the resulting folder, you should find the file named tcs34725.py. This is the driver code module. When ready, copy the module to your Pico and place it in the root folder (same folder as the example code).

Now that the driver is copied to our board, we can write the code. In this example, we will set up the I2C connection to the breakout board and run a loop to read values from the sensor. Sounds simple, but there is a bit of a trick to it. We will forego a lengthy discussion of the code and instead offer some key aspects allowing you to read the code yourself to see how it works.

The key components are setting up the I2C, sensor, a pin for controlling the LED, and reading from the sensor. The LED on the board can be turned on and off by setting a pin high (on) or low (off). First, the I2C code is as follows. Here, we initiate an object, then call the init() function setting the bus to master mode. The scan() function returns a list of addresses found on the bus. We can then print out the device addresses. Notice we define the pins for the SDA and SCL I2C operations too.

Tip

If you see an empty set displayed, your I2C wiring is not correct. Check it and try the code again.

# Setup the I2C - easy, yes?
sda = Pin(8)
scl = Pin(9)
i2c = SoftI2C(sda=sda,scl=scl,freq=400000)
print("I2C Devices found:", end="")
for addr in i2c.scan():
    print("{0} ".format(hex(addr)))
print("")
Notice here we are using something named SoftI2C. This is a special version of the I2C library that supports a different way of communicating with a breakout board. As it turns out, not all I2C devices will work correctly with the firmware implementation of I2C on the Pico. To use the firmware I2C, use the I2C library from the machine module as shown in the following. The only difference beside the name is the first parameter, which tells the I2C we want a master connection:
#i2c = I2C(0,sda=sda,scl=scl,freq=400000)

It is recommended to try the I2C library first, and if that doesn’t work, try the SoftI2C library. This is because the I2C firmware is much faster than the software implementation. We will see specific examples that use I2C and SoftI2C in later chapters.

The next part is the sensor itself. The driver makes this easy. All we need to do is pass in the I2C constructor function as shown:
# Setup the sensor
sensor = tcs34725.TCS34725(i2c)
Setting up the LED pin is also easy. All we need to do is call the Pin() class constructor passing in the pin name (P15) and setting it for output mode as follows:
# Setup the LED pin
led_pin = Pin(15, Pin.OUT)
led_pin.value(0)
Finally, we read from the sensor with the sensor.read() function passing in True, which tells the driver to return the RGBC values. We will then print these out in order. Listing 4-8 shows the completed code. Take a few moments to read through it so that you understand how it works.
"""listing_04_06.py"""
#
# Beginning MicroPython - Chapter 4
#
# Example of using the I2C interface via a driver
# for the Adafruit RGB Sensor tcs34725
#
# Requires library:
# https://github.com/adafruit/micropython-adafruit-tcs34725
#
from machine import I2C, SoftI2C, Pin
import sys
import tcs34725
import utime
# Method to read sensor and display results
def read_sensor(rgb_sense, led):
    sys.stdout.write("Place object in front of sensor now...")
    led.value(1)                 # Turn on the LED
    utime.sleep(5)               # Wait 5 seconds
    sys.stdout.write("reading. ")
    data = rgb_sense.read(True)  # Get the RGBC values
    print("Color Detected: {")
    print("    Red: {0:03}".format(data[0]))
    print("  Green: {0:03}".format(data[1]))
    print("   Blue: {0:03}".format(data[2]))
    print("  Clear: {0:03}".format(data[3]))
    print("} ")
    led.value(0)
# Setup the I2C - easy, yes?
sda = Pin(8)
scl = Pin(9)
i2c = SoftI2C(sda=sda,scl=scl,freq=400000)
print("I2C Devices found:", end="")
for addr in i2c.scan():
    print("{0} ".format(hex(addr)))
print("")
# Setup the sensor
sensor = tcs34725.TCS34725(i2c)
# Setup the LED pin
led_pin = Pin(15, Pin.OUT)
led_pin.value(0)
print("Reading object color every 10 seconds.")
print("When LED is on, place object in front of sensor.")
print("Press CTRL-C to quit.")
while True:
    utime.sleep(10)               # Sleep for 10 seconds
    read_sensor(sensor, led_pin)  # Read sensor and display values
Listing 4-8

Using the Adafruit RGB Sensor

Once you have the code, you can copy it to your board in the similar manner we did for the driver. All that is left is running the example and testing it.

Execute

After copying the code to the Pico, go ahead and run it from Thonny. Listing 4-9 shows an example of the code running. Note that you will get differing results for each object you test in a mixture of the RGB values as shown.
I2C Devices found:0x29
Reading Colors every 10 seconds.
When LED is on, place object in front of sensor.
Press CTRL-C to quit.
Place object in front of sensor now...reading.
Color Detected: {
    Red: 057
  Green: 034
   Blue: 032
  Clear: 123
}
Place object in front of sensor now...reading.
Color Detected: {
    Red: 054
  Green: 069
   Blue: 064
  Clear: 195
}
Place object in front of sensor now...reading.
Color Detected: {
    Red: 012
  Green: 013
   Blue: 011
  Clear: 036
}
...
Listing 4-9

Output from Using the Adafruit RGB Sensor

If you wanted another exercise, you could take these values from the sensor and map them to an RGB LED. Yes, you can do that! Go ahead, try it. See the example GitHub project at https://github.com/JanBednarik/micropython-ws2812 for inspiration. Tackle it after you’ve read the next section on SPI.

Serial Peripheral Interface (SPI)

The Serial Peripheral Interface (SPI) is designed to allow sending and receiving data between two devices using a dedicated line for each direction. That is, it uses two data lines along with a clock and a slave select pin. Thus, it requires six connections for bidirectional communication or only five for reading or writing only. Some SPI devices may require a seventh pin called a reset line.

Overview

Let’s look at an example of how to use an SPI breakout board. In this example, we want to use the Adafruit Thermocouple Amplifier MAX31855 breakout board (www.adafruit.com/product/269) and a Thermocouple Type-K sensor (www.adafruit.com/product/270) to read high temperatures. It can also read low or room temperature, so don’t worry. You won’t need to put this in a heater or oven to use it!

In fact, we’re going to use this example to show how easy it is to read one of the most common measurements (samples) taken – temperature. Once the code is running, you can simply touch the thermocouple and watch the values respond (change) as it heats up and again when you let go. A touchable project, cool!

Required Components

Don’t worry if you do not have or do not want to purchase the Adafruit Thermocouple Amplifier MAX31855 breakout board (although it is not expensive). This example is provided as a tutorial for working with SPI breakout boards. We will use another I2C breakout board in one of the example projects later in the book. Figure 4-5 shows the Adafruit Thermocouple Amplifier and Type-K sensor from Adafruit.
Figure 4-5

Adafruit Thermocouple breakout board and Type-K sensor (courtesy of adafruit.com)

The sensor can be used to measure high temperatures either through proximity or touch. The sensor can read temperature in the range –200°C to +1350°C output in 0.25 degree increments. One possible use of this sensor is to read the temperature of nozzles on 3D printers or any similar high heat output. It should be noted that the breakout board comes unassembled, so you will need to solder the header and terminal posts.

Set Up the Hardware

Now, let’s see how to wire the breakout board to our Pico. We will use only five wires since we are only reading data from the sensor on the breakout board. This requires a connection to power, ground (GND), the master input (MOSI), clock (CLK), and chip select (CS). We only receive information from the sensor, so the MISO (transmit) pin isn’t needed. Figure 4-6 shows the connections.
Figure 4-6

Wiring the Adafruit Thermocouple module

The connections we will use are shown in Table 4-5, which shows the pin for the Pico in the first three columns depicting the description, physical pin, and GPIO number with the pin on the breakout board in the last column. Recall, physical pins are numbered 1–20 on the left of the USB connector starting at the top and 21–40 on the right starting from the bottom.
Table 4-5

Connections for the MAX31855

Pico

Physical Pin

GPIO Number

RGB Sensor

Function

Pin Label

GND

38

N/A

GND

3V3

37

N/A

3V3

SPI RX/MOSI

6

GP4

SD0

SPI CS

2

GP1

CS

SPI CLK

9

GP6

CLK

Now, let’s look at the code!

Write the Code

In this example, we are not going to use a driver; rather, we’re going to see how to read directly from the breakout board using SPI. To do so, we first set up an object instance of the SPI interface and then choose a pin to use for chip select (also called code or even slave select). From there, all we need to do is read the data and interpret it. We will read the sensor in a loop and write a function to convert the data.

This is the tricky part. This example shows you what driver authors must do to make using the device easier. In this case, we must read the data from the breakout board and interpret it. We could just read the raw data, but that would not make any sense since it is in binary form. Thus, we can borrow some code from Adafruit that reads the raw data and makes sense of it.

The function is named normalize_data() as shown in the following, and it does some bit shifting and arithmetic to transform the raw data to a value in Celsius. This information comes from the data sheet for the breakout board, but the nice folks at Adafruit made it easy for us:
# Create a method to normalize the data into degrees Celsius
def normalize_data(data):
    temp = data[0] << 8 | data[1]
    if temp & 0x0001:
        return float('NaN')
    temp >>= 2
    if temp & 0x2000:
        temp -= 16384
    return (temp * 0.25)
Setting up the SPI class is easy. We initiate an SPI object using the class constructor passing in the SPI option. We will use 0 for the first SPI implementation. The other parameters tell the SPI class to set up the SCK, MISO, and MOSI pins (even though we are not using the MOSI pin) and set the baud rate, polarity, and phase (which can be found on the data sheet). We also set the CS pin and turn it on (set to high) after initializing the SPI library. The following shows the code we need to activate the SPI interface:
...
spi_cs = Pin(1)
spi = SPI(0, baudrate=1000000, sck=Pin(6), miso=Pin(4), mosi=Pin(3))
spi_cs.high()
...
Now, let’s look at the completed code. Listing 4-10 shows the complete code to use the Thermocouple Amplifier breakout board from Adafruit.
"""listing_04_07.py"""
#
# Beginning MicroPython - Chapter 4
#
# Example of using the SPI interface via direct access
# for the Adafruit Thermocouple Module MAX31855
#
from machine import Pin, SPI
import utime
# Create a method to normalize the data into degrees Celsius
def normalize_data(data):
    temp = data[0] << 8 | data[1]
    if temp & 0x0001:
        return float('NaN')
    temp >>= 2
    if temp & 0x2000:
        temp -= 16384
    return (temp * 0.25)
spi_cs = Pin(1)
spi = SPI(0, baudrate=1000000, sck=Pin(6), miso=Pin(4), mosi=Pin(3))
spi_cs.high()
# read from the chip
print("Reading temperature every second.")
print("Press CTRL-C to stop.")
while True:
    spi_cs.low()
    utime.sleep(1)
    print("Temperature is {:05.2F} C".format(normalize_data(spi.read(4))))
    spi_cs.high()
Listing 4-10

The Adafruit Thermocouple Module Example

Execute

At this point, you can make the hardware connections and plug in your Pico. Then, you can copy the file to your Pico and run it. Let it run for a few readings and then try to gently grasp the silver portion (the far end) of the thermocouple with two fingers. Be sure not to turn the Pico or the breakout board. You should see a change in temperature. You can let go and also see the temperature return to near room temperature.
Reading temperature every second.
Press CTRL-C to stop.
Temperature is 24.50 C
Temperature is 24.50 C
Temperature is 24.25 C
Temperature is 24.25 C
Temperature is 25.75 C
Temperature is 25.50 C
Temperature is 25.75 C
Temperature is 26.25 C
Temperature is 26.00 C
Temperature is 26.50 C
Temperature is 26.50 C
Temperature is 27.00 C
Temperature is 27.25 C
Temperature is 27.00 C
Temperature is 27.50 C
Temperature is 27.50 C
Temperature is 27.00 C
...

Once you run the example, you should see it produce values in degrees Celsius. If you see 00.00, or NaN, you likely do not have the SPI interface connected properly. Check your wiring against the preceding figure. If you see values but they go down when you expose the thermocouple tip to heat, you need to reverse the wires. Be sure to power off the board first to avoid damaging the sensor, breakout board, or your Pico!

Summary

Accessing the low-level hardware through the firmware is where the true elegance and in some cases complexity of using MicroPython begins. We also need to know what breakout boards and devices we want to connect to and if there are drivers or other libraries we can use to access them. In this case, most breakout boards with I2C or SPI interfaces will require some form of a driver.

In this chapter, we explored some of the low-level support in the firmware and specialized support for the Pico in MicroPython and explored some of the more commonly used built-in and MicroPython libraries that we will use in our projects. We also saw a lot of code in this chapter – more than any previous chapter. The examples in this chapter are meant to be examples for you to see how things are done rather than projects to implement on your own (although you’re welcome and encouraged to do so). We will see more hands-on projects with a greater level of detail in later chapters.

In the next chapter, we take a short detour in the form of a short tutorial on electronics. If you’ve never worked with electronics before, the next chapter will give you the information you need to complete the projects in this book and prepare you for an exciting new hobby – building MicroPython IoT projects!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.239.11.178