Using Sensors With The Raspberry Pi Pico
Using Sensors With The Raspberry Pi Pico
Daniel Quadros
This book is for sale at http://leanpub.com/picosensors
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools
and many iterations to get reader feedback, pivot until you have the right book and build
traction once you do.
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
The Raspberry Pi Pico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Getting the Example Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Using the Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Organization of This Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
How to Send Feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Using Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Measuring What We Really Want . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
A Sensor Can Affect What You Are Measuring . . . . . . . . . . . . . . . . . . . . . . 7
Accuracy and Resolution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
ADC Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Averaging Readings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Calibration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Readings Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
RFID . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471
RFID 125kHz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471
MIFARE and NFC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 492
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 517
Choosing Sensors and Using Them Correctly . . . . . . . . . . . . . . . . . . . . . . . 517
Using a New Sensor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 517
Writing Code and Using Libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518
What’s Next? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518
A Few Sensors
In this book, you are going to learn about sensors in general and the many ways they can send
data to a microcontroller. You will also see some of the typical strategies to get reliable data
in a timely way. On a more specific view, you will learn about the resources available in the
Raspberry Pi Pico board to connect sensors and how to use them when programming in C/C++,
MicroPython, and CircuitPython. And then we are going to look deeply into quite a few popular
sensors.
I tried to cover a good number of the more common sensors. Of course, new sensors enter the
market daily (and some old ones get harder to find). That is one of the reasons I explain in detail
Introduction 2
the workings of the sensors, with this knowledge you will have a head start when using a new
sensor.
I will talk about how to use the Pi Pico interfaces in your code and you are going to see many
code examples, but teaching programming languages is not the object of this book. You should
have a basic knowledge of C or Python to better understand the example code and be able to
write your own programs.
The Pi Pico uses the RP2040 microcontroller. It has two ARM Cortex M0+ cores, 264K of RAM,
and a good number of peripherals (GPIO, Timers, PWM, UART, I2C, SPI, ADC, and USB).
One important feature of the RP2040 is the PIO (Programmable I/O), which allows simple
hardware protocols to be implemented as short code routines that run parallel to the main ARM
cores. I will not go into details of how to program the PIO in this book. If you are interested, take
a look at my book Knowing the RP2040¹.
While the figures and text refer to the Pi Pico, most of the information and techniques presented
can be used with other boards that use the RP2040 microcontroller (and there are many of them,
take a look at some in Appendix A).
¹https://leanpub.com/rp2040
Introduction 3
C/C++ has long established itself as the language of choice for low-end embedded systems, giving
performance and predictability (at the cost of writing more lines of code). MicroPython brings
to microcontrollers the ease of use and power of the Python language. Both have direct support
from the people behind the Pi Pico. CircuitPython is a “fork” (a derivation) of MicroPython.
While it does not have direct support from the Raspberry Foundation for the Pi Pico, it has I/O
modules that make it easier to move code between different boards and a huge amount of drivers
and sample code from Adafruit.
The image below (taken from the official documentation) shows the Raspberry Pi Pico and the
signals at its pins. You will notice that some functions (like UART0 TX) appear in multiple pins
(1, 16, and 21 in this case). As part of initializing some of the peripherals, we will select the pin
that will be used.
The BOOTSEL button places the Pico in a special mode if you power (or reset) the Pico with it
Introduction 4
pressed.
For the examples in this book, we are going to connect the Pico to a PC through USB. This
connection will be used to:
Breadboard
Once you understand what holes are connected internally, it’s easy to change the position of the
components.
Introduction 5
To use the Pico and the sensors with a breadboard, they need to have pin connectors (headers)
soldered to their pins. In some cases, instead of inserting a sensor directly into the breadboard,
it’s better to use male-to-female jumpers.
I have included pictures (made with Fritzing²) showing the connections for all the examples.
TIP: There is no reset button on the Pico, but you can connect one between pin 30 (RUN) and
GND. Some small buttons have the exact spacing between pins 30 and 28, you can use one in a
breadboard or solder it directly to the board.
Appendix B talks about a problem I hope you don’t have to face: non-original sensors (clones and
fakes). There I share a couple of bad experiences I had while testing the examples for this book.
The book is now finished, but there may be updates to fix errors.
Acknowledgments
Looking back, there are too many people that, one way or another, have helped me come to the
point where I could write this book. This is where I mention and thank a few of them.
First, my mother and father nurtured my curiosity and addicted me to reading.
There were many teachers who not only gave important lessons but also encouraged me to learn
more.
In my professional life, I am grateful for all that believed I could deliver and those that helped
me do so.
A special mention to the late Alberto Fabiano, who introduced me to the wild community of
hackerspaces. Fabio Souza and Tiago Lima at Embarcados were very supportive of my writing.
Also Mauricio Aniche, my son-in-law was a big encourager.
Of course, this book would not exist if not for the patience of my wife Cecilia while I spent days
in front of a PC and playing with all those “little boards”.
• Clicking on the “Email the Author” button on the book page in leanpub.com
• Sending an email to [email protected]
• Sending me a message on Twitter (@DQSoft)
Using Sensors
Many years ago, I got this new magnetic sensor that should return its position regards the
magnetic North (like a compass). I studied the documentation and wrote some code to get a
reading. The sensor returned a number and, again following the docs, I converted this number
into an angle. I made some crude tests and the measurement changed when I rotated the sensor.
All fine, right?
Sometime later I decided to build a compass using this sensor, showing the North direction with
a 12 LED ring. To my dismay, I could not get it to work accurately. The reading changed a lot
with the sensor in a fixed position, there was no linear relation between readings and the rotation
and nearby objects would change the results!
Getting good results from sensors require a lot more than simply getting a number from
them. In this chapter, I will talk about some of the challenges and some ways to get through
them.
Better accuracy and resolution can cost you money or require more time per reading. You should
not go for the best sensor but figure out what you need for your project and find the best
compromise.
ADC Errors
Some sensors will give their result as an analog voltage. For example, the LM35 temperature
sensor outputs a voltage where each 0.01V corresponds to a Celsius degree. To get a number from
this output we use the microcontroller’s ADC (analog to digital converter).
When connecting an analog signal to an ADC we have to consider the ADC input impedance
and capacitance. The first can cause the signal to drop a little and the second limits how fast
the signal can change. If the signal is outside the range of the ADC we will have to reduce it
Using Sensors 9
(typically using a resistor divider) or amplify it (using active components). Both can affect the
signal. Electrical noise (from other parts of the circuit) can also affect the signal.
ADCs work by comparing the signal to a reference voltage and this is also a source for errors. The
reference voltage can change from one particular chip to the other and vary with the temperature
or the supply voltage.
When converting the number returned by the ADC into a voltage we normally assume that the
ADC is perfectly linear. This is not always the case, some devices can have a significant deviation.
The RP2040’s ADC is very good, but it is not perfect. While it will give a 12-bit result, the
Raspberry Pi Foundation estimates the effective number of bits as a little below 9.
Averaging Readings
A common practice to minimize random variations in the readings is to take an average of
multiple readings. While the random variations should cancel, if the property is really changing
the averaged reading will “lag” behind.
A simple way to use averaging is to make “n” readings, add them and then divide the sum by “n”,
as in the following C++ code:
There are a few things to notice about this strategy when compared to a single reading:
We can make it work faster (using more memory) by doing a moving average. We keep in
memory the last “n” readings, at each reading we discard the oldest (once we filled our history
of readings), get a new one, and compute the average:
Using Sensors 10
Moving averages make it more evident how old readings affect the current average.
When choosing an average strategy you should consider:
If a single reading is fast, a simple average will get a good “instantaneous” value. If it takes time
to get a reading, you may try a low “n” and/or use a moving average.
Calibration
Even in the best conditions, you will get variations between the readings from a sensor and the
real value of whatever you want to measure. One way to account for variations is to check the
Using Sensors 11
readings against values known to great precision and incorporate in your code the necessary
corrections. The process of registering these known values is a calibration.
Before embarking on this route, you need to understand from where the variations come.
Calibration works best when the variation is fixed in the normal working conditions.
Depending on what causes the variations you can use calibration:
• During development, if the variation does not depend on individual components or the
environment
• At “factory”, if the variation depends on individual components but not of the environment
• As a periodic procedure, if the variation depends on the environment but will change
slowly.
If the variations themselves can change fast during normal operation, calibration is probably not
a good option. You probably need a better sensor.
The actual correction from known values can be done, in most cases, by interpolating, as shown
in the example code in C++ below:
Readings Validation
Sensors can go wrong, by internal or external failure. If you are using the readings for some
important decision, you must check the values you got and plan some kind of safe mode if
Using Sensors 12
they are unreasonable. While in some cases the result of using a wrong reading is just comical,
some serious disasters have been caused by it.
The first check is range. A sensor has a range of values that it can report, anything outside
this range means it is not working properly. There is also a range of values for your particular
application.
For example, suppose you are building a thermostat for a fish bowl. The temperature sensor may
be able to measure temperatures between -50 and +150 Celsius, but the expected range is less
than that. A -73C reading is clearly a sensor failure, a +110C reading is very suspicious. If you
got a bad reading, what should you do? Keep the heater on and risk boiling the water or keep it off
and risk freezing? The “right” answer will depend on the characteristics of the bowl, the ambient,
and the fish. You should also have some way to signal the error so someone can intervene.
A second check is the rate of change. Most physical properties do not change abruptly, if you are
getting values all over the valid range something is probably amiss, even if the values by itself
look normal.
In some cases, you will use multiple sensors. If this is the case, you have to check if the sensors
agree with each other and, if not, decide what you should do.
Programming the Raspberry Pi Pico
There are quite a few options to write programs for the Pico boards. In this chapter, we will look
into the four environments we are going to use in the examples:
I assume you have a basic understanding of C/C++ and Python. A good introduction to these
languages needs a whole book (and there are many available).
Libraries
An important idea in software development is to reuse code. When it comes to using sensors,
there is a lot of good code already available.
The naive way to reuse code is just to paste it into your program. This way the reused code is
indistinguishable from the rest of the code, making it hard to be upgraded and reused.
The languages and environments we are going to use have ways to separate reusable code from
the application itself. I will call this reusable code “library” even when it is not in the specific
way used for the environment.
To use a library with the environments I mentioned, you have to place a copy of it on your
computer (and, in the case of MicroPython and CircuitPython, in your Pico board). There are
some automated and manual ways of doing this. Some libraries we are going to use are available
at github (in case you don’t know about it, github is a place to store code, among other things).
You can download them manually from the github page (the repository) as a zip file or you can
use the git program to download them.
git is a utility that records and manages changes in files. It has many commands and its full
use can be very challenging, but to download a repository you simply do github clone url
replacing url with the repository address.
Programming the Raspberry Pi Pico 14
A github repository
A good thing about having code in a separate library is that it makes it easy to upgrade… as long
as the fixes and enhancements do not require changes in the main application. For this reason, I
will document the version I used in my tests (when available).
Do not forget to check the license of the library (or any code you reuse). It will stipulate where
you can use the code and what are your obligations.
• With the Pico disconnected from the PC, press and hold the BOOTSEL button.
• Connect the Pico to the PC, keeping the BOOTSEL button pressed.
• Release the BOOTSEL button, in a few seconds an RP2 drive will show up.
• Copy the uf2 file to this drive.
• At the end of the copy the Pico will reset and start the application.
³https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf
Programming the Raspberry Pi Pico 15
Proper libraries will be compiled in .lib files, but sometimes it is just one (or more) .cpp (or .c)
files with the code and a .h (or .hpp) file with the needed declarations.
To inform how to generate the .uf2 file, a CMakeLists.txt file is used. This file is processed by
the utility CMake to create the make files that have detailed instructions of what needs to be
done when a file is changed. If you are doing things at the console, you will have to run CMake
each time you change CMakeLists.txt and make each time you change your source files. If you
are using an IDE (like Microsoft Visual Studio Code) these steps can be done automatically for
you.
I am including this environment for the more experienced readers. If you want to know more
about it, look for my book “Knowing the RP2040”.
https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_in-
dex.json”. Press OK to close the dialog.
3. Select Tools->Boards->Board Manager in the IDE, type “pico earle” in the search box and
find “Raspberry Pi Pico/RP2040” by Earle F. Philhower, III. Click the “Add” button to install.
Programming the Raspberry Pi Pico 16
After finishing the installation, select the “Raspberry Pi Pico” board at the top of the IDE. The
first time you load a program you will need to press the BOOTSEL button before connecting the
board to the PC.
By default, the coding language for the Arduino IDE is C++ (you can also program in plain C).
The IDE does a little processing in the source code before passing it to the compiler:
These changes are not made on your source file, the IDE will create a temporary file that will be
fed to the compiler.
The traditional C++ main() function is inside the Arduino runtime. It does something like
Programming the Raspberry Pi Pico 18
1 main () {
2 setup();
3 while (true) {
4 loop();
5 }
As a result, when you create a new program (sketch) in the Arduino IDE the editor starts with:
1 void setup() {
2 // put your setup code here, to run once:
3 }
4
5 void loop() {
6 // put your main code here, to run repeatedly:
7 }
The code on setup() will run once at the start and the code in loop() will be run over and over
again.
The IDE includes a way to install libraries from a zip file or download them from definitions at
an official online directory.
The Arduino runtime includes functions and classes for digital input and output, analog input,
and communication (UART, SPI, I2C), among others. The official documentation for the Arduino
runtime is at https://www.arduino.cc/reference/en/. We will see more about these functions in
the next chapter, but I will present here the ones related to time:
MicroPython
MicroPython is a lean and efficient implementation of the Python 3 programming language that
includes a small subset of the Python standard library and is optimized to run on microcontrollers.
The official MicroPython site is https://micropython.org/.
There are some special classes included in the module “machine” as part of the adaptation (port)
of MicroPython to the Raspberry Pi Pico (and other RP2040 boards):
• Pin
• Timer
• UART
• ADC
• SPI
• I2C
• sleep(t) suspends execution for t seconds. t can be a float value, so sleep(0.1) sleeps
for a tenth of a second.
• sleep_ms(t) suspends execution for t milliseconds. t should be an integer value.
• sleep_us(t) suspends execution for t microseconds. t should be an integer value.
• ticks_ms() returns the number of milliseconds since the Pico was powered up. This value
will wrap around after some time and should not be used directly with arithmetic or
comparison operators.
• ticks_diff(t1, t2) returns the number of milliseconds between two values returned
by ticks_ms(), as long as there has been no more than one wrap-around between them.
There is also an rp2 module that includes support for the PIO (Programmable I/O). A detailed
discussion of the PIO and its use in MicroPython is beyond the scope of this book, we will use it
more like a “black box”. You can find more about the PIO in my “Knowing the RP2040” book.
MicroPython has also some support for concurrency, including the ability to run code in the
second ARM core.
To use MicroPython, you have first to download the interpreter as a .uf2 file from
https://micropython.org/download/rp2-pico/ and install it on the Pico board.
Programming the Raspberry Pi Pico 21
MicroPython will create a disk-like storage in the Pico for your programs and libraries, but it
won’t be visible to a PC connected through USB.
Interaction with MycroPython is through the Interactive Interpreter Mode, commonly called the
REPL (read-eval-print-loop). You will need an IDE to edit programs, save them in the Pico and
execute them. At the end of this chapter, I will talk about the Thonny IDE.
If you save your MicroPython program in the Pico with the name main.py, it will start
automatically when the board is powered or reset. Libraries should be stored in the Pico in the
/lib directory.
One thing you may find confusing at first when using methods in Python is that there are two
ways to specify parameters:
• positional: parameters are associated in the order they appear in the call
• keyword: the association is made explicit by using a keyword
The order and keywords come from the definition of the method. For example, the method init
for the class UART is defined as
UART.init(baudrate=9600, bits=8, parity=None, stop=1, *, ...)
Where the *, ... means that other keyword-only parameters can follow the first 4 parameters.
Here are some uses of this method:
CircuitPython
CircuitPython is a “fork” (derivation) of the MicroPython. It uses most of the MicroPython
implementation but changes the way the microcontroller resources are used. Instead of a machine
module, with classes that change for each supported board, CircuitPython has core modules
intended on being consistent across all boards. Among them, we have:
• analogio
• board (includes board specific pin names)
• busio (includes support for hardware I2C, SPI and UART)
• digitalio
• sleep(t) suspends execution for t seconds. t can be a float value, so sleep(0.1) sleeps
for a tenth of a second.
• monotonic_ns() returns a counter of nanoseconds that is guaranteed to always increase.
Thonny IDE
The best way to develop programs in MicroPython and CircuitPython is by using an IDE
(Integrated Development Environment).
Programming the Raspberry Pi Pico 23
Thonny is very easy to install and use; it is available for Windows, Linux, and MacOS from
https://thonny.org. There are more powerful IDEs, but Thonny this a good one to start learning
MicroPython or CircuitPython.
Instructions for downloading and installing Thonny can be found at the official site, I will focus
here on a few points regarding its usage.
Thonny IDE
Setting Up Thonny
The first thing you should do is use the menu Tool Options… and click on the Interpreter tab. Select
“MicroPython (Raspberry Pi Pico)” or “CircuitPython (generic)” according to the interpreter
you are using. You can leave the port selection to automatic.
Next, go to the View menu and make sure that Files and Shell are selected.
Once you get the >>> prompt in the shell window, you can type Python commands for immediate
execution.
If it is a new file, Thonny will ask if you want to save it on the PC or on the board. It may take
you some time to get used to this. If you pay close attention to the filename in the tab, you will
notice that files in the board are indicated by [ ]. You will want to keep up-to-date copies of the
programs on the PC and the board.
Running a Program
During testing, you will use the Run icon (or F5) to run the program in the current editor tab (pay
attention that you have the correct tab selected).
To have a program run when the board is powered up or reset, save it to a file named main.py.
C/C++ SDK
In the SDK, the output is done through the C standard output. This is set up by the line pico_-
enable_stdio_usb(target 1) in the CMakeLists.txt file. The printf() function is used to
output messages.
To see the messages you will need a serial communication program. PuTTY (available at
https://putty.org/) is a popular communication program for Windows. In Linux, you can use
minicom or putty. You can also use the Serial Monitor in the Arduino IDE.
To connect to the virtual serial port you will need to know its name. In Windows, you can see it
using the Device Manager in the Control Panel. In Linux, it has a name like /dev/ttyUSBn.
Arduino
In Arduino, the output is done using the print() and println() methods of Serial. A
limitation of these methods is that they do not allow formatting and printing multiple variables.
One way to overcome this is to use the C sprintf to format the message into a string and then
output it.
The Arduino IDE includes a Serial Monitor that can be used to see the messages sent to Serial.
The name of the port is the same one used to load programs to the Pico.
• If you want to do professional programming, take the trouble to learn C and C++ and use
the official SDK. This will give you complete control over the hardware and allow you to
build efficient and reliable code, at the cost of more effort.
• If you want to accelerate your development, MicroPython and CircuitPython are the way.
MicroPython will give you better access to some Pi Pico features, and CircuitPython has
more library support (especially if you are using Adafruit modules).
• Arduino lies somewhere in the middle. It has a huge community and tons of examples on
the Internet (but only a small part is Pico-specific).
Interfaces and Protocols
There are many ways a sensor can be connected to a microcontroller. In this chapter, we will look
at a few of them, especially the standard ways used with various sensors (and other devices).
We will also look at how to use these interfaces in the various programming environments. We
will focus on the more common and useful ways of using the interfaces and not try to cover all
the objects, methods, and functions available in each environment.
• a VBUS pin, that is directly connected to the +5 VDC from the USB.
• a VSYS pin, this pin connects to the VBUS through a diode. We won’t be using this pin,
more information about it can be found in the Raspberry Pi Pico datasheet.
• a 3V3 pin that outputs 3.3 VDC (generated from VBUS).
• eight GND (ground) pins.
Interfaces and Protocols 28
One of the simplest electronic components is the resistor. When a voltage is applied to a resistor,
a current flows through it. The ratio between the voltage and current in a resistor is fixed (as long
as the resistor is under its proper working conditions), this is its resistance. This is summarized
by Ohm’s law:
V
R= I
to GPIO29. In the Pi Pico, only three of these pins are available for external connection. You can
select one input at a time or enable a mode where the channels are automatically changed after
each reading.
When a reading is completed, the result can be put in a four-element FIFO (First In First Out)
queue where it can be read by the ARM processors.
(V −0.706)
T = 27 − 0.001721
As the sensor is inside the chip, it will measure the chip’s temperature, not the ambient
temperature. So, for most applications, it is not a substitute for an external temperature sensor.
The temperature sensor must be enabled (powered on) before use.
Select the ADC input channel. Channels 0 to 3 correspond to GPIO26 to GPIO29, and channel 4
is the temperature sensor.
static uint adc_get_selected_input (void)
Returns the currently select ADC input channel (0 to 4).
static void adc_set_round_robin (uint input_mask)
input_mask should be between 0 and 31 (0x1F). If input_mask is zero, round robin will be
disabled. If not zero, round robin is enabled and the bits with value 1 in input_mask indicate
the channels to be sampled.
static void adc_set_temp_sensor_enabled (bool enable)
If enable is true the temperature sensor will be powered up. If enable is false it will be turned
off.
static uint16_t adc_read (void)
Performs an ADC conversion. Waits for the result and returns it.
The resolution affects only the returned value, the RP2040 microcontroller will always
use a 12 bit resolution.
1 import analogio
2 from board import *
3
4 pin = analogio.AnalogIn(A0)
5 print(pin.value)
Accessing the value property will cause an ADC conversion. The result is 16 bits (0 to 65535).
The internal temperature sensor is read through the temperature member of the cpu class in the
microcontroller module:
1 import microcontroller
2 print(microcontroller.cpu.temperature)
Voltage Divisor
The GPIO in the Pico has many features we will not talk about here. We will stick
with the basics you will need to interface with sensors.
Initializes a pin for GPIO use. The pin is configured for input.
void gpio_init_mask (uint gpio_mask)
Initializes the pins select by mask for GPIO use. The pins are configured for input.
static void gpio_set_dir (uint gpio, bool out)
Sets the direction (out = true for output, false for input) of a GPIO pin.
static void gpio_set_dir_masked (uint32_t mask, uint32_t value)
Sets the direction of the pins selected by mask. Each bit in value correspond to a pin (0 for input,
1 for output).
static void gpio_pull_up (uint gpio)
Connects the pull-up resistor.
static void gpio_pull_down (uint gpio)
Connects the pull-down resistor.
void gpio_set_pulls (uint gpio, bool up, bool down)
Control both pull resistors, true means connect, false disconnect.
static bool gpio_get (uint gpio)
Get the current state of a pin (0 for low, 1 for high).
static uint32_t gpio_get_all (void)
Get the current state of all pins. Each bit in the result corresponds to a pin (0 for low, 1 for high).
static void gpio_put (uint gpio, bool value)
Changes the state (value = true for high, false for low) of a GPIO pin.
static void gpio_put_masked (uint32_t mask, uint32_t value)
Changes the state of the pins selected by mask. Each bit in value correspond to a pin (0 for low,
1 for high).
• Waiting for the pin to be at the “not value” level. If this is not detected in the timeout, 0 is
returned
• Waiting for the pin to change from “not value” to “value”. If this change is not detected in
the timeout, 0 is returned
• Starting the timing
• Wait for the pin to change from “value” to “not value”. If this change is not detected in the
timeout, 0 is returned
• Return the time the pin stayed in the “value” level.
1 import digitalio
2 import board
3
4 # Read pin GP3
5 pin3 = digitalio.DigitalInOut(board.GP3)
6 pin3.pull = digitalio.Pull.UP
7 print(pin3.value)
8
9 # Change level of GP10 to HIGH
10 pin10 = digitalio.DigitalInOut(board.GP10)
11 pin10.direction = digitalio.Direction.OUTPUT
12 pin10.value = True
• 32 position queues (FIFOs - First In First Out) for transmission and reception
• programmable baud rate generator
• support for 5, 6, 7, and 8 bits of data, 1 or 2 stop bits, parity none, even or odd (see framing
in the next section)
• break detection and generation
• support for hardware flow control
• interrupt and DMA support
UART is not a very common interface for sensors, but we are going to see a couple of them in
the final chapters.
Interfaces and Protocols 38
Framing
When no communication is taking place, the signal stays at a high (“1”) level.
Individual words (typically a byte) are sent as a “frame” composed of:
• start bit: The signal goes to a low level and stays there for a “bit time” (as defined by the
baud rate). The change from high to low signals the receiver that a frame is starting and is
used to determine where individual bits will be.
• data bits: The individual bits of the word. The least significant bit is sent first and the most
significant bit is sent last.
• parity bit: optional bit used to detect communication errors. If using even parity, the total
number of “1” bits (considering the data bits and the parity bit) is even. In odd parity the
total number of “1” bits is odd.
• stop bit(s): the signal is kept at a high level to signal the end of the frame. Some really old
equipment required two stop bits (that is, the signal is kept high for at least two-bit times
before the next start bit); nowadays one stop bit is standard. As the beginning of the next
start bit is asynchronous to the stop bits, the line can be kept high for any time after the
minimum stop bit.
A special condition is break, where the signal is kept low for longer then the time to transmit a
word.
• RTS (Request To Send): This is an output signal that goes (in the standard) to a high level
to indicate that the DTE wants to transmit.
• CTS (Clear To Send) This is an input signal, high level means that the DCE can accept data
from the DTE.
The use of these signals is called hardware flow control, as opposed to software flow control
(where special characters or messages in the data signal when transmission must stop and
resume).
The RP2040 UARTs support the use of RTS and CTS in a non-standard way to control reception
and/or transmission:
• In RTS flow control, the RTS signal is used to inform the other side when to transmit. It will
be high as long as there is a configurable space in the reception queue. When the reception
queue fills up, RTS goes down to inform the other side to stop transmission.
• In CTS flow control, the transmission of each word only starts when CTS is high.
To use the hardware flow control, RTS and CTS pins must be configured and flow control enabled.
In a typical hardware flow control configuration, you will enable both options and cross the RTS
and CTS signals of the two sides.
Pins Options
The RP2040 has a somewhat flexible mapping of pins for the serial interfaces (UART, SPI, and
I²C). The options for UART0 are:
Function GPIOs
Tx 0, 12, 16, 28
Rx 1, 13, 17, 29
CTS 2, 14, 18
RTS 3, 15, 19
Function GPIOs
Tx 4, 8, 20, 24
Rx 5, 9, 21, 25
CTS 6, 10, 22, 26
RTS 7, 11, 23, 27
Interfaces and Protocols 40
Returns the first byte of incoming serial data available (or -1 if no data is available).
int Serial.readBytes(byte *buffer, int length)
Reads bytes from the serial port into a buffer. The function terminates if the determined length
has been read, or it times out (see Serial.setTimeout()).
Return the number of bytes put in the buffer.
void Serial.setTimeout(long time)
Sets the timeout for Serial.readBytes(). time is in milliseconds.
int Serial.write(int val)
Sends a byte. Returns the number of bytes sent.
int Serial.write(byte *buf, int len)
Sends len bytes from buf. Returns the number of bytes sent.
Configuration can be done at creation time or by explicitly calling the init method:
UART(id, baudrate=9600, *, ...)
init(baudrate=9600, bits=8, parity=None, stop=1, *, ...)
The following are some of the keyword-only parameters available:
• baudrate (int) – the transmit and receive speed. The default is 9600.
• bits (int) – the number of bits per byte, 5 to 9. The default is 8.
• parity (Parity) – the parity used for error checking. The default is None.
• stop (int) – the number of stop bits, 1 or 2. Default is 1
• timeout (float) – the timeout in seconds to wait for the first character and between
subsequent characters when reading. Raises ValueError if timeout > 100 seconds. The
default is 1.
SPI topology
When using sensors, the Raspberry Pi Pico will always be the master and the sensors will be
slaves.
SPI Signals
SPI uses four signals:
• SCLK: is the serial clock (an output for the master and input for the slaves)
• MOSI: the master out slave in data signal (an output for the master and input for the slaves)
• MISO: the master in slave out data signal (an input for the master, output for the selected
slave, and high-impedance for non-selected slaves)
Interfaces and Protocols 46
• SS: the slave select data signal (an output for the master and input for the slaves). Each slave
has a separate SS signal. In most cases this is an active low signal: it is normally HIGH, and
a LOW level asserts the selection.
Some devices use other names for these signals, like SCK, DI, DO, and CS.
You will also see references to 3-wire SPI. This is a half-duplex electrical protocol where MOSI
and MISO are combined in a single bi-directional signal. The RP2040 SPI peripheral does not
support this use (but it can be easily implemented with the PIO).
SPI Modes
SPI has four modes based in what is the idle state of the SCLK line and what edge of SCLK is
used to clock the data out and in. This characteristics are called clock polarity (CPOL) and clock
phase (CPHA):
Pins Options
The RP2040 has a somewhat flexible mapping of pins for the serial interfaces (UART, SPI, and
I2C).
The options for SPI0 are:
Function GPIOs
SCLK 2, 6, 18, 22
MISO 0, 4, 16, 20
MOSI 3, 7, 19, 23
SS 1, 5, 17, 21
Function GPIOs
SCLK 10, 14, 26
MISO 8, 12, 24, 28
MOSI 11, 15, 27
SS 9, 13, 25, 29
Note that the SS pin is only relevant when operating in slave mode (the peripheral will
automatically test it). In master mode, it can be any digital output pin. When you are using
SPI to communicate with a single slave it is customary (but not obligatory) to use one of the SS
Interfaces and Protocols 48
The SPI functions are in hardware_spilibrary. The spi parameter should be spi0 or spi1.
uint spi_init (spi_inst_t *spi, uint baudrate)
Initializes an SPI interface. This function must be called before the others.
The interface is put in master mode and the clock is set to the closest value to the baudrate
available.
Returns the actual baudrate.
uint spi_set_baudrate (spi_inst_t *spi, uint baudrate)
Sets clock to the closest value to baudrate available.
Returns the actual baudrate.
static void spi_set_format (spi_inst_t *spi, uint data_bits, spi_cpol_t cpol,
spi_cpha_t cpha, __unused spi_order_t order)
Configures the format for an SPI interface:
• data_bits: 4 to 16
• cpol: clock polarity (SPI_CPOL_0 or SPI_CPOL_1)
• cpha: clock phase (SPI_CPHA_0 or SPI_CPHA_1)
• order: must be SPI_MSB_FIRST
1 #include <SPI.h>
Interfaces and Protocols 50
The two SPI interfaces in Pico are referenced by SPI (spi0) and SPI1 (spi1). The pins used can
be set by the setRX(), setTX(), and setSCK() methods; they must be called before the begin
method. The standard way in Arduino is for the program to directly control the SS line (using
the digital I/O functions).
The settings for the SPI interface are defined by a SPISettings object:
SPISetting(speed, dataOrder, dataMode)
Configuration can be done at creation time or by explicitly calling the init method:
SPI(id, ...)
init(baudrate=1000000, *, polarity=0, phase=0, bits=8, firstbit=SPI.MSB,
sck=None, mosi=None, miso=None)
The following are the parameters available:
The interface (spi0 or spi1) is selected based on the pins you requested.
Configurations are done by the configure method (the SPI object must be locked):
configure(baudrate, polarity, phase, bits)
• baudrate (int) – the transmit and receive speed (in Hz). The default is 100kHz.
• polarity (int) – the base state of the clock line (0 or 1). The default is 0.
• phase (int) – the edge of the clock that data is captured: first (0) or second (1). Rising or
falling depends on clock polarity. The default is 0.
Interfaces and Protocols 53
I2 C
I²C is a very popular electrical protocol for connecting all kinds of devices to microcontrollers.
The objective of I²C is to allow a simple, short-distance, connection of multiple low-to-medium
speed devices. While it was initially envisioned for in-board connections between integrated
circuits (hence the name Inter-Integrated Circuit), today you can find many modules (like sensors,
displays, and real-time clocks) that use it.
I2 C Topology
I²C is organized as a multiple drop bus, using only two connections (“wires”).
I²C distinguishes between masters (or controllers) and slaves (or targets). I will use the original
terminology (master/slave) as this is what you will find in most of the literature.
Interfaces and Protocols 55
I²C Topology
Each slave should have a unique address. All communications are started by a master, that selects
a slave by its address and informs if it should receive or transmit.
In this text I will concentrate on the most common configuration where there is only one master
(the Raspberry Pi Pico) and the slaves (sensors) have 7-bit addresses.
I2 C Electrical Interface
The two signals used by I²C are:
• SCL: this is the clock that marks where are the bits. This line is always driven by the master.
• SDA: this is the data line and is bi-directional.
To allow the direct connection of multiple devices in a single wire, the devices must have:
Pull-up resistors guarantee that the signal is high if no device is pulling it to low. The value of
these resistors depends upon:
• The capacitance of the connection. Higher capacitance requires lower resistors to guarantee
that the signals will change in a short time.
Interfaces and Protocols 56
• The speed of the communication. Higher speeds require faster signals change which
requires lower resistors.
• Allowed power consumption. Lower resistors will consume more power.
In some cases, the pull-up resistors inside the RP2040 microcontroller will be enough. Also, many
modules contain pull-up resistors.
Connecting 3.3V (like the Pi Pico) and 5V devices directly in the same I²C bus is not recommended,
as the 3.3V device will be submitted to voltages slightly above 3.3V. Nevertheless, it is common
practice for hobbyists. For professional designs, you should use an I²C level converter or use
MOSFETs as in the circuit below.
The idle condition is for the two signals to be at a HIGH level (due to the pull-up resistors, as no
one is pulling the signals down).
During normal communication, the transmitter should only change SDA when SCL is LOW. The
receiver will read SDA when SCL changes from LOW to HIGH.
Two special conditions violate this rule, to signal the start and end of a transaction:
• A Start condition is generated by pulling SDA LOW while SCL is HIGH. This marks the
start of a transaction.
• A Stop condition is generated by pulling SDA LOW while SCL is LOW, letting SCL go
HIGH, and then letting SDA go HIGH. This marks the end of a transaction.
The Stop can be combined with a Start to start a new transaction without releasing the bus: let
SDA go HIGH then let SCL go HIGH and then pull SDA LOW. This is called a Restart.
These conditions are always generated by the master.
After a START, the master will send the address of the slave and signal if its a read or write
operation:
In the figure above, the blue areas indicate where the master sets the SDA line, and the green areas
indicate where the slave reads the SDA line. The first 7 bits after the start is the slave address,
and the last bit is 0 for a write and 1 for a read.
The slave addressed must acknowledge the selection by sending a “0”.
Read Operation
• The master sends the slave address, followed by a “1” bit (indicating read).
• The slave pulls down SDA, acknowledging the address.
• The slave controls SDA, sends 8 bits, and releases SDA.
• The master keeps SDA HIGH for 1 bit to request another byte or pull it LOW (followed by
a STOP condition) to end the transaction.
Write Operation
Many sensors use some kind of internal addressing, such as a register address. This address is
normally sent by the master as the first bytes written. As a result, a read operation on a device
is an I²C write transaction followed by an I²C read transaction. This is a good opportunity to
combine the STOP and START conditions.
Function GPIOs
SDA 0, 4, 8, 12, 16, 20, 24, 28
SCL 1, 5, 9, 13, 17, 21, 25, 29
Function GPIOs
SDA 2, 6, 10, 14, 18, 22, 26
SCL 3, 7, 11, 15, 19, 23, 27
This routine will try to send len bytes starting from src. addr is the slave address.
If nonstop is false, a STOP condition will be generated after the last byte. In master mode, a
START condition will initiate the next transfer.
If nonstop is true, the STOP condition will not be generated and a RESTART will be used at the
start of the next transfer.
The routine will block indefinitely until all bytes are transferred (or the address is not acknowl-
edged).
Returns the number of bytes sent or PICO_ERROR_GENERIC (if something else went wrong like
the device not acknowledging the address).
int i2c_read_blocking (i2c_inst_t *i2c, uint8_t addr, uint8_t *dst, size_t
len, bool nostop)
This routine will try to read len bytes to the memory starting at dst. addr is the slave address.
If nonstop is false, a STOP condition will be generated after the last byte. In master mode, a
START condition will initiate the next transfer.
If nonstop is true, the STOP condition will not be generated and a RESTART will be used at the
start of the next transfer.
The routine will block indefinitely until all bytes are transferred (or the address is not acknowl-
edged).
Returns the number of bytes read or PICO_ERROR_GENERIC (if something else went wrong like
the device not acknowledging the address).
1 #include <Wire.h>
The two i2c interfaces in Pico are referenced by Wire (i2c0) and Wire1 (i2c1). The pins used can
be set by the setSDA() and setSCL() methods; they must be called before the begin method.
The Wire library supports both master and slave operation, for sensors we are only interested in
the master support.
One thing to remember is that I²C operations are buffered by the library. When reading,
requestFrom() will put all the data from the device in the buffer and later you get it using
read(). When writing, you use write to put data in the buffer and then call endTransmission()
to send it.
Interfaces and Protocols 61
• 0: success.
• 1: data too long to fit in transmit buffer.
• 2: received NACK on transmit of address.
• 3: received NACK on transmit of data.
• 4: other error.
• 5: timeout
Returns the next byte in the buffer or -1 if there is no data available in the buffer. To get data in
the buffer you need to call requestFrom().
setSDA(int pin)
Sets the SDA pin.
setSCL(int pin)
Sets the SCL pin.
Using I2 C in MicroPython
In MicroPython the I²C interfaces are accessed through the class I2C in the machine module.
Configuration can be done at creation time or by explicitly calling the init method:
I2C(id, *, scl, sda, freq=400000)
init(scl, sda, *, freq=400000)
The following are the parameters available:
There is a very useful method that is not directly related to transferring data:
I2C.scan()
Scan all I²C addresses between 0x08 and 0x77 inclusive and return a list of those that respond.
The actual communication methods can be divided into two groups:
• Standard bus operations are the generic read and write I²C operations
• Memory operations are methods for devices that use internal addressing
Interfaces and Protocols 63
Using I2 C in CircuitPython
In CircuitPython the I²C interfaces are accessed through the class I2C in the busio module.
CircuitPython controls the simultaneous use of the I²C by means of a lock; you have to lock
the I²C before using most of the methods.
Configuration is done at creation time:
classbusio.I2C(scl, sda, frequency)
The interface (i2c0 or i2c1) is selected based on the pins you requested.
The following methods are available:
scan()
Scan all I²C addresses between 0x08 and 0x77 inclusive and return a list of those that respond.
try_lock()
Attempts to grab the I²C lock. Returns True on success.
Returns True when the lock has been grabbed
unlock()
Releases the I²C lock.
readfrom_into(address, buffer, start, end)
Reads into buffer from the device selected by address. At least one byte must be read.
If start or end is provided, then the buffer will be sliced as if buffer[start:end] were
passed, but without copying the data. The number of bytes read will be the length of
buffer[start:end].
Parameters:
| GND | GND
/cap4_interfaces-1.md
|
SDA GP8
SCL GP9
Connect the Pico to the PC and run Thonny. Type the following commands in the Shell window:
• HIGH-level voltage: this is critical If the sensor is powered by 5V check if the HIGH-level
voltage of the output is 3.3 or 5V. If it is 5V you cannot connect it directly to the Pico.
A simple voltage divisor made with two resistors will be sufficient for the sensors in this
chapter (see the Gas Sensor Example).
• Pinout: this is critical Boards with the same sensor but from different vendors may have
different pin assignments. Some boards may have additional pins that you may ignore
(many of the sensors we will see are available with an additional analog output). Take the
time to make sure which pins are used for powering the sensor.
• Power supply voltage: the Pico can supply 5 and 3.3 Volts, you must connect the power
supply pin on the sensor (normally marked Vcc) to the appropriate pin in the Pico.
• Logic level of the output: your sensor may output the complement of the sensor I used.
This will require small changes in the code.
• LEDs: the sensor boards may have one or two LEDs. A power LED will light up when the
sensor is powered. A LED showing the output of the sensor can be more tricky: sometimes
they will light when the sensor is triggered and sometimes when it is not triggered. The
sensor LED may also flick very briefly, making it hard to notice.
I wrote in the examples the characteristics of the particular sensors I used. If your sensor is
different you may need to:
Basic Digital Sensors 68
• Change the connections at the sensor (if the pin assignments are different).
• Power the sensor from a different pin in the Pico (if I used a 3.3V sensor and you got a 5V
sensor, or vice-versa).
• Add a voltage divisor between the sensor output and the Pico (if the HIGH level of the
output is 5V).
• Invert the logic in the code to reflect the output of your sensor.
• Show a count of how many times it was triggered (useful during testing to see if the output
will change multiple times in response to a single event).
• Make a sound or light a LED for a short time when it triggers, ignoring the output
meanwhile.
• Count how many times it triggers in a period and check if the count is bigger than a limit.
• Just outputting a message.
What you will do in your actual projects depends on what is your objective. If a gas sensor signals
a high concentration of a gas, maybe you will want to sound an alarm until a button is pressed.
On the other hand, you may decide that occasional detections of vibration can be ignored and
only take action if you get many detections in a certain period.
Buttons
A button (also called a momentary switch) connects its two contacts when pressed and discon-
nects them when released.
A Few Buttons
Basic Digital Sensors 69
Most of the time we will connect a button between a GPIO pin and GND and activate the internal
pull-up resistor in the GPIO pin. When the button is released the pin will be at a HIGH level,
pressing the button will change it to a LOW level. As a safety measure, you might put a 1k to 10k
resistor in series with the switch, to avoid damage if the GPIO is programmed to output a HIGH
level and the button is pressed.
The trouble with buttons is that, depending on their construction and how you press it, the
connection can be made and broken multiple times while it is being pressed or released. A possible
(but far from unique) cause is that the contacts can bounce when moved. Regardless of the cause,
the techniques to overcome this is commonly called debouncing.
A very complete document on debouncing can be found at http://www.ganssle.com/debouncing.htm.
We are going to use a very simple debounce by software in our examples.
The button used above is a pushbutton that has four pins that are connected two by two inside.
Basic Digital Sensors 70
Pushbutton connections
The code in the next sections will increment a counter each time the button is pressed. The counter
is sent to the PC connected through the USB.
The important point in the code is a routine (or method) that returns true if the button is
pressed and false otherwise. Debouncing is done by only considering changes that are stable
for a minimum time. The routine must be called frequently for the debounce to work.
You should try the code with different buttons and experiment with different debounce times.
1 /**
2 * @file button_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Button Example
5 * @version 1.0
6 * @date 2023-04-18
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include "pico/stdlib.h"
14 #include "hardware/gpio.h"
15
16
17 static inline uint32_t board_millis(void)
Basic Digital Sensors 71
18 {
19 return to_ms_since_boot(get_absolute_time());
20 }
21
22 class Button {
23 private:
24 int pinButton;
25 bool pressed;
26 int debounce;
27 bool last;
28 uint32_t lastTime;
29
30 public:
31
32 // Constructor
33 Button (int pin, int debounce = 20) {
34 pinButton = pin;
35 gpio_init(pinButton);
36 gpio_set_pulls(pinButton, true, false);
37 pressed = false;
38 this->debounce = debounce;
39 last = gpio_get(pinButton) == 0;
40 lastTime = board_millis();
41 }
42
43 // Teste if Button is pressed
44 bool isPressed() {
45 bool val = gpio_get(pinButton) == 0;
46 if (val != last) {
47 // reading changed
48 last = val;
49 lastTime = board_millis();
50 } else if (val != pressed) {
51 int dif = board_millis() - lastTime;
52 if (dif > debounce) {
53 // update button status
54 pressed = val;
55 }
56 }
Basic Digital Sensors 72
57 return pressed;
58 }
59 };
60
61 // Main Program
62 int main() {
63 // Init stdio
64 stdio_init_all();
65 #ifdef LIB_PICO_STDIO_USB
66 while (!stdio_usb_connected()) {
67 sleep_ms(100);
68 }
69 #endif
70
71 printf("\nButton Example\n");
72
73 Button button(15);
74 int counter = 0;
75
76 while(1) {
77 printf ("Button pressed %d times\n", counter);
78 while (button.isPressed())
79 ;
80 while (!button.isPressed())
81 ;
82 counter++;
83 }
84 }
In this example, I am using C++ so I can create a class that encapsulates the button code.
Arduino Code
Basic Digital Sensors 73
1 // Button Example
2
3 class Button {
4 private:
5 int pinButton;
6 bool pressed;
7 int debounce;
8 bool last;
9 uint32_t lastTime;
10
11 public:
12
13 // Constructor
14 Button (int pin, int debounce = 20) {
15 pinButton = pin;
16 pinMode (pinButton, INPUT_PULLUP);
17 pressed = false;
18 this->debounce = debounce;
19 last = digitalRead(pinButton) == LOW;
20 lastTime = millis();
21 }
22
23 // Test if Button is pressed
24 bool isPressed() {
25 bool val = digitalRead(pinButton) == LOW;
26 if (val != last) {
27 // reading changed
28 last = val;
29 lastTime = millis();
30 } else if (val != pressed) {
31 int dif = millis() - lastTime;
32 if (dif > debounce) {
33 // update button status
34 pressed = val;
35 }
36 }
37 return pressed;
38 }
Basic Digital Sensors 74
39 };
40
41 // Global variables
42 Button button(15);
43 int counter = 0;
44
45 // Initializations
46 void setup() {
47 Serial.begin(115200);
48 }
49
50 // Main loop
51 void loop() {
52 Serial.print ("Button pressed ");
53 Serial.print (counter);
54 Serial.println (" times");
55 while (button.isPressed())
56 ;
57 while (!button.isPressed())
58 ;
59 counter++;
60 }
This is very similar to the SDK code. Notice how the Arduino functions hide some of the low-level
details.
MicroPython Code
1 # Button Example
2
3 from machine import Pin
4 import time
5
6 class Button:
7 def __init__(self, pin, debounce=20):
8 self.pinButton = Pin(pin, Pin.IN, Pin.PULL_UP)
9 self.pressed = False
Basic Digital Sensors 75
10 self.debounce = debounce
11 self.last = self.pinButton.value()
12 self.lastTime = time.ticks_ms()
13
14 def isPressed(self):
15 val = self.pinButton.value()
16 if val != self.last:
17 # reading changed
18 self.last = val
19 self.lastTime = time.ticks_ms()
20 elif (val == 0) != self.pressed:
21 dif = time.ticks_diff(time.ticks_ms(), self.lastTime)
22 if dif > self.debounce:
23 # update button state
24 self.pressed = val == 0
25 return self.pressed
26
27 # Test program
28 button = Button(15)
29 counter = 0
30 while True:
31 print ("Button pressed {} times".format(counter))
32 while button.isPressed():
33 time.sleep_ms(1)
34 while not button.isPressed():
35 time.sleep_ms(1)
36 counter = counter+1
37
CircuitPython Code
Basic Digital Sensors 76
1 # Button Example
2
3 import digitalio
4 import board
5 import time
6
7 def time_ms():
8 return time.monotonic_ns() // 1000000
9
10 class Button:
11 def __init__(self, pin, debounce=20):
12 self.pinButton = digitalio.DigitalInOut(pin)
13 self.pinButton.pull = digitalio.Pull.UP
14 self.pressed = False
15 self.debounce = debounce
16 self.last = self.pinButton.value
17 self.lastTime = time_ms()
18
19 def isPressed(self):
20 val = self.pinButton.value
21 if val != self.last:
22 # reading changed
23 self.last = val
24 self.lastTime = time_ms()
25 elif (val == 0) != self.pressed:
26 dif = time_ms() - self.lastTime
27 if dif > self.debounce:
28 # update button state
29 self.pressed = val == 0
30 return self.pressed
31
32 # Test program
33 button = Button(board.GP15)
34 counter = 0
35 while True:
36 print ("Button pressed {} times".format(counter))
37 while button.isPressed():
38 time.sleep(0.001)
Basic Digital Sensors 77
Very similar to the MicroPython code, you can see how CircuitPython uses different classes for
interacting with the hardware.
Reed Switch
A reed switch is a switch that is controlled by an external magnetic force.
Reed Switch
The most common model contains a pair of magnetizable, flexible, metal reeds whose end
portions are separated by a small gap when no magnetic field is present. The reeds are
hermetically sealed within a tubular glass envelope. When a small magnet is placed next to it
the reeds will move and close the switch.
Reed switches are commonly used in anti-bugler alarms, to detect if a door or window is open
or closed.
From a programming viewpoint, a reed switch can be treated the same way as a button.
Keypads
A keypad is just several buttons connected in a matrix to reduce the number of GPIOs used.
Basic Digital Sensors 78
3x4 Keypad
Let’s start with a simple method to detect a single key pressed. We configure the row pins as
output and the columns pins as input with a pull-down. We put a HIGH level in one row at a
time (and a LOW level in the other rows) and read the level at the columns. A pressed key in that
row will read as HIGH and a released key will read as LOW.
The problem is that if you press two keys on the same column you end up connecting a HIGH-
level pin to a LOW-level pin.
Basic Digital Sensors 79
When you start pressing together multiple keys, another problem arises: ghosting. If you press
two buttons on the same row and a third button in the same column of one of them, the software
will think that a fourth button is pressed.
While you can write a routine that will check all the rows in a loop, a more common solution
is to check one row at each execution of the routine. This routine is then called periodically (for
example, from a timer interrupt if that is available in the programming environment). A code
is associated with each key, when a key is pressed the corresponding code is placed in a queue
(first-in, first-out). The main program will get the pressed keys from this queue and do whatever
needs to be done when they are pressed.
Keypad Example
In the keypad example, we will use a membrane 3x4 matrix Keypad. While they are pretty cheap,
they do not have diodes so you should not press multiple keys at the same time.
The diagram below shows the connection of the keypad to the Raspberry Pi Pico.
Basic Digital Sensors 81
The example code will send the key code to the PC connected through the USB.
The code may seem very complicated at first, but the algorithm for checking the keys in a row is
simple:
• We will only process key presses if we detect them multiple consecutive times.
• This is done by recording the previous state of each key (in a variable called previous) and
keeping a count for each key (in a variable called count). The count starts with a debounce
value and is decremented when the reading is the same as the previous until it reaches
zero.
• When the count reaches zero, we got a stable state and we check it with the previous one
(stored in a variable called validated). If they are different, validatedis updated, and, if
it’s a key press, the row and column are stored in a queue (called keys).
The main program continuously calls this function and check if there is a key in the queue.
1 /**
2 * @file keypad_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Keypad Example
5 * @version 1.0
6 * @date 2023-04-19
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include "pico/stdlib.h"
14 #include "hardware/gpio.h"
15 #include "hardware/sync.h"
16
17
18 // Keypad pins
19 #define NROWS 4
20 #define NCOLUMNS 3
21 #define ROW_1 0
22 #define ROW_2 1
23 #define ROW_3 2
24 #define ROW_4 3
25 #define COL_1 4
26 #define COL_2 5
27 #define COL_3 6
28
29 #define MSK_ROWS ((1 << ROW_1) | (1 << ROW_2) | (1 << ROW_3) | (1 << ROW_4))
30 #define MSK_COLS ((1 << COL_1) | (1 << COL_2) | (1 << COL_3))
31
32 // Debounce
33 #define DEBOUNCE 5
34
35 // Keypad control variables
36 int rows[] = { ROW_1, ROW_2, ROW_3, ROW_4 };
37 int columns[] = { COL_1, COL_2, COL_3 };
38 int curRow;
Basic Digital Sensors 83
117 stdio_init_all();
118 #ifdef LIB_PICO_STDIO_USB
119 while (!stdio_usb_connected()) {
120 sleep_ms(100);
121 }
122 #endif
123
124 printf("\nKeypad Example\n");
125
126 // init keypad controls
127 curRow = 0;
128 for (int i = 0; i < NROWS; i++) {
129 previous[i] = validated[i] = 0;
130 for (int j = 0; j < NCOLUMNS; j++) {
131 count[i][j] = DEBOUNCE;
132 }
133 }
134
135 // init keys queue
136 inkey = outkey = 0;
137
138 // init keypad pins
139 gpio_init_mask (MSK_ROWS | MSK_COLS);
140 for (int i = 0; i < NCOLUMNS; i++) {
141 gpio_set_pulls(columns[i], true, false);
142 }
143
144 // Scan keypad every 10 millseconds
145 add_repeating_timer_ms(10, checkRow, NULL, &timer);
146
147 // Main loop
148 while(1) {
149 int key = getKey();
150 if (key == -1) {
151 sleep_ms(100);
152 } else {
153 printf ("Pressed %c\n", key);
154 }
155 }
Basic Digital Sensors 86
156 }
In this code, we use the timer support in the SDK to run the routine that checks for key presses
every 10 milliseconds. The routine will be run inside the timer interrupt, stopping momentarily
the main execution.
Arduino Code
1 // Keypad Example
2
3 // Keypad pins
4 #define NROWS 4
5 #define NCOLUMNS 3
6 #define ROW_1 0
7 #define ROW_2 1
8 #define ROW_3 2
9 #define ROW_4 3
10 #define COL_1 4
11 #define COL_2 5
12 #define COL_3 6
13
14 // Debounce
15 #define DEBOUNCE 5
16
17 // Keypad control variables
18 int rows[] = { ROW_1, ROW_2, ROW_3, ROW_4 };
19 int columns[] = { COL_1, COL_2, COL_3 };
20 int curRow;
21 bool previous [NROWS][NCOLUMNS];
22 bool validated [NROWS][NCOLUMNS];
23 int count [NROWS][NCOLUMNS];
24
25 // Key codes
26 char codes[NROWS][NCOLUMNS] = {
27 { '1', '2', '3' },
28 { '4', '5', '6' },
29 { '7', '8', '9' },
Basic Digital Sensors 87
69 // restart validation
70 previous[curRow][col] = read;
71 count[curRow][col] = DEBOUNCE;
72 }
73 }
74 // return row to input
75 pinMode (pin, INPUT);
76 // move to next row
77 curRow = (curRow+1) % NROWS;
78 }
79
80 // Read a key from the key queue, returns -1 if queue empty
81 int getKey(void) {
82 int key = -1;
83 if (inkey != outkey) {
84 key = keys[outkey];
85 outkey = (outkey == MAX_KEYS) ? 0 : (outkey + 1);
86 }
87 return key;
88 }
89
90 // Initializations
91 void setup() {
92 Serial.begin(115200);
93 Serial.println("Keypad Example");
94
95 // init keypad controls
96 curRow = 0;
97 for (int i = 0; i < NROWS; i++) {
98 for (int j = 0; j < NCOLUMNS; j++) {
99 previous[i][j] = validated[i][j] = false;
100 count[i][j] = DEBOUNCE;
101 }
102 }
103
104 // init keys queue
105 inkey = outkey = 0;
106
107 // init keypad pins
Basic Digital Sensors 89
This is similar to the SDK code, but the check for keys is done in the main loop.
MicroPython Code
1 # Keypad Example
2
3 from machine import Pin,Timer
4 from time import sleep
5
6 class Keypad:
7
8 # init
9 def __init__(self, rowPins, columnPins, debounce=5):
10 self.nr = len(rowPins)
11 self.nc = len(columnPins)
12 self.debounce = debounce
Basic Digital Sensors 90
52 # None if none
53 def getKey(self):
54 if len(self.keys) == 0:
55 return None
56 else:
57 return self.keys.pop(0)
58
59 pad = Keypad([0, 1, 2, 3], [4, 5, 6])
60
61 # timer interrupt handler
62 def scanKeypad(timer):
63 pad.checkRow()
64
65 Timer(mode=Timer.PERIODIC, period=10, callback=scanKeypad)
66
67 codes = [['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['*', '0', '#']]
68
69 # main loop
70 while True:
71 key = pad.getKey()
72 if key is None:
73 sleep(5)
74 else:
75 print('{} -> {}'.format(key, codes[key[0]][key[1]]))
76
In this code, we make use of MicroPython’s support for timers. We create a timer that will call
the function scanKeypad every 10 milliseconds. This call is done independently of the execution
of the main loop. We could also use the experimental _thread module and use the second ARM
core to execute the scanning.
Also, notice how the use of lists simplifies the code compared to the SDK and Arduino versions.
CircuitPython Code
Basic Digital Sensors 92
1 # Keypad Example
2
3 import digitalio
4 import board
5 from time import sleep
6
7 class Keypad:
8
9 # init
10 def __init__(self, rowPins, columnPins, debounce=5):
11 self.nr = len(rowPins)
12 self.nc = len(columnPins)
13 self.debounce = debounce
14 self.rows = [digitalio.DigitalInOut(pin) for pin in rowPins]
15 self.columns = [digitalio.DigitalInOut(pin) for pin in columnPins]
16 for digPin in self.columns:
17 digPin.pull = digitalio.Pull.UP
18 self.curRow = 0
19 self.previous = [[[False] for _ in range(self.nc)] for _ in range(self.nr)]
20 self.count = [[[self.debounce] for _ in range(self.nc)] for _ in range(self.nr)]
21 self.validated = [[[False] for _ in range(self.nc)] for _ in range(self.nr)]
22 self.keys = []
23
24 # check next row
25 def checkRow(self):
26 # set row to LOW
27 pin = self.rows[self.curRow]
28 pin.direction = digitalio.Direction.OUTPUT
29 pin.value = False
30 # read columns and check for changes
31 for col in range(self.nc):
32 read = self.columns[col].value
33 if read == self.previous[self.curRow][col]:
34 if self.count[self.curRow][col] != 0:
35 self.count[self.curRow][col] = self.count[self.curRow][col] - 1
36 if self.count[self.curRow][col] == 0:
37 # reading validated
38 if read != self.validated[self.curRow][col]:
Basic Digital Sensors 93
39 self.validated[self.curRow][col] = read
40 if read == False:
41 # keypress detected
42 self.keys.append((self.curRow, col))
43 else:
44 # restart validation
45 self.previous[self.curRow][col] = read
46 self.count[self.curRow][col] = self.debounce
47 # return row to input
48 pin.direction = digitalio.Direction.INPUT
49 # move to next row
50 self.curRow = self.curRow + 1
51 if self.curRow == self.nr:
52 self.curRow = 0
53
54 # return next key pressed
55 # None if none
56 def getKey(self):
57 if len(self.keys) == 0:
58 return None
59 else:
60 return self.keys.pop(0)
61
62 pad = Keypad([board.GP0, board.GP1, board.GP2, board.GP3],
63 [board.GP4, board.GP5, board.GP6])
64
65 codes = [['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9'], ['*', '0', '#']]
66
67 # main loop
68 while True:
69 pad.checkRow()
70 key = pad.getKey()
71 if key is None:
72 sleep(0.01)
73 else:
74 print('{} -> {}'.format(key, codes[key[0]][key[1]]))
75
CircuitPython does not support timers or threads, so we call scanKeypad in the main loop.
Basic Digital Sensors 94
Vibration Sensor
This particular sensor has a small moving part (the roller) that will close or break a connection
as the sensor is moved or vibrated. The circuit in the module uses this closing/breaking to
charge or discharge a capacitor, so its voltage indicates how frequently the connection changes.
The capacitor’s voltage is compared to a reference set with a potentiometer, resulting (in this
particular module) in a digital output that is HIGH if the vibration is above the threshold set by
the potentiometer.
The sensor works with voltages from 3,3 to 5 VDC, the supply voltage will define the HIGH level
of the digital output.
In the code examples, we will check the sensor at each 10 ms and keep a count of how many
times a vibration was detected in the last 100 readings. The Pico internal LED will light when the
count exceeds 80.
The algorithm to keep the count is a variation of the one we saw for moving averages.
In the Pico W the LED is not connected to the RP2040, so this code won’t work on it.
You can add a LED (in series with a 1k Ohm resistor) between one of the Pico W pins
and ground and change the LED pin number in the code.
1 /**
2 * @file vibration_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Vibration Sensor Example
5 * @version 1.0
6 * @date 2023-04-19
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
Basic Digital Sensors 96
11
12 #include <stdio.h>
13 #include "pico/stdlib.h"
14 #include "hardware/gpio.h"
15
16
17 #define LED_PIN 25
18 #define SENSOR_PIN 16
19
20 // Previous readings
21 #define N_SAMPLES 100
22 uint8_t vibr[N_SAMPLES];
23 int inVibr, outVibr, nVibr;
24 int count;
25
26 // Main Program
27 int main() {
28 // Init LED gpio
29 gpio_init (LED_PIN);
30 gpio_set_dir (LED_PIN, true);
31 gpio_put (LED_PIN, false);
32
33 // Init sensor gpio
34 gpio_init (SENSOR_PIN);
35
36 // Init samples
37 inVibr = outVibr = nVibr = 0;
38 count = 0;
39
40 // Main loop
41 while(1) {
42 sleep_ms(10);
43 if (nVibr == N_SAMPLES) {
44 // Remove oldest
45 count -= vibr[outVibr];
46 outVibr = (outVibr+1) % N_SAMPLES;
47 } else {
48 nVibr++;
49 }
Basic Digital Sensors 97
50 vibr[inVibr] = gpio_get(SENSOR_PIN);
51 count += vibr[inVibr];
52 inVibr = (inVibr+1) % N_SAMPLES;
53 gpio_put (LED_PIN, count > 80);
54 }
55 }
Arduino Code
29 nVibr++;
30 }
31 vibr[inVibr] = digitalRead(SENSOR_PIN);
32 count += vibr[inVibr];
33 inVibr = (inVibr+1) % N_SAMPLES;
34 digitalWrite (LED_BUILTIN, (count > 80)? HIGH : LOW);
35 }
MicroPython Code
CircuitPython Code
Basic Digital Sensors 99
PIR Sensor
Basic Digital Sensors 100
To be more precise, these sensors detect the change in infrared radiation caused by movement.
Each sensor has two infrared detectors and its output will change if the two detectors record
different radiation levels. A potentiometer defines the amount of change that will trigger the
output (the sensitivity or range of the sensor).
The top of the enclosure of the sensor is a lens to widen the vision field and focus the radiation
on the detectors.
There are many variations of this sensor, take the time to check the specifications of the one you
want to use. The sensor I am using has the following characteristics:
What happens when the sensor detects movement depends on a jumper and a second potentiome-
ter. The potentiometer determines the minimum time the output will stay high after a detection. If
the jumper is in the L position (non-retriggering), the output will stay high for just this minimum
time, even if new detections occur while the output is high. If the jumper is in the H position
(retriggering), a detection, while the output is high, will restart the time count, extending the
output pulse.
In most applications, you will leave the jumper in the H position (in my module there is a solder
bridge instead of a jumper). The output will stay high until there is no movement during the time
set by the potentiometer.
After the output returns to LOW, detections are ignored for a fixed time.
Notice that the sensor is powered by 5 VDC but its output is 3.3 VDC so it can be connected
directly to the Pico.
If your module has a trigger jumper, make sure it is in the H position. You can try different
positions for the potentiometer and see how it affects the output of the program.
1 /**
2 * @file pir_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Presence (PIR) Sensor Example
5 * @version 1.0
6 * @date 2023-04-19
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include "pico/stdlib.h"
14 #include "hardware/gpio.h"
15
Basic Digital Sensors 102
16
17 #define SENSOR_PIN 16
18
19 // Main Program
20 int main() {
21 // Init stdio
22 stdio_init_all();
23 #ifdef LIB_PICO_STDIO_USB
24 while (!stdio_usb_connected()) {
25 sleep_ms(100);
26 }
27 #endif
28
29 printf("\nPIR Example\n");
30
31 // Init sensor gpio
32 gpio_init (SENSOR_PIN);
33
34 // Main loop
35 int counter = 0;
36 while(1) {
37 printf ("Sensor triggered %d times\n", counter);
38 while (gpio_get(SENSOR_PIN) == 0)
39 ;
40 while (gpio_get(SENSOR_PIN) != 0)
41 ;
42 counter++;
43 }
44 }
Arduino Code
Basic Digital Sensors 103
MicroPython Code
11 time.sleep_ms(1)
12 while sensor.value() == 1:
13 time.sleep_ms(1)
14 counter = counter+1
CircuitPython Code
Flame Sensor
Flame sensors also work by detecting infrared radiation. Below is a photo of a typical sensor and
the typical circuit. Some sensors may omit the analog output pin.
Basic Digital Sensors 105
Flame Sensor
The actual sensor is a photo-transistor. It will conduct when it receives light at the right frequency
(typically 760 nm to 1100 nm). The higher the intensity of the detected light, the closer the analog
output will be to zero. The analog output is connected to a comparator (the other input of the
comparator is a reference voltage set by a potentiometer) to generate the digital output.
Two LEDs on the board indicate if it is powered up and if a flame is detected.
The sensor works with voltages from 3,3 to 5 VDC, the supply voltage will define the maximum
value of the analog output (if present) and the HIGH level of the digital output.
The code to test the Flame sensor detects when the output is LOW and activates the buzzer for
half a second.
20 // Main Program
21 int main() {
22 // Init buzzer gpio
23 gpio_init (BUZZER_PIN);
24 gpio_set_dir (BUZZER_PIN, true);
25 gpio_put (BUZZER_PIN, false);
26
27 // Init sensor gpio
28 gpio_init (SENSOR_PIN);
29
30 // Main loop
31 while(1) {
32 if (gpio_get(SENSOR_PIN) == 0) {
33 gpio_put (BUZZER_PIN, true);
34 sleep_ms (500);
35 gpio_put (BUZZER_PIN, false);
36 }
37 }
38 }
Arduino Code
MicroPython Code
CircuitPython Code
12 while True:
13 if sensor.value == 0:
14 buzzer.value = True
15 time.sleep(0.5)
16 buzzer.value = False
Sound Sensor
The sensor works with voltages from 3,3 to 5 VDC, the supply voltage will define the HIGH level
of the digital output.
There are a few versions of these sensors. Some have an analog and a digital output, while others
have only a digital output. Most have two LEDs, one for power and the other for the output. The
module I used outputs a HIGH level when a sound was detected, but the output LED is normally
ON and goes OFF when a sound was detected.
The code uses the LED in the Pi Pico: when sound is detected, the LED is lit for half a second.
In the Pico W the LED is not connected to the RP2040, so this code won’t work on it.
You can add a LED (in series with a 1k Ohm resistor) between one of the Pico W pins
and ground and change the LED pin number in the code.
1 /**
2 * @file sound_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Sound Sensor Example
5 * @version 1.0
6 * @date 2023-04-19
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
Basic Digital Sensors 111
12 #include <stdio.h>
13 #include "pico/stdlib.h"
14 #include "hardware/gpio.h"
15
16
17 #define LED_PIN 25
18 #define SENSOR_PIN 16
19
20 // Main Program
21 int main() {
22 // Init LED gpio
23 gpio_init (LED_PIN);
24 gpio_set_dir (LED_PIN, true);
25 gpio_put (LED_PIN, false);
26
27 // Init sensor gpio
28 gpio_init (SENSOR_PIN);
29
30 // Main loop
31 while(1) {
32 if (gpio_get(SENSOR_PIN) == 1) {
33 gpio_put (LED_PIN, true);
34 sleep_ms (500);
35 gpio_put (LED_PIN, false);
36 }
37 }
38 }
Arduino Code
Basic Digital Sensors 112
MicroPython Code
CircuitPython Code
MQ Gas Sensors
A gas sensor detects the presence of certain gases in the environment. They are commonly found
in devices that are used to detect the leakage of harmful gases and/or monitor the air quality.
MQ Gas Sensors
Basic Digital Sensors 114
The most common gas sensor is the MQ series, it has more than a dozen models, each one specific
to a certain gas. Here is a partial list of them:
Sensor Gas
MQ-2 Methane, Butane, LPG, smoke
MQ-3 Alcohol, Ethanol, smoke
MQ-4 Methane, CNG
MQ-5 Natural gas, LPG
MQ-6 LPG, butane
MQ-7 Carbon Monoxide
MQ-8 Hydrogen
MQ-9 Carbon Monoxide
MQ-131 Ozone
MQ-137 Ammonia
These sensors are based on a chemiresistor, a component that will change its resistance based
on the concentration of a certain gas. For better sensitivity, the chemisresistor is heated by an
internal heater. A metal mesh protects the sensor while allowing the air to contact it.
The MQ sensor usually has an analog output (that allows measuring of the concentration of
the gas) and a digital output (that signals when the concentration is above a level set by a
potentiometer).
These sensors need a 5V power supply and their high output is 5V, so they should not be
connected directly to the Pico.
Notice that we are using a voltage divisor to reduce the 5V sensor output to a level compatible
with the Pico.
The sample program will turn on the Pico LED for half a second when the sensor indicates a gas
level above the threshold set by the potentiometer.
Testing gas sensors can be tricky. As mentioned, the sensor has a heater inside and it takes a long
time for the sensor to come to its working temperature.
In the Pico W the LED is not connected to the RP2040, so this code won’t work on it.
You can add a LED (in series with a 1k Ohm resistor) between one of the Pico W pins
and ground and change the LED pin number in the code.
Arduino Code
MicroPython Code
Basic Digital Sensors 118
CircuitPython Code
KY-003 Module
The KY-003 module uses an A3144 Hall effect sensor from Allegro. This sensor has internally a
Hall element and the circuit to control a digital output.
we typically use a pull-up resistor. We can also use the sensor to direct control (through the
ground) a load of up to 50 mA.
If no magnetic field (with the right polarity) is present, the output is fluctuating (HIGH level if
we use a pull-up). When a magnetic field is applied (with the right polarity), the output will be
grounded (LOW level).
Notice that this sensor will work with only one of the poles of a magnet. The sensor has a
hysteresis - distinct levels for changing in one direction and the other. This means you will not
get multiple changes when it enters and leaves a magnetic field.
Like the A3144, its output is an “open drain”, meaning it can be fluctuating (disconnected) or
grounded (connected to the ground through an internal MOSFET). To get a HIGH level when
the output is fluctuating we typically use a pull-up resistor. We can also use the sensor to direct
control (through the ground) a load of up to 50 mA.
The way this sensor changes between these two states (disconnected and ground) is curious:
• In the beginning, without a magnetic field present, the output will be grounded (LOW
level).
• If we approach the South pole of a magnet to the sensor, it will activate and its output will
fluctuate (HIGH level if we use a pull-up).
• The output will stay fluctuating (even if we remove the magnet) until a North pole is
presented.
This behavior can be seen as a very radical “debouncing” because it guarantees a single activation
when a magnetic field is detected. On the other hand, you will need a situation where the magnet
is turned around or two magnets are used.
Basic Digital Sensors 121
If you look closely at a US1881 sensor, you will notice that the marking on it is
something like U18xxx. The U18 identifies the sensor as a US1881, the last three digits
are a date code.
I fixed (with hot glue) a wood disk to the motor shaft and glued two neodymium magnets to it.
These magnets must show different poles to the sensor. How to find the poles? Just remember
that equal poles repeal and different poles attract. If you put the two magnets near each other,
they will stick two different poles together. If you want to know with the North Pole, place the
magnet near a compass. The North Pole will repeal the North indication.
The code will briefly blink the Pico’s LED when the sensor is triggered.
In the Pico W the LED is not connected to the RP2040, so this code won’t work on it.
You can add a LED (in series with a 1k Ohm resistor) between one of the Pico W pins
and ground and change the LED pin number in the code.
1 /**
2 * @file hall_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Hall Effect Sensor Example
5 * @version 1.0
6 * @date 2023-04-18
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include "pico/stdlib.h"
15 #include "hardware/gpio.h"
16
17 // Pins used
18 #define LED_PIN 25
19 #define SENSOR_PIN 16
20 #define STEPPER_1 2
21 #define STEPPER_2 3
22 #define STEPPER_3 4
23 #define STEPPER_4 5
24
25 static inline uint32_t board_millis(void)
26 {
27 return to_ms_since_boot(get_absolute_time());
28 }
29
30 // what pins to turn on at each step
31 int steps[4][4] = {
32 { 1, 0, 0, 1 },
33 { 1, 1, 0, 0 },
34 { 0, 1, 1, 0 },
35 { 0, 0, 1, 1 }
36 };
37
38 // Stepper motor control class
Basic Digital Sensors 124
39 class Stepper {
40 private:
41 int pins[4];
42 int step;
43
44 void setpins (const int *values) {
45 for (int i = 0; i < 4; i++) {
46 gpio_put (pins[i], values[i]);
47 }
48 }
49
50 public:
51 // Constructor
52 Stepper (int pin1, int pin2, int pin3, int pin4) {
53 pins[0] = pin1;
54 pins[1] = pin2;
55 pins[2] = pin3;
56 pins[3] = pin4;
57 step = 0;
58 for (int i = 0; i < 4; i++) {
59 gpio_init (pins[i]);
60 gpio_set_dir (pins[i], true);
61 gpio_put (pins[i], 0);
62 }
63 }
64
65 // Advance one step
66 void onestep() {
67 setpins (steps[step]);
68 if (++step == 4) {
69 step = 0;
70 }
71 }
72 };
73
74 // Hall Effect Sensor
75 class HallSensor {
76 private:
77 int pin;
Basic Digital Sensors 125
78 bool state;
79 uint32_t last, elapsed;
80 public:
81 // constructor
82 HallSensor(int pinSensor) {
83 pin = pinSensor;
84 gpio_init (pin);
85 gpio_set_pulls(pin, true, false);
86 state = gpio_get(pin);
87 last = elapsed = 0;
88 }
89
90 // detect sensor
91 bool detect() {
92 bool read = gpio_get(pin);
93 if (read != state) {
94 state = read;
95 uint32_t aux = board_millis();
96 if (read) {
97 if (last != 0) {
98 elapsed = aux - last;
99 }
100 last = aux;
101 return true;
102 }
103 }
104 return false;
105 }
106
107 // return time since previous detection
108 uint32_t getElapsed() {
109 return elapsed;
110 }
111 };
112
113 // Main Program
114 int main() {
115 // Init stdio
116 stdio_init_all();
Basic Digital Sensors 126
Arduino Code
37 pins[3] = pin4;
38 step = 0;
39 for (int i = 0; i < 4; i++) {
40 pinMode (pins[i], OUTPUT);
41 digitalWrite(pins[i], LOW);
42 }
43 }
44
45 // Advance one step
46 void onestep() {
47 setpins (steps[step]);
48 if (++step == 4) {
49 step = 0;
50 }
51 }
52 };
53
54 // Hall Effect Sensor
55 class HallSensor {
56 private:
57 int pin;
58 bool state;
59 uint32_t last, elapsed;
60 public:
61 // constructor
62 HallSensor(int pinSensor) {
63 pin = pinSensor;
64 pinMode (pin, INPUT_PULLUP);
65 state = digitalRead(pin);
66 last = elapsed = 0;
67 }
68
69 // detect sensor
70 bool detect() {
71 bool read = digitalRead(pin) == HIGH;
72 if (read != state) {
73 state = read;
74 uint32_t aux = millis();
75 if (read) {
Basic Digital Sensors 129
76 if (last != 0) {
77 elapsed = aux - last;
78 }
79 last = aux;
80 return true;
81 }
82 }
83 return false;
84 }
85
86 // return time since previous detection
87 uint32_t getElapsed() {
88 return elapsed;
89 }
90 };
91
92 Stepper stepper(STEPPER_1, STEPPER_2, STEPPER_3, STEPPER_4);
93 HallSensor sensor(SENSOR_PIN);
94
95 void setup() {
96 Serial.begin(115200);
97 Serial.println("Hall Effect Sensor Example");
98
99 // Init LED gpio
100 pinMode(LED_PIN, OUTPUT);
101 digitalWrite(LED_PIN, LOW);
102 }
103
104 void loop() {
105 // choose a random speed
106 int delay = (rand() % 1700) + 1500;
107 Serial.print ("Delay = ");
108 Serial.print (delay/1000.0);
109 Serial.println (" ms");
110 uint32_t changeTime = millis() + 30000;
111 while (millis() < changeTime) {
112 stepper.onestep();
113 if (sensor.detect()) {
114 digitalWrite(LED_PIN, HIGH);
Basic Digital Sensors 130
MicroPython Code
22 self.setpins()
23 self.step = 0
24
25 # advance one step
26 def onestep(self):
27 self.setpins(self.steps[self.step])
28 self.step = self.step+1
29 if self.step >= len(self.steps):
30 self.step = 0
31
32 # Hall Effect sensor
33 class HallSensor:
34 # init
35 def __init__(self, pin):
36 self.pin = pin
37 self.state = self.pin.value()
38 self.last = 0
39 self.elapsed = 0
40
41 # detect sensor
42 def detect(self):
43 read = self.pin.value()
44 if read != self.state:
45 self.state = read
46 aux = time.ticks_ms()
47 if read == 1:
48 if self.last != 0:
49 self.elapsed = aux - self.last
50 self.last = aux
51 return True
52 return False
53
54 # return time since previous detection
55 def getElapsed(self):
56 return self.elapsed
57
58 # Stepper connections
59 pins = [
60 Pin(2, Pin.OUT),
Basic Digital Sensors 132
61 Pin(3, Pin.OUT),
62 Pin(4, Pin.OUT),
63 Pin(5, Pin.OUT)
64 ]
65 stepper = Stepper(pins)
66
67 # Sensor
68 sensor = HallSensor (Pin(16, Pin.IN, Pin.PULL_UP))
69
70 # LED
71 led = Pin(25, Pin.OUT)
72 led.off()
73
74 # main loop
75 while True:
76 # choose a random speed
77 delay = random.randrange(1300, 3000)
78 print ("Delay = {} ms".format(delay/1000))
79 changeTime = time.ticks_ms() + 30000
80 while time.ticks_ms() < changeTime:
81 stepper.onestep()
82 if sensor.detect():
83 led.on()
84 elapsed = sensor.getElapsed()
85 if elapsed != 0:
86 print ("RPM = {}".format(60000 // elapsed))
87 time.sleep_us(delay)
88 led.off()
89 else:
90 time.sleep_us(delay)
CircuitPython Code
Basic Digital Sensors 133
39
40 # Hall Effect sensor
41 class HallSensor:
42 # init
43 def __init__(self, pin):
44 self.pin = digitalio.DigitalInOut(pin)
45 self.pin.pull = digitalio.Pull.UP
46 self.state = self.pin.value
47 self.last = 0
48 self.elapsed = 0
49
50 # detect sensor
51 def detect(self):
52 read = self.pin.value
53 if read != self.state:
54 self.state = read
55 aux = time_ms()
56 if read == 1:
57 if self.last != 0:
58 self.elapsed = aux - self.last
59 self.last = aux
60 return True
61 return False
62
63 # return time since previous detection
64 def getElapsed(self):
65 return self.elapsed
66
67 # Stepper connections
68 stepper = Stepper([board.GP2, board.GP3, board.GP4, board.GP5])
69
70 # Sensor
71 sensor = HallSensor(board.GP16)
72
73 # LED
74 led = digitalio.DigitalInOut(board.GP25)
75 led.direction = digitalio.Direction.OUTPUT
76 led.value = False
77
Basic Digital Sensors 135
78 # main loop
79 while True:
80 # choose a random speed
81 delay = random.randrange(1300, 3000)/1000
82 print ("Delay = {} ms".format(delay))
83 changeTime = time_ms() + 30000
84 while time_ms() < changeTime:
85 stepper.onestep()
86 if sensor.detect():
87 led.value = True
88 elapsed = sensor.getElapsed()
89 if elapsed != 0:
90 print ("RPM = {}".format(60000 // elapsed))
91 time.sleep(delay/1000)
92 led.value = False
93 else:
94 time.sleep(delay/1000)
Analog Sensors
In this chapter, we will see sensors that output an analog signal that we can input to the Pi Pico
ADC. I am not including temperature sensors (they will be in the next chapter).
Potentiometers
A potentiometer is a variable resistor. In the most common form, the resistance is changed by
turning a shaft that moves a contact (the wiper) over a resistive element. There are usually three
terminals, one at each extreme of the resistive element and one connected to the moving contact.
Potentiometer
The shaft of a potentiometer can be moved by a person or connected mechanically to some part
so we can detect and measure the part’s movement.
To connect a potentiometer to the ADC of the Pi Pico we must convert its resistance to a voltage
(in the range supported by the ADC). A simple way to do this is to connect one extreme to +3.3V,
the other to ground, and connect the wiper to the ADC. The voltage at the input of the ADC will
be proportional to the resistance between the wiper and the extreme connected to the ground.
Potentiometer Example
In this example, the potentiometer is connected as mentioned above (extremes to ground and
+3.3V, wiper to ADC). We continuously read the ADC and use the result to program the PWM
(Pulse Width Modulation) on the pin connected to the internal LED in the Pico. This way the
brightness of the LED will be controlled by the potentiometer. We will also send the reading to
the PC.
PWM works by alternating the pin output between HIGH and LOW levels and controlling how
long it stays at each level. For this it uses a clock to continuously count from 0 to a wrap value.
To generate the PWM signal, we program the count value where the pin output will change from
HIGH to LOW. The output will change from LOW to HIGH when the counter wraps.
In my tests, I used a 10kΩ linear potentiometer. You can use other values but don’t use too higher
Analog Sensors 138
a value (above 100kΩ) as the resistance of the analog input will be in parallel with it. Try also a
logarithm potentiometer, and see how the readings change with the turn of the shaft.
In the Pico W the LED is not connected to the RP2040, so this code won’t work on it.
You can add a LED (in series with a 1kΩ resistor) between one of the Pico W pins and
ground and change the LED pin number in the code.
1 /**
2 * @file potentiometer_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Potentiometer Example
5 * @version 1.0
6 * @date 2023-05-15
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include "pico/stdlib.h"
14 #include "hardware/gpio.h"
15 #include "hardware/adc.h"
16 #include "hardware/pwm.h"
17 #include "hardware/clocks.h"
18
19 // Pins
20 #define LED_PIN 25
21 #define SENSOR_PIN 27
Analog Sensors 139
22
23 // PWM configuration
24 #define WRAP 4095
25 #define FREQ 1000.0f
26
27
28 // Main Program
29 int main() {
30 // Init stdio
31 stdio_init_all();
32 #ifdef LIB_PICO_STDIO_USB
33 while (!stdio_usb_connected()) {
34 sleep_ms(100);
35 }
36 #endif
37
38 // Init LED gpio
39 uint slice_num = pwm_gpio_to_slice_num(LED_PIN);
40 uint chan_num = pwm_gpio_to_channel(LED_PIN);
41 // Configure the slice
42 // f = fsys / (clock divisor * (wrap value+1)
43 // clock divisor = fsys / (f * (wrap value+1))
44 float fsys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS)*1000.0f;
45 float div = fsys/(FREQ * (WRAP+1));
46 printf("fsys= %.2f div=%.2f\n", fsys, div);
47 printf("slice= %u channel=%d\n", slice_num, chan_num);
48 pwm_config config = pwm_get_default_config ();
49 pwm_config_set_wrap(&config, WRAP);
50 pwm_config_set_clkdiv(&config, div);
51 pwm_config_set_phase_correct(&config, false);
52 pwm_config_set_clkdiv_mode(&config, PWM_DIV_FREE_RUNNING);
53 pwm_init(slice_num, &config, false);
54 pwm_set_chan_level (slice_num, chan_num, 0);
55 pwm_set_enabled(slice_num, true);
56 gpio_set_function(LED_PIN, GPIO_FUNC_PWM);
57
58 // Init sensor ADC
59 adc_init();
60 adc_gpio_init(SENSOR_PIN);
Analog Sensors 140
61 adc_select_input(0);
62
63 // Main loop
64 while(1) {
65 uint16_t val = adc_read(); // 0-4095
66 printf("%u\n", val);
67 pwm_set_chan_level (slice_num, chan_num, val);
68 sleep_ms(200);
69 }
70 }
Arduino Code
The Arduino libraries hide all the ADC and PWM configurations. I am using the default options
(which come from the ATmega used in the first Arduino boards), where analogRead() returns
a 10-bit result (0 to 1023) and analogWrite() expects a value in the range 0-255.
Arduino Potentiometer Example
1 // Potentiometer Example
2
3 // Pins
4 #define PIN_SENSOR A0
5 #define PIN_LED 25
6
7 // Initialization
8 void setup() {
9 Serial.begin(115200);
10 }
11
12 // Main loop
13 void loop() {
14 int val = analogRead(PIN_SENSOR);
15 Serial.println(val);
16 // Convert val from 0-1023 to 0-256
17 analogWrite(PIN_LED, val>>2);
18 delay(200);
19 }
Analog Sensors 141
MicroPython Code
MicroPython also hides a lot of the details. Both the ADC and PWM use 16-bit values. A small
nuisance is that the version of MicroPython I used did not allow setting the frequency in the
PWM constructor (this is being corrected right now).
MicroPython Potentiometer Example
1 # Potentiometer example
2
3 from machine import Pin, ADC, PWM
4 from time import sleep
5
6 # Center of potenciometer at ADC0
7 sensor = ADC(0)
8
9 # Internal LED, PWM freq is 1KHz
10 led = PWM(Pin(25, Pin.OUT))
11 led.freq(1000)
12 led.duty_u16(0)
13
14 # Main loop
15 while True:
16 # read potentiomenter pos
17 val = sensor.read_u16()
18 print(val)
19 # set LED intensity
20 led.duty_u16(val)
21 # sleep between readings
22 sleep(0.2)
CircuitPython Code
The CircuitPython code is very similar to the MicroPython code.
Analog Sensors 142
1 # Potentiometer example
2
3 import analogio
4 import pwmio
5 import board
6 from time import sleep
7
8 # Center of potenciometer at ADC0
9 sensor = analogio.AnalogIn(board.A0)
10
11 # Internal LED, PWM freq is 1KHz
12 led = pwmio.PWMOut(board.LED, frequency=1000, duty_cycle=0)
13
14 # Main loop
15 while True:
16 # read potentiomenter pos
17 val = sensor.value
18 print(val)
19 # set LED intensity
20 led.duty_cycle=val
21 # sleep between readings
22 sleep(0.2)
Analog Joysticks
A joystick is a stick that can be moved around and report its position. An analog joystick has two
potentiometers that will move according to the projection of the movement in two orthogonal
axes.
Analog Sensors 143
Analog Joystick
In the center position of the joystick, the two potentiometers will be in the middle position. If we
look at the horizontal potentiometer:
The middle position may not be exactly at half the full resistance. A gaming joystick may have
an adjustment for this, but the software should ignore a small variance in resistance.
Depending on your joystick the pinout may be different. You may have to shuffle the LEDs
association depending on the orientation of your assembly. Also, you may need to change the
DEAD_ZONE value so that no LED is on when the stick is in the center.
1 /**
2 * @file joy_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Joystick Example
5 * @version 1.0
6 * @date 2023-05-16
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include "pico/stdlib.h"
14 #include "hardware/gpio.h"
15 #include "hardware/adc.h"
16 #include "hardware/pwm.h"
17 #include "hardware/clocks.h"
18
19 // LED connections
20 #define LED_RIGHT 12
21 #define LED_UP 13
22 #define LED_DOWN 14
23 #define LED_LEFT 15
24
25 // Joystick Connections
26 #define PIN_HORIZ 26
27 #define ADC_HORIZ 0
28 #define PIN_VERTIC 27
29 #define ADC_VERTIC 1
30
Analog Sensors 145
31 // PWM configuration
32 #define WRAP 4095
33 #define FREQ 1000.0f
34
35 // Ignore small variations around the center
36 #define DEAD_ZONE 60
37
38 // Simple class for LED control
39 class LED_PWM {
40 private:
41 int pin_num;
42 uint slice_num;
43 uint chan_num;
44
45 public:
46 // Constructor
47 LED_PWM(int pin) {
48 pin_num = pin;
49 slice_num = pwm_gpio_to_slice_num(pin);
50 chan_num = pwm_gpio_to_channel(pin);
51 }
52
53 // Initi pin for PWM
54 void init(void) {
55 // Configure the slice
56 // f = fsys / (clock divisor * (wrap value+1)
57 // clock divisor = fsys / (f * (wrap value+1))
58 float fsys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS)*1000.0f;
59 float div = fsys/(FREQ * (WRAP+1));
60 pwm_config config = pwm_get_default_config ();
61 pwm_config_set_wrap(&config, WRAP);
62 pwm_config_set_clkdiv(&config, div);
63 pwm_config_set_phase_correct(&config, false);
64 pwm_config_set_clkdiv_mode(&config, PWM_DIV_FREE_RUNNING);
65 pwm_init(slice_num, &config, false);
66 pwm_set_chan_level (slice_num, chan_num, 0);
67 pwm_set_enabled(slice_num, true);
68 gpio_set_function(pin_num, GPIO_FUNC_PWM);
69 }
Analog Sensors 146
70
71 // Set intensity
72 void set(uint16_t val) {
73 pwm_set_chan_level (slice_num, chan_num, val);
74 }
75 };
76
77 LED_PWM ledRight(LED_RIGHT);
78 LED_PWM ledLeft(LED_LEFT);
79 LED_PWM ledUp(LED_UP);
80 LED_PWM ledDown(LED_DOWN);
81
82 // Main Program
83 int main() {
84 // Init LEDs
85 ledRight.init();
86 ledLeft.init();
87 ledUp.init();
88 ledDown.init();
89
90 // Init ADC
91 adc_init();
92 adc_gpio_init(PIN_HORIZ);
93 adc_gpio_init(PIN_VERTIC);
94
95 // Main loop
96 while(1) {
97 // Read the joystick position
98 adc_select_input(ADC_HORIZ);
99 adc_read();
100 uint16_t horiz = adc_read(); // 0-4095
101 adc_select_input(ADC_VERTIC);
102 adc_read();
103 uint16_t vertic = adc_read(); // 0-4095
104
105 // Light the LEDs according to the position
106 if (horiz > (2047 + DEAD_ZONE)) {
107 ledRight.set(0);
108 ledLeft.set(horiz-2048);
Analog Sensors 147
Arduino Code
15
16 // Initialization
17 void setup() {
18 }
19
20 // Main loop
21 void loop() {
22 // Read the joystick position
23 int horiz = analogRead (PIN_HORIZ);
24 int vertic = analogRead (PIN_VERTIC);
25
26 // Light the LEDs according to the position
27 if (horiz > (511 + DEAD_ZONE)) {
28 analogWrite(LED_RIGHT, 0);
29 analogWrite(LED_LEFT, (horiz-512) >> 1);
30 } else if (horiz <= (511 - DEAD_ZONE)){
31 analogWrite(LED_RIGHT, (511-horiz) >> 1);
32 analogWrite(LED_LEFT, 0);
33 } else {
34 analogWrite(LED_RIGHT, 0);
35 analogWrite(LED_LEFT, 0);
36 }
37
38 if (vertic > (511 + DEAD_ZONE)) {
39 analogWrite(LED_DOWN, (vertic-512) >> 1);
40 analogWrite(LED_UP, 0);
41 } else if (vertic <= (511 - DEAD_ZONE)){
42 analogWrite(LED_DOWN, 0);
43 analogWrite(LED_UP, (511-vertic) >> 1);
44 } else {
45 analogWrite(LED_UP, 0);
46 analogWrite(LED_DOWN, 0);
47 }
48 }
MicroPython Code
Analog Sensors 149
1 # Joystick example
2
3 from machine import Pin, ADC, PWM
4 from time import sleep
5
6 # Potentiometers
7 horiz = ADC(0)
8 vert = ADC(1)
9
10 # Indicator LEDs, PWM freq is 1KHz
11 def initPWM(pin):
12 pwm = PWM(Pin(pin, Pin.OUT))
13 pwm.freq(1000)
14 pwm.duty_u16(0)
15 return pwm
16
17 ledRight = initPWM(12)
18 ledUp = initPWM(13)
19 ledDown = initPWM(14)
20 ledLeft = initPWM(15)
21
22 # Ignore variations up to
23 DEAD_ZONE = 3000
24
25 # Main loop
26 while True:
27 # sample potentiometers
28 h = horiz.read_u16()
29 v = vert.read_u16()
30 # horizontal axis
31 if h > (32767+DEAD_ZONE):
32 ledRight.duty_u16(0)
33 ledLeft.duty_u16(h-32768)
34 elif h <= (32767-DEAD_ZONE):
35 ledRight.duty_u16(32767-h)
36 ledLeft.duty_u16(0)
37 else:
38 ledRight.duty_u16(0)
Analog Sensors 150
39 ledLeft.duty_u16(0)
40
41 # vertical axis
42 if v > (32767+DEAD_ZONE):
43 ledDown.duty_u16(v-32768)
44 ledUp.duty_u16(0)
45 elif v <= (32767-DEAD_ZONE):
46 ledDown.duty_u16(0)
47 ledUp.duty_u16(32767-v)
48 else:
49 ledDown.duty_u16(0)
50 ledUp.duty_u16(0)
CircuitPython Code
1 # Joystick example
2
3 import analogio
4 import pwmio
5 import board
6
7 # Potentiometers
8 horiz = analogio.AnalogIn(board.A0)
9 vert = analogio.AnalogIn(board.A1)
10
11 # Indicator LEDs, PWM freq is 1KHz
12 ledRight = pwmio.PWMOut(board.GP12, frequency=1000, duty_cycle=0)
13 ledUp = pwmio.PWMOut(board.GP13, frequency=1000, duty_cycle=0)
14 ledDown = pwmio.PWMOut(board.GP14, frequency=1000, duty_cycle=0)
15 ledLeft = pwmio.PWMOut(board.GP15, frequency=1000, duty_cycle=0)
16
17 # Ignore variations up to
18 DEAD_ZONE = 3000
19
20 # Main loop
21 while True:
22 # sample potentiometers
Analog Sensors 151
23 h = horiz.value
24 v = vert.value
25 # horizontal axis
26 if h > (32767+DEAD_ZONE):
27 ledRight.duty_cycle = 0
28 ledLeft.duty_cycle = h-32768
29 elif h <= (32767-DEAD_ZONE):
30 ledRight.duty_cycle = 32767-h
31 ledLeft.duty_cycle = 0
32 else:
33 ledRight.duty_cycle = 0
34 ledLeft.duty_cycle = 0
35
36 # vertical axis
37 if v > (32767+DEAD_ZONE):
38 ledDown.duty_cycle = v-32768
39 ledUp.duty_cycle = 0
40 elif v <= (32767-DEAD_ZONE):
41 ledDown.duty_cycle = 0
42 ledUp.duty_cycle = 32767-v
43 else:
44 ledDown.duty_cycle = 0
45 ledUp.duty_cycle = 0
In LDR applications we are not interested in measuring absolute light intensity, we want to detect
changes and react to them. This allows us to create light-activated and dark-activated behaviors.
To convert the resistance of the LDR to a voltage we can measure with the ADC we will use a
resistive voltage divisor:
As the LDR’s resistance changes, the current flowing through the LDR/resistor pair also changes.
Supposing that the resistance R in the above figure is fixed, we can determine the LDR resistance
RL from the voltage V measured by the ADC:
(Vcc −V )
RL = R ∗ V
Despite being cheap and easy to use, LDRs are been phased out because they contain cadmium
(a toxic metal that is banned by the European RoHS). As we will see in the next sections, photo-
transistors and LEDs can be used as an alternative.
Analog Sensors 153
LDR Example
Let’s have some fun! This project will randomly emit short sounds, but only if it is in the dark.
If you power the Pico with a power bank, place the assembly in a closed box, and leave it in a
room, people will have a hard time finding out from where the sound is coming (as it will stop
when the box is opened).
You can experiment with changing the light/dark threshold and the range of sleep times.
1 /**
2 * @file ldr_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief LDR Example
5 * @version 1.0
6 * @date 2023-05-16
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
Analog Sensors 154
12 #include <stdlib.h>
13 #include "pico/stdlib.h"
14 #include "hardware/gpio.h"
15 #include "hardware/adc.h"
16
17 // Pins
18 #define BUZZER_PIN 15
19 #define SENSOR_PIN 26
20
21 // Ligh/Dark threshold
22 #define DARK 300
23
24
25 // Main Program
26 int main() {
27 // Init Buzzer gpio
28 gpio_init (BUZZER_PIN);
29 gpio_set_dir (BUZZER_PIN, true);
30 gpio_put (BUZZER_PIN, false);
31
32 // Init sensor ADC
33 adc_init();
34 adc_gpio_init(SENSOR_PIN);
35 adc_select_input(0);
36
37 // Main loop
38 while(1) {
39 // Sleep for 3 to 60 seconds
40 uint delay = (rand() % 57000) + 3000;
41 sleep_ms (delay);
42 // Short beep if dark
43 uint16_t val = adc_read(); // 0-4095
44 if (val < DARK) {
45 gpio_put (BUZZER_PIN, true);
46 sleep_ms (100);
47 gpio_put (BUZZER_PIN, false);
48 }
49 }
50 }
Analog Sensors 155
Arduino Code
1 // LDR example
2
3 // Pins
4 #define BUZZER_PIN 15
5 #define SENSOR_PIN A0
6
7 // Ligh/Dark threshold
8 #define DARK 75
9
10 // initialization
11 void setup() {
12 pinMode (BUZZER_PIN, OUTPUT);
13 digitalWrite(BUZZER_PIN, LOW);
14 }
15
16 // Main loop
17 void loop() {
18 // Sleep for 3 to 60 seconds
19 uint tsleep = (rand() % 57000) + 3000;
20 delay (tsleep);
21 // Short beep if dark
22 uint16_t val = analogRead(SENSOR_PIN); // 0-1023
23 if (val < DARK) {
24 digitalWrite (BUZZER_PIN, HIGH);
25 delay (100);
26 digitalWrite(BUZZER_PIN, LOW);
27 }
28 }
MicroPython Code
Analog Sensors 156
1 # LDR example
2
3 from machine import Pin, ADC
4 from time import sleep
5 import random
6
7 # LDR at ADC0
8 sensor = ADC(0)
9
10 # Buzzer at pin 15
11 buzzer = Pin(15, Pin.OUT)
12 buzzer.off()
13
14
15 # Ligh/Dark threshold
16 DARK = 5000
17
18 # Main loop
19 while True:
20 # sleep from 3 to 60 seconds
21 sleep(random.uniform(3, 60))
22 # read LDR and check if its is dark
23 val = sensor.read_u16()
24 if val < DARK:
25 # short beep
26 buzzer.on()
27 sleep(0.1)
28 buzzer.off()
CircuitPython Code
Analog Sensors 157
1 # LDR example
2
3 import analogio
4 import digitalio
5 import board
6 from time import sleep
7 import random
8
9 # LDR at ADC0
10 sensor = analogio.AnalogIn(board.A0)
11
12 # Buzzer at pin 15
13 buzzer = digitalio.DigitalInOut(board.GP15)
14 buzzer.direction = digitalio.Direction.OUTPUT
15 buzzer.value = False
16
17 # Ligh/Dark threshold
18 DARK = 5000
19
20 # Main loop
21 while True:
22 # sleep from 3 to 60 seconds
23 sleep(random.uniform(3, 60))
24 # read LDR and check if its is dark
25 val = sensor.value
26 if val < DARK:
27 # short beep
28 buzzer.value = True
29 sleep(0.1)
30 buzzer.value = False
Phototransistor
A phototransistor is a transistor that is activated by light. The most common type is a bipolar
transistor in a transparent case. Light falling into the base-emitter (or base-collector) junction
will generate a current that will be amplified.
Analog Sensors 158
The easiest type to find is infrared (IR) phototransistors. They are commonly used in remote
controls and obstacle detectors.
Phototransistor Example
The phototransistor used in the example is a TIL78 or equivalent. It’s an NPN transistor where
the base is activated by IR light. Physically it is identical to an LED, with a flat side in the case
indicating the collector. We connect the emitter directly to the ground and the collector to +3.3V
through a 1kΩ resistor. The collector goes to an ADC input.
Without incident IR light, the phototransistor does not conduct and the collector is at 3.3V. IR
light will make the phototransistor conduct and bring down the collector voltage.
Analog Sensors 159
The code will print the value returned by the ADC. To reduce the amount of output, it will only
print when there is a significant difference from the last value outputted.
You can test this example by pressing a key in an IR remote control pointed to the phototransistor.
Another way is to point an IR LED powered by a 3V coin cell⁴.
1 /**
2 * @file phototrans_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Phototransistor Example
5 * @version 1.0
6 * @date 2023-05-17
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include "pico/stdlib.h"
15 #include "hardware/adc.h"
16
17 // Pins
18 #define SENSOR_PIN 26
19
20 // Main Program
21 int main() {
22 // Init stdio
23 stdio_init_all();
24 #ifdef LIB_PICO_STDIO_USB
25 while (!stdio_usb_connected()) {
26 sleep_ms(100);
27 }
28 #endif
29
⁴https://www.evilmadscientist.com/2009/some-thoughts-on-throwies/
Analog Sensors 160
Arduino Code
1 // Phototransistor example
2
3 // Pins
4 #define SENSOR_PIN A0
5
6 // initialization
7 void setup() {
8 Serial.begin(115200);
9 }
10
11 // Main loop
12 void loop() {
13 static uint16_t val_ant = 0;
14 uint16_t val = analogRead(SENSOR_PIN);
15 int dif = (val > val_ant) ? val - val_ant : val_ant - val;
16 if (dif > 100) {
17 Serial.println(val);
18 val_ant = val;
Analog Sensors 161
19 }
20 }
MicroPython Code
CircuitPython Code
The programs are simple: they will send to the PC an average of 20 ADC readings every second.
You will find that an LED is not as sensitive as an LDR, but it can detect big differences in ambient
light. Experiment with different LEDs and different lights. I got better results with a red LED with
a clear case.
1 /**
2 * @file ledsensor_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Phototransistor Example
5 * @version 1.0
6 * @date 2023-05-18
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include "pico/stdlib.h"
15 #include "hardware/adc.h"
16
17 // Pins
18 #define SENSOR_PIN 26
19
20 // Main Program
21 int main() {
22 // Init stdio
23 stdio_init_all();
24 #ifdef LIB_PICO_STDIO_USB
25 while (!stdio_usb_connected()) {
26 sleep_ms(100);
27 }
28 #endif
29
30 // Init sensor ADC
31 adc_init();
32 adc_gpio_init(SENSOR_PIN);
33 adc_select_input(0);
34
35 // Main loop
36 while(1) {
37 uint32_t sum = 0;
38 for (int i = 0; i < 20; i++) {
Analog Sensors 164
39 sum +=adc_read();
40 }
41 printf("%u\n", sum/20);
42 sleep_ms(2000);
43 }
44 }
Arduino Code
MicroPython Code
Analog Sensors 165
CircuitPython Code
16 while True:
17 print (val() // 100)
18 sleep(2)
19
20
Gas Sensor
We have already experimented with the digital output of MQ gas sensors, let’s have a look now
at their analog output.
In case you missed it, you will find an explanation of gas sensors in Chapter 5 (Basic Digital
Sensors).
Notice that we are using a voltage divisor to reduce the 5V sensor output to a level compatible
with the Pico.
We can use the same programs as the “LED as a Light Sensor” example.
Analog Sensors 167
The A1301 requires a power supply (Vcc) between 4.5V and 6.0V. When no magnetic field is
applied, the output is Vcc/2. When a magnetic field is present, the output will go above or below
this value, depending of its polarity. The variation from Vcc/2 is proportional to the intensity of
the field.
In other words, this sensor can not only detect if a magnetic field is present, but also its polarity
and strength. If the magnetic field is generated by a magnet, we can find if it is presenting the
North or South pole to the sensor and how close it is.
The software will send to the PC an indication of the polarity and strength of the magnetic field.
At the start it will assume there is no magnetic field applied and use the reading as zero. You can
use a small magnet to exercise it.
1 /**
2 * @file analoghall_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Analog Hall Effect Sensor Example
5 * @version 1.0
6 * @date 2023-05-18
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
Analog Sensors 169
14 #include "pico/stdlib.h"
15 #include "hardware/adc.h"
16
17 // Pins
18 #define SENSOR_PIN 26
19
20 // Reading for no magnetic field
21 int zero;
22
23 // Read sensor (average 50 readings)
24 #define N_READINGS 50
25 uint readSensor() {
26 uint32_t sum = 0;
27 for (int i = 0; i < N_READINGS; i++) {
28 sum += adc_read();
29 }
30 return sum / N_READINGS;
31 }
32
33
34 // Main Program
35 int main() {
36 // Init stdio
37 stdio_init_all();
38 #ifdef LIB_PICO_STDIO_USB
39 while (!stdio_usb_connected()) {
40 sleep_ms(100);
41 }
42 #endif
43
44 // Init sensor ADC
45 adc_init();
46 adc_gpio_init(SENSOR_PIN);
47 adc_select_input(0);
48
49 // Assume no magnetic field at the start
50 zero = readSensor() ;
51 printf("Zero = %d\n", zero);
52 sleep_ms(1000);
Analog Sensors 170
53 printf ("Ready\n");
54
55 // Main loop
56 while(1) {
57 int field = readSensor();
58 if (((field-zero) < 30) && ((field-zero) > -30)) {
59 printf ("No field\n");
60 } else if (field > zero) {
61 printf ("Field = S %d\n", field-zero);
62 } else {
63 printf ("Field = N %d\n", zero-field);
64 }
65 sleep_ms(1000);
66 }
67 }
Arduino Code
20 void setup() {
21 Serial.begin(115200);
22
23 // Assume no magnetic field at the start
24 zero = readSensor() ;
25 Serial.print("Zero = ");
26 Serial.println(zero);
27 delay(1000);
28 Serial.println ("Ready");
29 }
30
31 // Main loop
32 void loop() {
33 int field = readSensor();
34 if (((field-zero) < 10) && ((field-zero) > -10)) {
35 Serial.println ("No field");
36 } else if (field > zero) {
37 Serial.print ("Field = S ");
38 Serial.println(field-zero);
39 } else {
40 Serial.print ("Field = N ");
41 Serial.println(zero-field);
42 }
43 delay(1000);
44 }
MicroPython Code
10 def val():
11 sum = 0
12 for i in range(50):
13 sum = sum + sensor.read_u16();
14 return sum // 50
15
16 # Assume no magnetic field at the start
17 zero = val()
18 print('Zero = {}'.format(zero))
19 sleep(1)
20 print ('Ready')
21
22 # Main loop
23 while True:
24 field = val()
25 if abs(field-zero) < 100:
26 print ('No field')
27 elif field > zero:
28 print ('Field = S {}'.format (field-zero))
29 else:
30 print ('Field = N {}'.format (zero- field))
31 sleep(1)
CircuitPython Code
13 for i in range(50):
14 sum = sum + sensor.value;
15 return sum // 50
16
17 # Assume no magnetic field at the start
18 zero = val()
19 print('Zero = {}'.format(zero))
20 sleep(1)
21 print ('Ready')
22
23 # Main loop
24 while True:
25 field = val()
26 if abs(field-zero) < 100:
27 print ('No field')
28 elif field > zero:
29 print ('Field = S {}'.format (field-zero))
30 else:
31 print ('Field = N {}'.format (zero- field))
32 sleep(1)
Temperature Sensors
In this chapter, we are looking at sensors that will measure temperature. The sensors examined
have very different interfaces and performance characteristics. There is no perfect sensor, you
have to balance precision, ease of use, and price for your particular application.
Note: In the next chapter, we will look at barometric pressure sensors that will also
measure ambient temperature.
As we saw in Chapter 2, you have to figure out where to position a temperature sensor so you
can measure what you want.
The sensors in the examples will return temperature in Celsius degrees. In case you don’t
remember, you can convert from Celsius to Fahrenheit with this simple formula:
TF = 9 ∗ TC
5
+ 32
Some of the sensors we will examine also measure Relative Humidity. This indicates a present
state of absolute humidity relative to a maximum humidity given the same temperature and is
expressed as a percentage (0 to 100%).
Thermistor
A thermistor is a resistor whose resistance varies with temperature. While nearly all resistors
vary in resistance with the temperature, we want the resistors we normally use in our circuits to
have a nearly constant resistance in the normal operating temperatures. In a thermistor, we want
that the resistance variation be predictable and measurable.
A Thermistor
Temperature Sensors 175
How do we measure the resistance with the Pi Pico? By converting it to a voltage that is fed into
the ADC. We can do this with a resistive voltage divisor:
As the thermistor’s resistance changes, the current flowing through the thermistor/resistor pair
also changes. Supposing that the resistance R in the above figure is fixed, we can determine the
thermistor resistance RT from the voltage V measured by the ADC:
(Vcc −V )
RT = R ∗ V
We are going to use the most common type of thermistor, called NTC (negative temperature
coefficient). The resistance of the NTC thermistor does not vary linearly with the temperature.
Two equations are normally used to convert the resistance to temperature. In these equations, the
temperature is in Kelvin (that is, the temperature in Celsius added to 273.15).
The first equation is based on a single parameter called Beta (β) and a know pair of resistance
and temperature (R₀ and T₀):
T = β ∗ ln RRx
Rx = R0 ∗ exp(− Tβ0 )
A more complete one (the Steinhart-Hart equation), uses three parameters (a, b, c):
1
T
= a + b ∗ ln(R) + c ∗ (ln(R)3 )
Temperature Sensors 176
Ideally, you should get the thermistor’s datasheet from the manufacturer and it should inform
these parameters.
Thermistor Example
In my example, I will assume that you do not have this information. Instead, we will do a
calibration.
You will need to subject the thermistor to two different temperatures and inform them to
the program. From the informed temperatures and the measured resistances, the program will
calculate the Beta parameter:
R
ln( RT 1 )
T2
β= 1 − T1
T1 2
Once we have the beta, we will show the temperature every second.
The diagram below shows the connection of the thermistor to the Raspberry Pi Pico.
Temperature Sensors 177
You will need to connect the Pico to a PC to interact with the program.
1 /**
2 * @file thermistor_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Thermistor Example
5 * @version 1.0
6 * @date 2023-05-19
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
Temperature Sensors 178
16 #include "hardware/adc.h"
17
18 // Thermistor connection
19 #define THERMISTOR_PIN 26
20
21 // Circuit Parameters
22 const float Vcc = 3.3;
23 const float R = 2200.0;
24
25 // Parameters
26 typedef struct {
27 float r;
28 float t;
29 } ResTemp;
30
31 // Returns the thermistor resistance
32 // uses a mean of 20 ADC readings
33 float getResistance() {
34 uint32_t sum = 0;
35 for (int i = 0; i < 20; i++) {
36 sum += adc_read();
37 }
38 float v = (Vcc*sum)/(20*4096.0);
39 return R*(Vcc-v)/v;
40 }
41
42 // Gets a reference temperature
43 void getReference(ResTemp *ref) {
44 printf("Temperature: ");
45 scanf("%f", &ref->t);
46 printf("%.1f", ref->t);
47 ref->t += 273.0;
48 ref->r = getResistance();
49 printf(" Resistance: %.0f\n", ref->r);
50 }
51
52 // Main Program
53 int main() {
54 ResTemp ref1, ref2;
Temperature Sensors 179
Arduino Code
Temperature Sensors 180
1 // Thermistor Example
2
3 #include <math.h>
4
5 // Thermistor connection
6 const int pinTermistor = A0;
7
8 // Circuit Parameters
9 const float Vcc = 3.3;
10 const float R = 2200.0;
11
12 // Thermistor parameters
13 float beta;
14 float rx;
15 float rt1, t1, rt2, t2;
16
17 // Returns the thermistor resistance
18 // uses a mean of 20 ADC readings
19 float getResistance() {
20 uint32_t sum = 0;
21 for (int i = 0; i < 20; i++) {
22 sum += analogRead(pinTermistor);
23 }
24 float v = (Vcc*sum)/(20*1024.0);
25 return R*(Vcc-v)/v;
26 }
27
28 // Gets a reference temperature
29 void getReference(float &t, float &r) {
30 Serial.print("Temperature: ");
31 // Wait for a response
32 while (Serial.available() == 0) {
33 delay(100);
34 }
35 t = Serial.parseFloat();
36 Serial.print(t, 1);
37 t += 273.0;
38 r = getResistance();
Temperature Sensors 181
MicroPython Code
Temperature Sensors 182
1 # Thermistor Example
2 import math
3 from machine import Pin, ADC
4 from time import sleep
5
6 # Circuit Parameters
7 Vcc = 3.3
8 R = 2200.0
9
10 # Thermistor at ADC0
11 sensor = ADC(0)
12
13 # Returns the thermistor resistance
14 # uses a mean of 20 ADC readings
15 def val():
16 sum = 0
17 for i in range(20):
18 sum = sum + sensor.read_u16();
19 v = (Vcc*sum)/(20*65536.0)
20 return R*(Vcc-v)/v
21
22 # Gets a reference (temperature + resistance)
23 def getRef():
24 while True:
25 x = input('Temperature: ')
26 try:
27 t = float(x)
28 r = val()
29 print('Resistance: {:.0f}'.format(r))
30 print()
31 return 273.0 + t, r
32 except ValueError:
33 print ('Invalid format!')
34
35 # Compute beta from two references
36 print('Reference 1:')
37 t1, rt1 = getRef()
38 print('Reference 2:')
Temperature Sensors 183
CircuitPython Code
The CircuitPython code is almost identical to the MicroPython, only the two lines related to the
ADC are changed.
CircuitPython Thermistor Example
1 # Thermistor Example
2 import math
3 import analogio
4 import board
5 from time import sleep
6
7 # Circuit Parameters
8 Vcc = 3.3
9 R = 2200.0
10
11 # Thermistor at ADC0
12 sensor = analogio.AnalogIn(board.A0)
13
14 # Returns the thermistor resistance
15 # uses a mean of 20 ADC readings
16 def val():
17 soma = 0
18 for i in range(20):
19 soma = soma + sensor.value;
20 v = (Vcc*soma)/(20*65536.0)
Temperature Sensors 184
21 return R*(Vcc-v)/v
22
23 # Gets a reference (temperature + resistance)
24 def getRef():
25 while True:
26 x = input('Temperature: ')
27 try:
28 t = float(x)
29 r = val()
30 print('Resistance: {:.0f}'.format(r))
31 print()
32 return 273.0 + t, r
33 except ValueError:
34 print ('Invalid format!')
35
36 # Compute beta from two references
37 print('Reference 1:')
38 t1, rt1 = getRef()
39 print('Reference 2:')
40 t2, rt2 = getRef()
41 beta = math.log(rt1/rt2)/((1/t1)-(1/t2))
42 print('Beta = {:.2f}'.format(beta))
43 rx = rt1 * math.exp(-beta/t1)
44
45 # Main loop
46 while True:
47 rt = val()
48 t = beta / math.log(rt/rx)
49 print ('Temperature: {:.1f}'.format(t-273.0))
50 sleep(1)
51
The LM35D works from 0 to 100°C and needs a 4V to 20V power supply. Its output is the
temperature (in Celsius) multiplied by 0.01V. For example, an output of 0,23V corresponds to
a temperature of 23°C. The sensor is designed for a ±2°C accuracy, the typical value is ±0.9°C.
T = V /0.01
There are other models in the LM35 line, the plain LM35, LM35A, LM35C, and LM35CA can
measure negative temperatures but require additional components for that (you will find the
circuits in the datasheet). These other models also have better precision.
The TMP36 works from −40°C to +125°C and needs a 2.7V to 5.5V power supply. It outputs 0.75V
at 25°C, so the temperature can be calculated from the output voltage by the following formula:
T = 25 + (V − 0.75)/0.01
The TMP36 is available in two grades (“F” and “G”) based on the accuracy:
Temperature Sensors 186
TMP76 Accuracy
These specifications were taken from the datasheets from National Semiconductor /
Texas Instrument (LM35) and Analog Devices (TMP36)
Notice that, while I am powering the sensors with +5V, their output will not exceed 1V (LM35 @
100°C) and 2V (TMP36 @ 125°C) and can be connected directly to the Pico’s ADC.
The code is pretty straightforward. For better accuracy, an average of 10 readings is used. The
output voltage is calculated from the ADC reading (V = adc*3.3/(max+1)) and then converted to
Temperature Sensors 187
the temperature by the previous equations. A key point is that the max adc reading varies with the
programming environment (1023 for the Arduino, 4095 for the SDK, and 65535 for MicroPython
and CircuitPython).
1 /**
2 * @file lm35_tmp36_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief LM35 and TMP36 Sensors Example
5 * @version 1.0
6 * @date 2023-05-18
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include "pico/stdlib.h"
15 #include "hardware/adc.h"
16
17 // Pins (-1 if not used)
18 #define LM35_PIN 26
19 #define TMP36_PIN 27
20
21 // returns the voltage in an ADC pin
22 // averages 10 readings
23 #define N_READINGS 10
24 float readSensor(int pin) {
25 if (pin == -1) {
26 return 0;
27 }
28
29 // pin 26 -> channel 0, pin 27 -> channel 1
30 // pin 28 -> channel 2, pin 29 -> channel 3
31 adc_select_input(pin-26);
32
Temperature Sensors 188
33 uint32_t sum = 0;
34 for (int i = 0; i < N_READINGS; i++) {
35 sum += adc_read();
36 }
37 return (sum*3.3 / N_READINGS)/4096.0;;
38 }
39
40
41 // Main Program
42 int main() {
43 // Init stdio
44 stdio_init_all();
45 #ifdef LIB_PICO_STDIO_USB
46 while (!stdio_usb_connected()) {
47 sleep_ms(100);
48 }
49 #endif
50
51 // Init sensor ADCs
52 adc_init();
53 if (LM35_PIN != -1) {
54 adc_gpio_init(LM35_PIN);
55 }
56 if (TMP36_PIN != -1) {
57 adc_gpio_init(TMP36_PIN);
58 }
59
60 // Main loop
61 while(1) {
62 float tempLM35 = readSensor(LM35_PIN)/0.01;
63 float tempTMP36 = 25.0 + (readSensor(TMP36_PIN)-0.75)/0.01;
64 printf("LM35 = %.2fC TMP36 = %.2fC\n", tempLM35, tempTMP36);
65 sleep_ms(2000);
66 }
67 }
Arduino Code
Temperature Sensors 189
MicroPython Code
CircuitPython Code
Temperature Sensors 191
DS18B20
The DS18B20 sensor can measure temperatures from -55°C to +125°C, with an accuracy of ±0.5°C
in the range -10°C to +85°C.
You will find the DS18B20 in a TO-92 encapsulation (like a plastic transistor) or a water-proof
metal casing. There is also an SMD version.
DS18B20 Sensors
The DS18B20 temperature sensor uses a unique 1-Wire interface that allows:
The 1-Wire interface is also used by a few other devices from the same manufacturer (Dallas
Semiconductor, now a part of Maxim Integrated).
The DS18B20 has three connections: ground (GND), data (DQ), and power (VDD). As usual in
multi-drop communication, the data line can fluctuate or be connected to ground, a 4.7kΩ pull-up
resistor makes it HIGH when no device is driving it. There are two basic wiring configurations
for the sensor:
Temperature Sensors 193
As noted above, parasite power requires an additional component (a MOSFET) that connects the
DQ line to the power (VPU) under the control of a second pin of the microcontroller. In this case,
VPU can be between 3.0 and 5.5V. When using “local” power, VDD can be between 3.0 and 5.5V
and VPU between 3.0V and VDD.
Each DS18B20 has a unique 64-bit code stored in ROM. This code is made of three parts:
• The 8 most significant bits are a CRC (cyclic redundancy check), which is calculated from
the other 56 bis.
• The next 48 bits are a unique serial number.
• The last 8 bits is the family code (0x28) that identify the sensor as a DS18B20.
1-Wire Protocol
The 1-wire protocol is based on timing. All communications are started by the master (in our
case, the Pi Pico).
The first thing the master has to do is a reset. This is done by pulling the DQ line (bus) to zero
for at least 480µs. Any DS18B20 connected to the bus will wait for 15µs to 60µs and then pull the
line to zero for 60µs to 240µs (the presence pulse). The reset guarantees that the devices in the bus
are ready to be addressed. The presence pulse allows the master to know that there is at least one
device in the bus.
The master will send two types of commands to the sensors: ROM Commands and DS18B20
Function Commands. The ROM commands deal with addressing and the Function commands
control the DS18B20.
The sending and receiving of bits on the bus is done in time slots. All slots must be a minimum
of 60µs in duration, with a minimum of 1µs recovery time between them. The slots are all started
by the master pulling the bus to zero.
When the master is sending a bit, the sensor will check the bus after 15µs to 60µs. To send a “0”
(“write 0 slots”), the master should keep the bus low for 60µs to 120µs (so the sensor will read a
Temperature Sensors 194
low and will not confuse the slot with a reset). To send a “1” (“write 1 slot”), the master should
keep the bus low for 1µs to 15µs (so the sensor can detect the pulse and read a high).
When the sensor is sending a bit, it will output it as soon as it detects the master pulse and
maintain it for 15µs. The recommended timing is for the master to keep the bus low for 1µs, and
check the signal towards the end of the 15µs period.
Bytes are sent with the least significant bit first.
ROM Commands
These commands are common to all 1-Wire devices.
The most used ROM command is the MATCH ROM (0x55). This command is followed by a 64-bit
address, only the device with this exact address will respond, all others will ignore anything until
they receive a reset pulse. To use the MATCH ROM, we need to know the address of the device.
If we know that there is only one device in the bus, we can use the READ ROM (0x33) command.
Upon receiving this command, the device will send its address. If there is more than one device
attached to the bus, we will have a data collision when two (or more) devices try to set the bus
to opposite levels.
Another special case is the SKIP ROM (0xCC). This allows us to send the same command to all
devices. This makes sense only if you know all the devices are of the same type and the command
sent has no response (or there is only one device in the bus). In the case of the DS18B20, the
Convert T can be used this way.
A more common approach is to use the SEARCH ROM (0xF0) command to find out the addresses
of the devices on the bus. A variation of this command is the ALARM SEARCH (0xEC), only
devices with a set “alarm flag” will respond to it.
The search algorithm uses a kind of binary search, taking advantage of the fact that when there
is a data collision, zeros prevail. After sending a SEARCH command, the master will generate
three time slots for each of the 64 bits of an address:
• The first slot is a read slot, the devices will output the current bit of the address.
• The second slot is a read slot, the devices will output the complement of the current bit of
the address.
• The third slot is a write slot, all devices whose current bit of the address does not match
the bit sent by the host will not respond until a reset pulse is sent.
The responses from the devices give information about the values of each bit in the devices
present, reducing the values that needed to be tested.
Temperature Sensors 195
The detailed algorithm can be found in Maxim Integrated application note 187.
Notice that the search algorithm will give us the addresses of the sensors in the bus, but it is still
up to our application to associate an address with where the sensor is installed (if that is relevant).
DS18B20 Memory
The first two bytes (temperature register) contain the result of the most recent measurement
(conversion in the datasheet).
The temperature data is stored as a 16-bit sign-extended two’s complement number:
The sign bits (S) indicate if the temperature is positive or negative: for positive numbers S = 0
and for negative numbers S = 1. If the DS18B20 is configured for 12-bit resolution, all bits in the
temperature register will contain valid data. For 11-bit resolution, bit 0 is undefined. For 10-bit
resolution, bits 1 and 0 are undefined, and for 9-bit resolution bits 2, 1, and 0 are undefined.
To get the temperature from the value in the register:
1. Mask out the least significant bits according to the resolution (do an AND with 0xFFFF for
12-bit, 0xFFFE for 11-bit, 0xFFFC for 10-bit, or 0xFFF8 for 9-bit).
Temperature Sensors 196
For example, if the resolution is 10-bit and the temperature register is 0xFF5E, we first to an AND
with 0xFFFC, resulting in 0xFF5C. This corresponds to -164 in two’s complement. Dividing by 16
we get -10,25°C
The next two bytes (TH and TL registers) define when the alarm flag is set. The alarm flag
affects only the ALARM SEARCH command, if you are not using this command you can use
these two bytes to store any information.
Every time a temperature is measured, the alarm flag is set if the temperature is lower than or
equal to TL or higher than or equal to TH. If the temperature is above TL and below TH, the
alarm flag is cleared.
The temperature in the TL and TH registers are coded as 8-bit two’s complement integers (that
is, they don’t have a fractional part).
The configuration register has only two significant bits, they control the resolution (and that
defines the maximum conversion time).
The CRC register can be used to check if a reading of the scratchpad was successful.
There are six functions commands available:
• Convert T (0x44)
• Write Scratchpad (0x4E)
• Read Scratchpad (0xBE)
• Copy Scratchpad (0x48)
• Recall E²(0xB8)
• Read Power Supply (0xB4)
Convert T initiates a temperature reading. When the reading finishes, the result will be placed
in the two-byte temperature register at the beginning of the scratchpad memory. If an external
Temperature Sensors 197
power supply is used for the DS18B20, it will respond to read slots by transmitting “0” until
the result is available. If you are using parasite power, the master must enable a strong pull-
up on the bus within 10µs (by activating the MOSFET) and keep it until the reading finishes.
As a consequence of the strong pull-up, the DS18B20 will not be able to notify the end of the
conversion.
Write Scratchpad writes the TH, TL, and configuration registers. The master must send all three
bytes.
Read Scratchpad reads all the scratchpad, starting from the temperature register. The master
may interrupt the reading by issuing a reset pulse.
Copy Scratchpad saves the contents of the TH, TL, and configuration registers in the EEPROM.
If you are using parasite power, the master must enable a strong pull-up on the bus within 10µs
(by activating the MOSFET) and keep it for at least 10ms.
Recall E² restores the contents of the TH, TL, and configuration registers from the EEPROM.
Read Power Supply allows the software to find if a device is using parasite power. A read slot
after this command will get a “0” if the device is using parasite power or a “1” if the device is
externally powered.
DS18B20 Example
In this example we will connect two DS18B20 devices to the Pico, using the external power
configuration. The code will list the addresses of the devices on the bus and then read the
temperatures with a 1-second interval.
Temperature Sensors 198
As the code to implement the 1-Wire protocol is complex, we are going to use libraries. These
libraries implement the 1-Wire protocol in software (“bit-banging”).
1 /**
2 * @file ds18b20_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief DS18B20 Temperature Sensor Example
5 * @version 1.0
6 * @date 2023-05-19
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include "pico/stdlib.h"
15 #include "pico-onewire/api/one_wire.h"
16
17 // Pins
18 #define SENSOR_PIN 16
19
20 static One_wire one_wire(SENSOR_PIN);
21
22 #define MAX_SENSORS 5
23 static int nSensors;
24 static rom_address_t sensor[MAX_SENSORS];
25
26 // Main Program
27 int main() {
28 // Init stdio
29 stdio_init_all();
30 #ifdef LIB_PICO_STDIO_USB
31 while (!stdio_usb_connected()) {
32 sleep_ms(100);
33 }
34 #endif
35
36 // Find sensors
37 one_wire.init();
38 int count = one_wire.find_and_count_devices_on_bus();
Temperature Sensors 200
39 nSensors = 0;
40 for (int i = 0; i < count; i++) {
41 auto address = One_wire::get_address(i);
42 if ((address.rom[0] == FAMILY_CODE_DS18B20) && (nSensors < MAX_SENSORS)) {
43 sensor[nSensors] = address;
44 nSensors++;
45 }
46 }
47
48 // Main loop
49 while(1) {
50 // Start conversion
51 for (int i = 0; i < nSensors; i++) {
52 one_wire.convert_temperature(sensor[i], i == (nSensors-1), false);
53 }
54
55 // Get results and print
56 for (int i = 0; i < nSensors; i++) {
57 auto address = sensor[i];
58 float temp = one_wire.temperature(address);
59 printf("%02x%02x%02x%02x%02x%02x%02x%02x ", address.rom[0],
60 address.rom[1], address.rom[2], address.rom[3], address.rom[4],
61 address.rom[5], address.rom[6], address.rom[7]);
62 printf(" %3.1fC\n", temp);
63 }
64
65 printf("\n");
66 sleep_ms(250);
67 }
68 }
Arduino Code
This was a little harder than expected. There is a “classic” OneWire library in the Library Manager,
but it is not compatible with the Pico when using the unofficial Arduino core. The one to use is
the OneWireNg:
Temperature Sensors 201
As this library is more oriented to “modern” C++, the code may look more complicated than it
is. In the main loop, we start by asking all sensors to start a temperature conversion. Then we
search for the devices using for (const auto& id: *ow). For each device found, we read its
scratchpad and get from it the temperature. getTemp() returns a value in 0.001°C units.
Temperature Sensors 202
39 temp = -temp;
40 Serial.print('-');
41 }
42 Serial.print(temp / 1000);
43 Serial.print('.');
44 Serial.print(temp % 1000);
45 Serial.print(" C");
46 }
47 Serial.println();
48 }
49 }
50 Serial.println();
51 delay(250);
52 }
53
54 // Print the device address and family name
55 // returns false if not supported
56 static bool printId(const OneWireNg::Id& id)
57 {
58 const char *name = DSTherm::getFamilyName(id);
59
60 Serial.print(id[0], HEX);
61 for (size_t i = 1; i < sizeof(OneWireNg::Id); i++) {
62 Serial.print(':');
63 Serial.print(id[i], HEX);
64 }
65 if (name) {
66 Serial.print(" -> ");
67 Serial.print(name);
68 }
69
70 return (name != NULL);
71 }
MicroPython Code
The rp2 port of MicroPython includes drivers for the OneWire protocol and for the DS18B20
sensor⁵.
⁵https://docs.micropython.org/en/latest/rp2/quickref.html#onewire-driver
Temperature Sensors 204
The convert_temp() method of the DS18X20 class uses the “SKIP ROM” command to send the
“Convert T” command to all devices. The read_temp method uses the “Read Scratchpad” to get the
result. If you’re curious, you can see the code at https://github.com/micropython/micropython-
lib/blob/master/micropython/drivers/sensor/ds18x20/ds18x20.py.
MicroPython DS18B20 Example
CircuitPython Code
We are going to use the official libraries from Adafruit:
1. Download the “Bundle for Version 8.x” (or whatever CircuitPython version you are using)
from https://circuitpython.org/libraries.
2. Expand the zip file, the files we want will be under the lib subdirectory.
3. Connect your Pico with CircuitPython installed. A drive called CIRCUITPY will appear on
your computer.
Temperature Sensors 205
4. Copy the directory adafruit_onewire and the file adafruit_ds18x20.mpy from the
expanded bundle to the lib directory in the CIRCUITPY drive.
22 sleep(1)
23 temp = ''
24 for sensor in sensors:
25 temp = temp + '{:.2f}C '.format(sensor.read_temperature())
26 print (temp)
Besides the size, the two devices have different ranges, accuracy, and resolution:
DHT11 DHT22
Humidity range 20 to 95% 0 to 100%
Humidity accuracy ±5% ±2%
Humidity resolution 1% 0.1%
Temperature range 0 to 50°C -40 to +80°C
Temperature accuracy ±2°C ±0.5°C
Temperature resolution 1°C 0.1°C
Both devices have four terminals, but only three are used (Vcc, GND, and Data). As usual, the
data line needs a pull-up resistor, as it will fluctuate or be pulled to the ground. You may find
these sensors as a module (mounted on a board) with only three terminals and a pull-up resistor
already soldered.
The serial protocol is simple, but requires attention to the short times:
Temperature Sensors 207
• The microcontroller pulls the data line to the ground for at least 18ms (DHT11) or 1ms
(DHT22), requesting a reading from the sensor.
• 20µs to 40µs after the microcontroller releases the data line, the sensor acknowledges the
request by pulling the line to ground for 80µs, releasing it, and waiting for another 80µs.
• The sensor will then send a 40-bit answer. Each bit has two parts:
– The data line is pulled to ground for 50µs.
– the data line is released for 26µs to 28µs for a “0” or 70µs for a “1”.
If you use a sensor mounted on a board, check the pinout and adjust the connections.
Also, if the board includes a pull-up resistor, do not mount another in the protoboard.
1 ;
2 ; DHT protocol for 'Using Sensor with the Raspberry Pi Pico' book
3 ; Copyright (c) 2023, Daniel Quadros
4 ;
5
6 .program dht
7
8 //wait for a start from the program
9 pull
10
11 // keep data low for the time provided
12 set pindirs, 1 // set pin to output
13 set pins, 0 // set pin low
14 mov x, osr
15 waitx:
16 nop [25]
17 jmp x--, waitx // wait for count*26/clock
18
19 // starts reading response
20 set pindirs, 0 // change pin to input
21 wait 1 pin 0 // wait for it to come back to high
22 wait 0 pin 0 // wait for starting pulse
23 wait 1 pin 0
24 wait 0 pin 0 // wait for start of first bit
25
26 // read data bits
27 readdata:
28 wait 1 pin 0 // wait for data high
29 set x, 20 // x is timeout
30 countdown:
31 jmp pin, continue // continue conting if data high
32
33 // pin low while counting -> bit 0
34 set y, 0
35 in y, 1 // put a 0 in result
36 jmp readdata // read next bit
37
38 // pin still high
Temperature Sensors 210
39 continue:
40 jmp x--, countdown // decrement count
41
42 // timeout -> bit 1
43 set y, 1
44 in y, 1 // put a 1 in the result
45 wait 0 pin 0 // wait for low
46 jmp readdata // read next bit
47
48
49 % c-sdk {
50 // Helper function to set a state machine to run our PIO program
51 static inline void dht_program_init(PIO pio, uint sm, uint offset,
52 uint dataPin) {
53
54 // Get an initialized config structure
55 pio_sm_config c = dht_program_get_default_config(offset);
56
57 // Map the state machine's pin groups
58 sm_config_set_set_pins(&c, dataPin, 1);
59 sm_config_set_in_pins(&c, dataPin);
60 sm_config_set_jmp_pin(&c, dataPin);
61
62 // Set the pin direction at the PIO
63 pio_sm_set_consecutive_pindirs(pio, sm, dataPin, 1, true);
64
65 // Turn on pull up on data pin
66 gpio_pull_up(dataPin);
67
68 // Make sure data is high at start
69 pio_sm_set_pins_with_mask(pio, sm, 1 << dataPin, 1);
70
71 // Set the pin GPIO function (connect PIO to the pad),
72 pio_gpio_init(pio, dataPin);
73
74 // Configure the FIFOs
75 sm_config_set_in_shift (&c, false, true, 8);
76 sm_config_set_out_shift (&c, true, false, 1);
77
Temperature Sensors 211
The main C++ program defines a class for handling the DHT sensors. When a reading is requested,
it will only send the request to the sensor if a minimum time elapsed from the previous.
C/C++ DHT11/DHT22 Example
1 /**
2 * @file dht_sdk.c
3 * @author Daniel Quadros
4 * @brief DHT Temperature Sensor Example
5 * @version 1.0
6 * @date 2023-05-20
7 *
8 * @copyright Copyright (c) 2022, Daniel Quadros
9 *
10 */
11
12 #include "stdio.h"
13 #include "pico/stdlib.h"
14 #include "hardware/pio.h"
15 #include "hardware/clocks.h"
16
17 // Our PIO program:
18 #include "dht_sdk.pio.h"
19
20 // DHT sensors connections (-1 if not used)
21 #define PIN_DHT11 16
22 #define PIN_DHT22 17
23
Temperature Sensors 212
24 // PIO
25 static PIO pio = pio0;
26 static uint offset = 0xFFFF;
27
28 // Get milliseconds from start
29 static inline uint32_t board_millis(void)
30 {
31 return to_ms_since_boot(get_absolute_time());
32 }
33
34 // Class to access DHT sensors
35 class DHT {
36 public:
37 typedef enum { DHT11, DHT22 } Model;
38
39 private:
40 uint dataPin;
41 Model model;
42 uint sm;
43 uint32_t lastreading;
44 uint8_t data [5];
45
46 // get data from sensor
47 bool read() {
48 // Init and start the state machine
49 dht_program_init(pio, sm, offset, dataPin);
50 // Start a reading
51 pio_sm_put (pio, sm, (model == DHT11) ? 969 : 54);
52 // Read 5 bytes
53 for (int i = 0; i < 5; i++) {
54 data[i] = (uint8_t) pio_sm_get_blocking (pio, sm);
55 }
56 // Stop the state machine
57 pio_sm_set_enabled (pio, sm, false);
58 uint32_t total = 0;
59 for (int i = 0; i < 4; i++) {
60 total += data[i];
61 }
62 if (data[4] == (total & 0xFF)) {
Temperature Sensors 213
63 lastreading = board_millis();
64 return true;
65 }
66 return false;
67 }
68
69 // use cached data or read from sensor
70 void getData() {
71 // make sure we have some data
72 while (lastreading == 0) {
73 if (!read()) {
74 sleep_ms(2000);
75 }
76 }
77 // use cache if less than 2 seconds
78 uint32_t now = board_millis();
79 if (lastreading > now) { // count wraped
80 lastreading = now;
81 }
82 if ((lastreading+2000) < now) {
83 read();
84 }
85 }
86
87
88 public:
89
90 // Constructor
91 DHT (uint pin, Model model) {
92 this->dataPin = pin;
93 this->model = model;
94 this->sm = pio_claim_unused_sm(pio, true);
95 this->lastreading = 0;
96 if (offset == 0xFFFF) {
97 offset = pio_add_program(pio, &dht_program);
98 }
99 }
100
101 // get humidity
Temperature Sensors 214
Arduino Code
There are quite a few libraries for DHT sensors in the Arduino Library Manager. For this example,
we are going to use the Adafruit DHT sensor library:
Temperature Sensors 216
1 #include "DHT.h"
2
3 // DHT sensors connections (-1 if not used)
4 #define PIN_DHT11 16
5 #define PIN_DHT22 17
6
7 #if PIN_DHT11 != -1
8 DHT dht11(PIN_DHT11, DHT11);
9 #endif
10
11 #if PIN_DHT22 != -1
12 DHT dht22(PIN_DHT22, DHT22);
13 #endif
14
15 // Initialization
16 void setup() {
17 Serial.begin(115200);
18 #if PIN_DHT11 != -1
19 dht11.begin();
20 #endif
21 #if PIN_DHT22 != -1
22 dht22.begin();
23 #endif
24 }
25
26 // Main Loop
27 void loop() {
28 #if PIN_DHT11 != -1
29 Serial.print("DHT11 Humidity: ");
30 Serial.print(dht11.readHumidity(), 1);
31 Serial.print("%, Temperature: ");
32 Serial.print(dht11.readTemperature(), 1);
33 Serial.println("C");
34 #endif
35
36 #if PIN_DHT22 != -1
37 Serial.print("DHT22 Humidity: ");
38 Serial.print(dht22.readHumidity(), 1);
Temperature Sensors 218
MicroPython Code
Since MicroPython supports PIO programming, we can use an adaptation of the C/C++ code.
MicroPython DHT11/DHT22 Example
1 # DHT11/DHT22 Example
2
3 import utime
4 import rp2
5 from rp2 import PIO, asm_pio
6 from machine import Pin
7
8 # Connections, -1 = not used
9 pinDHT11 = 16
10 pinDHT22 = 17
11
12 # PIO Program
13 @asm_pio(set_init=(PIO.OUT_HIGH),autopush=True, push_thresh=8)
14 def DHT_PIO():
15 # wait for a start from the program
16 pull()
17
18 # keep data low for the time provided
19 set(pindirs,1) #set pin to output
20 set(pins,0) #set pin low
21 mov (x,osr)
22 label ('waitx')
23 nop() [25]
24 jmp(x_dec,'waitx') # wait for count*26/clock
25
26 # starts reading response
Temperature Sensors 219
66 self.model = model
67 self.smID = smID
68 self.sm = rp2.StateMachine(self.smID)
69 self.lastreading = 0
70 self.data=[]
71
72 # execute a reading
73 def read(self):
74 data=[]
75 self.sm.init(DHT_PIO,freq=1400000,set_base=self.dataPin,in_base=self.dataPin\
76 ,jmp_pin=self.dataPin)
77 self.sm.active(1)
78 if self.model == DHT11:
79 self.sm.put(969) # wait 18 ms
80 else:
81 self.sm.put(54) # wait 1 ms
82 for i in range(5): # read 5 bytes
83 data.append(self.sm.get())
84 self.sm.active(0)
85 total=0
86 for i in range(4):
87 total=total+data[i]
88 if data[4] == (total & 0xFF):
89 # checksum ok, save the data
90 self.data = data
91 self.lastreading = utime.ticks_ms()
92 return True
93 else:
94 return False
95
96 # read or use last data
97 def getData(self):
98 # make sure we have some data
99 while len(self.data) == 0:
100 if not self.read():
101 utime.sleep_ms(2000)
102
103 # new read only if more than 2 seconds from previous
104 now = utime.ticks_ms()
Temperature Sensors 221
CircuitPython Code
We are going to use the official library from Adafruit:
1. Download the “Bundle for Version 8.x” (or whatever CircuitPython version you are using)
from https://circuitpython.org/libraries.
2. Expand the zip file, the files we want will be under the lib subdirectory.
3. Connect your Pico with CircuitPython installed. A drive called CIRCUITPY will appear on
your computer.
4. Copy the file adafruit_dht.mpy from the expanded bundle to the lib directory in the
CIRCUITPY drive.
1 # DHT11/DHT22 Example
2
3 import time
4 import adafruit_dht
5 import board
6
7 # Connections, -1 = not used
8 pinDHT11 = board.GP16
9 pinDHT22 = board.GP17
10
11 if pinDHT11 != -1:
12 dht11 = adafruit_dht.DHT11(pinDHT11)
13
14 if pinDHT22 != -1:
15 dht22 = adafruit_dht.DHT22(pinDHT22)
16
17 while True:
Temperature Sensors 223
18 if pinDHT11 != -1:
19 print("DHT11 Humidity: %.1f%%, Temperature: %.1fC" %
20 (dht11.humidity, dht11.temperature))
21 if pinDHT22 != -1:
22 print("DHT22 Humidity: %.1f%%, Temperature: %.1fC" %
23 (dht22.humidity, dht22.temperature))
24 time.sleep(3)
LM75A
The LM75A temperature sensor uses a standard I²C interface and has the following features:
LM75A Module
• Temperature (0)
• Hysteresis (1)
• Limit (2)
• Configuration (3)
Temperature Sensors 224
The temperature register is read-only and has two bytes (the most significant is sent first when
the register is read). Only the 11 most significant bits are relevant, they contain the temperature
as a 2-complement number in units of 0.125°C (an eighth of a degree).
The Hysteresis and Limit registers are used in the control of the OS pin. Their values are compared
to the temperature register. They also have two bytes, but only the 9 most significant bits are used.
They also use 2-complement notation, but the unit used is 0.5°C. They can be read and written.
In normal use, the value of the limit will be greater than the hysteresis.
The Configuration register is 8-bit and can be read and written. The following table explains the
meaning of its bits, the default values are marked by *.
When SHUTDOWN is 0, the LM75A sensor will do a temperature reading each 100ms, place the
result in the temperature register and update the OS pin. The power consumption in this mode
is 100µA with no communication, when the I²C is active it can go as high as 1mA.
If SHUTDOWN is 1, it will stop reading the temperature, the temperature register and the OS pin
will maintain their last values. Power consumption in shutdown mode is only 3.5µA.
Temperature Sensors 225
OS_POL controls the OS polarity. When it is “0”, OS is active on the LOW level and inactive on
the HIGH level. Changing OS_POL to “1” the signal is inverted. The OS output is open drain: the
LM75A can pull it to a LOW level or let it fluctuate. So a pull-up resistor is required (the datasheet
recommends a 200KΩ resistor, to keep power consumption down). The OS pin can sink currents
up to 10mA.
OS_F_QUE and OS_COMP_INT define the OS signal behavior. After each temperature reading, the
temperature is compared to the Hysteresis and Limit registers. The results are only considered
when there is no change in OS_F_QUE consecutive comparisons. This acts as a filter to ignore
momentary oscillation in the temperature.
If OS_COMP_INT is “0”, the LM75A is in comparison mode and acts like a thermostat. We start
with a temperature that is below the limit. When it rises above the limit, OS is activated. It stays
active until the temperature drops below the hysteresis.
If OS_COMP_INT is “1”, the LM75A is in interrupt mode. In this mode, the OS signal is used to
interrupt a microcontroller. Again, we start with a temperature that is below the limit. When it
rises above the limit, OS is activated and stays activated until any LM75A register is read. When
the temperature drops below the hysteresis, OS is activated again and stays activated until any
LM75A register is read.
LM75A Example
In our example, we will connect a LED to the OS output and configure the LM75A so the LED
will light when the temperature goes above 22.5°C and turn off when the temperature goes below
20°C. The current temperature is sent to the PC every half second.
As the interface is simple, we are going to write all the necessary code instead of using a library
specific to the sensor.
You will need to connect the Pico to a PC to see the program output.
1 /**
2 * @file lm75a_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief LM75A Temperature Sensor Example
5 * @version 1.0
6 * @date 2023-05-21
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
16 #include "hardware/i2c.h"
17
18 // Sensor connections
19 #define I2C_ID i2c0
20 #define I2C_SCL_PIN 17
21 #define I2C_SDA_PIN 16
22
23 #define BAUD_RATE 400000 // fast-mode 400KHz
24
25 // LM75A I2C Address
26 #define LM75A_ADDR 0x48
27
28 // LM75A Registers
29 #define REG_TEMP 0
Temperature Sensors 227
30 #define REG_CONF 1
31 #define REG_THYST 2
32 #define REG_TOS 3
33
34 //Write an 8-bit value into a register
35 void WriteReg8 (uint8_t reg, int8_t val) {
36 uint8_t buffer[2];
37
38 buffer[0] = reg;
39 buffer[1] = val;
40 i2c_write_blocking (I2C_ID, LM75A_ADDR, buffer, 2, false);
41 }
42
43 //Write a 16-bit value into a register
44 void WriteReg16 (uint8_t reg, int16_t val) {
45 uint8_t buffer[3];
46
47 buffer[0] = reg;
48 buffer[1] = val >> 8;
49 buffer[2] = val & 0xFF;
50 i2c_write_blocking (I2C_ID, LM75A_ADDR, buffer, 3, false);
51 }
52
53 // Read a 16-bit value from a register
54 int16_t ReadReg16 (uint8_t reg) {
55 uint8_t val[2];
56
57 // Select register
58 i2c_write_blocking (I2C_ID, LM75A_ADDR, ®, 1, true);
59
60 // Read value
61 i2c_read_blocking (I2C_ID, LM75A_ADDR, val, 2, false);
62 return ((int16_t) val[0] << 8) | val[1];
63 }
64
65 // Encode temperature for sensor
66 int16_t EncodeTemp (float temp) {
67 if (temp >= 0) {
68 return ((int16_t) (temp / 0.5)) << 7;
Temperature Sensors 228
69 } else {
70 return (512 + (int16_t) (temp / 0.5)) << 7;
71 }
72 }
73
74 // Decode temperature from sensor
75 float DecodeTemp (int16_t val) {
76 val = val / 32;
77 if (val >= 1024) {
78 return ((float) (val-2048)) * 0.125;
79 } else {
80 return ((float) val) * 0.125;
81 }
82 }
83
84 // Main Program
85 int main() {
86 // Init stdio
87 stdio_init_all();
88 #ifdef LIB_PICO_STDIO_USB
89 while (!stdio_usb_connected()) {
90 sleep_ms(100);
91 }
92 #endif
93
94 // Init I2C interface
95 uint baud = i2c_init (I2C_ID, BAUD_RATE);
96 printf ("I2C @ %u Hz\n", baud);
97 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
98 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
99 gpio_pull_up(I2C_SCL_PIN);
100 gpio_pull_up(I2C_SDA_PIN);
101
102 // Configure sensor and set limits for the OS output
103 WriteReg8 (REG_CONF, 0);
104 WriteReg16 (REG_TOS, EncodeTemp(22.5));
105 WriteReg16 (REG_THYST, EncodeTemp(20.0));
106
107 // Main loop
Temperature Sensors 229
108 while(1) {
109 sleep_ms(500);
110 printf("Temperature: %.3fC\n", DecodeTemp(ReadReg16(REG_TEMP)));
111 }
112 }
Arduino Code
The Arduino Environment requires a few calls for each I²C transaction.
Arduino LM75A Example
29 delay (500);
30 Serial.print("Temperature: ");
31 Serial.print(DecodeTemp(ReadReg16(REG_TEMP)));
32 Serial.println("C");
33 }
34
35 // Encode temperature for sensor
36 int16_t EncodeTemp (float temp)
37 {
38 if (temp >= 0) {
39 return ((int16_t) (temp / 0.5)) << 7;
40 } else {
41 return (512 + (int16_t) (temp / 0.5)) << 7;
42 }
43 }
44
45 // Decode temperature from sensor
46 float DecodeTemp (int16_t val)
47 {
48 val = val / 32;
49 if (val >= 1024) {
50 return ((float) (val-2048)) * 0.125;
51 } else {
52 return ((float) val) * 0.125;
53 }
54 }
55
56 //Write an 8-bit value into a register
57 void WriteReg8 (byte reg, int8_t val)
58 {
59 Wire.beginTransmission(ADDR);
60 Wire.write(reg);
61 Wire.write(val);
62 Wire.endTransmission();
63 }
64
65 //Write a 16-bit value into a register
66 void WriteReg16 (byte reg, int16_t val)
67 {
Temperature Sensors 231
68 Wire.beginTransmission(ADDR);
69 Wire.write(reg);
70 Wire.write((val >> 8) & 0xFF);
71 Wire.write(val & 0xFF);
72 Wire.endTransmission();
73 }
74
75 // Read a 16-bit value from a register
76 int16_t ReadReg16 (byte reg)
77 {
78 uint16_t val;
79
80 // Select register
81 Wire.beginTransmission(ADDR);
82 Wire.write(reg);
83 Wire.endTransmission();
84
85 // Read value
86 Wire.requestFrom(ADDR, 2);
87 val = Wire.read() << 8;
88 val |= Wire.read();
89 return (int16_t) val;
90 }
MicroPython Code
The MicroPython code is very concise, due to its built-in support for register-based I²C devices.
MicroPython LM75A Example
1 # LM75A Example
2
3 from machine import I2C,Pin
4 from time import sleep
5
6 # LM75A I2C Address
7 LM75_ADDR = 0x48
8
9 # LM75A Registers
10 LM75_TEMP = 0
Temperature Sensors 232
11 LM75_CONF = 1
12 LM75_THYST = 2
13 LM75_TOS = 3
14
15 # Encode temperature for sensor
16 def encodeTemp(temp):
17 if temp >= 0:
18 val = int(temp / 0.5) << 7
19 else:
20 val = (512 + int(temp/0.5)) << 7
21 return bytearray([val >> 8, val & 0xFF])
22
23 # Decode temperature from sensor
24 def decodeTemp(val):
25 val = val >> 5
26 if val >= 1024:
27 return (val-2048)*0.125
28 else:
29 return val*0.125
30
31 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
32
33 # Configure sensor and set limits for the OS output
34 i2c.writeto_mem(LM75_ADDR,LM75_CONF,b'\x00')
35 i2c.writeto_mem(LM75_ADDR,LM75_TOS,encodeTemp(22.5))
36 i2c.writeto_mem(LM75_ADDR,LM75_THYST,encodeTemp(20.0))
37
38 # Main loop: read temperature
39 while True:
40 sleep(0.5)
41 data = i2c.readfrom_mem(LM75_ADDR,LM75_TEMP,2)
42 temp = decodeTemp((data[0] << 8) + data[1])
43 print ('Temperature = {}C'.format (temp))
CircuitPython Code
In CircuitPython we need to write a few more lines than in MicroPython for accessing the LM75A
through I²C.
Temperature Sensors 233
1 # LM75A Example
2
3 import board
4 from busio import I2C
5 from time import sleep
6
7 # LM75A I2C Address
8 LM75_ADDR = 0x48
9
10 # LM75A Registers
11 LM75_TEMP = 0
12 LM75_CONF = 1
13 LM75_THYST = 2
14 LM75_TOS = 3
15
16 i2c = I2C(sda=board.GP16, scl=board.GP17)
17
18 # Encode temperature for sensor
19 def encodeTemp(temp):
20 if temp >= 0:
21 return int(temp / 0.5) << 7
22 else:
23 return (512 + int(temp/0.5)) << 7
24
25 # Decode temperature from sensor
26 def decodeTemp(val):
27 val = val >> 5
28 if val >= 1024:
29 return (val-2048)*0.125
30 else:
31 return val*0.125
32
33 # Write 8 bit value to a register
34 def writeReg8(reg, val):
35 data = bytearray([reg, val & 0xFF])
36 i2c.try_lock()
37 i2c.writeto(LM75_ADDR, data)
38 i2c.unlock()
Temperature Sensors 234
39
40 # Write 16 bit value to a register
41 def writeReg16(reg, val):
42 data = bytearray([reg, val >> 8, val & 0xFF])
43 i2c.try_lock()
44 i2c.writeto(LM75_ADDR, data)
45 i2c.unlock()
46
47 # Read 16 bit value from a register
48 def readReg16(reg):
49 selreg = bytearray([reg])
50 val = bytearray([0,0])
51 i2c.try_lock()
52 i2c.writeto_then_readfrom(LM75_ADDR, selreg, val)
53 i2c.unlock()
54 return val
55
56 # Configure sensor and set limits for the OS output
57 writeReg8(LM75_CONF,0)
58 writeReg16(LM75_TOS,encodeTemp(22.5))
59 writeReg16(LM75_THYST,encodeTemp(20.0))
60
61 # Main loop: read temperature
62 while True:
63 sleep(0.5)
64 data = readReg16(LM75_TEMP)
65 temp = decodeTemp((data[0] << 8) + data[1])
66 print ('Temperature = {}C'.format (temp))
Temperature Sensors 235
HDC1080
HDC1080 Module
The HDC1080 is a high-precision temperature and humidity sensor that uses a standard I²C
interface and has the following features:
The HDC1080 has a sleep mode and a measurement mode. It stays in sleep mode until it receives
a command to trigger a measurement. Once the measurement is completed, it returns to sleep
mode.
The HDC1080 has eight 16-bit registers:
The temperature register holds the result of a temperature measurement. Its two least significant
bits are always zero. The temperature in °C (T) can be calculated from the contents of this register
(R) by the following formula:
T = R
216
∗ 165 − 40
The humidity register holds the result of a humidity measurement. Its two least significant bits
are always zero. The relative humidity (H) can be calculated from the contents of this register (R)
by the following formula:
H= R
216
∗ 100
The configuration register controls the functionality of the sensor and also returns status
information:
Bits Name Description
15 RST Software reset, write “1” to reset the sensor. The bit returns to
0 when the reset is finished
14 Reserved Always 0
13 HEAT “1” will enable the heater. The heater can be used to test the
sensor or to drive condensation off the sensor. Once enabled,
the heater will be turned on when a measurement is made.
12 MODE “0” will acquire only temperature or humidity. “1” will
acquire temperature first and then humidity.
11 BTST “1” indicates that the power supply is below 2.8V
10 TRES Temperature measurement resolution. “0” is 14-bit and “1” is
11-bit.
9:8 HRES Humidity measurement resolution. “00” is 14-bit, “01” is
11-bit, and “10” is 8-bit.
7:0 Reserved Always 0
The three Serial ID registers contain a unique 40-bit serial number. The least significant byte of
the third register is always zero.
The Manufacturer ID and Device ID identify the device. They should be 0x5449 and 0x1050 for a
Texas Instruments HDC1080.
The way that the HDC1080 starts a measurement and returns the result is a little awkward. As
is common in I²C devices that have registers, the first byte written in a transaction is the register
address. What is uncommon is that if the address is 0x00 or 0x01 a measurement is started. If a
measurement was already in progress, it is canceled and a new measurement is started. If you
Temperature Sensors 237
try to read the temperature or humidity register and the measurement is not finished, a NAK is
returned.
To get the result you will have to wait for the measurement (see table below) or read until a
result (instead of a NAK) is returned. During the wait, you must not do a write transaction (as
this would select another register or start a new measurement).
The actual sequence of operations depends upon the MODE bit in the configuration register.
If MODE = 0, you do an I²C write to the HDC1080 with register address 0x00 to start a
temperature measurement or with register address 0x01 to start a humidity measurement. After
the measurement is completed, an I²C read will return the two bytes of the result.
If MODE = 1, you do an I²C write to the HDC1080 with register address 0x00, this starts the
temperature measurement. After the measurement is completed, a humidity measurement is
automatically started. After it finishes, an I²C read will return the four bytes of the result.
In a real application, you may not want to stop the program (“sleep”) while the measurement
is done. What you can do is start the measurement and set up a timer (using an interrupt or
checking the current time in the main loop) to retrieve the result. Care must be taken to not
access the HDC1080 while a measurement is running.
HDC1080 Example
In our example, the current humidity and temperature are sent to the PC every two seconds.
Temperature Sensors 238
I included the 4.7kΩ pull-ups because the module I used does not include them and CircuitPython
does not enable the RP2040 internal pull-ups.
As the interface is simple, we are going to write all the necessary code instead of using a library
specific to the sensor.
You will need to connect the Pico to a PC to see the program output.
1 /**
2 * @file hdc1080_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief HDC1080 Temperature Sensor Example
5 * @version 1.0
6 * @date 2023-05-22
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
Temperature Sensors 239
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
16 #include "hardware/i2c.h"
17
18 // Sensor connections
19 #define I2C_ID i2c0
20 #define I2C_SCL_PIN 17
21 #define I2C_SDA_PIN 16
22
23 #define BAUD_RATE 400000 // fast-mode 400KHz
24
25 // HDC1080 I2C Address
26 #define HDC1080_ADDR 0x40
27
28 // HDC1080 Registers
29 #define REG_TEMP 0
30 #define REG_HUM 1
31 #define REG_CONF 2
32 #define REG_MANID 0xFE
33 #define REG_DEVID 0xFF
34
35 // Read a 16-bit value from a register
36 int16_t ReadReg16 (uint8_t reg) {
37 uint8_t val[2];
38
39 // Select register
40 i2c_write_blocking (I2C_ID, HDC1080_ADDR, ®, 1, true);
41
42 // Read value
43 i2c_read_blocking (I2C_ID, HDC1080_ADDR, val, 2, false);
44 return ((int16_t) val[0] << 8) | val[1];
45 }
46
47 // Main Program
48 int main() {
49 // Init stdio
Temperature Sensors 240
50 stdio_init_all();
51 #ifdef LIB_PICO_STDIO_USB
52 while (!stdio_usb_connected()) {
53 sleep_ms(100);
54 }
55 #endif
56
57 // Init I2C interface
58 uint baud = i2c_init (I2C_ID, BAUD_RATE);
59 printf ("I2C @ %u Hz\n", baud);
60 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
61 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
62 gpio_pull_up(I2C_SCL_PIN);
63 gpio_pull_up(I2C_SDA_PIN);
64
65 // Check manufacture and device IDs
66 uint16_t manID = ReadReg16(REG_MANID);
67 uint16_t devID = ReadReg16(REG_DEVID);
68 printf ("Manufacturer: %04X Device: %04X\n",
69 manID, devID);
70
71 // Main loop
72 uint8_t reg[] = { REG_TEMP };
73 uint8_t val[4];
74 while(1) {
75 // Start conversion
76 i2c_write_blocking (I2C_ID, HDC1080_ADDR, ®, 1, true);
77 // Wait conversion
78 sleep_ms(20);
79 // Get result and print
80 i2c_read_blocking (I2C_ID, HDC1080_ADDR, val, 4, false);
81 uint16_t r = ((int16_t) val[0] << 8) | val[1];
82 float temp = r*165.0/65536.0 - 40.0;
83 r = ((int16_t) val[2] << 8) | val[3];
84 float humid = r*100.0/65536.0;
85 printf("Temperature: %.1fC Humidity: %.0f%%\n", temp, humid);
86 sleep_ms(2000);
87 }
88 }
Temperature Sensors 241
Arduino Code
37 Wire.write(REG_TEMP);
38 Wire.endTransmission();
39
40 // Wait conversion
41 delay(20);
42
43 // Get result and print
44 uint16_t r;
45 float temp, humid;
46 Wire.requestFrom(ADDR, 4);
47 r = Wire.read() << 8;
48 r |= Wire.read();
49 temp = r*165.0/65536.0 - 40.0;
50 r = Wire.read() << 8;
51 r |= Wire.read();
52 humid = r*100.0/65536.0;
53 Serial.print("Temperature: ");
54 Serial.print(temp, 1);
55 Serial.print("C Humidity: ");
56 Serial.print(humid, 0);
57 Serial.println("\%");
58 delay (2000);
59 }
60
61 // Read a 16-bit value from a register
62 int16_t ReadReg16 (byte reg)
63 {
64 uint16_t val;
65
66 // Select register
67 Wire.beginTransmission(ADDR);
68 Wire.write(reg);
69 Wire.endTransmission();
70
71 // Read value
72 Wire.requestFrom(ADDR, 2);
73 val = Wire.read() << 8;
74 val |= Wire.read();
75 return (int16_t) val;
Temperature Sensors 243
76 }
MicroPython Code
1 # HDC1080 Example
2
3 from machine import I2C,Pin
4 from time import sleep
5
6 # HDC1080 I2C Address
7 HDC_ADDR = 0x40
8
9 # HDC1080 Registers
10 HDC_TEMP = 0
11 HDC_HUM = 1
12 HDC_CONF = 2
13 HDC_MANID = 0xFE
14 HDC_DEVID = 0xFF
15
16 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
17
18 # Read 16-bit register
19 def readReg16(reg):
20 data = i2c.readfrom_mem(HDC_ADDR,reg,2)
21 return (data[0] << 8) + data[1]
22
23 # Check manufacture and device IDs
24 print ('Manufacturer: {:04X}'.format(readReg16(HDC_MANID)))
25 print ('Device: {:04X}'.format(readReg16(HDC_DEVID)))
26
27 while True:
28 i2c.writeto(HDC_ADDR, bytearray([HDC_TEMP]))
29 sleep (0.02)
30 data = i2c.readfrom(HDC_ADDR, 4)
31 r = (data[0] << 8) + data[1]
32 temp = r*165/65536 - 40.0
33 r = (data[2] << 8) + data[3]
Temperature Sensors 244
34 humid = r*100/65536
35 print ('Temperature: {:.1f}C Humidity: {:.0f}%'.format(temp, humid))
36 sleep(2)
CircuitPython Code
1 # HDC1080 Example
2
3 import board
4 from digitalio import DigitalInOut, Pull
5 from busio import I2C
6 from time import sleep
7
8 # HDC1080 I2C Address
9 HDC_ADDR = 0x40
10
11 # HDC1080 Registers
12 HDC_TEMP = 0
13 HDC_HUM = 1
14 HDC_CONF = 2
15 HDC_MANID = 0xFE
16 HDC_DEVID = 0xFF
17
18 i2c = I2C(sda=board.GP16, scl=board.GP17)
19
20 def readReg16(reg):
21 selreg = bytearray([reg])
22 val = bytearray([0,0])
23 i2c.try_lock()
24 i2c.writeto_then_readfrom(HDC_ADDR, selreg, val)
25 i2c.unlock()
26 return (val[0]<<8)+val[1]
27
28 # Check manufacture and device IDs
29 print ('Manufacturer: {:04X}'.format(readReg16(HDC_MANID)))
30 print ('Device: {:04X}'.format(readReg16(HDC_DEVID)))
31
Temperature Sensors 245
32 while True:
33 i2c.try_lock()
34 i2c.writeto(HDC_ADDR, bytearray([HDC_TEMP]))
35 i2c.unlock()
36 sleep (0.02)
37 i2c.try_lock()
38 data = bytearray([0,0,0,0])
39 i2c.readfrom_into(HDC_ADDR, data)
40 r = (data[0] << 8) + data[1]
41 temp = r*165/65536 - 40.0
42 r = (data[2] << 8) + data[3]
43 humid = r*100/65536
44 print ('Temperature: {:.1f}C Humidity: {:.0f}%'.format(temp, humid))
45 sleep(2)
MCP9808
The MCP9808 is a high-precision temperature sensor with an I²C interface.
The MCP9808 has seven 16-bit registers and one 8-bit register (RESOL):
Unless the MCP9808 is put in shutdown (low-power) mode, it is continuously updating the
Ambient Temperature register. The rate of update depends on the resolution set.
The writing on T_UPPER, T_LOWER, T_CRIT, and certain bits of CONFIG can be prohibited (locked)
through the configuration. Also, some configurations cannot be changed when the sensor is in
shutdown mode. This protects the operation of the Alert Function in the event of erroneous
updates due to a software failure.
The CONFIG register controls the operation of the sensor. Most of the configurations are related
to the Alert Function that is described in the next section. Bits 15 to 11 are not used and will
read as zeros. After a Power-on reset, the configuration register is all zeros.
• THYST (bits 10 and 9): defines a margin applied to the T_LOWER, T_UPPER, and T_CRIT
values when the temperature is decreasing. This means that the change in the Alert output
will occur later (when the temperature has fallen below the margin or started to increase).
These bits cannot be changed if Critical Lock or Window Lock is set.
THYST Margin
00 0°C
01 1.5°C
10 3.0°C
11 6.0°C
Temperature Sensors 247
• SHDN (bit 8): 0 is normal operation, and 1 is shutdown mode. If Critical Lock or Window
Lock is set, this bit cannot be changed to ‘1’.
• Critical Lock (bit 7): 0 is normal operation, 1 sets the Critical Lock (T_CRIT register and
Alert Configuration and THYST bits cannot be changed). Once set, it can only be cleared
by a Power-on Reset.
• Window Lock (bit 6): 0 is normal operation, 1 sets the Windows Lock (T_UPPER and T_-
LOWER registers and Alert Configuration and THYST bits cannot be changed). Once set, it
can only be cleared by a Power-on Reset.
• Int Clear (bit 5): write a ‘1’ in this bit to clear the interrupt output.
• Alert Status (bit 4): 0 indicates alert is not asserted, 1 indicates alert is asserted.
• Alert Control (bit 3): 0 disables the alert function, and 1 enables it. This bit cannot be
changed if the Critical or Window Lock is set.
• Alert Select (bit 2): O enables alert output based on the critical temperature and temper-
ature window (upper and lower temperature boundaries), and and 1 enables alert output
based only on the critical temperature. This bit cannot be changed if Window Lock is set.
• Alert Polarity (bit 1): 0 for active-low, 1 for active-high. This bit cannot be changed if the
Critical or Window Lock is set.
• Alert Mode (bit 0): 0 for comparator output, 1 for interrupt output. This bit cannot be
changed if the Critical or Window Lock is set.
The T_UPPER, T_LOWER, and T_CRIT register contain in bits 12 to 2 temperatures in an 11-bit
two’s complement format, in units of 0.25°C. These values are used in the Alert Function.
Some examples of temperature encoding (notice that the three most significant bits and the two
least significant bits are always zeros):
The TA register contains in bits 12 to 0 the ambient temperature in a 13-bit two’s complement
format, in units of 0.0625°C (1/16 °C). The three most significant bits indicate the result of the
comparison of TA with T_CRIT, T_UPPER, and T_LOWER:
To calculate the temperature from the contents of the TA register we need to:
Temperature Sensors 248
• Clear bits 15 to 13
• Consider bit 12 as a sign bit (1 if negative temperature)
• If negative, subtract 0x2000 from the result
• Divide the result by 16
The MCP9808 datasheet shows a different (but equivalent) procedure, that treats the
contents as two bytes instead of a 16-bit value.
The MAN_ID register should be 0x0054, indicating the sensor was manufactured by Microchip.
The DEV_ID register contains the device identification in the upper byte and the device revision
in the lower byte. The device id should be 0x04, the revision starts at 0x00.
The RESOL register selects the sensor resolution, this affects the temperature conversion time:
Alert Function
The MCP9808 has an Alert output pin. If the alert function is enabled by the Alert Control bit in
the configuration register, and the sensor is not in shutdown mode, the alert pin will be updated
every time the ambient temperature is updated.
The alert pin can be in an asserted or de-asserted state. By default, asserted corresponds to setting
the pin low, and deasserted to letting the pin float. This can be reversed by the Alert Polarity bit
in the configuration.
In comparator mode, the alert pin is asserted and de-asserted by the result of the comparison
of the ambient temperature with the critical, lower, and upper temperatures. It will be asserted
whenever the ambient temperature is above the critical temperature. Depending on the Alert
Select bit in the configuration register, alert can also be asserted when the ambient temperature
is below the lower boundary or above the upper boundary.
In interrupt mode, the alert pin is asserted when the ambient temperature crosses (rising or falling)
the limits set by the critical, lower, and upper temperatures. Once the alert pin is asserted it can
only be asserted by writing a 1 in the Int Clear bit in the configuration register.
Temperature Sensors 249
When the temperature is decreasing, the temperature boundaries are reduced by the hysteresis.
The figure below, taken from the datasheet, shows the changes in the alert output with the
ambient temperature graphically.
MCP9808 Example
In our example, we will connect a LED to the Alert output and configure the MCP9808 so the
LED will light when the temperature is above 23°C or below 20°C. The current temperature is
sent to the PC every half second.
Temperature Sensors 250
The Adafruit MCP9808 module has pull-downs at the Ax pins, so the I2C address is 0x18 if they
are left unconnected.
As the sensor interface is simple, we are going to write all the necessary code instead of using a
library specific to the it.
You will need to connect the Pico to a PC to see the program output.
1 /**
2 * @file mcp9808_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief MCP9808 Temperature Sensor Example
5 * @version 1.0
6 * @date 2023-05-22
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
Temperature Sensors 251
16 #include "hardware/i2c.h"
17
18 // Sensor connections
19 #define I2C_ID i2c0
20 #define I2C_SCL_PIN 17
21 #define I2C_SDA_PIN 16
22
23 #define BAUD_RATE 400000 // fast-mode 400KHz
24
25 // MCP9808 I2C Address
26 #define MCP9808_ADDR 0x18
27
28 // MCP9808 Registers
29 #define REG_CONFIG 1
30 #define REG_UPPER 2
31 #define REG_LOWER 3
32 #define REG_CRIT 4
33 #define REG_TA 5
34 #define REG_MANID 6
35 #define REG_DEVID 7
36 #define REG_RESOL 8
37
38 //Write a 16-bit value into a register
39 void WriteReg16 (uint8_t reg, int16_t val) {
40 uint8_t buffer[3];
41
42 buffer[0] = reg;
43 buffer[1] = val >> 8;
44 buffer[2] = val & 0xFF;
45 i2c_write_blocking (I2C_ID, MCP9808_ADDR, buffer, 3, false);
46 }
47
48 // Read a 16-bit value from a register
49 int16_t ReadReg16 (uint8_t reg) {
50 uint8_t val[2];
51
52 // Select register
53 i2c_write_blocking (I2C_ID, MCP9808_ADDR, ®, 1, true);
54
Temperature Sensors 252
55 // Read value
56 i2c_read_blocking (I2C_ID, MCP9808_ADDR, val, 2, false);
57 return ((int16_t) val[0] << 8) | val[1];
58 }
59
60 // Encode temperature for sensor
61 int16_t EncodeTemp (float temp) {
62 if (temp >= 0) {
63 return ((uint16_t) (temp/0.25)) << 2;
64 } else {
65 return ((uint16_t) (2048 + temp/0.25)) << 2;
66 }
67 }
68
69 // Decode temperature from sensor
70 float DecodeTemp (int16_t val) {
71 bool sign = (val & 0x1000) != 0;
72 val = val & 0x1FFF;
73 if (sign) {
74 val = val - 0x2000;
75 }
76 return (float)val/16.0;
77 }
78
79 // Main Program
80 int main() {
81 // Init stdio
82 stdio_init_all();
83 #ifdef LIB_PICO_STDIO_USB
84 while (!stdio_usb_connected()) {
85 sleep_ms(100);
86 }
87 #endif
88
89 // Init I2C interface
90 uint baud = i2c_init (I2C_ID, BAUD_RATE);
91 printf ("I2C @ %u Hz\n", baud);
92 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
93 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
Temperature Sensors 253
94 gpio_pull_up(I2C_SCL_PIN);
95 gpio_pull_up(I2C_SDA_PIN);
96
97 // Check manufacture and device IDs
98 uint16_t manID = ReadReg16(REG_MANID);
99 uint16_t devID = ReadReg16(REG_DEVID);
100 printf ("Manufacturer: %04X Device: %02X rev %u\n",
101 manID, devID >> 8, devID & 0xFF);
102
103 // Set limits for the Alert output
104 WriteReg16(REG_CRIT,EncodeTemp(30.0));
105 WriteReg16(REG_UPPER,EncodeTemp(23.0));
106 WriteReg16(REG_LOWER,EncodeTemp(20.0));
107 WriteReg16(REG_CONFIG,0x0008);
108
109 // Main loop
110 while(1) {
111 sleep_ms(500);
112 printf("Temperature: %.3fC\n", DecodeTemp(ReadReg16(REG_TA)));
113 }
114 }
Arduino Code
14 #define REG_MANID 6
15 #define REG_DEVID 7
16 #define REG_RESOL 8
17
18 // Initialization
19 void setup() {
20 Serial.begin (115200);
21 Wire.setSDA(16);
22 Wire.setSCL(17);
23 Wire.begin();
24
25 // Check manufacture and device IDs
26 uint16_t manID = ReadReg16(REG_MANID);
27 uint16_t devID = ReadReg16(REG_DEVID);
28 Serial.print ("Manufacturer: ");
29 Serial.println (manID, HEX);
30 Serial.print ("Device: ");
31 Serial.print (devID >> 8, HEX);
32 Serial.print (" rev ");
33 Serial.println (devID & 0xFF);
34
35 // Set limits for the Alert output
36 WriteReg16(REG_CRIT,EncodeTemp(30.0));
37 WriteReg16(REG_UPPER,EncodeTemp(23.0));
38 WriteReg16(REG_LOWER,EncodeTemp(20.0));
39 WriteReg16(REG_CONFIG,0x0008);
40 }
41
42 // Main loop: read temperature
43 void loop() {
44 delay (500);
45 Serial.print("Temperature: ");
46 Serial.print(DecodeTemp(ReadReg16(REG_TA)));
47 Serial.println("C");
48 }
49
50 // Encode temperature for sensor
51 int16_t EncodeTemp (float temp)
52 {
Temperature Sensors 255
53 if (temp >= 0) {
54 return ((uint16_t) (temp/0.25)) << 2;
55 } else {
56 return ((uint16_t) (2048 + temp/0.25)) << 2;
57 }
58 }
59
60 // Decode temperature from sensor
61 float DecodeTemp (int16_t val)
62 {
63 bool sign = (val & 0x1000) != 0;
64 val = val & 0x1FFF;
65 if (sign) {
66 val = val - 0x2000;
67 }
68 return (float)val/16.0;
69 }
70
71 //Write a 16-bit value into a register
72 void WriteReg16 (byte reg, int16_t val)
73 {
74 Wire.beginTransmission(ADDR);
75 Wire.write(reg);
76 Wire.write((val >> 8) & 0xFF);
77 Wire.write(val & 0xFF);
78 Wire.endTransmission();
79 }
80
81 // Read a 16-bit value from a register
82 int16_t ReadReg16 (byte reg)
83 {
84 uint16_t val;
85
86 // Select register
87 Wire.beginTransmission(ADDR);
88 Wire.write(reg);
89 Wire.endTransmission();
90
91 // Read value
Temperature Sensors 256
92 Wire.requestFrom(ADDR, 2);
93 val = Wire.read() << 8;
94 val |= Wire.read();
95 return (int16_t) val;
96 }
MicroPython Code
1 # MCP9808 Example
2
3 from machine import I2C,Pin
4 from time import sleep
5
6 # MCP9808 I2C Address
7 MCP9808_ADDR = 0x18
8
9 # MCP9808 Registers
10 MCP9808_CONFIG = 1
11 MCP9808_UPPER = 2
12 MCP9808_LOWER = 3
13 MCP9808_CRIT = 4
14 MCP9808_TA = 5
15 MCP9808_MANID = 6
16 MCP9808_DEVID = 7
17 MCP9808_RESOL = 8
18
19 # Encode temperature for sensor
20 def encodeTemp(temp):
21 if temp >= 0:
22 val = int(temp/0.25) << 2
23 else:
24 val = (2048 + int(temp/0.25)) << 2
25 return bytearray([val >> 8, val & 0xFF])
26
27 # Decode temperature from sensor
28 def decodeTemp(val):
29 sign = val & 0x1000
Temperature Sensors 257
CircuitPython Code
Temperature Sensors 258
1 # MCP9808 Example
2
3 import board
4 from busio import I2C
5 from time import sleep
6
7 # MCP9808 I2C Address
8 MCP9808_ADDR = 0x18
9
10 # MCP9808 Registers
11 MCP9808_CONFIG = 1
12 MCP9808_UPPER = 2
13 MCP9808_LOWER = 3
14 MCP9808_CRIT = 4
15 MCP9808_TA = 5
16 MCP9808_MANID = 6
17 MCP9808_DEVID = 7
18 MCP9808_RESOL = 8
19
20 # Encode temperature for sensor
21 def encodeTemp(temp):
22 if temp >= 0:
23 val = int(temp/0.25) << 2
24 else:
25 val = (2048 + int(temp/0.25)) << 2
26 return val
27
28 # Decode temperature from sensor
29 def decodeTemp(val):
30 sign = val & 0x1000
31 val = val & 0x1FFF
32 if sign != 0:
33 val = val - 0x2000
34 return val/16
35
36 i2c = I2C(sda=board.GP16, scl=board.GP17)
37
38 # Write 16 bit value to a register
Temperature Sensors 259
AHT10
AHT10 Module
The AHT10 sensor from Asair is a temperature and humidity sensor with an I²C interface and
the following characteristics:
A read I²C transaction returns a status byte, followed by five bytes with the humidity and
temperature data.
The AHT10 status byte has two bits of useful information:
• The Soft Reset command restarts the sensor. After sending this command, wait 20 ms.
• The Initialization command should be sent after powering up the sensor if the status
indicates a calibration is necessary. After sending this command, wait 10 ms.
• The Start Conversion command starts a humidity and temperature reading. You have to
wait for up to 80 ms for the result.
• In the first time after powering up the sensor, read the status. If bit 3 is zero, send the
Initialization command and wait 10 ms.
• Send the Start Conversion command.
• Wait 80 ms and/or read periodically the status, until it indicates that the reading is
completed.
• Read 6 bytes from the sensor. The first byte is the status and the next five the humidity
and temperature data.
The readings of humidity (H) and temperature (T) are calculated from the data (V e V) by the
following formulas:
H= Vu
220
∗ 100
T = Vt
220
∗ 200 − 50
Temperature Sensors 262
AHT10 Example
In this example we are simply going to show the humidity and temperature values every two
seconds.
Again, we will write all the code instead of using a library, as the sensor interface is simple.
You will need to connect the Pi Pico to a PC to see the program’s output.
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
16 #include "hardware/i2c.h"
17
18 // Sensor Connections
19 #define I2C_ID i2c0
20 #define I2C_SCL_PIN 17
21 #define I2C_SDA_PIN 16
22
23 #define BAUD_RATE 400000 // fast-mode 400KHz
24
25 // AHT10 I2C Address
26 #define AHT10_ADDR 0x38
27
28 // AHT10 Commands
29 uint8_t cmdInit[] = { 0xE1, 0x08, 0x00 };
30 uint8_t cmdConv[] = { 0xAC, 0x33, 0x00 };
31
32 // Read Status
33 int8_t getStatus () {
34 uint8_t val[1];
35
36 i2c_read_blocking (I2C_ID, AHT10_ADDR, val, 1, false);
37 return val[0];
38 }
39
40 // Main Program
41 int main() {
42 // Init stdio
43 stdio_init_all();
44 #ifdef LIB_PICO_STDIO_USB
45 while (!stdio_usb_connected()) {
46 sleep_ms(100);
47 }
48 #endif
Temperature Sensors 264
49
50 // Init I2C interface
51 uint baud = i2c_init (I2C_ID, BAUD_RATE);
52 printf ("I2C @ %u Hz\n", baud);
53 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
54 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
55 gpio_pull_up(I2C_SCL_PIN);
56 gpio_pull_up(I2C_SDA_PIN);
57
58 // Check if a calibration is needed
59 uint8_t status = getStatus();
60 if ((status & 0x08) == 0) {
61 printf ("Calibrating\n");
62 i2c_write_blocking (I2C_ID, AHT10_ADDR, cmdInit,
63 sizeof(cmdInit), false);
64 sleep_ms(10);
65 }
66
67 // Main Loop
68 while(1) {
69 // Starts a conversion
70 i2c_write_blocking (I2C_ID, AHT10_ADDR, cmdConv,
71 sizeof(cmdConv), false);
72 // Wait conversion
73 sleep_ms(80);
74 // Get and show result
75 uint8_t r[6];
76 i2c_read_blocking (I2C_ID, AHT10_ADDR, r, sizeof(r), false);
77 float humid = (r[1] << 12) + (r[2] << 4) + (r[3] >> 4);
78 humid = (humid / 0x100000) * 100.0;
79 float temp = ((r[3] & 0x0F) << 16) + (r[4] << 8) + r[5];
80 temp = (temp / 0x100000) * 200.0 - 50.0;
81 printf("Temperature: %.1fC Humidity: %.0f%%\n", temp, humid);
82 sleep_ms(2000);
83 }
84 }
Arduino Code
Temperature Sensors 265
39 // Wait conversion
40 delay(80);
41
42 // Get and show result
43 uint16_t r[6];
44 float temp, humid;
45 Wire.requestFrom(ADDR, 6);
46 for (int i = 0; i < 6; i++) {
47 r[i] = Wire.read();
48 }
49 humid = (r[1] << 12) + (r[2] << 4) + (r[3] >> 4);
50 humid = (humid / 0x100000) * 100.0;
51 temp = ((r[3] & 0x0F) << 16) + (r[4] << 8) + r[5];
52 temp = (temp / 0x100000) * 200.0 - 50.0;
53 Serial.print("Temperature: ");
54 Serial.print(temp, 1);
55 Serial.print("C Humidity: ");
56 Serial.print(humid, 0);
57 Serial.println("\%");
58 delay (2000);
59 }
60
61 // Read status
62 int8_t getStatus ()
63 {
64 Wire.requestFrom(ADDR, 1);
65 return Wire.read();
66 }
MicroPython Code
Temperature Sensors 267
39 sleep_ms(2000)
CircuitPython Code
34 i2c.writeto(AHT_ADDR, b'\xAC\x33\x00')
35 i2c.unlock()
36 # Wait conversion
37 sleep(0.08)
38 # Get result
39 resp = bytearray([0,0,0,0,0,0])
40 i2c.try_lock()
41 i2c.readfrom_into (AHT_ADDR, resp)
42 i2c.unlock()
43 # Decode result
44 umid = (resp[1] << 12) + (resp[2] << 4) + (resp[3] >> 4)
45 umid = (umid / 0x100000) * 100.0
46 temp = ((resp[3] & 0x0F) << 16) + (resp[4] << 8) + resp[5]
47 temp = (temp / 0x100000) * 200.0 - 50.0
48 # Show result
49 print ('Humidity = {:.1f}% Temperature = {:.1f}C'.format(umid, temp))
50 # Wait between readings
51 sleep(2)
1
altitude = 44330 ∗ (1 − ( pp0 ) 5.255 )
These formulas don’t take into account variations in pressure from the weather.
For a simple weather forecast, if we know the altitude, we can compare the measured pressure
with the expected pressure based on the altitude. We should have good weather if the measured
pressure is 2.5hPa or more above the expected pressure. We should have bad weather if it’s 2.5hPa
or move below.
In a practical situation, if we know the sensor is not changing altitude, we can record the pressure
over time and use the average to find the altitude and use the changes from it to predict changes
in the weather.
⁶It is also possible to construct a bridge with one or two sensors and fixed resistors, but the result is not as good.
Atmospheric Pressure Sensors 271
The atmospheric pressure sensors we are going to look at in this chapter are all from Bosch
Sensortec GmbH. Some of the older models have been discontinued, but still can be found. All
measure pressure and temperature; the BME280 model adds Humidity.
In the boards above, the sensors are the small metal-encased components in the center.
Each sensor is factory calibrated, the calibration coefficients are saved in an EEProm in the
sensor. The datasheets provide the formulas to calculate the temperature and pressure using these
coefficients.
All the sensors can operate from -40 to +85°C but only have full accuracy from 0 to +65°C.
These components are pretty small and are available only as surface-mounted devices (SMD), so
if you are a hobbyist (or just want to make some tests) you will probably use a module (like the
above) with it. Pay attention to what other components are in the module, typically you will find:
The examples will send to a PC the readings from the sensors. You will need to connect the Pico
to a PC to see the results.
In the C/C++ SDK examples, I will write the full code. For the Arduino, MicroPython, and
CircuitPython examples, I will use already available libraries.
They have an I²C interface, supporting standard, fast, and high-speed modes (clock up to
3.4Mbits/s). The address is fixed at 0x77.
There are eleven signed 16-bit coefficients. They are read by issuing an I²C write transaction with
the 8-bit register address (0xAA to 0xBE) followed by an I²C read transaction to get the two bytes
(most significant first). Since they don’t change, you can read them at the start of your program
and save them for later use.
You can do independent pressure and temperature readings. To do a temperature reading, you
write a command (0x2E) to the control register (address 0xF4), wait 4.5 ms, and then read the
unsigned 16-bit raw data from register 0xF6. The raw data is converted to the temperature by the
formula in the datasheet (you can also see it in the C/C++ SDK example).
Reading pressure is similar, but the command written to the control register (0x34) allows an
oversampling mode in the upper two bits:
Oversampling works by doing multiple readings to get more bits of precision. As you can see in
the table, the time for the measurement (and the power consumption) depends on the mode.
The raw pressure data has 19 to 16 bits, depending on the oversampling mode. You must read three
bytes starting at register 0xF6 (most significant first), shift it right 3, 2, 1, or 0 bits, depending on
the mode, and use the resulting value in the pressure formula (see C/C++ SDK example).
Atmospheric Pressure Sensors 273
BMP085/BMP180 Registers
Note that temperature and pressure data share the same registers, so you must start one
conversion, wait, read the result, start the other conversion, wait, and read the result.
The ID register returns 0x55 for both models. Writing 0xB6 to the Reset register will reset the
sensor to the power-on (default) condition.
The BMP085 has two additional connections to the microcontroller. The EOC output signals the
end of a conversion and the XCLR input resets the sensor when at a LOW level.
1 /**
2 * @file bmp180_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief BMP085/BMP180 Sensor Example
5 * @version 1.0
6 * @date 2023-06-11
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
Atmospheric Pressure Sensors 275
14 #include <math.h>
15 #include "pico/stdlib.h"
16 #include "hardware/i2c.h"
17
18 // Sensor connections
19 #define I2C_ID i2c0
20 #define I2C_SCL_PIN 17
21 #define I2C_SDA_PIN 16
22
23 #define BAUD_RATE 1000000 // 1MHz
24
25
26 // Class to access BMP180 Sensor
27 class BMP180 {
28
29 public:
30
31 // resolution
32 typedef enum : uint8_t
33 {
34 ULTRALOWPOWER = 0x00,
35 STANDARD = 0x01,
36 HIGHRES = 0x02,
37 ULTRAHIGHRES = 0x03
38 } OVERSAMPLING;
39
40
41 BMP180 (i2c_inst_t *i2c, OVERSAMPLING ovs = ULTRAHIGHRES);
42 void softReset();
43 uint8_t getDeviceId();
44 float getTemperature();
45 float getPressure();
46 float calcAltitude(float pressure, float sealevelPressure = 101325.0);
47
48 private:
49 static const uint8_t BMP180_ADDR = 0x77;
50 static const int waitPressure[4];
51
52 // Registers Address
Atmospheric Pressure Sensors 276
92 int16_t MC = 0;
93 int16_t MD = 0;
94
95 // Oversampling
96 uint8_t oss;
97
98 // Private routines
99 void readCalibCoeff(void);
100 uint16_t readRawTemperature(void);
101 uint32_t readRawPressure(void);
102 int32_t computeB5(int32_t UT);
103 void write8(uint8_t reg, uint8_t val);
104 uint8_t read8(uint8_t reg);
105 uint16_t read16(uint8_t reg);
106 };
107
108 // Constructor
109 BMP180::BMP180 (i2c_inst_t *i2c, OVERSAMPLING ovs) {
110 this->i2c = i2c;
111 this->oss = (uint8_t) ovs;
112 readCalibCoeff();
113 }
114
115 // Do a software reset
116 void BMP180::softReset () {
117 write8(SOFT_RESET, SOFT_RESET_VALUE);
118 }
119
120 // Read Device ID
121 uint8_t BMP180::getDeviceId() {
122 return read8(GET_ID);
123 }
124
125 // Get temperature in C
126 float BMP180::getTemperature() {
127 int16_t rawTemperature = readRawTemperature();
128 int32_t B5 = computeB5(rawTemperature);
129 return (float)(B5 + 8) / 160.0;
130 }
Atmospheric Pressure Sensors 278
131
132 // Get pressure in Pa
133 float BMP180::getPressure() {
134 int32_t UT = 0;
135 int32_t UP = 0;
136 int32_t B3 = 0;
137 int32_t B5 = 0;
138 int32_t B6 = 0;
139 int32_t X1 = 0;
140 int32_t X2 = 0;
141 int32_t X3 = 0;
142 int32_t pressure = 0;
143 uint32_t B4 = 0;
144 uint32_t B7 = 0;
145
146 UT = readRawTemperature(); //read uncom\
147 pensated temperature, 16-bit
148 UP = readRawPressure(); //read uncom\
149 pensated pressure, 19-bit
150 B5 = computeB5(UT);
151
152 B6 = B5 - 4000;
153 X1 = ((int32_t) B2 * ((B6 * B6) >> 12)) >> 11;
154 X2 = ((int32_t) AC2 * B6) >> 11;
155 X3 = X1 + X2;
156 B3 = ((((int32_t) AC1 * 4 + X3) << oss) + 2) / 4;
157
158 X1 = ((int32_t) AC3 * B6) >> 13;
159 X2 = ((int32_t) B1 * ((B6 * B6) >> 12)) >> 16;
160 X3 = ((X1 + X2) + 2) >> 2;
161 B4 = ((uint32_t) AC4 * (X3 + 32768L)) >> 15;
162 B7 = (UP - B3) * (50000UL >> oss);
163
164 pressure = (B7 < 0x80000000) ? (B7 * 2) / B4 : (B7 / B4) * 2;
165
166 X1 = (pressure >> 8)*(pressure >> 8);
167 X1 = (X1 * 3038L) >> 16;
168 X2 = (-7357L * pressure) >> 16;
169
Atmospheric Pressure Sensors 279
209
210 // Read raw pressure data
211 const int BMP180::waitPressure[4] = { 5, 8, 14, 26 };
212 uint32_t BMP180::readRawPressure(void)
213 {
214 write8(START_MEAS, GET_PRESS + (oss << 6));
215 sleep_ms(waitPressure[oss]);
216 uint32_t rawPressure = read16(ADC_MSB) << 8;
217 rawPressure |= read8(ADC_XLSB);
218 rawPressure >>= (8 - oss);
219 return rawPressure;
220 }
221
222 // Write an 8-bit value into a register
223 void BMP180::write8 (uint8_t reg, uint8_t val) {
224 uint8_t buffer[2];
225
226 buffer[0] = reg;
227 buffer[1] = val;
228 i2c_write_blocking (i2c, BMP180_ADDR, buffer, 2, false);
229 }
230
231 // Read a 8-bit value from a register
232 uint8_t BMP180::read8 (uint8_t reg) {
233 uint8_t val[1];
234
235 // Select register
236 i2c_write_blocking (i2c, BMP180_ADDR, ®, 1, true);
237
238 // Read value
239 i2c_read_blocking (i2c, BMP180_ADDR, val, 1, false);
240 return val[0];
241 }
242
243 // Read a 16-bit value from a register
244 uint16_t BMP180::read16 (uint8_t reg) {
245 uint8_t val[2];
246
247 // Select register
Atmospheric Pressure Sensors 281
Arduino Code
There are a few BMP085/BMP180 libraries. We are going to use the one from Adafruit.
16 }
17 }
18 }
19
20 void loop() {
21 Serial.print("Temperature: ");
22 Serial.print(bmp.readTemperature());
23 Serial.print(" C");
24
25 Serial.print(" Pressure: ");
26 Serial.print(bmp.readPressure());
27 Serial.print(" Pa");
28
29 // Calculate altitude assuming 'standard' barometric
30 // pressure of 1013.25 millibar = 101325 Pascal
31 Serial.print(" Altitude: ");
32 Serial.print(bmp.readAltitude());
33 Serial.println(" m");
34
35 delay(2000);
36 }
MicroPython Code
A nice class to use the BMP085/BMP180 sensor with MicroPython can be found at
https://github.com/micropython-IMU/micropython-bmp180
The file you will need to place in the lib directory in the Pi Pico is bmp180.py. You can do this
by:
1. Clicking the Code button in GitHub and downloading all the files as a ZIP file
2. Expanding the ZIP in a directory on your PC
3. Using Thonny to create the lib directory in the Pico and to copy the bmp180.py file to it.
The current version of the library will not run with the current version of MicroPython. You will
have to open the bmp180.py file in Thonny and remove the line⁷
⁷The start method is only available for software I²C and generates a start condition (that makes no sense in this context). I suspect
that the author of the library thought start was some kind of initialization that had to be called before other methods.
Atmospheric Pressure Sensors 284
self._bmp_i2c.start()
CircuitPython Code
Since the BMP085/BMP180 is no longer supported in the Adafruit CircuitPython bundle, we are
going to use a library available at
https://github.com/jposada202020/CircuitPython_BMP180/tree/master.
This library requires some classes that are part of the Adafruit bundle⁸. To set it all up, you will
need to:
2. Download the “Bundle for Version 8.x” (or whatever CircuitPython version you are using)
from https://circuitpython.org/libraries.
3. Expand the zip file, the files we want will be under the lib subdirectory.
4. Use Thonny to copy to the lib directory in the Pico the bmp180.py file, and the adafruit_-
bus_device and adafruit_register subdirectories from the bundle.
These modes can be selected using the mode bits in the ctrl_meas register (at address 0xF4).
A measurement cycle consists of up to four steps: measure temperature, measure pressure,
measure humidity (BME280 only), and filtering. Each step can be enabled or disabled.
The pressure oversampling is controlled by the osrs_p bits in the ctrl_meas register at address
0xF4:
Setting Oversampling Resolution
Skip measurement - -
ultra low power 1 16 bit
low power 2 17 bit
standard 4 18 bit
high resolution 8 19 bit
ultra high resolution 16 20 bit
Oversampling is available also for temperature and humidity measurement. For temperature, it
is controlled by the osrs_t bits in the ctrl_meas register at address 0xF4.
Oversampling for humidity (in the BME280) is controlled by the osrs_h bits in the ctrl_hum
register at address 0xF2.
In the following tables, the shaded information applies only to the BME280.
BMP280/BME280 Registers
Note that the raw data for the three measurements have separated registers.
The ID register returns 0x58 for the BMP280 and 0x60 for the BME280. Writing 0xB6 to the Reset
register will reset the sensor to the power-on condition.
Other differences between the BMP280 and the BME280:
• In the t_sb bits in the config register, the values 110 and 111 set different standby times:
10ms and 20ms for the BME280 and 2s and 5s for the BMP280.
• In the BME280, if the filter is enabled, pressure and temperature resolution will be 20-bit
(16 times oversampling), regardless of the osrs_p and osrs_t settings.
The sensors have four digital pins for interfacing with a microcontroller:
• CSB: Chip Select. To use the I²C interface, this pin must always be at a HIGH level. Once
this pin is pulled down, the I²C interface is disabled until the sensor is reset. If you are
using the SPI interface, this pin is a normal active low chip select.
• SDI: This is MOSI if you use SPI or SDA if you use I²C.
• SDO: This is MISO if you are using SPI. If you are using I²C, this pin selects the address
(0x76 if SDO is LOW, 0x77 if SDO is HIGH).
• SCK: This is SCK if you use SPI or SCL if you use I²C.
Atmospheric Pressure Sensors 289
1 /**
2 * @file bme180_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief BMP280/BME280 Sensor Example
5 * @version 1.0
6 * @date 2023-06-12
7 *
8 * A large part of the code was adapted from
9 * https://bitbucket.org/christandlg/bmx280mi/src/master/
10 *
11 * @copyright Copyright (c) 2023, Daniel Quadros
12 *
13 */
14
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <math.h>
18 #include "pico/stdlib.h"
19 #include "hardware/i2c.h"
20
21 // Sensor connections
22 #define I2C_ID i2c0
23 #define I2C_SCL_PIN 17
24 #define I2C_SDA_PIN 16
25
26 #define BAUD_RATE 1000000 // 1MHz
27
28
29 // Class to access BME280/BMP280 Sensor
30 class BMx280 {
31
32 public:
33
34 // oversampling
35 typedef enum : uint8_t
36 {
37 OVSP_SKIP = 0x00,
38 OVSP_1 = 0x01,
Atmospheric Pressure Sensors 291
39 OVSP_2 = 0x02,
40 OVSP_4 = 0x03,
41 OVSP_6 = 0x04,
42 OVSP_16 = 0x05
43 } OVS_PRESSURE;
44
45 typedef enum : uint8_t
46 {
47 OVST_SKIP = 0x00,
48 OVST_1 = 0x01,
49 OVST_2 = 0x02,
50 OVST_4 = 0x03,
51 OVST_8 = 0x04,
52 OVST_16 = 0x05
53 } OVS_TEMP;
54
55 typedef enum : uint8_t
56 {
57 OVSH_SKIP = 0x00,
58 OVSH_1 = 0x01,
59 OVSH_2 = 0x02,
60 OVSH_4 = 0x03,
61 OVSH_8 = 0x04,
62 OVSH_16 = 0x05
63 } OVS_HUMIDITY;
64
65 // filter
66 typedef enum : uint8_t
67 {
68 FILTER_OFF = 0x00,
69 FILTER_2 = 0x01,
70 FILTER_4 = 0x02,
71 FILTER_8 = 0x03,
72 FILTER_16 = 0x04,
73 } FILTER;
74
75 // modes
76 typedef enum : uint8_t
77 {
Atmospheric Pressure Sensors 292
78 MODE_SLEEP = 0x00,
79 MODE_FORCED = 0x01,
80 MODE_NORMAL = 0x03
81 } MODE;
82
83 // standby times
84 typedef enum : uint8_t
85 {
86 TSB_0_5 = 0x00,
87 TSB_62_5 = 0x01,
88 TSB_125 = 0x02,
89 TSB_250 = 0x03,
90 TSB_500 = 0x04,
91 TSB_1000 = 0x05,
92 TSB_10 = 0x06, // BME280
93 TSB_2000 = 0x06, // BMP280
94 TSB_20 = 0x07, // BME280
95 TSB_4000 = 0x07 // BMP280
96 } STANDBY;
97
98 static const uint8_t BMP280_ID = 0x58;
99 static const uint8_t BME280_ID = 0x60;
100
101 BMx280 (i2c_inst_t *i2c, uint8_t addr = 0x76,
102 OVS_TEMP ovs_t = OVST_16,
103 OVS_PRESSURE ovs_p = OVSP_16,
104 OVS_HUMIDITY ovs_h = OVSH_16,
105 FILTER filter = FILTER_OFF,
106 STANDBY stb = TSB_1000
107 );
108
109 void softReset();
110 uint8_t getDeviceId();
111 void setMode(MODE mode);
112 void doMeasure(bool bWait);
113 bool getRawData();
114 float getTemperature();
115 float getPressure();
116 float getHumidity();
Atmospheric Pressure Sensors 293
195 // Device ID
196 uint8_t id;
197
198 // Last read values
199 uint16_t uh;
200 uint32_t up;
201 uint32_t ut;
202
203 // Oversampling
204 uint8_t osrs_t;
205 uint8_t osrs_p;
206 uint8_t osrs_h;
207
208 // Standby time for normal mode
209 uint8_t stb_time;
210
211 // Filter
212 uint8_t filter;
213
214 // Private routines
215 int32_t calculateTempFine(void);
216 void readCalibCoeff(void);
217 uint16_t readRawTemperature(void);
218 uint32_t readRawPressure(void);
219 void write8(uint8_t reg, uint8_t val);
220 uint8_t read8(uint8_t reg);
221 uint16_t read16(uint8_t reg, bool LoHi = true);
222 uint32_t read20 (uint8_t reg);
223 };
224
225 // Constructor
226 BMx280::BMx280 (i2c_inst_t *i2c, uint8_t addr,
227 OVS_TEMP ovs_t, OVS_PRESSURE ovs_p,
228 OVS_HUMIDITY ovs_h , FILTER filter,
229 STANDBY stb
230 ) {
231 this->i2c = i2c;
232 this->addr = addr;
233 this->osrs_t = (uint8_t) ovs_t;
Atmospheric Pressure Sensors 296
390
391 return var1 + var2;
392 }
393
394 // Read calibration coefficients and save them
395 void BMx280::readCalibCoeff() {
396 T1 = read16 (CAL_T1);
397 T2 = (int16_t) read16 (CAL_T2);
398 T3 = (int16_t) read16 (CAL_T3);
399
400 P1 = read16 (CAL_P1);
401 P2 = (int16_t) read16 (CAL_P2);
402 P3 = (int16_t) read16 (CAL_P3);
403 P4 = (int16_t) read16 (CAL_P4);
404 P5 = (int16_t) read16 (CAL_P5);
405 P6 = (int16_t) read16 (CAL_P6);
406 P7 = (int16_t) read16 (CAL_P7);
407 P8 = (int16_t) read16 (CAL_P8);
408 P9 = (int16_t) read16 (CAL_P9);
409
410 if (id == BME280_ID) {
411 H1 = read8(CAL_H1);
412 H2 = (int16_t) read16(CAL_H2);
413 H3 = read8(CAL_H1);
414
415 uint8_t x = read8(CAL_H4_H5_LSB);
416 H4 = (read8(CAL_H4_MSB) << 4) | (x & 0x0F);
417 H5 = (read8(CAL_H5_MSB) << 4) | (x >> 4);
418 H6 - (int8_t) read8(CAL_H6);
419 }
420 }
421
422 // Write an 8-bit value into a register
423 void BMx280::write8 (uint8_t reg, uint8_t val) {
424 uint8_t buffer[2];
425
426 buffer[0] = reg;
427 buffer[1] = val;
428 i2c_write_blocking (i2c, addr, buffer, 2, false);
Atmospheric Pressure Sensors 301
429 }
430
431 // Read a 8-bit value from a register
432 uint8_t BMx280::read8 (uint8_t reg) {
433 uint8_t val[1];
434
435 // Select register
436 i2c_write_blocking (i2c, addr, ®, 1, true);
437
438 // Read value
439 i2c_read_blocking (i2c, addr, val, 1, false);
440 return val[0];
441 }
442
443 // Read a 16-bit value from a register
444 uint16_t BMx280::read16 (uint8_t reg, bool LoHi) {
445 uint8_t val[2];
446
447 // Select register
448 i2c_write_blocking (i2c, addr, ®, 1, true);
449
450 // Read value
451 i2c_read_blocking (i2c, addr, val, 2, false);
452
453 if (LoHi) {
454 return ((uint16_t) val[1] << 8) | val[0];
455 } else {
456 return ((uint16_t) val[0] << 8) | val[1];
457 }
458 }
459
460 // Read a 20-bit value from a register
461 uint32_t BMx280::read20 (uint8_t reg) {
462 uint8_t val[3];
463
464 // Select register
465 i2c_write_blocking (i2c, addr, ®, 1, true);
466
467 // Read value
Atmospheric Pressure Sensors 302
Arduino Code
For the Arduino environment, I choose the BMx280MI library from Gregor Christandl. It is very
complete, supporting both the BME280 and the BMP280 connected through I²C or SPI.
BMx280MI Library
Atmospheric Pressure Sensors 304
1 // BMP280/BME280 Example
2 #include <Arduino.h>
3 #include <Wire.h>
4 #include <BMx280I2C.h>
5
6 #define I2C_ADDRESS 0x76
7
8 BMx280I2C bmx280(I2C_ADDRESS);
9
10 // Initialization
11 void setup() {
12 // Init serial
13 Serial.begin (115200);
14 while (!Serial)
15 ;
16
17 // Init I2C
18 Wire.setSDA(16);
19 Wire.setSCL(17);
20 Wire.begin();
21
22 // Init sensor
23 if (!bmx280.begin()) {
24 Serial.println("Sensor not found.");
25 while (true)
26 ;
27 }
28
29 // Identify sensor
30 Serial.println (bmx280.isBME280()? "BME280 Sensor" : "BMP280 Sensor");
31
32 //reset sensor to default parameters.
33 bmx280.resetToDefaults();
34 bmx280.writeOversamplingPressure(BMx280MI::OSRS_P_x16);
35 bmx280.writeOversamplingTemperature(BMx280MI::OSRS_T_x16);
36 if (bmx280.isBME280()) {
37 bmx280.writeOversamplingHumidity(BMx280MI::OSRS_H_x16);
38 }
Atmospheric Pressure Sensors 305
39 }
40
41 // Main Loop
42 void loop() {
43 delay(2000);
44 if (bmx280.measure()) {
45 //wait for the measurement to finish
46 do {
47 delay(10);
48 } while (!bmx280.hasValue());
49
50 Serial.print("Temperature: ");
51 Serial.print(bmx280.getTemperature(),2);
52 Serial.print(" Pressure: ");
53 Serial.print(bmx280.getPressure(),0);
54 if (bmx280.isBME280()) {
55 Serial.print(" Humidity: ");
56 Serial.print(bmx280.getHumidity());
57 }
58 Serial.println();
59 }
60 }
MicroPython Code
The library I choose for MicroPython can be downloaded from
https://github.com/SebastianRoll/mpy_bme280_esp8266/blob/master/bme280.py
The bme280.py file must be saved in the lib directory in the Pico.
While the page mentions the ESP8266, nothing is specific to it in the code. It is a very minimal
library that supports only the BME280 through I²C, using forced mode and allowing only to set
the oversampling (using the same value for temperature, pressure, and humidity).
Atmospheric Pressure Sensors 306
1 # BME280 Example
2 from machine import I2C, Pin
3 from time import sleep
4 import bme280
5
6 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
7 bme = bme280.BME280(i2c=i2c)
8
9 while (True):
10 temp, press, humid = bme.read_compensated_data()
11 print('Temperature {:.1f}C Pressure {:.0f}Pa Humidity {:.1f}%'.format(
12 temp/100, press/256, humid/1024))
13 sleep(2)
CircuitPython Code
We are going to use one of the official libraries from Adafruit:
1. Download the “Bundle for Version 8.x” (or whatever CircuitPython version you are using)
from https://circuitpython.org/libraries.
2. Expand the zip file, the files we want will be under the lib subdirectory.
3. Connect to a PC your Pico with CircuitPython installed. A drive called CIRCUITPY will
appear on your computer.
4. Copy the directories adafruit_bme280 and adafruit_bus_device from the expanded
bundle to the lib directory in the CIRCUITPY drive.
BMP390
The BMP390 (including the BMP390L) is part of a newer generation of barometric pressure
sensors with better precision than the BMP280. It is also slightly faster and smaller but has
a slightly higher power consumption. Like the other BMP sensors, it measures pressure and
temperature.
It has the same three power modes as the BMP280 (sleep, forced, and normal). A measurement
cycle consists of up to three steps: measure temperature, measure pressure, and filtering. Each
step can be enabled or disabled in the PWR_CTRL register.
There are six levels of temperature and pressure oversampling:
Atmospheric Pressure Sensors 308
Pressure and temperature oversampling is set in the osr_p and osr_t bits in the OSR register.
The BMP390 has a 512 bytes FIFO (first-in, first-out) that can be used to store measurements
and measurement times. It allows the sensor to run autonomously (using the “normal” mode) for
some time before the microcontroller collects the measurements. Further detail can be found in
the datasheet.
The BMP390 has the same SCK, SDI, SDO, and CSB pins as the BMP280, and adds an interrupt pin
that can be used to notify a microcontroller when data is ready, the FIFO reached a configurable
watermark, or the FIFO is full. The interrupt pin is push-pull and active-hight (meaning that is
initially low and the sensor can drive it to LOW and HIGH levels) after a reset, but it can be
configured to open-drain and/or active-low.
BMP390 Registers
The CHIP_ID is 0x60, and a new VER_ID register gives information on the revision of the chip.
The ERR_REG register indicates some internal errors. The datasheet does not give much informa-
tion on these, my suggestion is to reset the sensor and retry the operation. If the error persists
then probably the sensor is faulty and should be replaced.
The STATUS register indicates if a command is in progress and if there is unread data (with
individual bits for pressure and temperature).
The data registers have 3 bytes for pressure and 3 bytes for temperature, with the lowest
significant byte first.
Sensor time also has 3 bytes (with the lowest significant byte first). This is a running counter
inside the sensor (in my tests it incremented every 2.3 milliseconds). It goes back to zero when it
Atmospheric Pressure Sensors 310
overflows (after a little less than 11 hours) or a reset is done. The sensor time can be stored in the
FIFO to help process the stored data.
The ‘EVENT’ register informs if a reset happened and if there was serial communication during
a conversion. Both pieces of information are cleared on read.
The INT_STATUS register informs the reason for an interrupt. INT_CTRL allows the configuration
of the interrupt pin.
The IF_CONFIG register allows to set up a “3-wire” SPI mode and enables a “watchdog” on the
I²C communication.
The PWR_CTRL register contains the enable bits for pressure and temperature measurements and
the mode selection bits.
We select the oversampling for pressure and temperature in the OSR registers.
The CONFIG register is used to configure the IIR filter.
The CMD register accepts three commands:
BMP390 Example
In the following diagram, I am taking advantage of the pull-up/pull-down resistor in the Adafruit
module to minimize the connections. The sensor will use the I²C interface with an address of 0x77.
Atmospheric Pressure Sensors 311
1 /**
2 * @file bmp390_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief BMP390 Sensor Example
5 * @version 1.0
6 * @date 2023-06-14
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <string.h>
15 #include <math.h>
16 #include "pico/stdlib.h"
17 #include "hardware/i2c.h"
18
19 #include "bmp3.h"
20
21 // Sensor connections
22 #define I2C_ID i2c0
23 #define I2C_SCL_PIN 17
24 #define I2C_SDA_PIN 16
25
26 #define BAUD_RATE 1000000 // 1MHz
27
28 #define BMP_ADDR 0x77
29
30 // Global variables
31 static struct bmp3_dev dev;
32 static uint8_t dev_addr;
33
34 // Local rotines
35 static void initSensor(void);
36 static BMP3_INTF_RET_TYPE bmp3_interface_init(struct bmp3_dev *bmp3);
37 static void bmp3_check_rslt(const char api_name[], int8_t rslt);
38
Atmospheric Pressure Sensors 313
39 // Main Program
40 int main() {
41 // Init stdio
42 stdio_init_all();
43 #ifdef LIB_PICO_STDIO_USB
44 while (!stdio_usb_connected()) {
45 sleep_ms(100);
46 }
47 #endif
48
49 // Init I2C interface
50 uint baud = i2c_init (I2C_ID, BAUD_RATE);
51 printf ("I2C @ %u Hz\n", baud);
52 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
53 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
54 gpio_pull_up(I2C_SCL_PIN);
55 gpio_pull_up(I2C_SDA_PIN);
56
57 // Init sensor
58 initSensor();
59 printf ("Ready\n");
60
61 // Main loop
62 struct bmp3_data data = { 0 };
63 struct bmp3_status status = { { 0 } };
64 while(1) {
65 int8_t rslt = bmp3_get_status(&status, &dev);
66 if ((rslt == BMP3_OK) && (status.intr.drdy == BMP3_ENABLE)) {
67 rslt = bmp3_get_sensor_data(BMP3_PRESS_TEMP, &data, &dev);
68 bmp3_check_rslt("bmp3_get_sensor_data", rslt);
69 bmp3_get_status(&status, &dev); // clears drdy
70 printf("Temperature: %.2f C Pressure: %.0f Pa\n",
71 data.temperature, data.pressure);
72 }
73 }
74 }
75
76 // Initialize the sensor
77 static void initSensor(void) {
Atmospheric Pressure Sensors 314
78 int8_t rslt;
79 uint16_t settings_sel;
80 struct bmp3_settings settings = { 0 };
81 struct bmp3_status status = { { 0 } };
82
83 rslt = bmp3_interface_init(&dev);
84 bmp3_check_rslt("bmp3_interface_init", rslt);
85
86 rslt = bmp3_init(&dev);
87 bmp3_check_rslt("bmp3_init", rslt);
88
89 settings.int_settings.drdy_en = BMP3_ENABLE;
90 settings.press_en = BMP3_ENABLE;
91 settings.temp_en = BMP3_ENABLE;
92
93 settings.odr_filter.press_os = BMP3_OVERSAMPLING_2X;
94 settings.odr_filter.temp_os = BMP3_OVERSAMPLING_2X;
95 settings.odr_filter.odr = BMP3_ODR_0_2_HZ;
96
97 settings_sel = BMP3_SEL_PRESS_EN | BMP3_SEL_TEMP_EN | BMP3_SEL_PRESS_OS |
98 BMP3_SEL_TEMP_OS | BMP3_SEL_ODR | BMP3_SEL_DRDY_EN;
99
100 rslt = bmp3_set_sensor_settings(settings_sel, &settings, &dev);
101 bmp3_check_rslt("bmp3_set_sensor_settings", rslt);
102
103 settings.op_mode = BMP3_MODE_NORMAL;
104 rslt = bmp3_set_op_mode(&settings, &dev);
105 bmp3_check_rslt("bmp3_set_op_mode", rslt);
106 }
107
108 // Check for errors
109 static void bmp3_check_rslt(const char api_name[], int8_t rslt) {
110 switch (rslt) {
111 case BMP3_OK:
112 // uncomment following line for debug
113 //printf ("API:%s OK (%d)\n", api_name, rslt);
114 break;
115 case BMP3_E_NULL_PTR:
116 printf ("API:%s Error:Null pointer (%d)\n", api_name, rslt);
Atmospheric Pressure Sensors 315
117 break;
118 case BMP3_E_COMM_FAIL:
119 printf ("API:%s Error:Communication failure (%d)\n", api_name, rslt);
120 break;
121 case BMP3_E_INVALID_LEN:
122 printf ("API:%s Error:Incorrect length (%d)\n", api_name, rslt);
123 break;
124 case BMP3_E_DEV_NOT_FOUND:
125 printf ("API:%s Error:Sensor not found (%d)\n", api_name, rslt);
126 break;
127 case BMP3_E_CONFIGURATION_ERR:
128 printf ("API:%s Error:Configuration erro (%d)\n", api_name, rslt);
129 break;
130 case BMP3_W_SENSOR_NOT_ENABLED:
131 printf ("API:%s Warning:Sensor not enabled (%d)\n", api_name, rslt);
132 break;
133 case BMP3_W_INVALID_FIFO_REQ_FRAME_CNT:
134 printf ("API:%s Warning:Invalid watermark level (%d)\n", api_name, rslt);
135 break;
136 default:
137 printf ("API:%s Error:Unknown (%d)\n", api_name, rslt);
138 break;
139 }
140 }
141
142
143 // Interface rotines for the Bosh API
144
145 /*!
146 * Delay function
147 */
148 static void bmp3_user_delay_us(uint32_t period, void *intf_ptr) {
149 sleep_us(period);
150 }
151
152 /*!
153 * I2C read function
154 */
155 static BMP3_INTF_RET_TYPE bmp3_user_i2c_read(uint8_t reg_addr, uint8_t *reg_data, ui\
Atmospheric Pressure Sensors 316
195 }
196
197 return rslt;
198 }
Arduino Code
We are going to use the Adafruit BMP3XX Library, if asked select to install all dependencies.
16
17 // Init I2C
18 Wire.setSDA(16);
19 Wire.setSCL(17);
20 Wire.begin();
21
22 // Init sensor
23 if (!bmp.begin_I2C()) {
24 Serial.println("Sensor not found.");
25 while (true)
26 ;
27 }
28
29 // Set up oversampling and filter initialization
30 bmp.setTemperatureOversampling(BMP3_OVERSAMPLING_8X);
31 bmp.setPressureOversampling(BMP3_OVERSAMPLING_4X);
32 bmp.setIIRFilterCoeff(BMP3_IIR_FILTER_COEFF_3);
33 bmp.setOutputDataRate(BMP3_ODR_50_HZ);
34 }
35
36 // Main Loop
37 void loop() {
38 delay(2000);
39 if (bmp.performReading()) {
40 Serial.print("Temperature: ");
41 Serial.print(bmp.temperature,2);
42 Serial.print("C Pressure: ");
43 Serial.print(bmp.pressure,0);
44 Serial.print("Pa Altitude: ");
45 Serial.print(bmp.readAltitude(SEALEVELPRESSURE_HPA));
46 Serial.println("m");
47 }
48 }
MicroPython Code
For MicroPython we are going to adapt an example from WaveShare. The code can be down-
loaded from
https://www.waveshare.com/w/upload/b/bf/BMP390_Barometric_Pressure_Sensor_code.zip
Atmospheric Pressure Sensors 319
The file we need is the BM3XX.py that is in the Pico subdirectory. Use Thonny to copy it to the
lib directory in the Pico.
The BMP3XX_I2C class in the library does not support the use of non-default pins for I²C, so I
wrote my own BMP390_I2C class.
MicroPython BMP390 Example
1 # BMP390 Example
2 from BMP3XX import BMP3XX, ULTRA_PRECISION
3 from machine import I2C, Pin
4 from time import sleep
5
6 # Class for generic I2C
7 class BMP390_I2C(BMP3XX):
8 def __init__(self, i2c, i2c_addr=0x77):
9 self._addr = i2c_addr
10 self.i2c = i2c
11 super(BMP390_I2C, self).__init__()
12
13 def _write_reg(self, reg, data):
14 if isinstance(data, int):
15 data = bytearray([data])
16 self.i2c.writeto_mem(self._addr, reg, data)
17
18 def _read_reg(self, reg, length):
19 return self.i2c.readfrom_mem(self._addr, reg, length)
20
21 # Main Program
22 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
23 bmp = BMP390_I2C(i2c)
24 print ('Chip ID:')
25 if bmp.begin():
26 print ()
27 bmp.set_common_sampling_mode(ULTRA_PRECISION)
28 while (True):
29 print('Temperature {:.1f}C Pressure {:.0f}Pa Altitude {:.2f}m'.format(
30 bmp.get_temperature, bmp.get_pressure, bmp.get_altitude))
31 sleep(2)
32 else:
33 print ('Sensor not found!')
Atmospheric Pressure Sensors 320
CircuitPython Code
We are going to use one of the official library from Adafruit:
1. Download the “Bundle for Version 8.x” (or whatever CircuitPython version you are using)
from https://circuitpython.org/libraries.
2. Expand the zip file, the file we want will be under the lib subdirectory.
3. Connect your Pico with CircuitPython installed. A drive called CIRCUITPY will appear on
your computer.
4. Copy the file adafruit_bmp3xx.mpyfrom the expanded bundle to the lib directory in the
CIRCUITPY drive.
• We can measure and discount later offsets, residual values in the measurements that do
not correspond to actual accelerations.
• We can get a better value for the scale by submitting the accelerometer to a known
acceleration (we can use Earth’s gravity for this). The scale may not be the same for all
axes.
A Gyroscopic Sensor measures the angular velocity, that is, how fast it is moving around each
axis.
All the sensors described here are only available as SMD parts and support I²C communication;
some of them also support SPI. Unless you are designing your board, you will probably use a
module that may not support SPI even if it’s supported by the sensor. Again, while some sensors
allow the selection of the I²C address, the module may have already made that selection for you.
The HMC5883L and the HMC5983 were made by Honeywell and were discontinued. The design
was licensed by QST, and it created the QMC5883L with the same functionality as the HMC5883L,
but with significant changes in the interface:
• The HMC5883L supports I²C and SPI connections, the QMC5883L only supports I²C.
• I²C addresses are different: 0x0D for QMC5883L and 0x1E for HMC5883L.
Electronic Compass, Accelerometers, and Gyroscopes 324
• The registers address and meanings are different between these two chips.
• The QMC5883L supports only Continuous Measurement Mode.
You will find some cheap “HMC5883L” I²C modules that use a QMC5883L sensor (like the one in
the previous photo). A quick way to check is to connect to the Pi Pico and use MicroPython or
CircuitPython to do an I²C scan and then check the address of the sensor.
The HMC5983 and the QMC5883L add a temperature sensor to the HMC5883L. The temperature
reading can be used to automatically compensate for the measured magnetic data.
The HMC5883L and HMC5983 sensors have 8 ranges of operation, from ±0.88 Gauss to ±8.1 Gauss,
and a 12-bit ADC. The QMC5883L has only two ranges (±2 Gauss and ±8 Gauss) and a 16-bit
ADC.
All three devices operate at standard (100KHz) and fast (400KHz) speeds for I²C, the HMC5983
also supports high-speed (3.4MHz)
The maximum output rate is 160 Hz for HMC5883L, 200 Hz for QMC5883L, and 220 Hz for
HMC5983.
The following tables show the register maps for the three sensors. You will notice that the
HMC5983 just adds temperature output registers to the HMC5883L.
HMC5883L Registers
Electronic Compass, Accelerometers, and Gyroscopes 325
HMC5983 Registers
QMC5883L Registers
The temperature sensor must be enabled (TS = 1) for the temperature to be available at the
corresponding output registers and for compensating the magnetic measurements. For the
HMC5883L this bit should always be 0.
MA selects between 1 (00), 2 (01), 4 (10), or 8 (11) samples per reading.
The Output Data Rate applies only in Continuous Mode (described in the Mode Register).
The Measurement Mode (MS) can be used for testing and for disabling the magnetic sensor in the
HMC5983:
MS Measurement Mode
00 Normal mode (default)
01 Positive bias for all axes, used for testing the sensor
10 Negative bias for all axes, used for testing the sensor
11 Disables magnetic sensor in the HMC5983
The positive/negative bias excites the magnetic sensor with a nominal magnetic field for testing
purposes.
The Control Register B is located at address 0x01. It controls the gain of the device, which defines
the resolution. You should use a gain as high as no overflow occurs.
Electronic Compass, Accelerometers, and Gyroscopes 327
• Bit 7 (HS) selects I²C high-speed mode when 1 (so you need first to access the sensor in
standard or fast mode to change this bit).
• Bit 5 (LP) selects Low Power Mode in the HMC5983 when 1. Low Power Mode forces OD
to 0.75 Hz, and MA to 1 sample.
• Bit 2 (SIM) selects the SPI mode for the HMC5983: 0 is the normal 4-wire SPI and 1 is the
3-wire SPI interface.
• Bits 1 and 0 (MD) select the operating mode:
MD Operating Mode
00 Continuous Measurement Mode: the sensor will continuously perform
measurements at the rate selected by ODR (the first measurement will
take double of the time). The RDY bit in the Status will signal when data is
available in the three output registers
01 Single Measurement Mode: when placed in this mode, the sensor will do a
single measurement, set the RDY bit in the status to HIGH and go into idle
mode.
10 Idle Mode: no measurement is done.
11 Idle Mode: no measurement is done.
The Data Output Registers, with addresses from 0x03 to 0x08, receive the measurement result.
There are a pair (MSB and LSB) for each axis that should be treated as a 16-bit value in 2’s
complement format. The order of the registers is X, Z, and Y.
To convert these numbers into Gauss, use the resolution corresponding to the gain set in Control
Register B. A value of -4096 indicates that an overflow or underflow occurred in the ADC (you
need to change the gain to get a proper reading).
When you read one of the Data Output Registers, new data cannot be put in any of them until
all six registers are read.
The Status Register at address 0x09 has the following information:
• Bit 4 (DOW ): is set if new data is written before the old is read. It is cleared when data is
Electronic Compass, Accelerometers, and Gyroscopes 328
The Identification Registers A, B, C at addresses 0x0A, 0x0B, and 0x0C contain 0x48, 0x34, and
0x33 (H43 in ASCII) in both devices.
The Temperature Output Registers at addresses 0x31 and 0x32 should be treated as a 16-bit 2’s
complement value (v) that can be converted to °C by the following formula:
v
temperature = 128
+ 25
• Bit 2 (DOR): is set if new data is written before the old is read. It is cleared when data is
read.
• Bit 1 (OVL): is set to 1 if data in any axis is out of range. It changes back to 0 when
measurements go back in range.
• Bit 0 (DRDY ): is set to 1 when all three axes data is ready. It changes back to 0 when any
data register is read.
Electronic Compass, Accelerometers, and Gyroscopes 329
The Temperature Output Registers at addresses 0x07 and 0x08 should be treated as a 16-bit 2’s
complement value. The temperature sensor is not calibrated for absolute temperature readings,
but the can be used to measure variations in temperature with a unit of about 0.01°C.
Control Register 1 is located in address 0x09, it sets the operational modes (MODE), output data
update rate (ODR), magnetic field measurement range or sensitivity of the sensors (RNG), and over
sampling rate (OSR).
Control register 2 is located in address 0x0A. It controls interrupt pin enabling (INT_ENB), pointer
roll-over function enabling (POL_PNT), and soft reset (SOFT_RST).
If pointer roll-over is enabled (1), the data pointer will increment automatically after a read of
any register in the range 0x00 to 0x06, going back to 0x00 after 0x06. This allows to read the
output registers without sending the register address at each reading.
Writing to control register 2 with the SOFT_RST set (1) will trigger a soft reset, restoring the default
value of all registers.
The Set/Reset Period Register at 0x0B should be written with 0x01.
The example codes assume that the sensor is parallel to the ground (they ignore the z-axis).
I could not find a board with the HMC5883L sensor, the ones I bought had QMC5883L
in them.
I am not particularly happy with the performance of these sensors. You will notice that the values
do not change regularly over a 360 degrees turn. If you plan to use one of them in a practical
application, do a calibration and compensate for the non-linearity of the sensors.
1 /**
2 * @file xMC5x83_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief HMC5883L/HMC5983/QMC5883L Sensor Example
5 * @version 1.0
6 * @date 2023-07-02
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
16 #include "hardware/i2c.h"
17
18 // Sensor connections
19 #define I2C_ID i2c0
20 #define I2C_SCL_PIN 17
21 #define I2C_SDA_PIN 16
22
23 #define BAUD_RATE 400000 // 400kHz
24
25 const float PI = 3.14159f;
26
27 // Class to access sensor
28 class COMPASS {
29
30 public:
31
32 // Sensor model
33 typedef enum { UNDEFINED, HMC_58, HMC_59, QMC } MODEL;
34
35 // Constructor
36 COMPASS (i2c_inst_t *i2c, MODEL model = UNDEFINED);
37
38 // Public methods
Electronic Compass, Accelerometers, and Gyroscopes 332
39 bool begin(void);
40 float getHeading(void);
41 MODEL getModel(void);
42
43 private:
44
45 // I2C Addresses
46 static const uint8_t ender_HMC = 0x1E;
47 static const uint8_t ender_QMC = 0x0D;
48
49 // Registers Address
50 static const int regCFGA_HMC = 0;
51 static const int regCFGB_HMC = 1;
52 static const int regMODE_HMC = 2;
53 static const int regXH_HMC = 3;
54 static const int regST_HMC = 9;
55
56 static const int regCR1_QMC = 9;
57 static const int regSR_QMC = 11;
58 static const int regXL_QMC = 0;
59 static const int regST_QMC = 6;
60
61 // I2C instance
62 i2c_inst_t *i2c;
63
64 // Sensor I2C Address
65 uint8_t addr;
66
67 // Sensor model
68 MODEL model;
69
70 // Private rotines
71 bool checkAddr(uint8_t addr);
72 void write8(uint8_t reg, uint8_t val);
73 uint8_t read8(uint8_t reg);
74 };
75
76 // Constructor
77 COMPASS::COMPASS (i2c_inst_t *i2c, MODEL model) {
Electronic Compass, Accelerometers, and Gyroscopes 333
78
79 this->i2c = i2c;
80 this->model = model;
81 }
82
83 // Set up sensor
84 bool COMPASS::begin() {
85
86 // Set up model and i2c address
87 if (model == UNDEFINED) {
88 // Try to identify chip
89 if (checkAddr(ender_QMC)) {
90 this->addr = ender_QMC;
91 this->model = QMC;
92 } else if (checkAddr(ender_HMC)) {
93 this->addr = ender_HMC;
94 // Test if we can set temperature compensation
95 write8(regCFGA_HMC, 0x90);
96 this->model = (read8(regCFGA_HMC) == 0x90)? HMC_59 : HMC_58;
97 } else {
98 return false; // sensor not faound
99 }
100 } else {
101 this->model = model;
102 this->addr = (model == QMC) ? ender_QMC: ender_HMC;
103 }
104
105 // init registers for continous mode
106 if (model == QMC) {
107 write8(regSR_QMC, 0x01); // as per datasheet
108 write8(regCR1_QMC, 0x81); // 10Hz, 2G, 512 osr
109 } else if (model == HMC_58) {
110 // default values as I could not find a HMC5883L to test
111 write8(regCFGA_HMC, 0x10);
112 write8(regCFGB_HMC, 0x01);
113 write8(regMODE_HMC, 0x00);
114 } else {
115 // crank up gain and samples
116 write8(regCFGA_HMC, 0xF0);
Electronic Compass, Accelerometers, and Gyroscopes 334
Arduino Code
We are going to use a library from DFRobot. It supports the QMC5883L and HMC5883L (but you
need to inform the I²C address of the sensor).
DFRobot_QMC5883 Library
The library has methods for setting the configuration on the sensor and the magnetic declination,
I just used the defaults.
Electronic Compass, Accelerometers, and Gyroscopes 338
MicroPython Code
The example code supports only the QMC5883L.
MicroPython QMC5883L Example
1 # QMC5883L Example
2 from machine import I2C, Pin
3 from time import sleep
4 from math import pi, atan2
5
6 # Class to acess sensor
7 class QMC5883L:
8
9 def __init__(self, i2c):
10 self.i2c = i2c
11 self.QMC_ADDR = 0x0D
12 self.QMC_REG_XL = 0
13 self.QMC_REG_ST = 6
14 self.QMC_REG_CR1 = 9
15 self.QMC_REG_SR = 11
16 i2c.writeto_mem(self.QMC_ADDR, self.QMC_REG_SR, b'\x01')
17 i2c.writeto_mem(self.QMC_ADDR, self.QMC_REG_CR1, b'\x81')
18
19 def getHeading(self):
20 st = i2c.readfrom_mem(self.QMC_ADDR,self.QMC_REG_ST,1)
21 while (st[0] & 1) == 0:
22 sleep(0.01)
23 st = i2c.readfrom_mem(self.QMC_ADDR,self.QMC_REG_ST,1)
24 data = i2c.readfrom_mem(self.QMC_ADDR,self.QMC_REG_XL,6)
25 x = data[0] + (data[1] << 8);
26 y = data[2] + (data[3] << 8);
27 if (x & 0x8000) != 0:
28 x = x - 0x10000
29 if (y & 0x8000) != 0:
30 y = y - 0x10000
31 angle = atan2(float(y),float(x))
32 return (angle*180)/pi + 180
33
34 # Test Program
35 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
Electronic Compass, Accelerometers, and Gyroscopes 340
36 compass = QMC5883L(i2c)
37 while True:
38 print ('Heading: {:.1f}'.format(compass.getHeading()))
39 sleep(2)
CircuitPython Code
The example code supports only the QMC5883L.
CircuitPython QMC5883L Example
1 # QMC5883L Example
2 import board
3 from digitalio import DigitalInOut, Pull
4 from busio import I2C
5 from time import sleep
6 from math import pi, atan2
7
8 # Class to acess sensor
9 class QMC5883L:
10
11 def __init__(self, i2c):
12 self.i2c = i2c
13 self.QMC_ADDR = 0x0D
14 self.QMC_REG_XL = 0
15 self.QMC_REG_ST = 6
16 self.QMC_REG_CR1 = 9
17 self.QMC_REG_SR = 11
18 i2c.try_lock()
19 i2c.writeto(self.QMC_ADDR, bytearray([self.QMC_REG_SR, 0x01]))
20 i2c.writeto(self.QMC_ADDR, bytearray([self.QMC_REG_CR1, 0x81]))
21 i2c.unlock()
22
23 def getHeading(self):
24 selreg = bytearray([self.QMC_REG_ST])
25 st = bytearray([0])
26 i2c.try_lock()
27 i2c.writeto_then_readfrom(self.QMC_ADDR, selreg, st)
28 i2c.unlock()
29 while (st[0] & 1) == 0:
Electronic Compass, Accelerometers, and Gyroscopes 341
30 sleep(0.01)
31 i2c.try_lock()
32 i2c.writeto_then_readfrom(self.QMC_ADDR, selreg, st)
33 i2c.unlock()
34 selreg = bytearray([self.QMC_REG_XL])
35 data = bytearray([0,0,0,0,0,0])
36 i2c.try_lock()
37 i2c.writeto_then_readfrom(self.QMC_ADDR, selreg, data)
38 i2c.unlock()
39 x = data[0] + (data[1] << 8);
40 y = data[2] + (data[3] << 8);
41 if (x & 0x8000) != 0:
42 x = x - 0x10000
43 if (y & 0x8000) != 0:
44 y = y - 0x10000
45 angle = atan2(float(y),float(x))
46 return (angle*180)/pi + 180
47
48 # Test Program
49 i2c = I2C(sda=board.GP16, scl=board.GP17)
50 compass = QMC5883L(i2c)
51 while True:
52 print ('Heading: {:.1f}'.format(compass.getHeading()))
53 sleep(2)
ADXL345 Module
In SPI mode, we use the SDI, SDO, SCLK, and CS signals as MOSI, MISO, SCK, and CS. You can
also configure for three-wire operation, with SDI as a bidirectional data pin. CS is active low.
To operate in I²C mode, CS must be kept HIGH. SDI is SDA and SCLK is SCL. SDO selects the I²C
address: 0x1D if HIGH, 0x53 if LOW. It supports standard (100KHz) and fast (400KHz) speeds.
The resolution can be selected between a fixed 10-bit resolution or a variable resolution that
maintains 0.004g for the lowest significant bit over all ranges (resulting in 13-bit for the ±16g
range).
It can detect single and double tap and free-fall.
The ADXL345 has a large number of registers:
Electronic Compass, Accelerometers, and Gyroscopes 343
ADXL345 Registers
• POWER_CTL (0x2D): activates and controls the sleep mode (for power economy)
• OFSx (0x1D to 0x20): these offset values are automatically subtracted from the measure-
ments.
• DATA_xx (0x32 to 0x37): these are the measurement results (after subtracting the offsets),
stored as 16-bit values in 2’s complement notation, LSB first. A 32-position FIFO can be
enabled to store the results before they are read through these registers.
• DATA_FORMAT (0x31): controls the format of the data in the DATA_XX registers. You can
select between two modes (fixed and full) and an operation range (±2g, ±4g, ±8g, or ±16g).
In the fixed mode the output has always 10 significant bits and the scale varies with the
range. In the full mode, the scale is fixed at 3.9mg e the number of significant bits changes
Electronic Compass, Accelerometers, and Gyroscopes 344
with the range. For example, let’s suppose that the acceleration is 5g and we have selected
the ±8g range. In fixed mode, the reading will be 0x140 (with 10 significant bits and a scale
of 15.6mg). In full mode, the reading will be 0x502 (with 11 significant bits and a scale of
3.9mg). This register also has a bit for self-test and a bit for selecting between 3 or 4 wire
SPI.
• TRESH_TAP (0x1D) and DUR (0x21): controls the short tap detection. TRESH_TAP sets the
minimum acceleration and DUR the max duration. Tap detection can be selected per axis.
Tap detection can trigger an interrupt.
• TRESH_FF (0x28) e TIME_FF (0x29): controls the free-fall detection. Free-fall can trigger
an interrupt.
• INT_SOURCE (0x30): informs the cause of an interrupt.
• DEVID (0x00): returns the device identification 0xE5 (345 in octal)
ADXL345 Example
We will use the I²C interface. The following diagram shows the connection of power and the I²C
bus, depending on your sensor board you may need to add a pull-up for the CS pin.
1 /**
2 * @file adlx245_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief ADLX345 Sensor Example
5 * @version 1.0
6 * @date 2023-07-06
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
16 #include "hardware/i2c.h"
17
18 // Sensor connections
19 #define I2C_ID i2c0
20 #define I2C_SCL_PIN 17
21 #define I2C_SDA_PIN 16
22
23 #define BAUD_RATE 400000 // 400kHz
24
25 // Class to access sensor
26 class ADLX345 {
27
28 public:
29
30 typedef struct {
31 float x;
32 float y;
33 float z;
34 } VECT_3D;
35
36 // Default I2C Address
37 static const uint8_t I2C_ADDR = 0x1D;
38
Electronic Compass, Accelerometers, and Gyroscopes 346
39 // Chip ID
40 static const uint8_t ID = 0xE5;
41
42 // Constructor
43 ADLX345 (i2c_inst_t *i2c, uint8_t addr = ADLX345::I2C_ADDR);
44
45 // Public methods
46 void begin(void);
47 uint8_t getId(void);
48 void getAccel(VECT_3D *vect, float scale = 256);
49
50 private:
51
52 // Registers Address
53 static const int DEVID = 0x00;
54 static const int POWER_CTL = 0x2D;
55 static const int DATA_FORMAT = 0x31;
56 static const int DATAX0 = 0x32;
57
58 // I2C instance
59 i2c_inst_t *i2c;
60
61 // Sensor I2C Address
62 uint8_t addr;
63
64 // Sensor ID
65 uint8_t id;
66
67 // Raw data
68 int16_t raw[3];
69
70 // Private rotines
71 void readRaw(void);
72 uint8_t read8(uint8_t reg);
73 void write8(uint8_t reg, uint8_t val);
74 };
75
76 // Constructor
77 ADLX345::ADLX345 (i2c_inst_t *i2c, uint8_t addr) {
Electronic Compass, Accelerometers, and Gyroscopes 347
78 this->i2c = i2c;
79 this->addr = addr;
80 }
81
82 // Set up sensor
83 void ADLX345::begin() {
84 id = read8(DEVID);
85 write8(POWER_CTL, 0x08); // measurement mode
86 write8(DATA_FORMAT, 0x00); // 2g range, 10-bit
87 }
88
89 // Returns the sensor id
90 uint8_t ADLX345::getId() {
91 return id;
92 }
93
94 // Get acceleration in g
95 void ADLX345::getAccel(VECT_3D *vect, float scale) {
96 readRaw();
97 vect->x = (float) raw[0] / scale;
98 vect->y = (float) raw[1] / scale;
99 vect->z = (float) raw[2] / scale;
100 }
101
102 // Read raw values
103 void ADLX345::readRaw() {
104 uint8_t data[6];
105
106 // Select first register
107 uint8_t reg = DATAX0;
108 i2c_write_blocking (i2c, addr, ®, 1, true);
109
110 // Read values
111 i2c_read_blocking (i2c, addr, data, 6, false);
112
113 // Convert to int16
114 for (int i = 0; i < 3; i++) {
115 raw[i] = (data[2*i+1] << 8) | data[2*i];
116 }
Electronic Compass, Accelerometers, and Gyroscopes 348
117 }
118
119 // Read a 8-bit value from a register
120 uint8_t ADLX345::read8 (uint8_t reg) {
121 uint8_t val[1];
122
123 // Select register
124 i2c_write_blocking (i2c, addr, ®, 1, true);
125
126 // Read value
127 i2c_read_blocking (i2c, addr, val, 1, false);
128 return val[0];
129 }
130
131 // Writea 8-bit value to a register
132 void ADLX345::write8 (uint8_t reg, uint8_t val) {
133 uint8_t aux[2];
134
135 aux[0] = reg;
136 aux[1] = val;
137 i2c_write_blocking (i2c, addr, aux, 2, false);
138 }
139
140
141
142 // Main Program
143 int main() {
144 // Init stdio
145 stdio_init_all();
146 #ifdef LIB_PICO_STDIO_USB
147 while (!stdio_usb_connected()) {
148 sleep_ms(100);
149 }
150 #endif
151
152 // Init I2C interface
153 uint baud = i2c_init (I2C_ID, BAUD_RATE);
154 printf ("I2C @ %u Hz\n", baud);
155 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
Electronic Compass, Accelerometers, and Gyroscopes 349
Arduino Code
There are many libraries available for the ADXL345. We are going to use the one from Adafruit.
Electronic Compass, Accelerometers, and Gyroscopes 350
The library gives the accelerations in m/s², we divide by 9.8 to get the results in g.
Electronic Compass, Accelerometers, and Gyroscopes 351
MicroPython Code
The code in this example is an adaptation of the code in the C/C++ SDK example.
MicroPython ADXL345 Example
1 # ADXL345 Example
2
3 from machine import I2C, Pin
4 from time import sleep
5
6 # Class to acess sensor
7 class ADXL345:
8
9 _DEFAULT_ADDRESS = 0x1D
10 _ALT_ADDRESS = 0x53
11 _DEVICE_ID = 0xE5
12
13 _DEVID = 0x00
14 _POWER_CTL = 0x2D
15 _DATA_FORMAT = 0x31
16 _DATAX0 = 0x32
17
18 # Constructor
19 def __init__(self, i2c, addr):
20 self.i2c = i2c
21 self.addr = addr
22
23 # Set up sensor
24 def begin(self):
25 self.id = self.read_reg(ADXL345._DEVID)
26 if self.id != ADXL345._DEVICE_ID:
27 print ('WARNING: Wrong ID! {:02X}'.format(self.id))
28 i2c.writeto_mem(self.addr, ADXL345._POWER_CTL, b'\x08')
29 i2c.writeto_mem(self.addr, ADXL345._DATA_FORMAT, b'\x00')
30
31 # Sensor ID
32 def get_id(self):
33 return self.id
34
35 # Read Raw Values
Electronic Compass, Accelerometers, and Gyroscopes 353
CircuitPython Code
We are going to use one of the official libraries from Adafruit:
1. Download the “Bundle for Version 8.x” (or whatever CircuitPython version you are using)
from https://circuitpython.org/libraries.
2. Expand the zip file, the files we want will be under the lib subdirectory.
Electronic Compass, Accelerometers, and Gyroscopes 354
3. Connect to a PC with your Pico with CircuitPython installed. A drive called CIRCUITPY
will appear on your computer.
4. Copy the file adafruit_adxl34x_mpy from the expanded bundle to the lib directory in
the CIRCUITPY drive.
The documentation for the class we are going to use is at
https://docs.circuitpython.org/projects/adxl34x/en/latest/api.html
The code shows tap, movement, and free-fall events detected by the sensor.
CircuitPython ADXL345 Example
1 # ADXL345 Example
2 import board
3 from digitalio import DigitalInOut, Pull
4 from busio import I2C
5 from time import sleep
6 import adafruit_adxl34x
7
8 # Set up sensor
9 i2c = I2C(sda=board.GP16, scl=board.GP17)
10 sensor = adafruit_adxl34x.ADXL345(i2c)
11 sensor.enable_motion_detection()
12 sensor.enable_tap_detection(threshold = 100)
13 sensor.enable_freefall_detection()
14
15 # Main loop: show events
16 while True:
17 sleep(0.1)
18 if sensor.events['tap']:
19 print('TAP', sensor.acceleration)
20 if sensor.events['motion']:
21 print('MOTION', sensor.acceleration)
22 if sensor.events['freefall']:
23 print('FREE FALL', sensor.acceleration)
MMA8452 Module
The MMA8452 has an embedded DSP (digital signal processor) that can detect free-fall, single
and double tap, and shake.
The MMA8452 has a large number of registers:
MMA8452 Registers
• STATUS (0x00): indicates data overwrite (new data available before old data read) and new
data ready. There are indicators for each axis and all axes indicators.
Electronic Compass, Accelerometers, and Gyroscopes 356
• OUT_ (0x01 to 0x06): this registers contains the 12-bit output sample data expressed as 2’s
complement numbers. For each axis, there are two 8-bit registers. The first has bits 11 to
4 of the result and the second bits 3 to 0 (shifted left 4 bits). The LSB registers can only be
read immediately following the read access of the corresponding MSB register.
• WHO_AM_I (0x0C): contains the device identification code (0x2A).
• XYZ_DATA_CFG (0x0E): sets the dynamic range and the high-pass filter.
• PL_STATUS (0x10): informs the landscape/portrait orientation. This comes from the
gravity vector and is not available if the sensor is subject to other accelerations or the
sensor is parallel to the ground.
• CTRL_REG1 (0x2A): Select the sample rate and the mode (standby or active).
MMA8452 Example
The following diagram shows the connection of power and the I²C bus, depending on your sensor
board you may need to add a pull-down for the SA0 pin.
1 /**
2 * @file mma8452_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief MMA8452 Sensor Example
5 * @version 1.0
6 * @date 2023-07-06
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
16 #include "hardware/i2c.h"
17
18 // Sensor connections
19 #define I2C_ID i2c0
20 #define I2C_SCL_PIN 17
21 #define I2C_SDA_PIN 16
22
23 #define BAUD_RATE 400000 // 400kHz
24
25 // Class to access sensor
26 class MMA8452 {
27
28 public:
29
30 typedef struct {
31 float x;
32 float y;
33 float z;
34 } VECT_3D;
35
36 // Default I2C Address
37 static const uint8_t I2C_ADDR = 0x1C;
38
Electronic Compass, Accelerometers, and Gyroscopes 358
39 // Chip ID
40 static const uint8_t ID = 42;
41
42 // Constructor
43 MMA8452 (i2c_inst_t *i2c, uint8_t addr = MMA8452::I2C_ADDR);
44
45 // Public methods
46 void begin(void);
47 uint8_t getId(void);
48 void getAccel(VECT_3D *vect, float scale = 1024.0);
49 void active(void);
50 void standby(void);
51
52 private:
53
54 // Registers Address
55 static const uint8_t XYZ_DATA_CFG = 0x0E;
56 static const uint8_t WHO_AM_I = 0x0D;
57 static const uint8_t CTRL_REG1 = 0x2A;
58 static const uint8_t PULSE_CFG = 0x21;
59 static const uint8_t OUT_X_MSB = 0x01;
60
61 // I2C instance
62 i2c_inst_t *i2c;
63
64 // Sensor I2C Address
65 uint8_t addr;
66
67 // Sensor ID
68 uint8_t id;
69
70 // Raw data
71 int16_t raw[14];
72
73 // Private rotines
74 void readRaw(void);
75 uint8_t read8(uint8_t reg);
76 void write8(uint8_t reg, uint8_t val);
77 };
Electronic Compass, Accelerometers, and Gyroscopes 359
78
79 // Constructor
80 MMA8452::MMA8452 (i2c_inst_t *i2c, uint8_t addr) {
81 this->i2c = i2c;
82 this->addr = addr;
83 }
84
85 // Set up sensor
86 void MMA8452::begin() {
87 this->id = read8(WHO_AM_I);
88 standby();
89 uint8_t val = read8(XYZ_DATA_CFG);
90 write8(XYZ_DATA_CFG, val& 0xFC); // scale = 2g
91 val = read8(CTRL_REG1);
92 write8(CTRL_REG1, val & 0xC7); // odr = 500Hz
93 write8(PULSE_CFG, 0x00); // disable tap detection
94 active();
95 }
96
97 // Returns the sensor id
98 uint8_t MMA8452::getId() {
99 return id;
100 }
101
102 // Go into active mode
103 void MMA8452::active() {
104 uint8_t val = read8(CTRL_REG1);
105 write8(CTRL_REG1, val | 0x01);
106 }
107
108 // Go into standby mode
109 void MMA8452::standby() {
110 uint8_t val = read8(CTRL_REG1);
111 write8(CTRL_REG1, val & 0xFE);
112 }
113
114
115 // Get acceleration in g
116 void MMA8452::getAccel(VECT_3D *vect, float scale) {
Electronic Compass, Accelerometers, and Gyroscopes 360
117 readRaw();
118 vect->x = (float) raw[0] / scale;
119 vect->y = (float) raw[1] / scale;
120 vect->z = (float) raw[2] / scale;
121 }
122
123 // Read raw values
124 void MMA8452::readRaw() {
125 uint8_t data[6];
126
127 // Select first register
128 uint8_t reg = OUT_X_MSB;
129 i2c_write_blocking (i2c, addr, ®, 1, true);
130
131 // Read values
132 i2c_read_blocking (i2c, addr, data, 6, false);
133
134 // Convert to int16
135 for (int i = 0; i < 3; i++) {
136 raw[i] = (data[2*i] << 8) | data[2*i+1];
137 raw[i] /= 16;
138 }
139 }
140
141 // Read a 8-bit value from a register
142 uint8_t MMA8452::read8 (uint8_t reg) {
143 uint8_t val[1];
144
145 // Select register
146 i2c_write_blocking (i2c, addr, ®, 1, true);
147
148 // Read value
149 i2c_read_blocking (i2c, addr, val, 1, false);
150 return val[0];
151 }
152
153 // Write a 8-bit value to a register
154 void MMA8452::write8 (uint8_t reg, uint8_t val) {
155 uint8_t aux[2];
Electronic Compass, Accelerometers, and Gyroscopes 361
156
157 aux[0] = reg;
158 aux[1] = val;
159 i2c_write_blocking (i2c, addr, aux, 2, false);
160 }
161
162
163
164 // Main Program
165 int main() {
166 // Init stdio
167 stdio_init_all();
168 #ifdef LIB_PICO_STDIO_USB
169 while (!stdio_usb_connected()) {
170 sleep_ms(100);
171 }
172 #endif
173
174 // Init I2C interface
175 uint baud = i2c_init (I2C_ID, BAUD_RATE);
176 printf ("I2C @ %u Hz\n", baud);
177 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
178 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
179 gpio_pull_up(I2C_SCL_PIN);
180 gpio_pull_up(I2C_SDA_PIN);
181
182 // Init sensor
183 MMA8452 sensor (I2C_ID);
184 sensor.begin();
185
186 // Display sensor ID
187 printf ("Sensor ID: %02X\n", sensor.getId());
188
189 // Main loop
190 MMA8452::VECT_3D data;
191 while(1) {
192 sleep_ms(2000);
193 sensor.getAccel(&data);
194 printf ("Accel X:%.1f Y:%.1f Z:%.1f\n", data.x, data.y, data.z);
Electronic Compass, Accelerometers, and Gyroscopes 362
195 }
196 }
Arduino Code
We are going to use a Sparkfun library
There is a small gotcha! in this library, when using an I²C address different from its default (0x1D):
if you do not specify the address in the begin call, it will ignore the address set in the constructor
and use the default.
MicroPython Code
The code has my own class for the MMA8452 (adapted from Sparkfun Arduino C++ library).
Electronic Compass, Accelerometers, and Gyroscopes 363
1 # MMA8452 Example
2
3 '''
4 MicroPython Class for MMA8452 sensor
5
6 Adapted form Sparkfun Arduino library
7 https://github.com/sparkfun/SparkFun_MMA8452Q_Arduino_Library
8
9 '''
10 import struct
11
12 class MMA8452():
13 # Constructor
14 def __init__(self, i2c=None, addr=0x1C):
15 # check parameters
16 if i2c is None:
17 raise ValueError("I2C not specified")
18 else:
19 self.i2c = i2c
20 self.addr = addr
21
22 # registers
23 self.STATUS_MMA8452Q = 0x00
24 self.OUT_X_MSB = 0x01
25 self.OUT_X_LSB = 0x02
26 self.OUT_Y_MSB = 0x03
27 self.OUT_Y_LSB = 0x04
28 self.OUT_Z_MSB = 0x05
29 self.OUT_Z_LSB = 0x06
30 self.SYSMOD = 0x0B
31 self.INT_SOURCE = 0x0C
32 self.WHO_AM_I = 0x0D
33 self.XYZ_DATA_CFG = 0x0E
34 self.HP_FILTER_CUTOFF = 0x0F
35 self.PL_STATUS = 0x10
36 self.PL_CFG = 0x11
37 self.PL_COUNT = 0x12
38 self.PL_BF_ZCOMP = 0x13
Electronic Compass, Accelerometers, and Gyroscopes 364
39 self.P_L_THS_REG = 0x14
40 self.FF_MT_CFG = 0x15
41 self.FF_MT_SRC = 0x16
42 self.FF_MT_THS = 0x17
43 self.FF_MT_COUNT = 0x18
44 self.TRANSIENT_CFG = 0x1D
45 self.TRANSIENT_SRC = 0x1E
46 self.TRANSIENT_THS = 0x1F
47 self.TRANSIENT_COUNT = 0x20
48 self.PULSE_CFG = 0x21
49 self.PULSE_SRC = 0x22
50 self.PULSE_THSX = 0x23
51 self.PULSE_THSY = 0x24
52 self.PULSE_THSZ = 0x25
53 self.PULSE_TMLT = 0x26
54 self.PULSE_LTCY = 0x27
55 self.PULSE_WIND = 0x28
56 self.ASLP_COUNT = 0x29
57 self.CTRL_REG1 = 0x2A
58 self.CTRL_REG2 = 0x2B
59 self.CTRL_REG3 = 0x2C
60 self.CTRL_REG4 = 0x2D
61 self.CTRL_REG5 = 0x2E
62 self.OFF_X = 0x2F
63 self.OFF_Y = 0x30
64 self.OFF_Z = 0x31
65
66 # scales
67 self.SCALE_2G = 2
68 self.SCALE_4G = 4
69 self.SCALE_8G = 8
70
71 # data rates
72 self.ODR_800 = 0
73 self.ODR_400 = 1
74 self.ODR_200 = 2
75 self.ODR_100 = 3
76 self.ODR_50 = 4
77 self.ODR_12 = 5
Electronic Compass, Accelerometers, and Gyroscopes 365
78 self.ODR_6 = 6
79 self.ODR_1 = 7
80
81 # orientations
82 self.PORTRAIT_U = 0
83 self.PORTRAIT_D = 1
84 self.LANDSCAPE_R = 2
85 self.LANDSCAPE_L = 3
86 self.LOCKOUT = 0x40
87
88 # system mode
89 self.SYSMOD_STANDBY = 0
90 self.SYSMOD_WAKE = 1
91 self.SYSMOD_SLEEP = 2
92
93 # Sensor init
94 def begin(self):
95 if self.readRegister(self.WHO_AM_I) != 42:
96 return False
97 self.scale = self.SCALE_2G
98 self.odr = self.ODR_800
99 self.setScale(self.scale)
100 self.setDataRate(self.odr)
101 self.setupPL() # Portrait / Landscape detection
102 self.setupTap(0x80, 0x80, 0x80) # disable
103 return True
104
105 # Get raw acceleration
106 # data in registers: hhhhhhhh llll0000
107 def getRawAcel(self):
108 acel = struct.unpack(">hhh", self.readRegisters (self.OUT_X_MSB, 6))
109 return [a >> 4 for a in acel]
110
111 # Get scaled acceleration
112 def getCalclulatedAcel(self):
113 return [float(a)/float(1 << 11)*float(self.scale) for a in self.getRawAcel()]
114
115 # Test is new data available
116 def available(self):
Electronic Compass, Accelerometers, and Gyroscopes 366
CircuitPython Code
The code has my own class for the MMA8452 (adapted from Sparkfun Arduino C++ library).
Electronic Compass, Accelerometers, and Gyroscopes 371
1 # MMA8452 Example
2
3 '''
4 CircuitPython Class for MMA8452 sensor
5
6 Adapted form Sparkfun Arduino library
7 https://github.com/sparkfun/SparkFun_MMA8452Q_Arduino_Library
8
9 '''
10 import struct
11
12 class MMA8452():
13 # Constructor
14 def __init__(self, i2c=None, addr=0x1C):
15 # check parameters
16 if i2c is None:
17 raise ValueError("I2C not specified")
18 else:
19 self.i2c = i2c
20 self.addr = addr
21
22 # registers
23 self.STATUS_MMA8452Q = 0x00
24 self.OUT_X_MSB = 0x01
25 self.OUT_X_LSB = 0x02
26 self.OUT_Y_MSB = 0x03
27 self.OUT_Y_LSB = 0x04
28 self.OUT_Z_MSB = 0x05
29 self.OUT_Z_LSB = 0x06
30 self.SYSMOD = 0x0B
31 self.INT_SOURCE = 0x0C
32 self.WHO_AM_I = 0x0D
33 self.XYZ_DATA_CFG = 0x0E
34 self.HP_FILTER_CUTOFF = 0x0F
35 self.PL_STATUS = 0x10
36 self.PL_CFG = 0x11
37 self.PL_COUNT = 0x12
38 self.PL_BF_ZCOMP = 0x13
Electronic Compass, Accelerometers, and Gyroscopes 372
39 self.P_L_THS_REG = 0x14
40 self.FF_MT_CFG = 0x15
41 self.FF_MT_SRC = 0x16
42 self.FF_MT_THS = 0x17
43 self.FF_MT_COUNT = 0x18
44 self.TRANSIENT_CFG = 0x1D
45 self.TRANSIENT_SRC = 0x1E
46 self.TRANSIENT_THS = 0x1F
47 self.TRANSIENT_COUNT = 0x20
48 self.PULSE_CFG = 0x21
49 self.PULSE_SRC = 0x22
50 self.PULSE_THSX = 0x23
51 self.PULSE_THSY = 0x24
52 self.PULSE_THSZ = 0x25
53 self.PULSE_TMLT = 0x26
54 self.PULSE_LTCY = 0x27
55 self.PULSE_WIND = 0x28
56 self.ASLP_COUNT = 0x29
57 self.CTRL_REG1 = 0x2A
58 self.CTRL_REG2 = 0x2B
59 self.CTRL_REG3 = 0x2C
60 self.CTRL_REG4 = 0x2D
61 self.CTRL_REG5 = 0x2E
62 self.OFF_X = 0x2F
63 self.OFF_Y = 0x30
64 self.OFF_Z = 0x31
65
66 # scales
67 self.SCALE_2G = 2
68 self.SCALE_4G = 4
69 self.SCALE_8G = 8
70
71 # data rates
72 self.ODR_800 = 0
73 self.ODR_400 = 1
74 self.ODR_200 = 2
75 self.ODR_100 = 3
76 self.ODR_50 = 4
77 self.ODR_12 = 5
Electronic Compass, Accelerometers, and Gyroscopes 373
78 self.ODR_6 = 6
79 self.ODR_1 = 7
80
81 # orientations
82 self.PORTRAIT_U = 0
83 self.PORTRAIT_D = 1
84 self.LANDSCAPE_R = 2
85 self.LANDSCAPE_L = 3
86 self.LOCKOUT = 0x40
87
88 # system mode
89 self.SYSMOD_STANDBY = 0
90 self.SYSMOD_WAKE = 1
91 self.SYSMOD_SLEEP = 2
92
93 # Sensor init
94 def begin(self):
95 if self.readRegister(self.WHO_AM_I) != 42:
96 return False
97 self.scale = self.SCALE_2G
98 self.odr = self.ODR_800
99 self.setScale(self.scale)
100 self.setDataRate(self.odr)
101 self.setupPL() # Portrait / Landscape detection
102 self.setupTap(0x80, 0x80, 0x80) # disable
103 return True
104
105 # Get raw acceleration
106 # data in registers: hhhhhhhh llll0000
107 def getRawAcel(self):
108 acel = struct.unpack(">hhh", self.readRegisters (self.OUT_X_MSB, 6))
109 return [a >> 4 for a in acel]
110
111 # Get scaled acceleration
112 def getCalclulatedAcel(self):
113 return [float(a)/float(1 << 11)*float(self.scale) for a in self.getRawAcel()]
114
115 # Test is new data available
116 def available(self):
Electronic Compass, Accelerometers, and Gyroscopes 374
MPU6050 Module
• CONFIG (0x1A) and SMPRT_DIV (0x19): control the Digital Low Pass Filter (DLPF) and
the Sample Rate Divider. The accelerometer generates 1k samples per second and the
gyroscope 8K (DLPF disabled) or 1k (DLPF enabled) samples per second. The rate of
samples output to the output registers and FIFO is the Gyroscope Output Rate divided
by the content of SMPRT_DIV plus one. If this rate is above 1kHz the same accelerometer
sample will be output multiple times. For example, if DLPF is enabled and SMPRT_DIV is 9,
the output registers will be updated 1000/(9+1) = 100 times per second.
⁹https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Register-Map1.pdf
¹⁰https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Datasheet1.pdf
Electronic Compass, Accelerometers, and Gyroscopes 380
• FIFO_EN (0x1D): determines which sensor measurements are loaded into the FIFO buffer.
• INT_PIN_CFG (0x37): configures the interrupt signal behavior at the INT pins.
• INT_ENABLE (0x38): enables interrupt generation by interrupt sources.
• INT_STATUS (0x3A): shows the interrupt status of each interrupt generation source. Each
bit will clear after the register is read.
• ACCEL_xOUT (0x3B to 0x40): store the most recent accelerometer measurements in 16-
bit 2’s complement format. To convert the read value into g, divide by the sensitivity
corresponding to the full-scale range.
• TEMP_OUT (0x41 and 0x42): store the most recent temperature sensor measurement in
16-bit 2’s complement format. To convert this value to the temperature in degrees C, divide
by 340 and add 36.53.
• GYRO_xOUT (0x43 to 0x48): store the most recent gyroscope measurements in 16-bit
2’s complement format. To convert the read value into °/s, divide by the sensitivity
corresponding to the full-scale range.
Electronic Compass, Accelerometers, and Gyroscopes 381
MPU6050 Example
The following diagram shows the connection of power and the I²C bus, depending on your sensor
board you may need to add a pull-down for the SA0 pin.
1 /**
2 * @file mpu6050_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief MPU6050 Sensor Example
5 * @version 1.0
6 * @date 2023-07-06
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
Electronic Compass, Accelerometers, and Gyroscopes 382
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
16 #include "hardware/i2c.h"
17
18 // Sensor connections
19 #define I2C_ID i2c0
20 #define I2C_SCL_PIN 17
21 #define I2C_SDA_PIN 16
22
23 #define BAUD_RATE 400000 // 400kHz
24
25 // Class to access sensor
26 class MPU6050 {
27
28 public:
29
30 typedef struct {
31 float x;
32 float y;
33 float z;
34 } VECT_3D;
35
36 // Default I2C Address
37 static const uint8_t I2C_ADDR = 0x68;
38
39 // Chip ID
40 static const uint8_t ID = 0x68;
41
42 // Constructor
43 MPU6050 (i2c_inst_t *i2c, uint8_t addr = MPU6050::I2C_ADDR);
44
45 // Public methods
46 void begin(void);
47 uint8_t getId(void);
48 void reset(void);
Electronic Compass, Accelerometers, and Gyroscopes 383
88
89 this->i2c = i2c;
90 this->addr = addr;
91 }
92
93 // Set up sensor
94 void MPU6050::begin() {
95 reset();
96 this->id = read8(WHO_AM_I);
97 write8(CONFIG, 0x00);
98 sleep_ms(100);
99 write8(ACCEL_CONFIG, 0x00);
100 write8(GYRO_CONFIG, 0x08);
101 write8(SMPLRT_DIV, 0x00);
102 write8(PWR_MGMT_1, 0x01);
103 write8(PWR_MGMT_2, 0x00);
104 sleep_ms(20);
105 }
106
107 // Returns the sensor id
108 uint8_t MPU6050::getId() {
109 return id;
110 }
111
112 // Reset the sensor
113 void MPU6050::reset(void) {
114 uint8_t val = read8(PWR_MGMT_1) | 0x80;
115 write8(PWR_MGMT_1, val);
116 while (val & 0x80) {
117 val = read8(PWR_MGMT_1);
118 sleep_ms(1);
119 }
120 val = read8(SIG_PATH_RESET) | 0x07;
121 write8(SIG_PATH_RESET, val);
122 while (val & 0x07) {
123 val = read8(SIG_PATH_RESET);
124 sleep_ms(1);
125 }
126 }
Electronic Compass, Accelerometers, and Gyroscopes 385
127
128 // Get acceleration in g
129 void MPU6050::getAccel(VECT_3D *vect, float scale) {
130 readRaw();
131 vect->x = (float) raw[0] / scale;
132 vect->y = (float) raw[1] / scale;
133 vect->z = (float) raw[2] / scale;
134 }
135
136 // Read gyroscope in degress per second
137 void MPU6050::getGyro(VECT_3D *vect, float scale) {
138 readRaw();
139 vect->x = (float) raw[4] / scale;
140 vect->y = (float) raw[5] / scale;
141 vect->z = (float) raw[6] / scale;
142 }
143
144 // get temperture in C
145 float MPU6050::getTemp() {
146 readRaw();
147 return (float) raw[3]/340.0 + 36.53;
148 }
149
150 // Read raw values
151 void MPU6050::readRaw() {
152 uint8_t data[14];
153
154 // Select first register
155 uint8_t reg = ACCEL_OUT;
156 i2c_write_blocking (i2c, addr, ®, 1, true);
157
158 // Read values
159 i2c_read_blocking (i2c, addr, data, 14, false);
160
161 // Convert to int16
162 for (int i = 0; i < 7; i++) {
163 raw[i] = (data[2*i] << 8) | data[2*i+1];
164 }
165 }
Electronic Compass, Accelerometers, and Gyroscopes 386
166
167 // Read a 8-bit value from a register
168 uint8_t MPU6050::read8 (uint8_t reg) {
169 uint8_t val[1];
170
171 // Select register
172 i2c_write_blocking (i2c, addr, ®, 1, true);
173
174 // Read value
175 i2c_read_blocking (i2c, addr, val, 1, false);
176 return val[0];
177 }
178
179 // Writea 8-bit value to a register
180 void MPU6050::write8 (uint8_t reg, uint8_t val) {
181 uint8_t aux[2];
182
183 aux[0] = reg;
184 aux[1] = val;
185 i2c_write_blocking (i2c, addr, aux, 2, false);
186 }
187
188
189
190 // Main Program
191 int main() {
192 // Init stdio
193 stdio_init_all();
194 #ifdef LIB_PICO_STDIO_USB
195 while (!stdio_usb_connected()) {
196 sleep_ms(100);
197 }
198 #endif
199
200 // Init I2C interface
201 uint baud = i2c_init (I2C_ID, BAUD_RATE);
202 printf ("I2C @ %u Hz\n", baud);
203 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
204 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
Electronic Compass, Accelerometers, and Gyroscopes 387
205 gpio_pull_up(I2C_SCL_PIN);
206 gpio_pull_up(I2C_SDA_PIN);
207
208 // Init sensor
209 MPU6050 sensor (I2C_ID);
210 sensor.begin();
211
212 // Display sensor ID
213 printf ("Sensor ID: %02X\n", sensor.getId());
214
215 // Main loop
216 MPU6050::VECT_3D data;
217 while(1) {
218 sleep_ms(2000);
219 sensor.getAccel(&data);
220 printf ("Accel X:%.1f Y:%.1f Z:%.1f\n", data.x, data.y, data.z);
221 sensor.getGyro(&data);
222 printf ("Gyro X:%.1f Y:%.1f Z:%.1f\n", data.x, data.y, data.z);
223 printf ("Temp: %.1f\n", sensor.getTemp());
224 printf("\n");
225 }
226 }
Arduino Code
There are quite a few libraries for the MPU6050. I choose to go with the MPU6050_light:
Electronic Compass, Accelerometers, and Gyroscopes 388
MPU6050_light Library
As the name suggests, this is a not-too-complex library, with limited support for configuration (I
stayed with the default). One interesting feature is the “offset calibration”: assuming the sensor
is resting parallel to the ground, the measurements are saved and later discounted.
Arduino MPU6050 Example
19
20 // Find values when the sensor is resting horizontally
21 Serial.println("Calculating offsets, do not move MPU6050");
22 delay(1000);
23 sensor.calcOffsets(true,true);
24 Serial.println("Done!\n");
25 }
26
27 // Main Loop
28 void loop() {
29 // call update frequently to get angles
30 sensor.update();
31 if (millis() > nextReading) {
32 Serial.print ("Accel X:");
33 Serial.print (sensor.getAccX(), 1);
34 Serial.print ("g Y:");
35 Serial.print (sensor.getAccY(), 1);
36 Serial.print ("g Z:");
37 Serial.print (sensor.getAccZ(), 1);
38 Serial.println ("g");
39
40 Serial.print ("Gyro X:");
41 Serial.print (sensor.getGyroX(), 1);
42 Serial.print ("dg/s Y:");
43 Serial.print (sensor.getGyroY(), 1);
44 Serial.print ("dg/s Z:");
45 Serial.print (sensor.getGyroZ(), 1);
46 Serial.println ("dg/s");
47
48 Serial.print ("Temp:");
49 Serial.print (sensor.getTemp(), 1);
50 Serial.println ("C");
51
52 Serial.println ();
53
54 nextReading = millis() + 2000;
55 }
56 }
Electronic Compass, Accelerometers, and Gyroscopes 390
MicroPython Code
The example code includes a simple class to set up the sensor with fixed parameters and read the
measurements.
MicroPython MPU6050 Example
1 # MPU6050 Example
2 from machine import I2C, Pin
3 from time import sleep
4
5 # Class to acess sensor
6 class MPU6050:
7
8 _DEFAULT_ADDRESS = 0x68 # MPU6050 default i2c address w/ AD0 low
9 _DEVICE_ID = 0x68 # original MPU6050_WHO_AM_I value
10
11 _SMPLRT_DIV = 0x19 # sample rate divisor register
12 _CONFIG = 0x1A # General configuration register
13 _GYRO_CONFIG = 0x1B # Gyro specfic configuration register
14 _ACCEL_CONFIG = 0x1C # Accelerometer specific configration register
15 _ACCEL_OUT = 0x3B # base address for sensor data reads
16 _TEMP_OUT = 0x41 # Temperature data high byte register
17 _GYRO_OUT = 0x43 # base address for sensor data reads
18 _SIG_PATH_RESET = 0x68 # register to reset sensor signal paths
19 _PWR_MGMT_1 = 0x6B # Primary power/sleep control register
20 _PWR_MGMT_2 = 0x6C # Secondary power/sleep control register
21 _WHO_AM_I = 0x75 # Device ID register
22
23 # Constructor
24 def __init__(self, i2c, addr):
25 self.i2c = i2c
26 self.addr = addr
27
28 # Set up sensor
29 def begin(self):
30 self.reset()
31 # Check ID
32 self.id = self.read_reg(MPU6050._WHO_AM_I)
33 if self.id != MPU6050._DEVICE_ID:
34 print ('WARNING: Wrong ID! {:02X}'.format(self.id))
Electronic Compass, Accelerometers, and Gyroscopes 391
35 # Set config
36 # Bandwith 260Hz (Accel) & 256Hz (Gyro)
37 # Range ±500°/s (Gyro) ±2g (Accel)
38 i2c.writeto_mem(self.addr, MPU6050._CONFIG, b'\x00')
39 sleep(0.05)
40 i2c.writeto_mem(self.addr, MPU6050._ACCEL_CONFIG, b'\x00')
41 i2c.writeto_mem(self.addr, MPU6050._GYRO_CONFIG, b'\x08')
42 i2c.writeto_mem(self.addr, MPU6050._SMPLRT_DIV, b'\x00')
43 i2c.writeto_mem(self.addr, MPU6050._PWR_MGMT_1, b'\x01')
44 i2c.writeto_mem(self.addr, MPU6050._PWR_MGMT_2, b'\x00')
45 sleep(0.02)
46
47 # Sensor ID
48 def get_id(self):
49 return self.id
50
51 # Reset sensor
52 def reset(self):
53 val = bytearray([self.read_reg(MPU6050._PWR_MGMT_1) | 0x80])
54 i2c.writeto_mem(self.addr, MPU6050._PWR_MGMT_1, val)
55 while (val[0] & 0x80) != 0:
56 val = i2c.readfrom_mem(self.addr, MPU6050._PWR_MGMT_1, 1)
57 sleep(0.001)
58 val = bytearray([self.read_reg(MPU6050._SIG_PATH_RESET) | 0x07])
59 i2c.writeto_mem(self.addr, MPU6050._SIG_PATH_RESET, val)
60 while (val[0] & 0x07) != 0:
61 val = i2c.readfrom_mem(self.addr, MPU6050._SIG_PATH_RESET, 1)
62 sleep(0.001)
63
64 # Read Raw Values
65 # accX, acccY, accZ, temp, gyroX, gyroY, gyroZ
66 def read_raw(self):
67 data = i2c.readfrom_mem(self.addr, MPU6050._ACCEL_OUT, 14)
68 raw = []
69 for i in range (0, 14, 2):
70 v = (data[i] << 8) + data[i+1]
71 if (v & 0x8000) != 0:
72 v = v - 0x10000
73 raw.append(v)
Electronic Compass, Accelerometers, and Gyroscopes 392
74 return raw
75
76 # Read acceleration in g
77 def get_accel(self, scale = 16384.0):
78 data = self.read_raw()
79 return (data[0]/scale, data[1]/scale, data[2]/scale)
80
81 # Read temperature in C
82 def get_temp(self):
83 data = self.read_raw()
84 return data[3]/340.00 + 36.53
85
86 # Read gyroscope in °/s
87 def get_gyro(self, scale = 65.5):
88 data = self.read_raw()
89 return (data[4]/scale, data[5]/scale, data[6]/scale)
90
91 # Read an 8-bit register
92 def read_reg(self, reg):
93 return i2c.readfrom_mem(self.addr, reg, 1)[0]
94
95
96 # Test Program
97 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
98 sensor = MPU6050(i2c, MPU6050._DEFAULT_ADDRESS)
99 sensor.begin()
100 print('ID = {:02X}'.format(sensor.get_id()))
101 while True:
102 print (sensor.read_raw())
103 x,y,z = sensor.get_accel()
104 print ('Accel X:{:.1f}g y:{:.1f}g Z:{:.1f}g'.format(x, y, z))
105 x,y,z = sensor.get_gyro()
106 print ('Gyro X:{:.1f} y:{:.1f} Z:{:.1f}'.format(x,y,z))
107 print ('Temp: {:.1f}C'.format(sensor.get_temp()))
108 sleep(2)
CircuitPython Code
We are going to use one of the official libraries from Adafruit:
Electronic Compass, Accelerometers, and Gyroscopes 393
1. Download the “Bundle for Version 8.x” (or whatever CircuitPython version you are using)
from https://circuitpython.org/libraries.
2. Expand the zip file, the files we want will be under the lib subdirectory.
3. Connect to a PC with your Pico with CircuitPython installed. A drive called CIRCUITPY
will appear on your computer.
4. Copy the file adafruit_mpu6050_mpyand the adafruit_bus_device and adafruit_reg-
ister subdirectories from the expanded bundle to the lib directory in the CIRCUITPY
drive.
• A positive pulse, with at least 10μs width, is applied at the Trig pin.
• Sensor sends eight ultrasonic pulses (with frequency around 40kHz) and raises the Echo
pin to a HIGH level.
• When the sensor detects the echo of the pulses, it lowers the Echo pin back to a LOW level.
• If no echo is detected in (approximately) 38ms, the Echo pin returns to a LOW level.
Miscellaneous Sensors 396
HC-SR04 Signals
We can find the time t it took for the pulses to go to the nearest obstacle and came back by
measuring the time the Echo pin stays high. Knowing the speed of sound s, we can calculate the
distance d to the obstacle:
d = f rac(t ∗ s2
The speed of sound at 20°C is 343m/s. The speed s changes with the temperature T:
s = 331.3 + 0, 606 ∗ T
The specification says that the sensor works for obstacles between 2,5cm (echo time = 150µs) and
4,3m (echo time = 25 ms). The ultrasonic pulse is sent with an angle of about 60°:
An obstacle at the maximum distance needs to have an area of at least 0,5m² (like a 70cm by 70cm
square) to give a satisfactory echo.
HY-SRF05
The HY-SRF05 sensor is sold as an enhancement to the HC-SR04, with (slightly) better precision
and range.
Miscellaneous Sensors 397
It has an additional pin marked as “OUT”. If this pin is unconnected, the sensor works as an HC-
SR04. If this pin is tied to the ground, the echo signal will appear on the trigger pin; you have to
change the trigger pin to input within 0.7ms after the end of the trigger pulse.
One problem I had with my HY-SRF05 was that it would not return the Echo pin to LOW when
it did not detect the echo. In this situation, the Echo pin would stay HIGH until the Trigger pin
was pulsed.
HC-SR04 Example
You can use an HY-SRF05 module if you adapt the connections to its pinout and leave the OUT
pin unconnected.
Miscellaneous Sensors 398
The example code will do a distance measurement every 2 seconds and send the result to a PC.
You will need to connect your Pico to a PC to see the results.
• A register can only the initialized with a value between 0 and 31. The initial counter value
is got from the transmission queue and moved to the counting register (PULL, MOV X,
OSR).
• The WAIT instruction has no timeout. If no sensor is connected the program will stall
waiting for the echo to go high.
• The JMP instruction can only JMP on a high in a pin. As we are testing for a low, the code
is a little convoluted.
• When using JMP for a loop, it will first (unconditionally) decrement the counter and then
jump if the counter was not zero before the decrement. If the counter was zero, it will not
jump, but the counter will now be 2³²-1. A little unusual, but works fine in our case.
The PIO program receives (through the transmission queue) the timeout value (I used 300000
cycles, 150ms) and sends back (through the reception queue) the remaining counter.
C/C++ HC-SR04 Example - PIO code
1 ;
2 ; Interface to HC-SR04 sensor for 'Using Sensors with the Raspberry Pi Pico' book
3 ; Copyright (c) 2023, Daniel Quadros
4 ;
5
6 .program hcsr04
7
8 .wrap_target
9
10 // wait for a request and save timeout
11 pull
12 mov x,osr
13
14 // send a 10 us (20 cycles) pulse
Miscellaneous Sensors 399
54
55 // Set the pins GPIO function (connect PIO to the pad),
56 pio_gpio_init(pio, triggerPin);
57 pio_gpio_init(pio, echoPin);
58
59 // Configure the FIFOs
60 sm_config_set_in_shift (&c, true, false, 1);
61 sm_config_set_out_shift (&c, true, false, 1);
62
63 // Configure the clock for 2 MHz
64 float div = clock_get_hz(clk_sys) / 2000000;
65 sm_config_set_clkdiv(&c, div);
66
67 // Load our configuration, and jump to the start of the program
68 pio_sm_init(pio, sm, offset, &c);
69
70 // Set the state machine running
71 pio_sm_set_enabled(pio, sm, true);
72 }
73 %}
The main program initializes the serial communication, pins, and the PIO. It then enters a loop
doing measurements. From the remaining counter returned by the PIO, we calculate the number
of cycles and microseconds for the echo signal to go down. This time is then multiplied by half
the speed of sound to get the distance.
To support the HY-SRF05, the remaining counter is tested for a timeout.
C/C++ HC-SR04 Example - C Code
1 /**
2 * @file hcsr04_sdk.c
3 * @author Daniel Quadros
4 * @brief HC-SR04 Ultrasonic Sensor Example
5 * @version 1.0
6 * @date 2023-07-24
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
Miscellaneous Sensors 401
12 #include "stdio.h"
13 #include "pico/stdlib.h"
14 #include "hardware/pio.h"
15 #include "hardware/clocks.h"
16
17 // Our PIO program:
18 #include "hcsr04.pio.h"
19
20 // Sensor connections
21 #define PIN_TRIGGER 17
22 #define PIN_ECHO 16
23
24 // PIO
25 static PIO pio = pio0;
26 static uint offset;
27 static uint sm;
28
29 #define TIMEOUT 300000
30
31 int main() {
32 // Init stdio
33 stdio_init_all();
34 #ifdef LIB_PICO_STDIO_USB
35 while (!stdio_usb_connected()) {
36 sleep_ms(100);
37 }
38 #endif
39
40 printf("\nHC-SR04 Example\n");
41
42 // Init and start the state machine
43 sm = pio_claim_unused_sm(pio, true);
44 offset = pio_add_program(pio, &hcsr04_program);
45 hcsr04_program_init(pio, sm, offset, PIN_TRIGGER, PIN_ECHO);
46
47 while (true) {
48 pio_sm_put (pio, sm, TIMEOUT);
49 uint32_t val = pio_sm_get_blocking (pio, sm);
50 if (val < TIMEOUT) {
Miscellaneous Sensors 402
Arduino Code
In the Arduino code, we use the PulseIn function to measure the echo pulse. This routine tests a
pin in a tight loop.
Arduino HC-SR04 Example
24 delayMicroseconds(10);
25 digitalWrite(PIN_TRIGGER, LOW);
26
27 // Measure echo pin
28 uint32_t ellapsed = pulseIn(PIN_ECHO, HIGH, 100000);
29
30 // Calculate distance
31 if (ellapsed != 0) {
32 float distance = (ellapsed * 0.0343) / 2.0;
33 Serial.print("Distance = ");
34 Serial.print(distance, 1);
35 Serial.println(" cm");
36 delay(2000);
37 } else {
38 Serial.println("** TIMEOUT **");
39 }
40 }
MicroPython Code
The MicroPython code uses the same PIO program as the C/C++ code. This allows better precision
than trying to measure the short times in interpreted code.
MicroPython HC-SR04 Example
16 set(pins,1) [19]
17 set(pins,0)
18
19 # wait for the start of the echo pulse
20 wait(1,pin,0)
21
22 # wait for the end of the echo pulse
23 # decrements X every 2 cycles (1us)
24 label('dowait')
25 jmp(pin,'continue')
26 jmp('end')
27 label('continue')
28 jmp(x_dec,'dowait')
29
30 # return pulse duration
31 label('end')
32 mov(isr,x)
33 push()
34
35 # Set up pins
36 trigger = Pin(17, Pin.OUT)
37 echo = Pin(16, Pin.IN)
38 sm = rp2.StateMachine(0)
39
40 # Do measurements
41 while True:
42 sm.init(ULTRA_PIO,freq=2000000,set_base=trigger,in_base=echo,jmp_pin=echo)
43 sm.active(1)
44 sm.put(300000)
45 val = sm.get()
46 sm.active(0)
47 if val < 300000:
48 ellapsed = 300000 - val
49 distance = (ellapsed * 0.0343) / 2
50 print('Distance = {0:.1f} cm'.format(distance))
51 utime.sleep_ms(1000)
52 else:
53 print('Timeout')
54
Miscellaneous Sensors 405
CircuitPython Code
Here we use the pulseio module to measure the echo pulse.
CircuitPython HC-SR04 Example
1 # HC-SR04 Ultrasonic Sensor Example
2
3 import digitalio
4 import pulseio
5 import board
6 from time import sleep
7 from time import monotonic
8
9 # Set up trigger pin
10 trigger = digitalio.DigitalInOut(board.GP17)
11 trigger.direction = digitalio.Direction.OUTPUT
12 trigger.value = False
13
14 # Set up echo pin
15 echo = pulseio.PulseIn(board.GP16, idle_state=False)
16 echo.pause()
17
18 # Do measurements
19 while True:
20 # restart pulsemeasurement
21 echo.clear()
22 echo.resume()
23
24 # pulse the trigger
25 trigger.value = True
26 sleep(0.001)
27 trigger.value = False
28
29 # wait for echo pulse capture
30 tout = monotonic() + 0.1
31 while (len(echo) == 0) and (monotonic() < tout):
32 pass
33 echo.pause()
34
35 # Calculate distance
Miscellaneous Sensors 406
36 if len(echo) > 0:
37 distance = (echo[0] * 0.0343) / 2
38 print('Distance = {0:.1f} cm'.format(distance))
39 else:
40 print('Timeout')
41
42 sleep(2)
Rotary Encoder
A rotary encoder may look outside like a potentiometer, but inside it is completely different and
has a different purpose.
Rotary Encoder
We have already looked at potentiometers and learned how they allow us to measure the absolute
angular position of its shaft.
A rotary encoder allows us to measure the speed and direction of the shaft rotation, but does not
inform the absolute position.
Internally to the rotary encoder, we have two contacts (usually called A and B) that will connect
and disconnect to a common terminal (normally connected to the ground) as the shaft is turned.
Miscellaneous Sensors 407
The order of the changes indicates the direction of the movement; by measuring the time between
changes we can measure the rotation speed.
Some models of a rotary encoder, designed for user interface, have a switch that is closed when
the shaft is pressed down. The sensor may also have mechanical “stops” that give a “click” when
the signals change. The specification says how many changes occur per full turn. Boards with
a rotary encoder may have pull-up resistors for the A and B signals (and the central switch, if
included).
We can have oscillations (bounce) when a contact is made or broken. Here are some usual
strategies to read a rotary encoder:
Miscellaneous Sensors 408
In the example, the rotary encoder is used to increase and decrease a number (from 0 to 100). The
number is sent to a PC, you will need to connect the Pico to the PC to see the program output.
1 ; --------------------------------------------------
2 ; Quadrature Encoder reader using PIO
3 ; by Christopher (@ZodiusInfuser) Parrott
4 ; from https://github.com/pimoroni/pimoroni-pico
5 ; --------------------------------------------------
6 ;
7 ; Watches any two pins (i.e. do not need to be
8 ; consecutive) for when their state changes, and
9 ; pushes that new state along with the old state,
10 ; and time since the last change.
11 ;
12 ; - X is used for storing the last state
13 ; - Y is used as a general scratch register
14 ; and for storing the current state
15 ; - OSR is used for storing the state-change timer
16 ;
17 ; After data is pushed into the system, a long delay
18 ; takes place as a form of switch debounce to deal
19 ; with rotary encoder dials. This is currently set
20 ; to 500 cycles, but can be changed using the debounce
21 ; constants below, as well as adjusting the frequency
22 ; the PIO state machine runs at. E.g. a freq_divider
23 ; of 250 gives a 1ms debounce.
24
25
26 ; Debounce Constants
27 ; --------------------------------------------------
28 .define SET_CYCLES 20
29 .define ITERATIONS 30
30 .define JMP_CYCLES 16
31 .define public ENC_DEBOUNCE_CYCLES (SET_CYCLES + (JMP_CYCLES * ITERATIONS))
32
33 ; Ensure that ENC_DEBOUNCE_CYCLES is a multiple of the
34 ; number of cycles the wrap takes, which is currently
35 ; 10 cycles, otherwise timing may be inaccurate
36
37
38 ; Encoder Program
Miscellaneous Sensors 410
39 ; --------------------------------------------------
40 .program encoder
41
42 .wrap_target
43 loop:
44 ; Copy the state-change timer from OSR,
45 ; decrement it, and save it back
46 mov y, osr
47 jmp y-- osr_dec
48 osr_dec:
49 mov osr, y
50 ; takes 3 cycles
51
52 ; Read the state of both encoder pins and check
53 ; if they are different from the last state
54 jmp pin enc_a_was_high
55 mov isr, null
56 jmp read_enc_b
57 enc_a_was_high:
58 set y, 1
59 mov isr, y
60 read_enc_b:
61 in pins, 1
62 mov y, isr
63 jmp x!=y state_changed [1]
64 ; takes 7 cycles on both paths
65 .wrap
66
67 state_changed:
68 ; Put the last state and the timer value into
69 ; ISR alongside the current state, and push that
70 ; state to the system. Then override the last
71 ; state with the current state
72 in x, 2
73 mov x, ~osr ; invert the timer value to give
74 ; a sensible value to the system
75 in x, 28
76 push noblock ; this also clears isr
77 mov x, y
Miscellaneous Sensors 411
78
79 ; Perform a delay to debounce switch inputs
80 set y, (ITERATIONS - 1) [SET_CYCLES - 1]
81 debounce_loop:
82 jmp y-- debounce_loop [JMP_CYCLES - 1]
83
84 ; Initialise the timer, as an inverse, and decrement
85 ; it to account for the time this setup takes
86 mov y, ~null
87 jmp y-- y_dec
88 y_dec:
89 mov osr, y
90 jmp loop [1]
91 ;takes 10 cycles, not counting whatever the debounce adds
92
93
94 ; Initialisation Code
95 ; --------------------------------------------------
96 % c-sdk {
97 static const uint8_t ENC_LOOP_CYCLES = encoder_wrap - encoder_wrap_target;
98
99 // The time that the debounce takes, as the number of wrap loops that the debounce i\
100 s equivalent to
101 static const uint8_t ENC_DEBOUNCE_TIME = ENC_DEBOUNCE_CYCLES / ENC_LOOP_CYCLES;
102 %}
The main code will use the state changes detected by the PIO program to detect the sequences to
increment or decrement the number. The timing information is ignored.
C/C++ Rotary Encoder Example
1 /**
2 * @file rotary_sdk.c
3 * @author Daniel Quadros
4 * @brief Rotary Encoder Example
5 * @version 1.0
6 * @date 2023-07-25
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
Miscellaneous Sensors 412
10 */
11
12 #include "stdio.h"
13 #include "pico/stdlib.h"
14 #include "hardware/pio.h"
15 #include "hardware/clocks.h"
16
17 // Our PIO program:
18 #include "encoder.pio.h"
19
20
21 // Encoder connections
22 #define ENCODER_A_PIN 16
23 #define ENCODER_B_PIN 17
24
25
26 // State information from the PIO program
27 static const uint32_t STATE_A_MASK = 0x80000000;
28 static const uint32_t STATE_B_MASK = 0x40000000;
29 static const uint32_t STATE_A_LAST_MASK = 0x20000000;
30 static const uint32_t STATE_B_LAST_MASK = 0x10000000;
31
32 static const uint32_t STATES_MASK = STATE_A_MASK | STATE_B_MASK |
33 STATE_A_LAST_MASK | STATE_B_LAST_MASK;
34
35 #define LAST_STATE(state) ((state) & 0b0011)
36 #define CURR_STATE(state) (((state) & 0b1100) >> 2)
37
38 enum MicroStep : uint8_t {
39 MICROSTEP_0 = 0b00,
40 MICROSTEP_1 = 0b10,
41 MICROSTEP_2 = 0b11,
42 MICROSTEP_3 = 0b01,
43 };
44
45 static uint8_t move_up = (MICROSTEP_2 << 6) | (MICROSTEP_3 << 4) | (MICROSTEP_0 << 2\
46 ) | MICROSTEP_1;
47 static uint8_t move_down = (MICROSTEP_1 << 6) | (MICROSTEP_0 << 4) | (MICROSTEP_3 <<\
48 2) | MICROSTEP_2;
Miscellaneous Sensors 413
49
50 // PIO and State Machine
51 static PIO enc_pio = pio0;
52 static uint enc_sm;
53
54
55 // Set up the PIO program
56 void encoder_program_init(uint enc_pin_a, uint enc_pin_b) {
57 // Load program and select state machine
58 uint offset = pio_add_program(enc_pio, &encoder_program);
59 enc_sm = pio_claim_unused_sm(enc_pio, true);
60
61 // Setup pins
62 pio_gpio_init(enc_pio, enc_pin_a);
63 pio_gpio_init(enc_pio, enc_pin_b);
64 gpio_pull_up(enc_pin_a);
65 gpio_pull_up(enc_pin_b);
66 pio_sm_set_consecutive_pindirs(enc_pio, enc_sm, enc_pin_a, 1, false);
67 pio_sm_set_consecutive_pindirs(enc_pio, enc_sm, enc_pin_b, 1, false);
68
69 // Config state machine
70 pio_sm_config c = encoder_program_get_default_config(offset);
71 sm_config_set_jmp_pin(&c, enc_pin_a);
72 sm_config_set_in_pins(&c, enc_pin_b);
73 sm_config_set_in_shift(&c, false, false, 1);
74 sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX);
75 sm_config_set_clkdiv_int_frac(&c, 250, 0);
76 pio_sm_init(enc_pio, enc_sm, offset, &c);
77
78 // Init the state
79 bool enc_state_a = gpio_get(enc_pin_a);
80 bool enc_state_b = gpio_get(enc_pin_b);
81 pio_sm_exec(enc_pio, enc_sm, pio_encode_set(pio_x, (uint)enc_state_a << 1 | (uint)\
82 enc_state_b));
83
84 // Start execution
85 pio_sm_set_enabled(enc_pio, enc_sm, true);
86 }
87
Miscellaneous Sensors 414
88
89 // Main Program
90 int main() {
91 // Init stdio
92 stdio_init_all();
93 #ifdef LIB_PICO_STDIO_USB
94 while (!stdio_usb_connected()) {
95 sleep_ms(100);
96 }
97 #endif
98
99 printf("\nRotary Encoder Example\n");
100
101 // Init and start the state machine
102 encoder_program_init(ENCODER_A_PIN, ENCODER_B_PIN);
103
104 uint8_t history = 0;
105 int val = 50;
106
107 printf ("%3d", val);
108
109 while (true) {
110 // Wait for a move
111 uint32_t received = pio_sm_get_blocking(enc_pio, enc_sm);
112
113 // Extract new state and push into the history
114 uint8_t states = (received & STATES_MASK) >> 28;
115 history = ((history & 0x3F) << 2) | CURR_STATE(states);
116
117 // Check for patterns of movement forward and backward
118 if (history == move_up) {
119 if (val < 100) {
120 val++;
121 printf ("\r%3d", val);
122 }
123 }
124 else if (history == move_down) {
125 if (val > 0) {
126 val--;
Miscellaneous Sensors 415
Arduino Code
The Arduino code uses the assembled PIO code from the C/C++ example. You have to copy the file
encoder.pio.h from the build directory of the C/C++ example to the directory of the Arduino
sketch.
Arduino Rotary Encoder Example
As you can see, we can call the SDK functions in our Arduino sketch, as long as it does not
conflict with the Arduino runtime.
MicroPython Code
We are going to use the Pimori PIO code one more time. Translating manually the PIO code to
the syntax used in MicroPython is a good exercise! You can also use the PIO assembler (pioasm)
to do that, but the resulting code is less readable.
MicroPython Rotary Encoder Example
62
63 # Init PIO
64 sm = rp2.StateMachine(0)
65 sm.init(ENCODER_PIO,freq=500000,
66 in_base=pin_B,in_shiftdir=PIO.SHIFT_LEFT,push_thresh=1,
67 jmp_pin=pin_A)
68 state = (pin_A.value() << 1) | pin_B.value()
69 sm.exec("set(x,"+str(state)+")")
70 sm.active(1)
71 hist = 0
72 val = 50
73 print(val)
74
75 while True:
76 state = (sm.get() & 0xC0000000) >> 30
77 hist = ((hist & 0x3F) << 2) | state
78 if hist == 0x4B:
79 if val < 100:
80 val = val+1
81 print(val)
82 elif hist == 0xE1:
83 if val > 0:
84 val = val-1
85 print(val)
CircuitPython Code
CircuitPython has a built-in module called rotary for working with rotary encoders. Its docu-
mentation is at https://docs.circuitpython.org/en/latest/shared-bindings/rotaryio/index.html.
Using the IncrementalEncoder class, we can write our example with just a few lines of code.
Miscellaneous Sensors 421
The difficulty in using load cells is that these changes in resistance are very small. To detect them,
we use a configuration of resistances called a Wheatstone Bridge.
In this configuration, a known voltage is applied at Vin and Vout is read. When R1 ∗ R4 =
R2 ∗ R3, Vout is zero (we say the bridge is balanced). A variation in the resistances can be
detected and calculated from Vout.
Some load cells have internally four sensors already connected in a bridge configuration. In this
case, the load cell has four wires for connection.
Models with a single sensor will have three wires. The best option¹¹ in this case is to construct a
bridge with four sensors, taking care of the polarities so that the differences do not cancel each
other.
¹¹It is also possible to construct a bridge with one or two sensors and fixed resistors, but the result is not as good.
Miscellaneous Sensors 423
Sensor Bridge
The HX711 has two differential channels for input, named A and B. The input to the ADC is the
difference between the two inputs of a channel multiplied by a gain.
The output uses two signals: DOUT and PD_SCK. DOUT is an output and PD_SCK is an input (from
the point of view of the HX711). While a conversion is been done, DOUT is HIGH, and PD_SCK
should be kept LOW. When data is available for reading, DOUT will go LOW. The microcontroller
Miscellaneous Sensors 424
then will then send 25 to 27 positive clock pulses on PD_SCK. The first 24 pulses will get the output
data (most significant bit first). The total number of pulses select the channel and gain for the
next conversion:
Total Number of Pulses Input Channel Gain
25 A 128
26 B 32
27 A 64
The typical clock frequency is 500 kHz (1µs HIGH, 1µs LOW). You can go as high as 2.5MHz and
as low as 10 kHz. You can reset the HX711 by keeping the clock signal HIGH for 60µs or more
(this is the reason for the lower limit).
• The program records the value (tare) with no weight over the surface
• The program records the value with a 1kg weight over the surface
The code assumes that the measured value from the cells is linearly proportional to the weight.
Miscellaneous Sensors 425
An I2C alphanumeric LCD is use to show the measured weight and a pushbutton is used to signal
the completion of each setup step.
1 ;
2 ; Interface to HX711 ADC for 'Using Sensors with the Raspberry Pi Pico' book
3 ; Copyright (c) 2023, Daniel Quadros
4 ;
5
6 .program hx711
7
8 .wrap_target
Miscellaneous Sensors 426
9
10 // wait for a new conversion
11 wait 1 pin 0
12 wait 0 pin 0
13
14 // read data bits
15 // 5 cycles HIGH, 5 cycles LOW
16 set x, 23
17 readbit:
18 set pins, 1 [3]
19 in pins, 1
20 set pins, 0 [3]
21 jmp x--, readbit
22
23 // add 1 pulse to set gain to 128
24 set pins, 1 [4]
25 set pins, 0
26
27 .wrap
28
29 % c-sdk {
30 // Helper function to set a state machine to run our PIO program
31 static inline void hx711_program_init(PIO pio, uint sm, uint offset,
32 uint dataPin, uint clockPin) {
33
34 // Get an initialized config structure
35 pio_sm_config c = hx711_program_get_default_config(offset);
36
37 // Map the state machine's pin groups
38 sm_config_set_set_pins(&c, clockPin, 1);
39 sm_config_set_in_pins(&c, dataPin);
40
41 // Set the pins directions at the PIO
42 pio_sm_set_consecutive_pindirs(pio, sm, clockPin, 1, true);
43 pio_sm_set_consecutive_pindirs(pio, sm, dataPin, 1, false);
44
45 // Make sure clock is low
46 pio_sm_set_pins_with_mask(pio, sm, 1 << clockPin, 0);
47
Miscellaneous Sensors 427
I will not list here the simple LCD driver I wrote, you can check it in the book GitHub
(https://github.com/dquadros/PicoSensors).
C/C++ Load Cell Example
1 /**
2 * @file hcsr04_sdk.c
3 * @author Daniel Quadros
4 * @brief HC-SR04 Ultrasonic Sensor Example
5 * @version 1.0
6 * @date 2023-07-24
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include "stdio.h"
13 #include "string.h"
14 #include "pico/stdlib.h"
15 #include "hardware/pio.h"
Miscellaneous Sensors 428
16 #include "hardware/clocks.h"
17 #include "hardware/i2c.h"
18 #include "lcd_i2c.h"
19
20 // Our PIO program:
21 #include "hx711.pio.h"
22
23 // Sensor connections
24 #define PIN_DATA 12
25 #define PIN_CLOCK 13
26
27 // Display connections
28 #define PIN_SDA 16
29 #define PIN_SCL 17
30
31 // Button connections
32 #define PIN_BUTTON 15
33
34 // PIO
35 static PIO pio = pio0;
36 static uint offset;
37 static uint sm;
38
39 // Parameters to convert value to weigh in kg
40 float tare, scale;
41
42 // Local routines
43 void keyPress(const char *msg);
44 void calibrate(void);
45 float hx711_avg(int count);
46
47 // Main Program
48 int main() {
49 // Init and start the state machine
50 sm = pio_claim_unused_sm(pio, true);
51 offset = pio_add_program(pio, &hx711_program);
52 hx711_program_init(pio, sm, offset, PIN_DATA, PIN_CLOCK);
53
54 // Init display
Miscellaneous Sensors 429
94 tare = hx711_avg(50);
95 keyPress("Put 1kg");
96 scale = (hx711_avg(50) - tare);
97 }
98
99 // Wait for a key press
100 void keyPress(const char *msg) {
101 lcd_clear();
102 lcd_write(0,0,msg);
103 lcd_write(1,0,"Press button");
104 while (gpio_get(PIN_BUTTON) == 1) {
105 sleep_ms(100);
106 }
107 sleep_ms(100); // debounce
108 lcd_write(1,0,"Release button");
109 while (gpio_get(PIN_BUTTON) == 0) {
110 sleep_ms(100);
111 }
112 sleep_ms(100); // debounce
113 lcd_write(1,0,"Wait ");
114 }
Arduino Code
We are going to use the “HX711 Arduino Library” to communicate with the HX711 module.
Miscellaneous Sensors 431
1 // LoadCell Example
2
3 #include <HX711.h>
4 #include <Wire.h>
5 #include <LiquidCrystal_PCF8574.h>
6
7 // LCD access
8 LiquidCrystal_PCF8574 lcd(0x27);
9
10 // HX711 ADC
11 const int HX711_DT = 12;
12 const int HX711_CK = 13;
13 HX711 sensor;
14
15 // Button
16 const int BUTTON = 15;
17
18 // Initialization
19 void setup() {
20 // Init button
21 pinMode (BUTTON, INPUT_PULLUP);
22
23 // Init display
24 Wire.setSDA(16);
25 Wire.setSCL(17);
26 Wire.begin();
27 lcd.begin(16, 2);
28 lcd.setBacklight(255);
29
30 // Initial callibration
31 sensor.begin(HX711_DT, HX711_CK);
32 keyPress("Empty scale");
33 sensor.tare(50);
34 keyPress("Put 1kg");
35 long reading = sensor.read_average(50);
36 sensor.set_scale ((reading - sensor.get_offset())/1.00f);
37 lcd.clear();
38 lcd.print("Scale Ready");
Miscellaneous Sensors 433
39 }
40
41 // Main loop
42 void loop() {
43 // read and show weight
44 float weight = sensor.get_units();
45 char msg[17];
46 dtostrf (weight, 7, 3, msg);
47 strcat (msg, "kg");
48 lcd.setCursor(0,1);
49 lcd.print(msg);
50
51 // wait a time between readings
52 delay (500);
53 }
54
55 // Wait for key press
56 void keyPress(const char *msg) {
57 lcd.clear();
58 lcd.print(msg);
59 lcd.setCursor(0,1);
60 lcd.print("Press button");
61 while (digitalRead(BUTTON) == HIGH) {
62 delay(100);
63 }
64 delay(100); // debounce
65 lcd.setCursor(0,1);
66 lcd.print("Release button");
67 while (digitalRead(BUTTON) == LOW) {
68 delay(100);
69 }
70 delay(100); // debounce
71 lcd.setCursor(0,1);
72 lcd.print("Wait ");
73 }
MicroPython Code
The MicroPython example uses the same PIO code as the C/C++ example.
Miscellaneous Sensors 434
I wrote a small driver for the I2C display, you can download it from the book GitHub
(https://github.com/dquadros/PicoSensors) and place it in the lib directory in the board.
MicroPython Load Cell Example
1 # Load cell Example
2
3 import rp2
4 from rp2 import PIO, asm_pio
5 from machine import Pin, I2C
6 from i2c_lcd import lcd_pcf8574
7 from time import sleep
8
9 # PIO Program
10 @asm_pio(set_init=rp2.PIO.OUT_LOW,autopush=True,
11 fifo_join=PIO.JOIN_RX)
12 def HX711_PIO():
13 # wait a new conversion
14 wait (1,pin,0)
15 wait (0,pin,0)
16
17 # read data bits
18 set (x,23)
19 label('readbit')
20 set (pins, 1) [3]
21 in_ (pins, 1)
22 set (pins, 0) [3]
23 jmp (x_dec, 'readbit')
24
25 # add 1 pulse to set gain to 128
26 set (pins,1) [4]
27 set (pins,0)
28
29 # Set up pins
30 pin_data = Pin(12, Pin.IN)
31 pin_clock = Pin(13, Pin.OUT)
32
33 # Start the PIO
34 sm = rp2.StateMachine(0)
35 sm.init(HX711_PIO,
36 set_base=pin_clock,
Miscellaneous Sensors 435
37 in_base=pin_data,
38 in_shiftdir=PIO.SHIFT_LEFT,
39 freq=5_000_000,
40 push_thresh=24)
41 sm.active(1)
42
43 # Clear PIO FIFO
44 def clearFIFO():
45 while sm.rx_fifo() > 0:
46 sm.get()
47
48 # Read a raw value from HX711
49 def hx711_raw():
50 val = sm.get()
51 if (val & 0x800000) != 0:
52 val -= 0x100000
53 return val
54
55 # Read an average
56 def hx711_avg(count = 10):
57 sum = 0
58 clearFIFO()
59 for _ in range(count):
60 sum += hx711_raw()
61 return sum // count
62
63 # Init button
64 button = Pin (15, Pin.IN, Pin.PULL_UP)
65
66 # Init display
67 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
68 lcd = lcd_pcf8574(i2c)
69 lcd.init()
70 lcd.backlightOn()
71
72 # Wait for a key press
73 def keyPress(msg):
74 lcd.displayClear()
75 lcd.displayWrite(0,0,msg)
Miscellaneous Sensors 436
76 lcd.displayWrite(1,0,'Press button')
77 while button.value():
78 sleep(0.1)
79 sleep(0.1) # debounce
80 lcd.displayWrite(1,0,'Release button')
81 while not button.value():
82 sleep(0.1)
83 sleep(0.1) # debounce
84 lcd.displayWrite(1,0,'Wait ')
85
86 # Initial calibration
87 keyPress('Empty scale')
88 tare = hx711_avg(50)
89 keyPress('Put 1kg')
90 scale = (hx711_avg(50) - tare)/1.0
91
92 # Main loop
93 lcd.displayClear()
94 lcd.displayWrite(0,0,'Scale Ready')
95 while True:
96 weight = (hx711_avg() - tare)/scale
97 lcd.displayWrite(1,0,'{:7.3f}kg'.format(weight))
98 sleep(0.5)
CircuitPython Code
We are going to use an HX711 library that uses the PIO to talk to the HX11. The library is
at https://github.com/fivesixzero/CircuitPython_HX711/tree/main. The easy way to install this
library is using the circup utility:
This assumes that you have Python and pip installed in your computer.
If you prefer to do it manually, you will need to download the files from GitHub and
copy the hx711 directory to the lib directory in your Pico. You will also need to install
in the lib directory the file adafruit_pioasm.mpy from the Adafruit CircuitPython Bundle
(https://circuitpython.org/libraries).
Miscellaneous Sensors 437
iButton
The Dallas Semiconductor (now a part of Maxim) iButton is a rugged auto-ID solution that uses
the 1-wire protocol that we saw in Chapter 7. The iButton itself is in a steel container (MicroCan);
to communicate with the microcontroller it is placed in a special connector.
The iButton has a unique ID (its 1-Wire address) and can also have:
• a non-volatile memory
• a real-time clock
• humidity and temperature sensors
Miscellaneous Sensors 439
A typical use of iButtons is in time and attendance control, mainly to register the time when a
security guard visits a specific location.
We are going to talk here about the simplest model, the Serial Number iButton (DS1990A). This
model has no additional feature and provides only the ID.
The 1-Wire address has 64 bits (8 bytes), but the first byte is the family code (0x01 for the DS1990)
and the last byte is the CRC. The useful part of the ID is 48 bits (6 bytes), which is printed (in
hex) at the top of the iButton.
As the contact between the iButton and the reader can fluctuate, it is important to check the CRC
of the addresses.
iButton Example
In this example, we are going to connect an iButton reader to the Raspberry Pi Pico and send the
ID of the buttons to a PC. The code assumes only one reader is connected (so we will have at
most one device in the OneWire bus).
You will need to connect the Pico to a PC to see the program output.
1 /**
2 * @file ibutton_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief iButton Example
5 * @version 1.0
6 * @date 2023-07-15
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <string.h>
15 #include "pico/stdlib.h"
16 #include "pico-onewire/api/one_wire.h"
17
18 // Pins
19 #define SENSOR_PIN 16
20
21 static One_wire one_wire(SENSOR_PIN);
22
23 #define FAMILY_CODE_DS1990A 0x01
24
25 static rom_address_t sensor;
26
27 // Main Program
28 int main() {
29 // Init stdio
30 stdio_init_all();
31 #ifdef LIB_PICO_STDIO_USB
32 while (!stdio_usb_connected()) {
33 sleep_ms(100);
34 }
Miscellaneous Sensors 441
35 #endif
36
37 // Init OneWire bus
38 one_wire.init();
39
40 // Main loop
41 memset (sensor.rom, 0xFF, sizeof(sensor.rom));
42 while(1) {
43
44 // Check if there is a sensor
45 int count = one_wire.find_and_count_devices_on_bus();
46 if (count > 0) {
47 // Found a sensor
48 auto address = One_wire::get_address(0);
49 if ((address.rom[0] == FAMILY_CODE_DS1990A) &&
50 (memcmp (address.rom, sensor.rom, sizeof(sensor.rom) != 0))) {
51 memcpy (&sensor, &address, sizeof(sensor));
52 printf("ID: %02X%02X%02X%02X%02X%02X\n",
53 address.rom[6], address.rom[5], address.rom[4],
54 address.rom[3], address.rom[2], address.rom[1]);
55 sleep_ms(2000);
56 }
57 } else {
58 memset (sensor.rom, 0xFF, sizeof(sensor.rom));
59 sleep_ms(500);
60 }
61 }
62 }
Arduino Code
I used the OneWireNg library:
Miscellaneous Sensors 442
1 // iButton Example
2 // Requires the OneWireNg library
3
4 #include "OneWireNg_CurrentPlatform.h"
5 #include "utils/Placeholder.h"
6
7
8 // One Wire bus is connected to this pin
9 # define OW_PIN 16
10
11 static Placeholder<OneWireNg_CurrentPlatform> ow;
12
13 static OneWireNg::Id last;
14
Miscellaneous Sensors 443
15 // Initialization
16 void setup() {
17 // Init the serial
18 while (!Serial) {
19 delay(100);
20 }
21 Serial.begin(115200);
22
23 // Instanciate the onewire bus
24 new (&ow) OneWireNg_CurrentPlatform(OW_PIN, false);
25 memset(last, 0xFF, sizeof(OneWireNg::Id));
26 }
27
28 // Main loop
29 void loop() {
30 OneWireNg::Id id;
31
32 ow->searchReset();
33 if ((ow->search(id) == OneWireNg::EC_SUCCESS) &&
34 (id[0] == 0x01) &&
35 (memcmp(id, last, sizeof(OneWireNg::Id)) != 0)) {
36 Serial.print("ID: ");
37 for (size_t i = sizeof(OneWireNg::Id)-2; i > 0; i--) {
38 Serial.print(id[i], HEX);
39 }
40 Serial.println();
41 memcpy(last, id, sizeof(OneWireNg::Id));
42 delay(2000);
43 } else {
44 memset(last, 0xFF, sizeof(OneWireNg::Id));
45 delay (500);
46 }
47 }
MicroPython Code
The rp2 port of MicroPython includes drivers for the OneWire protocol¹².
¹²https://docs.micropython.org/en/latest/rp2/quickref.html#onewire-driver
Miscellaneous Sensors 444
CircuitPython Code
We are going to use the official library from Adafruit:
1. Download the “Bundle for Version 8. x” (or whatever CircuitPython version you are using)
from https://circuitpython.org/libraries.
2. Expand the zip file, the files we want will be under the lib subdirectory.
3. Connect your Pico with CircuitPython installed. A drive called CIRCUITPY will appear on
your computer.
Miscellaneous Sensors 445
4. Copy the directory adafruit_onewire from the expanded bundle to the lib directory in
the CIRCUITPY drive.
1 # iButton Example
2
3 import board
4 from adafruit_onewire.bus import OneWireBus
5 from time import sleep
6
7 # Init OnewWire bus
8 ow = OneWireBus(board.GP16)
9
10 # Main Loop
11 last = None
12 while True:
13 if ow.reset():
14 # Got a presence pulse, scan for devices
15 sensors = ow.scan()
16 # Make sure it is a valid iButton
17 if (len(sensors) > 0) and (sensors[0].rom != last) and \
18 (ow.crc8(sensors[0].rom) == 0) and (sensors[0].family_code == 1):
19 # Extract and print ID
20 last = sensors[0].rom
21 id = ''
22 for i in range (6,0,-1):
23 id = id + '{:02X}'.format(last[i])
24 print('ID = '+id)
25 sleep(2)
26 else:
27 last = None
28 sleep(.5)
Fingerprint Sensors
Fingerprint Sensors combine an optical sensor with a specific processing capacity to capture
fingerprints and use them for biometric identification.
Fingerprint Sensor
The sensors we are going to study have an asynchronous serial interface, over which a small
protocol, with various commands, is implemented.
In this chapter, we will look at the basic operations that can be done with this kind of sensor.
The complete list of functionalities of a specific model (and how to use them) can be seen in the
respective specification (the datasheet).
Fingerprints Basics
The principle behind the use of fingerprints for identification is that there are no two identical
fingerprints. The problem, in practical applications, is that fingerprint images are distorted,
damaged, and incomplete. For this reason, identification cannot be done by a simple image
comparison.
The first step when using a fingerprint sensor is to capture an image of the fingerprint. In the
modules we are going to use, this capture is optical. In other words, the sensor takes a “photo” of
the fingerprint (in quotes, because the point here is not to take a faithful image but to highlight
the fingerprint lines).
The second step is to recognize characteristics (features) in this image. This is a very specific and
computing-heavy process.
Fingerprint Sensors 447
The third step is to codify these features in a template (also called a model). This is a relatively
small set of bytes (when compared to a whole image of the fingerprint). This template can be
saved, transmitted, reloaded, and, most importantly, compared in an efficient way.
A good thing is that there is a standard for templates, meaning that (potentially) they can be used
in pieces of equipment different from the one that created them.
Typically, fingerprint sensors have an internal non-volatile (Flash) memory to store a good
number of fingerprints (the fingerprint library). Each template in this memory has a number
associated with it (the template index).
The functions you will find in a fingerprint sensor will allow you to do:
When I say that two templates are compared, I am not talking about a simple byte by
byte comparison. There are a few algorithms for deciding if two templates correspond
to the same fingerprint.
The first category of fingerprint projects is when you have a fingerprint sensor connected to
an isolated microcontroller (the Raspberry Pi Pico, in our case). In this setup, the templates will
be kept at the sensor. The microcontroller will send the commands to do the above functions,
associate the templates indexes to other information (like a name) and take action when a person
is recognized (or not recognized).
Using the microcontroller to read and write templates from and to the sensor, we can do more
complex applications. Here are two examples:
• Using the fingerprint sensor only to capture and convert fingerprints to templates. The
templates are sent by the microcontroller to another (more powerful) equipment for storage
and comparison. This allows us to support a larger number of fingerprints but requires
software that can efficiently do template comparisons without the sensor.
• Make enrollments in a dedicated station (with a more sophisticated user interface) and
distribute the templates to various other (simpler) equipment that will use fingerprint
recognition to control access or execute functions.
Fingerprint Sensors 448
The details for each command, and its responses, are in the datasheet, here is a summary extracted
from it:
Fingerprint Sensors 449
Sensor Configuration
In the above list, we have two commands (ReadSysPara and SetSysPara) that work with the
sensor’s configuration (System Parameters).
Reading the configuration returns the following information:
All values are stored with the most significant byte first.
The write command allows to change only three parameters. You specify in the packet the
parameter number (1 byte) and its new value (also 1 byte):
Fingerprint Sensors 450
Enrolling a Fingerprint
The process to enroll a fingerprint requires the use of several commands:
1. The GenImg command is used to capture a fingerprint image. The resulting image is kept
in an area inside the sensor (ImageBuffer). As we will see later, we can download this
image to show it on a display.
2. The Img2Tz command converts the image in ImageBuffer into a feature template. The
result can be stored in one of two areas (CharBuffer1 or CharBuffer2) inside the module.
The area used is specified in the command.
3. The GenImg command is used again to read the same fingerprint (you have to remove the
finger from the sensor and place it again).
4. One more time, the Img2Tz command is used, but we place the template in the other area
this time.
5. The RegModel command generates a model from CharBuffer1 and CharBuffer2. The result
is placed on both buffers (two identical copies).
6. Finally, the Store command is used to save CharBuffer1 or CharBuffer2 in the fingerprint
library on the module. The parameters for this command are the buffer’s number (1 or 2)
and the position in the library (positions are numbered from zero).
• GenImg is a little fussy about the finger position when capturing the image.
• Img2Tz rejects the image if is not clear enough or if it cannot find the minimal points for
feature generation.
• RegModel rejects images that are too different (for example, if you use two different fingers
in the image captures).
In an actual system you will need to keep an external record of which library positions are in use
and to whom they are associated
A simplified way to manage the library positions is to store the fingerprints sequentially. A useful
command for this is TempleteNum (sic). It informs how many fingerprints are currently stored.
Fingerprint Sensors 451
This sequential storage is broken when you need to remove a fingerprint from the library
(something important in a real system). There are two commands to remove fingerprints: one
erases a specific position (DeletChar) and the other clears all stored fingerprints (Empty).
If you are not storing the fingerprint sequentially, remember that the TempleteNum command
informs the number of fingerprints stored, independent of position. If you clear a position where
no fingerprint is stored, the value returned by TempleteNum will not change.
Identifying a Fingerprint
There are two ways to check a captured fingerprint against the fingerprint library:
• Compare to a specific fingerprint. This is typically done when the fingerprint is used as
a kind of password after the person is identified (by typing a code or reading a card, for
example).
• Search for the fingerprint against all the content of the library. In this case, we are using
the fingerprint to find out who is the person.
The FP10A has two distinct commands for these operations. The MATCH command compares
the two fingerprints that are in CharBuffer1 and CharBuffer2. The SEARCH command searches a
fingerprint in CharBuffer1 or CharBuffer2 on a portion of the library (defined by a start position
and a count of positions to check).
To use the MATCH command you will need to use the LOAD command. This command places
in CharBuffer1 or CharBuffer2 a fingerprint from the library.
Both commands also return a number (the matching score) that indicates how close the two
fingerprints are (as computed by the algorithm).
The first data transfer command we will look at is the one that sends to the microcontroller the
fingerprint image captured by the sensor (UpImage). The image has a resolution of 256 by 288
pixels and is sent in grayscale (with 4 bits per pixel). In the data transfer, each byte corresponds to
2 pixels (the 4 most significant bits corresponding to the left pixel in the pair), totaling 36 kbytes.
The second data transfer command available is the one that sends to the microcontroller a
template created by the sensor (UpChar). Each template has 1536 bytes.
The last command is the inverse (DownChar). Here it is the microcontroller that sends a template
to the sensor.
To use this example, connect the Raspberry Pi Pico to a PC. At the start of the execution the
program erases all the fingerprints stored in the sensor (you can easily change that).
When you place a finger over the sensor, it will try to capture the fingerprint and search for it in
the sensor’s library:
• The LED will light BLUE whenever the software is trying to capture a fingerprint.
• If something goes wrong in the capture, the LED will blink RED.
• If the fingerprint is in the library, the LED will blink GREEN. The corresponding library
index is sent to the PC.
• If the fingerprint is not in the library, the LED will light BLUE again. Remove the finger
and place it again to enroll it.
– If something goes wrong in the enrollment, the LED will blink RED and a message
is sent to the PC.
– If everything goes well, the LED will blink GREEN, and the library index is sent to
the PC.
As the code is extensive, I will present here only the highlights. The full code is available at github.
1 // Start of packet
2 static const uint16_t START = 0xEF01;
3
4 // Commands
5 static const uint8_t CMD_GENIMG = 0x01;
6 static const uint8_t CMD_IMG2TZ = 0x02;
7 static const uint8_t CMD_MATCH = 0x03;
8 static const uint8_t CMD_SEARCH = 0x04;
9 static const uint8_t CMD_REGMODEL = 0x05;
10 static const uint8_t CMD_STORE = 0x06;
11 static const uint8_t CMD_LOAD = 0x07;
12 static const uint8_t CMD_DELETE = 0x0C;
13 static const uint8_t CMD_EMPTY = 0x0D;
14 static const uint8_t CMD_READSYSPARAM = 0x0F;
15 static const uint8_t CMD_TEMPLATECOUNT = 0x1D;
16
17 // Packet header
18 typedef struct {
19 uint8_t startHi;
20 uint8_t startLo;
21 uint8_t addr3;
22 uint8_t addr2;
23 uint8_t addr1;
24 uint8_t addr0;
25 uint8_t type;
26 uint8_t lenHi;
27 uint8_t lenLo;
28 } HEADER;
29 HEADER header;
30
31 // Commands structures
32 typedef struct {
33 uint8_t cmd;
34 } CMDNOPARAM;
The snippet below is the method used for sending a command packet:
Fingerprint Sensors 455
1 /**
2 * @file fingerprint_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Fingerprint Sensor Example
5 * @version 1.0
6 * @date 2023-08-22
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <math.h>
15 #include "pico/stdlib.h"
16 #include "hardware/uart.h"
17 #include "fpm10a_sdk.h"
18
19 // Sensor connections
20 #define UART_ID uart0
21 #define UART_RX_PIN 17
22 #define UART_TX_PIN 16
23
24 // RGB LED connections
25 #define LED_R_PIN 13
26 #define LED_G_PIN 14
27 #define LED_B_PIN 15
28 #define LED_MASK ((1 << LED_R_PIN) | (1 << LED_G_PIN) | (1 << LED_B_PIN))
29
30 FPM10A *sensor;
31
32 // Read fingerprint and create template
33 void captureFeature(uint8_t numbuf) {
34 while (true) {
35 gpio_put (LED_B_PIN, true);
36 printf ("Place finger on sensor\n");
37 while (!sensor->capture()) {
38 sleep_ms(10);
Fingerprint Sensors 457
39 }
40 gpio_put (LED_B_PIN, false);
41 printf("Image captured\n");
42
43 bool ok = sensor->createFeature(numbuf);
44 printf ("Remove finger from sensor\n");
45 sleep_ms(2000);
46 if (ok) {
47 printf ("Feature created\n");
48 return;
49 }
50 gpio_put (LED_R_PIN, true);
51 printf ("Bad image, try again\n");
52 sleep_ms(1000);
53 gpio_put (LED_R_PIN, false);
54 }
55 }
56
57 // Enroll fingerprint
58 void enroll() {
59 bool first = true;
60 while (true) {
61 if (!first) {
62 captureFeature(1);
63 }
64 captureFeature(2);
65 if (!sensor->createModel()) {
66 gpio_put (LED_R_PIN, true);
67 printf ("Images do not match, try again\n");
68 sleep_ms(1000);
69 gpio_put (LED_R_PIN, false);
70 continue;
71 }
72 int pos = sensor->count();
73 if (sensor->store(1, pos)) {
74 gpio_put (LED_G_PIN, true);
75 printf ("Fingerprint stored at %d\n", pos);
76 sleep_ms(1000);
77 gpio_put (LED_G_PIN, false);
Fingerprint Sensors 458
78 return;
79 }
80 gpio_put (LED_R_PIN, true);
81 printf ("Error %d while saving fingerprint\n", sensor->lastResponse());
82 sleep_ms(1000);
83 gpio_put (LED_R_PIN, false);
84 }
85 }
86
87 // Main Program
88 int main() {
89 // Init stdio
90 stdio_init_all();
91 #ifdef LIB_PICO_STDIO_USB
92 while (!stdio_usb_connected()) {
93 sleep_ms(100);
94 }
95 #endif
96 printf ("\nFingerprint Sensor Example\n\n");
97
98 // Init LED
99 gpio_init_mask (LED_MASK);
100 gpio_set_dir_masked (LED_MASK, LED_MASK);
101 gpio_put_masked (LED_MASK, 0);
102
103 // Init serial interface
104 gpio_set_function(UART_TX_PIN, GPIO_FUNC_UART);
105 gpio_set_function(UART_RX_PIN, GPIO_FUNC_UART);
106
107 // Init sensor
108 sensor = new FPM10A(UART_ID);
109 sensor->begin();
110
111 // Show how many fingerprints can be stores
112 printf ("Checking sensor capacity\n");
113 FPM10A::SYSPARAM sp;
114 if (sensor->leSysParam(&sp)) {
115 printf ("Sensor can store %d fingerprint\n",
116 (sp.libsize[0] << 8) + sp.libsize[1]);
Fingerprint Sensors 459
117 }
118
119 // Clear all stored fingerprints
120 int count = sensor->count();
121 if (count > 0) {
122 printf ("Erasing %d fingerprints\n", count);
123 if (sensor->clear()) {
124 printf ("Success\n");
125 }
126 }
127
128 // Main loop
129 while(1) {
130 printf("\n");
131 captureFeature(1);
132 printf("Searching...\n");
133 int pos = sensor->search(1);
134 if (pos == -1) {
135 printf ("Unknown, let\'s enroll\n");
136 enroll();
137 } else {
138 gpio_put (LED_G_PIN, true);
139 printf ("Fingerprint %d identified\n", pos);
140 sleep_ms(1000);
141 gpio_put (LED_G_PIN, false);
142 }
143 }
144 }
Arduino Code
For the Arduino environment, we are going to use a library from Adafruit. While it was developed
for a different sensor model, the commands we are using are the same for the FPM10A.
You can install the library from the Library Manager in the IDE:
Fingerprint Sensors 460
The main code using this library is very similar to what we use with the C/C++ SDK:
Fingerprint Sensor Example with the Arduino Environment
19 delay(100);
20 Serial.println("\n\nFingerprint Sensor Example\n");
21
22 // Init LED
23 pinMode (LED_R_PIN, OUTPUT); digitalWrite(LED_R_PIN, LOW);
24 pinMode (LED_G_PIN, OUTPUT); digitalWrite(LED_G_PIN, LOW);
25 pinMode (LED_B_PIN, OUTPUT); digitalWrite(LED_B_PIN, LOW);
26
27 // Init sensor
28 Serial1.setRX(17);
29 Serial1.setTX(16);
30 finger.begin(57600);
31
32 // Shows sensor capacity
33 Serial.println("Checking sensor capacity");
34 finger.getParameters();
35 Serial.print("Sensor can store ");
36 Serial.print(finger.capacity);
37 Serial.println(" fingerprints");
38
39 // Clear stored fingerprints
40 finger.getTemplateCount();
41 int count = finger.templateCount;
42 if (count > 0) {
43 Serial.print("Erasing ");
44 Serial.print(count);
45 Serial.println(" fingerprints");
46 if (finger.emptyDatabase() == FINGERPRINT_OK) {
47 Serial.println ("Success");
48 }
49 }
50 }
51
52 // Main Loop
53 void loop() {
54 Serial.println();
55 captureFeature(1);
56 Serial.println("Searching...");
57 uint8_t ret = finger.fingerSearch();
Fingerprint Sensors 462
58 if (ret == FINGERPRINT_NOTFOUND) {
59 Serial.println("Unknown, let\'s enroll");
60 enroll();
61 } else if (ret == FINGERPRINT_OK) {
62 Serial.print("Fingerprint ");
63 Serial.print(finger.fingerID);
64 Serial.println(" identified");
65 } else {
66 Serial.print ("Error ");
67 Serial.println (ret);
68 }
69 }
70
71 // Read fingerprint and create template
72 void captureFeature(uint8_t numbuf) {
73 while (true) {
74 digitalWrite(LED_B_PIN, HIGH);
75 Serial.println ("Place finger on sensor");
76 while (finger.getImage() != FINGERPRINT_OK) {
77 delay(10);
78 }
79 digitalWrite(LED_B_PIN, LOW);
80 Serial.println("Image captured");
81
82 bool ok = finger.image2Tz(numbuf) == FINGERPRINT_OK;
83 Serial.println ("Remove finger from sensor");
84 delay(2000);
85 if (ok) {
86 Serial.println ("Feature created");
87 return;
88 }
89 digitalWrite(LED_R_PIN, HIGH);
90 Serial.println ("Bad image, try again");
91 delay(1000);
92 digitalWrite(LED_R_PIN, LOW);
93 }
94 }
95
96 // Enroll fingerprint
Fingerprint Sensors 463
97 void enroll() {
98 bool first = true;
99 while (true) {
100 if (!first) {
101 captureFeature(1);
102 }
103 captureFeature(2);
104 if (finger.createModel() != FINGERPRINT_OK) {
105 digitalWrite(LED_R_PIN, HIGH);
106 Serial.println ("Images do not match, try again");
107 delay(1000);
108 digitalWrite(LED_R_PIN, LOW);
109 continue;
110 }
111 finger.getTemplateCount();
112 int pos = finger.templateCount;
113 int ret = finger.storeModel(pos);
114 if (ret == FINGERPRINT_OK) {
115 digitalWrite(LED_G_PIN, HIGH);
116 Serial.print ("Fingerprint stored at ");
117 Serial.println (pos);
118 delay(1000);
119 digitalWrite(LED_G_PIN, LOW);
120 return;
121 }
122 digitalWrite(LED_R_PIN, HIGH);
123 Serial.print ("Error ");
124 Serial.print (ret);
125 Serial.println (" while storing fingerprint");
126 delay(1000);
127 digitalWrite(LED_R_PIN, LOW);
128 }
129 }
MicroPython Code
The MicroPython code is broken into two modules, one with the class to interact with the sensor
(fingersensor_mpython.py) e the other with the main program (‘fingerdemo_mpython.py‘).
The interesting parts of the sensor class are the packet sending and receiving methods. These
Fingerprint Sensors 464
methods are used to implement the commands (in the following snippet you can see the command
that erases all fingerprints). The pack receiving was implemented as a simple state machine.
Fingerprint Sensor Communication with MicroPython
1 # Send command to sensor
2 def sendCmd(self, cmd):
3 header = bytearray(9)
4 checksum = bytearray(2)
5 header[0] = 0xEF
6 header[1] = 0x01
7 header[2] = 0xFF
8 header[3] = 0xFF
9 header[4] = 0xFF
10 header[5] = 0xFF
11 header[6] = 0x01
12 header[7] = (len(cmd)+2) >> 8
13 header[8] = (len(cmd)+2) & 0xFF
14 chk = header[6]+header[7]+header[8]
15 for c in cmd:
16 chk += c
17 checksum[0] = (chk >> 8) & 0xFF
18 checksum[1] = chk & 0xFF
19 self.uart.write(header)
20 self.uart.write(cmd)
21 self.uart.write(checksum)
22
23 # Read response from sensor
24 def getResponse(self):
25 pos = 0
26 state = 'S1'
27 ok = False
28 timeout = time()+10
29 while True:
30 rx = self.uart.read(1)
31 if rx == None:
32 if time() > timeout:
33 return False
34 continue
35 c = rx[0]
36 self.bufRx[pos] = c
Fingerprint Sensors 465
37 pos = pos+1
38 if state == 'S1':
39 if c == 0xEF:
40 state = 'S2'
41 else:
42 pos = 0
43 elif state == 'S2':
44 if c == 0x01:
45 state = 'ADDR'
46 else:
47 pos = 0
48 state = 'S1'
49 elif state == 'ADDR':
50 if pos == 6:
51 state = 'TAG'
52 elif state == 'TAG':
53 chk = c
54 state = 'LEN1'
55 elif state == 'LEN1':
56 chk += c
57 dsize = c << 8
58 state = 'LEN2'
59 elif state == 'LEN2':
60 chk += c
61 i = dsize = dsize + c - 2
62 state = 'DATA'
63 elif state == 'DATA':
64 chk = (chk + c) & 0xFFFF
65 i = i -1
66 if i == 0:
67 state = 'CHK1'
68 elif state == 'CHK1':
69 ok = c == (chk >> 8)
70 state = 'CHK2'
71 elif state == 'CHK2':
72 ok = ok and (c == (chk & 0xFF))
73 if ok:
74 self.response = self.bufRx[9]
75 return ok
Fingerprint Sensors 466
76
77 # Clear all stored fingerprints
78 def clear(self):
79 self.sendCmd(bytearray([FingerSensor.CMD_EMPTY]))
80 ok = self.getResponse()
81 return ok and self.response == FingerSensor.RESP_OK
Below is the full main program. The captureFeature() and enroll routines send the necessary
commands to capture a feature and enroll a fingerprint.
Fingerprint Sensor Example with MicroPython
29 ledB.off()
30 print ('Image captured')
31 ok = finger.createFeature(numbuf)
32 print ('Remove finger from sensor')
33 sleep(3)
34 if ok:
35 print('Feature created')
36 return
37 ledR.on()
38 print ('Bad image, try again')
39 sleep(1)
40
41 # Enroll fingerprint
42 def enroll():
43 first = True
44 while True:
45 if not first:
46 captureFeature(1)
47 captureFeature(2)
48 if not finger.createModel():
49 ledR.on()
50 first = False
51 print ('Images do not match, try again')
52 sleep(1)
53 ledR.off()
54 continue
55 pos = finger.count()
56 if finger.store(1,pos):
57 ledG.on()
58 print ('Fingerprint stored at {}'.format(pos))
59 sleep(1)
60 ledG.off()
61 return
62 else:
63 ledR.on()
64 print ('Error {} while storing fingerprint'.format(finger.lastResponse()\
65 ))
66 sleep(1)
67 ledR.off()
Fingerprint Sensors 468
68
69 # Main Loop
70 while True:
71 print()
72 captureFeature(1)
73 print('Searching...')
74 pos = finger.search(1)
75 if pos == -1:
76 print ('Unknown, lets enroll')
77 enroll()
78 else:
79 ledG.on()
80 print ('Fingerprint {} identified'.format(pos))
81 sleep(1)
82 ledG.off()
CircuitPython Code
The CircuitPython code is a straightforward adaptation of the MicroPython code:
• In the sensor class, the serial port initiation was removed, as it can only be done at the
UART object creation. Nothing else had to be changed.
• In the main program, I had to change the imports, the UART object creation, and the
handling of the digital outputs connected to the LED.
12 ledR.direction = digitalio.Direction.OUTPUT
13 ledG.direction = digitalio.Direction.OUTPUT
14 ledB.direction = digitalio.Direction.OUTPUT
15 ledR.value = False
16 ledG.value = False
17 ledB.value = False
18 uart = UART(tx=board.GP16, rx=board.GP17, baudrate=9600*6, bits=8, parity=None, stop\
19 =1)
20 finger = FingerSensor(uart)
21 sleep(0.1)
22 c = finger.count()
23 if c > 0:
24 print ('Erasing {} fingerprints'.format(c))
25 if finger.clear():
26 print ('Success')
27
28 # Read fingerprint and create template
29 def captureFeature(numbuf):
30 while True:
31 ledB.value = True
32 print ('Place finger on sensor')
33 while not finger.capture():
34 sleep(0.05)
35 ledB.value = False
36 print ('Image captured')
37 ok = finger.createFeature(numbuf)
38 print ('Remove finger from sensor')
39 sleep(3)
40 if ok:
41 print('Feature created')
42 return
43 ledR.value = True
44 print ('Bad image, try again')
45 sleep(1)
46 ledR.value = False
47
48 # Enroll fingerprint
49 def enroll():
50 first = True
Fingerprint Sensors 470
51 while True:
52 if not first:
53 captureFeature(1)
54 captureFeature(2)
55 if not finger.createModel():
56 ledR.value = True
57 first = False
58 print ('Images do not match, try again')
59 sleep(1)
60 ledR.value = False
61 continue
62 pos = finger.count()
63 if finger.store(1,pos):
64 ledG.value = True
65 print ('Fingerprint stored at {}'.format(pos))
66 sleep(1)
67 ledG.value = False
68 return
69 else:
70 ledR.value = True
71 print ('Error {} while storing fingerprint'.format(finger.lastResponse()\
72 ))
73 sleep(1)
74 ledR.value = False
75
76 # Main Loop
77 while True:
78 print()
79 captureFeature(1)
80 print('Searching...')
81 pos = finger.search(1)
82 if pos == -1:
83 print ('Unknown, lets enroll')
84 enroll()
85 else:
86 ledG.value = True
87 print ('Fingerprint {} identified'.format(pos))
88 sleep(1)
89 ledG.value = False
RFID
RFID is about reading an identification (ID) by means of radio frequency communication.
In an RFID system, the identification is in what we call a tag. In a tag, we normally have an
integrated circuit that includes the radio, a processor to implement the protocol, and a memory
where the id is stored. In not very precise terms, we say a tag is passive if it does not have an
internal power source. Passive tags must use the energy in the reader transmissions to do their
transmissions. This limits the processing power and communication range of this kind of tag.
Active tags have a battery or other kind of power source.
In the simplest form, a tag has a fixed identification, programmed at the factory. More sophisti-
cated tags have greater memories that can be read and written through RF. Many frequencies and
protocols are used to implement RFID. In the more simple ones, it is assumed that there is only
one tag in the reader range. More sophisticated systems allow the simultaneous presence and
identification of multiple tags. This requires mechanisms to avoid and recover collisions (when
two or more tags transmit at the same time, garbling the transmitted data).
The objective of this chapter is to give a short introduction to RFID, describing two very popular
standards.
RFID 125kHz
The first read and tags we are going to examine are very simple. The frequency of operation is
125 kHz, the range is a few centimeters and only one tag should be in range at a time.
The tags, that you will find in the form of cards, badges, keychains, and bracelets, have a fixed
32-bit identification and are implemented by an EM4100 chip (or equivalent).
RFID 472
The reader I am using is the RDM6300. You will find a few similar readers but with small
differences in the format of the data. If in doubt, check your reader’s documentation.
The communication between the RSM6300 and a microcontroller is asynchronous serial, TTL
level (0 corresponds to 0V and 1 to 5V), using the 8N1 format (8 data bits, no parity, one stop bit)
at 9600 bps. The communication is one-way from the reader to the microcontroller.
The reader will send a message with the tag ID as long as the tag is in range. The message format
is as follows:
• One byte containing 0x02 (STX), indicating the start of the message.
• Ten hexadecimal digits, corresponding to the tags’s ID. The first two digits should be
ignored.
RFID 473
• Two hexadecimal digits containing a checksum. This checksum is the exclusive or (XOR)
of the five bytes in the ID.
• One byte containing 0x03 (ETX), indicating the end of the message.
In the photo at the beginning of this section, you can see that some cards I used for testing have a
printed marking, with two parts. The first part is a 10-digit number and the second part contains
two numbers, one with 3 digits and the other with 5.
The 32-bit ID can be expressed as an 8-digit hexadecimal number or a 10-digit decimal number
from 0 to 4.294.967.295.
As the reader works at 5V, we are using a resistive divisor to adapt the signal to the 3.3V Pico.
The reader antenna is not shown in this figure. As can be seen in the previous photo, the antenna
consists of a few turns of enameled copper wire and is connected to the reader by a two-pin
connector.
The operation of the example is simple:
• To register a tag as “authorized”, press and release the button and then present the tag to
the reader. Successful registration of the tag is indicated by a blink of the Pico’s LED. In the
SDK and Arduino versions, up to 10 tags can be registered as “authorized”. In the Python
versions, there is no limit.
• If a tag is presented without previously pressing the button, its ID is searched in the
“authorized” list. If it is in it, the servomotor moves (as if opening a gate). If not, the buzzer
emits a beep.
In the Pico W the LED is not connected to the RP2040, so this code won’t work on it.
You can add an LED (in series with a 1k Ohm resistor) between one of the Pico W pins
and ground and change the LED pin number in the code (or you can simply remove
the code that used the LED).
A servomotor is controlled by a PWM (Pulse Width Modulation) signal. Each cycle takes 20 ms
(corresponding to a frequency of 50Hz). The time the signal stays high in each cycle controls the
motor axle position.
Because there are many parts in this project, the code is a little long. I suggest you study first the
MicroPython version, as it is the more compact. The other versions were adapted from it, so all
versions have the same structure.
MicroPython Code
RFID 475
78 c = rx[0]
79 if self.pos==0 and c!=0x02:
80 return None
81 self.bufRx[self.pos] = c
82 self.pos = self.pos+1
83 if self.pos == 14:
84 self.pos = 0
85 if c == 0x03:
86 # got full message
87 self.last_read = time_ms()
88 crc = 0
89 for i in range(1,13,2):
90 x = self.bufRx[i:i+2].decode()
91 crc = crc ^ int(x,16)
92 if crc == 0:
93 tag = self.bufRx[3:11].decode()
94 if tag != self.last:
95 self.last = tag
96 return tag
97 return None
98
99 # Create objects
100 led = Pin(25, Pin.OUT)
101 buzzer = Buzzer(2)
102 button = Button(16)
103 servo = Servo(17)
104 rfid = RFID(0,13)
105
106 #
107 led.on()
108 buzzer.beep()
109 servo.pos(0)
110 led.off()
111
112 # Main Loop
113 autorized = set()
114 cadastro = False
115 closeTime = None
116 while True:
RFID 478
I wrote classes for the various parts, The Button class is an adaptation of the one we saw back
in Chapter 5. The program’s basic architecture is to have a main loop that continuously calls the
methods that treat the button and the RFID reader.
The main method in the RFID read class is read(). It may not show at first look, but this is a
state machine, where pos (the position in the buffer where the next received byte will be stored(
is the state:
When checking the CRC, instead of calculating the CRC over the data and comparing the result
with the received CRC, we just calculate the CRC over the data and CRC and test the final result
for zero. This works because the XOR of a number with itself results in zero.
Once we get a correct message, the tag ID is compared with the last ID received. If they are the
same, the tag is ignored. After one second with no received message the last ID is cleared, so you
can read the same tag again but move it out of range and then present it again to the reader.
63 }
64 }
65 }
66 return false;
67 }
68
69 };
1 // Main Program
2 int main() {
3 // Init stdio
4 stdio_init_all();
5 #ifdef LIB_PICO_STDIO_USB
6 while (!stdio_usb_connected()) {
7 sleep_ms(100);
8 }
9 #endif
10 printf ("\n125kHz RFID Example\n\n");
11
12 // Init LED
13 gpio_init (LED_PIN);
14 gpio_set_dir (LED_PIN, true);
15 gpio_put (LED_PIN, false);
16
17 // Create objects
18 Buzzer buzzer(BUZZER_PIN);
19 Button button(BUTTON_PIN);
20 Servo servo(SERVO_PIN);
21 RFID rfid(UART_ID, UART_RX_PIN);
22
23 // Blink LED, "close door" and beep
24 gpio_put (LED_PIN, true);
25 buzzer.beep();
26 servo.pos(0);
27 gpio_put (LED_PIN, false);
28
RFID 482
29 // Main Loop
30 bool enroll = false;
31 uint32_t closeTime = 0;
32 char tag[9];
33 while(1) {
34 if ((closeTime != 0) && (board_millis() > closeTime)) {
35 servo.pos(0);
36 closeTime = 0;
37 }
38 if (button.released()) {
39 enroll = true;
40 }
41 if (rfid.read(tag)) {
42 if (enroll) {
43 if (isAuthorized(tag)) {
44 printf ("Tag %s already authorized\n", tag);
45 } else if (nAuthorized == MAX_TAGS) {
46 printf ("Authorized tag list is full\n");
47 } else {
48 gpio_put (LED_PIN, true);
49 strcpy (autorized[nAuthorized++], tag);
50 printf ("Tag %s authorized\n", tag);
51 sleep_ms(300);
52 gpio_put (LED_PIN, false);
53 }
54 enroll = false;
55 }
56 else if (isAuthorized(tag)) {
57 printf ("Tag %s authorized\n", tag);
58 servo.pos(180); // "open door"
59 closeTime = board_millis()+3000;
60 } else {
61 printf ("Tag %s NOT authorized\n", tag);
62 buzzer.beep();
63 }
64 }
65 }
66 }
RFID 483
34 pressed = false;
35 this->debounce = debounce;
36 last = digitalRead(pinButton) == LOW;
37 lastTime = millis();
38 }
39
40 // Tests if a button was pressed and released
41 bool released() {
42 bool val = digitalRead(pinButton) == LOW;
43 if (val != last) {
44 // reading changed
45 last = val;
46 lastTime = millis();
47 } else if (val != pressed) {
48 int dif = millis() - lastTime;
49 if (dif > debounce) {
50 // updates button state
51 pressed = val;
52 return !pressed;
53 }
54 }
55 return false;
56 }
57 };
58
59 // Defines UART to use (Serial1 or Serial2)
60 #define SERIAL_RFID Serial1
61
62 // Class to get RFID reader messages
63 class RFID {
64 private:
65 byte last[9];
66 uint32_t last_read;
67 byte bufRx[14];
68 int pos;
69
70 inline byte decodHex(byte c) {
71 if ((c >= '0') && (c <= '9')) {
72 return c - '0';
RFID 485
73 }
74 if ((c >= 'A') && (c <= 'F')) {
75 return c - 'A' + 10;
76 }
77 return 0;
78 }
79
80 public:
81 RFID(int pinRx, int pinTx) {
82 SERIAL_RFID.setRX(pinRx);
83 SERIAL_RFID.setTX(pinTx);
84 SERIAL_RFID.begin(9600, SERIAL_8N1);
85 pos = 0;
86 last[0] = 0;
87 last_read = 0;
88 }
89
90 bool read(char *tag) {
91 if (SERIAL_RFID.available() == 0) {
92 uint32_t ellapsed = millis() - last_read;
93 if ((last[0] != 0) && (ellapsed > 1000)) {
94 // Long time without messages, forget last tag
95 last[0] = 0;
96 }
97 return false;
98 }
99 int c = SERIAL_RFID.read();
100 if (c < 0) {
101 return false;
102 }
103 if ((pos == 0) && (c != 0x02)) {
104 return false;
105 }
106 bufRx[pos++] = (byte) c;
107 if (pos == 14) {
108 pos = 0;
109 if (c == 0x03) {
110 last_read = millis();
111 byte crc = 0;
RFID 486
112 byte x;
113 for (int i = 1; i < 13; i = i+2) {
114 x = (decodHex(bufRx[i]) << 4) + decodHex(bufRx[i+1]);
115 crc ^= x;
116 }
117 if (crc == 0) {
118 if (memcmp (bufRx+3, last, 8) != 0) {
119 memcpy(last, bufRx+3, 8);
120 last[8] = 0;
121 strcpy(tag, (const char *)last);
122 return true;
123 }
124 }
125 }
126 }
127 return false;
128 }
129
130 };
131
132 // Declare and create objects
133 Buzzer buzzer(2);
134 Button button(16);
135 Servo servo;
136 RFID *rfid;
137
138 #define MAX_TAGS 10
139 char autorized[MAX_TAGS][9];
140 int nAuthorized = 0;
141 bool enroll = false;
142 uint32_t closeTime = 0;
143
144 // Init
145 void setup() {
146 while (!Serial) {
147 delay(100);
148 }
149 Serial.println("125kHz RFID Example\n");
150 rfid = new RFID(13, 12);
RFID 487
151 servo.attach(17);
152 pinMode(LED_BUILTIN, OUTPUT);
153 digitalWrite(LED_BUILTIN, HIGH);
154 buzzer.beep();
155 servo.write(0);
156 digitalWrite(LED_BUILTIN, LOW);
157 }
158
159 // Main Loop
160 void loop() {
161 char tag[9];
162
163 if ((closeTime != 0) && (millis() > closeTime)) {
164 servo.write(0);
165 closeTime = 0;
166 }
167 if (button.released()) {
168 enroll = true;
169 }
170 if (rfid->read(tag)) {
171 if (enroll) {
172 if (isAuthorized(tag)) {
173 Serial.print ("Tag ");
174 Serial.print (tag);
175 Serial.println (" already authorized");
176 } else if (nAuthorized == MAX_TAGS) {
177 Serial.println ("Authorized tags list is full");
178 } else {
179 digitalWrite(LED_BUILTIN, HIGH);
180 strcpy (autorized[nAuthorized++], tag);
181 Serial.print ("Tag ");
182 Serial.print (tag);
183 Serial.println (" authorized");
184 delay(300);
185 digitalWrite(LED_BUILTIN, LOW);
186 }
187 enroll = false;
188 }
189 else if (isAuthorized(tag)) {
RFID 488
The observations on the MicroPython version also apply here. Notice that the code is identical to
the SDK version, except for the functions that directly interact with the hardware.
A small detail is that the RFID object is created in setup() while the other objects are declared
globally and are initialized before setup() runs. The problem here is that the Serial methods
cannot be called before setup(). An alternative would be to move these calls to a new begin
method and call it in setup(), like what is done in the Servo class.
78 return None
79 c = rx[0]
80 if self.pos==0 and c!=0x02:
81 return None
82 self.bufRx[self.pos] = c
83 self.pos = self.pos+1
84 if self.pos == 14:
85 self.pos = 0
86 if c == 0x03:
87 # got full message
88 self.last_read = time_ms()
89 crc = 0
90 for i in range(1,13,2):
91 x = self.bufRx[i:i+2].decode()
92 crc = crc ^ int(x,16)
93 if crc == 0:
94 tag = self.bufRx[3:11].decode()
95 if tag != self.last:
96 self.last = tag
97 return tag
98 return None
99
100 # Create objects
101 led = digitalio.DigitalInOut(25)
102 led.direction = digitalio.Direction.OUTPUT
103 buzzer = Buzzer(board.GP2)
104 button = Button(board.GP16)
105 servo = Servo(board.GP17)
106 rfid = RFID(board.GP13)
107
108 # Blink LED, "close door" and beep
109 led.value = True
110 buzzer.beep()
111 servo.pos(0)
112 led.value = False
113
114 # Main Loop
115 autorized = set()
116 enroll = False
RFID 492
Like the 125 kHz RFID, you will find MIFARE tags in the form of cards, badges, keychains, and
bracelets.
There are many models with diverse specifications, I will focus here on the MIFARE Classic with
a 1 kbyte memory; the concepts presented apply to the other models.
NFC (Near Field Communication) is a radio communication standard for short distances (up
to 10 cm). Not by coincidence, this standard is the same used by MIFARE cards and tags
communication.
We will look here at two readers that use NXP chips. The first uses the MRFC422 (which is specific
to MIFARE) and the second the PN532 (which is an NFC controller).
This subject (MIFARE and NFC) is very complex and there are whole books devoted exclusively
to this. Here I am going to present just some basic notions about MIFARE tags.
Memory Addressing
The memory available in the tag is divided into sectors, numbered from zero. Each sector can be
protected independently from the others. A sector is divided into 16 byte blocks. The last sector
in each block is used to configure the sector’s access.
The first block in the first sector is the manufacturer’s block and cannot be changed. It contains
the tag’s unique identification.
In the case of MIFARE Classic 1k, we have 16 sectors, each one with 4 blocks. Checking up, 16 x 4 x
16 = 1024 bytes. Notice, however, that we have only 15 x 3 x 16 = 752 bytes for data stored (and the
first block in the tag cannot be changed). The rest of the bytes are used for access configuration.
(To be more precise, four bytes in the last block in each sector can be eventually used for data,
but I don’t recommend it).
Besides reading and writing blocks, the MIFARE tags have safe commands for incrementing
and decrementing values stored in a special format (described ahead). They are essential for
applications where the tag is used for ticketing.
To make it more confusing, the access configuration has four parts, with 3 bits each. For greater
security, these bits are stored twice: the normal value and the inverted value.
In a 1k card, each part directly controls the access of one block (including controlling access to
the block that controls access). The three bits in each part define what key (A or B) is needed
for reading and writing the block and are interpreted differently for the data and configuration
blocks:
RFID 495
Before any reading or writing operation, an authentication is needed, using one of the keys. A
basic rule is that a key that can be read cannot be used for authentication. Key A cannot be ever
read, but you can allow Key B reading (first three lines in the first table above); in this case key
B is now regular data, not an access key.
In every access, the tag will check if the configuration block is valid. If not, the whole
sector is definitively disabled (you lost it forever).
New tags came configured for reading and writing the data blocks with either key. But the
configuration block can only be read or written with the A key and the B key can be read. This
means that only key A can be used for authentication; to use the tag the manufacturer must
inform it (a typical value is FFFFFFFFFFFF).
RFID 496
Great care must be taken when configuring access to the blocks, as you can prohibit
all future reading and writing. As key A cannot be read back, you must be sure you
know what you are writing. It is very easy to write a bad configuration block and lose
access to a sector!
Ticketing
In a ticketing or electronic wallet application, the card (tag) is loaded with some “credits” that
are later consumed in exchange for goods or services.
Looking again at the access control table for data blocks, notice that there are two possible uses
for a block: data or value. While you could implement ticketing with the data mode, the value
mode exists to safely implement the load and consume operations.
When you select the data mode, the card will not care what you are writing in the block. To use
the value mode, the content of the block must be in a specific format:
In this figure, value is the balance in the card; for greater security, it is stored thrice in the block
(inverted in one case). The value is an unsigned four-byte signed number (in two’s complement
format); that is from -2.147.483.648 to 2.147.483.647.
The adr field can be used by the application to store the address of another block that will store a
copy of the value (as a backup in case of failure). Even if you don’t use adr the four corresponding
bytes must be in the correct format (for example, 0x00, 0xFF, 0x00, 0xFF).
Once a block is configured for value mode, its access is conditioned to it having the correct format.
Four basic operations can be done with a value block:
• increment, where a “credit” is added to the value; the result is stored in a temporary register
in the card.
• decrement, where a “debit” is decremented from the value; the result is stored in a
temporary register in the card.
• restore, where the current value is copied to the temporary register (overwriting any
increment or decrement results in it)
• transfer, where the temporary register is written to the block.
RFID 497
A “credits load” is done by combining an increment with a transfer and a “credits use” is done by
combining a decrement with a transfer.
Looking one last time at the access control table for data blocks, notice that in mode “001” any
key can be used for decrement, but there is no way to do an increment (this is useful for prepaid
disposable cards). In mode “110”, either key can be used for a decrement, but only the B key can
be used for an increment (the many points where credit is used know only key A; key B is used
only the few points where credit is added).
MFRC522 Reader
RFID-RC422 Reader
The MFRC522 integrated circuit (IC) supports SPI, I²C, and asynchronous serial (UART) commu-
nication, but the board I am using only supports SPI (MFRC522 pins IIC and EA are connected
to GND and VCC, selecting SPI). The maximum clock for SPI is 10 Mbps. The signal marked as
SDA in the connector is the SS SPI signal.
The power supply for the IC must be between 2.5 and 3.6V. As this board does not have a regulator,
power must be in this range (3.3V is normally used). Communication signals voltages follow the
power supply. The datasheet says you should not apply a voltage above Vcc in any input pin
(MOSI, SCK, SDA, and RST). In practice, you will find examples with a direct connection of a 5V
microcontroller (like the Arduino UNO) to this board. The board has a pull-up resistor for the
RST (reset) signal.
Most space on the board is occupied by copper trails that work as an antenna.
RFID 498
PN532 Reader
Leitor PN532
The PN532 IC supports SPI, I²C, and asynchronous serial (UART) communication. In the board
above, the communication mode is selected by two dip-switches:
The power supply for the IC must be between 2.5 and 3.6V, but this board is a regulator and must
be powered by 5V. Input signals have converts or protection to allow the direct connection of 5V
signals.
The antenna is at the board’s edge, the manufacturer recommends that connecting wires cross
the antenna at a right angle to minimize interference.
The PN52 IC has many more features than the MFRC522, but it costs more.
MIFARE Examples
Software drivers for MFRC522 and PN532 are a lot more complex than anything we have seen so
far in this book, so I am using available libraries. The details of these libraries’ implementation
are beyond this introductory text.
If you are curious, the datasheet of the ICs are easily found on the Internet and the
libraries I used are open source so you can check what they are doing.
Unlike the other chapters, I won’t be showing the same example for all programming environ-
ments.
RFID 499
Download and expand the zip file, then use Thonny to install the file mfrc522.py into the lib
directory in the Raspberry Pi Pico.
The following figure shows the connection of the reader to the Pi Pico:
RFID 500
You will need to connect the Raspberry Pi Pico to a PC to interact with the program. The program
accepts two commands:
After receiving a valid command, the program waits for a card to be presented to the reader and
then executes the command. It is assumed that the card has the factory configuration, allowing
access with the default key FFFFFFFFFFFF. The program writes a special mark in block 0, and
the message in block 1.
MRFC522 Reader Example in MicroPython
9 # Connections
10 pinSCK = 18
11 pinMISO = 16
12 pinMOSI = 19
13 pinCS = 17
14 pinRST = 20
15
16 # Card key and App mark
17 cardKey = [ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ] # factory default
18 mark = [ 0x00, 0x11, 0x22, 0x33, 0x44, 0x66, 0x66, 0x77,
19 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]
20
21 # Inicia MFRC522
22 card = MFRC522(spi_id=0,sck=pinSCK,miso=pinMISO,mosi=pinMOSI,
23 cs=pinCS,rst=pinRST)
24 card.init()
25
26 # Wait for a card in range
27 def waitForCard():
28 print ('Present a card')
29 while True:
30 utime.sleep_ms(50)
31 (stat, tag_type) = card.request(card.REQIDL)
32 if stat == card.OK:
33 (stat, uid) = card.SelectTagSN()
34 if stat == card.OK:
35 print("Card ID: {}".format(
36 hex(int.from_bytes(bytes(uid),"little",False)).upper()))
37 return uid
38
39 # Write on card
40 def writeToCard(param):
41 uid = waitForCard()
42 sector = int(param[1])
43 msg = bytes((param[2]+16*' '),'utf-8')[0:16]
44 stat = card.writeSectorBlock(uid, sector, 0, mark, cardKey)
45 if stat != card.OK:
46 print ('Write error!')
47 else:
RFID 502
The ideas in this program can be extended for the storage and retrieval of more complex
information.
After initializing, the card will have a sector with a non-default configuration. The
program has an option to restore factory settings.
In this example, we are using the MFRC522 library by “GithubCommunity”. This library supports
the MIFARE value manipulation commands, but (in the version available as I am writing this
book) has an error that impedes its compilation in the Arduino IDE. Fortunately, this error is
easily corrected.
The first step is to install the library, using the Library Manager in the IDE:
Next, try to compile our example. If you have the same version as me, you will the error ordered
comparison of pointer with integer zero in the MFRC522Extended.cpp file. Check in the
error message where this file is and change the two lines containing
RFID 504
to
28 void setup() {
29 while (!Serial) {
30 delay(100);
31 }
32
33 // Init serial port
34 Serial.begin (115200);
35
36 // Init RFID reader
37 SPI.begin();
38 mfrc522.PCD_Init();
39
40 // Init card keys
41 for (byte i = 0; i < 6; i++) {
42 defaultKey.keyByte[i] = 0xFF;
43 }
44 appKeyA.keyByte[0] = 'S'; appKeyA.keyByte[1] = 'e'; appKeyA.keyByte[2] = 'c';
45 appKeyA.keyByte[3] = 'r'; appKeyA.keyByte[4] = 'e'; appKeyA.keyByte[5] = 't';
46 appKeyB.keyByte[0] = '1'; appKeyB.keyByte[1] = '2'; appKeyB.keyByte[2] = '3';
47 appKeyB.keyByte[3] = '4'; appKeyB.keyByte[4] = '5'; appKeyB.keyByte[5] = '6';
48 }
49
50 // Main loop
51 void loop() {
52 // Request operation
53 Serial.print ("(I)nit card, (D)ebit, or (R)estore factory settings?");
54 int c;
55 while (Serial.read() != -1) {
56 delay(10);
57 }
58 do {
59 c = Serial.read();
60 c = toupper(c);
61 } while (strchr("IDR", c) == NULL);
62 char cmd[2] = "";
63 cmd[0] = (char) c;
64 Serial.println (cmd);
65
66 // Asks to present a card to the reader
RFID 506
67 if (askForCard()) {
68 // Execute operation
69 if (c == 'I') {
70 initCard();
71 } else if (c == 'D') {
72 useCard();
73 } else if (c == 'R') {
74 resetCard();
75 }
76 mfrc522.PICC_HaltA();
77 mfrc522.PCD_StopCrypto1();
78 }
79 Serial.println();
80 }
81
82 // Prepare card for use
83 void initCard() {
84 MFRC522::StatusCode status;
85
86 // Authenticate with the B key
87 status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_B, CFG_BLK,
88 (cardKey == FACTORY)? &defaultKey : &appKeyB, &(mfrc522.uid));
89 if (status != MFRC522::STATUS_OK) {
90 Serial.println ("Authentication error!");
91 return;
92 }
93
94 // Write balance sector with the proper format
95 if (!setupValue(VALUE_BLK)) {
96 return;
97 }
98
99 // Reconfigure the sector to use our keys and the needed access conditions
100 // 1st block 000 standard config, can access with A or B keys
101 // 2nd block 110 value, read/decrement with A or B key, write/decrement with B key\
102 only
103 // 3rd block 000 standard config, can access with A or B keys
104 // 4rd block 011 writ with B key only, key A can only read access bits
105 byte cfgBuffer[16];
RFID 507
184 }
185
186 // Shows current balance
187 void showBalance() {
188 MFRC522::StatusCode status;
189
190 if (cardKey != OUR_APP) {
191 Serial.println ("Card not initialized!");
192 return;
193 }
194
195 // Authenticates with A key
196 status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, CFG_BLK,
197 &appKeyA, &(mfrc522.uid));
198 if (status != MFRC522::STATUS_OK) {
199 Serial.println ("Authentication error!");
200 return;
201 }
202
203 // Read value
204 int32_t value;
205 status = mfrc522.MIFARE_GetValue(VALUE_BLK, &value);
206 if (status != MFRC522::STATUS_OK) {
207 Serial.println ("Read Error!");
208 return;
209 }
210 Serial.print ("Balance: ");
211 Serial.println (value);
212 }
213
214 // Reset card to factory settings
215 void resetCard() {
216 MFRC522::StatusCode status;
217
218 // Authenticates with B key
219 status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_B, CFG_BLK,
220 (cardKey == FACTORY)? &defaultKey : &appKeyB, &(mfrc522.uid));
221 if (status != MFRC522::STATUS_OK) {
222 Serial.println ("Authentication error!");
RFID 510
223 return;
224 }
225
226 // Reconfigura o setor para a configuração padrão
227 byte cfgBuffer[16];
228 memset (cfgBuffer, 0, 16);
229 memcpy (cfgBuffer, &defaultKey, 6);
230 memcpy (cfgBuffer+10, &defaultKey, 6);
231 mfrc522.MIFARE_SetAccessBits(&cfgBuffer[6], 0, 0, 0, 1);
232 status = mfrc522.MIFARE_Write(CFG_BLK, cfgBuffer, 16);
233 if (status != MFRC522::STATUS_OK) {
234 Serial.println ("Error while configuring!");
235 }
236 Serial.println ("Card reset to factory settings");
237 }
238
239 // Wait for a card or a key
240 bool askForCard() {
241 while (Serial.read() != -1)
242 ;
243 Serial.println ("Present a card (or press a key to cancel)");
244 while (true) {
245 if (Serial.read() != -1) {
246 return false;
247 }
248 if (!mfrc522.PICC_IsNewCardPresent())
249 continue;
250 if (mfrc522.PICC_ReadCardSerial())
251 break;
252 }
253
254 Serial.print ("ID:");
255 for (byte i = 0; i < mfrc522.uid.size; i++) {
256 Serial.print(mfrc522.uid.uidByte[i] < 0x10 ? " 0" : " ");
257 Serial.print(mfrc522.uid.uidByte[i], HEX);
258 }
259 Serial.println();
260
261 Serial.print(F("Card type: "));
RFID 511
The application offers three options, selecting by typing one letter in the PC:
• I: Initializes the card, configuring the sector and writing the initial balance.
• D: executes a debit operation, charging a fixed amount from the balance. A message is
shown if the balance is not enough.
• R: restores the manufacturer’s key in the sector.
Experiment with changing the program to include a “recharge” option, where a fixed amount of
credits are added to the balance.
This assumes that you have Python and pip installed on your computer. To do a manual
installation, you need to download the “Bundle for Version 8.x” (or whatever CircuitPython
version you are using) from https://circuitpython.org/libraries, expand the zi file e copy to the lib
directory in the Pico the adafruit_pn532 and adafruit_bus_device subdirectories.
The library documentation is at https://docs.circuitpython.org/projects/pn532/en/latest/api.html.
The library supports connection through SPI, I²C, and UART. We are using I²C (don’t forget to
set the dip switches on the board).
The following figure shows the assembly:
39 y) and \
40 pn532.mifare_classic_write_block(block, mark) and \
41 pn532.mifare_classic_write_block(block+1, msg):
42 print ('Written.')
43 else:
44 print ('Write error!')
45
46 # Read from card
47 def readFromCard(param):
48 uid = waitForCard()
49 block = int(param[1])*4
50 if pn532.mifare_classic_authenticate_block(uid, block, MIFARE_CMD_AUTH_A, cardKe\
51 y):
52 data = pn532.mifare_classic_read_block(block)
53 if data == mark:
54 msg = pn532.mifare_classic_read_block(block+1)
55 if msg != None:
56 print ('Msg: '+msg.decode('utf-8'))
57 return
58 elif data != None:
59 print ('No message on sector!')
60 return
61 print ('Read error!')
62
63 # Laco Principal
64 cmdRead = "^([lL]) ([1-9])$"
65 cmdWrite = "^([gG]) ([1-9]) (.+)$"
66 while True:
67 # Read command
68 print()
69 cmd = input("Command? ")
70 m = re.search(cmdRead, cmd)
71 if m != None:
72 readFromCard(m.groups())
73 else:
74 m = re.search(cmdWrite, cmd)
75 if m != None:
76 writeToCard(m.groups())
77 else:
RFID 516
• Find the manufacturer’s specifications (datasheet). While a datasheet text may not always
be an easy read, it is (generally) the most complete and precise reference.
• If you are going to use a sensor that is already mounted on a board, look for its schematic.
Important points to check are the presence of pull-up and pull-down resistors, voltage
regulators and level converters, and the pinout of the connectors.
• Check what kind of interface the sensor uses. If it is an asynchronous serial, check the
baud rate and data format. If it is SPI, check the maximum clock speed and the polarity
of the selection signal. If it is I²C, check the address and the maximum speed. Proprietary
digital protocols are more complex, examine carefully the diagrams that show the signal
transitions, checking the timings and their tolerances.
• When using a complex sensor that has internal registers, a register map and a detailed
description of each bit in the registers is essential.
Conclusion 518
What’s Next?
Nothing better to settle what you’ve learned than using it in practice. The Raspberry Pi Pico and
most of the sensors we looked at are easy to find and not expensive. The software used is all free
software. So there’s no reason you shouldn’t try some of the examples (if you haven’t already).
The next step is to do your own projects. What else can you do with the sensors we studied?
Try also to use sensors that are not featured in this book. Getting started can be a little arduous,
but you’ll find similarities to what we’ve seen and enjoy the explanations and codes as a jump-
start.
If you would like to learn more about the microcontroller used in the Raspberry Pi Pico, and
about programming with the C/C++ SDK, take a look at my book “Knowing the RP2040”.
Above all, try to have fun while learning.
Happy Hacking!
Daniel
September/2023
Appendix A - Other Boards Based on
the RP2040 Microcontroller
From the very beginning, the Raspberry Pi Foundation made the RP2040 microcontroller available
to other companies. A few boards, from companies close to the Foundation, were announced at
the same time the Pi Pico. In the following months, other boards came to market, including some
“clones” that have the same form factor and pinout as the Pico.
In this appendix I will show a few of these boards, highlighting their differences from the Pico
and their pinouts.
All these boards are supported by the C/C++ SDK, Arduino, MicroPython, and CircuitPython.
The Feather RP2040 has the following additional features (when compared to the Pico):
¹³https://learn.adafruit.com/adafruit-feather
Appendix A - Other Boards Based on the RP2040 Microcontroller 520
• 8 MB Flash
• Reset button
• RGB LED
• STEMMA QT connector for easy connection of I2C devices
• USB C connector
It is more expensive than the Pico and does not have all the I/O pins in the connectors, but the
additional Flash and battery support are very helpful for some applications.
The XIAO RP2040 has a 2MB Flash, reset button, RGB LED, and USB C connector.
This board is very compact and costs about the same as the Pico. The downside is that it has few
I/O pins.
It includes a u-blox NINA-W102 radio module that gives WiFi and Bluetooth connectivity, a
built-in mic, and a six-axis smart IMU. Flash size is a whopping 16 MB.
This is a very capable board, but it is also the most expensive of the boards in this appendix.
Appendix A - Other Boards Based on the RP2040 Microcontroller 523
Raspberry Pi Pico W
This board expands the original Pico by adding WiFi and Bluetooth communication (thanks to
an Infineon CYW43439 chip).
The Pico W has the same pinout as the Pico, but there is one difference: the LED is connected to
the CYW43439 instead of the RP2040.
To run on the Pico W examples that use the Pico internal LED, you will have to attach a LED (in
series with a 1k Ohm resistor) between one of the Pico W pins and ground and change the LED
pin number in the code.
In the above diagram, the LED is connected to GP28. Pay attention to LED polarity: the anode (+)
is connected to the resistor and the cathode (-) is connected to the ground. The cathode is marked
by a flat edge on the casing and a shorter lead.
Appendix B - Non-Original Sensors
A growing concern for users of electronic devices is the use of parts that were not manufactured
by the original manufacturer (or by a company licensed by him).
In some cases, companies develop parts that attempt to be compatible with popular parts and
sell them under a different name (part number). Regardless of the legal status of these copies (or
clones), they probably have different parameters from the originals. Quality control may also be
more relaxed.
Things get worse when these non-original parts are labeled and sold as if they were made by the
original manufacturer.
In more extreme cases, crooks sometimes relabel a random part and sell it as something else.
Sensors are no exception to this problem.
The best way to avoid a non-original part is to buy parts only from official distributors and
resellers. This can be harder when you are buying modules (as the parts are bought from someone
else) or you are a small company (or a hobbyist).
In this appendix, I will tell a few bad experiences I had with the sensors I bought for creating the
examples in this book.
Some of these non-original sensors can have a severe impact on your project, including bad
performance, non-support for the parasitic power mode, and the use of a supercapacitor instead
of an EEPROM to make configurations non-volatile.
There is an Arduino Library (available in the Library Manager) for identifying DS18B20 chips.
A quick search showed that I was not the only one with these chips. I found a report of the same
result from a chip with exactly the same markings.