The PlanTower PMS5003 Dust Sensor - uraich/IoT4AQ GitHub Wiki
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 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) |
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 |
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;
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.
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:
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
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);
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);
}