Seeeduino XIAOによるCO2モニター(4) Arduinoスケッチ実装編

2020年5月10日日曜日

CO2モニター Seeeduino XIAO

t f B! P L
今回はArduinoスケッチ実装について説明します。

Arduino環境準備

初回に記載しましたが、Arduino自体とSeeeduino XIAOのBoard Managersの設定は、こちらを見て行いました。

DHT22のライブラリは Arduino → ツール → ライブラリの管理 から以下のものをインストールして使用しました。
DHT22ライブラリの選択

Arduinoスケッチのビルド

Arduinoスケッチは今回のプロジェクトのGitHubリポジトリのarduino_SeeeduinoXIAO_RX9QR_DHT22ディレクトリに登録しておきましたのでgit cloneで取得してください。

次にEXSEN社 CO2センサーのライブラリのソースは、EXSEN社のRX-9 GitHubから 
  • RX-9_QR_Header/RX9QR.cpp
  • RX-9_QR_Header/RX9QR.h
を Arduinoのスケッチと同じフォルダに配置します。
まとめると以下になります。

$ git clone https://github.com/elehobica/CO2monitor.git
$ cd CO2monitor/arduino_SeeeduinoXIAO_RX9QR_DHT22/
$ curl -O https://raw.githubusercontent.com/EXSEN/RX-9/master/RX-9_QR_Header/RX9QR.cpp
$ curl -O https://raw.githubusercontent.com/EXSEN/RX-9/master/RX-9_QR_Header/RX9QR.h

上記が取得できたら、arduino_SeeeduinoXIAO_RX9QR_DHT22.inoのスケッチを開いてSeeeduino XIAOをUSBに接続して、ツール→シリアルポートからSeeeduino XIAOに対応するCOMポートを選択すれば、ビルド、書き込みができます。

Arduinoビルド

Arduinoスケッチの説明

簡単にarduino_SeeeduinoXIAO_RX9QR_DHT22.inoの説明をしておきます。

CO2濃度の計算

RX-9ライブラリはRX9QRクラスが定義されており、EMF値(起電力)とTHER値(サーミスタによる温度)を与えれば、
  • ウォームアップ中かどうかの状態
  • CO2濃度(ppm)
  • 5段階のステップ値
が取得できるようになっていますが、ADCとサーミスタ特性に依存する部分はユーザープログラム側で計算するようなサンプルが提供されていましたので、今回のArduinoスケッチでもそれをそのまま踏襲します。修正した点は以下です。
  • ピン割り当ての設定 (EMF_pin,  THER_pin) 
  • ADCvolt (ADCのVoltage) 5V → 3.3V
  • mein値 (?) をAutomotiveの120からHome or indoorの1440へ変更
  • RX9QRインスタンスの生成をスタティックに行っていた部分を、Flashから読み込んだキャリブレーションパラメータを反映するため、動的生成に変更
キャリブレーションパラメータの初期値はサンプルにある値そのままとしておきました。
/* ========== CO2 Sensor RX-9 (Begin) ========== */
/* https://github.com/EXSEN/RX-9 */
/* Utilizing RX-9 QR Sample Code
 *  date: 2020.03.04
 *  Carbon Dioxide Gas sensor(RX-9) with
 *  ATMEGA328p, 16Mhz, 5V
 *  file name: RX9SampleCodeQR_RX9
 *  
 *  RX-9 have 4 pin
 *  E: EMF
 *  T: Thermistor for sensor
 *  G: GND
 *  V: 3.3V > 200 mA
 */
#include "RX9QR.h"
#define EMF_pin 5   // RX-9 E
#define THER_pin 6  // RX-9 T
#define ADCvolt 3.3
#define ADCResol 1024
#define Base_line 432
#define meti 60  
#define mein 1440 //Automotive: 120, Home or indoor: 1440

//CO2 calibrated number
float cal_A = 372.1; // you can take the data from RX-9 bottom side QR data #### of first 4 digits. you type the data to cal_A as ###.#
float cal_B = 63.27; // following 4 digits after cal_A is cal_B, type the data to cal_B as ##.##

//CO2 Step range
#define cr1  700      // Base_line ~ cr1
#define cr2  1000     // cr1 ~ cr2
#define cr3  2000     // cr2 ~ cr3
#define cr4  4000     // cr3 ~ cr4 and over cr4

// 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;

//RX9QR RX9(cal_A, cal_B, Base_line, meti, mein, cr1, cr2, cr3, cr4);
RX9QR *RX9; // Initialize later to reflect cal_A and cal_B in Flash
/* ========== CO2 Sensor RX-9 (End) ========== */

bool getCO2(unsigned int *val) {
  int status_sensor = 0;
  unsigned int co2_ppm = 0;
  unsigned int co2_step = 0;
  float EMF = 0;
  float THER = 0;

  //read EMF data from RX-9, RX-9 Simple START-->
  EMF = analogRead(EMF_pin);
  delay(1);
  EMF = EMF / (ADCResol - 1);
  EMF = EMF * ADCvolt;
  EMF = EMF / 6;
  EMF = EMF * 1000;
  // <-- data="" emf="" end="" from="" read="" rx-9="" simple="" start--="" ther="">
  THER = analogRead(THER_pin);
  delay(1);
  THER = 1/(C1+C2*log((Resist_0*THER)/(ADCResol-THER))+C3*pow(log((Resist_0*THER)/(ADCResol-THER)),3))-273.15;
  // <-- data="" end="" from="" read="" rx-9="" simple="" status_sensor="RX9-" ther="">status_co2();   //read status_sensor, status_sensor = 0 means warming up, = 1 means stable
  co2_ppm = RX9->cal_co2(EMF,THER);    //calculation carbon dioxide gas concentration. 
  co2_step = RX9->step_co2();          //read steps of carbon dioixde gas concentration. you can edit the step range with cr1~cr4 above.
  *val = co2_ppm;
  return (status_sensor == 1);
}

Flashの読み出し・書き込み

Flashデータの読み出し、書き込みに関してはAtmel SAMD Native Libraryを直接インクルードして使用します。基本的な使い方は以下です。

  • FlashClassインスタンスをFlashメモリの開始アドレスとサイズを与えて生成しておく
  • 読み出しはflash.read(アドレス, ポインタ, サイズ)で可能
  • イレースはflash.erase(アドレス, サイズ)で行い、対象の領域を含むページ単位(64Byte)でイレースが行われる。(全bit 1クリアされる)
  • 書き込みはflash.write(アドレス, ポインタ, サイズ)で可能。ただしバイト単位の書き込みは不可で、16bit, 32bit単位かそれ以上で書き込み可能。

つまり、特定領域にWriteしたい場合、その範囲を含むページ単位(64Byte)で前もってEraseする必要がありますので、消えてしまう分もケアした形で必要であれば書き戻しを行えばよいです。
今回のスケッチでの操作は、
  • 起動時、ブート回数パラメータをFlashから読み出し、0xFFFFFFFFつまり初回起動時ならばパラメータ保存領域を初期化。(キャリブレーションパラメータはデフォルト値)
  • ブート回数パラメータが記録されていれば、キャリブレーションパラメータを読み出す。ブート回数パラメータをインクリメントして書き戻す。
  • キャリブレーション設定コマンド受信時は、キャリブレーションパラメータとキャリブレーション回数パラメータをインクリメントして書き戻す。
です。なお、毎回起動時にブート回数パラメータとキャリブレーション回数パラメータがシリアルに出力されますので、キャリブレーション回数パラメータが1以上になっているかどうかで、キャリブレーション済みかどうかを判定できます。
/*
Flash total size: 256KB
number of page 4096
page size 64Bytes
Use 0x0003FFC0 ~ 0x0003FFFF (64Bytes) as config area
*/
#include "FlashStorage.h" // Atmel SAMD Native Library
FlashClass flash((const void *) 0x00000000, 256*1024);
#define CONFIG_AREA_BASE    ((const volatile void *) 0x0003FFC0)
#define CONFIG_AREA_SIZE    64
#define CONFIG_USED_SIZE    0x10
#define CONFIG_BOOT_COUNT   ((const volatile void *) (CONFIG_AREA_BASE + 0x00))
#define CONFIG_CALIB_COUNT  ((const volatile void *) (CONFIG_AREA_BASE + 0x04))
#define CONFIG_CAL_A        ((const volatile void *) (CONFIG_AREA_BASE + 0x08))
#define CONFIG_CAL_B        ((const volatile void *) (CONFIG_AREA_BASE + 0x0C))
uint32_t boot_count, calib_count;

void updateFlashConfig() {
  uint32_t *data = (uint32_t *) malloc(CONFIG_AREA_SIZE);
  for (int i = 0; i < CONFIG_AREA_SIZE/4; i++) {
    flash.read(CONFIG_AREA_BASE + i*4, &data[i], sizeof(data[i]));
  }
  flash.erase(CONFIG_AREA_BASE, CONFIG_AREA_SIZE);
  flash.write(CONFIG_BOOT_COUNT, &boot_count, sizeof(boot_count));
  flash.write(CONFIG_CALIB_COUNT, &calib_count, sizeof(calib_count));
  flash.write(CONFIG_CAL_A, &cal_A, sizeof(cal_A));
  flash.write(CONFIG_CAL_B, &cal_B, sizeof(cal_B));
  for (int i = CONFIG_USED_SIZE/4; i < CONFIG_AREA_SIZE/4; i++) {
    flash.write(CONFIG_AREA_BASE + i*4, &data[i], sizeof(data[i]));
  }
  free(data);
}

void setup() {
  // Serial COM
  SerialUSB.begin(115200); // This has no meaning in case of Seeeduino XIAO
  delay(1000);

  // Load Config Parameters from flash
  flash.read(CONFIG_BOOT_COUNT, &boot_count, sizeof(boot_count));
  flash.read(CONFIG_CALIB_COUNT, &calib_count, sizeof(calib_count));
  if (boot_count == 0xffffffff) {
    // Config Area Initialize
    boot_count = 0;
    calib_count = 0;
    flash.erase(CONFIG_AREA_BASE, CONFIG_AREA_SIZE);
    flash.write(CONFIG_BOOT_COUNT, &boot_count, sizeof(boot_count));
    flash.write(CONFIG_CALIB_COUNT, &calib_count, sizeof(calib_count));
    flash.write(CONFIG_CAL_A, &cal_A, sizeof(cal_A));
    flash.write(CONFIG_CAL_B, &cal_B, sizeof(cal_B));
  } else {
    if (boot_count < 0xfffffffe) boot_count++;
    // Load Calibration data
    flash.read(CONFIG_CALIB_COUNT, &calib_count, sizeof(calib_count));
    flash.read(CONFIG_CAL_A, &cal_A, sizeof(cal_A));
    flash.read(CONFIG_CAL_B, &cal_B, sizeof(cal_B));
    // update boot_count
    updateFlashConfig();
  }
  // Create CO2 Sensor instance here with cal_A, cal_B
  RX9 = new RX9QR(cal_A, cal_B, Base_line, meti, mein, cr1, cr2, cr3, cr4);
  SerialUSB.println("");
  SerialUSB.println("Seeeduino XIAO CO2/Temp/Humidity Monitor ver 1.00");
  SerialUSB.print("Boot count: ");
  SerialUSB.print(boot_count);
  SerialUSB.print(" Calibration count: ");
  SerialUSB.println(calib_count);
  SerialUSB.print("cal_A: ");
  SerialUSB.print(cal_A);
  SerialUSB.print(" cal_B: ");
  SerialUSB.println(cal_B);
  
  // 以降省略
}

シリアルコマンド受信

シリアルコマンド受信は、loop()がコールされる毎にチェックをしています。(この部分は手抜きコードになってます。)
// Serial Read
char rcv_msg[32] = {};
int rcv_msg_pos = 0;
bool rcv_end = false;

void loop() {

  // 1秒ごとの処理は省略

  // Serial Read
  while (SerialUSB.available()) {
    int inByte = SerialUSB.read();
    if (inByte == '\r' || inByte == '\n' || rcv_msg_pos >= 31) {
      rcv_msg[rcv_msg_pos] = '\0';
      rcv_end = true;
      break;
    } else {
      rcv_msg[rcv_msg_pos++] = inByte;
    }
  }
  if (rcv_end && rcv_msg_pos > 0) {
    //SerialUSB.println(rcv_msg);
    while (SerialUSB.available()) Serial.read(); // Force buffer empty
    rcv_msg_pos = 0;
    rcv_end = false;
    String rcvMsg = rcv_msg;
    
    // 以下各コマンドの受信処理
    if (rcvMsg.equalsIgnoreCase("enable_monitor")) {
    // 省略
    }
  }
}
キャリブレーションコマンドを受信した際の処理部分のコードは以下です。
    } else if (rcvMsg.substring(0, 6).equalsIgnoreCase("calib ")) {
      if (rcvMsg.length() == 6+21 || rcvMsg.length() == 6+22) {
        // QR-code example "calib 15742214167K0544CAB07A"
        String factor_A = rcvMsg.substring(6, 10);
        String factor_B = rcvMsg.substring(10, 14);
        String comp_factor = rcvMsg.substring(14, 17);
        String serial_number = rcvMsg.substring(17, 28); // Officially it's 22 char. but there are modules which have 21 char.
        SerialUSB.print("cal_A: ");
        SerialUSB.println(factor_A);
        SerialUSB.print("cal_B: ");
        SerialUSB.println(factor_B);
        SerialUSB.print("Temp comp: ");
        SerialUSB.println(comp_factor);
        SerialUSB.print("Serial Number: ");
        SerialUSB.println(serial_number);
        if (comp_factor.equals("167") && serial_number.charAt(0) == 'K') {
          // Update Calibration parameter in flash
          cal_A = factor_A.toFloat() / 10.0;
          cal_B = factor_B.toFloat() / 100.0;
          calib_count++;
          updateFlashConfig();
          SerialUSB.println("OK: Calibration Parameters are correctly stored. Type 'reset' to reflect.");
        } else {
          SerialUSB.print("ERROR: Illegal Calibration Value '");
          SerialUSB.print(rcvMsg);
          SerialUSB.println("'");  
        }
      } else {
        SerialUSB.print("ERROR: Illegal Calibration Format '");
        SerialUSB.print(rcvMsg);
        SerialUSB.println("'");       
      }
    }

その他

その他、loop()にて1秒ごとに以下の処理を行っています。
  • Yellow LED点灯 (約125 ms)
  • CO2濃度、温度、湿度の値の取得 → シリアル送出
以上でArduino側の説明は終わりです。次回はProcessing側の説明を記載します。


自己紹介

自分の写真
電子工作&プログラミング、オーディオ・音楽

注目の投稿

Raspberry Pi Pico Wで電波時計を合わせる (JJY標準電波エミュレータ)

Raspberry Pi Pico Wのアプリケーションとして 最少の周辺部品で電波時計むけJJYエミュレータ(時刻合わせ用)を製作しました。 ※2023年6月6日: ソースコード修正の内容を反映させました。 時刻合わせ風景 概要 電波時計は電波が届くところで使...

QooQ