Calibrating the Shinyei PPD Sensors…..Part 1

I started down the air pollution measurement path with the “MQ” series of sensors and then after getting widely different results under the same conditions, I decided to switch to measuring particle counts. Like a few others I have read about, I excited about doing air pollution work because I suffered from respiratory attacks that were (finally) chased down to air pollution as the root cause.

However, the cheap PM sensors also showed variability and came with instructions of converting pulse durations or voltages into particle counts, but not into PM2.5 or PM10 measures. So I raised money ($4K) for a school project and purchased a PM2.5 sensor from Shinyei the AES-1 or PMS-1 – this was the least expensive one I could find – still cost me $1000! Shinyei says it is calibrated to the SHARP5030, an instrument that costs $15K, using the “Federal Equivalence Method”.

I hooked up the PPD42NS and the PPD60PV-T2 (courtesy Michael Heimbinder) to a Raspberry-Pi and the AES to the net and made measurements. While the correlation was not great, the T2 was much better than the 42. These plots are all taken from TempoDB, an excellent time-series cloud database that is super easy to write to and read from. All my data is stored there.

Shinyei PPD42NS every 30 sec.

Shinyei PPD42NS every 30 sec.

Shinyei PPD60PV-T2 every 30 sec.

Shinyei PPD60PV-T2 every 30 sec.

I got the idea of adding a fan from Tim Dye. After much trial and error I found that as airtight a seal between the sensor and the fan is needed to get better results. But when I did that successfully (more on this later), the results were much much better than relying on the heater in the sensor. Below is the plot of the T2 with the fan compare to the T2 without the fan above.

Shinyei PPD60PV-T2 with fan every 30 sec.

Shinyei PPD60PV-T2 with fan every 30 sec.

The vertical axis is the PCS count reported by either mapping the analog voltage or the low pulse occupancy from the Shinyei data sheets. A lot of the variability (called “noise”) in the T2 without the fan has gone away when the fan was added!

Now what remains is to compare the T2 with and without the fan to the AES1 PM2.5 sensor.

Next…..Calibrating the T2 with the fan with the Shinyei AES-1

This entry was posted in Air Pollution and tagged , , , , . Bookmark the permalink.

8 Responses to Calibrating the Shinyei PPD Sensors…..Part 1

  1. Alex says:

    Hello there!

    – Which software did you used for make this measures?
    – Is that the fan’s wind have to be more fast or slow?
    – Also, can you post a comparison between PPD60PV-T2 vs PPD42NS both with fan every 30 sec? Please, please! 🙂


    • enetnut says:

      Hi Alex,

      Sorry for not replying earlier, I got busy with finals! I used TempoIQ to store my time series, and to visualize the data. And of course, the Arduino required some C to upload to TempoIQ, and I had to write some python scripts for uploading the rest of the data to TempoIQ and visualizing it via For the fan, I use a Sunon 25MM fan, that has an airflow of 3CFM when run at 5V, but I run it from the 3.3V pin of the Arduino, so it’s probably producing 2CFM. I have not tried changing the fan speed, but I will do it and post my results on the blog soon! I will post my comparison between the PPD60PV-T2 vs PPD42NS shortly.


      • Hi there,

        Could you please provide the code you are using on your Arduino?

      • A.J. says:

        Here it is, I don’t use Pulsein as it is blocking. I use interrupts. And this is over a 10s interval.
        I don’t think this will help with your issue as you are seeing 40% LPO.


        #define RXPIN2 2 // INTERRUPT 0 = DIGITAL PIN 2 - use the interrupt number in attachInterrupt
        #define RXPIN3 3 // INTERRUPT 1 = DIGITAL PIN 3 - use the interrupt number in attachInterrupt

        #define FASTADC 0
        // defines for setting and clearing register bits
        #ifndef cbi
        #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
        #ifndef sbi
        #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

        volatile unsigned long duration0; // volatile
        volatile unsigned long duration1;
        volatile unsigned long StartPeriod0 = 0; // set in the interrupt
        volatile unsigned long StartPeriod1 = 0; // set in the interrupt
        volatile boolean pulse0 = false; // set in the interrupt and read in the loop
        volatile boolean pulse1 = false;
        unsigned long starttime;
        unsigned long currtime;
        unsigned long startmicros = 0;
        unsigned long endmicros = 0;
        unsigned long sampletime_ms = 10000;
        unsigned long lowpulseoccupancy0 = 0;
        unsigned long lowpulseoccupancy1 = 0;
        unsigned long analogcount;
        unsigned long analogvalue = 0;
        double analogtotal;

        void setup()
        #if FASTADC
        // set prescale to 16
        sbi(ADCSRA,ADPS2) ;
        cbi(ADCSRA,ADPS1) ;
        cbi(ADCSRA,ADPS0) ;

        pinMode(RXPIN2, INPUT);
        pinMode(RXPIN3, INPUT);
        attachInterrupt(0, isrduration0,CHANGE);
        attachInterrupt(1, isrduration1,CHANGE);
        //Only for Leonardo
        //while(!Serial) {
        // ;

        void loop()
        //startmicros = micros();
        analogvalue = analogvalue + analogRead(A4);
        //endmicros = micros();
        lowpulseoccupancy0 = lowpulseoccupancy0+duration0;
        //Serial.print("Int0 ");
        pulse0 = false;
        lowpulseoccupancy1 = lowpulseoccupancy1+duration1;
        //Serial.print("Int1 ");
        pulse1 = false;
        currtime = millis();
        if ((currtime-starttime) > sampletime_ms)

        analogvalue = analogvalue/analogcount;
        analogtotal = (analogvalue*5.0)/1023;
        lowpulseoccupancy0 = 0;
        lowpulseoccupancy1 = 0;
        analogvalue = 0;
        analogcount = 0;
        starttime = millis();


        void isrduration0()
        if(digitalRead(RXPIN2) == LOW)
        StartPeriod0 = micros();
        if(StartPeriod0 && (pulse0 == false))
        duration0 = micros() - StartPeriod0;
        StartPeriod0 = 0;
        pulse0 = true;

        void isrduration1()
        // if the pin is low, its the start of an interrupt
        if(digitalRead(RXPIN3) == LOW)
        StartPeriod1 = micros();
        if(StartPeriod1 && (pulse1 == false))
        duration1 = micros() - StartPeriod1;
        StartPeriod1 = 0;
        pulse1 = true;

  2. Tony Strawa says:

    This is very work you have done here. I am searching for a vendor for the Shinyei PPD 60. Do you know of a US vendor?

    • A.J. says:

      Hi Tony, I got mine from the Airbeam guys since they were purchasing in bulk. If you are starting on PM sensors, I would recommend using one of the laser ones like the SDS011 it is a lot cheaper than the PPD60T2. I will be posting the results of testing it shortly.

Leave a Reply to A.J. Cancel reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s