The PlanTower PMS5003 Dust Sensor - uraich/IoT4AQ GitHub Wiki

Introduction

The Plantower PMS5003 is a sensor measuring the dust concentration in air. It communicates with its micro-controller over a serial line running at 9600 baud. The photos below show the sensor and a connection PCB that allows to easily connect the module to the ESP32.

The PMS5003 sensor PMS5003 interface board

The hardware connections

The PMS5003 is connected to the ESP32 through its UART2. A UART (Universal Asynchronous Receiver Transmitter) is an interface that was formally used by external terminals. The ESP32 has 3 such UART ports. The first one (UART0) is used by the Arduino SDK for flash programming and serial communication. Serial.print() uses this UART. The second one (UART1) is used by some ESP32 CPU cards to interface to external serial RAM (PSRAM) chips. The third UART (UART2) is free for general use.

The connections are made as follows:

PMS5003 pin number PMS5003 pin significance ESP32 GPIO pin number
Pin 1 Vcc 5V
Pin 2 GND GND
Pin 3 Set (suspend mode) not used
Pin 4 Rx GPIO 17 (UART2 TX)
Pin 5 Tx GPIO 16 (UART2 RX
Pin 6 Reset not used
Pin 7 NC (not connected)

Reading the PMS5003 sensor

To fully understand the functioning of the PMS5003 sensor you must read the PMS5003 data sheet

The PMS5003 dust sensor sends a message of 32 bytes over its serial line which represent 16 data words of 16 bits each. The high order byte is always sent first. It does this approximately once every second. The table below shows the meaning of each data word:

Byte numbers value meaning
0 Header, character 'B' used to synchronize
1 Header, character 'M' the message
2..3 2*12+2 = 26 frame length (data + check bytes)
4..5 pm1.0 concentration [ug/m3]
6..7 pm2.5 concentration [ug/m3]
8..9 pm10 concentration [ug/m3]
10..11 pm1.0 under atmospheric pressure
12..13 pm2.5 under atmospheric pressure
14..15 pm10 under atmospheric pressure
16..17 number of particle beyond 0.3 um in 1L volume
18..19 number of particle beyond 0.5 um in 1L volume
20..21 number of particle beyond 1.0 um in 1L volume
22..23 number of particle beyond 2.5 um in 1L volume
24..25 number of particle beyond 5.0 um in 1L volume
26..27 number of particle beyond 10 um in 1L volume
28..29 reserved
30..31 checksum

A bit more on C++ programming

Data structures

C++ does not only use arrays as composite data types but also structures. In a structure, each element has a name:

struct mystruc {
    int a;
    int b;
}

To declare a variable of this datatype we would write

struct mystruct s;
s.a = 7;
s.b = 5;

If we define this data structure as a new data type, access becomes easier:

typedef struct mystruc {
    int a;
    int b;
}
mystruct s;

We can also define a pointer to this data structure:

mystruct *structPtr;
structPtr -> a = 7;
structPtr -> b = 5;

A word on data encoding

As we said in the chapter on CPUs, computers only understand digital data, which is nothing but bit combinations. Memory locations can be 32 or even 64 bits wide, but access to single bytes is usually possible. A single byte stores 8 bits of data. Let's have a look at 4 bits only. How many combinations of zeros and ones can we construct from 4 bits?

bit combination decimal value hexadecimal value
0000 0 0x00
0001 1 0x01
0010 2 0x02
0011 3 0x03
0100 4 0x04
0101 5 0x05
0110 6 0x06
0111 7 0x07
1000 8 0x08
1001 9 0x09
1020 10 0x0a
1011 11 0x0b
1100 12 0x0c
1101 13 0x0d
1110 14 0x0e
1111 15 0x0f

We see that in 4 bits be can have 16 combinations and for this reason computer scientists often do not calculate in base 10 (as we usually do) but in base 16, the hexadecimal number system indicated by the "0x" in front of the numbers. This means that the carry over does not happen at the number 9 but at the number 0xf = 15. Let's do a quick calculation: What is 0x18 + 0x18?

Since 8 + 8 = 16 and we have the carry over at 15 we get 0x08 + 0x08 = 0x10 and therefore 0x18 + 0x18 = 0x30. Fortunately there are a number of calculators which can calculate in number base 8 and 16.

Hex Calculator

ASCII encoding

Now that we know how numbers are encoded, what about characters? How do we encode "Hello World!" ? In fact, each character is encoded as an 8 bit number as follows:

ASCII table

Here is a little program that shows, how "Hello World!" is encoded:

void setup () {
    char *hello = "Hello World!";
    uint8_t *helloPtr = (uint8_t *) hello;
    Serial.begin(115200);
    while (!Serial)
        delay(10);

    while (*helloPtr != '\0') { // go through the C string until you find
                                // the terminating zero

        Serial.print("0x");
        Serial.print(*helloPtr++,HEX); // print the contents of the memory location to which helloPtr points
                                       // increment the pointer (which now points to the next character)
        Serial.print(", ");
    }
    Serial.print("0x");
    Serial.print(*helloPtr);
}

... and here the result. Please compare it to the ASCII table above:

0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x0

Bit-wise logical operations

C++ has operators that allow bit-wise logical operations, namely

  • "|" : logical or
  • "&" : logical and
  • "<<": shift left
  • ">>": shift right.

If we have 2 bytes with the values 0x37 and the value 0x82. What will be result of

uint8_t a = 0x37;   //uint8_t is a data type the stands for unsigned integer of 8 bits.
uint8_t b = 0x82;
uint8_t c = a | b;
uint8_t d = a & b;

Let's write down the two values in bits: 0011 0111, this is 0x37 and 1000 0010 is 0x82. If now we "or" each bit:

    0011 0111
 or 1000 0010
 ------------
    1011 0111

we will get 1011 0111 which, written in hex notation gives us 0xb7. If we "and" the two numbers

    0011 0111
and 1000 0010
-------------
    0000 0010

we get 0000 0010 which is 0x02. If we shift the 0x37 by one bit:

uint8_t c = 0x37 << 1; 

then we will get 0011 0111 -> 0110 1110 = 0x6e, which corresponds to a multiplication by 2 (check with your calculator) If you want to print values in hexadecimal notation instead of the decimal one, this is what you do:

int a = 0x1ac4;
Serial.print("a in hex notation: ");
Serial.println(a,HEX);

The library

It is not particularly difficult to read the PMS5003 protocol and to extract the values. Nevertheless we provide a library, which makes it even simpler. Here is how it works:

The Serial driver in the Arduino SDK provides a function Serial.available() which returns true if new data are available. We wait until this function returns true. Then we read a data byte and we check if it corresponds of the letter 'B', the first letter in the header. If not, we wait for the next byte. Once 'B' has been received we do the same for the letter 'M'. Once the two letters of the header have been received, we know that a valid protocol message is about to arrive and we save all the following 30 bytes into an array of raw data. From the raw data we can easily extract the data word of valid PMS5003 values:

int result;
for (int i=2;i<32,i++) {
    result = (rawData[i] << 8) | rawData[i+1]; // the library uses pointers to accomplish this
                                               // but the principle is the same
}

The result words are saved in a data structure, which maps to the protocol layout:

typedef struct pms5003Data {
    char header[2];
    uint16_t framelength;
    uint16_t pm1_0;       // data 1
    uint16_t pm2_5;
    uint16_t pm10;
    uint16_t pm1_0_atm;   // data 4
    uint16_t pm2_5_atm;
    uint16_t pm10_atm;
    uint16_t nb_0_3;      // data 7
    uint16_t nb_0_5;
    uint16_t nb_1;;
    uint16_t nb_2_5;
    uint16_t nb_5;
    uint16_t nb_10;       // data 12
    uint16_t reserved;
    uint16_t checksum;
};

All there is left to do is the calculation of the checksum and this is pretty simple. We must add all entries in the raw data structure except of the reserved bytes and the checksum itself. Then we compare the result with the checksum entry of the pms5003Data structure.

Here are the methods (functions) of the PMS5003 class:

uint8_t      *readRaw();                 // reads the raw values
void         printRaw(uint8_t *);        // prints them
pms5003Data  *evaluate(uint8_t *);       // assembles the data words into a pms5003Data structure and returns a pointer to it
void         printMsg(pms5003Data *);    // prints the pms5003Data structure
int          calcChecksum(uint8_t *);    // calculates the checksum
int          readChecksum(uint8_t *);    // retrieves the checksum from the protocol
bool         verifyChecksum(uint8_t *);  // returns true is the checksums match, false otherwise
pms5003Data  readMeas();                 // combines readRaw, evaluate, verifyChecksum and returns a filled result pms5003Data structure

readRaw() reads the PMS5003 protocol and returns a pointer to the 32 byte values printRaw() takes a pointer to the raw data and prints the inn hex evaluate() takes a pointer to the raw data and returns a pointer to a filled pms5003Data structure calcChecksum() takes a pointer to the raw data and calculates the checksum readChecksum() reads the checksum from the pms5003Data structure verifyChecksum returns true if the calculated and the read checksums match readMeas() reads the raw data, verifies the checksum, extracts the data and returns a filled pms5003Data structure

The easiest way to get at the data is therefore

#include <PMS5003.h>
pms5003 = PMS5003();
void setup() {
    if (!setup)
        delay(10);
}

void loop() {
    pms5003Data results;
    results = pms5003.readMeas();
    Serial.print("pm 2.5 concentration: ");
    Serial.println(results.pm2_5);
}
⚠️ **GitHub.com Fallback** ⚠️