Project Overview and Target Board Variant

Building a reliable gas sensor Arduino project requires more than just copying a basic analog read sketch. The MQ-135 sensor detects ammonia, nitrogen oxide, alcohol, benzene, smoke, and CO2 by measuring the conductivity change in a tin dioxide (SnO2) sensing layer. However, its high-impedance output and strict voltage requirements trip up many makers.

Target Board Variant: This guide and the provided code specifically target the Arduino Uno R3 (DIP ATmega328P). Why not an ESP32? The MQ-135 requires a 5V heater supply and outputs an analog voltage up to 5V. The ESP32's ADC is notoriously non-linear above 2.5V and strictly limited to 3.3V, which would require a hardware voltage divider and software linearization. The Uno R3's 10-bit ADC and native 5V logic provide a stable, linear baseline for gas concentration calculations without extra components.

Difficulty: Intermediate (Requires basic soldering and serial debugging)
Time to Complete: 45 minutes for wiring/code, plus 24-48 hours for sensor burn-in.

Hardware Spec Sheet and Parts List

Do not buy bare MQ-135 sensors without the comparator board unless you plan to design your own PCB with the required load resistors and filter capacitors. The standard 4-pin module includes the necessary signal conditioning.

ComponentExact Model / VariantApprox. Cost (2026)Notes
MicrocontrollerArduino Uno R3 Rev3 (ATmega328P)$25.00Must be 5V logic variant.
Gas Sensor ModuleMQ-135 4-Pin Module (with LM393)$3.50Ensure it has the 1KΩ load resistor (R_L) populated.
Jumper Wires22 AWG Dupont Male-to-Female$4.00Keep analog runs under 6 inches to avoid noise.
Power Supply5V 2A USB Barrel Jack Adapter$6.00The MQ-135 heater draws ~150mA; standard PC USB ports may brownout.

Pin Mapping and Wiring Steps

The MQ-135 module has four pins: VCC, GND, AOUT (Analog Out), and DOUT (Digital Out). We only use AOUT for proportional gas concentration readings. DOUT is a simple threshold trigger controlled by the onboard potentiometer and is useless for measuring actual ppm (parts per million).

MQ-135 Module PinArduino Uno R3 PinWire Color (Standard)
VCC5VRed
GNDGNDBlack
AOUTA0Yellow
DOUTNot Connected-
Bench Tip: Never wire the MQ-135 VCC to the 3.3V pin. The internal heating element requires 5V ±0.1V to reach the optimal 20°C operating temperature. Undervolting the heater will result in permanently sluggish response times and inaccurate baseline resistance (R0) calculations.
  1. De-energize the board: Ensure the Arduino is unplugged from the PC and wall power.
  2. Connect Power: Route the red jumper from the module's VCC to the Arduino's 5V pin, and black from GND to GND.
  3. Connect Signal: Route the yellow jumper from AOUT to A0. Keep this wire away from the onboard voltage regulator to avoid thermal noise coupling.
  4. Inspect the Load Resistor: Flip the sensor module over. Verify the surface mount resistor labeled '102' (1KΩ) is present. This is critical for the math in our code.
  5. Power Up: Plug the Arduino into a dedicated 5V 2A wall adapter, not your laptop USB port, to handle the initial heater inrush current.

Complete Arduino Code with Error Handling

This sketch calculates the sensor's resistance (Rs), establishes a clean-air baseline (R0), and estimates a relative Air Quality Index (AQI). It includes a moving average filter to smooth out ADC noise and explicit error handling for common wiring faults.

/*
 * MQ-135 Gas Sensor Arduino Project
 * Target: Arduino Uno R3 (ATmega328P)
 * Author: ElectricalFlux
 */

// --- PIN DEFINITIONS ---
#define SENSOR_ANALOG_PIN A0

// --- HARDWARE CONSTANTS ---
#define ADC_RESOLUTION 1023.0
#define VCC_VOLTAGE 5.0
#define RL_VALUE 1.0  // Load resistor on module in Kilo-ohms (1K = 1.0)
#define RO_CLEAN_AIR_FACTOR 9.8  // Rs/R0 ratio in clean air per Hanwei datasheet

// --- FILTER CONSTANTS ---
#define SAMPLE_SIZE 10

float R0 = 0.0;
int readBuffer[SAMPLE_SIZE];
int bufferIndex = 0;

void setup() {
  Serial.begin(115200);
  pinMode(SENSOR_ANALOG_PIN, INPUT);
  
  Serial.println(F("MQ-135 Initializing..."));
  Serial.println(F("Warning: Sensor requires 24-48hr burn-in for accurate R0."));
  
  // Pre-heat check
  delay(2000); // Allow ADC to settle
  calibrateR0();
}

void loop() {
  int rawADC = readSensorAverage();
  
  // --- ERROR HANDLING ---
  if (rawADC >= 1023) {
    Serial.println(F("Error: Analog read saturated at 1023"));
    delay(2000);
    return;
  }
  if (rawADC <= 0) {
    Serial.println(F("Error: Analog read is 0. Check GND wiring."));
    delay(2000);
    return;
  }

  // Calculate Sensor Resistance (Rs)
  float voltage = (rawADC / ADC_RESOLUTION) * VCC_VOLTAGE;
  float Rs = ((VCC_VOLTAGE * RL_VALUE) / voltage) - RL_VALUE;
  
  // Calculate ppm ratio (Rs/R0)
  float ratio = Rs / R0;
  
  Serial.print(F("Raw ADC: "));
  Serial.print(rawADC);
  Serial.print(F(" | Rs: "));
  Serial.print(Rs, 2);
  Serial.print(F("K | Ratio: "));
  Serial.println(ratio, 2);
  
  delay(1000);
}

// --- FUNCTIONS ---
void calibrateR0() {
  float sumRs = 0;
  for (int i = 0; i < 50; i++) {
    int raw = analogRead(SENSOR_ANALOG_PIN);
    if (raw <= 0 || raw >= 1023) {
      Serial.println(F("Fatal: R0 calibration failed, value <= 0 or saturated"));
      R0 = 1.0; // Fallback to prevent divide-by-zero
      return;
    }
    float v = (raw / ADC_RESOLUTION) * VCC_VOLTAGE;
    sumRs += ((VCC_VOLTAGE * RL_VALUE) / v) - RL_VALUE;
    delay(20);
  }
  R0 = (sumRs / 50.0) / RO_CLEAN_AIR_FACTOR;
  Serial.print(F("Calibration Complete. R0 = "));
  Serial.print(R0, 2);
  Serial.println(F("K"));
}

int readSensorAverage() {
  readBuffer[bufferIndex] = analogRead(SENSOR_ANALOG_PIN);
  bufferIndex = (bufferIndex + 1) % SAMPLE_SIZE;
  
  long sum = 0;
  for (int i = 0; i < SAMPLE_SIZE; i++) {
    sum += readBuffer[i];
  }
  return sum / SAMPLE_SIZE;
}

Debugging: First Three Things to Check

When your serial monitor outputs garbage data or flatlines, do not immediately rewrite the code. Gas sensors are physical devices governed by thermodynamics and chemistry. Here are the first three things to verify on the bench.

  1. Verify the Burn-In Period: Out of the box, the MQ-135's SnO2 layer has manufacturing residues. It must be powered continuously for 24 to 48 hours in clean air before the R0 calibration will hold. If your readings drift wildly every time you reboot, the sensor is not burned in.
  2. Check for Voltage Drop on VCC: Put your multimeter in DC Voltage mode. Probe the VCC and GND pins directly on the sensor module while it is running. If you read less than 4.8V, your USB cable or breadboard power rails are sagging under the 150mA heater load. This drops the sensor temperature and artificially inflates the Rs value.
  3. Confirm the Load Resistor (RL) Value: The code assumes a 1KΩ load resistor (marked '102'). Some cheap clones ship with a 10KΩ resistor (marked '103'). If your module has a 10KΩ resistor, you must change #define RL_VALUE 1.0 to #define RL_VALUE 10.0 in the sketch, or your ppm math will be off by a factor of 10.

Common Serial Error Strings and Ranked Causes

If the serial monitor throws one of these exact strings, follow the ranked causes to fix it.

Exact Error: Error: Analog read saturated at 1023

  • Cause 1 (Most Likely): The AOUT pin is physically shorted to the 5V VCC pin, or the sensor is exposed to an extremely high concentration of VOCs (like holding a permanent marker directly to the mesh).
  • Cause 2: The load resistor (RL) is missing or desoldered from the module, pulling the analog line high.
  • Cause 3: You are using a 3.3V board (like an ESP32) and the 5V sensor output is saturating the lower-voltage ADC.

Exact Error: Fatal: R0 calibration failed, value <= 0 or saturated

  • Cause 1: The A0 pin is floating (disconnected wire). The Arduino's internal pull-ups or stray capacitance are causing random 0 or 1023 reads.
  • Cause 2: The sensor heater element is burnt out (open circuit). The sensor remains cold, resistance stays infinitely high, and the voltage divider outputs 0V.

Extending and Simplifying the Build

Once you have the baseline gas sensor Arduino circuit running reliably, you can adapt it to your specific project constraints.

How to Simplify: If you only need a binary 'alarm' state (e.g., trigger a buzzer when smoke is detected) and don't care about actual ppm calculations, ditch the AOUT pin entirely. Wire the module's DOUT pin to Arduino Digital Pin 2. Use a simple digitalRead() and adjust the blue potentiometer on the module with a small flathead screwdriver until it triggers at your desired threshold. This eliminates the need for R0 calibration and floating-point math.

How to Extend: To log data for EPA indoor air quality compliance, add an RTC (Real Time Clock) module like the DS3231 via I2C and an SD card breakout board. You can also implement a temperature and humidity compensation algorithm. The MQ-135 is highly sensitive to ambient humidity; integrating an AHT20 sensor and applying a software correction matrix to the Rs value will drastically improve long-term accuracy in unclimate-controlled environments.

Frequently Asked Questions

Why does my gas sensor Arduino project read high CO2 in fresh air?

The MQ-135 is often mislabeled as a dedicated CO2 sensor. It is actually a broad-spectrum VOC (Volatile Organic Compound) sensor. In fresh outdoor air, it will still register a baseline resistance due to natural atmospheric gases and humidity. Furthermore, the Arduino analogRead() function maps to voltage, not direct ppm. To get true CO2 ppm, you must use an NDIR (Non-Dispersive Infrared) sensor like the MH-Z19, which costs roughly five times more but provides actual calibrated CO2 data.

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

Yes, but with a caveat. The Arduino Uno's onboard linear voltage regulator (NCP1117) can typically supply up to 800mA safely when fed via the barrel jack, but the USB VBUS line is limited by your PC's USB port (usually 500mA). Because the MQ-135 heater draws roughly 150mA continuously, it eats up a significant chunk of your current budget. If you add an LCD screen, a WiFi module, and relays, you will likely brownout the ATmega328P. Always use an external 5V buck converter for projects with multiple peripherals.

How long does the MQ-135 burn-in period actually take?

The Hanwei datasheet specifies a 24-hour continuous power-on period for initial stabilization. However, bench testing shows that for the SnO2 layer to fully stabilize and for the R0 value to stop drifting by more than 5% per day, a 48 to 72-hour burn-in in a clean, well-ventilated room yields the most reliable baseline. Do not attempt to calibrate R0 in a garage or a room with strong air fresheners, as these will permanently skew your clean-air baseline.