blog

How to use ZP01 sensor to test air quality

air quality sensor1

What is ZP01 Sensor

Ever wanted to test for weird smells and air quality issues at home or in your car? Think formaldehyde, benzene, carbon monoxide, ammonia, alcohol, cigarette smoke, fragrances, and more. Let’s be real—hiring a professional air quality testing company for these spots just isn’t practical. And you don’t need to splurge on a fancy professional air quality monitor either. All you need is a finger-sized module to get the job done. It’s simple, affordable, and works like a charm!
The ZP01/Odor Sensor is basically an air quality detection module. You can use it with any regular control board—like the common Arduino board—to run air quality tests. Let’s build the ultimate air quality monitor together!
 
It can handle home air quality testing and indoor air quality testing, and it makes for a totally portable air quality monitor. What’s more, it’s tiny and super convenient—you can even use it as an outdoor air quality monitor, no problem at all. With this little module, you can check the air quality around you anytime, anywhere.
 
On top of that, you can challenge yourself to build an air purifier with a built-in air quality monitor. You could even add an air quality monitor app for remote monitoring! It’s fun, saves you money, and you’ll pick up electronics knowledge along the way. If you want to keep things simple, it’s also a great fit for a smart wardrobe project.
 
Next up, let’s take a close look at this module first, then write the code to build our own air quality detection module!

Module Introduction

Sensitivity Curves of the ZP01 Module for Various Gas Detection

Air-Quality Detection2

Application of schematic diagram

Air-Quality Detection3
  • Pin 1: GND (Input power -)
  • Pin 2: 5V (Input power +)
  • Pin 3: A (Output signal A)
  • Pin 4: B (Output signal B)

Output signal

  • Grade 0: Output A (0V), Output B (0V) → Clean
  • Grade 1: Output A (0V), Output B (+5V) → Light pollution
  • Grade 2: Output A (+5V), Output B (0V) → Moderate pollution
  • Grade 3: Output A (+5V), Output B (+5V) → Severe pollution
Obviously, you just need to write the program for the air odor quality module detector based on the logic of its output signals. Let me first explain why the output voltage levels of the A and B pins on the ZP01 module differ when the level of air odor pollution varies. If you just want to use it directly and don’t care about the details, you can skip straight to the program section below.
 
The module has a built-in dual-threshold comparator circuit, so the digital output voltage levels of its two channels (A and B) change with the pollution level. It achieves quantitative differentiation of pollution levels through graded threshold judgment—this isn’t just a simple “presence/absence” detection. Its core working principle leverages the resistance-concentration characteristics of the gas-sensitive element.
 
The ZP01 houses a metal oxide gas sensor (MOS). When it comes into contact with odorous/harmful gases, the resistance of the gas-sensitive element changes nonlinearly with gas concentration (the higher the concentration, the lower the resistance; the resistance is at its maximum in clean air). The module then converts this resistance change into a voltage signal.
 
Additionally, it has two independent voltage comparators built-in, with two preset different reference thresholds (Vth1 = mild pollution threshold, Vth2 = moderate pollution threshold). The module compares the real-time voltage (Vreal) converted by the gas-sensitive element with these two thresholds, and outputs high or low voltage levels via the A and B pins to achieve a 4-level pollution classification. To put it simply, the dual-channel output design is meant to convert continuous concentration changes into discrete 4-level states.

Code 1

				
					// Define LCD pins (RS, EN, D4, D5, D6, D7)
LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
// ZP01 digital output pins
const int PIN_ZP01_A = 2;
const int PIN_ZP01_B = 3;
// Buzzer pin (Low level triggers alarm)
const int PIN_BUZZER = 4;
void setup() {
  // Initialize pin modes
  pinMode(PIN_ZP01_A, INPUT);
  pinMode(PIN_ZP01_B, INPUT);
  pinMode(PIN_BUZZER, OUTPUT);
  // Initialize LCD (16 columns × 2 rows)
  lcd.begin(16, 2);
  // Serial debugging (optional)
  Serial.begin(9600);
  // Power-on prompt
  lcd.print("ZP01 Detector");
  delay(1000);
  lcd.clear();
}
void loop() {
  // Read ZP01 A/B output levels (HIGH=5V, LOW=0V)
  int valA = digitalRead(PIN_ZP01_A);
  int valB = digitalRead(PIN_ZP01_B);
  // Judge pollution grade based on A/B levels
  String gradeStr;
  int grade;
  if (valA == LOW && valB == LOW) {
    grade = 0;
    gradeStr = "Clean";      // Grade 0: No pollution (clean air)
  } else if (valA == LOW && valB == HIGH) {
    grade = 1;
    gradeStr = "Light";      // Grade 1: Light pollution
  } else if (valA == HIGH && valB == LOW) {
    grade = 2;
    gradeStr = "Moderate";   // Grade 2: Moderate pollution
  } else {
    grade = 3;
    gradeStr = "Severe";     // Grade 3: Severe pollution
  }
  // Buzzer alarm logic: Trigger when grade ≥ 1
  if (grade > 0) {
    digitalWrite(PIN_BUZZER, LOW); // Activate alarm (low level trigger)
    // Extension: Set different alarm sounds for different grades (frequency/interval)
  } else {
    digitalWrite(PIN_BUZZER, HIGH); // Turn off alarm
  }
  // LCD display content
  lcd.setCursor(0, 0);
  lcd.print("Pollution: ");
  lcd.print(gradeStr);       // Display pollution grade
  lcd.setCursor(0, 1);
  if (grade > 0) {
    lcd.print("ALARM! Lv"); 
    lcd.print(grade);        // Display alarm grade
  } else {
    lcd.print("No Alarm      "); // Clear residual characters
  }  
  delay(500); // Refresh interval (adjustable)
}
				
			
Among these, digitalRead() is the core API for Arduino to read digital voltage levels—it only returns two states: HIGH or LOW. For LCD display, there are dedicated libraries you can download and use, which are super convenient.
 
A combination of the voltage levels from channels A and B corresponds to 4 pollution levels (00 = clean, 01 = mild, 10 = moderate, 11 = severe). This is a graded encoding design at the sensor’s hardware level: 1 IO channel can only distinguish 2 states, 2 IO channels can distinguish 2²=4 states, and n IO channels can distinguish 2ⁿ states.
 
In the code, a low voltage level triggers the alarm and a high level turns it off—and this is for an active buzzer. It has a built-in driver circuit, so you only need high/low voltage levels to control whether it beeps or stops. If you want to use a passive buzzer instead, you’ll need to use tone() to generate a PWM square wave.
air quality sensor4

Module Wiring

  • GND connects to Arduino GND
  • 5V connects to Arduino 5V
  • Pin A connects to Arduino D2
  • Pin B connects to Arduino D3
  • Buzzer negative pin connects to Arduino GND
  • Buzzer positive pin connects to Arduino D4
  • LCD1602 Display:
  • RS connects to Arduino D7
  • EN connects to Arduino D8
  • D4 connects to Arduino D9
  • D5 connects to Arduino D10
  • D6 connects to Arduino D11
  • D7 connects to Arduino D12
  • VCC connects to Arduino 5V
  • GND connects to Arduino GND

Pin V0 is externally connected to a 10K potentiometer (the two ends of the potentiometer are connected to 5V and GND respectively, which is used to adjust the display contrast)

The program above is a basic one for using this module. If you want to implement more complex functions, you can check out the program below.

Code 2

				
					// ====================== Hardware Pin Definitions ======================
// LCD1602 pins (RS, EN, D4, D5, D6, D7)
LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
// ZP01 odor sensor pins
const int PIN_ZP01_A = 2;
const int PIN_ZP01_B = 3;
// Alarm output pins
const int PIN_BUZZER = 4;
const int LED_LIGHT = 5;   // Green LED (light pollution)
const int LED_MODERATE = 6;// Yellow LED (moderate pollution)
const int LED_SEVERE = 13; // Red LED (severe pollution)
// Buttons (pull-up input, trigger on LOW level)
const int KEY_THRESHOLD = A0; // Adjust alarm threshold
const int KEY_MUTE = A1;      // Mute alarm

// ====================== Global Variables ======================
int odorGrade = 0;          // Current odor grade (0-3: clean to severe)
int alarmThreshold = 1;     // Alarm threshold (1=light+, 2=moderate+, 3=severe only)
bool isMute = false;        // Mute status (true = buzzer off)
bool lastMuteState = false; // Last mute state (prevent repeated trigger)
unsigned long lastKeyTime = 0; // Key debounce timestamp
const int DEBOUNCE_DELAY = 20; // Debounce delay (ms)

// ====================== Function Declarations ======================
void readOdorGrade();       // Read ZP01 signal and judge odor grade
void updateLED();           // Control LED indicators based on odor grade
void updateBuzzer();        // Control buzzer based on grade, threshold and mute state
void handleKeyInput();      // Process button input (debounce + function logic)
void updateLCD();           // Update LCD display (no flicker)
void printSerialLog();      // Output formatted log to serial monitor

void setup() {
  // Initialize pin modes
  pinMode(PIN_ZP01_A, INPUT);
  pinMode(PIN_ZP01_B, INPUT);
  pinMode(PIN_BUZZER, OUTPUT);
  pinMode(LED_LIGHT, OUTPUT);
  pinMode(LED_MODERATE, OUTPUT);
  pinMode(LED_SEVERE, OUTPUT);
  pinMode(KEY_THRESHOLD, INPUT_PULLUP); // Enable internal pull-up resistor
  pinMode(KEY_MUTE, INPUT_PULLUP);

  // Initialize LCD
  lcd.begin(16, 2);
  lcd.print("ZP01 Detector");
  delay(1000);
  lcd.clear();
  lcd.print("Threshold: ");
  lcd.print(alarmThreshold);
  delay(500);
  lcd.clear();

  // Initialize serial monitor (9600 baud rate)
  Serial.begin(9600);
  Serial.println("=== ZP01 Odor Detector Start ===");
  Serial.print("Initial Alarm Threshold: ");
  Serial.println(alarmThreshold);

  // Turn off all outputs by default
  digitalWrite(PIN_BUZZER, HIGH);
  digitalWrite(LED_LIGHT, LOW);
  digitalWrite(LED_MODERATE, LOW);
  digitalWrite(LED_SEVERE, LOW);
}

void loop() {
  readOdorGrade();    // Get current odor grade
  handleKeyInput();   // Check button presses
  updateLED();        // Update LED status
  updateBuzzer();     // Update buzzer status
  updateLCD();        // Refresh LCD if status changed
  printSerialLog();   // Output log to serial
  delay(200);         // Main loop interval (avoid frequent refresh)
}

// Read ZP01 A/B pin levels and determine odor grade
// 0 = Clean, 1 = Light pollution, 2 = Moderate pollution, 3 = Severe pollution
void readOdorGrade() {
  int valA = digitalRead(PIN_ZP01_A);
  int valB = digitalRead(PIN_ZP01_B);

  if (valA == LOW && valB == LOW) {
    odorGrade = 0;
  } else if (valA == LOW && valB == HIGH) {
    odorGrade = 1;
  } else if (valA == HIGH && valB == LOW) {
    odorGrade = 2;
  } else {
    odorGrade = 3;
  }
}

// Control LED indicators: green for light, yellow for moderate, all on for severe
void updateLED() {
  // Turn off all LEDs first to avoid residual light
  digitalWrite(LED_LIGHT, LOW);
  digitalWrite(LED_MODERATE, LOW);
  digitalWrite(LED_SEVERE, LOW);

  switch (odorGrade) {
    case 1:
      digitalWrite(LED_LIGHT, HIGH); // Green LED on for light pollution
      break;
    case 2:
      digitalWrite(LED_MODERATE, HIGH); // Yellow LED on for moderate pollution
      break;
    case 3:
      // All LEDs on for severe pollution
      digitalWrite(LED_LIGHT, HIGH);
      digitalWrite(LED_MODERATE, HIGH);
      digitalWrite(LED_SEVERE, HIGH);
      break;
    default: // Grade 0 (clean), all LEDs off
      break;
  }
}

// Control buzzer: different beep frequency for different grades (if not muted)
void updateBuzzer() {
  // Trigger buzzer only if grade >= threshold and not muted
  if (odorGrade >= alarmThreshold && !isMute) {
    // Different beep frequency based on odor grade
    switch (odorGrade) {
      case 1: // Light pollution: beep once per second (200ms on, 800ms off)
        digitalWrite(PIN_BUZZER, LOW);
        delay(200);
        digitalWrite(PIN_BUZZER, HIGH);
        delay(800);
        break;
      case 2: // Moderate pollution: beep twice per second (100ms on, 100ms off)
        digitalWrite(PIN_BUZZER, LOW);
        delay(100);
        digitalWrite(PIN_BUZZER, HIGH);
        delay(100);
        break;
      case 3: // Severe pollution: buzzer on continuously
        digitalWrite(PIN_BUZZER, LOW);
        break;
    }
  } else {
    digitalWrite(PIN_BUZZER, HIGH); // Turn off buzzer
    // Auto cancel mute if grade drops below threshold
    if (odorGrade < alarmThreshold) {
      isMute = false;
    }
  }
}

// Process button input with debounce to avoid false triggers
void handleKeyInput() {
  unsigned long currentTime = millis();
  // Button 1: Adjust alarm threshold (cycle 1→2→3→1)
  if (digitalRead(KEY_THRESHOLD) == LOW && currentTime - lastKeyTime > DEBOUNCE_DELAY) {
    alarmThreshold++;
    if (alarmThreshold > 3) alarmThreshold = 1;
    lastKeyTime = currentTime;
    Serial.print("Alarm Threshold Updated: ");
    Serial.println(alarmThreshold);
    delay(100); // Short delay to prevent continuous press
  }
  // Button 2: Toggle mute status
  if (digitalRead(KEY_MUTE) == LOW && currentTime - lastKeyTime > DEBOUNCE_DELAY) {
    isMute = !isMute;
    lastKeyTime = currentTime;
    Serial.print("Mute State: ");
    Serial.println(isMute ? "ON" : "OFF");
    delay(100);
  }
}

// Update LCD only when status changes (no flicker)
void updateLCD() {
  static int lastGrade = -1;
  static int lastThreshold = -1;
  static bool lastMute = false;

  // Refresh LCD only if grade/threshold/mute status changes
  if (odorGrade != lastGrade || alarmThreshold != lastThreshold || isMute != lastMute) {
    lcd.clear();
    // First line: Odor grade + mute status
    lcd.setCursor(0, 0);
    lcd.print("Odor: ");
    switch (odorGrade) {
      case 0: lcd.print("Clean  "); break;
      case 1: lcd.print("Light  "); break;
      case 2: lcd.print("Moderate"); break;
      case 3: lcd.print("Severe "); break;
    }
    lcd.print(isMute ? "(Mute)" : "      ");

    // Second line: Alarm threshold
    lcd.setCursor(0, 1);
    lcd.print("Thresh: ");
    lcd.print(alarmThreshold);
    lcd.print(" (Lv");
    lcd.print(alarmThreshold);
    lcd.print("+)");

    // Update last status to compare next time
    lastGrade = odorGrade;
    lastThreshold = alarmThreshold;
    lastMute = isMute;
  }
}

// Output formatted serial log (once per second to avoid spam)
void printSerialLog() {
  static unsigned long lastLogTime = 0;
  // Output log every 1 second
  if (millis() - lastLogTime > 1000) {
    Serial.print("[");
    Serial.print(millis() / 1000); // Elapsed time in seconds
    Serial.print("s] Odor Grade: ");
    Serial.print(odorGrade);
    Serial.print(" | Alarm Thresh: ");
    Serial.print(alarmThreshold);
    Serial.print(" | Mute: ");
    Serial.println(isMute ? "YES" : "NO");
    lastLogTime = millis();
  }
}
				
			
This program adds LED level indication, adjustable alarm thresholds, and a mute function. A green light means mild odors, a yellow light indicates moderate odors, and if all three (red, green, and yellow) lights are on, it’s severe pollution. If there are no odors at all, all LEDs stay off.
 
The button connected to A0 is for adjusting the alarm threshold—each press cycles through 1, 2, and 3. Setting 1 triggers the alarm for mild odors and above, setting 2 for moderate and above, and setting 3 only for severe odors. When the threshold changes, you’ll see the update on both the LCD screen and the Serial Monitor right away.
 
The button connected to A1 is the mute button. Press it once, and the buzzer stops beeping—but the LEDs still show the pollution level normally. Once the odor concentration drops below the set threshold, the mute function cancels automatically, and the buzzer goes back to working as usual.
 
You can also open the Serial Monitor (set the baud rate to 9600). It outputs a log every second, which includes the device’s runtime (in seconds), current odor level, alarm threshold, and mute status. This makes it easy for you to check and verify the data later on.
air quality sensor5

Problem

No display on LCD1602:

Two possible causes—either the contrast potentiometer isn’t adjusted, or the wiring is reversed. First, rotate the potentiometer until the characters on the screen are clear. Then double-check if the RS, EN, D4-D7 pins are wired incorrectly, and also verify if the LCD’s VCC and GND are reversed.

Overlapping characters on LCD:

Residual characters on the screen aren’t being cleared. Add spaces after outputting content with lcd.print(), or call lcd.clear() to wipe the screen clean before each display.

Pollution level always shows “Clean”:

This could be because the ZP01 module hasn’t finished preheating, or the module pin wiring is wrong. Wait for the module to preheat for 3 minutes before testing, then double-check if the A/B pins of the ZP01 module are connected to pins 2/3 of the Arduino. Also, make sure the module’s VCC is connected to 5V (connecting it to 3.3V will cause detection to fail).

Frequent level fluctuations:

The sensor’s output voltage level is jittering. Enable the debounce function in the code, or adjust the delay value in the loop() function to over 200ms to extend the sampling interval.

FAQ

Are home air quality tests worth it?

Totally worth it! Especially for newly renovated houses—harmful gases like formaldehyde and benzene tend to exceed safety limits easily. If you have elderly people, kids, or anyone with an allergic constitution at home, you need to be extra careful. Testing it yourself is super affordable, costing only tens to hundreds of yuan. It lets you spot problems in time—way better than breathing bad air long-term, and way more cost-effective than paying over a thousand yuan for professional testing services.
 

Can I test air quality myself?

Absolutely! Grab a simple odor sensor, pair it with an Arduino board and a small display. Just wire them up, flash a ready-made code, and you’re good to go. It shows the pollution level directly and even sounds an alarm—more than enough for daily screening of odors and harmful gases.
 

Can my phone test air quality?

Not directly. Phones don’t have built-in sensors to detect formaldehyde or odors. Those so-called air quality testing apps either use public data from local environmental protection departments, or require you to buy an extra Bluetooth sensor accessory. Essentially, they rely on hardware—your phone can’t do the testing on its own.

Related Products

Leave a Reply

Your email address will not be published. Required fields are marked *