Arduino Uno: PM2.5 sensing revisited

Teodor Costachioiu
Nov 6, 2018 · 7 min read

Today it’s finally the time to revisit the blog post on using the Honeywell HPMA115S0 particle sensor with an Arduino Uno board. The basics of the operation of the HPMA115S0 and a preliminary version of this code that works with Arduino Due are published in this blog post, so please take a few minutes and read it.

After publishing that blog post, so many of you have asked me to come with a way to make this sensor work with an Arduino Uno. It’s relatively easy to do it, but one must overcome two minor issues:

  • Albeit the HPMA115S0 is powered from a 5V supply, it uses 3.3V logic. To make it work with an Arduino Uno, one must use a logic level converter. In this blog post, I used one BOB-12009 from Sparkfun.
  • The Arduino Uno has a single serial port, which is commonly used to communicate with the PC. The only way to make the PM2.5 sensor work with the Arduino Uno is to use SoftwareSerial, with some minor changes in the code and some quirks that are explained below.

Connections and schematic

The wires used in the blog post to connect the sensor were all red colored. Getting the right way to connect things was a bit difficult for some people, so I tried to make things a bit easier this time.

In the above picture, we can see the sensor with its connector. The pin numbering is a bit odd, with PIN1 near the fan and PIN8 to the edge of the sensor. Only four pins are used in this project: Vcc, RX, TX, and GND.

The wires are still all red, but I’ve put some colored heat shrink tubing on some wires to increase visibility:

  • red is Vcc
  • yellow is TX — this goes to SoftwareSerial RX pin
  • blue is RX — this goes to SoftwareSerial TX pin
  • black is GND

The sensor is connected as follows:

Honeywell PM2.5 sensor Arduino Uno connections. Created with Fritzing.org

We can see that the logic level converter receives both 5V (red wire) and 3.3V (orange wire) from the Arduino board. GND is connected both to the PM2.5 sensor and to the logic level converter. The sensor is powered from 5V (red wire going to pin2 of the sensor).

Pin11 of the Arduino Uno is SoftwareSerial TX pin and will go to the HV4 pin of the logic level converter. The opposite LV4 pin goes to pin7 of the HPMA115S0 sensor,

Pin10 of the Arduino Uno is SoftwareSerial RX pin and will go to the HV3 pin of the logic level converter. The opposite LV3 pin goes to pin6 (TX) of the HPMA115S0 sensor.

Beloe there are some pictures of the sensor connected to an Arduino Uno:

Arduino Uno code

The code is a variation of the code in the original blog post, but now it uses Software Serial.

// The big change: we use software serial for H2S sensor
#include <SoftwareSerial.h>
// software serial #1: RX = digital pin 10, TX = digital pin 11
SoftwareSerial portOne(10, 11);

bool my_status;

// IMPORTANT!!! When working on an Arduino DUE,
// int is 32 bit (-2,147,483,648 to 2,147,483,647)
// For Arduino Uno int size is 8 bit, that is -32,768 to 32,767
// Use long or float if working with an Uno or simmilar 8-bit board
long PM25;
long PM10;

void setup() {
Serial.begin(9600);
while (!Serial);
Serial.println("Start!");
portOne.begin(9600);
//Just discard everything the sensor will send at the beginning
flush_portOne();

// Stop autosend
Serial.println("Stopping autosend...");
my_status = stop_autosend();
// Serial print is used just for debugging
// But one can design a more complex code if desired
Serial.print("Stop autosend status is ");
Serial.println(my_status, BIN);
Serial.println(" ");
delay(500);


// Start fan
Serial.println("Starting measurements...");
my_status = start_measurement();
// Serial print is used just for debugging
// But one can design a more complex code if desired
Serial.print("Start measurement status is ");
Serial.println(my_status, BIN);
Serial.println(" ");
delay(5000);
}


void loop() {
// Read the particle data every minute
my_status = read_measurement();
// Serial print is used just for debugging
// But one can design a more complex code if desired
Serial.print("Read measurement status is ");
Serial.println(my_status, BIN);
Serial.print("PM2.5 value is ");
Serial.println(PM25, DEC);
Serial.print("PM10 value is ");
Serial.println(PM10, DEC);
Serial.println(" ");
// Wait some time
delay(10000);
}

bool start_measurement(void)
{
// First, we send the command
byte start_measurement[] = {0x68, 0x01, 0x01, 0x96 };
portOne.write(start_measurement, sizeof(start_measurement));
//Then we wait for the response
while(portOne.available() < 2);
uint8_t read1 = portOne.read();
uint8_t read2 = portOne.read();
// Test the response
if ((read1 == 0xA5) && (read2 == 0xA5)){
// ACK
return 1;
}
else if ((read1 == 0x96) && (read2 == 0x96))
{
// NACK
return 0;
}
else return 0;
}

bool stop_measurement(void)
{
// First, we send the command
byte stop_measurement[] = {0x68, 0x01, 0x02, 0x95 };
portOne.write(stop_measurement, sizeof(stop_measurement));
//Then we wait for the response
while(portOne.available() < 2);
uint8_t read1 = portOne.read();
uint8_t read2 = portOne.read();
// Test the response
if ((read1 == 0xA5) && (read2 == 0xA5)){
// ACK
return 1;
}
else if ((read1 == 0x96) && (read2 == 0x96))
{
// NACK
return 0;
}
else return 0;
}

bool read_measurement (void)
{
// Send the command 0x68 0x01 0x04 0x93
byte read_particle[] = {0x68, 0x01, 0x04, 0x93 };
portOne.write(read_particle, sizeof(read_particle));
// A measurement can return 0X9696 for NACK
// Or can return eight bytes if successful
// We wait for the first two bytes
while(portOne.available() < 1);
byte HEAD = portOne.read();
while(portOne.available() < 1);
byte LEN = portOne.read();
// Test the response
if ((HEAD == 0x96) && (LEN == 0x96)){
// NACK
Serial.println("NACK");
return 0;
}
else if ((HEAD == 0x40) && (LEN == 0x05))
{
// The measuremet is valid, read the rest of the data
// wait for the next byte
while(portOne.available() < 1);
byte COMD = portOne.read();
while(portOne.available() < 1);
byte DF1 = portOne.read();
while(portOne.available() < 1);
byte DF2 = portOne.read();
while(portOne.available() < 1);
byte DF3 = portOne.read();
while(portOne.available() < 1);
byte DF4 = portOne.read();
while(portOne.available() < 1);
byte CS = portOne.read();
// Now we shall verify the checksum
if (((0x10000 - HEAD - LEN - COMD - DF1 - DF2 - DF3 - DF4) % 0XFF) != CS){
Serial.println("Checksum fail");
return 0;
}
else
{
// Checksum OK, we compute PM2.5 and PM10 values
PM25 = DF1 * 256 + DF2;
PM10 = DF3 * 256 + DF4;
return 1;
}
}
}

bool stop_autosend(void)
{
// Stop auto send
byte stop_autosend[] = {0x68, 0x01, 0x20, 0x77 };
portOne.write(stop_autosend, sizeof(stop_autosend));
//Then we wait for the response
while(portOne.available() < 2);
uint8_t read1 = portOne.read();
uint8_t read2 = portOne.read();
// Test the response
if ((read1 == 0xA5) && (read2 == 0xA5)){
// ACK
return 1;
}
else if ((read1 == 0x96) && (read2 == 0x96))
{
// NACK
return 0;
}
else return 0;
}

bool start_autosend(void)
{
// Start auto send
byte start_autosend[] = {0x68, 0x01, 0x40, 0x57 };
portOne.write(start_autosend, sizeof(start_autosend));
//Then we wait for the response
while(portOne.available() < 2);
uint8_t read1 = portOne.read();
uint8_t read2 = portOne.read();
// Test the response
if ((read1 == 0xA5) && (read2 == 0xA5)){
// ACK
return 1;
}
else if ((read1 == 0x96) && (read2 == 0x96))
{
// NACK
return 0;
}
else return 0;
}

void flush_portOne(void){
uint8_t inchar;
uint8_t count = 0;
bool is_timeout = 0;
// We set a 10 second timeout;
unsigned long timeout = millis() + 10000;
Serial.println ("Waiting for the first autosend.");
while(portOne.available() < 1){
if (millis() > timeout){
Serial.println("Timeout");
break;
}
}
// Do we have data in the serial buffer?
// If so, flush it
if (portOne.available() > 0){
Serial.println ("Flushing buffer...");
// A data frame is 32 bytes
while(count < 32){
inchar = portOne.read();
count++;
delay(10);
Serial.print(inchar, HEX);
}
Serial.println("");
}
}

One might observe the change in the flush_portOne routine, which now expects an array of 32 bytes. As the sensor doesn’t issue any string terminator. A 10-second timeout was also implemented, unlike in the original code. This routine is required as the sensor starts in auto-send mode, and we want to put in in manual mode.

Even with those changes, there are some minor quirks and unsolved things. In particular, when the reset button of the Arduino Uno is pressed, the sensor becomes unresponsive. I assume this has something to do with the way SoftwareSerial initializes, as this won’t happen with hardware serial. The only way to make the sensor work is to briefly remove 5V power from the sensor and reapply it before the timeout expires.

[Update March 19, 2019] I wrote a code library for the sensor, you can download it from https://github.com/Electronza/HPMA115S0.


Originally published at https://electronza.com on November 6, 2018. Moved to Medium on MAy 3, 2020.

Electronza

DIY electronics projects and more

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store