部品 · 読了まで15分

MQ135 Arduinoチュートリアル:空気質センサー、キャリブレーションとppm

MQ135 Arduinoガイド:検知できるガス、24〜48時間のエージング、R0のキャリブレーションとppmの式、CO2は測れるのか、ファン制御、ESP32での記録、CCS811との違いを解説します。

MQ135 Arduinoのプロジェクトは、部屋の空気がどれくらいきれいかを安く簡単に見守りたい人にとって最初の一歩です。MQ-135は、アンモニア、窒素酸化物、ベンゼン、アルコール、煙などのガスに反応する、ヒーター付きの金属酸化物ガスセンサーです。ホビー用キットでは、コンパレーターICと調整用ポテンショメーターを載せた小さなモジュールとして売られていることが多く、アナログ出力からArduinoにつないで、analogRead()1つで読み取れます。ただし、このセンサーが出す数値を意味のある測定値に変えるには、多くのチュートリアルが説明している以上の知識が少し必要です。

このガイドでは、センサー内部で何が起きているのか、どのガスにどれくらい敏感なのか、そして最初に本格的に使う前に24〜48時間の加熱が必要な理由を説明します。続いて、負荷抵抗(RL)、センサー抵抗(Rs)、基準抵抗(R0)を使ってppmの計算をステップごとに組み立て、きれいな空気の中でR0のキャリブレーションを行い、いちばんよくある質問に正直に答えます。MQ-135は本当にCO2を測れるのでしょうか?最後に、換気用のファンを自動で動かし、ESP32で測定値をタイムスタンプ付きのファイルに記録し、Raspberry Pi Pico用のMicroPythonコードを書き、MQ-135をCCS811やNDIR方式のCO2センサーと比べます。

MQ-135空気質センサーモジュール
MQ-135モジュール:ヒーター付き金属酸化物センサー、LM393コンパレーター、感度調整用ポテンショメーター、VCC・GND・DO・AOピン。

MQ-135とは?その仕組み

ステンレスの網の奥には小さなセラミック管があり、その表面は二酸化スズ(SnO2)で覆われています。中を通るヒーター線がこの層を数百℃まで加熱します。きれいな空気の中では、酸素分子がSnO2の表面に付着して材料中の自由電子を捕らえるため、層の抵抗は高いままです。アンモニア、アルコール、ベンゼンのような還元性ガスが空気中に入ると、表面の酸素と反応して電子が解放され、抵抗が下がります。ガスが濃いほど、抵抗は低くなります。モジュールはこの抵抗を負荷抵抗と組み合わせて分圧回路を作っているので、ガスが増えるとAOピンの電圧が上がります。

特徴値
検知材料二酸化スズ(SnO2)、ヒーター付きセラミック管
ヒーター電圧5 V ± 0.1 V
ヒーター抵抗と電流33 Ω ± 5%、約150 mA
ヒーター電力最大800 mW
検知範囲NH3 10〜300 ppm、ベンゼン 10〜1000 ppm、アルコール 10〜300 ppm
初回の予熱24時間を超える
動作環境−10〜45 °C、相対湿度95%未満
モジュールの出力AO(アナログ)とDO(LM393によるデジタルしきい値)

MQ-135はどのガスを検知しますか?

MQ-135は、1種類のガスだけを測る選択的なセンサーではありません。加熱されたSnO2の表面と反応する多くのガスに同時に反応し、それらを区別できません。メーカーは、特にアンモニア、硫黄化合物、ベンゼン系の蒸気に敏感なセンサーとしています。アルコール、煙、窒素酸化物にもはっきり反応します。そのため、MQ-135は「空気の汚れの指標」と考えるのがいちばんです。値が上がれば空気中で何かが変わったことはわかりますが、何が変わったかまではわかりません。都市ガスやLPG(プロパンガス)の漏れには、その用途向けに設計されたMQ-2ガスセンサーのほうがずっと適しています。

ガス家庭での発生源MQ-135の反応
アンモニア(NH3)掃除用品、猫砂、肥料高い
ベンゼン、トルエン、溶剤塗料、シンナー、接着剤、たばこの煙高い
アルコールの蒸気手指消毒剤、オーデコロン、香水高い
煙料理、たばこ、ろうそく中程度
窒素酸化物(NOx)ガスコンロ、排気ガス中程度
二酸化炭素(CO2)人の呼気、閉め切った混雑した部屋弱い、間接的

MQ-135はCO2を測れますか?

短く答えると、本当の意味では測れず、推定しています。MQ-135のデータシートにはCO2の曲線があり、ネット上のライブラリの多くはそこから「CO2 ppm」の値を計算しています。しかし、CO2は化学的に安定したガスで、SnO2の表面とはごく弱くしか反応しません。閉め切った部屋で人が呼吸すると、CO2と一緒に湿度や体から出る揮発性有機化合物も増え、MQ-135が実際に反応しているのはこちらです。つまり、センサーが示す「CO2」は、実は部屋全体の空気の汚れから推定した値です。いちばん簡単な証拠として、センサーの横でオーデコロンのふたを開けると、「CO2」の値が5000 ppmまで跳ね上がります。

とはいえ、MQ-135が役に立たないわけではありません。「部屋の空気がこもってきたので窓を開ける」「キッチンに煙が出ているので換気扇を回す」といった相対的な判断にはとてもよく使えます。ただし、教室、オフィス、寝室で本当のCO2濃度を知りたいなら、このガイドの最後の比較で紹介するNDIR方式のCO2センサーを使ってください。下の表は、本物のCO2計が示す値の意味をまとめたものです。MQ-135の推定値も同じ範囲で読み取れます。

CO2(ppm)意味
400–450屋外のきれいな空気
450–800換気のよい室内
800–1000許容範囲、換気を考えましょう
1000–1500空気がこもり、集中力が落ちることがあります。換気してください
1500–2000頭痛や眠気が出ることがあります
5000職場での8時間の暴露限界

MQ-135とArduinoの配線

モジュールのピンArduino UnoESP32Raspberry Pi Pico
VCC(ヒーターを含む)5VVIN(USBからの5 V)VBUS(USBからの5 V)
GNDGNDGNDGND
AO(アナログ出力)A0GPIO 34(10 kΩ / 20 kΩの分圧回路経由)GP26(ADC0、10 kΩ / 20 kΩの分圧回路経由)
DO(デジタルしきい値、任意)D2分圧回路経由で任意のGPIO分圧回路経由で任意のGPピン
MQ-135のヒーターだけで約150 mAを消費し、必ず5 Vが必要です。ESP32やPicoの3V3ピンから電源をとると、センサーが十分に熱くならず、測定値は意味のないものになります。モジュールを5 Vで動かすと、AO出力は5 Vまで上がることがあり、ESP32やPicoの3.3 VのADC入力を壊してしまいます。AOとGNDの間に10 kΩ + 20 kΩの分圧回路を作り、中点をADCピンにつないでください:5 V × 20 / 30 = 3.33 V。ほかの抵抗の組み合わせは分圧計算ツールで試せます。動作中のセンサーは手で持てないほど熱くなるので、網の部分に触れないでください。

予熱:24〜48時間の初回エージングと毎日のウォームアップ

MQシリーズのセンサーには2種類のウォームアップがあり、よく混同されます。1つ目は初回の予熱(エージング、burn-in)です。新品のセンサーや、使わずに長く保管していたセンサーの表面には、湿気や汚れがたまっています。メーカーは、標準的な測定の前にセンサーに24時間以上通電するよう求めています。実際には24〜48時間連続で動かすと、値がゆっくりドリフトして最後に安定するのがわかります。この期間が終わる前にR0のキャリブレーションをすると、数日のうちにすべての測定値がずれていきます。2つ目は毎日のウォームアップです。エージングを終えたセンサーでも、電源を入れるたびにヒーターが安定するまで数分かかり、最初の1分間の値は明らかに高すぎたり低すぎたりします。そのためコードには少なくとも2〜3分の待ち時間を入れています。精密な測定には10〜20分待つほうが安心です。

Rs、RL、R0:MQ-135でppmを計算するには?

モジュール内部では、センサーの抵抗Rsと基板上の負荷抵抗RLが直列につながり、5 Vを分け合っています。AOピンの電圧はVout = 5 × RL / (Rs + RL)になります。この式を逆にすると、測った電圧からセンサーのその時点の抵抗がわかります:Rs = RL × (5 − Vout) / Vout。Arduino UnoではADCも同じ5 Vを基準にしているので、電圧に変換する必要すらありません:Rs = RL × (1023 − 生の値) / 生の値。Rs単体には意味がありません。センサーの抵抗は個体ごとに大きく異なるからです。大事なのは、既知の空気の中での値、つまりR0に対するRsの比です。

データシートの曲線はRs/R0の比とガス濃度の関係を示していて、両対数グラフ上ではほぼ直線です。そのため、べき関数で表せます:ppm = A × (Rs/R0)^−B。よく使われるMQ-135のライブラリがCO2の曲線に使う係数は、A = 116.602、B = 2.769です。R0を求めるには、センサーをきれいな屋外の空気の中に置きます。現在の屋外のCO2は約420 ppmなので、式を逆にしてR0 = Rs × (420 / A)^(1/B) ≈ Rs × 1.59と計算します。これらの係数はほかのガスでは異なること、そして結果はあくまで推定値であることを忘れないでください。

モジュール裏面の小さなSMD抵抗の1つがRLで、表面のコードから値がわかります:102 = 1 kΩ、103 = 10 kΩ、203 = 20 kΩ。コードの読み方はSMD抵抗のコードで説明しています。RLを間違えて入力してもppmの結果は変わりません。R0も同じRLで計算されるので、比は変わらないからです。画面に表示されるRsとR0の数値の大きさが変わるだけです。ただし、RLがとても小さい(1 kΩ)と、きれいな空気ではAOの電圧がかなり低くなり、ADCの分解能が悪くなります。値がふらつくようなら、平均をとるのがよい対策です。

MQ-135のR0キャリブレーションの手順

  1. モジュールのVCCを5Vに、GNDをGNDに、AOをA0につなぎ、センサーを24〜48時間止めずに動かして初回の予熱を済ませます。
  2. 基板裏面の負荷抵抗のコードを読み、下のコードのRL_KOHMをそれに合わせて設定します。
  3. 回路をベランダや開けた窓の前など、車の排気ガス、たばこ、キッチンから離れたきれいな屋外の空気の中に持っていきます。
  4. キャリブレーション用のコードを書き込み、シリアルモニタを9600ボーで開いて、少なくとも10分待ちます。
  5. 画面のR0の平均値が数分間ほとんど変わらなくなったら、メモします。これがそのセンサーのR0です。
C++ (Arduino IDE)
1const byte MQ_PIN = A0;                 // MQ-135 AO
2const float RL_KOHM = 1.0;              // karttaki yük direnci: 102 = 1, 103 = 10 / load resistor on the board
3const float PARA = 116.6020682;         // CO2 eğrisi katsayıları / CO2 curve coefficients
4const float PARB = 2.769034857;
5const float CLEAN_AIR_PPM = 420.0;      // dış havadaki CO2 / CO2 in outdoor air
6
7float r0Average = 0;
8
9float readRs() {
10  long sum = 0;
11  for (int i = 0; i < 50; i++) {        // 50 ölçüm, yaklaşık 1 saniye / 50 readings, about 1 second
12    sum += analogRead(MQ_PIN);
13    delay(20);
14  }
15  float raw = sum / 50.0;
16  if (raw < 1) raw = 1;                 // sıfıra bölmeyi önle / avoid dividing by zero
17  return RL_KOHM * (1023.0 - raw) / raw;   // Rs = RL × (Vc − Vout) / Vout
18}
19
20void setup() {
21  Serial.begin(9600);
22  Serial.println(F("Temiz havada en az 10 dk bekle / wait at least 10 min in clean air"));
23}
24
25void loop() {
26  float rs = readRs();
27  float r0 = rs * pow(CLEAN_AIR_PPM / PARA, 1.0 / PARB);    // R0 = Rs × (420 / A)^(1/B)
28  if (r0Average == 0) r0Average = r0;
29  r0Average = r0Average * 0.95 + r0 * 0.05;                // yavaş ortalama / slow running average
30
31  Serial.print(F("Rs = "));
32  Serial.print(rs, 2);
33  Serial.print(F(" kOhm   R0 = "));
34  Serial.print(r0, 2);
35  Serial.print(F("   ortalama / average R0 = "));
36  Serial.println(r0Average, 2);
37}

コードは毎秒50回の測定を平均し、そこからRsとその時点のR0を計算して、この値をゆっくり変化する移動平均に加えます。移動平均のおかげで、風で運ばれてきた一瞬の排気ガスのにおいで結果が乱れることはありません。求めたR0は、そのセンサーとそのRLの値だけのものです。センサーを交換したら、キャリブレーションをやり直してください。数か月のうちにセンサーが劣化するとR0はゆっくりずれていくので、長く動かすプロジェクトでは数か月ごとにキャリブレーションを繰り返す習慣をつけるとよいでしょう。

測定コード:推定ppmと空気質のレベル

R0がわかったら、測定コードに入力します。コードはまず3分間ウォームアップを待ち、その後2秒ごとにRs/R0の比、推定ppm、上の表にもとづく空気質のラベルをシリアルモニタに表示します。きれいな空気では比は0.6前後で、小さくなるほど空気中のガスが多いことを意味します。試すには、センサーの近くで綿にオーデコロンを数滴たらしてください。数秒で比が下がってppmが上がり、綿を離すと数分で値が元に戻ります。

C++ (Arduino IDE)
1const byte MQ_PIN = A0;
2const float RL_KOHM = 1.0;              // kalibrasyondaki RL ile aynı / same RL as in calibration
3const float R0_KOHM = 3.6;              // kendi kalibrasyon sonucunla değiştir / replace with your calibration result
4const float PARA = 116.6020682;
5const float PARB = 2.769034857;
6const unsigned long WARMUP_MS = 180000; // 3 dk ısınma / 3 min warm-up
7
8float readRs() {
9  long sum = 0;
10  for (int i = 0; i < 20; i++) {
11    sum += analogRead(MQ_PIN);
12    delay(10);
13  }
14  float raw = sum / 20.0;
15  if (raw < 1) raw = 1;
16  return RL_KOHM * (1023.0 - raw) / raw;
17}
18
19const __FlashStringHelper* airLabel(float ppm) {
20  if (ppm < 800)  return F("Iyi / good");
21  if (ppm < 1000) return F("Orta / moderate");
22  if (ppm < 1500) return F("Havalandir / ventilate");
23  return F("Kotu / poor");
24}
25
26void setup() {
27  Serial.begin(9600);
28  Serial.println(F("Isiniyor / warming up..."));
29  while (millis() < WARMUP_MS) delay(100);
30}
31
32void loop() {
33  float ratio = readRs() / R0_KOHM;           // gaz arttıkça küçülür / falls as gas rises
34  float ppm = PARA * pow(ratio, -PARB);       // tahmini CO2 eşdeğeri / estimated CO2 equivalent
35
36  Serial.print(F("Rs/R0 = "));
37  Serial.print(ratio, 2);
38  Serial.print(F("   ~"));
39  Serial.print(ppm, 0);
40  Serial.print(F(" ppm   "));
41  Serial.println(airLabel(ppm));
42  delay(2000);
43}

温度と湿度はMQ-135にどう影響しますか?

金属酸化物センサーの抵抗は、ガスだけでなく室温や湿度にも左右されます。データシートの曲線によると、同じ空気でも温度が20 °Cから40 °Cに上がるとRs/R0の比がはっきり下がり、湿度が上がってもやはり下がります。センサーはこれを「ガスが増えた」と読み取ります。その結果、午後に日が差す部屋や、シャワーを浴びた後の浴室では、ppmの値が勝手に上がっていきます。これを減らす方法は2つあります。センサーを使う場所に近い温度と湿度でキャリブレーションするか、DHT22またはDHT11で温度と湿度を測り、ライブラリにある補正式を当てはめるかです。既製のMQ135ライブラリのgetCorrectedPPM(温度, 湿度)のような関数は、まさにこれを行います。DHT11より正確に温度と湿度を測りたいなら、BME280センサーが使えます。

換気扇の自動化

MQ-135がいちばん役立つのは、空気がよどんだときにファンや換気扇を自動で回す仕事です。ここでは2つのポイントが大切です。1つ目はヒステリシスです。ファンは1000 ppmを超えると回り始めますが、止まるのは値が800 ppmを下回ってからです。しきい値が1つだけだと、値がその前後でふらつく間、リレーが1秒ごとにカチカチ鳴ります。2つ目は最短運転時間です。ファンは一度回り始めたら少なくとも2分間は動くので、部屋の空気がきちんと入れ替わります。コードは測定値をなめらかにして、一瞬の跳ね上がりでファンが動かないようにもしています。

C++ (Arduino IDE)
1const byte MQ_PIN = A0;
2const byte FAN_PIN = 7;                    // röle modülü IN / relay module IN
3const bool RELAY_ACTIVE_LOW = true;        // çoğu modül LOW ile çeker / most modules switch on with LOW
4const float RL_KOHM = 1.0;
5const float R0_KOHM = 3.6;                 // kendi kalibrasyon sonucun / your calibration result
6const float PARA = 116.6020682;
7const float PARB = 2.769034857;
8const float FAN_ON_PPM = 1000;             // bunun üstünde fanı aç / fan on above this
9const float FAN_OFF_PPM = 800;             // bunun altında fanı kapat / fan off below this
10const unsigned long MIN_RUN_MS = 120000;   // fan en az 2 dk çalışır / fan runs at least 2 min
11const unsigned long WARMUP_MS = 180000;    // 3 dk ısınma / 3 min warm-up
12
13bool fanOn = false;
14unsigned long fanStart = 0;
15float smoothPpm = 0;
16
17void setFan(bool on) {
18  fanOn = on;
19  if (on) fanStart = millis();
20  bool level = RELAY_ACTIVE_LOW ? !on : on;
21  digitalWrite(FAN_PIN, level ? HIGH : LOW);
22}
23
24float readPpm() {
25  long sum = 0;
26  for (int i = 0; i < 20; i++) {
27    sum += analogRead(MQ_PIN);
28    delay(10);
29  }
30  float raw = sum / 20.0;
31  if (raw < 1) raw = 1;
32  float rs = RL_KOHM * (1023.0 - raw) / raw;
33  return PARA * pow(rs / R0_KOHM, -PARB);
34}
35
36void setup() {
37  digitalWrite(FAN_PIN, RELAY_ACTIVE_LOW ? HIGH : LOW);   // önce kapalı seviye, sonra çıkış / off level first, then output
38  pinMode(FAN_PIN, OUTPUT);
39  Serial.begin(9600);
40  while (millis() < WARMUP_MS) delay(100);
41  smoothPpm = readPpm();
42}
43
44void loop() {
45  smoothPpm = smoothPpm * 0.8 + readPpm() * 0.2;          // ani sıçramaları yumuşat / smooth out spikes
46
47  if (!fanOn && smoothPpm > FAN_ON_PPM) {
48    setFan(true);
49  } else if (fanOn && smoothPpm < FAN_OFF_PPM && millis() - fanStart > MIN_RUN_MS) {
50    setFan(false);
51  }
52
53  Serial.print(F("~"));
54  Serial.print(smoothPpm, 0);
55  Serial.print(F(" ppm   fan: "));
56  Serial.println(fanOn ? F("ACIK / ON") : F("KAPALI / OFF"));
57  delay(1000);
58}

リレーモジュールのINピンはD7に、VCCとGNDはArduinoにつなぎます。ファンは、リレーのCOM端子とNO端子を通して専用の電源につなぎます。12 VのPCファンを使うなら、リレーよりMOSFETのほうが静かで長持ちします。配線はミニウォーターポンプの回路と同じです。浴室の換気扇のような230 Vの機器をスイッチングする場合(日本の商用電源は100V(50/60Hz)です)、リレーモジュールのガイドにある安全上のルールを必ず守ってください。商用電源側は閉じたケースに収め、通電中は絶対に回路に触れないでください。setup()でまずピンをオフのレベルにしてから出力に設定すると、アクティブLOWのリレーが電源投入時に一瞬オンになるのを防げます。

ESP32で空気質を記録する

空気質が1日の中でどう変わるかを見ると、その場の1回の測定よりはるかに多くのことがわかります。朝までに寝室にたまるよどんだ空気、料理中のキッチンでの急上昇、窓を開けたときの効果は、記録があって初めて見えてきます。下のESP32のコードは1分ごとに測定し、インターネット(NTP)から時刻を取得して各行に日付と時刻を付け、結果をESP32自身のフラッシュメモリにあるLittleFSファイルシステムにCSVとして保存します。ブラウザでESP32のIPアドレスを開くと最新の測定値が表示され、ワンクリックで記録全体をExcelで開けるCSVファイルとしてダウンロードできます。追加のライブラリは必要ありません。

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <WebServer.h>
3#include <LittleFS.h>
4#include <time.h>
5
6const char* WIFI_SSID = "AgAdi";              // Wi-Fi adın / your Wi-Fi name
7const char* WIFI_PASS = "Sifre123";           // Wi-Fi şifren / your Wi-Fi password
8const int MQ_PIN = 34;                        // AO, 10k/20k bölücü üzerinden (ADC1) / through a 10k/20k divider (ADC1)
9const float DIVIDER = 1.5;                    // (10k + 20k) / 20k
10const float RL_KOHM = 1.0;
11const float R0_KOHM = 3.6;                    // ESP32 ile yeniden kalibre et / recalibrate with the ESP32
12const float PARA = 116.6020682;
13const float PARB = 2.769034857;
14const unsigned long LOG_INTERVAL = 60000;     // dakikada bir kayıt / one entry per minute
15
16WebServer server(80);
17unsigned long lastLog = 0;
18float lastPpm = 0;
19
20float readPpm() {
21  uint32_t mv = 0;
22  for (int i = 0; i < 20; i++) {
23    mv += analogReadMilliVolts(MQ_PIN);       // kalibre edilmiş mV / calibrated mV
24    delay(5);
25  }
26  float vout = (mv / 20.0) / 1000.0 * DIVIDER;   // sensör çıkışı (V) / sensor output (V)
27  if (vout < 0.05) vout = 0.05;
28  float rs = RL_KOHM * (5.0 - vout) / vout;
29  return PARA * pow(rs / R0_KOHM, -PARB);
30}
31
32void handleRoot() {
33  String html = "<!DOCTYPE html><html><head><meta charset='utf-8'>"
34                "<meta name='viewport' content='width=device-width,initial-scale=1'></head>"
35                "<body style='font-family:sans-serif'><h2>MQ-135</h2><p>Son ölçüm / last reading: ~";
36  html += String(lastPpm, 0);
37  html += " ppm</p><p><a href='/log.csv'>CSV indir / download</a> | "
38          "<a href='/clear'>Kaydı sil / clear log</a></p></body></html>";
39  server.send(200, "text/html", html);
40}
41
42void handleCsv() {
43  File f = LittleFS.open("/log.csv", "r");
44  if (!f) {
45    server.send(404, "text/plain", "Kayit yok / no log yet");
46    return;
47  }
48  server.streamFile(f, "text/csv");
49  f.close();
50}
51
52void handleClear() {
53  LittleFS.remove("/log.csv");
54  server.sendHeader("Location", "/");
55  server.send(303);
56}
57
58void setup() {
59  Serial.begin(115200);
60  analogSetPinAttenuation(MQ_PIN, ADC_11db);  // yaklaşık 0-3,1 V ölçüm aralığı / about 0-3.1 V range
61  if (!LittleFS.begin(true)) {                // true: ilk açılışta biçimlendir / format on first use
62    Serial.println("LittleFS hatasi / error");
63  }
64  WiFi.mode(WIFI_STA);
65  WiFi.begin(WIFI_SSID, WIFI_PASS);
66  while (WiFi.status() != WL_CONNECTED) {
67    delay(500);
68    Serial.print(".");
69  }
70  Serial.println();
71  Serial.println(WiFi.localIP());
72  configTime(3 * 3600, 0, "pool.ntp.org");    // UTC+3; kendi saat dilimine göre değiştir / change to your time zone
73
74  server.on("/", handleRoot);
75  server.on("/log.csv", handleCsv);
76  server.on("/clear", handleClear);
77  server.begin();
78  lastLog = millis();                         // ilk kayıt 1 dk sonra, sensör ısınırken / first entry after 1 min
79}
80
81void loop() {
82  server.handleClient();
83  if (millis() - lastLog >= LOG_INTERVAL) {
84    lastLog = millis();
85    lastPpm = readPpm();
86    char stamp[20] = "saat-yok";
87    struct tm now;
88    if (getLocalTime(&now, 100)) {
89      strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M", &now);
90    }
91    File f = LittleFS.open("/log.csv", "a");  // dosyanın sonuna ekle / append to the file
92    if (f) {
93      f.printf("%s,%.0f\n", stamp, lastPpm);
94      f.close();
95    }
96    Serial.printf("%s  ~%.0f ppm\n", stamp, lastPpm);
97  }
98}

ESP32のADCはUnoとは測り方が違うので、この回路でR0のキャリブレーションをやり直すのがおすすめです。キャリブレーション用コードのreadRs()関数を、ここでのreadPpm()の考え方に合わせて書き換えるだけです。センサーはGPIO 32〜39のADC1ピンのどれかにつないでください。ADC2のピンはWi-Fiがオンの間は使えません。1行は約22バイトで、1分に1件記録すると1日に約32 KBのデータがたまります。デフォルトのパーティション構成にある1.5 MBの領域で、おおよそ1か月半もちます。もっと長く記録したい場合はSDカードモジュールを使えます。データを直接スプレッドシートに送りたいなら、ESP32とGoogleスプレッドシートの方法を見てください。

Raspberry Pi PicoでMQ-135を使う(MicroPython)

Picoでは、モジュールにVBUSピンから5 Vを供給し、AO出力はやはり10 kΩ / 20 kΩの分圧回路を通してGP26につなぎます。PicoのADCは3.3 Vを基準とした16ビットの値(0〜65535)を返します。コードはこれをまず電圧に、続いて分圧比を使ってセンサーの出力に変換します。式はArduinoと同じです。VBUSの電圧はUSBの電源によって4.8〜5.1 Vの間で変わることがあるので、精密な結果がほしいときはテスターでVBUSを測り、コードの5.0を修正してください。

MicroPython
1from machine import ADC
2import time
3
4adc = ADC(26)                          # GP26 = ADC0, AO bölücü üzerinden / AO through a divider
5DIVIDER = 1.5                          # (10k + 20k) / 20k
6RL = 1.0                               # kΩ, karttaki yük direnci / load resistor on the board
7R0 = 3.6                               # kΩ, kendi kalibrasyon sonucun / your calibration result
8PARA, PARB = 116.6020682, 2.769034857
9VCC = 5.0                              # VBUS gerilimi / VBUS voltage
10
11def read_rs():
12    total = 0
13    for _ in range(20):
14        total += adc.read_u16()
15        time.sleep_ms(5)
16    v = total / 20 / 65535 * 3.3 * DIVIDER    # sensör çıkışı (V) / sensor output (V)
17    v = max(v, 0.05)                          # sıfıra bölmeyi önle / avoid dividing by zero
18    return RL * (VCC - v) / v
19
20print("Isiniyor / warming up...")
21time.sleep(180)                        # 3 dk ısınma / 3 min warm-up
22while True:
23    ratio = read_rs() / R0
24    ppm = PARA * ratio ** -PARB        # tahmini CO2 eşdeğeri / estimated CO2 equivalent
25    print("Rs/R0 = %.2f   ~%d ppm" % (ratio, ppm))
26    time.sleep(2)

MQ-135、CCS811、NDIR方式CO2センサーの比較

空気質センサーには3つの種類があり、それぞれ答える問いが違います。MQ-135は安くて、アナログで、シンプルです。空気の全体的な変化を示します。CCS811も金属酸化物センサーですが、内部に専用のマイコンを持ち、ヒーターを自分で管理して、TVOC(総揮発性有機化合物)とeCO2(等価CO2)の値をI2Cで直接出力します。詳しくはCCS811空気質センサーのガイドで解説しています。本当のCO2を測れる唯一のグループがNDIR(非分散型赤外線)センサーです。MH-Z19BやSenseAir S8のようなモジュールは、4.26 µmの赤外線がCO2分子にどれだけ吸収されるかを測るので、ほかのガスの影響をほとんど受けません。

特徴MQ-135CCS811NDIR(MH-Z19B、SenseAir S8)
測定原理加熱したSnO2の抵抗金属酸化物 + 内蔵マイコン赤外線の吸収
本当のCO2を測れる?測れない、推定する測れない、VOCからeCO2を計算測れる
出力アナログ(とデジタルしきい値)I2CUARTまたはPWM
電源5 V、約150 mA3.3 V、平均約30 mA5 V、平均60 mA未満
ウォームアップ新品時24〜48時間、その後は数分新品時48時間、その後は20分約3分
精度低い、相対的な変化向け中程度、相対的なVOCの追跡向け約±50 ppm + 読み取り値の3〜5%
価格いちばん安い中程度いちばん高い

まとめると、学習、アラームやファンの起動、空気の変化の観察にはMQ-135で十分です。塗料、接着剤、掃除用品の蒸気を追跡したいなら、CCS811のほうがなめらかで安定したデータが得られます。教室、オフィス、寝室で「そろそろ窓を開けるべき?」に答える信頼できる数値が必要なら、NDIR方式のCO2センサーへの投資が必要です。両方を組み合わせる人も多く、NDIRセンサーでCO2を追い、MQ-135でキッチンの煙や溶剤のにおいのような突然の出来事をとらえます。

MQ-135を使ったプロジェクトアイデア

  • 部屋の空気インジケーター:空気質に応じて緑・黄・赤に光るRGB LED、またはOLEDディスプレイでのリアルタイムグラフ。
  • スマートなキッチンの換気扇:料理中に煙や湯気が増えると換気扇を回し、空気がきれいになると止めるシステム。
  • ガレージや塗装作業場の警報器:溶剤の蒸気が増えるとブザーで知らせ、換気を始める回路。
  • 猫砂のモニター:アンモニアが増えて猫砂を替える時期になるとスマホに通知するESP32のプロジェクト。
  • 教室の空気ボード:ESP32で測定値を記録して授業時間と比べ、換気の習慣を見えるようにするグラフ。
  • ホームオートメーション:値をMQTTでHome Assistantに送り、空気が悪くなったらスマートプラグで空気清浄機をオンにします。

よくある間違いと対策

症状考えられる原因対策
値が何時間もずれ続ける初回の予熱が終わっていないセンサーを24〜48時間止めずに動かしてからキャリブレーションする
電源投入時に値が高すぎる、または低すぎる毎日のウォームアップを待っていない測定前に少なくとも3分、できれば10〜20分待つ
きれいな空気でも数千ppmになるR0が間違っている、または室内でキャリブレーションしたきれいな屋外の空気でキャリブレーションをやり直す
ESP32で値がいつも最大になるAOが5 Vまで上がっている、またはセンサーを3.3 Vで動かしているセンサーを5 Vで動かし、AOに10k/20kの分圧回路を入れる
Arduinoが熱くなる、またはUSBが切れる150 mAのヒーターとほかの負荷がUSBに負担をかけている外部の5 V電源を使い、GNDを共通にする
日光が当たったりシャワーの後にppmが上がる温度と湿度の影響似た条件でキャリブレーションするか、DHT22で補正する
MQ-135の値は絶対的な測定値ではなく、基準値(ベースライン)からの変化として読み取ってください。センサーが温まったら、空気がきれいなときに測った値を基準値とし、「基準値より30%高い」のような相対的なしきい値で判断します。この方法は、キャリブレーションの誤差、センサーの劣化、季節の変化に対してずっと強くなります。測定値をなめらかにする移動平均やメディアンフィルタは、センサーデータのフィルタリングで解説しています。

限界さえ理解していれば、MQ-135は空気質を学び、換気を自動化するための最高の入門センサーです。5 Vで動かし、24〜48時間予熱し、きれいな空気でR0を求め、結果は相対的に読み取り、本当のCO2が必要になったらNDIRセンサーに乗り換えましょう。BlueGraysアプリには、「ガスセンサーの使い方」、「炎・ガスセンサーアラーム」、「ESP32 MQ-2ガス・煙検知アラーム」のプロジェクトに加え、DHT11とDHT22による温湿度測定やESP32温度データロガーのプロジェクトが、ステップごとの回路図とコード付きでそろっています。

よくある質問

MQ135でCO2は測れますか?

MQ-135は本当の意味でCO2を測るのではなく、推定しています。センサーのCO2への反応はごく弱く、実際に反応しているのはアルコール、アンモニア、ベンゼンなどのガスや、人が出す揮発性の化合物です。ライブラリが表示するCO2 ppmの値はこの全体的な汚れから計算したもので、本当のCO2を測るにはMH-Z19BのようなNDIRセンサーが必要です。

MQ135のウォームアップには何分かかりますか?

新品のMQ-135には24〜48時間の初回の予熱が必要で、キャリブレーションの前に必ず済ませておきます。その後は、電源を入れるたびに約2〜3分で使えるようになります。最も安定した値を得るには、10〜20分待つほうがよいでしょう。

MQ135のR0はどうやって求めますか?

予熱を終えたセンサーをきれいな屋外の空気の中に置き、Rs = RL × (1023 − 生の値) / 生の値で抵抗を計算します。屋外のCO2を約420 ppmとして、R0 = Rs × (420 / 116.6)^(1 / 2.769)を計算します。数分間の測定値を平均してください。求めたR0は、そのセンサーとその負荷抵抗だけのものです。

MQ135とMQ2の違いは何ですか?

どちらもヒーター付きの金属酸化物センサーですが、反応するように調整されたガスが違います。MQ-2はLPG、プロパン、メタン、水素、煙向けなので、ガス漏れ警報に使います。MQ-135はアンモニア、ベンゼン、アルコール、NOx、煙などの空気汚染物質に敏感で、空気質の監視に使います。

MQ135はESP32で使えますか?

使えますが、2つの点に注意してください。センサーのヒーターには5 Vが必要なので、モジュールはESP32のVINピンから電源をとります。また、AO出力も5 Vまで上がることがあるので、10 kΩと20 kΩの分圧回路で3.3 Vに下げ、GPIO 32〜39のADC1ピンにつないでください。

MQ135はどのガスを検知しますか?

MQ-135がいちばん敏感なのは、アンモニア、硫黄化合物、ベンゼンのような芳香族の蒸気です。アルコール、煙、窒素酸化物にもはっきり反応し、CO2と一酸化炭素には弱く反応します。これらのガスを区別することはできず、空気全体の変化を示すだけです。

全プロジェクトを見る
DHT11で温度と湿度を測定
#55中級

DHT11で温度と湿度を測定

部屋の温度は何度で、空気は湿っているでしょうか、それとも乾燥しているでしょうか?DHT11センサーが温度と湿度を測定し、Arduinoが2行のLCDディスプレイに値を表示して、数秒ごとに更新します。センサーに異常があればエラーメッセージが表示されます。自分だけの小さな気象観測装置を作る第一歩です。

4.7(47)
73
ガスセンサーの使い方
#51初級
プレミアム

ガスセンサーの使い方

キッチンのガス漏れに人より先に気づく小さな見張り役です。ガスセンサーは空気中のガスを常に監視します。異常がないときは緑色のLEDが点灯し、ガス濃度が一定の値を超えると赤色のLEDが点灯してブザーが鳴ります。家庭用ガス警報器の仕組みを実際に確かめられます。

4.6(37)
50
炎・ガスセンサーで警報
#52上級
プレミアム

炎・ガスセンサーで警報

ガスが漏れたり炎が出たりすると、すぐに知らせる小さな見張り役です。ガスセンサーは空気中のガスを、炎センサーは火の光を監視します。異常がないときは緑色のLEDが点灯し、どちらかが危険を検出すると、赤色のLEDの点滅とブザーの断続音で警報を出します。危険がなくなるまで警報は続きます。

4.8(52)
67
ESP32 MQ-2ガス・煙検知アラーム
#122中級

ESP32 MQ-2ガス・煙検知アラーム

このプロジェクトでは、MQ-2ガスセンサーモジュールで可燃性ガス(LPG、メタン、プロパン)と煙を検知し、パッシブブザーでサイレンのような警報音を鳴らします。センサーのしきい値判定用デジタル出力(DO)をESP32のGPIO 4ピンに接続します。ガスや煙がしきい値を超えるとDOがLOWになり、アラームが作動します。感度はモジュール上のポテンショメーターで調整します。パッシブブザーをtone()で駆動し、2つの周波数を交互に切り替えてサイレン音を作ります。

4.7(28)
43
ESP32 DHT22温湿度計(LCDディスプレイ付き)
#117中級

ESP32 DHT22温湿度計(LCDディスプレイ付き)

DHT22センサーで周囲の温度と湿度を測定し、16x2 I2C LCDにリアルタイムで表示するプロジェクトです。Adafruit DHTライブラリを使い、1本の信号線を使うプロトコルでセンサーからデータを読み取ります。IoT気象観測装置やスマートホームの自動化、温室の環境監視システムの基礎となります。ESP32のGPIO 4ピンを介してDHT22と通信します。

4.8(38)
56
ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)
#144上級
プレミアム

ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)

ESP32で日時付きの温度・湿度記録システムを作ります!DHT22で1分ごとに温度と湿度を読み取り、DS1307 RTCから取得した日時とともにmicroSDカードへCSV形式で記録します。OLEDには最新の温度を大きな文字で表示し、湿度のパーセント値、日時、総記録数も表示します。SDカードの/temp_log.csvファイルはExcel、Google Sheets、Pythonのpandasで開いて分析できます。温室の監視、家や部屋の温度管理、実験室のデータ収集、食品の保管環境や養蜂箱の監視に適しています。

4.7(7)
12
ESP32 I2C LCDディスプレイ(16x2)
#119中級
プレミアム

ESP32 I2C LCDディスプレイ(16x2)

このプロジェクトでは、16x2文字のLCDをI2CでESP32に接続して文字を表示します。I2Cならデータ用の2本のピン(SDAとSCL)だけで制御でき、従来のパラレル接続のように6本以上のピンを使わずに済みます。コードでは1行目に固定メッセージ、2行目に毎秒増えるカウンターを表示します。また、起動時にI2Cスキャナーを実行し、ディスプレイのアドレス(通常は0x27または0x3F)をシリアルモニタに表示します。

4.9(23)
40
ボタンを押している間だけ220V電球を点灯
#19上級
プレミアム

ボタンを押している間だけ220V電球を点灯

ボタンを押している間は実際の220ボルトの電球が点灯し、離すとすぐに消灯します。Arduinoの小さな信号では電球を直接点灯できないため、電気で動くスイッチであるリレーを間に入れ、電球の回路を開閉します。ボードで家庭の照明を操作するための第一歩です。家庭用電源を扱う際は十分に注意し、不安がある場合は詳しい人に手伝ってもらってください。

4.4(30)
38