こんにちは、ENGかぴです。
Arduino環境ではアナログ電圧をAD変換する標準ライブラリが実装されています。CO2ガスセンサーであるMG812を使ってArduinoのアナログ電圧値からCO2濃度に換算する方法など動作確認したことについてまとめました。
CO2ガスセンサアンプキットAE-MG-812(秋月電子)を使用しています。MG812モジュールの使用方法とppmに換算してシリアルモニターに表示して動作確認を行いました。
Arduino UNO(以下Arduinoとします。)を対象とします。Arduinoのライブラリを使用して動作確認したことをまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
CO2ガスセンサーの情報を取得する
MG812はWinsen社のCO2センサーです。温度や湿度の影響が受けにくい化学式のガスセンサーであり固定電解質セルの原理によって二酸化炭素の検出ができます。
センサー内蔵のヒータに電圧を印加している状態でセンサーがCO2にさらされると正と負の電力により起電力が発生するため測定雰囲気との起電力の差によってCO2濃度が検出できます。
MG812は内部抵抗が大きいため出力電流が微小となります。そのためMG812の出力をマイコンのAD変換に直接接続することができません。
AE-MG-812はMG812の出力電圧をオペアンプで増幅しているためArduinoマイコンに直接接続することができるようになっているのが便利です。(以下ではセンサー単品の説明はMG812とし、AE-MG-812を指す場合はMG812モジュールとします。)
広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
センサー情報取得の例
uint32_t value;
//タイマで取得タイミングを管理
if( timcalco2 == TIME_UP){
timcalco2 = SAMP_TIME;
value = analogRead(A1);
}
アナログ電圧の取得はanalogRead()関数を使用します。引数にAD変換するアナログピンの番号を指定します。AD変換値が戻り値にセットされるので変数に格納します。
例ではタイマでセンサー情報を取得するタイミングを管理しています。CO2濃度の状態変化のみを把握目的であれば数秒に一度の頻度のデータ取得でよい場合もありますが、ppmに換算して処理を分岐させる場合はアナログ入力であることを考慮して複数回取得したデータを平均化してノイズなどの外部要因による影響を減らした方が良い場合もあります。
動作検証(エージング)
MG812モジュールは初めて使用する場合や長く通電していなかった場合CO2濃度に関わらず出力電圧が低下します。そのため数時間から数日程度電源を入れた状態にしてセンサーを安定させる(エージング)必要があります。外気の雰囲気で出力電圧の様子をシリアルモニタで確認しました。
MG812のデータシートによると以下の条件でエージングを行う必要があります。
購入して電源を一度も入れていない場合はMore than six monthsの条件に当てはまるため少なくても168時間(1週間)はエージングが必要です。
エージングは電源を常時ONした状態とするため別途アダプターでDC5Vを印加したまま外気の雰囲気に近い窓際に放置しました。エージング開始時は出力電圧が不安定で1V程度からのスタートでした。
シリアルモニターのinitvccはMG812モジュールの出力電圧を電圧換算した結果を表示しています。MG812の電圧を10のゲインで増幅した結果になります。
エージング開始から48時間経過した結果です。出力電圧が2.0V付近で推移していることが分かりました。MG812のデータシートによるとで外気の雰囲気で閾値が200mV~500mV(MG812モジュールに置き換えると2V~5V)となっています。そのため出力電圧が収束に向かう推移になりそうです。
エージング開始から168時間経過した結果です。出力電圧が2.25V付近で安定していることが分かりました。当記事で使用しているMG812モジュールの外気の雰囲気の出力電圧は2.25V(CO2濃度が400ppm)を基準としてよさそうです。
出力電圧をCO2濃度に換算する
MG812の出力電圧はCO2濃度が高くなるほど起電力が小さくなります。データシートを確認すると起電力は400ppmを超えたあたりからCO2濃度の対数に比例しています。測定する範囲に対して基準ガスの濃度と起電力を決めることでCO2濃度を求めることができます。
$$k = \frac{1}{E_0-E_1}×log_{10}\frac{P_1}{P_0}・・(1)$$
$$P = P_0 ×10^{k×(E_0-E)}[ppm]・・(2)$$
以上を考慮して基準ガスの濃度をP0を400[ppm]、P1を4000[ppm]とします。P0時の起電力の値はモジュールによって閾値が変わるためE0とします。P1時の起電力をE1とするとE1はE0から4000ppm時の起電力を引いた値になります。データシートのグラフから差分を取ると約45mVになります。
例)データシートのグラフの環境でppmを計算します。E0=0.325[V]、E1=0.28[V]、P0=400[ppm]、P1=4000[ppm]とした時の起電力E=0.305[V]時のCO2濃度を計算します。(1)により対数の傾きを計算するとk=22.22になります。(2)によりP=1113[ppm]になります。
#define EMF_RANGE (double)0.045 //E0とE1の差
double e; //MG812の出力電圧
double k; //対数の調整値(対数の傾き)
double p; //ppm換算値
emfmax = initvcc; //P0
e = (float)mg812.avrage/0x400*AD_VCC / BOARD_GAIN;
k = ((double)1/EMF_RANGE)*log10((double)EMF_1_PPM/EMF_0_PPM);
p = (double)EMF_0_PPM*pow(10,(k*(emfmax-e)));
式(1)と(2)の計算の例を説明します。ArduinoのAD変換値を電圧値eに換算します。Arduinoの5Vは5Vを下回ることがあるためテスターで確認した値を使用しています。誤差を気にしない場合は5Vで換算しても問題ありません。
MG812モジュールの出力電圧が外部要因によって若干ふらつくことがあるため複数回取得した平均値を使ってCO2濃度に換算します。MG812モジュールはゲイン10倍しているため10で割ってMG812の出力電圧に換算しています。
kを計算する場合はdoubleの型にキャストしてから除算を行います。起電力の範囲(E0とE1の差分)に対する対数の割合を計算するため傾きを求めることになります。
換算値のpは基準値である400ppm(起電力がE0時の濃度)に対して10の(k*(E0-e))乗になります。C言語では乗数はpow()関数を使用します。第1引数は対象となる数値を指定し、第2引数に乗数を指定します。
広告
マイベスト3年連続1位を獲得した実績を持つ実践型のプログラミングスクール
CO2濃度判定の初期値の更新
MG812の出力電圧はCO2濃度の高くなるほど低くなるため基本的に設置環境が安定していると外気の雰囲気で調整した初期値の出力電圧以上になる可能性は低くなります。
エージング時間の不足や外部要因によって出力電圧が上昇することがあります。そのためCO2濃度に換算した値が400ppm以下になることがあるため初期値を更新することで400ppm以下になる頻度が減るように調整します。
MG812モジュールの出力電圧はノイズなどの外部要因によって一時的に上昇してしまう可能性があるので複数回取得したデータの平均値を使用しています。平均化に加えて初期値の更新を頻繁に行わないようにするため初期値よりも0.02V以上になっていることを確認して初期値の更新を行います。
動作確認
ArduinoのA1ピンにMG812モジュールの出力電圧を入力します。Arduinoの5VはMG812モジュールを接続すると4.5V付近まで電圧降下してしまうため外部のアダプターから5Vを供給しています。
ArduinoでAD変換してCO2濃度を算出します。初期化中はLED1(赤)を点灯し初期化時の電圧が2.0V以下であればLED1を点灯/消灯するようにします。MG812モジュールの出力電圧が2.0V以上になるとセンサーが安定しているとみなして通常の動作に遷移させます。
通常の動作ではLED2(緑)を点灯/消灯させます。C02濃度が1000ppm以上になるとLED1(赤)を点灯して換気の注意を促すようにします。
MG812モジュールの出力電圧が動作中に上昇して初期化時の電圧を超えると初期値の更新を行って通常の動作に戻るようにします。シリアルモニタで動作を確認しました。
電源をONすると5秒間はヒータの電源が安定するように待機させます。ガスの初期値(initvcc)を決定して通常の動作に遷移しています。通常の動作では400ppm以上のCO2濃度になっていますが、息を吹きかけるとMG812モジュールの出力電圧が低下し1554ppm程度まで上昇しています。また出力電圧が初期値よりも上昇すると初期値の更新を行っていることが分かりました。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
エージングに使用したソースコード:
#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define SAMP_TIME 500
#define AD_VCC (double)4.85
#define EMF_RANGE (double)0.045
#define EMF_CHK_RANGE (double)0.01
#define EMF_0_PPM 400
#define EMF_1_PPM 4000
#define EMF_BASE (double)0.20
#define BOARD_GAIN 10
#define PIN_DO_1 7
#define PIN_DO_2 8
#define LED_FL 50
#define INIT_WAIT 100
uint32_t value;
uint32_t beforetimCnt = millis();
int16_t timcalco2;
int16_t timinitLed;
int8_t cnt10ms;
double emf_chk;
double initvcc;
double emfmax;
void mainTimer(void);
void initApp(void);
void setup() {
Serial.begin(115200);
pinMode(PIN_DO_1,OUTPUT);
pinMode(PIN_DO_2,OUTPUT);
timinitLed = INIT_WAIT;
digitalWrite(PIN_DO_1,HIGH);
initApp();
}
void loop() {
}
/* 初期化処理*/
void initApp(void){
bool chk = true;
while(chk){
mainTimer();
if( timinitLed == TIME_UP ){
timinitLed = LED_FL;
digitalWrite(PIN_DO_1,!digitalRead(PIN_DO_1));
value = analogRead(A1);
Serial.print("value: ");
Serial.print(value);
initvcc = (float)value/0x400*AD_VCC;// / BOARD_GAIN;;
Serial.print(" initvcc: ");
Serial.println(initvcc,2);
}
}
}
/* Timer Management function add */
void mainTimer(void){
if ( millis() - beforetimCnt >= BASE_CNT ){
beforetimCnt = millis();
if( timcalco2 > TIME_UP ){
timcalco2--;
}
if( timinitLed > TIME_UP ){
timinitLed--;
}
}
}
動作確認用に使用したソースコード:
#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define SAMP_TIME 500
#define AD_VCC (double)4.85
#define EMF_RANGE (double)0.045
#define EMF_CHK_RANGE (double)0.002
#define EMF_0_PPM 400
#define EMF_1_PPM 4000
#define EMF_BASE (double)0.20
#define BOARD_GAIN 10
#define PIN_DO_1 7
#define PIN_DO_2 8
#define CHK_CNT 8
#define LED_FL 50
#define INIT_WAIT 500
#define CHK_CAUTION 1000
struct AI_FILT{
uint8_t wp;
uint16_t data[CHK_CNT];
uint16_t avrage;
};
uint32_t beforetimCnt = millis();
int16_t timcalco2;
int16_t timinitLed;
int8_t cnt10ms;
double emf_chk;
double initvcc;
double emfmax;
AI_FILT mg812;
uint16_t sum;
void mainTimer(void);
void mainApp(void);
void initApp(void);
void setup() {
Serial.begin(115200);
pinMode(PIN_DO_1,OUTPUT);
pinMode(PIN_DO_2,OUTPUT);
timinitLed = INIT_WAIT;
digitalWrite(PIN_DO_1,HIGH);
initApp();
}
void loop() {
mainTimer();
mainApp();
}
/* 初期化処理*/
void initApp(void){
bool chk = true;
bool vccokflg = false;
double chkvcc[CHK_CNT];
uint8_t chkcnt=0;
while(chk){
mainTimer();
if( timinitLed == TIME_UP ){
timinitLed = LED_FL;
digitalWrite(PIN_DO_1,!digitalRead(PIN_DO_1));
mg812.data[mg812.wp] = analogRead(A1);
if(++mg812.wp >=CHK_CNT){
mg812.wp = 0;
vccokflg = true;
}
if( vccokflg ){
sum = 0;
for(uint8_t i=0 ; i < CHK_CNT; i++ ){
sum += mg812.data[i];
}
mg812.avrage = sum / CHK_CNT;
initvcc = (double)mg812.avrage/0x400*AD_VCC / BOARD_GAIN;
Serial.print(" initvcc: ");
Serial.println(initvcc,3);
if( initvcc > EMF_BASE ){
emfmax = initvcc;
chk = false;
digitalWrite(PIN_DO_1,LOW);
digitalWrite(PIN_DO_2,HIGH);
}
}
}
}
}
/* Timer Management function add */
void mainTimer(void){
if ( millis() - beforetimCnt >= BASE_CNT ){
beforetimCnt = millis();
if( timcalco2 > TIME_UP ){
timcalco2--;
}
if( timinitLed > TIME_UP ){
timinitLed--;
}
}
}
/* メイン処理*/
void mainApp(void){
double e;
double k;
double p;
if( timcalco2 == TIME_UP){
timcalco2 = SAMP_TIME;
Serial.print("value: ");
Serial.print(mg812.avrage);
e = (double)mg812.avrage/0x400*AD_VCC / BOARD_GAIN;
Serial.print(" e: ");
Serial.print(e,3);
k = ((double)1/EMF_RANGE)*log10((double)EMF_1_PPM/EMF_0_PPM);
Serial.print(" k: ");
Serial.print(k);
p = (double)EMF_0_PPM*pow(10,(k*(emfmax-e)));
Serial.print(" C02: ");
Serial.print(p);
Serial.println("ppm");
if( p >= (double)EMF_0_PPM){
if( p > CHK_CAUTION ){
digitalWrite(PIN_DO_1,HIGH);
}
else{
digitalWrite(PIN_DO_1,LOW);
}
}
else{
emf_chk = emfmax + EMF_CHK_RANGE;
if( e >= emf_chk ){
emfmax = e;
Serial.print(" update initvcc: ");
Serial.println(emfmax,3);
}
}
}
if(timinitLed == TIME_UP ){
timinitLed = LED_FL;
digitalWrite(PIN_DO_2,!digitalRead(PIN_DO_2));
mg812.data[mg812.wp] = analogRead(A1);
if(++mg812.wp >=CHK_CNT){
mg812.wp = 0;
}
sum = 0;
for(uint8_t i=0 ; i < CHK_CNT; i++ ){
sum += mg812.data[i];
}
mg812.avrage = sum / CHK_CNT;
}
}
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
最後まで、読んでいただきありがとうございました。
エージングが長くなることや電源ON時に基準ガスの調整のため外気の雰囲気でスタートする必要があるため若干使いにくい印象がありました。またArduinoの5Vに接続すると電圧降下するため外部電源が必要になるのも使いにくさの要因になりそうです。正確にCO2濃度を算出したい場合はAD変換の基準電圧を安定させることも必要です。