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 ESP8266 | MQ-135 gas sensor module | Wire | Notes |
|---|---|---|---|
| VIN | VCC | red | 5 V heater, about 150 mA. VIN is USB 5 V (often 4.7 V after the board’s diode). |
| GND | GND | black | |
| A0 | AO | yellow | Via 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_SCALEif 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()usesdelay()inside the wait loop. An emptywhilebusy-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
// 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);
}// 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
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-upMQ-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.