The MQ-135 is sold as an “air quality” or “CO2” sensor. It is really a heated sensor whose resistance falls when many different gases are present — good for spotting stuffy or smoky air, not for measuring one gas.
On the ESP32 DevKit V1 the heater goes on VIN (5 V) and the analog output on D35 through a 10 kΩ / 20 kΩ divider, because AO can rise towards 5 V. The sketch turns the voltage back into the sensor’s resistance and compares it with a fresh-air reference, which is the honest way to use this part.
Wiring
| ESP32 DevKit V1 | MQ-135 gas sensor module | Wire | Notes |
|---|---|---|---|
| VIN | VCC | red | 5 V for the heater (about 150 mA). VIN is 5 V only while the board runs from USB or a 5 V supply. |
| GND | GND | black | |
| D35 | AO | yellow | Via 10 kΩ / 20 kΩ divider — 10 kΩ from AO to D35, 20 kΩ from D35 to GND. A 5 V AO arrives as 3.33 V; the sketch multiplies by 1.5. |
5 V (heater: about 150 mA) part, 3.3 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 sketch converts the voltage into the sensor’s resistance,
Rs = RL × (5 V − AO) / AO, because that is what the MQ-135 datasheet curves are drawn in. Read the load resistor on your module (marked 102 = 1 kΩ or 103 = 10 kΩ) and setRL_KOHM. - It compares Rs with a reference taken in fresh air at start-up, so power it up by an open window or outdoors. A ratio near 1 means “as clean as the reference”; it falls as pollutants rise.
- Online “MQ-135 CO2 ppm” formulas assume a calibrated sensor at 20 °C / 65 % RH and only CO2 in the air. The sensor also reacts to alcohol, ammonia, smoke and benzene, and drifts with humidity — the monsoon shifts it noticeably. For real CO2 numbers use an SCD40.
- D35 is ADC1 and input-only — fine for analog, and it keeps working with Wi-Fi on.
analogReadMilliVolts()(core 3.x) gives calibrated millivolts; the usable range with default attenuation ends near 3.1 V, which the divider respects. - Burn a new sensor in for 24–48 hours of continuous power before trusting the reference. Feed DO through its own divider if you ever use it — it swings to 5 V.
Code
// MQ-135 air-quality module on an ESP32 DevKit V1.
// VCC → VIN (5 V heater), GND → GND, AO → 10 kΩ → D35 (GPIO35), 20 kΩ from D35 to GND.
// Computes the sensor resistance Rs and compares it with a fresh-air reference.
// Relative indication only — the MQ-135 can't tell CO2 from alcohol, smoke or ammonia.
constexpr uint8_t MQ_PIN = 35; // ADC1_CH7: keeps working with Wi-Fi on, input-only
constexpr float DIVIDER = 1.5f; // (10 kΩ + 20 kΩ) / 20 kΩ
constexpr float VC = 5.0f; // module supply on VIN — measure yours and adjust
constexpr float RL_KOHM = 1.0f; // load resistor on the module ("102" = 1 kΩ, "103" = 10 kΩ)
float rsFresh = 0; // Rs in fresh air, measured at start-up
float readRsKohm() {
uint32_t sum = 0;
for (int i = 0; i < 32; i++) sum += analogReadMilliVolts(MQ_PIN);
float vout = (sum / 32.0f) / 1000.0f * DIVIDER; // volts at AO
if (vout < 0.05f) vout = 0.05f; // avoid divide-by-zero with no heater
return RL_KOHM * (VC - vout) / vout;
}
const char *band(float ratio) {
if (ratio >= 0.8f) return "good";
if (ratio >= 0.5f) return "moderate";
return "poor — ventilate";
}
void setup() {
Serial.begin(115200);
Serial.println("MQ-135 on GPIO35 — warming up 2 min in fresh air");
delay(120000);
float sum = 0;
for (int i = 0; i < 10; i++) {
sum += readRsKohm();
delay(1000);
}
rsFresh = sum / 10;
Serial.printf("Fresh-air Rs = %.2f kΩ (reference)\n", rsFresh);
}
void loop() {
float rs = readRsKohm();
float ratio = rs / rsFresh; // falls as pollutants rise
Serial.printf("Rs %.2f kΩ Rs/Rfresh %.2f Air: %s\n", rs, ratio, band(ratio));
delay(2000);
}// MQ-135 air-quality module on an ESP32 DevKit V1.
// VCC → VIN (5 V heater), GND → GND, AO → 10 kΩ → D35 (GPIO35), 20 kΩ from D35 to GND.
// Computes the sensor resistance Rs and compares it with a fresh-air reference.
// Relative indication only — the MQ-135 can't tell CO2 from alcohol, smoke or ammonia.
constexpr uint8_t MQ_PIN = 35; // ADC1_CH7: keeps working with Wi-Fi on, input-only
constexpr float DIVIDER = 1.5f; // (10 kΩ + 20 kΩ) / 20 kΩ
constexpr float VC = 5.0f; // module supply on VIN — measure yours and adjust
constexpr float RL_KOHM = 1.0f; // load resistor on the module ("102" = 1 kΩ, "103" = 10 kΩ)
float rsFresh = 0; // Rs in fresh air, measured at start-up
float readRsKohm() {
uint32_t sum = 0;
for (int i = 0; i < 32; i++) sum += analogReadMilliVolts(MQ_PIN);
float vout = (sum / 32.0f) / 1000.0f * DIVIDER; // volts at AO
if (vout < 0.05f) vout = 0.05f; // avoid divide-by-zero with no heater
return RL_KOHM * (VC - vout) / vout;
}
const char *band(float ratio) {
if (ratio >= 0.8f) return "good";
if (ratio >= 0.5f) return "moderate";
return "poor — ventilate";
}
void setup() {
Serial.begin(115200);
Serial.println("MQ-135 on GPIO35 — warming up 2 min in fresh air");
delay(120000);
float sum = 0;
for (int i = 0; i < 10; i++) {
sum += readRsKohm();
delay(1000);
}
rsFresh = sum / 10;
Serial.printf("Fresh-air Rs = %.2f kΩ (reference)\n", rsFresh);
}
void loop() {
float rs = readRsKohm();
float ratio = rs / rsFresh; // falls as pollutants rise
Serial.printf("Rs %.2f kΩ Rs/Rfresh %.2f Air: %s\n", rs, ratio, band(ratio));
delay(2000);
}Expected serial output
MQ-135 on GPIO35 — warming up 2 min in fresh air
Fresh-air Rs = 18.40 kΩ (reference)
Rs 18.22 kΩ Rs/Rfresh 0.99 Air: good
Rs 11.85 kΩ Rs/Rfresh 0.64 Air: moderate
Rs 6.90 kΩ Rs/Rfresh 0.38 Air: poor — ventilateMQ-135 on GPIO35 — warming up 2 min in fresh air Fresh-air Rs = 18.40 kΩ (reference) Rs 18.22 kΩ Rs/Rfresh 0.99 Air: good Rs 11.85 kΩ Rs/Rfresh 0.64 Air: moderate Rs 6.90 kΩ Rs/Rfresh 0.38 Air: poor — ventilate
Common errors
Rs reads around 99 kΩ and never changes
AO is near 0 V: the heater isn’t powered, or the divider is reversed. 10 kΩ goes from AO to D35 and 20 kΩ from D35 to GND.
Ratio drifts down slowly all day
Room air getting stale, humidity rising, or a sensor still burning in. Take a new reference in fresh air (press EN) after the first day of use.
Numbers jump when the ESP32 starts Wi-Fi
Supply dips on VIN shift the heater temperature. Use a solid 5 V supply (1 A or more) and a 100 µF capacitor across the module’s VCC and GND.