Particle sensors (Part 1)

Part 1
I am amazed by the massive improvements of sensors along the last 10 years. Not so long ago, some sensors would fit in a shoe box and require 24 VDC, not to talk about the interfaces, weight, etc. These improvements are mainly due to the incorporation of advanced sensors in smart phones: camera, accelerometers, gyroscope, light sensors, gps, touch sensors, etc. Theses sensors come tiny, 3V, ULP, featuring digital interfaces and they come cheap too. Too bad for the pressure sensors, most industrial accelerometers: as there is no application yet for smart phone, the are still big, greedy, analog and expensive !

However, most sensor manufacturers try their best to offer new types of sensors. Some of them feature some well known components. The power of theses sensors lies in the embedded intelligence, the DSP inside which perform awesome calculations at high speeds. Among these sensors, I was intrigued by the particle sensor from Honeywell, namely the HPMA115S0-XXX, HPM Series Particle Sensor. I got one sample from Honeywell and gave this literally black box a try. The datasheet contains a very limited set of information, enough to make it run but probably insufficient for designing an industrial product.

The principle of operation is rather simple. A fan located on one exit of a S shaped channel creates a gentle air stream which carries particles. A light beam is directed across the air stream path while a photo-diode collects the photons. The signal of this photo-diode is computed locally my a MCU (FPGA ?) and outputs a serial signal on a UART port. This is how it looks really

Important: As the sensor features a fan, it requires a 5 VDC supply (<80 mA peak). However the UART port manages 3 V signals. Although the serial port is 5V tolerant, my advise is to use a bidirectional level translator such as this one:

Some code is available from Github, but, sorry, I did not like it. So I decided to rewrite it from scratch. Firstly, I decided to get rid of the continuous reading mode as it outputs large frames of mostly empty data. I understand that they made room for data that will be issued along with future development of the product. So far, I will skip this option. The code mainly consists in a command sender and a response reader. Both types of command are made flexible in order to match with commands requiring arguments or not as well as responses holding data or acknowledgment bytes. Next is the main command sender function. Use it for commands which require an argument.

/*
Send command with argument
*/
void sendCommand(uint8_t cmdType, uint8_t *vData, uint8_t dataSize)
{
	uint8_t vBytes[8];
	const uint8_t dataOffset = 3;
	/* Header in first place */
	vBytes[0] = 0x68; 
	/* Length in second place, equals (data size + 1) */
	vBytes[1] = (dataSize + 1);
	/* Command type in third place */
	vBytes[2] = cmdType;
	/* Data if any */
	for (uint8_t i = 0; i < dataSize; i++)
	{
		vBytes[dataOffset + i] = vData[i];
	}	
	/* Compute and record checksum */
	vBytes[dataOffset + dataSize] = checkSum(vBytes, (dataSize + 3));
	/* Send array of bytes */
	for (uint8_t i = 0; i < (dataSize + 4); i++)
	{
		mySerial.write(vBytes[i]);
	}
}

Each byte is buffered in order to compute the check sum according to their respective values. Next is the overloaded function that you will use for (most) commands which do not require an argument:

/*
Send command without argument
*/
void sendCommand(uint8_t cmdType)
{
	uint8_t *mock;
	sendCommand(cmdType, mock, 0);
}

Plain easy. Reading responses is a little bit more tricky as shown below

/*
Read response from device 
*/
bool readResponse(data_t *data)
{
	/* Set default response */
	bool res = false;
	/* Receiving buffer */
	uint8_t vBuffer[8];
	uint8_t ptr = 0; 
	/* Set default length to its maximum (data_size + 1) */
	uint8_t length = 5;
	uint8_t cmdType = 0;
	/* States */
	bool eofd = false;
	bool sofd = false;
	/* Read incoming bytes */
	if (mySerial.available())
	{
		while (mySerial.available())
		{
			/* Read and convert character */
			uint8_t readByte = (uint8_t)mySerial.read();
			if (!sofd)
			{
				switch (readByte)
				{
					case 0x40:
						/* Data header */
						sofd = true;
						break;
					case 0x96:
						/* Negative acknowledgment */
						sofd = true;
						eofd = true;
						res = false;
						break;
					case 0xA5:
						/* Positive acknowledgment */
						sofd = true;
						eofd = true;
						res = true;
						break;
				}
			}
			if (sofd && !eofd)
			{
				/* Record byte */
				vBuffer[ptr] = readByte; 	
				if (ptr == 1)
				/* Length in second place */
				{
					length = readByte;	
				}
				else if (ptr == 2)
				/* Command type in third place */
				{
					cmdType = readByte;
				}
				else if (ptr == (length + 2))
				/* End of frame */
				{
					eofd = true;							
					/* Check sum */
					if (vBuffer[length + 2] == checkSum(vBuffer, (length + 2)))
					{
						/* Compute data */
						switch (cmdType)
						{
							case cmdReadParticleMeasuringResults:
								data->pm_2_5 = ((uint16_t)vBuffer[3] << 8) | vBuffer[4];
								data->pm_10 = ((uint16_t)vBuffer[5] << 8) | vBuffer[6];	
								res = true;
								break;
							case cmdReadCustomerAdjustmentCoefficient:
								data->custAdjCoeff = ((uint16_t)vBuffer[3] << 8) | vBuffer[4];
								res = true;
								break;
						}
					}
				}
				/* Increment pointer */
				ptr += 1; 
			}
		}
	}
	/* Return result */
	return(res);
}

The function is built so that it will return a true state if every thing went right and vice versa. As the acknowledgment bytes are doubled, I simplified the code and check only the first incoming acknowledgment bytes (0xA5 = positive, 0x96 = negative). 0x40 is the first place announces data. All received bytes from a response containing data are buffered, once again for checking their sum. And here is the checksum function:

/* 
Compute the check sum from an array of bytes
the specified "bytes" number of bytes is used 
*/
uint8_t checkSum(uint8_t *vData, uint8_t bytes)
{
	/* Sum bytes */
	uint16_t sum = 0;
	for (uint8_t i = 0; i < bytes; i++)
	{
		sum += vData[i];
	}
	/* Compute the check sum */
	uint8_t check = uint8_t((0x10000 - sum) & 0xFF);
	/* Return checksum */
	return(check);
}

 

 

 

Leave a Reply

You must be logged in to post a comment.