Project Overview & Difficulty Rating
Building a reliable detector gas Arduino system requires more than just plugging in a sensor and reading an analog pin. The MQ-series chemiresistors are notorious for high current draw, thermal drift, and non-linear output curves. This guide walks through a dual-sensor build targeting the Arduino Uno R3 (ATmega328P), combining an MQ-135 (Air Quality/NH3/CO2) and an MQ-7 (Carbon Monoxide) to monitor indoor environmental safety.
Estimated Build Time: 2 hours
Target Board: Arduino Uno R3 (Rev3) or compatible ATmega328P clone
Core Challenge: Managing the 800mA+ combined preheat current draw without triggering USB brownouts.
Chemiresistors work by passing current through a tin dioxide (SnO2) sensing layer. When target gas molecules adsorb onto the heated layer, the material's resistance drops. We measure this voltage drop across a fixed load resistor to calculate parts per million (ppm). According to OSHA chemical exposure limits, CO becomes immediately dangerous at 1200 ppm, making rapid, accurate detection critical.
Hardware BOM & Pin Mapping
Do not power this build solely from a laptop USB port. The MQ-7 requires a pulsed heating cycle (5V for 60s, 1.5V for 90s in standard datasheet specs, though most hobby modules run a fixed 5V), and the MQ-135 draws roughly 150mA continuously. Combined with the LCD and microcontroller, you will exceed the 500mA USB limit. Use an external 5V 2A buck converter or a dedicated wall adapter.
| Component | Exact Variant / Spec | Est. Cost |
|---|---|---|
| Microcontroller | Arduino Uno R3 (ATmega328P) | $24.00 |
| Air Quality Sensor | MQ-135 Module (with 1KΩ RL) | $3.50 |
| CO Sensor | MQ-7 Module (with 1KΩ RL) | $3.50 |
| Display | 16x2 LCD with PCF8574T I2C Backpack | $5.00 |
| Power Supply | 5V 2A USB-C Buck Module or Wall Wart | $6.00 |
| Alert | 5V Active Piezo Buzzer | $1.00 |
Spec-Sheet Pin Mapping Table
| Module Pin | Arduino Uno R3 Pin | Notes |
|---|---|---|
| MQ-135 VCC | 5V (External Rail) | Do not use 3.3V |
| MQ-135 GND | GND | Common ground required |
| MQ-135 AOUT | A0 | Analog output |
| MQ-7 VCC | 5V (External Rail) | Draws high peak current |
| MQ-7 GND | GND | Common ground required |
| MQ-7 AOUT | A1 | Analog output |
| I2C LCD SDA | A4 | Hardware I2C on Uno |
| I2C LCD SCL | A5 | Hardware I2C on Uno |
| Buzzer + | D8 | PWM capable if needed |
Wiring Steps & Sensor Preheating
- Establish the Power Rail: Connect your external 5V 2A supply to the breadboard power rails. Connect the supply GND to the Arduino GND. Never skip the common ground; floating grounds will destroy the ADC readings.
- Wire the Sensors: Connect VCC and GND for both MQ-135 and MQ-7 to the external 5V rail. Route MQ-135 AOUT to A0 and MQ-7 AOUT to A1.
- Wire the I2C LCD: Connect SDA to A4, SCL to A5, VCC to 5V, and GND to GND. Adjust the blue trimpot on the back of the PCF8574T backpack until the LCD contrast shows clear blocks.
- Preheat Phase: Power on the system. The MQ-135 requires a 24-hour initial burn-in for first-time use to stabilize the baseline resistance. For subsequent uses, allow a 3-minute preheat before trusting the ppm calculations.
Complete Arduino Code with Error Handling
This sketch targets the Arduino Uno R3. It includes explicit pin definitions, I2C bus verification, and ADC saturation checks to prevent false alarms from disconnected wires. We use a simplified logarithmic regression based on the Hanwei datasheet sensitivity curves.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <math.h>
// --- PIN DEFINITIONS ---
#define MQ135_PIN A0
#define MQ7_PIN A1
#define BUZZER_PIN 8
// --- I2C LCD SETUP ---
// PCF8574T is usually 0x27, PCF8574AT is usually 0x3F
LiquidCrystal_I2C lcd(0x27, 16, 2);
// --- CALIBRATION CONSTANTS ---
const float RL = 1.0; // Load resistor on module in kOhms
const float R0_135 = 9.9; // MQ-135 Rs/R0 ratio in clean air (approx)
const float R0_7 = 3.3; // MQ-7 Rs/R0 ratio in clean air (approx)
const int ADC_MAX = 1023;
// Thresholds for alerts (ppm)
const float CO_ALERT_PPM = 35.0; // OSHA 8-hour TWA limit
void setup() {
Serial.begin(115200);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// I2C Error Handling
Wire.begin();
Wire.beginTransmission(0x27);
byte i2cError = Wire.endTransmission();
if (i2cError != 0) {
Serial.println("[ERROR] LCD I2C not found at 0x27. Check wiring or try 0x3F.");
// Blink LED to indicate fatal I2C error
pinMode(LED_BUILTIN, OUTPUT);
while(1) { digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); delay(100); }
}
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Gas Detector");
lcd.setCursor(0, 1);
lcd.print("Preheating...");
// Allow sensors to stabilize thermally before reading
delay(3000);
lcd.clear();
}
void loop() {
// Read ADC values
int raw135 = analogRead(MQ135_PIN);
int raw7 = analogRead(MQ7_PIN);
// ADC Saturation Error Handling
if (raw135 >= ADC_MAX || raw135 == 0) {
Serial.println("[ERROR] MQ-135 ADC saturated at 1023 or 0. Check wiring.");
lcd.setCursor(0, 0); lcd.print("ERR: MQ135 PIN ");
delay(2000); return;
}
if (raw7 >= ADC_MAX || raw7 == 0) {
Serial.println("[ERROR] MQ-7 ADC saturated at 1023 or 0. Check wiring.");
lcd.setCursor(0, 0); lcd.print("ERR: MQ7 PIN ");
delay(2000); return;
}
// Calculate Rs (Sensor Resistance)
float rs135 = RL * ((float)ADC_MAX / raw135 - 1.0);
float rs7 = RL * ((float)ADC_MAX / raw7 - 1.0);
// Calculate PPM using log/log10 regression (approximate curves)
// Formula: ppm = a * (Rs/R0)^b
float ppm135 = 105.0 * pow((rs135 / R0_135), -2.5); // Air Quality Index proxy
float ppm7 = 98.0 * pow((rs7 / R0_7), -3.1); // CO ppm proxy
// Constrain to realistic display values
ppm135 = constrain(ppm135, 0, 9999);
ppm7 = constrain(ppm7, 0, 9999);
// Display Data
lcd.setCursor(0, 0);
lcd.print("AQ:"); lcd.print((int)ppm135); lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print("CO:"); lcd.print((int)ppm7); lcd.print("ppm ");
// Alert Logic
if (ppm7 > CO_ALERT_PPM) {
digitalWrite(BUZZER_PIN, HIGH);
Serial.print("[ALERT] CO Level High: "); Serial.println(ppm7);
} else {
digitalWrite(BUZZER_PIN, LOW);
}
// Serial Telemetry
Serial.print("RAW135:"); Serial.print(raw135);
Serial.print(" | RAW7:"); Serial.print(raw7);
Serial.print(" | AQ:"); Serial.print(ppm135);
Serial.print(" | CO:"); Serial.println(ppm7);
delay(1000);
}
Debugging: "Sensor Reading Stuck at 1023 or 0"
The most common failure mode in gas detector Arduino projects is an ADC reading that refuses to change, triggering the serial output: [ERROR] MQ-135 ADC saturated at 1023 or 0. Check wiring.
Ranked Causes:
- Floating Ground or Unpowered Sensor: If the sensor GND is disconnected, the analog pin floats high to 5V (reading 1023). If VCC is disconnected, it reads 0.
- Missing Load Resistor (RL): Cheap clone modules sometimes have cold solder joints on the 1KΩ surface-mount RL resistor. Without it, the voltage divider is broken.
- USB Brownout: The MQ-7 heater draws ~350mA. If powered via a weak laptop USB port, the 5V rail drops below 4.5V, causing the ATmega328P ADC reference to collapse and return erratic 0 or 1023 values.
- Measure the 5V rail under load: Put your multimeter probes directly on the MQ module's VCC and GND pins while powered. It must read >4.8V.
- Verify GND continuity: With power off, check resistance from Arduino GND to the sensor module GND. It should be < 1 ohm.
- Inspect the module PCB: Look for the 3-pin SMD component labeled '102' (1KΩ). Ensure it hasn't been knocked off the board.
Extending and Simplifying the Build
To Simplify: If you only care about general indoor air quality (VOCs, ammonia), drop the MQ-7 and the I2C LCD. Use the Arduino Serial Plotter to graph the raw A0 values. This eliminates I2C address conflicts and cuts power draw in half, allowing you to safely power the project via a standard USB cable.
To Extend: For a production-grade IoT node, swap the Uno R3 for an ESP32-WROOM-32. The ESP32's 12-bit ADC (0-4095) provides finer resolution for the Rs calculation. You can integrate the PubSubClient library to publish the ppm telemetry to an MQTT broker (like Mosquitto or Home Assistant) over WiFi, and use the ESP32's deep sleep modes between readings to run the detector on a 18650 lithium cell for weeks.
Frequently Asked Questions
Can I power the Arduino gas detector directly from a laptop USB port?
Technically yes, but practically no. A standard USB 2.0 port supplies 500mA. The Arduino Uno draws ~50mA, the LCD ~20mA, the MQ-135 ~150mA, and the MQ-7 ~350mA. You are pulling ~570mA, which will trip the USB overcurrent protection on your laptop or cause a brownout on the Arduino's 5V regulator. Always use an external 5V 2A supply injected into the breadboard rails or the Arduino's VIN pin.
Why does my Arduino MQ sensor take 3 minutes to give accurate readings?
The SnO2 sensing layer requires a specific operating temperature (usually 200°C–300°C internally) to catalyze the gas oxidation reaction. When you first apply power, the heating element takes 2 to 3 minutes to reach thermal equilibrium. Readings taken before this preheat phase will show artificially high resistance (low ppm) and should be discarded by your code.
How do I calibrate the R0 value for my specific gas detector Arduino project?
The R0 constant in the code represents the sensor's resistance in clean air (typically 20.9% O2, 78% N2, 400ppm CO2). To calibrate: take your sensor outside to a known clean-air environment, let it preheat for 15 minutes, and run a sketch that calculates Rs using the voltage divider formula. Assign that calculated Rs value to your R0 constant in the main code. Recalibrate if you move to a significantly different altitude or humidity baseline.
Is the MQ-135 actually accurate for measuring CO2 in ppm?
No. While the MQ-135 is sensitive to CO2, it is highly cross-sensitive to humidity, temperature, VOCs, and alcohol vapors. It cannot distinguish between a human exhaling CO2 and a bottle of open rubbing alcohol. For accurate, isolated CO2 measurement, use an NDIR (Non-Dispersive Infrared) sensor like the MH-Z19B or Sensirion SCD40. The MQ-135 is best used as a general "Air Quality Index" anomaly detector.






