diff --git a/README.md b/README.md index 2735adc..792688d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,21 @@ # SCD4x Low-Power Calibration (LPC) Algorithm [in progress] The SCD4x sensor has a built-in Automatic Self-Calibration (ASC) that is not available for ultra-low power applications that require power-down of the sensor between measurements. -This C++ library attempts to emulate the behaviors of the ASC in single-shot mode, based on the ASC Application Note (p. 2-5,11-13). The key assumption is that the lowest measured co2 is no less than SCD4X_LPC_BASELINE_CO2_PPM.
-It also is designed to run asynchonously and delays in between function calls are up to the user to enforce. The HAL function sensirion_i2c_hal_sleep_usec must do nothing. +This C++ library attempts to emulate the behaviors of the ASC in single-shot mode, based on the ASC Application Note (p. 2-5,11-13). The key assumption is that the lowest measured co2 is no less than SCD4X_LPC_BASELINE_CO2_PPM.
+Do **not** use this library (or the ASC) if this assumption does not hold for the application requirements.
+ +![Graph](https://github.com/edward62740/SCD4x-LPC/blob/master/graph.png) +
*ASC Application Note (p. 5)* +
+ +It also is designed to run asynchonously (from the sensor) and delays in between function calls are up to the user to enforce. This algorithm provides some safety by taking in a platform defined millisecond timer in the constructor and blocking function calls for the given sensor execution durations (specified in SCD4x datasheet p.8). The HAL function sensirion_i2c_hal_sleep_usec must be implemented as such: +``` +void sensirion_i2c_hal_sleep_usec(uint32_t useconds) { + if(useconds > 1000) return; + platform_delay_us(useconds); // platform specific delay or RTC sleep +} +``` +The only function calls that are not handled by this method are the FRC (400ms) and self-test(10000ms, never called by this lib). These need to be manually modified by the user if maximum power savings are needed. In general, this library has a FSM that goes through 3 states: - First: First reading calibration diff --git a/graph.png b/graph.png new file mode 100644 index 0000000..17c0eb7 Binary files /dev/null and b/graph.png differ diff --git a/scd4x_driver/.clang-format b/scd4x_driver/.clang-format new file mode 100644 index 0000000..047f2ad --- /dev/null +++ b/scd4x_driver/.clang-format @@ -0,0 +1,14 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +IndentWidth: 4 +AlignAfterOpenBracket: Align +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +IndentCaseLabels: true +SpacesBeforeTrailingComments: 2 +PointerAlignment: Left +AlignEscapedNewlines: Left +ForEachMacros: ['TEST_GROUP', 'TEST'] +... diff --git a/scd4x_driver/scd4x_i2c.c b/scd4x_driver/scd4x_i2c.c index 47b54de..0703e93 100644 --- a/scd4x_driver/scd4x_i2c.c +++ b/scd4x_driver/scd4x_i2c.c @@ -40,8 +40,9 @@ #include "sensirion_common.h" #include "sensirion_i2c.h" #include "sensirion_i2c_hal.h" +#include "sl_sleeptimer.h" -#define SCD4X_I2C_ADDRESS 98 +#define SCD4X_I2C_ADDRESS 0x62 int16_t scd4x_start_periodic_measurement() { int16_t error; @@ -73,7 +74,7 @@ int16_t scd4x_read_measurement_ticks(uint16_t* co2, uint16_t* temperature, error = sensirion_i2c_read_data_inplace(SCD4X_I2C_ADDRESS, &buffer[0], 6); if (error) { - return error; + return error; } *co2 = sensirion_common_bytes_to_uint16_t(&buffer[0]); *temperature = sensirion_common_bytes_to_uint16_t(&buffer[2]); @@ -234,7 +235,7 @@ int16_t scd4x_perform_forced_recalibration(uint16_t target_co2_concentration, return error; } - sensirion_i2c_hal_sleep_usec(400000); + sl_sleeptimer_delay_millisecond(400); error = sensirion_i2c_read_data_inplace(SCD4X_I2C_ADDRESS, &buffer[0], 2); if (error) { @@ -362,7 +363,7 @@ int16_t scd4x_perform_self_test(uint16_t* sensor_status) { return error; } - sensirion_i2c_hal_sleep_usec(10000000); + sl_sleeptimer_delay_millisecond(10000); error = sensirion_i2c_read_data_inplace(SCD4X_I2C_ADDRESS, &buffer[0], 2); if (error) { diff --git a/scd4x_driver/sensirion_i2c_hal.c b/scd4x_driver/sensirion_i2c_hal.c index d63f815..d9387a7 100644 --- a/scd4x_driver/sensirion_i2c_hal.c +++ b/scd4x_driver/sensirion_i2c_hal.c @@ -32,7 +32,8 @@ #include "sensirion_i2c_hal.h" #include "sensirion_common.h" #include "sensirion_config.h" - +#include "sl_i2cspm.h" +#include "sl_sleeptimer.h" /* * INSTRUCTIONS * ============ @@ -84,8 +85,19 @@ void sensirion_i2c_hal_free(void) { * @returns 0 on success, error code otherwise */ int8_t sensirion_i2c_hal_read(uint8_t address, uint8_t* data, uint16_t count) { - /* TODO:IMPLEMENT */ - return NOT_IMPLEMENTED_ERROR; + + I2C_TransferSeq_TypeDef i2cTransfer; + I2C_TransferReturn_TypeDef result; + + // Initialize I2C transfer + i2cTransfer.addr = address << 1; + i2cTransfer.flags = I2C_FLAG_READ; // must write target address before reading + i2cTransfer.buf[0].data = data; + i2cTransfer.buf[0].len = count; + + result = I2CSPM_Transfer(I2C0, &i2cTransfer); + + return 0; } /** @@ -101,8 +113,17 @@ int8_t sensirion_i2c_hal_read(uint8_t address, uint8_t* data, uint16_t count) { */ int8_t sensirion_i2c_hal_write(uint8_t address, const uint8_t* data, uint16_t count) { - /* TODO:IMPLEMENT */ - return NOT_IMPLEMENTED_ERROR; + I2C_TransferSeq_TypeDef i2cTransfer; + I2C_TransferReturn_TypeDef result; + + // Initialize I2C transfer + i2cTransfer.addr = address << 1 ; + i2cTransfer.flags = I2C_FLAG_WRITE; + i2cTransfer.buf[0].data = data; + i2cTransfer.buf[0].len = count; + + result = I2CSPM_Transfer(I2C0, &i2cTransfer); + return 0; } /** @@ -114,5 +135,6 @@ int8_t sensirion_i2c_hal_write(uint8_t address, const uint8_t* data, * @param useconds the sleep time in microseconds */ void sensirion_i2c_hal_sleep_usec(uint32_t useconds) { - /* TODO:IMPLEMENT */ + if(useconds > 1000) return; + sl_sleeptimer_delay_millisecond(useconds/1000); } diff --git a/scd4x_lpc.cpp b/scd4x_lpc.cpp index f94564c..c41ea27 100644 --- a/scd4x_lpc.cpp +++ b/scd4x_lpc.cpp @@ -7,11 +7,12 @@ #include "scd4x_lpc.h" #include "scd4x_i2c.h" + namespace LPC { SCD4X::SCD4X(int16_t (*wu)(void), int16_t (*pd)(void), int16_t (*meas)(void), int16_t (*drdy)(bool *ready), int16_t (*read)(uint16_t* co2, int32_t* temp, int32_t* hum), - int16_t (*frc)(uint16_t target, uint16_t* corr), int16_t (*persist)(void)) + int16_t (*frc)(uint16_t target, uint16_t* corr), int16_t (*persist)(void), uint32_t (*getMsTick)(void)) { scd_fp.wu = wu; scd_fp.pd = pd; @@ -20,6 +21,7 @@ namespace LPC { scd_fp.read = read; scd_fp.frc = frc; scd_fp.persist = persist; + scd_fp.getMsTick = getMsTick; fsm.state = STATE_FIRST; fsm.expend = false; @@ -48,74 +50,82 @@ void SCD4X::get_last_frc(int32_t *offset, uint32_t *age, uint32_t *num) } lpc_ret SCD4X::powerOn(void) { + if(!_mutex_request_unlock()) return ERR_WAIT; if (scd_fp.wu()) return _sig_scd_fail(true); _sig_scd_fail(false); + _mutex_request_lock(SCD4X_LPC_CMD_WU_LOCK_DUR_MS); return ERR_NONE; } lpc_ret SCD4X::powerOff(void) { + if(!_mutex_request_unlock()) return ERR_WAIT; if (scd_fp.pd()) return _sig_scd_fail(true); _sig_scd_fail(false); + _mutex_request_lock(SCD4X_LPC_CMD_PD_LOCK_DUR_MS); return ERR_NONE; } lpc_ret SCD4X::discardMeasurement(void) { + if(!_mutex_request_unlock()) return ERR_WAIT; if (scd_fp.meas()) return _sig_scd_fail(true); _sig_scd_fail(false); + _mutex_request_lock(SCD4X_LPC_CMD_MEAS_LOCK_DUR_MS); return ERR_NONE; } lpc_ret SCD4X::measure() { - + if(!_mutex_request_unlock()) return ERR_WAIT; if (scd_fp.meas()) return _sig_scd_fail(true); if (fsm.ttl <= 1) return WARN_PRETRIG; // warn if yield fn should be passed _sig_scd_fail(false); + _mutex_request_lock(SCD4X_LPC_CMD_MEAS_LOCK_DUR_MS); return ERR_NONE; } - lpc_ret SCD4X::read(uint16_t *co2, int32_t *temp, int32_t *hum, void (*yield)(void)) + lpc_ret SCD4X::read(uint16_t *co2, int32_t *temp, int32_t *hum) { bool tmp = false; scd_fp.drdy(&tmp); if(!tmp) return ERR_WAIT; - + if(!_mutex_request_unlock()) return ERR_WAIT; if(scd_fp.read(co2, temp, hum)) return _sig_scd_fail(true); if(*co2 == 0) return _sig_scd_fail(true); _sig_scd_fail(false); if(*co2 < fsm.gl_min) fsm.gl_min = *co2; prev_frc_age++; - return _proc_fsm(*co2, yield); + return _proc_fsm(*co2); } - lpc_ret SCD4X::_proc_fsm(uint16_t co2, void (*yield)(void)) + lpc_ret SCD4X::_proc_fsm(uint16_t co2) { if(fsm._cons_fail > SCD4X_LPC_SCD_FAIL_TOL_TH) return ERR_PANIC; if (co2 < SCD4X_LPC_BASELINE_CO2_PPM) // called if co2 < 400 { + fsm.expend = true; uint32_t offset = SCD4X_LPC_BASELINE_CO2_PPM - co2; fsm.limit = offset > _sig_get_fsm_offset(); uint16_t ret = 0xFFFF; scd_fp.frc(fsm.limit ?co2+_sig_get_fsm_offset() : SCD4X_LPC_BASELINE_CO2_PPM, &ret); + _mutex_request_lock(SCD4X_LPC_CMD_FRC_LOCK_DUR_MS); frc_ctr++; prev_frc_offset = ret - 0x8000U; - if (yield != NULL) - yield(); //yield to app function ptr if (ret == 0xFFFF) return ERR_LPC; // return on next calls, ttl will be -ve } if (--fsm.ttl <= 0) { _sig_fsm_next(); + uint16_t _gl_tmp = fsm.gl_min; fsm.ttl = _sig_get_fsm_cnt(); fsm.gl_min = 0xFFFF; @@ -125,16 +135,16 @@ lpc_ret SCD4X::measure() } // do not frc again if already done // called iff co2 >= 400 for the entire period - uint32_t offset = fsm.gl_min - SCD4X_LPC_BASELINE_CO2_PPM; + uint32_t offset = _gl_tmp - SCD4X_LPC_BASELINE_CO2_PPM; fsm.limit = offset > _sig_get_fsm_offset(); uint16_t ret = 0xFFFF; scd_fp.frc(co2 - (fsm.limit ? _sig_get_fsm_offset() : offset), &ret); + + _mutex_request_lock(SCD4X_LPC_CMD_FRC_LOCK_DUR_MS); frc_ctr++; prev_frc_offset = ret - 0x8000U; prev_frc_age = 0; - if (yield != NULL) - yield(); //yield to app function ptr if (ret == 0xFFFF) return ERR_LPC; // return on next calls, ttl will be -ve } @@ -143,6 +153,7 @@ lpc_ret SCD4X::measure() } void SCD4X::_sig_fsm_next(void) { + switch (fsm.state) { case STATE_FIRST: { fsm.state = STATE_INIT; @@ -156,6 +167,8 @@ void SCD4X::_sig_fsm_next(void) { fsm.state = STATE_STD; break; } + + } } @@ -186,8 +199,40 @@ uint32_t SCD4X::_sig_get_fsm_cnt(void) { lpc_ret SCD4X::_sig_scd_fail(bool trig) { - if(!trig) fsm._cons_fail = 0; + if (!trig) { + fsm._cons_fail = 0; + return ERR_NONE; + } else fsm._cons_fail++; + return ERR_SCD; } + + +bool SCD4X::_mutex_request_lock(uint32_t dur) { + //CORE_DECLARE_IRQ_STATE; + + //CORE_ENTER_ATOMIC(); + + if (_mutex.state) + return false; + if(dur > SCD4X_LPC_CMD_MAX_LOCK_DUR_MS) return false; + _mutex.lastTickMs = scd_fp.getMsTick(); + _mutex.reqLockDurMs = dur; + return true; + + //CORE_EXIT_ATOMIC(); +} + +bool SCD4X::_mutex_request_unlock(void) { + if(!_mutex.state) return true; + int64_t diff = scd_fp.getMsTick() - _mutex.lastTickMs; + if (diff < 0 || diff > _mutex.reqLockDurMs) { + _mutex.state = false; + return true; + } + else return false; + +} + } diff --git a/scd4x_lpc.h b/scd4x_lpc.h index ad0ccca..f4eeb6c 100644 --- a/scd4x_lpc.h +++ b/scd4x_lpc.h @@ -26,6 +26,15 @@ const uint32_t SCD4X_LPC_STD_MAX_CAL_OFFSET = 100; const uint32_t SCD4X_LPC_SCD_FAIL_TOL_TH = 50; +const uint32_t SCD4X_LPC_CMD_MAX_LOCK_DUR_MS = 10000; +const uint32_t SCD4X_LPC_CMD_WU_LOCK_DUR_MS = 20; +const uint32_t SCD4X_LPC_CMD_PD_LOCK_DUR_MS = 1; +const uint32_t SCD4X_LPC_CMD_MEAS_LOCK_DUR_MS = 5000; +const uint32_t SCD4X_LPC_CMD_DRDY_LOCK_DUR_MS = 0; +const uint32_t SCD4X_LPC_CMD_READ_LOCK_DUR_MS = 0; +const uint32_t SCD4X_LPC_CMD_FRC_LOCK_DUR_MS = 0; +const uint32_t SCD4X_LPC_CMD_PERSIST_LOCK_DUR_MS = 800; + namespace LPC { typedef enum { @@ -37,7 +46,8 @@ namespace LPC { public: SCD4X(int16_t (*wu)(void), int16_t (*pd)(void), int16_t (*meas)(void), int16_t (*drdy)(bool *ready), int16_t (*read)(uint16_t* co2, int32_t* temp, int32_t* hum), - int16_t (*frc)(uint16_t target, uint16_t* corr), int16_t (*persist)(void)); + int16_t (*frc)(uint16_t target, uint16_t* corr), int16_t (*persist)(void), + uint32_t (*getMsTick)(void)); lpc_ret powerOn(void); lpc_ret powerOff(void); @@ -59,19 +69,12 @@ namespace LPC { * @brief This function is called at least 5000ms after measure() and returns * sensor measurement data and performs FRC if applicable. * - * The yield() passed to this function should run for at - * least 400ms and is recommended to be a low-power entry code that - * preserves NVM. - * - * measure() will provide forewarning of FRC, iff co2 is above baseline - * else the app must handle sporadic calls to yield() - * * * @param co2,temp,hum pointers to store sensor data * @param yield pointer to app function * @retval lpc_ret error code */ - lpc_ret read(uint16_t *co2, int32_t *temp, int32_t *hum, void (*yield)(void) = NULL); + lpc_ret read(uint16_t *co2, int32_t *temp, int32_t *hum); void get_last_frc(int32_t *offset, uint32_t *age, uint32_t *num); @@ -88,6 +91,7 @@ namespace LPC { int16_t (*read)(uint16_t* co2, int32_t* temp, int32_t* hum); int16_t (*frc)(uint16_t target, uint16_t* corr); int16_t (*persist)( void ); + uint32_t (*getMsTick)(void); } scd_fp; typedef enum @@ -107,7 +111,15 @@ namespace LPC { uint32_t _cons_fail; } fsm; - lpc_ret _proc_fsm(uint16_t co2, void (*yield)(void)); + struct { + bool state = false; + uint32_t lastTickMs = 0; + uint32_t reqLockDurMs = 0; + } _mutex; + bool _mutex_request_lock(uint32_t dur); + bool _mutex_request_unlock(void); + + lpc_ret _proc_fsm(uint16_t co2); void _sig_fsm_next(void); uint32_t _sig_get_fsm_offset(void); uint32_t _sig_get_fsm_cnt(void);