L14-Click 1.0
STM32WLE5CC LoRaWAN Sensor Platform
Loading...
Searching...
No Matches
sps30.c
Go to the documentation of this file.
1/*
2 * sps30.c
3 *
4 * Created on: 2. 1. 2026
5 * Author: Milan
6 */
7#include "sps30.h"
8
9#ifdef SENSOR_SPS30
10
11#include "i2c.h"
12#include "utils/mydefs.h"
13#include "utils/utils.h"
14#include "mymems.h"
15#include <stdio.h>
16
17#define SPS30_I2C_ADDR (0x69 << 1)
18
19// Commands
20#define SPS30_CMD_START_MEAS 0x0010
21#define SPS30_CMD_STOP_MEAS 0x0104
22#define SPS30_CMD_READ_DRDY 0x0202
23#define SPS30_CMD_READ_MEAS 0x0300
24#define SPS30_CMD_SOFT_RESET 0xD304
25#define SPS30_CMD_START_CLEAN 0x5607
26#define SPS30_CMD_SLEEP 0x1001
27#define SPS30_CMD_WAKEUP 0x1103
28#define SPS30_CMD_CLEAN_INTERVAL 0x8004
29
37
39sps30_t _bck_sps30Data = {}; // backup
40
41static uint8_t _isSps30 = 0;
42static const uint8_t _cleaningInterval = 100; // the cleaning interval, the count of starting is save in memory Sens_SPS30Start
43
44static clearing_t _isClearing = CLR_NONE; // 0 - no, 1 - ON, 2 - processing, 3 - OFF
46
48{
49 float pm2_5 = _sps30Data.Mass_pm2_5;
50
51 if (pm2_5 <= 12.0f)
52 {
53 *label = "Good";
54 return AQI_GOOD;
55 }
56 else if (pm2_5 <= 35.4f)
57 {
58 *label = "Moderate";
59 return AQI_MODERATE;
60 }
61 else if (pm2_5 <= 55.4f)
62 {
63 *label = "Unhealthy for Sensitive Groups";
65 }
66 else if (pm2_5 <= 150.4f)
67 {
68 *label = "Unhealthy";
69 return AQI_UNHEALTHY;
70 }
71 else if (pm2_5 <= 250.4f)
72 {
73 *label = "Very Unhealthy";
74 return AQI_VERY_UNHEALTHY;
75 }
76 else
77 {
78 *label = "Hazardous";
79 return AQI_HAZARDOUS;
80 }
81}
82
83int8_t sps30_Is(I2C_HandleTypeDef *hi2c, int8_t tryInit) //
84{
85 if (!_isSps30 && tryInit)
86 sps30_Init(hi2c);
87 return _isSps30;
88}
89
90HAL_StatusTypeDef sps30_Init(I2C_HandleTypeDef *hi2c)
91{
92 uint8_t cmd[5] = {};
93 HAL_StatusTypeDef status;
94
96
97 // 1. Wake up the sensor
98 cmd[0] = (SPS30_CMD_WAKEUP >> 8);
99 cmd[1] = (SPS30_CMD_WAKEUP & 0xFF);
100 status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100);
101 HAL_Delay(10); // Minimum 5ms delay required after wakeup
102
103 status = I2C_IsDeviceReadyMT(hi2c, SPS30_I2C_ADDR, 2, 2); // first check
104
105 if (status == HAL_OK)
106 {
107 uint8_t cmd[2] =
108 { (SPS30_CMD_SOFT_RESET >> 8), (SPS30_CMD_SOFT_RESET & 0xFF) };
109
110 do
111 {
112 if ((status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100)) != HAL_OK)
113 break;
114 HAL_Delay(110); // Wait for sensor to reboot
115
116 // go to sleep
117 cmd[0] = (SPS30_CMD_SLEEP >> 8);
118 cmd[1] = (SPS30_CMD_SLEEP & 0xFF);
119 if ((status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100)) != HAL_OK)
120 break;
121 HAL_Delay(10); // min 5ms
122 } while (0);
123 }
124 _isSps30 = (status == HAL_OK);
125 return status;
126}
127
128HAL_StatusTypeDef sps30_On(I2C_HandleTypeDef *hi2c)
129{
130 HAL_StatusTypeDef status = HAL_ERROR;
131
132 _sps30Data.IsDataValid = 0;
133 if (_isSps30)
134 {
135 uint8_t cmd[5] = {};
136 int wakeUp = 2; // 2x wake up i2c
137
138 do
139 {
140 // 1. Wake up the sensor 2x
141 cmd[0] = (SPS30_CMD_WAKEUP >> 8);
142 cmd[1] = (SPS30_CMD_WAKEUP & 0xFF);
143 while (wakeUp-- > 0 && (status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100)) != HAL_OK)
144 HAL_Delay(20); // delay
145
146 if (status != HAL_OK)
147 break;
148 HAL_Delay(10); // Minimum 5ms delay required after wakeup
149
150 // 2. Start Measurement
151 cmd[0] = (SPS30_CMD_START_MEAS >> 8);
152 cmd[1] = (SPS30_CMD_START_MEAS & 0xFF);
153 cmd[2] = 0x03; // Output format: Float
154 cmd[3] = 0x00; // Dummy
155 cmd[4] = systemParams_CalculateCrc(&cmd[2], 2);
156
157 if ((status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 5, 100)) != HAL_OK)
158 break;
159 HAL_Delay(25); // min 20
160
161 // Note: It takes about 1 second for the first measurement to be ready
162 if (_isClearing == CLR_NONE)
163 {
164 _memsMainBlock.Sens_SPS30Start++; // need for clearing sensor
166 if ((_memsMainBlock.Sens_SPS30Start % _cleaningInterval) == 0)
167 _isClearing = CLR_START; // the clearing is neceseary, the count of working has been reach
168 }
169 } while (0);
170 _isSps30 = (status == HAL_OK);
171 }
172 return status;
173}
174
175HAL_StatusTypeDef sps30_Off(I2C_HandleTypeDef *hi2c)
176{
177 HAL_StatusTypeDef status = HAL_ERROR;
178
179 if (_isSps30)
180 {
181 uint8_t cmd[2] =
182 { (SPS30_CMD_STOP_MEAS >> 8), (SPS30_CMD_STOP_MEAS & 0xFF) };
183
184 do
185 {
186 if ((status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100)) != HAL_OK)
187 break;
188 HAL_Delay(25); // min 20
189
190 cmd[0] = (SPS30_CMD_SLEEP >> 8);
191 cmd[1] = (SPS30_CMD_SLEEP & 0xFF);
192 if ((status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100)) != HAL_OK)
193 break;
194 HAL_Delay(10); // min 5ms
195 } while (0);
196 }
197 return status;
198
199}
200
201HAL_StatusTypeDef sps30_IsDataReady(I2C_HandleTypeDef *hi2c)
202{
203 HAL_StatusTypeDef status = HAL_ERROR;
204
205 if (_isSps30)
206 {
207 uint8_t cmd[2] =
208 { (SPS30_CMD_READ_DRDY >> 8), (SPS30_CMD_READ_DRDY & 0xFF) };
209 uint8_t data[3]; // 2 bytes data + 1 byte CRC
210
211 do
212 {
213 if ((status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100)) != HAL_OK)
214 break;
215 if ((status = HAL_I2C_Master_Receive(hi2c, SPS30_I2C_ADDR, data, 3, 100)) != HAL_OK)
216 break;
217 // Check if the LSB of the second byte is 1
218 status = (data[1] == 1) ? HAL_OK : HAL_BUSY;
219 } while (0);
220 }
221 return status;
222}
223
224HAL_StatusTypeDef sps30_Read(I2C_HandleTypeDef *hi2c)
225{
226 HAL_StatusTypeDef status = sps30_IsDataReady(hi2c);
227
228 if (status == HAL_OK)
229 {
230 uint8_t cmd[2] =
231 { (SPS30_CMD_READ_MEAS >> 8), (SPS30_CMD_READ_MEAS & 0xFF) };
232 uint8_t buffer[60]; // 10 values * (2 bytes + 1 CRC) * 2 (for 32-bit floats)
233 do
234 {
235 if ((status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100)) != HAL_OK)
236 break;
237 // We expect 10 floats. Each float is 2 chunks of (2 bytes + CRC) = 6 bytes per float.
238 if ((status = HAL_I2C_Master_Receive(hi2c, SPS30_I2C_ADDR, buffer, 60, 500)) != HAL_OK)
239 break;
240
241 float *f_ptr = (float*) &_sps30Data;
242 for (int i = 0; i < 10; i++)
243 {
244 uint8_t raw_float[4];
245 int b_idx = i * 6;
246
247 if (systemParams_CalculateCrc(&buffer[b_idx], 2) != buffer[b_idx + 2] ||
248 systemParams_CalculateCrc(&buffer[b_idx + 3], 2) != buffer[b_idx + 5])
249 {
250 status = HAL_ERROR; // CRC mismatch - reject entire frame
251 _sps30Data.IsDataValid = 0;
252 break;
253 }
254
255 // Reconstruct float bytes while skipping CRC bytes
256 raw_float[3] = buffer[b_idx];
257 raw_float[2] = buffer[b_idx + 1];
258 // buffer[b_idx+2] is CRC
259 raw_float[1] = buffer[b_idx + 3];
260 raw_float[0] = buffer[b_idx + 4];
261 // buffer[b_idx+5] is CRC
262
263 memcpy(&f_ptr[i], raw_float, 4);
264 _sps30Data.IsDataValid = 1;
265 }
266 } while (0);
267 }
268 return status;
269}
270
271HAL_StatusTypeDef sps30_IsOnOff(I2C_HandleTypeDef *hi2c, uint8_t *onOff)
272{
273 // The SPS30 doesn't have a single "Is Running" register,
274 // but we can infer it by trying to read the Data Ready flag.
275 // If it responds without error, we assume it's powered.
276 // Alternatively, you can track state in a global variable.
277 HAL_StatusTypeDef status = sps30_IsDataReady(hi2c);
278
279 if (status == HAL_OK || status == HAL_BUSY)
280 {
281 if (onOff != NULL)
282 *onOff = (status == HAL_OK);
283 status = HAL_OK;
284 }
285 return status;
286}
287
288HAL_StatusTypeDef sps30_StartCleaning(I2C_HandleTypeDef *hi2c)
289{
290 HAL_StatusTypeDef status = HAL_ERROR;
291 uint8_t cmd[2] =
292 { (SPS30_CMD_START_CLEAN >> 8), (SPS30_CMD_START_CLEAN & 0xFF) };
293
294 // The sensor must be actively measuring for this command to work.
295 if (_isSps30)
296 status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100);
297 return status;
298}
299
300HAL_StatusTypeDef sps30_GetAutoCleanInterval(I2C_HandleTypeDef *hi2c, uint32_t *interval_sec)
301{
302 HAL_StatusTypeDef status = HAL_ERROR;
303
304 if (_isSps30)
305 {
306 uint8_t cmd[2] =
308 uint8_t buffer[6]; // 2 words (4 bytes) + 2 CRC bytes
309
310 do
311 {
312 if ((status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 2, 100)) != HAL_OK)
313 break;
314 if ((status = HAL_I2C_Master_Receive(hi2c, SPS30_I2C_ADDR, buffer, 6, 100)) != HAL_OK)
315 break;
316 // Verify CRCs before assembling
317 if (systemParams_CalculateCrc(&buffer[0], 2) != buffer[2] || systemParams_CalculateCrc(&buffer[3], 2) != buffer[5])
318 {
319 status = HAL_ERROR;
320 break;
321 }
322 // Reconstruct 32-bit value (Big Endian)
323 *interval_sec = ((uint32_t) buffer[0] << 24) | ((uint32_t) buffer[1] << 16) | ((uint32_t) buffer[3] << 8) | (uint32_t) buffer[4];
324 } while (0);
325 }
326 return status;
327}
328
329HAL_StatusTypeDef sps30_SetAutoCleanInterval(I2C_HandleTypeDef *hi2c, uint32_t interval_sec)
330{
331 HAL_StatusTypeDef status = HAL_ERROR;
332
333 if (_isSps30)
334 {
335 uint8_t cmd[8];
336 do
337 {
338 cmd[0] = (SPS30_CMD_CLEAN_INTERVAL >> 8);
339 cmd[1] = (SPS30_CMD_CLEAN_INTERVAL & 0xFF);
340
341 // Split 32-bit into two 16-bit chunks with CRC
342 cmd[2] = (uint8_t) (interval_sec >> 24);
343 cmd[3] = (uint8_t) (interval_sec >> 16);
344 cmd[4] = systemParams_CalculateCrc(&cmd[2], 2);
345
346 cmd[5] = (uint8_t) (interval_sec >> 8);
347 cmd[6] = (uint8_t) (interval_sec & 0xFF);
348 cmd[7] = systemParams_CalculateCrc(&cmd[5], 2);
349 if ((status = HAL_I2C_Master_Transmit(hi2c, SPS30_I2C_ADDR, cmd, 8, 100)) != HAL_OK)
350 break;
351 } while (0);
352 }
353 return status;
354}
355
356void sps30_LogData(char *buf)
357{
358 if (buf != NULL)
359 {
360 char *txt = NULL;
361
362 sps30_ClassifyPM25(&txt);
363 sprintf(buf, "|sps30: pm1_0:" PRIf_02 " pm2_5:" PRIf_02 " %s ", PRIf_02D(_sps30Data.Mass_pm1_0), PRIf_02D(_sps30Data.Mass_pm2_5), ((txt != NULL) ? txt : "(none)"));
364 }
365}
366
367int8_t sps30_Service(I2C_HandleTypeDef *hi2c)
368{
369 HAL_StatusTypeDef status;
370
371 switch (_isClearing)
372 {
373 case CLR_START: // start of clearing
374 status = sps30_On(hi2c);
375 writeLog("sps30 clearing start, status:%d", (int) status);
376 if (status == HAL_OK)
377 {
378 status = sps30_StartCleaning(hi2c);
379 writeLog("sps30 clearing process, status:%d", (int) status);
380 }
381 if (status == HAL_OK)
382 {
384 sleeper_Init(&_clearingTime, 10100); // sleaper is stated for 10s
385 }
386 else
387 _isClearing = CLR_STOP; // error - finish clearing
388 break;
389 case CLR_PROCESS:
392 break;
393 case CLR_STOP:
394 status = sps30_Off(hi2c);
395 _isClearing = CLR_NONE; // finish clearing
396 writeLog("sps30 clearing stop, status:%d", (int) status);
397 break;
398 default:
399 }
400 return _isClearing != CLR_NONE;
401}
402
403#endif
This file contains all the function prototypes for the i2c.c file.
HAL_StatusTypeDef I2C_IsDeviceReadyMT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout)
Wrapper around HAL_I2C_IsDeviceReady() that recovers from a busy bus. If the HAL I2C bus is in a BUSY...
Definition i2c.c:158
void writeLog(const char *format,...)
Format and send a log message over UART (printf-style). Available only when WRITELOG is defined; comp...
Definition main.c:97
#define PRIf_02D(fData)
Expands to integer and fractional arguments for use with PRIf_02. Splits a float/double into the inte...
Definition mydefs.h:38
#define PRIf_02
printf format string for printing a float/double as integer with 2 decimal places....
Definition mydefs.h:30
mems_MainBlock_t _memsMainBlock
Global instance of the main flash control block; loaded by mems_ReadMainBlock().
Definition mymems.c:27
HAL_StatusTypeDef mems_WriteMainBlock()
writing the main block on flash. The main block is stored on address 0
Definition mymems.c:117
uint8_t systemParams_CalculateCrc(uint8_t *data, uint8_t len)
Calculate a CRC-8 checksum (Sensirion polynomial) over a byte buffer. Used to validate sensor I2C res...
AQI_Level_t sps30_ClassifyPM25(char **label)
Classify the current PM2.5 mass concentration according to the AQI scale.
Definition sps30.c:47
HAL_StatusTypeDef sps30_Read(I2C_HandleTypeDef *hi2c)
Read data if data is available.
Definition sps30.c:224
HAL_StatusTypeDef sps30_StartCleaning(I2C_HandleTypeDef *hi2c)
manually start fan for cleaning, runs for about 10s Note: The sensor will be busy cleaning for 10 sec...
Definition sps30.c:288
static sleeper_t _clearingTime
Definition sps30.c:45
#define SPS30_CMD_START_CLEAN
Definition sps30.c:25
#define SPS30_CMD_CLEAN_INTERVAL
Definition sps30.c:28
#define SPS30_CMD_STOP_MEAS
Definition sps30.c:21
clearing_t
Definition sps30.c:31
@ CLR_STOP
Definition sps30.c:35
@ CLR_PROCESS
Definition sps30.c:34
@ CLR_NONE
Definition sps30.c:32
@ CLR_START
Definition sps30.c:33
void sps30_LogData(char *buf)
log data to buffer
Definition sps30.c:356
HAL_StatusTypeDef sps30_SetAutoCleanInterval(I2C_HandleTypeDef *hi2c, uint32_t interval_sec)
set interval for auto cleaning
Definition sps30.c:329
#define SPS30_CMD_READ_MEAS
Definition sps30.c:23
#define SPS30_CMD_START_MEAS
Definition sps30.c:20
HAL_StatusTypeDef sps30_IsOnOff(I2C_HandleTypeDef *hi2c, uint8_t *onOff)
helper to check if sensor is on or not
Definition sps30.c:271
HAL_StatusTypeDef sps30_Off(I2C_HandleTypeDef *hi2c)
Turn off laser and fan to stop measurements.
Definition sps30.c:175
static uint8_t _isSps30
Definition sps30.c:41
static const uint8_t _cleaningInterval
Definition sps30.c:42
HAL_StatusTypeDef sps30_GetAutoCleanInterval(I2C_HandleTypeDef *hi2c, uint32_t *interval_sec)
interval for auto cleaning
Definition sps30.c:300
#define SPS30_CMD_SOFT_RESET
Definition sps30.c:24
int8_t sps30_Is(I2C_HandleTypeDef *hi2c, int8_t tryInit)
check if SPS30 sensor is present
Definition sps30.c:83
int8_t sps30_Service(I2C_HandleTypeDef *hi2c)
the sensor need to be cleaning
Definition sps30.c:367
HAL_StatusTypeDef sps30_Init(I2C_HandleTypeDef *hi2c)
initialization of sensor sps30
Definition sps30.c:90
HAL_StatusTypeDef sps30_IsDataReady(I2C_HandleTypeDef *hi2c)
Check if data is available.
Definition sps30.c:201
static clearing_t _isClearing
Definition sps30.c:44
HAL_StatusTypeDef sps30_On(I2C_HandleTypeDef *hi2c)
Turn on laser and fan to allow measurements.
Definition sps30.c:128
#define SPS30_I2C_ADDR
Definition sps30.c:17
#define SPS30_CMD_SLEEP
Definition sps30.c:26
#define SPS30_CMD_READ_DRDY
Definition sps30.c:22
#define SPS30_CMD_WAKEUP
Definition sps30.c:27
sps30_t _bck_sps30Data
Snapshot copy of the last completed SPS30 measurement; used for LoRaWAN transmission.
Definition sps30.c:39
AQI_Level_t
Air Quality Index (AQI) level based on PM2.5 mass concentration. Follows US EPA / WHO PM2....
Definition sps30.h:96
@ AQI_HAZARDOUS
Definition sps30.h:102
@ AQI_GOOD
Definition sps30.h:97
@ AQI_UNHEALTHY
Definition sps30.h:100
@ AQI_VERY_UNHEALTHY
Definition sps30.h:101
@ AQI_MODERATE
Definition sps30.h:98
@ AQI_UNHEALTHY_SENSITIVE
Definition sps30.h:99
sps30_t _sps30Data
Live measurement data from the SPS30 sensor; updated by sps30_Read().
Definition sps30.c:38
Non-blocking timer utility – similar to HAL_Delay but without CPU blocking.
Definition utils.h:28
Measurement data produced by the SPS30 particulate matter sensor. Populated by sps30_Read(); check Is...
Definition sps30.h:76
void HAL_Delay(__IO uint32_t Delay)
Definition sys_app.c:369
void sleeper_Init(sleeper_t *v, uint32_t sleepMS)
Initialise the sleeper and start timing from now.
Definition utils.c:13
int sleeper_IsElapsed(const sleeper_t *v)
Check whether the configured time interval has elapsed.
Definition utils.c:21