/* USER CODE BEGIN Header */ /** ****************************************************************************** * @file sts_aq_sgp30.c * * @author Yunhorn (r) Technology Limited Application Team * * @brief Yunhorn (r) SmarToilets (r) Product configuration file. * ****************************************************************************** * @attention * * Copyright (c) 2025 Yunhorn Technology Limited. * Copyright (c) 2025 Shenzhen Yunhorn Technology Co., Ltd. * All rights reserved. * * This software is licensed under terms that can be found in the LICENSE file * in the root directory of this software component. * If no LICENSE file comes with this software, it is provided AS-IS. * ****************************************************************************** */ /* USER CODE END Header */ #include "sgp30.h" #include "sensirion_arch_config.h" #include "sensirion_common.h" #include "sensirion_i2c.h" #include "sgp_git_version.h" #define SGP30_PRODUCT_TYPE 0 static const uint8_t SGP30_I2C_ADDRESS = 0x58; /* command and constants for reading the serial ID */ #define SGP30_CMD_GET_SERIAL_ID 0x3682 #define SGP30_CMD_GET_SERIAL_ID_DURATION_US 500 #define SGP30_CMD_GET_SERIAL_ID_WORDS 3 /* command and constants for reading the featureset version */ #define SGP30_CMD_GET_FEATURESET 0x202f #define SGP30_CMD_GET_FEATURESET_DURATION_US 10000 #define SGP30_CMD_GET_FEATURESET_WORDS 1 /* command and constants for on-chip self-test */ #define SGP30_CMD_MEASURE_TEST 0x2032 #define SGP30_CMD_MEASURE_TEST_DURATION_US 220000 #define SGP30_CMD_MEASURE_TEST_WORDS 1 #define SGP30_CMD_MEASURE_TEST_OK 0xd400 /* command and constants for IAQ init */ #define SGP30_CMD_IAQ_INIT 0x2003 #define SGP30_CMD_IAQ_INIT_DURATION_US 10000 /* command and constants for IAQ measure */ #define SGP30_CMD_IAQ_MEASURE 0x2008 #define SGP30_CMD_IAQ_MEASURE_DURATION_US 12000 #define SGP30_CMD_IAQ_MEASURE_WORDS 2 /* command and constants for getting IAQ baseline */ #define SGP30_CMD_GET_IAQ_BASELINE 0x2015 #define SGP30_CMD_GET_IAQ_BASELINE_DURATION_US 10000 #define SGP30_CMD_GET_IAQ_BASELINE_WORDS 2 /* command and constants for setting IAQ baseline */ #define SGP30_CMD_SET_IAQ_BASELINE 0x201e #define SGP30_CMD_SET_IAQ_BASELINE_DURATION_US 10000 /* command and constants for raw measure */ #define SGP30_CMD_RAW_MEASURE 0x2050 #define SGP30_CMD_RAW_MEASURE_DURATION_US 25000 #define SGP30_CMD_RAW_MEASURE_WORDS 2 /* command and constants for setting absolute humidity */ #define SGP30_CMD_SET_ABSOLUTE_HUMIDITY 0x2061 #define SGP30_CMD_SET_ABSOLUTE_HUMIDITY_DURATION_US 10000 /* command and constants for getting TVOC inceptive baseline */ #define SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE 0x20b3 #define SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE_DURATION_US 10000 #define SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE_WORDS 1 /* command and constants for setting TVOC baseline */ #define SGP30_CMD_SET_TVOC_BASELINE 0x2077 #define SGP30_CMD_SET_TVOC_BASELINE_DURATION_US 10000 /** * sgp30_check_featureset() - Check if the connected sensor has a certain FS * * @needed_fs: The featureset that is required * * Return: STATUS_OK if the sensor has the required FS, * SGP30_ERR_INVALID_PRODUCT_TYPE if the sensor is not an SGP30, * SGP30_ERR_UNSUPPORTED_FEATURE_SET if the sensor does not * have the required FS, * an error code otherwise */ static int16_t sgp30_check_featureset(uint16_t needed_fs) { int16_t ret; uint16_t fs_version; uint8_t product_type; ret = sgp30_get_feature_set_version(&fs_version, &product_type); if (ret != STATUS_OK) return ret; if (product_type != SGP30_PRODUCT_TYPE) return SGP30_ERR_INVALID_PRODUCT_TYPE; if (fs_version < needed_fs) return SGP30_ERR_UNSUPPORTED_FEATURE_SET; return STATUS_OK; } int16_t sgp30_measure_test(uint16_t *test_result) { uint16_t measure_test_word_buf[SGP30_CMD_MEASURE_TEST_WORDS]; int16_t ret; *test_result = 0; ret = sensirion_i2c_delayed_read_cmd( SGP30_I2C_ADDRESS, SGP30_CMD_MEASURE_TEST, SGP30_CMD_MEASURE_TEST_DURATION_US, measure_test_word_buf, SENSIRION_NUM_WORDS(measure_test_word_buf)); if (ret != STATUS_OK) return ret; *test_result = *measure_test_word_buf; if (*test_result == SGP30_CMD_MEASURE_TEST_OK) return STATUS_OK; return STATUS_FAIL; } int16_t sgp30_measure_iaq() { return sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_IAQ_MEASURE); } int16_t sgp30_read_iaq(uint16_t *tvoc_ppb, uint16_t *co2_eq_ppm) { int16_t ret; uint16_t words[SGP30_CMD_IAQ_MEASURE_WORDS]; ret = sensirion_i2c_read_words(SGP30_I2C_ADDRESS, words, SGP30_CMD_IAQ_MEASURE_WORDS); *tvoc_ppb = words[1]; *co2_eq_ppm = words[0]; return ret; } int16_t sgp30_measure_iaq_blocking_read(uint16_t *tvoc_ppb, uint16_t *co2_eq_ppm) { int16_t ret; ret = sgp30_measure_iaq(); if (ret != STATUS_OK) return ret; sensirion_sleep_usec(SGP30_CMD_IAQ_MEASURE_DURATION_US); return sgp30_read_iaq(tvoc_ppb, co2_eq_ppm); } int16_t sgp30_measure_tvoc() { return sgp30_measure_iaq(); } int16_t sgp30_read_tvoc(uint16_t *tvoc_ppb) { uint16_t co2_eq_ppm; return sgp30_read_iaq(tvoc_ppb, &co2_eq_ppm); } int16_t sgp30_measure_tvoc_blocking_read(uint16_t *tvoc_ppb) { uint16_t co2_eq_ppm; return sgp30_measure_iaq_blocking_read(tvoc_ppb, &co2_eq_ppm); } int16_t sgp30_measure_co2_eq() { return sgp30_measure_iaq(); } int16_t sgp30_read_co2_eq(uint16_t *co2_eq_ppm) { uint16_t tvoc_ppb; return sgp30_read_iaq(&tvoc_ppb, co2_eq_ppm); } int16_t sgp30_measure_co2_eq_blocking_read(uint16_t *co2_eq_ppm) { uint16_t tvoc_ppb; return sgp30_measure_iaq_blocking_read(&tvoc_ppb, co2_eq_ppm); } int16_t sgp30_measure_raw_blocking_read(uint16_t *ethanol_raw_signal, uint16_t *h2_raw_signal) { int16_t ret; ret = sgp30_measure_raw(); if (ret != STATUS_OK) return ret; sensirion_sleep_usec(SGP30_CMD_RAW_MEASURE_DURATION_US); return sgp30_read_raw(ethanol_raw_signal, h2_raw_signal); } int16_t sgp30_measure_raw() { return sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_RAW_MEASURE); } int16_t sgp30_read_raw(uint16_t *ethanol_raw_signal, uint16_t *h2_raw_signal) { int16_t ret; uint16_t words[SGP30_CMD_RAW_MEASURE_WORDS]; ret = sensirion_i2c_read_words(SGP30_I2C_ADDRESS, words, SGP30_CMD_RAW_MEASURE_WORDS); *ethanol_raw_signal = words[1]; *h2_raw_signal = words[0]; return ret; } int16_t sgp30_get_iaq_baseline(uint32_t *baseline) { int16_t ret; uint16_t words[SGP30_CMD_GET_IAQ_BASELINE_WORDS]; ret = sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_GET_IAQ_BASELINE); if (ret != STATUS_OK) return ret; sensirion_sleep_usec(SGP30_CMD_GET_IAQ_BASELINE_DURATION_US); ret = sensirion_i2c_read_words(SGP30_I2C_ADDRESS, words, SGP30_CMD_GET_IAQ_BASELINE_WORDS); if (ret != STATUS_OK) return ret; *baseline = ((uint32_t)words[1] << 16) | ((uint32_t)words[0]); if (*baseline) return STATUS_OK; return STATUS_FAIL; } int16_t sgp30_set_iaq_baseline(uint32_t baseline) { int16_t ret; uint16_t words[2] = {(uint16_t)((baseline & 0xffff0000) >> 16), (uint16_t)(baseline & 0x0000ffff)}; if (!baseline) return STATUS_FAIL; ret = sensirion_i2c_write_cmd_with_args(SGP30_I2C_ADDRESS, SGP30_CMD_SET_IAQ_BASELINE, words, SENSIRION_NUM_WORDS(words)); sensirion_sleep_usec(SGP30_CMD_SET_IAQ_BASELINE_DURATION_US); return ret; } int16_t sgp30_get_tvoc_inceptive_baseline(uint16_t *tvoc_inceptive_baseline) { int16_t ret; ret = sgp30_check_featureset(0x21); if (ret != STATUS_OK) return ret; ret = sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE); if (ret != STATUS_OK) return ret; sensirion_sleep_usec(SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE_DURATION_US); return sensirion_i2c_read_words( SGP30_I2C_ADDRESS, tvoc_inceptive_baseline, SGP30_CMD_GET_TVOC_INCEPTIVE_BASELINE_WORDS); } int16_t sgp30_set_tvoc_baseline(uint16_t tvoc_baseline) { int16_t ret; ret = sgp30_check_featureset(0x21); if (ret != STATUS_OK) return ret; if (!tvoc_baseline) return STATUS_FAIL; ret = sensirion_i2c_write_cmd_with_args( SGP30_I2C_ADDRESS, SGP30_CMD_SET_TVOC_BASELINE, &tvoc_baseline, SENSIRION_NUM_WORDS(tvoc_baseline)); sensirion_sleep_usec(SGP30_CMD_SET_TVOC_BASELINE_DURATION_US); return ret; } int16_t sgp30_set_absolute_humidity(uint32_t absolute_humidity) { int16_t ret; uint16_t ah_scaled; if (absolute_humidity > 256000) return STATUS_FAIL; /* ah_scaled = (absolute_humidity / 1000) * 256 */ ah_scaled = (uint16_t)((absolute_humidity * 16777) >> 16); ret = sensirion_i2c_write_cmd_with_args( SGP30_I2C_ADDRESS, SGP30_CMD_SET_ABSOLUTE_HUMIDITY, &ah_scaled, SENSIRION_NUM_WORDS(ah_scaled)); sensirion_sleep_usec(SGP30_CMD_SET_ABSOLUTE_HUMIDITY_DURATION_US); return ret; } const char *sgp30_get_driver_version() { return SGP_DRV_VERSION_STR; } uint8_t sgp30_get_configured_address() { return SGP30_I2C_ADDRESS; } int16_t sgp30_get_feature_set_version(uint16_t *feature_set_version, uint8_t *product_type) { int16_t ret; uint16_t words[SGP30_CMD_GET_FEATURESET_WORDS]; ret = sensirion_i2c_delayed_read_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_GET_FEATURESET, SGP30_CMD_GET_FEATURESET_DURATION_US, words, SGP30_CMD_GET_FEATURESET_WORDS); if (ret != STATUS_OK) return ret; *feature_set_version = words[0] & 0x00FF; *product_type = (uint8_t)((words[0] & 0xF000) >> 12); return STATUS_OK; } int16_t sgp30_get_serial_id(uint64_t *serial_id) { int16_t ret; uint16_t words[SGP30_CMD_GET_SERIAL_ID_WORDS]; ret = sensirion_i2c_delayed_read_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_GET_SERIAL_ID, SGP30_CMD_GET_SERIAL_ID_DURATION_US, words, SGP30_CMD_GET_SERIAL_ID_WORDS); if (ret != STATUS_OK) return ret; *serial_id = (((uint64_t)words[0]) << 32) | (((uint64_t)words[1]) << 16) | (((uint64_t)words[2]) << 0); return STATUS_OK; } int16_t sgp30_iaq_init() { int16_t ret = sensirion_i2c_write_cmd(SGP30_I2C_ADDRESS, SGP30_CMD_IAQ_INIT); sensirion_sleep_usec(SGP30_CMD_IAQ_INIT_DURATION_US); return ret; } int16_t sgp30_probe() { int16_t ret = sgp30_check_featureset(0x20); if (ret != STATUS_OK) return ret; return sgp30_iaq_init(); } /* * Copyright (c) 2018, Sensirion AG * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * * Neither the name of Sensirion AG nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "sgp30.h" #include // PRIu64 #include // printf #include // sleep /* TO USE CONSOLE OUTPUT (printf) AND WAIT (sleep) YOU MAY NEED TO ADAPT THE * INCLUDES ABOVE OR DEFINE THEM ACCORDING TO YOUR PLATFORM. * #define printf(...) * #define sleep(...) */ int uut_sgp30_main(void) { uint16_t i = 0; int16_t err; uint16_t tvoc_ppb, co2_eq_ppm; uint32_t iaq_baseline; uint16_t ethanol_raw_signal, h2_raw_signal; const char* driver_version = sgp30_get_driver_version(); if (driver_version) { printf("SGP30 driver version %s\n", driver_version); } else { printf("fatal: Getting driver version failed\n"); return -1; } /* Initialize I2C bus */ sensirion_i2c_init(); /* Busy loop for initialization. The main loop does not work without * a sensor. */ int16_t probe; while (1) { probe = sgp30_probe(); if (probe == STATUS_OK) break; if (probe == SGP30_ERR_UNSUPPORTED_FEATURE_SET) printf( "Your sensor needs at least feature set version 1.0 (0x20)\n"); printf("SGP sensor probing failed\n"); sensirion_sleep_usec(1000000); } printf("SGP sensor probing successful\n"); uint16_t feature_set_version; uint8_t product_type; err = sgp30_get_feature_set_version(&feature_set_version, &product_type); if (err == STATUS_OK) { printf("Feature set version: %u\n", feature_set_version); printf("Product type: %u\n", product_type); } else { printf("sgp30_get_feature_set_version failed!\n"); } uint64_t serial_id; err = sgp30_get_serial_id(&serial_id); if (err == STATUS_OK) { printf("SerialID: %" PRIu64 "\n", serial_id); } else { printf("sgp30_get_serial_id failed!\n"); } /* Read gas raw signals */ err = sgp30_measure_raw_blocking_read(ðanol_raw_signal, &h2_raw_signal); if (err == STATUS_OK) { /* Print ethanol raw signal and h2 raw signal */ printf("Ethanol raw signal: %u\n", ethanol_raw_signal); printf("H2 raw signal: %u\n", h2_raw_signal); } else { printf("error reading raw signals\n"); } /* Consider the two cases (A) and (B): * (A) If no baseline is available or the most recent baseline is more than * one week old, it must discarded. A new baseline is found with * sgp30_iaq_init() */ err = sgp30_iaq_init(); if (err == STATUS_OK) { printf("sgp30_iaq_init done\n"); } else { printf("sgp30_iaq_init failed!\n"); } /* (B) If a recent baseline is available, set it after sgp30_iaq_init() for * faster start-up */ /* IMPLEMENT: retrieve iaq_baseline from presistent storage; * err = sgp30_set_iaq_baseline(iaq_baseline); */ /* Run periodic IAQ measurements at defined intervals */ while (1) { /* * IMPLEMENT: get absolute humidity to enable humidity compensation * uint32_t ah = get_absolute_humidity(); // absolute humidity in mg/m^3 * sgp30_set_absolute_humidity(ah); */ err = sgp30_measure_iaq_blocking_read(&tvoc_ppb, &co2_eq_ppm); if (err == STATUS_OK) { printf("tVOC Concentration: %dppb\n", tvoc_ppb); printf("CO2eq Concentration: %dppm\n", co2_eq_ppm); } else { printf("error reading IAQ values\n"); } /* Persist the current baseline every hour */ if (++i % 3600 == 3599) { err = sgp30_get_iaq_baseline(&iaq_baseline); if (err == STATUS_OK) { /* IMPLEMENT: store baseline to presistent storage */ } } /* The IAQ measurement must be triggered exactly once per second (SGP30) * to get accurate values. */ sleep(1); // SGP30 } return 0; } /********************************************************** *@file sgp30.h *@brief * 基于STM32 HAL 库的SGP30 甲醛传感器驱动 * *@note * 1. 驱动默认使用硬件i2c 1 * 2. 如果不需要打印报错信息,可以去掉printf相关 **********************************************************/ #include "sgp30.h" sgp30_data_t sgp30_data; I2C_HandleTypeDef SGP30_I2C_Handle_Name; /** * @brief 向SGP30发送一条指令(16bit) * @param cmd SGP30指令 * @retval 成功返回HAL_OK */ static uint8_t sgp30_send_cmd(sgp30_cmd_t cmd) { uint8_t cmd_buffer[2]; cmd_buffer[0] = cmd >> 8; cmd_buffer[1] = cmd; return HAL_I2C_Master_Transmit(&SGP30_I2C_Handle_Name, SGP30_ADDR_WRITE, (uint8_t*) cmd_buffer, 2, 0xFFFF); } /** * @brief 软复位SGP30 * @param none * @retval 成功返回HAL_OK */ static int sgp30_soft_reset(void) { uint8_t cmd = 0x06; return HAL_I2C_Master_Transmit(&SGP30_I2C_Handle_Name, 0x00, &cmd, 1, 0xFFFF); } /** * @brief 初始化SGP30空气质量测量模式 * @param none * @retval 成功返回0,失败返回-1 */ int sgp30_init(void) { int status; status = sgp30_soft_reset(); if (status != HAL_OK) { return -1; } HAL_Delay(100); status = sgp30_send_cmd(INIT_AIR_QUALITY); HAL_Delay(100); return status == 0 ? 0 : -1; } /** * @brief 初始化SGP30空气质量测量模式 * @param none * @retval 成功返回HAL_OK */ static int sgp30_start(void) { return sgp30_send_cmd(MEASURE_AIR_QUALITY); } #define CRC8_POLYNOMIAL 0x31 static uint8_t CheckCrc8(uint8_t* const message, uint8_t initial_value) { uint8_t remainder; //余数 uint8_t i = 0, j = 0; ////循环变量 /* 初始化 */ remainder = initial_value; for(j = 0; j < 2;j++) { remainder ^= message[j]; /* 从最高位开始依次计算 */ for (i = 0; i < 8; i++) { if (remainder & 0x80) { remainder = (remainder << 1)^CRC8_POLYNOMIAL; } else { remainder = (remainder << 1); } } } /* 返回计算的CRC码 */ return remainder; } /** * @brief 读取一次空气质量数据 * @param none * @retval 成功返回0,失败返回-1 */ int sgp30_read(void) { int status; uint8_t recv_buffer[6]={0}; /* 启动测量 */ status = sgp30_start(); if (status != 0) { printf("sgp30 start fail\r\n"); return -1; } HAL_Delay(100); /* 读取测量数据 */ status = HAL_I2C_Master_Receive(&SGP30_I2C_Handle_Name, SGP30_ADDR_READ, (uint8_t*)recv_buffer, 6, 0xFFFF); if (status != HAL_OK) { printf("I2C Master Receive fail\r\n"); return -1; } /* 校验接收的测量数据 */ if (CheckCrc8(&recv_buffer[0], 0xFF) != recv_buffer[2]) { printf("co2 recv data crc check fail\r\n"); return -1; } if (CheckCrc8(&recv_buffer[3], 0xFF) != recv_buffer[5]) { printf("tvoc recv data crc check fail\r\n"); return -1; } /* 转换测量数据 */ sgp30_data.co2 = recv_buffer[0] << 8 | recv_buffer[1]; sgp30_data.tvoc = recv_buffer[3] << 8 | recv_buffer[4]; return 0; }