Can I select multiple sensors through a loop in SPI connection, if so how?

So I want to use multiple BME280 sensors for a project and I want it to be scalable. I was previously using a i2c multiplexer where I could add sensors to the multiplexer and have a single sensor object for all the sensors on the multiplexer and just switch the MUX pin through a for loop. You can see the code below.

#include "Wire.h"
#include "SparkFunBME280.h"

#define i2cmux 0x70

BME280 sensor;

#define sensor_number 4

float temp[sensor_number], hum[sensor_number];

const unsigned int DELAY = 2000;
unsigned long prevMillis = 0;

void setup(){

  Wire.begin();
  Serial.begin(9600);

  for (byte i=0; i < sensor_number; i++){
    muxport(i);
    set_sensor();
    delay(5);
  }

  delay(500);

  
}

void loop(){
  if(millis() - prevMillis > DELAY){
    read_sensor();
    print_sensor("Temp", temp, 1);
    print_sensor("Hum", hum, 0); 
    Serial.println();
    prevMillis = millis();
  }
}

void muxport(byte i){ //MUX port selector
  if (i > 7) return;
  Wire.beginTransmission(i2cmux);
  Wire.write(1 << i);
  Wire.endTransmission();  
}

void read_sensor(){ //Reads values from sensors connected to the MUX in order
  for (byte i=0; i < sensor_number; i++){
    muxport(i);
    temp[i] = sensor.readTempC();
    hum[i] = sensor.readFloatHumidity();
  }
}

void print_sensor(String type, float value[], byte decimal){ //Prints values from sensors in order
  Serial.print(" ");
  Serial.print(type);
  Serial.print(": ");
  for (byte i=0; i < sensor_number; i++){
    Serial.print(value[i], decimal);
    Serial.print(" | ");
  }
}

This has worked okay for some time however I have noticed calibration errors between the sensors and when I swap sensor places, the values change randomly. For example when temperature reads were like 24 24 23 25 , and I swap sensors 1 and 3 the expected readings should be 23 24 24 25 but it becomes something like 22 23 24 24 (I have simplified the results, I know they change depending on time however this also happens to the average over time values.) Anyways I have switched to SPI connection and get rid of the multiplexer and the swapping errors decreased significantly and I am getting more stable results from the sensors, but now I have to set each sensor by hand, and read values from then one at a time instead of just looping them.

#include "SparkFunBME280.h"

#define SENSOR_NUMBER 4

BME280 sensor0, sensor1, sensor2, sensor3;

float temp[SENSOR_NUMBER], hum[SENSOR_NUMBER], pres[SENSOR_NUMBER];

const unsigned int DELAY = 2000;
unsigned long prevMillis = 0;

void setup(){

  Serial.begin(9600);
  
  sensor0.beginSPI(10);
  sensor1.beginSPI(9);
  sensor2.beginSPI(8);
  sensor3.beginSPI(7);
  
}

void loop(){
  
  if(millis() - prevMillis > DELAY){
    
    read_sensor();
    
    print_sensor("Temp", temp, 1);
    print_sensor("Hum", hum, 1);
    print_sensor("Pres", pres, 0);
    
    Serial.println();
    
    prevMillis = millis();
    
  }
}

void read_sensor(){
  
  temp[0] = sensor0.readTempC();
  temp[1] = sensor1.readTempC();
  temp[2] = sensor2.readTempC();
  temp[3] = sensor3.readTempC();
  
  hum[0] = sensor0.readFloatHumidity();
  hum[1] = sensor1.readFloatHumidity();
  hum[2] = sensor2.readFloatHumidity();
  hum[3] = sensor3.readFloatHumidity();
  
  pres[0] = sensor0.readFloatPressure();
  pres[1] = sensor1.readFloatPressure();
  pres[2] = sensor2.readFloatPressure();
  pres[3] = sensor3.readFloatPressure();

}

void print_sensor(String type, float value[], byte decimal){
  
  Serial.print(" ");
  Serial.print(type);
  Serial.print(": ");
  for (byte i=0; i < SENSOR_NUMBER; i++){
    Serial.print(value[i], decimal);
    Serial.print(" | ");
  }
  
}

I want to apply the scalability of the i2c multiplexer to the SPI connection however I haven't found a way to do that while there are multiple sensor objects. Can it be done or am I stuck with manually writing each line for each sensor when I go from 4 sensors to 8?

Can it be done

Yes, each sensor needs it own chip enable pin.

am I stuck with manually writing each line for each sensor when I go from 4 sensors to 8?

No use an array to hold the pin numbers and use a for loop to address each sensor in turn. When you go to 8 sensors all you have to do is to extend the array holding the pin numbers. Out the readings in an array as well.

But how can I manage that with multiple sensor objects? If I do it like the way I did with the i2c one, from a single sensor object, how can I specify which sensor I am referring to? I tried manually writing the select pin but I dont get meaningful results.

void read_sensor(){
  
  for(byte i = 0; i<SENSOR_NUMBER; i++){
    digitalWrite(CS_pin[i], LOW);
    temp[i] = sensor.readTempC();
    hum[i] = sensor.readFloatHumidity();
    pres[i] = sensor.readFloatPressure();
    digitalWrite(CS_pin[i], HIGH);
  }
}

one approach that I would take if I wanted to allow my setup to be scalable would be to use the CS_pins in the order the board pins are ordered.

for example: for 6 devices use pins 3-9

then in your 'for loop', something lie this:

for(byte i = 0; i<SENSOR_NUMBER; i++){
digitalWrite(i+OFFSET, LOW);
temp[ i ] = sensor.readTempC();
hum[ i ] = sensor.readFloatHumidity();
pres[ i ] = sensor.readFloatPressure();
digitalWrite(i+OFFSET, HIGH);
}

where 'OFFSET' here would be equal to 3.

the only issue I see here is the array where you are storing the data which probably would be of fixed size unless you are willing to use functions like 'malloc' to dymanically resize them.

Use an array of sensor objects. Each one needs a separate GPIO pin connected to its CS input:

#include <SparkFunBME280.h>

struct Sensor_t {
  uint8_t csPin;
  BME280 sensor;

  Sensor_t(uint8_t cs): csPin(cs) {}
};

Sensor_t mySensors[] = {10, 9, 8, 7, 6, 5};
const uint8_t numSensors = sizeof(mySensors) / sizeof(mySensors[0]);

void setup() {
  for (uint8_t i=0;i<numSensors;i++) {
    mySensors[i].sensor.beginSPI(mySensors[i].csPin);
  }
}

void loop() {
  float temp[numSensors];
  
  for (uint8_t i=0;i<numSensors;i++) {
    temp[i] = mySensors[i].sensor.readTempC();
  }
}

If you have multiple variables of the same type and name except a trailing number, it's time to use an array:

#include "SparkFunBME280.h"


const int SENSOR_NUMBER = 4;


BME280 Sensors[SENSOR_NUMBER];
const byte SensorPins[SENSOR_NUMBER] = {10, 9, 8, 7};

float temp[SENSOR_NUMBER], hum[SENSOR_NUMBER], pres[SENSOR_NUMBER];


const unsigned int DELAY = 2000;
unsigned long prevMillis = 0;


void setup()
{
  Serial.begin(9600);


  for (int i = 0; i < SENSOR_NUMBER; i++)
    Sensors[i].beginSPI(SensorPins[i]);
}


void loop()
{
  if (millis() - prevMillis > DELAY)
  {
    prevMillis += DELAY;


    read_sensors();


    print_sensor("Temp", temp, 1);
    print_sensor("Hum", hum, 1);
    print_sensor("Pres", pres, 0);
  }
}


void read_sensors()
{
  for (int i = 0; i < SENSOR_NUMBER; i++)
  {
    temp[i] = Sensors[i].readTempC();
    hum[i]  = Sensors[i].readFloatHumidity();
    pres[i] = Sensors[i].readFloatPressure();
  }
}


void print_sensor(String type, float value[], byte decimal)
{
  Serial.print(" ");
  Serial.print(type);
  Serial.print(": ");
  for (byte i = 0; i < SENSOR_NUMBER; i++)
  {
    Serial.print(value[i], decimal);
    Serial.print(" | ");
  }
}

The sensor[] array doesn't work.

A solution I found in the Sparkfun BME280 library is like this;

BME280 sensor;

void setup(){
  Serial.begin(9600);
  for(byte i = 0; i<SENSOR_NUMBER; i++){
    sensor.settings.chipSelectPin = CS_pin[i];
    sensor.beginSPI(CS_pin[i]);
  }
}

void read_sensor(){
  for(byte i = 0; i<SENSOR_NUMBER; i++){
    sensor.settings.chipSelectPin = CS_pin[i];
    temp[i] = sensor.readTempC();
    hum[i] = sensor.readFloatHumidity();
    pres[i] = sensor.readFloatPressure() / 100;
  }
}

The code compiles the sensor[] arrays but when I run it all I get is 0es and sometimes -0.3s for temp. I wish it worked because it is a much more convenient way.

muwimax:
The code compiles the sensor[] arrays but when I run it all I get is 0es and sometimes -0.3s for temp. I wish it worked because it is a much more convenient way.

Then something else is wrong.

How does the chip that was set for I2C now know that it should use SPI instead?

Did you read this bit on the SparkFun site?

The BME280 is a 3.3V device! Supplying voltages greater than ~3.6V can permanently damage the IC. As long as your Arduino has a 3.3V supply output, and you're OK with using I2C, you shouldn't need any extra level shifting. But if you want to use SPI, you may need a Logic Level Converter.

gfvalvo:
Then something else is wrong.

I have only changed the sensor objects from being single lines to an array and other parts of my code works as intended. Though I will definitely look more into it.

johnwasser:
How does the chip that was set for I2C now know that it should use SPI instead?

I am initializing it with sensor.beginSPI(CS_pin); , and already powering them with external 3V3, plus I am not using a Sparkfun sensor. I just use their library as it was the easiest to setup. I have also tried Adafruit's and some generic BME280 libraries.

Okay I get the sensor[] array work correctly! Thank you all for your helps. Here is the complete code;

I added the auto sensor number line from gfvalvo's comment too. Now I can just add or remove select pins and don't have to change sensor number manually.

#include <SparkFunBME280.h>

byte CS_pin[] = {7, 5, 3, 22, 48, 19, 40, 44}; // Sensor select pins in order

// Calculates the sensor number from the entered pin count
const int SENSOR_NUMBER = sizeof(CS_pin) / sizeof(CS_pin[0]); 

BME280 bme[SENSOR_NUMBER]; // Sensor array for easy looping

float temp[SENSOR_NUMBER], hum[SENSOR_NUMBER], pres[SENSOR_NUMBER];

unsigned long prevMillis = 0;

#define DELAY 2000 //Delay between each reading

void setup(){

  Serial.begin(9600);

  //Initialize the sensors for each chip select pin
  for (byte i=0; i < SENSOR_NUMBER; i++){
    bme[i].beginSPI(CS_pin[i]);
  }
}

void loop(){

  if(millis() - prevMillis > DELAY){

    read_sensor();
    print_sensor("Temp", temp, 1);
    print_sensor("Hum", hum, 1);
    print_sensor("Pres", pres, 0);
    Serial.println();
    prevMillis = millis();

  }
}

// Writes the sensor readings to respected arrays

void read_sensor(){

  for (byte i=0; i < SENSOR_NUMBER; i++){

    temp[i] = bme[i].readTempC(); 
    hum[i] = bme[i].readFloatHumidity();
    pres[i] = bme[i].readFloatPressure() / 100; // divided by 100 to get hPa results

  }
}

// Print array values separated by |, can choose the name, and decimal points

void print_sensor(String type, float value[], byte decimal){

  Serial.print(" ");
  Serial.print(type);
  Serial.print(": ");

  for (byte i=0; i < SENSOR_NUMBER; i++){

    Serial.print(value[i], decimal);
    Serial.print(" | ");

  }
}

The code I showed you in Reply #4 accomplishes essentially the same thing. But, it binds each sensor to its pin number in a single object. More in the spirit of OOP.