How to Make an Arduino Air Quality Sensor

Arduino Air Quality Sensor - Connections - VueVille


Ever wondered what’s in the air you are breathing? All over the world, our cities are getting more and more polluted. Air pollution is an invisible killer that takes over 4 million lives every year.

But it’s not just outdoor air that can be harmful. Common activities like cooking or burning candles togetherwith poor ventilation can cause a severe drop in indoor air quality. This is because burning of fuels release countless numbers of tiny particles into the air.

In the past, air quality sensors were expensive and not very accessible. But now with readily available micro-controllers like the Arduino and affordable DIY sensors, we can make our own DIY Arduino air quality sensor in less than 15 minutes.

Video Tutorial

A quick note: As an Amazon Associate I earn from qualifying purchases. This post contains affiliate link(s). An affiliate link means I may earn advertising or referral fees if you make a purchase through my link, at no extra cost to you.

Air Quality Monitoring

The specific methods used to measure air quality depends on whether you are thinking of measuring indoor or outdoor air quality.

Indoor air quality is affected by pollutants such as dust, CO2 and Volatile Organic Compounds (VOC). By measuring one or more of these, we can get a good idea of indoor air quality. Different sensors are required for each of these pollutants.

Outdoor air quality usually depends on pollutants from combustion engines. The burning of fuel produces tiny particles called Particulate Matter (PM) that are classified by how small they are.

The smaller the particles, the more dangerous they are. PM2.5 refers to atmospheric particulate matter (PM) that have a diameter of less than 2.5 micrometres. This is about 3% the diameter of a human hair.

Arduino Air Quality Sensor - LCD 1602A - VueVille

Exposure to PM2.5 particles over a long period of time is harmful for health. The key to managing your exposure to these harmful particles is to be able to measure it.

For this tutorial, we will focus on measuring PM2.5 and its close cousins, PM1.0 and PM10 (particle size of 1 and 10 micrometres respectively).

These figures will give us the Air Quality Index (AQI), the most commonly used metric for measuring and comparing air quality levels of different cities in the world.

You will be able to use this Arduino Air Quality Monitor indoors to measure pollution from cooking, smoking etc., and outdoors to measure the air quality of your neighbourhood.

Air Quality Sensors

The most common type of air quality sensor that measures PM2.5 uses a laser to detect particulate matter. There are two types of these laser-based sensors: ones without a fan like the Samyoung DSM501A and ones which have a fan like the Plantower PMS5003.

The fanless versions are cheaper but are also less accurate. The Plantower PMS5003 that we will use is the best DIY model that is still quite affordable.

PMS5003 Air Quality Sensor

The Plantower PMS5003 sensor can detect particles as small as 0.3 micrometres. This is why the model number of the sensor ends with ‘003’. The 5 refers to the series generation.

Arduino Air Quality Sensor - PMS5003 - VueVille

You can see the air intake fan on the back. That’s what makes this sensor more accurate than fanless designs like the Samyoung DSM501A.

Arduino Air Quality Sensor - PMS5003 - VueVille
Arduino Air Quality Sensor - PMS5003 - Back - VueVille

Actually the PMS5003 is not a current generation model, that’s actually the PMS7003. If you can find the PMS3003 or PMS7003 at a better price than the PMS5003, by all means go for it. The Arduino code may be a bit different though.

Working principle

The PMS5003 uses a laser to scatter and radiate suspended particles in the air. It then analyses the scattered light to obtain the curve of how the light scattering changes with time.

The sensor then calculates the number of particles of various diameters per unit 0.1L volume of air.

The PMS5003 can output the following:

  • PM1.0, PM2.5 and PM10.0 concentration in both standard & environmental units
  • Particulate matter per 0.1L air, categorized by 0.3um, 0.5um, 1.0um, 2.5um, 5.0um and 10um sizes

Connecting the PMS5003 to Arduino

The PMS5003 has an 8-pin JST port and comes with a cable that fits the port. But the easiest way to connect the sensor to the Arduino is by getting a JST to DIP 2.54mm standard spacing adaptor board.

This makes it super easy to interface the sensor to your Arduino.

Components required

If you have an Arduino Starter Kit like this one, the only thing you need is the PM2.5 sensor.

Here’s the full parts list.

  1. Arduino Uno Rev3
  2. Plantower PMS5003 Air Quality Sensor
  3. 1602A LCD Display – 2 rows, 16 characters per row
  4. Breadboard
  5. Breadboard power supply
  6. Connecting Wires

Wiring diagram

Arduino Air Quality Sensor - Connection Diagram - VueVille
Click to enlarge

PMS5003 sensor

Connect the PMS5003 as below if you are using a JST to DIP 2.54mm standard spacing adaptor board (recommended):

  1. VCC to 5V (do not use Arduino to supply this voltage)
  2. GND to common ground with Arduino
  3. Arduino pin #8 to TX pin on sensor (this is incoming data from the sensor)
  4. Leave pin #9 disconnected even though it is defined in the code below

1602A LCD display

I strongly recommend using the I2C method to connect the 1602A LCD display to your Arduino. This means you need to use just 4 pins instead of over 9 with the traditional method.

For this get a version of the LCD that is already soldered with an I2C module. Most of the starter kits include this version so that beginners don’t get overwhelmed.

This is how you wire it:

  1. VCC to 5V (do not use Arduino to supply this voltage)
  2. GND to common ground with Arduino
  3. SDA to pin A4
  4. SCL to pin A5

Arduino Code

Copy the code below into a new .ino file, compile it and upload to your Arduino. That’s it!

Leave a comment below if this article helped you!

// Created by VueVille.com based on the Adafruit code for PMS5003 PM2.5 sensor
// Read the full how-to article at https://www.vueville.com/arduino/arduino-air-quality-sensor/

// Connect the PMS5003 like this if you are using a JST to DIP 2.54mm standard spacing adaptor board (easier):
// VCC to 5V (do not use Arduino to supply this voltage), GND to common ground with Arduino
// Arduino pin #8 to TX pin on sensor (this is incoming data from the sensor), leave pin #9 disconnected even though it is defined in the code below

// Connect the 1602A LCD like this if you are using an I2C module (easier)
// VCC to 5V (do not use Arduino to supply this voltage), GND to common ground with Arduino
// SDA to pin A4
// SCL to pin A5

#include <SoftwareSerial.h>
SoftwareSerial pmsSerial(8, 9);

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
//I2C pins declaration for 1602A with I2C module
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

unsigned long previousMillis = 0;
unsigned long interval = 1000; //Refreshes the LCD once every 1000ms (1 second) without holding up the Arduino processor like delay() does. If you use a delay(), the PMS5003 will produce checksum errors
 
void setup() {
  // debugging output, don't forget to set serial monitor to 115200 baud
  Serial.begin(115200);
 
  // sensor baud rate is 9600
  pmsSerial.begin(9600);
  lcd.begin(16,2);//Defining 16 columns and 2 rows of lcd display
  lcd.backlight();//To Power ON the back light
}
 
struct pms5003data {
  uint16_t framelen;
  uint16_t pm10_standard, pm25_standard, pm100_standard;
  uint16_t pm10_env, pm25_env, pm100_env;
  uint16_t particles_03um, particles_05um, particles_10um, particles_25um, particles_50um, particles_100um;
  uint16_t unused;
  uint16_t checksum;
};
 
struct pms5003data data;
    
void loop() {
// Serial output code begins
if (readPMSdata(&pmsSerial))
  {
  // reading data was successful!
  Serial.println();
  Serial.println("---------------------------------------");
  Serial.println("Concentration Units (standard)");
  Serial.print("PM 1.0: "); Serial.print(data.pm10_standard);
  Serial.print("\t\tPM 2.5: "); Serial.print(data.pm25_standard);
  Serial.print("\t\tPM 10: "); Serial.println(data.pm100_standard);
  Serial.println("---------------------------------------");
  Serial.println("Concentration Units (environmental)");
  Serial.print("PM 1.0: "); Serial.print(data.pm10_env);
  Serial.print("\t\tPM 2.5: "); Serial.print(data.pm25_env);
  Serial.print("\t\tPM 10: "); Serial.println(data.pm100_env);
  Serial.println("---------------------------------------");
  Serial.print("Particles > 0.3um / 0.1L air:"); Serial.println(data.particles_03um);
  Serial.print("Particles > 0.5um / 0.1L air:"); Serial.println(data.particles_05um);
  Serial.print("Particles > 1.0um / 0.1L air:"); Serial.println(data.particles_10um);
  Serial.print("Particles > 2.5um / 0.1L air:"); Serial.println(data.particles_25um);
  Serial.print("Particles > 5.0um / 0.1L air:"); Serial.println(data.particles_50um);
  Serial.print("Particles > 10.0 um / 0.1L air:"); Serial.println(data.particles_100um);
  Serial.println("---------------------------------------");
  }

// Converting the sensor readings to string values that the LCD can display
String pm1String = String(data.pm10_standard);
String pm25String = String(data.pm25_standard);
String pm100String = String(data.pm100_standard);

// Non-blocking code for displaying the sensor readings on the LCD
unsigned long currentMillis = millis();
if (currentMillis - previousMillis > interval) {
  previousMillis = currentMillis;
  lcd.clear();//Clear the screen
  lcd.setCursor(0,0);
  lcd.print("PM1  PM2.5  PM10");
  lcd.setCursor(0,1);
  lcd.print(pm1String);
  lcd.setCursor(5,1);
  lcd.print(pm25String);
  lcd.setCursor(12,1);
  lcd.print(pm100String);
  }
}

// Code for calculating the sensor readings 
boolean readPMSdata(Stream *s) {
  if (! s->available()) {
    return false;
  }
  
  // Read a byte at a time until we get to the special '0x42' start-byte
  if (s->peek() != 0x42) {
    s->read();
    return false;
  }
 
  // Now read all 32 bytes
  if (s->available() < 32) {
    return false;
  }
    
  uint8_t buffer[32];    
  uint16_t sum = 0;
  s->readBytes(buffer, 32);
 
  // get checksum ready
  for (uint8_t i=0; i<30; i++) {
    sum += buffer[i];
  }
 
  /* debugging
  for (uint8_t i=2; i<32; i++) {
    Serial.print("0x"); Serial.print(buffer[i], HEX); Serial.print(", ");
  }
  Serial.println();
  */
  
  // The data comes in endian'd, this solves it so it works on all platforms
  uint16_t buffer_u16[15];
  for (uint8_t i=0; i<15; i++) {
    buffer_u16[i] = buffer[2 + i*2 + 1];
    buffer_u16[i] += (buffer[2 + i*2] << 8);
  }
 
  // put it into a nice struct :)
  memcpy((void *)&data, (void *)buffer_u16, 30);
 
  if (sum != data.checksum) {
    Serial.println("Checksum failure");
    return false;
  }
  // success!
  return true;
}
Daniel Ross

Daniel Ross

I am Daniel and VueVille is where I document my DIY smart home journey. I focus on 100% local-processing and local-storage because that’s the only way to secure my family’s safety and privacy. Oh and I don’t like monthly subscriptions!

6 Comments
  1. i use exakt this code and wiring, but the values are very high. PM1: 345, PM2.5: 1500 PM10: 2600
    as if everything were x100 or a comma is missing.can not find the error. an idea?

  2. Hi,
    I have one question:
    Where can I get the SMD to DIP board?

  3. Can we configure to output data to graph?

  4. I’m a beginner in this field compelled by the imperious need of a powder control at the outlet of an industrial powder separator. Entered with fear in dealing this discipline and now enjoying it. We construct the entire equipment using PMS5003; Arduino UNO R3; LCD_I2C 20X4 and all the connection as it’s shown here.
    From the first intention of Up Loading the process was blocked in his progress on line 7: LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); which we solved adopting LiquidCrystal_I2C lcd(0x27, 2, 1); and we thought best: LiquidCrystal_I2C lcd(0x27, 20, 4);
    Now the Up Loading occurs in perfection but finally at the LCD screen nothing appears. We checked it for days of tray and errors modifications, for NOTHING!
    Thanks for your attention. Roberto Iogna Iogna@kifiba.com

    Leave a reply

    VueVille
    Logo