EIS-BLE-S3/main/eis.c

681 lines
21 KiB
C

#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);
}
void eis_set_ph_cal(float slope, float offset)
{
ph_slope_cached = slope;
ph_offset_cached = offset;
nvs_handle_t h;
if (nvs_open(NVS_OCAL_NS, NVS_READWRITE, &h) != ESP_OK) return;
nvs_set_blob(h, NVS_PH_SLOPE_KEY, &slope, sizeof(slope));
nvs_set_blob(h, NVS_PH_OFFSET_KEY, &offset, sizeof(offset));
nvs_commit(h);
nvs_close(h);
}
float eis_get_ph_slope(void) { return ph_slope_cached; }
float eis_get_ph_offset(void) { return ph_offset_cached; }
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_slope_cached);
if (nvs_get_blob(h, NVS_PH_SLOPE_KEY, &ph_slope_cached, &len) != ESP_OK || len != sizeof(ph_slope_cached))
ph_slope_cached = 0.0f;
len = sizeof(ph_offset_cached);
if (nvs_get_blob(h, NVS_PH_OFFSET_KEY, &ph_offset_cached, &len) != ESP_OK || len != sizeof(ph_offset_cached))
ph_offset_cached = 0.0f;
nvs_close(h);
}