logo elektroda
logo elektroda
X
logo elektroda

How do I interface with the SPS30 PM1.0/PM2.5/PM4/PM10 dust/air quality sensor via UART using an ESP

p.kaczmarek2  3 1170 Cool? (+6)
📢 Listen (AI voice):

TL;DR

  • ESP32 interfacing with the Sensirion SPS30 particulate matter sensor over UART, reading PM1.0, PM2.5, PM4.0, PM10, particle counts, and typical particle size.
  • It first uses Sensirion's arduino-uart-sps30 library, then builds a standalone C/C++ SHDLC driver that handles frame parsing, byte-stuffing, checksums, and IEEE-754 floats.
  • The sensor runs at 4.5 to 5.5 V, draws up to 80 mA in measurement mode, and returns measurements no faster than once per second.
  • In clean indoor air, readings stayed in single-digit μg/m³, while spraying aerosol disinfectant nearby produced an immediate spike on the Python PyQt6/PyQtGraph chart.
  • Powering the SPS30 at 3.3 V produced wildly inflated nonsense values, even though the serial number still read correctly, making the fault easy to miss.
AI summary based on the discussion. May contain errors.

The SPS30 is an advanced particulate matter (PM) sensor from Sensirion that uses laser scattering technology to precisely measure PM1.0, PM2.5, PM4.0 and PM10. Unlike cheaper alternatives, such as the PMS5003 or SDS011, the SPS30 offers exceptional longevity (over 10 years of continuous operation) thanks to its integrated fan self-cleaning function and sealed, dirt-resistant optical design. It provides detailed data on both mass concentration (in μg/m³) and particle number concentration (in particles per cm³), as well as an estimate of the typical particle size.

It can communicate with microcontrollers via both the I2C bus and a standard UART serial port. The following presentation will focus on communication via the UART interface using the SHDLC protocol, first demonstrating support via a ready-made library, and then creating a minimalist, fully independent driver from scratch in pure C/C++.

First, however, here is some information from the datasheet. The SPS30 operates at a voltage of 4.5 to 5.5 V, drawing up to 80 mA in measurement mode and up to 360 µA in idle mode; there is also a sleep mode option, with a current draw of less than 50 µA.

Its dimensions are 41 x 41 x 12 mm, it weighs 26 g, and the pinout is shown in the diagram:

The SEL pin is used to select the operating mode; by default (when no signal is connected), UART is selected.

We still need to consider the price of this sensor – there may be quite a surprise here. On Polish websites, I’ve even seen it for as much as 200–300 zł. From China, however, you can import it for as little as 50 zł. Still expensive, but four times better than here.

Environment used
I implemented the project in the PlatformIO environment, which makes managing libraries and code much easier compared to the standard Arduino IDE. I have already described it in several of my other posts, including in the PCF8574 presentation .

On the hardware side, I used the cheapest ESP32 board – the Devkit V1 Type C:



Starting point – Hello World
It is always best to start with a tried-and-tested, working "Hello World" code to ensure that our board is communicating correctly with the computer. It is also useful to have an LED flashing to indicate that the code is running, and to send some data via UART to help diagnose the programme.
Code: C / C++
Log in, to see the code

From this point, you can proceed to running the sensor.


Integration via the Sensirion UART SPS30 library
There are at least several different libraries available online for this sensor, but in this project I have focused on the official library provided by the manufacturer:
https://github.com/Sensirion/arduino-uart-sps30
It offers full support communication and relieves us of the burden of manually parsing and checking checksums for SHDLC frames.

First, we add it to the PIO – either via Libraries or manually in platformio.ini; the IDE will download it automatically:
Code: Ini
Log in, to see the code

Using this library to control the sensor is very straightforward. You must first initialise the serial interface (in this case, I connected the sensor to pins 22 and 23), link the sensor object to it, and then call the startMeasurement() function.

Below is the code that initialises the system, reads the serial number and, in a loop, retrieves pollution readings:
Code: C / C++
Log in, to see the code

Here is a screenshot from the tests:

If everything has been connected correctly, the readings will be accurate. In a typical, clean domestic environment, low dust concentrations in the order of single micrograms (μg/m³) can be expected, as shown in the log below:
                      
Device Type: 00080000 | Serial: A311420AFB1497CD

Mass Concentration [ug/m3]:
  PM1.0:  3.96                         
  PM2.5:  4.54                              
  PM4.0:  4.83                                         
  PM10.0: 4.98
                                                                
Number Concentration [#/cm3]:
  PM0.5:  25.52                                   
  PM1.0:  31.05                                   
  PM2.5:  31.53                                   
  PM4.0:  31.61                                   
  PM10.0: 31.63
                                                               
Typical Particle Size: 0.563 um

However, an interesting situation arises when I powered the sensor with 3.3V (instead of the required 5V). In that case, the readings become completely distorted and show astronomically inflated, rubbish values:

Device Type: 00080000 | Serial: A311420AFB1497CD
                               
Mass Concentration [ug/m3]:      
  PM1.0:  13149.68              
  PM2.5:  57284.13                            
  PM4.0:  93137.79                           
  PM10.0: 111020.64
                                                           
Number Concentration [#/cm3]:
  PM0.5:  0.00                             
  PM1.0:  53360.93                         
  PM2.5:  97958.79                        
  PM4.0:  106490.73                       
  PM10.0: 108235.54
                                                           
Typical Particle Size: 1.593 um

Despite this, the device’s serial number is read correctly. This can be a major pitfall for beginners – because the partially correct reading lulls one into a false sense of security, even though the data is nonsense.

Chart and Python
To better visualise how the sensor works, I have prepared a simple Python script using the PyQt6 and PyQtGraph libraries. To use it, we need to ensure that the ESP32 sends data in the form of a compact CSV stream. The main loop code (still using the ready-made library) has been simplified to the format
DATA:val1,val2...
:
Code: C / C++
Log in, to see the code

I tried to speed up the readings, but the SPS30 wouldn’t give me measurements any faster than once per second. The measurements are then sent to the serial port. The script on the computer listens on this port, processes the line and draws a nice graph.
(The Python code is in the attachment at the very bottom of the thread).

A typical reading from clean air looks like this:

When, as part of an experiment, I sprayed an aerosol disinfectant nearby (the sprayed droplets are also ‘particles’ for the sensor), the graph immediately reacted with a huge spike (note the scale of the Y-axis):




SPS30 protocol
Based on UART, the SPS30 uses an interesting SHDLC protocol – UART acts as the byte carrier here, whilst the SPS30 operates at a higher layer and is responsible for frame exchange; it is based on a master/slave architecture. The SPS30 acts as a slave device here. Each transfer is initiated by the master sending a request frame. The sensor responds to the request frame with a slave response.

The frames are appropriately marked. The 0x7E character is sent at the start and end of the frame to signal its start and stop. This means that, necessarily, if this byte (0x7E) occurs anywhere else in the frame, it must be replaced by two other bytes (byte-stuffing). This also applies to the characters 0x7D, 0x11 and 0x13.

For example: Data to be sent = [0x43, 0x11, 0x7F] → Data sent = [0x43, 0x7D, 0x31, 0x7F].

Additionally, frames contain a checksum. This allows errors to be detected. The checksum is generated before byte-stuffing and verified after the stuffed bytes have been removed from the frame. The checksum is defined as follows:
1. Sum all bytes between the start and stop (excluding the start and stop bytes).
2. Take the least significant byte of the result and invert it. This will be the checksum.
If the checksum from the packet does not match the calculated one, it is clear that interference has occurred and something in the frame is wrong; such a frame can be silently discarded, ensuring that erroneous measurements do not reach the user interface.

The table below provides an overview of the available SHDLC commands.
SHDLC command table for SPS30 showing CMD codes, actions, response times, and required firmware
The commands allow you to manage the measurement status, read values, put the sensor to sleep and wake it up, and even retrieve information about its versions and clean the internal fan.

Before we move on to writing our own code, it is worth knowing the format in which the sensor returns results. According to the documentation, floating-point numbers are transmitted in the IEEE-754 standard (most significant byte first – Big-Endian):
SPS30 datasheet excerpt showing Start Measurement table and MOSI/MISO frame examples

When we call the command to read the measurements, we receive a 40-byte data frame (which corresponds exactly to ten 4-byte
float
values). Their order in the data packet is shown in the table below:
SPS30 datasheet excerpt: “Read Measured Values” command 0x03 with an example MOSI frame

Armed with all this knowledge, we can proceed to create a driver that is completely independent of external libraries. Below is the full code for the ESP32, which independently builds and validates SHDLC frames, decodes ‘byte-stuffing’ characters, and converts bytes to floating-point numbers:
Code: C / C++
Log in, to see the code

The code above produces identical results to the library used previously.

Summary
The SPS30 has proved to be a very promising and, at the same time, easy-to-set-up sensor. It is not the cheapest of gadgets, but it may be worth buying. You just need to consider which shop to choose – as you can see, prices vary significantly. The SPS30 also features an internal self-cleaning system, which ensures trouble-free operation over the long term. The manufacturer’s library works very well and significantly speeds up the setup process. However, as I later demonstrated, creating your own ‘lightweight’ driver in pure C based on the official SHDLC protocol specification is also not difficult, and it gives you full control whilst making the project independent of external libraries. Sensirion’s documentation itself deserves a huge plus – I was pleasantly surprised to find that the manufacturer provides ready-made, byte-by-byte examples of packets for each command type. This makes it much easier to implement your own solution and diagnose errors in frames at the data level.
Have you used the SPS30 yet, and if so, in what projects? What applications do you see for this sensor?
Attachments:
  • realtime_plot.zip (4.22 KB) You must be logged in to download this attachment.

About Author
p.kaczmarek2
p.kaczmarek2 wrote 14748 posts with rating 12848 , helped 659 times. Been with us since 2014 year.

Comments

austin007 13 Jun 2026 07:41

Have you checked whether deep sleep is working properly? Does it reach the nominal value or lower (and) than in the note? If not, I recommend it. [Read more]

aadeer 14 Jun 2026 16:02

Did you buy the one taken from that ‘supervisor’ module fitted in hire cars? ;) [Read more]

p.kaczmarek2 15 Jun 2026 21:05

@austin007 Good idea, I’ll give it a go. Combined with a sleep mode on, say, an ESP32, it could be a fully battery-powered sensor... @aadeer it’s obviously this one recovered from a Bosch IVS SLIM... [Read more]

FAQ

TL;DR: For ESP32 users, power the SPS30 at 5 V and use UART at 115200 baud with SEL left unconnected; otherwise you may get serial access but "rubbish values" for PM1.0-PM10. This FAQ shows the correct wiring, PlatformIO setup, official Sensirion library flow, and a minimal SHDLC driver for reliable particulate readings and live CSV plotting. [#21919183]

Why it matters: The thread shows a real failure mode that looks half-correct in hardware tests: the sensor can return a valid serial number while its air-quality measurements are completely wrong.

Sensor Position in the thread Long-term detail mentioned Price note mentioned
SPS30 Advanced reference option Over 10 years continuous operation; self-cleaning fan Approx. 200-300 zł locally, about 50 zł from China
PMS5003 Cheaper alternative No long-life feature highlighted Mentioned only as cheaper
SDS011 Cheaper alternative No long-life feature highlighted Mentioned only as cheaper

Key insight: Correct power matters more than a superficially successful UART link. If the SPS30 is not fed its required 5 V, you can still read identification data yet get grossly inflated PM values instead of usable measurements.

Quick Facts

  • The SPS30 measures PM1.0, PM2.5, PM4.0, and PM10 by laser scattering, and it also reports particle number concentration in #/cm³ plus a typical particle size estimate in µm. [#21919183]
  • Electrical limits in the thread are specific: operating voltage is 4.5-5.5 V, current draw is up to 80 mA in measurement mode, up to 360 µA in idle, and under 50 µA in sleep. [#21919183]
  • The module dimensions are 41 × 41 × 12 mm and mass is 26 g, which makes it compact enough for fixed indoor monitors and portable logging builds. [#21919183]
  • The UART demo uses an ESP32 Devkit V1 Type C, Serial1 at 115200, and GPIO 22/23 for the sensor serial link in PlatformIO. [#21919183]
  • The SPS30’s UART protocol returns a 40-byte measurement payload, which maps directly to 10 big-endian IEEE-754 float values. [#21919183]

How do I connect a Sensirion SPS30 dust sensor to an ESP32 over UART, including the correct RX, TX, power, and SEL pin setup?

Connect the SPS30 to the ESP32 over UART, power it from 5 V, and leave SEL unconnected for UART mode. In the thread, the ESP32 uses GPIO 22 as RX and GPIO 23 as TX on Serial1 at 115200 baud. The sensor’s allowed supply range is 4.5-5.5 V, so 3.3 V is not valid for normal measurements. A practical setup is: 1. connect 5 V and GND, 2. cross RX/TX between sensor and ESP32 UART, 3. keep SEL floating unless you want to force another interface. [#21919183]

What is the SHDLC protocol used by the SPS30, and how does it work over a standard UART serial link?

SHDLC is the SPS30’s framed command protocol that runs on top of a normal UART byte stream. "SHDLC is a serial framing protocol that wraps UART bytes into master/slave packets, adds byte-stuffing, and verifies integrity with a checksum." In this design, the ESP32 acts as the master and the SPS30 acts as the slave. Each exchange starts with a request frame from the master, and the sensor returns a response frame. The frame uses 0x7E as both start and stop marker, so the sensor needs escaping rules and checksum validation. [#21919183]

How can I read PM1.0, PM2.5, PM4.0, and PM10 values from the SPS30 on an ESP32 using the official Sensirion UART SPS30 library?

Use the official Sensirion UART library, bind it to Serial1, start measurement, and call the float read function. The thread shows this flow: 1. Serial1.begin(115200, SERIAL_8N1, 22, 23), 2. sensor.begin(Serial1) and sensor.startMeasurement(...), 3. sensor.readMeasurementValuesFloat(...) inside loop(). That returns 10 float values, including the four PM mass concentrations. The example also reads the device serial number and product type first, which helps confirm communication before you trust the measurements. [#21919183]

Why does the SPS30 return wildly inflated particulate readings when powered from 3.3V instead of the required 5V?

It returns nonsense because the SPS30 is specified for 4.5-5.5 V, not 3.3 V, and the thread shows real bad data under undervoltage. With 3.3 V applied, the author still read the serial number correctly but saw impossible mass readings such as PM1.0 = 13149.68 µg/m³ and PM10 = 111020.64 µg/m³ in ordinary conditions. That partial success is the trap: identification traffic may work while measurement electronics do not. Treat valid ID plus absurd PM values as a power-supply fault first. [#21919183]

What is byte-stuffing in SPS30 SHDLC frames, and why are bytes like 0x7E, 0x7D, 0x11, and 0x13 escaped?

Byte-stuffing prevents control bytes from being mistaken for frame boundaries or special flow markers. In SPS30 SHDLC, 0x7E marks frame start and stop, so a literal 0x7E inside data must be replaced. The same applies to 0x7D, 0x11, and 0x13. The thread gives a concrete example: data [0x43, 0x11, 0x7F] is transmitted as [0x43, 0x7D, 0x31, 0x7F]. On receive, your code must undo that escaping before checksum verification and payload parsing. [#21919183]

How do I build a minimal custom SPS30 driver in pure C or C++ without using the Sensirion Arduino library?

Build it by implementing SHDLC frame send, receive, unstuffing, checksum validation, and float decoding yourself. The thread’s custom driver does exactly that with functions such as sendShdlcFrame, receiveShdlcFrame, readUnstuffedByte, and bytesToFloat. It sends command 0x00 to start measurement, 0x03 to read values, and 0xD0 with a subcommand to fetch strings like serial number. The code uses Serial1 on pins 22/23, verifies the returned command byte, checks the stop byte 0x7E, and rejects checksum or state errors before printing data. [#21919183]

Which measurements does the SPS30 actually provide besides PM mass concentration, such as particle number concentration and typical particle size?

It provides more than PM mass concentration. Along with PM1.0, PM2.5, PM4.0, and PM10 in µg/m³, the SPS30 also reports particle number concentration for PM0.5, PM1.0, PM2.5, PM4.0, and PM10 in #/cm³, plus a typical particle size in µm. The thread’s clean-air log shows a typical particle size of 0.563 µm and number concentration values such as 31.63 #/cm³ for PM10. That broader output is useful when you want trend detail, not just one air-quality headline number. [#21919183]

SPS30 vs PMS5003 vs SDS011 — which particulate sensor is better for long-term air quality monitoring with an ESP32?

The thread presents the SPS30 as the stronger long-term option. It explicitly contrasts the SPS30 with cheaper PMS5003 and SDS011 modules, then highlights the SPS30’s over 10 years of continuous operation, internal fan self-cleaning, and sealed dirt-resistant optical design. That makes it the better fit when you want a durable ESP32 air monitor and lower maintenance. The trade-off is cost: the thread mentions 200-300 zł on some local Polish listings versus about 50 zł from China. [#21919183]

What is the correct checksum calculation for SPS30 SHDLC UART packets, and how should checksum errors be handled?

Calculate the checksum by summing all bytes between the start and stop markers, taking the least significant byte, and inverting it. The thread states that you compute this before byte-stuffing on transmit and verify it after unstuffing on receive. The custom driver compares the received checksum with ~checksum and returns an error if they differ. The safest handling is to discard the entire frame silently, because a checksum mismatch means interference or corruption and you should not pass bad PM data to the user interface. [#21919183]

How do I decode the 40-byte SPS30 measurement response into 10 IEEE-754 big-endian float values?

Split the 40-byte payload into 10 groups of 4 bytes and decode each group as a big-endian IEEE-754 float. The thread’s helper builds a uint32_t from bytes [0] << 24, [1] << 16, [2] << 8, and [3], then copies that bit pattern into a float. That order matters because the SPS30 sends the most significant byte first. In the custom loop, the code walks through rxBuf[i * 4] for i = 0..9, producing the four mass concentrations, five number concentrations, and the typical particle size. [#21919183]

Why does the SPS30 seem limited to about one new reading per second even if my ESP32 polls it faster?

It seems limited because the sensor itself does not deliver fresh values faster than about once per second in this setup. The author reduced the ESP32 loop delay to 1 ms for CSV streaming, but still reports that the SPS30 would not provide measurements faster than once per second. Faster polling can still read the interface, but not produce genuinely newer particulate data. Design your logger and plots around roughly 1 Hz updates unless your own tests show a different sensor-side cadence. [#21919183]

What should I put in platformio.ini to use the official Sensirion UART SPS30 library with an ESP32 Devkit V1 in PlatformIO?

Use an ESP32 Arduino environment and add the official library under lib_deps. The thread’s platformio.ini contains: platform = espressif32, board = esp32dev, framework = arduino, monitor_speed = 115200, and lib_deps = sensirion/Sensirion UART SPS30. That is the minimum configuration shown for an ESP32 Devkit V1 project in PlatformIO. After that, PlatformIO downloads the dependency automatically and you can call #include <SensirionUartSps30.h> in your source. [#21919183]

How can I stream SPS30 readings as CSV from an ESP32 and plot them live in Python with PyQt6 and PyQtGraph?

Print the measurements as one compact CSV line per sample, then let Python read the serial port and graph each line. The thread changes the ESP32 loop to emit DATA:val1,val2,... using Serial.printf, including 10 numeric values and a newline. On the PC side, the supplied Python script uses PyQt6 and PyQtGraph to parse those lines and draw live charts. The author shows both a low baseline in clean air and a strong spike after spraying an aerosol disinfectant nearby. [#21919183]

What troubleshooting steps help when an SPS30 serial number reads correctly but the measurement data is clearly nonsense?

Check power first, then wiring, then trust only plausible readings. The thread proves that a valid serial number does not guarantee valid measurement data: the serial number read correctly even when the sensor was wrongly powered from 3.3 V. Use this order: 1. verify the supply is 5 V within the 4.5-5.5 V range, 2. confirm RX/TX pins and 115200 UART settings, 3. compare the PM results against realistic indoor values such as single-digit µg/m³. If IDs look fine but PM values are astronomical, treat the readings as invalid. [#21919183]

What projects and practical applications is the SPS30 best suited for, from home air quality monitoring to aerosol detection?

It suits indoor air-quality monitors, particulate loggers, and experiments that need a fast visual response to airborne particles. The thread shows normal home readings in the low single-digit µg/m³ range, then demonstrates a clear spike when aerosol disinfectant was sprayed nearby. That makes the SPS30 useful for room monitoring, ventilation experiments, pollution trend logging, and aerosol event detection. Its self-cleaning fan and stated 10+ year continuous life also make it attractive for longer-running fixed installations with an ESP32. [#21919183]
AI summary based on the discussion. May contain errors.
%}