I welcome you to this tutorial. This tutorial is part of my roadtest review about Arduino Nano 33 BLE Sense. My review is split into multiple blog posts. This tutorial does not contain any my thoughts about Arduino, you can find my thoughts about it and some further details about this Arduino and related parts in chapters with name beginning with "Review". Following table of contents links to other parts of my roadtest review.
Table of Contents
- Introduction
- Review of Development Board
- Review of Onboard Sensors
- Review of Microcontroller and BLE Module
- Review of Software
- Review of Documentation
- Tutorial 01: Accessing Sensor Values (this article)
- Tutorial 02: nRF52840 Application without Arduino IDE
Tutorial 01: Accessing Sensor Values
In this tutorial I will show how to read values from all sensors presented on Arduino Nano 33 BLE Sense. I will show how to read:
- Temperature from HTS221HTS221 sensor
- Humidity from HTS221HTS221 sensor
- Barometric pressure from LPS22HB sensor
- Gyroscope data from LSM9DS1 sensor
- Acceleration data from LSM9DS1 sensor
- Magnetometer data from LSM9DS1 sensor
- Proximity from APDS9960 sensor
- Detected colour from APDS9960 sensor
- Detected gesture from APDS9960 sensor
- Microphone signal from MP34DT05 PDM microphone
Tutorial expects that you have installed Arduino IDE from https://www.arduino.cc/en/software. Download will forward you to donate page, but you do not need to donate any money – just click “Just download”. Installation is easy and seamless. In this tutorial I will use stable version of Arduino IDE but you can use Arduino 2.0 beta if you want.
Adding Support for Arduino Nano 33 BLE Sense to Arduino IDE
Arduino IDE does not know anything about Arduino Nano 33 BLE Sense by default and you must add support for this board. Go to Tools > Board > Board Manager.
Search for “nrf”, select “Arduino nRF528x Boards (Mbed OS)” and click Install.
Wait until installation complete and then close the Board Manager. Now we must install libraries for accessing sensors. Go to Sketch > Include Library > Manage Libraries…
Search for HTS221HTS221 and install Arduino_HTS221 library.
Search for LPS22HB and install Arduino_LPS22HB library.
Search for LSM9DS1 and install Arduino_LSM9DS1 library.
Search APDS9960 for and install Arduino_ APDS9960 library.
You have installed all required libraries for now and we can write code. At first, we must include libraries for accessing sensor values.
#include <Arduino_APDS9960.h> #include <Arduino_HTS221.h> #include <Arduino_LPS22HB.h> #include <Arduino_LSM9DS1.h> #include <PDM.h>
At beginning of setup() function we must initialize Serial for serial communication with computer. Serial bus is emulated over USB on this Arduino. You do not need worry about baudrate because USB emulated port has no baudrate and throughput is only limited by USB bus (bitrate on this bus is 12 Mib/s which is 1.5 MiB/s but this is only theoretical limit and USB overhead is significant). In fact, Serial library completely ignores this parameter on this Arduino, and it will work even you specify some negative crazy value as -12345 for example. Also note that this apply only for main Serial. If you want to use secondary serial for communication over GPIO ports with some sensors, GPS module, Wi-Fi module, and so on, you of course must specify correct baudrate.
Serial.begin(115200); while (!Serial) {}
Reading temperature and humidity from HTS221HTS221 sensor
In setup function, we need to initialize library. Library creates HTS object which have standard begin method. In case of error we will print error message over serial bus and then go to infinite loop because it does not make sense to proceed without working library/sensor.
if (!HTS.begin()) { Serial.println("HTS init failed"); while (1) {} }
In the loop we can read temperature and humidity using readTemperature and readHumidity methods.
float temperature = HTS.readTemperature(); float humidity = HTS.readHumidity(); and finally, we can print measured values over serial. Serial.printf("%+7.2f°C %+7.2f%%\r\n", temperature, humidity);
You have seen one of the good things on Arduino Nano 33 BLE Sense in comparison with AVR Arduinos. Because main MCU is powerful ARM, serial library has printf method which is not present on AVR based Arduinos and this method support formatting you may know from standard C programs. It also supports floats. Formation sequence %f means, that we want print float. %.2f is extension of %f format specifier and means that we want to print number rounded to 2 decimal places, plus sign in sequence means that we always want to print sign (plus or minus) in every number (this is because I want to align numbers in columns. Without plus negative numbers will have one extra character). And finally, 7 before dot means that I want to expend number by spaces to have 7 characters in length. Last sequence %% means that I want to print one %. I must specify double % because % is reserved char.
Reading barometric pressure from LPS22HB
We must initialize library in setup function similarly as we have done in previous example.
if (!BARO.begin()) { Serial.println("BARO init failed"); while (1) {} }
In loop we can read pressure using readPressure function.
float pressure = BARO.readPressure();
And we can print it. Update the line printing temperature and humidity to following line:
Serial.printf("%+7.2f°C %+7.2f%% %+7.2f hPa ", temperature, humidity, pressure);
We will write new line characters later in tutorial.
Reading gyroscope, accelerometer, and magnetometer data from LSM9DS1
Initialization in setup() function is similar to previous initializations:
if (!IMU.begin()) { Serial.println("IMU init failed"); while (1) {} }
Reading values is more complicated because we should check that any data are available. Code is following. It checks for new data for every kind of provided data and in case when they are available, program reads them.
bool accelAvailable; float accelX = -1; float accelY = -1; float accelZ = -1; if ((accelAvailable = IMU.accelerationAvailable())) { IMU.readAcceleration(accelX, accelY, accelZ); } bool gyroAvailable; float gyroX = -1; float gyroY = -1; float gyroZ = -1; if ((gyroAvailable = IMU.gyroscopeAvailable())) { IMU.readGyroscope(gyroX, gyroY, gyroZ); } bool magAvailable; float magX = -1; float magY = -1; float magZ = -1; if ((magAvailable = IMU.magneticFieldAvailable())) { IMU.readMagneticField(magX, magY, magZ); }
Printing is also complicated because we must handle state, that data are not available for some fields. I will print exact number of spaces to fill fixed space in text which is sent over serial.
if (accelAvailable) { Serial.printf("accel(%+5.2f, %+5.2f, %+5.2f) ", accelX, accelY, accelZ); } else { Serial.print(" "); } if (gyroAvailable) { Serial.printf("gyro(%+7.2f, %+7.2f, %+7.2f) ", gyroX, gyroY, gyroZ); } else { Serial.print(" "); } if (magAvailable) { Serial.printf("mag(%+6.2f, %+6.2f, %+6.2f) ", magX, magY, magZ); } else { Serial.print(" "); }
Reading colour, proximity, and gesture from APDS-9960APDS-9960
Initialization is simple:
if (!APDS.begin()) { Serial.println("APDS init failed"); while (1) {} }
Reading is like previous example. We must also check availability of data here. Returned gesture is one of constants. Possible values from readGestures are shown in code printing it later.
bool colorAvailable; int colorR = -1; int colorG = -1; int colorB = -1; if ((colorAvailable = APDS.colorAvailable())) { APDS.readColor(colorR, colorG, colorB); } bool proximityAvailable; int proximity = -1; if ((proximityAvailable = APDS.proximityAvailable())) { proximity = APDS.readProximity(); } bool gestureAvailable; int gesture; if ((gestureAvailable = APDS.gestureAvailable())) { gesture = APDS.readGesture(); } Print of measured data is following: if (colorAvailable) { Serial.printf("clr(%3d, %3d, %3d) ", colorR, colorG, colorB); } else { Serial.print(" "); } if (proximityAvailable) { Serial.printf("%+5d ", proximity); } else { Serial.print(" "); } switch (gesture) { case GESTURE_UP: Serial.print("UP "); break; case GESTURE_DOWN: Serial.print("DOWN "); break; case GESTURE_LEFT: Serial.print("LEFT "); break; case GESTURE_RIGHT: Serial.print("RIGHT "); break; default: break; }
Reading microphone signal from MP34DT06J
In this example I will read maximum and minimum value (amplitude) retrieved from PDM microphone. When you will run this in silent environment you probably see values near zero and in case if you say (or sing) something, values will grow. PDM library (and peripheral) is implemented in way that it continuously reads data from microphone to buffer at constant sampling rate. When buffer is full, library will call passed callback and you can read data from buffer (using PDM.read function). Library do not overwrite data in your buffer after callback get called. It uses two buffers and when it passes one buffer to you, it uses second buffer for reading new data in background. If processing of data in callback takes too long, some data get lost.
At first, we must write callback for processing data. I will read values until I exhaust buffer of retrieved data and compare to minimum and maximum saved in global variable. Lets declare needed global variables.
int16_t minMicrophoneSample; int16_t maxMicrophoneSample; int microphoneSamples; In setup function initialize them to following values: minMicrophoneSample = INT16_MAX; maxMicrophoneSample = INT16_MIN; microphoneSamples = 0;
Now we can write callback for processing received data. I always read 2 bytes because sample is 16-bit value.
void PDM_DataReceivedCallback() { int16_t val; while (PDM.available()) { PDM.read(&val, 2); if (val < minMicrophoneSample) { minMicrophoneSample = val; } if (val > maxMicrophoneSample) { maxMicrophoneSample = val; } microphoneSamples++; } }
Now we can initialize library with created callback and sampling rate 16 kHz (16000 Hz). Board has only one microphone, so we use single channel PDM mode.
PDM.onReceive(PDM_DataReceivedCallback); if (!PDM.begin(1, 16000)) { Serial.println("PDM init failed"); while (1) {} }
In loop function we can read values from global variables directly, but it is not correct way how to do it. Our callback is called from interrupt which can occur anytime. It may occur even between instruction reading min and instruction reading max values. In this case interrupt handler overwrite min and max values and our code get incorrect results because it has read min from previous sampling period and max from current sampling period. To make code correct we must temporarily disable PDM interrupt, read values to safe variable (safe variable is variable which is not affected by callback. Usually this is just normal local variable.), reset our counters, minimum and maximum to default values for next sampling period. After that we can enable interrupt again and then we can print values which we have stored into our safe variables. We will also handle state when no microphone data was received since last loop() iteration. Code is following:
NVIC_DisableIRQ(PDM_IRQn); bool microphoneAvailabile = minMicrophoneSample != INT16_MAX && maxMicrophoneSample != INT16_MIN; int16_t microphoneMinBackup = minMicrophoneSample; int16_t microphoneMaxBackup = maxMicrophoneSample; int microphoneSamplesBackup = microphoneSamples; minMicrophoneSample = INT16_MAX; maxMicrophoneSample = INT16_MIN; microphoneSamples = 0; NVIC_EnableIRQ(PDM_IRQn);
At first line I disable interrupt. At second line I check that any new sample was received since last round. Then I backup variables to new variables for using them later after reenabling interrupt. Lastly before enabling interrupt I reset counter, min and max to default values (the same values which you have set in setup() function). At last line I enable interrupt again. Best practise is to have code between disabling and enabling interrupt as fast as possible. Note that I have not done any complicated things here. I only read and write values to variables and do some basic comparisons. If you look to generated assembler generated code, this is probably implemented by less than 50 instructions. I should do not call any Serial.print here because it is quite slow call (on AVR Arduinos without emulated serial it is even slower!) and in case when print call consume so much time (for example because of long waiting for transmit buffer emption), it may cause data loss.
Finally, outside the critical section we can print values from backup variables. Of course, only in case when there are any values.
if (microphoneAvailabile) { Serial.printf("MIC: %d samples between %d and %d\r\n", microphoneSamplesBackup, microphoneMinBackup, microphoneMaxBackup); } else { Serial.printf("\r\n"); }
At last we can add some delay before next round.
delay(100);
And this is all. You can upload your program using button in menu and you can open serial monitor to view measurements from device.
Full code is attached below this text.