Raspberry Pi Pico Tips and Tricks
Raspberry Pi Pico Tips and Tricks
Malcolm Maclean
Buy on Leanpub

Table of Contents

Introduction

Welcome!

Hi there. Congratulations on getting your hands on this book. I hope that you’re excited to learning about using a Raspberry Pi Pico.

This will be a journey of discovery for both of us. By experimenting with microcontrollers we will be learning about interfacing from the computing world to the physical world. Others have written many fine words about doing this sort of thing, but I have an ulterior motive. I write books to learn and document what I’ve done. The hope is that by sharing the journey others can learn something from my efforts :-).

Am I ambitious? Maybe :-). But if you’re reading this, I managed to make some headway. I dare say that like other books I have written (or are currently writing) it will remain a work in progress. They are living documents, open to feedback, comment, expansion, change and improvement. Please feel free to provide your thoughts on ways that I can improve things. Your input would be much appreciated.

You will find that I eschew a simple “Do this approach” for more of a story telling exercise. Some explanations are longer and more flowery than might be to everyone’s liking, but there you go, that’s my way :-).

There’s a lot of information in the book. There’s ‘stuff’ that people with a reasonable understanding of microcontrollers and programming will find excessive. Sorry about that. I have gathered a lot of the content from other books I’ve written to create this guide. As a result, it is as full of usable information as possible to help people who could be using the Pico and coding for the first time.

I’m sure most authors try to be as accessible as possible. I’d like to do the same, but be warned… There’s a good chance that if you ask me a technical question I may not know the answer. So please be gentle with your emails :-).

Email: d3noobmail+pico@gmail.com

What are we trying to do?

Put simply, we are going to examine the wonder that is the Raspberry Pi Pico microcontroller and use it to accomplish ‘stuff’.

Along the way we’ll;

  • Look at the Raspberry Pi Pico and its history.
  • We’ll examine the difference between computers and microcontrollers and work out when it might be better to use one over the other.
  • Work out how to get software loaded onto the Pico.
  • Write / install and configure our applications.
  • Write some code to interface with the physical world.
  • Explore just what our system can do for us.

Who is this book for?

You!

By getting hold of a copy of this book you have demonstrated a desire to learn, to explore and to challenge yourself. That’s the most important criteria you will want to have when trying something new. Your experience level will come second place to a desire to learn.

It will be useful to be comfortable using a standard desktop operating system. You should be broadly comfortable with the concept of programming, but you needn’t have tried it before. Before you learn anything new, it pretty much always appears indistinguishable from magic. but once you start having a play, the mystery falls away.

What will we need?

Well, you could just read the book and learn a bit. By itself that’s not a bad thing, but trust me when I say that actually experimenting with computers is fun and rewarding.

The list below is flexible in most cases and will depend on how you want to measure the values.

  • A Raspberry Pi Pico. The standard Pico is okay, but I’m pretty much always going to be using the wireless enabled version, the Pico W.
  • A power supply for the Pico (almost any micro-USB charger will do the job).
  • A remote computer (like your normal desktop PC) that you can use to program the Pico.
  • An Internet connection for getting and updating the software.

As we work through the book we will be covering off the different aspects required and you should get a good overview of what your options are in different circumstances.

Why on earth did I write this rambling tome?

That’s a really good question. Writing the other books was an enjoyable process, so I thought that I’d carry on and write more. This is my eighteenth (?, I lose track) book. So I suppose this a ‘thing’ I do now. Will this continue? Who knows, stay tuned…

Where can you get more information?

The Raspberry Pi as a concept has provided an extensible and practical framework for introducing people to the wonders of computing in the real world. At the same time there has been a boom of information available for people to use them. The following is a far from exhaustive list of sources, but from my own experience it represents a useful subset of knowledge.

raspberrypi.org

Raspberry Pi Stack Exchange

Microcontrollers vs Computers

You might be thinking to yourself, surely all this IT stuff is the same? Well… from the perspective of it being a bunch of highly integrated electronics designed to automate instructions and actions, you’re exactly right. But there are differences in complexity and scale that make some methods of carrying out tasks more complex or more capable than another, and that is where the distinction between microcontrollers and computers comes in.

Microcontrollers

Microcontrollers are compact integrated circuits designed to operate embedded in a larger system. Typical microcontrollers include a microprocessor, memory, timers, input/output connections and converters (Analog-to-digital (ADC) and digital-to-analog (DAC)) on a single chip.

They are often referred to as an embedded controllers and can be found in in a huge number of different areas. They are basically simple computers designed to control small features of a larger component, without a great deal of complexity.

They are typically designed with a specific task (or a limited subset of tasks) in mind and as such they can be simpler to use, but less flexible about their application.

There are a wide range of different options for microcontrollers depending on the users requirements. Strictly speaking, the microcontroller is the highly integrated chip that provides the function on a board, but typically people will refer to them by the manufacturer or model of the board that carries the chip. In that respect the leader of the pack would be the Arduino series of boards. Praised for their simplicity and small size, they have a range of boards for many applications. Some microcontrollers are so ubiquitous that the boards that they are part of are more broadly referred to by their chip name such as those based on the ESP32 or the ESP8266.

One of the more recent entrants to the world of microcontrollers is the Raspberry Pi Foundation. They have released their RP2040 microcontroller chip which has been distributed on their Raspberry Pi Pico boards.

Raspberry Pi Pico W

Computers

Computers are complex devices that are typically comprised of separate microprocessors, memory, bus’s and connectivity for peripheral devices. They are designed to be able to carry out a wide range of tasks and they can vary in size and complexity from large examples which can take up a room to everyday laptop and desktop machines or even our phones.

The feature that they share is that they are collections of discrete circuits that are combined to create a functioning unit. This provides them with greater flexibility so that things like more or less memory can be simply added or a different operating system can be loaded. Like all things, with that capability comes the burden of greater complexity and ultimately cost.

The Raspberry Pi foundation has been manufacturing small, single board computers since 2012 and as such they have come to be a market leader in the supply of small computer boards for computer and electronic hobbyists.

Raspberry Pi 4 B

What’s the difference to you?

It’s all very well knowing that there are these different things out there that look kind of similar and act kind of the same, but which have a significant enough difference that people talk about them in quite different ways. What you really want to know is what impact it has on you and the application that you have in mind.

To my way of thinking the application is the first thing to consider when looking at a potential technology direction to go down. Is this a simple application that won’t require a great deal of complexity or change throughout it’s lifetime? In which case a microcontroller could be a good direction. Or, is the application complex, demanding a high level of computing power or frequent updating? In which case a computer could be a better option. There are even cases where either could be viable.

The short answer is that there will always be so many considerations that need to be taken into account that there can’t be a simple guide that can be used to make a decision on whether to use a full blown computer or a microcontroller for a job. The good news is that that piece of information allows us to understand how to approach the problem. In other words, there is unlikely to be a bad decision to make, just different decisions.

That’s where we come full circle here. I’m writing this book so that I can understand the practical use of microcontrollers in a better way. I understand the theory of why they have advantages and disadvantages, but I haven’t really used them in a serious way. I recognise that I need to explore their capabilities and learn more about them so that I can make better decisions about where I could better use a computer over a microcontroller. Hopefully if you’re reading this book, you’re on a similar journey.

The picture below shows a Raspberry Pi Pico W microcontroller board on the left and a Raspberry Pi 4B computer on the right. They are shown to scale to illustrate their equivalent size, but that’s pretty much where the ease of comparison ends.

Raspberry Pi Pico W and 4 B to Scale Relative to Each Other

The Raspberry Pi Pico

The raspberry Pi Pico is a microcontroller board initially released by the Raspberry Pi Foundation as the ‘Raspberry Pi Pico’ in 2021. It is a board based around the in-house designed microcontroller chip the RP2040.

The RP2040 Microcontroller Chip

Raspberry Pi Pico Pinout

The RP2040 is the first microcontroller released by the Raspberry Pi Foundation. It was designed to deliver high performance, low power consumption and a wide variety of input / output options to provide beginner and hobbyist users with access to a modern and capable option for microcontroller based circuit boards.

It’s key features are;

  • A Dual ARM Cortex-M0+ running at 133MHz
  • 264kB on-chip SRAM in six independent banks
  • Support for up to 16MB of off-chip Flash memory via dedicated QSPI bus
  • A Direct Memory Access (DMA) controller
  • Fully-connected AMBA High-performance Bus (AHB) crossbar
  • Interpolator and integer divider peripherals
  • On-chip programmable LDO to generate core voltage
  • 2 on-chip Phase Locked Loops (PLLs) to generate USB and core clocks
  • 30 General Purpose Input Output (GPIO) pins, 4 of which can be used as analogue inputs

It includes peripheral interconnects in the form of;

  • 2 Universal Asynchronous Receiver/Transmitters (UARTs)
  • 2 Serial Peripheral Interface (SPI) controllers
  • 2 Inter-Integrated Circuit (I2C) controllers
  • 16 Pulse-width modulation (PWM) channels
  • A USB 1.1 controller with host and device support
  • 8 Programmable Input/Output (PIO) state machines (PIO allows you to create additional hardware interfaces, or even new types of interfaces)

The chip can be purchased separately and has been incorporated into a number of different boards manufactured by organisations such as Arduino, Pimoroni, Adafruit, Sparkfun and Lone Dynamics. But arguably the most obvious board manufacturer is the Raspberry Pi Foundation itself.

The Raspberry Pi Pico W Microcontroller Board

At the end of January 2021, the Raspberry Pi Foundation announced the Raspberry Pi Pico as it’s first foray into the world of microcontrollers. The following year the Pico W was released that added (amongst other things) wireless functionality. The description below and pretty much any examples I describe will be using the Pico W.

The board includes the following features;

  • 21 mm × 51 mm form factor
  • RP2040 microcontroller chip designed by Raspberry Pi in the UK
  • 2MB on-board QSPI flash
  • 2.4GHz 802.11n wireless LAN option
  • Micro USB B port for power and data (and for reprogramming the flash)
  • 26 multifunction GPIO pins, including 3 analogue inputs
  • 2 × UART, 2 × SPI controllers, 2 × I2C controllers, 16 × PWM channels
  • 12-bit 500ksps analogue to digital converter (ADC)
  • 1 × USB 1.1 controller and PHY, with host and device support
  • 8 × Programmable I/O (PIO) state machines for custom peripheral support
  • Supported input power 1.8–5.5V DC and several options for powering the unit from micro USB, external supplies or batteries
  • The castellated module allows soldering direct to carrier boards
  • Drag-and-drop programming using mass storage over USB
  • Low-power sleep and dormant modes
  • Accurate on-chip clock
  • Temperature sensor
  • Accelerated integer and floating-point libraries on-chip

The Pico provides minimum of external circuitry to support the RP2040 chip: flash memory, a crystal, power supplies and decoupling, and USB connector. Four RP2040 I/O are used for internal functions: driving an LED, on-board switch mode power supply (SMPS) power control, and sensing the system voltages. The Pico W has an on-board 2.4GHz wireless interface using 802.11n. The antenna is an onboard antenna formed as a resonant cavity by etching away copper on each layer of the PCB structure. The wireless interface is connected via SPI to the RP2040.

All in all the Raspberry Pi Pico established itself as an immediate realistic option for users of microcontrollers around the World. This in itself is a difficult thing in a dynamic market saturated with options.

Pinout

The Pico W has been designed to make available as much of the RP2040 functionality as possible.

Raspberry Pi Pico W Pinout

Apart from GPIO and ground pins, there are seven other pins on the main 40-pin interface;

  • PIN40 VBUS is the micro-USB input voltage, connected to micro-USB port pin 1. This is nominally 5V.
  • PIN39 VSYS is the main system input voltage, which can vary in the allowed range 1.8V to 5.5V.
  • PIN37 3V3_EN connects to the on-board SMPS enable pin, and is pulled high (to VSYS) via a 100kΩ resistor. To disable the 3.3V (which also powers off the RP2040), short this pin low.
  • PIN36 3V3 is the main 3.3V supply to RP2040 and its I/O, generated by the on-board SMPS. This pin can be used to power external circuitry. It is recommended to keep the load on this pin under 300mA.
  • PIN35 ADC_VREF is the ADC power supply (and reference) voltage, and is generated on Pico W by filtering the 3.3V supply. This pin can be used with an external reference if better ADC performance is required.
  • PIN33 AGND is the ground reference for GPIO26-29. There is a separate analogue ground plane running under these signals and terminating at this pin. If the ADC is not used or ADC performance is not critical, this pin can be connected to digital ground.
  • PIN30 RUN is the RP2040 enable pin, and has an internal (on-chip) pull-up resistor to 3.3V of about ~50kΩ. To reset RP2040, short this pin low.

There is a pdf of the pinout available as an extra when you download the book from Leanpub. I recommend at the least printing out page size copy to have on the bench beside you when working or have it printed to poster size for the wall!

Powering the Pico

There are three main ways we can apply power to the Raspberry Pi Pico. The method used will depend on our application. We can power Raspberry Pi Pico from one of the following;

  • The micro USB connector on the device
  • The VBUS pin (40)
  • The VSYS pin (39).

Powering from the USB connector is by far and away the simplest method, but not always desirable because of limitations of space or supply types.

If we provide a supply to the VBUS pin our Raspberry Pi Pico can take a voltage of between 1.8 and 5.5V, as it has an internal buck-boost regulator (which can regulate the output to a higher or lower voltage than its input). This will internally power VSYS via a Schottky diode, but we must be sure not to connect another power supply to Raspberry Pi Pico’s USB connector at the same time.

The VSYS pin is the main system power supply on Raspberry Pi Pico. From here the Raspberry Pi Pico generates its own 3.3V supply which is used to power RP2040, and also the 3V3 output pin (36). A safe way to add a second power source to Pico W is to feed it into VSYS via another Schottky diode. This will ‘OR’ the two voltages, allowing the higher of either the external voltage (or VBUS) to power VSYS, with the diodes preventing either supply from back-powering the other.

Set up

Setting up our Raspberry Pi Pico for first use is a fairly simple task and I suggest that we should approach it as an exercise in just getting going without too much of an eye to the future.

By that I mean that we should aim to get up and operating with a running program on the Pico. We’ll ignore any plans for connecting peripherals or preparing for installing the device somewhere separate. Our only aim is to get it working and along the way establish how easy it is. We do this so that we can break down any mystique about the process being difficult. This way, if we have a problem, we can work through it with a minimum of complexity.

Our aim therefore is to connect our Raspberry Pi Pico install ‘Thonny’ (which is the programming environment we will use to interact with the Pico) and write a MicroPython program to blink the onboard LED. This is a pretty common example program and should serve to demonstrate that we can get things up and running and from there we can think about more complicated adventures.

Hardware

The hardware requirements are pretty minimal. We will want the following;

  • A Raspberry Pi Pico (I will strongly recommend a Pico W and there’s no need to solder any headers onto the board just yet)
  • A computer that can run the Thonny Integrated Development Environment (IDE). Pretty much all will be able to.
  • A micro USB cable to connect between the Pico and the computer
  • A 5V micro USB power source (optional, but cool if we want to demonstrate the Pico running independently from the computer)

Software

The project will guide you through the installation of:

  • The Thonny Python IDE
  • MicroPython firmware for Raspberry Pi Pico

What is Thonny?

Thonny is a simple Integrated Development Environment (IDE) that is designed to be the logical interface between you (the programmer) and the Pico. This is the application where your can write you code, run it and see the output (and any errors!). IDE’s can be incredibly complex systems that support advanced software development. Thonny is designed for beginners who want to use Python and as such it will more than adequately serve to get us started. It’s also Open Source and as such there are few limitations on getting hold of a copy for use.

Install Thonny

To get hold of the software, go to the official Thonny web site and click on the ‘Download’ button. That will list out the different options that you can choose from depending on the type of computer you are going to be using. Follow the instructions and you will have Thonny installed in a couple of minutes. Open it up.

Thonny Start

The basic Thonny interface as shown provides us with a code editor in the top section, where we will write all of your code. The bottom half is our ‘Shell’, where we will see any output when we run our code.

In the classic manner of programmers everywhere we can test that things are working correctly by writing a ‘Hello World’ program.

Type the following into the code editor;

print("Hello World")

Then press the ‘Run Script’ button (or press F5).

Hello World

In the shell section of Thonny we should see that the program has run and it has printed out the phrase ‘Hello World’! Congratulations! You’re a programmer! Although perhaps we shouldn’t get ahead of ourselves ;-).

To get a feel for how Thonny can help us out, deliberately break your Hello World program by deleting one of the parenthesis. When we press run again, we should be presented with feedback in the shell that there in an error in the code and it should even provide some indication of where in the code it has occurred. Have a bit of a play and see what changes you can make to both break and expand the code.

What we have been doing above is writing Python code and having it run on our desktop. Now we’re now ready to move on to the next step and connect our Raspberry Pi Pico to Thonny and have the code run on the Pico.

MicroPython

What is MicroPython?

MicroPython is a programming language that is an implementation of the core of Python 3 and includes a small subset of the Python standard library. The simplicity of the Python programming language makes it an excellent choice for beginners who are new to programming and hardware. However, in spite of its name, MicroPython is reasonably full-featured and supports most of Python’s syntax so if you’re comfortable with Python you will be in familiar territory.

MicroPython is optimised for microcontrollers and microcomputers. It is a firmware solution designed to run in constrained environments while allowing a small subset of standard libraries into embedded programming.

MicroPython firmware can run in a footprint of 256 Kilobytes and 16 Kilobytes of RAM. The means we can write clean and simple Python code to control hardware instead of having to use complex low-level languages like C.

So let’s get started!

Connect our Pico

Automatically Installing the Firmware

With Thonny running, connect the Pico to the computer via the cable with the micro USB connector.

In Thonny go to Tools > Options and click on the Interpreter tab. From the interpreter dropdown list select MicroPython (Raspberry Pi Pico).

Selecting MicroPython for the Pico

The firmware update dialogue box will open.

Pico Firmware Update

Click on ‘Install’ and once complete we should see the notification in the lower right hand side of the Thonny application indicating that we are running MicroPython on the Raspberry Pi Pico.

MicroPython on the Pico

Manually Installing the Firmware

Because the Pico W is quite new at time of writing (2022-09-03), we need to be using the latest unstable version of the firmware for it to operate to it’s full potential (at least for the moment).

To load that firmware, download the latest firmware from here.

Then, with the Pico W disconnected from the Pi, press the BOOTSEL button (on the Pico) and plug in the Pico while holding the button down.

Pico BOOTSEL Button

Then release the BOOTSEL button. This will make the Pico act like a mass storage device.

Pico Connected to the Raspberry Pi

Copy the unstable firmware onto the Pico (just drag it and drop it). Wait for a moment and it will install itself. Once completed, we should see a very modern version of the firmware noted in the Thonny Shell.

Updating Firmware

Because the firmware for the Pico will improve over time, it’s generally a good thing to have it’s firmware updated to the most recent version.

To do this, on the Thonny menu go to Tools >> Options and then select the ‘Interpreter’ tab

Pico Connected to the Raspberry Pi

Assuming that we have the correct device selected, select the ‘Install or update firmware’ link.

The firmware update dialogue box will open.

Pico Firmware Update

Follow the instructions to plug in the Pico while holding the BOOTSEL button. Once the device information appears (or at the least, the ‘Install’ button isn’t greyed out), click on ‘Install’.

The firmware should be automatically copied from MicroPython.org and installed. I have had an error occur (‘socket.timeout’) in the past, but I just simply clicked on ‘Install’ again and it proceeded without problem.

Close the Options dialog box and press the ‘Stop / Reset’ button on Thonny and we should see our new version of MicroPython displayed at the bottom of the Shell.

Use the Shell

Now we have our Pico connected to our computer and the MicroPython (Raspberry Pi Pico) interpreter in use on Thonny.

This means we can type commands directly into the Shell and have them run on our Pico.

Now we are going to get a little more practical :-).

MicroPython uses hardware-specific modules, such as one called machine, that we can use to program our Pico.

We can create a machine.Pin object to correspond with the on-board LED, which, on the Pico W can be accessed using the reference LED in code.

If you set the value of the LED to 1, it turns on.

Enter the following code in the Thonny editor pane, making sure that we press ‘Enter’ after each line.

from machine import Pin
led = Pin('LED', Pin.OUT)
led.value(1)

If we then press the ‘Run’ icon, a dialog box will come up asking where we want to save our code. This time we’re going to save it to the Pico.

Pico Firmware Update

Give the code an appropriate name like led.py and save it. It’s important that we use the file extension ‘py’ as this is what will help the Pico determine how to operate the file.

We should now see the on-board LED light up! Our code has had an effect on the physical world!!!

Edit the code to set led.value to 0 and press the run icon again’ This should turn the LED off.

Turn the LED on and off as many times as you like. Go on. You deserve it :-).

But really… That’s a pretty manual process right? Time to automate!

It’s time to write a MicroPython program to blink the on-board LED on and off.

Click in the main editor pane of Thonny.

Enter the following code to toggle the LED.

from machine import Pin
import time

led = Pin('LED', Pin.OUT)

while (True):
    led.toggle()
    time.sleep(.2)

Click the Run button to run/save your code. Again, save onto the Pico and a file name like blink.py seems appropriate

We should see the on-board LED turn on and off until we click the Stop button.

Now we’re really starting to cook. But we can do better! Let’s make the led start blinking automatically whenever the Pico is powered on.

Automatically run your program

If you want to run your Raspberry Pi Pico without it being attached to a computer, you need to use a power supply that will conform to the details we laid out earlier for connecting to power. By far and away the easiest method is to simply use a USB power plug.

To automatically run a MicroPython program, all we need to do is save it to the device with the name main.py. Whenever the Pico is powered up, if it sees a file named ‘main.py’ it will automatically start it up.

With our blink.py program in Thonny, go to File >> Save as… Select the Raspberry Pi Pico as the location to save to and name our file main.py.

You can now disconnect our Raspberry Pi Pico from your computer and use a micro USB cable to connect it to a mobile power source, such as a battery pack or a wall-wart.

Once connected, the main.py file should run automatically and our LED will blink!

This is a pretty cool moment because it puts together a bunch of different capabilities that open up a world of new possibilities.

We now know how to program our Raspberry Pi Pico using a language that will allow us to interact with peripherals (all be it an on-board one) and to have that program automatically start whenever our Pico is plugged in.

I think that we’re ready to move on to some tips and tricks :-).

Connectivity

Arguably one of the most important features of a microcontroller is it’s ability to interface with systems outside of itself. Whether that be via a direct connection to the GPIO pins and their many Input / Output (IO) options, via WiFi or even the USB interface. There are a myriad of potential pathways for communication to sensors, other IT devices or directly to us mere humans.

Connecting using Dupont Connectors

Event if you don’t recognise the name, if you’ve played around inside a computer there is a better than even chance that you’ve come across a Dupont connector.

They’re those small black plastic plugs that are used to connect things like the leds or USB connectors to your computers motherboard. They come in a range of different configurations and they are possibly one of the most underused mechanisms available for making ad-hoc connections between your Raspberry Pi Pico and external sensors or small boards. In fact, they can be used with a wide range of different areas and are limited only by the presence of a suitable connection point.

What are Dupont Connectors?

Technically there’s no actual industry term that calls out Dupont connectors. The style people commonly refer to as ‘Dupont’ is a variation of a black, low profile rectangular form with a 2.54mm standard pitch.

Dupont Connectors in the Wild

Off course the Dupont connector is just one half of a connector pair. The most common mating platform for them is to a header pin. A header pin (or simply a header) is a form of electrical connector. A male pin header consists of one or more rows of metal pins molded into a plastic base, 2.54 mm (0.1 in) apart.

Header Pins

These can be straight, angled, single-in-line, dual-in-line and a myriad of other options.

The Dupont connector slips directly onto a header pin and because they share the same pitch (distance apart of the pins) of 2.54mm they can be similarly ganged together in a myriad of ways.

Female Pin Enclosures

By far and away the simplest method of utilising this method of connectivity is to purchase bulk lots of the pre-made connectors. These can be male or female and commonly come joined to what is called ‘Rainbow Cable’.

Pre-made Dupont Connectors

These are incredibly cheap and unless you have a very specific length that is required for a project, they are so easy to use they will quickly become ubiquitous for your project work.

Re-using Connectors

One of the cool things about Dupont connectors is that they can be adjusted by slipping the internal metal connectors out of their casings and placed into new casings. So if you have a set of cables in a three way connector, but the header that you want to connect to doesn’t have the connection points directly beside each other, not problem. Just use a small, flat bladed jewellers screwdriver or similar to gently bend up the plastic flap that is keeping the connector shroud in place. You can then slip the internal wire and connector out of the black plastic housing and place it into three separate single housings. Easy peasy.

Crimping Your Own Dupont Connectors

This is totally do-able and you will find all the materials to carry out the task online. However, as I mentioned earlier, unless you have a specific use for it, it’s probably just easier to utilise the pre-made versions.

However, if you have a need to construct your own connectors, the most important thing to know is that it’s a good idea to practice a few times before doing it for real. It isn’t too hard, but it’s worth having a few tries to get the feel of it. I’m not going to describe the method for constructing your own pins since there are a wealth of different methods and the written word can’t compare to a YouTube video of it being done and that’s not really my bag (maybe one day). I can advise that while there are plenty of tools for doing the job, with a little bit of practice you can get by with a sharp knife / scalpel for stripping the wires (for crying out loud be careful) and a par of needle nosed pliers. Certainly if you’re doing connectors on a regular basis or in an area where there needs to be a high standard of consistency and finish, proper tooling will be essential. But if you’re a hobbyist then why not?

Connectivity via WiFi

The Raspberry Pi Pico W includes an on-board 2.4GHz wireless interface which has the following features:

  • WiFi 4 (802.11n), Single-band (2.4 GHz)
  • WiFi Protected Access (WPA) 3
  • Software enabled Access Point (SoftAP) which supports up to 4 clients

The antenna is a tuned cavity design which is licensed from ABRACON (formerly ProAnt). The wireless interface is connected via a Serial Peripheral Interface (SPI) to the RP2040 microcontroller.

It’s possible to use a standard Pico connected to an ESP8266 or similar to enable WiFi connectivity, but in enabling this I found there was more heartache than I cared to endure. With the release of the Pico W with WiFi built in, this should be the go-to option for connecting to a WiFi network if you’re using a Pico.

Using the network and socket modules

The network module includes functionality in the form of network drivers and routing configuration which is specific to the MicroPython. Drivers for the Pico W hardware is available within this module and it can be used to configure the network interface. Network services are then available for use via the socket module.

Scan for wireless networks

As an example of how the network module provides access consider the following code;

import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
print(wlan.scan()) 

When run on a Raspberry Pi Pico W it will enable the network interface, scan for wireless networks and print them out

The import network line imports the network module.

wlan = network.WLAN(network.STA_IF) creates a WLAN network interface object with a client interface type (as opposed to an access point type, which would use network.AP_IF) .

We then activate the network interface with wlan.active(True).

Lastly we print out the results of a scan of available wireless networks with print(wlan.scan()).

The type of output that we would see might look something like the following;

[(b'outside', b"\xb8'\xeb\x81\xb9m", 1, -76, 3, 5), (b'inside', b'l\xb0\xcel\xc0\\
xf4', 6, -37, 5, 9), (b'highway', b'\xdc\xa62*M[', 11, -31, 5, 6)]

Here there are three access points returned with the information apparently separated as ssid, bssid, channel, RSSI, security and hidden. Although, I’ll be honest and say that I know some of those networks and some of those ‘security’ and ‘hidden’ values don’t appear to be correct. More research could be required here.

bssid’ is the hardware address of an access point, in binary form, returned as a bytes object. We could use binascii.hexlify() to convert it to ASCII form if we got excited (we will do this in a future project collecting data from temperature sensors).

There are five values for security:

  • 0 – open
  • 1 – WEP
  • 2 – WPA-PSK
  • 3 – WPA2-PSK
  • 4 – WPA/WPA2-PSK

and two for whether or not the ssid is hidden:

  • 0 – visible
  • 1 – hidden

Serve a web page

Serving up a web page is well within the Pico W’s capabilities. To do this we will need to active the network interface, connect to a wireless network and then create an http server with a socket connection and then listen for connections and serve up an HTML page.

This is definitely a more complicated process and we are going to make it slightly more so by using two external files to our main.py file. Don’t worry, there’s good reasons for doing so.

Firstly, create a file called secrets.py on the Pico with contents as follows;

secrets = {
    'ssid': 'Replace-with-WiFi-ssid',
    'pw': 'Replace-with-WiFi-Password'
    } 

Edit the file and put the name of the network (ssid) that you’re going to connect to and it’s password in the appropriate places. We are doing this so that when we write our main code, we don’t have to expose things that we would rather not when and if we share our main code.

Next create a file with the contents below and save it as index.html. This will be the web page that we will go to when connecting to the Pico W via the network.

<!DOCTYPE html>
<html>
    <head>
        <title>Pico W</title>
    </head>
    <body>
        <h1>Pico W</h1>
        <p>This is a very simple web page.</p>
        <p>REALLY simple.</p>
    </body>
</html>

Lastly, create the following file on the Pico and call it main.py. This will allow it to automatically start when the Pico is connected to power.

import rp2
import network
import machine
import time
import socket
from secrets import secrets

# Set country to avoid possible errors
rp2.country('NZ')

wlan = network.WLAN(network.STA_IF)
wlan.active(True)

# Load login data from different file for security!
ssid = secrets['ssid']
pw = secrets['pw']

wlan.connect(ssid, pw)

# Wait for connection with 10 second timeout
timeout = 10
while timeout > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    timeout -= 1
    print('Waiting for connection...')
    time.sleep(1)

wlan_status = wlan.status()

if wlan_status != 3:
    raise RuntimeError('Wi-Fi connection failed')
else:
    print('Connected')
    status = wlan.ifconfig()
    print('ip = ' + status[0])
    
# Function to load in html page    
def get_html(html_name):
    with open(html_name, 'r') as file:
        html = file.read()
    return html

# HTTP server with socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]

s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(1)

print('Listening on', addr)

# Listen for connections
while True:
    cl, addr = s.accept()
    print('Client connected from', addr)
    r = cl.recv(1024)
       
    response = get_html('index.html')
    cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
    cl.send(response)
    cl.close()

Now run the main.py program. We should see feedback from the shell showing us the connection process and it should include the ip address of the Pico.

Connected
ip = 10.1.1.41
Listening on ('0.0.0.0', 80)

Put the IP address that appears in the shell into a browser that’s on our network and we should see a page giving us the happy news that we have created a web page!

Thonny Hello World

At the same time we should see an indication in the Shell of the connection requests coming in each time we refresh the page.

Connected
ip = 10.1.1.41
Listening on ('0.0.0.0', 80)
Client connected from ('10.1.1.99', 57142)
Client connected from ('10.1.1.99', 57144)
Client connected from ('10.1.1.99', 57145)

There we go. We have just turned our Pico W into a web server! Not only that, but because we have named our program that does it main.py it will operate as soon as we plug in power.

Setting up a static IP address

Enabling network access to the Pico is a really useful thing. This has allowed us to access our device from a separate computer. But when we did it we relied on knowing what the IP address of the Pico was in order to enter that into our browser. The allocation of the address is dynamic and will be dependant on the configuration of our wireless network that is set up by our router. However, we can set up that address so that we know what it is going to be before hand. This is what is called a static IP address.

An Internet Protocol address (IP address) is a numerical label assigned to each device (e.g., computer, printer) participating in a computer network that uses the Internet Protocol for communication.

This description of setting up a static IP address makes the assumption that we have a device running on our network that is assigning IP addresses as required. This sounds complicated, but in fact it is a very common service to be running on even a small home network and most likely on an ADSL modem/router or similar. This function is run as a service called DHCP (Dynamic Host Configuration Protocol). You will need to have access to this device for the purposes of knowing what the allowable ranges are for a static IP address.

The Netmask

A common feature for home modems and routers that run DHCP devices is to allow the user to set up the range of allowable network addresses that can exist on the network. At a higher level we should be able to set a ‘netmask’ which will do the job for us. A netmask looks similar to an IP address, but it allows you to specify the range of addresses for ‘hosts’ (in our case computers) that can be connected to the network.

A very common netmask is 255.255.255.0 which means that the network in question can have any one of the combinations where the final number in the IP address varies. In other words with a netmask of 255.255.255.0, the IP addresses available for devices on the network ‘10.1.1.x’ range from 10.1.1.0 to 10.1.1.255 or in other words any one of 256 unique addresses.

Distinguish Dynamic from Static

The other service that our DHCP server will allow is the setting of a range of addresses that can be assigned dynamically. In other words we will be able to declare that the range from 10.1.1.20 to 10.1.1.255 can be dynamically assigned which leaves 10.1.1.0 to 10.1.1.19 which can be set as static addresses.

Because there are a huge range of different DHCP servers being run on different home networks, I will have to leave you with those descriptions and the advice to consult your devices manual to help you find an IP address that can be assigned as a static address. Make sure that the assigned number has not already been taken by another device. In a perfect world we would hold a list of any devices which have static addresses so that our Pico’s address does not clash with any other device.

For the sake of this exercise we will assume that the address 10.1.1.110 is available.

Default Gateway

We will also need to find out what the default gateway is for our network. A default gateway is an IP address that a device (typically a router) will use when it is asked to go to an address that it doesn’t immediately recognise. This would most commonly occur when a computer on a home network wants to contact a computer on the Internet. The default gateway is therefore typically the address of the modem / router on your home network.

We can check to find out what our default gateway is from Windows by going to the command prompt (Start > Accessories > Command Prompt) and typing;

This should present a range of information including a section that looks a little like the following;

Ethernet adapter Local Area Connection:

  IPv4 Address. . . . . . . . . . . : 10.1.1.15
  Subnet Mask . . . . . . . . . . . : 255.255.255.0
  Default Gateway . . . . . . . . . : 10.1.1.1

The default router gateway is therefore ‘10.1.1.1’.

With all that information we are now ready to configure our static IP address.

Configure the static IP

Our network module and our WLAN class include an ifconfig method that uses all our gathered information’. We will need to declare it in the format wlan.ifconfig([(ip, subnet, gateway, dns)]).

When called with no arguments, this method returns a 4-tuple with the above information. To set the above values, pass a 4-tuple with the required information. For example:

wlan.ifconfig(('10.1.1.110', '255.255.255.0', '10.1.1.1', '8.8.8.8'))

Where 8.8.8.8 is a suitable DNS server.

To include it in the example of server our web server, we would slot it into the section where we are configuring the wlan settings;

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)
wlan.ifconfig(('10.1.1.110','255.255.255.0','10.1.1.1','8.8.8.8'))

Give it a try!

General Purpose Input / Output (GPIO)

The Raspberry Pi Pico has 26 multi-function General Purpose Input / Output (GPIO) pins available for connecting to external devices. 23 of them are digital only pins and three are also capable of acting as software selectable Analogue to Digital Converters (ADC). The voltage for the digital output is made available from the 3.3V rail.

General Purpose Input / Output

The observant reader will notice that the numbers above seem a little out of whack with the numbered pins on the Pico pinout. Here they go from zero to 28, but numbers 23, 24 and 25 are missing. Hence 26 total are exposed to the header pins.

The three missing pins (23, 24 and 25) in the sequence along with GPIO29 are used for internal board functions;

On the original Pico those functions are;

  • GPIO23 OP wireless power on signal
  • GPIO24 OP/IP wireless SPI data/IRQ
  • GPIO25 OP wireless SPI CS when high also enables GPIO29 ADC pin to read VSYS
  • GPIO29 OP/IP wireless SPI CLK/ADC mode (ADC3) to measure VSYS/3

And on the Pico W the functions are;

  • GPIO23 OP Controls the on-board SMPS Power Save pin
  • GPIO24 IP VBUS sense high if VBUS is present, else low
  • GPIO25 OP Connected to user LED
  • GPIO29 IP Used in ADC mode (ADC3) to measure VSYS/3

The exposed GPIO pins can be utilised as inputs or outputs for a range of different protocols and functions. These are in turn configured and enabled in software and can include;

  • Serial Peripheral Interface (SPI): is a synchronous serial communication interface specification used for short-distance communication.
  • Universal Asynchronous Receiver-Transmitter (UART): is a function for asynchronous serial communication where the data format and transmission speeds are configurable.
  • Inter-Integrated Circuit (I2C): is a commonly used 2-wire interface that can be used to connect devices for low speed data transfer using clock and data wires.
  • Pulse width modulation (PWM): is a method of reducing the average power delivered by an electrical signal, by effectively chopping it up into discrete parts.
  • Analogue to Digital Converter (ADC): takes an analogue signal, such as a variable voltage level, into a digital signal.

Pull-up and Pull-down Resistors

When setting up a GPIO pin as an input, it is important to provide a stable state at the pin to enable a reliable reading. It’s easy to simply think that this has to occur when an important event occurs like sensing a high level from a switch or when triggering from a pulse, but the reality is that a GPIO pin is looking for a reading of one of two states. We can call them a range of different things like;

  • High and low
  • On and off
  • 3.3V and ground
  • 1 and 0

Whatever we call them the important fact is that the signal level can be distinguished between two different states.

This means that when we want to register when a high voltage occurs on a pin, first we need to know what the low voltage is. And visa versa. If we are looking to try and read when a pin drops to ground, first the pin needs to be able to recognise the 3.3V is the high point.

We accomplish this feat of knowing our reference points by using pull-up and pull-down resistors on our Pico.

What does pull-up and pull-down actually mean?

The answer to the question is kind of in the name, but that doesn’t necessarily make it obvious. It’s also really useful to frame the question by using an example, and in our case (since this is being written for a book about the Raspberry Pi Pico) we can use a GPIO pin on the Pico as our case study.

We like to talk about our ability to read either a high or low signal on our GPIO inputs, but the reality is that there are three states that our voltage measuring effort could result in. A low state that will typically be ground or 0V, a high state that will typically be about 3.3V, and a mysterious third state which is ‘floating’.

We can illustrate this by attempting to measure the voltages on our Pico.

The low state

Using a meter to measure the voltage on our Pico, we can first ensure that there is a common reference point set by connecting our negative probe to a ground pin (here pin 23) and we can then measure the amount of voltage or potential difference is present between that ground pin and another (pin 33 shown here).

Reading Ground

Unsurprisingly, we should read 0V. This is because the two ground pins are connected together on the circuit board and represent exactly the same voltage. Therefore the difference between them is 0V.

The high state

With our voltmeter measuring the difference between our common reference point of ground and the 3V3(OUT) pin (36), we are naturally going to read a voltage of 3.3V.

Reading 3.3V

Again unsurprisingly we see 3.3V because there is a potential difference between the ground pin (23) and the 3V3(OUT) pin (36) of 3.3V

The floating state

And now to our ‘floating’ state. with our red lead removed from our Pico and floating in mid air, we have an uncertain reading on our voltmeter

Reading Nothing At All

This situation is a bit like Schroedinger’s cat in the quantum state analogy. We can’t confirm if it’s alive or dead, so it’s both simultaneously. Except in this case, the reading is uncertain and we cannot state what it will be since there the meter is not fully connected to the circuit.

This floating state is the initial configuration of our GPIO pins on the Raspberry Pi Pico. Each one of them is essentially disconnected and as a result we can’t expect to read a steady voltage off them.

Pulling our pin

So to set our GPIO pins to a state where they have a reliable reference voltage we need to ‘pull’ the voltage either up or down so that in a resting state they read high or low.

A pull-up or pull-down resistor is connected so that the GPIO pin is connected to either ground or 3.3V via a resistance.

Pull-down, Pull-up and Floating connections

In some circuits this might be necessary to implement using discrete components, but the RP2040 microcontroller can configure a GPIO pin as either pull-up or pull-down internally and we just need to instruct it to do so via software. The resistance value in the pico is specified as being between a minimum of 50kΩ and a maximum of 80kΩ.

For example, in the PIR section of this book we set up our GPIO pin as an input and then we configured the pin as pull-down via the following code;

from machine import Pin

pir = Pin(22, Pin.IN, Pin.PULL_DOWN)

We can test our thinking by considering reading the voltage at our nominal GPIO pin with the Pin.PULL_DOWN configuration set. That will read 0V.

Reading Ground

Conversely with the GPIO pin set to Pin.PULL_UP we will have the following circuit where we will read a voltage of 3.3V.

Reading Ground

Once we have set the GPIO pin to its default state of high or low we can then go about the job of varying that pin to the alternate state via whatever input we choose. For example via a PIR or a common switch.

Reed Switches with the Raspberry Pi Pico

Reading the state of a switch is a pretty basic function for a microcontroller, but it’s an action that is worth understanding and we can add a little bit of spice to it by using a switch that is commonly used in security systems and to detect the state of items that need to determine whether they are open or closed (fridges, laptops, washing machines).

What is a Reed Switch?

Reed switches are type of switch that are actuated via the presence of a magnetic field. They are typically constructed of two thin, flexible, ferromagnetic metal wires or blades (these are the reason that the switch is called a ‘reed’ switch). The blades are positioned slightly apart in a sealed glass tube. When it is placed in close enough proximity to a magnet that has enough strength to trigger the required level of actuation, the metal reed bends and the switch is made.

While this is a pretty cool type of switch, it’s function is the same as a wide range of other switch types from a standard push-button to a lever type to simply pressing two wires together. Basically it’s a way of connecting an electrical circuit.

The Magnetic Reed Switch

Our reed switch comes as the switch proper which is encased in a plastic mount with flying leads coming off it. In the picture that follows I have crimped two Dupont connectors onto the end of the leads for ease of connecting to the Pico. There is also a separate magnetic activator which, when moved close to the switch will enable it. I.e. we can imagine the portion with the leads mounted on a window frame and the magnetic activator portion mounted on the window proper.

Magnetic proximity reed switch

How do we read a switch?

A switch is one of the simplest sensors to read. Once the input is configured in software, the two leads of the switch can be connected to the appropriate pins and we’re good to go. When the switch is made (or un-made) the state of the input changes and the Pico can read the input.

Pull-up or pull-down?

The main driver for the connection type is understanding what the switch is going to be switching. This might be dictated if you’re using a particular type of sensor, but for standard switch, we have two choices depending on whether we want to connect our switch to the 3.3V connection or the ground (the other end of the switch will be going to the GPIO pin).

Pull-up or Pull-down connections

For a deeper explanation of how pull-up and pull-down resistors are selected and implemented in a circuit or a Pico, check out the section on pull-up and pull-down setting of GPIO pins earlier in the book.

If we’re connecting a simple switch, either method is as good as the other. However, I would normally opt for the pull-up option on the basis that it is easier to find a spare ground connector than it is to try and connect to the sole 3.3V pin.

With that in mind, the example that follows will configure our GPIO pin with a pull-up resistor and we will connect our reed switch between a ground pin (we’ll go for pin 23) and GPIO 22 (pin 29).

Connecting up the switch to the Pico

Because we are opting to use an internal pull-up resistor, we will connect our switch between a ground pin (pin 23 in this case) and GPIO22 (pin 29).

Switch connected to Pico

It makes no difference which way round the leads are connected.

Code

The code below will designate the GPIO pin to be used as our input (GPIO22) and set it to a default high state with a pull up resistor.

switch = Pin(22, Pin.IN, Pin.PULL_UP)

We will also need to set the GPIO pin to be an input (seen above as Pin.IN). Once set to input, the GPIO line is high impedance so it won’t draw very much current, no matter what we connect it to.

Our method to read the state of the switch is via the switch.value() parameter. Normally our GPIO pin will be high since our switch is a ‘normally open’ switch, but once the magnet is moved close to the main body of the reed switch, the mechanism closes, the switch is made and the GPIO pin is then connected to ground and reads 0V.

Just to make things interesting, it increments a counter and loops repeatedly incrementing when the switch is closed.

import time
from machine import Pin

switch = Pin(22, Pin.IN, Pin.PULL_UP)
count = 0

time.sleep(1)
print('Ready to switch!')

while True:
     if switch.value() != 1:
          count = count + 1
          print('Switch closed ', count)
          time.sleep(4)
     time.sleep(1)

Controlling a Servo from the Raspberry Pi Pico

What is a Servo Motor?

Servo motors are types of motors that have been designed to rotate precisely in response to control signals. They are commonly used in applications such as robotics and remote control vehicles to provide a specific angle or distance of movement.

SG90 Micro Servo Motor

They are rated in kg/cm (kilogram per centimetre) which translates as how much weight the servo motor can lift at a particular distance. For example: A 5kg/cm servo motor should be able to lift 5kg when its load is suspended 1cm from the motors shaft, the greater the distance of the weight from the shaft, the less the weight lifting capacity.

How does a Servo Motor Work?

A servo motor can use an AC or DC motor as its driving force and this is one of the many different methods of describing them. In a very simple example, a DC servo motor will employ a simple DC motor, gears, a potentiometer and a control circuit. In response to an input signal on the control circuit, the motor and the gears rotate and the potentiometer’s resistance changes. This provides feedback to the control circuit which regulates how much movement there is and in which direction.

Servo Motor Operation

How is a Servo Motor Controlled?

A typical servo motor will have three wires. Two for power supply (positive and ground) and one for the controlling signal.

The controlling signal is a pulse of variable width, and thus the controlling mechanism is named Pulse Width Modulation (PWM). A pulse is sent every 20 milliseconds. The width of the pulses determine the position of the shaft. So, a pulse width of 1ms will move the shaft anticlockwise by 90°, a pulse of 1.5ms will move the shaft to a ‘neutral’ position at 0° and a pulse of 2ms will move the shaft clockwise by +90°.

Pule Width Modulation Signals

A typical servo motor will only turn 90° in either direction and will have a mechanical stop to prevent further movement.

When a servos is commanded to move, it will move to the position specified by the pulse width and hold that position. The maximum amount of force the servo can exert is called the torque rating of the servo. As an example of this force, when we are running our code below and the servo motor is connected and powered on, try (gently) to move the arm. There should be a reasonable resistance. If you disconnect the power the servo can be moved relatively easily.

Connecting Everything Up to the Pico

The example shown here uses a SG90 micro servo. It is a very small and inexpensive servo that can rotate approximately 180 degrees (90 in each direction). These are immensely popular for simple jobs or learning about the principles of servos. It typically comes with 3 arms and fixing hardware. Its operating voltage is from 4.8V to 6V and at 4.8V it has a torque of 2.0kg/cm and will move through 60° in 0.1s.

We will connect;

  • The red wire (VCC) to the VBUS pin (40) on the Pico (this makes the assumption that we are powering our Pico from a source connected to the micro USB connector with the standard 5V applied)
  • The brown wire (Ground) to the ground pin (38) on the Pico
  • The orange wire (the PWM signal) to GP28 (pin 34) on the Pico

All of the GP pins on the Pico can be used for pulse width modulation control. This is because the RP2040 has 8 identical PWM ‘slices’, each with two output channels (A/B), where the B pin can also be used as an input for frequency / duty cycle measurement. This means that each slice can drive two PWM output signals, or measure the frequency / duty cycle of an input signal. This provides a total of up to 16 controllable PWM outputs.

Since all of the GP pins can be driven by the PWM block, it’s just a matter of selecting which one you would like to use.

I selected GP28 (pin 34) for no better reason than it made drawing the connection diagram prettier!

Connecting the Pico to the Servo

While the SG90 comes with it’s wires terminated in a 3 way du-pont connector, in order to make the connection to our header pins simple, replace the 3 way connector with three single pins.

Connecting the Pico to the Servo - IRL

Code

The code below will designate the GP pin to be used as an output (GP28) and the frequency of the signal (50Hz (which equates to a period of 20ms)).

The aim is to sweep the servo through an arc of 180 degrees and then back again.

We then use two while loops to move the servo from a pulse width of 0.52ms to 2.6ms and then back again in an endless loop. Now, I’ll be the first to admit that this does not equate with the expected values of 1ms and 2ms. I have selected the values in the code, because that’s what roughly equates to the -90 and +90 degrees of movement. Why is it not nore accurate? Good question. I tried a few iterations including using duty_u16 (from 3277 to 6554) instead of duty_ns, but it still didn’t seem to work accurately. I suppose my takeaway is that the servo is particularly cheap and maybe I got what I paid for. Irrespective, it was possible to manually calibrate the servo to discover appropriate values.

import time
from machine import Pin, PWM

pwm = PWM(Pin(28))
pwm.freq(50)

while True:
    for pulse in range(520,2600,10):
        pwm.duty_ns(pulse*1000)
        time.sleep_ms(5)
    for pulse in range(2600,520,-10):
        pwm.duty_ns(pulse*1000)
        time.sleep_ms(5)

Warning

I initially operated the servo in it’s sweeping motion while my cat was nearby. She quickly took an interest and had a bit of a play with it.

Servo cat strikes!

Later I heard a strange noise from the office and found her up on the desk investigating it some more. I have since found that I can operate it and she will quickly come and investigate what’s happening. In short, she’s obsessed. I now have to keep the office door shut.

So fair warning.

Controlling a Motor with the Raspberry Pi Pico

Being able to control a motor takes the principles of cross-over from a computing world to the physical world into a different dimension. Almost literally, because it provides the mechanism to induce movement and effect into the environment. Little wonder then at the excitement of building a robot or driving a tracked vehicle since it represents a direct engagement into the way that we (as humans) also interact with the world.

What are the principles of motor control?

A DC motor converts electrical energy into mechanical energy (and heat). It works on the principle that when a current carrying conductor is placed in a magnetic field, it experiences a mechanical force. It does this because the conductor generates a magnetic field of its own and the interaction of the two magnetic fields creates the force (this can be quantified by Flemings left hand rule). There are many different mechanisms and classifications associated with motor design, but those basic principles of current flow and interacting magnetic fields are the way that the motion is produced.

The two most common methods of controlling a DC motor are via varying an applied voltage level or by pulsing the voltage for varying lengths of time (Pulse Width Modulation (PWM)). PWM control is the most popular method because of the increased efficiency and easy of control over the motor. In effect PWM varies the motor speed by simulating a variation in supply voltage. These periodic pulses, when combined with a smoothing effect, makes the motor act as if it is being powered by an adjustable voltage.

The other common piece of the motor control puzzle is the use of what is called an ‘H Bridge’. This is a circuit comprised of four switches controlled in pairs. When either of these pairs are closed, they complete the circuit and subsequently power the motor with current running a different direction. This allows for direction control of the motor.

Don’t be misled by the overly simple explanation above of the motor control. It is a complicated topic that could be the subject of a lifetimes review and research, but feel happy with the concept of a varying electric current creating a magnetic field that in turn (see what I did there) interacts with another magnetic field to produce rotation.

It’s also worth mentioning that most of the above will be completely obscured from us as we can simply apply an appropriate signal to produce the required effect. Nonetheless, it’s useful to understand the very basics of what is happening and why.

How will we implement it?

Motor

Any list of requirements should start with the thing that will drive subsequent selection criteria. In this case we should start with the motor. We will use what are commonly called ‘TT Motors’. These are a simple combination of motor and gearbox in a plastic housing. They may or may not come with wires attached, and possibly a wheel(!) so select as appropriate. We will start with a specification for using two, since ultimately it would be good to use our project to build a device with the ability to drive two independent motors for direction control of a vehicle or similar.

TT Motor

These motors can be operated with anywhere from 3v to 12V although the recommended range is from 3V to 9V. With 6V applied they will rotate at approximately 200rpm drawing 200mA. This is all assuming a ‘no-load’ condition where the motor is just turning it’s own shaft and not being put under strain (like driving a vehicle). When put under a load that resists the turning force of the motor it will draw up to 1.5A (at 6V) when forced to to a stall (stop).

Power

Running a motor takes a reasonable amount of current. Our Raspberry Pi Pico is not designed to pass significant amounts of current through it. The possible sources of electrical power from a Pico would be via either the VBUS (40) or 3V3 (36) pins.

  • VBUS represent the micro-USB input voltage, connected to micro-USB port pin 1. This is nominally 5V, and the amount of current will be limited to the connected supply. While this might technically be a possible source, for power for a motor, it would only be suitable in situations where you were super careful about the type of motor and the USB power supply used. I wouldn’t do it and I don’t recommend it.
  • The other option is from the 3V3 pin which is also the main 3.3V supply to RP2040. This pin can be used to power external circuitry, but the maximum output current that it can supply should be kept under 300mA. This is not realistically enough to power even a tiny DC motor.

In short, we won’t be taking the power supply for our motor from our Raspberry Pi Pico.

The sensible source for our power will be from an external battery or dedicated DC power supply. For the sake of simplicity we will use a simple 4 AA cell battery pack. This will be able to supply a voltage close to our 6V nominal identified for the motors. It should be able to supply over 1A, although running it under that much load for an extended period will reduce the voltage quickly)

4 AA Battery Pack

If we were making this a stand-alone project (for a robot or vehicle), we would probably use this as the source for the power for the Pico as well (with a suitable regulator). Likewise, if this was for a project that was intending to be used a lot, we should consider some form of rechargeable option.

Controller / Driver

So from our requirements gathering process above we want to have a motor driver / controller that can supply two motors with a peak output of 1.5A per motor (just in case), it should be able to accept 6V input and in an ideal world it would act as a supply for our Pico with 5V out.

With these requirements in mind I have selected the Maker-Drive board from Cytron. It meets our requirements nicely and is very reasonably priced.

Maker Drive Board

Types of Motion

There are two objectives that we will want to achieve with our motor control. The first will be simple movement forwards, backwards and stopped. The second will be to adjust the speed of any movement.

Simple movement

The simplest form of control is making our motor turn clockwise, anticlockwise or to have it stop.

Nost DC motor controllers will use two control signals to accomplish this. Either control signal can be high or low and therefore, any combination of the two will provide us with with four different signal combinations.

Simple motor control

As we can see form the table above, whenever any of the two signals are both high or both low the motor will be stopped and whenever the signals are different it will be rotating.

Variable Speed

To vary the speed of our motor we need to apply Pulse Width Modulation (PWM) to our control signals. When the motor is turning in the simple example above, one of the control signals is set low and the other high. To vary the speed we can ‘pulse’ (modulate) the high pin off and on very quickly so that when combined with a smoothing effect, the voltage will appear reduced to the motor. The way that we will control this voltage is by varying the ‘duty cycle’ of the pulses.

The duty cycle of a signal is commonly expressed at the ratio of the time that the signal is high compared to the total period of one cycle. Thus a 70% duty cycle means the signal is on 70% of the time but off 30% of the time.

Duty Cycle

In MicroPython (as we will see in the code that follows) the duty cycle can be set as a parameter called duty_u16 where it varies between 0 (0% duty cycle) to 65535 (100% duty cycle).

Connecting Up the motor controller and battery

Connecting up our various components is as much about following a logical approach as it is about keeping everything simple.

The battery pack has its positive and negative connections connected to the VB+ and VB- pins of the Maker Drive board

The motor is connected to the M1A and M1B screw in connections on the Maker Drive. This will send the voltage out to the motor. It doesn’t matter which way round you connect the wires to the motor as it will be difficult to determine which direction the motor will turn before you test it. The rule of thumb here is that we will connect up our motor and test it and then change the connection if it is turning in the wrong direction.

The Pico has two PWM connection enabled pins connected (every GPIO pin can be configured as a PWM output) to the Maker Drive M1A and M1B header pins. In the case of the diagram below we are using GPIO pins 4 and 5 (physical pins 6 and 7). We also need to connect up a ground connector between the Maker Drive and the Pico.

Motor Connection

The connection above is assuming that we still have our Pico connected via USB to our computer for programming and testing. If we get to a point where we want to operate the Pico and the entire ensemble independently, we can also connect the Maker Drive 5VO pin to the VBUS pin (40) on the Pico and the Pico will take it’s power from the Maker Drive board which is in turn taking its power from the battery pack.

While the higher current connections to the Maker Drive board will go to the screw terminals, the connections that carry lower power (like the signalling) can be connected using Dupont connectors. A practical example of that can be seen below.

Motor Connection IRL

Code

The two different approaches to controlling the motors are outlined below.

Simple constant speed approach

The code below is a simple test which runs the motor forward and then backward for two second each way.

import time
from machine import Pin

motor1a = Pin(4, Pin.OUT)
motor1b = Pin(5, Pin.OUT)

# Forward
motor1a.high()
motor1b.low()

time.sleep(2)

# Backward
motor1a.low()
motor1b.high()

time.sleep(2)

# Stop
motor1a.low()
motor1b.low()

Variable speed demonstration

The code below is demonstrates the ability to vary the speed of the motor by using pulse width modulation on the signals.

import time
from machine import Pin, PWM

motor1a = Pin(4, Pin.OUT)

pwm_motor1b = PWM(Pin(5))
pwm_motor1b.freq(50)

# 1/4 speed
motor1a.low()
pwm_motor1b.duty_u16(16383)
time.sleep(2)

# 1/2 Speed
motor1a.low()
pwm_motor1b.duty_u16(32767)
time.sleep(2)

# FULL SPEED!!!!
motor1a.low()
pwm_motor1b.duty_u16(65535)
time.sleep(2)

# Stop
motor1a.low()
pwm_motor1b.duty_u16(0)

Connecting an SD Card to the Raspberry Pi Pico

The Raspberry Pi Pico is a very capable device, but it lacks the ability to store sizeable amounts of data on the board. This is a useful function when using the Pico for tasks such as data logging. To make this function practical we can use an SD card for expanding storage via an adapter.

SD cards are great for storing logging and data from microcontroller projects that can then be read on a computer.

We can use SD cards in Serial Peripheral (SPI) mode which allows us to rely on easy to use SPI peripherals and libraries for communication. SPI mode is perfect if we’re writing short amounts of data to a file (e.g. event logging).

SD card adapter or adaptor.

First the elephant in the room. Whether you spell it adapter or adaptor, it means the same thing. Let’s not get hung up on the semantics of language usage. As a gesture of improving international relations (or perhaps it’s just a personal weakness) I will spell it both ways, although I tend to lean towards adapter.

My personal SD Card adapter journey

I’ll be the first to admit that I haven’t exactly got a large amount of experience in using SD card adapters, but while going the through the process of testing different adapters and cards, I came across a few inconsistencies. This varied between adapters and card types. With that in mind, stay flexible and be patient. Have a range of cards on hand, and don’t be afraid to try something new like rolling your own adapter made out of a standard adapter and some header pins. Also try to start from a consistent baseline with a freshly formatted SD card, ensuring that it’s formatted using the FAT32 file system. And while we’re here, we can’t do anything fancy like having multiple partitions on our SD card. There can be only one! (Gratuitous Highlander reference)

Choose your weapon

While writing this section I used a few different adapters. From the range below I had consistently good results from the smaller adapter and the ghetto version that I created from a traditional adapter and some header pins. I never god results that I was happy with using the larger circuit board version. This might have been some combination of software, hardware and my own personal incompetence, but there it is.

Various SD Card Adapters

Install the SDCard Library.

The modules for supporting SD card use are not yet (as at 2022-10-31) built into the MicroPython distribution for the Pico, so we can add them simply enough manually (once they are included, if an observant reader notices before I do, could you let me know and I will update this section of the book :-))

To make use of the module we will need to download it from GitHub and then copy it over to our Pico. I found this most easily accomplished by first downloading the file to the main computer and then going File >> Open on Thonny and selecting the appropriate file. From there go File >> Save as… and select the Pico as the location to save the file (making sure to save it with the appropriate name (sdcard.py))

Connect the SD Card Adapter

The Serial Peripheral Interface (SPI) utilises four physical connections. Typically on an adapter they will be labelled as such;

  • SCK: Serial Clock
  • MOSI: Master Out Slave In
  • MISO: Master In Slave Out
  • CS: Chip Select

In turn they will be connected to one of the sets of SPI connections on the Raspberry Pi Pico. In our case we will use controller 0 (the pico has two controllers and four sets of possible connections to the GPIO pins) and GPIO pins 16 - 19

  • SPI0 RX (GPIO16) <-> MISO: Master In Slave Out
  • SPI0 CSN (GPIO17) <-> CS: Chip Select
  • SPI0 SCK (GPIO18) <-> SCK: Serial Clock
  • SPI0 TX (GPIO19) <-> MOSI: Master Out Slave In
SD Card Connection

Of course we also connect up or 3.3V and ground connections.

In a practical sense, connecting up with Dupont connectors is a simple method to test functionality.

SD Card Connection

Code

In a new new document, enter the following code:

import machine
import sdcard
import os

# Set the Chip Select (CS) pin high
cs = machine.Pin(17, machine.Pin.OUT)

# Intialize the SD Card
spi = machine.SPI(0,
                  baudrate=1000000,
                  polarity=0,
                  phase=0,
                  bits=8,
                  firstbit=machine.SPI.MSB,
                  sck=machine.Pin(18),
                  mosi=machine.Pin(19),
                  miso=machine.Pin(16))
sd = sdcard.SDCard(spi, cs)

# Mount filesystem
vfs = os.VfsFat(sd)
os.mount(vfs, "/sd")

# Create a file in write mode and write something
with open("/sd/sdtest.txt", "w") as file:
    file.write("Hello World!\r\n")
    file.write("This is a test\r\n")

# Open the file in read mode and read from it
with open("/sd/sdtest.txt", "r") as file:
    data = file.read()
    print(data)

Make sure you have the SD card inserted into the breakout board and click the Run button. You should see the contents of the file that is created (sdtest.txt) printed out in the shell. We can go one step further and eject the SD card from the adapter and plug it into our desktop machine where we can browse to the file and read it from the computer.

Our code above has the feature of creating the file sdtest.txt when it writes to it. This also means that it will overwrite the file every time it is run. If we want to append information to an already existing file we can use an a instead of a w. Something similar to the below would do the trick placed between the write and read blocks;

# Append information to a file
with open("/sd/sdtest.txt", "a") as file:
    file.write("With even more information!\r\n")

If we were utilising the SD card as a store for a data logging application, we would be appending information.

Bonus Connection!

If you’re keen to DIY you can solder header pins to a more traditional SD to Micro SD card adapter and connect that up to our Pico. I was a little sceptical before trying it out, but it worked like a charm.

With that in mind, here is a connection diagram for when you have mastered your soldering skills.

SD Card Ghetto Connection

Connecting MQ Series Gas Detectors to the Pico

This project will measure the presence of types of gas in the air using one of the MQ series of sensors.

The Sensor

MQ-2 Gas Sensor

The MQ-2 is a commonly used gas sensor in MQ sensor series. It is what’s referred to as a Chemiresistor as the detection is based upon change of resistance of the sensing material when the gas comes in contact with a Metal Oxide Semiconductor (MOS). The value of the analog signal output varies as the gas concentration varies.

Different metal oxides have different chemiresistive properties allowing them to sense different gasses.

The most obvious feature of the sensor is the surrounding layer (actually two layers) of stainless steel mesh called an ‘anti-explosion network’. This is present to make sure that the heater element inside the sensor doesn’t cause an explosion while it is in the presence of flammable gasses. It also acts as a filter to allow only gases to pass through to the sensor.

The MQ-2 sensor which we will be using can detect LPG, butane, propane, methane, alcohol, Hydrogen and smoke concentrations from 200 to 10000ppm.

There are a wide range of sensors in the MQ series that can detect the presence of different gasses.

The sensor we will be using is mounted on a circuit board for ease of connection. We provide it with a 3.3VDC supply and it returns an analog signal that varies in proportion to the concentration of our target gas. That signal can vary between 0VDC and 3.3VDC. The board also includes a digital output option, but this is designed to provide a breakpoint level of gas, rather than a value. The variable resistor (potentiometer) on the board allows this breakpoint to be varied.

MQ-2 Sensor Board Underside

The connections on the board are as follows

  • VCC: Is the power input. This will require 3.3VDC in our case, but it can actually accept anywhere from 2.5VDC to 5VDC)
  • GND: Is the ground pin
  • DO: Provides the digital output set by the potentiometer
  • AO: is the analog output signal.

It is this analog voltage that is then digitised with our Analog to Digital Converter (ADC) that is built into the microcontroller.

Connect Everything Up

We will want to connect;

  • VCC on the MQ-2 to the 3V3 pin (36) on the Pico
  • GND on the MQ-2 to the Ground pin (38) on the Pico
  • A0 on the MQ-2 to ADC0 pin (31) on the Pico

The power and ground pins are fairly self explanatory, and because the MQ-2 has an analog output that will vary from the applied voltage to 0V, we will apply this to one of our Analog to Digital Converter (ADC) pins. In this case ADC0 on pin 31.

MQ-2 connection to the Pico

Connecting the sensor practically can be achieved in a number of ways. But because the connection is relatively simple we can build a minimal configuration that will plug directly onto the pins using Dupont header connectors and jumper wire.

Code

The following code will read the value from the MQ-2 and convert that to the effective voltage that should be present on the analog pin. It will print this out every second.

import machine
import time

mq2 = machine.ADC(26)
conversion_factor = 3.3 / (65535)

while True:
    voltage = mq2.read_u16() * conversion_factor
    print("Output voltage is",voltage)
    time.sleep(1)

Distance Measurement using Time of Flight Sensor

What is a Time Of Flight Sensor?

A Time of Flight (ToF) sensor measures the time it takes for a signal to travel a distance through a medium. This is a deliberately broad definition since there are different ways to carry this out depending on the application. For the purposes of our explanation we are going to be describing a sensor that measures the time elapsed between the emission of a pulse of light, its reflection off an object, and its return to the ToF sensor.

In this case, the sensor itself is an extremely compact device that is popular for applications in robotics and cameras.

VL53L0X Time of Flight Sensor Package

The sensor we will be using is a VL53L0X Time-of-Flight laser-ranging module which can provide an accurate distance measurement to objects up to 2m away.

The VL53L0X uses a 940nm (infrared) Vertical Cavity Surface-Emitting Laser which is invisible to the human eye. The output is engineered to remain within Class 1 laser safety limits and as such is safe under all operating conditions. Have a read of the vl53l0x Datasheet for all the good info.

How does a Time Of Flight Sensor Work?

ToF sensors use a laser to emit infrared light. The light reflects off any object it strikes and returns to the sensor. Based on the time difference between the emission of the light and its return it is able to measure the distance between the object and the sensor.

Time of Flight Distance Measurement

The VL53L0X precisely measures how long it takes for emitted pulses of infrared laser light to reach the nearest object and be reflected back to a detector, so it can be considered a tiny, self-contained lidar system. The sensor can measure distances of up to 2m with 1 mm resolution, but its effective range and accuracy depend on ambient conditions and target characteristics like size and degree of reflectivity. The sensor’s accuracy can vary from ±3% at best to over ±10% in less optimal conditions.

The beam of the emitted light is quite narrow and the orientation of the sensor and the measured object will be factors in recording accurate values. This is also a positive thing since the narrow light source is good for determining distance of only the surface directly in front of it. Unlike audio based systems that utilise ultrasonic waves, the ‘cone’ of sensing is very narrow.

Time of Flight Sensor Field of View

How is a Time Of Flight Sensor Controlled?

The sensor is controlled via I2C, but we can abstract the complexities of this via a prebuilt MicroPython module. This was initially developed by Robin Matzner and was then adapted by Kevin McAleer. To make use of the module we will need to download it from GitHub and then copy it over to our Pico. I found this most easily accomplished by first downloading the file to the main computer and then going File >> Open on Thonny and selecting the appropriate file. From there go File >> Save as… and select the Pico as the location to save the file (making sure to save it with the appropriate name (vl53l0x.py))

Because of the abstraction afforded by the library, the adjustments that we can make are nicely simplified.

Range Timing Budget

The first thing we can adjust is the range timing budget. This is set up to manage the ‘ranging phase’ of the measurement where, several pulses are emitted, then reflected back by the target object, and detected by the receiving array. The typical timing budget for a range timing budget is 33ms with 200ms being used for high accuracy and 20ms recommended for high speed. This is changed in the MicroPython code via the line;

tof.set_measurement_timing_budget(100000)

Pulse Period

The other major adjustment that we can introduce is to the period of the pulse that is send out. The shorter the pulse, the better for closer measurements, the longer the pulse, the better for more distant measurement. There are two period ‘types’, Pre Range (Type 0) and Final Range (Type 1). Longer periods increase the potential range of the sensor. Valid values are even numbers only. These can be set in the MicroPython code via the lines;

tof.set_Vcsel_pulse_period(tof.vcsel_period_type[0], 18)
tof.set_Vcsel_pulse_period(tof.vcsel_period_type[1], 14)

The Pre Range settings can go from: 12 to 18 (default is 14) and the Final Range settings can go from 8 to 14 (default is 10).

Connecting a Time Of Flight Sensor Up to the Pico

The connection is fairly simple with only four connections being required. Power, ground, Serial CLock line (SCL) and Serial DAta line (SDA). The following connections are used for this example;

  • VL53L0X GND to Ground (pin 38) on the Pico (Black)
  • VL53L0X VCC to the 3V3(OUT) (pin 36) on the Pico (Red)
  • VL53L0X SCL to I2C1 SCL (pin 32) on the Pico (Orange)
  • VL53L0X SDA to I2C1 SDA (pin 31) on the Pico (Brown)
ToF Sensor Connected to the Pico

When selecting the I2C connections on the Pico, because the RP2040 microcontroller has two I2C controllers we need to ensure that we define which controller we are using in the code. I2C0 = id 0 and I2C1 = id 1. This is set in the following lines in the MicroPython code;

id = 1

i2c = I2C(id=id, sda=sda, scl=scl)

The best place to ensure that we have the id correctly identified is on the pinout.

Assuming that we have header pins soldered onto our Pico and the ToF sensor, the easiest ways to make a connection is via Dupont connectors.

Time of Flight Sensor Connected

The only other point to note is that there are reports of some inconsistent measurements if the XSHUT pin is left ‘floating’ (i.e, not tied to a low (ground) or high pin). I haven’t experienced this myself, but if you’re seeing something that you can’t explain, this could be worth investigating

Code

The code below is largely that written by Kevin McAleer and published on GitHub. However, it is adapted to provide for the connection as described above and it is tuned to optimise for longer distance readings. Likewise I have included a small piece of code to average out the readings to improve consistency.

import time
from machine import Pin, I2C
from vl53l0x import VL53L0X

print("setting up i2c")
sda = Pin(26)
scl = Pin(27)
id = 1

i2c = I2C(id=id, sda=sda, scl=scl)

print(i2c.scan())

print("creating vl53lox object")
# Create a VL53L0X object
tof = VL53L0X(i2c)

# the measuting_timing_budget is a value in micro seconds, the
# longer the budget, the more accurate the reading. (originally 40000)
budget = tof.measurement_timing_budget_us
print("Budget was:", budget)
tof.set_measurement_timing_budget(100000)

# Sets the VCSEL (vertical cavity surface emitting laser) pulse period
# for the given period type (VL53L0X::VcselPeriodPreRange or
# VL53L0X::VcselPeriodFinalRange) to the given value (in PCLKs).
# Longer periods increase the potential range of the sensor. 
# Valid values are (even numbers only):

# tof.set_Vcsel_pulse_period(tof.vcsel_period_type[0], 18) 12 default
tof.set_Vcsel_pulse_period(tof.vcsel_period_type[0], 18)

# tof.set_Vcsel_pulse_period(tof.vcsel_period_type[1], 14) 8 default
tof.set_Vcsel_pulse_period(tof.vcsel_period_type[1], 14)

# Number of readings to average
n = 20
reading_group = []

while True:
# Start ranging
    new_value = tof.ping()-50
    if new_value != 8141 and new_value != 8140:
        reading_group.append(new_value)
        if len(reading_group) > n:
            reading_group.pop(0)
        print(sum(reading_group)/ len(reading_group), " ", new_value)
    time.sleep(1)

Reading the on-board Temperature of a Raspberry Pi Pico

As well as providing many marvellous ways of interfacing to the world via external sensors, the Raspberry Pi Pico, or more accurately, the RP2040 microcontroller around which the Pico is built, includes an on-board temperature sensor that we can access to get a feel (see what I did there) for the environment.

About the sensor

As we know from reading about the RP2040 microcontroller, it includes an Analogue to Digital Converter (ADC) which can read signals that vary across a (relatively) broad range of input voltage levels and convert them to discrete values that we can read.

The RP2040 has five ADC inputs. Three are connected to the GPIO pins. One is connected to GPIO29 (which isn’t exposed as a header pin) which is used to measure VSYS and one input is dedicated to an internal temperature sensor. Don’t go trying to look for the sensor on the Pico board, it is integrated into the main RP2040 microcontroller chip!

The ADC on the Pico will allow for 12 bit resolution. That means it will return analogue values to a digital range between 0 and 4095. However, we will be scaling those values to a 16 bit range when we read them with MicroPython to between 0 and 65535. So for us, a voltage range between 0v and 3.3v will equal a digital numeric range of between 0 and 65535.

Technically the temperature of 27°C should equal the voltage of 0.706V which should equal 14021 on our numeric range between 0 and 65535.

As our temperature increases, the voltage reading drops by 1.721mV per degree.

Therefore the logical process that we need to follow to arrive at a temperature is;

  1. Read our 16 bit value. (variable = reading)
  2. Convert our 16 bit value into the equivalent voltage (variable = voltage) by multiplying our reading by 3.3/65536.
  3. Solve for the last unknown which is the variable temperature. Since we know where one point of the linear graph of voltage to temperature is (0.706V at 27°C) and we know the voltage of our ADC input and we know the rate of change (gradient) of our graph (-1.721mv per °C).
Solving for Temperature

Points to note from the datasheet

The rate of change of the sensor can vary over the temperature range and from device to device, therefore some degree of calibration may be required to gain a more accurate measurement.

Likewise, the sensor is very sensitive to errors in the reference voltage. Any error in the reference voltage will be passed to the measurement. The method to improve accuracy is therefore to add a more accurate and stable external reference voltage.

Code

The code below is a very simple affair that declares our sensor then enters a loop where a reading is taken, converted to a voltage, translated to a temperature and printed. This is repeated every two seconds.

import machine
import time

sensor = machine.ADC(4)

while True:
    reading = sensor.read_u16()
    voltage = reading * ( 3.3 / 65535) 
    temperature = 27 - (voltage - 0.706) / 0.001721
    print('Temperature: ', temperature)
    time.sleep(2)

The output can be adjusted by carefully breathing on the Pico which should raise the temperature slightly.

Temperature:  15.3408
Temperature:  14.87265
Temperature:  15.80894
Temperature:  15.3408

It should be noted that the large number of decimal places in the output is not indicative of a commensurate level of accuracy!

Multiple Temperature Measurements

This project will measure the temperature at multiple points using DS18B20 sensors. We will use the waterproof version of the sensors since they are more practical for external applications.

The DS18B20 Sensor

The DS18B20 is a ‘1-Wire’ digital temperature sensor manufactured by Maxim Integrated Products Inc. It provides a 9-bit to 12-bit precision, Celsius temperature measurement and incorporates an alarm function with user-programmable upper and lower trigger points.

Its temperature range is between -55C to 125C and they are accurate to +/- 0.5C between -10C and +85C.

It is called a ‘1-Wire’ device as it can operate over a single wire bus thanks to each sensor having a unique 64-bit serial code that can identify each device.

While the DS18B20 comes in a TO-92 package, it is also available in a waterproof, stainless steel package that is pre-wired and therefore slightly easier to use in conditions that require a degree of protection. The measurement project that we will undertake will use the waterproof version.

Single DS18B20 Sensor

The sensors can come with a couple of different wire colour combinations. They will typically have a black wire that needs to be connected to ground. A red wire that should be connected to a voltage source (in our case a 3.3V pin from the Pico) and a blue or yellow wire that carries the signal.

The DS18B20 can be powered from the signal line, but in our project we will use the supply from the Pico.

Hardware required

  • 3 x DS18B20 sensors (the waterproof version)
  • 4.7k Ohm resistor (I have used 10k Ohm resistor without problem)
  • Jumper cables with Dupont connectors on the end
  • Solder
  • Heat-shrink

Connecting everything up

The DS18B20 sensors needs to be connected with the black wires to ground, the red wires to the 3V3 pin and the blue or yellow (some sensors have blue and some have yellow) wires to GP26 (pin 31). A resistor between the value of 4.7k Ohms to 10k Ohms needs to be connected between the 3V3 and GP26 pins to act as a ‘pull-up’ resistor.

We can actually use any of our GP pins to connect our sensors, as our code will will rely on a software library to manage the communications, not one of the hardware implementations on the microcontroller.

The following diagram is a simplified view of the connection.

Connection of multiple temperature sensors

Connecting the sensor practically can be achieved in a number of ways. But because the connection is relatively simple we can build a minimal configuration that will plug directly onto the appropriate GPIO pins using Dupont connectors. The resistor is concealed under the heat-shrink and indicated with the arrow.

Minimal Triple DS18B20 Connection

Code

The following code will read the temperature values from all the DS18B20 sensors that it finds and print out the unique serial numbers of each sensor and the temperature that it is reading.

import machine
import onewire
import ds18x20
import time
import binascii

gp_pin = machine.Pin(26)

ds18b20_sensor = ds18x20.DS18X20(onewire.OneWire(gp_pin))

sensors = ds18b20_sensor.scan()

print('Found devices: ', sensors)

while True:
    ds18b20_sensor.convert_temp()
    time.sleep_ms(750)
    for device in sensors:
        s = binascii.hexlify(device)
        readable_string = s.decode('ascii')
        print(readable_string)
        print(ds18b20_sensor.read_temp(device))
    time.sleep(10)

The output will look something like the following (which has three sensors connected);

Found devices:  [bytearray(b'(\xff\xef\x8d>\x04\x00\xa8'), bytearray(b'(\xffI\x90\
>\x04\x00\xa2'), bytearray(b'(\xff\xf8n;\x04\x00q')]
28ffef8d3e0400a8
19.8125
28ff49903e0400a2
20.4375
28fff86e3b040071
19.625

AHT10 Temperature and Relative Humidity

It is quite common to package multiple sensors into a single package and the AHT10 is a good example that measures temperature and relative humidity.

AHT10 Details

The AHT10 is an accurate temperature and humidity sensor in a very small package that can be accessed via an I2C interface. While it has a temperature measurement range of between -40°C and 85°C, its typical error of ± 0.3 is achieved between around 0°C and 55°C. Normal operating range for humidity is between 20 and 80% relative humidity. In that range it has a typical accuracy of ±2.

AHT10 Temperature and Relative Humidity Sensor

Each sensor is calibrated and tested with a product lot number printed on the surface. be aware that humidity sensors are not ordinary electronic components and should be carefully handled and operated. Prolonged exposure to high concentrations of chemical vapour will cause the sensor reading to drift.

If the sensor is exposed to adverse conditions or chemical vapours and the readings drift, it can be restored to its calibrated state by the following process;

  • Drying: maintained at 80-85 ° C and <5% relative humidity for 10 hours;
  • Rehydration: 12 hours at 20-30 ° C and >75% relative humidity.

The devices address is 0x38 (which we will see when we scan it) and in spite of there also being the address 0x39 also printed on the sensor PCB, there is no indication of how to enable that address. As a result, only a single AHT10 can be used on an I2C bus.

How is the AHT10 sensor accessed?

The sensor is accessed via I2C, but we can abstract the complexities of this via a pre-built MicroPython module. This was developed by Andreas Bühl. To make use of the module we will need to download it from GitHub and then copy it over to our Pico. I found this most easily accomplished by first downloading the file to the main computer and then going File >> Open on Thonny and selecting the appropriate file. From there go File >> Save as… and select the Pico as the location to save the file (making sure to save it with the appropriate name (ahtx0.py))

Because of the abstraction afforded by the library, the reading of the sensor is nicely simplified.

Connecting the AHT10 to the Pico

The connection is fairly simple with only four connections being required. Power, ground, Serial CLock line (SCL) and Serial DAta line (SDA). The following connections are used for this example;

  • AHT10 GND to Ground (pin 38) on the Pico (Black)
  • AHT10 VIN to the 3V3(OUT) (pin 36) on the Pico (Red)
  • AHT10 SCL to I2C1 SCL (pin 32, or GPIO27) on the Pico (Orange)
  • AHT10 SDA to I2C1 SDA (pin 31, or GPIO26) on the Pico (Brown)
AHT10 Connection

Assuming that we have header pins soldered onto our Pico and the AHT10 sensor, the easiest ways to make a connection is via Dupont connectors.

AHT10 Connection via Dupont Connectors

An important point to note when we are connecting our Pico is that because the RP2040 microcontroller has two I2C controllers we need to ensure that we define which controller we are using in the code. I2C0 = id 0 and I2C1 = id 1. This is set in the following lines in the MicroPython code;

i2c = I2C(1, sda=sda, scl=scl)

Code

As a neat method of confirming the address of our sensor we can run the following code on our Pico that will scan the I2C bus;

from machine import Pin, I2C

# Create I2C object
sda = Pin(26)
scl = Pin(27)
i2c = I2C(1, scl=scl, sda=sda)

# Print out any addresses found
devices = i2c.scan()

if devices:
    for d in devices:
        print(hex(d))

This will print out;

0x38

Which is the address printed on the circuit board.

The code to measure the temperature and relative humidity is;

import time
from machine import Pin, I2C

import ahtx0

# Create I2C object
sda = Pin(26)
scl = Pin(27)
i2c = I2C(1, scl=scl, sda=sda)

# Create the sensor object using I2C
sensor = ahtx0.AHT10(i2c)

temperature = round(sensor.temperature, 2)
humidity = round(sensor.relative_humidity, 2)

while True:
    print("Temperature: ", temperature, "C")
    print("Humidity: ", humidity, "%")
    print()
    time.sleep(5)

Just a reminder that if we use different I2C pin than GPIO26 and GPIO27, we will need to check the pinout to know which I2C id should be used. In the case above it is 1.

Which produces something like the following;

Temperature:  21.98 C
Humidity:  55.41 %

Temperature:  21.98 C
Humidity:  55.41 %

Temperature:  21.98 C
Humidity:  55.41 %

To see some variation, we can softly breath on the sensor.

Motion Sensing with the Raspberry Pi Pico

What is a PIR Sensor?

A passive infrared (PIR) sensor measures and evaluates InfraRed (IR) light emitted from nearby objects. They are referred to as ‘passive’ due to the fact that the sensor does not emit any heat or energy. Living animals emit infrared radiation and the sensor can pick this up and register it electronically. It uses a clever mechanism to detect a change in the infrared light it is receiving and as a result trigger a signal that can be read by an external device. In our case that will be a Raspberry Pi Pico.

AM312 PIR Detector

PIR sensors are used in sensing applications, such as security alarms, motion detectors, and automatic lights.

How does a PIR Sensor Work?

Pretty much everything (humans, animals, even inanimate objects) emit a certain amount of infrared radiation. The amount relates to the body or object’s warmth and composition.

The PIR sensor proper is actually under the white hemispherical covering that is prominent on a PIR. The covering is in fact a lens that focusses radiation onto the sensor. The sensor has two slots in it where each slot allows infrared radiation to interact with pyroelectric receptors that are very sensitive to this type of emission at room temperature. The sensor is a hermetically sealed metal enclosure which improves immunity to noise, temperature and humidity. The sensor incorporates a window made of infrared-transmissive material for protection.

PIR Assembly

When there is no warm moving object in the sensors field of view it is idle and both slots detect the same amount of radiation. However, when a warm body like a human or animal comes into view, a signal is first detected by one of the pyroelectric sensors, which causes a positive differential change between the two halves. When the warm body leaves the sensing area, the reverse happens and the sensor generates a negative differential change. These changing pulses are what determines that movement has been detected.

The PIR sensor is mounted on a printed circuit board which supports the electronics that interpret the signals from the sensor itself. In a practical setting the complete assembly is usually contained within a housing which is located to provide a view over the area to be monitored.

PIR

The white hemispherical lens is essentially a ‘window’ through which the infrared energy can enter. The plastic lens acts as a focusing mechanism which condenses a large area into a small one (in the same way that a camera lens works). To minimise the cost and size required the covering is normally a fresnel lens.

The HC-SR501

I’m including the HC-SR501 description here because I have a few hanging around and I had great plans to use one for a particular project involving a Raspberry Pi Zero some months ago. However, in the process of testing I found that it would trigger at times through interference with the WiFi signal on the Pi. This took quite a period to determine as I went through troubleshooting which included multiple sensors and different Pis. Ultimately I came to the conclusion that the analog portion of the design of the HC-SR501 that allowed the device to trigger unintentionally was not suitable for my application and I used the AM312 instead. That device uses digital signal processing which was unaffected by the WiFi signal.

HC-SR501

The HC-SR501 has a 3-pin connector that interfaces it to the outside world. The connections are as follows;

  • VCC is the power supply for HC-SR501 PIR sensor which we can connect a 5V pin.
  • Output pin is a 3.3V TTL logic output. LOW indicates no motion is detected, HIGH means some motion has been detected.
  • GND should be connected to the ground.

The HC-SR501 has a built-in voltage regulator so it can be powered by any DC voltage from 4.5 to 12 volts, typically 5V is used.

There are more than one model of this type of sensor. be careful to ensure that you have the connections correct. The best mechanism (other than following the labels if there are any) is to look for the protection diode as a reference. Failing that, if there aren’t any labels on the bottom of the circuit board, check on the board, under the lens.

There are two potentiometers on the board to adjust a couple of parameters;

  • Sensitivity: This sets the maximum distance that motion can be detected. It ranges from 3 meters to approximately 7 meters. The layout of the area being covered can affect the range.
  • Time: This sets how long that the output will remain high after detection is triggered. The minimum is 3 seconds and the maximum is 300 seconds or 5 minutes.

The board also has a jumper with two settings;

  • H: This is the Hold / Repeat / Retriggering setting. In this position the HC-SR501 will continue to generate a high output while it continues to detect movement.
  • L: This is the Intermittent or No-Repeat / Non-Retriggering setting. Here the output will stay high for the period set by the Time potentiometer.

As with most PIR sensors the HC-SR501 requires some time to adjust to the infrared environment that it sees in any room. This will take from 30 to 60 seconds when the sensor is first powered up. It also has a ‘reset’ period of about 5 or 6 seconds after making a reading. During this time it will not detect any motion.

The AM312

AM312

As mentioned earlier, the AM312 utilises digital signal processing which removes one of the reasons that interference can affect the HC-SR501.

AM312 is described as a new digital intelligent PIR sensor! It is a much simpler and smaller device than the HC-SR501 with the digital detector and electronic circuitry built into the detector housing.

This sensor also has the advantage of being ultra-low power with a quiescent current of only 8uA making it suitable for battery applications where a very long battery life is required.

The pin connections are as follows with the orientation of the sensor with the header pins uppermost;

  • Vin is the power supply which requires 3.3VDC.
  • The Signal pin will present a high when motion is detected.
  • Ground should be connected to the ground.
AM312 Pins

How do we read a PIR?

A PIR is one of the simplest sensors to read with the signal pin going high when motion is detected. This means that we can merely set any one of our GPIO pins to act as an input and then read when it goes high.

Connecting Up a PIR to the Pico

For the AM312 we can connect the sensor to the Pico as follows;

- Vin on the AM312 to the 3V3(Out) pin (36) on the Pico.
- Signal pin on the AM312 to GPIO 28 (pin 34) on the Pico.
- Ground should be connected to the GND pin (38) on the Pico

PIR Connections

Connecting the PIR practically can be achieved in a number of ways. But because the connection is relatively simple we can build a minimal configuration that will plug directly onto the pins using Dupont header connectors and jumper wire.

PIR Connections IRL

Be aware that there are a few similar models of this type of sensor. Be careful to ensure that you have the connections correct. The best mechanism (other than following the labels if there are any) is to look for the protection diode as a reference on the HC-SR501 and be aware that the diagram that I have shown here for the AM312 is shown with the header pins uppermost. For example the sensor I am using is not labelled, and the VCC and GND pins are in a different location to those shone on at least one connection diagram on the Internet.

Code

The code below will designate the GPIO pin to be used as our input (GPIO28) and set it low with a pull down resistor.

pir = Pin(28, Pin.IN, Pin.PULL_DOWN)

We can use any of the GPIO pins, so feel free to pick a convenient one. We use an internal pull down resistor to avoid having a ‘floating’ input. This will set the pin to be a logic 0 so long as it doesn’t have a signal applied.

It pauses momentarily to gather itself and then goes into an eternal loop where it prints out an alert when movement is detected. It also pauses after detection to give the detectors signal output an opportunity to return to a low state.

import time
from machine import Pin

pir = Pin(28, Pin.IN, Pin.PULL_DOWN)
count = 0

time.sleep(1)
print('Ready to detect movement!')

while True:
     if pir.value() == 1:
          count = count + 1
          print('Movement detected ', count)
          time.sleep(4)
     time.sleep(1)

Using an OLED Display attached to a Pico

This project will use an attached OLED display unit to present information from our Raspberry Pi Pico.

The OLED Display

The display that we’ll use in this example is based on the SSD1306 driver chip, which acts as a bridge between the display matrix and the Pico. The matrix can come in a variety of resolutions (128x64, 128x32, 72x40, 64x48) and colours (white, yellow, blue). The display itself uses an organic light-emitting diode (OLED) technology that allows it to be bright, fairly detailed and to have a wide viewing angle. It also doesn’t hurt that the device is also very reasonably priced.

The specifications of the unit used here is 0.96 inches on the diagonal, has a resolution of 128x64 and is white on a black background.

The SSD1306 Display

The green tab on the side is simply there to make removal of the protective film on the screen easy.

The SSD1306 micro-chip driver uses an I2C communications protocol and there is a PyPI library available that can be used for basic controls. There are some models that will also include SPI connectivity and these can be identified by having more than the four connecting pins that are shown on the model above. For more information on the unit we can consult the datasheet.

Connecting the Display to the Pico

The connection is fairly simple with only four connecting wires being required. Power, ground, Serial CLock line (SCL) and Serial DAta line (SDA). The following connections are used for this example;

  • Display GND to Ground (pin 38) on the Pico (Black)
  • Display VIN to the 3V3(OUT) (pin 36) on the Pico (Red)
  • Display SCL to I2C1 SCL (pin 32, or GPIO27) on the Pico (Orange)
  • Display SDA to I2C1 SDA (pin 31, or GPIO26) on the Pico (Brown)
OLED Connection

An important point to note when we are connecting our Pico is that because the RP2040 microcontroller has two I2C controllers, we need to ensure that we define which controller we are using in the code. I2C0 = id 0 and I2C1 = id 1. This is set in the following lines in the MicroPython code;

i2c = I2C(1, sda=sda, scl=scl)

In our case, since we are using GPIO26 and GPIO27 for the SDA and SCK connections we are using I2C Controller 1.

Loading the ssd1306 PyPI module

The display is accessed via I2C, but we can abstract the complexities of this via a pre-built MicroPython library. To make use of the library we will use Thonny to find and download it.

  1. With our Pico connected to our desktop computer, open Thonny.
  2. Click on Tools > Manage Packages to access Thonny’s package manager.
  3. Type ‘ssd1306’ in the search bar and click on ‘Search on PyPI’. This will return a few results.
  4. Click on ‘micropython-ssd1306’ (ssd1306 module for MicroPython) and then click on ‘Install’. This will copy the library to our Pico.
ssd1306 module library download

Click on ‘Close’ to return to the main screen.

And that’s it, we’re all set up to use the ‘ssd1306’ library.

Code

The code below is incredibly basic and aimed at providing an example of how easy it it is to get started using the display. There is a great deal more that can be done with the display to add shapes, pictures and movement, but our aim is to get up and running and then from there we can push on to greater things :-).

The code below will print ‘Pico’ five times staggered across the screen.

from machine import Pin, I2C
from ssd1306 import SSD1306_I2C

# Create I2C object
sda = Pin(26)
scl = Pin(27)
i2c = I2C(1, scl=scl, sda=sda, freq=400000)

oled = SSD1306_I2C(128, 64, i2c)

oled.text("Pico", 0, 0)
oled.text("Pico", 20, 10)
oled.text("Pico", 40, 20)
oled.text("Pico", 60, 30)
oled.text("Pico", 80, 40)

oled.show()
Pico Pico Pico Pico Pico

While the example above is limited, we can also use some of the code’s functions available for the display to add greater complexity. Feel free to have a play with some of the options below;

oled.poweroff()     # power off the display, pixels persist in memory
oled.poweron()      # power on the display, pixels redrawn
oled.contrast(0)    # dim
oled.contrast(255)  # bright
oled.invert(1)      # display inverted
oled.invert(0)      # display normal
oled.rotate(True)   # rotate 180 degrees
oled.rotate(False)  # rotate 0 degrees
oled.show()         # write the contents of the FrameBuffer to display memory

For greater illustration of what can presented on the display, check out the MicroPython docs page for the ESP8266 which presents further options.

And as a final tribute to the MicroPython instructions, run the following code;

from machine import Pin, I2C
from ssd1306 import SSD1306_I2C

# Create I2C object
sda = Pin(26)
scl = Pin(27)
i2c = I2C(1, scl=scl, sda=sda, freq=400000)

oled = SSD1306_I2C(128, 64, i2c)

oled.fill(0)
oled.fill_rect(0, 0, 32, 32, 1)
oled.fill_rect(2, 2, 28, 28, 0)
oled.vline(9, 8, 22, 1)
oled.vline(16, 2, 22, 1)
oled.vline(23, 8, 22, 1)
oled.fill_rect(26, 24, 2, 4, 1)
oled.text('MicroPython', 40, 0, 1)
oled.text('SSD1306', 40, 12, 1)
oled.text('OLED 128x64', 40, 24, 1)

oled.show()

Enjoy!

Using a Dot-Matrix Display Attached to a Pico

A dot matrix display is a little like a compromise between the simplicity of a seven segment display and a modern screen. They have a lot of flexibility and through widely available Python modules can be easily used with a Raspberry Pi Pico.

The Dot-Matrix Display

The display is an array of LEDs that can be lit in patterns to represent text, patterns and images. This tutorial will use the MAX7219 dot matrix display to demonstrate how easily they can be used. There is a good chance that you may have seen a MAX7219 module at some point since they are relatively common and inexpensive and therefore popular as a solution for display based projects. They have the additional advantage of being able to be connected together to create larger arrays and therefore images.

Four Connected MAX7219 Dot Matrix Display Modules

How is the display accessed?

The display is accessed via SPI, but we can abstract the complexities of this via a pre-built MicroPython module. This has been published on GitHub by FideliusFalcon. To make use of the module we will need to download it from GitHub and then copy it over to our Pico. I found this most easily accomplished by first downloading the file to the main computer and then going File >> Open on Thonny and selecting the appropriate file. From there go File >> Save as… and select the Pico as the location to save the file (making sure to save it with the appropriate name (max7219.py)).

Because of the abstraction afforded by the library, the reading of the sensor is nicely simplified.

Connecting the Display to the Pico

While under normal conditions SPI interfaces rely on sending and receiving data, in this application the Pico only sends. Therefore, while we would normally include a MISO (master in, slave out) connection on the peripheral, in this case it is not required. As always, since there are a range of different ways for labelling SPI-compatible signalling lines, try not to second guess things and figure it out, The connection diagram below is correct. The table below also makes an attempt to match up the naming conventions, but honestly, the diagram is our best benchmark.

Function                SPI   Pico      MAX7219
Output from controller  MOSI  SPI0 TX   DIN
Input to controller     MISO  SPI0 RX   N/A
Clock                   SCK   SPI0 SCK  CLK
Chip select             CS    SPI0 CSN  CS

The display module is labelled ‘DIN’, ‘SCK’ and ‘CS’, so the wiring should be relatively clear (but look to the connection diagram if in doubt). As with so many of these connections, some simple Dupont connecting wires will suffice.

The Dot-matrix Display Connection

Code

The code below rotates the words ‘RPi’ and ‘Pico’ on the display. Take the opportunity add your own text and adjust the brightness settings to get a feel for the variations that are possible.

from machine import Pin, SPI
import max7219
from time import sleep

spi = SPI(0,sck=Pin(18),mosi=Pin(19))
cs = Pin(17, Pin.OUT)

display = max7219.Matrix8x8(spi, cs, 4)

display.brightness(10)

while True:

    display.fill(0)
    display.text('RPi',0,0,1)
    display.show()
    sleep(3)
    
    display.fill(0)
    display.text('PICO',0,0,1)
    display.show()
    sleep(3)
Displaying Pico

Scrolling

Showing text is one thing, but scrolling text is another :-). The code below takes a string of text and moves it across the face of the display.

from machine import Pin, SPI
import max7219
import time

#Intialize the SPI
spi = SPI(0, baudrate=10000000, polarity=1, phase=0, sck=Pin(18), mosi=Pin(19))
cs = Pin(17, Pin.OUT)

display = max7219.Matrix8x8(spi, cs, 4)

display.brightness(0)
scrolling_message = "RASPBERRY PI PICO SCROLLING DISPLAY"
length = len(scrolling_message)

column = (length * 8)

display.fill(0)
display.show()

time.sleep(1)

while True:
    for x in range(32, -column, -1):     
        display.fill(0)
        display.text(scrolling_message ,x,0,1)
        display.show()
        time.sleep(0.05)

In this instance, play with the message and the sleep time to adjust what is being displayed and the speed of movement.

Using the Raspberry Pi Pico as a Prometheus Node

About Prometheus and Grafana

Prometheus is an open source application used for monitoring and alerting. It records real-time metrics in a time series database built using a HTTP ‘pull’ model.

It was was created because of the need to monitor multiple microservices that might be running in a system. It employs a modular architecture and employs modules called exporters, which allow the capture of metrics from a range of platforms, IT hardware and software.

Prometheus’s ‘pull model’ of metrics gathering means that it will actively request information for recording. It collects metrics at regular intervals and stores them locally. These metrics are pulled from nodes that run ‘exporters’. An exporter can be defined as a module that extracts information and translates it into the Prometheus format.

Prometheus data is stored as metrics, with each having a name that is used for referencing and querying. This is what makes it very good at recording time series data.

Prometheus is commonly used in combination with the Grafana platform which has a very powerful visualisation capability.

I have written a separate book on installing and using Prometheus and Grafana here and I would recommend it to anyone who is interested in monitoring their physical or IT environment.

Using the Pico as an Exporter

This particular guide will describe how to use the Raspberry Pi Pico W as an exporter node. This will allow the distribution of simple sensors to be even more widespread than is possible with a Raspberry Pi Zero or similar since they are cheaper and have lower power requirements.

There isn’t a dedicated Prometheus exporter available for the Pico, so we will make one ourselves.

The good news is that when gathering metrics for use in a Prometheus / Grafana stack installation, metrics can be made available from a device via a simple web query that details various metric values for consumption.

The information presented on the web page is set out in the exposition format published here.

In its most simple form the information can take the format of a metric name and a value separated by any number of blank spaces or tabs. If more than one line (metric) is being presented, these must be separated by a line feed character (\n). The last line must end with a line feed character. Empty lines are ignored.

For example;

weather_inside_temperature_C 21.7
weather_barometer_mb 1035.6
weather_sunshine_hours_hours 11.0

A great deal more complexity can be integrated into the metric values including label names, and a time-stamp, but for the purposes of demonstrating the technique we will focus on a very simple example. For guidance on best practices for naming conventions and metric formatting in general, see the page on writing exporters here.

It is worth reinforcing here that this code is dependant on using the Pico W since it provides the mechanism for connecting to the Prometheus platform via a web request.

Code

The astute reader will recognise the following as being heavily based on the example used earlier in the book to serve a web page from the Pico W. Well spotted. You can also download this code as an extra with the book. It is bundled with the code samples extra and is called prometheus.py.

import network
import socket
import time
import random
import rp2

from secrets import secrets

ssid = secrets['ssid']
password = secrets['pw']

# Set country to avoid possible errors
rp2.country('NZ')

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)
wlan.ifconfig(('10.1.1.161','255.255.255.0','10.1.1.1','8.8.8.8'))

html = """# HELP pico_temp Temperature in C
# TYPE pico_temp gauge
pico_temp pico_temperature
# HELP pico_rand An Indication of a random number
# TYPE pico_rand gauge
pico_rand pico_random
"""

# Wait for connect or fail
max_wait = 10
while max_wait > 0:
    if wlan.status() < 0 or wlan.status() >= 3:
        break
    max_wait -= 1
    print('waiting for connection...')
    time.sleep(1)

# Handle connection error
if wlan.status() != 3:
    raise RuntimeError('network connection failed')
else:
    print('connected')
    status = wlan.ifconfig()
    print( 'ip = ' + status[0] )

# Open socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(1)

print('listening on', addr)

# Configure for reading temperature 
sensor = machine.ADC(4)

def temperature_reading():
    reading = sensor.read_u16()
    voltage = reading * ( 3.3 / 65535) 
    temperature = 27 - (voltage - 0.706) / 0.001721
    return(temperature)

# Listen for connections
while True:
    try:
        cl, addr = s.accept()
        print('client connected from', addr)

        request = cl.recv(1024)
        print(request)

        temperature = temperature_reading()
        rando = random.randint(0,99)

        print(rando)
        print(temperature)

        first = html.replace("pico_random",str(rando))
        last = first.replace("pico_temperature",str(temperature))
        
        response = last
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        cl.close()
        print('connection closed')

This code combines several different components. It connects the Pico to a local network via WiFi. It sets itself a static IP address. It makes content available via port 80 so that it can be read by a browser. It serves content in an OpenMetric and Prometheus exposition format so that it can be read by Prometheus.

One of the more important parts of that is the setting of the static IP address via the following line;

wlan.ifconfig(('10.1.1.161','255.255.255.0','10.1.1.1','8.8.8.8'))

This is important so that we can tell Prometheus where to go to read the metrics. It’s important to remember from the section earlier in the book that these settings need to be particular to your network.

The HTML

The HTML section is where the metric information is recorded for presentation to Prometheus. It took a bit of trial and error to get to the point where this was being presented in a format where Prometheus could read it from a practical perspective and then it took a bit more effort to ensure that the data was being presented correctly.

html = """# HELP pico_temp Temperature in C
# TYPE pico_temp gauge
pico_temp pico_temperature
# HELP pico_rand An Indication of a random number
# TYPE pico_rand gauge
pico_rand pico_random
"""

The first thing to notice is that it doesn’t include any HTML tags that we would expect for a regular page. It turns out that this did not play well with Prometheus. It refused to connect, showing the message "INVALID" is not a valid start token.

I then made the horrible mistake of thinking that the information on the lines with the # marks were the equivalent of comments in code. Boy was I wrong and it was a classic case of RTFM. The error response on Prometheus was invalid metric type "about the variable". So, after reading the doc on the Prometheus exposition format I could see that the HELP and TYPE lines also have to be specifically formatted!

Lines with a # as the first non-whitespace character are comments. They are ignored unless the first token after # is either HELP or TYPE. Those lines are treated as follows:

  • If the token is HELP, at least one more token is expected, which is the metric name. All remaining tokens are considered the docstring for that metric name. HELP lines may contain any sequence of UTF-8 characters (after the metric name), but the backslash and the line feed characters have to be escaped as \ and \n, respectively. Only one HELP line may exist for any given metric name.
  • If the token is TYPE, exactly two more tokens are expected. The first is the metric name, and the second is either counter, gauge, histogram, summary, or untyped, defining the type for the metric of that name. Only one TYPE line may exist for a given metric name. The TYPE line for a metric name must appear before the first sample is reported for that metric name. If there is no TYPE line for a metric name, the type is set to untyped.

So…. we could just omit the HELP and TYPE lines, but let’s persist.

The metrics

The astute reader (that’s you) will have noted that as well as the two metric names that we have included in our HTML section (pico_temp and pico_rand) we have also included a couple of place-holders that we will use in a few moments to substitute in our actual metric values. The place-holders are pico_temperature and pico_random.

Because our temperature measurement takes a bit of code to read, that is mostly included in the function temperature_reading.

sensor = machine.ADC(4)

def temperature_reading():
    reading = sensor.read_u16()
    voltage = reading * ( 3.3 / 65535) 
    temperature = 27 - (voltage - 0.706) / 0.001721
    return(temperature)

The remainder of the metric code is in our while loop.

        temperature = temperature_reading()
        rando = random.randint(0,99)

The last piece of the puzzle is where we replace our place-holders with our metric values so that the information can be served and read.

        first = html.replace("pico_random",str(rando))
        last = first.replace("pico_temperature",str(temperature))

With all of that complete, we are able to configure Prometheus and look at our target list to see glorious success! From there we can make a simple graph to display our metrics.

Graphs of the random and temperature values

The graph above shows a 24 hour read-out of the random number and temperature metrics. The ‘blip’ that we can see around 1600 hrs is actually when the sun came through the office and passed over the Pico when it was sitting on the bench.

Make it your own

To use this code for yourself you will need to ensure that the metric you’re recording is made available in the HTML code in the while loop. Then ensure that you can replace the unique place-holder with the metric value. From there, Prometheus should do the rest.

You will have noticed that this description of how to make a measured value available to Prometheus for monitoring and display does not include a description of how to install and configure Prometheus. That’s a much longer story and I would recommend that if you don’t have an instance already installed, that you take a look at the book on installing it here.