EIS-BLE-S3/main/eis.c

819 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;
fp.DftNum = DFTNUM_4096;
}
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;
}
/* paired DFT: two measurements under continuous WG excitation */
static void dft_measure_pair(
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);
AD5940_Delay10us(25);
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, short settle */
AD5940_ADCMuxCfgS(mux2_p, mux2_n);
AD5940_ReadAfeResult(AFERESULT_DFTREAL);
AD5940_ReadAfeResult(AFERESULT_DFTIMAGE);
AD5940_INTCClrFlag(AFEINTSRC_DFTRDY);
AD5940_Delay10us(5);
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(iImpCar_Type *out_hstia)
{
iImpCar_Type v_rcal, v_raw;
dft_measure_pair(
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;
/* switch to RCAL 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 before — capture raw HSTIA DFT for ratiometric diagnostic */
iImpCar_Type rcal_hstia;
fImpCar_Type rtia_before = measure_rtia(&rcal_hstia);
/* DUT forward */
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);
AD5940_Delay10us(50);
dft_measure_pair(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;
v_tia.Real = -v_tia.Real;
v_tia.Image = -v_tia.Image;
iImpCar_Type v_tia_fwd = v_tia;
iImpCar_Type v_sense_fwd = v_sense;
/* RCAL after */
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_Delay10us(50);
fImpCar_Type rtia_after = measure_rtia(NULL);
/* DUT reverse (DUT first, then RCAL) */
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);
AD5940_Delay10us(50);
dft_measure_pair(ADCMUXP_HSTIA_P, ADCMUXN_HSTIA_N, &v_tia,
ctx.dut_mux_vp, ctx.dut_mux_vn, &v_sense);
v_tia.Real = -v_tia.Real;
v_tia.Image = -v_tia.Image;
iImpCar_Type v_tia_rev = v_tia;
iImpCar_Type v_sense_rev = v_sense;
/* RCAL reverse */
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_Delay10us(50);
fImpCar_Type rtia_rev = measure_rtia(NULL);
/* 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);
/* forward Z using averaged RTIA bracket */
fImpCar_Type rtia_avg = {
.Real = (rtia_before.Real + rtia_after.Real) * 0.5f,
.Image = (rtia_before.Image + rtia_after.Image) * 0.5f,
};
fImpCar_Type fs_fwd = { (float)v_sense_fwd.Real, (float)v_sense_fwd.Image };
fImpCar_Type ft_fwd = { (float)v_tia_fwd.Real, (float)v_tia_fwd.Image };
fImpCar_Type num = AD5940_ComplexMulFloat(&fs_fwd, &rtia_avg);
fImpCar_Type z_fwd = AD5940_ComplexDivFloat(&num, &ft_fwd);
/* reverse Z using RTIA from RCAL measured after DUT */
fImpCar_Type fs_rev = { (float)v_sense_rev.Real, (float)v_sense_rev.Image };
fImpCar_Type ft_rev = { (float)v_tia_rev.Real, (float)v_tia_rev.Image };
num = AD5940_ComplexMulFloat(&fs_rev, &rtia_rev);
fImpCar_Type z_rev = AD5940_ComplexDivFloat(&num, &ft_rev);
(void)z_rev;
/* HSTIA-only 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_ratio = AD5940_ComplexDivFloat(&fr, &fd);
z_ratio.Real *= ctx.rcal_ohms;
z_ratio.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_fwd);
fImpCar_Type y_corr = AD5940_ComplexSubFloat(&y_meas, &ocal.y[k]);
z_fwd = AD5940_ComplexDivFloat(&one, &y_corr);
break;
}
}
}
float mag_fwd = AD5940_ComplexMag(&z_fwd);
out->freq_hz = freq_hz;
out->z_real = z_fwd.Real;
out->z_imag = z_fwd.Image;
out->mag_ohms = mag_fwd;
out->phase_deg = AD5940_ComplexPhase(&z_fwd) * (float)(180.0 / M_PI);
out->rtia_mag_before = AD5940_ComplexMag(&rtia_before);
out->rtia_mag_after = AD5940_ComplexMag(&rtia_after);
out->rev_mag = AD5940_ComplexMag(&z_ratio);
out->rev_phase = AD5940_ComplexPhase(&z_ratio) * (float)(180.0 / M_PI);
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 | %12s %10s %6s\n",
"Freq(Hz)", "|Z|dual", "Ph_dual", "Re_dual", "Im_dual",
"|Z|ratio", "Ph_ratio", "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 | %12.2f %10.2f %6lu\n",
out[0].freq_hz, out[0].mag_ohms, out[0].phase_deg,
out[0].z_real, out[0].z_imag,
out[0].rev_mag, out[0].rev_phase,
(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 | %12.2f %10.2f %6lu\n",
out[i].freq_hz, out[i].mag_ohms, out[i].phase_deg,
out[i].z_real, out[i].z_imag,
out[i].rev_mag, out[i].rev_phase,
(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 ---- */
static const float PH_BUFFERS[PH_CAL_BUFFERS] = {4.0f, 6.86f, 9.0f};
#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 = PH_BUFFERS[i];
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];
}