Arduino air quality monitor
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.
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.
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:
- 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.
- 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".
- 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
Six wires total, plus power.
MQ-135 → Arduino Nano
- VCC → 5V
- GND → GND
- AO → A0
- DO → leave unconnected
SSD1306 OLED → Arduino Nano
- VCC → 5V (or 3V3 — most modules accept both)
- GND → GND
- SDA → A4
- SCL → A5
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.
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:
- 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.
- Run a one-line sketch that prints
gasSensor.getRZero()once a minute. - After it stabilises, average the last 30 minutes of readings. That's your
RZERO. - Either edit
MQ135.hto setRZEROto 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(notDO) is wired to A0 — the digital output pin won't give you analog readings. - OLED stays blank. Try changing
OLED_ADDRfrom0x3Cto0x3D. 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
- 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.
- 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).
- Check I²C communication: Upload the "Wire → Scan" example from the Arduino IDE examples, open Serial Monitor at 9600 baud. You should see
0x3Cor0x3D. If nothing appears, SDA/SCL are swapped or disconnected. - 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 ofsetup()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"}