EIS-BLE-S3/main/echem.c

554 lines
17 KiB
C

#include "echem.h"
#include "ad5940.h"
#include "ble.h"
#include <math.h>
#include <string.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
/* LP RTIA register mapping */
static const uint32_t lp_rtia_map[] = {
[LP_RTIA_200] = LPTIARTIA_200R,
[LP_RTIA_1K] = LPTIARTIA_1K,
[LP_RTIA_2K] = LPTIARTIA_2K,
[LP_RTIA_4K] = LPTIARTIA_4K,
[LP_RTIA_10K] = LPTIARTIA_10K,
[LP_RTIA_20K] = LPTIARTIA_20K,
[LP_RTIA_40K] = LPTIARTIA_40K,
[LP_RTIA_100K] = LPTIARTIA_100K,
[LP_RTIA_512K] = LPTIARTIA_512K,
};
/* LP RTIA ohms for current conversion */
static const float lp_rtia_ohms[] = {
[LP_RTIA_200] = 200.0f,
[LP_RTIA_1K] = 1000.0f,
[LP_RTIA_2K] = 2000.0f,
[LP_RTIA_4K] = 4000.0f,
[LP_RTIA_10K] = 10000.0f,
[LP_RTIA_20K] = 20000.0f,
[LP_RTIA_40K] = 40000.0f,
[LP_RTIA_100K] = 100000.0f,
[LP_RTIA_512K] = 512000.0f,
};
/*
* LPDAC math (2.5V reference):
* 6-bit DAC → VZERO: 200mV + code * 34.375mV (code 0-63)
* 12-bit DAC → VBIAS: 200mV + code * 0.537mV (code 0-4095)
* Cell potential: V_cell = VZERO - VBIAS
*
* VZERO fixed at ~1100mV (code 26).
* VBIAS swept to set cell potential.
*/
#define VZERO_CODE 26
#define VZERO_MV (200.0f + VZERO_CODE * 34.375f) /* ~1093.75 mV */
#define VBIAS_OFFSET 200.0f
#define VBIAS_LSB 0.537f
static uint16_t mv_to_vbias_code(float v_cell_mv)
{
/* V_cell = VZERO - VBIAS → VBIAS = VZERO - V_cell */
float vbias_mv = VZERO_MV - v_cell_mv;
float code = (vbias_mv - VBIAS_OFFSET) / VBIAS_LSB;
if (code < 0) code = 0;
if (code > 4095) code = 4095;
return (uint16_t)(code + 0.5f);
}
static void echem_init_lp(uint32_t rtia_reg)
{
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.LpBandgapEn = bTRUE;
ref.LpRefBufEn = bTRUE;
ref.LpRefBoostEn = bTRUE;
AD5940_REFCfgS(&ref);
AD5940_AFEPwrBW(AFEPWR_LP, AFEBW_250KHZ);
LPLoopCfg_Type lp;
AD5940_StructInit(&lp, sizeof(lp));
lp.LpDacCfg.LpdacSel = LPDAC0;
lp.LpDacCfg.LpDacSrc = LPDACSRC_MMR;
lp.LpDacCfg.LpDacVzeroMux = LPDACVZERO_6BIT;
lp.LpDacCfg.LpDacVbiasMux = LPDACVBIAS_12BIT;
lp.LpDacCfg.LpDacSW = LPDACSW_VZERO2LPTIA | LPDACSW_VBIAS2LPPA;
lp.LpDacCfg.LpDacRef = LPDACREF_2P5;
lp.LpDacCfg.DataRst = bFALSE;
lp.LpDacCfg.PowerEn = bTRUE;
lp.LpDacCfg.DacData6Bit = VZERO_CODE;
lp.LpDacCfg.DacData12Bit = mv_to_vbias_code(0);
lp.LpAmpCfg.LpAmpSel = LPAMP0;
lp.LpAmpCfg.LpAmpPwrMod = LPAMPPWR_BOOST3;
lp.LpAmpCfg.LpPaPwrEn = bTRUE;
lp.LpAmpCfg.LpTiaPwrEn = bTRUE;
lp.LpAmpCfg.LpTiaRf = LPTIARF_SHORT;
lp.LpAmpCfg.LpTiaRload = LPTIARLOAD_SHORT;
lp.LpAmpCfg.LpTiaRtia = rtia_reg;
/* CE0=drive, RE0=ref, SE0=working: SW2,4,5,7,12 */
lp.LpAmpCfg.LpTiaSW = LPTIASW(2) | LPTIASW(4) | LPTIASW(5) |
LPTIASW(7) | LPTIASW(12);
AD5940_LPLoopCfgS(&lp);
/* ADC: SINC2+Notch on LPTIA output */
ADCBaseCfg_Type adc;
adc.ADCMuxP = ADCMUXP_LPTIA0_P;
adc.ADCMuxN = ADCMUXN_LPTIA0_N;
adc.ADCPga = ADCPGA_1P5;
AD5940_ADCBaseCfgS(&adc);
ADCFilterCfg_Type filt;
AD5940_StructInit(&filt, sizeof(filt));
filt.ADCSinc3Osr = ADCSINC3OSR_4;
filt.ADCSinc2Osr = ADCSINC2OSR_667;
filt.ADCAvgNum = ADCAVGNUM_16;
filt.ADCRate = ADCRATE_800KHZ;
filt.BpNotch = bFALSE;
filt.BpSinc3 = bFALSE;
filt.Sinc2NotchEnable = bTRUE;
filt.Sinc3ClkEnable = bTRUE;
filt.Sinc2NotchClkEnable = bTRUE;
filt.DFTClkEnable = bFALSE;
filt.WGClkEnable = bFALSE;
AD5940_ADCFilterCfgS(&filt);
AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_SINC2RDY, bTRUE);
AD5940_INTCCfg(AFEINTC_1, AFEINTSRC_SINC2RDY, 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);
}
static float read_current_ua(float rtia_ohms)
{
AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);
AD5940_AFECtrlS(AFECTRL_ADCPWR, bTRUE);
AD5940_Delay10us(25);
AD5940_AFECtrlS(AFECTRL_ADCCNV, bTRUE);
AD5940_ClrMCUIntFlag();
while (!AD5940_GetMCUIntFlag())
vTaskDelay(1);
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_ADCPWR, bFALSE);
AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);
uint32_t raw = AD5940_ReadAfeResult(AFERESULT_SINC2);
int32_t code = (raw & (1UL << 15)) ? (int32_t)(raw | 0xFFFF0000UL) : (int32_t)raw;
/* clamp near ADC saturation to prevent sign-flip wrap artifact */
if (code > 32700) code = 32700;
if (code < -32700) code = -32700;
/* I = V_tia / RTIA, V_tia = code * Vref / (PGA * 32768) */
float v_tia = (float)code * 1.82f / (1.5f * 32768.0f);
float i_a = v_tia / rtia_ohms;
return i_a * 1e6f; /* convert to uA */
}
static void echem_init_adc(void)
{
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.LpBandgapEn = bTRUE;
ref.LpRefBufEn = bTRUE;
ref.LpRefBoostEn = bTRUE;
AD5940_REFCfgS(&ref);
AD5940_AFEPwrBW(AFEPWR_LP, AFEBW_250KHZ);
ADCFilterCfg_Type filt;
AD5940_StructInit(&filt, sizeof(filt));
filt.ADCSinc3Osr = ADCSINC3OSR_4;
filt.ADCSinc2Osr = ADCSINC2OSR_667;
filt.ADCAvgNum = ADCAVGNUM_16;
filt.ADCRate = ADCRATE_800KHZ;
filt.BpNotch = bFALSE;
filt.BpSinc3 = bFALSE;
filt.Sinc2NotchEnable = bTRUE;
filt.Sinc3ClkEnable = bTRUE;
filt.Sinc2NotchClkEnable = bTRUE;
filt.DFTClkEnable = bFALSE;
filt.WGClkEnable = bFALSE;
AD5940_ADCFilterCfgS(&filt);
AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_SINC2RDY, bTRUE);
AD5940_INTCCfg(AFEINTC_1, AFEINTSRC_SINC2RDY, 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);
}
static float read_voltage_mv(uint32_t muxp)
{
AD5940_ADCMuxCfgS(muxp, ADCMUXN_VSET1P1);
AD5940_Delay10us(50);
AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);
AD5940_AFECtrlS(AFECTRL_ADCPWR, bTRUE);
AD5940_Delay10us(25);
AD5940_AFECtrlS(AFECTRL_ADCCNV, bTRUE);
AD5940_ClrMCUIntFlag();
while (!AD5940_GetMCUIntFlag())
vTaskDelay(1);
AD5940_AFECtrlS(AFECTRL_ADCCNV | AFECTRL_ADCPWR, bFALSE);
AD5940_INTCClrFlag(AFEINTSRC_SINC2RDY);
uint32_t raw = AD5940_ReadAfeResult(AFERESULT_SINC2);
int32_t code = (raw & (1UL << 15)) ? (int32_t)(raw | 0xFFFF0000UL) : (int32_t)raw;
/* V_diff = code * Vref / (PGA * 32768), PGA=1.5, Vref=1.82V */
return (float)code * 1820.0f / (1.5f * 32768.0f);
}
static void echem_shutdown_lp(void)
{
LPLoopCfg_Type lp;
AD5940_StructInit(&lp, sizeof(lp));
lp.LpDacCfg.LpdacSel = LPDAC0;
lp.LpDacCfg.LpDacSrc = LPDACSRC_MMR;
lp.LpDacCfg.LpDacVzeroMux = LPDACVZERO_6BIT;
lp.LpDacCfg.LpDacVbiasMux = LPDACVBIAS_12BIT;
lp.LpDacCfg.LpDacSW = 0;
lp.LpDacCfg.LpDacRef = LPDACREF_2P5;
lp.LpDacCfg.PowerEn = bFALSE;
lp.LpDacCfg.DataRst = bFALSE;
lp.LpDacCfg.DacData6Bit = 0;
lp.LpDacCfg.DacData12Bit = 0;
lp.LpAmpCfg.LpAmpSel = LPAMP0;
lp.LpAmpCfg.LpAmpPwrMod = LPAMPPWR_NORM;
lp.LpAmpCfg.LpPaPwrEn = bFALSE;
lp.LpAmpCfg.LpTiaPwrEn = bFALSE;
lp.LpAmpCfg.LpTiaRf = LPTIARF_OPEN;
lp.LpAmpCfg.LpTiaRload = LPTIARLOAD_SHORT;
lp.LpAmpCfg.LpTiaRtia = LPTIARTIA_OPEN;
lp.LpAmpCfg.LpTiaSW = 0;
AD5940_LPLoopCfgS(&lp);
SWMatrixCfg_Type sw = { SWD_OPEN, SWP_OPEN, SWN_OPEN, SWT_OPEN };
AD5940_SWMatrixCfgS(&sw);
}
/* ---- public ---- */
void echem_default_lsv(LSVConfig *cfg)
{
memset(cfg, 0, sizeof(*cfg));
cfg->v_start = 0.0f;
cfg->v_stop = 500.0f;
cfg->scan_rate = 50.0f;
cfg->lp_rtia = LP_RTIA_10K;
}
void echem_default_amp(AmpConfig *cfg)
{
memset(cfg, 0, sizeof(*cfg));
cfg->v_hold = 200.0f;
cfg->interval_ms = 100.0f;
cfg->duration_s = 60.0f;
cfg->lp_rtia = LP_RTIA_10K;
}
static void lsv_calc_step(const LSVConfig *cfg, uint32_t max_points,
uint32_t *n_out, float *step_out)
{
float v_range = cfg->v_stop - cfg->v_start;
uint32_t n_lsb = (uint32_t)(fabsf(v_range / VBIAS_LSB) + 0.5f);
uint32_t n_steps = n_lsb;
uint32_t step_mult = 1;
if (n_steps > max_points) {
step_mult = (n_lsb + max_points - 1) / max_points;
n_steps = (n_lsb + step_mult - 1) / step_mult;
}
if (n_steps < 2) n_steps = 2;
*n_out = n_steps;
*step_out = (v_range > 0) ? VBIAS_LSB * step_mult : -VBIAS_LSB * step_mult;
}
uint32_t echem_lsv_calc_steps(const LSVConfig *cfg, uint32_t max_points)
{
float v_range = cfg->v_stop - cfg->v_start;
if (fabsf(v_range) < 0.001f) return 0;
uint32_t n;
float step;
lsv_calc_step(cfg, max_points, &n, &step);
return n;
}
int echem_lsv(const LSVConfig *cfg, LSVPoint *out, uint32_t max_points, lsv_point_cb_t cb)
{
if (cfg->lp_rtia >= LP_RTIA_COUNT) return 0;
float rtia = lp_rtia_ohms[cfg->lp_rtia];
echem_init_lp(lp_rtia_map[cfg->lp_rtia]);
/* set starting voltage and flush SINC2 filter */
AD5940_LPDAC0WriteS(mv_to_vbias_code(cfg->v_start), VZERO_CODE);
vTaskDelay(pdMS_TO_TICKS(50));
for (int i = 0; i < 4; i++)
read_current_ua(rtia);
float v_range = cfg->v_stop - cfg->v_start;
if (fabsf(v_range) < 0.001f) return 0;
uint32_t n_steps;
float step;
lsv_calc_step(cfg, max_points, &n_steps, &step);
float delay_ms = fabsf(step / cfg->scan_rate) * 1000.0f;
if (delay_ms < 1.0f) delay_ms = 1.0f;
TickType_t ticks = pdMS_TO_TICKS((uint32_t)delay_ms);
if (ticks < 1) ticks = 1;
printf("\n%10s %10s\n", "V(mV)", "I(uA)");
printf("------------------------\n");
for (uint32_t i = 0; i < n_steps; i++) {
float v_mv = cfg->v_start + i * step;
uint16_t code = mv_to_vbias_code(v_mv);
AD5940_LPDAC0WriteS(code, VZERO_CODE);
vTaskDelay(ticks);
float i_ua = read_current_ua(rtia);
out[i].v_mv = v_mv;
out[i].i_ua = i_ua;
printf("%10.1f %10.3f\n", v_mv, i_ua);
if (cb) cb((uint16_t)i, v_mv, i_ua);
}
echem_shutdown_lp();
AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
return (int)n_steps;
}
int echem_amp(const AmpConfig *cfg, AmpPoint *out, uint32_t max_points, amp_point_cb_t cb)
{
if (cfg->lp_rtia >= LP_RTIA_COUNT) return 0;
float rtia = lp_rtia_ohms[cfg->lp_rtia];
echem_init_lp(lp_rtia_map[cfg->lp_rtia]);
uint16_t code = mv_to_vbias_code(cfg->v_hold);
AD5940_LPDAC0WriteS(code, VZERO_CODE);
vTaskDelay(pdMS_TO_TICKS(50));
for (int i = 0; i < 4; i++)
read_current_ua(rtia);
TickType_t interval = pdMS_TO_TICKS((uint32_t)cfg->interval_ms);
if (interval < 1) interval = 1;
uint32_t max_samples = max_points;
if (cfg->duration_s > 0) {
uint32_t duration_n = (uint32_t)(cfg->duration_s * 1000.0f / cfg->interval_ms + 0.5f);
if (duration_n < max_samples) max_samples = duration_n;
}
printf("\n%10s %10s\n", "t(ms)", "I(uA)");
printf("------------------------\n");
TickType_t t0 = xTaskGetTickCount();
uint32_t count = 0;
for (uint32_t i = 0; i < max_samples; i++) {
BleCommand cmd;
if (ble_recv_command(&cmd, 0) == 0 && cmd.type == CMD_STOP_AMP)
break;
float i_ua = read_current_ua(rtia);
float t_ms = (float)(xTaskGetTickCount() - t0) * portTICK_PERIOD_MS;
out[i].t_ms = t_ms;
out[i].i_ua = i_ua;
count++;
printf("%10.1f %10.3f\n", t_ms, i_ua);
if (cb) cb((uint16_t)i, t_ms, i_ua);
vTaskDelay(interval);
}
echem_shutdown_lp();
AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
return (int)count;
}
void echem_default_cl(ClConfig *cfg)
{
memset(cfg, 0, sizeof(*cfg));
cfg->v_cond = 800.0f; /* +800 mV conditioning pulse */
cfg->t_cond_ms = 2000.0f;
cfg->v_free = 100.0f; /* +100 mV for HOCl reduction */
cfg->v_total = -200.0f; /* -200 mV for total chlorine */
cfg->t_dep_ms = 5000.0f; /* 5s settling */
cfg->t_meas_ms = 5000.0f; /* 5s sampling */
cfg->lp_rtia = LP_RTIA_10K;
}
static uint32_t sample_phase(float v_mv, float t_dep_ms, float t_meas_ms,
uint8_t phase, float rtia_ohms,
ClPoint *out, uint32_t idx, uint32_t max_points,
TickType_t t0, float *avg_out, cl_point_cb_t cb)
{
AD5940_LPDAC0WriteS(mv_to_vbias_code(v_mv), VZERO_CODE);
/* settling — no samples recorded */
vTaskDelay(pdMS_TO_TICKS((uint32_t)t_dep_ms));
/* measurement — sample at ~50ms intervals */
uint32_t n_samples = (uint32_t)(t_meas_ms / 50.0f + 0.5f);
if (n_samples < 2) n_samples = 2;
TickType_t interval = pdMS_TO_TICKS(50);
float sum = 0;
uint32_t count = 0;
for (uint32_t i = 0; i < n_samples && idx < max_points; i++) {
float i_ua = read_current_ua(rtia_ohms);
float t_ms = (float)(xTaskGetTickCount() - t0) * portTICK_PERIOD_MS;
out[idx].t_ms = t_ms;
out[idx].i_ua = i_ua;
out[idx].phase = phase;
if (cb) cb((uint16_t)idx, t_ms, i_ua, phase);
idx++;
sum += i_ua;
count++;
vTaskDelay(interval);
}
*avg_out = (count > 0) ? sum / (float)count : 0.0f;
return idx;
}
int echem_chlorine(const ClConfig *cfg, ClPoint *out, uint32_t max_points,
ClResult *result, cl_point_cb_t cb)
{
if (cfg->lp_rtia >= LP_RTIA_COUNT) return 0;
float rtia = lp_rtia_ohms[cfg->lp_rtia];
echem_init_lp(lp_rtia_map[cfg->lp_rtia]);
TickType_t t0 = xTaskGetTickCount();
uint32_t idx = 0;
printf("Cl: conditioning at %.0f mV for %.0f ms\n", cfg->v_cond, cfg->t_cond_ms);
AD5940_LPDAC0WriteS(mv_to_vbias_code(cfg->v_cond), VZERO_CODE);
vTaskDelay(pdMS_TO_TICKS((uint32_t)cfg->t_cond_ms));
printf("Cl: free chlorine at %.0f mV\n", cfg->v_free);
idx = sample_phase(cfg->v_free, cfg->t_dep_ms, cfg->t_meas_ms,
CL_PHASE_FREE, rtia, out, idx, max_points, t0,
&result->i_free_ua, cb);
printf("Cl: total chlorine at %.0f mV\n", cfg->v_total);
idx = sample_phase(cfg->v_total, cfg->t_dep_ms, cfg->t_meas_ms,
CL_PHASE_TOTAL, rtia, out, idx, max_points, t0,
&result->i_total_ua, cb);
printf("Cl: free=%.3f uA, total=%.3f uA\n", result->i_free_ua, result->i_total_ua);
echem_shutdown_lp();
AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
return (int)idx;
}
void echem_default_ph(PhConfig *cfg)
{
memset(cfg, 0, sizeof(*cfg));
cfg->stabilize_s = 30.0f;
cfg->temp_c = 25.0f;
}
int echem_ph_ocp(const PhConfig *cfg, PhResult *result)
{
echem_init_adc();
/* ADC mux: read SE0 and RE0 pin voltages directly */
ADCBaseCfg_Type adc;
adc.ADCMuxP = ADCMUXP_VSE0;
adc.ADCMuxN = ADCMUXN_VSET1P1;
adc.ADCPga = ADCPGA_1P5;
AD5940_ADCBaseCfgS(&adc);
printf("pH: stabilizing %0.f s\n", cfg->stabilize_s);
vTaskDelay(pdMS_TO_TICKS((uint32_t)(cfg->stabilize_s * 1000.0f)));
/* average N readings of V(SE0) and V(RE0) */
#define PH_AVG_N 10
float sum_se0 = 0, sum_re0 = 0;
for (int i = 0; i < PH_AVG_N; i++) {
sum_se0 += read_voltage_mv(ADCMUXP_VSE0);
sum_re0 += read_voltage_mv(ADCMUXP_VRE0);
vTaskDelay(pdMS_TO_TICKS(100));
}
float v_se0 = sum_se0 / PH_AVG_N;
float v_re0 = sum_re0 / PH_AVG_N;
float ocp = v_se0 - v_re0;
float t_k = cfg->temp_c + 273.15f;
float slope = 0.1984f * t_k; /* mV/pH at temperature */
result->v_ocp_mv = ocp;
result->ph = 7.0f - ocp / slope;
result->temp_c = cfg->temp_c;
printf("pH: SE0=%.1f mV, RE0=%.1f mV, OCP=%.1f mV, pH=%.2f\n",
v_se0, v_re0, ocp, result->ph);
echem_shutdown_lp();
AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
return 0;
}