Embedded/Arduino

아두이노 나노(Arduino Nano)로 EXSEN CO2 Sensor(RX-9) 테스트

변화의 물결1 2024. 5. 8. 00:29

 

 

안녕하세요.

 

  지난번 9가지 가스센서 테스트를 진행했습니다. 그러나 CO2를 감지하는 센서는 포함되어 있지 않았습니다. 그래서 조금 금액이 나가지만, CO2 수치를 확인할 수 있는 센서를 테스트해 보았습니다.

 


 

1. CO2센서

 

  "이산화탄소 센서 또는 CO2 센서는 이산화탄소의 측정을 위한 장비이다. CO2 센서의 가장 공통적인 원리는 적외선 가스 센서 ( NDIR)와 화학 가스 센서이다. 이산화탄소를 측정하는 것은 실내 공기 품질과 여러 산업 공정을 모니터 하는데 중요하다. 많은 공조기(에어컨)에서 이들 센서는 공기의 품질을 모니터 하는 데 사용될 수 있다.

 

NDIR의 원리는 CO나 CO2 등 가스상 물질들이 적외선(Infrared light)에 대해 특정한 흡수 스펙트럼을 갖는 것을 이용해서 특정성분의 농도를 구하는 방법으로, 이산화탄소가 흡수하는 주파수의 적외선을 쏘고 이산화탄소 분자에 흡수되지 않고 검출되는 적외선의 양을 측정하는 방식이다."

 - 위키백과 참조

 

 

   CO2를 가스센서라고 볼 경우 접촉식, 복합식, 광학식으로 나눌 수 있는데 비교표로 만들어 놓은 자료는 아래 링크를 참조하시면 도움이 될 것입니다.

http://www.ntrexgo.com/archives/38522

 

[60호]주목받는 비분산적외선 가스센서란 무엇인가 | NTREXGO - 디바이스마트, 엔티렉스 컨텐츠 통

[60호]주목받는 비분산적외선 가스센서란 무엇인가 Posted by 디바이스마트 매거진 on Tuesday, June 30, 2020 · Leave a Comment  이엘티센서 주목받는 비분산적외선 가스센서란 무엇인가 글 | 이엘티센서 천

www.ntrexgo.com

 

 

2. RX-9 스펙

 

RX-9

 

 

  RX-9 시리즈를 보면 RX-9과 RX-9 Simple로 나눠집니다. RX-9는 기본적으로 PPM 수치를 표시할 수 있도록 만들어진 제품이라 보정할 수 있는 온도센서 값을 받을 수 있습니다. 그러나 RX-9 Simple 조금 가격을 싸게 해서 위험 단계로 표시할 수 있도록 만들어진 것으로 보입니다.

 

  "RX-9은 시장의 요구인 정확도, 가격 경쟁력, 소형화, 장수명을 구현한 센서로써, 경쟁방식(NDIR)과 비교해도 손 색이 없는 정확도와 세계 최고의 가격 경쟁력, 최소 사이즈(20 mm x 12 mm) 그리고 약 10년의 수명을 갖추고 있습니다. 또한, RX-9은 두 가지 모드를 제공합니다. 개발자가 구현하고자 하는 시스템에서 표시하려고 하는 이산화탄소 농도가 단계 혹은 색이라면 RX-9 Simple이 적합하고, 농도 출력(ppm)이 필요하다면 RX-9를 사용하면 됩니다."

– EXSEN 참조

 

 

RX-9 Series Spec

 

 

3. 연결방법

 

  RX-9 구매 사이트에 매뉴얼을 참고하였고, 매뉴얼 상 잘못된 부분을 찾아 담당자분에게 전달하였습니다. 매뉴얼대로 하면 센서 값을 정상적으로 받을 수 없을 수도 있으니 참조하시면 도움이 될 겁니다.

 

 

1) CO2 센서 작업

 

  - CO2 센서에 달린 헤더 핀이 간격이 2.45mm 핀이 아니라, 2.0mm입니다. 많이 사용하고 있는 2.54mm 소켓을 가지고 있다면 맞지 않을 겁니다. 그래서 센서 뒷면에 별도로 납땜해서 사용했습니다. 그리고 글루건으로 잘 떨어지지 않게 고정을 해주었습니다.

 

 

Sensor 케이블 작업

 

 

2) 아두이노 나노와 CO2 연결

 

   왼쪽은 기존 매뉴얼 사진, 오른쪽은 제가 수정한 사진으로, CO2센서에 외부 전원을 연결할 경우 나노와 GND핀을 연결하여 기준 전압을 맞춰주어야 정상적인 아날로그 값을 수신할 수 있습니다.  매뉴얼상 스위치와 LCD도 달려있는 것이 있는데 소스는 시리얼 데이터로 결과를 볼 수 있게 작성되어 있습니다.

 

  CO2에 3.3V 외부 전원을 인가해주어야 합니다. 이유는 센서에 히터(Heater)가 있어 130mA 이상 전류가 필요할 수 있기 때문에 아두이노 핀으로는 무리가 있기 때문에 별도의 전원에 연결해 줍니다.

 

 나노 A0핀 - CO2센서 E핀(EMF),  나노 A1핀 – CO2센서 T핀(Temperature)

 

 

매뉴얼 수정사항

 

 

3) 실제 연결

 

  - 외부 전원(Step down regulator)을 사용해서 3.3V를 공급해 주었습니다.

 

 

아두이노 나노와 RX-9 실제 연결

 

 

4. 소스 확인

 

  몇 가지 매뉴얼 상에 보정 버전 등 소스가 나와 있었는데 현재는 2가지 정도로 정리된 것으로 보입니다.

소스에는 ABC(Auto Baseline Calibration) 알고리즘이 적용되어 있습니다. 그리고 EEPROM Read/Write 하는 부분도 들어 있습니다.

  가끔 사이트 링크가 깨어져서 소스를 볼 수 없는 경우가 생겨서 전체를 복사해서 남겨두었습니다.

 

1) PPM 수치 등 여러 가지 데이터가 표시되는 소스

 

https://colorscripter.com/s/Jnrf03K

/*
 * coded by EXSEN 
 * date: 2019.09.30  
 * EMF pin connected to A0 of Arduino
 * THER pin connected to A1 of Arduino
 * 3.3V external power supply connected to V of RX-9 simple
 * RX-9 GND connected to arduino
 * Arduino GND connected external power  supply
 * file name = RX-9_PPM_2CH_ABC_ORIGINAL_CODE_190930
 * source code = GITHUB.com/EXSEN/RX-9
 * tested by arduino nano V3.0 328p
 */
#include <EEPROM.h>
//EARTH
int CO2_EARTH = 414; //ABC
int CO2_BASE_Compensation = 35; //EXSEN nearby road in downtown
float under_cut = 0.99; //최저값의 99% 이하의 값이 되면 센서값을 업데이트함(EMF_INIT())
/* 센서를 설치할 장소가 도시인 경우 지구 CO2 농도(CO2_EARTH)에 값을 더해줘야 합니다.
 * 주변에 차량이동이 많은 곳인경우: 40~80
 * 차량이동이 적지만, 왕복 4차선 정도의 도로가 있고, 차량 통행이 잦은 경우: 20~60
 * 도로가 인접해있지는 않지만, 도심인 경우: 20~40
 * 교외인경우 10~20
 * 통행량이 적은 왕복 2차선 주변인 경우: 10~20
 * 도로와 걸어서 10분 이상 떨어진 경우: 0~10
 * 도로와 이산화탄소의 농도 최저값을 연결시킨 이유는 실제로 가솔린 기관에서 CO2가 많이 발생하기 때문에
 * 출/퇴근 시간에는 이산화탄소 농도가 그렇지 않은 시간에 비해 약 50 ppm 정도 차이가 날 정도로 차량에서
 * 발생하는 이산화탄소 농도가 주변에 미치는 영향이 큽니다. 
 * if you live in city, you add more value. 
 * below is normal compensation value at your residence
 * nearby heavy traffic road = 40~80;
 * downtown = 20~40;
 * Suburb = 10~20;
 * Country apart from road = 0~10;
 * Country nearby road = 10~20;
 * 
 */
//Timing Setting
int warm_up_time = 180; 
/*Warming up 시간은 센서를 특정 온도까지 가열하는데 걸리는 시간입니다.
 * 센서가 완전히 식은(1일 이상 비가동) 상태에서는 수시간에서 수십시간의 안정화 시간이 필요합니다.
 * 많은 시간이 필요한 이유는 센서 내부에 흡착된 수분, 기타 가스상들을 제거하기 까지 걸리는 시간과
 * 그것을 보상하는 알고리즘이 원할하게 돌아가게 하기 위한 시간입니다. 
 * 센서를 계속해서 사용하던 중이라면 warm-up 시간이면 충분히 안정화가 됩니다. 
 */
unsigned long current_time = 0; //1초마다 값을 계산하고 출력하기 위해 타이밍 계산용 변수입니다.
unsigned long prev_time = 0;  //1초마다 값을 계산하고 출력하기 위해 타이밍 계산용 변수입니다.
unsigned long prev_time_METI = 0; //ABC 자동보정을 주기적으로 실행하기 위한 변수입니다.
int set_time = 1; //set_time: 1인경우 1초마다, 10인경우 10초마다
//PIN
int EMF_PIN = 0; //아두이노 아날로그 핀 0번에 RX-9의 E 핀이 연결된 경우
int THER_PIN = 1; // 아두이노 아날로그 핀 1번에 RX-9의 T 핀이 연결된 경우
//moving averaging of Sensor value
int averaging_count_EMF = 10; 
//몇 포인트에 대한 이동평균을 계산할 것인가에 대한 변수, 10이면 10개, 100이면 100개, 
//숫자가 커지면 반응이 느려지는 대신에 데이터가 튀지않고 깔끔해지고 숫자가 작아지면 
//반응이 빨라지는 대신이 데이터가 다소 튈 수 있음
float EMF = 0.0;  //raw data of EMF, 이동 평균 계산하기 전 EMF 값
float EMF_AVR[11] = {0,}; //평균값을 저장하기 위한 공간, averaging_count가 10이기 때문에 11로 설정함, averaging_count가 커지면 더 큰 값으로 변경해야 함
int EMF_count = 0; //이동 평균 관련 변수, EMF_count가 averaging_count 보다 작은 경우에는 이동평균을 계산하지 않음, 그렇기 때문에 그 시간에는 EMF 값이 업데이트 되지 않음
float EMF_data = 0;//이동 평균한 EMF 값
float EMF_SUM = 0; //이동 평균 관련 변수
int averaging_count_THER = 10; //EMF의 이동평균과 동일함, THER의 이동평균 관련 변수
float THER = 0.0; //2CH
float THER_AVR[11] = {0,}; //2CH
int THER_count = 0; //2CH
float THER_data = 0; //2CH
float THER_SUM = 0; //2CH
float THER_ini = 0; //2CH
//THERMISTOR constant number
//THERMISTOR 계산을 위한 상수입니다. 변경 불필요
#define C1 0.00230088
#define C2 0.000224
#define C3 0.00000002113323296
float Resist_0 = 15;
//switch
//스위치를 연결한 경우, Manual cal을 하드웨어로 동작시킬 경우 필요합니다.
int sw = 2;
int sw_status = 0;
//EEPROM ADDRESS
bool full_init = 0; //ERASE EEPROM data, 아두이노는 그대로 인데, 센서가 변경되는 경우 EEPROM에 쓰여 있는 값을 모두 초기화해야 오동작이 없음
//cal_A값 저장용 ADDRESS, cal_A는 RX-9의 뒷면에 붙어 있는 QR code에 적혀있는 정보
int cal_A_ADD_01=4; //ppm
int cal_A_ADD_02=5; //ppm
int cal_A_ADD_03=6; //ppm
//CANU 저장용 ADDRESS, CANU는 CAlibration NUmber의 약자로서 Calibration을 몇 번 진행했는지에 대한 정보임
//Manual CAL과 ABC의 횟수가 이에 해당되고 두 값은 공통으로 1씩 증가함
int CANU_ADD_01 = 2; //ABC
int CANU_ADD_02 = 3; //ABC
//EMF_max, ABC 계산용 변수, 특정 주기 내에 최대 EMF_max 값을 로깅하여 그 값을 기준으로 센서를 자동보정함
int EMF_max_ADD_01=12; //ABC
int EMF_max_ADD_02=13; //ABC
int EMF_max_ADD_03=14; //ABC
//THER_ini 온도보상을 하기위한 변수
int THER_ini_ADD_01=17; //2CH
int THER_ini_ADD_02=18; //2CH
//EMF_max와 마찬가지로 자동보정을 하기 위해 EMF_max일 때의 THER값을 받아와서 THER_max로 저장함
int THER_max_ADD_01=15; //2CH
int THER_max_ADD_02=16; //2CH
//Status of Sensor
bool Sensor_status = 0; //warming-up 시간 내에는 0, 이후에는 1
bool Reset_mode = 0; //setup()에서 init_off와 canu의 숫자를 비교해서 껏다 켤 때마다 EMF_init()를 할 것인지
// 아니면 지정한 숫자만큼한 할 것인지
// 아에 하지 않을 것인지를 결정함, 단순 변수이므로 변경 불필요
int INIT_OFF = 0;
// 중요 변수(INIT_OFF)
// 위 Reset_mode에서 설명한 것과 같이 초기 리셋을 할 것인지 몇번할 것인지를 결정하는 중요한 변수
// 0인 경우: 켤 때마다 warming up 종료 후 리셋
// 특정 수인 경우: CANU가 해당 숫자가 될 때 까지 리셋
// 1인 경우: 최초 1회만 리셋하고 이후 리셋하지 않음, 사실상 해제와 같음
// 시스템에서 Manual CAL을 할 수 있도록 하드웨어/소프트웨어 기능을 사용자에게 제공하는 경우에는
// 100 정도의 값을 설정하는 것을 권장하고, 고객이 직접 Manual CAL을 실행할 수 있도록 가이드해야함
// Manual CAL의 조건, 주변이 완전히 환기가 되었고 센서 구동이 30분 이상 계속된 상태에서 manual_cal 실행
// Manual/CAL을 고객이 본인의 의지로 할 수 없다면 0으로 설정하기를 권장함
//STEP of co2
//RX-9 Simple의 경우, 단계표시를 할 때, 몇 ppm 기준으로 단계를 나눌 것인지의 기준
int V1 = 700; //700 ppm 미만: 매우 좋음, 700 ppm 이상: 좋음, 
int V2 = 1000; //1000 ppm 이상: 나쁨
int V3 = 2000; //2000 ppm 이상: 매우 나쁨
int STEP_status = 0;
//STEP_status = 0: 0단계: 매우 좋음
//1단계: 좋음
//2단계: 나쁨
//3단계: 매우 나쁨
//Calibration data
//RX-9의 뒷면에 붙어 있는 QR code 교정값
float cal_A = 372.1; //<--- QR CODE DATA
float cal_B = 56.0; //<--- QR CODE DATA
float cal_B_offset = 1.0; //기구별로 cal_B값이 다르게 구현되기 때문에 완제품 완성 후 보정값 적용해야 함
float CO2_ppm = 0.0;
float DEDT = 1.00; //<--- QR CODE DATA, 기본적으로 1.00을 기본으로 함, 기구물이 완성되면 해당 기구물상수로 넣어주면 됨. 굳이 업데이트하지 않아도 됌
//Auto Calibration coeff
//자동보정(ABC)를 실행하기 위한 변수들
int32_t MEIN = 1440; //자동보정 주기, Default = 1440, 1440 = 1 day, 2880 = 2 day
int32_t METI = 60; //ABC, EMF_max를 비교하기 위한 주기, 60인경우, 60초마다 1회 EMF_max 값을 비교하여 필요한 경우 EMF_max를 업데이트함
float EMF_max = 0; //ABC
float THER_max = 0; //2CH
int ELTI = 0; //ABC, ELapsed TIme의 약자, METI가 60인 경우 60초에 한번씩 비교하는데, 1회 비교시 ELTI가 1씩 증가함, ELTI가 MEIN에 설정한 값과 같거나 커지면 자동보정을 실행함
int upper_cut = 0; 
//EMF_max를 업데이트하기 위한 변수, 일시적인 EMF의 튐이 자동보정의 계수로 사용되면 안 되기 때문에
// 장시간 동안 EMF_max보다 현재 EMF_data가 높으면 그 때, EMF_max를 업데이트함, 
// 장시간: 현재 계산수준으로 METI x 3: 3분
int under_cut_count = 0; //현재 출력되는 ppm 값이 CO2_EARTH + CO2_BASE_Compensation 보다 작은 경우 count
//Operating time checking
int32_t CANU = 0; //ABC, CANU는 CAlibration NUmber의 약자로서 Calibration을 몇 번 진행했는지에 대한 정보임
void setup(){
  Serial.begin(9600); //아두이노 - PC간 시리얼 연결 속도, Default = 9600
  pinMode(sw, INPUT_PULLUP); //Manual CAL용 스위치 핀모드 설정
  delay(1000);
  
  //parameter init
  //주요 파라미터들을 EEPROM의 값을 불러와서 계산에 사용하기 위해서 초기화 시켜줌
  
  cal_A = EEPROM.read(cal_A_ADD_01)*256 + EEPROM.read(cal_A_ADD_02) + (float)EEPROM.read(cal_A_ADD_03)/100; //ppm
  if(cal_A<0){cal_A = 0;} //ABC
  CANU = EEPROM.read(CANU_ADD_01)*256+(float)EEPROM.read(CANU_ADD_02); //ABC
  if(CANU<0){CANU = 0;} //ABC
  EMF_max = EEPROM.read(EMF_max_ADD_01)*256 + EEPROM.read(EMF_max_ADD_02) + (float)EEPROM.read(EMF_max_ADD_03)/100;  //ABC
  if(EMF_max<0){EMF_max = 0;} //ABC
  THER_max = EEPROM.read(THER_max_ADD_01)+EEPROM.read(THER_max_ADD_02)/100; //2CH
  if(THER_max<0){THER_max = 0;} //2CH
  THER_ini = EEPROM.read(THER_ini_ADD_01) + (float)EEPROM.read(THER_ini_ADD_02)/100;
  if(THER_ini<0){THER_ini = 0;} //2CH  
  //parameter init END
  //Reset on/off
  //3분 리셋을 할 것인지 말 것인지를 정하는 부분
  if(CANU<INIT_OFF || INIT_OFF == 0){
    Reset_mode = 1;
  }
  else{
    Reset_mode = 0;
  }
  
  //Erase EEPROM 
  //EEPROM을 초기화하기 위한 부분
  if(full_init){
    for(int i = 0;i<1024;i++){
      EEPROM.write(i,0);
      delay(10);
    }
  //Erase EEPROM END
  }
}
void loop(){
  //Timing check
  current_time = millis()/1000;
  if(current_time - prev_time >= set_time){ //every 1 second, if you change set_time to 60, every 1 minute
    EMF = analogRead(EMF_PIN);
    EMF = EMF/1024; //10 bit resolution
    EMF = EMF *5; //max voltage = 5V
    EMF = EMF / 6; // opamp value = 6
    EMF = EMF * 1000; //mV to V
    delay(1);
    THER = analogRead(THER_PIN);
    delay(1);
    THER = 1/(C1+C2*log((Resist_0*THER)/(1024-THER))+C3*pow(log((Resist_0*THER)/(1024-THER)),3))-273.15;  
    
    
    //EMF Moving Averaging START
    if(EMF_count<averaging_count_EMF){
      EMF_AVR[EMF_count] = EMF;
      EMF_count++;
    }
    else if(EMF_count >= averaging_count_EMF){
      for(int i = 0;i<averaging_count_EMF;i++){
        EMF_SUM = EMF_SUM+EMF_AVR[i];
        EMF_AVR[i] = EMF_AVR[i+1];
      }
      EMF_data = EMF_SUM/averaging_count_EMF;
      
      EMF_SUM = 0;
      EMF_AVR[averaging_count_EMF] = EMF;
    }
    //EMF Moving Averaging END
    //THER Moving Averaging START
    if(THER_count<averaging_count_THER){
      THER_AVR[THER_count] = THER;
      THER_count++;
    }
    else if(THER_count >= averaging_count_THER){
      for(int i = 0;i<averaging_count_THER;i++){
        THER_SUM = THER_SUM+THER_AVR[i];
        THER_AVR[i] = THER_AVR[i+1];
      }
      THER_data = THER_SUM/averaging_count_THER;
      
      THER_SUM = 0;
      THER_AVR[averaging_count_THER] = THER;
    }
    //THER Moving Averaging END
    //3분 리셋
    if(current_time >= warm_up_time && Sensor_status == 0){
      if(Reset_mode){
        EMF_init(EMF_data, THER_data);
      }
      Sensor_status = 1;
    }
    else{
      //do nothing
    }
    //3분 리셋 END
    //Step, ppm calculation START
    if(Sensor_status == 1){
      PPM_CAL(EMF_data,THER_data); //warming-up 종료 후 ppm 계산
      //Define STEP START
      if(CO2_ppm <= V1 && CO2_ppm >= (CO2_EARTH + CO2_BASE_Compensation)){
        STEP_status = 0; 
        under_cut_count = 0; //ABC
      }
      else if(CO2_ppm > V1 && CO2_ppm <= V2){
        STEP_status = 1;
        under_cut_count = 0; //ABC
      }
      else if(CO2_ppm > V2 && CO2_ppm <= V3){
        STEP_status = 2;
        under_cut_count = 0; //ABC
      }
      else if(CO2_ppm >= V3){
        STEP_status = 3;
        under_cut_count = 0; //ABC
      }
      else if(CO2_ppm <= ((CO2_EARTH + CO2_BASE_Compensation) * under_cut)){
        under_cut_count++; //ABC
        if(under_cut_count>5){ //ABC
          EMF_init(EMF_data, THER_data); //ABC
          under_cut_count = 0; //ABC
        }
        else{
          //do nothing
        }
      }
      //Define STEP END
      //ABC START
      if(current_time - prev_time_METI >= METI && Sensor_status == 1){
        if(ELTI<MEIN){
          ELTI++;
        }
        else if(ELTI>=MEIN){
          Auto_CAL(); //2CH, ABC
        }
        if(EMF_max >= EMF_data){
          //do nothing
          upper_cut = 0;
        }
        else if(EMF_max < EMF_data){
          upper_cut++;
          if(upper_cut > 3){
            EMF_max = EMF_data; //ppm
            THER_max = THER_data; //2CH
            upper_cut = 0; //ppm
          }
        }
        prev_time_METI = current_time;
      }
      //ABC END
    }
    //Step, ppm calculation END
    
    Serial.print("TIME = ");
    Serial.print(current_time);
    Serial.print(" CO2 ppm = ");
    Serial.print(CO2_ppm,0);
    Serial.print(" EMF = ");
    Serial.print(EMF_data,0);
    Serial.print(" Cal_A = ");
    Serial.print(cal_A,0);
    Serial.print(" Cal_B = ");
    Serial.print(cal_B,0);
    
    Serial.print(" THER = ");
    Serial.print(THER_data,0);
    Serial.print(" THER_ini = ");
    Serial.print(THER_ini,0);
    Serial.print(" ELTI = ");
    Serial.print(ELTI);
    
    Serial.print(" EMF_max = ");
    Serial.print(EMF_max,0);
    Serial.print(" THER_max = ");
    Serial.print(THER_max,0);
    
    Serial.print(" CO2 STEP = ");
    Serial.print(STEP_status);
    Serial.print(" : ");
    switch(STEP_status){
      case 0:
              
              if(Sensor_status != 1){
              Serial.print("W/U");
              }
              else{
              Serial.print("Fresh");
              }
              break;
      case 1:
              Serial.print("Good");              
              break;
      case 2:
              Serial.print("Bad");
              break;
      case 3:
              Serial.print("Very Bad");
              break;
    } 
    Serial.print(", Sensor status =");
    if(Sensor_status == 0){
      Serial.print(" WR");
    }
    else{
      Serial.print(" NR");
    }
      Serial.print(" CANU = ");
      Serial.print(CANU);
      Serial.println(" ");
    prev_time = current_time;    
  }
  //Timing check END
  
  if(digitalRead(sw) == LOW){
    EMF_init(EMF_data, THER_data);   
    delay(300);
  }
}
void EMF_init(float EMF_data, float THER_data){
  THER_ini = THER_data; //2CH
  EMF_max = EMF_data; //ABC
  THER_max = THER_data; //2CH
  ELTI = 0;  //ABC
  CANU++;  //ABC
  cal_A = EMF_data +log10(CO2_EARTH + CO2_BASE_Compensation)*cal_B;
  
  EEPROM.write(cal_A_ADD_01,(int)cal_A/256);
  EEPROM.write(cal_A_ADD_02,(int)cal_A%256);
  EEPROM.write(cal_A_ADD_03,(int(cal_A*100)%100));
  EEPROM.write(CANU_ADD_01,CANU/256); //ABC
  EEPROM.write(CANU_ADD_02,CANU%256); //ABC
  EEPROM.write(EMF_max_ADD_01,(int)EMF_max/256); //ABC
  EEPROM.write(EMF_max_ADD_02,(int)EMF_max%256); //ABC
  EEPROM.write(EMF_max_ADD_03,(int(EMF_max*100)%100)); //ABC
  EEPROM.write(THER_max_ADD_01,(int)THER_max%256); //2CH
  EEPROM.write(THER_max_ADD_02,(int(THER_max*100)%100)); //2CH
  EEPROM.write(THER_ini_ADD_01,(int)THER_ini%256); //2CH
  EEPROM.write(THER_ini_ADD_02,(int(THER_ini*100)%100)); //2CH
}    
void PPM_CAL(float EMF_data, float THER_data){
  CO2_ppm = pow(10,((cal_A-(EMF_data+DEDT*(THER_ini-THER_data)))/(cal_B*cal_B_offset)));
}
void Auto_CAL(){
  //cal_A = EMF_max +log10(CO2_EARTH + CO2_BASE_Compensation)*cal_B;
  cal_A = (EMF_max + DEDT*(THER_max - THER_data)) +log10(CO2_EARTH + CO2_BASE_Compensation)*cal_B;
  THER_ini = THER_data; //2CH
  EMF_max = EMF_data; //ABC
  THER_max = THER_data; //2CH
  THER_ini = THER_data; //2CH
  
  ELTI = 0; //ABC
  CANU++;  //ABC
  
  EEPROM.write(CANU_ADD_01,CANU/256); //ABC
  EEPROM.write(CANU_ADD_02,CANU%256); //ABC
  
  EEPROM.write(cal_A_ADD_01,(int)cal_A/256);
  EEPROM.write(cal_A_ADD_02,(int)cal_A%256);
  EEPROM.write(cal_A_ADD_03,(int(cal_A*100)%100));
  EEPROM.write(EMF_max_ADD_01,(int)EMF_max/256); //ABC
  EEPROM.write(EMF_max_ADD_02,(int)EMF_max%256); //ABC
  EEPROM.write(EMF_max_ADD_03,(int(EMF_max*100)%100)); //ABC
  EEPROM.write(THER_max_ADD_01,(int)THER_max%256); //2CH
  EEPROM.write(THER_max_ADD_02,(int(THER_max*100)%100)); //2CH
  EEPROM.write(THER_ini_ADD_01,(int)THER_ini%256); //2CH
  EEPROM.write(THER_ini_ADD_02,(int(THER_ini*100)%100)); //2CH
  
}

 

 

 

2) Library로 구현할 수 있는 버전(QR코드 보정값 적용할 수 있는 코드)

 

https://github.com/EXSEN/RX-9    

 

/*
   coded by EXSEN
   date: 2020.03.27
   CO2 sensor is attached to ATMEGA328P, 16 Mhz, 5V
   Board Ver: not specified
   file name: RX-9_SAMPLE_CODE_WO_EEP_200303.ino
   you can ask about this to ykkim@exsen.co.kr
   CAUTIONS
   1. Don't use 3.3V of arduino to RX-9 V, RX-9 consume more than arduino's 3.3V output.
   2. Use external 3.3V source. you can use this (https://ko.aliexpress.com/item/1996291344.html?spm=a2g0s.9042311.0.0.27424c4dytyPzF)
   All remarks are written under the code to explain
   모든 주석은 설명하고자 하는 코드 아래에 쓰여집니다.
*/
const String VER = "RX-9_SAMPLE_CODE_WO_EEP_200312";

#define EMF_pin 0  //RX-9 E, Analog, A0 of Arduino, 아날로그 핀 연결 번호
#define THER_pin 1 //RX-9 T, Analog, A1 of Arduino, 아날로그 핀 연결 번호

#define Base_line 432
/* 지구의 최저 이산화탄소 농도, 국제 표준 이산화탄소 농도인 하와이 측정값은 2020년에 413.4 ppm 이었음
   하지만, 하와이는 매우 청정한 구역이기 때문에 우리가 지내는 장소와는 최저 농도가 차이가 있음
   이를 보정하기 위해 본 센서의 최저농도를 하와이의 농도보다는 약간 더 높게 설정할 필요가 있음
   보정 수치
      - 대도시, 주변에 교통량이 많은 경우: +40~80 ppm
      - 대도시, 보통 통행량: +20~40 ppm
      - 교외 지역: + 10~20 ppm
      - 도로로부터 10분이상 떨어진 곳, 차량이 근접할 수 없는 곳: + 0~10 ppm
   일반적으로 가솔린 엔진 기관에서 이산화탄소가 발생하기 때문에 차량의 이동이 잦은 곳에서는 최저 이산화탄소 농도가 높음
*/
// Lowest earth CO2 concentration 2020, in Hawaii is 413.4 ppm
// You can check this data at https://www.esrl.noaa.gov/gmd/ccgg/trends/
// but as you know, Hawaii don't emit mass CO2. so we add some number to Hawaii data.
// Where you are live in
//   - Big city and nearby huge traffic road: 40~80 ppm add to Hawaii ppm
//   - Big city and normal traffic road: 20~40 ppm
//   - Suburbs: 10~20 ppm
//   - 10 minutes on foot from traffic road, car can not reach: 0~10 ppm
//   normally gasoline engine makes huge carbon dioxide. if you use this sensor to indoor you should check your environment like above.

const int32_t max_co2 = 6000;
/*  RX-9은 400~4000 ppm 의 농도에서 신뢰할 수 있는 수치를 나타냄
    그 이상의 농도에서도 측정은 가능하지만, 정확도가 떨어짐
    그래서 EXSEN은 4000 ppm 혹은 6000 ppm을 최대 농도로 제한할 것을 추천함
*/
// RX-9 is reliable 400 ~ 4000 ppm of CO2.
// RX-9 can measure 10000 ppm or more. but it shows little low accuracy.
// So, EXSEN recommend max_co2 as 6000 ppm.

//Timing Setting
int warm_up_time = 180; //default = 180, Rx-9 takes time to heat the core of sensor. 180 seconds is require.
unsigned long current_time = 0;
unsigned long prev_time = 0;
int set_time = 1;  // default = 1, every 1 second, output the data.
/*
   특정 주기별로 센서의 값을 계산하고 출력하기 위한 Timing 관련 변수
   기본적으로 warm-up 시간은 3분, 180초로 함
   센서 출력 및 계산 주기는 1초(set_time)으로 함
*/

//Moving Average
#define averaging_count 10 // default = 10, moving average count, if you want to see the data more actively use lower number or stably use higher number
float m_averaging_data[averaging_count + 1][2] = {0,};
float sum_data[2] = {0,};
float EMF = 0;
float THER = 0;
int averaged_count = 0;
/*
   10개의 데이터를 이동평균하는 것이 default
   센서의 변화가 더 빠르기를 원하는 경우 이 값(averaging_count)를 적은 수로 변경하고
   센서가 더 안정적인 값을 출력하기를 원하는 경우 이 값(averaging_count)를 더 큰 값을 변경
   이동평균은 E핀과 T핀에서 입력된 값에 대해서 수행함
   EMF: 이동평균 전 값, 현재 실시간 데이터
   THER: 이동평균 전 값, 현재 실시간 데이터
*/
//Sensor data
float EMF_data = 0;
float THER_data = 0;
float THER_ini = 0;
unsigned int mcu_resol = 1024;
float mcu_adc = 5;
/*
    센서 데이터를 저장하기 위한 변수
    EMF_data: 이동평균 후 값
    THER_data: 이동평균 후 값
    THER_ini: 써미스터의 기준 값, 3분 워밍없이 끝난 직후의 값
*/
// Thermister constant
// RX-9 have thermistor inside of sensor package. this thermistor check the temperature of sensor to compensate the data
// don't edit the number
#define C1 0.00230088
#define C2 0.000224
#define C3 0.00000002113323296
float Resist_0 = 15;
/*
   써미스터의 입력값을 섭씨로 변경하기 위한 상수값
   변경할 필요 없음
   Resist_0: 15kohm
*/
//Status of Sensor
bool status_sensor = 0;
/*
   센서 상태
   워밍업 이후에는 1, 이전에는 0
*/

//Step of co2
int CD1 = 700; // Very fresh, In Korea,
int CD2 = 1000; // normal
int CD3 = 2000; // high
int CD4 = 4000; // Very high
int status_step_CD = 0;
/*
   단계표시용 기준 농도
   400~700 ppm:   매우 좋음
   700~1000 ppm:  보통
   1000~2000 ppm: 높음
   2000~4000 ppm: 매우 높음
   수치는 사용하는 어플리케이션이나 상황에 따라 변경
   일반적으로 한국의 기준은
    기계환기인경우(환풍기를 사용하는 경우): 1000 ppm 이상인 경우 환기 권장
    자연환기인경우(창문을 열어서 환기하는 경우): 1500 ppm 이상인 경우 환기 권장
*/
// Standard of CO2 concenctration
/*
     South of Korea
        - Mechanical ventilation: 1000 ppm
        - natural ventilation: 1500 ppm
     Japan
        - School: 1500 ppm
        - Construction law: 1000 ppm
     WHO Europe: 920 ppm
     ASHRAE(US): 1000 ppm
     Singapore: 1000 ppm
*/
// CO2 to Human
/*
   < 450 ppm  : outdoor co2 concentration, Very fresh and healthy             NATURE
   ~ 700 ppm  : normal indoor concentration                                   HOME
   ~ 1000 ppm : no damage to human but sensitive person can feel discomfort,  OFFICE
   ~ 2000 ppm : little sleepy,                                                BUS
   ~ 3000 ppm : Stiff shoulder, headache,                                     METRO
   ~ 4000 ppm : Eye, throat irritation, headache, dizzy, blood pressure rise
   ~ 6000 ppm : Increased respiratory rate
   ~ 8000 ppm : Shortness of breath
   ~ 10000 ppm: black out in 2~3 minutes
   ~ 20000 ppm: very low oxygen exchange at lung. could be dead.
*/

//Calibration data
float cal_A = 372.1;        //Calibrated number
float cal_B = 63.27;        //Calibrated number
float cal_B_offset = 1.0;   //cal_B could be changed by their structure.
float co2_ppm = 0.0;
float co2_ppm_output = 0.0;
float DEDT = 1.0;           //Delta EMF/Delta THER, if DEDT = 1 means temperature change 1 degree in Celsius, EMF value can changed 1 mV
/*
   cal_A: 교정 계수
   cal_B: 교정 계수
   cal_B_offset: 센서가 특정 기구물에 삽입되게 되면 기구물의 영향을 받게 되므로, 기구물 오프셋을 설정해줘야 함, 기구물이 완성된 다음에 농도테스트로 설정이 가능함
   co2_ppm: 계산된 co2 농도
   co2_ppm_output: 계산된 co2 농도를 min/max를 내의 값으로 표현하기 위한 변수
   DEDT: delta EMF/delta THER, 온도에 따른 EMF 변화량
   RX-9 Simple은 cal_A와 cal_B를 특별히 넣지 않고 특정값으로 계산함
   RX-9은 cal_A와 cal_B값이 제품 뒤에 QR 코드로 명시가 되어 있으며, 해당 값을 제품마다 1:1로 넣어서 ppm을 계산할 수 있도록 해야 함
   알고리즘 내에서 cal_A는 계속 변화하고 업데이트가 되는 반면 cal_B는 변화하지 않음
*/
//Auto Calibration Coeff
unsigned long prev_time_METI = 0;
int MEIN = 120;                      //CAR: 120, HOME: 1440, Every MEIN minutes, Autocalibration is executed.
int MEIN_common = 0;
int MEIN_start = 1;
bool MEIN_flag = 0;
int start_stablize = 300;
int METI = 60;                        //Every 60 second, check the autocalibration coef.
float EMF_max = 0;
float THER_max = 0;
int ELTI = 0;
int upper_cut = 0;
int under_cut_count = 0;
int under_cut_cnt_value = 300;
float under_cut = 0.99;               // if co2_ppm shows lower than (Base_line * undercut), sensor do re-calculation
/*
   prev_time_METI: 특정 주기적으로 자동보정을 실시하기 위한 변수
   MEIN: 센서를 자동차에서 사용하는 경우 120분 주기로 실시, 일반가정인 경우 1440분(하루, 24시간) 주기로 실시
   MEIN_common: 설정한 MEIN을 상황별로 변경해주기위한 변수, 예를 들어 센서가 데미지를 입었거나, 구동 초기인 경우 MEIN_common의 값을 변경하여 자동보정 주기가 달라질 수 있도록 설정하여 사용함
   MEIN_start: 센서를 구동하기 시작한지 얼마 안 되었을 때, 센서값이 불안정한 경우가 있어서, 5분 이내에는 자동보정이 1분주기로 실행될 수 있도록함
   MEIN_flag: MEIN의 값을 구동 초기 인지, 구동 초기가 아닌지에 따라서 변경해주기 위한 변수
   start_stablize: 센서를 장시간 비가동한 경우, 센서를 전원 인가 후 안정화 시퀀스를 돌리기 위한 시간변수, default = 300 초
   METI: 자동보정시 사용하는 변수, 자동보정을 하기 위해서 1주기(MEIN의 횟수) 내에 최대 EMF값(최소 ppm 값, EMF와 ppm은 반비례 관계)과 최대 EMF 값일 때
         THER값을 기억하여 MEIN의 횟수가 만료될 때, 최대 EMF값을 Base_line에서 설정한 값으로 변경해주는데, 이 최대 EMF값과 THER값을 현재의 값과 비교하여
         더 높은 EMF 값을 기억하게 되는데, 이 비교하는 주기에 대한 수치임. 60인 경우 60초에 한번씩 EMF 값을 비교함.
         이 횟수는 MEIN과 연동이 되기 때문에 하루 주기로 자동보정이 돌아가게 하기 위해서 METI가 60인경우 MEIN이 1440이라면, 1주기는 1440분
         이라, 하루마다 1회씩 작동함.
         METI가 30인경우 하루 주기로 자동보정이 작동하게 하기 위해서는 MEIN을 2880으로 설정해야 함.
   EMF_max: 자동보정 1주기 내에 METI에서 설정한 수치마다 비교하여 기억하는 최대 EMF값(최소 ppm 값)
   THER_max: EMF_max를 업데이트할 때, 함께 기억하는 값. 실제로 THER의 최대값은 아니고, EMF_max값이 업데이트될 때, 함께 업데이트 됨
*/
// Damage recovery
// Sensor can be damaged from chemical gas like high concentrated VOC(Volatile Organic Compound), H2S, NH3, Acidic gas, etc highly reactive gas
// so if damage is come to sensor, sensor don't lose their internal calculation number about CO2.
// this code and variant are to prevent changing calculation number with LOCKING
bool damage_cnt_fg = 0;
unsigned int damage_cnt = 0;
unsigned int ppm_max_cnt = 0;
float cal_A_LOG[2][20] = {0,};
bool cal_A_LOCK = 0;
float cal_A_LOCK_value = 0.0;
float emf_LOCK_value = 0.0;
unsigned int LOCK_delta = 50;
unsigned int LOCK_disable = 5;
unsigned int LOCK_timer = 15;
unsigned int LOCK_timer_cnt = 0;
unsigned int S3_cnt = 0;
/*
    센서는 데미지를 받을 수 있습니다. 다양한 종류의 데미지가 존재할 수 있으나, 가장 일반적인 데미지는 VOC와 같은 반응성이 높은 가스가
    센서로 고농도로 인입되는 경우 센서의 값이 흔들릴 수 있음
    이런 경우에 센서의 수치를 빠르게 회복시키기 위한 함수와 그에 대한 변수들.
    damage_cnt_fg: 센서가 데미지를 받아서 일반적인 실내에서 발생할 수 없는 높은 ppm이 발생한 경우, 1분 이상 해당 높은 농도가 유지된 경우
                  damage_cnt_fg의 값을 1로 변경함, 일반적인 경우 0
    damage_cnt: damage_cnt_fg가 1인 경우 자동보정 주기를 1분마다 진행될 수 있도록 변경하고 자동 보정을 몇 회 진행했을 지를 카운트함
    ppm_max_cnt: damage_cnt_fg에서 참조하고 있는 높은 농도가 유지된 경우 몇 초나 유지가 되어야 damage_cnt_fg를 1로 변경할지를 카운트함
    cal_A_LOG: 센서가 일시적으로 강한 데미지를 받았는지를 확인하기 위한 배열, 17초 전 데이터와 현재 데이터를 비교하여 급격한 변화가 발생한 경우
              데미지를 받았다고 판단하기 위해 17초 전 데이터를 보관함
*/
//debug
bool debug = 0;
bool display_mode = 0;

//command
const int timeout = 1000; //UART timeout


void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  Serial.setTimeout(timeout);
  delay(1000);
  cal_A_LOCK = 0;
  damage_cnt_fg = 0;
  damage_cnt = 0;

}

void loop() {
  // put your main code here, to run repeatedly:
  /*
   * 
   */
  current_time = millis() / 1000;
  if (current_time - prev_time >= set_time) {
    warm_up_chk();
    ppm_cal(); 
    DMG_REC();
    DMG_5000();
    step_cal_CD();
    auto_calib_co2();
    display_data();
    prev_time = current_time;
    /*
     *   Function       /                 Description               /     Note            /
     *   warm_up_chk    / warming up timing chk                     / RX-9, RX-9 Simple   /
     *   ppm_cal        / calculation ppm value with EMF and THER   / RX-9, RX-9 Simple   /
     *   DMG_REC        / avoid damage from high reactivity gas     / RX-9, RX-9 Simple   /
     *   DMG_5000       / when sensor shows 5000 ppm, recovery      / RX-9, RX-9 Simple   /
     *   step_cal_CD    / step calculation, divided from ppm value  / RX-9 Simple         /
     *   auto_calib_co2 / periodically auto calibration             / RX-9, RX-9 Simple   /
     *   display_data   / data serial output, you can edit this     / Option              /
     *      
     */
  }
}

void warm_up_chk() {
  if (current_time < warm_up_time) {
    status_sensor = 0;
  }
  else if (current_time >= warm_up_time && status_sensor == 0) {
    status_sensor = 1;
    sensor_reset();
  }
}

void ppm_cal() {
  /*
   * to calculate the concentration of CO2
   * 1. collet EMF(from E pin of RX-9) and THER(from T pin of RX-9)
   * 2. moving average to minimize noise
   * 3. calculate equation with EMF and THER
   * 4. limitate co2 value maximum and minimum
   * 5. if sensor shows too low co2 value, update the variant of calculation equation
   */
  EMF = analogRead(EMF_pin);
  EMF = EMF / mcu_resol; // 10 bits, Change the number if your MCU have other resolution
  EMF = EMF * mcu_adc;    // 5V     , Change the number if your MCU have other voltage
  EMF = EMF / 6;    //OPAMP   , Don't change!
  EMF = EMF * 1000; //V to mV conversion
  delay(1);
  THER = analogRead(THER_pin);
  THER = 1 / (C1 + C2 * log((Resist_0 * THER) / (mcu_resol - THER)) + C3 * pow(log((Resist_0 * THER) / (mcu_resol - THER)), 3)) - 273.15;
  delay(1);

  // Moving Average START --->
  m_averaging_data[averaged_count][0] = EMF;
  m_averaging_data[averaged_count][1] = THER;

  if (averaged_count < averaging_count) {
    averaged_count++;
  }
  else if (averaged_count >= averaging_count) {
    for (int i = 0; i < averaging_count; i++) {
      sum_data[0] = sum_data[0] + m_averaging_data[i][0]; //EMF
      sum_data[1] = sum_data[1] + m_averaging_data[i][1]; //THER
      for (int j = 0; j < 2; j++) {
        m_averaging_data[i][j] = m_averaging_data[i + 1][j];
      }
    }
    EMF_data = sum_data[0] / averaging_count;
    THER_data = sum_data[1] / averaging_count;

    sum_data[0] = 0;
    sum_data[1] = 0;
  }
  // <---Moving Average END

  // CO2 Concentratio Calculation START --->
  co2_ppm = pow(10, ((cal_A - (EMF_data + DEDT * (THER_ini - THER_data))) / (cal_B * cal_B_offset)));
  co2_ppm = co2_ppm * 100 / 100;
  // <--- CO2 Concentration Calculation END

  // CO2 ppm min, max regulation START-->
  if (co2_ppm > max_co2) {
    co2_ppm_output = max_co2;
  }
  else if (co2_ppm <= Base_line) {
    co2_ppm_output = Base_line;
  }
  else {
    co2_ppm_output = co2_ppm;
  }
  // <--- CO2 ppm min, max regulation END

  // CO2 ppm baseline update START -->
  if (co2_ppm <= Base_line * under_cut) {
    under_cut_count++;
    if (under_cut_count > under_cut_cnt_value) {
      under_cut_count = 0;
      sensor_reset();
    }
  }
  else {
    under_cut_count = 0;
  }
  // <-- CO2 ppm baseline update END
}

void sensor_reset() {
  /*
   *  센서의 상태가 정상상태인 경우
   *  센서의 출력값을 초기화시킴
   *  센서의 정상상태 판단 (Warm-up이 끝났을 것, DMG_REC()에서 판정하는 기준에 해당하지 않을 것)
   *    
   *  
   */
  if (cal_A_LOCK == 0) {
    THER_ini = THER_data;
    EMF_max = EMF_data;
    THER_max = THER_data;
    ELTI = 0;
    cal_A = EMF_data + log10(Base_line) * (cal_B * cal_B_offset);
  }
}

void step_cal_CD() {
  /*  
   *  RX-9 Simple에 해당
   *  CD1~CD4에서 지정한 값에 해당할 경우 센서의 단계를 표현함
   *  본 함수에서는 0~4의 총 5단계로 구분됨
   *  ppm 출력을 하지 않는 RX-9 Simple의 경우에도 ppm_cal()이 필요한 이유가
   *  다음의 단계 출력을 위해 ppm 값을 사용하기 때문임
   *  
   */
  
  if (status_sensor == 1) {
    if (co2_ppm < CD1) {
      status_step_CD = 0;
    }
    else if (co2_ppm >= CD1 && co2_ppm < CD2) {
      status_step_CD = 1;
    }
    else if (co2_ppm >= CD2 && co2_ppm < CD3) {
      status_step_CD = 2;
    }
    else if (co2_ppm >= CD3 && co2_ppm < CD4) {
      status_step_CD = 3;
    }
    else if (co2_ppm >= CD4) {
      status_step_CD = 4;
    }
  }
}

void auto_calib_co2() {
  /*
   * 자동보정함수
   * 모든 센서가 센서를 구동하다 보면 센서의 출력값이 정상 범위를 벗어나기 마련인데,
   * 이산화탄소 센서는 영점 조정을 해줄만한 좋은 레퍼런스가 있기 때문에 자동보정으로 0점 조정을 할 수 있음
   * 좋은 레퍼런스는 지구의 이산화탄소 농도인데, 일일간 최저농도의 편차가 크지 않기 때문에 일정 기간 중 측정된 값의 최저값을
   * 지그의 이산화탄소 농도에 맞춰 0점 조정을 진행함. 
   * 모든 이산화탄소 센서는 이와 같은 자동보정을 하고 있음.
   * 
   * ELTI(Elapsed Time): 자동보정을 위한 시간을 카운트함
   * MEIN(Measurement Interval): 자동보정의 1주기 횟수
   * METI(Measurement Time): 측정 주기
   * 
   * 주요 동작 시퀀스
   * 1. METI에서 설정한 측정주기마다 EMF_max값과 현재의 EMF 값을 비교함
   * 2-1. EMF_max보다 현재의 EMF값이 크면 3회 동안 추이를 보고 3회 연속 큰 경우, EMF_max 값을 현재의 EMF 값으로 업데이트함, 이 때 THER_max 값을 현재의 THER값으로 함께 업데이트함
   * 2-2. EMF_max보다 현재의 EMF값이 작으면 EMF_max 값을 업데이트하지 않고 그대로 둠
   * 3. 2에서 EMF_max와 EMF현재값을 비교할 때마다 ELTI는 1씩 증가함
   * 4. ELTI가 MEIN에서 설정한 횟수가 되면 자동보정을 실시함
   * 5. 자동보정은 MEIN에서 설정한 1주기(횟수) 동안 비교한 EMF 값 중 가장 높은 값(EMF_max)을 지구대기 농도(Base_line)로 환산함
   * 6. 다음주기를 시작함, 관련변수를 현재값으로 초기화함
   * 
   * 기타
   * cal_A_LOCK: 센서가 데미지를 받은 상황이라면, autocal이 진행되지 않음
   * 
   */
  if (current_time < start_stablize && MEIN_flag == 0) {
    MEIN_flag = 1;
    MEIN_common = MEIN_start;
  }
  /*
   * 
   */
  else if (current_time >= start_stablize + 1 && MEIN_flag == 1 && damage_cnt_fg == 0) {
    MEIN_common = MEIN;
    //if(display_mode){Serial.println("MEIN_common = MEIN");}
  }
  if (current_time - prev_time_METI >= METI && status_sensor == 1) {
    if (ELTI < MEIN_common) {
      if (cal_A_LOCK == 0) {
        ELTI++;
      }
      else {
        LOCK_timer_cnt++;
      }
    }
    else if (ELTI >= MEIN_common) {
      if (cal_A_LOCK == 0) {
        cal_A = (EMF_max + DEDT * (THER_max - THER_data)) + log10(Base_line) * (cal_B * cal_B_offset);
        THER_ini = THER_data;
        EMF_max = EMF_data;
        THER_max = THER_data;
        ELTI = 0;
      }
      if (damage_cnt_fg == 1)
      {
        damage_cnt++;
      }
    }
    if (EMF_max >= EMF_data) {
      upper_cut = 0;
    }
    else if (EMF_max < EMF_data) {
      upper_cut++;
      if (upper_cut > 3) {
        EMF_max = EMF_data;
        THER_max = THER_data;
        upper_cut = 0;
      }
    }
    prev_time_METI = current_time;
  }
}

void DMG_REC() {
  /*
   * 데미지 감지 함수
   * 센서가 데미지를 받으면 특히, 반응성이 좋은 가스(에탄올, 포름알데히드, 암모니아, CO 등)에 노출되었을 때, 센서의 EMF값이 급격하게 증가할 수 있음
   * 이런 경우에 데미지를 받았는지 판정하고 데미지를 받았다고 판단된 경우, 센서를 보정한 계수(cal_A)의 업데이트를 멈추고, 데미지를 받기 전의 cal_A를 적용하여
   * 데미지에 의해서 센서의 보정계수가 바뀌지 않도록 하는 함수
   * 
   * 주요시퀀스
   * 1. 최근 20초간 EMF, cal_A 값을 저장함
   * 2. 센서가 워밍업을 지난 시간이라면 현재의 EMF와 17초 전 EMF를 비교함
   * 3-1. 3초 연속으로 17초전 EMF와 현재의 EMF차이가 LOCK_delta에서 지정한 값보다 크면 cal_A를 업데이트하는 것을 정지하고 19초전의 cal_A값과, EMF 값을 기억함
   * 3-2. EMF가 최대값에 근접한 경우 3-1이 작동하지 않기 때문에 최대값 한정 조건문으로 최대값에 가깝더라도 급격하게 바뀐 경우 작동하도록 함
   * 4. 3-1 이나 3-2에서 cal_A_LOCK = 1이 된 경우 다음의 두 조건으로 cal_A_LOCK을 해제함
   * 4-1. 3-1에서 기억한 EMF값과 현재의 EMF값의 차이가 LOCK_disable에서 지정한 값보다 작아지는 경우 cal_A_LOCK을 해제함
   * 4-2. cal_A_LOCK이 설정된 이후 시간이 LOCK_timer(단위: 분)에서 설정한 값보다 커지면 cal_A_LOCK을 해제함
   */
  for (int i = 0; i < 19; i++) {
    cal_A_LOG[0][i] = cal_A_LOG[0][i + 1];
    cal_A_LOG[1][i] = cal_A_LOG[1][i + 1];
  }
  cal_A_LOG[0][19] = cal_A;
  cal_A_LOG[1][19] = EMF_data;
  if (status_sensor == 1) {
    if ((cal_A_LOG[1][19] - cal_A_LOG[1][2] >= LOCK_delta) && (cal_A_LOG[1][18] - cal_A_LOG[1][1] >= LOCK_delta) && (cal_A_LOG[1][17] - cal_A_LOG[1][0] >= LOCK_delta)) {
      if (cal_A_LOCK == 0) {
        cal_A_LOCK = 1;
        cal_A_LOCK_value = cal_A_LOG[0][0];
        emf_LOCK_value = cal_A_LOG[1][0];
        cal_A = cal_A_LOCK_value;
        //if(debug); Serial.println("S1 ---- cal_A_LOG[1][0] = " + cal_A_LOG[1][0] + "cal_A_LOG[1][9] = " + cal_A_LOG[1][9]);
      }
    }
    else if ((cal_A_LOG[1][2] > 540 - LOCK_delta) && (cal_A_LOG[1][1] > 540 - LOCK_delta) && (cal_A_LOG[1][0] > 540 - LOCK_delta) && (cal_A_LOG[1][2] <= 540 - LOCK_disable) && (cal_A_LOG[1][1] <= 540 - LOCK_disable) && (cal_A_LOG[1][0] <= 540 - LOCK_disable)) {
      if ((cal_A_LOG[1][17] > 540) && (cal_A_LOG[1][18] > 540) && (cal_A_LOG[1][19] > 540)) {
        if (cal_A_LOCK == 0) {
          cal_A_LOCK = 1;
          cal_A_LOCK_value = cal_A_LOG[0][0];
          emf_LOCK_value = cal_A_LOG[1][0];
          cal_A = cal_A_LOCK_value;
          //if(debug); Serial.println("S2 ---- cal_A_LOG[1][0] = " + cal_A_LOG[1][0] + "cal_A_LOG[1][9] = " + cal_A_LOG[1][9]);
        }
      }
    }
    else {
      //do nothing
    }
  }
  if (cal_A_LOCK == 1) {
    if (EMF_data - emf_LOCK_value < LOCK_disable) {
      S3_cnt++;
      if (S3_cnt >= 10) {
        S3_cnt = 0;
        cal_A_LOCK = 0;
        ELTI = 0;
        THER_ini = THER_data;
        EMF_max = EMF_data;
        THER_max = THER_data;
        LOCK_timer_cnt = 0;
      }
      else {
        //do nothing
      }
    }
    else if (LOCK_timer_cnt >= LOCK_timer) {
      cal_A_LOCK = 0;
      ELTI = 0;
      THER_ini = THER_data;
      EMF_max = EMF_data;
      THER_max = THER_data;
      LOCK_timer_cnt = 0;
    }
    else {
      S3_cnt = 0;
    }
  }
  else {
    //do nothing
  }
}

void display_data() {
  /*
   * 데이터 표시
   */
  if (display_mode == 0) {
    Serial.print("# ");
    if (co2_ppm <= 999) {
      Serial.print("0");
      Serial.print(co2_ppm_output, 0);
    }
    else {
      Serial.print(co2_ppm_output, 0);
    }
    Serial.print(" ");

    if (status_sensor == 0) {
      Serial.print("WU");
    }
    else {
      Serial.print("NR");
    }
    Serial.println("");
  }
  else if (display_mode == 1) {
    Serial.print("T ");
    Serial.print(current_time);
    Serial.print(" # ");
    if (co2_ppm <= 999) {
      Serial.print("0");
      Serial.print(co2_ppm, 0);
    }
    else {
      Serial.print(co2_ppm, 0);
    }
    Serial.print(" | CS: ");
    Serial.print(status_step_CD);
    Serial.print(" | ");
    Serial.print(EMF_data);
    Serial.print(" mV | ");
    Serial.print(THER_data);

    if (status_sensor == 0) {
      Serial.print(" oC | WU");
    }
    else {
      Serial.print(" | NR");
    }
    Serial.println("");
  }
}

void DMG_5000()
{
  /* 데미지 회복 함수
   * 일반적으로 발생하기 어려운 5000 ppm 이상의 CO2가 1분 이상 유지된 경우, 센서가 데미지를 받은 것이라 판단하여 
   * 자동보정주기를 3분(2로 표기)으로 변경하여 센서가 데미지로부터 빠르게 회복할 수 있도록 하고, 3분 간격으로 자동보정을 5회 진행한 후에는
   * 다시 원래의 주기로 복귀함
   * 
   * 1. 현재 출력되는 co2 농도가 5000 ppm 이상인 경우 1분을 체크함
   * 2. 1분 이상 5000 ppm 이상이 유지된 경우 damage_cnt_fg를 1로 변경하여 damage_cnt를 증가하게 두고 damage_cnt가 5를 초과할 때까지 자동보정을 3분 주기로 진행함, 총 15분(damage_cnt * 3분) 소요
   */
  if (status_sensor == 1) {
    if (co2_ppm_output >= 5000) {
      if (ppm_max_cnt > 60) {
        MEIN_common = 2;
        damage_cnt_fg = 1;
        ppm_max_cnt = 0;
      }
      else {
        ppm_max_cnt++;
      }
    }
    else {
      ppm_max_cnt = 0;
    }
  }
  if (damage_cnt > 5) {
    MEIN_common = MEIN;
    damage_cnt = 0;
    damage_cnt_fg = 0;
  }
  else {
    //do nothing
  }
}

 

 

5. 동작확인

 

  - 1초마다 상태를 확인하게 되어 있으며, 180초(3분) 동안 준비작업(예열)하는 시간이 걸립니다.

  - 기본적으로 사무실 400ppm대를 가리키고 있었습니다. 성냥으로 이산화탄소를 발생시키고 확인해 보았습니다. 

 

  재미난 것은 성냥 연기를 바로 할 경우보다 꺼지고 2~3분 지나서부터 ppm 수치가 계속 800까지 천천히 증가하였습니다. 창문을 열어 놓고 테스트해서 값이 조금 정확하지 않을 수 있습니다.

 

 

 

 

CO2 농도간 인체에 미치는 영향

 

 <EXSEN Manual 자료>

 

6. 추가 정보

 

   Arduino MKR ZERO 보드를 사용할 경우 소스에서 EEPROM.h 헤더 파일이 없다고 에러가 발생합니다.

그럴 경우 외국 분이 잘 만들어 놓은 라이브러리 도움을 받으시면 됩니다.

 

   github src 다운로드하여서 아두이노 소스 폴더에 같이 넣어두고 #include "FlashAsEEPROM.h"로 바꿔주시면 MKR ZERO 보드로도 테스트해 볼 수 있습니다.

 

https://github.com/cmaglie/FlashStorage/src

 

 

감사합니다.

 

 

<참조사이트>

1. 이산화탄소 센서

https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%82%B0%ED%99%94_%ED%83%84%EC%86%8C_%EC%84%BC%EC%84%9C

2. 주목받는 비분산적외선 가스센서란 무엇인가

http://www.ntrexgo.com/archives/38522

3. EXSEN RX-9 Datasheet

 

RX-9_Datasheet.zip
3.23MB

반응형