808 lines
24 KiB
C
808 lines
24 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;
|
||
}
|
||
|
||
/* 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];
|
||
}
|