Multiple Sensor Data via Serial
All of the sensing recipes published demonstrate the basic connections needed to get them working with a microcontroller.
There are situations where you might need more than one sensor in your project. In addition, you may want to poll these sensors in quick succession in order to derive a more complete observation of the physical phenomena.
Depending on the types of sensors you are using, there will be different techniques to do so. In order of increasing complexity, they are:
- Analogue sensing on additional analogue input pins
- I2C sensors daisy-chained on the I2C bus
- SPI sensors similarly daisy-chained (we won’t cover SPI here)
There are two parts to this puzzle. On the hardware side, you will need to know how to wire up multiple sensors, and this will depend on the three categories of sensors mentioned above.
On the software side, you will have to write code that can sequentially poll both sensors, and send consolidated data by carefully re-constituting sensed values into a merged ‘string’ – read on below.
1. Analogue sensing on additional analogue input pins
The most straightforward approach – circuit-wise, build the relevant analogue sensing circuits, with each sensing ‘point’ going into an Analogue Input pin of the microcontroller.
Code-wise, place additional analogRead()
commands to the pins you wish to read, and if you have to report these readings back to a host computer via Serial, print out the readings as a consolidated set of values. Refer to the bottom CODE
section for examples on how to do this.
2. I2C ‘daisy-chained’ sensors
The I2C standard allows up to 127 devices to be on the same Data and Clock line (i.e. ‘bus’).
I2C sensors can be interconnected on the same bus by simply connecting their Data
and Clock
lines together, along with the prerequisite power and GND lines to provide the sensors with power. Careful here: I2C sensors today can run at either 3.3V or 5V. Most manufacturers of sensor boards add 5V ‘compatibility’, but not all of them do.
The wiring diagram below shows an example of how we can ‘daisy-chain’ multiple I2C sensors:
For I2C sensors, there are two more things that need to be verified on the physical circuit:
A. I2C Pull-up Resistors
In order for the signal to be reliable, I2C sensor boards have the option to enable/disable the pull-up resistors. There should only be one set of pull-up resistors on the entire I2C Bus. This is only a concern if you have more than one I2C device on the same bus. That said, if you are using just two I2C devices, you can sometimes get away with this additional work (just don’t share this with engineers while you’re prototyping, shhh…)
An example can be seen on the underside of these sensors:
B. I2C Device Addresses
All I2C devices are assigned a fixed address from the manufacturer. For example, the VEML6030 ambient light sensor will have
0x48
as its default address. If you add another same sensor to the bus, there will be an address conflict as there can only be one unique address per sensor.With two different sensors from different manufacturer/models, this isn’t a problem. However, if you need to use two identical sensors, you need to assign each with its own address.
Most I2C sensors today offer two possible addresses. Looking at the image above, you will notice the
ASW ON 0x10
andASW OFF 0x48
labels. A tiny switch on the front side of this board allows you to set the address to avoid the duplicate conflict.This does suggest that one can only use up to two of the identical sensor on an I2C bus. Not true! There are I2C multiplexers that will resolve this issue (but is beyond the scope of this article). Check out this tutorial from Adafruit to learn more about such multiplexers.
Collating Serial Data for Multiple Sensors
A common strategy is to ‘pack’ all sensor readings into a single line that gets sent out on the Serial Port. This creates a tidy ‘packet’ of data that is predictable and consistent. This is critical when sending the sensor data out of the microcontroller.
For example, if we have two ambient light sensors, we might send the data out in the following format:
234,541
The comma ,
is used as the ‘delimiter’ to separate both readings, and any upstream software can easily perform the necessary string splits to retrieve each sensor’s data. The pipe character |
is also a commonly-used delimiter.
Going pro or pro-am?
This method of sending literal strings of numbers might be seen as inefficient. The above example takes up 7 bytes to transmit! By using byte representation of values we can eliminate the need for delimiters and scrunch down the data ‘payload’ down to 4 bytes here (using two 16-bit integers).
Consequently, if we have say FOUR sensors, we can collate the readings this way:
234,541,113,412
…and again it will be up to the upstream software to ‘decode’ this set of values for later usage. If you are using Google Sheets, for example, running the SPLIT
function on the data can be done to separate them into individual column cells for further processing:
=SPLIT(A1,",")
Code
Depending on your setup, click on the tabs below to view the code can be written. We are using the Arduino IDE’s Serial output as a demonstration here:
-
If you are using only analogue sensors, the code to push out simultaneous readings is trivial:
1 2 3 4 5 6 7 8 9 10 11
... void loop() { // just a basic example – pay more attention to the Serial output int sensorA = analogRead(A0); int sensorB = analogRead(A1); Serial.print(sensorA); // these lines are Serial.print() Serial.print(","); // " Serial.println(sensorB); // the final line is Serial.println() } ...
-
For up to TWO IDENTICAL I2C sensors, you will need to initialise each identical sensor with its own unique address, and follow the breadboard diagram above (‘daisy-chained sensors’) to set the hardware switches properly. Let’s use the VEML6030 code as an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
/* TWO ambient light (lux) level sensors This excerpt is to illustrate the use of TWO identical I2C sensors and the code needed to make this work. For the fullly-conmented code for the Ambient light (lux) sensor please refer to the sensor recipe. */ #define AL_ADDR_A 0x10 // one unique address for each identical sensor... check the sensor – in the bottom corner, the tiny 'ASW' switch should be pushed towards the '1' label. #define AL_ADDR_B 0x48 // likewise, the second sensor's ASW switch should be pushed towards the '0' label. #define LUX_MAX 550 #define LUX_MIN 10 #define LED_MINBRIGHT 0 #define LED_MAXBRIGHT 255 float gain = .125; int time = 100; #include <Wire.h> #include "SparkFun_VEML6030_Ambient_Light_Sensor.h" // create TWO sensor objects in code: // (notice how many things below are also 'duplicated') SparkFun_Ambient_Light lightA(AL_ADDR_A); SparkFun_Ambient_Light lightA(AL_ADDR_B); long luxVal = 0; void setup() { Wire.begin(); Serial.begin(115200); if (lightA.begin()) Serial.println("Ready to sense some lightA!"); else Serial.println("Could not communicate with the sensor A!"); if (lightB.begin()) Serial.println("Ready to sense some lightB!"); else Serial.println("Could not communicate with the sensor B!"); lightA.setGain(gain); lightA.setIntegTime(time); lightB.setGain(gain); lightB.setIntegTime(time); } void loop() { luxValA = lightA.readLight(); luxValB = lightB.readLight(); Serial.print(luxValA); Serial.print(","); Serial.println(luxValB); delay(5); // intentionally add a 5ms delay so as not to 'spam' the Serial port unnecessarily } /* Please note that the code provided here is licensed under the MIT license. The MIT License (MIT) Copyright © 2024 Chuan Khoo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
-
For up to TWO different I2C sensors, you still need to ensure their addresses are unique, and will need to import the relevant code libraries for both sensors. Let’s use the VEML6030 (ambient light) + VL53L1X (distance) code as an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
/* Ambient light (lux) + Distance sensors This excerpt is to illustrate the use of TWO different I2C sensors and the code needed to make this work. Refer to the sensor recipes for the fullly-conmented code. */ // lux sensor #define AL_ADDR 0x10 #define LUX_MAX 550 #define LUX_MIN 10 float gain = .125; int time = 100; // distance sensor #define IRQ_PIN 2 #define XSHUT_PIN 3 #define TIMING_BUDGET 50 #include <Wire.h> #include "Adafruit_VL53L1X.h" #include "SparkFun_VEML6030_Ambient_Light_Sensor.h" SparkFun_Ambient_Light light(AL_ADDR); Adafruit_VL53L1X vl53 = Adafruit_VL53L1X(XSHUT_PIN, IRQ_PIN); void setup() { Wire.begin(); Serial.begin(115200); if (light.begin()) Serial.println("Ready to sense some lux!"); else Serial.println("Could not communicate with the lux sensor!"); light.setGain(gain); light.setIntegTime(time); if (!vl53.begin(0x29, &Wire)) { Serial.print("Error initialising VL sensor: "); Serial.println(vl53.vl_status); while (1) delay(10); } if (!vl53.startRanging()) { Serial.print("Couldn't start ranging: "); Serial.println(vl53.vl_status); while (1) delay(10); } vl53.setTimingBudget(TIMING_BUDGET); } void loop() { long luxVal = light.readLight(); int16_t distance; if (vl53.dataReady()) { // new measurement for the taking! distance = vl53.distance(); if (distance == -1 || distance == 0) { return; } Serial.print(luxVal); Serial.print(","); Serial.println(distance); // this is expressed in mm // data is read out, time for another reading! vl53.clearInterrupt(); } delay(5); // intentionally add a 5ms delay so as not to 'spam' the Serial port unnecessarily } /* Please note that the code provided here is licensed under the MIT license. The MIT License (MIT) Copyright © 2024 Chuan Khoo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */