If you are building an air quality monitor, the MQ-135 is the most common arduino gas sensor you will encounter. The direct answer for wiring is simple: power the module with 5V, connect the analog out (AOUT) to pin A0, and read the inverse voltage drop across the onboard load resistor. However, the MQ-135 is a chemiresistor, not a precision digital instrument. It requires a proper burn-in period, a stable 5V rail capable of sourcing 150mA, and logarithmic math to convert raw ADC values into parts-per-million (ppm).

Target Board: This guide and code specifically target the Arduino Uno R3 (ATmega328P) operating at 5V. If you are using a 3.3V board (like an Arduino Nano 33 IoT or ESP32), skip to the extension section for voltage divider requirements.

Difficulty Rating: Intermediate (Hardware is easy; calibration math and power management require attention).
Time to Build: 20 minutes for wiring/code; 24 hours for sensor burn-in.

Parts List and Sensor Selection

Before soldering, verify exactly which module variant you have. Cheap clone manufacturers frequently swap the load resistor ($R_L$) value without updating the silkscreen, which completely breaks your ppm calculations.

Sensor Model Target Gases Voltage / Current Typical Price (2026)
MQ-135 NH3, NOx, CO2, VOCs, Smoke 5V / ~150mA (Heater) $2.00 - $4.00
MQ-2 LPG, Butane, Propane, Methane, H2 5V / ~800mA (Heater) $2.50 - $4.50
Bosch BME680 (Digital Alt) VOCs (IAQ index), Temp, Hum, Press 1.8V-3.6V / ~2mA $15.00 - $22.00

Note: The MQ-2 draws nearly 800mA for its heater. You cannot power it from the Arduino Uno's onboard 5V regulator if you are also powering it via USB. You must inject 5V directly to the module's VCC pin from an external buck converter.

Wiring the Arduino Gas Sensor (Pin Mapping)

The standard MQ-135 breakout board includes an LM393 comparator chip, giving you both an analog (AOUT) and digital (DOUT) output. For air quality monitoring, ignore the DOUT pin. The digital pin only triggers when gas levels cross a physical threshold set by the blue trimpot, which is useless for logging continuous ppm data.

Pin Mapping Table

MQ-135 Module Pin Arduino Uno R3 Pin Notes
VCC 5V Must be 5V. 3.3V will not heat the SnO2 layer.
GND GND Common ground required.
AOUT A0 Analog voltage output (0-5V).
DOUT Not Connected Leave disconnected for analog logging.

Step-by-Step Wiring

  1. De-energize the board: Unplug the USB cable from the Arduino Uno.
  2. Connect Power: Run a jumper from the Arduino 5V pin to the MQ-135 VCC pin, and GND to GND.
  3. Connect Signal: Run a jumper from MQ-135 AOUT to Arduino A0.
  4. Verify the Load Resistor: Flip the sensor module over. Locate the SMD resistor labeled RL or positioned near the sensor pins. Read the 3-digit code. 202 means 2kΩ. 103 means 10kΩ. Write this down; you will need it for the code constants.
  5. Power Up: Plug in the USB. The sensor will begin to get hot. This is normal.

Complete Arduino Code with Calibration & Error Handling

This code targets the Arduino Uno R3. It includes a moving average filter to smooth out ADC noise, a warm-up timer to prevent false readings during the heater ramp-up, and math domain error handling to prevent the serial monitor from locking up with nan values.

#include <Arduino.h>

// --- PIN DEFINITIONS ---
#define SENSOR_AOUT_PIN A0
#define LED_WARN_PIN    13

// --- CALIBRATION CONSTANTS ---
// Change RL_VALUE based on the SMD resistor on your specific module!
#define RL_VALUE       2.0    // Load resistor in kOhm (e.g., 2.0 for '202', 10.0 for '103')
#define R0_CLEAN_AIR   9.9    // Typical Rs/R0 ratio in clean air (from Hanwei datasheet)
#define VCC_VOLTAGE    5.0    // Arduino Uno operating voltage
#define ADC_MAX        1023.0 // 10-bit ADC resolution

// Curve constants for NH3 (Approximate, requires field calibration for precision)
#define CURVE_A        102.2  
#define CURVE_B        -2.47  

// --- TIMING & FILTERING ---
#define WARMUP_SECONDS 300    // 5 minutes minimum warmup
#define SAMPLE_SIZE    20     // Moving average window

unsigned long startTime;
int adcSamples[SAMPLE_SIZE];
int sampleIndex = 0;

void setup() {
  Serial.begin(115200);
  pinMode(LED_WARN_PIN, OUTPUT);
  pinMode(SENSOR_AOUT_PIN, INPUT);
  
  Serial.println(F("MQ-135 Air Quality Monitor Booting..."));
  Serial.println(F("Heater ramping up. Please wait 5 minutes."));
  startTime = millis();
  
  // Initialize sample array
  for(int i=0; i<SAMPLE_SIZE; i++) {
    adcSamples[i] = 0;
  }
}

void loop() {
  // 1. Read and filter ADC
  int rawADC = analogRead(SENSOR_AOUT_PIN);
  adcSamples[sampleIndex] = rawADC;
  sampleIndex = (sampleIndex + 1) % SAMPLE_SIZE;
  
  long sum = 0;
  for(int i=0; i<SAMPLE_SIZE; i++) {
    sum += adcSamples[i];
  }
  float avgADC = sum / SAMPLE_SIZE;
  
  // 2. Error Handling: Check for ADC saturation or shorts
  if (avgADC >= ADC_MAX) {
    Serial.println(F("ERROR: ADC stuck at 1023. Check VCC and wiring."));
    digitalWrite(LED_WARN_PIN, HIGH);
    delay(2000);
    return;
  }
  if (avgADC <= 0) {
    Serial.println(F("ERROR: ADC reads 0. Check for short to GND."));
    digitalWrite(LED_WARN_PIN, HIGH);
    delay(2000);
    return;
  }
  
  // 3. Calculate Sensor Resistance (Rs)
  // Voltage divider math: Vout = (RL / (Rs + RL)) * VCC
  float voltage = (avgADC / ADC_MAX) * VCC_VOLTAGE;
  float Rs = ((VCC_VOLTAGE * RL_VALUE) / voltage) - RL_VALUE;
  
  // 4. Calculate R0 and PPM
  float R0 = Rs / R0_CLEAN_AIR;
  float ratio = Rs / R0;
  
  // Prevent math domain error in pow() if ratio is somehow negative or zero
  if (ratio <= 0) {
    Serial.println(F("ERROR: Math domain fault. Ratio <= 0."));
    delay(2000);
    return;
  }
  
  float ppm = CURVE_A * pow(ratio, CURVE_B);
  
  // 5. Output and Warmup Check
  unsigned long elapsed = (millis() - startTime) / 1000;
  if (elapsed < WARMUP_SECONDS) {
    Serial.print(F("Warming up... "));
    Serial.print(WARMUP_SECONDS - elapsed);
    Serial.println(F("s remaining."));
  } else {
    Serial.print(F("ADC: ")); Serial.print(avgADC, 1);
    Serial.print(F(" | Rs: ")); Serial.print(Rs, 2);
    Serial.print(F(" kOhm | Est. PPM: ")); Serial.println(ppm, 2);
    
    // Trigger LED if PPM exceeds arbitrary threshold (e.g. 50 ppm)
    if (ppm > 50.0) {
      digitalWrite(LED_WARN_PIN, HIGH);
    } else {
      digitalWrite(LED_WARN_PIN, LOW);
    }
  }
  
  delay(500); // Sample twice per second
}

Debugging: Why Your Sensor Reads 0, 1023, or Drifts

The MQ-135 is notorious for confusing beginners when the serial monitor outputs garbage data. Here are the exact error strings you will see, ranked by their most likely causes.

Symptom: Serial output prints "ADC stuck at 1023" or raw reads are constantly 1023

  1. Cause 1: You wired DOUT instead of AOUT. The digital pin floats high until the trimpot threshold is crossed. Move your jumper to AOUT.
  2. Cause 2: Insufficient Heater Voltage. The SnO2 sensing layer requires high heat to react with gases. If your USB port is sagging to 4.2V, the sensor resistance stays infinitely high, pulling the analog pin to VCC (1023). Measure the VCC pin on the module with a multimeter; it must read >4.8V.
  3. Cause 3: Clean Air Saturation. If you are in an extremely clean environment and the load resistor is too high, the voltage divider may max out the ADC. This is rare but possible with 10kΩ load resistors.

Symptom: Serial output prints "ADC reads 0" or raw reads are constantly 0

  1. Cause 1: Short Circuit. AOUT is shorted to GND, or your jumper wire is broken internally and grounding the pin.
  2. Cause 2: Extreme Gas Saturation. You are testing the sensor by holding a butane lighter directly against the mesh. The sensor resistance drops to near zero, pulling AOUT to GND. Move the gas source away; the sensor needs time to off-gas.

Symptom: Serial output prints "Est. PPM: nan" or wild fluctuations (e.g., jumping from 10 to 4000)

  1. Cause 1: Incorrect $R_L$ Constant. You left RL_VALUE at 2.0 in the code, but your clone board uses a 10kΩ resistor. Flip the board and read the SMD code.
  2. Cause 2: Skipping Burn-in. The sensor requires a minimum of 3 minutes just to stabilize the heater, and up to 24 hours for the chemical layer to settle into a baseline $R0$. If you read it 10 seconds after plugging it in, the math will break down.
The First 3 Things to Check When It Fails:
1. Measure VCC at the module header with a DMM (must be >4.8V).
2. Read the SMD code on the load resistor and update the RL_VALUE in the code.
3. Let the sensor run continuously for 24 hours before attempting to establish your clean-air $R0$ baseline.

Extending and Simplifying Your Build

The Arduino Uno and MQ-135 are great for learning, but they fall short for permanent home automation deployments. Here is how to adapt the project based on your end goal.

How to Extend: Add WiFi Logging (ESP32)

If you want to push ppm data to an MQTT broker or Home Assistant, swap the Uno for an ESP32 DevKit V1. Warning: The ESP32 operates at 3.3V, and its ADC pins will be damaged by the 5V output of the MQ-135. You must build a voltage divider using a 10kΩ and 22kΩ resistor to step the AOUT signal down to a safe ~3.2V maximum before feeding it to GPIO 34. Furthermore, you must power the MQ-135 VCC from the ESP32's VIN pin (assuming 5V USB input), not the 3V3 pin, or the heater will brownout the ESP32's onboard regulator.

How to Simplify: Switch to Digital I2C (BME680)

If you are tired of dealing with heater currents, analog noise, and logarithmic curve approximations, abandon the MQ-series entirely. The Bosch BME680 is a digital environmental sensor that communicates via I2C. It runs natively on 3.3V, draws negligible current, and uses Bosch's proprietary BSEC library to output a highly accurate Indoor Air Quality (IAQ) index from 0-500. It costs about $18, but it eliminates 90% of the debugging headaches associated with chemiresistors.

Arduino Gas Sensor FAQ

How do I convert the Arduino gas sensor analog reading to ppm?

You cannot map it linearly. The MQ-135 responds on a logarithmic curve. You must first calculate the sensor's resistance ($Rs$) using the voltage divider formula based on your known load resistor ($R_L$). Then, you divide $Rs$ by the baseline clean-air resistance ($R0$) to get a ratio. Finally, you apply the formula ppm = a * pow(ratio, b), where a and b are constants derived from the sensor's datasheet log-log graph for the specific gas you are targeting (e.g., NH3 vs CO2).

Can I power the MQ-135 directly from the Arduino Uno 5V pin?

Yes, but with a caveat. The MQ-135 heater draws roughly 150mA. The Arduino Uno's onboard 5V regulator (when powered via the barrel jack) can overheat if you are also powering relays or displays. If you are powering the Uno via USB, the USB port's polyfuse usually limits current to 500mA, which is sufficient for the Uno and one MQ-135. If you add an MQ-2 (which draws 800mA), you must use an external 5V power supply wired directly to the sensor module.

Why does my Arduino gas sensor get hot to the touch?

This is intentional and required for operation. The MQ-135 contains a tin dioxide (SnO2) sensing layer. When clean, this layer adsorbs oxygen, creating a potential barrier (high resistance). When target gases are present, they react with the oxygen, releasing electrons and lowering the resistance. This chemical reaction requires the internal micro-heater to maintain the SnO2 layer at roughly 20°C to 30°C above ambient. If the sensor is cold, it is broken or underpowered.

Is the MQ-135 accurate for measuring CO2 in a classroom?

No. While the datasheet lists CO2, the MQ-135 is highly cross-sensitive to humidity, temperature, and VOCs (like hand sanitizer or cleaning supplies). In a crowded classroom, humidity and body odor will skew the readings wildly. For accurate classroom CO2 monitoring to determine ventilation rates, use a dedicated NDIR (Non-Dispersive Infrared) sensor like the Sensirion SCD30 or SCD40, which costs around $30-$40 but provides true, cross-interference-free CO2 measurements.

Disclaimer: The MQ-135 is a hobbyist-grade indicator, not a calibrated life-safety device. Never use it as a primary alarm for carbon monoxide or explosive gas leaks. Always use UL/CE-listed commercial detectors for life-safety applications. For more on analog pin behavior and ADC sampling, refer to the official Arduino Analog documentation. For EPA thresholds on criteria air pollutants, consult the NAAQS tables.