Sensirion SPS30 dust sensor: I2C comms

I’m attempting to use the Sensirion SPS30 with a Boron and the library (GitHub - paulvha/sps30: Sensirion SPS30 driver for ESP32, SODAQ, MEGA2560, UNO, ESP8266, Particle-photon on UART OR I2C coummunication) from @paulvha (thank you @paulvha - I would never have made any progress without this library!).

The library supports serial and I2C comms. I’ve got serial comms working fine, but I’d prefer to use I2C (so I can keep the serial port available for debugging). By default, the I2C buffer is only 32 bytes, which isn’t large enough to receive all of the data from the SPS30. The examples in the library helpfully provide a warning about this.

I understand that it’s possible to increase the buffer size using acquireWireBuffer (Particle acquireWireBuffer - Wire (I2C)), and I have added the example code from the documentation into one of the library’s examples immediately above setup(). However, I’m still not getting the full data from the sensor.

Clearly I’m missing something, but I’m afraid I’m right at the edge of my coding ability and I don’t know where to start looking. I’m hoping that @paulvha, or someone else who’s used this sensor/library, will be able to chip in and give me a nudge in the right direction.

Thanks!

HI

While acquireWireBuffer will extend the I2C buffer it will NOT impact the ‘auto’ detection of the buffer size.

So make sure to add extending the buffer size with acquireWireBuffer in setup() as shown in the provided link.

Now in SPS30.h , around line 240 change #define I2C_LENGTH 32 to #define I2C_LENGTH 512 (if that is the new I2C_BUFFER_SIZE). You actually only need at least 64 bytes.

I expect this will solve your issue.

regards,
Paul

Hi Paul (@paulvha )

Thanks for that. I’ve now got it working :smiley:

I changed the #define I2C_LENGTH in SPS30.h, as you suggested, but this didn’t work. I think the length was being changed in the following lines designed for particular hardware (not Particle devices). Commenting these out fixed the problem.

For the benefit of others, the complete solution is:

Add the following declaration and function outside of setup/loop:

constexpr size_t I2C_BUFFER_SIZE = 64;

HAL_I2C_Config acquireWireBuffer() {
       HAL_I2C_Config config = {
        .size = sizeof(HAL_I2C_Config),
        .version = HAL_I2C_CONFIG_VERSION_1,
        .rx_buffer = new (std::nothrow) uint8_t[I2C_BUFFER_SIZE],
        .rx_buffer_size = I2C_BUFFER_SIZE,
        .tx_buffer = new (std::nothrow) uint8_t[I2C_BUFFER_SIZE],
        .tx_buffer_size = I2C_BUFFER_SIZE
    };
    return config;
}

Add acquireWireBuffer(); to setup()

In SPS30.h, within #if defined INCLUDE_I2C, change the definition of I2C_LENGTH to #define I2C_LENGTH 64 and comment out the remaining lines before #endif // INCLUDE_I2C

Thanks again @paulvha for contributing this library. We’re planning to install a network of low cost sensors in Kenya as part of a research project to monitor spatial patterns of atmospheric dust. Atmospheric dust has significant health impacts in that part of the World.

HI David,

I have tried it on my Argon and with the following changes it works. You have to make sure you SET the new structure as well else you might get memory violations / buffer overrun.

Sketch changes

Globally define:

constexpr size_t I2C_BUFFER_SIZE_N = 128;

 hal_i2c_config_t config = {
        .size = sizeof(hal_i2c_config_t),
        .version = HAL_I2C_CONFIG_VERSION_1,
        .rx_buffer = new (std::nothrow) uint8_t[I2C_BUFFER_SIZE_N],
        .rx_buffer_size = I2C_BUFFER_SIZE_N,
        .tx_buffer = new (std::nothrow) uint8_t[I2C_BUFFER_SIZE_N],
        .tx_buffer_size = I2C_BUFFER_SIZE_N
};

Now you have a structure to upload.

In setup BEFORE SP30_COMMS.begin();
hal_i2c_init(HAL_I2C_INTERFACE1, &config);

This will set the new structure, assuming you use WIRE. If you use WIRE1 then it should be HAL_I2C_INTERFACE2

sps30.h

Change line 240 to
#define I2C_LENGTH 128

As you pointed out you have change else I2C_BUFFER_LENGTH will set it back
comment out lines 247 to 250

    //#if defined I2C_BUFFER_LENGTH       // ESP32
    //    #undef  I2C_LENGTH
    //    #define I2C_LENGTH  I2C_BUFFER_LENGTH
    //#endif

regards,
Paul

1 Like

Thanks Paul. I confirm this works on my Boron.

I am very much still learning C++. Please would you explain the advantage of the method you described? I presume it’s related to memory violations / buffer overrun, but I don’t understand the reason why. (I had already changed references to HAL_I2C_Config in my code to hal_i2c_config_t as the compiler warns that HAL_I2C_Config is deprecated).

With the function acquireWireBuffer() all you do is create a config structure that can enable buffer of the size I2C_BUFFER_SIZE, but it is has NOT changed the Wire-buffer.
Normally config is passed along when creating the Wire instance, e.g. TwoWire Wire(HAL_I2C_INTERFACE1, &default_config);
As Wire is already defined, with the call hal_i2c_init(HAL_I2C_INTERFACE1, &config); we now change the Wire-buffer from the default (32) to 128. You MUST call this before Wire.begin() as here it will initialize pointers.

If you set debug = 2 in the sketch, and comment out the hal_i2c_init-line, you will see that despite acquireWireBuffer() we still have the default config. We only receive 0x22 (34) bytes of the 0x40 (64) bytes. That means Concentration [#/cm3] that are displayed will be wrong (they have NOT been received from the SPS30 due to Wire buffer-size). Not sure why that is 34 as the default buffer is only 32, hence my concern about memory violation. (I used 2.1.0 dev-OS)

Now the same, but with the Wire-buffer at 128 (due to hal_i2c_init), you will see 0x40 (64) bytes are received and the displayed Concentration [#/cm3] is now showing the data from the SPS30.

regards,
Paul

Thanks Paul. I think I follow the gist of what you’re saying, if not the detail.

I have implemented your suggestions, and I’m pleased to have got the sensor working reliably over I2C.

One thing of note is that it takes quite a while for readings to stabilise after starting measurements. This is only really an issue when the sensor is turned off between measurements to save power (which is my use case). Sensirion recommend waiting 30 seconds before collecting readings, and then averaging readings over 30 seconds (https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9.6_Particulate_Matter/Application_Notes/Sensirion_Particulate_Matter_AppNotes_SPS30_Low_Power_Operation_D1.pdf).

In practise, based on observations I’ve made with the sensor on my desk, I’ve found these recommendations to be quite conservative. My observations suggest that waiting 15 seconds, then averaging (median, to mitigate against outliers) 15 readings will result in acceptable reproducibility.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.