0% found this document useful (0 votes)
349 views531 pages

Using Sensors With The Raspberry Pi Pico

This book, 'Using Sensors with the Raspberry Pi Pico' by Daniel Quadros, provides comprehensive guidance on utilizing various sensors with the Raspberry Pi Pico. It covers topics such as programming environments, interfacing protocols, and specific sensor types, along with practical examples and code. The book is designed for readers interested in hands-on projects and feedback-driven development.

Uploaded by

acarlosss
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
349 views531 pages

Using Sensors With The Raspberry Pi Pico

This book, 'Using Sensors with the Raspberry Pi Pico' by Daniel Quadros, provides comprehensive guidance on utilizing various sensors with the Raspberry Pi Pico. It covers topics such as programming environments, interfacing protocols, and specific sensor types, along with practical examples and code. The book is designed for readers interested in hands-on projects and feedback-driven development.

Uploaded by

acarlosss
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 531

Using Sensors with the Raspberry Pi Pico

Daniel Quadros
This book is for sale at http://leanpub.com/picosensors

This version was published on 2023-09-08

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.

© 2022 - 2023 Daniel Quadros


Contents

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

Programming the Raspberry Pi Pico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13


Libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
The Official C/C++ SDK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
The Arduino IDE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
MicroPython . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
CircuitPython . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Thonny IDE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Seeing Program Output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Which Environment Should I Use? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

Interfaces and Protocols . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27


A Little Bit of Electronics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
A General View of Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
CONTENTS

The ADC - Analog to Digital Converter . . . . . . . . . . . . . . . . . . . . . . . . . 29


Connecting Digital Signals from Sensors to the Pico . . . . . . . . . . . . . . . . . . . 33
GPIO - General Purpose (Digital) Input and Output . . . . . . . . . . . . . . . . . . . 34
UART - Universal Asynchronous Receiver and Transceiver . . . . . . . . . . . . . . . 37
SPI - Serial Peripheral Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
I²C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54

Basic Digital Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67


Read This Before Trying the Examples . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Buttons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Reed Switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Keypads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Vibration Sensor (SW-420) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
Presence Sensor (PIR) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Flame Sensor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
Digital Sound Sensor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
MQ Gas Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Digital Hall Effect Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119

Analog Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136


Potentiometers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
Analog Joysticks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Light-Dependent Resistors (LDR) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
Phototransistor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
Using LEDs as a Light Sensor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
Gas Sensor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
Analog Hall Effect Sensor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167

Temperature Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174


Thermistor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
LM35D and TMP36 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
DS18B20 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
DHT11 and DHT22 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
LM75A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
HDC1080 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
MCP9808 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
AHT10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260
Sensors Comparison Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269

Atmospheric Pressure Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 270


BMP085 and BMP180 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
CONTENTS

BMP280 and BME280 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285


BMP390 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307
Sensors Comparison Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320

Electronic Compass, Accelerometers, and Gyroscopes . . . . . . . . . . . . . . . . . . 322


HMC5883L, HMC5983, and QMC5883L 3-Axis Magnetic Sensor . . . . . . . . . . . . 323
ADXL345 3-Axis Accelerometer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341
MMA8452 3-Axis Accelerometer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
MPU6050 3-Axis Accelerometer and Gyroscope . . . . . . . . . . . . . . . . . . . . . 379
Sensors Comparison Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393

Miscellaneous Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395


HC-SR04 Ultrasonic Sensor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
Rotary Encoder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
Load Cell (Strain Gauge) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421
iButton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438

Fingerprint Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 446


Fingerprints Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 446
FPM10A Sensor Protocol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
Sensor Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 449
Enrolling a Fingerprint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 450
Identifying a Fingerprint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451
Transferring the Image and the Template . . . . . . . . . . . . . . . . . . . . . . . . . 451
Fingerprint Sensor Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452

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

Appendix A - Other Boards Based on the RP2040 Microcontroller . . . . . . . . . . . 519


Adafruit Feather RP2040 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 519
SeeedStudio XIAO RP2040 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 520
Arduino Nano RP2040 Connect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 521
Raspberry Pi Pico W . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 523
CONTENTS

Appendix B - Non-Original Sensors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524


Non-original DS18B20 Temperature Sensors . . . . . . . . . . . . . . . . . . . . . . . 524
Conterfeit MPU6050 Accelerometer . . . . . . . . . . . . . . . . . . . . . . . . . . . . 525
Introduction
Sensors allow an embedded system to get a glimpse of what is happening in the physical world.
In a typical system, data from sensors are processed in a microcontroller and used to decide what
actions should be taken.

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 Raspberry Pi Pico


The Raspberry Pi Pico was selected for this book for many reasons: it is an easily found, cheap,
and powerful board. By powerful I mean not only that it has a good number of interfaces for
connecting sensors and some serious processing power, but also that this processing power
and the memory available allows us to use a sophisticated language such as MicroPython and
CircuitPython.

The Raspberry Pi Pico

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.

Raspberry Pi Pico Pinout

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:

• Power the board (and the sensors connected to it)


• Load programs in the board
• Interact with the program running on the Pico

We will talk more about all this in Chapter 3.

Getting the Example Code


In this book, I will list only the more important parts of the code. The full code can be downloaded
from
https://github.com/dquadros/PicoSensors

Using the Examples


The easiest way to play with the examples is by assembling the circuits on a breadboard.
A breadboard (or protoboard) is a board with holes where components can be inserted and
internal connections between these holes. Wires (jumpers) are used to make the connections
that cannot be made with the internal ones.

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.

Organization of This Book


This introduction (Chapter 1) gives a general idea of what are sensors and what you will find in
this book.
Chapter 2 talks (in a general way) about why sensors will sometimes not give the right value and
how to get better results from them.
Chapter 3 is about how we can write programs for the Pi Pico. We are going to use four options
in this book: the official C/C++ SDK, the Arduino IDE, MicroPython, and CircuitPython.
In Chapter 4 we see the various ways a sensor can send data to a microcontroller and how they
are accessed in the four programming environments.
The next chapters will talk about specific sensors. We can organize sensors in many ways,
including how they are connected and the property they measure. I have adopted a mixed way:
Chapter 5 describes sensors that have a single digital output to give a yes/no or on/off result. A
good example is a button that will signal if it is pressed or released.
Chapter 6 is about sensors that output an analog signal, like a light detector that informs how
light or dark it is.
In Chapter 7 we change to the “what is measured” classification and talk about temperature
sensors.
Chapter 8 is about pressure sensors and Chapter 9 is about accelerometers and compasses.
In Chapter 10 I talk about a few other sensors that did not fit in the previous chapters.
Chapter 11 is about fingerprint sensors and in Chapter 12 you will learn about two types of RFID.
Chapter 13 concludes the text with a quick recap and some reminders about using sensors in a
project.
Appendix A shows a few other boards based on the RP2040 microprocessor.
²https://fritzing.org/
Introduction 6

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”.

How to Send Feedback


If you find an error or have a suggestion or comment, you can reach me by:

• 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.

Measuring What We Really Want


Inside the RP2040 there is a temperature sensor. With a few lines of code, we can get a reading.
But what are we measuring? The sensor is inside the chip so it will give a better idea of the
internal temperature than of the ambient temperature. Indeed, most of the time you will get a
temperature high above the ambient temperature. The reading will increase when the ARM cores
are processing.
Sensor placement is fundamental to measuring what you want. In many cases, the property
you want to measure varies with the position. It is up to you to figure out what is the important
value for your application. Some testing may be needed to learn how it varies and what is the
best placement. In some cases, you may need to use multiple sensors and use an average or have
a more complicated logic using all the values,

A Sensor Can Affect What You Are Measuring


While this is not that common, you should look out for cases where the sensor can affect the
property you are measuring. One example is when you are measuring electrical properties, like
current and voltage. In many cases the sensor is wired in series or parallel with the circuit and
its resistance and capacitance may affect what you are measuring.
Using Sensors 8

Accuracy and Resolution


Even with all outside things OK, the sensor itself cannot give perfect results. In the specification,
you will see an accuracy figure that will state what is the maximum difference between the value
you get from the sensor and the real value of the sensed property.
For instance, you may find that a temperature sensor spec says it has an accuracy of +/- 1 degree
Celsius. This means that you may get a reading of 15C if the temperature is between 14 and 16C.
Don’t forget that outside factors can increase this margin.
You should also note that accuracy may vary from one individual sensor to another and over the
range of readings.
Many sensors will internally convert the measurement into a number. This is where the
resolution comes in: it is the minimum step for the readings. For example, a temperature sensor
with a 0.25C resolution might return a reading of 15.0, 15.25, or 15.5, but not 15.1, 15.2, or 15.3.

DHT22 Humity and Temperature Sensor Specifications

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:

1 const int n = 10;


2
3 int avg_reading() {
4 int sum = 0;
5 for (int i = 0; i < n; i++)
6 sum += read_sensor();
7 return sum / n;
8 }

There are a few things to notice about this strategy when compared to a single reading:

• It consumes a little more memory


• It takes more time (“n” times 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

1 const int n = 10;


2 int readings[10]; // previous readings
3 int old_reading = 0;
4 int n_readings = 0
5 int sum = 0;
6
7 int avg_reading() {
8 if (n_readings == n) { // history is full
9 // replace oldest with new reading
10 sum -= readings[old_reading];
11 readings[old_reading] = read_sensor();
12 sum += readings[old_reading];
13 old_reading = (old_reading+1) % n;
14 } else {
15 // add reading to the history
16 readings[n_readings] = read_sensor();
17 sum += readings[n_readings];
18 n_redings++;
19 }
20 return sum / n_readings;
21 }

Moving averages make it more evident how old readings affect the current average.
When choosing an average strategy you should consider:

• How much time a single reading takes?


• At what rate will we take an average?
• How much the readings change from error or noise?
• How fast is it expected that the property will change?

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:

1 // Calibration data, last pair reading/value is the max possible


2 const int nSamples = 10;
3 int known_readings[] = {};
4 int known_values[] = {};
5
6 int correct_reading(int reading) {
7 int i;
8 for (i = 0;
9 (i < nSamples) && (reading < kwnown_readings[i]);
10 i++) {
11 }
12 if (i == (nSamples-1)) {
13 return known_values[i]; // max value
14 return known_values[i] + (known_values[i+1]-known_values[i])/
15 (reading-kwnown_readings[i])

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 -73C reading is clearly a sensor failure, a +110C 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:

• The official C/C++ SDK


• The Arduino IDE
• MicroPython
• CircuitPython

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.

The Official C/C++ SDK


This is the basis for all programming with the RP2040 microcontroller used in the Pico boards. It
is low-level, meaning that it allows greater control of the hardware at the cost of writing more
code.
The C and C++ languages are very popular in embedded programming and there are many
resources to learn them.
The procedure to set up the SDK is detailed in the document “Getting Started with the Raspberry
Pi Pico” ³. Executable generation is done by typing commands or using an IDE (like Visual Code)
properly configured. The result is a .uf2 file. To load this file into the Pico, follow these steps:

• 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”.

The Arduino IDE


The Arduino IDE (Integrated Development Environment) was created for programming the
official Arduino boards but has evolved to support many kinds of boards. While there is an
official Arduino board with the RP2040 and the associated software supports the Pico and other
RP2040 boards, I will use the amazing (but not-official) RP2040 support maintained by Earle F.
Philhower, III.
The following instructions are for version 2 of the Arduino IDE (but the process for previous
versions is similar).

1. Download and install the IDE from Arduino.cc


2. Open the Arduino IDE and go to File->Preferences. In the dialog that pops up, add to the
“Additional Boards Manager URLs” field: “

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

Arduino IDE Preferences

Additional Boards Manager URLs


Programming the Raspberry Pi Pico 17

Arduino IDE Board Manager

This procedure will install:

• The tools needed to generate executables for the Pico.


• A version of the standard Arduino runtime and libraries for the Pico.
• Examples for the Pico.

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:

• An include for the Arduino header file is added at the top.


• Declarations (prototypes) for local functions are automatically generated and placed at the
start of the source.
• If you use multiple source files they will be glued together in a single file.

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.

Installing a library from a zip file


Programming the Raspberry Pi Pico 19

The Library Manager

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:

• delay(n) will pause the execution for n milliseconds (1/1000 of a second).


• delayMicroseconds(n) will pause the execution for n microseconds (1/1000000 of a
second).
• millis() returns the number of milliseconds since the Pi Pico started executing the
program. This number will roll back to zero after approximately 50 days.
Programming the Raspberry Pi Pico 20

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

I will talk about these classes in the next chapter.


A module that will be used in many examples is time. This module includes the following
functions:

• 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:

1 from machine import UART


2
3 uart = UART(1) # create the UART object
4
5 uart.init(19200, 7) # init with baudrate=19200 and bits=7
6 # parity and stop will use the defaults
7 # None and 1
8
9 uart.init(bits=7, baudrate=115200) # does the same thing
10
11 # Init with baudrate 19200, defaults for bits, parity, and stop
12 # and the `rx` and `tx` keyword-only parameters
13 uart.init(19200, rx=Pin(4), tx=Pin(5))
Programming the Raspberry Pi Pico 22

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

I will talk about these classes in the next chapter.


A module that will be used in many examples is time. This module includes the following
functions:

• 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.

CircuitPython also cuts down Micro Python’s concurrency support.


To use CircuitPython, you have first to download the interpreter as a .uf2 file from
https://circuitpython.org/board/raspberry_pi_pico/ and install it in the Pico board.
CircuitPython will create a disk-like storage in the Pico for your programs and libraries, and it
will be visible to a PC connected through USB (with the name CIRCUITPY). CircuitPython will
automatically run a file called main.py (or code.py) after the Pico is powered up or reset, or the
file is changed.
Interaction with CircuitPython can also be done through the Interactive Interpreter Mode,
commonly called the REPL (read-eval-print-loop). You will need an IDE to access the REPL. In
the next section, I will talk about the Thonny IDE.
Libraries should be stored in the Pico in the /lib directory.

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.

Accessing the REPL


To access the REPL, the Pi Pico must be attached to the PC and your Python app must not be
running. If you disconnect the board (or start Thonny with no board connected) you will get an
error. After connecting the board, click on the STOP icon to reconnect.
If you get a message saying the board is busy, press Control C to interrupt your app.
Programming the Raspberry Pi Pico 24

Once you get the >>> prompt in the shell window, you can type Python commands for immediate
execution.

Entering, Editing, and Saving Programs


Entering and editing programs are done in the main window. You can have multiple source files
open at the same time. File New will open a new tab for entering code.
To save a file, you can:

• use File Save in the menu


• click on the Save icon
• press Control S

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.

Moving Files Between the Pi Pico and the PC


With CircuitPython you can access the files in the Pico just like files on your PC. With
MicroPython, you will have to use the IDE.
The Files Window shows the directories and files in the PC and the Pi Pico. Operations are done
by selecting a file or directory and pressing the right mouse button.
Note: The File menu, including Move/rename, affects the selected tab in the editor window, not
the files selected in the Files Window.
Moving files between the PC and Pico is done using the options Upload (send a PC file to the
board) and Download (send a file in the board to the PC). These operations use the focused
directory. To focus a directory, select it, press the right mouse button, and select Focus. You can
Upload and Download single files or a whole directory.
Programming the Raspberry Pi Pico 25

Seeing Program Output


Some of the examples will output text messages sent to a PC connected to the Pico. Outputting
messages is a very popular method to debug programs.
In the examples, the messages are sent through a virtual serial connection over the USB.

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.

MicroPython and CircuitPython


Both MicroPython and CircuitPython support output using the print() method, just like Python
3. To format the messages I will use the String format() method:

1 print ("Temperature = {}, humidity = {}".format(temp, hum))

Thonny will show the messages in the shell window.


Programming the Raspberry Pi Pico 26

Which Environment Should I Use?


Each of the four mentioned environments has advantages and disadvantages. Here are my
recommendations:

• 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 Little Bit of Electronics


Let’s define a few electronic terms that will be used in the rest of this book.
In most of the circuits we will see, electrical current will always flow in the same direction. This
is called direct current, or simply DC. Electrical current is a result of an electrical potential
difference, commonly known as voltage. As the word difference implies, voltage is measured
between two points, the unit for voltage in Volts (V). One point in the circuit is normally used as
a reference for all other points, this is the 0V point, also called ground.
In our examples, the Raspberry Pi Pico will be powered by its USB connector. The voltage present
at the USB is 5 VDC, a circuit on the Pico lowers this voltage to the 3.3 VDC used by the RP2040
microcontroller. If you look at the pinout of the Pico you will notice that it has:

• 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

Raspberry Pi Pico Power Pins

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

(where R is the resistance, V is the voltage and I is the current).


As a result of the RP2040 being powered by 3.3 V, this is the highest voltage that can be applied
to its pins. In most situations we will be dealing with digital signals, that is, signals that can be
at two levels - HIGH or LOW - that will be read by the RP2040 as 1 or 0.
If a digital pin is an output, the Pico will force it to a HIGH or LOW state. If a digital pin is
an input, its level will be set by an outside circuit. In this case, the Pico will try not to interfere
with the outside circuit by placing the pin in a condition where it will be similar to not being
connected. This condition is called high impedance (sometimes called “three state”, because it is
not in the HIGH or LOW state).
Interfaces and Protocols 29

A General View of Interfaces


First, we can divide the ways to connect a sensor into two large groups: analog (signals can
assume any value in a range) and digital (signals can only assume a HIGH or LOW value).
Analog interface is very straightforward: the sensor outputs an analog signal (normally a
voltage) that can be converted to the measurement. We use the Analog to Digital Converter
(ADC) of the RP2040 to convert this signal into a number.
Digital interface can be a lot more complicated, involving one or more digital signals.
A simple sensor, like a switch, can output a single digital signal. More complicated sensors may
need to receive and transmit multiple words of commands and data. In this case, it’s common
to use some kind of serial communication. In a serial interface, words are transferred one bit at
a time through a single wire. While this is more complex than sending all bits at the same time
through individual signals (a parallel interface), it requires fewer pins of the microcontroller and
fewer connections to the sensor. The standard serial interfaces we are going to see in this chapter
are the asynchronous (UART), SPI, and I²C. All three have hardware support in the RP2040.
The standard serial interfaces will transfer words (normally 8-bit bytes) between the microcon-
troller and the sensor. What these words mean varies from one sensor to another. In some
cases, there is some kind of protocol - a precise sequence of commands and responses that must
be followed. Complex sensors may use many words of configuration, status, and readings. A
common strategy in these cases is to organize these words as a collection of registers, each with
an individual address (like a small memory).
Some sensors use some special digital signaling. This is normally implemented by manually
controlling and checking the digital signals. The RP2040 has a feature called PIO (Programmable
I/O) that allows us to defer this to a part of the hardware that will run concurrently with the
ARM CPUs.

The ADC - Analog to Digital Converter


The ADC (Analog to Digital Converter) is used to measure an analog voltage applied to a Pi Pico
pin. The result of the conversion is a number that is proportional to the voltage.
The ADC in the RP2040 returns a 12-bit result. The maximum result plus one (4096) corresponds
to an external reference voltage. In the Raspberry Pi Pico, this reference voltage is the same 3.3V
that powers the microcontroller. Damage can occur if a voltage greater than the reference is
applied to an analog input.
There is only one ADC in the RP2040 microcontroller, but it has five inputs (or channels). One
is an internal temperature sensor, and the other four are connected to the same pins as GPIO26
Interfaces and Protocols 30

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.

The Internal Temperature Sensor


The RP2040 includes an internal temperature sensor, connected to channel 4 of the ADC. The
expected voltage given by the sensor is 0.706V at 27 degrees C; this voltage will drop 1.721mV per
additional degree C, which suggests the formula

(V −0.706)
T = 27 − 0.001721

Unfortunately, this may not work in most cases as:

• The values can change from device to device.


• The drop per degree is not constant, it will change with the temperature.
• As the drop per degree is low, small differences in the reference voltage will result in a
significant difference in the calculated temperature (the RP2040 datasheet mentions a 4-
degree difference for a 1% change in the reference voltage).

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.

Using the ADC with the C/C++ SDK


The functions for the ADC are in the library hardware_adc.
void adc_init (void)
Initializes the ADC hardware.
static void adc_gpio_init (uint gpio)
Prepares a GPIO pin to be used as an ADC input (disables all digital functions). gpio must be
between 26 and 29.
static void adc_select_input (uint input)
Interfaces and Protocols 31

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.

Using the ADC in the Arduino Environment


In the Arduino, the analog pins in the Pico are referred by A0 to A3. The internal temperature
sensor is read by a specific function.
There are three functions in the RP2040 Arduino library for using the ADC:
int analogRead(pin_size_t pin)
Returns a value corresponding to the ADC reading of the specific pin (A0 to A3). The range of
the value depends on the resolution set by the following function. By default, it is 10 bits, so the
value returned will be between 0 and 1023.

The resolution affects only the returned value, the RP2040 microcontroller will always
use a 12 bit resolution.

The result is obtained by scaling the ADC value, for example:


Interfaces and Protocols 32

Bits Range Scale


8 0 to 255 1/16
9 0 to 511 1/8
10 0 to 1023 1/4
11 0 to 2047 1/2
12 0 to 4095 1
13 0 to 8191 2
14 0 to 16383 4
15 0 to 32767 8
16 0 to 65535 16

void analogReadResolution(int bits)


Determines the resolution (in bits) of the value returned by the analogRead() function. The
default resolution is 10 bits.
While values between 1 and 31 are accepted, I suggest you use either 10 (for Arduino Uno
compatibility) or 12 (to fully use the result from the hardware).
float analogReadTemp(float vref = 3.3f)
Returns the temperature, in Celsius, of the thermal sensor inside the RP2040 microcontroller. If
you have a custom Vref for the ADC on your RP2040 board, you can pass it in as a parameter.
Calling with no parameters assumes the normal, 3.3V Vref.
Remember that this reading is not much accurate.

Using the ADC in MicroPython


In MicroPython, interaction with the ADC is done through the class ADC in the machine module.
The ADC pins are referred to by the GPIO that shares the same physical pin (26 to 29) or the
channel (0 to 4).

1 from machine import ADC, Pin


2 adc = ADC(Pin(26)) # create ADC object on ADC pin
3 adc.read_u16() # read value, 0-65535 across voltage range 0.0v - 3.3v
4
5 adcTemp = ADC(4) # create ADC object for channel 4
6 # aka the temperature sensor

Using the ADC in CircuitPython


In CircuitPython, interaction with the ADC is done through the class AnalogIn in the analogio
module. The ADC pins are referred to by names defined in the board object. For the Pi Pico, they
are board.A0 through board.A3.
Interfaces and Protocols 33

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)

The result is in Celsius degrees.

Connecting Digital Signals from Sensors to the Pico


As said, the Raspberry Pi Pico works at 3.3V. When we talk about digital signals, we will simplify
a lot and treat the LOW level as 0V (GND) and the HIGH level as 3.3V. The exact voltages
corresponding to 0 and 1 in input and output can be found in the RP2040 datasheet and should
be checked against the voltages in the sensor datasheet.
Note: I²C is a little different, as we will see later on.
Most sensors you will find will use 3.3V or 5.0V digital signals. 3.3V sensors (most of the time)
can be connected directly to the Pi Pico.
In some cases, a 5V sensor is available in a board (module) that includes additional circuitry to
permit operation at 3.3V. Again, these modules can be connected directly to the Pi Pico.
Things get more complicated if you have to connect a 5V sensor to the Pi Pico. Most of the time
the 5V sensor will accept a 3.3V Pico output in its inputs. On the other hand, if you connect a 5V
sensor output to an input of Pi Pico, it will damage the Pico.
There exist chips and modules to interface between 3V and 5V. Some are unidirectional (meaning
that it’s fixed who is input and who is output) and others are bi-directional.
In many unidirectional situations we can simplify things by connecting Pi Pico outputs directly
to the sensor inputs and using a pair of resistors as a voltage divisor to connect a sensor output
to a Pi Pico input.
Interfaces and Protocols 34

Voltage Divisor

GPIO - General Purpose (Digital) Input and Output


GPIO is the basic form of digital input and output. In digital input, the voltage in a pin is read as
0 or 1. In digital output, you write a 0 or 1 to set the voltage in a pin to a LOW or HIGH level.
By default, the I/O pins in the Raspberry Pi Pico are set to GPIO input. They need to be configured
to be used for other functions (like ADC or SPI). Depending on the programming environment,
this can be done automatically or you may need to do it explicitly.
If you use digital input in an unconnected input, the result is undetermined. You can change
this by enabling the pull-up or pull-down resistors. These are resistors (with values somewhere
between 50k and 80k Ohms) internal to the microcontroller that can connect a pin to the ground
(pull-down) or the power source (pull-up).

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.

Using GPIO with the C/C++ SDK


The SDK functions numbers the GPIO pins from 0 to 29. Some functions use a mask, a 32-bit
value where bit n is associated with pin n.
The functions below are in the library hardware_gpio.
void gpio_init (uint gpio)
Interfaces and Protocols 35

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).

Using GPIO the Arduino Environment


Arduino numbers the digital pins from 0 to 29, using the same numbers as the C/C++ SDK.
There are three functions related to basic digital I/O in Arduino:
pinMode(int pin, int mode)
Sets the mode of a pin to INPUT, OUTPUT, or INPUT_PULLUP.
Interfaces and Protocols 36

int digitalRead(int pin)


Reads a pin, returning HIGH or LOW.
digitalWrite(int pin, int value)
Sets the level of a pin, the value should be HIGH or LOW.
Another function that is useful in Arduino is
unsigned long pulseIn(int pin, int value, unsigned long timeout)
This function will measure a pulse on a pin. pin is the pin number, value is the value (HIGH
or LOW) during the pulse, and timeout is the maximum time (in microseconds) to wait for the
measurement to complete. timeout can be omitted (the function assumes 1 second).
This function works by:

• 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.

Using GPIO in MicroPython


MicroPython numbers the digital pins from 0 to 29, using the same numbers as the C/C++ SDK.
Digital pins are accessed through the class Pin in the machine module.

1 from machine import Pin


2
3 p0 = Pin(0, Pin.OUT) # create output pin on GPIO0
4 p0.on() # set pin to "on" (high) level
5 p0.off() # set pin to "off" (low) level
6 p0.value(1) # set pin to on/high
7
8 p2 = Pin(2, Pin.IN) # create input pin on GPIO2
9 print(p2.value()) # get value, 0 or 1
10
11 p4 = Pin(4, Pin.IN, Pin.PULL_UP) # enable internal pull-up resistor
12 p5 = Pin(5, Pin.OUT, value=1) # set pin high on creation
Interfaces and Protocols 37

Using GPIO in CircuitPython


In CircuitPython digital pins are referred to by names defined in the board object. For the
Pi Pico, they are board.GP0 through board.GP29. Digital pins are accessed through the class
DigitalInOut in the digitalio module.

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

UART - Universal Asynchronous Receiver and


Transceiver
Asynchronous serial communication is one of the oldest forms of serial communication. Bits are
sent serially (one after the other) over a wire with no common clock signal to synchronize the
receiver to the transmitter and determine where are the individual bits. A communication speed
(called, not precisely, baud rate) must be previously agreed upon by the two sides. In the most
common form (full-duplex) receiving and transmitting is done through separate wires.
The Raspberry Pi Pico has two UARTs (UART0 and UART1), with the following features:

• 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.

UART Framing - Transmission of 0x35 7 bits, even parity

A special condition is break, where the signal is kept low for longer then the time to transmit a
word.

Control Signals and Hardware Flow Control


PC users may recall the use of serial communications with modems and phone lines to connect
to the Internet. Standards define several control signals between a computer (or DTE - Data
Transmission Equipment) and a modem (or DCE - Data Communication Equipment). The RP2040
supports only two control signals:
Interfaces and Protocols 39

• 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

The options for UART1 are:

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

Using the UART with the C/C++ SDK


These functions are the library hardware_uart. I am only listing here the “blocking” functions
(the ones that wait for a result). For better performance and doing other things concurrently with
communication, there is support for using interrupts and DMA.
In this functions, uart should be uart0 or uart1.
uint uart_init (uart_inst_t *uart, uint baudrate)
Initialize a UART, baudrate is in bps (bits per second). Must be called before the other functions.
Returns the actual baud rate programmed.
static void uart_set_hw_flow (uart_inst_t *uart, bool cts, bool rts)
Turns on or off the hardware flow control options.
static void uart_set_format (uart_inst_t *uart, uint data_bits, uint
stop_bits, uart_parity_t parity)
Set the format of the data sent and received:

• data_bits must be between 5 and 8


• stop_bits must be 1 or 2
• parity must be one of the following: UART_PARITY_NONE, UART_PARITY_EVEN, UART_-
PARITY_ODD

static void uart_set_fifo_enabled (uart_inst_t *uart, bool enabled)


Enables or disables the FIFOs in the UART. The RP2040 does not allow independent control over
RX and TX FIFOs, you can have both or none.
static bool uart_is_readable (uart_inst_t *uart)
Return true if there is data in the receive FIFO.
static bool uart_is_writable (uart_inst_t *uart)
Return true if there is space available in the TX FIFO.
static void uart_tx_wait_blocking (uart_inst_t *uart)
Blocks until the TX FIFO and the transmit shift register are empty.
static void uart_putc_raw (uart_inst_t *uart, char c)
Waits for space in the TX FIFO and puts a character in it.
The function returns when the character is put in the FIFO, not when it is sent (this can take the
same time if there are more characters in the FIFO and/or hardware flow control is used).
Interfaces and Protocols 41

static void uart_write_blocking (uart_inst_t *uart, const uint8_t *src,


size_t len)
Sends len characters starting from src. The function returns when the last character is put in
the FIFO, blocking for space as necessary.
static char uart_getc (uart_inst_t *uart)
Read a character from the UART, and block until one is available in the RX FIFO.
static void uart_read_blocking (uart_inst_t *uart, uint8_t *dst, size_t len)
Reads len characters into dst, blocking as necessary for the characters to be received.
static void uart_set_break (uart_inst_t *uart, bool en)
Turns on or off the transmission of a break condition.

Using the UART in the Arduino Environment


The Arduino-Pico core implements a software-based Serial-over-USB port. Serial is this USB
serial port, and while Serial.begin() does allow specifying a baud rate, this rate is ignored since
it is USB-based. (Also be aware that this USB Serial port is responsible for resetting the RP2040
during the upload process, following the Arduino standard resetting to the bootloader when a
connection is made at 1200 bps). This serial is mainly used to connect to a PC, not to sensors.
The two hardware-based UARTS can be used as Serial1 for UART0, and Serial2 for UART1.
To use the hardware UARTs you must configure their pins using the setRX() and setTX() calls
prior to calling begin().
In the functions below, Serial can be replaced by Serial1 or Serial2.
bool Serial.setRX(int pin)
bool Serial.setTX(int pin)
Define the UART pins. Returns false if pin is not allowed for the UART.
int Serial.available()
Returns the number of bytes available for reading.
void Serial.begin(long speed)
void Serial.begin(long speed, int config)
Initializes the serial port, setting the speed and, optionally, the format of the data. config has
the form SERIAL_{bits}{parity}{stops}. The default for the format is SERIAL_8N1, other
common options are SERIAL_7E1 and SERIAL_7O1.
int Serial.read()
Interfaces and Protocols 42

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.

Using the UART in MicroPython


In MicroPython serial ports are accessed through the class UART in the machine module.

1 from machine import UART, Pin


2
3 # Init UART1 for 9600bps, using pins 4&5 for TX and RX
4 uart1 = UART(1, baudrate=9600, tx=Pin(4), rx=Pin(5))
5
6 uart1.write('hello') # write 5 bytes
7 uart1.read(5) # read up to 5 bytes

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:

• tx specifies the TX pin to use.


• rx specifies the RX pin to use.
• timeout specifies the time to wait for the first character (in ms).
• timeout_char specifies the time to wait between characters (in ms).
Interfaces and Protocols 43

The following methods can be used:


UART.read()
UART.read(nbytes)
Read characters. If nbytes is specified, then read at most that many bytes, otherwise read as
much data as possible. It may return sooner if a timeout is reached.
Returns a bytes object containing the bytes read in or None on timeout for the first byte.
UART.readinto(buf)
UART.readinto(buf, nbytes)
Read bytes into the buf. If nbytes is specified, then read at most that many bytes. Otherwise,
read at most len(buf) bytes. It may return sooner if a timeout is reached.
Returns the number of bytes read and stored into buf or None on timeout.
UART.readline()
Read a line, ending in a newline character. It may return sooner if a timeout is reached. The
timeout is configurable in the constructor.
Returns the line read or None on timeout.
UART.write(buf)
Write the buffer of bytes to the bus.
Returns the number of bytes written or None on timeout.
UART.sendbreak()
Send a break condition on the bus.

Using the UART in CircuitPython


In CircuitPython serial ports are accessed through the class UART in the busio module.
Configuration can be done at creation time:
busio.UART(tx, rx, *, rts, cts, baudrate, bits, parity, stop, timeout)
Parameters:

• tx (Pin) – the pin to transmit with, or None if this UART is receive-only.


• rx (Pin) – the pin to receive on, or None if this UART is transmit-only.
• rts (Pin) – the pin for rts, or None if rts not in use. The default is None.
• cts (Pin) – the pin for cts, or None if cts not in use. The default is None.
Interfaces and Protocols 44

• 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.

The following properties are available:


baudrate
The current baudrate (int).
in_waiting
The number of bytes in the input buffer, available to be read (int).
timeout
The current timeout, in seconds (float).
The following methods are available:
read(nbytes)
Reads bytes. If nbytes (int) is specified then reads at most that many bytes. Otherwise, read
everything that arrives until the connection times out. Providing the number of bytes expected
is highly recommended because it will be faster.
If no bytes are read, return None.
readinto(buf)
Read bytes into the buf (a WriteableBuffer). Read at most len(buf) bytes.
Returns the number of bytes read and stored into buf.
readline()
Read a line, ending in a newline character, or return None if a timeout occurs sooner, or return
everything readable if no newline is found and timeout is 0.
write(buf)
Write the content of buf(a ReadableBuffer) to the bus.
Returns the number of bytes written.
reset_input_buffer()
Discard any unread characters in the input buffer.
Interfaces and Protocols 45

SPI - Serial Peripheral Interface


SPI is a very popular electrical protocol for connecting all kinds of devices to microcontrollers,
particularly when high speed is needed (like SD cards and LCDs).
SPI is notable for simultaneously transferring data in both directions with a single clock: one bit
is sent and one bit is received with each clock pulse. In situations where you only want to receive
some data you still have to send something (zeros are a common value but some devices require
specific values).
It uses a master/slave multi-drop topology where the master generates the clock and asserts a
signal that selects the slave. Multiple slaves can be connected to the same data and clock lines of
a master, but each has a separate selection signal.

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):

• CPOL=0 means that SCLK idles at the LOW level.


• CPOL=1 means that SCLK idles at a HIGH level.
• CPHA=0 means that the “out” side changes the data on the trailing edge of the preceding
clock cycle (or before the first cycle if it is the first bit), while the “in” side captures the
data on the leading edge of the clock cycle.
• CPHA=1 means that the “out” side changes the data on the leading edge of the clock cycle,
while the “in” side captures the data on the trailing edge of the clock cycle.

SPI Polarity and Phase

It is common to refer to the four possible combinations by a mode number:


Interfaces and Protocols 47

Mode CPOL CPHA


0 0 0
1 0 1
2 1 0
3 1 1

SPI in the Raspberry Pi Pico


The RP2040 microcontroller has two SPI peripherals (SPI0 and SPI1) with the following features:

• Can be used as master or slave


• Support data size of 4 to 16 bits
• Has 8 position FIFOs for reception and transmission
• Support the four SPI modes
• Flexible clock generation
• Can generate interrupts and work with DMA

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

The options for SPI1 are:

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

pins of the peripheral.

Using the SPI with the C/C++ SDK


The RP2040 datasheet and C/C++ SDK use the following names for the SPI signals:

• SCLK: SSPCLK / SCK


• MISO: SSPRXD / RX
• MOSI: SSPTXD / TX
• SS: SSPFSS / CSN

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

static bool spi_is_writable (const spi_inst_t *spi)


Returns true if there is space in the transmission FIFO.
static bool spi_is_readable (const spi_inst_t *spi)
Returns true if there is data in the reception FIFO.
Interfaces and Protocols 49

static bool spi_is_busy (const spi_inst_t *spi)


Returns true if the SPI is transmitting and/or receiving a frame or the transmit FIFO is not empty.
False means that no data is transferring and no data is waiting in the FIFO to be transmitted.
int spi_write_read_blocking (spi_inst_t *spi, const uint8_t *src, uint8_t
*dst, size_t len)
int spi_write16_read16_blocking (spi_inst_t *spi, const uint16_t *src,
uint16_t *dst, size_t len)
Write len items from src and, simultaneously, read len items to dst. Blocks until all data is
transferred.
The first version uses byte buffers and is for data length up to 8 bits. In the second version, the
buffers hold 16-bit values.
Returns the number of items transferred.
int spi_write_blocking (spi_inst_t *spi, const uint8_t *src, size_t len)
int spi_write16_blocking (spi_inst_t *spi, const uint16_t *src, size_t len)
Write len items from src and ignore the received data. Blocks until all data is transferred.
The first version uses a byte buffer and is for data length up to 8 bits. In the second version, the
buffer holds 16-bit values.
Returns the number of items transferred.
int spi_read_blocking (spi_inst_t *spi, uint8_t repeated_tx_data, uint8_t
*dst, size_t len)
int spi_read16_blocking (spi_inst_t *spi, uint16_t repeated_tx_data, uint16_t
*dst, size_t len)
Read len items into dst, sending len items equal to repeated_tx_data. Blocks until all data is
transferred.
The first version uses a byte buffer and is for data length up to 8 bits. In the second version, the
buffer holds 16-bit values.
Returns the number of items transferred.

Using the SPI in the Arduino Environment


To use the SPI interface, you have to add at the top of your Arduino program:

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)

• speed: the clock frequency


• dataOrder: MSBFIRST or LSBFIRST
• dataMode: SPI_MODE0, SPI_MODE1, SPI_MODE2, or SPI_MODE3

The methods available are:


void begin(void)
Initialize the SPI interface.
void beginTransaction(SPISettings mysettings)
Sets the SPI interface to the specified settings.
void endTransaction(void)
Frees the SPI interface for use with other settings.
byte transfer(byte val)
Sends a byte, and returns the received byte.
uint16_t transfer(uint16_t val)
Sends a 16-bit word, and returns the received word.
void transfer(byte *buffer, int size)
Sends the size bytes in buffer, the received bytes are written in the same buffer (overwriting
what was sent).
setRX(int pin)
Sets the RX (MISO) pin.
setTX(int pin)
Sets the TX (MOSI) pin.
setSCK(int pin)
Sets the SCK pin.
Interfaces and Protocols 51

Using the SPI in MicroPython


In MicroPython the SPI interfaces are accessed through the class SPI in the machine module.

1 from machine import SPI, Pin


2
3 # Init SPI1 with 10MHz clock, using pins 14, 15 and 12
4 spi1 = SPI(1, 10_000_000, sck=Pin(14), mosi=Pin(15), miso=Pin(12))
5
6 # Reads 50 bytes into a buffer, sending zeros
7 buf = bytearray(50)
8 spi1.readinto(buf)
9
10 # Write 5 bytes and ignore the received bytes
11 spi1.write(b'12345')
12
13 # Write 5 bytes and put the received bytes in a buffer
14 buf = bytearray(5)
15 spi1.write_readinto(b'12345', buf)

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:

• baudrate is the clock outputted in SCK.


• polarity is 0 or 1 (the level for SCK when idle)
• phase is 0 or 1 (sample the data in the first or second clock edge)
• bits is the number of bits in each transfer, usually 8
• firstbit is either SPI.MSB or SPI.LSB
• sck specifies the SCK pin to use.
• mosi specifies the MOSI pin to use.
• miso specifies the MISO pin to use.

The following methods can be used:


SPI.read(nbytes, write=0x00)
Interfaces and Protocols 52

Reads nbytes bytes while continuously sending the write value.


Returns a bytes object containing the bytes read.
SPI.readinto(buf, write=0x00)
Reads len(buf) bytes into the buf while continuously sending the write value.
Returns None.
SPI.write(buf)
Write the buffer of bytes, ignoring the data received.
Returns None.
SPI.write_readinto(write_buf, read_buf)
Write the bytes from write_buf while reading into read_buf. The buffers can be the same or
different, but both buffers must have the same length.
Returns None.

Using the SPI in CircuitPython


In CircuitPython the SPI interfaces are accessed through the class SPI in the busio module.
CircuitPython controls the simultaneous use of the SPI by means of a lock; you have to lock the
SPI before using most of the methods.
Selection of the pins is done at creation time:
busio.SPI(clock, MOSI, MISO)

• clock (Pin) – the pin to be used for the clock (SCK).


• MOSI (Pin) – the pin used to transmit on.
• MISO (Pin) – the pin used to receive.

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

• bits (int) – the number of bits per word. The default is 8.

The following methods are available:


try_lock()
Attempts to grab the SPI lock. Returns True on success.
unlock()
Releases the SPI lock.
write(buffer, start, end)
Write the data contained in the buffer. The SPI object must be locked. If the buffer is empty,
nothing happens.
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 written will be the length of
buffer[start:end].
Parameters:

• buffer (ReadableBuffer) – write out bytes from this buffer.


• start (int) – beginning of buffer slice; 0 if not specified.
• end (int) – end of buffer slice; if not specified, use len(buffer).

readinto(buffer, start, end, write_value)


Read into buffer while writing write_value for each byte read. The SPI object must be locked.
If the number of bytes to read is 0, nothing happens.
If start or end is provided, then the buffer will be sliced as if buffer[start:end] were passed.
The number of bytes read will be the length of buffer[start:end].
Parameters:

• buffer (WriteableBuffer) – read bytes into this buffer


• start (int) – beginning of buffer slice, 0 if not specified
• end (int) – end of buffer slice; if not specified, it will be the equivalent value
of len(buffer) and for any value provided it will take the value of min(end,
len(buffer))
• write_value (int) – value to write while reading
Interfaces and Protocols 54

write_readinto(out_buffer, in_buffer, out_start, out_end, in_start, in_end)


Write out the data in out_buffer while simultaneously reading data into in_buffer. The SPI
object must be locked.
If out_start or out_end is provided, then the buffer will be sliced as if out_buffer[out_-
start:out_end] were passed, but without copying the data. The number of bytes written will
be the length of buffer[out_start:out_end].
If in_start or in_end is provided, then the buffer will be sliced as if in_buffer[in_start:in_-
end] were passed. The number of bytes read will be the length of buffer[in_start:in_end].
The lengths of the slices defined by out_buffer[out_start:out_end] and in_buffer[in_-
start:in_end] must be equal. If buffer slice lengths are both 0, nothing happens.
Parameters:

• out_buffer (ReadableBuffer) – write out bytes from this buffer.


• in_buffer (WriteableBuffer) – read bytes into this buffer
• out_start (int) – beginning of out_buffer slice; 0 if not specified.
• out_end (int) – end of out_buffer slice; if not specified, use len(out_buffer).
• in_start (int) – beginning of in_buffer slice, 0 if not specified
• in_end (int) – end of in_buffer slice; if not specified, it will be the equivalent value of
len(in_buffer)

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:

• An input buffer to read the level in the wire.


• An open drive driver that can pull the wire to a low level or allow it to fluctuate.

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.

Interconnection of 3.3V and 5V I²C devices

Some clock speeds (modes) are standardized:

• standard: 100 kbits/s


• fast: 400 kbit/s
• fast plus: 1 Mbit/s
• high-speed: 3.4 Mbit/s
• ultra-fast: 5 Mbit/s

The RP2040 does not support these last two modes.

Read and Write Operations in I2 C


The details in this section are somewhat hidden from the programmer but can be handy if you
are studying the datasheet of a sensor or trying to understand why things are not working.
Interfaces and Protocols 57

Start and Stop Conditions

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:

I²C Start Condition and Device Addressing

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

A read operation follows these steps:

• SCL and SDA are high (idle).


• The master pulls down SDA, signaling a START condition. After that the master will pulse
SCL for each bit, the transmitter will change SDA when SCL is LOW and the receiver will
read SDA when SCL changes to HIGH.
Interfaces and Protocols 58

• 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.

I²C Read Operation

Write Operation

A write operation follows these steps:

• SCL and SDA are high (idle).


• The master pulls down SDA, signaling a START condition. After that the master will pulse
SCL for each bit, the transmitter will change SDA when SCL is LOW and the receiver will
read SDA when SCL changes to HIGH.
• The master sends the slave address, followed by a “0” bit (indicating write).
• The slave pulls down SDA, acknowledging the address.
• The master controls SDA, sends 8 bits, and releases SDA.
• The slave pulls down SDA, acknowledging the data.
• The previous two steps can be repeated for more bytes
• The master sends a STOP condition to signal the end of the transaction

I²C Write Operation


Interfaces and Protocols 59

Combined Write/Read 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.

I2 C in the Raspberry Pi Pico


The Raspberry Pi Pico has two I²C peripherals (I2C0 e I2C1) with many features that make them
general and allow fast operation with low overhead.
The RP2040 has a somewhat flexible mapping of pins for the serial interfaces (UART, SPI, and
I2C).
The options for I2C0 are:

Function GPIOs
SDA 0, 4, 8, 12, 16, 20, 24, 28
SCL 1, 5, 9, 13, 17, 21, 25, 29

The options for I2C1 are:

Function GPIOs
SDA 2, 6, 10, 14, 18, 22, 26
SCL 3, 7, 11, 15, 19, 23, 27

Using I2 C With the C/C++ SDK


The I²C functions are in the library hardware_i2c. The i2c parameter should be i2c0 or i2c1.
The SDK functions we are going to use are:
uint i2c_init (i2c_inst_t *i2c, uint baudrate)
Initializes an I²C peripheral for master mode, setting the clock configurations for baudrate. For
slave mode, call i2c_set_slave_mode after this function; baudrate must be informed (although
the clock is not generated in this case) for the right configuration.
This function must be called before the others.
Returns the actual baudrate.
int i2c_write_blocking (i2c_inst_t *i2c, uint8_t addr, const uint8_t *src,
size_t len, bool nostop)
Interfaces and Protocols 60

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).

Using I2 C in the Arduino Environment


To use the I²C interface, you have to add at the top of your Arduino program:

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

The methods we are going to use are:


void begin(void)
Initialize the I²C interface.
void beginTransmition(byte address)
Prepares to transmit bytes to the device with the given address.
size_t write(byte value)
size_t write(String string)
size_t write(byte *mybuf, size_t len)
Places data in the library buffer to be sent later by endTransmission().
Returns the number of bytes put in the buffer.
byte endTransmition(void)
byte endTransmition(bool stop)
Send the data in the buffer. If stop is omitted or true, a stop condition is sent at the end of the
buffer, releasing the bus. If stop is false, a restart condition is sent and the bus is not released.
Returns:

• 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

size_t requestFrom(uint8_t address, size_t quantity, bool stop)


size_t requestFrom(uint8_t address, size_t quantity)
Requests quantity bytes from the device with the given address. Waits until all available data
is received and stores it in the library buffer for reading with read().
If stop is omitted or true, a stop condition is sent at the end of the buffer, releasing the bus. If
stop is false, a restart condition is sent and the bus is not released.
Returns the number of bytes actually received.
int available(void)
Returns the number of bytes available for reading.
int read(void)
Interfaces and Protocols 62

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.

1 from machine import I2C, Pin


2
3 # Init I2C1 with 400KHz clock, using pins 3 and 2
4 i2c = I2C(1, scl=Pin(3), sda=Pin(2), freq=400_000)
5
6 i2c.readfrom(0x3a, 4) # read 4 bytes from device with address 0x3a
7 i2c.writeto(0x3a, '12') # write '12' to device with address 0x3a

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:

• freq is the clock frequency for SCK.


• scl specifies the SCL pin to use.
• sda specifies the SDA pin to use.

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

The standard bus operation methods are:


I2C.readfrom(addr, nbytes, stop=True)
Reads nbytes from the device specified by addr. If stop is true then a STOP condition is
generated at the end of the transfer.
Returns a bytes object with the data read.
I2C.readfrom_into(addr, buf, stop=True)
Read into buf from the device specified by addr. The number of bytes read will be the length of
buf. If stop is true then a STOP condition is generated at the end of the transfer.
This method returns None.
I2C.writeto(addr, buf, stop=True)
Write the bytes from buf to the device specified by addr. If a NACK is received following the
write of a byte from buf then the remaining bytes are not sent.
If stop is true then a STOP condition is generated at the end of the transfer, even if a NACK is
received. The function returns the number of ACKs that were received.
The memory operation methods are:
I2C.readfrom_mem(addr, memaddr, nbytes, *, addrsize=8)
Reads nbytes from the device specified by addr starting from the internal address specified by
memaddr. The argument addrsize specifies the address size in bits.
Returns a bytes object with the data read.
I2C.readfrom_mem_into(addr, memaddr, buf, *, addrsize=8)
Read into buf from the device specified by addr starting from the internal address specified by
memaddr. The number of bytes read is the length of buf. The argument addrsize specifies the
address size in bits.
This method returns None.
I2C.writeto_mem(addr, memaddr, buf, *, addrsize=8)
Write buf to the device specified by addr starting from the internal address specified by memaddr.
The argument addrsize specifies the address size in bits.
This method returns None.
Interfaces and Protocols 64

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)

• scl (Pin) – the pin to be used for the clock (SCL).


• sda (Pin) – the pin used for data (SDA).
• frequency (int) - the clock frequency in Hz.

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:

• address (int) – 7-bit device address


• buffer (WriteableBuffer) – buffer to write into
• start (int) – beginning of buffer slice; 0 if not specified
• end (int) – end of buffer slice; if not specified, use len(buffer)
Interfaces and Protocols 65

writeto(address, buffer, start, end)


Write the bytes from buffer to the device selected by address and then transmit a stop condition.
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 written will be the length of
buffer[start:end].
Writing a buffer or slice of length zero is permitted, as it can be used to poll for the existence of
a device.
Parameters:

• address (int) – 7-bit device address


• buffer (ReadableBuffer) – buffer containing the bytes to write
• start (int) – beginning of buffer slice; 0 if not specified
• end (int) – end of buffer slice; if not specified, use len(buffer)

writeto_then_readfrom(address, out_buffer, in_buffer, out_start, out_end,


in_start, in_end)
Write the bytes from out_buffer to the device selected by address, generate a restart, and
read into in_buffer. out_buffer and in_buffer can be the same buffer because they are used
sequentially.
If out_start or out_end is provided, then the buffer will be sliced as if out_buffer[out_-
start:out_end] were passed, but without copying the data. The number of bytes written will
be the length of out_buffer[out_start:out_end].
If in_start or in_end is provided, then the input buffer will be sliced as if in_buffer[in_-
start:in_end] were passed, The number of bytes read will be the length of in_buffer[in_-
start:in_end].
Parameters:

• address (int) – 7-bit device address


• out_buffer (ReadableBuffer) – buffer containing the bytes to write
• in_buffer (WriteableBuffer) – buffer to write into
• out_start (int) – beginning of out_buffer slice; 0 if not specified
• out_end (int) – end of out_buffer slice; if not specified, use len(out_buffer)
• in_start (int) – beginning of in_buffer slice; 0 if not specified
• in_end (int) – end of in_buffer slice; if not specified, use len(in_buffer)
Interfaces and Protocols 66

Finding the I2 C Address of a Device


Sometimes you get a new I²C device and you are not sure what is its address. You can find it
easily using MicroPython.
Load MicroPython in your Raspberry Pi Pico, disconnect the Pico from the PC, and connect your
device to it:
Device Pico
Vcc 3V3 or VBUS (5V) as needed by the device

| 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:

1 from machine import I2C


2 i2c = I2C(0)
3 print (i2c.scan())

Finding I²C Address


Basic Digital Sensors
The sensors in this chapter have a single digital output that can signal one of two conditions and
are connected to a digital input.
The examples here are simple, but they illustrate the basic techniques of testing a digital input.

Read This Before Trying the Examples


There are many variations of the sensors we are going to use. It is very common for a sensor
design to be copied by many manufacturers, and the copies are not always identical.
To avoid the frustration of an example not working (or, worse, damaging your Pi Pico), it’s
important to get and study the documentation of the particular sensor you got. Some key points
to check:

• 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.

In the examples I show some ways to use the output of a 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.

Button Use Example


The figure below shows the assembly we are going to use as an example.

Connecting a Button to the Pi Pico

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.

C/C++ SDK Code

C/C++ SDK Button Example

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

Arduino Button Example

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

MicroPython Button Example

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

Again a class is used to encapsulate the button code.

CircuitPython Code
Basic Digital Sensors 76

CircuitPython Button Example

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

39 while not button.isPressed():


40 time.sleep(0.001)
41 counter = counter+1

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.

Detecting a Key - First Version

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

Detecting a Key - Problem with the First Version

There are three simple ways to solve this:

• Include resistors in series with the row connections.


• Include diodes in series with the row connections.
• Instead of putting the other rows in a LOW level, reconfigure the pins to input. This way
they will behave as if unconnected (more precisely, as high impedance).

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.

Detecting a Key - Ghosting


Basic Digital Sensors 80

This can be solved by placing a diode in series with each button.

Detecting a Key - Using Diodes

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

Connecting a Keypad to the Pi Pico

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.

C/C++ SDK Code


Basic Digital Sensors 82

C/C++ SDK Keypad Example

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

39 uint32_t previous [NROWS];


40 uint32_t validated [NROWS];
41 int count [NROWS][NCOLUMNS];
42
43 // Key codes
44 char codes[NROWS][NCOLUMNS] = {
45 { '1', '2', '3' },
46 { '4', '5', '6' },
47 { '7', '8', '9' },
48 { '*', '0', '#' }
49 };
50
51 // Keys queue
52 #define MAX_KEYS 16
53 char keys[MAX_KEYS+1];
54 int inkey, outkey;
55
56 // Timer to scan the keypad
57 static struct repeating_timer timer;
58
59 // Check for pressed keys in current row
60 bool checkRow(struct repeating_timer *t) {
61 // set current row to LOW
62 int pin = rows[curRow];
63 gpio_set_dir (pin, true);
64 gpio_put(pin, false);
65
66 // read columns and check for changes
67 for (int col = 0; col < NCOLUMNS; col++) {
68 uint32_t msk = 1u << columns[col];
69 uint32_t read = gpio_get_all() & msk;
70 if (read == (previous[curRow] & msk)) {
71 if (count[curRow][col] != 0) {
72 if (--count[curRow][col] == 0) {
73 // reading validated
74 if (read != (validated[curRow] & msk)) {
75 validated[curRow] = (validated[curRow] & ~msk) | read;
76 if (read == 0) {
77 // keypress detected
Basic Digital Sensors 84

78 int aux = inkey+1;


79 if (aux > MAX_KEYS) {
80 aux = 0;
81 }
82 if (aux != outkey) {
83 keys[inkey] = codes[curRow][col];
84 inkey = aux;
85 }
86 }
87 }
88 }
89 }
90 } else {
91 // restart validation
92 previous[curRow] = (previous[curRow] & ~msk) | read;
93 count[curRow][col] = DEBOUNCE;
94 }
95 }
96 // return row to input
97 gpio_set_dir (pin, false);
98 // move to next row
99 curRow = (curRow+1) % NROWS;
100
101 return true; // keep executing
102 }
103
104 // Read a key from the key queue, returns -1 if queue empty
105 static int getKey(void) {
106 int key = -1;
107 if (inkey != outkey) {
108 key = keys[outkey];
109 outkey = (outkey == MAX_KEYS) ? 0 : (outkey + 1);
110 }
111 return key;
112 }
113
114 // Main Program
115 int main() {
116 // Init stdio
Basic Digital Sensors 85

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

Arduino Keypad Example

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

30 { '*', '0', '#' }


31 };
32
33 // Keys queue
34 #define MAX_KEYS 16
35 char keys[MAX_KEYS+1];
36 int inkey, outkey;
37
38 // Check for pressed keys in current row
39 void checkRow() {
40 // set current row to LOW
41 int pin = rows[curRow];
42 pinMode (pin, OUTPUT);
43 digitalWrite(pin, LOW);
44
45 // read columns and check for changes
46 for (int col = 0; col < NCOLUMNS; col++) {
47 bool read = digitalRead (columns[col]) == LOW;
48 if (read == previous[curRow][col]) {
49 if (count[curRow][col] != 0) {
50 if (--count[curRow][col] == 0) {
51 // reading validated
52 if (read != validated[curRow][col]) {
53 validated[curRow][col] = read;
54 if (read) {
55 // keypress detected
56 int aux = inkey+1;
57 if (aux > MAX_KEYS) {
58 aux = 0;
59 }
60 if (aux != outkey) {
61 keys[inkey] = codes[curRow][col];
62 inkey = aux;
63 }
64 }
65 }
66 }
67 }
68 } else {
Basic Digital Sensors 88

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

108 for (int i = 0; i < NROWS; i++) {


109 pinMode(rows[i], INPUT);
110 }
111 for (int i = 0; i < NCOLUMNS; i++) {
112 pinMode(columns[i], INPUT_PULLUP);
113 }
114 }
115
116 // Main Loop
117 void loop() {
118 checkRow();
119 int key = getKey();
120 if (key == -1) {
121 delay(10);
122 } else {
123 Serial.print ("Pressed ");
124 Serial.println ((char) key);
125 Serial.flush ();
126 }
127 }

This is similar to the SDK code, but the check for keys is done in the main loop.

MicroPython Code

MicroPython Keypad Example

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

13 self.rows = [Pin(pin, Pin.IN) for pin in rowPins]


14 self.columns = [Pin(pin, Pin.IN, Pin.PULL_UP) for pin in columnPins]
15 self.curRow = 0
16 self.previous = [[[0] for _ in range(self.nc)] for _ in range(self.nr)]
17 self.count = [[[self.debounce] for _ in range(self.nc)] for _ in range(self.nr)]
18 self.validated = [[[0] for _ in range(self.nc)] for _ in range(self.nr)]
19 self.keys = []
20
21 # check next row
22 def checkRow(self):
23 # set row to LOW
24 pin = self.rows[self.curRow]
25 pin.init(mode=Pin.OUT)
26 pin.low()
27 # read columns and check for changes
28 for col in range(self.nc):
29 read = self.columns[col].value()
30 if read == self.previous[self.curRow][col]:
31 if self.count[self.curRow][col] != 0:
32 self.count[self.curRow][col] = self.count[self.curRow][col] - 1
33 if self.count[self.curRow][col] == 0:
34 # reading validated
35 if read != self.validated[self.curRow][col]:
36 self.validated[self.curRow][col] = read
37 if read == 0:
38 # keypress detected
39 self.keys.append((self.curRow, col))
40 else:
41 # restart validation
42 self.previous[self.curRow][col] = read
43 self.count[self.curRow][col] = self.debounce
44 # return row to input
45 pin.init(mode=Pin.IN)
46 # move to next row
47 self.curRow = self.curRow + 1
48 if self.curRow == self.nr:
49 self.curRow = 0
50
51 # return next key pressed
Basic Digital Sensors 91

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

CircuitPython Keypad Example

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 (SW-420)


There are many types of vibration sensors. The module I will use here is based on the SW-420
sensor, an omnidirectional option with low cost and good sensitivity.

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.

Vibration Sensor Example


The diagram below shows the connection of the sensor to the Raspberry Pi Pico.
Basic Digital Sensors 95

Connecting a Vibration Sensor to the Pi Pico

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.

C/C++ SDK Code

C/C++ SDK Vibration Sensor Example

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

Arduino Vibration Sensor Example

1 // Vibration Sensor Example


2
3 #define SENSOR_PIN 16
4
5 #define N_SAMPLES 100
6
7 // Samples storage
8 uint8_t vibr[N_SAMPLES];
9 int inVibr, outVibr, nVibr;
10 int count;
11
12 // Initializations
13 void setup() {
14 pinMode (LED_BUILTIN, OUTPUT);
15 digitalWrite (LED_BUILTIN, LOW);
16 pinMode (SENSOR_PIN, INPUT);
17 inVibr = outVibr = nVibr = 0;
18 count = 0;
19 }
20
21 // Main Loop
22 void loop() {
23 delay(10);
24 if (nVibr == N_SAMPLES) {
25 // Remove oldest
26 count -= vibr[outVibr];
27 outVibr = (outVibr+1) % N_SAMPLES;
28 } else {
Basic Digital Sensors 98

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

MicroPython Vibration Sensor Example

1 # Vibration Sensor Example


2
3 from machine import Pin
4 import time
5
6 sensor = Pin(16, Pin.IN)
7 led = Pin(25, Pin.OUT)
8 led.off()
9 vibr = []
10 count = 0
11 while True:
12 time.sleep_ms(10)
13 if len(vibr) >= 100:
14 # discard oldest
15 count = count - vibr.pop()
16 vibr.insert(0, sensor.value())
17 count = count + vibr[0]
18 if count > 80:
19 led.on()
20 else:
21 led.off()

CircuitPython Code
Basic Digital Sensors 99

CircuitPython Vibration Sensor Example

1 # Vibration Sensor Example


2
3 import digitalio
4 import board
5 import time
6
7 sensor = digitalio.DigitalInOut(board.GP16)
8 led = digitalio.DigitalInOut(board.GP25)
9 led.direction = digitalio.Direction.OUTPUT
10 led.value = False
11 vibr = []
12 count = 0
13 while True:
14 time.sleep(0.01)
15 if len(vibr) >= 100:
16 # discard oldest
17 count = count - vibr.pop()
18 vibr.insert(0, sensor.value)
19 count = count + vibr[0]
20 led.value = count > 80

Presence Sensor (PIR)


Presence sensors by infrared, also called PIR (passive infrared), are sensors that detect infrared
light in the frequency corresponding to the temperature of “warm blood animals”. This is a
common sensor for alarms and automatic lights.

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:

• Power supply: 4.5 to 20 VDC


• Output: 3.3V (HIGH level) indicates the presence

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.

Presence (PIR) Sensor Example


The example will output a count of the number of times the sensor is triggered.
The diagram below shows the connection of the sensor to the Raspberry Pi Pico. Check the pins
by the markings under the plastic enclosure
Basic Digital Sensors 101

Connecting a PIR Sensor to the Pi Pico

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.

C/C++ SDK Code

C/C++ SDK PIR Example

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

Arduino PIR Example

1 // Presence (PIR) Sensor Example


2
3 #define SENSOR_PIN 16
4
5 // Global variables
6 int counter = 0;
7
8 // Initializations
9 void setup() {
10 pinMode (SENSOR_PIN, INPUT);
11 }
12
13 // Main Loop
14 void loop() {
15 Serial.print ("Sensor Triggered ");
16 Serial.print (counter);
17 Serial.println (" times");
18 while (digitalRead(SENSOR_PIN) == LOW)
19 ;
20 while (digitalRead(SENSOR_PIN) == HIGH)
21 ;
22 counter++;
23 }

MicroPython Code

MicroPython PIR Example

1 # Presence (PIR) Sensor Example


2
3 from machine import Pin
4 import time
5
6 sensor = Pin(16, Pin.IN)
7 counter = 0
8 while True:
9 print ("Sensor triggered {} times".format(counter))
10 while sensor.value() == 0:
Basic Digital Sensors 104

11 time.sleep_ms(1)
12 while sensor.value() == 1:
13 time.sleep_ms(1)
14 counter = counter+1

CircuitPython Code

CircuitPython PIR Example

1 # Presence (PIR) Sensor Example


2
3 import digitalio
4 import board
5 import time
6
7 sensor = digitalio.DigitalInOut(board.GP16)
8 counter = 0
9 while True:
10 print ("Sensor triggered {} times".format(counter))
11 while sensor.value == 0:
12 time.sleep(0.001)
13 while sensor.value == 1:
14 time.sleep(0.001)
15 counter = counter+1

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.

Flame Sensor Example


The diagram below shows the connection of the sensor to the Raspberry Pi Pico. We are also
using a buzzer, a device that emits a sound when powered. You should use a 3V active buzzer (a
buzzer that generates internally a fixed frequency).
Basic Digital Sensors 106

Connecting a Flame Sensor to the Pi Pico

The code to test the Flame sensor detects when the output is LOW and activates the buzzer for
half a second.

C/C++ SDK Code

C/C++ SDK Flame Example


1 /**
2 * @file flame_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Flame 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 "pico/stdlib.h"
14 #include "hardware/gpio.h"
15
16
17 #define BUZZER_PIN 15
18 #define SENSOR_PIN 16
19
Basic Digital Sensors 107

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

Arduino Flame Example

1 // Flame Sensor Example


2
3 #define BUZZER_PIN 15
4 #define SENSOR_PIN 16
5
6 // Initializations
7 void setup() {
8 pinMode (BUZZER_PIN, OUTPUT);
9 digitalWrite (BUZZER_PIN, LOW);
10 pinMode (SENSOR_PIN, INPUT);
11 }
12
13 // Main Loop
14 void loop() {
15 if (digitalRead(SENSOR_PIN) == LOW) {
Basic Digital Sensors 108

16 digitalWrite (BUZZER_PIN, HIGH);


17 delay(500);
18 digitalWrite (BUZZER_PIN, LOW);
19 }
20 }

MicroPython Code

MicroPython Flame Example

1 # Flame Sensor Example


2
3 from machine import Pin
4 import time
5
6 sensor = Pin(16, Pin.IN)
7 buzzer = Pin(15, Pin.OUT)
8 buzzer.off()
9 while True:
10 if sensor.value() == 0:
11 buzzer.on()
12 time.sleep(0.5)
13 buzzer.off()

CircuitPython Code

CircuitPython Flame Example

1 # Flame Sensor Example


2
3 import digitalio
4 import board
5 import time
6
7 sensor = digitalio.DigitalInOut(board.GP16)
8 buzzer = digitalio.DigitalInOut(board.GP15)
9 buzzer.direction = digitalio.Direction.OUTPUT
10 buzzer.value = False
11
Basic Digital Sensors 109

12 while True:
13 if sensor.value == 0:
14 buzzer.value = True
15 time.sleep(0.5)
16 buzzer.value = False

Digital Sound Sensor


A digital sound sensor uses a microphone (normally an electret condenser microphone) to capture
sound. The output of the microphone is connected to a comparator (the other input of the
comparator is a reference voltage set by a potentiometer) to generate the digital output.

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.

Digital Sound Sensor Example


The diagram below shows the connection of the sensor to the Raspberry Pi Pico.
Basic Digital Sensors 110

Connecting a Sound Sensor to the Pi Pico

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.

C/C++ SDK Code

C/C++ SDK Sound Sensor Example

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

Arduino Sound Sensor Example


1 // Sound Sensor Example
2
3 #define SENSOR_PIN 16
4
5 // Initializations
6 void setup() {
7 pinMode (LED_BUILTIN, OUTPUT);
8 digitalWrite (LED_BUILTIN, LOW);
9 pinMode (SENSOR_PIN, INPUT);
10 }
11
12 // Main Loop
13 void loop() {
14 if (digitalRead(SENSOR_PIN) == HIGH) {
15 digitalWrite (LED_BUILTIN, HIGH);
16 delay(500);
17 digitalWrite (LED_BUILTIN, LOW);
18 }
19 }

MicroPython Code

MicroPython Sound Sensor Example


1 # Sound Sensor Example
2
3 from machine import Pin
4 import time
5
6 sensor = Pin(16, Pin.IN)
7 led = Pin(25, Pin.OUT)
8 led.off()
9 while True:
10 if sensor.value() == 1:
11 led.on()
12 time.sleep(0.5)
13 led.off()
14
Basic Digital Sensors 113

CircuitPython Code

CircuitPython Sound Sensor Example


1 # Sound Sensor Example
2
3 import digitalio
4 import board
5 import time
6
7 sensor = digitalio.DigitalInOut(board.GP16)
8 led = digitalio.DigitalInOut(board.GP25)
9 led.direction = digitalio.Direction.OUTPUT
10 led.value = False
11
12 while True:
13 if sensor.value == 1:
14 led.value = True
15 time.sleep(0.5)
16 led.value = False

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.

MQ Gas Sensor Example


The diagram below shows the connection of the sensor to the Raspberry Pi Pico.
Basic Digital Sensors 115

Connecting a Gas Sensor to the Pi 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.

C/C++ SDK Code


Basic Digital Sensors 116

C/C++ SDK MQ Sensor Example


1 /**
2 * @file gas_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief Gas 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
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 }
Basic Digital Sensors 117

Arduino Code

Arduino MQ Sensor Example

1 // Gas Sensor Example


2
3 #define LED_PIN 25
4 #define SENSOR_PIN 16
5
6 // Initialization
7 void setup() {
8 // Init LED gpio
9 pinMode(LED_PIN, OUTPUT);
10 digitalWrite(LED_PIN, LOW);
11
12 // Init sensor gpio
13 pinMode (SENSOR_PIN, INPUT);
14 }
15
16 // Main loop
17 void loop() {
18 if (digitalRead(SENSOR_PIN) == HIGH) {
19 digitalWrite(LED_PIN, HIGH);
20 delay (500);
21 digitalWrite(LED_PIN, LOW);
22 }
23 }

MicroPython Code
Basic Digital Sensors 118

MicroPython MQ Sensor Example

1 # Gas Sensor Example


2
3 from machine import Pin
4 import time
5
6 sensor = Pin(16, Pin.IN)
7 led = Pin(25, Pin.OUT)
8 led.off()
9 while True:
10 if sensor.value() == 1:
11 led.on()
12 time.sleep(0.5)
13 led.off()

CircuitPython Code

CircuitPython MQ Sensor Example

1 # Gas Sensor Example


2
3 import digitalio
4 import board
5 import time
6
7 sensor = digitalio.DigitalInOut(board.GP16)
8 led = digitalio.DigitalInOut(board.GP25)
9 led.direction = digitalio.Direction.OUTPUT
10 led.value = False
11
12 while True:
13 if sensor.value == 1:
14 led.value = True
15 time.sleep(0.5)
16 led.value = False
Basic Digital Sensors 119

Digital Hall Effect Sensors


The Hall Effect is “the creation of a potential difference across an electrical conductor that is
transverse to an electric current in the conductor and to an applied magnetic field perpendicular
to the current” (from https://en.wikipedia.org/wiki/Hall_effect).
Simplifying, if you take an electrical conductor connected to a power supply (so there is a current
flowing through it) and submit it to an magnetic field, a potential difference (“a voltage”) will
appear between the edges of the conductor that are perpendicular to the current and the magnetic
field.
A Hall effect sensor uses this effect in a semiconductor to detect a magnetic field. There are many
variations of this sensor, here we will ignore the ones with an analog output and examine two
popular models that have a digital output.
Physically these sensors look like transistors, with power, ground, and output pins. An important
point to look at in the documentation is the orientation of the sensor.
In most applications, the magnetic field will be generated by a small magnet. A Hall Effect Sensor
can be used in place of a reed switch to get a better range (or sensitivity) and better reliability (as
it has no moving parts).

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.

KY-003 Hall Effect Sensor

This sensor operates with 5 VDC.


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
Basic Digital Sensors 120

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.

US1881 Hall Effect Sensor


This sensor operates with voltages from 3.5 a 24 VDC.

US1881 Hall Effect Sensor

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.

Hall Effect Sensor Example


In this example, we are going to use the Hall effect sensor to build a simple tachometer. We will
attach a disc with two magnets to a motor and use the sensor to detect one of the magnets. By
measuring the time between detections we can find out how many revolutions per minute the
motor is doing.
Why two magnets? So we can use the same setup for both sensors. We will position the magnets
so that each shows a different pole to the sensor. The A3144 will ignore one of the magnets and
the US1881 will change from LOW to HIGH with one of them and back to LOW with the other.
To automate the test, we will use a stepper motor controlled by the Pico; its speed will change
randomly from time to time. A stepper motor is a motor that moves in small increments
(the steps). The movement is generated by the interaction between permanent magnets and
electromagnets. To move a step we need to activate and deactivate the electromagnets in a certain
order.
In this example, we are going to use a very popular stepper (model 28BYJ-48). It is normally sold
with a small driver board based on a ULN2003 chip.
The following figures show the assembly, notice that only the sensor changes between them.

Hall Effect Sensor Example with KT-003


Basic Digital Sensors 122

Hall Effect Sensor Example with US1881

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.

C/C++ SDK Code


Basic Digital Sensors 123

C/C++ SDK Hall Sensor Example

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

117 #ifdef LIB_PICO_STDIO_USB


118 while (!stdio_usb_connected()) {
119 sleep_ms(100);
120 }
121 #endif
122
123 printf("\nHall Effect Example\n");
124
125 Stepper stepper(STEPPER_1, STEPPER_2, STEPPER_3, STEPPER_4);
126 HallSensor sensor(SENSOR_PIN);
127
128 // Init LED gpio
129 gpio_init (LED_PIN);
130 gpio_set_dir (LED_PIN, true);
131 gpio_put (LED_PIN, false);
132
133 // Main loop
134 while(1) {
135 // choose a random speed
136 int delay = (rand() % 1700) + 1500;
137 printf ("Delay = %.1f ms\n", delay/1000.0);
138 uint32_t changeTime = board_millis() + 30000;
139 while (board_millis() < changeTime) {
140 stepper.onestep();
141 if (sensor.detect()) {
142 gpio_put (LED_PIN, true);
143 uint32_t elapsed = sensor.getElapsed();
144 if (elapsed != 0) {
145 printf ("RPM = %.1f\n", 60000.0 / elapsed);
146 }
147 sleep_us(delay);
148 gpio_put (LED_PIN, false);
149 }
150 else {
151 sleep_us(delay);
152 }
153 }
154 }
155 }
Basic Digital Sensors 127

Arduino Code

Arduino Hall Sensor Example

1 // Hall Efect Sensor Example


2
3 // Pins used
4 #define LED_PIN 25
5 #define SENSOR_PIN 16
6 #define STEPPER_1 2
7 #define STEPPER_2 3
8 #define STEPPER_3 4
9 #define STEPPER_4 5
10
11 // what pins to turn on at each step
12 int steps[4][4] = {
13 { HIGH, LOW, LOW, HIGH },
14 { HIGH, HIGH, LOW, LOW },
15 { LOW, HIGH, HIGH, LOW },
16 { LOW, LOW, HIGH, HIGH }
17 };
18
19 // Stepper motor control class
20 class Stepper {
21 private:
22 int pins[4];
23 int step;
24
25 void setpins (const int *values) {
26 for (int i = 0; i < 4; i++) {
27 digitalWrite (pins[i], values[i]);
28 }
29 }
30
31 public:
32 // Constructor
33 Stepper (int pin1, int pin2, int pin3, int pin4) {
34 pins[0] = pin1;
35 pins[1] = pin2;
36 pins[2] = pin3;
Basic Digital Sensors 128

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

115 uint32_t elapsed = sensor.getElapsed();


116 if (elapsed != 0) {
117 Serial.print ("RPM = ");
118 Serial.println (60000.0 / elapsed);
119 }
120 sleep_us(delay);
121 digitalWrite (LED_PIN, LOW);
122 }
123 else {
124 sleep_us(delay);
125 }
126 }
127 }

MicroPython Code

MicroPython Hall Sensor Example

1 # Hall Effect Sensor Example


2
3 from machine import Pin
4 import time
5 import random
6
7 # Stepper motor control class
8 class Stepper():
9 # set motor control pins
10 def setpins(self, val=[0,0,0,0]):
11 for i in range(len(self.pins)):
12 self.pins[i].value(val[i])
13
14 # init
15 def __init__(self, pins=None):
16 if pins is None:
17 raise ValueError("Must specify pins!")
18 if len(pins) != 4:
19 raise ValueError("There must be 4 pins")
20 self.pins = pins
21 self.steps = [[1,0,0,1], [1,1,0,0], [0,1,1,0], [0,0,1,1]]
Basic Digital Sensors 131

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

CircuitPython Hall Sensor Example

1 # Hall Effect Sensor Example


2
3 import digitalio
4 import board
5 import time
6 import random
7
8 def time_ms():
9 return time.monotonic_ns() // 1000000
10
11 # Stepper motor control class
12 class Stepper():
13 # set motor control pins
14 def setpins(self, val=[0,0,0,0]):
15 for i in range(len(self.pins)):
16 self.pins[i].value = val[i]
17
18 # init
19 def __init__(self, pins=None):
20 if pins is None:
21 raise ValueError("Must specify pins!")
22 if len(pins) != 4:
23 raise ValueError("There must be 4 pins")
24 self.pins = []
25 for pin in pins:
26 digPin = digitalio.DigitalInOut(pin)
27 digPin.direction = digitalio.Direction.OUTPUT
28 self.pins.append(digPin)
29 self.steps = [[1,0,0,1], [1,1,0,0], [0,1,1,0], [0,0,1,1]]
30 self.setpins()
31 self.step = 0
32
33 # advance one step
34 def onestep(self):
35 self.setpins(self.steps[self.step])
36 self.step = self.step+1
37 if self.step >= len(self.steps):
38 self.step = 0
Basic Digital Sensors 134

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

You will find two types of potentiometers: linear and logarithm.


In a linear potentiometer the resistance is directly proportional to the position of the wiper. For
example, you will get half the full resistance in the center position.
In a logarithm potentiometer the resistance is roughly proportional to the logarithm of the
position of the wiper. In the center position, you get less than half the full resistance. Logarithmic
potentiometers are often used for volume or signal level in audio systems, as human perception
of audio volume is logarithmic.
To reduce costs, logarithm potentiometers usually are made by using two (linear) resistive
elements that overlap at the middle position, the result is good enough for our ears.
The markings in a potentiometer usually show the full resistance and a letter indicating the type
(A for logarithm and B for linear). In the previous photo, we have a linear 1kΩ potentiometer.
Analog Sensors 137

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.

Connecting a Potentiometer to the Pi Pico

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.

C/C++ SDK Code


Most of the code in the SDK example is to set up the PWM for the LED. PWM in the RP2040
microcontroller is implemented by the PWM peripheral. It has sixteen slices, each one with two
channels. With the SDK we have great control over the clock and the wrap value.
The ADC returns a 12-bit result (0 to 4095), so we program the PWM for a wrap value of 4095
and use the ADC reading as the count to change from HIGH to LOW.
C/C++ Potentiometer Example

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

CircuitPython Potentiometer Example

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:

• Resistance will decrease as we move the stick to the right


• Resistance will increase as we move to the left

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.

Analog Joystick Example


In this example we are going to use four LEDs, two on each axis, to indicate the readings of the
joystick. PWM is used to control the brightness of the LEDs (see explanations in the Potentiometer
example).

Connecting an Analog Joystick to the Pi Pico


Analog Sensors 144

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.

C/C++ SDK Code


A class is used to avoid repetition of the PWM code for each LED.
C/C++ Joystick Example

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

109 } else if (horiz <= (2047 - DEAD_ZONE)) {


110 ledRight.set(2047-horiz);
111 ledLeft.set(0);
112 } else {
113 ledRight.set(0);
114 ledLeft.set(0);
115 }
116
117 if (vertic > (2047 + DEAD_ZONE)) {
118 ledDown.set(vertic-2048);
119 ledUp.set(0);
120 } else if (vertic <= (2047 - DEAD_ZONE)){
121 ledDown.set(0);
122 ledUp.set(2047-vertic);
123 } else {
124 ledDown.set(0);
125 ledUp.set(0);
126 }
127 }
128 }

Arduino Code

Arduino Joystick Example

1 // Analog Joystick Example


2
3 // LED connections
4 #define LED_RIGHT 12
5 #define LED_UP 13
6 #define LED_DOWN 14
7 #define LED_LEFT 15
8
9 // Joystick Connections
10 #define PIN_HORIZ A0
11 #define PIN_VERTIC A1
12
13 // Ignore small variations around the center
14 #define DEAD_ZONE 30
Analog Sensors 148

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

MicroPython Joystick Example

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

CircuitPython Joystick Example

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

Light-Dependent Resistors (LDR)


Light-dependent resistors (LDRs or photo-resistors) are components whose resistance varies with
the intensity of light on them. Typical resistance in the dark is above 1MΩ, with intense light it
will drop to a few hundred ohms.
Analog Sensors 152

Light Detecting Resistors

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:

Measuring an LDR Resistance

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).

LDR Example with the Raspberry Pi Pico

You can experiment with changing the light/dark threshold and the range of sleep times.

C/C++ SDK Code

C/C++ LDR Example

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

Arduino LDR Example

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

MicroPython LDR Example

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

CircuitPython LDR Example

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

Phototransistor (clear case)

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.

Phototransistor Example with the Raspberry Pi Pico

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⁴.

C/C++ SDK Code

C/C++ Phototransistor Example

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

30 // Init sensor ADC


31 adc_init();
32 adc_gpio_init(SENSOR_PIN);
33 adc_select_input(0);
34
35 // Main loop
36 uint16_t val_ant = 0;
37 while(1) {
38 uint16_t val = adc_read(); // 0-4095
39 int dif = (val > val_ant)? val - val_ant : val_ant - val;
40 if (dif > 300) {
41 printf("%u\n", val);
42 val_ant = val;
43 }
44 }
45 }

Arduino Code

Arduino Phototransistor Example

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

CircuitPython Phototransistor Example


1 # Phototransistor example
2
3 import analogio
4 import digitalio
5 import board
6
7 # Phototransistor at ADC0
8 sensor = analogio.AnalogIn(board.A0)
9
10 val_ant = 0
11
12 # Main loop
13 while True:
14 val = sensor.value
15 dif = abs(val - val_ant)
16 if dif > 5000:
17 print(val)
18 val_ant = val

Using LEDs as a Light Sensor


Light-emitting diodes (LEDs) can also be used as light sensors. An LED is a photodiode sensitive
to light at and above the wavelength it emits (barring any filtering effects of a colored plastic
package). Visible light wavelengths can be listed from longest wavelength to shortest wavelength
as Red, Orange, Yellow, Green, Blue, Indigo, and Violet. A green LED is sensitive to blue light and
to some green light, but not to yellow or red light. When exposed to light photodiodes produce a
current that is directly proportional to the intensity of the light.
To use a LED as a light sensor, we have to reverse bias it, that is, wire it so the cathode (negative
terminal) is at a higher voltage level than the anode (positive terminal).
Analog Sensors 162

LED as a Light Sensor Example


The current generated by the LED is small, so we are going to use a transistor to amplify it.

Connecting a LED as a Light Sensor to the Pi Pico

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.

C/C++ SDK Code


Analog Sensors 163

C/C++ LED Light Sensor Example

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

Arduino LED Light Sensor Example

1 // LED as ligh sensor 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 uint32_t sum = 0;
14 for (int i = 0; i < 20; i++) {
15 sum += analogRead(SENSOR_PIN);
16 }
17 Serial.println(sum/20);
18 delay(2000);
19 }

MicroPython Code
Analog Sensors 165

MicroPython LED Light Sensor Example

1 # LED as Light Sensor Example


2
3 from machine import Pin, ADC
4 from time import sleep
5
6 sensor = ADC(Pin(26))
7
8 def val():
9 sum = 0
10 for i in range(20):
11 sum = sum + sensor.read_u16();
12 return sum//20
13
14 while True:
15 print (val() // 100)
16 sleep(2)
17
18

CircuitPython Code

CircuitPython LED Light Sensor Example

1 # LED as Light Sensor Example


2
3 import analogio
4 import digitalio
5 import board
6 from time import sleep
7
8 sensor = analogio.AnalogIn(board.A0)
9
10 def val():
11 sum = 0
12 for i in range(20):
13 sum = sum + sensor.value;
14 return sum//20
15
Analog Sensors 166

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).

Analog Gas Sensor Example


The diagram below shows the connection of the sensor to the ADC of the Raspberry Pi Pico.

Connecting a Gas Sensor to the ADC of the Pi Pico

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

Analog Hall Effect Sensor


Hall Effect Sensors are also available with analog output. I am using as an example the A1301
from Allegro, another option you may find is the KY-035 sensor module.

A1301 Hall Sensor (left) and KY-035 module (right)

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.

Analog Hall Effect Sensor Example


Here is how to connect the A1301 sensor to a Raspberry Pi Pico. Again we use a resistor divisor
(22kΩ and 33kΩ resistors) to reduce the output from 5V to 3V.
Analog Sensors 168

Connecting a A1301 Hall Sensor to the ADC of the Pi Pico

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.

C/C++ SDK Code

C/C++ Analog Hall Effect Sensor Example

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

Arduino Analog Hall Effect Sensor Example

1 // Hall Effect Alnalog Sensor


2
3 // Pins
4 #define SENSOR_PIN A0
5
6 // Reading for no magnetic field
7 int zero;
8
9 // Read sensor (average 50 readings)
10 #define N_READINGS 50
11 uint readSensor() {
12 uint32_t sum = 0;
13 for (int i = 0; i < N_READINGS; i++) {
14 sum += analogRead(SENSOR_PIN);
15 }
16 return sum / N_READINGS;
17 }
18
19 // initialization
Analog Sensors 171

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

MicroPython Analog Hall Effect Sensor Example

1 # Hall Effect Analog Sensor Example


2
3 from machine import Pin, ADC
4 from time import sleep
5
6 # Sensor is at ADC0
7 sensor = ADC(Pin(26))
8
9 # Get an average of 50 readings
Analog Sensors 172

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

CircuitPython Analog Hall Effect Sensor Example

1 # Hall Effect Analog Sensor Example


2
3 import analogio
4 import board
5 from time import sleep
6
7 # Sensor is at ADC0
8 sensor = analogio.AnalogIn(board.A0)
9
10 # Get an average of 50 readings
11 def val():
12 sum = 0
Analog Sensors 173

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:

Converting a Thermistor’s Resistance into a Voltage

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.

Example of a Thermistor Specification

In the above specifications you will find:

• The resistance at 25°C (R₂₅) and its tolerance


• The beta (B) value and its tolerance
• The range of temperatures for the beta value (“Definition of the B Value”).

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

Connecting a Thermistor to the Pi Pico

You will need to connect the Pico to a PC to interact with the program.

C/C++ SDK Code

C/C++ Thermistor Example

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

55 float rx, beta;


56
57 // Init stdio
58 stdio_init_all();
59 #ifdef LIB_PICO_STDIO_USB
60 while (!stdio_usb_connected()) {
61 sleep_ms(100);
62 }
63 #endif
64
65 // Init sensor ADCs
66 adc_init();
67 adc_gpio_init(THERMISTOR_PIN);
68 adc_select_input(THERMISTOR_PIN-26);
69
70 // Gets references
71 printf("Reference 1\n");
72 getReference (&ref1);
73 printf("Reference 2\n");
74 getReference (&ref2);
75
76 // Compute beta and rx
77 beta = log(ref1.r/ref2.r)/((1/ref1.t)-(1/ref2.t));
78 printf("Beta = %.2f\n", beta);
79 rx = ref1.r * exp(-beta/ref1.t);
80
81 // Main loop
82 while(1) {
83 float rt = getResistance();
84 float t = beta / log(rt/rx);
85 printf("Temperature: %.1f\n", t-273.0);
86 sleep_ms(1000);
87 }
88 }

Arduino Code
Temperature Sensors 180

Arduino Thermistor Example

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

39 Serial.print(" Resistance: ");


40 Serial.println(r, 0);
41 }
42
43 // Initialization
44 void setup() {
45 Serial.begin(115200);
46 Serial.setTimeout(10000);
47 // Wait for user to press Enter
48 while (Serial.read() == -1) {
49 delay(100);
50 }
51
52 // Get references
53 Serial.println("Reference 1");
54 getReference (t1, rt1);
55 Serial.println("Reference 2");
56 getReference (t2, rt2);
57
58 // Compute beta and rx
59 beta = log(rt1/rt2)/((1/t1)-(1/t2));
60 Serial.print("Beta = ");
61 Serial.println(beta, 2);
62 rx = rt1 * exp(-beta/t1);
63 }
64
65 // Main Loop
66 void loop() {
67 float rt = getResistance();
68 float t = beta / log(rt/rx);
69 Serial.print("Temperature: ");
70 Serial.println(t-273.0, 1);
71 delay (1000);
72 }

MicroPython Code
Temperature Sensors 182

MicroPython Thermistor Example

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

39 t2, rt2 = getRef()


40 beta = math.log(rt1/rt2)/((1/t1)-(1/t2))
41 print('Beta = {:.2f}'.format(beta))
42 rx = rt1 * math.exp(-beta/t1)
43
44 # Main loop
45 while True:
46 rt = val()
47 t = beta / math.log(rt/rx)
48 print ('Temperature: {:.1f}'.format(t-273.0))
49 sleep(1)
50

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

LM35D and TMP36


These temperature sensors use a very direct approach: they output a voltage that is linearly
proportional to the temperature, at the rate of 10mV per Celsius degree.
Temperature Sensors 185

LM35 (left) and TMP36 (right) Sensors

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)

LM35D and TMP36 Example


In this example, I am using both sensors. If you want, you can mount only one and change the
pin corresponding to the other one to -1 in the code.

Connecting LM35 and TMP36 Sensors to the Pi Pico

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).

C/C++ SDK Code

C/C++ LM35D and TMP36 Example

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

Arduino LM35D and TMP36 Example


1 // LM35D and TMP36 Temperature Sensor Example
2
3 // Pins (-1 if not used)
4 #define LM35_PIN A0
5 #define TMP36_PIN A1
6
7 //returns the voltage in an ADC pin
8 // averages 10 readings
9 #define N_READINGS 10
10 float readSensor(int pin) {
11 uint32_t sum = 0;
12
13 if (pin == -1) {
14 return 0;
15 }
16
17 for (int i = 0; i < N_READINGS; i++) {
18 sum += analogRead(pin);
19 }
20 return (sum*3.3 / N_READINGS)/1024.0;
21 }
22
23 // initialization
24 void setup() {
25 Serial.begin(115200);
26 }
27
28 // Main loop
29 void loop() {
30 float tempLM35 = readSensor(LM35_PIN)/0.01;
31 float tempTMP36 = 25.0 + (readSensor(TMP36_PIN)-0.75)/0.01;
32 Serial.print("LM35 = ");
33 Serial.print(tempLM35,1);
34 Serial.print("C TMP36 = ");
35 Serial.print(tempTMP36,1);
36 Serial.println("C");
37 delay(2000);
38 }
Temperature Sensors 190

MicroPython Code

MicroPython LM35D and TMP36 Example


1 # LM35D and TMP36 Temperature Sensor Example
2 from machine import ADC
3 from time import sleep
4
5 # Connections (-1 if not used)
6 pinLM35 = 26
7 pinTMP36 = 27
8
9 if pinLM35 != -1:
10 adcLM35 = ADC(pinLM35)
11 if pinTMP36 != -1:
12 adcTMP36 = ADC(pinTMP36)
13
14 # returns the voltage in an ADC pin
15 # averages 10 readings
16 def readADC(adc):
17 sum = 0
18 for i in range(10):
19 sum = sum+adc.read_u16()
20 return (sum*0.33)/65536
21
22 # Main Loop
23 while True:
24 tempLM35 = 0
25 if pinLM35 != -1:
26 vLM35 = readADC(adcLM35)
27 tempLM35 = vLM35/0.01
28 tempTMP36 = 0
29 if pinTMP36 != -1:
30 vTMP36 = readADC(adcTMP36)
31 tempTMP36 = 25.0 + (vTMP36-0.75)/0.01
32 print("LM35 = {:.1f}C TMP36 = {:.1f}C".format(tempLM35, tempTMP36))
33 sleep(2)

CircuitPython Code
Temperature Sensors 191

CircuitPython LM35D and TMP36 Example

1 # LM35D and TMP36 Temperature Sensor Example


2 import analogio
3 import board
4 from time import sleep
5
6 # Connections (-1 if not used)
7 pinLM35 = board.A0
8 pinTMP36 = board.A1
9
10 if pinLM35 != -1:
11 adcLM35 = analogio.AnalogIn(pinLM35)
12 if pinTMP36 != -1:
13 adcTMP36 = analogio.AnalogIn(pinTMP36)
14
15 # returns the voltage in an ADC pin
16 # averages 10 readings
17 def readADC(adc):
18 sum = 0
19 for i in range(10):
20 sum = sum+adc.value
21 return (sum*0.33)/65536
22
23 # Main Loop
24 while True:
25 tempLM35 = 0
26 if pinLM35 != -1:
27 vLM35 = readADC(adcLM35)
28 tempLM35 = vLM35/0.01
29 tempTMP36 = 0
30 if pinTMP36 != -1:
31 vTMP36 = readADC(adcTMP36)
32 tempTMP36 = 25.0 + (vTMP36-0.75)/0.01
33 print("LM35 = {:.1f}C TMP36 = {:.1f}C".format(tempLM35, tempTMP36))
34 sleep(2)
Temperature Sensors 192

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:

• Bi-directional communication on a single connection (the data line).


• Connection of multiple sensors to the same data line.
• Powering the sensor from the data line (parasite power), eliminating the need for a separate
power connection.

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

Powering the DS18B20

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 Function Commands


In typical use, you will send a reset pulse, a MATCH ROM command to select one sensor, and
then one or more function commands.
The DS18B20 has 9 bytes of information (the Scratchpad); three of these bytes can be saved in an
EEPROM and are automatically restored at power-up.

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:

DS18B20 Temperature Register

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

2. Use the result as a signed number in two’s complement.


3. Divide the result by 16.

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).

DS18B20 Configuration Register

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

Connecting DS18B20 Sensors to the Pi Pico

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”).

C/C++ SDK Code


I used the “Raspberry Pi Pico One Wire Library” by Adam Boardman. You need to download the
library from https://github.com/adamboardman/pico-onewire and place it in your project under
a subdirectory called pico-onewire.
The code is simple. I opted to send the start conversion for each sensor, the convert_tem-
perature() method can also send the command to all devices on the bus. The last call to
convert_temperature() will wait for the result.
Temperature Sensors 199

C/C++ DS18B20 Example

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

Installing OneWireNg Library

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

Arduino DS18B20 Example

1 // DS18B20 Temperature Sensor Example


2 // Requires the OneWireNg library
3 // Adapted from DallasTemperature example
4
5 #include "OneWireNg_CurrentPlatform.h"
6 #include "drivers/DSTherm.h"
7 #include "utils/Placeholder.h"
8
9 // One Wire bus is connected to this pin
10 # define OW_PIN 16
11
12 static Placeholder<OneWireNg_CurrentPlatform> ow;
13
14 // Initialization
15 void setup() {
16 // Init the serial
17 Serial.begin(115200);
18
19 // Instanciate the onewire bus
20 new (&ow) OneWireNg_CurrentPlatform(OW_PIN, false);
21 }
22
23 // Main loop
24 void loop() {
25 DSTherm drv(ow);
26
27 // Start convertion in all sensors
28 drv.convertTempAll(DSTherm::MAX_CONV_TIME, false);
29
30 // Read temperature from the sensors
31 Placeholder<DSTherm::Scratchpad> scrpd;
32 for (const auto& id: *ow) {
33 if (printId(id)) {
34 if (drv.readScratchpad(id, scrpd) == OneWireNg::EC_SUCCESS) {
35 const uint8_t *scrpd_raw = ((const DSTherm::Scratchpad&) scrpd).getRaw();
36 long temp = ((const DSTherm::Scratchpad&) scrpd).getTemp();
37 Serial.print(" Temp:");
38 if (temp < 0) {
Temperature Sensors 203

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

1 # DS18B20 Temperature Sensor Example


2
3 from machine import Pin
4 import onewire
5 import ds18x20
6 from time import sleep
7
8 # Identify sensors on the OneWire bus
9 ow = onewire.OneWire(Pin(16))
10 ds = ds18x20.DS18X20(ow)
11 sensors = ds.scan()
12 for sensor in sensors:
13 print(''.join(hex(i)[2:4] for i in sensor))
14 print()
15
16 # Main Loop
17 while True:
18 ds.convert_temp()
19 sleep(1)
20 temp = ''
21 for sensor in sensors:
22 temp = temp + '{:.2f}C '.format(ds.read_temp(sensor))
23 print (temp)

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.

The documentation for the adafruit_onewire library is at


https://docs.circuitpython.org/projects/onewire/en/latest/api.html.
We are going to use only the scan method, it returns a list of the address found on the bus. There
is a limit for the number of devices. By default, it is 10, but you can increase it by setting the
property maximum_devices.
The documentation for the adafruit_ds18x20 library is at
https://docs.circuitpython.org/projects/ds18x20/en/latest/api.html.
When you read the property temperature, a new reading is done (the library starts a conversion,
waits for it to be finished, and gets the result from the scratchpad). This can take up to 0.75 seconds.
If you want to do an asynchronous read, you can start it by calling the start_temperature_read
method and get the result by calling read_temperature. As there is no method to test if the result
is ready, you will have to wait the maximum conversion time.
CircuitPython DS18B20 Example

1 # DS18B20 Temperature Sensor Example


2
3 import board
4 from adafruit_onewire.bus import OneWireBus
5 from adafruit_ds18x20 import DS18X20
6 from time import sleep
7
8 # Identify devices on the OneWire bus
9 ow_bus = OneWireBus(board.GP16)
10 devices = ow_bus.scan()
11 sensors = []
12 for device in devices:
13 print(''.join(hex(i)[2:4] for i in device.rom))
14 if device.family_code == 0x28:
15 sensors.append(DS18X20(ow_bus, device))
16 print()
17
18 # Main Loop
19 while True:
20 for sensor in sensors:
21 sensor.start_temperature_read()
Temperature Sensors 206

22 sleep(1)
23 temp = ''
24 for sensor in sensors:
25 temp = temp + '{:.2f}C '.format(sensor.read_temperature())
26 print (temp)

DHT11 and DHT22


These sensors measure Humidity and Temperature and employ a proprietary single-wire bi-
direction serial communication. They can be powered by 3V to 5V.

DHT11 (left) and DHT22 (right) Modules

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”.

The 40-bit answer contains:

• 16 bits for humidity


• 16 bits for temperature
• 8 bit for a checksum (the sum of the preceding 4 bytes)

The interpretation of the values is different for the two devices.


In the DHT11, each 16-bit value is broken into an 8-bit integer part and an 8-bit decimal part, the
decimal part is always zero.
In the DHT22, each 16-bit value is broken into a 1-bit signal and a 15-bit number. For humidity,
the signal bit is always zero and the number is in units of 0.1%. For temperature, the signal bit is
1 for negative values and 0 for positive values; the number is in units of 0.1°C.
The datasheets warn of possible inaccurate results if you send a new request less than a few
seconds after the previous one.

DHT11 and DHT22 Example


In this example, I am using both sensors. If you want, you can mount only one and change the
pin corresponding to the other one to -1 in the code.
Temperature Sensors 208

Connecting DHT Sensors to the Pi Pico

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.

C/C++ SDK Code


The DHT protocol is a good opportunity to use Pico’s PIO. The PIO program used
is an adaptation of https://github.com/ashchap/PIO_DHT11_Python, using ideas from
https://github.com/danjperron/PicoDHT22. This code will hang if there is no DHT connected,
so an external timeout should be implemented.
Temperature Sensors 209

DHT PIO Program

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

78 // Configure the clock for 1.4 MHz


79 float div = clock_get_hz(clk_sys) / 1400000;
80 sm_config_set_clkdiv(&c, div);
81
82 // Load our configuration, and jump to the start of the program
83 pio_sm_init(pio, sm, offset, &c);
84
85 // Set the state machine running
86 pio_sm_set_enabled(pio, sm, true);
87 }
88 %}

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

102 float humidity() {


103 getData();
104 if (model == DHT11) {
105 return 0.1*data[1] + data[0];
106 } else {
107 return 0.1*((data[0] << 8) + data[1]);
108 }
109 }
110
111 // get temperature
112 float temperature() {
113 getData();
114 if (model == DHT11) {
115 return 0.1*data[3] + data[2];
116 } else {
117 float s = (data[2] & 0x80) ? -0.1 : 0.1;
118 return s*(((data[2] & 0x7f) << 8) + data[3]);
119 }
120 }
121 };
122
123
124 int main() {
125 // Init stdio
126 stdio_init_all();
127 #ifdef LIB_PICO_STDIO_USB
128 while (!stdio_usb_connected()) {
129 sleep_ms(100);
130 }
131 #endif
132
133 printf("\nDHT11/DHT22 Example\n");
134
135 DHT *dht11 = (PIN_DHT11 == -1)? NULL : new DHT(PIN_DHT11, DHT::DHT11);
136 DHT *dht22 = (PIN_DHT22 == -1)? NULL : new DHT(PIN_DHT22, DHT::DHT22);
137
138 while (true) {
139 if (PIN_DHT11 != -1) {
140 printf("DHT11 Humidity: %.1f%%, Temperature: %.1fC\n",
Temperature Sensors 215

141 dht11->humidity(), dht11->temperature());


142 }
143 if (PIN_DHT22 != -1) {
144 printf("DHT22 Humidity: %.1f%%, Temperature: %.1fC\n",
145 dht22->humidity(), dht22->temperature());
146 }
147 sleep_ms(3000);
148 }
149 }

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

Installing the Adafruit DHT Sensor Library


Temperature Sensors 217

Arduino DHT11/DHT22 Example

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

39 Serial.print("%, Temperature: ");


40 Serial.print(dht22.readTemperature(), 1);
41 Serial.println("C");
42 #endif
43
44 delay(3000);
45 }

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

27 set(pindirs,0) # change pin to input


28 wait(1,pin,0) # wait for it to come back to high
29 wait(0,pin,0) # wait for starting pulse
30 wait(1,pin,0)
31 wait(0,pin,0) # wait for start of first bit
32
33 # read data bits
34 label('readdata')
35 wait(1,pin,0) # wait for data high
36 set(x,20) # x is timeout
37 label('countdown')
38 jmp(pin,'continue') # continue conting if data high
39
40 # pin low while counting -> bit 0
41 set(y,0)
42 in_(y, 1) # put a 0 in result
43 jmp('readdata') # read next bit
44
45 # pin still high
46 label('continue')
47 jmp(x_dec,'countdown') # decrement count
48
49 # timeout -> bit 1
50 set(y,1)
51 in_(y, 1) # put a 1 in the result
52 wait(0,pin,0) # wait for low
53 jmp('readdata') # read next bit
54
55 DHT11 = 0
56 DHT22 = 1
57
58 class DHT:
59
60 # Construtor
61 # dataPin: pin connected to the DHT
62 # model: DHT11 ou DHT22
63 # smID: state machine id
64 def __init__(self, dataPin, model, smID=0):
65 self.dataPin = dataPin
Temperature Sensors 220

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

105 if self.lastreading > now:


106 self.lastreading = now # count wraped around
107 if (self.lastreading+2000) < now:
108 self.read()
109
110 # return humidity
111 def humidity(self):
112 self.getData()
113 if self.model == DHT11:
114 return self.data[0] + self.data[1]*0.1
115 else:
116 return ((self.data[0] << 8) + self.data[1]) * 0.1
117
118 # return temperature
119 def temperature(self):
120 self.getData()
121 if self.model == DHT11:
122 return self.data[2] + self.data[3]*0.1
123 else:
124 s = 1
125 if (self.data[2] & 0x80) == 1:
126 s = -1
127 return s * (((self.data[2] & 0x7F) << 8) + self.data[3]) * 0.1
128
129 #main program
130
131 if pinDHT11 != -1:
132 dht11_data = Pin(pinDHT11, Pin.IN, Pin.PULL_UP)
133 dht11 = DHT(dht11_data, DHT11, 0)
134
135 if pinDHT22 != -1:
136 dht22_data = Pin(pinDHT22, Pin.IN, Pin.PULL_UP)
137 dht22 = DHT(dht22_data, DHT22, 1)
138
139 while True:
140 if pinDHT11 != -1:
141 print("DHT11 Humidity: %.1f%%, Temperature: %.1fC" %
142 (dht11.humidity(), dht11.temperature()))
143 if pinDHT22 != -1:
Temperature Sensors 222

144 print("DHT22 Humidity: %.1f%%, Temperature: %.1fC" %


145 (dht22.humidity(), dht22.temperature()))
146 utime.sleep_ms(3000)

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.

The documentation for the adafruit_dht library is at https://docs.circuitpython.org/projects/dht/en/latest/index.


In the rp2 port, CircuitPython uses the PIO to measure the pulses. If a checksum error, or
insufficient data, occurs, an error is raised. The library implements a 2-second cache.
CircuitPython DHT11/DHT22 Example

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:

• Can be used with 2.8V to 5.5V power supply


• Measures temperatures from -25°C to +125°C
• Has ±2°C accuracy in the range -25°C to +100°C
• Uses an 11-bit ADC to achieve 0.125°C resolution
• Has 8 possible I²C addresses (0x48 to 0x4F)
• Has an output (OS) that can be configured to activate based on the temperature

LM75A Module

Internally the LM75A has four registers (addresses in parentheses):

• 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).

In n-bit 2-complement notation, positive numbers are expressed as themselves, and


negative numbers are expressed as 2ⁿ - the number.

Some examples may make this more clear:

• Register = 00000000 001xxxxx -> temperature = 1*0.125 = 0.125°C


• Register = 00000001 000xxxxx -> temperature = 8*0.125 = 1°C
• Register = 11111111 000xxxxx -> temperature = -8*0.125 = -1°C

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 *.

Bit Name Value Description


7:5 reserved 000* should be kept as zeros
4:3 OS_F_QUE 00* queue = 1
01 queue = 2
10 queue = 4
11 queue = 6
2 OS_POL 0* OS active LOW
1 OS active HIGH
1 OS_COMP_INT 0* OS is comparator output
1 OS is interrupt output
0 SHUTDOWN 0* normal operation
1 shutdown

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.

Connecting the LM75A to the Pi Pico


Temperature Sensors 226

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.

C/C++ SDK Code


Accessing the LM75A registers is pretty straightforward. A few auxiliary routines make the main
program very short.
C/C++ LM75A Example

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, &reg, 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

1 // LM75A Temperature Sensor Example


2
3 #include <Wire.h>
4
5 // LM75A I2C Address
6 #define ADDR 0x48
7
8 // LM75A Registers
9 #define REG_TEMP 0
10 #define REG_CONF 1
11 #define REG_THYST 2
12 #define REG_TOS 3
13
14 // Initialization
15 void setup() {
16 Serial.begin (115200);
17 Wire.setSDA(16);
18 Wire.setSCL(17);
19 Wire.begin();
20
21 // Configure sensor and set limits for the OS output
22 WriteReg8 (REG_CONF, 0);
23 WriteReg16 (REG_TOS, EncodeTemp(22.5));
24 WriteReg16 (REG_THYST, EncodeTemp(20.0));
25 }
26
27 // Main loop: read temperature
28 void loop() {
Temperature Sensors 230

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

CircuitPython LM75A Example

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:

• Can be used with 2.7V to 5.5V power supply


• Temperature:
– from -40°C to +125°C
– ±0.2°C accuracy
– 11-bit or 14-bit ADC
• Humidity:
– from 0 to 100%
– ±2% accuracy
– 8-bit, 11-bit, or 14-bit ADC
• Has a fixed I²C address (0x40) so you can have only one sensor in a I²C bus.

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:

Address Name Reset value Description


0x00 Temperature 0x0000 Temperature output
0x01 Humidity 0x0000 Humidity output
0x02 Configuration 0x1000 Configuration and status
0xFB Serial ID First two bytes
0xFC Serial ID Middle two bytes
0xFD Serial ID Last byte
0xFE Manufacturer ID 0x5449 ID of Texas Instruments
0xFF DeviceID 0x1050 ID of the device
Temperature Sensors 236

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).

Measurement Conversion Time


Temperature, 11-bit 3.65 ms
Temperature, 14-bit 6.35 ms
Humidity, 8-bit 2.50 ms
Humidity, 11-bit 3.85 ms
Humidity, 14-bit 6.50 ms

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

Connecting the HDC1080 to the Pi Pico

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.

C/C++ SDK Code

C/C++ HDC1080 Example

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, &reg, 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, &reg, 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

Arduino HDC1080 Example

1 // HDC1080 Temperature Sensor Example


2
3 #include <Wire.h>
4
5 // HDC1080 I2C Address
6 #define ADDR 0x40
7
8 // HDC1080 Registers
9 #define REG_TEMP 0
10 #define REG_HUM 1
11 #define REG_CONF 2
12 #define REG_MANID 0xFE
13 #define REG_DEVID 0xFF
14
15 // Initialization
16 void setup() {
17 Serial.begin (115200);
18 Wire.setSDA(16);
19 Wire.setSCL(17);
20 Wire.begin();
21
22 delay(5000);
23
24 // Check manufacture and device IDs
25 uint16_t manID = ReadReg16(REG_MANID);
26 uint16_t devID = ReadReg16(REG_DEVID);
27 Serial.print ("Manufacturer: ");
28 Serial.print (manID, HEX);
29 Serial.print (" Device: ");
30 Serial.println (devID, HEX);
31 }
32
33 // Main loop: read temperature
34 void loop() {
35 // Start conversion
36 Wire.beginTransmission(ADDR);
Temperature Sensors 242

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

MicroPython HDC1080 Example

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

CircuitPython HDC1080 Example

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.

Adafruit MCP9808 Module

It has the following features:

• Can be used with 2.7V to 5.5V power supply


• Temperature:
– from -40°C to +125°C
– ±0.25°C (typical) accuracy from -40°C to +125°C
Temperature Sensors 246

– ±0.5°C (maximum) accuracy from -20°C to +10°C


– ±1°C (maximum) accuracy from -40°C to +125°C
– +0.5°C, +0.25°C, +0.125°C or +0.0625°C resolution
• Has 8 possible I²C addresses (0x18 to 0x1F).
• Has an output (Alert) that can be configured to activate based on the temperature. This pin
is an “open drain output” that can be pulled to the ground or let fluctuate.

The MCP9808 has seven 16-bit registers and one 8-bit register (RESOL):

Address Name Reset value Description


0x01 CONFIG 0x0000 Configuration
0x02 T_UPPER 0x0000 Alert Upper Temperature Boundary
0x03 T_LOWER 0x1000 Alert Lower Temperature Boundary
0x04 T_CRIT Critical Temperature
0x05 TA Ambient Temperature
0x06 MAN_ID 0x5449 Manufacturer ID
0x07 DEV_ID 0x1050 ID and revision of the device
0x08 RESOL Resolution

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):

Temperature Value (binary) Value (hex)


0°C 0000 0000 0000 0000 0x0000
+120.5°C 0000 0111 1000 1000 0x0788
-10.25°C 0001 1111 0101 1100 0x1F5C

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:

• bit 15 is 0 if TA < T_CRIT


• bit 14 is 0 if TA ≤ T_UPPER
• bit 13 is 0 if TA ≥ 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:

Value Resolution Conversion Time


0 0.5°C 30 ms
1 0.25°C 65 ms
2 0.125°C 130 ms
3 0.0625°C 250 ms

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 Alert Function

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

Connecting the MCP9808 to the Pi Pico

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.

C/C++ SDK Code

C/C++ MCP9808 Example

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, &reg, 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

Arduino MCP9808 Example

1 // MCP9808 Temperature Sensor Example


2
3 #include <Wire.h>
4
5 // MCP9808 I2C Address
6 #define ADDR 0x18
7
8 // MCP9808 Registers
9 #define REG_CONFIG 1
10 #define REG_UPPER 2
11 #define REG_LOWER 3
12 #define REG_CRIT 4
13 #define REG_TA 5
Temperature Sensors 254

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

MicroPython MCP9808 Example

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

30 val = val & 0x1FFF


31 if sign != 0:
32 val = val - 0x2000
33 return val/16
34
35 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
36
37 # Read 16-bit register
38 def readReg16(reg):
39 data = i2c.readfrom_mem(MCP9808_ADDR,reg,2)
40 return (data[0] << 8) + data[1]
41
42 # Check manufacture and device IDs
43 print ('Manufacturer: {:04X}'.format(readReg16(MCP9808_MANID)))
44 devID = readReg16(MCP9808_DEVID);
45 print ('Device: {:02X} rev {}'.format(devID >>8, devID&0xFF))
46
47 # Set limits for the Alert output
48 i2c.writeto_mem(MCP9808_ADDR,MCP9808_CRIT,encodeTemp(30.0))
49 i2c.writeto_mem(MCP9808_ADDR,MCP9808_UPPER,encodeTemp(23.0))
50 i2c.writeto_mem(MCP9808_ADDR,MCP9808_LOWER,encodeTemp(20.0))
51 i2c.writeto_mem(MCP9808_ADDR,MCP9808_CONFIG,b'\x00\x08')
52
53 # Main loop: read temperature
54 while True:
55 sleep(0.5)
56 print ('Temperature = {}C'.format(
57 decodeTemp(readReg16(MCP9808_TA))))

CircuitPython Code
Temperature Sensors 258

CircuitPython MCP9808 Example

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

39 def writeReg16(reg, val):


40 data = bytearray([reg,val >> 8, val & 0xFF])
41 i2c.try_lock()
42 i2c.writeto(MCP9808_ADDR, data)
43 i2c.unlock()
44
45 # Read 16 bit value from a register
46 def readReg16(reg):
47 selreg = bytearray([reg])
48 val = bytearray([0,0])
49 i2c.try_lock()
50 i2c.writeto_then_readfrom(MCP9808_ADDR, selreg, val)
51 i2c.unlock()
52 return (val[0] << 8) + val[1]
53
54 # Check manufacture and device IDs
55 print ('Manufacturer: {:04X}'.format(readReg16(MCP9808_MANID)))
56 devID = readReg16(MCP9808_DEVID);
57 print ('Device: {:02X} rev {}'.format(devID >>8, devID&0xFF))
58
59 # Set limits for the Alert output
60 writeReg16(MCP9808_CRIT,encodeTemp(30.0))
61 writeReg16(MCP9808_UPPER,encodeTemp(23.0))
62 writeReg16(MCP9808_LOWER,encodeTemp(20.0))
63 writeReg16(MCP9808_CONFIG,0x0008)
64
65 # Main loop: read temperature
66 while True:
67 sleep(0.5)
68 print ('Temperature = {}C'.format(
69 decodeTemp(readReg16(MCP9808_TA))))
Temperature Sensors 260

AHT10

AHT10 Module

The AHT10 sensor from Asair is a temperature and humidity sensor with an I²C interface and
the following characteristics:

• Can be used with a 1.8V to 3.6V power supply


• Temperature:
– from -40°C to +80°C
– ±0.3°C (typical) accuracy
– ±1.5°C (maximum) accuracy
• Humidity:
– From 0 to 100%
– ±2% (typical) accuracy
– ±5% (maximum) accuracy
• The datasheet lists only one I²C address (0x38) and explicit say that only one AHT10 can be
present in an I²C bus. Regardless, you will find modules with a selection between addresses
0x38 and 0x39. My advice: stick with the 0x38 address.

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:

• Bit 7: 1 if the sensor is doing a measurement, 0 if the sensor is idle.


• Bit 3: 1 if the sensor is calibrated, 0 if a calibration is required.

In a write I²C transaction, the AHT10 accepts three commands:


Temperature Sensors 261

Command Code Parameters


Soft Reset 0xBA
Initialization 0xE1 0x08 0x00
Start Conversion 0xAC 0x33 0x00

• 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.

The sequence for reading humidity and temperature is as follows:

• 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.

Humidity and temperature data have 20 bits each:

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.

Connecting a AHT10 Sensor to the Pi Pico

C/C++ SDK Code

C/C++ Example Code for AHT10


1 /**
2 * @file aht10_sdk.c
3 * @author Daniel Quadros ([email protected])
4 * @brief AHT10 Temperature Sensor Example
5 * @version 1.0
6 * @date 2023-06-07
7 *
8 * @copyright Copyright (c) 2023, Daniel Quadros
9 *
Temperature Sensors 263

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

Arduino Example Code for AHT10

1 // AHT10 Temperature Sensor Example


2
3 #include <Wire.h>
4
5 // AHT10 I2C Address
6 #define ADDR 0x38
7
8 // AHT10 Commands
9 uint8_t cmdInit[] = { 0xE1, 0x08, 0x00 };
10 uint8_t cmdConv[] = { 0xAC, 0x33, 0x00 };
11
12 // Init
13 void setup() {
14 Serial.begin (115200);
15 Wire.setSDA(16);
16 Wire.setSCL(17);
17 Wire.begin();
18
19 delay(5000);
20
21 // Check if a calibration is needed
22 uint8_t status = getStatus();
23 if ((status & 0x08) == 0) {
24 Serial.println ("Calibrating");
25 Wire.beginTransmission(ADDR);
26 Wire.write(cmdInit, sizeof(cmdInit));
27 Wire.endTransmission();
28 delay(10);
29 }
30 }
31
32 // Main Loop
33 void loop() {
34 // Start conversion
35 Wire.beginTransmission(ADDR);
36 Wire.write(cmdConv, sizeof(cmdConv));
37 Wire.endTransmission();
38
Temperature Sensors 266

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

MicroPython Example Code for AHT10

1 # AHT10 Temperature Sensor Example


2
3 from machine import I2C,Pin
4 from time import sleep_ms
5
6 # AHT10 I2C Address
7 AHT_ADDR = 0x38
8
9 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
10
11 # Read ADH10 status
12 def getStatus():
13 resp = i2c.readfrom (AHT_ADDR, 1)
14 return resp[0]
15
16 # Check if a calibration is needed
17 status = getStatus()
18 if (status & 0x08) == 0:
19 print ('Calibrating')
20 i2c.writeto(AHT_ADDR, b'\xE1\x08\x00')
21 sleep_ms(10)
22
23 # Main Loop
24 while True:
25 # Start convertion
26 i2c.writeto(AHT_ADDR, b'\xAC\x33\x00')
27 # Wait convertion
28 sleep_ms(80)
29 # Get result
30 resp = i2c.readfrom (AHT_ADDR, 6)
31 # Decode result
32 umid = (resp[1] << 12) + (resp[2] << 4) + (resp[3] >> 4)
33 umid = (umid / 0x100000) * 100.0
34 temp = ((resp[3] & 0x0F) << 16) + (resp[4] << 8) + resp[5]
35 temp = (temp / 0x100000) * 200.0 - 50.0
36 # Show result
37 print ('Humidity = {:.1f}% Temperature = {:.1f}C'.format(umid, temp))
38 # Wait between readings
Temperature Sensors 268

39 sleep_ms(2000)

CircuitPython Code

CircuitPython Example Code for AHT10

1 # AHT10 Temperature Sensor Example


2
3 import board
4 from digitalio import DigitalInOut, Pull
5 from busio import I2C
6 from time import sleep
7
8 # AHT10 I2C Address
9 AHT_ADDR = 0x38
10
11 i2c = I2C(sda=board.GP16, scl=board.GP17)
12
13 # Read AHT status
14 def getStatus():
15 resp = bytearray([0])
16 i2c.try_lock()
17 i2c.readfrom_into (AHT_ADDR, resp)
18 i2c.unlock()
19 return resp[0]
20
21 # Check if a calibration is needed
22 status = getStatus()
23 if (status & 0x08) == 0:
24 print ('Calibrating')
25 i2c.try_lock()
26 i2c.writeto(AHT_ADDR, b'\xE1\x08\x00')
27 i2c.unlock()
28 sleep(0.01)
29
30 # Main loop
31 while True:
32 # Start conversion
33 i2c.try_lock()
Temperature Sensors 269

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)

Sensors Comparison Table


The following table resumes the characteristics of the sensors we examined.

Sensor Interface Temperature Humidity


Thermistor Analog depends on model -
LM35D Analog 0 to 100°C ±2°C -
TMP36 Analog −40 to +125°C ±2°C -
DS18B20 1-Wire −55 to +125°C ±0.5°C -
DHT11 Proprietary 0 to +50°C ±2°C 20 to 95% ±5%
DHT22 Proprietary -40 to +80°C ±0.5°C 0 to 100% ±2%
LM75A I²C -25°C to 125°C ±2°C -
HDC1080 I²C -40°C to 125°C ±0.2°C 0 to 100% ±2%
MCP9808 I²C -40°C to 125°C ±0.25°C -
AHT10 I²C -40°C to 80°C ±0.3°C 0 to 10% ±2%
Atmospheric Pressure Sensors
Atmospheric pressure (also called barometric pressure) is the pressure from the Earth’s atmo-
sphere. In most circumstances, it’s closely approximated by the pressure from the weight of the
air above the measurement point. Atmospheric pressure decreases with increasing elevation. At
low altitudes above sea level, the pressure decreases by about 1.2 kPa⁶ (12 hPa) for every 100
meters elevation.
Atmospheric pressure is also an indicator of weather. When a low-pressure system moves into an
area, it usually leads to cloudiness, wind, and precipitation. High-pressure systems usually lead
to fair, calm weather.
So, there are two uses for a pressure reading: finding the altitude and (crudely) predicting the
weather.
For finding the altitude, given the measure pressure p and that the pressure at sea level, p0 is
1013.25 hPa, we can use the simple formula (for low altitudes)

altitude = 100 ∗ (p − p0 )/12

or the more precise

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.

A Few Bosch Atmospheric Pressure Sensors Modules

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:

• Voltage regulator and converters to allow connection to a 5V device.


• Pull-up or pull-down resistors to select the interface (I²C or SPI) or address.
• I²C bus pull-ups.

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.

BMP085 and BMP180


The BMP085 is the older of the sensors in this chapter. The BMP180 is function compatible with
it, but it is physically smaller and consumes less energy. Both are discontinued (but still easily
found).
Atmospheric Pressure Sensors 272

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.

BMP085/BMP180 Calibration Coefficients

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:

Bits Mode Samples Time Current


00 ultra low power 1 4.5 ms 3 μA
01 standard 2 7.5 ms 5 μA
10 high resolution 4 13.5 ms 7 μA
11 ultra high resolution 8 25.5 ms 12 μA

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.

BMP085 and BMP180 Example


We will ignore the EOC and XCLR pins in the BMP085 and just connect the power and the I²C
signals.

Check the pinout of your breakout board!


Atmospheric Pressure Sensors 274

Connecting a BMP085 (or BMP180) Sensor to the Pi Pico

C/C++ SDK Code


In the following code, a class (BMP180) encapsulates the interface with the sensor. The methods
for getting temperature and pressure will start a new conversion and wait for the result. If your
application has other things to do while the sensor is doing the reading, you might want to break
the functions into two parts, one to start a conversion and the other to get the result and apply
the calibration coefficients.
C/C++ BMP085/BMP180 Example

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

53 typedef enum : uint8_t {


54 CAL_AC1 = 0xAA,
55 CAL_AC2 = 0xAC,
56 CAL_AC3 = 0xAE,
57 CAL_AC4 = 0xB0,
58 CAL_AC5 = 0xB2,
59 CAL_AC6 = 0xB4,
60 CAL_B1 = 0xB6,
61 CAL_B2 = 0xB8,
62 CAL_MB = 0xBA,
63 CAL_MC = 0xBC,
64 CAL_MD = 0xBE,
65 GET_ID = 0xD0,
66 SOFT_RESET = 0xE0,
67 START_MEAS = 0xF4,
68 ADC_MSB = 0xF6,
69 ADC_LSB = 0xF7,
70 ADC_XLSB = 0xF8
71 } REGS;
72
73 static const uint8_t SOFT_RESET_VALUE = 0xB6;
74 static const uint8_t GET_TEMP = 0x2E;
75 static const uint8_t GET_PRESS = 0x34;
76
77 // I2C instance
78 i2c_inst_t *i2c;
79
80 // Calibration coefficients
81 int16_t AC1 = 0;
82 int16_t AC2 = 0;
83 int16_t AC3 = 0;
84 uint16_t AC4 = 0;
85 uint16_t AC5 = 0;
86 uint16_t AC6 = 0;
87
88 int16_t B1 = 0;
89 int16_t B2 = 0;
90
91 int16_t MB = 0;
Atmospheric Pressure Sensors 277

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

170 return pressure + ((X1 + X2 + 3791L) >> 4);


171 }
172
173 // Calculate altitude from pressure
174 float BMP180::calcAltitude(float pressure, float sealevelPressure) {
175 return 44330.0 * (1.0 - pow(pressure/sealevelPressure, 0.1903));
176 }
177
178 // Read calibration coefficients and save them
179 void BMP180::readCalibCoeff() {
180 AC1 = read16 (CAL_AC1);
181 AC2 = read16 (CAL_AC2);
182 AC3 = read16 (CAL_AC3);
183 AC4 = read16 (CAL_AC4);
184 AC5 = read16 (CAL_AC5);
185 AC6 = read16 (CAL_AC6);
186 B1 = read16 (CAL_B1);
187 B2 = read16 (CAL_B2);
188 MB = read16 (CAL_MB);
189 MC = read16 (CAL_MC);
190 MD = read16 (CAL_MD);
191 }
192
193 // Calculate B5 parameter
194 int32_t BMP180::computeB5(int32_t UT)
195 {
196 int32_t X1 = ((UT - (int32_t) AC6) * (int32_t) AC5) >> 15;
197 int32_t X2 = ((int32_t) MC << 11) / (X1 + (int32_t) MD);
198
199 return X1 + X2;
200 }
201
202 // Read raw temperature data
203 uint16_t BMP180::readRawTemperature(void)
204 {
205 write8(START_MEAS, GET_TEMP);
206 sleep_ms(5);
207 return read16(ADC_MSB);
208 }
Atmospheric Pressure Sensors 280

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, &reg, 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

248 i2c_write_blocking (i2c, BMP180_ADDR, &reg, 1, true);


249
250 // Read value
251 i2c_read_blocking (i2c, BMP180_ADDR, val, 2, false);
252 return ((uint16_t) val[0] << 8) | val[1];
253 }
254
255
256 // Main Program
257 int main() {
258 // Init stdio
259 stdio_init_all();
260 #ifdef LIB_PICO_STDIO_USB
261 while (!stdio_usb_connected()) {
262 sleep_ms(100);
263 }
264 #endif
265
266 // Init I2C interface
267 uint baud = i2c_init (I2C_ID, BAUD_RATE);
268 printf ("I2C @ %u Hz\n", baud);
269 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
270 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
271 gpio_pull_up(I2C_SCL_PIN);
272 gpio_pull_up(I2C_SDA_PIN);
273
274 // Init sensor
275 BMP180 bmp180 (I2C_ID);
276
277 // Display sensor ID
278 printf ("Sensor ID = 0x%02X\n", bmp180.getDeviceId());
279
280 // Main loop
281 while(1) {
282 sleep_ms(2000);
283 float temp = bmp180.getTemperature();
284 float pressure = bmp180.getPressure();
285 float altitude = bmp180.calcAltitude(pressure);
286 printf("Temperature: %.1f C Pressure: %.0f Pa Altitude: %.1f m\n",
Atmospheric Pressure Sensors 282

287 temp, pressure, altitude);


288 }
289 }

Arduino Code
There are a few BMP085/BMP180 libraries. We are going to use the one from Adafruit.

Adafruit BMP085/BMP180 Library

The code below is an adaptation of the library example.


Arduino BMP085/BMP180 Example
1 // BMP085/BMP180 Pressure Sensor Example
2 // Adapted from Adafruit Example
3
4 #include <Adafruit_BMP085.h>
5
6 Adafruit_BMP085 bmp;
7
8 // Initialization
9 void setup() {
10 Serial.begin (115200);
11 Wire.setSDA(16);
12 Wire.setSCL(17);
13 if (!bmp.begin()) {
14 Serial.println("Could not find a BMP085/BMP180 sensor!");
15 while (true) {
Atmospheric Pressure Sensors 283

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()

and save the bmp180.py back in the Pico.


MicroPython BMP085/BMP180 Example

1 # BMP085 / BMP180 Example


2 from bmp180 import BMP180
3 from machine import I2C, Pin
4 from time import sleep
5
6 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
7 bmp180 = BMP180(i2c)
8 bmp180.oversample_sett = 2
9 bmp180.baseline = 101325
10
11 print ('ID = {}'.format(hex(bmp180.chip_id[0])))
12
13 while True:
14 temp = bmp180.temperature
15 p = bmp180.pressure
16 altitude = bmp180.altitude
17 print('Temperature {:.1f}C Pressure {:.0f}Pa Altitude {:.2f}m'.format(
18 temp, p, altitude))
19 sleep(2)

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:

1. Download the file at https://raw.githubusercontent.com/jposada202020/CircuitPython_-


BMP180/master/bmp180.py and save it as bmp180.py in your PC.
⁸If you are going to write many I²C ou SPI drivers for devices in CircuitPython, the Adafruit infrastructure in these classes may be
helpful, as they abstract the concept of device registers.
Atmospheric Pressure Sensors 285

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.

CircuitPython BMP085/BMP180 Example

1 # BMP085/BMP180 Sensor Example


2 import board
3 from busio import I2C
4 from time import sleep
5 import bmp180
6
7 i2c = I2C(sda=board.GP16, scl=board.GP17)
8 bmp = bmp180.BMP180(i2c)
9
10 while True:
11 press = bmp.pressure
12 temp = bmp.temperature
13 alt = bmp.altitude
14 print ('Temperature: {:.1f} C Pressure: {:.0f} Pa {:.1f} m'.format(
15 temp, press, alt))
16 sleep(2)

BMP280 and BME280


The BMP280 is smaller, more precise, has lower power consumption, and makes faster readings
than the BMP180. It also adds an SPI interface (making it easier to connect multiple sensors).
The BME280 is (mostly) upwards compatible with the BMP280 and adds humidity measurement.
These sensors can operate in three power modes:

• sleep: no measurement is made


• normal: the sensor automatically alternates between an active measurement period and an
inactive standby period.
• forced: a single measurement is made and when it is finished the sensor returns to sleep
mode.
Atmospheric Pressure Sensors 286

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.

Setting Oversampling Resolution


000 Skip measurement -
001 1 16 bit
010 2 17 bit
011 4 18 bit
100 8 19 bit
101,110,111 16 20 bit

Oversampling for humidity (in the BME280) is controlled by the osrs_h bits in the ctrl_hum
register at address 0xF2.

Setting Oversampling Resolution


000 Skip measurement -
001 1 16 bit
010 2 17 bit
011 4 18 bit
100 8 19 bit
101,110,111 16 20 bit

Changes in ctrl_hum only become effective after a write operation to ctrl_meas


The IIR (Infinite Impulse Response) filter can suppress short-term disturbances (like a door
slamming). It is configured by the filter bits in the config register at address 0xF5.
In the “normal” mode, the time the sensor stays in sleep mode is configured by the t_sb bits in
the config register at address 0xF5.
Atmospheric Pressure Sensors 287

Bosch recommends some settings based on the use case:

Use Case Mode osrs_p osrs_t IIR filter rate


handheld low-power Normal 16 2 4 10 Hz
handheld dynamic Normal 4 1 16 83.3 Hz
weather monitoring Forced 1 1 off 1/60 Hz
drop detection Normal 2 1 off 125
indoor navigation Normal 16 2 16 26.3

In the following tables, the shaded information applies only to the BME280.

BMP280/BME280 Calibration Coefficients


Atmospheric Pressure Sensors 288

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

BMP280 and BME280 Example


We will use the sensor’s I²C interface with address 0x76. Depending on the board you may
not need the resistors in the diagram. You may also find a board that does not support SPI
communication (it will have only SDA and SCL pins).

Check the pinout, pull-ups, and pull-downs of your breakout board!

Connecting a BMP280 (or BME280) Sensor to the Pi Pico

C/C++ SDK Code


The sensor is accessed through a BMx280 class that supports both the BME280 and the BMP280. A
large part of this code is an adaptation of the code in the library used for the Arduino environment.
To reduce the code size, I am supporting only I²C and the configuration is done only at object
creation time.
Atmospheric Pressure Sensors 290

C/C++ BMP280/BME280 Example

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

117 float calcAltitude(float pressure, float sealevelPressure = 101325.0);


118
119 private:
120
121 // Registers Address
122 typedef enum : uint8_t {
123 CAL_T1 = 0x88,
124 CAL_T2 = 0x8A,
125 CAL_T3 = 0x8C,
126
127 CAL_P1 = 0x8E,
128 CAL_P2 = 0x90,
129 CAL_P3 = 0x92,
130 CAL_P4 = 0x94,
131 CAL_P5 = 0x96,
132 CAL_P6 = 0x98,
133 CAL_P7 = 0x9A,
134 CAL_P8 = 0x9C,
135 CAL_P9 = 0x9E,
136
137 CAL_H1 = 0xA1,
138 CAL_H2 = 0xE1,
139 CAL_H3 = 0xE3,
140 CAL_H4_MSB = 0xE4,
141 CAL_H4_H5_LSB = 0xE5,
142 CAL_H5_MSB = 0xE6,
143 CAL_H6 = 0xE7,
144
145 GET_ID = 0xD0,
146 SOFT_RESET = 0xE0,
147 CTRL_HUM = 0xF2,
148 STATUS = 0xF3,
149 CTRL_MEAS = 0xF4,
150 CONFIG = 0xF5,
151 PRESSURE = 0xF7,
152 TEMPERATURE = 0xFA,
153 HUMIDITY = 0xFD
154 } REGS;
155
Atmospheric Pressure Sensors 294

156 static const uint8_t MASK_MODE = 0x03;


157 static const uint8_t SHIFT_MODE = 0;
158 static const uint8_t SHIFT_OSRSP = 2;
159 static const uint8_t SHIFT_OSRST = 5;
160 static const uint8_t SHIFT_STB = 5;
161 static const uint8_t SHIFT_FILTER = 2;
162
163 static const uint8_t SOFT_RESET_VALUE = 0xB6;
164 static const uint8_t STATUS_UPDATE = 0x01;
165 static const uint8_t STATUS_MEASURING = 0x08;
166
167 // I2C instance
168 i2c_inst_t *i2c;
169
170 // Sensor I2C Address
171 uint8_t addr;
172
173 // Calibration coefficients
174 uint16_t T1 = 0;
175 int16_t T2 = 0;
176 int16_t T3 = 0;
177
178 uint16_t P1 = 0;
179 int16_t P2 = 0;
180 int16_t P3 = 0;
181 int16_t P4 = 0;
182 int16_t P5 = 0;
183 int16_t P6 = 0;
184 int16_t P7 = 0;
185 int16_t P8 = 0;
186 int16_t P9 = 0;
187
188 uint16_t H1 = 0;
189 int16_t H2 = 0;
190 uint8_t H3 = 0;
191 int16_t H4 = 0;
192 int16_t H5 = 0;
193 int8_t H6 = 0;
194
Atmospheric Pressure Sensors 295

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

234 this->osrs_p = (uint8_t) ovs_p;


235 this->osrs_h = (uint8_t) ovs_h;
236 this->filter = (uint8_t) filter;
237 this->stb_time = (uint8_t) stb;
238 this->id = read8(GET_ID);
239
240 // Read calibration coefficients
241 readCalibCoeff();
242
243 // Init registers
244 write8(CTRL_HUM, osrs_h);
245 write8(CONFIG, (stb_time << SHIFT_STB) | (filter << SHIFT_FILTER));
246 setMode(MODE_SLEEP);
247 }
248
249 // Do a software reset
250 void BMx280::softReset () {
251 write8(SOFT_RESET, SOFT_RESET_VALUE);
252 }
253
254 // Read Device ID
255 uint8_t BMx280::getDeviceId() {
256 return id;
257 }
258
259 // Change sensor mode
260 // programs Temperature and Pressure oversampling
261 void BMx280::setMode(MODE mode) {
262 write8 (CTRL_MEAS,
263 (osrs_t << SHIFT_OSRST) | (osrs_p << SHIFT_OSRSP) | mode);
264 }
265
266 // Start a measurement
267 // If bWait = true, waits for it to finish
268 void BMx280::doMeasure(bool bWait) {
269 uint8_t ctrl = read8(CTRL_MEAS);
270 uint8_t mode = (ctrl & MASK_MODE) >> SHIFT_MODE;
271
272 if (mode == MODE_SLEEP) {
Atmospheric Pressure Sensors 297

273 // Force a measurement


274 setMode (MODE_FORCED);
275 }
276 if (bWait) {
277 sleep_ms(1); // wait for status to change
278 while (!getRawData()) {
279 sleep_ms(1);
280 }
281 }
282 }
283
284 // Get raw data, if available
285 // return true if data was available
286 bool BMx280::getRawData() {
287 if (read8(STATUS) & STATUS_MEASURING) {
288 return false; // measurment not finished
289 } else {
290 ut = read20(TEMPERATURE);
291 up = read20(PRESSURE);
292 if (id == BME280_ID) {
293 uh = read16(HUMIDITY, false);
294 }
295 //printf ("UT: %04X UP: %04X UH = %04X\n", ut, up, uh);
296 return true;
297 }
298 }
299
300 // Get temperature in C
301 float BMx280::getTemperature() {
302 if (getRawData() && (ut != 0x80000)) {
303 return (float) ((calculateTempFine() * 5 + 128) >> 8) / 100.0f;
304 } else {
305 return NAN;
306 }
307 }
308
309 // Get pressure in Pa
310 float BMx280::getPressure() {
311 if (getRawData() && (up != 0x80000)) {
Atmospheric Pressure Sensors 298

312 int32_t temp_fine = calculateTempFine();


313 int32_t var1, var2;
314 uint32_t p;
315
316 var1 = ((int32_t)temp_fine >> 1) - 64000L;
317 var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * (int32_t) P6;
318 var2 = var2 + ((var1*(int32_t)P5) << 1);
319 var2 = (var2 >> 2) + ((int32_t)P4 << 16);
320 var1 = (((P3 * (((var1 >> 2) * (var1 >> 2)) >> 13)) >> 3) +
321 (((int32_t)P2 * var1) >> 1)) >> 18;
322 var1 = ((32768L + var1) * (int32_t)P1) >> 15;
323
324 if (var1 == 0)
325 return NAN; // avoid exception caused by division by zero
326
327 p = (((uint32_t)(1048576L - up)) - (var2 >> 12)) * 3125;
328
329 if (p < 0x80000000)
330 p = (p << 1) /(uint32_t)var1;
331 else
332 p = p / (uint32_t)var1 * 2;
333
334 var1 = ((int32_t) P9 *
335 (int32_t) (((p >> 3) * (p >> 3)) >> 13)
336 ) >> 12;
337 var2 = ((int32_t)(p >> 2) * (int32_t)P8) >> 13;
338 p = (uint32_t)((int32_t)p + ((var1 + var2 + P7) >> 4));
339 return (float) p;
340 } else {
341 return NAN;
342 }
343 }
344
345 // Get humidity in %
346 float BMx280::getHumidity() {
347 if (id == BME280_ID) {
348 if (getRawData() && (uh != 0x8000)) {
349 int32_t temp_fine = calculateTempFine();
350 int32_t v_x1 = temp_fine - 76800L;
Atmospheric Pressure Sensors 299

351 int32_t v_x2 = ((int32_t) uh << 14) - ((int32_t) H4 << 20) -


352 ((int32_t) H5 * v_x1);
353 v_x2 = (v_x2 + 16384L) >> 15;
354 int32_t v_x3 = ((v_x1 * (int32_t) H6) >> 10 ) *
355 (((v_x1 * (int32_t) H3) >> 11 ) + 32768L);
356 v_x3 = ((v_x3 >> 10) + 2097152L) * (int32_t) H2 + 8192L;
357 v_x1 = v_x2 * (v_x3 >> 14);
358 v_x1 = v_x1 - ( ((( (v_x1 >> 15) * (v_x1 >> 15) ) >> 7) * (int32_t) H1)
359 >> 4 );
360 if (v_x1 < 0) {
361 v_x1 = 0;
362 } else if (v_x1 > 419430400L) {
363 v_x1 = 419430400L;
364 }
365 return (float) (v_x1 >> 12) / 1024.0f;
366 } else {
367 return NAN;
368 }
369 } else {
370 return NAN; // No humidity in BMP280
371 }
372 }
373
374 // Calculate altitude from pressure
375 float BMx280::calcAltitude(float pressure, float sealevelPressure) {
376 return 44330.0 * (1.0 - pow(pressure/sealevelPressure, 0.1903));
377 }
378
379 // Returns "fine resolution temperature"
380 // (as defined in section 8.2 of the datasheet)
381 int32_t BMx280::calculateTempFine() {
382 int32_t var1, var2;
383
384 var1 = ((int32_t) ut >> 3) - ((int32_t) T1 << 1);
385 var1 = (var1 * (int32_t) T2) >> 11;
386
387 var2 = ((int32_t) ut >> 4) - (int32_t) T1;
388 var2 = ((var2 * var2) >> 12) * (int32_t) T3;
389 var2 = var2 >> 14;
Atmospheric Pressure Sensors 300

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, &reg, 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, &reg, 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, &reg, 1, true);
466
467 // Read value
Atmospheric Pressure Sensors 302

468 i2c_read_blocking (i2c, addr, val, 3, false);


469 return ((uint32_t) val[0] << 12) |
470 ((uint32_t) val[1] << 4) |
471 ((uint32_t) val[2] >> 4);
472 }
473
474
475 // Main Program
476 int main() {
477 // Init stdio
478 stdio_init_all();
479 #ifdef LIB_PICO_STDIO_USB
480 while (!stdio_usb_connected()) {
481 sleep_ms(100);
482 }
483 #endif
484
485 // Init I2C interface
486 uint baud = i2c_init (I2C_ID, BAUD_RATE);
487 printf ("I2C @ %u Hz\n", baud);
488 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
489 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
490 gpio_pull_up(I2C_SCL_PIN);
491 gpio_pull_up(I2C_SDA_PIN);
492
493 // Init sensor
494 BMx280 bmp (I2C_ID);
495
496 // Display sensor ID
497 uint8_t id = bmp.getDeviceId();
498 printf ("Sensor ID = 0x%02X - %s\n", id,
499 (id == BMx280::BME280_ID)? "BME280" : "BMP280");
500
501 // Main loop
502 while(1) {
503 sleep_ms(2000);
504 bmp.doMeasure(true);
505 float temp = bmp.getTemperature();
506 float pressure = bmp.getPressure();
Atmospheric Pressure Sensors 303

507 float altitude = bmp.calcAltitude(pressure);


508 printf("Temperature: %.1f C Pressure: %.0f Pa Altitude: %.1f m",
509 temp, pressure, altitude);
510 if (bmp.getDeviceId() == BMx280::BME280_ID) {
511 float humidity = bmp.getHumidity();
512 printf(" Humidity: %.1f %%", humidity);
513 }
514 printf("\n");
515 }
516 }

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

Arduino BMP280/BME280 Example

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

MicroPython BMP280/BME280 Example

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.

The documentation for the class we are going to use is at


https://docs.circuitpython.org/projects/bme280/en/latest/api.html.
The code uses the “basic” implementation, there is also an “advanced” class with more function-
ality.
Atmospheric Pressure Sensors 307

CircuitPython BMP280/BME280 Example

1 # BME280 Sensor Example


2 import board
3 from busio import I2C
4 from time import sleep
5 from adafruit_bme280 import basic as bme280
6
7 i2c = I2C(sda=board.GP16, scl=board.GP17)
8 bme = bme280.Adafruit_BME280_I2C(i2c, 0x76)
9
10 while True:
11 press = bme.pressure
12 temp = bme.temperature
13 alt = bme.altitude
14 humid = bme.humidity
15 print ('Temperature: {:.1f}C Pressure: {:.0f}Pa Altitude: {:.1f}m Humidity: {\
16 :.1f}%'.format(
17 temp, press*100, alt, humid))
18 sleep(2)

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

Setting Oversampling Resolution


000 1 16 bit
001 2 17 bit
010 4 18 bit
011 8 19 bit
100 16 20 bit
101 32 21 bit

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 Calibration Coefficients


Atmospheric Pressure Sensors 309

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:

Value Name Description


0x00 NOP Does nothing. This is the value returned when you
read the CMD register
0xB0 FIFO_FLUSH Clears all data in the FIFO
0xB6 RESET Triggers a reset, all configurations go back to the
power-on defaults

The other registers are related to the FIFO.

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

Connecting Adafruit BMP390 Sensor Module to the Pi Pico

Check the pinout, pull-ups, and pull-downs of your breakout board!

C/C++ SDK Code


For the C/C++ SDK example, I am using the sample code (“API”) from Bosch. You can download
it from
https://github.com/boschsensortec/BMP3-Sensor-API
The files we need are bmp3_defs.h, bmp3.h, and bmp3.c. The code in this files relays on a few
routines we will provide in our main source file. I have implemented only support for I²C. The
CMakeLists.txt defines BMP3_FLOAT_COMPENSATION, so the library will return floating point
measurements.
The code uses the “Normal” mode to get readings at a rate of 0.2 Hz (one reading every five
seconds). The main loop checks the “Data Ready” bit in the interrupt status to find out when it
should get the data.
Atmospheric Pressure Sensors 312

C/C++ BMP390 Example

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

156 nt32_t len, void *intf_ptr) {


157 // Select register
158 i2c_write_blocking (I2C_ID, BMP_ADDR, &reg_addr, 1, true);
159
160 // Read data
161 i2c_read_blocking (I2C_ID, BMP_ADDR, reg_data, len, false);
162
163 return BMP3_OK;
164 }
165
166 /*!
167 * I2C write function
168 */
169 static BMP3_INTF_RET_TYPE bmp3_user_i2c_write(uint8_t reg_addr, const uint8_t *reg_d\
170 ata, uint32_t len, void *intf_ptr) {
171 // The SDK does not have a function for doing a continuous write from two buffers
172 // so we have to concatenate reg_addr and reg_data
173 uint8_t *aux = malloc (len+1);
174 aux[0] = reg_addr;
175 memcpy(aux+1, reg_data, len);
176 i2c_write_blocking (I2C_ID, BMP_ADDR, aux, len+1, false);
177 free (aux);
178
179 return BMP3_OK;
180 }
181
182 // Interface init
183 static BMP3_INTF_RET_TYPE bmp3_interface_init(struct bmp3_dev *bmp3) {
184 int8_t rslt = BMP3_OK;
185
186 if (bmp3 != NULL) {
187 dev_addr = BMP_ADDR;
188 bmp3->intf = BMP3_I2C_INTF;
189 bmp3->read = bmp3_user_i2c_read;
190 bmp3->write = bmp3_user_i2c_write;
191 bmp3->delay_us = bmp3_user_delay_us;
192 bmp3->intf_ptr = &dev_addr;
193 } else {
194 rslt = BMP3_E_NULL_PTR;
Atmospheric Pressure Sensors 317

195 }
196
197 return rslt;
198 }

Arduino Code
We are going to use the Adafruit BMP3XX Library, if asked select to install all dependencies.

Adafruit BMP3XX Library

Arduino BMP390 Example

1 // BMP390 Sensor Example


2 #include <Wire.h>
3 #include <Adafruit_Sensor.h>
4 #include "Adafruit_BMP3XX.h"
5
6 #define SEALEVELPRESSURE_HPA (1013.25)
7
8 Adafruit_BMP3XX bmp;
9
10 // Initialization
11 void setup() {
12 // Init serial
13 Serial.begin (115200);
14 while (!Serial)
15 ;
Atmospheric Pressure Sensors 318

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.

The documentation for the class we are going to use is at


https://docs.circuitpython.org/projects/bmp3xx/en/latest/api.html.
CircuitPython BMP390 Example

1 # BMP390 Sensor Example


2 import board
3 from busio import I2C
4 from time import sleep
5 from adafruit_bmp3xx import BMP3XX_I2C
6
7 i2c = I2C(sda=board.GP16, scl=board.GP17)
8 bmp = BMP3XX_I2C(i2c)
9
10 while True:
11 press = bmp.pressure
12 temp = bmp.temperature
13 alt = bmp.altitude
14 print ('Temperature: {:.1f}C Pressure: {:.0f}Pa Altitude: {:.1f}m'.format(
15 temp, press*100, alt))
16 sleep(2)

Sensors Comparison Table


The following table resumes the interface and max resolution characteristics of the sensors we
examined.
Atmospheric Pressure Sensors 321

Sensor Interface Pressure Temperature Humidity


BMP085 I²C 19-bit 19-bit
biBM180 I²C 19-bit 19-bit
BMP280 I²C, SPI 20-bit 20-bit
BME280 I²C, SPI 20-bit 20-bit 20-bit
BMP390 I²C, SPI 21-bit 21-bit
Electronic Compass, Accelerometers,
and Gyroscopes
The sensors in this chapter can detect heading, acceleration, and angular velocity. They can
be used for navigating some special user interfaces by detecting the position and movement of
whatever is attached.
They are all “3-axis”, meaning they will measure along three orthogonal directions (X, Y, Z). To
use these measurements you need to know how the axes relates to the sensor and the position
where the sensor is fixed to.
An Electronic Compass is a magnetic sensor that can measure the strength of Earth’s magnetic
field, by combining the strength in the various axes you can find the heading to the magnetic
North.
If you want to find the heading to the geographic North, you have to add the magnetic declination,
various sites on the Internet supply this information for your location.
Accelerometers measure what is formally called proper acceleration, the acceleration relative
to a free-fall observer. In practical terms, this means that, if an accelerometer is at rest over an
immobile surface (relative to the ground), it will measure the acceleration due to Earth’s gravity
(g). If an accelerometer is in free fall, it will measure zero acceleration.
If you know the accelerometer is at rest, you can use it to measure its inclination. The magnitude
of the three dimension acceleration vector will be 1 g.
If you know the (fixed) position of the accelerometer in a moving body, you can discount the
gravity and find out the acceleration. In most applications, we are not interested in the exact
values but want to check if the body is subjected to accelerations above some threshold (for
example, to detect movement).
Tapping over the accelerometer (or whatever it is affixed to), will generate an acceleration for a
very short time. Using this and inclination, you can create a user interface where commands are
given by tapping or turning an object.
Accelerometers return numbers proportional to the acceleration. To convert that to an actual
measurement (in gs or m/s²) you have to multiply or divide these numbers by a scale. The
datasheets will give the typical value for the scale as resolution or sensitivity, it can be expressed
as what fraction of a g each count corresponds (g/LSB) or how many counts corresponds to one
g (LSB/g). (LSB means least significant bit).
Electronic Compass, Accelerometers, and Gyroscopes 323

There are two types of calibration we can do to an accelerometer:

• 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.

HMC5883L, HMC5983, and QMC5883L 3-Axis Magnetic


Sensor
These sensors measure the magnetic field in three axes. If you don’t have a strong magnetic field
nearby, these measurements can be used to locate the heading to the magnetic North.

HMC5883L/QMC5883L (left) and HMC5983(right) Modules

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

HMC5883L and HMC5983 Registers and Operation


The Control Register A is located in address 0x00, it controls the temperature sensor (TS -
HMC5983 only), the number of samples averaged for a reading (MA), the output data rate (DO),
and the measurement mode (MS).
Electronic Compass, Accelerometers, and Gyroscopes 326

HMC5883L/HMC5983 Control Register A

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).

DO Data Output Rate (Hz)


000 0.75
001 1.5
010 3
011 7.5
100 15 (default)
101 30
110 75
111 220 (HMC5983 only)

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

HMC5883L/HMC5983 Control Register B

The unused bits should be set to zero for correct operation.


When you change the gain, the new setting will be effective from the second measurement on.
The Mode Register is located in address 0x02 and controls a few things besides selecting the
operating mode:

• 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

read. Only available in HMC5983.


• Bit 1 (LOCK): is set if the mode register is read or some (but not all) data output registers
are read. Once the lock is set, no new data can be placed in the data output registers. To
clear this bit, you need to: (1) read all data output registers (the bit will be cleared when
the next measurement starts) or (2) write into the mode register or (3) write into Control
Register A or (4) power is reset.
• Bit 0 (RDY ): is set when new data is written into all the data output registers. It’s cleared
when new data is written into the first data output register. This information is also
available at the DRDY pin. Notice that data ready is signaled by the LOW to HIGH
transition not by a HIGH state.

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

QMC5883L Registers and Operation


Note that the 16-bit output registers in the QMC5883L have LSB first, while in the HMC5x83 the
MSB is first. The order of the axis is also different.
The Data Output Registers, with addresses from 0x00 to 0x05, 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, Y, and Z.
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 Registers at 0x06 has the following information:

• 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).

QMC5883L Control Register 1

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).

QMC5883L Control Register 2

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.

HMC5883L, HMC5983, and QMC5883L Example


We will use the I²C interface in this 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-up for the
SPI_CS pin in the HMC sensors.
Electronic Compass, Accelerometers, and Gyroscopes 330

Connecting a HMC5883L, HMC5983, or QMC5883L Sensor to the Pi Pico

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.

C/C++ SDK Code


The code supports all three sensor models. You can specify the model in the constructor or let the
class figure it out (this will work only if there is a single sensor in the I²C bus). The configuration
for the sensors is fixed in the code.
Electronic Compass, Accelerometers, and Gyroscopes 331

C/C++ HMC5883L, HMC5983, and QMC5883L Example

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

117 write8(regCFGB_HMC, 0x00);


118 write8(regMODE_HMC, 0x00);
119 }
120
121 return true;
122 }
123
124 // Returns the sensor model
125 COMPASS::MODEL COMPASS::getModel() {
126 return model;
127 }
128
129 // Get the compass heading (0 to 360 degrees)
130 float COMPASS::getHeading() {
131 uint8_t data[6];
132 uint8_t reg[1];
133 int16_t x,y,z;
134
135
136 if (model == UNDEFINED) {
137 return 0.0f;
138 }
139
140 if (model == QMC) {
141 // wait for data
142 while ((read8(regST_QMC) & 0x01) == 0)
143 ;
144 // read data
145 reg[0] = regXL_QMC;
146 i2c_write_blocking (i2c, addr, reg, 1, true);
147 i2c_read_blocking (i2c, addr, data, 6, false);
148 // get 16-bit info
149 x = data[0] + (data[1] << 8);
150 y = data[2] + (data[3] << 8);
151 z = data[4] + (data[5] << 8);
152 } else {
153 // wait for data
154 while ((read8(regST_HMC) & 0x01) == 0)
155 ;
Electronic Compass, Accelerometers, and Gyroscopes 335

156 // read data


157 reg[0] = regXH_HMC;
158 i2c_write_blocking (i2c, addr, reg, 1, true);
159 i2c_read_blocking (i2c, addr, data, 6, false);
160 // get 16-bit info
161 x = data[1] + (data[0] << 8);
162 z = data[3] + (data[2] << 8);
163 y = data[5] + (data[4] << 8);
164 }
165
166 // compute heading
167 float angle = atan2((float)y,(float)x);
168 //printf ("%8d %8d %8d %.2f\n", x, y, z, angle/PI);
169 return (angle*180.0f)/PI + 180.0f;
170 }
171
172 // Check if there is something at an I2C address
173 bool COMPASS::checkAddr(uint8_t addr) {
174 int ret;
175 uint8_t rxdata;
176
177 ret = i2c_read_blocking(this->i2c, addr, &rxdata, 1, false);
178 return ret >= 0;
179 }
180
181 // Write an 8-bit value into a register
182 void COMPASS::write8 (uint8_t reg, uint8_t val) {
183 uint8_t buffer[2];
184
185 buffer[0] = reg;
186 buffer[1] = val;
187 i2c_write_blocking (i2c, addr, buffer, 2, false);
188 }
189
190 // Read a 8-bit value from a register
191 uint8_t COMPASS::read8 (uint8_t reg) {
192 uint8_t val[1];
193
194 // Select register
Electronic Compass, Accelerometers, and Gyroscopes 336

195 i2c_write_blocking (i2c, addr, &reg, 1, true);


196
197 // Read value
198 i2c_read_blocking (i2c, addr, val, 1, false);
199 return val[0];
200 }
201
202
203 // Main Program
204 int main() {
205 // Init stdio
206 stdio_init_all();
207 #ifdef LIB_PICO_STDIO_USB
208 while (!stdio_usb_connected()) {
209 sleep_ms(100);
210 }
211 #endif
212
213 // Init I2C interface
214 uint baud = i2c_init (I2C_ID, BAUD_RATE);
215 printf ("I2C @ %u Hz\n", baud);
216 gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C);
217 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);
218 gpio_pull_up(I2C_SCL_PIN);
219 gpio_pull_up(I2C_SDA_PIN);
220
221 // Init sensor
222 COMPASS compass (I2C_ID);
223 if (!compass.begin()) {
224 printf ("Sensor not found!");
225 while (true) {
226 sleep_ms(100);
227 }
228 }
229
230 // Display sensor model
231 COMPASS::MODEL model = compass.getModel();
232 printf ("Sensor Model = %s\n",
233 (model == COMPASS::QMC) ? "QMC5883L" :
Electronic Compass, Accelerometers, and Gyroscopes 337

234 (model == COMPASS::HMC_58) ? "HMC5883L" : "HMC5983"


235 );
236
237 // Main loop
238 while(1) {
239 sleep_ms(2000);
240 printf ("Heading: %.1f\n", compass.getHeading());
241 }
242 }

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

Arduino HMC5883L, HMC5983, and QMC5883L Example

1 // QMC5883L & HMC5883L Example


2
3 #include <DFRobot_QMC5883.h>
4
5 //DFRobot_QMC5883 compass(&Wire, QMC5883_ADDRESS);
6 DFRobot_QMC5883 compass(&Wire, HMC5883L_ADDRESS);
7
8
9 // Initialization
10 void setup() {
11 while(!Serial) {
12 delay(100);
13 }
14 Serial.begin(115200);
15 Wire.setSDA(16);
16 Wire.setSCL(17);
17 while (!compass.begin())
18 {
19 Serial.println("Could not find a valid 5883 sensor, check wiring!");
20 delay(2000);
21 }
22 Serial.println(compass.isHMC() ? "HMC5883L" : "QMC5883L");
23 }
24
25 // Main Loop
26 void loop() {
27 // Reads the data from the sensor and returns a reference to
28 // the internal vector
29 sVector_t mag = compass.readRaw();
30 // Fills the heading field in the vector
31 compass.getHeadingDegrees();
32 Serial.print("Heading: ");
33 Serial.println(mag.HeadingDegress,1);
34 delay(2000);
35 }
Electronic Compass, Accelerometers, and Gyroscopes 339

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 3-Axis Accelerometer


This sensor supports I²C and SPI connections and has two interrupt outputs (INT1 and INT2).
Electronic Compass, Accelerometers, and Gyroscopes 342

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

Here is information on the most useful 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.

Connecting an ADXL345 Sensor to the Pi Pico

C/C++ SDK Code


In this code, we are using a fixed configuration (2g range with 10-bit measurements)with no
calibration.
Electronic Compass, Accelerometers, and Gyroscopes 345

C/C++ ADXL345 Example

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, &reg, 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, &reg, 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

156 gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C);


157 gpio_pull_up(I2C_SCL_PIN);
158 gpio_pull_up(I2C_SDA_PIN);
159
160 // Init sensor
161 ADLX345 sensor (I2C_ID, 0x53);
162 sensor.begin();
163
164 // Display sensor ID
165 printf ("Sensor ID: %02X\n", sensor.getId());
166
167 // Main loop
168 ADLX345::VECT_3D data;
169 while(1) {
170 sleep_ms(2000);
171 sensor.getAccel(&data);
172 printf ("Accel X:%.1f Y:%.1f Z:%.1f\n", data.x, data.y, data.z);
173 }
174 }

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

Adafruit ADXL345 Library

If asked to install dependencies, select “install all”.

Install Library Dependencies

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

Arduino ADXL345 Example

1 // ADXL345 Sensor Example


2
3 #include "Wire.h"
4 #include <Adafruit_Sensor.h>
5 #include <Adafruit_ADXL345_U.h>
6
7 Adafruit_ADXL345_Unified sensor = Adafruit_ADXL345_Unified(1);
8
9 const float G = 9.8; // to convert m/s^2 to g
10
11 // Initialization
12 void setup() {
13 while(!Serial) {
14 delay(100);
15 }
16 Serial.begin(115200);
17 Wire.setSDA(16);
18 Wire.setSCL(17);
19 sensor.begin();
20 sensor.setRange(ADXL345_RANGE_2_G);
21 }
22
23 // Main Loop
24 void loop() {
25 sensors_event_t event;
26 sensor.getEvent(&event);
27
28 Serial.print ("Accel X:");
29 Serial.print (event.acceleration.x/G, 1);
30 Serial.print ("g Y:");
31 Serial.print (event.acceleration.y/G, 1);
32 Serial.print ("g Z:");
33 Serial.print (event.acceleration.z/G, 1);
34 Serial.println ("g");
35
36 delay(2000);
37 }
Electronic Compass, Accelerometers, and Gyroscopes 352

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

36 # accX, acccY, accZ, temp, gyroX, gyroY, gyroZ


37 def read_raw(self):
38 data = i2c.readfrom_mem(self.addr, ADXL345._DATAX0, 6)
39 raw = []
40 for i in range (0, 6, 2):
41 v = (data[i+1] << 8) + data[i]
42 if (v & 0x8000) != 0:
43 v = v - 0x10000
44 raw.append(v)
45 return raw
46
47 # Read acceleration in g
48 def get_accel(self, scale = 256.0):
49 data = self.read_raw()
50 return (data[0]/scale, data[1]/scale, data[2]/scale)
51
52 # Read an 8-bit register
53 def read_reg(self, reg):
54 return i2c.readfrom_mem(self.addr, reg, 1)[0]
55
56 # Test Program
57 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
58 sensor = ADXL345(i2c, ADXL345._ALT_ADDRESS)
59 sensor.begin()
60 print('ID = {:02X}'.format(sensor.get_id()))
61 while True:
62 print (sensor.read_raw())
63 x,y,z = sensor.get_accel()
64 print ('Accel X:{:.1f}g y:{:.1f}g Z:{:.1f}g'.format(x, y, z))
65 print ()
66 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.
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 3-Axis Accelerometer


This sensor communicates via I²C, supporting standard (100KHz) and fast (400KHz) speeds. You
can select its address between 0x1C and 0x1D through the SA0 pin (LOW selects 0x1C, HIGH
selects 0x1D). It has two interrupt outputs (INT1 and INT2).
Electronic Compass, Accelerometers, and Gyroscopes 355

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

Here is information on the most useful 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.

Connecting an MMA8452 Sensor to the Pi Pico

C/C++ SDK Code


The example code includes a simple class to set up the sensor with fixed parameters and read the
measurements (adapted from my CircuitPython code, itself adapted from Sparkfun Arduino C++
library).
Electronic Compass, Accelerometers, and Gyroscopes 357

C/C++ SDK MMA8452 Example

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, &reg, 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, &reg, 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

Sparkfun MMA8452 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

MicroPython MMA8452 Example

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

117 return (self.readRegister(self.STATUS_MMA8452Q) & 0x08) != 0


118
119 # Selects scale
120 def setScale(self, fsr):
121 if not fsr in (self.SCALE_2G, self.SCALE_4G, self.SCALE_8G):
122 raise ValueError("Invalid scale")
123
124 # Scale can only be changed in standby mode
125 if self.isActive():
126 self.standby()
127
128 # Update config
129 cfg = self.readRegister(self.XYZ_DATA_CFG)
130 cfg = (cfg & 0xFC) | (fsr >> 2)
131 self.writeRegister(self.XYZ_DATA_CFG, cfg)
132
133 # Goes to active mode
134 self.active()
135
136 # Selects data rate (ODR)
137 def setDataRate(self, odr):
138 if not odr in (self.ODR_800, self.ODR_400, self.ODR_200, self.ODR_100,
139 self.ODR_50, self.ODR_12, self.ODR_6, self.ODR_1):
140 raise ValueError("Invalid data rate")
141
142 # can only be changed in standby mode
143 if self.isActive():
144 self.standby()
145
146 # Update config
147 ctrl = self.readRegister(self.CTRL_REG1)
148 ctrl = (ctrl & 0xC7) | (odr << 3)
149 self.writeRegister(self.CTRL_REG1, ctrl)
150
151 # Goes to active mode
152 self.active()
153
154 # Set tap detection
155 def setupTap(self, xThs, yThs, zThs):
Electronic Compass, Accelerometers, and Gyroscopes 367

156 # can only be changed in standby mode


157 if self.isActive():
158 self.standby()
159
160 # Update config
161 temp = 0x40
162 if (xThs & 0x80) == 0:
163 temp |= 0x03
164 self.writeRegister(self.PULSE_THSX, xThs)
165 if (yThs & 0x80) == 0:
166 temp |= 0x0C
167 self.writeRegister(self.PULSE_THSY, yThs)
168 if (zThs & 0x80) == 0:
169 temp |= 0x30
170 self.writeRegister(self.PULSE_THSZ, zThs)
171 self.writeRegister(self.PULSE_CFG, temp)
172 self.writeRegister(self.PULSE_TMLT, 0x30)
173 self.writeRegister(self.PULSE_LTCY, 0xA0)
174 self.writeRegister(self.PULSE_WIND, 0xFF)
175
176 # Goes to active mode
177 self.active()
178
179 # Read tap status
180 def readTap(self):
181 tapStat = self.readRegister(self.PULSE_SRC)
182 if (tapStat & 0x80) != 0:
183 return tapStat & 0x7F
184 else:
185 return 0
186
187 # Set portrait/landscape detection
188 def setupPL(self):
189 # can only be changed in standby mode
190 if self.isActive():
191 self.standby()
192
193 # Update config
194 self.writeRegister(self.PL_CFG, self.readRegister(self.PL_CFG) | 0x40)
Electronic Compass, Accelerometers, and Gyroscopes 368

195 self.writeRegister(self.PL_COUNT, 0x50)


196
197 # Goes to active mode
198 self.active()
199
200 # Read portrait/landscape status
201 def readPL(self):
202 plStat = self.readRegister(self.PL_STATUS)
203 if (plStat & 0x40) != 0:
204 return self.LOCKOUT
205 else:
206 return (plStat & 0x06) >> 1
207
208 # Orientation tests
209 def isRight(self):
210 return self.redPL() == self.LANDSCAPE_R
211
212 def isLeft(self):
213 return self.redPL() == self.LANDSCAPE_L
214
215 def isUp(self):
216 return self.redPL() == self.LANDSCAPE_U
217
218 def isDown(self):
219 return self.redPL() == self.LANDSCAPE_D
220
221 def isFlat(self):
222 return self.redPL() == self.LOCKOUT
223
224 # Enter standby mode
225 def standby(self):
226 c = self.readRegister(self.CTRL_REG1)
227 self.writeRegister(self.CTRL_REG1, c & 0xFE)
228
229 # Enter active mode
230 def active(self):
231 c = self.readRegister(self.CTRL_REG1)
232 self.writeRegister(self.CTRL_REG1, c | 0x01)
233
Electronic Compass, Accelerometers, and Gyroscopes 369

234 # Check if active


235 def isActive(self):
236 state = self.readRegister(self.SYSMOD) & 0x03
237 return state != self.SYSMOD_STANDBY
238
239 # Read a register
240 def readRegister(self, reg):
241 return self.i2c.readfrom_mem(self.addr, reg, 1)[0]
242
243 # Read multiple registers
244 def readRegisters(self, reg, nregs):
245 result = bytearray(nregs)
246 self.i2c.readfrom_mem_into(self.addr, reg, result)
247 return result
248
249 # Write into a register
250 def writeRegister(self, reg, value):
251 self.i2c.writeto_mem(self.addr, reg, bytes([value]))
252
253 if __name__ == "__main__":
254 '''
255 Quick test
256 '''
257 from machine import Pin, I2C
258 from time import sleep
259
260 i2c = I2C(0, sda=Pin(16), scl=Pin(17))
261 sensor = MMA8452(i2c)
262 if not sensor.begin():
263 print ("Sensor not found")
264 else:
265 sensor.active()
266 print ("Raw:")
267 for i in range(0, 5):
268 while not sensor.available:
269 pass
270 print (sensor.getRawAcel())
271 sleep(3)
272 print ("Scaled:")
Electronic Compass, Accelerometers, and Gyroscopes 370

273 for i in range(0, 5):


274 while not sensor.available:
275 pass
276 print (sensor.getCalclulatedAcel())
277 sleep(3)
278 print ("Position:")
279 sensor.setupPL()
280 oldpos = -1
281 while True:
282 sleep (0.3)
283 newpos = sensor.readPL()
284 if newpos == oldpos:
285 continue
286 oldpos = newpos
287 if newpos == sensor.LOCKOUT:
288 print ("Laying down")
289 elif newpos == sensor.LANDSCAPE_R:
290 print ("Landscape facing right")
291 elif newpos == sensor.LANDSCAPE_L:
292 print ("Landscape facing left")
293 elif newpos == sensor.PORTRAIT_U:
294 print ("Portrait facing up")
295 elif newpos == sensor.PORTRAIT_D:
296 print ("Portrait facing down")
297 else:
298 print ("Invalid position: "+str(newpos))

CircuitPython Code
The code has my own class for the MMA8452 (adapted from Sparkfun Arduino C++ library).
Electronic Compass, Accelerometers, and Gyroscopes 371

CircuitPython MMA8452 Example

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

117 return (self.readRegister(self.STATUS_MMA8452Q) & 0x08) != 0


118
119 # Selects scale
120 def setScale(self, fsr):
121 if not fsr in (self.SCALE_2G, self.SCALE_4G, self.SCALE_8G):
122 raise ValueError("Invalid scale")
123
124 # Scale can only be changed in standby mode
125 if self.isActive():
126 self.standby()
127
128 # Update config
129 cfg = self.readRegister(self.XYZ_DATA_CFG)
130 cfg = (cfg & 0xFC) | (fsr >> 2)
131 self.writeRegister(self.XYZ_DATA_CFG, cfg)
132
133 # Goes to active mode
134 self.active()
135
136 # Selects data rate (ODR)
137 def setDataRate(self, odr):
138 if not odr in (self.ODR_800, self.ODR_400, self.ODR_200, self.ODR_100,
139 self.ODR_50, self.ODR_12, self.ODR_6, self.ODR_1):
140 raise ValueError("Invalid data rate")
141
142 # can only be changed in standby mode
143 if self.isActive():
144 self.standby()
145
146 # Update config
147 ctrl = self.readRegister(self.CTRL_REG1)
148 ctrl = (ctrl & 0xC7) | (odr << 3)
149 self.writeRegister(self.CTRL_REG1, ctrl)
150
151 # Goes to active mode
152 self.active()
153
154 # Set tap detection
155 def setupTap(self, xThs, yThs, zThs):
Electronic Compass, Accelerometers, and Gyroscopes 375

156 # can only be changed in standby mode


157 if self.isActive():
158 self.standby()
159
160 # Update config
161 temp = 0x40
162 if (xThs & 0x80) == 0:
163 temp |= 0x03
164 self.writeRegister(self.PULSE_THSX, xThs)
165 if (yThs & 0x80) == 0:
166 temp |= 0x0C
167 self.writeRegister(self.PULSE_THSY, yThs)
168 if (zThs & 0x80) == 0:
169 temp |= 0x30
170 self.writeRegister(self.PULSE_THSZ, zThs)
171 self.writeRegister(self.PULSE_CFG, temp)
172 self.writeRegister(self.PULSE_TMLT, 0x30)
173 self.writeRegister(self.PULSE_LTCY, 0xA0)
174 self.writeRegister(self.PULSE_WIND, 0xFF)
175
176 # Goes to active mode
177 self.active()
178
179 # Read tap status
180 def readTap(self):
181 tapStat = self.readRegister(self.PULSE_SRC)
182 if (tapStat & 0x80) != 0:
183 return tapStat & 0x7F
184 else:
185 return 0
186
187 # Set portrait/landscape detection
188 def setupPL(self):
189 # can only be changed in standby mode
190 if self.isActive():
191 self.standby()
192
193 # Update config
194 self.writeRegister(self.PL_CFG, self.readRegister(self.PL_CFG) | 0x40)
Electronic Compass, Accelerometers, and Gyroscopes 376

195 self.writeRegister(self.PL_COUNT, 0x50)


196
197 # Goes to active mode
198 self.active()
199
200 # Read portrait/landscape status
201 def readPL(self):
202 plStat = self.readRegister(self.PL_STATUS)
203 if (plStat & 0x40) != 0:
204 return self.LOCKOUT
205 else:
206 return (plStat & 0x06) >> 1
207
208 # Orientation tests
209 def isRight(self):
210 return self.redPL() == self.LANDSCAPE_R
211
212 def isLeft(self):
213 return self.redPL() == self.LANDSCAPE_L
214
215 def isUp(self):
216 return self.redPL() == self.LANDSCAPE_U
217
218 def isDown(self):
219 return self.redPL() == self.LANDSCAPE_D
220
221 def isFlat(self):
222 return self.redPL() == self.LOCKOUT
223
224 # Enter standby mode
225 def standby(self):
226 c = self.readRegister(self.CTRL_REG1)
227 self.writeRegister(self.CTRL_REG1, c & 0xFE)
228
229 # Enter active mode
230 def active(self):
231 c = self.readRegister(self.CTRL_REG1)
232 self.writeRegister(self.CTRL_REG1, c | 0x01)
233
Electronic Compass, Accelerometers, and Gyroscopes 377

234 # Check if active


235 def isActive(self):
236 state = self.readRegister(self.SYSMOD) & 0x03
237 return state != self.SYSMOD_STANDBY
238
239 # Read a register
240 def readRegister(self, reg):
241 selreg = bytearray([reg])
242 data = bytearray(1)
243 i2c.try_lock()
244 i2c.writeto_then_readfrom(self.addr, selreg, data)
245 i2c.unlock()
246 return data[0]
247
248 # Read multiple registers
249 def readRegisters(self, reg, nregs):
250 selreg = bytearray([reg])
251 result = bytearray(nregs)
252 i2c.try_lock()
253 i2c.writeto_then_readfrom(self.addr, selreg, result)
254 i2c.unlock()
255 return result
256
257 # Write into a register
258 def writeRegister(self, reg, value):
259 i2c.try_lock()
260 i2c.writeto(self.addr, bytearray([reg, value]))
261 i2c.unlock()
262
263 if __name__ == "__main__":
264 '''
265 Quick test
266 '''
267 import board
268 from digitalio import DigitalInOut, Pull
269 from busio import I2C
270 from time import sleep
271
272 i2c = I2C(sda=board.GP16, scl=board.GP17)
Electronic Compass, Accelerometers, and Gyroscopes 378

273 sensor = MMA8452(i2c)


274 if not sensor.begin():
275 print ("Sensor not found")
276 else:
277 sensor.active()
278 print ("Raw:")
279 for i in range(0, 5):
280 while not sensor.available:
281 pass
282 print (sensor.getRawAcel())
283 sleep(3)
284 print ("Scaled:")
285 for i in range(0, 5):
286 while not sensor.available:
287 pass
288 print (sensor.getCalclulatedAcel())
289 sleep(3)
290 print ("Position:")
291 sensor.setupPL()
292 oldpos = -1
293 while True:
294 sleep (0.3)
295 newpos = sensor.readPL()
296 if newpos == oldpos:
297 continue
298 oldpos = newpos
299 if newpos == sensor.LOCKOUT:
300 print ("Laying down")
301 elif newpos == sensor.LANDSCAPE_R:
302 print ("Landscape facing right")
303 elif newpos == sensor.LANDSCAPE_L:
304 print ("Landscape facing left")
305 elif newpos == sensor.PORTRAIT_U:
306 print ("Portrait facing up")
307 elif newpos == sensor.PORTRAIT_D:
308 print ("Portrait facing down")
309 else:
310 print ("Invalid position: "+str(newpos))
Electronic Compass, Accelerometers, and Gyroscopes 379

MPU6050 3-Axis Accelerometer and Gyroscope


This sensor can communicate via I²C, supporting standard (100KHz) and fast (400KHz) speeds.
You can select its I²C address between 0x68 and 0x69.

MPU6050 Module

There is also an MPU6000 model that adds SPI communication at up to 1MHz.


The MPU6050 has an acceleration selectable full scale of ±2g, ±4g, ±8, and ±16 g, and an angular
selectable rate of ±250, ±500, ±1000, and ±2000 dps (degrees per second). It features 16-bit ADCs
and a temperature sensod. It also supports an external auxiliary sensor and sync signal (FSYNC).
The MPU6050 contains a 1024-byte FIFO. The FIFO configuration register determines which data
is written into the FIFO. Possible choices include gyro data, accelerometer data, temperature
readings, auxiliary sensor readings, and FSYNC input.
The MPU has a huge number of registers, they are described in a
specific document⁹ instead of the datasheet¹⁰. I will give here information on the most useful
ones:

• 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

MPU6050 DLPF settings

• GYRO_CONFIG (0x1B): selects the full scale of the gyroscope outputs.

FD_SEL Full Scale Range Sensitivity


0 ± 250°/s 131 LSB/°/s
1 ± 500°/s 65.5 LSB/°/s
2 ± 1000°/s 32.8 LSB/°/s
3 ± 2000°/s 16.4 LSB/°/s

• AFS_CONFIG (0x1C): selects the full scale of the accelerometer outputs.

AFS_SEL Full Scale Range Sensitivity


0 ± 2g 16384 LSB/g
1 ± 4g 8192 LSB/g
2 ± 8g 4096 LSB/g
3 ± 16g 2048 LSB/g

• 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

• WHO_AM_I (0x75): contains the device id, 0x68.

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.

Connecting an MPU6050 Sensor to the Pi Pico

C/C++ SDK Code


The example code includes a simple class to set up the sensor with fixed parameters and read the
measurements.
C/C++ SDK MPU6050 Example

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

49 void getAccel(VECT_3D *vect, float scale = 16384.0);


50 void getGyro(VECT_3D *vect, float scale = 65.5);
51 float getTemp();
52
53 private:
54
55 // Registers Address
56 static const int SMPLRT_DIV = 0x19;
57 static const int CONFIG = 0x1A;
58 static const int GYRO_CONFIG = 0x1B;
59 static const int ACCEL_CONFIG = 0x1C;
60 static const int ACCEL_OUT = 0x3B;
61 static const int TEMP_OUT = 0x41;
62 static const int GYRO_OUT = 0x43;
63 static const int SIG_PATH_RESET = 0x68;
64 static const int PWR_MGMT_1 = 0x6B;
65 static const int PWR_MGMT_2 = 0x6C;
66 static const int WHO_AM_I = 0x75;
67
68 // I2C instance
69 i2c_inst_t *i2c;
70
71 // Sensor I2C Address
72 uint8_t addr;
73
74 // Sensor ID
75 uint8_t id;
76
77 // Raw data
78 int16_t raw[14];
79
80 // Private rotines
81 void readRaw(void);
82 uint8_t read8(uint8_t reg);
83 void write8(uint8_t reg, uint8_t val);
84 };
85
86 // Constructor
87 MPU6050::MPU6050 (i2c_inst_t *i2c, uint8_t addr) {
Electronic Compass, Accelerometers, and Gyroscopes 384

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, &reg, 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, &reg, 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

1 // MPU6050 Sensor Example


2
3 #include "Wire.h"
4 #include <MPU6050_light.h>
5
6 MPU6050 sensor(Wire);
7 long nextReading = 0L;
8
9 // Initialization
10 void setup() {
11 while(!Serial) {
12 delay(100);
13 }
14 Serial.begin(115200);
15 Wire.setSDA(16);
16 Wire.setSCL(17);
17 Wire.begin();
18 sensor.begin();
Electronic Compass, Accelerometers, and Gyroscopes 389

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.

The documentation for the class we are going to use is at


https://docs.circuitpython.org/projects/mpu6050/en/latest/api.html
The code shows tap, movement, and free-fall events detected by the sensor.
CircuitPython MPU6050 Example
1 # MPU6050 Sensor Example
2 import board
3 from busio import I2C
4 from time import sleep
5 import adafruit_mpu6050
6
7 # Set up sensor
8 i2c = I2C(sda=board.GP16, scl=board.GP17)
9 sensor = adafruit_mpu6050.MPU6050(i2c)
10
11 # Main loop
12 G = 9.8
13 while True:
14 x,y,z = sensor.acceleration
15 print ('Accel X:{:.1f}g y:{:.1f}g Z:{:.1f}g'.format(x/G, y/G, z/G))
16 x,y,z = sensor.gyro
17 print ('Gyro X:{:.1f} y:{:.1f} Z:{:.1f}'.format(x,y,z))
18 print ('Temp: {:.1f}C'.format(sensor.temperature))
19 print ()
20 sleep(2)

Sensors Comparison Table


The following table gives an idea of the features available in the sensors examined in this chapter.
Electronic Compass, Accelerometers, and Gyroscopes 394

feature xMC5x83 ADXL345 MMA8452 MPU6050


interface I2C I2C, SPI I2C
compass x - - -
temperature * - - *
accelerometer - x x x
accel ranges - 2,4,8,16 2,4,8 2,4,8,16
accel bits - 13 12 16
tap detection - x x x
gyroscope - - - x
gyro ranges - - - 250,500,1000,2000
gyro bits - - - 16

* Only HMC5983 and QMC5883L


Miscellaneous Sensors
In this chapter, we will see some very useful sensors that did not fit in the previous chapters.

HC-SR04 Ultrasonic Sensor


The HC-SR04 is a very popular sensor for measuring distance using ultrasonic sound pulses.

HC-SR04 Ultrasonic Sensor

The sensor has four pins:

• GND: should be connected to the ground.


• Vcc: should be connected to 5V (as specified in the datasheet). The are reports of this sensor
working with 3.3V, my experience is that it will not work reliably with this voltage.
• Trig: a pulse in this pin will trigger the transmission of the ultrasonic pulses. This is an
input pin and should typically be at a LOW level.
• Echo: signals the detection of the echo of the pulses. This is an output pin and is at a LOW
level when no measurement is happening.

A measurement follows these steps:

• 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°:

HC-SR04 signal dispersion

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

HY-SRF05 Ultrasonic Sensor

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

Connecting the HC-SR04 Sensor to the Pi Pico

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.

C/C++ SDK Code


The PIO in the RP2040 microcontroller is great for measuring pulses. As we want to measure
(with good precision) a time between 150us and 38ms, a half-microsecond cycle (2MHz clock) is
a good choice.
The PIO program is not complicated, but there are some PIO instructions peculiarities:

• 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

15 set pins, 1 [19]


16 set pins, 0
17
18 // wait for the start of the echo pulse
19 wait 1 pin 0
20
21 // wait for the end of the echo pulse
22 // decrements X every 2 cycles (1us)
23 dowait:
24 jmp pin, continue
25 jmp end
26 continue:
27 jmp x--, dowait
28
29 // return pulse duration
30 end:
31 mov isr,x
32 push
33 .wrap
34
35 % c-sdk {
36 // Helper function to set a state machine to run our PIO program
37 static inline void hcsr04_program_init(PIO pio, uint sm, uint offset,
38 uint triggerPin, uint echoPin) {
39
40 // Get an initialized config structure
41 pio_sm_config c = hcsr04_program_get_default_config(offset);
42
43 // Map the state machine's pin groups
44 sm_config_set_set_pins(&c, triggerPin, 1);
45 sm_config_set_in_pins(&c, echoPin);
46 sm_config_set_jmp_pin(&c, echoPin);
47
48 // Set the pins directions at the PIO
49 pio_sm_set_consecutive_pindirs(pio, sm, triggerPin, 1, true);
50 pio_sm_set_consecutive_pindirs(pio, sm, echoPin, 1, false);
51
52 // Make sure trigger is low
53 pio_sm_set_pins_with_mask(pio, sm, 1 << triggerPin, 0);
Miscellaneous Sensors 400

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

51 uint32_t ellapsed = TIMEOUT - val;


52 float distance = (ellapsed * 0.0343) / 2;
53 printf ("Distance = %.1f cm\n", distance);
54 sleep_ms(2000);
55 } else {
56 printf ("** TIMEOUT **\n");
57 }
58 }
59 }

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

1 // HC-SR04 Ultrasonic Sensor Example


2
3 #define PIN_TRIGGER 17
4 #define PIN_ECHO 16
5
6 // Initialization
7 void setup() {
8 // Init the serial
9 while (!Serial) {
10 delay(100);
11 }
12 Serial.begin(115200);
13
14 // Set up pins
15 pinMode (PIN_TRIGGER, OUTPUT);
16 pinMode (PIN_ECHO, INPUT);
17 digitalWrite(PIN_TRIGGER, LOW);
18 }
19
20 // Main loop
21 void loop() {
22 // Pulse trigger pin
23 digitalWrite(PIN_TRIGGER, HIGH);
Miscellaneous Sensors 403

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

1 # HC-SR04 Ultrasonic Sensor Example


2
3 import utime
4 import rp2
5 from rp2 import PIO, asm_pio
6 from machine import Pin
7
8 # PIO Program
9 @asm_pio(set_init=rp2.PIO.OUT_LOW,autopush=False)
10 def ULTRA_PIO():
11 # wait for a request and save timeout
12 pull()
13 mov (x,osr)
14
15 # send a 10 us (20 cycles) pulse
Miscellaneous Sensors 404

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

Rotary Encoder (adapted from Wikipedia, original by Matt Hercules)

The order of the changes indicates the direction of the movement; by measuring the time between
changes we can measure the rotation speed.

Signals rotating clockwise (left) and anti-clockwise (right)

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

• Continuous Monitoring: we do a tight loop detecting changes in the signals. We need to


implement some kind of debouncing by software or hardware. It is hard to do other things
at the same time when we use this approach (unless you can use a core or the PIO for this).
• Interrupts: we set the microcontroller to interrupt when either signal changes. This makes
it easy to do other things while reading the encoder, but debouncing may be tricky.
• Periodic Monitoring: we set a periodic interrupt to sample the signals. The interval
between samples can be used for debouncing. We have to choose an interval that allows us
to detect all changes when the shaft is turned quickly and implement a robust debouncing.

Rotary Encoder Example

Connecting a Rotary Encoder to the Pi Pico

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.

C/C++ SDK Code


Here we are using the PIO to monitor the outputs of the rotary encoder. When a state change is
detected, the old and the new states and the time from the previous are sent to the ARM processor.
The PIO program comes from https://github.com/pimoroni/pimoroni-pico.
Miscellaneous Sensors 409

C/C++ Rotary Encoder PIO Program

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

127 printf ("\r%3d", val);


128 }
129 }
130 }
131 }

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

1 // Rotary Encoder Example


2
3 #include "hardware/pio.h"
4 #include "encoder.pio.h"
5
6 #define ENCODER_A_PIN 16
7 #define ENCODER_B_PIN 17
8
9 static const uint32_t STATE_A_MASK = 0x80000000;
10 static const uint32_t STATE_B_MASK = 0x40000000;
11 static const uint32_t STATE_A_LAST_MASK = 0x20000000;
12 static const uint32_t STATE_B_LAST_MASK = 0x10000000;
13
14 static const uint32_t STATES_MASK = STATE_A_MASK | STATE_B_MASK |
15 STATE_A_LAST_MASK | STATE_B_LAST_MASK;
16
17 #define LAST_STATE(state) ((state) & 0b0011)
18 #define CURR_STATE(state) (((state) & 0b1100) >> 2)
19
20 enum MicroStep : uint8_t {
21 MICROSTEP_0 = 0b00,
22 MICROSTEP_1 = 0b10,
23 MICROSTEP_2 = 0b11,
24 MICROSTEP_3 = 0b01,
25 };
26
Miscellaneous Sensors 416

27 static uint8_t move_up = (MICROSTEP_2 << 6) | (MICROSTEP_3 << 4) |


28 (MICROSTEP_0 << 2) | MICROSTEP_1;
29 static uint8_t move_down = (MICROSTEP_1 << 6) | (MICROSTEP_0 << 4) |
30 (MICROSTEP_3 << 2) | MICROSTEP_2;
31 static uint8_t history = 0;
32
33 PIO enc_pio = pio0;
34 uint enc_sm;
35
36 int val;
37
38 // Initialization
39 void setup() {
40 // Init the serial
41 while (!Serial) {
42 delay(100);
43 }
44 Serial.begin(115200);
45
46 // Start the PIO program
47 encoder_program_init(ENCODER_A_PIN, ENCODER_B_PIN);
48
49 // Init value
50 val = 50;
51 Serial.println (val);
52 }
53
54 // Main loop
55 void loop() {
56
57 // Wait for a move
58 uint32_t received = pio_sm_get_blocking(enc_pio, enc_sm);
59
60 // Extract new state and push into the history
61 uint8_t states = (received & STATES_MASK) >> 28;
62 history = ((history & 0x3F) << 2) | CURR_STATE(states);
63
64 // Check for patterns of movement forward and backward
65 if (history == move_up) {
Miscellaneous Sensors 417

66 if (val < 100) {


67 val++;
68 Serial.println(val);
69 }
70 }
71 else if (history == move_down) {
72 if (val > 0) {
73 val--;
74 Serial.println(val);
75 }
76 }
77 }
78
79 // Set up the PIO program
80 void encoder_program_init(uint enc_pin_a, uint enc_pin_b) {
81 // Load program and select state machine
82 uint offset = pio_add_program(enc_pio, &encoder_program);
83 enc_sm = pio_claim_unused_sm(enc_pio, true);
84
85 // Setup pins
86 pio_gpio_init(enc_pio, enc_pin_a);
87 pio_gpio_init(enc_pio, enc_pin_b);
88 gpio_pull_up(enc_pin_a);
89 gpio_pull_up(enc_pin_b);
90 pio_sm_set_consecutive_pindirs(enc_pio, enc_sm, enc_pin_a, 1, false);
91 pio_sm_set_consecutive_pindirs(enc_pio, enc_sm, enc_pin_b, 1, false);
92
93 // Config state machine
94 pio_sm_config c = encoder_program_get_default_config(offset);
95 sm_config_set_jmp_pin(&c, enc_pin_a);
96 sm_config_set_in_pins(&c, enc_pin_b);
97 sm_config_set_in_shift(&c, false, false, 1);
98 sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX);
99 sm_config_set_clkdiv_int_frac(&c, 250, 0);
100 pio_sm_init(enc_pio, enc_sm, offset, &c);
101
102 // Init the state
103 bool enc_state_a = gpio_get(enc_pin_a);
104 bool enc_state_b = gpio_get(enc_pin_b);
Miscellaneous Sensors 418

105 pio_sm_exec(enc_pio, enc_sm, pio_encode_set(pio_x,


106 (uint)enc_state_a << 1 | (uint)enc_state_b));
107
108 // Start execution
109 pio_sm_set_enabled(enc_pio, enc_sm, true);
110 }

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

1 # Rotary Encoder Example


2
3 import rp2
4 from rp2 import PIO, asm_pio
5 from machine import Pin
6
7 # PIO Program
8 @asm_pio(fifo_join=PIO.JOIN_RX)
9 def ENCODER_PIO():
10 wrap_target()
11 label('loop')
12 # Copy the state-change timer from OSR,
13 # decrement it, and save it back
14 mov (y,osr)
15 jmp(y_dec,'osr_dec')
16
17 label('osr_dec')
18 mov (osr, y)
19
20 # Read the state of both encoder pins and check
21 # if they are different from the last state
22 jmp (pin, 'enc_a_was_high')
Miscellaneous Sensors 419

23 mov (isr, null)


24 jmp ('read_enc_b')
25
26 label('enc_a_was_high')
27 set (y, 1)
28 mov (isr, y)
29
30 label('read_enc_b')
31 in_(pins, 1)
32 mov (y, isr)
33 jmp (x_not_y, 'state_changed') [1]
34 wrap ()
35
36 label('state_changed')
37 # Put the last state and the timer value into
38 # ISR alongside the current state, and push that
39 # state to the system. Then override the last
40 # state with the current state
41 in_ (x, 2)
42 mov (x, invert(osr))
43 in_ (x, 28)
44 push (noblock)
45 mov (x, y)
46
47 # Perform a delay to debounce switch inputs
48 set (y, 29) [19]
49 label('debounce_loop')
50 jmp (y_dec, 'debounce_loop') [15]
51
52 # Initialise the timer
53 mov (y, invert(null))
54 jmp (y_dec, 'y_dec')
55 label('y_dec')
56 mov (osr, y)
57 jmp ('loop') [1]
58
59 # Set up pins
60 pin_A = Pin(16, Pin.IN, Pin.PULL_UP)
61 pin_B = Pin(17, Pin.IN, Pin.PULL_UP)
Miscellaneous Sensors 420

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

CircuitPython Rotary Encoder Example

1 # Rotary Encoder Example


2 import rotaryio
3 import board
4
5 encoder = rotaryio.IncrementalEncoder(board.GP16, board.GP17)
6 last_position = encoder.position
7 val = 50
8 print(val)
9 while True:
10 position = encoder.position
11 if position != last_position:
12 while position > last_position:
13 last_position = last_position+1
14 if val < 100:
15 val = val+1
16 print(val)
17 while position < last_position:
18 last_position = last_position-1
19 if val > 0:
20 val = val-1
21 print(val)

Load Cell (Strain Gauge)


A Load Cell is a sensor that outputs an electrical signal when a force is applied to it. We are going
to use here a specific type of load cell, the Strain Gauge. When a force is applied to a strain gauge,
it deforms and changes its resistance.
Miscellaneous Sensors 422

Load Cell rated for 50kg

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.

Wheatstone Bridge Configuration

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 ADC


Even with a Wheatstone bridge, the changes in the voltages are too small to be read directly with
the Pico ADC. A typical solution is to use a module with a HX711 chip. The HX711 is a 24-bit
ADC designed for use with load cells in a Wheatstone bridge configuration.

HX711 typical circuit

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).

HX711 timing (from datasheet)

Load Cell Example


In this example, we are going to construct a simple scale. Place a flat surface on top of the four
sensors so that the weight placed on it is distributed evenly between them.
A two-step calibration is done at the start:

• 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.

A scale build with a load cell and a Pi Pico

Note: the I2C display must use a PCF8574 chip.

C/C++ SDK Code


The PIO is used to interface with the HX711. I started with the PIO code from the library used in
the CircuitPython example, but could not resist changing it a little.
PIO code to interface with the HX711

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

48 // Set the pins GPIO function (connect PIO to the pad),


49 pio_gpio_init(pio, clockPin);
50 pio_gpio_init(pio, dataPin);
51
52 // Configure the FIFOs
53 sm_config_set_in_shift (&c, false, true, 24);
54 sm_config_set_fifo_join (&c, PIO_FIFO_JOIN_RX);
55
56 // Configure the clock for 5 MHz
57 float div = clock_get_hz(clk_sys) / 5000000;
58 sm_config_set_clkdiv(&c, div);
59
60 // Load our configuration, and jump to the start of the program
61 pio_sm_init(pio, sm, offset, &c);
62
63 // Set the state machine running
64 pio_sm_set_enabled(pio, sm, true);
65 }
66 %}

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

55 lcd_init (i2c0, PIN_SDA, PIN_SCL, 0x27);


56 lcd_backlight(true);
57
58 // Init button
59 gpio_init(PIN_BUTTON);
60 gpio_set_pulls(PIN_BUTTON, true, false);
61
62 // Calibatre the sensor
63 calibrate();
64 lcd_write(0, 0, "Scale Ready");
65
66 // Main Loop
67 while (true) {
68 char buf[20];
69 float weight = (hx711_avg(10) - tare)/scale;
70 sprintf(buf,"%7.3f", weight);
71 strcat(buf,"kg");
72 lcd_write(1, 0, buf);
73 sleep_ms(500);
74 }
75 }
76
77 // Get an average reading
78 float hx711_avg(int count) {
79 int32_t sum = 0;
80 pio_sm_clear_fifos (pio, sm);
81 for (int i = 0; i < count; i++) {
82 int32_t val = pio_sm_get_blocking(pio, sm);
83 if (val & 0x800000) {
84 val = val - 0x1000000;
85 }
86 sum += val;
87 }
88 return ((float)sum)/count;
89 }
90
91 // Initial calibration
92 void calibrate() {
93 keyPress("Empty scale");
Miscellaneous Sensors 430

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

Installing the HX711 Library

We also need a library for the LCD:

Installing the LCD Library

With these two libraries, we do not need to write much code:


Miscellaneous Sensors 432

Arduino Load Cell Example

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:

pip3 install circup


circup install hx711

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

The documentation for the HX711 library is at https://circuitpython-hx711.readthedocs.io/en/latest/api.html.


I wrote a small driver for the I2C display, you can download it from the book GitHub
(https://github.com/dquadros/PicoSensors).
CircuitPython Load Cell Example

1 # Load cell Example


2
3 import board
4 import digitalio
5 from busio import I2C
6 from i2c_lcd import lcd_pcf8574
7 from hx711.hx711_pio import HX711_PIO
8 from time import sleep
9
10 # Init button
11 button = digitalio.DigitalInOut(board.GP15)
12 button.pull = digitalio.Pull.UP
13
14 # Init display
15 i2c = I2C(sda=board.GP16, scl=board.GP17)
16 lcd = lcd_pcf8574(i2c)
17 lcd.init()
18 lcd.backlightOn()
19
20 # Wait for a key press
21 def keyPress(msg):
22 lcd.displayClear()
23 lcd.displayWrite(0,0,msg)
24 lcd.displayWrite(1,0,'Press button')
25 while button.value:
26 sleep(0.1)
27 sleep(0.1) # debounce
28 lcd.displayWrite(1,0,'Release button')
29 while not button.value:
30 sleep(0.1)
31 sleep(0.1) # debounce
32 lcd.displayWrite(1,0,'Wait ')
33
34 # Initial calibration
Miscellaneous Sensors 438

35 sensor = HX711_PIO(board.GP12, board.GP13)


36 keyPress('Empty scale')
37 sensor.tare()
38 keyPress('Put 1kg')
39 sensor.determine_scalar(1.0)
40 if sensor.read() < 0:
41 sensor.scalar = -sensor.scalar
42
43 # Main loop
44 lcd.displayClear()
45 lcd.displayWrite(0,0,'Scale Ready')
46 while True:
47 weight = sensor.read()
48 lcd.displayWrite(1,0,'{:7.3f}kg'.format(weight))
49 sleep(0.5)

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.

iButtons and reader

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).

Connecting an iButton to the Pi Pico

You will need to connect the Pico to a PC to see the program output.

C/C++ SDK Code


I used the “Raspberry Pi Pico One Wire Library” by Adam Boardman. You need to download the
library from https://github.com/adamboardman/pico-onewire and place it in your project under
Miscellaneous Sensors 440

a subdirectory called pico-onewire.


This library will print warnings if there is no device on the 1-Wire bus, you may want to edit the
file one_wire.cpp to comment out all the printf calls.
C/C++ iButton Example

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

Installing OneWireNg Library

Arduino iButton Example

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

MicroPython iButton Example


1 # iButton Example
2
3 from machine import Pin
4 import onewire
5 from time import sleep
6
7 # Init OnewWire bus
8 ow = onewire.OneWire(Pin(16))
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] != last) and \
18 (ow.crc8(sensors[0]) == 0) and (sensors[0][0] == 1):
19 # Extract and print ID
20 last = sensors[0]
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)

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.

The documentation for the adafruit_onewire library is at


https://docs.circuitpython.org/projects/onewire/en/latest/api.html.
CircuitPython iButton Example

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:

• enrollment: a digital is captured, converted into a template, and stored in memory.


• 1:1 match: a digital is captured, converted into a template, and compared to one specific
template in memory,
• 1:n match: a digital is captured, converted into a template and the memory is searched for
a matching template.

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

FPM10A Sensor Protocol


Many fingerprint sensor models support similar commands, but not always the same. I am using
as an example the FPM10A sensor.
The connection of the sensor to other devices is done by asynchronous serial communication.
The sensor I used works with power and signals at 3.3V. The default communication speed is
57600 bps, it can be changed to other multiples of 9600 bps. The data format is 8N1 (eight data
bits, no parity, one stop bit). After powered on, the sensor needs 0.5 seconds to initialize and only
communicates after this.
Data is organized in packets with the following format:

DPM10A Sensor Data Packets

The details for each command, and its responses, are in the datasheet, here is a summary extracted
from it:
Fingerprint Sensors 449

FPM10A Sensor Commands

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:

Offset Size Content


0 2 bytes Status Register
2 2 bytes Reserved
4 2 bytes Maximum number of fingerprints that can be stored
6 2 bytes Security level (1 to 5)
8 4 bytes Sensor’s address
12 2 bytes Size of data packet (0 = 32, 1 = 64, 2 = 128, 3 = 256)
14 2 bytes Communication speed (1 a 12), in units of 9600 bps

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

Parameter Number Values


Baudrate 4 1 to 12
Security level 5 1 to 5
Data packet size 6 0 to 3

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).

It is important to detect and treat errors in these steps:

• 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).

Transferring the Image and the Template


The commands described in this section move a larger quantity of data between the sensor and
the microcontroller.
Three types of packets are used in these transfers: Response (aka Acknowledge), Data, and
End of Data. After receiving a command that requires data transfer, the sensor will first send a
response packet, confirming that the command was accepted. Next, we will have multiple data
packets, ended by an end of data packet. The amount of bytes sent in each packet is configurable
(in the System Parameters), the default is 128 bytes. One caveat is that there is no total size
information in the packets, you must assume the sizes listed in the datasheet.
Fingerprint Sensors 452

FPM10A Sensor Data Transfers

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.

Fingerprint Sensor Example


I am going to show here a simple example, featuring only the enrollment and identification of
fingerprints. The figure below shows the connection of the sensor to the Raspberry Pi Pico, check
the connections in your sensor’s documentation. Along with the sensor, I connected an RGB
LED (common cathode) to show what the software is doing. You can change it for three separate
LEDs (or just not mount them and follow the operation from the messages sent to the PC).
Fingerprint Sensors 453

Connecting a Fingerprint Sensor to the Pi Pico

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.

C/C++ SDK Code


The interaction with the sensor is encapsulated in a class. The file fpm10a_sdk.h declares
this class and the constants and structures needed. The following snippet shows the definition
of the command codes, the packet header structure, and the structure for commands without
parameters:
Fingerprint Sensors 454

Definitions for Comunication with the Fingerprint Sensor

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

Sending a Command to the Fingerprint Sensor

1 // Send command to sensor


2 void FPM10A::sendCmd (uint8_t *cmd, int size) {
3 // fill header and send
4 header.startHi = START >> 8;
5 header.startLo = START & 0xFF;
6 header.addr3 = 0xFF; // default address
7 header.addr2 = 0xFF;
8 header.addr1 = 0xFF;
9 header.addr0 = 0xFF;
10 header.type = 0x01; // command
11 header.lenHi = (size+2) >> 8;
12 header.lenLo = (size+2) & 0xFF;
13 uart_write_blocking (uart, (const uint8_t *) &header, sizeof(header));
14
15 // compute checksum
16 uint16_t chksum = header.type + header.lenHi + header.lenLo;
17 for (int i = 0; i < size; i++) {
18 chksum += cmd[i];
19 }
20
21 // send command
22 uart_write_blocking (uart, (const uint8_t *) cmd, size);
23
24 // send checksum
25 uart_putc_raw (uart, chksum >> 8);
26 uart_putc_raw (uart, chksum & 0xFF);
27 }

The main program uses this class to implement our application:


Fingerprint Sensors 456

Fingerprint Sensor Example with the C/C++ SDK

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

Installing Adafruit’s Fingerprint Library

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

1 // Fingerprint Sensor Example


2
3 #include <Adafruit_Fingerprint.h>
4
5 #define mySerial Serial1
6 Adafruit_Fingerprint finger = Adafruit_Fingerprint(&mySerial);
7
8 // RGB LED connections
9 #define LED_R_PIN 13
10 #define LED_G_PIN 14
11 #define LED_B_PIN 15
12
13 // Initialization
14 void setup() {
15 // Start communication with the PC
16 while (!Serial)
17 ;
18 Serial.begin(115200);
Fingerprint Sensors 461

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

1 # Fingerprint Sensor Example


2 from machine import UART, Pin
3 from time import sleep
4 from fingersensor_mpython import FingerSensor
5
6 # Init
7 ledR = Pin(13, Pin.OUT)
8 ledG = Pin(14, Pin.OUT)
9 ledB = Pin(15, Pin.OUT)
10 ledR.off()
11 ledG.off()
12 ledB.off()
13 uart = UART(0, tx=Pin(16), rx=Pin(17))
14 finger = FingerSensor(uart)
15 sleep(0.5)
16 c = finger.count()
17 if c > 0:
18 print ('Erasing {} fingerprints'.format(c))
19 if finger.clear():
20 print ('Success')
21
22 # Read fingerprint and create template
23 def captureFeature(numbuf):
24 while True:
25 ledB.on()
26 print ('Place finger on sensor')
27 while not finger.capture():
28 sleep(0.05)
Fingerprint Sensors 467

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.

Here is the full main program:


Fingerprint Sensor Example with CircuitPython

1 # Fingerprint Sensor Example


2 import board
3 import digitalio
4 from busio import UART
5 from time import sleep
6 from fingersensor_cpython import FingerSensor
7
8 # Init
9 ledR = digitalio.DigitalInOut(board.GP13)
10 ledG = digitalio.DigitalInOut(board.GP14)
11 ledB = digitalio.DigitalInOut(board.GP15)
Fingerprint Sensors 469

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

RFID 125 kHz Tags

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.

RDM6300 Reader for RFID 125 kHz

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.

• The first part is the full ID in decimal.


• The second part corresponds to the three least significant bytes of the ID. The first number
is the third byte in decimal (0 to 255) and the second number is the other two bytes, treated
as a 16-bit number, also in decimal (0 to 65535).

RFID 125kHz Example


This example illustrates the use of the reader and tags for access control. We are connecting the
reader, a button, an active buzzer, and a servomotor to the Raspberry Pi Pico:

Connecting the RDM6300 RFID Reader to the Pi Pico


RFID 474

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.

You can see informative messages by connecting the Pico to a PC.

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

RFID 125kHz MicroPython Example

1 # 125kHz RFID Reader Example


2 from machine import Pin,PWM,UART
3 from time import sleep, ticks_ms, ticks_add, ticks_diff
4
5 def time_ms():
6 return ticks_ms()
7
8 # Class to control a buzzer
9 class Buzzer:
10 def __init__(self, pin):
11 self.pinBuzzer = Pin(pin, Pin.OUT)
12 self.pinBuzzer.off()
13
14 def beep(self):
15 self.pinBuzzer.on()
16 sleep(.3)
17 self.pinBuzzer.off()
18
19 # Class to check a button
20 class Button:
21 def __init__(self, pin, debounce=20):
22 self.pinButton = Pin(pin, Pin.IN, Pin.PULL_UP)
23 self.pressed = False
24 self.debounce = debounce
25 self.last = self.pinButton.value()
26 self.lastTime = time_ms()
27
28 def released(self):
29 val = self.pinButton.value()
30 if val != self.last:
31 # reading changed
32 self.last = val
33 self.lastTime = time_ms()
34 elif (val == 0) != self.pressed:
35 dif = ticks_diff(time_ms(),self.lastTime)
36 if dif > self.debounce:
37 # update button state
38 self.pressed = val == 0
RFID 476

39 return not self.pressed


40 return False
41
42 # Class to control a servomotor
43 class Servo:
44 def __init__(self, pin, time0deg=0.6, time180deg=2.4):
45 self.pwmServo = PWM(Pin(pin, Pin.OUT))
46 self.pwmServo.freq(50)
47 self.pwmServo.duty_u16(0)
48 self.time0deg = time0deg
49 self.time180deg = time180deg
50
51 def pos(self, angle):
52 ontime = self.time0deg+(self.time180deg-self.time0deg)*angle/180
53 val = ontime * 65535/20
54 self.pwmServo.duty_u16(int(val))
55 sleep(0.3)
56 self.pwmServo.duty_u16(0)
57
58 # Class to get RFID reader messages
59 class RFID:
60 def __init__(self, id, pin):
61 self.uart = UART(id, tx=None, rx=Pin(pin), baudrate=9600, bits=8, parity=Non\
62 e, stop=1)
63 self.last = ''
64 self.last_read = 0
65 self.bufRx = bytearray(14)
66 self.pos = 0
67
68 def read(self):
69 if self.uart.any() == 0:
70 ellapsed = ticks_diff(time_ms(), self.last_read)
71 if (self.last != '') and (ellapsed > 1000):
72 # Long time without messages, forget last tag
73 self.last = ''
74 return None
75 rx = self.uart.read(1)
76 if rx == None:
77 return None
RFID 477

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

117 if (closeTime != None) and (ticks_diff(time_ms(), closeTime) > 0):


118 servo.pos(0)
119 closeTime = None
120 if button.released():
121 # pressed and released button
122 cadastro = True
123 tag = rfid.read()
124 if tag != None:
125 if cadastro:
126 if tag in autorized:
127 print ('Tag {} already authorized'.format(tag))
128 else:
129 led.on()
130 autorized.add(tag)
131 print ('Tag {} authorized'.format(tag))
132 sleep(0.3)
133 led.off()
134 cadastro = False
135 else:
136 if tag in autorized:
137 print ('Tag {} authorized'.format(tag))
138 servo.pos(180) # "open door"
139 closeTime = ticks_add(time_ms(), 3000)
140 else:
141 print ('Tag {} NOT authorized'.format(tag))
142 buzzer.beep()

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:

• In the first position we only accept 0x02.


• The next thirteen characters are stored in the buffer.
• When we receive the fourteenth character we do a quick check: the last character must be
0x03 and the CRC must be correct.
RFID 479

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.

Code for SDK C/C++


As this code is longer, I am listing here only the interesting parts. The full code can be seen and
downloaded from https://github.com/dquadros/PicoSensors.
The observations on the MicroPython version also apply to the SDK version.
Here is the class for the RFID reader:
RFID 125kHz Example for the C/C++ SDK (extract)

1 // Class to get RFID reader messages


2 class RFID {
3 private:
4 uart_inst_t *uart;
5 uint8_t last[9];
6 uint32_t last_read;
7 uint8_t bufRx[14];
8 int pos;
9
10 inline uint8_t decodHex(uint8_t c) {
11 if ((c >= '0') && (c <= '9')) {
12 return c - '0';
13 }
14 if ((c >= 'A') && (c <= 'F')) {
15 return c - 'A' + 10;
16 }
17 return 0;
18 }
19
20 public:
21 RFID(uart_inst_t *uartid, int pinRx) {
22 uart = uartid;
23 gpio_set_function(pinRx, GPIO_FUNC_UART);
RFID 480

24 uart_init (uart, 9600);


25 uart_set_format (uart, 8, 1, UART_PARITY_NONE);
26 uart_set_fifo_enabled (uart, true);
27 pos = 0;
28 last[0] = 0;
29 last_read = 0;
30 }
31
32 bool read(char *tag) {
33 if (uart_is_readable(uart) == 0) {
34 uint32_t ellapsed = board_millis() - last_read;
35 if ((last[0] != 0) && (ellapsed > 1000)) {
36 // Long time without messages, forget last tag
37 last[0] = 0;
38 }
39 return false;
40 }
41 uint8_t c = uart_getc(uart);
42 if ((pos == 0) && (c != 0x02)) {
43 return false;
44 }
45 bufRx[pos++] = c;
46 if (pos == 14) {
47 pos = 0;
48 if (c == 0x03) {
49 last_read = board_millis();
50 uint8_t crc = 0;
51 uint8_t x;
52 for (int i = 1; i < 13; i = i+2) {
53 x = (decodHex(bufRx[i]) << 4) + decodHex(bufRx[i+1]);
54 crc ^= x;
55 }
56 if (crc == 0) {
57 if (memcmp (bufRx+3, last, 8) != 0) {
58 memcpy(last, bufRx+3, 8);
59 last[8] = 0;
60 strcpy(tag, (const char *)last);
61 return true;
62 }
RFID 481

63 }
64 }
65 }
66 return false;
67 }
68
69 };

Here is the main program:


RFID 125kHz Example for the C/C++ SDK (extract)

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

Code for the Arduino Environment


In the Arduino environment, we can use the Servo library to control the servomotor. The IDE
will warn that there are two versions of this library available, one the standard Arduino library
and the other part of the Raspberry Pi Pico support. The IDE will use the latter.
RFID 125kHz Example for the Arduino Environment
1 // 125kHz RFID Example
2 #include <Servo.h>
3
4 // Class to control a buzzer
5 class Buzzer {
6 private:
7 int pinBuzzer;
8 public:
9 Buzzer(int pin) {
10 pinBuzzer = pin;
11 pinMode (pinBuzzer, OUTPUT);
12 digitalWrite(pinBuzzer, LOW);
13 }
14
15 void beep() {
16 digitalWrite(pinBuzzer, HIGH);
17 delay(300);
18 digitalWrite(pinBuzzer, LOW);
19 }
20 };
21
22 // Class to check a button
23 class Button {
24 private:
25 int pinButton;
26 bool pressed;
27 int debounce;
28 bool last;
29 uint32_t lastTime;
30 public:
31 Button(int pin, int debounce = 20) {
32 pinButton = pin;
33 pinMode (pinButton, INPUT_PULLUP);
RFID 484

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

190 Serial.print ("Tag ");


191 Serial.print (tag);
192 Serial.println (" authorized");
193 servo.write(180); // "open door"
194 closeTime = millis()+3000;
195 } else {
196 Serial.print ("Tag ");
197 Serial.print (tag);
198 Serial.println (" NOT authorized");
199 buzzer.beep();
200 }
201 }
202 }
203
204 // Check if tag is in authorized list
205 bool isAuthorized(char * tag) {
206 for (int i = 0; i < nAuthorized; i++) {
207 if (strcmp(autorized[i], tag) == 0) {
208 return true;
209 }
210 }
211 return false;
212 }

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.

Code for CircuitPython


The CircuitPython code is a trivial adaptation of the MicroPython code.
RFID 489

RFID 125kHz Example for CircuitPython

1 # 125kHz RFID Reader Example


2 import board
3 import digitalio
4 import pwmio
5 from busio import UART
6 from time import sleep, monotonic_ns
7
8 def time_ms():
9 return monotonic_ns() // 1000000
10
11 # Class to control a buzzer
12 class Buzzer:
13 def __init__(self, pin):
14 self.pinBuzzer = digitalio.DigitalInOut(pin)
15 self.pinBuzzer.direction = digitalio.Direction.OUTPUT
16 self.pinBuzzer.value = False
17
18 def beep(self):
19 self.pinBuzzer.value = True
20 sleep(.3)
21 self.pinBuzzer.value = False
22
23 # Class to check a button
24 class Button:
25 def __init__(self, pin, debounce=20):
26 self.pinButton = digitalio.DigitalInOut(pin)
27 self.pinButton.pull = digitalio.Pull.UP
28 self.pressed = False
29 self.debounce = debounce
30 self.last = self.pinButton.value
31 self.lastTime = time_ms()
32
33 def released(self):
34 val = self.pinButton.value
35 if val != self.last:
36 # reading changed
37 self.last = val
38 self.lastTime = time_ms()
RFID 490

39 elif (val == 0) != self.pressed:


40 dif = time_ms() - self.lastTime
41 if dif > self.debounce:
42 # update button state
43 self.pressed = val == 0
44 return not self.pressed
45 return False
46
47 # Class to control a servomotor
48 class Servo:
49 def __init__(self, pin, time0deg=0.6, time180deg=2.4):
50 self.pwmServo = pwmio.PWMOut(pin, duty_cycle=0, frequency=50)
51 self.time0deg = time0deg
52 self.time180deg = time180deg
53
54 def pos(self, angle):
55 ontime = self.time0deg+(self.time180deg-self.time0deg)*angle/180
56 val = ontime * 65535/20
57 self.pwmServo.duty_cycle = int(val)
58 sleep(0.3)
59 self.pwmServo.duty_cycle = 0
60
61 # Class to get RFID reader messages
62 class RFID:
63 def __init__(self, pin):
64 self.uart = UART(tx=None, rx=pin, baudrate=9600, bits=8, parity=None, stop=1)
65 self.last = ''
66 self.last_read = 0
67 self.bufRx = bytearray(14)
68 self.pos = 0
69
70 def read(self):
71 if self.uart.in_waiting == 0:
72 if (self.last != '') and ((time_ms()- self.last_read) > 1000):
73 # Long time without messages, forget last tag
74 self.last = ''
75 return None
76 rx = self.uart.read(1)
77 if rx == None:
RFID 491

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

117 closeTime = None


118 while True:
119 if (closeTime != None) and (time_ms() > closeTime):
120 servo.pos(0)
121 closeTime = None
122 if button.released():
123 # pressed and released button
124 enroll = True
125 tag = rfid.read()
126 if tag != None:
127 if enroll:
128 if tag in autorized:
129 print ('Tag {} already authorized'.format(tag))
130 else:
131 led.value = True
132 autorized.add(tag)
133 print ('Tag {} authorized'.format(tag))
134 sleep(0.3)
135 led.value = False
136 enroll = False
137 else:
138 if tag in autorized:
139 print ('Tag {} authorized'.format(tag))
140 servo.pos(180) # "open door"
141 closeTime = time_ms()+3000
142 else:
143 print ('Tag {} NOT authorized'.format(tag))
144 buzzer.beep()

MIFARE and NFC


MIFARE is a trademark of Philips (now NXP) for a series of integrated circuits used in RFID
cards and tags that work in the 13.56MHz band. The name is also used to refer to proprietary
technologies based on the ISO/IEC 14443 Type A standard.
MIFARE cards and tags have, in addition to a unique and unalterable identification programmed
in the factory, a non-volatile memory that can be read and written. One of the objectives of
MIFARE is to enable applications where a card’s contents cannot be duplicated or changed. An
example is electronic ticketing for public transportation (hence the name).
RFID 493

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.

Memory Map, MIFARE Classic 1k


RFID 494

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.

The Configuration Block


The configuration block has the following format:

• Six bytes with the “A” key


• Three bytes with the access configuration
• One unused byte
• Six bytes with the “B” key (or free, if the configuration uses only the A key)

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.

Access Configuration Storage

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

Access Configuration for the Configuration Block

Access Configuration for the Data Blocks

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:

Value Storage 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:

Mode SW1 SW2


UART OFF OFF
I²C ON OFF
SPI OFF ON

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

Reading and Writing Data in MicroPython with the MFRC522


Reader
The objective of this example is to show the reading and writing of data in a card sector.
We are going to use the mfrc552 library that you should download from
https://github.com/danjperron/micropython-mfrc522:

Downloading the MFRC522 Library for MicroPython

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

Connecting the MFRC522 Reader to the Pi Pico

You will need to connect the Raspberry Pi Pico to a PC to interact with the program. The program
accepts two commands:

• W sector message: Writes message (up to 16 characters) in sector (1 to 9).


• R sector: If sector (1 to 9) contains a message written by the program, show it. Otherwise
shows an error message.

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

1 # Mifare RFID Example with MFRC522


2
3 from machine import Pin
4 from mfrc522 import MFRC522
5 import time
6 import utime
7 import re
8
RFID 501

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

48 stat = card.writeSectorBlock(uid, sector, 1, msg, cardKey)


49 if stat != card.OK:
50 print ('Write error!')
51 else:
52 print ('Written.')
53 card.stop_crypto1()
54
55 # Le do cartao
56 def readFromCard(param):
57 uid = waitForCard()
58 sector = int(param[1])
59 (stat, blockMark) = card.readSectorBlock(uid, sector, 0, cardKey)
60 if stat != card.OK:
61 print ('Read error!')
62 elif blockMark != mark:
63 print ('No message on sector!')
64 else:
65 (stat, blockMsg) = card.readSectorBlock(uid, sector, 1, cardKey)
66 if (stat != card.OK):
67 print ('Read error!')
68 else:
69 print ('Msg: '+bytes(blockMsg).decode('utf-8'))
70 card.stop_crypto1()
71
72 # Main Loop
73 cmdRead = "^([rR]) ([1-9])$"
74 cmdWrite = "^([wW]) ([1-9]) (.+)$"
75 while True:
76 # Read command
77 print()
78 cmd = input("Command? ")
79 m = re.search(cmdRead, cmd)
80 if m != None:
81 readFromCard(m.groups())
82 else:
83 m = re.search(cmdWrite, cmd)
84 if m != None:
85 writeToCard(m.groups())
86 else:
RFID 503

87 print ('Unknown command')

The ideas in this program can be extended for the storage and retrieval of more complex
information.

Ticketing with MFRC522 Reader in the Arduino Environment


This is a more sophisticated example, as we are going to change the configuration of a sector to
use it in the “value” mode, with our keys. The application simulates a prepaid card, with some
“credits” written when the card is initialized; these “credits” can then be used in debit transactions.

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:

Installing the MFRC522 Library in the Arduino 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

if (backData && (backLen > 0)) {

to

if (backData && (backLen != nullptr)) {

With these changes, the example should compile fine.


The assembly for this example is the same as in the MicroPython example.
You will need to connect the Raspberry Pi Pico to a PC to interact with the program.
MFRC522 Reader Example in the Arduino Environment

1 // MIFARE MFRC522 RFID reader Example


2
3 #include <SPI.h>
4 #include <MFRC522.h>
5
6 // RFID reader control
7 const byte RST_PIN = 20; // reset pin
8 const byte SS_PIN = 17; // select pin
9 MFRC522 mfrc522(SS_PIN, RST_PIN);
10
11 // Card access keys
12 typedef enum { FACTORY, OUR_APP, OTHER } KEY_TYPE;
13 MFRC522::MIFARE_Key defaultKey;
14 MFRC522::MIFARE_Key appKeyA;
15 MFRC522::MIFARE_Key appKeyB;
16 KEY_TYPE cardKey; // key type of current card
17
18 // Selects the sector and block to be used to store balance
19 const byte SECTOR = 2;
20 const byte VALUE_BLK = 8;
21 const byte CFG_BLK = 11;
22
23 // Initial and debit values
24 const int32_t INITIAL_VALUE = 30;
25 const int32_t DEBIT_VALUE = 7;
26
27 // Init
RFID 505

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

106 memset (cfgBuffer, 0, 16);


107 memcpy (cfgBuffer, &appKeyA, 6);
108 memcpy (cfgBuffer+10, &appKeyB, 6);
109 mfrc522.MIFARE_SetAccessBits(&cfgBuffer[6], 0, 6, 0, 3);
110 status = mfrc522.MIFARE_Write(CFG_BLK, cfgBuffer, 16);
111 if (status != MFRC522::STATUS_OK) {
112 Serial.println ("Error while configuring card!");
113 }
114
115 Serial.println ("Card initialized");
116 cardKey = OUR_APP;
117
118 // Authenticate with new B key
119 status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_B, CFG_BLK,
120 &appKeyB, &(mfrc522.uid));
121 if (status != MFRC522::STATUS_OK) {
122 Serial.println ("Authentication error!");
123 return;
124 }
125
126 status = mfrc522.MIFARE_Increment(VALUE_BLK, INITIAL_VALUE);
127 if (status != MFRC522::STATUS_OK) {
128 Serial.println ("Error loading initial value!");
129 return;
130 }
131 status = mfrc522.MIFARE_Transfer(VALUE_BLK);
132 if (status != MFRC522::STATUS_OK) {
133 Serial.println ("Error saving new balance!");
134 return;
135 }
136
137 // Shows initial balance
138 showBalance();
139 }
140
141 // Use DEBIT_VALUE "credits"
142 void useCard() {
143 MFRC522::StatusCode status;
144
RFID 508

145 if (cardKey != OUR_APP) {


146 Serial.println ("Card not initialized!");
147 return;
148 }
149
150 // Authenticates with A key
151 status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, CFG_BLK,
152 &appKeyA, &(mfrc522.uid));
153 if (status != MFRC522::STATUS_OK) {
154 Serial.println ("Authentication error!");
155 return;
156 }
157
158 // Check if enough balance
159 int32_t value;
160 status = mfrc522.MIFARE_GetValue(VALUE_BLK, &value);
161 if (status != MFRC522::STATUS_OK) {
162 Serial.println ("Read error!");
163 return;
164 }
165 if (value < DEBIT_VALUE) {
166 Serial.println ("Insuficient balance!");
167 return;
168 }
169
170 // Update balance
171 status = mfrc522.MIFARE_Decrement(VALUE_BLK, DEBIT_VALUE);
172 if (status != MFRC522::STATUS_OK) {
173 Serial.println ("Debit error!");
174 return;
175 }
176 status = mfrc522.MIFARE_Transfer(VALUE_BLK);
177 if (status != MFRC522::STATUS_OK) {
178 Serial.println ("Error while saving new balance!");
179 return;
180 }
181
182 // Shows new balance
183 showBalance();
RFID 509

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

262 MFRC522::PICC_Type tipo = mfrc522.PICC_GetType(mfrc522.uid.sak);


263 Serial.println(mfrc522.PICC_GetTypeName(tipo));
264
265 // Check type
266 if ( (tipo == MFRC522::PICC_TYPE_MIFARE_MINI) ||
267 (tipo == MFRC522::PICC_TYPE_MIFARE_1K) ||
268 (tipo == MFRC522::PICC_TYPE_MIFARE_4K) ) {
269 cardKey = checkKey();
270 return cardKey != OTHER;
271 }
272 Serial.println ("Card not supported!");
273 return false;
274 }
275
276 // Checks what type of key is used by the card
277 KEY_TYPE checkKey() {
278 MFRC522::StatusCode status;
279
280 status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, CFG_BLK,
281 &defaultKey, &(mfrc522.uid));
282 if (status == MFRC522::STATUS_OK) {
283 Serial.println ("Accepts factory key");
284 return FACTORY;
285 }
286
287 // Reset communication to try another key
288 mfrc522.PICC_HaltA();
289 mfrc522.PCD_StopCrypto1();
290 mfrc522.PICC_IsNewCardPresent();
291 mfrc522.PICC_ReadCardSerial();
292
293 status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, CFG_BLK,
294 &appKeyA, &(mfrc522.uid));
295 if (status == MFRC522::STATUS_OK) {
296 Serial.println ("Accepts our key");
297 return OUR_APP;
298 }
299
300 Serial.println ("Unknown key!");
RFID 512

301 return OTHER;


302 }
303
304 // Set up a block for value operations
305 bool setupValue(byte block) {
306 MFRC522::StatusCode status;
307 byte valueBlock[] = {
308 0, 0, 0, 0,
309 255, 255, 255, 255,
310 0, 0, 0, 0,
311 block, (byte) ~block, block, (byte) ~block };
312 status = mfrc522.MIFARE_Write(block, valueBlock, 16);
313 if (status != MFRC522::STATUS_OK) {
314 Serial.print("Write error!");
315 return false;
316 }
317 return true;
318 }

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.

Reading and Writing Data in CircuitPython with the PN532


Reader
This example is similar to what we saw with MicroPython, but here we are using the PN532
reader and programming in CircuitPython.
We are going to use the PN532 library from Adafruit. The easiest way to install it is using the
circup utility:
RFID 513

pip3 install circup


circup install adafruit_pn532

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:

Connecting the PN532 Reader to the Pi Pico

Here is the full program:


RFID 514

PN532 Exemple with CircuitPython

1 # Mifare RFID Example with PN532


2
3 import board
4 import busio
5 import re
6 from adafruit_pn532.i2c import PN532_I2C
7 from adafruit_pn532.adafruit_pn532 import MIFARE_CMD_AUTH_A
8
9 # Init PN532
10 i2c = busio.I2C(sda=board.GP16,scl=board.GP17)
11 pn532 = PN532_I2C(i2c, debug=False)
12 ic, ver, rev, support = pn532.firmware_version
13 print('Found PN532 with firmware version: {0}.{1}'.format(ver, rev))
14 pn532.SAM_configuration()
15
16 # Card key and App mark
17 mark = bytes([ 0x00, 0x11, 0x22, 0x33, 0x44, 0x66, 0x66, 0x77,
18 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF])
19 cardKey = bytes([ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ]) # factory default
20
21
22 # Wait for a card in range
23 def waitForCard():
24 print ('Present a card')
25 while True:
26 uid = pn532.read_passive_target(timeout=0.5)
27 if uid is None:
28 continue
29 print("Card ID: {}".format(
30 hex(int.from_bytes(uid,"little")).upper()))
31 return uid
32
33 # Write on card
34 def writeToCard(param):
35 uid = waitForCard()
36 block = int(param[1])*4
37 msg = bytes((param[2]+16*' '),'utf-8')[0:16]
38 if pn532.mifare_classic_authenticate_block(uid, block, MIFARE_CMD_AUTH_A, cardKe\
RFID 515

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

78 print ('Unknown command')

Program operation is the same as the MicroPython example.


Conclusion
And we get to the end of our journey!
In this final chapter, I will review some important points I hope you learned (or remembered)
with your reading.

Choosing Sensors and Using Them Correctly


There is no ideal sensor! Each sensor has its set of characteristics and is the designer’s responsibil-
ity to find the best compromise between the advantages and disadvantages for each project and
write code in such a way as to best take advantage of the information provided by the sensor.
The Raspberry Pi Pico is very versatile in the ways it connect sensors, giving options that
reduce the processing by the main processors. If you are going to use a board with another
microcontroller, you may find that you have few interface options and this will impact your
sensor selection.
In addition to proper processing of the data, the physical placement of the sensor can have a big
impact on the measurements.

Using a New Sensor


This book is a far way from covering all existing kinds and models of sensors. If you need to use
a new sensor, the following steps will help you:

• 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

Writing Code and Using Libraries


In this book examples, I used several forms to interact with sensors.
An already available library can speed development, but you may not always find one that covers
your needs (including the question of code quality). An important point when using a third party
library is its license. Most libraries you will find on the Internet are open source and can be freely
used in personal projects with no commercial use. Sometimes a license imposes restrictions for
commercial uses, but that is not common. Pay attention to the conditions imposed by the license.
Some are very permissive, others require the mention of the original author and the license used,
but some may require that you share your source code (with the same license). If you are going
to use third-party libraries in a professional project, inform this, and the license used, to the
company you are working for.
If you are writing your own code, organizing it in classes can make it more readable and make it
easier to use and maintain. But it can also have a negative impact on performance and memory
used (this is more critical in boards less powerful than the Pi Pico). In this case, it may be more
adequate to use a more relaxed organization using plain routines.

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.

Adafruit Feather RP2040


Adafruit has a family of boards¹³ called Feather that, while using different microcontrollers, have
common characteristics:

• All have the same form factor (size, connectors)


• All have the same basic pinout (with a few restrictions for some microcontrollers)
• Operation is at 3.3V
• Support for battery operation (including charging)

The Adafruit Feather RP2040 board

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

The Adafruit Feather RP2040 pinout

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.

SeeedStudio XIAO RP2040


SeeedStudio also has family of boards¹⁴ that use different microcontrollers but with the same
form factor and pinout. The focus here is on compactness.
¹⁴https://www.seeedstudio.com/xiao-series-page
Appendix A - Other Boards Based on the RP2040 Microcontroller 521

The XIAO RP2040 board

The XIAO RP2040 has a 2MB Flash, reset button, RGB LED, and USB C connector.

The XIAO RP2040 pinout

This board is very compact and costs about the same as the Pico. The downside is that it has few
I/O pins.

Arduino Nano RP2040 Connect


The Arduino Nano RP2040 Connect uses the form factor and pinout of the Arduino Nano (but
works at 3.3V).
Appendix A - Other Boards Based on the RP2040 Microcontroller 522

The Arduino Nano RP2040 Connect board

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.

The Arduino Nano RP2040 Connect pinout

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 Raspberry Pi Pico W board

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.

Connecting an External LED to the Raspberry Pi Pico W

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.

Non-original DS18B20 Temperature Sensors


There are a lot of non-original DS18B20 sensors going around. A comprehensive listing of fake,
counterfeit, or clone DS18B20, with details on how to identify them and their differences, can be
found at https://github.com/cpetrich/counterfeit_DS18B20.
Appendix B - Non-Original Sensors 525

This is actualy a XSEC SE18B20

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.

Conterfeit MPU6050 Accelerometer


When I tried to use an MPU6050 library with a GY-521 module, I got a warning that the chip ID
was not correct. While the original MPU6050 reports a value of 0x68 in the “WHO AM I” register,
the chip on the module reported 0x72.

Board with a fake MPU6050

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.

You might also like