#include "eis.h" #include #include #include #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; /* 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(); 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; }