604 lines
18 KiB
C
604 lines
18 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 */
|
|
const uint32_t lp_rtia_map[] = {
|
|
[LP_RTIA_200] = LPTIARTIA_200R,
|
|
[LP_RTIA_1K] = LPTIARTIA_1K,
|
|
[LP_RTIA_2K] = LPTIARTIA_2K,
|
|
[LP_RTIA_3K] = LPTIARTIA_3K,
|
|
[LP_RTIA_4K] = LPTIARTIA_4K,
|
|
[LP_RTIA_6K] = LPTIARTIA_6K,
|
|
[LP_RTIA_8K] = LPTIARTIA_8K,
|
|
[LP_RTIA_10K] = LPTIARTIA_10K,
|
|
[LP_RTIA_12K] = LPTIARTIA_12K,
|
|
[LP_RTIA_16K] = LPTIARTIA_16K,
|
|
[LP_RTIA_20K] = LPTIARTIA_20K,
|
|
[LP_RTIA_24K] = LPTIARTIA_24K,
|
|
[LP_RTIA_30K] = LPTIARTIA_30K,
|
|
[LP_RTIA_32K] = LPTIARTIA_32K,
|
|
[LP_RTIA_40K] = LPTIARTIA_40K,
|
|
[LP_RTIA_48K] = LPTIARTIA_48K,
|
|
[LP_RTIA_64K] = LPTIARTIA_64K,
|
|
[LP_RTIA_85K] = LPTIARTIA_85K,
|
|
[LP_RTIA_96K] = LPTIARTIA_96K,
|
|
[LP_RTIA_100K] = LPTIARTIA_100K,
|
|
[LP_RTIA_120K] = LPTIARTIA_120K,
|
|
[LP_RTIA_128K] = LPTIARTIA_128K,
|
|
[LP_RTIA_160K] = LPTIARTIA_160K,
|
|
[LP_RTIA_196K] = LPTIARTIA_196K,
|
|
[LP_RTIA_256K] = LPTIARTIA_256K,
|
|
[LP_RTIA_512K] = LPTIARTIA_512K,
|
|
};
|
|
|
|
/* LP RTIA ohms for current conversion */
|
|
const float lp_rtia_ohms[] = {
|
|
[LP_RTIA_200] = 200.0f,
|
|
[LP_RTIA_1K] = 1000.0f,
|
|
[LP_RTIA_2K] = 2000.0f,
|
|
[LP_RTIA_3K] = 3000.0f,
|
|
[LP_RTIA_4K] = 4000.0f,
|
|
[LP_RTIA_6K] = 6000.0f,
|
|
[LP_RTIA_8K] = 8000.0f,
|
|
[LP_RTIA_10K] = 10000.0f,
|
|
[LP_RTIA_12K] = 12000.0f,
|
|
[LP_RTIA_16K] = 16000.0f,
|
|
[LP_RTIA_20K] = 20000.0f,
|
|
[LP_RTIA_24K] = 24000.0f,
|
|
[LP_RTIA_30K] = 30000.0f,
|
|
[LP_RTIA_32K] = 32000.0f,
|
|
[LP_RTIA_40K] = 40000.0f,
|
|
[LP_RTIA_48K] = 48000.0f,
|
|
[LP_RTIA_64K] = 64000.0f,
|
|
[LP_RTIA_85K] = 85000.0f,
|
|
[LP_RTIA_96K] = 96000.0f,
|
|
[LP_RTIA_100K] = 100000.0f,
|
|
[LP_RTIA_120K] = 120000.0f,
|
|
[LP_RTIA_128K] = 128000.0f,
|
|
[LP_RTIA_160K] = 160000.0f,
|
|
[LP_RTIA_196K] = 196000.0f,
|
|
[LP_RTIA_256K] = 256000.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 ---- */
|
|
|
|
int echem_clean(float v_mv, float duration_s)
|
|
{
|
|
echem_init_lp(LPTIARTIA_200R);
|
|
|
|
uint16_t code = mv_to_vbias_code(v_mv);
|
|
AD5940_LPDAC0WriteS(code, VZERO_CODE);
|
|
|
|
printf("Clean: %.0f mV for %.0f s\n", v_mv, duration_s);
|
|
vTaskDelay(pdMS_TO_TICKS((uint32_t)(duration_s * 1000.0f)));
|
|
|
|
echem_shutdown_lp();
|
|
AD5940_AFECtrlS(AFECTRL_ALL, bFALSE);
|
|
printf("Clean: done\n");
|
|
return 0;
|
|
}
|
|
|
|
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;
|
|
}
|