Getting and using input from sensors enables Arduino to respond to or report on the world around it. This is one of the most common tasks you will encounter. This chapter provides simple and practical recipes for how to use the most popular input devices and sensors. Wiring diagrams show how to connect and power the devices, and code examples demonstrate how to use data derived from the sensors.
Sensors respond to input from the physical world and convert this into an electrical signal that Arduino can read on an input pin. The nature of the electrical signal provided by a sensor depends on the kind of sensor and how much information it needs to transmit. Some sensors (such as photoresistors and Piezo knock sensors) are constructed from a substance that alters its electrical properties in response to physical change. Others are sophisticated electronic modules that use their own microcontroller to process information before passing a signal on for the Arduino.
Sensors use the following methods to provide information:
Some devices, such as the tilt sensor in Recipe 6.2 and the motion sensor in Recipe 6.4, simply switch a voltage on and off. These can be treated like the switch recipes shown in Chapter 5.
Other sensors provide an analog signal (a voltage that is proportional to what is being sensed, such as temperature or light level). The recipes for detecting light (Recipe 6.3), temperature (Recipe 6.9), and sound (Recipe 6.8) demonstrate how analog sensors can be used. All of them use the analogRead
command that is discussed in Chapter 5.
Distance sensors, such as the PING))) in Recipe 6.5, provide data using pulse duration proportional to the distance value. Applications using these sensors measure the duration of a pulse using the pulseIn
command.
Some sensors provide values using a serial protocol. For example, the GPS in Recipe 6.14 communicates through the Arduino serial port (see Chapter 4 for more on serial). Most Arduino boards only have one hardware serial port, so read Recipe 6.14 for an example of how you can add additional software serial ports if you have multiple serial sensors or the hardware serial port is occupied for some other task.
The I2C and SPI digital serial communications interfaces were created for processors and microcontrollers like Arduino to talk to external sensors and modules. For example, Recipe 6.15 shows how a gyroscope module is connected using I2C. These protocols are used extensively for sensors, actuators, and peripherals, and they are covered in detail in Chapter 13.
There is another generic class of sensing devices that you may make use of. These are consumer devices that contain sensors but are sold as devices in their own right, rather than as sensors. An example of this in this chapter is a PS/2 mouse. These devices can be very useful; they provide sensors already incorporated into robust and ergonomic devices. They are also inexpensive (often less expensive than buying the raw sensors that they contain), as they are mass-produced. You may have some of these lying around.
If you are using a device that is not specifically covered in a recipe, check the Arduino Library Manager to see if there is a library available for it (see Recipe 16.2). If not, you may be able to adapt a recipe for a device that produces a similar type of output. Information about a sensor’s output signal is usually available from the company from which you bought the device or from a datasheet for your device (which you can find through a Google search of the device part number or description).
Datasheets are aimed at engineers designing products to be manufactured, and they usually provide more detail than you need to just get the product up and running. If you can’t find a datasheet at the component vendor’s website, you can usually find it with a search engine by specifying the name of the component and the word “datasheet.” The information on output signal will usually be in a section referring to data format, interface, output signal, or something similar. Don’t forget to check the maximum voltage (usually in a section labeled Absolute Maximum Ratings) to ensure that you don’t damage the component.
Sensors designed for a maximum of 3.3 volts can be destroyed by connecting them to a voltage above that, such as an output pin on an Arduino board that operates at a 5-volt logic level. Check the absolute maximum rating for your device before connecting. If you need to connect a 5V output to a 3.3V-tolerant input, you can use a voltage divider in most cases. See Recipe 5.11 for more details on working with a voltage divider.
Reading sensors from the messy analog world is a mixture of science, art, and perseverance. You may need to use ingenuity and trial and error to get a successful result. A common problem is that the sensor just tells you a physical condition has occurred, not what caused it. Putting the sensor in the right context (location, range, orientation) and limiting its exposure to things that you don’t want to activate it are skills you will acquire with experience.
Another issue concerns separating the desired signal from background noise; Recipe 6.7 shows how you can use a threshold to detect when a signal is above a certain level, and Recipe 6.8 shows how you can take the average of a number of readings to smooth out noise spikes.
For information on working with and connecting electronic components, see Make: Electronics by Charles Platt (Make Community).
Making Things Talk by Tom Igoe (Make Community) addresses the intersection of science, art, and perseverance in designing and implementing sensor-based systems with Arduino.
See the introduction to Chapter 5 and Recipe 5.6 for more on reading analog values from sensors.
The Arduino Nano 33 BLE Sense is designed exactly for this type of situation. It is very small, inexpensive, fast, and includes eight sensor capabilities that are provided by a group of components built right into the board. Table 6-1 lists the components, their capabilities, and the name of the supporting library. Before you can use the Nano 33 BLE Sense, first open the Arduino Boards Manager and install the Arduino nRF528x Boards (Mbed OS) package (see Recipe 1.7). Next, install each of the libraries listed in the Library name column using the Library Manager (see Recipe 16.2).
Component | Features | Library name |
---|---|---|
Broadcom APDS-9960 |
Gesture, Proximity, RGB Color |
Arduino_APDS9960 |
ST HTS221 |
Temperature, Relative Humidity |
Arduino_HTS221 |
ST LPS22HB |
Barometric Pressure |
Arduino_LPS22HB |
ST LSM9DS1 |
9DOF Inertial Measurement Unit (IMU): accelerometer, gyroscope, magnetometer |
Arduino_LSM9DS1 |
ST MP34DT05 |
Digital microphone |
(Installed by default with Nano 33 BLE board package) |
After you’ve installed support for the Nano 33 BLE Sense board and the supporting libraries, use the Tools menu to configure the Arduino IDE to use the Nano 33 BLE board and set the correct port. As of this writing, both the Nano 33 BLE and Nano 33 BLE Sense use the same board setting in the IDE (the Nano 33 BLE is the same as the Nano 33 BLE Sense, just without all the cool sensors). Next, load the following sketch onto the board and open the Serial Monitor:
/*
* Arduino Nano BLE Sense sensor demo
*/
#include <Arduino_APDS9960.h>
#include <Arduino_HTS221.h>
#include <Arduino_LPS22HB.h>
#include <Arduino_LSM9DS1.h>
void
setup
()
{
Serial
.
begin
(
9600
);
while
(
!
Serial
);
if
(
!
APDS
.
begin
())
{
// Initialize gesture/color/proximity sensor
Serial
.
println
(
"Could not initialize APDS9960."
);
while
(
1
);
}
if
(
!
HTS
.
begin
())
{
// Initialize temperature/humidity sensor
Serial
.
println
(
"Could not initialize HTS221."
);
while
(
1
);
}
if
(
!
BARO
.
begin
())
{
// Initialize barometer
Serial
.
println
(
"Could not initialize LPS22HB."
);
while
(
1
);
}
if
(
!
IMU
.
begin
())
{
// Initialize inertial measurement unit
Serial
.
println
(
"Could not initialize LSM9DS1."
);
while
(
1
);
}
prompt
();
// Tell users what they can do.
}
void
loop
()
{
// If there's a gesture, run the appropriate function.
if
(
APDS
.
gestureAvailable
())
{
int
gesture
=
APDS
.
readGesture
();
switch
(
gesture
)
{
case
GESTURE_UP
:
readTemperature
();
break
;
case
GESTURE_DOWN
:
readHumidity
();
break
;
case
GESTURE_LEFT
:
readPressure
();
break
;
case
GESTURE_RIGHT
:
Serial
.
println
(
"Spin the gyro!
\n
x, y, z"
);
for
(
int
i
=
0
;
i
<
10
;
i
++
)
{
readGyro
();
delay
(
250
);
}
break
;
default
:
break
;
}
prompt
();
// Show the prompt again
}
}
void
prompt
()
{
Serial
.
println
(
"
\n
Swipe!"
);
Serial
.
println
(
"Up for temperature, down for humidity"
);
Serial
.
println
(
"Left for pressure, right for gyro fun.
\n
"
);
}
void
readTemperature
()
{
float
temperature
=
HTS
.
readTemperature
(
FAHRENHEIT
);
Serial
.
(
"Temperature: "
);
Serial
.
(
temperature
);
Serial
.
println
(
" °F"
);
}
void
readHumidity
()
{
float
humidity
=
HTS
.
readHumidity
();
Serial
.
(
"Humidity: "
);
Serial
.
(
humidity
);
Serial
.
println
(
" %"
);
}
void
readPressure
()
{
float
pressure
=
BARO
.
readPressure
(
PSI
);
Serial
.
(
"Pressure: "
);
Serial
.
(
pressure
);
Serial
.
println
(
" psi"
);
}
void
readGyro
()
{
float
x
,
y
,
z
;
if
(
IMU
.
gyroscopeAvailable
())
{
IMU
.
readGyroscope
(
x
,
y
,
z
);
Serial
.
(
x
);
Serial
.
(
", "
);
Serial
.
(
y
);
Serial
.
(
", "
);
Serial
.
println
(
z
);
}
}
The Serial Monitor will display a prompt that tells you how you can interact with the Arduino Nano 33 BLE Sense. To swipe in a given direction, hold your hand over the top of the board and make a wiping motion. To swipe up, wave your hand in a motion that moves from the board’s USB port up to the u-blox module on the opposite end.
The code in this recipe uses several of the sensors built into the Nano 33 BLE Sense: the gesture sensor (APDS-9960), the temperature/humidity sensor (HTS221), the barometer (LPS22HB), and the gyroscope (LSM9DS1). The setup
function waits until the serial port is open, then it initializes each of these devices, and if it encounters an error, it will display an error message and hang by entering an endless loop with while(1);
. At the end of setup
, the sketch calls the prompt
routine, which displays instructions on the Serial Monitor.
Within the loop
, the sketch checks to see if the APDS-9960 has detected a gesture. If so, it dispatches execution to a function that corresponds to the desired sensor. Each of these functions reads the state of the sensor and displays it on the Serial Monitor. For the gyroscope, the sketch prompts you to spin the board around, and then enters a loop where it reads the gyro 10 times with a slight delay so you can see how the values change with your motion.
Arduino has a forum dedicated to the Nano 33 BLE Sense. You may also want to visit the forum for the Nano 33 BLE, which is a variant of the board without all the built-in sensors.
Recipe 6.15 has more on using a gyroscope with Arduino.
Recipe 6.17 has more on accelerometers.
This sketch uses a switch that closes a circuit when tilted, called a tilt sensor. The switch recipes in Chapter 5 (Recipes 5.1 and 5.2) will work with a tilt sensor substituted for the switch.
The following sketch (circuit shown in Figure 6-1) will switch on the LED attached to pin 11 when the tilt sensor is tilted one way, and the LED connected to pin 12 when it is tilted the other way:
/*
* tilt sketch
*
* a tilt sensor attached to pin 2 lights one of
* the LEDs connected to pins 11 and 12 depending
* on which way the sensor is tilted
*/
const
int
tiltSensorPin
=
2
;
// pin the tilt sensor is connected to
const
int
firstLEDPin
=
11
;
// pin for one LED
const
int
secondLEDPin
=
12
;
// pin for the other
void
setup
()
{
pinMode
(
tiltSensorPin
,
INPUT_PULLUP
);
// Tilt sensor connected to this pin
pinMode
(
firstLEDPin
,
OUTPUT
);
// first output LED
pinMode
(
secondLEDPin
,
OUTPUT
);
// and the second
}
void
loop
()
{
if
(
digitalRead
(
tiltSensorPin
)
==
LOW
){
// The switch is on (upright)
digitalWrite
(
firstLEDPin
,
HIGH
);
// Turn on the first LED
digitalWrite
(
secondLEDPin
,
LOW
);
// and turn off the second.
}
else
{
// The switch is off (tilted)
digitalWrite
(
firstLEDPin
,
LOW
);
// Turn the first LED off
digitalWrite
(
secondLEDPin
,
HIGH
);
// and turn on the second.
}
}
The most common tilt sensor is a ball bearing in a tube with contacts at one end. When the tube is tilted the ball rolls away from the contacts and the connection is broken. When the tube is tilted to roll the other way, the ball touches the contacts and completes a circuit. Markings, or pin configurations, may show which way the sensor should be oriented. Tilt sensors are sensitive to small movements of around 5 to 10 degrees when oriented with the ball just touching the contacts. If you position the sensor so that the ball bearing is directly above the contacts, the LED state will only change if it is just turned over. This can be used to tell if something is upright or upside down.
To determine if something is being shaken, you need to check how long it’s been since the state of the tilt sensor changed (this recipe’s Solution just checks if the switch was open or closed). If it hasn’t changed for a time you consider significant, the object is not shaking. Changing the orientation of the tilt sensor will change how vigorous the shaking needs to be to trigger it. The following code lights the built-in LED when the sensor is shaken:
/*
* shaken sketch
* tilt sensor connected to pin 2
* using the built-in LED
*/
const
int
tiltSensorPin
=
2
;
const
int
ledPin
=
LED_BUILTIN
;
int
tiltSensorPreviousValue
=
0
;
int
tiltSensorCurrentValue
=
0
;
long
lastTimeMoved
=
0
;
int
shakeTime
=
50
;
void
setup
()
{
pinMode
(
tiltSensorPin
,
INPUT_PULLUP
);
pinMode
(
ledPin
,
OUTPUT
);
}
void
loop
()
{
tiltSensorCurrentValue
=
digitalRead
(
tiltSensorPin
);
if
(
tiltSensorPreviousValue
!=
tiltSensorCurrentValue
)
{
lastTimeMoved
=
millis
();
tiltSensorPreviousValue
=
tiltSensorCurrentValue
;
}
if
(
millis
()
-
lastTimeMoved
<
shakeTime
){
digitalWrite
(
ledPin
,
HIGH
);
}
else
{
digitalWrite
(
ledPin
,
LOW
);
}
}
Many mechanical switch sensors can be used in similar ways. A float switch can turn on when the water level in a container rises to a certain level (similar to the way a float valve works in a toilet cistern). A pressure pad such as the one used in shop entrances can be used to detect when someone stands on it. If your sensor turns a digital signal on and off, something similar to this recipe’s sketch will be suitable.
Chapter 5 contains background information on using switches with Arduino.
Recipe 12.1 has more on using the millis
function to determine delay.
The easiest way to detect light levels is to use a photoresistor, also known as a light-dependent resistor (LDR). This changes resistance with changing light levels, and when connected in the circuit shown in Figure 6-2 it produces a change in voltage that the Arduino analog input pins can sense.
The sketch for this recipe is simple:
/*
* Light sensor sketch
*
* Varies the blink rate based on the measured brightness
*/
const
int
ledPin
=
LED_BUILTIN
;
// Built-in LED
const
int
sensorPin
=
A0
;
// connect sensor to analog input 0
void
setup
()
{
pinMode
(
ledPin
,
OUTPUT
);
// enable output on the led pin
}
void
loop
()
{
int
rate
=
analogRead
(
sensorPin
);
// read the analog input
digitalWrite
(
ledPin
,
HIGH
);
// set the LED on
delay
(
rate
);
// wait duration dependent on light level
digitalWrite
(
ledPin
,
LOW
);
// set the LED off
delay
(
rate
);
}
Photoresistors contain a compound (cadmium sulfide) that is a hazardous substance. A phototransistor is a perfectly good alternative to a photoresistor. A phototransistor has a long lead and a short lead, like an LED. You can wire it as shown in Figure 6-2, but you must connect the long lead to 5V and the short lead to the resistor and pin 0. Be sure to buy a phototransistor, such as Adafruit part number 2831, that can sense visible light so you can test it with a common light source.
The circuit for this recipe is the standard way to use any sensor that changes its resistance based on some physical phenomenon (see Chapter 5 for background information on responding to analog signals). With the circuit in Figure 6-2, the voltage on analog pin 0 changes as the resistance of the photoresistor (or phototransistor) changes with varying light levels.
A circuit such as this will not give the full range of possible values from the analog input—0 to 1,023—as the voltage will not be swinging from 0 volts to 5 volts. This is because there will always be a voltage drop across each resistance, so the voltage where they meet will never reach the limits of the power supply. When using sensors such as these, it is important to check the actual values the device returns in the situation in which you will be using it. Then you have to determine how to convert them to the values you need to control whatever you are going to control. See Recipe 5.7 for more details on changing the range of values.
The photoresistor is a simple kind of sensor called a resistive sensor. A range of resistive sensors respond to changes in different physical characteristics.
Arduino cannot measure resistance directly, so the Solution uses a fixed-value resistor in combination with a resistive sensor to form a voltage divider like you saw back in Recipe 5.11. The analog pins read voltage, not resistance, so the only way for Arduino to measure resistance is if that resistance is somehow changing a voltage. A voltage divider uses a pair of resistors to produce an output voltage that is dependent on the relationship between the input voltage and two resistors. So, you can combine a fixed-value resistor with a component of variable resistance, such as a photoresistor, and Arduino’s analog pin will see a voltage that changes based on what the photoresistor is sensing.
Similar circuits will work for other kinds of simple resistive sensors, although you may need to adjust the resistor to suit the sensor. Choosing the best resistor value depends on the photoresistor you are using and the range of light levels you want to monitor. Engineers would use a light meter and consult the datasheet for the photoresistor, but if you have a multimeter, you can measure the resistance of the photoresistor at a light level that is approximately midway in the range of illumination you want to monitor. Note the reading and choose the nearest convenient resistor to this value. You can also read the values from Arduino, print it to the serial port, and use the Serial Plotter to show the highs and lows (see Recipe 4.1).
Be aware of any artificial light sources in your environment that flicker on and off at an unusual rate, such as neon or some LED lights. Even though they turn off and on too quickly for a human to discern, these may register as low-light conditions to an Arduino. You can adjust for this by taking a moving average of the readings (you can see an example of this calculation in Recipe 6.8).
This sketch was introduced in Recipe 1.6; see that recipe for more on this and variations on this sketch.
Use a motion sensor such as a Passive Infrared (PIR) sensor to change values on a digital pin when a living creature (or an object that radiates warmth) moves nearby.
Sensors such as the Adafruit PIR (motion) Sensor (part number 189) and the Parallax PIR Sensor (555-28027) can be easily connected to Arduino pins, as shown in Figure 6-3. Some PIR sensors, such as the SparkFun PIR Motion Sensor (SEN-13285) require a pull-up resistor on the sensor’s output. If you use the pull-up resistor, you will need to use the INPUT_PULLUP
mode and invert the logic in the sketch as described in the Discussion.
Check the datasheet for your sensor to identify the correct pins. For example, the Adafruit sensor has pins marked “OUT,” “-,” and “+” (for Output, GND, and +5V) and the Parallax sensor is labeled GND, VCC, and OUT.
The following sketch will light your board’s built-in LED when the sensor detects motion:
/*
PIR sketch
a Passive Infrared motion sensor connected to pin 2
lights the LED on the built-in LED
*/
const
int
ledPin
=
LED_BUILTIN
;
// choose the pin for the LED
const
int
inputPin
=
2
;
// choose the input pin (for the PIR sensor)
void
setup
()
{
pinMode
(
ledPin
,
OUTPUT
);
// declare LED as output
pinMode
(
inputPin
,
INPUT
);
// declare pin as input
}
void
loop
(){
int
val
=
digitalRead
(
inputPin
);
// read input value
if
(
val
==
HIGH
)
// check if the input is HIGH
{
digitalWrite
(
ledPin
,
HIGH
);
// turn LED on if motion detected
delay
(
500
);
digitalWrite
(
ledPin
,
LOW
);
// turn LED off
}
}
This code is similar to the pushbutton examples shown in Chapter 5. That’s because the sensor acts like a switch when motion is detected. Different kinds of PIR sensors are available, and you should check the information for the one you have connected.
Some sensors, such as the Parallax and Adafruit PIR sensors, have a jumper that determines how the output behaves when motion is detected. In one mode, the output remains HIGH
while motion is detected, or it can be set so that the output goes HIGH
briefly and then LOW
when triggered. The example sketch in this recipe’s Solution will work in either mode.
Other sensors may go LOW
on detecting motion. If your sensor’s output pin goes LOW
when motion is detected, change the line that checks the input value so that the LED is turned on when LOW
:
if
(
val
==
LOW
)
// motion detected when the input is LOW
If your sensor’s documentation indicates that it needs a pull-up resistor, you should change the code in setup
that initializes inputPin
:
pinMode
(
inputPin
,
INPUT_PULLUP
);
// declare pin as input with pull-up resistor
PIR sensors come in a variety of styles and are sensitive over different distances and angles. Careful choice and positioning can make them respond to movement in part of a room, rather than all of it. Some PIR sensors have a potentiometer that you can adjust with a screwdriver to change the PIR’s sensitivity.
This recipe uses the Parallax PING))) ultrasonic distance sensor to measure the distance of an object ranging from 2 centimeters to around 3 meters. It displays the distance on the Serial Monitor and flashes an LED faster as objects get closer (Figure 6-4 shows the connections):
/* Ping))) Sensor
* prints distance and changes LED flash rate
* depending on distance from the Ping))) sensor
*/
const
int
pingPin
=
5
;
const
int
ledPin
=
LED_BUILTIN
;
// LED pin
void
setup
()
{
Serial
.
begin
(
9600
);
pinMode
(
ledPin
,
OUTPUT
);
}
void
loop
()
{
int
cm
=
ping
(
pingPin
);
Serial
.
println
(
cm
);
digitalWrite
(
ledPin
,
HIGH
);
delay
(
cm
*
10
);
// each centimeter adds 10 ms delay
digitalWrite
(
ledPin
,
LOW
);
delay
(
cm
*
10
);
}
// Measure distance and return the result in centimeters
int
ping
(
int
pingPin
)
{
long
duration
;
// This will store the measured duration of the pulse
// Set the pingPin to output.
pinMode
(
pingPin
,
OUTPUT
);
digitalWrite
(
pingPin
,
LOW
);
// Stay low for 2μs to ensure a clean pulse
delayMicroseconds
(
2
);
// Send a pulse of 5μs
digitalWrite
(
pingPin
,
HIGH
);
delayMicroseconds
(
5
);
digitalWrite
(
pingPin
,
LOW
);
// Set the pingPin to input and read the duration of the pulse.
pinMode
(
pingPin
,
INPUT
);
duration
=
pulseIn
(
pingPin
,
HIGH
);
// convert the time into a distance
return
duration
/
29
/
2
;
}
Ultrasonic sensors provide a measurement of the time it takes for sound to bounce off an object and return to the sensor.
The “ping” sound pulse is generated when the pingPin
level goes HIGH
for two microseconds. The sensor will then generate a pulse that terminates when the sound returns. The width of the pulse is proportional to the distance the sound traveled, and the sketch then uses the pulseIn
function to measure that duration. The speed of sound is about 340 meters per second, which is 29 microseconds per centimeter. The formula for the distance of the round trip is: duration in microseconds / 29
.
So, the formula for the one-way distance in centimeters is: duration in microseconds / 29 / 2
. The 340 meters per second figure is the approximate speed of sound at 20°C/68°F. If your ambient temperature is significantly different, you can use a speed of sound calculator such as that hosted by the United States National Weather
Service.
A lower-cost alternative to the Parallax PING))) sensor is the HC-SR04, which is available from many suppliers and also on eBay. Although this has less accuracy and range, it can be suitable where the price is more important than performance. The HC-SR04 has separate pins to trigger the sound pulse and detect the echo. This variation on the previous sketch shows its use:
/* HC-SR04 Sensor
* prints distance and changes LED flash rate
* depending on distance from the HC-SR04 sensor
*/
const
int
trigPin
=
5
;
// Pin to send the ping from
const
int
echoPin
=
6
;
// Pin to read the response from
const
int
ledPin
=
LED_BUILTIN
;
// LED pin
void
setup
()
{
Serial
.
begin
(
9600
);
pinMode
(
ledPin
,
OUTPUT
);
pinMode
(
trigPin
,
OUTPUT
);
pinMode
(
echoPin
,
INPUT
);
}
void
loop
()
{
int
cm
=
calculateDistance
(
trigPin
);
Serial
.
println
(
cm
);
digitalWrite
(
ledPin
,
HIGH
);
delay
(
cm
*
10
);
// each centimeter adds 10 ms delay
digitalWrite
(
ledPin
,
LOW
);
delay
(
cm
*
10
);
delay
(
60
);
// datasheet recommends waiting at least 60ms between measurements
}
int
calculateDistance
(
int
trigPin
)
{
long
duration
;
// This will store the measured duration of the pulse
digitalWrite
(
trigPin
,
LOW
);
delayMicroseconds
(
2
);
// Stay low for 2μs to ensure a clean pulse
digitalWrite
(
trigPin
,
HIGH
);
delayMicroseconds
(
10
);
// Send a pulse of 10μs to ensure a clean pulse
digitalWrite
(
trigPin
,
LOW
);
// Read the duration of the response pulse
duration
=
pulseIn
(
echoPin
,
HIGH
);
// convert time into distance
return
duration
/
29
/
2
;
}
The HC-SR04 datasheet recommends at least 60 ms between measurements, but blinking the LED takes up some time, so the delay(60);
adds more of a delay than is needed. But if you are writing code that does not add its own delay, you’ll want to keep that 60 ms delay in there.
The HC-SR04 works best with 5 volts but can be used with 3.3V boards that are 5-volt tolerant, such as the Teensy 3. Figure 6-5 shows the wiring for a 5V board.
The MaxBotix EZ1 is another ultrasonic sensor that can be used to measure distance. It is easier to integrate than the Ping))) or the HC-SR04 because it does not need to be “pinged” and it can operate on 3.3 or 5 volts. It provides continuous distance information, either as an analog voltage or proportional to pulse width. Figure 6-6 shows the connections.
The sketch that follows uses the EZ1 pulse width (PW) output to produce output similar to that of the previous sketch:
/*
* EZ1Rangefinder Distance Sensor
* prints distance and changes LED flash rate
* depending on distance from the sensor
*/
const
int
sensorPin
=
5
;
const
int
ledPin
=
LED_BUILTIN
;
void
setup
()
{
Serial
.
begin
(
9600
);
pinMode
(
ledPin
,
OUTPUT
);
}
void
loop
()
{
long
value
=
pulseIn
(
sensorPin
,
HIGH
)
;
int
cm
=
value
/
58
;
// pulse width is 58 microseconds per cm
Serial
.
println
(
cm
);
digitalWrite
(
ledPin
,
HIGH
);
delay
(
cm
*
10
);
// each centimeter adds 10 ms delay
digitalWrite
(
ledPin
,
LOW
);
delay
(
cm
*
10
);
delay
(
20
);
}
The EZ1 is powered through +5V and ground pins and these are connected to the respective Arduino pins. Connect the EZ1 PW pin to Arduino digital pin 5. The sketch measures the width of the pulse with the pulseIn
command. The width of the pulse is 58 microseconds per centimeter, or 147 microseconds per inch.
You may need to add a capacitor across the +5V and GND lines to stabilize the power supply to the sensor if you are using long connecting leads. If you get erratic readings, connect a 10 uF capacitor at the sensor (see Appendix C for more on using decoupling capacitors).
You can also obtain a distance reading from the EZ1 through its analog output—connect the AN pin to an analog input and read the value with analogRead
. The following code prints the analog input converted to cm:
int
value
=
analogRead
(
A0
);
float
mv
=
(
value
/
1024.0
)
*
5000
;
float
inches
=
mv
/
9.8
;
// 9.8mv per inch per datasheet
float
cm
=
inches
*
2.54
;
Serial
.
(
"in: "
);
Serial
.
println
(
inches
);
Serial
.
(
"cm: "
);
Serial
.
println
(
cm
);
The value from analogRead
is around 4.8mV per unit (see Recipe 5.6 for more on analogRead
), and according to the datasheet, the EZ1 output is 9.8mV/inch when powered at 5V, and 6.4mV/inch at 3.3V. Multiply the result in inches by 2.54 to get the distance in centimeters.
Recipe 5.6 explains how to convert readings from analogInput
into voltage values.
You want to measure how far objects are from the Arduino with more precision than in Recipe 6.5.
Time of flight distance sensors use a tiny laser and sensor to measure how long it takes for a laser light signal to return to it. While they have a much more narrow field of view than the ultrasonic sensors you saw in Recipe 6.5, laser-based time of flight sensors can be more precise. However, time of flight sensors typically have a smaller range. For example, while the HC-SR04 has a range of 2 cm to 4 meters, the VL6180X time of flight sensor can measure 5 cm to 10 cm. This sketch provides similar functionality to Recipe 6.5, but it uses the VL6180X Time of Flight Distance Ranging Sensor from Adafruit (product ID 3316). Figure 6-7 shows the connections. To use this sketch, you’ll need to install the Adafruit_VL6180X library (see Recipe 16.2):
/* tof-distance sketch
* prints distance and changes LED flash rate based on distance from sensor
*/
#include <Wire.h>
#include "Adafruit_VL6180X.h"
Adafruit_VL6180X
sensor
=
Adafruit_VL6180X
();
const
int
ledPin
=
LED_BUILTIN
;
// LED pin
void
setup
()
{
Serial
.
begin
(
9600
);
while
(
!
Serial
);
if
(
!
sensor
.
begin
())
{
Serial
.
println
(
"Could not initialize VL6180X"
);
while
(
1
);
}
}
void
loop
()
{
// Read the range and check the status for any errors
byte
cm
=
sensor
.
readRange
();
byte
status
=
sensor
.
readRangeStatus
();
if
(
status
==
VL6180X_ERROR_NONE
)
{
Serial
.
println
(
cm
);
digitalWrite
(
ledPin
,
HIGH
);
delay
(
cm
*
10
);
// each centimeter adds 10 ms delay
digitalWrite
(
ledPin
,
LOW
);
delay
(
cm
*
10
);
}
else
{
// Major errors are worth mentioning
if
((
status
>=
VL6180X_ERROR_SYSERR_1
)
&&
(
status
<=
VL6180X_ERROR_SYSERR_5
))
{
Serial
.
println
(
"System error"
);
}
}
delay
(
50
);
}
The VL6180X sensor uses the I2C protocol (see Chapter 13) to communicate, which requires a connection between the Arduino and the sensor’s SCL and SDA pins. The sketch includes the Wire library, which provides support for I2C, and also includes the Adafruit_VL6180X library to provide functions for working with the sensor. Before the setup
function, the sketch defines an object (sensor
) to represent the sensor, and later initializes it in setup
.
The setup
function initializes the serial port and attempts to initialize the sensor. If that fails, it prints an error message to the serial port, and stops running the sketch by entering an infinite while
loop.
On each run through the loop
, the sketch reads the range and also checks the sensor status to make sure it’s not in an error state. If it gets a good reading, it displays the distance to the serial port and blinks the LED at a rate determined by the distance it measured. The example included with the Adafruit_VL6180X library has a more exhaustive check of all the possible error states. With the exception of the system errors that this sketch checks for, most errors are transient and will be corrected on a subsequent reading.
Detailed comparisons of ultrasonic, LED, and laser-based distance sensors are available from DIY Projects and SparkFun.
A Piezo sensor responds to vibration. It works best when connected to a larger surface that vibrates. Figure 6-8 shows the connections:
/* piezo sketch
* lights an LED when the Piezo is tapped
*/
const
int
sensorPin
=
0
;
// the analog pin connected to the sensor
const
int
ledPin
=
LED_BUILTIN
;
// pin connected to LED
const
int
THRESHOLD
=
100
;
void
setup
()
{
pinMode
(
ledPin
,
OUTPUT
);
}
void
loop
()
{
int
val
=
analogRead
(
sensorPin
);
if
(
val
>=
THRESHOLD
)
{
digitalWrite
(
ledPin
,
HIGH
);
delay
(
100
);
// to make the LED visible
}
else
digitalWrite
(
ledPin
,
LOW
);
}
A Piezo sensor, also known as a knock sensor, produces a voltage in response to physical stress. The more it is stressed, the higher the voltage. The Piezo is polarized and the positive side (usually a red wire or a wire marked with a “+”) is connected to the analog input; the negative wire (usually black or marked with a “–”) is connected to ground. A high-value resistor (1 megohm) is connected across the sensor. The resistor is included to protect the Arduino pins against excessive current or voltage.
The voltage is detected by Arduino analogRead
to turn on an LED (see Chapter 5 for more about the analogRead
function). The THRESHOLD
value determines the level from the sensor that will turn on the LED, and you can decrease or increase this value to make the sketch more or less sensitive.
Piezo sensors can be bought in plastic cases or as bare metal disks with two wires attached. The components are the same; use whichever fits your project best.
Some sensors, such as the Piezo, can be driven by the Arduino to produce the thing that they can sense. Chapter 9 has more about using a Piezo to generate sound.
This recipe uses the BOB-12758 breakout board for the Electret Microphone (SparkFun). Connect the board as shown in Figure 6-9 and load the code to the board. If you are using a 3.3V board, you should connect the microphone’s VCC pin to 3.3V instead of 5V.
The built-in LED will turn on when you clap, shout, or play loud music near the microphone. You may need to adjust the threshold—use the Serial Monitor to view the high and low values, and change the threshold value so that it is between the high values you get when noise is present and the low values when there is little or no noise. Upload the changed code to the board and try again:
/* microphone sketch
* SparkFun breakout board for Electret Microphone is connected to analog pin 0
*/
const
int
micPin
=
A0
;
// Microphone connected to analog 0
const
int
ledPin
=
LED_BUILTIN
;
// the code will flash the built-in LED
const
int
middleValue
=
512
;
// the middle of the range of analog values
const
int
numberOfSamples
=
128
;
// how many readings will be taken each time
int
sample
;
// the value read from microphone each time
long
signal
;
// the reading once you have removed DC offset
long
newReading
;
// the average of that loop of readings
long
runningAverage
=
0
;
// the running average of calculated values
const
int
averagedOver
=
16
;
// how quickly new values affect running avg
// bigger numbers mean slower
const
int
threshold
=
400
;
// at what level the light turns on
void
setup
()
{
pinMode
(
ledPin
,
OUTPUT
);
Serial
.
begin
(
9600
);
}
void
loop
()
{
long
sumOfSquares
=
0
;
for
(
int
i
=
0
;
i
<
numberOfSamples
;
i
++
)
{
// take many readings and average them
sample
=
analogRead
(
micPin
);
// take a reading
signal
=
(
sample
-
middleValue
);
// work out its offset from the center
signal
*=
signal
;
// square it
sumOfSquares
+=
signal
;
// add to the total
}
newReading
=
sumOfSquares
/
numberOfSamples
;
// calculate running average
runningAverage
=
(((
averagedOver
-
1
)
*
runningAverage
)
+
newReading
)
/
averagedOver
;
Serial
.
(
"new:"
);
Serial
.
(
newReading
);
Serial
.
(
","
);
Serial
.
(
"running:"
);
Serial
.
println
(
runningAverage
);
if
(
runningAverage
>
threshold
){
// is average more than the threshold?
digitalWrite
(
ledPin
,
HIGH
);
// if it is turn on the LED
}
else
{
digitalWrite
(
ledPin
,
LOW
);
// if it isn't turn the LED off
}
}
A microphone produces very small electrical signals. If you connected it straight to the pin of an Arduino, you would not get any detectable change. The signal needs to be amplified first to make it usable by Arduino. The SparkFun board has the microphone with an amplifier circuit built in to amplify the signal to a level readable by Arduino.
Because you are reading an audio signal in this recipe, you will need to do some additional calculations to get useful information. An audio signal changes fairly quickly, and the value returned by analogRead
will depend on what point in the undulating signal you take a reading. If you are unfamiliar with using analogRead
, see Chapter 5 and Recipe 6.3. An example waveform for an audio tone is shown in Figure 6-10. As time changes from left to right, the voltage goes up and down in a regular pattern. If you take readings at the three different times marked on it, you will get three different values. If you used this to make decisions, you might incorrectly conclude that the signal got louder in the middle.
An accurate measurement requires multiple readings taken close together. The peaks and troughs increase as the signal gets bigger. The difference between the bottom of a trough and the top of a peak is called the amplitude of the signal, and this increases as the signal gets louder.
To measure the size of the peaks and troughs, you measure the difference between the midpoint voltage and the levels of the peaks and troughs. You can visualize this midpoint value as a line running midway between the highest peak and the lowest trough, as shown in Figure 6-11. The line represents the DC offset of the signal (it’s the DC value when there are no peaks or troughs). If you subtract the DC offset value from your analogRead
values, you get the correct reading for the signal amplitude.
As the signal gets louder, the average size of these values will increase, but as some of them are negative (where the signal has dropped below the DC offset), they will cancel each other out, and the average will tend to be zero. To fix that, we square each value (multiply it by itself). This will make all the values positive, and it will increase the difference between small changes, which helps you evaluate changes as well. The average value will now go up and down as the signal amplitude does.
To do the calculation, we need to know what value to use for the DC offset. To get a clean signal, the amplifier circuit for the microphone will have been designed to have a DC offset as close as possible to the middle of the possible range of voltage so that the signal can get as big as possible without distorting. The code assumes this and uses the value 512 (right in the middle of the analog input range of 0 to 1,023). Each time the sketch takes the average of the squared values to calculate a new reading, the sketch updates the running average. The running average is calculated by multiplying the current running average by averagedOver - 1
. With averagedOver
set to 16, this weights the current running average by 15. Next, the sketch adds the new reading in (a weighting of 1), and divides by averagedOver
to get the weighted average, which yields the new running average: (currentAverage * 15 + newReading)/16.
The sketch prints the values of the new reading and the running average in such a way that you can view them with the Serial Plotter (Tools→Serial Plotter). You can see the relationship between the new reading and the running average in Figure 6-12. The running average is less spiky which means the LED will stay on long enough for someone to notice it, rather than just flickering briefly during a spike.
The values of variables at the top of the sketch can be varied if the sketch does not trigger well for the level of sound you want.
The numberOfSamples
is set at 128—if it is set too small, the average may not adequately cover complete cycles of the waveform and you will get erratic readings. If the value is set too high, you will be averaging over too long a time, and a very short sound might be missed as it does not produce enough change once a large number of readings are averaged. It could also start to introduce a noticeable delay between a sound and the light going on. Constants used in calculations, such as numberOfSamples
and averagedOver
, are set to powers of 2 (128 and 16, respectively). Try to use values evenly divisible by two for these to give you the fastest performance (see Chapter 3 for more on math functions).
While the values as calculated work well for detecting sound levels, you can change the sketch so it lines up with standard methods for measuring sound levels (decibels). First, you’ll need to change the way newReading
is calculated to take the square root of the average (this is called a Root Mean Square, or RMS). Next, you’ll want to take the common logarithm of both values and multiply it by 20 to get decibels. This is unlikely to yield an accurate measurement without calibration, but it is a starting point:
newReading
=
sqrt
(
sumOfSquares
/
numberOfSamples
)
;
// calculate running average
runningAverage
=
(
(
(
averagedOver
-
1
)
*
runningAverage
)
+
newReading
)
/
averagedOver
;
Serial
.
(
"
new:
"
)
;
Serial
.
(
20
*
log10
(
newReading
)
)
;
Serial
.
(
"
,
"
)
;
Serial
.
(
"
running:
"
)
;
Serial
.
println
(
20
*
log10
(
runningAverage
)
)
;
You will also need to modify the threshold to something much lower:
const
int
threshold
=
30
;
// at what level the light turns on
This recipe displays the temperature in Fahrenheit and Celsius (Centigrade) using the popular TMP36 heat detection sensor. The sensor looks similar to a transistor and is connected as shown in Figure 6-13.
If you are using a 3.3V board, you must connect the TMP36 power pin to 3.3V instead of 5V, and change float millivolts = (value / 1024.0) * 5000;
to float millivolts = (value / 1024.0) * 3300;
in the sketch.
/*
* tmp36 sketch
* prints the temperature to the Serial Monitor
* and turns on the LED when a threshold is reached
*/
const
int
inPin
=
A0
;
// analog pin
const
int
ledPin
=
LED_BUILTIN
;
const
int
threshold
=
80
;
// Turn on the LED over 80F
void
setup
()
{
Serial
.
begin
(
9600
);
}
void
loop
()
{
int
value
=
analogRead
(
inPin
);
// Use 3300 instead of 5000 for 3.3V boards
float
millivolts
=
(
value
/
1024.0
)
*
5000
;
// 10mV per degree Celsius with a 500mv offset
float
celsius
=
(
millivolts
-
500
)
/
10
;
float
fahrenheit
=
(
celsius
*
9
)
/
5
+
32
;
Serial
.
(
"C:"
);
Serial
.
(
celsius
);
Serial
.
(
","
);
Serial
.
(
"F:"
);
Serial
.
println
(
fahrenheit
);
// converts to fahrenheit
if
(
fahrenheit
>
threshold
){
// is the temperature over the threshold?
digitalWrite
(
ledPin
,
HIGH
);
// if it is turn on the LED
}
else
{
digitalWrite
(
ledPin
,
LOW
);
// if it isn't turn the LED off
}
delay
(
1000
);
// wait for one second
}
The TMP36 temperature sensor produces an analog voltage directly proportional to temperature with an output of 1 millivolt (mV) per 0.1°C (10mV per degree), but with a 500mV offset.
The sketch converts the analogRead
values into millivolts (see Chapter 5). It then subtracts 0.5V (500mV), the offset voltage specified in the TMP36 datasheet, and then divides the result by 10 to get degrees C. If the temperature exceeds the threshold value, the sketch lights the onboard LED. You can easily get the sensor to go over F80 by holding the sensor between two fingers, but avoid touching your fingers to the sensor’s leads so as to not interfere with the electrical signaling.
There are many temperature sensors available, but an interesting alternative is the waterproof DS18B20 digital temperature sensor (Adafruit part 381, SparkFun part SEN-11050, available from other suppliers as well). It is wired and used differently than the TMP36.
The DS18B20 is based on the 1-Wire protocol pioneered by Dallas Semiconductor (now Maxim), and requires two libraries. The first is the OneWire library. There are several libraries available with OneWire in their name, so be sure to choose the OneWire library by Jim Studt, Tom Pollard, et al. You will also need the DallasTemperature library. You can install both using the Library Manager (see Recipe 16.2). To wire the DS18B20, connect the red wire to 5V (or 3.3V if on a 3.3V board), black to ground, and the signal wire (yellow, white, or some other color) to digital pin 2 with a 4.7K resistor between the signal and power (5V or 3.3V) pin, as shown in Figure 6-14.
Here’s the sketch for reading the temperature:
/* DS18B20 temperature
* Reads temperature from waterproof sensor probe
*/
#include <OneWire.h>
#include <DallasTemperature.h>
#define ONE_WIRE_BUS 2
// The pin that the sensor wire is connected to
const
int
ledPin
=
LED_BUILTIN
;
const
int
threshold
=
80
;
// Turn on the LED over 80F
OneWire
oneWire
(
ONE_WIRE_BUS
);
// Prepare the OneWire connection
DallasTemperature
sensors
(
&
oneWire
);
// Declare the temp sensor object
void
setup
(
void
)
{
Serial
.
begin
(
9600
);
// Initialize the sensor
sensors
.
begin
();
}
void
loop
(
void
)
{
sensors
.
requestTemperatures
();
// Request a temperature reading
// Retrieve the temperature reading in F and C
float
fahrenheit
=
sensors
.
getTempFByIndex
(
0
);
float
celsius
=
sensors
.
getTempCByIndex
(
0
);
// Display the temperature readings in a Serial Plotter-friendly format
Serial
.
(
"C:"
);
Serial
.
(
celsius
);
Serial
.
(
","
);
Serial
.
(
"F:"
);
Serial
.
println
(
fahrenheit
);
if
(
fahrenheit
>
threshold
){
// is the temperature over the threshold?
digitalWrite
(
ledPin
,
HIGH
);
// if it is turn on the LED
}
else
{
digitalWrite
(
ledPin
,
LOW
);
// if it isn't turn the LED off
}
delay
(
1000
);
}
The sketch pulls in the header files for each library, and initializes the data structures needed to work with the 1-Wire protocol and with the sensor. Inside the loop, the sketch requests a temperature reading, and then reads the temperature in Celsius, then Fahrenheit. Note that you do not need to perform any arithmetic conversion on the results you get from the sensor. Everything is handled by the library. Note also that you do not need to make any code changes (but make sure you wire the sensor’s power to 3.3V, not 5V) when you use a 3.3V board.
Figure 6-15 shows a PN532 NFC reader connected to Arduino over serial pins (TX and RX). PN532 NFC readers are available from a number of suppliers. The Seeed Studio Grove NFC reader (part 113020006) is connected as shown in the diagram. You can also find a PN532 reader in a shield form factor (SeeedStudio part 113030001, Adafruit part 789). You will need to install the SeeedStudio Seeed_Arduino_NFC library (see Recipe 16.2). The Seeed library includes a from modified version of the NDEF library so you do not need to install that library.
PN532 readers work with 13.56 MHz MIFARE Classic and MIFARE Ultralight tags. If you are using a different reader, check the documentation for information on wiring the reader to Arduino and for example code.
The sketch reads an NFC tag and displays its unique ID:
/* NFC Tag Scanner - Serial
* Look for an NFC tag and display its unique identifier.
*/
#include <NfcAdapter.h>
#include <PN532/PN532/PN532.h>
#include <PN532/PN532_HSU/PN532_HSU.h>
PN532_HSU
pn532hsu
(
Serial1
);
NfcAdapter
nfc
(
pn532hsu
);
void
setup
()
{
Serial
.
begin
(
9600
);
nfc
.
begin
();
// Initialize the NFC reader
}
void
loop
()
{
Serial
.
println
(
"Waiting for a tag"
);
if
(
nfc
.
tagPresent
())
// If the reader sees an NFC tag
{
NfcTag
tag
=
nfc
.
read
();
// read the NFC tag
Serial
.
println
(
tag
.
getUidString
());
// Display its id
}
delay
(
500
);
}
NFC (Near-Field Communication) is a specialized variant of RFID (Radio Frequency Identification) technology that operates at a frequency of 13.56 MHz and supports a data format called NDEF (NFC Data Exchange Format). NDEF provides a variety of structured messages you can store on a tag, a small electronic device that can be embedded in cards, stickers, keychain fobs, and other objects. The tag consists of a relatively large antenna that receives signals from an RFID/NFC reader. The reader can be embedded in a computer or a mobile phone, or can be a module that you connect to your Arduino (as with the PN532 module). When the tag receives the signal, it harvests enough energy from it to energize the circuitry on the tag, which responds to the signal by transmitting the information contained in its memory. There are also tags that have their own power, such as a motor vehicle transponder used in automated toll payment systems. Such tags are known as active tags, while the energy-harvesting type is called a passive tag.
An NDEF tag transmits a collection of data when it is activated by a reader. This data includes information that identifies the tag, along with any information stored on the tag. The Solution uses Don Coleman’s NDEF library to simplify reading the tag data.
The code shown in the Solution will work with the Seeed Studio Grove NFC module connected over Serial1
. It uses the USB serial connection to send information that you can view in the Serial Monitor. Serial1
is not present on the Arduino Uno (see “Serial Hardware”), which means you would need to use SoftwareSerial with this module because on the Uno (and compatible boards based on the ATmega328), USB Serial and the TX/RX pins are shared, so these boards cannot talk to a serial device and over the USB Serial connection at the same time. See Recipe 4.11 for information on SoftwareSerial. You can also reconfigure the Grove NFC module to use I2C.
The Seeed Studio NFC shield communicates over SPI. If you want to use it with the Seeed Studio NFC shield, change the lines at the top of the sketch to:
#include <SPI.h>
#include <NfcAdapter.h>
#include <PN532/PN532/PN532.h>
#include <PN532/PN532_SPI/PN532_SPI.h>
PN532_SPI
pn532spi
(
SPI
,
10
);
NfcAdapter
nfc
=
NfcAdapter
(
pn532spi
);
If you want to use it with the Adafruit shield or the Grove NFC module in I2C mode, change the lines at the top of the sketch to:
#include <Wire.h>
#include <NfcAdapter.h>
#include <PN532/PN532/PN532.h>
#include <PN532/PN532_I2C/PN532_I2C.h>
PN532_I2C
pn532i2c
(
Wire
);
NfcAdapter
nfc
=
NfcAdapter
(
pn532i2c
);
You can also read any message on that tag and write your own message (assuming the tag has not been locked) using the NDEF library. If you replace the loop
function with the following, the sketch will read the tag, and then use the NfcTag
object’s print
function to display the tag ID and any message on it. It will then display a countdown. If you leave the tag in place, it will write a URL to the tag. If you have an NFC-enabled mobile phone, you can hold the tag up to the phone and it should open the URL in a web browser:
void
loop
()
{
Serial
.
println
(
"Waiting for a tag"
);
if
(
nfc
.
tagPresent
())
// If the reader sees an NFC tag
{
NfcTag
tag
=
nfc
.
read
();
// read the NFC tag
tag
.
();
// print whatever is currently on it
// Give the user time to avoid writing to the tag
Serial
.
(
"Countdown to writing the tag: 3"
);
for
(
int
i
=
2
;
i
>=
0
;
i
--
)
{
delay
(
1000
);
Serial
.
(
"..."
);
Serial
.
(
i
);
}
Serial
.
println
();
// Write a message to the tag
NdefMessage
message
=
NdefMessage
();
message
.
addUriRecord
(
"http://oreilly.com"
);
bool
success
=
nfc
.
write
(
message
);
if
(
!
success
)
Serial
.
println
(
"Write failed."
);
else
Serial
.
println
(
"Success."
);
}
delay
(
500
);
}
To sense rotary motion you can use a rotary encoder that is attached to the object you want to track. Connect the encoder as shown in Figure 6-16:
/*
* Read a rotary encoder
* This simple version polls the encoder pins
* The position is displayed on the Serial Monitor
*/
const
int
encoderPinA
=
3
;
const
int
encoderPinB
=
2
;
const
int
encoderStepsPerRevolution
=
16
;
int
angle
=
0
;
int
encoderPos
=
0
;
bool
encoderALast
=
LOW
;
// remembers the previous pin state
void
setup
()
{
Serial
.
begin
(
9600
);
pinMode
(
encoderPinA
,
INPUT_PULLUP
);
pinMode
(
encoderPinB
,
INPUT_PULLUP
);
}
void
loop
()
{
bool
encoderA
=
digitalRead
(
encoderPinA
);
if
((
encoderALast
==
HIGH
)
&&
(
encoderA
==
LOW
))
{
if
(
digitalRead
(
encoderPinB
)
==
LOW
)
{
encoderPos
--
;
}
else
{
encoderPos
++
;
}
angle
=
(
encoderPos
%
encoderStepsPerRevolution
)
*
360
/
encoderStepsPerRevolution
;
Serial
.
(
encoderPos
);
Serial
.
(
" "
);
Serial
.
println
(
angle
);
}
encoderALast
=
encoderA
;
}
A rotary encoder produces two signals as it is turned. Both signals alternate between HIGH
and LOW
as the shaft is turned, but the signals are slightly out of phase with each other. If you detect the point where one of the signals changes from HIGH
to LOW
, the state of the other pin (whether it is HIGH
or LOW
) will tell you which way the shaft is rotating.
So, the first line of code in the loop
function reads one of the encoder pins:
int
encoderA
=
digitalRead
(
encoderPinA
);
Then it checks this value and the previous one to see if the value has just changed to LOW
:
if
((
encoderALast
==
HIGH
)
&&
(
encoderA
==
LOW
))
If it has not, the code doesn’t execute the following block; it goes to the bottom of loop
, saves the value it has just read in encoderALast
, and goes back around to take a fresh reading.
When the following expression is true
:
if
((
encoderALast
==
HIGH
)
&&
(
encoderA
==
LOW
))
the code reads the other encoder pin and increments or decrements encoderPos
depending on the value returned. It calculates the angle of the shaft (taking 0 to be the point the shaft was at when the code started running). It then sends the values down the serial port so that you can see it in the Serial Monitor.
Encoders come in different resolutions, quoted as steps per revolution. This indicates how many times the signals alternate between HIGH
and LOW
for one revolution of the shaft. Values can vary from 16 to 1,000. The higher values can detect smaller movements, and these encoders cost much more money. The value for the encoder is hardcoded in the code in the following line:
const
int
encoderStepsPerRevolution
=
16
;
If your encoder is different, you need to change that to get the correct angle values.
If you get values out that don’t go up and down, but increase regardless of the direction you turn the encoder, try changing the test to look for a rising edge rather than a falling one. Swap the LOW
and HIGH
values in the line that checks the values so that it looks like this:
if
((
encoderALast
==
LOW
)
&&
(
encoderA
==
HIGH
))
Rotary encoders just produce an increment/decrement signal; they cannot directly tell you the shaft angle. The code calculates this, but it will be relative to the start position each time the code runs. The code monitors the pins by polling (continuously checking the value of) them. There is no guarantee that the pins have not changed a few times since the last time the code looked, so if the code does lots of other things as well, and the encoder is turned very quickly, it is possible that some of the steps will be missed. For high-resolution encoders this is more likely, as they will send signals much more often as they are turned.
To work out the speed, you need to count how many steps are registered in one direction in a set time.
The circuit is the same as the one for Recipe 6.11. We will use a library that is optimized for reading rotary encoders. It uses Arduino’s interrupt capabilities (Recipe 18.2) to respond quickly to changes in the pin states. Use the Library Manager to install the Encoder library by Paul Stoffregen (see Recipe 16.2), and run the following sketch:
/* Rotary Encoder library sketch
* Read the rotary encoder with a library that uses interrupts
* to process the encoder's activity
*/
#include <Encoder.h>
Encoder
myEnc
(
2
,
3
);
// On MKR boards, use pins 6, 7
void
setup
()
{
Serial
.
begin
(
9600
);
}
long
lastPosition
=
-
999
;
void
loop
()
{
long
currentPosition
=
myEnc
.
read
();
if
(
currentPosition
!=
lastPosition
)
{
// If the position changed
lastPosition
=
currentPosition
;
// Save the last position
Serial
.
println
(
currentPosition
);
// print it to the Serial monitor
}
}
With the Solution from Recipe 6.11, as your code has more things to do, the encoder pins will be checked less often. If the pins go through a whole step change before getting read, the Arduino will simply not detect that step. Moving the shaft quickly will cause more errors, as the steps will be happening more quickly.
To make sure the code responds every time a step happens, you need to use interrupts. When the interrupt condition happens (such as a pin changing state), the code jumps from wherever it is, handles the interrupt, and then returns to where it was and carries on. The Encoder library will perform best with pins that support hardware interrupts, but it will do its best with pins that do not.
On the Arduino Uno and for other boards based on the ATmega328, only two pins can be used as interrupts: pins 2 and 3. See this list of which pins are supported on specific boards. You declare and initialize a rotary encoder with the following line of code:
Encoder
myEnc
(
2
,
3
);
The parameters to the Encoder
initialization are the two pins the encoder is attached to. If you find that the encoder value is decreasing when you expect it to increase, you can swap the arguments or swap your wiring. Once you’ve initialized an encoder, whenever you spin the encoder it will interrupt the sketch briefly to keep track of the movement. You can read the value at any time with myEnc.read()
.
You can create as many encoders as you have pins, but whenever possible, use pins that support interrupts. The following sketch will handle two encoders, and will work optimally on a board that can handle interrupts on the selected pins such as the SAMD21-based M0 boards (Adafruit Metro M0, SparkFun RedBoard Turbo, and Arduino Zero). If you are using a different board, you may need to use different pins. The Uno and other ATmega328-based boards only support interrupts on pins 2 and 3, so the quality of readings will be diminished on the second encoder no matter which pins you choose with one of those boards:
#include <Encoder.h>
Encoder
myEncA
(
2
,
3
);
// MKR boards use pins 4, 5
Encoder
myEncB
(
6
,
7
);
// Mega boards use pins 18, 19
void
setup
()
{
Serial
.
begin
(
9600
);
while
(
!
Serial
);
}
long
lastA
=
-
999
;
long
lastB
=
-
999
;
void
loop
()
{
long
currentA
=
myEncA
.
read
();
long
currentB
=
myEncB
.
read
();
if
(
currentA
!=
lastA
||
currentB
!=
lastB
)
{
// If either position changed
lastA
=
currentA
;
// Save both positions
lastB
=
currentB
;
// Print the positions to the Serial Monitor (or Serial Plotter)
Serial
.
(
"A:"
);
Serial
.
(
currentA
);
Serial
.
(
" "
);
Serial
.
(
"B:"
);
Serial
.
println
(
currentB
);
}
}
The Arduino MKR Vidor 4000 includes an FPGA that is capable of reading a rotary encoder with much more accuracy than with an Arduino alone.
This solution uses LEDs to indicate mouse movement. The brightness of the LEDs changes in response to mouse movement in the x (left and right) and y (nearer and farther) directions. Clicking the mouse buttons sets the current position as the reference point (Figure 6-17 shows the connections).
To use this sketch, you will need to install the PS/2 library. As of this writing, you will need to use a text editor to open the ps2.h file in the ps2 directory and change #include "WProgram.h"
to #include "Arduino.h"
:
/*
Mouse
an arduino sketch using ps2 mouse library
from http://www.arduino.cc/playground/ComponentLib/Ps2mouse
*/
#include <ps2.h>
const
int
dataPin
=
5
;
const
int
clockPin
=
6
;
const
int
xLedPin
=
9
;
// Use pin 8 on the MKR boards
const
int
yLedPin
=
10
;
const
int
mouseRange
=
255
;
// the maximum range of x/y values
char
x
;
// values read from the mouse
char
y
;
byte
status
;
int
xPosition
=
0
;
// values incremented and decremented when mouse moves
int
yPosition
=
0
;
int
xBrightness
=
128
;
// values increased and decreased based on mouse position
int
yBrightness
=
128
;
const
byte
REQUEST_DATA
=
0xeb
;
// command to get data from the mouse
PS2
mouse
(
clockPin
,
dataPin
);
// Declare the mouse object
void
setup
()
{
mouseBegin
();
// Initialize the mouse
}
void
loop
()
{
// get a reading from the mouse
mouse
.
write
(
REQUEST_DATA
);
// ask the mouse for data
mouse
.
read
();
// ignore ack
status
=
mouse
.
read
();
// read the mouse buttons
if
(
status
&
1
)
// this bit is set if the left mouse btn pressed
xPosition
=
0
;
// center the mouse x position
if
(
status
&
2
)
// this bit is set if the right mouse btn pressed
yPosition
=
0
;
// center the mouse y position
x
=
mouse
.
read
();
y
=
mouse
.
read
();
if
(
x
!=
0
||
y
!=
0
)
{
// here if there is mouse movement
xPosition
=
xPosition
+
x
;
// accumulate the position
xPosition
=
constrain
(
xPosition
,
-
mouseRange
,
mouseRange
);
xBrightness
=
map
(
xPosition
,
-
mouseRange
,
mouseRange
,
0
,
255
);
analogWrite
(
xLedPin
,
xBrightness
);
yPosition
=
constrain
(
yPosition
+
y
,
-
mouseRange
,
mouseRange
);
yBrightness
=
map
(
yPosition
,
-
mouseRange
,
mouseRange
,
0
,
255
);
analogWrite
(
yLedPin
,
yBrightness
);
}
}
void
mouseBegin
()
{
// reset and initialize the mouse
mouse
.
write
(
0xff
);
// reset
delayMicroseconds
(
100
);
mouse
.
read
();
// ack byte
mouse
.
read
();
// blank
mouse
.
read
();
// blank
mouse
.
write
(
0xf0
);
// remote mode
mouse
.
read
();
// ack
delayMicroseconds
(
100
);
}
If you are using a 3.3V board, you will either need to add a voltage divider to both the clock and data pins, or you may try powering the mouse from 3.3V instead of 5V (which may or may not work, depending on your mouse). See Recipe 5.11 for a discussion of voltage dividers.
Figure 6-17 shows a female PS/2 connector (the socket you plug the mouse into) from the front. If you don’t have a female connector and don’t mind chopping the end off your mouse, you can note which wires connect to each of these pins and solder to pin headers that plug directly into the correct Arduino pins. A continuity test from a pin to a wire will let you quickly determine which wires go to which pins, but if you are testing the pins from the male plug end that you cut off of your mouse, you need to reverse the diagram left to right.
Connect the mouse signal (clock and data) and power leads to Arduino, as shown in Figure 6-17. This solution only works with PS/2-compatible devices, so you will need to find an older mouse—most mice with the round PS/2 connector should work.
The mouseBegin
function initializes the mouse to respond to requests for movement and button status. The PS/2 library handles the low-level communication. The mouse.write
command is used to instruct the mouse that data will be requested. The first call to mouse.read
gets an acknowledgment (which is ignored in this example). The next call to mouse.read
gets the button status, and the last two mouse.read
calls get the x and y movement that has taken place since the previous request.
The sketch tests to see which bits are HIGH
in the status
value to determine if the left or right mouse button was pressed. The two rightmost bits will be HIGH
when the left and right buttons are pressed, and these are checked in the following lines:
status
=
mouse
.
read
();
// read the mouse buttons
if
(
status
&
1
)
// rightmost bit is set if the left mouse btn pressed
xPosition
=
0
;
// center the mouse x position
if
(
status
&
2
)
// this bit is set if the right mouse btn pressed
yPosition
=
0
;
// center the mouse y position
The x
and y
values read from the mouse represent the movement since the previous request, and these values are accumulated in the variables xPosition
and yPosition
.
The values of x
and y
will be positive if the mouse moves right or away from you, and negative if it moves left or toward you.
The sketch ensures that the accumulated value does not exceed the defined range (mouseRange
) using the constrain
function:
xPosition
=
xPosition
+
x
;
// accumulate the position
xPosition
=
constrain
(
xPosition
,
-
mouseRange
,
mouseRange
);
The yPosition
calculation shows a shorthand way to do the same thing; here the calculation for the y
value is done within the call to constrain
:
yPosition
=
constrain
(
yPosition
+
y
,
-
mouseRange
,
mouseRange
);
The xPosition
and yPosition
variables are reset to zero if the left and right mouse buttons are pressed.
LEDs are illuminated to correspond to position using analogWrite
—half brightness in the center, and increasing and decreasing in brightness as the mouse position increases and decreases. You must use a PWM-capable pin in order for this to work correctly. If your board does not support PWM on pins 9 and 10 (most do), you will see the lights turn on and off instead of dimming. On the MKR family of boards, pin 9 does not support PWM so you need to change the wiring and the code to use a pin that does.
The position can be graphed on the Serial Plotter by adding the following line just after the second call to analogWrite()
:
printValues
();
// show button and x and y values on Serial Monitor/Plotter
You’ll also need to add this line to setup()
:
Serial
.
begin
(
9600
);
Add the following function to the end of the sketch to print or plot the current position of the mouse:
void
printValues
()
{
Serial
.
(
"X:"
);
Serial
.
(
xPosition
);
Serial
.
(
",Y:"
);
Serial
.
(
yPosition
);
Serial
.
println
();
}
The Adafruit site has a suitable PS/2 connector with built-in wires.
A number of Arduino-compatible GPS units are available today. Most use a familiar serial interface to communicate with their host microcontroller using a protocol known as NMEA 0183. This industry standard provides for GPS data to be delivered to listener devices such as Arduino as human-readable ASCII sentences. For example, the following NMEA sentence:
$
GPGLL
,
4916.45
,
N
,
12311.12
,
W
,
225444
,
A
,
*
1
D
describes, among other things, a location on the globe at 49° 16.45’ north latitude by 123° 11.12’ west longitude.
To establish location, your Arduino sketch must parse these strings and convert the relevant text to numeric form. Writing code to manually extract data from NMEA sentences can be tricky and cumbersome in the Arduino’s limited address space, but fortunately there is a useful library that does this work for you: Mikal Hart’s TinyGPS++. Download it from Mikal’s GitHub site and install it. (For instructions on installing third-party libraries, see Recipe 16.2.)
The general strategy for using a GPS is as follows:
Physically connect the GPS device to the Arduino.
Read serial NMEA data from the GPS device.
Process the data to determine location.
Using TinyGPSPlus, you do the following:
Physically connect the GPS device to the Arduino.
Create a TinyGPSPlus object.
Read serial NMEA data from the GPS device.
Process each byte with TinyGPSPlus’s encode()
method.
Periodically query TinyGPSPlus’s get_position()
method to determine location.
The following sketch illustrates how you can acquire data from a GPS attached to Arduino’s serial port. Every five seconds, it blinks the built-in LED once if the device is in the southern hemisphere and twice if it is in the northern hemisphere. If your Arduino’s TX and RX pins are associated with another serial device such as Serial1
, change the definition of GPS_SERIAL
(see Table 4-1):
/* GPS sketch
* Indicate which hemisphere your GPS is in with the built-in LED.
*/
#include "TinyGPS++.h"
// Change this to the serial port your GPS uses (Serial, Serial1, etc.)
#define GPS_SERIAL Serial
TinyGPSPlus
gps
;
// create a TinyGPS++ object
#define HEMISPHERE_PIN LED_BUILTIN
void
setup
()
{
GPS_SERIAL
.
begin
(
9600
);
// GPS devices frequently operate at 9600 baud
pinMode
(
HEMISPHERE_PIN
,
OUTPUT
);
digitalWrite
(
HEMISPHERE_PIN
,
LOW
);
// turn off LED to start
}
void
loop
()
{
while
(
GPS_SERIAL
.
available
())
{
// encode() each byte; if encode() returns "true",
// check for new position.
if
(
gps
.
encode
(
GPS_SERIAL
.
read
()))
{
if
(
gps
.
location
.
isValid
())
{
if
(
gps
.
location
.
lat
()
<
0
)
// Southern Hemisphere?
blink
(
HEMISPHERE_PIN
,
1
);
else
blink
(
HEMISPHERE_PIN
,
2
);
}
else
// panic
blink
(
HEMISPHERE_PIN
,
5
);
delay
(
5000
);
// Wait 5 seconds
}
}
}
void
blink
(
int
pin
,
int
count
)
{
for
(
int
i
=
0
;
i
<
count
;
i
++
)
{
digitalWrite
(
pin
,
HIGH
);
delay
(
250
);
digitalWrite
(
pin
,
LOW
);
delay
(
250
);
}
}
Start serial communications using the rate required by your GPS. See Chapter 4 if you need more information on using Arduino serial communications.
A 9,600-baud connection is established with the GPS. Once bytes begin flowing, they are processed by encode()
, which parses the NMEA data. A true
return from encode()
indicates that TinyGPSPlus has successfully parsed a complete sentence and that fresh position data may be available. This is a good time to check whether the position is valid with a call to gps.location.isValid()
.
TinyGPSPlus’s gps.location.lat()
returns the most recently observed latitude, which this sketch examines; if it is less than zero (that is, south of the equator), the LED blinks once. If it is greater than zero (at or north of the equator), it blinks twice. If the GPS is unable to get a valid fix, it blinks five times.
Attaching a GPS unit to an Arduino is usually as simple as connecting two or three data lines from the GPS to input pins on the Arduino as shown in Table 6-2. If you are using a 5V board such as the Uno, you can use either a 3.3V or 5V GPS module. If you are using a board that is not 5V tolerant, such as a SAMD-based board like the Arduino Zero, Adafruit Metro M0/M4, or SparkFun Redboard Turbo, you must use a 3.3V GPS module.
GPS line | Arduino pin |
---|---|
GND |
GND |
5V or 3.3V |
5V or 3.3V |
RX |
TX (pin 1) |
TX |
RX (pin 0) |
Some GPS modules use RS-232 voltage levels, which are incompatible with Arduino’s TTL logic and will permanently damage the board. If your GPS uses RS-232 levels, then you need some kind of intermediate logic conversion device like the MAX232 integrated circuit.
The code in the Solution assumes that the GPS is connected directly to Arduino’s built-in serial pins. On an ATmega328-based board like the Arduino Uno, this is not usually the most convenient design because RX and TX (pins 0 and 1) are shared with the USB serial connection. In many projects, you’ll use the hardware serial port to communicate with a host PC or other peripheral, which means that port cannot be used by the GPS. In cases like this, select another pair of digital pins and use a serial port emulation (“soft serial”) library to talk to the GPS instead.
With the Arduino and GPS powered down, move the GPS’s TX line to Arduino pin 2 and RX line to pin 3 to free up the hardware serial port for debugging (see Figure 4-8). With the USB cable connected to the host PC, try the following sketch to get a detailed glimpse of TinyGPS in action through the Arduino’s Serial Monitor:
/* GPS sketch with logging
*/
#include "TinyGPS++.h"
// Delete the next four lines if your board has a separate hardware serial port
#include "SoftwareSerial.h"
#define GPS_RX_PIN 2
#define GPS_TX_PIN 3
SoftwareSerial
softserial
(
GPS_RX_PIN
,
GPS_TX_PIN
);
// create soft serial object
// If your board has a separate hardware serial port,
// change "softserial" to that port
#define GPS_SERIAL softserial
TinyGPSPlus
gps
;
// create a TinyGPSPlus object
void
setup
()
{
Serial
.
begin
(
9600
);
// for debugging
GPS_SERIAL
.
begin
(
9600
);
// Use Soft Serial object to talk to GPS
}
void
loop
()
{
while
(
GPS_SERIAL
.
available
())
{
int
c
=
GPS_SERIAL
.
read
();
Serial
.
write
(
c
);
// display NMEA data for debug
// Send each byte to encode()
// Check for new position if encode() returns "True"
if
(
gps
.
encode
(
c
))
{
Serial
.
println
();
float
lat
=
gps
.
location
.
lat
();
float
lng
=
gps
.
location
.
lng
();
unsigned
long
fix_age
=
gps
.
date
.
age
();
if
(
!
gps
.
location
.
isValid
())
Serial
.
println
(
"Invalid fix"
);
else
if
(
fix_age
>
2000
)
Serial
.
println
(
"Stale fix"
);
else
Serial
.
println
(
"Valid fix"
);
Serial
.
(
"Lat: "
);
Serial
.
(
lat
);
Serial
.
(
" Lon: "
);
Serial
.
println
(
lng
);
}
}
}
For a more detailed discussion on software serial, see Recipes 4.11 and 4.12.
Note that you can use a different baud rate for connection to the Serial Monitor and the GPS.
This new sketch behaves the same as the earlier example (but for brevity, omits the LED blinking code) but is much easier to debug. At any time, you can connect a serial LCD (see Recipe 4.11) to the built-in serial port to watch the NMEA sentences and TinyGPSPlus data scrolling by. You could also connect to the serial port using Arduino’s Serial Monitor.
When power is turned on, a GPS unit begins transmitting NMEA sentences. However, the sentences containing valid location data are only transmitted after the GPS establishes a fix, which requires the GPS antenna to have visibility of the sky and can take up to two minutes or more. Stormy weather or the presence of buildings or other obstacles may also interfere with the GPS’s ability to pinpoint location. So, how does the sketch know whether TinyGPSPlus is delivering valid position data? The answer lies in the return value from the gps.location.isValid()
function. A false value means TinyGPS has not yet parsed any valid sentences containing position data. In this case, you’ll know that the returned latitude and longitude are invalid as well.
You can also check how old the fix is. The gps.date.age()
function returns the number of milliseconds since the last fix. The sketch stores its value in fix_age
. Under normal operation, you can expect to see quite low values for fix_age
. Modern GPS devices are capable of reporting position data as frequently as one to five times per second or more, so a fix_age
in excess of 2,000 ms or so suggests that there may be a problem. Perhaps the GPS is traveling through a tunnel or a wiring flaw is corrupting the NMEA data stream, invalidating the checksum (a calculation to check that the data is not corrupted). In any case, a large fix_age
indicates that the coordinates returned by get_position()
are stale.
For a deeper understanding of the NMEA protocol, read the Wikipedia articles.
Several shops sell GPS modules that interface well with TinyGPS and Arduino. These differ mostly in power consumption, voltage, accuracy, physical interface, and whether they support serial NMEA. Adafruit sells a variety of modules as does SparkFun.
GPS technology has inspired lots of creative Arduino projects. A very popular example is the GPS data logger, in which a moving device records location data at regular intervals to the Arduino EEPROM or other onboard storage. See the https://oreil.ly/w0asL for an example. Adafruit makes a popular GPS data logging shield.
Other interesting GPS projects include hobby airplanes and helicopters that maneuver themselves to preprogrammed destinations under Arduino software control. Mikal Hart has built a GPS-enabled “treasure chest” with an internal latch that cannot be opened until the box is physically moved to a certain location. See his post about this project.
Gyroscopes provide an output related to rotation rate (as opposed to an accelerometer, which indicates rate of change of velocity). In the early days of Arduino, most low-cost gyroscopes used an analog voltage proportional to rotation rate. Now, with the ubiquitous use of gyroscopes and accelerometers in smartphones, it is cheaper and easier to find gyroscopes and accelerometers combined using the I2C protocol. See Chapter 13 for more on using I2C.
The Arduino Nano 33 BLE Sense board has a gyroscope and accelerometer built onto the board. See Recipe 6.1 for more information.
The MPU-9250 inertial measurement unit is a relatively inexpensive nine degrees of freedom (9DOF) sensor that works well with Arduino. It is available on a breakout board from many suppliers, including SparkFun (part number SEN-13762). There are several libraries available that support the MPU-9250. The following sketch uses the Bolder Flight Systems MPU9250 library that you can install using the Arduino Library Manager. (For instructions on installing third-party libraries, see Recipe 16.2.) Connect the sensor as shown in Figure 6-18:
/* Gyro sketch
* Read a gyro and display rotation in degrees/sec
*/
#include "MPU9250.h"
// I2C address of IMU. If this doesn't work, try 0x69.
#define IMU_ADDRESS 0x68
MPU9250
IMU
(
Wire
,
IMU_ADDRESS
);
// Declare the IMU object
void
setup
()
{
Serial
.
begin
(
9600
);
while
(
!
Serial
);
// Initialize the IMU
int
status
=
IMU
.
begin
();
if
(
status
<
0
)
{
Serial
.
println
(
"Could not initialize the IMU."
);
Serial
.
(
"Error value: "
);
Serial
.
println
(
status
);
while
(
1
);
// halt the sketch
}
// Set the full range of the gyro to +/- 500 degrees/sec
status
=
IMU
.
setGyroRange
(
MPU9250
::
GYRO_RANGE_500DPS
);
if
(
status
<
0
)
{
Serial
.
println
(
"Could not change gyro range."
);
Serial
.
(
"Error value: "
);
Serial
.
println
(
status
);
}
}
void
loop
()
{
IMU
.
readSensor
();
// Obtain the rotational velocity in rads/second
float
gx
=
IMU
.
getGyroX_rads
();
float
gy
=
IMU
.
getGyroY_rads
();
float
gz
=
IMU
.
getGyroZ_rads
();
// Display velocity in degrees/sec
Serial
.
(
"gx:"
);
Serial
.
(
gx
*
RAD_TO_DEG
,
4
);
Serial
.
(
",gy:"
);
Serial
.
(
gy
*
RAD_TO_DEG
,
4
);
Serial
.
(
",gz:"
);
Serial
.
(
gz
*
RAD_TO_DEG
,
4
);
Serial
.
println
();
delay
(
100
);
}
The MPU-9250 is a 3.3V I2C device, so if you are not using a 3.3V Arduino board you will need a logic-level converter to protect the gyro’s SCL and SDA pins. See the introduction to Chapter 13 for more on I2C and using 3.3V devices.
The sketch starts out by including the MPU9250 library and declaring an object to represent the IMU. Within setup()
, it attempts to initialize the IMU. If this fails, you may need to change the IMU_ADDRESS
definition to 0x69 or check your wiring. After the IMU is initialized, the sketch changes the gyro’s full range to +/– 500 degrees per second.
Within loop
, the sketch reads the sensor and obtains the rotational velocity in radians per second. It then uses the RAD_TO_DEG
Arduino constant to convert this to degrees per second. The output of the sketch is readable in either the Serial Monitor or Serial Plotter.
See Chapter 13 for more about I2C.
See “Using 3.3-Volt Devices with 5-Volt Boards” for more about connecting 3.3V devices to 5V boards.
Try the SparkFun tutorial for the MPU-9250. This tutorial uses a different library, but the concepts are the same.
This recipe uses the magnetometer in the MPU-9250 nine degrees of freedom (9DOF) inertial measurement unit (IMU) from Recipe 6.15. Connect the sensor as shown in Figure 6-18. Each of the MPU-9250’s three primary sensors (gyro, magnetometer, and accelerometer) read values in three dimensions (x, y, z), which is where the nine degrees of freedom come from:
Before you use the magnetometer, you must calibrate it. You can find a calibration sketch in this GitHub issue. That sketch will store the calibration values in your microcontroller board’s nonvolatile EEPROM memory. You will need to load the calibration values any time you want to work with the magnetometer, as shown in the next sketch. If you use the sensor with a different microcontroller board, you’ll need to run the calibration sketch again. Also, if you store anything else in the EEPROM, you’ll need to make sure you don’t store it in the same location as the calibration values.
/* Magnetometer sketch
Read a magnetometer and display magnetic field strengths
*/
#include "MPU9250.h"
#include <math.h>
#include "EEPROM.h"
// I2C address of IMU. If this doesn't work, try 0x69.
#define IMU_ADDRESS 0x68
// Change this to the declination for your location.
// See https://www.ngdc.noaa.gov/geomag/calculators/magcalc.shtml
#define DECLINATION (-14)
MPU9250
IMU
(
Wire
,
IMU_ADDRESS
);
// Declare the IMU object
void
setup
()
{
int
status
;
Serial
.
begin
(
9600
);
while
(
!
Serial
);
// Initialize the IMU
status
=
IMU
.
begin
();
if
(
status
<
0
)
{
Serial
.
println
(
"Could not initialize the IMU."
);
Serial
.
(
"Error value: "
);
Serial
.
println
(
status
);
while
(
1
);
// halt the sketch
}
load_calibration
();
}
void
loop
()
{
IMU
.
readSensor
();
// Obtain the magnetometer values across each axis in units of microTesla
float
mx
=
IMU
.
getMagX_uT
();
float
my
=
IMU
.
getMagY_uT
();
float
mz
=
IMU
.
getMagZ_uT
();
// From https://github.com/bolderflight/MPU9250/issues/33
// Normalize the magnetometer data.
float
m
=
sqrtf
(
mx
*
mx
+
my
*
my
+
mz
*
mz
);
mx
/=
m
;
my
/=
m
;
mz
/=
m
;
// Display the magnetometer values
Serial
.
(
"mx:"
);
Serial
.
(
mx
,
4
);
Serial
.
(
",my:"
);
Serial
.
(
my
,
4
);
Serial
.
(
",mz:"
);
Serial
.
(
mz
,
4
);
Serial
.
println
();
float
constrained
=
constrainAngle360
(
atan2f
(
-
my
,
mx
)
+
(
DECLINATION
*
DEG_TO_RAD
));
float
calcAngle
=
constrained
*
RAD_TO_DEG
;
Serial
.
(
calcAngle
);
Serial
.
println
(
" degrees"
);
delay
(
100
);
}
// From https://github.com/bolderflight/MPU9250/issues/33
float
constrainAngle360
(
float
dta
)
{
dta
=
fmod
(
dta
,
2.0
*
PI
);
if
(
dta
<
0.0
)
dta
+=
2.0
*
PI
;
return
dta
;
}
// Load the calibration from the eeprom
// From https://github.com/bolderflight/MPU9250/issues/33
void
load_calibration
()
{
float
hxb
,
hxs
,
hyb
,
hys
,
hzb
,
hzs
;
uint8_t
eeprom_buffer
[
24
];
for
(
unsigned
int
i
=
0
;
i
<
sizeof
(
eeprom_buffer
);
i
++
)
{
eeprom_buffer
[
i
]
=
EEPROM
.
read
(
i
);
}
memcpy
(
&
hxb
,
eeprom_buffer
,
sizeof
(
hxb
));
memcpy
(
&
hyb
,
eeprom_buffer
+
4
,
sizeof
(
hyb
));
memcpy
(
&
hzb
,
eeprom_buffer
+
8
,
sizeof
(
hzb
));
memcpy
(
&
hxs
,
eeprom_buffer
+
12
,
sizeof
(
hxs
));
memcpy
(
&
hys
,
eeprom_buffer
+
16
,
sizeof
(
hys
));
memcpy
(
&
hzs
,
eeprom_buffer
+
20
,
sizeof
(
hzs
));
IMU
.
setMagCalX
(
hxb
,
hxs
);
IMU
.
setMagCalY
(
hyb
,
hys
);
IMU
.
setMagCalZ
(
hzb
,
hzs
);
}
If you want to use the IMU with a 5-volt Arduino board, see “Using 3.3-Volt Devices with 5-Volt Boards” for details on how to use a logic-level converter.
The compass module provides magnetic field intensities on three axes (x, y, and z). These values vary as the compass orientation is changed with respect to the Earth’s magnetic field (magnetic north).
As with the sketch shown in Recipe 6.15, this sketch configures and initializes the IMU, but instead of showing gyro data, it reads magnetometer readings in units of microTesla and converts them to a compass bearing. (Another big difference is that it loads the calibration data from the EEPROM.) For this sketch to work properly, the IMU must be on a level surface. You must also set the declination for your geographic location by changing the value of DECLINATION
at the top of the sketch (use a negative number for a west declination, positive for east). For more, refer to the NGDC declination lookup tool.
The magnetometer readings are normalized by then dividing each reading by the square root of the sum of the squares (RSS) of all of the readings. The angle to magnetic north is calculated by adding the declination (in radians) to the following formula: radians = arctan2(–my, mx)
, constrained to 360 degrees (2 * pi radians) by the constrainAngle360
function. That result is converted to degrees by multiplying it by the RAD_TO_DEG
constant. Zero degrees indicates magnetic north.
To make a servo follow the compass direction over the first 180 degrees, use the techniques shown in “Servos”, but use calcAngle
to move the servo as shown:
angle
=
constrain
(
calcAngle
,
0
,
180
);
myservo
.
write
(
calcAngle
);
This recipe uses the accelerometer in the MPU-9250 nine degrees of freedom (9DOF) inertial measurement unit (IMU) from Recipe 6.15. Connect the sensor as shown in Figure 6-18.
If you want to use the IMU with a 5-volt Arduino board, see “Using 3.3-Volt Devices with 5-Volt Boards” for details on how to use a logic-level converter.
The simple sketch here uses the MPU-9250 to display the acceleration in the x-, y-, and z-axes:
/* Accelerometer sketch
* Read an accelerometer and display acceleration in m/s/s
*/
#include "MPU9250.h"
// I2C address of IMU. If this doesn't work, try 0x69.
#define IMU_ADDRESS 0x68
MPU9250
IMU
(
Wire
,
IMU_ADDRESS
);
// Declare the IMU object
void
setup
()
{
Serial
.
begin
(
9600
);
while
(
!
Serial
);
// Initialize the IMU
int
status
=
IMU
.
begin
();
if
(
status
<
0
)
{
Serial
.
println
(
"Could not initialize the IMU."
);
Serial
.
(
"Error value: "
);
Serial
.
println
(
status
);
while
(
1
);
// halt the sketch
}
}
void
loop
()
{
IMU
.
readSensor
();
// Obtain the rotational velocity in rads/second
float
ax
=
IMU
.
getAccelX_mss
();
float
ay
=
IMU
.
getAccelY_mss
();
float
az
=
IMU
.
getAccelZ_mss
();
// Display velocity in degrees/sec
Serial
.
(
"ax:"
);
Serial
.
(
ax
,
4
);
Serial
.
(
",ay:"
);
Serial
.
(
ay
,
4
);
Serial
.
(
",az:"
);
Serial
.
(
az
,
4
);
Serial
.
println
();
delay
(
100
);
}
This sketch is similar to the gyro sketch from Recipe 6.15, except that it displays acceleration along each axis in meters per second squared (m/s/s). Even when stationary, you’ll notice that the z acceleration hovers around –9.8 m/s/s. At least that’s what you’ll see if you’re running this sketch on Earth, where gravity is roughly 9.8 m/s/s. If you see a value of 0 along the z-axis, then the sensor is in free fall. The force that causes the 9.8 m/s/s acceleration is the mechanical force of whatever is keeping the sensor from falling (your hand, a table, the floor). Although the object appears to have no acceleration from your viewpoint, it is accelerating relative to free fall, which is the condition that would apply if there was nothing (no floor, no table, no hand) between your sensor and the center of the Earth. If there was nothing between your sensor and the center of the Earth, that would be a somewhat unusual and certainly undesirable configuration of Earth’s mass, at least from the viewpoint of Earth’s life forms.
You can use techniques from the previous recipes to extract information from the accelerometer readings. You might need to check for a threshold to work out movement (see Recipe 6.7 for an example of threshold detection). You may find it useful to apply a moving average formula to the incoming data.
If the accelerometer is reading horizontally, you can use the values directly to work out movement. If it is reading vertically, you will need to take into account the effects of gravity on the values. This is similar to the DC offset in Recipe 6.8, but it can be complicated, as the accelerometer may be changing orientation so that the effect of gravity is not a constant value for each reading.
The data produced by accelerometers can be difficult to work with, particularly trying to make decisions about movement over time—detecting gestures, not just positions. Machine learning techniques are starting to be used to process live sensor data and recognize how they relate to example sets of data produced before. These approaches currently need to run on a computer, and are still quite fiddly to set up, but can produce very useful results.
An excellent example that is integrated with Arduino boards is the Example-based Sensor Prediction system by David Mellis, built on top of the Gesture Recognition Toolkit.
Also worth looking at is Wekinator.
SparkFun’s advanced library for the MPU-9250 includes pedometer, tap, and orientation direction. It requires a SAMD-based Arduino or Arduino compatible.