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 NodeMCU ESP8266 — wiring and Rs/R0 code

Wire an MQ-135 to a NodeMCU ESP8266 with a 5 V heater and an AO divider into A0, then convert the reading to sensor resistance and an Rs/R0 trend.

Arduino C++Verified — core esp8266 3.1.2, 3 Nov 2026Compile-only

Community draft — report a mistake

The MQ-135 reacts to ammonia, CO2, smoke and alcohol vapour, so it is popular for “air quality monitor” projects. On a NodeMCU it needs a 5 V heater supply from VIN and a divider so its up-to-5 V output fits the A0 pin.

Instead of printing a raw number, the sketch converts A0 back to the sensor’s resistance Rs and compares it with a clean-air value R0 taken at start-up. A falling Rs/R0 means the air is getting worse — a far more honest output than a made-up ppm figure.

Wiring

NodeMCU ESP8266Only the pins used are shown · 3.3 V logicMQ-135 gas sensor module10k│20kA0GNDVINVCCGNDDO (not connected)AO
Also needed: 10 kΩ resistor, 20 kΩ resistor (or 2 × 10 kΩ in series).
Every connection
NodeMCU ESP8266MQ-135 gas sensor moduleWireNotes
VINVCCred5 V heater, about 150 mA. VIN is USB 5 V (often 4.7 V after the board’s diode).
GNDGNDblack
A0AOyellowVia 10 kΩ / 20 kΩ divider — 10 kΩ from AO to A0, 20 kΩ from A0 to GND keeps A0 under 3.3 V.

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

  • Two dividers are in series here: yours (10 kΩ / 20 kΩ) and the NodeMCU’s own 220 kΩ / 100 kΩ in front of the chip’s 0–1 V ADC. The board’s 320 kΩ sits in parallel with your 20 kΩ, so the sketch multiplies the A0 voltage by 1.53, not 1.5, to get back to AO.
  • Full scale on A0 is about 3.2 V on most NodeMCUs (1 V × 320 k / 100 k), not exactly 3.3 V. Feed A0 a known voltage, or measure it, and set ADC_FULL_SCALE if you want accurate volts.
  • The ESP8266 has only one analog input, so an MQ-135 and, say, a soil sensor can’t both use A0. Add an ADS1115 over I2C (D1/D2) for more channels.
  • The 2-minute warm-up in setup() uses delay() inside the wait loop. An empty while busy-wait that long would trip the ESP8266’s software watchdog and reset the board.
  • The “ppm” formulas found online assume a factory-calibrated sensor at 20 °C and 65 % RH. Treat Rs/R0 as a relative indoor-air indicator (ventilate when it drops), not a CO2 meter — use an SCD40 for real CO2.

Code

mq135-nodemcu.inoArduino C++Open in PlaygroundOpen in PlaygroundFlash
// MQ-135 air-quality sensor module on a NodeMCU (ESP8266).
// VCC → VIN (5 V heater), GND → GND, AO → 10 kΩ → A0 with 20 kΩ from A0 to GND.
// Works out the sensor resistance Rs from the voltage, then the ratio Rs/R0 to a clean-air
// baseline. A falling ratio means more gas (CO2, NH3, smoke, alcohol vapour…). Not a calibrated
// ppm meter — use it to spot trends.
const float ADC_FULL_SCALE = 3.2;  // volts at A0 that read 1023 (NodeMCU divider 220k/100k; measure yours)
const float DIVIDER_RATIO = 1.53;  // 10k over (20k || the board's 320k): AO volts = A0 volts × this
const float RL_KOHM = 1.0;         // load resistor on the module (marked 102 = 1 kΩ; 103 = 10 kΩ)
const float VCC = 5.0;             // heater/sensor supply — measure VIN, it is often about 4.7 V on USB
float r0 = 0;                      // clean-air resistance, measured at start-up

float readRs() {
  long sum = 0;
  for (int i = 0; i < 16; i++) {
    sum += analogRead(A0);
    delay(5);
  }
  float vA0 = (sum / 16.0) * ADC_FULL_SCALE / 1023.0;
  float vAO = vA0 * DIVIDER_RATIO;
  if (vAO < 0.05) vAO = 0.05;  // avoid divide-by-zero with AO disconnected
  return RL_KOHM * (VCC - vAO) / vAO;
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("MQ-135: warming up 2 min in clean air, then measuring R0");
  unsigned long t0 = millis();
  while (millis() - t0 < 120000) delay(100);  // delay() feeds the watchdog; an empty busy-wait would not
  float sum = 0;
  for (int i = 0; i < 10; i++) sum += readRs();
  r0 = (sum / 10) / 3.6;  // datasheet curve: Rs/R0 is about 3.6 in clean air
  Serial.printf("R0 = %.2f kOhm\n", r0);
}

void loop() {
  float rs = readRs();
  float ratio = rs / r0;
  Serial.printf("Rs %.2f kOhm  Rs/R0 %.2f  %s\n", rs, ratio, ratio < 2.5 ? "air worse than at start-up" : "similar to start-up");
  delay(2000);
}

Expected serial output

Serial Monitor · 115200Output
MQ-135: warming up 2 min in clean air, then measuring R0
R0 = 4.87 kOhm
Rs 17.21 kOhm  Rs/R0 3.53  similar to start-up
Rs 10.94 kOhm  Rs/R0 2.25  air worse than at start-up

Common errors

R0 comes out as a tiny or huge number

Check RL_KOHM against the resistor marked on your module (102 = 1 kΩ, 103 = 10 kΩ), and that the divider is wired as described.

Board resets during the warm-up

Usually a supply that can’t hold 150 mA of heater current. See rst cause 4 if the reset reason says watchdog.

↑↓ to move · Enter to open