STC31 CO2 sensor - confused b high oxygen?

Dear Forum,

I have a STC31 CO2 sensor conected to an Arduino Uno R4 Wifi via the Qwiic cable (~3 in long). Using the example sketch for temperature and relative humidity correction via the include STHC3, I am getting fairly good CO2 readings. Ambient CO is averaging a believable 0.3 - 0.4%. Pure CO2 is reading as 100 +/- 0.3%.

I have noticed two unexpected behaviours, and am wondering if they should actually be expected?:

  1. When I place the sensor in a cell culture incubator (>90% RH) that has been calibrated to 5% CO2 with a Fyrite kit, the STC31 initially reads very close to 5%. But over the next few minutes, as the temperature on the board climbs, the CO2 reading also climbs to ~5.3%, which seems a bit high. I am curious if this slow climb from ~4.9% to 5.3% over ~10 minutes is to be expected. The RH and temp correction is being performed before each measurement. (as per the code below)
  2. When I try to measure to % CO2 in a mixture of 5% CO2 / 95% O2, the STC31 reads negative values of CO2. Is this an expected issue when exposing the unit to high O2 with the “balance air” setting?

FWIW, have have an ExplorIR sensor that handles the 5% CO2 / 95% O2 mix, but it’s much bigger and more expensive, so I am hoping to be able to shift to the STC31 for this project.

Here is the code used, not including the header file where I define some structures to aid in calculating average readings:

/*
Reading CO2 concentration from the STC3x
By: Paul Clark
Based on earlier code by: Nathan Seidle
SparkFun Electronics
Date: June 11th, 2021
License: MIT. See license file for more information but you can
basically do whatever you want with this code.

Feel like supporting open source hardware?
Buy a board from SparkFun! CO₂ Sensor - STC31 (Qwiic)

This examples shows how to compensate for pressure, temperature and humidity
Temperature and humidity are provided by the SHTC3 on the STC31 breakout

Hardware Connections:
Attach RedBoard to computer using a USB cable.
Connect STC31 to RedBoard using Qwiic cable.
Open Serial Monitor at 115200 baud.
*/

#include <Wire.h>
#include “SparkFun_STC3x_Arduino_Library.h” //Click here to get the library: http://librarymanager/All#SparkFun_STC3x
STC3x mySensor;
#include “SparkFun_SHTC3.h” //Click here to get the library: http://librarymanager/All#SparkFun_SHTC3
SHTC3 mySHTC3;
#include “structures.h”;

serialMsg serialMain(23); //hold buffer and variables related to serial monitor messages

float SHTC3temp = 0;
generalSensor CO2(10,“CO2”);
float RH = 0;

void setup()
{
Serial.begin(115200);
Serial.println(“Press any key to continue…”);
while (Serial.available() == 0) {
// Wait here until data is received from the Serial Monitor
}
Serial.println(F(“STC3x Example”));
Wire1.begin();

//mySensor.enableDebugging(); // Uncomment this line to get helpful debug messages on Serial

if (mySensor.begin(0x29, Wire1) == false)
{
Serial.println(F(“STC3x not detected. Please check wiring. Freezing…”));
while (1)
;
}

if (mySHTC3.begin(Wire1) != SHTC3_Status_Nominal)
{
Serial.println(F(“SHTC3 not detected. Please check wiring. Freezing…”));
while (1)
;
}

//We need to tell the STC3x what binary gas and full range we are using
//Possible values are:
// STC3X_BINARY_GAS_CO2_N2_100 : Set binary gas to CO2 in N2. Range: 0 to 100 vol%
// STC3X_BINARY_GAS_CO2_AIR_100 : Set binary gas to CO2 in Air. Range: 0 to 100 vol%
// STC3X_BINARY_GAS_CO2_N2_25 : Set binary gas to CO2 in N2. Range: 0 to 25 vol%
// STC3X_BINARY_GAS_CO2_AIR_25 : Set binary gas to CO2 in Air. Range: 0 to 25 vol%

if (mySensor.setBinaryGas(STC3X_BINARY_GAS_CO2_AIR_100) == false)
{
Serial.println(F(“Could not set the binary gas! Freezing…”));
while (1)
;
}

//We can compensate for temperature and relative humidity using the readings from the SHTC3

if (mySHTC3.update() != SHTC3_Status_Nominal) // Request a measurement
{
Serial.println(F(“Could not read the RH and T from the SHTC3! Freezing…”));
while (1)
;
}

//In case the ‘Set temperature command’ has been used prior to the measurement command,
//the temperature value given out by the STC31 will be that one of the ‘Set temperature command’.
//When the ‘Set temperature command’ has not been used, the internal temperature value can be read out.
float temperature = mySHTC3.toDegC(); // “toDegC” returns the temperature as a floating point number in deg C
Serial.print(F("Setting STC3x temperature to "));
Serial.print(temperature, 2);
Serial.print(F("C was "));
if (mySensor.setTemperature(temperature) == false)
Serial.print(F("not "));
Serial.println(F(“successful”));

RH = mySHTC3.toPercent(); // “toPercent” returns the percent humidity as a floating point number
Serial.print(F("Setting STC3x RH to “));
Serial.print(RH, 2);
Serial.print(F(”% was "));
if (mySensor.setRelativeHumidity(RH) == false)
Serial.print(F("not "));
Serial.println(F(“successful”));

//If we have a pressure sensor available, we can compensate for ambient pressure too.
//As an example, let’s set the pressure to 840 mbar (== SF Headquarters)
uint16_t pressure = 960;
Serial.print(F("Setting STC3x pressure to "));
Serial.print(pressure);
Serial.print(F("mbar was "));
if (mySensor.setPressure(pressure) == false)
Serial.print(F("not "));
Serial.println(F(“successful”));

Serial.print(F("CO2(%):"));
//Serial.print(mySensor.getCO2(), 2);
Serial.print(F("\tCO2.value:"));
//Serial.print(CO2.value, 2);
Serial.print(F("\tCO2.average:"));
//Serial.print(CO2.average, 2);
Serial.print(F("\tCO2 expected:"));
//Serial.print(CO2.setpoint, 2);
Serial.print(F("\tTemp(C):"));
//Serial.print(mySensor.getTemperature(), 2);
mySHTC3.update();
Serial.print(F("\tSHTC3_Temp(C):"));
//Serial.print(SHTC3temp, 2);
Serial.print(F("\tRH(%):"));
//Serial.print(RH, 2);
Serial.println();

}

void loop()
{

if (Serial.available() > 0) {
// Read serialMain.incoming until end character is reached or maxMessageLength characters are read.
serialMain.length = Serial.readBytesUntil(serialMain.end, serialMain.incoming, serialMain.size);
if (serialMain.length <= serialMain.size) {
if (serialMain.incoming[0] == ‘c’) {
// indicate the known CO2 % in the gas on the STC31 as nnn/10.
char co2char[3] = {serialMain.incoming[1],serialMain.incoming[2],serialMain.incoming[3]};
CO2.setpoint = atof(co2char)/10;
//Serial.print("CO2 expected now ");
//Serial.println(CO2.setpoint,1);
}
}
}

if (mySensor.measureGasConcentration()) {// measureGasConcentration will return true when fresh data is available

SHTC3temp = mySHTC3.toDegC();
RH = mySHTC3.toPercent();

mySensor.setTemperature(SHTC3temp);
mySensor.setRelativeHumidity(RH);
CO2.value = mySensor.getCO2();
sensorUpdate(CO2);
        
//Serial.print(F("CO2(%):"));
Serial.print(F("\t"));
Serial.print(mySensor.getCO2(), 2);
//Serial.print(F("\tCO2.value:"));
Serial.print(F("\t"));
Serial.print(CO2.value, 2);
//Serial.print(F("\tCO2.average:"));
Serial.print(F("\t"));
Serial.print(CO2.average, 2);
//Serial.print(F("\tCO2 expected:"));
Serial.print(F("\t"));
Serial.print(CO2.setpoint, 2);
//Serial.print(F("\tTemp(C):"));
Serial.print(F("\t"));
Serial.print(mySensor.getTemperature(), 2);
mySHTC3.update();
//Serial.print(F("\tSHTC3_Temp(C):"));
Serial.print(F("\t"));
Serial.print(SHTC3temp, 2);
//Serial.print(F("\tRH(%):"));
Serial.print(F("\t"));
Serial.print(RH, 2);
Serial.println();

} else {
//Serial.print(F(“.”));
}
delay(1000);
}

void sensorUpdate(generalSensor& sensor) {

//add the value to the array and note the time that value is added to the history in millis
sensor.history[sensor.index] = sensor.value;
sensor.deviation = sensor.value - sensor.setpoint;
sensor.time[sensor.index] = millis();
if (sensor.value < sensor.minimum) sensor.minimum = sensor.value;
if (sensor.value > sensor.maximum) sensor.maximum = sensor.value;

//determine slope and append to slope history
if (sensor.historyFilled) {
//determine slope
sensor.slope[sensor.index] = (sensor.value - sensor.history[(sensor.index - sensor.slopeInterval) % sensor.historySize]) / (sensor.slopeUnits * (sensor.time[sensor.index] - sensor.time[(sensor.index - sensor.slopeInterval) % sensor.historySize]));

//calculate average over the history
float thisAvg = 0;
for (int i = 0; i < sensor.historySize; i++) {

  thisAvg += sensor.history[i];
}
sensor.average = thisAvg / sensor.historySize;

} else {
sensor.slope[sensor.index] = 0;
sensor.average = sensor.value;
}

sensor.index++;

//this is essentially used like a circular buffer
if (sensor.index == (sensor.historySize)) {
sensor.historyFilled = true;
sensor.index = 0; // reset
}
}

Thanks,
Damon

1 - Does it consistently tend to creep high, or have you seen low figures? Was the calibration performed on an already-warm/running device? I would let it run for however long it takes for the creep to settle out (‘burn-in period’) and then do the calibration, as internal temps may be throwing it off some

2 - I’m not entirely certain on this one; change the setting to experiment and see

If the readings are really consistent but high you could likely adjust for this on the back-end with a small bit of code to compensate

Thanks for the quick reply.

  1. I ran another test to assess the readings when the STC31 is placed in the cell culture incubator. I let the sensor run for 7 minutes at ambient conditions before placing it in the incubator. It looks like the CO2 readings did settle 6-7 minutes. However, it looks like the values are a bit hi. Once I better sealed the door so that the incubator would start injecting CO2 again, the STC31 read closure to 6% CO2. This is well outside the range of accuracy that I am hoping for.

Is there a method for lasting calibration (i.e. that doesn’t require recalibration when the power is cycled?

This image shows the readers of CO2, temp and RH over the course of the test Zoomed into Y-values during the incubator section of the test.

  1. I cycled through the setting options below:

// STC3X_BINARY_GAS_CO2_N2_100 : Set binary gas to CO2 in N2. Range: 0 to 100 vol%
// STC3X_BINARY_GAS_CO2_AIR_100 : Set binary gas to CO2 in Air. Range: 0 to 100 vol%
// STC3X_BINARY_GAS_CO2_N2_25 : Set binary gas to CO2 in N2. Range: 0 to 25 vol%
// STC3X_BINARY_GAS_CO2_AIR_25 : Set binary gas to CO2 in Air. Range: 0 to 25 vol%

This resulted in the following mean CO2 (%) readings in ambient air:
-1.97
0.29
-1.94
0.29

My initial readings were done using the STC3X_BINARY_GAS_CO2_AIR_100 option, and it doesn't look like alternate settings will lead to more accurate CO2 measurement in this case. Do you know if working in ~95% O2 would simply be outside the usable operating conditions?

I would say it might be outside the realm of linearity being that high in the range (95% maximal); can you perhaps perform a ‘regular’ calibration, but make an enclosure that has much higher RH (close to 95%)

For long-term programming: you can certainly hard-code some variables if you are getting good measurements and have fairly repeatable results

For clarification, I have a few questions:

  1. When you say “that high in the range” are you referring to the CO2 or RH?
  2. Similarly, when suggesting that I make an enclosure with >95% RH, are you suggesting that the devices needs near saturated are to give correction values?
  3. In terms of performing a “regular” calibration, or you referring to a forced calibration of the device in an environment with a known % CO? For example, if I want to use this device to accurately measure and control CO2 in one enclosure at say 5%, then I should expect to need to get it all warmed up, have a reference enclosure than is at 5%, run a forced calibration, then move the device back to my new enclosure? Just wanting to be clear on what you are suggesting.

FWIW, when I was testing the 5% CO2 / 95% O2, that air was very dry (7-10% RH) coming straigh from a tank. Is it reasonable to assume that thislow RH will cause the device to give erroneous measurements?

Many thanks,
D

RH
No, I’m saying calibrating at as similar conditions to the use-case as possible is ideal…or at least calibrating under predictable/controllable conditions
Yes, essentially. Perform the calibration in the end-use enclosure if possible (even better if you verify/standardize against the fancier ExplorIR’s readings)…after allowing it to warm up