Interfacing the SPS30 particle sensor

Interfacing the SPS30 particle sensor

Sensirion recently released the SPS30, a particulate matter sensor with both a serial interface and an I2C interface. So, obviously, we didn't have to wait for long before someone asked us whether we could interface it with Yoctopuce modules.

The SPS30 particulate matter sensor from Sensirion has the shape of a small green box of about 4x4x1cm. It's aim is to count dust particulate matters in suspension in the air depending on their sizes. It is able to measure PM1, PM2.5, PM4, and PM10 particles, which respectively corresponds to particulate matters with a size below 1, 2.5, 4.0, and 10.0µm.

The Sensirion SPS30 sensor
The Sensirion SPS30 sensor

In theory, to interface this sensor, one only needs to use one of the corresponding electrical interface modules, that is a Yocto-Serial or a Yocto-I2C. Let's have a closer look at these options.

With a Yocto-Serial

You can interface the SPS30 with a TTL series link, which can be done with a Yocto-Serial. To do so, the sensor must be powered in 5V, the communication must occur at 115200 bauds, with a signal level at 3.3 or 5V.

Connections between the Yocto-Serial and the SPS30
Connections between the Yocto-Serial and the SPS30

Configuration of the Yocto-Serial to communicate with the SPS30
Configuration of the Yocto-Serial to communicate with the SPS30

The protocol is as follows:

  • Start the measures by sending the 7E0000020103F97E frame
  • Periodically query the sensor with the 7E000300FC7E frame
  • The sensor then answers with a frame with a somewhat specific structure

    • One byte = 0x7E;
    • The address of the sensor over one byte (always 0)
    • The (0x03) command
    • The length of the data (normally 40)
    • The data
    • Checksum over 1 byte
    • Final byte = 0x7E;

The data are a series of 10 encoded numbers in the big-endian float IEEE754 format, which means 4 bytes per float. This series respectively corresponds to the PM1.0, PM2.5, PM4.0, PM10 (in µg/m3), PM0.5, PM1.0, PM2.5, PM4.0 et PM10 (in count/m3) and typical size (in µm). However, before sending these data, the sensor has potentially performed some substitutions that you need to reverse before decoding the data.

  • 0x7E was replaced by 0x7D, 0x5E
  • 0x7D was replaced by 0x7D, 0x5D
  • 0x11 was replaced by 0x7D, 0x31
  • 0x13 was replaced by 0x7D, 0x33

Here is a Python example which configures a Yocto-Serial, queries the SPS30, and displays the answer.

# -*- coding: utf-8 -*-
import os, sys
from struct import *
from yocto_api import *
from yocto_serialport import *
from yocto_poweroutput import *

def decodeBlob(blob):
  # revert Sensirion's Byte-Stuffing
  blob =  blob.replace("7D5E","7E")
  blob =  blob.replace("7D31","11")
  blob =  blob.replace("7D33","13")
  blob =  blob.replace("7D5D","7D")
  # consistency checks
  if (len(blob) <16): return print("Frame too short")
  if (blob[:2] != "7E"):return  print("First byte is not 7E")
  if (blob[-2:] != "7E"):return print("Last byte is not 7E")
  size=  int(blob[8:10],16)
  if len(blob) != 2*(7 + size):return  print("invalid frame size")
  if (size!=40):return  print("suspicious data size")
  # at this point, blob data contains 10 hex-encoded floats
  # starting at character #10. note : 1 float = 4 bytes = 8 characters
  keys = ['MassPM1','MassPM2.5', 'MassPM4', 'MassPM10', 'CountPM0.5',
        'CountPM1', 'CountPM2.5' ,'CountPM4', 'CountPM10','TypSize']
  # lets use some python black magic to convert data into a dictionnary
  values = unpack('!ffffffffff', bytes.fromhex(blob[10:90]))
  return dict(zip(keys, values))

errmsg = YRefParam()
# connects through the virtualhub, so one can use it
# monitor communications
if YAPI.RegisterHub("", errmsg) != YAPI.SUCCESS:
    sys.exit("init error" + errmsg.value)

## assumes there is only one Yoctopuce serial port available
## otherwise one has to use logical names.
serial = YSerialPort.FirstSerialPort()
if serial is None :
     sys.exit("No Serial Port found")

# configure serial port an power supply
power = YPowerOutput.FindPowerOutput(serial.get_module().get_serialNumber()+".powerOutput");
serial.set_voltageLevel( YSerialPort.VOLTAGELEVEL_TTL3V)

serial.queryHex("7E0000020103F97E", 100) # start automatic  measurement
while True:
  YAPI.Sleep(1000,errmsg) # each measure takes ~ 1 sec
  blob = serial.queryHex("7E000300FC7E", 100) # ask or result
  data = decodeBlob(blob)  # decode result
  print(data) # tadaaa !

About jobs

Sensirion decided to use special bytes to delimit the frames of the protocol, which then forced them to perform substitutions to avoid that these special characters surface in the middle of frames. This specificity prevents us from writing an autonomous job which would run on the Yocto-Serial, because the job system doesn't allow us to define these substitutions. Modifying the firmware of the Yocto-Serial for it to be able to perform the substitutions wouldn't make much sense: the case is too specific.

With a Yocto-I2C

You can also interface the SPS30 with an I2C link, which falls under the jurisdiction of the Yocto-I2C. For this, the sensor must be powered in 5V, the communication must occur at 100kps. Signals can be in 3.3V or 5.5V. Note, the sensor apparently doesn't support the "Restart" condition.

Connections between the Yocto-I2C and the SPS30
Connections between the Yocto-I2C and the SPS30

Configuration of Yocto-I2C to communicated with the le SPS30
Configuration of Yocto-I2C to communicated with the le SPS30

To start the measures, you must send the sequence


Then, to read the measure, you must send the following sequence at a regular interval.


The sensor answers with a series of 10 single precision floating point numbers, which are consequently supposed to fit over 4 bytes each. Unfortunately, here as well, Sensirion has somewhat innovated: each of these numbers is encoded in the shape of two bytes, a CRC, two further bytes, and another CRC. Each float takes therefore 6 bytes, including a CRC byte in the middle. In the almost ten years we've been working with Sensirion sensors, we've noticed that they love to put CRCs all over the place, but frankly, this is bordering on the ridiculous.

Anyway, we modified the Yocto-I2C firmware (version 42159) so that it can read these Sensirion floats encoded over 6 bytes, that we called "FLOAT32S". This allows us to write an autonomous job to automatically read the sensor. The job is made of two tasks:

  • An initialization task (run only once)

    • assert ! isset($started)
    • writeLine {S}D200100300AC{P}
    • expect 69:{A}{A}{A}{A}{A}{A}
    • wait 2000
    • compute $started=1

  • A reading task (run once / second)

    • assert isset($started)
    • assert $started==1
    • writeLine {S}D20300{P}
    • expect 69:{A}{A}{A}
    • writeLine {S}D3xx{A}xx{A}xx{A}xx{A}xx{A}xx{A}xx{A}xx{A}xx{A}xx{A}xx{A}xx{N}{P}
    • expect 69:{A}($1:FLOAT32S)($2:FLOAT32S))

Just to remain somewhat readable, the reading task above maps only the first two values PM1.0, PM2.5 (in µg/m3) over "generic sensors" 1 and 2, but nothing prevents you to read the remainder. Here is for example a job file which reads and maps the first 9 values.


You can use a Yocto-Serial as well as a Yocto-I2C to interface a SPS30. However, some unfortunate choices made by Sensirion impose some limitations with a Yocto-Serial. Therefore, we rather recommend the use of a Yocto-I2C which allows you to use the SPS30 without writing a single line of code and also to display the data directly in Yocto-Visualization.

If you use a Yocto-I2C, you can see the data in Yocto-Visualization
If you use a Yocto-I2C, you can see the data in Yocto-Visualization

Add a comment No comment yet Back to blog

Yoctopuce, get your stuff connected.