EIS-BLE-S3/main/eis.c

808 lines
24 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "eis.h"
#include <math.h>
#include <string.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "nvs.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* resolved hardware state */
static struct {
EISConfig cfg;
float sys_clk;
float rcal_ohms;
uint32_t rcal_sw_d, rcal_sw_p, rcal_sw_n, rcal_sw_t;
uint32_t dut_sw_d, dut_sw_p, dut_sw_n, dut_sw_t;
uint32_t dut_mux_vp, dut_mux_vn;
uint32_t rtia_reg;
uint32_t dertia_reg;
} ctx;
/* cell constant K (cm⁻¹), cached from NVS */
static float cell_k_cached;
static float cl_factor_cached;
static float ph_slope_cached;
static float ph_offset_cached;
/* open-circuit calibration data */
static struct {
fImpCar_Type y[EIS_MAX_POINTS]; /* admittance at each freq */
float freq[EIS_MAX_POINTS];
uint32_t n;
int valid;
} ocal;
static const uint32_t rtia_map[] = {
[RTIA_200] = HSTIARTIA_200,
[RTIA_1K] = HSTIARTIA_1K,
[RTIA_5K] = HSTIARTIA_5K,
[RTIA_10K] = HSTIARTIA_10K,
[RTIA_20K] = HSTIARTIA_20K,
[RTIA_40K] = HSTIARTIA_40K,
[RTIA_80K] = HSTIARTIA_80K,
[RTIA_160K] = HSTIARTIA_160K,
[RTIA_EXT_DE0] = HSTIARTIA_OPEN,
};
static void resolve_config(void)
{
/* RTIA */
ctx.rtia_reg = rtia_map[ctx.cfg.rtia];
ctx.dertia_reg = (ctx.cfg.rtia == RTIA_EXT_DE0) ? HSTIADERTIA_TODE : HSTIADERTIA_OPEN;
/* RCAL */
switch (ctx.cfg.rcal) {
case RCAL_200R:
ctx.rcal_ohms = 200.0f;
ctx.rcal_sw_d = SWD_RCAL0;
ctx.rcal_sw_p = SWP_RCAL0;
ctx.rcal_sw_n = SWN_RCAL1;
ctx.rcal_sw_t = SWT_RCAL1 | SWT_TRTIA;
break;
default: /* RCAL_3K */
ctx.rcal_ohms = 3000.0f;
ctx.rcal_sw_d = SWD_RCAL0;
ctx.rcal_sw_p = SWP_RCAL0;
ctx.rcal_sw_n = SWN_AIN0;
ctx.rcal_sw_t = SWT_AIN0 | SWT_TRTIA;
break;
}
/* DUT electrode routing */
switch (ctx.cfg.electrode) {
case ELEC_3WIRE:
ctx.dut_sw_d = SWD_CE0;
ctx.dut_sw_p = SWP_RE0;
ctx.dut_sw_n = SWN_SE0;
ctx.dut_sw_t = SWT_SE0LOAD | SWT_TRTIA;
ctx.dut_mux_vp = ADCMUXP_P_NODE;
ctx.dut_mux_vn = ADCMUXN_N_NODE;
break;
default: /* ELEC_4WIRE */
ctx.dut_sw_d = SWD_AIN3;
ctx.dut_sw_p = SWP_AIN3;
ctx.dut_sw_n = SWN_AIN0;
ctx.dut_sw_t = SWT_AIN0 | SWT_TRTIA;
ctx.dut_mux_vp = ADCMUXP_AIN2;
ctx.dut_mux_vn = ADCMUXN_AIN1;
break;
}
}
static void apply_hsloop(void)
{
HSLoopCfg_Type hs;
AD5940_StructInit(&hs, sizeof(hs));
hs.HsDacCfg.ExcitBufGain = EXCITBUFGAIN_2;
hs.HsDacCfg.HsDacGain = HSDACGAIN_0P2;
hs.HsDacCfg.HsDacUpdateRate = 7;
hs.HsTiaCfg.HstiaBias = HSTIABIAS_1P1;
hs.HsTiaCfg.HstiaRtiaSel = ctx.rtia_reg;
hs.HsTiaCfg.HstiaCtia = 16;
hs.HsTiaCfg.HstiaDeRtia = ctx.dertia_reg;
hs.HsTiaCfg.HstiaDeRload = HSTIADERLOAD_OPEN;
hs.HsTiaCfg.HstiaDe1Rtia = HSTIADERTIA_OPEN;
hs.HsTiaCfg.HstiaDe1Rload = HSTIADERLOAD_OPEN;
hs.HsTiaCfg.DiodeClose = bFALSE;
hs.WgCfg.WgType = WGTYPE_SIN;
hs.WgCfg.GainCalEn = bTRUE;
hs.WgCfg.OffsetCalEn = bTRUE;
hs.WgCfg.SinCfg.SinAmplitudeWord = ctx.cfg.excit_amp;
hs.WgCfg.SinCfg.SinFreqWord = 0;
hs.WgCfg.SinCfg.SinOffsetWord = 0;
hs.WgCfg.SinCfg.SinPhaseWord = 0;
hs.SWMatCfg.Dswitch = ctx.rcal_sw_d;
hs.SWMatCfg.Pswitch = ctx.rcal_sw_p;
hs.SWMatCfg.Nswitch = ctx.rcal_sw_n;
hs.SWMatCfg.Tswitch = ctx.rcal_sw_t;
AD5940_HSLoopCfgS(&hs);
if (ctx.cfg.rtia == RTIA_EXT_DE0)
AD5940_WriteReg(REG_AFE_DE0RESCON, 0x97);
}
/* ---------- public ---------- */
void eis_default_config(EISConfig *cfg)
{
memset(cfg, 0, sizeof(*cfg));
cfg->freq_start_hz = 1000.0f;
cfg->freq_stop_hz = 200000.0f;
cfg->points_per_decade = 10;
cfg->rtia = RTIA_5K;
cfg->rcal = RCAL_3K;
cfg->electrode = ELEC_4WIRE;
cfg->pga = ADCPGA_1P5;
cfg->excit_amp = 500;
}
uint32_t eis_calc_num_points(const EISConfig *cfg)
{
if (cfg->points_per_decade == 0)
return 1;
if (cfg->freq_start_hz == cfg->freq_stop_hz)
return 24; /* fixed-freq repeatability test */
float lo = fminf(cfg->freq_start_hz, cfg->freq_stop_hz);
float hi = fmaxf(cfg->freq_start_hz, cfg->freq_stop_hz);
float decades = log10f(hi / lo);
uint32_t n = (uint32_t)(decades * cfg->points_per_decade + 0.5f) + 1;
if (n > EIS_MAX_POINTS) n = EIS_MAX_POINTS;
if (n < 2) n = 2;
return n;
}
void eis_init(const EISConfig *cfg)
{
memcpy(&ctx.cfg, cfg, sizeof(EISConfig));
ctx.sys_clk = 16000000.0f;
resolve_config();
/* reset to clear stale AFE state from prior measurement mode */
AD5940_SoftRst();
AD5940_Initialize();
CLKCfg_Type clk;
memset(&clk, 0, sizeof(clk));
clk.HFOSCEn = bTRUE;
clk.HfOSC32MHzMode = bFALSE;
clk.SysClkSrc = SYSCLKSRC_HFOSC;
clk.ADCCLkSrc = ADCCLKSRC_HFOSC;
clk.SysClkDiv = SYSCLKDIV_1;
clk.ADCClkDiv = ADCCLKDIV_1;
clk.LFOSCEn = bTRUE;
clk.HFXTALEn = bFALSE;
AD5940_CLKCfg(&clk);
AFERefCfg_Type ref;
AD5940_StructInit(&ref, sizeof(ref));
ref.HpBandgapEn = bTRUE;
ref.Hp1V1BuffEn = bTRUE;
ref.Hp1V8BuffEn = bTRUE;
ref.HSDACRefEn = bTRUE;
ref.LpBandgapEn = bFALSE;
ref.LpRefBufEn = bFALSE;
AD5940_REFCfgS(&ref);
AD5940_AFEPwrBW(AFEPWR_HP, AFEBW_250KHZ);
AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_DFTRDY, bTRUE);
AD5940_INTCCfg(AFEINTC_1, AFEINTSRC_DFTRDY, bTRUE);
AD5940_INTCClrFlag(AFEINTSRC_ALLINT);
AGPIOCfg_Type gpio;
AD5940_StructInit(&gpio, sizeof(gpio));
gpio.FuncSet = GP0_INT;
gpio.OutputEnSet = AGPIO_Pin0;
AD5940_AGPIOCfg(&gpio);
AD5940_WriteReg(REG_AFE_FIFOCON, 0);
SEQCfg_Type seq;
seq.SeqMemSize = SEQMEMSIZE_4KB;
seq.SeqBreakEn = bFALSE;
seq.SeqIgnoreEn = bFALSE;
seq.SeqCntCRCClr = bFALSE;
seq.SeqEnable = bTRUE;
seq.SeqWrTimer = 0;
AD5940_SEQCfg(&seq);
apply_hsloop();
ADCBaseCfg_Type adc;
adc.ADCMuxP = ADCMUXP_P_NODE;
adc.ADCMuxN = ADCMUXN_N_NODE;
adc.ADCPga = cfg->pga;
AD5940_ADCBaseCfgS(&adc);
AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
}
void eis_reconfigure(const EISConfig *cfg)
{
memcpy(&ctx.cfg, cfg, sizeof(EISConfig));
resolve_config();
}
/* ---------- internal helpers ---------- */
static void configure_freq(float freq_hz)
{
FreqParams_Type fp = AD5940_GetFreqParameters(freq_hz);
if (fp.HighPwrMode) {
fp.DftSrc = DFTSRC_ADCRAW;
fp.ADCSinc3Osr = ADCSINC3OSR_2;
fp.ADCSinc2Osr = 0;
}
/* widest DFT window to suppress non-coherent leakage */
fp.DftNum = DFTNUM_16384;
AD5940_WriteReg(REG_AFE_WGFCW,
AD5940_WGFreqWordCal(freq_hz, ctx.sys_clk));
ADCFilterCfg_Type filt;
AD5940_StructInit(&filt, sizeof(filt));
filt.ADCSinc3Osr = fp.ADCSinc3Osr;
filt.ADCSinc2Osr = fp.ADCSinc2Osr;
filt.ADCAvgNum = ADCAVGNUM_16;
filt.ADCRate = ADCRATE_800KHZ;
filt.BpNotch = bTRUE;
filt.BpSinc3 = bFALSE;
filt.Sinc2NotchEnable = bTRUE;
filt.Sinc3ClkEnable = bTRUE;
filt.Sinc2NotchClkEnable = bTRUE;
filt.DFTClkEnable = bTRUE;
filt.WGClkEnable = bTRUE;
AD5940_ADCFilterCfgS(&filt);
DFTCfg_Type dft;
dft.DftNum = fp.DftNum;
dft.DftSrc = fp.DftSrc;
dft.HanWinEn = bTRUE;
AD5940_DFTCfgS(&dft);
}
static int32_t sign_extend_18(uint32_t v)
{
return (v & (1UL << 17)) ? (int32_t)(v | 0xFFFC0000UL) : (int32_t)v;
}
/* settles for a fixed count of excitation periods, floored for high-frequency overhead */
static void settle(float freq_hz, float cycles, uint32_t floor_us)
{
float us = cycles * 1e6f / freq_hz;
uint32_t d_us = (us > (float)floor_us) ? (uint32_t)us : floor_us;
AD5940_Delay10us(d_us / 10);
}
/* paired DFT: two measurements under continuous WG excitation */
static void dft_measure_pair(
float freq_hz,
uint32_t mux1_p, uint32_t mux1_n, iImpCar_Type *out1,
uint32_t mux2_p, uint32_t mux2_n, iImpCar_Type *out2)
{
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bFALSE);
AD5940_WriteReg(REG_AFE_FIFOCON, 0);
AD5940_ReadAfeResult(AFERESULT_DFTREAL);
AD5940_ReadAfeResult(AFERESULT_DFTIMAGE);
AD5940_INTCClrFlag(AFEINTSRC_DFTRDY);
AD5940_ADCMuxCfgS(mux1_p, mux1_n);
AD5940_AFECtrlS(AFECTRL_WG | AFECTRL_ADCPWR, bTRUE);
settle(freq_hz, 2.0f, 100);
AD5940_ClrMCUIntFlag();
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bTRUE);
while (!AD5940_GetMCUIntFlag())
vTaskDelay(1);
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bFALSE);
AD5940_INTCClrFlag(AFEINTSRC_DFTRDY);
out1->Real = sign_extend_18(AD5940_ReadAfeResult(AFERESULT_DFTREAL));
out1->Image = sign_extend_18(AD5940_ReadAfeResult(AFERESULT_DFTIMAGE));
out1->Image = -out1->Image;
/* switch ADC mux, flush stale pipeline, settle one period */
AD5940_ADCMuxCfgS(mux2_p, mux2_n);
AD5940_ReadAfeResult(AFERESULT_DFTREAL);
AD5940_ReadAfeResult(AFERESULT_DFTIMAGE);
AD5940_INTCClrFlag(AFEINTSRC_DFTRDY);
settle(freq_hz, 1.0f, 50);
AD5940_ClrMCUIntFlag();
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT, bTRUE);
while (!AD5940_GetMCUIntFlag())
vTaskDelay(1);
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_DFT |
AFECTRL_WG | AFECTRL_ADCPWR, bFALSE);
AD5940_INTCClrFlag(AFEINTSRC_DFTRDY);
out2->Real = sign_extend_18(AD5940_ReadAfeResult(AFERESULT_DFTREAL));
out2->Image = sign_extend_18(AD5940_ReadAfeResult(AFERESULT_DFTIMAGE));
out2->Image = -out2->Image;
}
static fImpCar_Type measure_rtia(float freq_hz, iImpCar_Type *out_hstia)
{
iImpCar_Type v_rcal, v_raw;
dft_measure_pair(freq_hz,
ADCMUXP_P_NODE, ADCMUXN_N_NODE, &v_rcal,
ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_raw);
if (out_hstia) *out_hstia = v_raw;
v_raw.Real = -v_raw.Real;
v_raw.Image = -v_raw.Image;
fImpCar_Type rtia = AD5940_ComplexDivInt(&v_raw, &v_rcal);
rtia.Real *= ctx.rcal_ohms;
rtia.Image *= ctx.rcal_ohms;
return rtia;
}
/* ---------- measurement ---------- */
int eis_measure_point(float freq_hz, EISPoint *out)
{
configure_freq(freq_hz);
SWMatrixCfg_Type sw;
iImpCar_Type v_tia, v_sense;
/* RCAL reference before power-up */
sw.Dswitch = ctx.rcal_sw_d;
sw.Pswitch = ctx.rcal_sw_p;
sw.Nswitch = ctx.rcal_sw_n;
sw.Tswitch = ctx.rcal_sw_t;
AD5940_SWMatrixCfgS(&sw);
AD5940_AFECtrlS(AFECTRL_HPREFPWR | AFECTRL_HSTIAPWR | AFECTRL_INAMPPWR |
AFECTRL_EXTBUFPWR | AFECTRL_DACREFPWR | AFECTRL_HSDACPWR |
AFECTRL_SINC2NOTCH, bTRUE);
/* RCAL reference: raw HSTIA DFT plus measured RTIA */
iImpCar_Type rcal_hstia;
fImpCar_Type rtia = measure_rtia(freq_hz, &rcal_hstia);
/* DUT: raw HSTIA DFT */
sw.Dswitch = ctx.dut_sw_d;
sw.Pswitch = ctx.dut_sw_p;
sw.Nswitch = ctx.dut_sw_n;
sw.Tswitch = ctx.dut_sw_t;
AD5940_SWMatrixCfgS(&sw);
settle(freq_hz, 2.0f, 200);
dft_measure_pair(freq_hz,
ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_tia,
ctx.dut_mux_vp, ctx.dut_mux_vn, &v_sense);
iImpCar_Type dut_hstia_raw = v_tia;
(void)v_sense;
/* power down, open switches */
AD5940_AFECtrlS(AFECTRL_WG | AFECTRL_ADCPWR | AFECTRL_ADCCNV |
AFECTRL_DFT | AFECTRL_SINC2NOTCH | AFECTRL_HSDACPWR |
AFECTRL_HSTIAPWR | AFECTRL_INAMPPWR |
AFECTRL_EXTBUFPWR, bFALSE);
sw.Dswitch = SWD_OPEN;
sw.Pswitch = SWP_OPEN;
sw.Nswitch = SWN_OPEN;
sw.Tswitch = SWT_OPEN;
AD5940_SWMatrixCfgS(&sw);
/* ratiometric Z: (DftRcal / DftDut) * RCAL */
fImpCar_Type fr = { (float)rcal_hstia.Real, (float)rcal_hstia.Image };
fImpCar_Type fd = { (float)dut_hstia_raw.Real, (float)dut_hstia_raw.Image };
fImpCar_Type z = AD5940_ComplexDivFloat(&fr, &fd);
z.Real *= ctx.rcal_ohms;
z.Image *= ctx.rcal_ohms;
/* apply open-circuit compensation if available */
if (ocal.valid) {
for (uint32_t k = 0; k < ocal.n; k++) {
if (fabsf(ocal.freq[k] - freq_hz) < freq_hz * 0.01f) {
fImpCar_Type one = {1.0f, 0.0f};
fImpCar_Type y_meas = AD5940_ComplexDivFloat(&one, &z);
fImpCar_Type y_corr = AD5940_ComplexSubFloat(&y_meas, &ocal.y[k]);
z = AD5940_ComplexDivFloat(&one, &y_corr);
break;
}
}
}
float mag = AD5940_ComplexMag(&z);
float phase = AD5940_ComplexPhase(&z) * (float)(180.0 / M_PI);
float rtia_mag = AD5940_ComplexMag(&rtia);
out->freq_hz = freq_hz;
out->z_real = z.Real;
out->z_imag = z.Image;
out->mag_ohms = mag;
out->phase_deg = phase;
out->rtia_mag_before = rtia_mag;
out->rtia_mag_after = rtia_mag;
out->rev_mag = mag;
out->rev_phase = phase;
out->pct_err = 0.0f;
return 0;
}
int eis_sweep(EISPoint *out, uint32_t max_points, eis_point_cb_t cb)
{
uint32_t n = eis_calc_num_points(&ctx.cfg);
if (n > max_points) n = max_points;
/* guard: throwaway at start frequency to warm up AFE */
EISPoint guard;
eis_measure_point(ctx.cfg.freq_start_hz, &guard);
SoftSweepCfg_Type sweep;
sweep.SweepEn = bTRUE;
sweep.SweepStart = ctx.cfg.freq_start_hz;
sweep.SweepStop = ctx.cfg.freq_stop_hz;
sweep.SweepPoints = n;
sweep.SweepLog = bTRUE;
sweep.SweepIndex = 0;
printf("\n%10s %12s %10s %12s %12s %6s\n",
"Freq(Hz)", "|Z|", "Phase", "Re", "Im", "ms");
printf("------------------------------------------------------------------\n");
uint32_t t0 = xTaskGetTickCount();
eis_measure_point(ctx.cfg.freq_start_hz, &out[0]);
uint32_t t1 = xTaskGetTickCount();
printf("%10.1f %12.2f %10.2f %12.2f %12.2f %6lu\n",
out[0].freq_hz, out[0].mag_ohms, out[0].phase_deg,
out[0].z_real, out[0].z_imag,
(unsigned long)((t1 - t0) * portTICK_PERIOD_MS));
if (cb) cb(0, &out[0]);
for (uint32_t i = 1; i < n; i++) {
float freq;
AD5940_SweepNext(&sweep, &freq);
t0 = xTaskGetTickCount();
eis_measure_point(freq, &out[i]);
t1 = xTaskGetTickCount();
printf("%10.1f %12.2f %10.2f %12.2f %12.2f %6lu\n",
out[i].freq_hz, out[i].mag_ohms, out[i].phase_deg,
out[i].z_real, out[i].z_imag,
(unsigned long)((t1 - t0) * portTICK_PERIOD_MS));
if (cb) cb((uint16_t)i, &out[i]);
}
/* guard: throwaway at stop frequency to cap the sweep cleanly */
eis_measure_point(ctx.cfg.freq_stop_hz, &guard);
AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
return (int)n;
}
#define NVS_OCAL_NS "eis"
#define NVS_OCAL_KEY "ocal"
static void ocal_save_nvs(void)
{
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READWRITE, &h) != ESP_OK) return;
nvs_set_blob(h, NVS_OCAL_KEY, &ocal, sizeof(ocal));
nvs_commit(h);
nvs_close(h);
}
static void ocal_erase_nvs(void)
{
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READWRITE, &h) != ESP_OK) return;
nvs_erase_key(h, NVS_OCAL_KEY);
nvs_commit(h);
nvs_close(h);
}
void eis_load_open_cal(void)
{
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READONLY, &h) != ESP_OK) return;
size_t len = sizeof(ocal);
if (nvs_get_blob(h, NVS_OCAL_KEY, &ocal, &len) == ESP_OK && len == sizeof(ocal) && ocal.valid) {
printf("Open-circuit cal loaded from NVS: %u points\n", (unsigned)ocal.n);
} else {
ocal.valid = 0;
ocal.n = 0;
}
nvs_close(h);
}
int eis_open_cal(EISPoint *buf, uint32_t max_points, eis_point_cb_t cb)
{
ocal.valid = 0;
ocal.n = 0;
int n = eis_sweep(buf, max_points, cb);
if (n <= 0) return n;
fImpCar_Type one = {1.0f, 0.0f};
for (int i = 0; i < n && i < EIS_MAX_POINTS; i++) {
fImpCar_Type z = { buf[i].z_real, buf[i].z_imag };
ocal.y[i] = AD5940_ComplexDivFloat(&one, &z);
ocal.freq[i] = buf[i].freq_hz;
}
ocal.n = (uint32_t)n;
ocal.valid = 1;
ocal_save_nvs();
printf("Open-circuit cal stored: %d points\n", n);
return n;
}
void eis_clear_open_cal(void)
{
ocal.valid = 0;
ocal.n = 0;
ocal_erase_nvs();
}
int eis_has_open_cal(void)
{
return ocal.valid;
}
#define NVS_CELLK_KEY "cell_k"
#define NVS_CLFACTOR_KEY "cl_factor"
#define NVS_PH_SLOPE_KEY "ph_slope"
#define NVS_PH_OFFSET_KEY "ph_offset"
void eis_set_cell_k(float k)
{
cell_k_cached = k;
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READWRITE, &h) != ESP_OK) return;
nvs_set_blob(h, NVS_CELLK_KEY, &k, sizeof(k));
nvs_commit(h);
nvs_close(h);
}
float eis_get_cell_k(void)
{
return cell_k_cached;
}
void eis_load_cell_k(void)
{
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READONLY, &h) != ESP_OK) return;
size_t len = sizeof(cell_k_cached);
if (nvs_get_blob(h, NVS_CELLK_KEY, &cell_k_cached, &len) != ESP_OK || len != sizeof(cell_k_cached))
cell_k_cached = 0.0f;
nvs_close(h);
}
void eis_set_cl_factor(float f)
{
cl_factor_cached = f;
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READWRITE, &h) != ESP_OK) return;
nvs_set_blob(h, NVS_CLFACTOR_KEY, &f, sizeof(f));
nvs_commit(h);
nvs_close(h);
}
float eis_get_cl_factor(void)
{
return cl_factor_cached;
}
void eis_load_cl_factor(void)
{
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READONLY, &h) != ESP_OK) return;
size_t len = sizeof(cl_factor_cached);
if (nvs_get_blob(h, NVS_CLFACTOR_KEY, &cl_factor_cached, &len) != ESP_OK || len != sizeof(cl_factor_cached))
cl_factor_cached = 0.0f;
nvs_close(h);
}
float eis_get_ph_slope(void) { return ph_slope_cached; }
float eis_get_ph_offset(void) { return ph_offset_cached; }
/* ---- 3-buffer × 3-temperature pH calibration ---- */
/* nominal 25 C pH of the three NIST primary buffers (phthalate, phosphate, borate) */
static const float PH_BUFFERS[PH_CAL_BUFFERS] = {4.01f, 6.86f, 9.18f};
/* NIST SRM buffer pH vs temperature, 0-50 C in 5 C steps */
#define PH_NIST_T_MIN 0
#define PH_NIST_T_STEP 5
#define PH_NIST_N 11
static const float PH_NIST_PHTHALATE[PH_NIST_N] = {
4.003f, 3.999f, 3.998f, 3.999f, 4.002f, 4.008f,
4.015f, 4.024f, 4.035f, 4.047f, 4.060f
};
static const float PH_NIST_PHOSPHATE[PH_NIST_N] = {
6.984f, 6.951f, 6.923f, 6.900f, 6.881f, 6.865f,
6.853f, 6.844f, 6.838f, 6.834f, 6.833f
};
static const float PH_NIST_BORATE[PH_NIST_N] = {
9.464f, 9.395f, 9.332f, 9.276f, 9.225f, 9.180f,
9.139f, 9.102f, 9.068f, 9.038f, 9.011f
};
static const float * const PH_NIST_TABLES[PH_CAL_BUFFERS] = {
PH_NIST_PHTHALATE, PH_NIST_PHOSPHATE, PH_NIST_BORATE
};
float eis_ph_buffer_at_temp(uint8_t buf, float temp_c)
{
if (buf >= PH_CAL_BUFFERS) return 0.0f;
const float *tbl = PH_NIST_TABLES[buf];
if (temp_c <= (float)PH_NIST_T_MIN) return tbl[0];
float t_max = (float)(PH_NIST_T_MIN + PH_NIST_T_STEP * (PH_NIST_N - 1));
if (temp_c >= t_max) return tbl[PH_NIST_N - 1];
float f = (temp_c - (float)PH_NIST_T_MIN) / (float)PH_NIST_T_STEP;
int i = (int)f;
float frac = f - (float)i;
return tbl[i] + frac * (tbl[i + 1] - tbl[i]);
}
#define NVS_PH_CAL_PTS_KEY "ph_cal9"
typedef struct {
float ocp_mv;
float temp_c;
} PhCalSample;
static struct {
PhCalSample s[PH_CAL_BUFFERS][PH_CAL_TEMPS];
uint16_t valid;
} ph_cal;
static float ph_temp_slope_cold;
static float ph_temp_slope_hot;
static void ph_cal_recalculate(void)
{
/* baseline slope/offset from the 3 baseline (tslot=1) points */
int n = 0;
float sx = 0, sy = 0, sxx = 0, sxy = 0;
for (int i = 0; i < PH_CAL_BUFFERS; i++) {
int bit = i * PH_CAL_TEMPS + PH_TEMP_BASE;
if (!(ph_cal.valid & (1 << bit))) continue;
float x = ph_cal.s[i][PH_TEMP_BASE].ocp_mv;
float y = eis_ph_buffer_at_temp(i, ph_cal.s[i][PH_TEMP_BASE].temp_c);
sx += x; sy += y; sxx += x * x; sxy += x * y;
n++;
}
if (n < 2) {
ph_slope_cached = 0;
ph_offset_cached = 0;
} else {
float d = (float)n * sxx - sx * sx;
if (fabsf(d) < 1e-10f) {
ph_slope_cached = 0;
ph_offset_cached = 0;
} else {
ph_slope_cached = ((float)n * sxy - sx * sy) / d;
ph_offset_cached = (sy - ph_slope_cached * sx) / (float)n;
}
}
printf("pH cal: baseline slope=%.6f offset=%.4f (%d pts)\n",
ph_slope_cached, ph_offset_cached, n);
/* temperature drift from off-temperature points */
ph_temp_slope_cold = 0;
ph_temp_slope_hot = 0;
int nc = 0, nh = 0;
for (int i = 0; i < PH_CAL_BUFFERS; i++) {
int base_bit = i * PH_CAL_TEMPS + PH_TEMP_BASE;
if (!(ph_cal.valid & (1 << base_bit))) continue;
float ocp_base = ph_cal.s[i][PH_TEMP_BASE].ocp_mv;
int cold_bit = i * PH_CAL_TEMPS + PH_TEMP_BELOW;
if (ph_cal.valid & (1 << cold_bit)) {
float dt = ph_cal.s[i][PH_TEMP_BELOW].temp_c - 25.0f;
if (fabsf(dt) > 0.5f) {
ph_temp_slope_cold += (ph_cal.s[i][PH_TEMP_BELOW].ocp_mv - ocp_base) / dt;
nc++;
}
}
int hot_bit = i * PH_CAL_TEMPS + PH_TEMP_ABOVE;
if (ph_cal.valid & (1 << hot_bit)) {
float dt = ph_cal.s[i][PH_TEMP_ABOVE].temp_c - 25.0f;
if (fabsf(dt) > 0.5f) {
ph_temp_slope_hot += (ph_cal.s[i][PH_TEMP_ABOVE].ocp_mv - ocp_base) / dt;
nh++;
}
}
}
if (nc > 0) ph_temp_slope_cold /= nc;
if (nh > 0) ph_temp_slope_hot /= nh;
if (nc > 0 || nh > 0)
printf("pH cal: temp drift cold=%.4f hot=%.4f mV/C\n",
ph_temp_slope_cold, ph_temp_slope_hot);
}
static void ph_cal_save(void)
{
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READWRITE, &h) != ESP_OK) return;
nvs_set_blob(h, NVS_PH_CAL_PTS_KEY, &ph_cal, sizeof(ph_cal));
nvs_commit(h);
nvs_close(h);
}
float eis_get_ph_temp_slope_cold(void) { return ph_temp_slope_cold; }
float eis_get_ph_temp_slope_hot(void) { return ph_temp_slope_hot; }
void eis_load_ph_cal(void)
{
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READONLY, &h) != ESP_OK) return;
size_t len = sizeof(ph_cal);
if (nvs_get_blob(h, NVS_PH_CAL_PTS_KEY, &ph_cal, &len) != ESP_OK
|| len != sizeof(ph_cal)) {
memset(&ph_cal, 0, sizeof(ph_cal));
}
nvs_close(h);
ph_cal_recalculate();
}
int eis_ph_cal_set_point(uint8_t buf, uint8_t tslot, float ocp_mv, float temp_c)
{
if (buf >= PH_CAL_BUFFERS || tslot >= PH_CAL_TEMPS) return -1;
ph_cal.s[buf][tslot].ocp_mv = ocp_mv;
ph_cal.s[buf][tslot].temp_c = temp_c;
ph_cal.valid |= (1 << (buf * PH_CAL_TEMPS + tslot));
ph_cal_recalculate();
ph_cal_save();
return 0;
}
int eis_ph_cal_clear_point(uint8_t buf, uint8_t tslot)
{
if (buf >= PH_CAL_BUFFERS || tslot >= PH_CAL_TEMPS) return -1;
int bit = buf * PH_CAL_TEMPS + tslot;
ph_cal.valid &= ~(1 << bit);
memset(&ph_cal.s[buf][tslot], 0, sizeof(PhCalSample));
ph_cal_recalculate();
ph_cal_save();
return 0;
}
void eis_ph_cal_clear_all(void)
{
memset(&ph_cal, 0, sizeof(ph_cal));
ph_slope_cached = 0;
ph_offset_cached = 0;
ph_temp_slope_cold = 0;
ph_temp_slope_hot = 0;
ph_cal_save();
}
bool eis_ph_cal_get_point(uint8_t buf, uint8_t tslot, float *ocp_mv, float *temp_c)
{
if (buf >= PH_CAL_BUFFERS || tslot >= PH_CAL_TEMPS) return false;
if (!(ph_cal.valid & (1 << (buf * PH_CAL_TEMPS + tslot)))) return false;
if (ocp_mv) *ocp_mv = ph_cal.s[buf][tslot].ocp_mv;
if (temp_c) *temp_c = ph_cal.s[buf][tslot].temp_c;
return true;
}
int eis_ph_cal_count(void)
{
int n = 0;
for (int i = 0; i < PH_CAL_BUFFERS * PH_CAL_TEMPS; i++)
if (ph_cal.valid & (1 << i)) n++;
return n;
}
float eis_ph_cal_buffer_ph(uint8_t buf)
{
if (buf >= PH_CAL_BUFFERS) return 0;
return PH_BUFFERS[buf];
}