Module Name: src Committed By: brad Date: Thu Oct 14 13:54:46 UTC 2021
Modified Files: src/distrib/sets/lists/debug: module.mi src/distrib/sets/lists/man: mi src/distrib/sets/lists/modules: mi src/share/man/man4: Makefile src/sys/dev/i2c: files.i2c src/sys/modules: Makefile Added Files: src/share/man/man4: sgp40mox.4 src/sys/dev/i2c: sensirion_arch_config.h sensirion_voc_algorithm.c sensirion_voc_algorithm.h sgp40.c sgp40reg.h sgp40var.h src/sys/modules/sgp40mox: Makefile sgp40mox.ioconf Log Message: A driver for the Sensirion SGP40 MOx gas sensor. An example of this chip from Adafruit is: https://www.adafruit.com/product/4829 This is a moderately priced gas sensor that can detect volatile organic compounds in the air. The driver uses the 3-clause BSD licensed VOC algorithm provided by Sensirion to turn the raw sensor metric into a VOC index which can indicate the quality of the air in a particular indoor environment. All published functions of the chip are supported and one unpublished feature. To generate a diff of this commit: cvs rdiff -u -r1.13 -r1.14 src/distrib/sets/lists/debug/module.mi cvs rdiff -u -r1.1727 -r1.1728 src/distrib/sets/lists/man/mi cvs rdiff -u -r1.147 -r1.148 src/distrib/sets/lists/modules/mi cvs rdiff -u -r1.717 -r1.718 src/share/man/man4/Makefile cvs rdiff -u -r0 -r1.1 src/share/man/man4/sgp40mox.4 cvs rdiff -u -r1.117 -r1.118 src/sys/dev/i2c/files.i2c cvs rdiff -u -r0 -r1.1 src/sys/dev/i2c/sensirion_arch_config.h \ src/sys/dev/i2c/sensirion_voc_algorithm.c \ src/sys/dev/i2c/sensirion_voc_algorithm.h src/sys/dev/i2c/sgp40.c \ src/sys/dev/i2c/sgp40reg.h src/sys/dev/i2c/sgp40var.h cvs rdiff -u -r1.258 -r1.259 src/sys/modules/Makefile cvs rdiff -u -r0 -r1.1 src/sys/modules/sgp40mox/Makefile \ src/sys/modules/sgp40mox/sgp40mox.ioconf Please note that diffs are not public domain; they are subject to the copyright notices on the relevant files.
Modified files: Index: src/distrib/sets/lists/debug/module.mi diff -u src/distrib/sets/lists/debug/module.mi:1.13 src/distrib/sets/lists/debug/module.mi:1.14 --- src/distrib/sets/lists/debug/module.mi:1.13 Mon Oct 4 07:04:39 2021 +++ src/distrib/sets/lists/debug/module.mi Thu Oct 14 13:54:46 2021 @@ -1,4 +1,4 @@ -# $NetBSD: module.mi,v 1.13 2021/10/04 07:04:39 brad Exp $ +# $NetBSD: module.mi,v 1.14 2021/10/14 13:54:46 brad Exp $ ./usr/libdata/debug/@MODULEDIR@ modules-base-kernel kmod,debug ./usr/libdata/debug/@MODULEDIR@/accf_dataready modules-base-kernel kmod,debug ./usr/libdata/debug/@MODULEDIR@/accf_dataready/accf_dataready.kmod.debug modules-base-kernel kmod,debug @@ -332,6 +332,8 @@ ./usr/libdata/debug/@MODULEDIR@/securelevel/securelevel.kmod.debug modules-base-kernel kmod,debug ./usr/libdata/debug/@MODULEDIR@/sequencer modules-base-kernel kmod,debug ./usr/libdata/debug/@MODULEDIR@/sequencer/sequencer.kmod.debug modules-base-kernel kmod,debug +./usr/libdata/debug/@MODULEDIR@/sgp40mox modules-base-kernel kmod,debug +./usr/libdata/debug/@MODULEDIR@/sgp40mox/sgp40mox.kmod.debug modules-base-kernel kmod,debug ./usr/libdata/debug/@MODULEDIR@/sht4xtemp modules-base-kernel kmod,debug ./usr/libdata/debug/@MODULEDIR@/sht4xtemp/sht4xtemp.kmod.debug modules-base-kernel kmod,debug ./usr/libdata/debug/@MODULEDIR@/si70xxtemp modules-base-kernel kmod,debug Index: src/distrib/sets/lists/man/mi diff -u src/distrib/sets/lists/man/mi:1.1727 src/distrib/sets/lists/man/mi:1.1728 --- src/distrib/sets/lists/man/mi:1.1727 Tue Oct 12 04:55:19 2021 +++ src/distrib/sets/lists/man/mi Thu Oct 14 13:54:46 2021 @@ -1,4 +1,4 @@ -# $NetBSD: mi,v 1.1727 2021/10/12 04:55:19 msaitoh Exp $ +# $NetBSD: mi,v 1.1728 2021/10/14 13:54:46 brad Exp $ # # Note: don't delete entries from here - mark them as "obsolete" instead. # @@ -1720,6 +1720,7 @@ ./usr/share/man/cat4/sgimips/pic.0 man-sys-catman .cat ./usr/share/man/cat4/sgimips/sq.0 man-sys-catman .cat ./usr/share/man/cat4/sgimips/wdsc.0 man-sys-catman .cat +./usr/share/man/cat4/sgp40mox.0 man-sys-catman .cat ./usr/share/man/cat4/sgsmix.0 man-sys-catman .cat ./usr/share/man/cat4/shb.0 man-sys-catman .cat ./usr/share/man/cat4/shmif.0 man-sys-catman .cat @@ -4895,6 +4896,7 @@ ./usr/share/man/html4/sgimips/pic.html man-sys-htmlman html ./usr/share/man/html4/sgimips/sq.html man-sys-htmlman html ./usr/share/man/html4/sgimips/wdsc.html man-sys-htmlman html +./usr/share/man/html4/sgp40mox.html man-sys-htmlman html ./usr/share/man/html4/sgsmix.html man-sys-htmlman html ./usr/share/man/html4/shb.html man-sys-htmlman html ./usr/share/man/html4/shmif.html man-sys-htmlman html @@ -7976,6 +7978,7 @@ ./usr/share/man/man4/sgimips/pic.4 man-sys-man .man ./usr/share/man/man4/sgimips/sq.4 man-sys-man .man ./usr/share/man/man4/sgimips/wdsc.4 man-sys-man .man +./usr/share/man/man4/sgp40mox.4 man-sys-man .man ./usr/share/man/man4/sgsmix.4 man-sys-man .man ./usr/share/man/man4/shb.4 man-sys-man .man ./usr/share/man/man4/shmif.4 man-sys-man .man Index: src/distrib/sets/lists/modules/mi diff -u src/distrib/sets/lists/modules/mi:1.147 src/distrib/sets/lists/modules/mi:1.148 --- src/distrib/sets/lists/modules/mi:1.147 Sun Oct 3 17:27:02 2021 +++ src/distrib/sets/lists/modules/mi Thu Oct 14 13:54:46 2021 @@ -1,4 +1,4 @@ -# $NetBSD: mi,v 1.147 2021/10/03 17:27:02 brad Exp $ +# $NetBSD: mi,v 1.148 2021/10/14 13:54:46 brad Exp $ # # Note: don't delete entries from here - mark them as "obsolete" instead. # @@ -397,6 +397,8 @@ ./@MODULEDIR@/securelevel/securelevel.kmod modules-base-kernel kmod ./@MODULEDIR@/sequencer modules-base-kernel kmod ./@MODULEDIR@/sequencer/sequencer.kmod modules-base-kernel kmod +./@MODULEDIR@/sgp40mox modules-base-kernel kmod +./@MODULEDIR@/sgp40mox/sgp40mox.kmod modules-base-kernel kmod ./@MODULEDIR@/sht4xtemp modules-base-kernel kmod ./@MODULEDIR@/sht4xtemp/sht4xtemp.kmod modules-base-kernel kmod ./@MODULEDIR@/si70xxtemp modules-base-kernel kmod Index: src/share/man/man4/Makefile diff -u src/share/man/man4/Makefile:1.717 src/share/man/man4/Makefile:1.718 --- src/share/man/man4/Makefile:1.717 Tue Oct 12 04:55:19 2021 +++ src/share/man/man4/Makefile Thu Oct 14 13:54:45 2021 @@ -1,4 +1,4 @@ -# $NetBSD: Makefile,v 1.717 2021/10/12 04:55:19 msaitoh Exp $ +# $NetBSD: Makefile,v 1.718 2021/10/14 13:54:45 brad Exp $ # @(#)Makefile 8.1 (Berkeley) 6/18/93 MAN= aac.4 ac97.4 acardide.4 aceride.4 acphy.4 \ @@ -56,8 +56,8 @@ MAN= aac.4 ac97.4 acardide.4 aceride.4 a rnd.4 route.4 rs5c372rtc.4 rtk.4 rtsx.4 rtw.4 rtwn.4 rum.4 run.4 \ s390rtc.4 satalink.4 sbus.4 schide.4 \ scsi.4 sctp.4 sd.4 se.4 seeprom.4 sem.4 \ - ses.4 sf.4 sfb.4 sgsmix.4 shb.4 shmif.4 shpcic.4 sht4xtemp.4 si70xxtemp.4 \ - siisata.4 siop.4 sip.4 siside.4 sk.4 sl.4 slide.4 \ + ses.4 sf.4 sfb.4 sgp40mox.4 sgsmix.4 shb.4 shmif.4 shpcic.4 sht4xtemp.4 \ + si70xxtemp.4 siisata.4 siop.4 sip.4 siside.4 sk.4 sl.4 slide.4 \ sm.4 smscphy.4 smsh.4 sn.4 sony.4 spc.4 speaker.4 spif.4 sqphy.4 \ srt.4 ss.4 \ ssdfb.4 st.4 ste.4 stge.4 sti.4 stpcide.4 sv.4 \ Index: src/sys/dev/i2c/files.i2c diff -u src/sys/dev/i2c/files.i2c:1.117 src/sys/dev/i2c/files.i2c:1.118 --- src/sys/dev/i2c/files.i2c:1.117 Sun Oct 3 17:27:02 2021 +++ src/sys/dev/i2c/files.i2c Thu Oct 14 13:54:46 2021 @@ -1,4 +1,4 @@ -# $NetBSD: files.i2c,v 1.117 2021/10/03 17:27:02 brad Exp $ +# $NetBSD: files.i2c,v 1.118 2021/10/14 13:54:46 brad Exp $ obsolete defflag opt_i2cbus.h I2C_SCAN define i2cbus { } @@ -400,6 +400,12 @@ device sht4xtemp attach sht4xtemp at iic file dev/i2c/sht4x.c sht4xtemp +# Sensirion SGP40 MOx gas sensor +device sgp40mox +attach sgp40mox at iic +file dev/i2c/sgp40.c sgp40mox +file dev/i2c/sensirion_voc_algorithm.c sgp40mox + # Philips PCA955x GPIO device pcagpio: leds attach pcagpio at iic Index: src/sys/modules/Makefile diff -u src/sys/modules/Makefile:1.258 src/sys/modules/Makefile:1.259 --- src/sys/modules/Makefile:1.258 Sat Oct 9 07:01:34 2021 +++ src/sys/modules/Makefile Thu Oct 14 13:54:45 2021 @@ -1,4 +1,4 @@ -# $NetBSD: Makefile,v 1.258 2021/10/09 07:01:34 ryo Exp $ +# $NetBSD: Makefile,v 1.259 2021/10/14 13:54:45 brad Exp $ .include <bsd.own.mk> @@ -70,6 +70,7 @@ SUBDIR+= hythygtemp SUBDIR+= si70xxtemp SUBDIR+= am2315temp SUBDIR+= sht4xtemp +SUBDIR+= sgp40mox SUBDIR+= i2cexec SUBDIR+= i2c_bitbang SUBDIR+= if_agr Added files: Index: src/share/man/man4/sgp40mox.4 diff -u /dev/null src/share/man/man4/sgp40mox.4:1.1 --- /dev/null Thu Oct 14 13:54:46 2021 +++ src/share/man/man4/sgp40mox.4 Thu Oct 14 13:54:45 2021 @@ -0,0 +1,107 @@ +.\" $NetBSD: sgp40mox.4,v 1.1 2021/10/14 13:54:45 brad Exp $ +.\" +.\" Copyright (c) 2021 Brad Spencer <b...@anduin.eldar.org> +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd October 7, 2021 +.Dt SGP40MOX 4 +.Os +.Sh NAME +.Nm sgp40mox +.Nd Driver for Sensirion SGP40 MOx gas sensor +.Sh SYNOPSIS +.Cd "sgp40mox* at iic? addr 0x59" +.Sh DESCRIPTION +The +.Nm +driver provides an air quality measurement from the SGP40 +sensor via the +.Xr envsys 4 +framework. +The +.Nm +.Ar addr +argument selects the address at the +.Xr iic 4 +bus. +The crc validity and temperature and %RH compensation can be changed through +.Xr sysctl 8 +nodes. +.Pp +In order to calculate the VOC index, the volatile organic compounds index, which +is the measure of air quality the sensor is polled once a second and the raw sensor +value is fed into the Sensirion VOC algorithm. This VOC algorithm used in this driver +is licensed under a 3 clause BSD license and was pulled from the Sensirion Github +repository at +.Rs +.%U https://github.com/Sensirion/embedded-sgp +.Re +.Sh SYSCTL VARIABLES +The following +.Xr sysctl 3 +variables are provided: +.Bl -tag -width indent +.It Li hw.sgp40mox0.compensation.temperature +This should be set to the temperature in Celsius of the environment that the sensor +is in. The valid values are from -45 to 130 degrees Celsius. +.It Li hw.sgp40mox0.compensation.humidity +This should be set to the %RH of the environment that the sensor is in. The valid +values are from 0 to 100. +.Pp +For the best performance of the VOC algorithm it is important that the temperature +and %RH compensation values be current and set using the +.Xr sysctl 3 +variables mentioned above. +This data will need to be pulled from another source, such as a another sensor in +the environment that the SGP40 is in. +.It Li hw.sgp40mox0.ignorecrc +If set, the crc calculation will be ignored on the calls to the chip for the purposes +of measurement. +.It Li hw.sgp40mox0.debug +If the driver is compiled with +.Dv SGP40_DEBUG , +this node will appear and can be used to set the debugging level. +.It Li hw.sgp40mox0.readattempts +To read the air quality metric from the chip requires that the command be sent, +then a delay must be observed before a read can be done to get the values +back. +The delays are documented in the data sheet for the chip. +The driver will attempt to read back the values readattempts number of +times. +The default is 10 which should be more than enough for most purposes. +.El +.Sh SEE ALSO +.Xr envsys 4 , +.Xr iic 4 , +.Xr envstat 8 , +.Xr sysctl 8 +.Sh HISTORY +The +.Nm +driver first appeared in +.Nx 10.0 . +.Sh AUTHORS +.An -nosplit +The +.Nm +driver was written by +.An Brad Spencer Aq Mt b...@anduin.eldar.org . +.Sh BUGS +The driver does not make complete use of the VOC algorithm. In particular, there is no +need to restart the algorithm from scratch if there is a stoppage of polling for less than +10 minutes. The driver does not have the ability to determine that, and therefore +assumes that the sensor is completely cold each time the driver attaches to the chip. +.Pp +The temperature and humidity compensation could be allowed to contain fractional degrees Celsius +and %RH. The driver only supports setting whole numbers for either of those. Index: src/sys/dev/i2c/sensirion_arch_config.h diff -u /dev/null src/sys/dev/i2c/sensirion_arch_config.h:1.1 --- /dev/null Thu Oct 14 13:54:46 2021 +++ src/sys/dev/i2c/sensirion_arch_config.h Thu Oct 14 13:54:46 2021 @@ -0,0 +1,33 @@ +/* $NetBSD: sensirion_arch_config.h,v 1.1 2021/10/14 13:54:46 brad Exp $ */ + +/* + * Copyright (c) 2021 Brad Spencer <b...@anduin.eldar.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGL`IGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + + +/* The Sensirion code wants a file called sensirion_arch_config.h. This + is the shim for NetBSD +*/ + +#ifndef SENSIRION_ARCH_CONFIG_H +#define SENSIRION_ARCH_CONFIG_H + +#include <sys/cdefs.h> +#include <sys/param.h> +#include <sys/systm.h> +#include <sys/kernel.h> + + +#endif /* SENSIRION_ARCH_CONFIG_H */ Index: src/sys/dev/i2c/sensirion_voc_algorithm.c diff -u /dev/null src/sys/dev/i2c/sensirion_voc_algorithm.c:1.1 --- /dev/null Thu Oct 14 13:54:46 2021 +++ src/sys/dev/i2c/sensirion_voc_algorithm.c Thu Oct 14 13:54:46 2021 @@ -0,0 +1,809 @@ +/* + * $NetBSD: sensirion_voc_algorithm.c,v 1.1 2021/10/14 13:54:46 brad Exp $ + */ + +/* + * Copyright (c) 2021, 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 "sensirion_voc_algorithm.h" + +/* The fixed point arithmetic parts of this code were originally created by + * https://github.com/PetteriAimonen/libfixmath + */ + +/*!< the maximum value of fix16_t */ +#define FIX16_MAXIMUM 0x7FFFFFFF +/*!< the minimum value of fix16_t */ +#define FIX16_MINIMUM 0x80000000 +/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not + * specified */ +#define FIX16_OVERFLOW 0x80000000 +/*!< fix16_t value of 1 */ +#define FIX16_ONE 0x00010000 + +static inline fix16_t fix16_from_int(int32_t a) { + return a * FIX16_ONE; +} + +static inline int32_t fix16_cast_to_int(fix16_t a) { + return (a >= 0) ? (a >> 16) : -((-a) >> 16); +} + +/*! Multiplies the two given fix16_t's and returns the result. */ +static fix16_t fix16_mul(fix16_t inArg0, fix16_t inArg1); + +/*! Divides the first given fix16_t by the second and returns the result. */ +static fix16_t fix16_div(fix16_t inArg0, fix16_t inArg1); + +/*! Returns the square root of the given fix16_t. */ +static fix16_t fix16_sqrt(fix16_t inValue); + +/*! Returns the exponent (e^) of the given fix16_t. */ +static fix16_t fix16_exp(fix16_t inValue); + +static fix16_t fix16_mul(fix16_t inArg0, fix16_t inArg1) { + // Each argument is divided to 16-bit parts. + // AB + // * CD + // ----------- + // BD 16 * 16 -> 32 bit products + // CB + // AD + // AC + // |----| 64 bit product + uint32_t absArg0 = (uint32_t)((inArg0 >= 0) ? inArg0 : (-inArg0)); + uint32_t absArg1 = (uint32_t)((inArg1 >= 0) ? inArg1 : (-inArg1)); + uint32_t A = (absArg0 >> 16), C = (absArg1 >> 16); + uint32_t B = (absArg0 & 0xFFFF), D = (absArg1 & 0xFFFF); + + uint32_t AC = A * C; + uint32_t AD_CB = A * D + C * B; + uint32_t BD = B * D; + + uint32_t product_hi = AC + (AD_CB >> 16); + + // Handle carry from lower 32 bits to upper part of result. + uint32_t ad_cb_temp = AD_CB << 16; + uint32_t product_lo = BD + ad_cb_temp; + if (product_lo < BD) + product_hi++; + +#ifndef FIXMATH_NO_OVERFLOW + // The upper 17 bits should all be zero. + if (product_hi >> 15) + return (fix16_t)FIX16_OVERFLOW; +#endif + +#ifdef FIXMATH_NO_ROUNDING + fix16_t result = (fix16_t)((product_hi << 16) | (product_lo >> 16)); + if ((inArg0 < 0) != (inArg1 < 0)) + result = -result; + return result; +#else + // Adding 0x8000 (= 0.5) and then using right shift + // achieves proper rounding to result. + // Handle carry from lower to upper part. + uint32_t product_lo_tmp = product_lo; + product_lo += 0x8000; + if (product_lo < product_lo_tmp) + product_hi++; + + // Discard the lowest 16 bits and convert back to signed result. + fix16_t result = (fix16_t)((product_hi << 16) | (product_lo >> 16)); + if ((inArg0 < 0) != (inArg1 < 0)) + result = -result; + return result; +#endif +} + +static fix16_t fix16_div(fix16_t a, fix16_t b) { + // This uses the basic binary restoring division algorithm. + // It appears to be faster to do the whole division manually than + // trying to compose a 64-bit divide out of 32-bit divisions on + // platforms without hardware divide. + + if (b == 0) + return (fix16_t)FIX16_MINIMUM; + + uint32_t remainder = (uint32_t)((a >= 0) ? a : (-a)); + uint32_t divider = (uint32_t)((b >= 0) ? b : (-b)); + + uint32_t quotient = 0; + uint32_t bit = 0x10000; + + /* The algorithm requires D >= R */ + while (divider < remainder) { + divider <<= 1; + bit <<= 1; + } + +#ifndef FIXMATH_NO_OVERFLOW + if (!bit) + return (fix16_t)FIX16_OVERFLOW; +#endif + + if (divider & 0x80000000) { + // Perform one step manually to avoid overflows later. + // We know that divider's bottom bit is 0 here. + if (remainder >= divider) { + quotient |= bit; + remainder -= divider; + } + divider >>= 1; + bit >>= 1; + } + + /* Main division loop */ + while (bit && remainder) { + if (remainder >= divider) { + quotient |= bit; + remainder -= divider; + } + + remainder <<= 1; + bit >>= 1; + } + +#ifndef FIXMATH_NO_ROUNDING + if (remainder >= divider) { + quotient++; + } +#endif + + fix16_t result = (fix16_t)quotient; + + /* Figure out the sign of result */ + if ((a < 0) != (b < 0)) { +#ifndef FIXMATH_NO_OVERFLOW + if (result == FIX16_MINIMUM) + return (fix16_t)FIX16_OVERFLOW; +#endif + + result = -result; + } + + return result; +} + +static fix16_t fix16_sqrt(fix16_t x) { + // It is assumed that x is not negative + + uint32_t num = (uint32_t)x; + uint32_t result = 0; + uint32_t bit; + uint8_t n; + + bit = (uint32_t)1 << 30; + while (bit > num) + bit >>= 2; + + // The main part is executed twice, in order to avoid + // using 64 bit values in computations. + for (n = 0; n < 2; n++) { + // First we get the top 24 bits of the answer. + while (bit) { + if (num >= result + bit) { + num -= result + bit; + result = (result >> 1) + bit; + } else { + result = (result >> 1); + } + bit >>= 2; + } + + if (n == 0) { + // Then process it again to get the lowest 8 bits. + if (num > 65535) { + // The remainder 'num' is too large to be shifted left + // by 16, so we have to add 1 to result manually and + // adjust 'num' accordingly. + // num = a - (result + 0.5)^2 + // = num + result^2 - (result + 0.5)^2 + // = num - result - 0.5 + num -= result; + num = (num << 16) - 0x8000; + result = (result << 16) + 0x8000; + } else { + num <<= 16; + result <<= 16; + } + + bit = 1 << 14; + } + } + +#ifndef FIXMATH_NO_ROUNDING + // Finally, if next bit would have been 1, round the result upwards. + if (num > result) { + result++; + } +#endif + + return (fix16_t)result; +} + +static fix16_t fix16_exp(fix16_t x) { + // Function to approximate exp(); optimized more for code size than speed + + // exp(x) for x = +/- {1, 1/8, 1/64, 1/512} +#define NUM_EXP_VALUES 4 + static const fix16_t exp_pos_values[NUM_EXP_VALUES] = { + F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)}; + static const fix16_t exp_neg_values[NUM_EXP_VALUES] = { + F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)}; + const fix16_t* exp_values; + + fix16_t res, arg; + uint16_t i; + + if (x >= F16(10.3972)) + return FIX16_MAXIMUM; + if (x <= F16(-11.7835)) + return 0; + + if (x < 0) { + x = -x; + exp_values = exp_neg_values; + } else { + exp_values = exp_pos_values; + } + + res = FIX16_ONE; + arg = FIX16_ONE; + for (i = 0; i < NUM_EXP_VALUES; i++) { + while (x >= arg) { + res = fix16_mul(res, exp_values[i]); + x -= arg; + } + arg >>= 3; + } + return res; +} + +static void VocAlgorithm__init_instances(VocAlgorithmParams* params); +static void +VocAlgorithm__mean_variance_estimator__init(VocAlgorithmParams* params); +static void VocAlgorithm__mean_variance_estimator___init_instances( + VocAlgorithmParams* params); +static void VocAlgorithm__mean_variance_estimator__set_parameters( + VocAlgorithmParams* params, fix16_t std_initial, + fix16_t tau_mean_variance_hours, fix16_t gating_max_duration_minutes); +static void +VocAlgorithm__mean_variance_estimator__set_states(VocAlgorithmParams* params, + fix16_t mean, fix16_t std, + fix16_t uptime_gamma); +static fix16_t +VocAlgorithm__mean_variance_estimator__get_std(VocAlgorithmParams* params); +static fix16_t +VocAlgorithm__mean_variance_estimator__get_mean(VocAlgorithmParams* params); +static void VocAlgorithm__mean_variance_estimator___calculate_gamma( + VocAlgorithmParams* params, fix16_t voc_index_from_prior); +static void VocAlgorithm__mean_variance_estimator__process( + VocAlgorithmParams* params, fix16_t sraw, fix16_t voc_index_from_prior); +static void VocAlgorithm__mean_variance_estimator___sigmoid__init( + VocAlgorithmParams* params); +static void VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + VocAlgorithmParams* params, fix16_t L, fix16_t X0, fix16_t K); +static fix16_t VocAlgorithm__mean_variance_estimator___sigmoid__process( + VocAlgorithmParams* params, fix16_t sample); +static void VocAlgorithm__mox_model__init(VocAlgorithmParams* params); +static void VocAlgorithm__mox_model__set_parameters(VocAlgorithmParams* params, + fix16_t SRAW_STD, + fix16_t SRAW_MEAN); +static fix16_t VocAlgorithm__mox_model__process(VocAlgorithmParams* params, + fix16_t sraw); +static void VocAlgorithm__sigmoid_scaled__init(VocAlgorithmParams* params); +static void +VocAlgorithm__sigmoid_scaled__set_parameters(VocAlgorithmParams* params, + fix16_t offset); +static fix16_t VocAlgorithm__sigmoid_scaled__process(VocAlgorithmParams* params, + fix16_t sample); +static void VocAlgorithm__adaptive_lowpass__init(VocAlgorithmParams* params); +static void +VocAlgorithm__adaptive_lowpass__set_parameters(VocAlgorithmParams* params); +static fix16_t +VocAlgorithm__adaptive_lowpass__process(VocAlgorithmParams* params, + fix16_t sample); + +void VocAlgorithm_init(VocAlgorithmParams* params) { + + params->mVoc_Index_Offset = F16(VocAlgorithm_VOC_INDEX_OFFSET_DEFAULT); + params->mTau_Mean_Variance_Hours = + F16(VocAlgorithm_TAU_MEAN_VARIANCE_HOURS); + params->mGating_Max_Duration_Minutes = + F16(VocAlgorithm_GATING_MAX_DURATION_MINUTES); + params->mSraw_Std_Initial = F16(VocAlgorithm_SRAW_STD_INITIAL); + params->mUptime = F16(0.); + params->mSraw = F16(0.); + params->mVoc_Index = 0; + VocAlgorithm__init_instances(params); +} + +static void VocAlgorithm__init_instances(VocAlgorithmParams* params) { + + VocAlgorithm__mean_variance_estimator__init(params); + VocAlgorithm__mean_variance_estimator__set_parameters( + params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, + params->mGating_Max_Duration_Minutes); + VocAlgorithm__mox_model__init(params); + VocAlgorithm__mox_model__set_parameters( + params, VocAlgorithm__mean_variance_estimator__get_std(params), + VocAlgorithm__mean_variance_estimator__get_mean(params)); + VocAlgorithm__sigmoid_scaled__init(params); + VocAlgorithm__sigmoid_scaled__set_parameters(params, + params->mVoc_Index_Offset); + VocAlgorithm__adaptive_lowpass__init(params); + VocAlgorithm__adaptive_lowpass__set_parameters(params); +} + +void VocAlgorithm_get_states(VocAlgorithmParams* params, int32_t* state0, + int32_t* state1) { + + *state0 = VocAlgorithm__mean_variance_estimator__get_mean(params); + *state1 = VocAlgorithm__mean_variance_estimator__get_std(params); + return; +} + +void VocAlgorithm_set_states(VocAlgorithmParams* params, int32_t state0, + int32_t state1) { + + VocAlgorithm__mean_variance_estimator__set_states( + params, state0, state1, F16(VocAlgorithm_PERSISTENCE_UPTIME_GAMMA)); + params->mSraw = state0; +} + +void VocAlgorithm_set_tuning_parameters(VocAlgorithmParams* params, + int32_t voc_index_offset, + int32_t learning_time_hours, + int32_t gating_max_duration_minutes, + int32_t std_initial) { + + params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset)); + params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours)); + params->mGating_Max_Duration_Minutes = + (fix16_from_int(gating_max_duration_minutes)); + params->mSraw_Std_Initial = (fix16_from_int(std_initial)); + VocAlgorithm__init_instances(params); +} + +void VocAlgorithm_process(VocAlgorithmParams* params, int32_t sraw, + int32_t* voc_index) { + + if ((params->mUptime <= F16(VocAlgorithm_INITIAL_BLACKOUT))) { + params->mUptime = + (params->mUptime + F16(VocAlgorithm_SAMPLING_INTERVAL)); + } else { + if (((sraw > 0) && (sraw < 65000))) { + if ((sraw < 20001)) { + sraw = 20001; + } else if ((sraw > 52767)) { + sraw = 52767; + } + params->mSraw = (fix16_from_int((sraw - 20000))); + } + params->mVoc_Index = + VocAlgorithm__mox_model__process(params, params->mSraw); + params->mVoc_Index = + VocAlgorithm__sigmoid_scaled__process(params, params->mVoc_Index); + params->mVoc_Index = + VocAlgorithm__adaptive_lowpass__process(params, params->mVoc_Index); + if ((params->mVoc_Index < F16(0.5))) { + params->mVoc_Index = F16(0.5); + } + if ((params->mSraw > F16(0.))) { + VocAlgorithm__mean_variance_estimator__process( + params, params->mSraw, params->mVoc_Index); + VocAlgorithm__mox_model__set_parameters( + params, VocAlgorithm__mean_variance_estimator__get_std(params), + VocAlgorithm__mean_variance_estimator__get_mean(params)); + } + } + *voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5)))); + return; +} + +static void +VocAlgorithm__mean_variance_estimator__init(VocAlgorithmParams* params) { + + VocAlgorithm__mean_variance_estimator__set_parameters(params, F16(0.), + F16(0.), F16(0.)); + VocAlgorithm__mean_variance_estimator___init_instances(params); +} + +static void VocAlgorithm__mean_variance_estimator___init_instances( + VocAlgorithmParams* params) { + + VocAlgorithm__mean_variance_estimator___sigmoid__init(params); +} + +static void VocAlgorithm__mean_variance_estimator__set_parameters( + VocAlgorithmParams* params, fix16_t std_initial, + fix16_t tau_mean_variance_hours, fix16_t gating_max_duration_minutes) { + + params->m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes = + gating_max_duration_minutes; + params->m_Mean_Variance_Estimator___Initialized = false; + params->m_Mean_Variance_Estimator___Mean = F16(0.); + params->m_Mean_Variance_Estimator___Sraw_Offset = F16(0.); + params->m_Mean_Variance_Estimator___Std = std_initial; + params->m_Mean_Variance_Estimator___Gamma = + (fix16_div(F16((VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING * + (VocAlgorithm_SAMPLING_INTERVAL / 3600.))), + (tau_mean_variance_hours + + F16((VocAlgorithm_SAMPLING_INTERVAL / 3600.))))); + params->m_Mean_Variance_Estimator___Gamma_Initial_Mean = + F16(((VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING * + VocAlgorithm_SAMPLING_INTERVAL) / + (VocAlgorithm_TAU_INITIAL_MEAN + VocAlgorithm_SAMPLING_INTERVAL))); + params->m_Mean_Variance_Estimator___Gamma_Initial_Variance = F16( + ((VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING * + VocAlgorithm_SAMPLING_INTERVAL) / + (VocAlgorithm_TAU_INITIAL_VARIANCE + VocAlgorithm_SAMPLING_INTERVAL))); + params->m_Mean_Variance_Estimator__Gamma_Mean = F16(0.); + params->m_Mean_Variance_Estimator__Gamma_Variance = F16(0.); + params->m_Mean_Variance_Estimator___Uptime_Gamma = F16(0.); + params->m_Mean_Variance_Estimator___Uptime_Gating = F16(0.); + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = F16(0.); +} + +static void +VocAlgorithm__mean_variance_estimator__set_states(VocAlgorithmParams* params, + fix16_t mean, fix16_t std, + fix16_t uptime_gamma) { + + params->m_Mean_Variance_Estimator___Mean = mean; + params->m_Mean_Variance_Estimator___Std = std; + params->m_Mean_Variance_Estimator___Uptime_Gamma = uptime_gamma; + params->m_Mean_Variance_Estimator___Initialized = true; +} + +static fix16_t +VocAlgorithm__mean_variance_estimator__get_std(VocAlgorithmParams* params) { + + return params->m_Mean_Variance_Estimator___Std; +} + +static fix16_t +VocAlgorithm__mean_variance_estimator__get_mean(VocAlgorithmParams* params) { + + return (params->m_Mean_Variance_Estimator___Mean + + params->m_Mean_Variance_Estimator___Sraw_Offset); +} + +static void VocAlgorithm__mean_variance_estimator___calculate_gamma( + VocAlgorithmParams* params, fix16_t voc_index_from_prior) { + + fix16_t uptime_limit; + fix16_t sigmoid_gamma_mean; + fix16_t gamma_mean; + fix16_t gating_threshold_mean; + fix16_t sigmoid_gating_mean; + fix16_t sigmoid_gamma_variance; + fix16_t gamma_variance; + fix16_t gating_threshold_variance; + fix16_t sigmoid_gating_variance; + + uptime_limit = F16((VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__FIX16_MAX - + VocAlgorithm_SAMPLING_INTERVAL)); + if ((params->m_Mean_Variance_Estimator___Uptime_Gamma < uptime_limit)) { + params->m_Mean_Variance_Estimator___Uptime_Gamma = + (params->m_Mean_Variance_Estimator___Uptime_Gamma + + F16(VocAlgorithm_SAMPLING_INTERVAL)); + } + if ((params->m_Mean_Variance_Estimator___Uptime_Gating < uptime_limit)) { + params->m_Mean_Variance_Estimator___Uptime_Gating = + (params->m_Mean_Variance_Estimator___Uptime_Gating + + F16(VocAlgorithm_SAMPLING_INTERVAL)); + } + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(1.), F16(VocAlgorithm_INIT_DURATION_MEAN), + F16(VocAlgorithm_INIT_TRANSITION_MEAN)); + sigmoid_gamma_mean = + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gamma); + gamma_mean = + (params->m_Mean_Variance_Estimator___Gamma + + (fix16_mul((params->m_Mean_Variance_Estimator___Gamma_Initial_Mean - + params->m_Mean_Variance_Estimator___Gamma), + sigmoid_gamma_mean))); + gating_threshold_mean = + (F16(VocAlgorithm_GATING_THRESHOLD) + + (fix16_mul( + F16((VocAlgorithm_GATING_THRESHOLD_INITIAL - + VocAlgorithm_GATING_THRESHOLD)), + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gating)))); + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(1.), gating_threshold_mean, + F16(VocAlgorithm_GATING_THRESHOLD_TRANSITION)); + sigmoid_gating_mean = + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, voc_index_from_prior); + params->m_Mean_Variance_Estimator__Gamma_Mean = + (fix16_mul(sigmoid_gating_mean, gamma_mean)); + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(1.), F16(VocAlgorithm_INIT_DURATION_VARIANCE), + F16(VocAlgorithm_INIT_TRANSITION_VARIANCE)); + sigmoid_gamma_variance = + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gamma); + gamma_variance = + (params->m_Mean_Variance_Estimator___Gamma + + (fix16_mul( + (params->m_Mean_Variance_Estimator___Gamma_Initial_Variance - + params->m_Mean_Variance_Estimator___Gamma), + (sigmoid_gamma_variance - sigmoid_gamma_mean)))); + gating_threshold_variance = + (F16(VocAlgorithm_GATING_THRESHOLD) + + (fix16_mul( + F16((VocAlgorithm_GATING_THRESHOLD_INITIAL - + VocAlgorithm_GATING_THRESHOLD)), + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gating)))); + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(1.), gating_threshold_variance, + F16(VocAlgorithm_GATING_THRESHOLD_TRANSITION)); + sigmoid_gating_variance = + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, voc_index_from_prior); + params->m_Mean_Variance_Estimator__Gamma_Variance = + (fix16_mul(sigmoid_gating_variance, gamma_variance)); + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = + (params->m_Mean_Variance_Estimator___Gating_Duration_Minutes + + (fix16_mul(F16((VocAlgorithm_SAMPLING_INTERVAL / 60.)), + ((fix16_mul((F16(1.) - sigmoid_gating_mean), + F16((1. + VocAlgorithm_GATING_MAX_RATIO)))) - + F16(VocAlgorithm_GATING_MAX_RATIO))))); + if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes < + F16(0.))) { + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = F16(0.); + } + if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes > + params->m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes)) { + params->m_Mean_Variance_Estimator___Uptime_Gating = F16(0.); + } +} + +static void VocAlgorithm__mean_variance_estimator__process( + VocAlgorithmParams* params, fix16_t sraw, fix16_t voc_index_from_prior) { + + fix16_t delta_sgp; + fix16_t c; + fix16_t additional_scaling; + + if ((params->m_Mean_Variance_Estimator___Initialized == false)) { + params->m_Mean_Variance_Estimator___Initialized = true; + params->m_Mean_Variance_Estimator___Sraw_Offset = sraw; + params->m_Mean_Variance_Estimator___Mean = F16(0.); + } else { + if (((params->m_Mean_Variance_Estimator___Mean >= F16(100.)) || + (params->m_Mean_Variance_Estimator___Mean <= F16(-100.)))) { + params->m_Mean_Variance_Estimator___Sraw_Offset = + (params->m_Mean_Variance_Estimator___Sraw_Offset + + params->m_Mean_Variance_Estimator___Mean); + params->m_Mean_Variance_Estimator___Mean = F16(0.); + } + sraw = (sraw - params->m_Mean_Variance_Estimator___Sraw_Offset); + VocAlgorithm__mean_variance_estimator___calculate_gamma( + params, voc_index_from_prior); + delta_sgp = (fix16_div( + (sraw - params->m_Mean_Variance_Estimator___Mean), + F16(VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING))); + if ((delta_sgp < F16(0.))) { + c = (params->m_Mean_Variance_Estimator___Std - delta_sgp); + } else { + c = (params->m_Mean_Variance_Estimator___Std + delta_sgp); + } + additional_scaling = F16(1.); + if ((c > F16(1440.))) { + additional_scaling = F16(4.); + } + params->m_Mean_Variance_Estimator___Std = (fix16_mul( + fix16_sqrt((fix16_mul( + additional_scaling, + (F16(VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING) - + params->m_Mean_Variance_Estimator__Gamma_Variance)))), + fix16_sqrt(( + (fix16_mul( + params->m_Mean_Variance_Estimator___Std, + (fix16_div( + params->m_Mean_Variance_Estimator___Std, + (fix16_mul( + F16(VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING), + additional_scaling)))))) + + (fix16_mul( + (fix16_div( + (fix16_mul( + params->m_Mean_Variance_Estimator__Gamma_Variance, + delta_sgp)), + additional_scaling)), + delta_sgp)))))); + params->m_Mean_Variance_Estimator___Mean = + (params->m_Mean_Variance_Estimator___Mean + + (fix16_mul(params->m_Mean_Variance_Estimator__Gamma_Mean, + delta_sgp))); + } +} + +static void VocAlgorithm__mean_variance_estimator___sigmoid__init( + VocAlgorithmParams* params) { + + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(0.), F16(0.), F16(0.)); +} + +static void VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + VocAlgorithmParams* params, fix16_t L, fix16_t X0, fix16_t K) { + + params->m_Mean_Variance_Estimator___Sigmoid__L = L; + params->m_Mean_Variance_Estimator___Sigmoid__K = K; + params->m_Mean_Variance_Estimator___Sigmoid__X0 = X0; +} + +static fix16_t VocAlgorithm__mean_variance_estimator___sigmoid__process( + VocAlgorithmParams* params, fix16_t sample) { + + fix16_t x; + + x = (fix16_mul(params->m_Mean_Variance_Estimator___Sigmoid__K, + (sample - params->m_Mean_Variance_Estimator___Sigmoid__X0))); + if ((x < F16(-50.))) { + return params->m_Mean_Variance_Estimator___Sigmoid__L; + } else if ((x > F16(50.))) { + return F16(0.); + } else { + return (fix16_div(params->m_Mean_Variance_Estimator___Sigmoid__L, + (F16(1.) + fix16_exp(x)))); + } +} + +static void VocAlgorithm__mox_model__init(VocAlgorithmParams* params) { + + VocAlgorithm__mox_model__set_parameters(params, F16(1.), F16(0.)); +} + +static void VocAlgorithm__mox_model__set_parameters(VocAlgorithmParams* params, + fix16_t SRAW_STD, + fix16_t SRAW_MEAN) { + + params->m_Mox_Model__Sraw_Std = SRAW_STD; + params->m_Mox_Model__Sraw_Mean = SRAW_MEAN; +} + +static fix16_t VocAlgorithm__mox_model__process(VocAlgorithmParams* params, + fix16_t sraw) { + + return (fix16_mul((fix16_div((sraw - params->m_Mox_Model__Sraw_Mean), + (-(params->m_Mox_Model__Sraw_Std + + F16(VocAlgorithm_SRAW_STD_BONUS))))), + F16(VocAlgorithm_VOC_INDEX_GAIN))); +} + +static void VocAlgorithm__sigmoid_scaled__init(VocAlgorithmParams* params) { + + VocAlgorithm__sigmoid_scaled__set_parameters(params, F16(0.)); +} + +static void +VocAlgorithm__sigmoid_scaled__set_parameters(VocAlgorithmParams* params, + fix16_t offset) { + + params->m_Sigmoid_Scaled__Offset = offset; +} + +static fix16_t VocAlgorithm__sigmoid_scaled__process(VocAlgorithmParams* params, + fix16_t sample) { + + fix16_t x; + fix16_t shift; + + x = (fix16_mul(F16(VocAlgorithm_SIGMOID_K), + (sample - F16(VocAlgorithm_SIGMOID_X0)))); + if ((x < F16(-50.))) { + return F16(VocAlgorithm_SIGMOID_L); + } else if ((x > F16(50.))) { + return F16(0.); + } else { + if ((sample >= F16(0.))) { + shift = (fix16_div( + (F16(VocAlgorithm_SIGMOID_L) - + (fix16_mul(F16(5.), params->m_Sigmoid_Scaled__Offset))), + F16(4.))); + return ((fix16_div((F16(VocAlgorithm_SIGMOID_L) + shift), + (F16(1.) + fix16_exp(x)))) - + shift); + } else { + return (fix16_mul( + (fix16_div(params->m_Sigmoid_Scaled__Offset, + F16(VocAlgorithm_VOC_INDEX_OFFSET_DEFAULT))), + (fix16_div(F16(VocAlgorithm_SIGMOID_L), + (F16(1.) + fix16_exp(x)))))); + } + } +} + +static void VocAlgorithm__adaptive_lowpass__init(VocAlgorithmParams* params) { + + VocAlgorithm__adaptive_lowpass__set_parameters(params); +} + +static void +VocAlgorithm__adaptive_lowpass__set_parameters(VocAlgorithmParams* params) { + + params->m_Adaptive_Lowpass__A1 = + F16((VocAlgorithm_SAMPLING_INTERVAL / + (VocAlgorithm_LP_TAU_FAST + VocAlgorithm_SAMPLING_INTERVAL))); + params->m_Adaptive_Lowpass__A2 = + F16((VocAlgorithm_SAMPLING_INTERVAL / + (VocAlgorithm_LP_TAU_SLOW + VocAlgorithm_SAMPLING_INTERVAL))); + params->m_Adaptive_Lowpass___Initialized = false; +} + +static fix16_t +VocAlgorithm__adaptive_lowpass__process(VocAlgorithmParams* params, + fix16_t sample) { + + fix16_t abs_delta; + fix16_t F1; + fix16_t tau_a; + fix16_t a3; + + if ((params->m_Adaptive_Lowpass___Initialized == false)) { + params->m_Adaptive_Lowpass___X1 = sample; + params->m_Adaptive_Lowpass___X2 = sample; + params->m_Adaptive_Lowpass___X3 = sample; + params->m_Adaptive_Lowpass___Initialized = true; + } + params->m_Adaptive_Lowpass___X1 = + ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass__A1), + params->m_Adaptive_Lowpass___X1)) + + (fix16_mul(params->m_Adaptive_Lowpass__A1, sample))); + params->m_Adaptive_Lowpass___X2 = + ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass__A2), + params->m_Adaptive_Lowpass___X2)) + + (fix16_mul(params->m_Adaptive_Lowpass__A2, sample))); + abs_delta = + (params->m_Adaptive_Lowpass___X1 - params->m_Adaptive_Lowpass___X2); + if ((abs_delta < F16(0.))) { + abs_delta = (-abs_delta); + } + F1 = fix16_exp((fix16_mul(F16(VocAlgorithm_LP_ALPHA), abs_delta))); + tau_a = + ((fix16_mul(F16((VocAlgorithm_LP_TAU_SLOW - VocAlgorithm_LP_TAU_FAST)), + F1)) + + F16(VocAlgorithm_LP_TAU_FAST)); + a3 = (fix16_div(F16(VocAlgorithm_SAMPLING_INTERVAL), + (F16(VocAlgorithm_SAMPLING_INTERVAL) + tau_a))); + params->m_Adaptive_Lowpass___X3 = + ((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass___X3)) + + (fix16_mul(a3, sample))); + return params->m_Adaptive_Lowpass___X3; +} Index: src/sys/dev/i2c/sensirion_voc_algorithm.h diff -u /dev/null src/sys/dev/i2c/sensirion_voc_algorithm.h:1.1 --- /dev/null Thu Oct 14 13:54:46 2021 +++ src/sys/dev/i2c/sensirion_voc_algorithm.h Thu Oct 14 13:54:46 2021 @@ -0,0 +1,191 @@ +/* + * $NetBSD: sensirion_voc_algorithm.h,v 1.1 2021/10/14 13:54:46 brad Exp $ + */ + +/* + * Copyright (c) 2021, 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. + */ + +#ifndef VOCALGORITHM_H_ +#define VOCALGORITHM_H_ + +#include "sensirion_arch_config.h" + +/* The fixed point arithmetic parts of this code were originally created by + * https://github.com/PetteriAimonen/libfixmath + */ + +typedef int32_t fix16_t; + +#define F16(x) \ + ((fix16_t)(((x) >= 0) ? ((x)*65536.0 + 0.5) : ((x)*65536.0 - 0.5))) + +// Should be set by the building toolchain +#ifndef LIBRARY_VERSION_NAME +#define LIBRARY_VERSION_NAME "custom build" +#endif + +#define VocAlgorithm_SAMPLING_INTERVAL (1.) +#define VocAlgorithm_INITIAL_BLACKOUT (45.) +#define VocAlgorithm_VOC_INDEX_GAIN (230.) +#define VocAlgorithm_SRAW_STD_INITIAL (50.) +#define VocAlgorithm_SRAW_STD_BONUS (220.) +#define VocAlgorithm_TAU_MEAN_VARIANCE_HOURS (12.) +#define VocAlgorithm_TAU_INITIAL_MEAN (20.) +#define VocAlgorithm_INIT_DURATION_MEAN ((3600. * 0.75)) +#define VocAlgorithm_INIT_TRANSITION_MEAN (0.01) +#define VocAlgorithm_TAU_INITIAL_VARIANCE (2500.) +#define VocAlgorithm_INIT_DURATION_VARIANCE ((3600. * 1.45)) +#define VocAlgorithm_INIT_TRANSITION_VARIANCE (0.01) +#define VocAlgorithm_GATING_THRESHOLD (340.) +#define VocAlgorithm_GATING_THRESHOLD_INITIAL (510.) +#define VocAlgorithm_GATING_THRESHOLD_TRANSITION (0.09) +#define VocAlgorithm_GATING_MAX_DURATION_MINUTES ((60. * 3.)) +#define VocAlgorithm_GATING_MAX_RATIO (0.3) +#define VocAlgorithm_SIGMOID_L (500.) +#define VocAlgorithm_SIGMOID_K (-0.0065) +#define VocAlgorithm_SIGMOID_X0 (213.) +#define VocAlgorithm_VOC_INDEX_OFFSET_DEFAULT (100.) +#define VocAlgorithm_LP_TAU_FAST (20.0) +#define VocAlgorithm_LP_TAU_SLOW (500.0) +#define VocAlgorithm_LP_ALPHA (-0.2) +#define VocAlgorithm_PERSISTENCE_UPTIME_GAMMA ((3. * 3600.)) +#define VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING (64.) +#define VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__FIX16_MAX (32767.) + +/** + * Struct to hold all the states of the VOC algorithm. + */ +typedef struct { + fix16_t mVoc_Index_Offset; + fix16_t mTau_Mean_Variance_Hours; + fix16_t mGating_Max_Duration_Minutes; + fix16_t mSraw_Std_Initial; + fix16_t mUptime; + fix16_t mSraw; + fix16_t mVoc_Index; + fix16_t m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes; + bool m_Mean_Variance_Estimator___Initialized; + fix16_t m_Mean_Variance_Estimator___Mean; + fix16_t m_Mean_Variance_Estimator___Sraw_Offset; + fix16_t m_Mean_Variance_Estimator___Std; + fix16_t m_Mean_Variance_Estimator___Gamma; + fix16_t m_Mean_Variance_Estimator___Gamma_Initial_Mean; + fix16_t m_Mean_Variance_Estimator___Gamma_Initial_Variance; + fix16_t m_Mean_Variance_Estimator__Gamma_Mean; + fix16_t m_Mean_Variance_Estimator__Gamma_Variance; + fix16_t m_Mean_Variance_Estimator___Uptime_Gamma; + fix16_t m_Mean_Variance_Estimator___Uptime_Gating; + fix16_t m_Mean_Variance_Estimator___Gating_Duration_Minutes; + fix16_t m_Mean_Variance_Estimator___Sigmoid__L; + fix16_t m_Mean_Variance_Estimator___Sigmoid__K; + fix16_t m_Mean_Variance_Estimator___Sigmoid__X0; + fix16_t m_Mox_Model__Sraw_Std; + fix16_t m_Mox_Model__Sraw_Mean; + fix16_t m_Sigmoid_Scaled__Offset; + fix16_t m_Adaptive_Lowpass__A1; + fix16_t m_Adaptive_Lowpass__A2; + bool m_Adaptive_Lowpass___Initialized; + fix16_t m_Adaptive_Lowpass___X1; + fix16_t m_Adaptive_Lowpass___X2; + fix16_t m_Adaptive_Lowpass___X3; +} VocAlgorithmParams; + +/** + * Initialize the VOC algorithm parameters. Call this once at the beginning or + * whenever the sensor stopped measurements. + * @param params Pointer to the VocAlgorithmParams struct + */ +void VocAlgorithm_init(VocAlgorithmParams* params); + +/** + * Get current algorithm states. Retrieved values can be used in + * VocAlgorithm_set_states() to resume operation after a short interruption, + * skipping initial learning phase. This feature can only be used after at least + * 3 hours of continuous operation. + * @param params Pointer to the VocAlgorithmParams struct + * @param state0 State0 to be stored + * @param state1 State1 to be stored + */ +void VocAlgorithm_get_states(VocAlgorithmParams* params, int32_t* state0, + int32_t* state1); + +/** + * Set previously retrieved algorithm states to resume operation after a short + * interruption, skipping initial learning phase. This feature should not be + * used after inerruptions of more than 10 minutes. Call this once after + * VocAlgorithm_init() and the optional VocAlgorithm_set_tuning_parameters(), if + * desired. Otherwise, the algorithm will start with initial learning phase. + * @param params Pointer to the VocAlgorithmParams struct + * @param state0 State0 to be restored + * @param state1 State1 to be restored + */ +void VocAlgorithm_set_states(VocAlgorithmParams* params, int32_t state0, + int32_t state1); + +/** + * Set parameters to customize the VOC algorithm. Call this once after + * VocAlgorithm_init(), if desired. Otherwise, the default values will be used. + * + * @param params Pointer to the VocAlgorithmParams struct + * @param voc_index_offset VOC index representing typical (average) + * conditions. Range 1..250, default 100 + * @param learning_time_hours Time constant of long-term estimator. + * Past events will be forgotten after about + * twice the learning time. + * Range 1..72 [hours], default 12 [hours] + * @param gating_max_duration_minutes Maximum duration of gating (freeze of + * estimator during high VOC index signal). + * 0 (no gating) or range 1..720 [minutes], + * default 180 [minutes] + * @param std_initial Initial estimate for standard deviation. + * Lower value boosts events during initial + * learning period, but may result in larger + * device-to-device variations. + * Range 10..500, default 50 + */ +void VocAlgorithm_set_tuning_parameters(VocAlgorithmParams* params, + int32_t voc_index_offset, + int32_t learning_time_hours, + int32_t gating_max_duration_minutes, + int32_t std_initial); + +/** + * Calculate the VOC index value from the raw sensor value. + * + * @param params Pointer to the VocAlgorithmParams struct + * @param sraw Raw value from the SGP40 sensor + * @param voc_index Calculated VOC index value from the raw sensor value. Zero + * during initial blackout period and 1..500 afterwards + */ +void VocAlgorithm_process(VocAlgorithmParams* params, int32_t sraw, + int32_t* voc_index); + +#endif /* VOCALGORITHM_H_ */ Index: src/sys/dev/i2c/sgp40.c diff -u /dev/null src/sys/dev/i2c/sgp40.c:1.1 --- /dev/null Thu Oct 14 13:54:46 2021 +++ src/sys/dev/i2c/sgp40.c Thu Oct 14 13:54:46 2021 @@ -0,0 +1,802 @@ +/* $NetBSD: sgp40.c,v 1.1 2021/10/14 13:54:46 brad Exp $ */ + +/* + * Copyright (c) 2021 Brad Spencer <b...@anduin.eldar.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGL`IGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/cdefs.h> +__KERNEL_RCSID(0, "$NetBSD: sgp40.c,v 1.1 2021/10/14 13:54:46 brad Exp $"); + +/* + Driver for the Sensirion SGP40 MOx gas sensor for air quality +*/ + +#include <sys/param.h> +#include <sys/systm.h> +#include <sys/kernel.h> +#include <sys/device.h> +#include <sys/module.h> +#include <sys/sysctl.h> +#include <sys/mutex.h> +#include <sys/condvar.h> +#include <sys/kthread.h> + +#include <dev/sysmon/sysmonvar.h> +#include <dev/i2c/i2cvar.h> +#include <dev/i2c/sgp40reg.h> +#include <dev/i2c/sgp40var.h> + +#include <dev/i2c/sensirion_arch_config.h> +#include <dev/i2c/sensirion_voc_algorithm.h> + +static uint8_t sgp40_crc(uint8_t *, size_t); +static int sgp40_cmdr(struct sgp40_sc *, uint16_t, uint8_t *, uint8_t, uint8_t *, size_t); +static int sgp40_poke(i2c_tag_t, i2c_addr_t, bool); +static int sgp40_match(device_t, cfdata_t, void *); +static void sgp40_attach(device_t, device_t, void *); +static int sgp40_detach(device_t, int); +static void sgp40_refresh(struct sysmon_envsys *, envsys_data_t *); +static int sgp40_verify_sysctl(SYSCTLFN_ARGS); +static int sgp40_verify_temp_sysctl(SYSCTLFN_ARGS); +static int sgp40_verify_rh_sysctl(SYSCTLFN_ARGS); +static void sgp40_thread(void *); +static void sgp40_stop_thread(void *); +static void sgp40_take_measurement(void *, VocAlgorithmParams *); + +#define SGP40_DEBUG +#ifdef SGP40_DEBUG +#define DPRINTF(s, l, x) \ + do { \ + if (l <= s->sc_sgp40debug) \ + printf x; \ + } while (/*CONSTCOND*/0) +#else +#define DPRINTF(s, l, x) +#endif + +CFATTACH_DECL_NEW(sgp40mox, sizeof(struct sgp40_sc), + sgp40_match, sgp40_attach, sgp40_detach, NULL); + +static struct sgp40_sensor sgp40_sensors[] = { + { + .desc = "VOC index", + .type = ENVSYS_INTEGER, + } +}; + +static struct sgp40_timing sgp40_timings[] = { + { + .cmd = SGP40_MEASURE_RAW, + .typicaldelay = 25000, + }, + { + .cmd = SGP40_MEASURE_TEST, + .typicaldelay = 240000, + }, + { + .cmd = SGP40_HEATER_OFF, + .typicaldelay = 100, + }, + { + .cmd = SGP40_GET_SERIAL_NUMBER, + .typicaldelay = 100, + }, + { + .cmd = SGP40_GET_FEATURESET, + .typicaldelay = 1000, + } +}; + +void +sgp40_thread(void *aux) +{ + struct sgp40_sc *sc = aux; + int rv; + VocAlgorithmParams voc_algorithm_params; + + mutex_enter(&sc->sc_threadmutex); + + VocAlgorithm_init(&voc_algorithm_params); + + while (sc->sc_stopping == false) { + rv = cv_timedwait(&sc->sc_condvar, &sc->sc_threadmutex, mstohz(1000)); + if (rv == EWOULDBLOCK && sc->sc_stopping == false) { + sgp40_take_measurement(sc,&voc_algorithm_params); + } + } + mutex_exit(&sc->sc_threadmutex); + kthread_exit(0); +} + +static void +sgp40_stop_thread(void *aux) +{ + struct sgp40_sc *sc; + sc = aux; + int error; + + mutex_enter(&sc->sc_threadmutex); + sc->sc_stopping = true; + cv_signal(&sc->sc_condvar); + mutex_exit(&sc->sc_threadmutex); + + /* wait for the thread to exit */ + kthread_join(sc->sc_thread); + + mutex_enter(&sc->sc_mutex); + error = iic_acquire_bus(sc->sc_tag, 0); + if (error) { + DPRINTF(sc, 2, ("%s: Could not acquire iic bus for heater off in stop thread: %d\n", + device_xname(sc->sc_dev), error)); + } else { + error = sgp40_cmdr(sc, SGP40_HEATER_OFF,NULL,0,NULL,0); + if (error) { + DPRINTF(sc, 2, ("%s: Error turning heater off: %d\n", + device_xname(sc->sc_dev), error)); + } + iic_release_bus(sc->sc_tag, 0); + } + mutex_exit(&sc->sc_mutex); +} + +static int +sgp40_compute_temp_comp(int unconverted) +{ + /* The published algorithm for this conversion is: + (temp_in_celcius + 45) * 65535 / 175 + + However, this did not exactly yield the results that + the example in the data sheet, so something a little + different was done. + + (temp_in_celcius + 45) * 65536 / 175 + + This was also scaled up by 10^2 and then scaled back to + preserve some percision. 37449 is simply (65536 * 100) / 175 + and rounded. + */ + + return (((unconverted + 45) * 100) * 37449) / 10000; +} + +static int +sgp40_compute_rh_comp(int unconverted) +{ + int q; + + /* The published algorithm for this conversion is: + %rh * 65535 / 100 + + However, this did not exactly yield the results that + the example in the data sheet, so something a little + different was done. + + %rh * 65536 / 100 + + This was also scaled up by 10^2 and then scaled back to + preserve some percision. The value is also latched to 65535 + as an upper limit. + */ + + q = ((unconverted * 100) * 65536) / 10000; + if (q > 65535) + q = 65535; + return q; +} + +static void +sgp40_take_measurement(void *aux, VocAlgorithmParams* params) +{ + struct sgp40_sc *sc; + sc = aux; + uint8_t args[6]; + uint8_t buf[3]; + uint16_t rawmeasurement; + int error; + uint8_t crc; + uint16_t convertedrh, convertedtemp; + int32_t voc_index; + + mutex_enter(&sc->sc_mutex); + convertedrh = (uint16_t)sgp40_compute_rh_comp(sc->sc_rhcomp); + convertedtemp = (uint16_t)sgp40_compute_temp_comp(sc->sc_tempcomp); + + DPRINTF(sc, 2, ("%s: Converted RH and Temp: %04x %04x\n", + device_xname(sc->sc_dev), convertedrh, convertedtemp)); + + args[0] = convertedrh >> 8; + args[1] = convertedrh & 0x00ff; + args[2] = sgp40_crc(&args[0],2); + args[3] = convertedtemp >> 8; + args[4] = convertedtemp & 0x00ff; + args[5] = sgp40_crc(&args[3],2); + + /* The VOC algoritm has a black out time when it first starts to run + and does not return any indicator that is going on, so voc_index + in that case would be 0.. however, that is also a valid response + otherwise, although an unlikely one. + */ + error = iic_acquire_bus(sc->sc_tag, 0); + if (error) { + DPRINTF(sc, 2, ("%s: Could not acquire iic bus for take measurement: %d\n", + device_xname(sc->sc_dev), error)); + sc->sc_voc = 0; + sc->sc_vocvalid = false; + } else { + error = sgp40_cmdr(sc, SGP40_MEASURE_RAW, args, 6, buf, 3); + iic_release_bus(sc->sc_tag, 0); + if (error == 0) { + crc = sgp40_crc(&buf[0],2); + DPRINTF(sc, 2, ("%s: Raw ticks and crc: %02x%02x %02x %02x\n", + device_xname(sc->sc_dev), buf[0], buf[1], buf[2],crc)); + if (buf[2] == crc) { + rawmeasurement = buf[0] << 8; + rawmeasurement |= buf[1]; + VocAlgorithm_process(params, rawmeasurement, &voc_index); + DPRINTF(sc, 2, ("%s: VOC index: %d\n", + device_xname(sc->sc_dev), voc_index)); + sc->sc_voc = voc_index; + sc->sc_vocvalid = true; + } else { + sc->sc_voc = 0; + sc->sc_vocvalid = false; + } + } else { + DPRINTF(sc, 2, ("%s: Failed to get measurement %d\n", + device_xname(sc->sc_dev), error)); + sc->sc_voc = 0; + sc->sc_vocvalid = false; + } + } + + mutex_exit(&sc->sc_mutex); +} + +int +sgp40_verify_sysctl(SYSCTLFN_ARGS) +{ + int error, t; + struct sysctlnode node; + + node = *rnode; + t = *(int *)rnode->sysctl_data; + node.sysctl_data = &t; + error = sysctl_lookup(SYSCTLFN_CALL(&node)); + if (error || newp == NULL) + return error; + + if (t < 0) + return EINVAL; + + *(int *)rnode->sysctl_data = t; + + return 0; +} + +int +sgp40_verify_temp_sysctl(SYSCTLFN_ARGS) +{ + int error, t; + struct sysctlnode node; + + node = *rnode; + t = *(int *)rnode->sysctl_data; + node.sysctl_data = &t; + error = sysctl_lookup(SYSCTLFN_CALL(&node)); + if (error || newp == NULL) + return error; + + if (t < -45 || t > 130) + return EINVAL; + + *(int *)rnode->sysctl_data = t; + + return 0; +} + +int +sgp40_verify_rh_sysctl(SYSCTLFN_ARGS) +{ + int error, t; + struct sysctlnode node; + + node = *rnode; + t = *(int *)rnode->sysctl_data; + node.sysctl_data = &t; + error = sysctl_lookup(SYSCTLFN_CALL(&node)); + if (error || newp == NULL) + return error; + + if (t < 0 || t > 100) + return EINVAL; + + *(int *)rnode->sysctl_data = t; + + return 0; +} + +static int +sgp40_cmddelay(uint16_t cmd) +{ + int r = -1; + + for(int i = 0;i < __arraycount(sgp40_timings);i++) { + if (cmd == sgp40_timings[i].cmd) { + r = sgp40_timings[i].typicaldelay; + break; + } + } + + if (r == -1) { + panic("Bad command look up in cmd delay: cmd: %d\n",cmd); + } + + return r; +} + +static int +sgp40_cmd(i2c_tag_t tag, i2c_addr_t addr, uint8_t *cmd, + uint8_t clen, uint8_t *buf, size_t blen, int readattempts) +{ + int error; + int cmddelay; + uint16_t cmd16; + + cmd16 = cmd[0] << 8; + cmd16 = cmd16 | cmd[1]; + + error = iic_exec(tag,I2C_OP_WRITE_WITH_STOP,addr,cmd,clen,NULL,0,0); + + /* Every command returns something except for turning the heater off + and the general soft reset which returns nothing. */ + if (error == 0 && cmd16 != SGP40_HEATER_OFF) { + /* Every command has a particular delay for how long + it typically takes and the max time it will take. */ + cmddelay = sgp40_cmddelay(cmd16); + delay(cmddelay); + + for (int aint = 0; aint < readattempts; aint++) { + error = iic_exec(tag,I2C_OP_READ_WITH_STOP,addr,NULL,0,buf,blen,0); + if (error == 0) + break; + delay(1000); + } + } + + return error; +} + +static int +sgp40_cmdr(struct sgp40_sc *sc, uint16_t cmd, uint8_t *extraargs, uint8_t argslen, uint8_t *buf, size_t blen) +{ + uint8_t fullcmd[8]; + uint8_t cmdlen; + int n; + + /* The biggest documented command + arguments is 8 uint8_t bytes long. */ + /* Catch anything that ties to have an arglen more than 6 */ + + KASSERT(argslen <= 6); + + memset(fullcmd, 0, 8); + + fullcmd[0] = cmd >> 8; + fullcmd[1] = cmd & 0x00ff; + cmdlen = 2; + + n = 0; + while (extraargs != NULL && n < argslen && cmdlen <= 7) { + fullcmd[cmdlen] = extraargs[n]; + cmdlen++; + n++; + } + DPRINTF(sc, 2, ("%s: Full command and arguments: %02x %02x %02x %02x %02x %02x %02x %02x\n", + device_xname(sc->sc_dev), fullcmd[0], fullcmd[1], + fullcmd[2], fullcmd[3], fullcmd[4], fullcmd[5], + fullcmd[6], fullcmd[7])); + return sgp40_cmd(sc->sc_tag, sc->sc_addr, fullcmd, cmdlen, buf, blen, sc->sc_readattempts); +} + +static uint8_t +sgp40_crc(uint8_t * data, size_t size) +{ + uint8_t crc = 0xFF; + + for (size_t i = 0; i < size; i++) { + crc ^= data[i]; + for (size_t j = 8; j > 0; j--) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x31; + else + crc <<= 1; + } + } + return crc; +} + +static int +sgp40_poke(i2c_tag_t tag, i2c_addr_t addr, bool matchdebug) +{ + uint8_t reg[2]; + uint8_t buf[9]; + int error; + + /* Possible bug... this command may not work if the chip is not idle, + however, it appears to be used by a lot of other code as a probe. + */ + reg[0] = SGP40_GET_SERIAL_NUMBER >> 8; + reg[1] = SGP40_GET_SERIAL_NUMBER & 0x00ff; + + error = sgp40_cmd(tag, addr, reg, 2, buf, 9, 10); + if (matchdebug) { + printf("poke X 1: %d\n", error); + } + return error; +} + +static int +sgp40_sysctl_init(struct sgp40_sc *sc) +{ + int error; + const struct sysctlnode *cnode; + int sysctlroot_num; + + if ((error = sysctl_createv(&sc->sc_sgp40log, 0, NULL, &cnode, + 0, CTLTYPE_NODE, device_xname(sc->sc_dev), + SYSCTL_DESCR("SGP40 controls"), NULL, 0, NULL, 0, CTL_HW, + CTL_CREATE, CTL_EOL)) != 0) + return error; + + sysctlroot_num = cnode->sysctl_num; + +#ifdef SGP40_DEBUG + if ((error = sysctl_createv(&sc->sc_sgp40log, 0, NULL, &cnode, + CTLFLAG_READWRITE, CTLTYPE_INT, "debug", + SYSCTL_DESCR("Debug level"), sgp40_verify_sysctl, 0, + &sc->sc_sgp40debug, 0, CTL_HW, sysctlroot_num, CTL_CREATE, + CTL_EOL)) != 0) + return error; + +#endif + + if ((error = sysctl_createv(&sc->sc_sgp40log, 0, NULL, &cnode, + CTLFLAG_READWRITE, CTLTYPE_INT, "readattempts", + SYSCTL_DESCR("The number of times to attempt to read the values"), + sgp40_verify_sysctl, 0, &sc->sc_readattempts, 0, CTL_HW, + sysctlroot_num, CTL_CREATE, CTL_EOL)) != 0) + return error; + + if ((error = sysctl_createv(&sc->sc_sgp40log, 0, NULL, &cnode, + CTLFLAG_READWRITE, CTLTYPE_BOOL, "ignorecrc", + SYSCTL_DESCR("Ignore the CRC byte"), NULL, 0, &sc->sc_ignorecrc, + 0, CTL_HW, sysctlroot_num, CTL_CREATE, CTL_EOL)) != 0) + return error; + + if ((error = sysctl_createv(&sc->sc_sgp40log, 0, NULL, &cnode, + 0, CTLTYPE_NODE, "compensation", + SYSCTL_DESCR("SGP40 measurement compensations"), NULL, 0, NULL, 0, CTL_HW, + sysctlroot_num, CTL_CREATE, CTL_EOL)) != 0) + return error; + int compensation_num = cnode->sysctl_num; + + if ((error = sysctl_createv(&sc->sc_sgp40log, 0, NULL, &cnode, + CTLFLAG_READWRITE, CTLTYPE_INT, "temperature", + SYSCTL_DESCR("Temperature compensation in celsius"), + sgp40_verify_temp_sysctl, 0, &sc->sc_tempcomp, 0, CTL_HW, + sysctlroot_num, compensation_num, CTL_CREATE, CTL_EOL)) != 0) + return error; + + if ((error = sysctl_createv(&sc->sc_sgp40log, 0, NULL, &cnode, + CTLFLAG_READWRITE, CTLTYPE_INT, "humidity", + SYSCTL_DESCR("Humidity compensation in %RH"), + sgp40_verify_rh_sysctl, 0, &sc->sc_rhcomp, 0, CTL_HW, + sysctlroot_num, compensation_num, CTL_CREATE, CTL_EOL)) != 0) + return error; + + return 0; +} + +static int +sgp40_match(device_t parent, cfdata_t match, void *aux) +{ + struct i2c_attach_args *ia = aux; + int error, match_result; + const bool matchdebug = false; + + if (matchdebug) + printf("in match\n"); + + if (iic_use_direct_match(ia, match, NULL, &match_result)) + return match_result; + + /* indirect config - check for configured address */ + if (ia->ia_addr != SGP40_TYPICAL_ADDR) + return 0; + + /* + * Check to see if something is really at this i2c address. This will + * keep phantom devices from appearing + */ + if (iic_acquire_bus(ia->ia_tag, 0) != 0) { + if (matchdebug) + printf("in match acquire bus failed\n"); + return 0; + } + + error = sgp40_poke(ia->ia_tag, ia->ia_addr, matchdebug); + iic_release_bus(ia->ia_tag, 0); + + return error == 0 ? I2C_MATCH_ADDRESS_AND_PROBE : 0; +} + +static void +sgp40_attach(device_t parent, device_t self, void *aux) +{ + struct sgp40_sc *sc; + struct i2c_attach_args *ia; + int error, i; + int ecount = 0; + uint8_t buf[9]; + uint8_t tstcrc; + uint16_t chiptestvalue; + uint64_t serial_number = 0; + uint8_t sn_crc1, sn_crc2, sn_crc3, sn_crcv1, sn_crcv2, sn_crcv3; + uint8_t fs_crc, fs_crcv; + uint16_t featureset; + + ia = aux; + sc = device_private(self); + + sc->sc_dev = self; + sc->sc_tag = ia->ia_tag; + sc->sc_addr = ia->ia_addr; + sc->sc_sgp40debug = 0; + sc->sc_readattempts = 10; + sc->sc_ignorecrc = false; + sc->sc_stopping = false; + sc->sc_voc = 0; + sc->sc_vocvalid = false; + sc->sc_tempcomp = SGP40_DEFAULT_TEMP_COMP; + sc->sc_rhcomp = SGP40_DEFAULT_RH_COMP; + sc->sc_sme = NULL; + + aprint_normal("\n"); + + mutex_init(&sc->sc_threadmutex, MUTEX_DEFAULT, IPL_NONE); + mutex_init(&sc->sc_mutex, MUTEX_DEFAULT, IPL_NONE); + cv_init(&sc->sc_condvar, "sgp40cv"); + sc->sc_numsensors = __arraycount(sgp40_sensors); + + if ((sc->sc_sme = sysmon_envsys_create()) == NULL) { + aprint_error_dev(self, + "Unable to create sysmon structure\n"); + sc->sc_sme = NULL; + return; + } + if ((error = sgp40_sysctl_init(sc)) != 0) { + aprint_error_dev(self, "Can't setup sysctl tree (%d)\n", error); + goto out; + } + + error = iic_acquire_bus(sc->sc_tag, 0); + if (error) { + aprint_error_dev(self, "Could not acquire iic bus: %d\n", + error); + goto out; + } + + /* Usually one would reset the chip here, but that is not possible + without resetting the entire bus, so we won't do that. + + What we will do is make sure that the chip is idle by running the + turn-the-heater command. + */ + + error = sgp40_cmdr(sc, SGP40_HEATER_OFF, NULL, 0, NULL, 0); + if (error) { + aprint_error_dev(self, "Failed to turn off the heater: %d\n", + error); + ecount++; + } + + error = sgp40_cmdr(sc, SGP40_GET_SERIAL_NUMBER, NULL, 0, buf, 9); + if (error) { + aprint_error_dev(self, "Failed to get serial number: %d\n", + error); + ecount++; + } + + sn_crc1 = sgp40_crc(&buf[0],2); + sn_crc2 = sgp40_crc(&buf[3],2); + sn_crc3 = sgp40_crc(&buf[6],2); + sn_crcv1 = buf[2]; + sn_crcv2 = buf[5]; + sn_crcv3 = buf[8]; + serial_number = buf[0]; + serial_number = (serial_number << 8) | buf[1]; + serial_number = (serial_number << 8) | buf[3]; + serial_number = (serial_number << 8) | buf[4]; + serial_number = (serial_number << 8) | buf[6]; + serial_number = (serial_number << 8) | buf[7]; + + DPRINTF(sc, 2, ("%s: raw serial number: %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + device_xname(sc->sc_dev), buf[0], buf[1], buf[2], buf[3], buf[4], + buf[5], buf[6], buf[7], buf[8])); + + error = sgp40_cmdr(sc, SGP40_GET_FEATURESET, NULL, 0, buf, 3); + if (error) { + aprint_error_dev(self, "Failed to get featureset: %d\n", + error); + ecount++; + } + + fs_crc = sgp40_crc(&buf[0],2); + fs_crcv = buf[2]; + featureset = buf[0]; + featureset = (featureset << 8) | buf[1]; + + DPRINTF(sc, 2, ("%s: raw feature set: %02x %02x %02x\n", + device_xname(sc->sc_dev), buf[0], buf[1], buf[2])); + + error = sgp40_cmdr(sc, SGP40_MEASURE_TEST, NULL, 0, buf, 3); + if (error) { + aprint_error_dev(self, "Failed to perform a chip test: %d\n", + error); + ecount++; + } + + tstcrc = sgp40_crc(&buf[0],2); + + DPRINTF(sc, 2, ("%s: chip test values: %02x%02x - %02x ; %02x\n", + device_xname(sc->sc_dev), buf[0], buf[1], buf[2], tstcrc)); + + iic_release_bus(sc->sc_tag, 0); + if (error != 0) { + aprint_error_dev(self, "Unable to setup device\n"); + goto out; + } + + chiptestvalue = buf[0] << 8; + chiptestvalue |= buf[1]; + + for (i = 0; i < sc->sc_numsensors; i++) { + strlcpy(sc->sc_sensors[i].desc, sgp40_sensors[i].desc, + sizeof(sc->sc_sensors[i].desc)); + + sc->sc_sensors[i].units = sgp40_sensors[i].type; + sc->sc_sensors[i].state = ENVSYS_SINVALID; + + DPRINTF(sc, 2, ("%s: registering sensor %d (%s)\n", __func__, i, + sc->sc_sensors[i].desc)); + + error = sysmon_envsys_sensor_attach(sc->sc_sme, + &sc->sc_sensors[i]); + if (error) { + aprint_error_dev(self, + "Unable to attach sensor %d: %d\n", i, error); + goto out; + } + } + + sc->sc_sme->sme_name = device_xname(sc->sc_dev); + sc->sc_sme->sme_cookie = sc; + sc->sc_sme->sme_refresh = sgp40_refresh; + + DPRINTF(sc, 2, ("sgp40_attach: registering with envsys\n")); + + if (sysmon_envsys_register(sc->sc_sme)) { + aprint_error_dev(self, + "unable to register with sysmon\n"); + sysmon_envsys_destroy(sc->sc_sme); + sc->sc_sme = NULL; + return; + } + + error = kthread_create(PRI_NONE, KTHREAD_MUSTJOIN, NULL, + sgp40_thread, sc, &sc->sc_thread, + device_xname(sc->sc_dev)); + if (error) { + aprint_error_dev(self,"Unable to create measurement thread\n"); + goto out; + } + + aprint_normal_dev(self, "Sensirion SGP40, Serial number: %jx%sFeature set word: 0x%jx%s%s%s", + serial_number, + (sn_crc1 == sn_crcv1 && sn_crc2 == sn_crcv2 && sn_crc3 == sn_crcv3) ? ", " : " (bad crc), ", + (uintmax_t)featureset, + (fs_crc == fs_crcv) ? ", " : " (bad crc), ", + (chiptestvalue == SGP40_TEST_RESULTS_ALL_PASSED) ? "All chip tests passed" : + (chiptestvalue == SGP40_TEST_RESULTS_SOME_FAILED) ? "Some chip tests failed" : + "Unknown test results", + (tstcrc == buf[2]) ? "\n" : " (bad crc)\n"); + return; +out: + sysmon_envsys_destroy(sc->sc_sme); + sc->sc_sme = NULL; +} + +static void +sgp40_refresh(struct sysmon_envsys * sme, envsys_data_t * edata) +{ + struct sgp40_sc *sc; + sc = sme->sme_cookie; + + mutex_enter(&sc->sc_mutex); + if (sc->sc_vocvalid == true) { + edata->value_cur = (uint32_t)sc->sc_voc; + edata->state = ENVSYS_SVALID; + } else { + edata->state = ENVSYS_SINVALID; + } + mutex_exit(&sc->sc_mutex); +} + +static int +sgp40_detach(device_t self, int flags) +{ + struct sgp40_sc *sc; + + sc = device_private(self); + + /* stop the measurement thread */ + sgp40_stop_thread(sc); + + /* Remove the sensors */ + mutex_enter(&sc->sc_mutex); + if (sc->sc_sme != NULL) { + sysmon_envsys_unregister(sc->sc_sme); + sc->sc_sme = NULL; + } + mutex_exit(&sc->sc_mutex); + + /* Remove the sysctl tree */ + sysctl_teardown(&sc->sc_sgp40log); + + /* Remove the mutex */ + mutex_destroy(&sc->sc_mutex); + mutex_destroy(&sc->sc_threadmutex); + + return 0; +} + +MODULE(MODULE_CLASS_DRIVER, sgp40mox, "i2cexec,sysmon_envsys"); + +#ifdef _MODULE +#include "ioconf.c" +#endif + +static int +sgp40mox_modcmd(modcmd_t cmd, void *opaque) +{ + + switch (cmd) { + case MODULE_CMD_INIT: +#ifdef _MODULE + return config_init_component(cfdriver_ioconf_sgp40mox, + cfattach_ioconf_sgp40mox, cfdata_ioconf_sgp40mox); +#else + return 0; +#endif + case MODULE_CMD_FINI: +#ifdef _MODULE + return config_fini_component(cfdriver_ioconf_sgp40mox, + cfattach_ioconf_sgp40mox, cfdata_ioconf_sgp40mox); +#else + return 0; +#endif + default: + return ENOTTY; + } +} Index: src/sys/dev/i2c/sgp40reg.h diff -u /dev/null src/sys/dev/i2c/sgp40reg.h:1.1 --- /dev/null Thu Oct 14 13:54:46 2021 +++ src/sys/dev/i2c/sgp40reg.h Thu Oct 14 13:54:46 2021 @@ -0,0 +1,50 @@ +/* $NetBSD: sgp40reg.h,v 1.1 2021/10/14 13:54:46 brad Exp $ */ + +/* + * Copyright (c) 2021 Brad Spencer <b...@anduin.eldar.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _DEV_I2C_SGP40REG_H_ +#define _DEV_I2C_SGP40REG_H_ + +#define SGP40_TYPICAL_ADDR 0x59 + +/* There are three documented commands for this chip, plus one that is called a + soft reset. But really the soft reset is a general reset of all devices on + the bus, hence it is not as usable as one would hope + */ + +#define SGP40_MEASURE_RAW 0x260f +#define SGP40_MEASURE_TEST 0x280e +#define SGP40_HEATER_OFF 0x3615 +/* The get serial number command is documented in version 1.1 of the + datasheet. +*/ +#define SGP40_GET_SERIAL_NUMBER 0x3682 +/* The get featureset command is not documented in any datasheet that could + be found. However, it is present and used in a lot of example code. + It has no additional arguments aside from the command itself, and returns + a uint16_t for the data and a uint8_t for the crc. +*/ +#define SGP40_GET_FEATURESET 0x202f + +/* The results of a self test are documented as being either everything is ok, or + something failed. + */ + +#define SGP40_TEST_RESULTS_ALL_PASSED 0xd400 +#define SGP40_TEST_RESULTS_SOME_FAILED 0x4b00 + +#endif Index: src/sys/dev/i2c/sgp40var.h diff -u /dev/null src/sys/dev/i2c/sgp40var.h:1.1 --- /dev/null Thu Oct 14 13:54:46 2021 +++ src/sys/dev/i2c/sgp40var.h Thu Oct 14 13:54:46 2021 @@ -0,0 +1,61 @@ +/* $NetBSD: sgp40var.h,v 1.1 2021/10/14 13:54:46 brad Exp $ */ + +/* + * Copyright (c) 2021 Brad Spencer <b...@anduin.eldar.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _DEV_I2C_SGP40VAR_H_ +#define _DEV_I2C_SGP40VAR_H_ + +#define SGP40_NUM_SENSORS 1 +#define SGP40_VOC_SENSOR 0 + +#define SGP40_DEFAULT_TEMP_COMP 25 +#define SGP40_DEFAULT_RH_COMP 50 + + +struct sgp40_sc { + int sc_sgp40debug; + device_t sc_dev; + i2c_tag_t sc_tag; + i2c_addr_t sc_addr; + kmutex_t sc_threadmutex; /* for the measurement kthread */ + kmutex_t sc_mutex; /* for reading the i2c bus */ + kcondvar_t sc_condvar; + struct lwp *sc_thread; + int sc_numsensors; + struct sysmon_envsys *sc_sme; + struct sysctllog *sc_sgp40log; + envsys_data_t sc_sensors[SGP40_NUM_SENSORS]; + uint16_t sc_voc; + bool sc_vocvalid; + bool sc_ignorecrc; + int sc_readattempts; + bool sc_stopping; + int sc_tempcomp; + int sc_rhcomp; +}; + +struct sgp40_sensor { + const char *desc; + enum envsys_units type; +}; + +struct sgp40_timing { + uint16_t cmd; + int typicaldelay; +}; + +#endif Index: src/sys/modules/sgp40mox/Makefile diff -u /dev/null src/sys/modules/sgp40mox/Makefile:1.1 --- /dev/null Thu Oct 14 13:54:46 2021 +++ src/sys/modules/sgp40mox/Makefile Thu Oct 14 13:54:46 2021 @@ -0,0 +1,12 @@ +.include "../Makefile.inc" + +.PATH: ${S}/dev/i2c + +KMOD= sgp40mox +IOCONF= sgp40mox.ioconf +SRCS= sgp40.c +SRCS+= sensirion_voc_algorithm.c + +WARNS= 3 + +.include <bsd.kmodule.mk> Index: src/sys/modules/sgp40mox/sgp40mox.ioconf diff -u /dev/null src/sys/modules/sgp40mox/sgp40mox.ioconf:1.1 --- /dev/null Thu Oct 14 13:54:46 2021 +++ src/sys/modules/sgp40mox/sgp40mox.ioconf Thu Oct 14 13:54:46 2021 @@ -0,0 +1,8 @@ +ioconf sgp40mox + +include "conf/files" + +pseudo-root iic* + +sgp40mox* at iic? addr 0x59 +