← All guides

Arduino air quality monitor

A pocket-sized desk gadget that reads the gunk in your indoor air and shows it as a number plus a verdict ("Good" / "Poor" / "Get a window open"). Five components, an evening of work, and you'll know whether your home office is actually a C

By TomKnox · 11 min read · 2176 words

Build an air quality monitor with Arduino, MQ-135, and an OLED display

A pocket-sized desk gadget that reads the gunk in your indoor air and shows it as a number plus a verdict ("Good" / "Poor" / "Get a window open"). Five components, an evening of work, and you'll know whether your home office is actually a CO₂ swamp by lunchtime.

Finished MQ-135 air quality monitor on a breadboard with OLED display showing readings

The MQ-135 is a cheap, popular tin-dioxide gas sensor that responds to CO₂, NH₃, NOₓ, benzene, alcohol, and smoke. It is not a calibrated lab instrument — read the "What this sensor actually tells you" section below before you trust the numbers — but it is great at showing trends (kitchen frying, a room full of people, a stale bedroom in the morning), and that is what most makers actually want.

What you'll need

Item AliExpress Amazon
Arduino Nano (CH340 clone is fine) AliExpress Link Amazon Link
MQ-135 air quality sensor module (with onboard comparator) AliExpress Link Amazon Link
0.96″ SSD1306 OLED, 128×64, I²C AliExpress Link Amazon Link
DHT22 temperature & humidity sensor (optional, for compensation) AliExpress Link Amazon Link
830-point breadboard AliExpress Link Amazon Link
Dupont jumper wires (M-M and M-F, 40-pin) AliExpress Link Amazon Link
5 V / 2 A USB power supply (for permanent install) AliExpress Link Amazon Link

Notes on the parts

  • Get the MQ-135 on a breakout board, not a bare sensor. The board gives you a 5 V regulator, a load resistor, and an LM393 comparator with a screw-pot for the digital threshold. You'll only use the analog (AO) pin in this build.
  • Skip the 0.91″ OLED. It's the same money as the 0.96″ but half the screen real estate. You want the 128×64 SSD1306.
  • The DHT22 is optional. The MQ-135 drifts with temperature and humidity. If you want to compensate (and write the slightly more advanced sketch later), grab one. If you just want a number on a screen, skip it.

MQ-135 sensor module showing the metal sensor cap, comparator IC, and four-pin header

What this sensor actually tells you

This is the part most tutorials skip and then everyone is confused later.

The MQ-135 is a resistive gas sensor. A heater inside warms a tin-dioxide (SnO₂) bead; the bead's resistance drops when reducing gases hit it. The module gives you a voltage that maps inversely to that resistance.

Three things you need to internalise before wiring anything:

  1. It needs to warm up. A lot. Datasheet says 24 hours of "burn-in" the first time you power it on. Practical reality: the first 10–15 minutes of every power-on are useless, and the first day after unboxing is really useless. Plan for that.
  2. It is not selective. A high reading could be CO₂, or it could be the alcohol from the hand sanitiser you just used three feet away. The sensor responds to a basket of gases. Treat the output as "general air-yuckiness index", not "CO₂ ppm".
  3. Every individual sensor has a different baseline. Two MQ-135s from the same batch can read 30 % apart in identical air. If you want anything close to absolute ppm you must calibrate your specific sensor in known clean air (see "Calibrating R0" below).

If you want true, calibrated CO₂ in ppm, you eventually want an SCD40 or SCD41 NDIR sensor. But the MQ-135 is $3 and gets you 80 % of the way there, which is why we're here.

Wiring it up

Fritzing-style wiring diagram showing Arduino Nano connected to MQ-135 and SSD1306 OLED on a breadboard

Six wires total, plus power.

MQ-135 → Arduino Nano
- VCC5V
- GNDGND
- AOA0
- DO → leave unconnected

SSD1306 OLED → Arduino Nano
- VCC5V (or 3V3 — most modules accept both)
- GNDGND
- SDAA4
- SCLA5

That's it. The OLED's I²C address is almost always 0x3C; if your display stays blank, scan the bus with an I2C-scanner sketch and try 0x3D.

A couple of pitfalls:

  • The MQ-135 heater pulls about 150 mA continuously. That is fine over USB, but if you try to run this off a 9 V battery through the Nano's onboard regulator you'll cook the regulator. Power it off USB or a proper 5 V supply.
  • The sensor cap gets hot. Like, "don't touch it for more than a second" hot. That's normal — it's the heater doing its job. Just don't enclose it in something flammable.

The code

Two libraries from the Arduino Library Manager: Adafruit SSD1306 (it'll pull in Adafruit GFX automatically) and MQ135 by Georg Krocker. Optionally DHT sensor library by Adafruit if you added the DHT22.

Here's the bare-minimum sketch — reads the analog value, classifies it, and prints to the OLED:

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <MQ135.h>

#define OLED_W 128
#define OLED_H 64
#define MQ_PIN A0

Adafruit_SSD1306 display(OLED_W, OLED_H, &Wire, -1);
MQ135 gasSensor = MQ135(MQ_PIN);

void setup() {
  Serial.begin(9600);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
}

void loop() {
  int raw  = analogRead(MQ_PIN);
  float ppm = gasSensor.getPPM();   // CO2-equivalent ppm
  const char *verdict;
  if      (raw < 200) verdict = "GOOD";
  else if (raw < 400) verdict = "MODERATE";
  else if (raw < 600) verdict = "POOR";
  else                verdict = "OPEN A WINDOW";

  display.clearDisplay();
  display.setCursor(0, 0);  display.setTextSize(1);
  display.print("Air Quality");
  display.setCursor(0, 16); display.setTextSize(2);
  display.print(verdict);
  display.setCursor(0, 44); display.setTextSize(1);
  display.print("raw="); display.print(raw);
  display.print(" ppm="); display.print(ppm, 0);
  display.display();

  Serial.print(raw); Serial.print('\t'); Serial.println(ppm);
  delay(1000);
}

The thresholds (200 / 400 / 600) are starting points, not gospel — yours will depend on your specific sensor's baseline. Run it for an hour in clean air, see what raw settles to, and bump the bands up by that amount.

OLED display showing air quality verdict and raw sensor reading

Calibrating R0 (optional, but worth it)

The getPPM() call above is using the library's default RZERO of 76.63, which is almost certainly wrong for your sensor. To get a sensible CO₂-equivalent ppm number:

  1. Power the sensor on and let it run for at least 24 hours. Outside, on a balcony, or by an open window — somewhere with genuinely fresh air.
  2. Run a one-line sketch that prints gasSensor.getRZero() once a minute.
  3. After it stabilises, average the last 30 minutes of readings. That's your RZERO.
  4. Either edit MQ135.h to set RZERO to that number, or fork the library and add a setter. Recompile, reflash, done.

There is one more subtlety: most cheap MQ-135 modules ship with a 1 kΩ load resistor soldered between AO and GND, but the datasheet (and the MQ135.h library) assume a 22 kΩ load. If you want the absolute ppm number to be even close to right, either desolder the SMD resistor and swap in a 22 kΩ, or set #define RLOAD 1.0 in MQ135.h. For a "trend monitor" you can ignore all this and just watch the raw value.

Troubleshooting

Common issues

  • Sensor reads nothing or stays at zero. The MQ-135 needs 24 hours of burn-in on first power-up, and 10–15 minutes after every cold start. If the raw value is stuck at 0 or 1023, check that AO (not DO) is wired to A0 — the digital output pin won't give you analog readings.
  • OLED stays blank. Try changing OLED_ADDR from 0x3C to 0x3D. If still blank, run an I²C scanner sketch to confirm the display's address and that it appears on the bus at all.
  • Readings jump wildly between sane values and nonsense. The sensor is picking up a transient gas source — hand sanitiser, cooking fumes, or even breath directed at the sensor. Move it away from your desk and let it settle for 5 minutes in still air.
  • Every individual sensor has a different baseline. Two MQ-135s from the same batch can read 30 % apart in identical air. Calibrate R0 (see above) before trusting absolute numbers.

Diagnostic procedures

  1. Open Serial Monitor at 9600 baud and watch the raw + ppm values for 2 minutes. A healthy sensor shows a slowly drifting number that responds to breath or incense within seconds. If it's flat, check wiring.
  2. Verify power with a multimeter: measure between MQ-135 VCC and GND — should read ~4.8–5.0 V. Measure OLED VCC to GND — should also be ~4.8–5.0 V (or 3.3 V if you wired it that way).
  3. Check I²C communication: Upload the "Wire → Scan" example from the Arduino IDE examples, open Serial Monitor at 9600 baud. You should see 0x3C or 0x3D. If nothing appears, SDA/SCL are swapped or disconnected.
  4. Sensor heater check: After 2 minutes of power-on, the metal cap on the MQ-135 should be warm (not scalding). If it's cold, the sensor isn't getting power — check VCC and GND connections.

Code fixes

Error message Cause Fix
'MQ135' was not declared in this scope MQ135 library not installed Library Manager → search "MQ135" by Georg Krocker → Install
Adafruit_SSD1306.h: No such file or directory SSD1306 library missing Library Manager → install "Adafruit SSD1306" (pulls in GFX automatically)
'Wire' was not declared Missing #include <Wire.h> Add it at the top of the sketch — required for both I²C OLED and any DHT22
exit status 1 — too few arguments to function Library version mismatch Check that you're using Georg Krocker's MQ135 library (v1.0+), not a fork with different API

Hardware checks

  • MQ-135 heater draws ~150 mA continuously. Verify your power supply can handle this — USB from the IDE is fine, but a 9 V battery through the Nano's onboard regulator will overheat and brown out. Use a proper 5 V / 2 A wall adapter for permanent installs.
  • Check wire continuity: Unplug everything and use a multimeter on continuity mode to verify each jumper wire conducts from end to end. Cheap Dupont wires are the #1 cause of intermittent connections.
  • Sensor cap gets hot — this is normal. The internal heater runs at ~250 °C. Don't enclose it in flammable material or seal it in a tight plastic box without ventilation holes.

Common mistakes

  • Reading the sensor in the first 60 seconds. The heater hasn't reached temperature. Add a delay(60000) at the end of setup() if you care.
  • Mounting the sensor in still air inside a sealed enclosure. It needs airflow. Either drill plenty of vents or leave it on an open breadboard.
  • Putting the sensor next to anything alcohol-based. Hand sanitiser, perfume, isopropyl, marker pens — they all spike it hard. If your monitor reads "TOXIC" every time you sit down, check what's on your hands.
  • Trusting the absolute ppm number without the calibration steps above. Use the raw analog value as a relative trend indicator instead.

Where to take it next

  • Add a buzzer on a digital pin and beep when the value crosses your "open a window" threshold.
  • Log to an SD card with a DS3231 RTC for time-stamped data — then plot a week of your bedroom CO₂ overnight and prepare to be horrified.
  • Send readings to Home Assistant via an ESP32 instead of the Nano. Same wiring, same code with minor tweaks (Wire.begin(SDA, SCL) plus your favourite MQTT library).
  • Compensate for temperature and humidity with the DHT22. The MQ-135 library has a getCorrectedPPM(temp, humidity) method that takes both.

A weekend's work, total.


{"slot": "finished-build",
"url": "https://projects.arduinocontent.cc/465c1ed7-1a4a-420b-8e41-72f6046f2645.jpg",
"source": "projecthub.arduino.cc",
"alt": "Finished MQ-135 air quality monitor on a breadboard with OLED display showing readings"}


{"slot": "wiring-diagram",
"url": "https://hacksterio.s3.amazonaws.com/uploads/attachments/1923130/screenshot_2026-01-20_221910_hiRePslXXv.png",
"source": "hackster.io",
"alt": "Wiring diagram showing Arduino connected to MQ-135 sensor"}


{"slot": "mq135-module-closeup",
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/MQ-2_gas_sensor.jpg/1024px-MQ-2_gas_sensor.jpg",
"source": "commons.wikimedia.org",
"alt": "MQ-series gas sensor module closeup showing metal cap, comparator IC, and pin header"}