Spresense를 사용하여 Sensirion의 co2 센서 SCD41을 시도했습니다.

SONY Spresense용 코2 센서 SCD41 addon 보드를 제작해 평가한 것이다.
SCD4x 페이지
프로비저닝
이름:
유형 이름
제조사
CPU
SPRESENSE
SONY
LTE M 보드
CXD5602PWBLM1J
SONY
CO2 센서
SCD41
Sensirion
LTE SIM
할당량 10MB 계획
MEEQ
사진.

회로도
Spresenses와 SCD41은 I2C로 연결되지만 현재 등급의 이동IC는 모두 사용할 수 없기 때문에 N-Ch MOSFET로 구성되어 있습니다.

기판



3D View
다이어그램
샘플 수량은 아직 적지만 이런 느낌입니다.
(Google chart)

SPRESENSE 프로그램
이것은 시험용 절차이니 정리하지 않았으니 참고하세요.
SCD41(40)용 라이브러리는 Sensirion에서 제공하기 때문에 사용했습니다.
SCD40 arduino 라이브러리
데이터는 HTTP GET로 5분마다 자사 시스템에 전송됩니다.
SCD40_test.ino
/*
  Sample program for the Sensirion SCD30 Sensor
  By: Kazuaki Ueno
  Next Step LLC
  Date: Jun. 20th, 2021
  License: This code is public domain.

  SCD30 Datasheet: https://www.mouser.jp/datasheet/2/682/Sensirion_CO2_Sensors_SCD30_Datasheet-1901872.pdf

  This example reads the sensors calculated CO2 and TEMP and Humidity.
*/

// libraries
#include <stdlib.h>
#include <LTE.h>                                                                // LTEライブラリ
#include <Wire.h>                                                               // I2Cライブラリ
#include <Watchdog.h>                                                           // ウォッチドッグライブラリ https://github.com/janelia-arduino/Watchdog
#include <SensirionI2CScd4x.h>                                                  // SCD4Xライブラリ        https://github.com/Sensirion/arduino-i2c-scd4x
                                                                                // Sensirion Arduino Core Library  https://github.com/Sensirion/arduino-core/
                                                                                //                                 SCD4Xライブラリで include されている

SensirionI2CScd4x scd4x;

// APN data
// MEEQ
#define LTE_APN       "meeq.io"                                                 // replace your APN
#define LTE_USER_NAME "meeq"                                                    // replace with your username
#define LTE_PASSWORD  "meeq"                                                    // replace with your password
// SORACOM
// #define LTE_APN       "soracom"                                              // replace your APN
// #define LTE_USER_NAME "sora"                                                 // replace with your username
// #define LTE_PASSWORD  "sora"                                                 // replace with your password

// アップロード間隔
#define SAMPLE_NUMS (30)                                                        // 5分間隔 設定値×10秒

// Timer割り込み間隔
#define INTERVAL 1000                                                           // 1000uS = 1mS

// センシング間隔
#define SENSOR_INTERVAL (10000)                                                 // センサーの読み込み間隔(mS)

// Watchdog
#define WATCHDOG_TIME   (15000)

// initialize the library instance
// U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// SCD30 airSensor;                                                             //create an object of the SCD30 class
LTEModem modem;
LTE lteAccess;
LTEClient client;
LTEScanner scannerNetworks;

// URL, path & port (for example: arduino.cc)
char server[] = "server_url";                                                   // 使用するザーバーの情報に書き換えて下さい。
char path[200];
int  port = 80;                                                                 // port 80 is the default for HTTP

// Grubal variable
bool lte_connect = false ;
uint16_t int_counter  = 0;
uint8_t  sensing_flag = 0;
String IMEI;
char imei_char[50];

// Function prototype
uint8_t callback_func(void);
uint8_t compareFloat(const void*, const void*);
float   get_median(const float*, uint8_t);                                        // メディアンフィルター
void    printUint16Hex(uint16_t);
void    printSerialNumber(uint16_t, uint16_t, uint16_t);
void    sensing_job();

uint8_t callback_func()
{
  int_counter++ ;
  if(int_counter >= SENSOR_INTERVAL){
    sensing_flag = 1;
    int_counter = 0;
  }
  return INTERVAL;
}

uint8_t compareFloat(const void* a, const void* b)
{
    int aNum = *(float*)a;
    int bNum = *(float*)b;

    if( aNum < bNum ){
        return -1;
    }
    else if( aNum > bNum ){
        return 1;
    }
    return 0;
}

/* メディアンフィルタ処理 */
float get_median(const float* array, uint8_t size)
{
    float median;

    float* array_copy = malloc(sizeof(float) * size);
    memcpy(array_copy, array, sizeof(float) * size);
    qsort(array_copy, size, sizeof(float), compareFloat);
    median = array_copy[size / 2];
    free(array_copy);
    return median;
}

void printUint16Hex(uint16_t value)
{
    Serial.print(value < 4096 ? "0" : "");
    Serial.print(value < 256 ? "0" : "");
    Serial.print(value < 16 ? "0" : "");
    Serial.print(value, HEX);
}

void printSerialNumber(uint16_t serial0, uint16_t serial1, uint16_t serial2)
{
    Serial.print("Serial: 0x");
    printUint16Hex(serial0);
    printUint16Hex(serial1);
    printUint16Hex(serial2);
    Serial.println();
}


void sensing_job()
{
  static uint8_t counter = 0 ;
  static float co2_arry[5]  = {0.0};
  static float temp_arry[5] = {0.0};
  static float hum_arry[5]  = {0.0};

  static float co2_sum  = 0.0;
  static float temp_sum = 0.0;
  static float hum_sum  = 0.0;

  static uint16_t disp_counter = 0;

  float co2,temp,hum;
  uint16_t co2_i;
  uint16_t temp_i;
  uint16_t hum_i;

  uint8_t i;

  String rssi_str;
  int16_t  rssi;

  uint16_t error;
  char errorMessage[256];

  disp_counter++;
  Watchdog.start(WATCHDOG_TIME);

  // Obtain data from the sensors.
  error = scd4x.readMeasurement(co2_i, temp_i, hum_i);
  if (error) {
      Serial.print("Error trying to execute readMeasurement(): ");
      errorToString(error, errorMessage, 256);
      Serial.println(errorMessage);
  } else if (co2_i == 0) {
      Serial.println("Invalid sample detected, skipping.");
  } else {
      co2 = co2_i * 1.0;
      temp = temp_i * 175.0 / 65536.0 - 45.0;
      hum  = hum_i * 100.0 / 65536.0;
  }

  // Median Filtering.
  for(i = 1 ; i < 5; i++){
    co2_arry[i-1]  = co2_arry[i];
    temp_arry[i-1] = temp_arry[i];
    hum_arry[i-1]  = hum_arry[i];
  }
  co2_arry[4]  = co2;
  temp_arry[4] = temp;
  hum_arry[4]  = hum;

  co2   = get_median(co2_arry,5);
  temp  = get_median(temp_arry,5);
  hum   = get_median(hum_arry,5);
  // End Median Filtering.
  co2_sum  += co2;
  temp_sum += temp;
  hum_sum  += hum;

  // Data sent to serial port
  Serial.print("Counter: ");
  Serial.print(disp_counter);
  Serial.print(" ");

  Serial.print("CO2: ");
  Serial.print(co2,0);
  Serial.print("ppm");

  Serial.print("  RH: ");
  Serial.print(hum, 2);
  Serial.print("%  TEMP: ");
  Serial.print(temp, 2);
  Serial.println("℃");

  counter++;

  // Data upload to Thingspeak.
  if(counter >= SAMPLE_NUMS){
    if(lte_connect == true){
      // read signal strength
      rssi_str = scannerNetworks.getSignalStrength();
      rssi = rssi_str.toInt(); 
      Serial.print("RSSI : ");
      Serial.println(rssi);

      // データ送信先と送信データのセット
      sprintf(path,"/uec/sensor.php?id=%s&co2=%3.0f&temp=%5.2f&hum=%5.2f&rssi=%2d",imei_char,co2_sum/SAMPLE_NUMS,temp_sum/SAMPLE_NUMS,hum_sum/SAMPLE_NUMS,rssi);
      Serial.print(server);
      Serial.println(path);

      if (client.connect(server, port)) {
        Serial.println("connected");
        // Make a HTTP request:
        client.print("GET ");
        client.print(path);
        client.println(" HTTP/1.1");
        client.print("Host: ");
        client.println(server);
        client.println("Connection: close");
        client.println();
        Serial.println("Data send.");
      } else {
        // if you didn't get a connection to the server:
        Serial.println("connection failed");
      }
      delay(100);

      // if the server's disconnected, stop the client:
      if (!client.available() && !client.connected()) {
        Serial.println();
        Serial.println("disconnecting.");
        client.stop();

      // do nothing forevermore:
        for (;;)
          sleep(1);
      }
    }
    co2_sum  = 0.0;
    temp_sum = 0.0;
    hum_sum  = 0.0;
    counter = 0;
  }

  // Watch dog clear
  Watchdog.kick();
}

void setup()
{
  uint16_t error;
  char errorMessage[256];
  Wire.begin();

  // initialize serial communications and wait for port to open:
  Serial.begin(115200);
  while (!Serial) {
      ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println("SCD41 Example");

  if (LTE_IDLE == modem.begin()) {               
    Serial.println("modem.begin() succeeded.");
  } else {
    Serial.println("ERROR, no modem answer.");
  }
  scd4x.begin(Wire);

  // stop potentially previously started measurement
  error = scd4x.stopPeriodicMeasurement();
  if (error) {
    Serial.print("Error trying to execute stopPeriodicMeasurement(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  }

  uint16_t serial0;
  uint16_t serial1;
  uint16_t serial2;

  error = scd4x.getSerialNumber(serial0, serial1, serial2);
  if (error) {
    Serial.print("Error trying to execute getSerialNumber(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  } else {
    printSerialNumber(serial0, serial1, serial2);
  }

  // Start Measurement
  error = scd4x.startPeriodicMeasurement();
  if (error) {
    Serial.print("Error trying to execute startPeriodicMeasurement(): ");
    errorToString(error, errorMessage, 256);
    Serial.println(errorMessage);
  }

IMEI = modem.getIMEI();
  IMEI.toCharArray(imei_char, 50);

  Serial.print("Modem IMEi = ");
  Serial.println(IMEI);

  /* LTE 通信開始 */
  Serial.println("Starting web client.(LTE connection)");

  // If your SIM has PIN, pass it as a parameter of begin() in quotes
  while (true) {
    if (lteAccess.begin() == LTE_SEARCHING) {
      if (lteAccess.attach(LTE_APN, LTE_USER_NAME, LTE_PASSWORD) == LTE_READY) {
        Serial.println("attach succeeded.");
        lte_connect = true;
        break;
      }
      Serial.println("An error occurred, shutdown and try again.");
      lteAccess.shutdown();
//      sleep(1);
      break;
    }
  }  
  // Watch dog start
  attachTimerInterrupt(callback_func, INTERVAL);
  Watchdog.begin();
}

void loop()
{
  if(sensing_flag == 1){
    sensing_job();
    sensing_flag = 0;
  }
}
어제부터 데이터를 찾기 시작했기 때문에 평가는 어렵지만 데이터는 실용적이라고 생각합니다.
같은 곳에서.스카이프를 사용한 장치에서도 측정하고 있는데 데이터가 더 쌓이면 비교해보자.
2021년 9월 7일 추기
Sensirion의 SCD30

좋은 웹페이지 즐겨찾기