SparkFun_SCD30_Arduino_Library.cpp 14.3 KB
Newer Older
1
2
3
4
5
6
7
8
/*
  This is a library written for the SCD30
  SparkFun sells these at its website: www.sparkfun.com
  Do you like this library? Help support SparkFun. Buy a board!
  https://www.sparkfun.com/products/14751

  Written by Nathan Seidle @ SparkFun Electronics, May 22nd, 2018

9
  Updated February 1st 2021 to include some of the features of paulvha's version of the library
Eric Duminil's avatar
Eric Duminil committed
10
11
12
  (while maintaining backward-compatibility):
  https://github.com/paulvha/scd30
  Thank you Paul!
13

14
15
16
17
18
19
20
21
  The SCD30 measures CO2 with accuracy of +/- 30ppm.

  This library handles the initialization of the SCD30 and outputs
  CO2 levels, relative humidty, and temperature.

  https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library

  Development environment specifics:
22
  Arduino IDE 1.8.13
23

24
25
  SparkFun code, firmware, and software is released under the MIT License.
  Please see LICENSE.md for more details.
26
27
28
29
30
31
32
33
34
*/

#include "SparkFun_SCD30_Arduino_Library.h"

SCD30::SCD30(void)
{
  // Constructor
}

Eric Duminil's avatar
Eric Duminil committed
35
// Initialize the Serial port
36
37
38
39
40
#ifdef USE_TEENSY3_I2C_LIB
bool SCD30::begin(i2c_t3 &wirePort, bool autoCalibrate, bool measBegin)
#else
bool SCD30::begin(TwoWire &wirePort, bool autoCalibrate, bool measBegin)
#endif
41
{
Eric Duminil's avatar
Eric Duminil committed
42
  _i2cPort = &wirePort; // Grab which port the user wants us to use
43
44
45
46
47
48
49
50
51
52

  /* Especially during obtaining the ACK BIT after a byte sent the SCD30 is using clock stretching  (but NOT only there)!
   * The need for clock stretching is described in the Sensirion_CO2_Sensors_SCD30_Interface_Description.pdf
   *
   * The default clock stretch (maximum wait time) on the ESP8266-library (2.4.2) is 230us which is set during _i2cPort->begin();
   * In the current implementation of the ESP8266 I2C driver there is NO error message when this time expired, while
   * the clock stretch is still happening, causing uncontrolled behaviour of the hardware combination.
   *
   * To set ClockStretchlimit() a check for ESP8266 boards has been added in the driver.
   *
53
   * With setting to 20000, we set a max timeout of 20mS (> 20x the maximum measured) basically disabling the time-out
54
55
56
57
58
59
60
   * and now wait for clock stretch to be controlled by the client.
   */

#if defined(ARDUINO_ARCH_ESP8266)
  _i2cPort->setClockStretchLimit(200000);
#endif

Eric Duminil's avatar
Eric Duminil committed
61
  if (isConnected() == false)
62
63
64
65
66
    return (false);

  if (measBegin == false) // Exit now if measBegin is false
    return (true);

Eric Duminil's avatar
Eric Duminil committed
67
68
  // Check for device to respond correctly
  if (beginMeasuring() == true) // Start continuous measurements
69
  {
Eric Duminil's avatar
Eric Duminil committed
70
71
    setMeasurementInterval(2);             // 2 seconds between measurements
    setAutoSelfCalibration(autoCalibrate); // Enable auto-self-calibration
72
73
74
75

    return (true);
  }

Eric Duminil's avatar
Eric Duminil committed
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
  return (false); // Something went wrong
}

// Returns true if device responds to a firmware request
bool SCD30::isConnected()
{
  uint16_t fwVer;
  if (getFirmwareVersion(&fwVer) == false) // Read the firmware version. Return false if the CRC check fails.
    return (false);

  if (_printDebug == true)
  {
    _debugPort->print(F("Firmware version 0x"));
    _debugPort->println(fwVer, HEX);
  }

  return (true);
93
94
}

Eric Duminil's avatar
Eric Duminil committed
95
96
// Calling this function with nothing sets the debug port to Serial
// You can also call it with other streams like Serial1, SerialUSB, etc.
97
98
void SCD30::enableDebugging(Stream &debugPort)
{
Eric Duminil's avatar
Eric Duminil committed
99
100
  _debugPort = &debugPort;
  _printDebug = true;
101
102
}

Eric Duminil's avatar
Eric Duminil committed
103
104
// Returns the latest available CO2 level
// If the current level has already been reported, trigger a new read
105
106
uint16_t SCD30::getCO2(void)
{
Eric Duminil's avatar
Eric Duminil committed
107
108
109
110
111
  if (co2HasBeenReported == true) // Trigger a new read
  {
    if (readMeasurement() == false) // Pull in new co2, humidity, and temp into global vars
      co2 = 0;                      // Failed to read sensor
  }
112
113
114

  co2HasBeenReported = true;

Eric Duminil's avatar
Eric Duminil committed
115
  return (uint16_t)co2; // Cut off decimal as co2 is 0 to 10,000
116
117
}

Eric Duminil's avatar
Eric Duminil committed
118
119
// Returns the latest available humidity
// If the current level has already been reported, trigger a new read
120
121
float SCD30::getHumidity(void)
{
Eric Duminil's avatar
Eric Duminil committed
122
123
124
  if (humidityHasBeenReported == true) // Trigger a new read
    if (readMeasurement() == false)    // Pull in new co2, humidity, and temp into global vars
      humidity = 0;                    // Failed to read sensor
125
126
127
128
129
130

  humidityHasBeenReported = true;

  return humidity;
}

Eric Duminil's avatar
Eric Duminil committed
131
132
// Returns the latest available temperature
// If the current level has already been reported, trigger a new read
133
134
float SCD30::getTemperature(void)
{
Eric Duminil's avatar
Eric Duminil committed
135
136
137
  if (temperatureHasBeenReported == true) // Trigger a new read
    if (readMeasurement() == false)       // Pull in new co2, humidity, and temp into global vars
      temperature = 0;                    // Failed to read sensor
138
139
140
141
142
143

  temperatureHasBeenReported = true;

  return temperature;
}

Eric Duminil's avatar
Eric Duminil committed
144
// Enables or disables the ASC
145
146
147
bool SCD30::setAutoSelfCalibration(bool enable)
{
  if (enable)
Eric Duminil's avatar
Eric Duminil committed
148
    return sendCommand(COMMAND_AUTOMATIC_SELF_CALIBRATION, 1); // Activate continuous ASC
149
  else
Eric Duminil's avatar
Eric Duminil committed
150
    return sendCommand(COMMAND_AUTOMATIC_SELF_CALIBRATION, 0); // Deactivate continuous ASC
151
152
}

Eric Duminil's avatar
Eric Duminil committed
153
154
// Set the forced recalibration factor. See 1.3.7.
// The reference CO2 concentration has to be within the range 400 ppm ≤ cref(CO2) ≤ 2000 ppm.
155
156
157
158
bool SCD30::setForcedRecalibrationFactor(uint16_t concentration)
{
  if (concentration < 400 || concentration > 2000)
  {
Eric Duminil's avatar
Eric Duminil committed
159
    return false; // Error check.
160
161
162
163
  }
  return sendCommand(COMMAND_SET_FORCED_RECALIBRATION_FACTOR, concentration);
}

Eric Duminil's avatar
Eric Duminil committed
164
// Get the temperature offset. See 1.3.8.
165
float SCD30::getTemperatureOffset(void)
166
167
168
{
  uint16_t response = readRegister(COMMAND_SET_TEMPERATURE_OFFSET);

169
170
171
172
173
  union
  {
    int16_t signed16;
    uint16_t unsigned16;
  } signedUnsigned; // Avoid any ambiguity casting int16_t to uint16_t
Eric Duminil's avatar
Eric Duminil committed
174
175
176
  signedUnsigned.signed16 = response;

  return (((float)signedUnsigned.signed16) / 100.0);
177
178
}

Eric Duminil's avatar
Eric Duminil committed
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
// Set the temperature offset to remove module heating from temp reading
bool SCD30::setTemperatureOffset(float tempOffset)
{
  // Temp offset is only positive. See: https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library/issues/27#issuecomment-971986826
  //"The SCD30 offset temperature is obtained by subtracting the reference temperature from the SCD30 output temperature"
  // https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9.5_CO2/Sensirion_CO2_Sensors_SCD30_Low_Power_Mode.pdf

  if (tempOffset < 0.0)
    return (false);

  uint16_t value = tempOffset * 100;

  return sendCommand(COMMAND_SET_TEMPERATURE_OFFSET, value);
}

// Get the altitude compenstation. See 1.3.9.
195
196
197
uint16_t SCD30::getAltitudeCompensation(void)
{
  return readRegister(COMMAND_SET_ALTITUDE_COMPENSATION);
198
199
}

Eric Duminil's avatar
Eric Duminil committed
200
// Set the altitude compenstation. See 1.3.9.
201
202
203
204
205
bool SCD30::setAltitudeCompensation(uint16_t altitude)
{
  return sendCommand(COMMAND_SET_ALTITUDE_COMPENSATION, altitude);
}

Eric Duminil's avatar
Eric Duminil committed
206
207
// Set the pressure compenstation. This is passed during measurement startup.
// mbar can be 700 to 1200
208
209
210
211
212
213
214
215
216
bool SCD30::setAmbientPressure(uint16_t pressure_mbar)
{
  if (pressure_mbar < 700 || pressure_mbar > 1200)
  {
    return false;
  }
  return sendCommand(COMMAND_CONTINUOUS_MEASUREMENT, pressure_mbar);
}

217
218
219
// SCD30 soft reset
void SCD30::reset()
{
Eric Duminil's avatar
Eric Duminil committed
220
  sendCommand(COMMAND_RESET);
221
222
223
224
225
226
}

// Get the current ASC setting
bool SCD30::getAutoSelfCalibration()
{
  uint16_t response = readRegister(COMMAND_AUTOMATIC_SELF_CALIBRATION);
Eric Duminil's avatar
Eric Duminil committed
227
228
  if (response == 1)
  {
229
230
    return true;
  }
Eric Duminil's avatar
Eric Duminil committed
231
232
233
  else
  {
    return false;
234
235
236
  }
}

Eric Duminil's avatar
Eric Duminil committed
237
238
239
240
241
// Begins continuous measurements
// Continuous measurement status is saved in non-volatile memory. When the sensor
// is powered down while continuous measurement mode is active SCD30 will measure
// continuously after repowering without sending the measurement command.
// Returns true if successful
242
243
244
245
246
bool SCD30::beginMeasuring(uint16_t pressureOffset)
{
  return (sendCommand(COMMAND_CONTINUOUS_MEASUREMENT, pressureOffset));
}

Eric Duminil's avatar
Eric Duminil committed
247
// Overload - no pressureOffset
248
249
250
251
252
bool SCD30::beginMeasuring(void)
{
  return (beginMeasuring(0));
}

253
254
255
// Stop continuous measurement
bool SCD30::StopMeasurement(void)
{
Eric Duminil's avatar
Eric Duminil committed
256
  return (sendCommand(COMMAND_STOP_MEAS));
257
258
}

Eric Duminil's avatar
Eric Duminil committed
259
260
// Sets interval between measurements
// 2 seconds to 1800 seconds (30 minutes)
261
262
263
264
265
bool SCD30::setMeasurementInterval(uint16_t interval)
{
  return sendCommand(COMMAND_SET_MEASUREMENT_INTERVAL, interval);
}

Eric Duminil's avatar
Eric Duminil committed
266
267
268
269
270
271
272
273
274
275
// Gets interval between measurements
// 2 seconds to 1800 seconds (30 minutes)
uint16_t SCD30::getMeasurementInterval(void)
{
  uint16_t interval = 0;
  getSettingValue(COMMAND_SET_MEASUREMENT_INTERVAL, &interval);
  return (interval);
}

// Returns true when data is available
276
277
278
279
280
281
282
283
284
bool SCD30::dataAvailable()
{
  uint16_t response = readRegister(COMMAND_GET_DATA_READY);

  if (response == 1)
    return (true);
  return (false);
}

Eric Duminil's avatar
Eric Duminil committed
285
286
287
// Get 18 bytes from SCD30
// Updates global variables with floats
// Returns true if success
288
289
bool SCD30::readMeasurement()
{
Eric Duminil's avatar
Eric Duminil committed
290
  // Verify we have data from the sensor
291
292
293
  if (dataAvailable() == false)
    return (false);

Eric Duminil's avatar
Eric Duminil committed
294
295
296
297
298
299
  ByteToFl tempCO2;
  tempCO2.value = 0;
  ByteToFl tempHumidity;
  tempHumidity.value = 0;
  ByteToFl tempTemperature;
  tempTemperature.value = 0;
300
301

  _i2cPort->beginTransmission(SCD30_ADDRESS);
Eric Duminil's avatar
Eric Duminil committed
302
303
  _i2cPort->write(COMMAND_READ_MEASUREMENT >> 8);   // MSB
  _i2cPort->write(COMMAND_READ_MEASUREMENT & 0xFF); // LSB
304
  if (_i2cPort->endTransmission() != 0)
Eric Duminil's avatar
Eric Duminil committed
305
    return (0); // Sensor did not ACK
306

Eric Duminil's avatar
Eric Duminil committed
307
308
  delay(3);

309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
  const uint8_t receivedBytes = _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)18);
  bool error = false;
  if (_i2cPort->available())
  {
    byte bytesToCrc[2];
    for (byte x = 0; x < 18; x++)
    {
      byte incoming = _i2cPort->read();

      switch (x)
      {
      case 0:
      case 1:
      case 3:
      case 4:
Eric Duminil's avatar
Eric Duminil committed
324
        tempCO2.array[x < 3 ? 3 - x : 4 - x] = incoming;
325
326
327
328
329
330
        bytesToCrc[x % 3] = incoming;
        break;
      case 6:
      case 7:
      case 9:
      case 10:
Eric Duminil's avatar
Eric Duminil committed
331
        tempTemperature.array[x < 9 ? 9 - x : 10 - x] = incoming;
332
333
334
335
336
337
        bytesToCrc[x % 3] = incoming;
        break;
      case 12:
      case 13:
      case 15:
      case 16:
Eric Duminil's avatar
Eric Duminil committed
338
        tempHumidity.array[x < 15 ? 15 - x : 16 - x] = incoming;
339
340
341
        bytesToCrc[x % 3] = incoming;
        break;
      default:
Eric Duminil's avatar
Eric Duminil committed
342
        // Validate CRC
343
        uint8_t foundCrc = computeCRC8(bytesToCrc, 2);
344
345
        if (foundCrc != incoming)
        {
346
347
348
349
350
351
352
353
354
          if (_printDebug == true)
          {
            _debugPort->print(F("readMeasurement: found CRC in byte "));
            _debugPort->print(x);
            _debugPort->print(F(", expected 0x"));
            _debugPort->print(foundCrc, HEX);
            _debugPort->print(F(", got 0x"));
            _debugPort->println(incoming, HEX);
          }
355
356
357
358
359
360
361
362
          error = true;
        }
        break;
      }
    }
  }
  else
  {
363
364
365
366
367
368
    if (_printDebug == true)
    {
      _debugPort->print(F("readMeasurement: no SCD30 data found from I2C, i2c claims we should receive "));
      _debugPort->print(receivedBytes);
      _debugPort->println(F(" bytes"));
    }
369
370
371
372
373
    return false;
  }

  if (error)
  {
374
375
    if (_printDebug == true)
      _debugPort->println(F("readMeasurement: encountered error reading SCD30 data."));
376
377
    return false;
  }
Eric Duminil's avatar
Eric Duminil committed
378
  // Now copy the uint32s into their associated floats
379
380
381
  co2 = tempCO2.value;
  temperature = tempTemperature.value;
  humidity = tempHumidity.value;
382

Eric Duminil's avatar
Eric Duminil committed
383
  // Mark our global variables as fresh
384
385
386
387
  co2HasBeenReported = false;
  humidityHasBeenReported = false;
  temperatureHasBeenReported = false;

Eric Duminil's avatar
Eric Duminil committed
388
  return (true); // Success! New data available in globals.
389
390
}

Eric Duminil's avatar
Eric Duminil committed
391
392
// Gets a setting by reading the appropriate register.
// Returns true if the CRC is valid.
393
394
395
bool SCD30::getSettingValue(uint16_t registerAddress, uint16_t *val)
{
  _i2cPort->beginTransmission(SCD30_ADDRESS);
Eric Duminil's avatar
Eric Duminil committed
396
397
  _i2cPort->write(registerAddress >> 8);   // MSB
  _i2cPort->write(registerAddress & 0xFF); // LSB
398
  if (_i2cPort->endTransmission() != 0)
Eric Duminil's avatar
Eric Duminil committed
399
    return (false); // Sensor did not ACK
400

Eric Duminil's avatar
Eric Duminil committed
401
402
  delay(3);

403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
  _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)3); // Request data and CRC
  if (_i2cPort->available())
  {
    uint8_t data[2];
    data[0] = _i2cPort->read();
    data[1] = _i2cPort->read();
    uint8_t crc = _i2cPort->read();
    *val = (uint16_t)data[0] << 8 | data[1];
    uint8_t expectedCRC = computeCRC8(data, 2);
    if (crc == expectedCRC) // Return true if CRC check is OK
      return (true);
    if (_printDebug == true)
    {
      _debugPort->print(F("getSettingValue: CRC fail: expected 0x"));
      _debugPort->print(expectedCRC, HEX);
      _debugPort->print(F(", got 0x"));
      _debugPort->println(crc, HEX);
    }
  }
  return (false);
}

Eric Duminil's avatar
Eric Duminil committed
425
// Gets two bytes from SCD30
426
427
428
uint16_t SCD30::readRegister(uint16_t registerAddress)
{
  _i2cPort->beginTransmission(SCD30_ADDRESS);
Eric Duminil's avatar
Eric Duminil committed
429
430
  _i2cPort->write(registerAddress >> 8);   // MSB
  _i2cPort->write(registerAddress & 0xFF); // LSB
431
  if (_i2cPort->endTransmission() != 0)
Eric Duminil's avatar
Eric Duminil committed
432
    return (0); // Sensor did not ACK
433

Eric Duminil's avatar
Eric Duminil committed
434
435
  delay(3);

436
437
438
439
440
441
442
  _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)2);
  if (_i2cPort->available())
  {
    uint8_t msb = _i2cPort->read();
    uint8_t lsb = _i2cPort->read();
    return ((uint16_t)msb << 8 | lsb);
  }
Eric Duminil's avatar
Eric Duminil committed
443
  return (0); // Sensor did not respond
444
445
}

Eric Duminil's avatar
Eric Duminil committed
446
// Sends a command along with arguments and CRC
447
448
449
450
451
bool SCD30::sendCommand(uint16_t command, uint16_t arguments)
{
  uint8_t data[2];
  data[0] = arguments >> 8;
  data[1] = arguments & 0xFF;
Eric Duminil's avatar
Eric Duminil committed
452
  uint8_t crc = computeCRC8(data, 2); // Calc CRC on the arguments only, not the command
453
454

  _i2cPort->beginTransmission(SCD30_ADDRESS);
Eric Duminil's avatar
Eric Duminil committed
455
456
457
458
  _i2cPort->write(command >> 8);     // MSB
  _i2cPort->write(command & 0xFF);   // LSB
  _i2cPort->write(arguments >> 8);   // MSB
  _i2cPort->write(arguments & 0xFF); // LSB
459
460
  _i2cPort->write(crc);
  if (_i2cPort->endTransmission() != 0)
Eric Duminil's avatar
Eric Duminil committed
461
    return (false); // Sensor did not ACK
462
463
464
465

  return (true);
}

Eric Duminil's avatar
Eric Duminil committed
466
// Sends just a command, no arguments, no CRC
467
468
469
bool SCD30::sendCommand(uint16_t command)
{
  _i2cPort->beginTransmission(SCD30_ADDRESS);
Eric Duminil's avatar
Eric Duminil committed
470
471
  _i2cPort->write(command >> 8);   // MSB
  _i2cPort->write(command & 0xFF); // LSB
472
  if (_i2cPort->endTransmission() != 0)
Eric Duminil's avatar
Eric Duminil committed
473
    return (false); // Sensor did not ACK
474
475
476
477

  return (true);
}

Eric Duminil's avatar
Eric Duminil committed
478
479
480
481
482
// Given an array and a number of bytes, this calculate CRC8 for those bytes
// CRC is only calc'd on the data portion (two bytes) of the four bytes being sent
// From: http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html
// Tested with: http://www.sunshine2k.de/coding/javascript/crc/crc_js.html
// x^8+x^5+x^4+1 = 0x31
483
484
uint8_t SCD30::computeCRC8(uint8_t data[], uint8_t len)
{
Eric Duminil's avatar
Eric Duminil committed
485
  uint8_t crc = 0xFF; // Init with 0xFF
486
487
488
489
490
491
492
493
494
495
496
497
498
499

  for (uint8_t x = 0; x < len; x++)
  {
    crc ^= data[x]; // XOR-in the next input byte

    for (uint8_t i = 0; i < 8; i++)
    {
      if ((crc & 0x80) != 0)
        crc = (uint8_t)((crc << 1) ^ 0x31);
      else
        crc <<= 1;
    }
  }

Eric Duminil's avatar
Eric Duminil committed
500
  return crc; // No output reflection
501
}