SCD41 を ESP32 で 動かすスニペット

性能の良い CO2 センサである SCD41 を、ライブラリなどを使わずに動かしてみて様々な機能をベア動作で確認してみます。

# 環境

- Arduino 1.8.19 portable 化済
- Linux Ubuntu 22.04 LTS Desktop Ja
# 接続
GND,VCC,SDA,SCLをそれぞれ接続しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/d0677de2-9b46-3e93-1e61-6d64d37483c2.png)

光⾳響 NDIR センサー技術

sensirion には見た目の同じような SCD40 , SCD41, SCD42 の3つのセンサーがあります。
何が違うのかな・・・調べてみたら、以下のような違いだそうです。


||測定範囲|測定精度|低消費電力|
|:---:|:---:|:---:|:---:|
|SCD40|400-2000ppm|±50 ppm||
|SCD41|400-5000ppm|±40 ppm|対応|
|SCD42|400-1000ppm|±75 ppm||

今回は低消費電力がどのようになるか調べてみます。

# コマンドリスト 

22個しかないので、簡単♪
|コマンド種類|コマンド|コード|送受信|実行時間(ミリ秒)|定期測定中のコマンド実行|
|:---:|:---:|:---:|:---:|:---:|:---:|
|基本コマンド|start_periodic_measurement |0x21b1|送信|-|不可|
|^|read_measurement |0xec05 |受信|1|可|
|^|stop_periodic_measurement| 0x3f86 |送信|500|可|
|補償コマンド|set_temperature_offset|0x241d|送信|1|不可|
|^|get_temperature_offset|0x2318|受信|1|不可|
|^|set_sensor_altitude|0x2427|送信|1|不可|
|^|get_sensor_altitude|0x2322|受信|1|不可|
|^|set_ambient_pressure|0xe000|送信|1|可|
|キャリブレーション|perform_forced_recalibration|0x362f|送信+結果取得|400|不可|
|^|set_automatic_self_calibration_enabled|0x2416|送信|1|不可|
|^|get_automatic_self_calibration_enabled|0x2313|受信|1|不可|
|省電力|start_low_power_periodic_measurement|0x21ac|送信|-|不可|
|^|get_data_ready_status|0xe4b8|受信|1|可|
|高度なコマンド|persist_settings|0x3615|送信|800|不可|
|^|get_serial_number |0x3682|受信|1|不可|
|^|perform_self_test|0x3639|受信|10000|不可|
|^|perform_factry_reset|0x3632|送信|1200|不可|
|^|reinit|0x3646|送信|20|不可|
|省電力シングルショット|mesure_signle_shot|0x219d|送信|5000|不可|
|^|mesure_signle_shot_rht_only|0x2196|送信|50|不可|
|^|power_down|0x36e0|送信|1|不可|
|^|wake_up|0x36f6|送信|20|不可|

# 動作確認

まずは、get_serial_number 0x3682 を試してみます。



```
#include <Wire.h>

uint8_t crc(const uint8_t data0, uint8_t data1) {
  uint8_t crc=0xFF ,  crc_bit;

  crc ^= data0;
  for (crc_bit = 8 ; crc_bit > 0 ; --crc_bit) {
    if (crc & 0x80){
      crc = (crc<<1) ^ 0x31;
    } else {
      crc = (crc<<1);
    }
  }
  crc ^= data1;
  for (crc_bit = 8 ; crc_bit > 0 ; --crc_bit) {
    if (crc & 0x80){
      crc = (crc<<1) ^ 0x31;
    } else {
      crc = (crc<<1);
    }
  }
  return(crc);
}

void setup() {
    Serial.begin(115200);
    delay(100);
    Serial.println("start");
}
void loop() {
    int size,j;
    byte data[9];
    Wire.begin();
    Wire.beginTransmission(0x62);
    data[0]=0x36;data[1]=0x82;
    Wire.write(data,2);
    Wire.endTransmission();
    Wire.requestFrom(0x62,9);
    
    for (size =0; Wire.available(); data[size++] = Wire.read())
    {}
    Serial.print("Serial ");
    for ( j=0; j<(size/3); j++ ){
//    Serial.printf(" %02X",data[j]);
      if ( data[j*3+2]== crc(data[j*3],data[j*3+1]))
      {
        Serial.printf("%02X%02X",data[j*3],data[j*3+1]);   
      } else {
        Serial.print( "  CRC error! ");
      }
    }
    Serial.println("");
    delay(5000);
}
```

I2Cアドレスは 0x62 です。0x3682コマンド"get_serial_number" を送ると9バイトの結果が帰ってきますが、3バイトごとに、2バイトのデータ+1バイトの CRC というフォーマットになっています。CRC チェックを通したシリアル番号を表示するようにしています。

実行結果
```
start
Serial 9C6EB7073BFF
Serial 9C6EB7073BFF
Serial 9C6EB7073BFF
...

```

同様に、 perform_self_test 0x3639 を試してみます。
上記のコードの一部を以下のように変更しましたが・・・うまくいきませんでした。

```
    Wire.beginTransmission(0x62);
    data[0]=0x36;data[1]=0x39;
    Wire.write(data,2);
    Wire.endTransmission();
    Wire.requestFrom(0x62,3);
    for (size =0; Wire.available(); data[size++] = Wire.read())
    {}
    Serial.print("self test ");
```

# 基本的な測定を試してみる

まず、start_periodic_measurement 0x21b1 を送ります。
そのあと、 read_measurement 0xec05 を送ります。
測定が終わると、stop_periodic_measurement 0x3f86 を送ります。


上記のコードの一部を更に以下のように変更します。

```
.
.
.
byte data[9];
.
.
.
void setup() {
    Serial.begin(115200);
    delay(100);
    Wire.begin();
    Wire.beginTransmission(0x62);
    data[0]=0x21;data[1]=0xb1;
    Wire.write(data,2);
    Wire.endTransmission();
    Serial.println("start ");
}
void loop() {
    int size,j;
    Wire.begin();
    Wire.beginTransmission(0x62);
    data[0]=0xec;data[1]=0x05;
    Wire.write(data,2);
    Wire.endTransmission();
    Wire.requestFrom(0x62,9);
    for (size =0; Wire.available(); data[size++] = Wire.read())
    {}
    for ( j=0; j<(size/3); j++ ){
      if ( data[j*3+2]== crc(data[j*3],data[j*3+1]))
      {
        Serial.printf(" %02X %02X  ",data[j*3],data[j*3+1]);   
      } else {
        Serial.print( "  CRC error! ");
      }
    }
    float temp,rh;
    Serial.printf("CO2 %02d ppm",data[0]*256+data[1]);
    temp = ((int) data[3]*256 + (int) data[4] )*175/65535-45;
    Serial.printf(" Temp %3.1f C", temp);
    Serial.printf(" RH %02d %\n",((int)data[6]*256+(int)data[7])*100/65535);   
    delay(5000);
}
```

測定結果
```
start 
 02 0D   65 7C   6C 26  CO2 525 ppm Temp 24.0 C RH 42 
 02 0E   65 7B   6C 0D  CO2 526 ppm Temp 24.0 C RH 42 
 02 0F   65 7A   6C 3A  CO2 527 ppm Temp 24.0 C RH 42 
 02 0F   65 72   6C 07  CO2 527 ppm Temp 24.0 C RH 42 
 02 10   65 78   6C 19  CO2 528 ppm Temp 24.0 C RH 42 
 02 14   65 80   6B FF  CO2 532 ppm Temp 24.0 C RH 42 
.
.
.
```

# 省電力機能を試してみる

データシートによると、


- 測定間のアイドル消費  0.15 mA (標準)、0.2 mA (最⼤)
- 1ショットに費やすエネルギー 243 mJ(標準) 296 mJ(最大)

試してみました。

```
#include <Wire.h>

byte data[100];
uint8_t crc(const uint8_t data0, uint8_t data1) {
  uint8_t crc=0xFF ,  crc_bit;

  crc ^= data0;
  for (crc_bit = 8 ; crc_bit > 0 ; --crc_bit) {
    if (crc & 0x80){
      crc = (crc<<1) ^ 0x31;
    } else {
      crc = (crc<<1);
    }
  }
  crc ^= data1;
  for (crc_bit = 8 ; crc_bit > 0 ; --crc_bit) {
    if (crc & 0x80){
      crc = (crc<<1) ^ 0x31;
    } else {
      crc = (crc<<1);
    }
  }
  return(crc);
}

void writeSCD4X(int command )
{
  Wire.beginTransmission(0x62);
  data[0]=(byte) ( command / 256 );data[1]= (byte) ( command % 256 );
  //Serial.printf("write: %02X %02X  ",data[0],data[1]);   
  Wire.write(data,2);
  Wire.endTransmission();
}
int readSCD4X(int command,int size )
{
  int count,j;
  writeSCD4X(command);
  delay(10);
  Wire.requestFrom(0x62,size);
  delay(10);
  for ( count = 0; Wire.available(); data[count++] = Wire.read())
  {}
  for ( j=0; j<(count/3); j++ ){
    if ( data[j*3+2]== crc(data[j*3],data[j*3+1]))
    {
      // Serial.printf(" %02X %02X  ",data[j*3],data[j*3+1]);   
    } else {
      Serial.print( "  CRC error! ");
      return(-1);
    }
  }
  //Serial.printf("return byte count: %d\n ",count );
  return(size);
}
void printCO2TEMPRH()
{
    float temp,rh;
    Serial.printf("CO2 %02d ppm",data[0]*256+data[1]);
    temp = ((int) data[3]*256 + (int) data[4] )*175/65535-45;
    Serial.printf(" Temp %3.1f C", temp);
    Serial.printf(" RH %02d %\n",((int)data[6]*256+(int)data[7])*100/65535);   
}

void measuresingleshot(){

    // measure single shot
    writeSCD4X(0x219d );
    delay(5000); 

    // get_data_ready_status
    for ( data[0] = data[1] = 0;
          0 == (0b11111111111 & (data[0]*256+data[1]));
          readSCD4X(0xe4b8,3) )
    {
      // Serial.printf( "ready_status:%02X%02X\n",data[0],data[1]);
    }
    //*/

    // read_measurement
    if ( 9 != readSCD4X(0xec05,9)) 
    {
       Serial.print("error measurement");
       return;
    }
    printCO2TEMPRH();
}

void setup() {
    Serial.begin(115200);
    delay(100);
    Wire.begin();
    // measure single shot
  //  writeSCD4X(0x219d );  
    delay(5000);
    Serial.println("start ");
}
void loop() {
  //wake_up
  writeSCD4X(0x36f6 );
  Serial.println("wake up");
    
  //get serial number    Serial: 0x9C6EB7073BFF
  if ( 9 != readSCD4X(0x3682,9) )
  {
    Serial.print("error read serial number");
    delay(10000);  
  }
  // Serial.printf( "Serial:%02X%02X%02X%02X%02X%02X\n",data[0],data[1],data[3],data[4],data[6],data[7]);

  // measure
  for ( int  i = 0; i< 5; i++){
    measuresingleshot();
  }    
    
  // powerdown SCD41
  writeSCD4X(0x36e0 );
  Serial.println("power down");
  delay(1000*300);
}
```
データシート 3.10.4 wake_up

> Description: Wake up the sensor from sleep mode into idle mode. Note that the SCD4x does not acknowledge the wake_up
command. To verify that the sensor is in the idle state after issuing the wake_up command, the serial number can be read out
(chapter 3.9.2). Note that

とあるので、シリアルナンバーを読んでいます。


測定結果

```


rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0030,len:1184
load:0x40078000,len:13160
load:0x40080400,len:3036
entry 0x400805e4
start 
wake up
CO2 810 ppm Temp 24.0 C RH 44 
CO2 881 ppm Temp 24.0 C RH 44 
CO2 864 ppm Temp 24.0 C RH 44 
CO2 922 ppm Temp 24.0 C RH 44 
CO2 905 ppm Temp 24.0 C RH 44 
power down
wake up
CO2 916 ppm Temp 21.0 C RH 48 
CO2 1005 ppm Temp 21.0 C RH 48 
CO2 977 ppm Temp 22.0 C RH 48 
CO2 1000 ppm Temp 22.0 C RH 48 
CO2 943 ppm Temp 22.0 C RH 47 
power down
wake up
CO2 1091 ppm Temp 21.0 C RH 49 
CO2 1058 ppm Temp 21.0 C RH 49 
CO2 832 ppm Temp 21.0 C RH 49 
CO2 879 ppm Temp 22.0 C RH 49 
CO2 968 ppm Temp 22.0 C RH 48 
power down
wake up
CO2 1096 ppm Temp 21.0 C RH 49 
CO2 1097 ppm Temp 21.0 C RH 49 
CO2 1097 ppm Temp 21.0 C RH 49 
CO2 1089 ppm Temp 22.0 C RH 49 
CO2 1072 ppm Temp 22.0 C RH 49 
power down
wake up
CO2 1092 ppm Temp 21.0 C RH 50 
CO2 1079 ppm Temp 21.0 C RH 50 
CO2 1070 ppm Temp 21.0 C RH 49 
CO2 1072 ppm Temp 21.0 C RH 49 
CO2 1067 ppm Temp 22.0 C RH 49 
power down

```
データシート 3.10 Low power single shot (SCD41)

> To reduce noise levels, the I2C master can perform several single shot measurements in a row and average the CO2 output values. After a power cycle, the initial single shot reading should be discarded to maximize accuracy. 

とあるので、 wake up と power down との間に5回読んでいます。