CJMCU SGP30: nothing fits like expected

Hey enthusiasts.

I got myself a CJMCU-30 SGP30 (Sensor de Gas Digital de varios píxeles SGP30, módulo Detector de aire para interior, medición de aire, I2C, TVOC/eCO2, CJMCU 30, 3V 5V|measuring sensor| - AliExpress) and was expecting the Adafruit and SparkFun Libraries to work of the shelf with my NodeMCU esp8266.

The Sensor wasn’t even detected by the libs, but a I2C bus scan showed a device at 0x58. So I took the Sensirion datasheet and started talking to the sensor with the Wire library. My results confirm that Adafruit and SparkFun Libraries are supposed to refuse. I get Feature Set 0x0030 instead of 0x0020 and the CRC8 I received for the 0x0030 is 0x07 and not 0x16 what I calculated.

Ignoring all that mismatches between my results and what the datasheet expected me to get, I started measuring by sending Init_air_quality and then Measure_air_quality I get for the first 15 seconds a value off 472ppm eCO2 instead of 400ppm stated by the datasheet (and still ignoring the CRC). After 15 seconds warmup I get reasonable results. So playing around breathing on the sensor, blowing smoke or letting it sniff alcohol based Fluxfluid pushes the eCO2 and TVOC values.

Someone has a clue what is going on here? I feel a bit retarded because I don’t get a picture.

Test-Code:

#include <Wire.h>

#define I2C_SGP30  0x58

void setup() {
  Wire.begin();
  Wire.setClock(400000);

  Serial.begin(115200);

  Serial.println("CJMCU-30 SGP30 Test");
  Serial.println("");
  Serial.println("CRC test, expected: CRC (0xBEEF) = 0x92");
  Serial.print("CRC(lookup): 0x");
  Serial.println(lookupCRC8(0x92),HEX);
  Serial.print("CRC(calc.) : 0x");
  Serial.println(calcCRC8(0x92),HEX);
  Serial.println("");

  Wire.beginTransmission(I2C_SGP30); // Get_feature_set_version
  Wire.write(byte(0x20));
  Wire.write(byte(0x2F));
  Wire.endTransmission();
  delay(2);

  Wire.requestFrom(I2C_SGP30, 3);
  
  uint16_t feature_set;
  uint8_t  feature_crc;

  feature_set  = Wire.read() << 8;
  feature_set |= Wire.read();
  feature_crc  = Wire.read();

  Serial.print("Feature Set, expected: 0x0020, got: 0x");
  Serial.println(feature_set,HEX);
  
  Serial.print("CRC(received): 0x");
  Serial.println(feature_crc,HEX);
  
  Serial.print("CRC(lookup)  : 0x");
  Serial.println(lookupCRC8(feature_crc),HEX);
  
  Serial.print("CRC(calc.)   : 0x");
  Serial.println(calcCRC8(feature_crc),HEX);
}

void loop() {
}

const uint8_t CRC8LookupTable[16][16] = {
  {0x00, 0x31, 0x62, 0x53, 0xC4, 0xF5, 0xA6, 0x97, 0xB9, 0x88, 0xDB, 0xEA, 0x7D, 0x4C, 0x1F, 0x2E},
  {0x43, 0x72, 0x21, 0x10, 0x87, 0xB6, 0xE5, 0xD4, 0xFA, 0xCB, 0x98, 0xA9, 0x3E, 0x0F, 0x5C, 0x6D},
  {0x86, 0xB7, 0xE4, 0xD5, 0x42, 0x73, 0x20, 0x11, 0x3F, 0x0E, 0x5D, 0x6C, 0xFB, 0xCA, 0x99, 0xA8},
  {0xC5, 0xF4, 0xA7, 0x96, 0x01, 0x30, 0x63, 0x52, 0x7C, 0x4D, 0x1E, 0x2F, 0xB8, 0x89, 0xDA, 0xEB},
  {0x3D, 0x0C, 0x5F, 0x6E, 0xF9, 0xC8, 0x9B, 0xAA, 0x84, 0xB5, 0xE6, 0xD7, 0x40, 0x71, 0x22, 0x13},
  {0x7E, 0x4F, 0x1C, 0x2D, 0xBA, 0x8B, 0xD8, 0xE9, 0xC7, 0xF6, 0xA5, 0x94, 0x03, 0x32, 0x61, 0x50},
  {0xBB, 0x8A, 0xD9, 0xE8, 0x7F, 0x4E, 0x1D, 0x2C, 0x02, 0x33, 0x60, 0x51, 0xC6, 0xF7, 0xA4, 0x95},
  {0xF8, 0xC9, 0x9A, 0xAB, 0x3C, 0x0D, 0x5E, 0x6F, 0x41, 0x70, 0x23, 0x12, 0x85, 0xB4, 0xE7, 0xD6},
  {0x7A, 0x4B, 0x18, 0x29, 0xBE, 0x8F, 0xDC, 0xED, 0xC3, 0xF2, 0xA1, 0x90, 0x07, 0x36, 0x65, 0x54},
  {0x39, 0x08, 0x5B, 0x6A, 0xFD, 0xCC, 0x9F, 0xAE, 0x80, 0xB1, 0xE2, 0xD3, 0x44, 0x75, 0x26, 0x17},
  {0xFC, 0xCD, 0x9E, 0xAF, 0x38, 0x09, 0x5A, 0x6B, 0x45, 0x74, 0x27, 0x16, 0x81, 0xB0, 0xE3, 0xD2},
  {0xBF, 0x8E, 0xDD, 0xEC, 0x7B, 0x4A, 0x19, 0x28, 0x06, 0x37, 0x64, 0x55, 0xC2, 0xF3, 0xA0, 0x91},
  {0x47, 0x76, 0x25, 0x14, 0x83, 0xB2, 0xE1, 0xD0, 0xFE, 0xCF, 0x9C, 0xAD, 0x3A, 0x0B, 0x58, 0x69},
  {0x04, 0x35, 0x66, 0x57, 0xC0, 0xF1, 0xA2, 0x93, 0xBD, 0x8C, 0xDF, 0xEE, 0x79, 0x48, 0x1B, 0x2A},
  {0xC1, 0xF0, 0xA3, 0x92, 0x05, 0x34, 0x67, 0x56, 0x78, 0x49, 0x1A, 0x2B, 0xBC, 0x8D, 0xDE, 0xEF},
  {0x82, 0xB3, 0xE0, 0xD1, 0x46, 0x77, 0x24, 0x15, 0x3B, 0x0A, 0x59, 0x68, 0xFF, 0xCE, 0x9D, 0xAC}
};

uint8_t lookupCRC8(uint16_t data) {
  uint8_t CRC = 0xFF; //inital value
  CRC ^= (uint8_t)(data >> 8); //start with MSB
  CRC = CRC8LookupTable[CRC >> 4][CRC & 0xF]; //look up table [MSnibble][LSnibble]
  CRC ^= (uint8_t)data; //use LSB
  CRC = CRC8LookupTable[CRC >> 4][CRC & 0xF]; //look up table [MSnibble][LSnibble]
  return CRC;
}

uint8_t calcCRC8(uint16_t data) {
  uint8_t crc = 0xFF;
  uint8_t msb = byte(data >> 8);
  uint8_t lsb = byte(data);

  crc ^= msb;
  for (uint8_t b = 0; b < 8; b++) {
    if (crc & 0x80)
      crc = (crc << 1) ^ 0x31;
    else
      crc <<= 1;
  }
  crc ^= lsb;
  for (uint8_t b = 0; b < 8; b++) {
    if (crc & 0x80)
      crc = (crc << 1) ^ 0x31;
    else
      crc <<= 1;
  }
  return crc;
}

acki:
I get Feature Set 0x0030 instead of 0x0020

The feature set number can change when the hardware is tweaked, as stated in the SGP30 datasheet.

That datasheet is dated April 2019, so it is basically brand new, and yet it still only suggests 0x0022 as a feature set. It doesn’t even mention the 0x0020, though you can do a search for the preliminary datasheet from 2017 which does.
One can only assume that the 0x0030 is an even newer version. I guess you just have to hope that they have only extended the functionality without changing older stuff.

You could consider writing them an email asking for information on that version or an updated datasheet.

acki:
the CRC8 I received for the 0x0030 is 0x07 and not 0x16 what I calculated.

The idea of the CRC is to check the data bytes and not the CRC byte… :wink:

  Serial.println(lookupCRC8(0x92),HEX); // should be 0xBEEF
...
  Serial.println(calcCRC8(0x92),HEX); // ditto
...
  delay(2); // consider upping this to 10ms as per the datasheet max for this command
...
  Serial.println(lookupCRC8(feature_crc),HEX); // should be feature_set
...
  Serial.println(calcCRC8(feature_crc),HEX); // ditto

HTH.

That datasheet is dated April 2019, so it is basically brand new, and yet it still only suggests 0x0022 as a feature set.

Thanks a lot Arduan for pointing that out. I was still working with the 0x0020 feature set datasheet.

The idea of the CRC is to check the data bytes and not the CRC byte...

Sure. You are totally right. I'm just going nuts because of not getting the expected crc8. That's the reason I checked on the crc8 implementation.

My approach would be to check the result provided crc8 with my calculated one. It just doesn't match, so I expect the crc8 params to be changed or even the crc-agorithm. I asked Sensorion regarding a update for that datasheet and will provide ASAP if I get a response.

I'm not sure from your reply if you saw my comments in the code box about the CRC. Please look at my last post again if you didn't.

Oh my good :o NOW I see it ... Thanks for pointing it out even a 2nd time.

Got btw. confirmation from Sensirion that commands from Feature Set 0x0022 till higher didn't change.

No problem.

Really cool that Sensirion are so on the ball, impressive.

Really cool that Sensirion are so on the ball, impressive.

Yes. It's a really pleasant experience being in contact with them.

But it gets even better. There IS NO FEATURE SET 0x0030 ... But that is the answer when I send the (valid) command 0x202F what also includes a (3bit) CRC. I'm going nuts ...

The command “sgp30_measure_test” which is included for integration and production line testing runs an on-chip self-test. In case of a successful self-test the sensor returns the fixed data pattern 0xD400 (with correct CRC).

Same with the on-chip internal test. I send a valid command and I get 0xFE00, but the answer has to be 0xD400 ... WTF? I have to assume my sensor is broken even when the measurement look reasonable, but I have no reference to ensure reliability of measurements.

Well, in the interest of sanity, post your updated sketch.
A picture of your hardware showing the connections might be useful too.

acki:
But it gets even better. There IS NO FEATURE SET 0x0030 …

What do you mean, did Sensirion tell you this?

arduarn:
Well, in the interest of sanity, post your updated sketch.
A picture of your hardware showing the connections might be useful too.


#include <Wire.h>

#define I2C_SGP30  0x58

void setup() {
  Wire.begin();
  Wire.setClock(400000);

  Serial.begin(115200);

  Serial.println("CJMCU-30 SGP30 Test");
  Serial.println("");
  Serial.println("CRC test, expected: CRC (0xBEEF) = 0x92");
  Serial.print("CRC(lookup): 0x");
  Serial.println(lookupCRC8(0xBEEF),HEX);
  Serial.print("CRC(calc.) : 0x");
  Serial.println(calcCRC8(0xBEEF),HEX);
  Serial.println("");

  Wire.beginTransmission(I2C_SGP30); // sgp30_get_feature_set
  Wire.write(byte(0x20));
  Wire.write(byte(0x2F));
  Wire.endTransmission();
  delay(10);

  Wire.requestFrom(I2C_SGP30, 3);

  uint16_t feature_set;
  uint8_t  feature_crc;

  feature_set  = Wire.read() << 8;
  feature_set |= Wire.read();
  feature_crc  = Wire.read();

  Serial.print("Feature Set, got: 0x");
  Serial.println(feature_set,HEX);

  Serial.print("CRC(received): 0x");
  Serial.println(feature_crc,HEX);

  Serial.print("CRC(lookup)  : 0x");
  Serial.println(lookupCRC8(feature_set),HEX);

  Serial.print("CRC(calc.)   : 0x");
  Serial.println(calcCRC8(feature_set),HEX);

  Wire.beginTransmission(I2C_SGP30); // sgp30_get_iaq_baseline
  Wire.write(byte(0x20));
  Wire.write(byte(0x15));
  Wire.endTransmission();
  delay(10);

  Wire.requestFrom(I2C_SGP30, 6);
  
  uint16_t eco2_baseline;
  uint8_t  eco2_crc;

  eco2_baseline  = Wire.read() << 8;
  eco2_baseline |= Wire.read();
  eco2_crc       = Wire.read();

  uint16_t tvoc_baseline;
  uint8_t  tvoc_crc;

  tvoc_baseline  = Wire.read() << 8;
  tvoc_baseline |= Wire.read();
  tvoc_crc       = Wire.read();

  Serial.print("IAQ Baseline, got: 0x");
  Serial.print(eco2_baseline,HEX);
  Serial.print(":0x");
  Serial.println(tvoc_baseline,HEX);

  Serial.print("eco2_baseline CRC(received): 0x");
  Serial.println(eco2_crc,HEX);

  Serial.print("eco2_baseline CRC(lookup)  : 0x");
  Serial.println(lookupCRC8(eco2_baseline),HEX);

  Serial.print("eco2_baseline CRC(calc.)   : 0x");
  Serial.println(calcCRC8(eco2_baseline),HEX);

  Serial.print("tvoc_baseline CRC(received): 0x");
  Serial.println(tvoc_crc,HEX);

  Serial.print("tvoc_baseline CRC(lookup)  : 0x");
  Serial.println(lookupCRC8(tvoc_baseline),HEX);

  Serial.print("tvoc_baseline CRC(calc.)   : 0x");
  Serial.println(calcCRC8(tvoc_baseline),HEX);

  Wire.beginTransmission(I2C_SGP30); // Get Serial ID
  Wire.write(byte(0x36));
  Wire.write(byte(0x82));
  Wire.endTransmission();
  delay(1);

  Serial.print("Serial ID, got: 0x");
  Wire.requestFrom(I2C_SGP30, 9);
  Serial.print(Wire.read(),HEX);Serial.print(" ");
  Serial.print(Wire.read(),HEX);Serial.print(" ");
  Serial.print(Wire.read(),HEX);Serial.print(" ");
  Serial.print(Wire.read(),HEX);Serial.print(" ");
  Serial.print(Wire.read(),HEX);Serial.print(" ");
  Serial.print(Wire.read(),HEX);Serial.print(" ");
  Serial.print(Wire.read(),HEX);Serial.print(" ");
  Serial.print(Wire.read(),HEX);Serial.print(" ");
  Serial.println(Wire.read(),HEX);

  Wire.beginTransmission(I2C_SGP30); // sgp30_measure_test
  Wire.write(byte(0x20));
  Wire.write(byte(0x32));
  Wire.endTransmission();
  delay(220);

  Wire.requestFrom(I2C_SGP30, 3);

  uint16_t test_result;
  uint8_t  test_crc;

  test_result  = Wire.read() << 8;
  test_result |= Wire.read();
  test_crc     = Wire.read();

  Serial.print("Test result, got: 0x");
  Serial.println(test_result,HEX);
  Serial.print("Test result CRC :0x");
  Serial.println(test_crc,HEX);

}

void loop() {
}

const uint8_t CRC8LookupTable[16][16] = {
  {0x00, 0x31, 0x62, 0x53, 0xC4, 0xF5, 0xA6, 0x97, 0xB9, 0x88, 0xDB, 0xEA, 0x7D, 0x4C, 0x1F, 0x2E},
  {0x43, 0x72, 0x21, 0x10, 0x87, 0xB6, 0xE5, 0xD4, 0xFA, 0xCB, 0x98, 0xA9, 0x3E, 0x0F, 0x5C, 0x6D},
  {0x86, 0xB7, 0xE4, 0xD5, 0x42, 0x73, 0x20, 0x11, 0x3F, 0x0E, 0x5D, 0x6C, 0xFB, 0xCA, 0x99, 0xA8},
  {0xC5, 0xF4, 0xA7, 0x96, 0x01, 0x30, 0x63, 0x52, 0x7C, 0x4D, 0x1E, 0x2F, 0xB8, 0x89, 0xDA, 0xEB},
  {0x3D, 0x0C, 0x5F, 0x6E, 0xF9, 0xC8, 0x9B, 0xAA, 0x84, 0xB5, 0xE6, 0xD7, 0x40, 0x71, 0x22, 0x13},
  {0x7E, 0x4F, 0x1C, 0x2D, 0xBA, 0x8B, 0xD8, 0xE9, 0xC7, 0xF6, 0xA5, 0x94, 0x03, 0x32, 0x61, 0x50},
  {0xBB, 0x8A, 0xD9, 0xE8, 0x7F, 0x4E, 0x1D, 0x2C, 0x02, 0x33, 0x60, 0x51, 0xC6, 0xF7, 0xA4, 0x95},
  {0xF8, 0xC9, 0x9A, 0xAB, 0x3C, 0x0D, 0x5E, 0x6F, 0x41, 0x70, 0x23, 0x12, 0x85, 0xB4, 0xE7, 0xD6},
  {0x7A, 0x4B, 0x18, 0x29, 0xBE, 0x8F, 0xDC, 0xED, 0xC3, 0xF2, 0xA1, 0x90, 0x07, 0x36, 0x65, 0x54},
  {0x39, 0x08, 0x5B, 0x6A, 0xFD, 0xCC, 0x9F, 0xAE, 0x80, 0xB1, 0xE2, 0xD3, 0x44, 0x75, 0x26, 0x17},
  {0xFC, 0xCD, 0x9E, 0xAF, 0x38, 0x09, 0x5A, 0x6B, 0x45, 0x74, 0x27, 0x16, 0x81, 0xB0, 0xE3, 0xD2},
  {0xBF, 0x8E, 0xDD, 0xEC, 0x7B, 0x4A, 0x19, 0x28, 0x06, 0x37, 0x64, 0x55, 0xC2, 0xF3, 0xA0, 0x91},
  {0x47, 0x76, 0x25, 0x14, 0x83, 0xB2, 0xE1, 0xD0, 0xFE, 0xCF, 0x9C, 0xAD, 0x3A, 0x0B, 0x58, 0x69},
  {0x04, 0x35, 0x66, 0x57, 0xC0, 0xF1, 0xA2, 0x93, 0xBD, 0x8C, 0xDF, 0xEE, 0x79, 0x48, 0x1B, 0x2A},
  {0xC1, 0xF0, 0xA3, 0x92, 0x05, 0x34, 0x67, 0x56, 0x78, 0x49, 0x1A, 0x2B, 0xBC, 0x8D, 0xDE, 0xEF},
  {0x82, 0xB3, 0xE0, 0xD1, 0x46, 0x77, 0x24, 0x15, 0x3B, 0x0A, 0x59, 0x68, 0xFF, 0xCE, 0x9D, 0xAC}
};

uint8_t lookupCRC8(uint16_t data) {
  uint8_t CRC = 0xFF; //inital value
  CRC ^= (uint8_t)(data >> 8); //start with MSB
  CRC = CRC8LookupTable[CRC >> 4][CRC & 0xF]; //look up table [MSnibble][LSnibble]
  CRC ^= (uint8_t)data; //use LSB
  CRC = CRC8LookupTable[CRC >> 4][CRC & 0xF]; //look up table [MSnibble][LSnibble]
  return CRC;
}

uint8_t calcCRC8(uint16_t data) {
  uint8_t crc = 0xFF;
  uint8_t msb = byte(data >> 8);
  uint8_t lsb = byte(data);

  crc ^= msb;
  for (uint8_t b = 0; b < 8; b++) {
    if (crc & 0x80)
      crc = (crc << 1) ^ 0x31;
    else
      crc <<= 1;
  }
  crc ^= lsb;
  for (uint8_t b = 0; b < 8; b++) {
    if (crc & 0x80)
      crc = (crc << 1) ^ 0x31;
    else
      crc <<= 1;
  }
  return crc;
}

and the sketch output:

18:10:20.221 → CJMCU-30 SGP30 Test
18:10:20.255 →
18:10:20.255 → CRC test, expected: CRC (0xBEEF) = 0x92
18:10:20.255 → CRC(lookup): 0x92
18:10:20.255 → CRC(calc.) : 0x92
18:10:20.255 →
18:10:20.255 → Feature Set, got: 0x30
18:10:20.255 → CRC(received): 0x7
18:10:20.255 → CRC(lookup) : 0x44
18:10:20.255 → CRC(calc.) : 0x44
18:10:20.255 → IAQ Baseline, got: 0xCF7F:0xCF1E
18:10:20.255 → eco2_baseline CRC(received): 0x5F
18:10:20.255 → eco2_baseline CRC(lookup) : 0x64
18:10:20.255 → eco2_baseline CRC(calc.) : 0x64
18:10:20.255 → tvoc_baseline CRC(received): 0xF9
18:10:20.288 → tvoc_baseline CRC(lookup) : 0xEE
18:10:20.288 → tvoc_baseline CRC(calc.) : 0xEE
18:10:20.288 → Serial ID, got: 0x0 0 C1 0 FC FF FC F1 73
18:10:20.487 → Test result, got: 0xFE00
18:10:20.487 → Test result CRC :0xE7

arduarn:
What do you mean, did Sensirion tell you this?

Yes. After all, Sensirion is quite sure that this is a copy and not an original Sensirion SGP30 as they cannot see a laser engravement on the photo I send:


Tomorrow a colleague brings his microscope to the office and we’ll make a better picture.

Thanks for posting all the information I requested.

If it's not a genuine part then all bets are off with respect to its behaviour. Since you appear to be using it for more than just hobbyist use, I would be inclined to advise you just to chuck it away and invest in a genuine part from a reputable supplier. You will just be wasting time playing with a copy that you will never have confidence in. Besides, after the support that Sensirion have provided you with, I think they deserve a sale.

But get it confirmed first and please do keep us updated.

arduarn:
I would be inclined to advise you just to chuck it away [...snipp..]

Meanwhile,

  • I got my hands on a Sensirion Environmental Sensor Shield (ESS), which includes beside the SGP30 also a SHTC1 temperature/humidity sensor and a set of 3 smd led.

  • The ESS SGP30 runs on my code as expected. Everything works like the datasheet describes. Same with the Sensirion library, but I expected nothing different now.

  • I got also my I2C communication with the BME280 to work, but the SHTC1 fits in my case perfectly. Especially the effort of getting a value of the BME280 to display it somewhere is tremendous in comparison to the SHTC1 if you don't want to use a library and DIY. And every value from either the SGP30 or the SHTC1 is CRC8 signed.

  • me learned to not piggyback a temperature sensor on top of anything. It ruins everything, besides the view (see picture above).

  • Sensirion (CH) is investigating, me keeping in the loop. Impressive support. In maybe 2 weeks their investigation already has a result. I offered my whatever-looks-like-a-sgp30 "sensor" to Sensirion and therefor it's still on my desk until the case is closed. But there is in my understanding consensus that this is very probably a copy.

Really appreciate you posting the update. Whilst it may not stop others from buying a "copy", at least it will be a point of reference when they come looking for answers.

Sensirion seem to be a pretty cool company. There are developer resources for using the devices on their website, both for Arduino and RaspberryPi.