Preview build — page text is a draft awaiting human review. Code badges come from real compiles. The flasher is real but not yet hardware-tested; the Playground is still a preview.

MQ-135 air quality sensor with ESP32 DevKit V1 — Rs ratio code

Wire an MQ-135 to an ESP32 DevKit V1 with a 5 V heater on VIN and AO divided into ADC1 pin D35, then turn the voltage into sensor resistance and a fresh-air ratio.

Arduino C++Verified — core esp32 3.3.12, 3 Nov 2026Compile-only

Community draft — report a mistake

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 V1Only the pins used are shown · 3.3 V logicMQ-135 gas sensor module10k│20kD35GNDVINVCCGNDDO (not connected)AO
Also needed: 10 kΩ and 20 kΩ resistors.
Every connection
ESP32 DevKit V1MQ-135 gas sensor moduleWireNotes
VINVCCred5 V for the heater (about 150 mA). VIN is 5 V only while the board runs from USB or a 5 V supply.
GNDGNDblack
D35AOyellowVia 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 set RL_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

mq135-esp32.inoArduino C++Open in PlaygroundOpen in PlaygroundFlash
// 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

Serial Monitor · 115200Output
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 — 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.

↑↓ to move · Enter to open