The MQ-135 is the cheap (about ₹150) “air quality” gas sensor in many school and college projects. A heated tin-oxide element changes its resistance when it meets pollutant gases, so the module can show whether a room is getting stuffy or smoky.
On the Uno it runs from 5V — its heater needs 5 V and about 150 mA. The analog output goes to A0 and the threshold output to D2. The Uno’s 5 V ADC range matches the module’s output exactly, so no divider is needed, unlike on ESP32.
Instead of printing raw numbers, the sketch works out the sensor’s resistance Rs and compares it with R0, its value in clean air. The ratio Rs/R0 falls as the air gets worse. It also prints a rough CO₂-equivalent from the datasheet curve. Calibrate R0 once outdoors and the trend becomes meaningful.
Wiring
| Arduino Uno R3 | MQ-135 gas sensor module | Wire | Notes |
|---|---|---|---|
| 5V | VCC | red | The heater draws about 150 mA. Fine from USB with little else connected; with a relay or servo too, use an external 5 V supply (common GND). |
| GND | GND | black | |
| A0 | AO | yellow | |
| D2 | DO | green | Optional threshold alarm (LOW past the pot setting on most boards). |
5 V (heater: about 150 mA) part, 5 V board. The heater needs 5 V. AO can reach about 5 V, so on ESP32 and Pico feed AO through a 10 kΩ / 20 kΩ divider (or similar) to stay under 3.3 V. DO is driven by the comparator from VCC — divide it too on 3.3 V boards.
Board quirks
- The MQ-135 reacts to many gases at once — CO₂, ammonia, alcohol, smoke, benzene — and can’t tell them apart. The “ppm” here is a rough CO₂-equivalent from the datasheet curve. Use it to see the air getting worse or better, not as a calibrated meter.
- A new sensor needs a 24-hour burn-in powered on before the readings settle, and about 2 minutes of heating at every start. The sketch marks those readings as warming.
- Check the load resistor on your board:
102means 1 kΩ,103means 10 kΩ. Put it inRL— a wrong RL scales every result. - Calibrate R0 outdoors or by an open window, not in a closed room in the evening, when CO₂ is already high. Humidity and temperature also shift the reading; monsoon days read differently from dry winter days.
- The sensor body gets hot (around 50 °C). Keep it away from plastic and from the DHT or BME you might use for temperature.
Code
// MQ-135 air-quality sensor module on an Arduino Uno R3.
// VCC → 5V, GND → GND, AO → A0, DO → D2.
// Reads the sensor resistance Rs and compares it with R0, your clean-air value.
// Calibrate: run it outdoors (or by an open window) for 30 min, copy the printed Rs into R0.
const uint8_t AO_PIN = A0, DO_PIN = 2;
const float RL = 1000.0; // load resistor on the module: "102" = 1 kΩ (some boards: "103" = 10 kΩ)
float R0 = 3600.0; // clean-air Rs - replace with your own reading
const unsigned long WARMUP_MS = 120000UL;
void setup() {
pinMode(DO_PIN, INPUT);
Serial.begin(9600);
Serial.println(F("MQ-135 heating up - readings settle after 2 minutes"));
}
void loop() {
long sum = 0;
for (uint8_t i = 0; i < 32; i++) sum += analogRead(AO_PIN);
float adc = sum / 32.0;
if (adc < 1) adc = 1;
float rs = RL * (1023.0 - adc) / adc; // from Vout = 5 V x RL / (Rs + RL)
float ratio = rs / R0;
// Rough CO2-equivalent from the datasheet curve. Treat it as a trend, not a measurement.
float ppm = 116.602 * pow(ratio, -2.769);
Serial.print(millis() < WARMUP_MS ? F("[warming] ") : F(""));
Serial.print(F("AO "));
Serial.print(adc, 0);
Serial.print(F(" Rs "));
Serial.print(rs, 0);
Serial.print(F(" ohm Rs/R0 "));
Serial.print(ratio, 2);
Serial.print(F(" ~"));
Serial.print(ppm, 0);
Serial.print(F(" ppm "));
if (ratio > 0.8) Serial.print(F("Air OK"));
else if (ratio > 0.5) Serial.print(F("Stuffy - ventilate"));
else Serial.print(F("Poor - smoke/fumes?"));
Serial.println(digitalRead(DO_PIN) == LOW ? F(" [DO alarm]") : F(""));
delay(2000);
}// MQ-135 air-quality sensor module on an Arduino Uno R3.
// VCC → 5V, GND → GND, AO → A0, DO → D2.
// Reads the sensor resistance Rs and compares it with R0, your clean-air value.
// Calibrate: run it outdoors (or by an open window) for 30 min, copy the printed Rs into R0.
const uint8_t AO_PIN = A0, DO_PIN = 2;
const float RL = 1000.0; // load resistor on the module: "102" = 1 kΩ (some boards: "103" = 10 kΩ)
float R0 = 3600.0; // clean-air Rs - replace with your own reading
const unsigned long WARMUP_MS = 120000UL;
void setup() {
pinMode(DO_PIN, INPUT);
Serial.begin(9600);
Serial.println(F("MQ-135 heating up - readings settle after 2 minutes"));
}
void loop() {
long sum = 0;
for (uint8_t i = 0; i < 32; i++) sum += analogRead(AO_PIN);
float adc = sum / 32.0;
if (adc < 1) adc = 1;
float rs = RL * (1023.0 - adc) / adc; // from Vout = 5 V x RL / (Rs + RL)
float ratio = rs / R0;
// Rough CO2-equivalent from the datasheet curve. Treat it as a trend, not a measurement.
float ppm = 116.602 * pow(ratio, -2.769);
Serial.print(millis() < WARMUP_MS ? F("[warming] ") : F(""));
Serial.print(F("AO "));
Serial.print(adc, 0);
Serial.print(F(" Rs "));
Serial.print(rs, 0);
Serial.print(F(" ohm Rs/R0 "));
Serial.print(ratio, 2);
Serial.print(F(" ~"));
Serial.print(ppm, 0);
Serial.print(F(" ppm "));
if (ratio > 0.8) Serial.print(F("Air OK"));
else if (ratio > 0.5) Serial.print(F("Stuffy - ventilate"));
else Serial.print(F("Poor - smoke/fumes?"));
Serial.println(digitalRead(DO_PIN) == LOW ? F(" [DO alarm]") : F(""));
delay(2000);
}Expected serial output
MQ-135 heating up - readings settle after 2 minutes
[warming] AO 412 Rs 1483 ohm Rs/R0 0.41 ~1360 ppm Poor - smoke/fumes?
AO 225 Rs 3547 ohm Rs/R0 0.99 ~119 ppm Air OK
AO 290 Rs 2528 ohm Rs/R0 0.70 ~313 ppm Stuffy - ventilateMQ-135 heating up - readings settle after 2 minutes [warming] AO 412 Rs 1483 ohm Rs/R0 0.41 ~1360 ppm Poor - smoke/fumes? AO 225 Rs 3547 ohm Rs/R0 0.99 ~119 ppm Air OK AO 290 Rs 2528 ohm Rs/R0 0.70 ~313 ppm Stuffy - ventilate
Common errors
Readings keep falling for the first hour
Normal for a new or long-unused sensor. Leave it powered for a day before calibrating R0.
AO stuck near 1023 or 0
Heater not powered (sensor stays cold) or AO not on A0. The sensor should feel warm after a minute.
Huge ppm numbers
R0 is still the default. Calibrate it in clean air with your own sensor and board.