Skip to content

Commit

Permalink
Completed initial test
Browse files Browse the repository at this point in the history
  • Loading branch information
edward62740 committed May 21, 2023
1 parent e087957 commit 8aecac9
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 34 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.<br>
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.<br>
Do **not** use this library (or the ASC) if this assumption does not hold for the application requirements.<br>

![Graph](https://github.com/edward62740/SCD4x-LPC/blob/master/graph.png)
<br>*ASC Application Note (p. 5)*
<br>

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
Expand Down
Binary file added graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions scd4x_driver/.clang-format
Original file line number Diff line number Diff line change
@@ -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']
...
9 changes: 5 additions & 4 deletions scd4x_driver/scd4x_i2c.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
34 changes: 28 additions & 6 deletions scd4x_driver/sensirion_i2c_hal.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
* ============
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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);
}
69 changes: 57 additions & 12 deletions scd4x_lpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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
}
Expand All @@ -143,6 +153,7 @@ lpc_ret SCD4X::measure()
}

void SCD4X::_sig_fsm_next(void) {

switch (fsm.state) {
case STATE_FIRST: {
fsm.state = STATE_INIT;
Expand All @@ -156,6 +167,8 @@ void SCD4X::_sig_fsm_next(void) {
fsm.state = STATE_STD;
break;
}


}
}

Expand Down Expand Up @@ -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;

}

}
32 changes: 22 additions & 10 deletions scd4x_lpc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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);

Expand All @@ -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
Expand All @@ -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);
Expand Down

0 comments on commit 8aecac9

Please sign in to comment.