From 6adf55bc478c97161b2d4be6ec597242b7716d0f Mon Sep 17 00:00:00 2001 From: pszsh Date: Sat, 31 Jan 2026 21:37:27 -0800 Subject: [PATCH] commit --- .gitignore | 1 + Amperometric.c | 495 +++++++++++++++++++++ Amperometric.h | 95 ++++ CMakeLists.txt | 2 + Impedance.c | 19 +- RampTest.c | 583 +++++++++++++++++++++++++ RampTest.h | 66 +++ host/src/GraphWidget.cpp | 309 +++++++++---- host/src/GraphWidget.h | 38 +- host/src/MainWindow.cpp | 904 +++++++++++++++++++++++++++++++-------- host/src/MainWindow.h | 78 +++- host/src/main.cpp | 58 ++- main.c | 367 +++++++++++++--- 13 files changed, 2691 insertions(+), 324 deletions(-) create mode 100644 Amperometric.c create mode 100644 Amperometric.h create mode 100644 RampTest.c create mode 100644 RampTest.h diff --git a/.gitignore b/.gitignore index c352adf..3bf84da 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ build* *.png icons/ *.pdf +*.txt examples/ build/ diff --git a/Amperometric.c b/Amperometric.c new file mode 100644 index 0000000..c9d39fc --- /dev/null +++ b/Amperometric.c @@ -0,0 +1,495 @@ +// Amperometric.c +/*! + ***************************************************************************** + @file: Amperometric.c + @author: $Author: mlambe $ + @brief: Amperometric measurement. + @version: $Revision: 766 $ + @date: $Date: 2018-03-21 14:09:35 +0100 (Wed, 21 Mar 2018) $ + ----------------------------------------------------------------------------- + +Copyright (c) 2017-2019 Analog Devices, Inc. All Rights Reserved. + +This software is proprietary to Analog Devices, Inc. and its licensors. +By using this software you agree to the terms of the associated +Analog Devices Software License Agreement. + +*****************************************************************************/ +#include "Amperometric.h" + +#define AD5940ERR_STOP 10 + +/* + Application configuration structure. Specified by user from template. + The variables are usable in this whole application. + It includes basic configuration for sequencer generator and application related parameters +*/ +AppAMPCfg_Type AppAMPCfg = +{ + .bParaChanged = bFALSE, + .SeqStartAddr = 0, + .MaxSeqLen = 0, + + .SeqStartAddrCal = 0, + .MaxSeqLenCal = 0, + .FifoThresh = 5, /* Number of points for FIFO */ + + .SysClkFreq = 16000000.0, + .WuptClkFreq = 32000.0, + .AdcClkFreq = 16000000.0, + .AmpODR = 1.0, /* Sample time in seconds. I.e. every 5 seconds make a measurement */ + .NumOfData = -1, + .RcalVal = 100.0, /* RCAL = 100 Ohms */ + .PwrMod = AFEPWR_LP, + .AMPInited = bFALSE, + .StopRequired = bFALSE, + + /* LPTIA Configure */ + .ExtRtia = bFALSE, /* Set to true if using external RTIA */ + .LptiaRtiaSel = LPTIARTIA_4K, /* COnfigure RTIA */ + .LpTiaRf = LPTIARF_1M, /* Configure LPF resistor */ + .LpTiaRl = LPTIARLOAD_100R, + .ReDoRtiaCal = bTRUE, + .RtiaCalValue = 0, + .ExtRtiaVal = 0, + +/*LPDAC Configure */ + .Vzero = 1100, /* Sets voltage on SE0 and LPTIA */ + .SensorBias = 500, /* Sets voltage between RE0 and SE0 */ + +/* ADC Configure*/ + .ADCPgaGain = ADCPGA_1P5, + .ADCSinc3Osr = ADCSINC3OSR_4, + .ADCSinc2Osr = ADCSINC2OSR_22, + .DataFifoSrc = FIFOSRC_SINC2NOTCH, + .ADCRefVolt = 1.8162, /* Measure voltage on ADCRefVolt pin and enter here*/ +}; + +/** + This function is provided for upper controllers that want to change + application parameters specially for user defined parameters. +*/ +AD5940Err AppAMPGetCfg(void *pCfg) +{ + if(pCfg){ + *(AppAMPCfg_Type**)pCfg = &AppAMPCfg; + return AD5940ERR_OK; + } + return AD5940ERR_PARA; +} + +AD5940Err AppAMPCtrl(int32_t AmpCtrl, void *pPara) +{ + switch (AmpCtrl) + { + case AMPCTRL_START: + { + WUPTCfg_Type wupt_cfg; + + AD5940_ReadReg(REG_AFE_ADCDAT); /* Any SPI Operation can wakeup AFE */ + if(AppAMPCfg.AMPInited == bFALSE) + return AD5940ERR_APPERROR; + /* Start it */ + wupt_cfg.WuptEn = bTRUE; + wupt_cfg.WuptEndSeq = WUPTENDSEQ_A; + wupt_cfg.WuptOrder[0] = SEQID_0; + wupt_cfg.SeqxSleepTime[SEQID_0] = 4-1; + wupt_cfg.SeqxWakeupTime[SEQID_0] = (uint32_t)(AppAMPCfg.WuptClkFreq*AppAMPCfg.AmpODR)-4-1; + AD5940_WUPTCfg(&wupt_cfg); + + AppAMPCfg.FifoDataCount = 0; /* restart */ + break; + } + case AMPCTRL_STOPNOW: + { + AD5940_ReadReg(REG_AFE_ADCDAT); /* Any SPI Operation can wakeup AFE */ + /* Start Wupt right now */ + AD5940_WUPTCtrl(bFALSE); + /* There is chance this operation will fail because sequencer could put AFE back + to hibernate mode just after waking up. Use STOPSYNC is better. */ + AD5940_WUPTCtrl(bFALSE); + break; + } + case AMPCTRL_STOPSYNC: + { + AppAMPCfg.StopRequired = bTRUE; + break; + } + case AMPCTRL_SHUTDOWN: + { + AppAMPCtrl(AMPCTRL_STOPNOW, 0); /* Stop the measurement if it's running. */ + /* Turn off LPloop related blocks which are not controlled automatically by sleep operation */ + AFERefCfg_Type aferef_cfg; + LPLoopCfg_Type lp_loop; + memset(&aferef_cfg, 0, sizeof(aferef_cfg)); + AD5940_REFCfgS(&aferef_cfg); + memset(&lp_loop, 0, sizeof(lp_loop)); + AD5940_LPLoopCfgS(&lp_loop); + AD5940_EnterSleepS(); /* Enter Hibernate */ + } + break; + default: + break; + } + return AD5940ERR_OK; +} + +/* Generate init sequence */ +static AD5940Err AppAMPSeqCfgGen(void) +{ + AD5940Err error = AD5940ERR_OK; + uint32_t const *pSeqCmd; + uint32_t SeqLen; + + AFERefCfg_Type aferef_cfg; + LPLoopCfg_Type lp_loop; + DSPCfg_Type dsp_cfg; + SWMatrixCfg_Type sw_cfg; + /* Start sequence generator here */ + AD5940_SEQGenCtrl(bTRUE); + + //AD5940_AFECtrlS(AFECTRL_ALL, bFALSE); /* Init all to disable state */ + + aferef_cfg.HpBandgapEn = bTRUE; + aferef_cfg.Hp1V1BuffEn = bTRUE; + aferef_cfg.Hp1V8BuffEn = bTRUE; + aferef_cfg.Disc1V1Cap = bFALSE; + aferef_cfg.Disc1V8Cap = bFALSE; + aferef_cfg.Hp1V8ThemBuff = bFALSE; + aferef_cfg.Hp1V8Ilimit = bFALSE; + aferef_cfg.Lp1V1BuffEn = bTRUE; + aferef_cfg.Lp1V8BuffEn = bTRUE; + /* LP reference control - turn off them to save power*/ + aferef_cfg.LpBandgapEn = bTRUE; + aferef_cfg.LpRefBufEn = bTRUE; + aferef_cfg.LpRefBoostEn = bFALSE; + AD5940_REFCfgS(&aferef_cfg); + + lp_loop.LpDacCfg.LpdacSel = LPDAC0; + lp_loop.LpDacCfg.LpDacSrc = LPDACSRC_MMR; + lp_loop.LpDacCfg.LpDacSW = LPDACSW_VBIAS2LPPA|LPDACSW_VBIAS2PIN|LPDACSW_VZERO2LPTIA|LPDACSW_VZERO2PIN; + lp_loop.LpDacCfg.LpDacVzeroMux = LPDACVZERO_6BIT; + lp_loop.LpDacCfg.LpDacVbiasMux = LPDACVBIAS_12BIT; + lp_loop.LpDacCfg.LpDacRef = LPDACREF_2P5; + lp_loop.LpDacCfg.DataRst = bFALSE; + lp_loop.LpDacCfg.PowerEn = bTRUE; + lp_loop.LpDacCfg.DacData6Bit = (uint32_t)((AppAMPCfg.Vzero-200)/DAC6BITVOLT_1LSB); + lp_loop.LpDacCfg.DacData12Bit =(int32_t)((AppAMPCfg.SensorBias)/DAC12BITVOLT_1LSB) + lp_loop.LpDacCfg.DacData6Bit*64; + if(lp_loop.LpDacCfg.DacData12Bit>lp_loop.LpDacCfg.DacData6Bit*64) + lp_loop.LpDacCfg.DacData12Bit--; + lp_loop.LpAmpCfg.LpAmpSel = LPAMP0; + lp_loop.LpAmpCfg.LpAmpPwrMod = LPAMPPWR_NORM; + lp_loop.LpAmpCfg.LpPaPwrEn = bTRUE; + lp_loop.LpAmpCfg.LpTiaPwrEn = bTRUE; + lp_loop.LpAmpCfg.LpTiaRf = AppAMPCfg.LpTiaRf; + lp_loop.LpAmpCfg.LpTiaRload = AppAMPCfg.LpTiaRl; + if(AppAMPCfg.ExtRtia == bTRUE) + { + lp_loop.LpAmpCfg.LpTiaRtia = LPTIARTIA_OPEN; + lp_loop.LpAmpCfg.LpTiaSW = LPTIASW(9)|LPTIASW(2)|LPTIASW(4)|LPTIASW(5)|LPTIASW(12)|LPTIASW(13); + }else + { + lp_loop.LpAmpCfg.LpTiaRtia = AppAMPCfg.LptiaRtiaSel; + lp_loop.LpAmpCfg.LpTiaSW = LPTIASW(5)|LPTIASW(2)|LPTIASW(4)|LPTIASW(12)|LPTIASW(13); + } + AD5940_LPLoopCfgS(&lp_loop); + + + dsp_cfg.ADCBaseCfg.ADCMuxN = ADCMUXN_VZERO0; + dsp_cfg.ADCBaseCfg.ADCMuxP = ADCMUXP_AIN4; + dsp_cfg.ADCBaseCfg.ADCPga = AppAMPCfg.ADCPgaGain; + + memset(&dsp_cfg.ADCDigCompCfg, 0, sizeof(dsp_cfg.ADCDigCompCfg)); + memset(&dsp_cfg.DftCfg, 0, sizeof(dsp_cfg.DftCfg)); + dsp_cfg.ADCFilterCfg.ADCAvgNum = ADCAVGNUM_16; /* Don't care because it's disabled */ + dsp_cfg.ADCFilterCfg.ADCRate = ADCRATE_800KHZ; /* Tell filter block clock rate of ADC*/ + dsp_cfg.ADCFilterCfg.ADCSinc2Osr = AppAMPCfg.ADCSinc2Osr; + dsp_cfg.ADCFilterCfg.ADCSinc3Osr = AppAMPCfg.ADCSinc3Osr; + dsp_cfg.ADCFilterCfg.BpSinc3 = bFALSE; + dsp_cfg.ADCFilterCfg.BpNotch = bFALSE; + dsp_cfg.ADCFilterCfg.Sinc2NotchEnable = bTRUE; + + memset(&dsp_cfg.StatCfg, 0, sizeof(dsp_cfg.StatCfg)); /* Don't care about Statistic */ + AD5940_DSPCfgS(&dsp_cfg); + + sw_cfg.Dswitch = 0; + sw_cfg.Pswitch = 0; + sw_cfg.Nswitch = 0; + sw_cfg.Tswitch = 0; + AD5940_SWMatrixCfgS(&sw_cfg); + + /* Enable all of them. They are automatically turned off during hibernate mode to save power */ + AD5940_AFECtrlS(AFECTRL_HPREFPWR|AFECTRL_SINC2NOTCH, bTRUE); + AD5940_AFECtrlS(AFECTRL_SINC2NOTCH, bFALSE); + AD5940_SEQGpioCtrlS(0/*AGPIO_Pin6|AGPIO_Pin5|AGPIO_Pin1*/); //GP6->endSeq, GP5 -> AD8233=OFF, GP1->RLD=OFF . + + /* Sequence end. */ + AD5940_SEQGenInsert(SEQ_STOP()); /* Add one extra command to disable sequencer for initialization sequence because we only want it to run one time. */ + + /* Stop here */ + error = AD5940_SEQGenFetchSeq(&pSeqCmd, &SeqLen); + AD5940_SEQGenCtrl(bFALSE); /* Stop sequencer generator */ + if(error == AD5940ERR_OK) + { + AppAMPCfg.InitSeqInfo.SeqId = SEQID_1; + AppAMPCfg.InitSeqInfo.SeqRamAddr = AppAMPCfg.SeqStartAddr; + AppAMPCfg.InitSeqInfo.pSeqCmd = pSeqCmd; + AppAMPCfg.InitSeqInfo.SeqLen = SeqLen; + /* Write command to SRAM */ + AD5940_SEQCmdWrite(AppAMPCfg.InitSeqInfo.SeqRamAddr, pSeqCmd, SeqLen); + } + else + return error; /* Error */ + return AD5940ERR_OK; +} + +static AD5940Err AppAMPSeqMeasureGen(void) +{ + AD5940Err error = AD5940ERR_OK; + uint32_t const *pSeqCmd; + uint32_t SeqLen; + + uint32_t WaitClks; + ClksCalInfo_Type clks_cal; + + clks_cal.DataType = DATATYPE_SINC2; + clks_cal.DataCount = 1; + clks_cal.ADCSinc2Osr = AppAMPCfg.ADCSinc2Osr; + clks_cal.ADCSinc3Osr = AppAMPCfg.ADCSinc3Osr; + clks_cal.ADCAvgNum = 0; + clks_cal.RatioSys2AdcClk = AppAMPCfg.SysClkFreq/AppAMPCfg.AdcClkFreq; + AD5940_ClksCalculate(&clks_cal, &WaitClks); + WaitClks += 15; + AD5940_SEQGenCtrl(bTRUE); + AD5940_SEQGpioCtrlS(AGPIO_Pin2); + AD5940_AFECtrlS(AFECTRL_ADCPWR|AFECTRL_SINC2NOTCH, bTRUE); + AD5940_SEQGenInsert(SEQ_WAIT(16*250)); /* wait 250us */ + AD5940_AFECtrlS(AFECTRL_ADCCNV, bTRUE); /* Start ADC convert*/ + AD5940_SEQGenInsert(SEQ_WAIT(WaitClks)); /* wait for first data ready */ + AD5940_AFECtrlS(AFECTRL_ADCPWR|AFECTRL_ADCCNV|AFECTRL_SINC2NOTCH, bFALSE); /* Stop ADC */ + AD5940_SEQGpioCtrlS(0); + AD5940_EnterSleepS();/* Goto hibernate */ + /* Sequence end. */ + error = AD5940_SEQGenFetchSeq(&pSeqCmd, &SeqLen); + AD5940_SEQGenCtrl(bFALSE); /* Stop sequencer generator */ + + if(error == AD5940ERR_OK) + { + AppAMPCfg.MeasureSeqInfo.SeqId = SEQID_0; + AppAMPCfg.MeasureSeqInfo.SeqRamAddr = AppAMPCfg.InitSeqInfo.SeqRamAddr + AppAMPCfg.InitSeqInfo.SeqLen ; + AppAMPCfg.MeasureSeqInfo.pSeqCmd = pSeqCmd; + AppAMPCfg.MeasureSeqInfo.SeqLen = SeqLen; + /* Write command to SRAM */ + AD5940_SEQCmdWrite(AppAMPCfg.MeasureSeqInfo.SeqRamAddr, pSeqCmd, SeqLen); + } + else + return error; /* Error */ + return AD5940ERR_OK; +} +static AD5940Err AppAMPRtiaCal(void) +{ +fImpPol_Type RtiaCalValue; /* Calibration result */ + LPRTIACal_Type lprtia_cal; + AD5940_StructInit(&lprtia_cal, sizeof(lprtia_cal)); + + lprtia_cal.bPolarResult = bTRUE; /* Magnitude + Phase */ + lprtia_cal.AdcClkFreq = AppAMPCfg.AdcClkFreq; + lprtia_cal.SysClkFreq = AppAMPCfg.SysClkFreq; + lprtia_cal.ADCSinc3Osr = ADCSINC3OSR_4; + lprtia_cal.ADCSinc2Osr = ADCSINC2OSR_22; /* Use SINC2 data as DFT data source */ + lprtia_cal.DftCfg.DftNum = DFTNUM_2048; /* Maximum DFT number */ + lprtia_cal.DftCfg.DftSrc = DFTSRC_SINC2NOTCH; /* For frequency under 12Hz, need to optimize DFT source. Use SINC3 data as DFT source */ + lprtia_cal.DftCfg.HanWinEn = bTRUE; + lprtia_cal.fFreq = AppAMPCfg.AdcClkFreq/4/22/2048*3; /* Sample 3 period of signal, 13.317Hz here. Do not use DC method, because it needs ADC/PGA calibrated firstly(but it's faster) */ + lprtia_cal.fRcal = AppAMPCfg.RcalVal; + lprtia_cal.LpTiaRtia = AppAMPCfg.LptiaRtiaSel; + lprtia_cal.LpAmpPwrMod = LPAMPPWR_NORM; + lprtia_cal.bWithCtia = bFALSE; + AD5940_LPRtiaCal(&lprtia_cal, &RtiaCalValue); + AppAMPCfg.RtiaCalValue = RtiaCalValue; + + return AD5940ERR_OK; +} +/* This function provide application initialize. */ +AD5940Err AppAMPInit(uint32_t *pBuffer, uint32_t BufferSize) +{ + AD5940Err error = AD5940ERR_OK; + SEQCfg_Type seq_cfg; + FIFOCfg_Type fifo_cfg; + + if(AD5940_WakeUp(10) > 10) /* Wakeup AFE by read register, read 10 times at most */ + return AD5940ERR_WAKEUP; /* Wakeup Failed */ + + /* Configure sequencer and stop it */ + seq_cfg.SeqMemSize = SEQMEMSIZE_2KB; /* 2kB SRAM is used for sequencer, others for data FIFO */ + seq_cfg.SeqBreakEn = bFALSE; + seq_cfg.SeqIgnoreEn = bFALSE; + seq_cfg.SeqCntCRCClr = bTRUE; + seq_cfg.SeqEnable = bFALSE; + seq_cfg.SeqWrTimer = 0; + AD5940_SEQCfg(&seq_cfg); + + /* Do RTIA calibration */ + if(((AppAMPCfg.ReDoRtiaCal == bTRUE) || \ + AppAMPCfg.AMPInited == bFALSE) && AppAMPCfg.ExtRtia == bFALSE) /* Do calibration on the first initializaion */ + { + AppAMPRtiaCal(); + AppAMPCfg.ReDoRtiaCal = bFALSE; + }else + AppAMPCfg.RtiaCalValue.Magnitude = AppAMPCfg.ExtRtiaVal; + + /* Reconfigure FIFO */ + AD5940_FIFOCtrlS(DFTSRC_SINC3, bFALSE); /* Disable FIFO firstly */ + fifo_cfg.FIFOEn = bTRUE; + fifo_cfg.FIFOMode = FIFOMODE_FIFO; + fifo_cfg.FIFOSize = FIFOSIZE_4KB; /* 4kB for FIFO, The reset 2kB for sequencer */ + fifo_cfg.FIFOSrc = AppAMPCfg.DataFifoSrc; + fifo_cfg.FIFOThresh = AppAMPCfg.FifoThresh; + AD5940_FIFOCfg(&fifo_cfg); + + AD5940_INTCClrFlag(AFEINTSRC_ALLINT); + + /* Start sequence generator */ + /* Initialize sequencer generator */ + if((AppAMPCfg.AMPInited == bFALSE)||\ + (AppAMPCfg.bParaChanged == bTRUE)) + { + if(pBuffer == 0) return AD5940ERR_PARA; + if(BufferSize == 0) return AD5940ERR_PARA; + AD5940_SEQGenInit(pBuffer, BufferSize); + + /* Generate initialize sequence */ + error = AppAMPSeqCfgGen(); /* Application initialization sequence using either MCU or sequencer */ + if(error != AD5940ERR_OK) return error; + + /* Generate measurement sequence */ + error = AppAMPSeqMeasureGen(); + if(error != AD5940ERR_OK) return error; + + AppAMPCfg.bParaChanged = bFALSE; /* Clear this flag as we already implemented the new configuration */ + } + /* Initialization sequencer */ + AppAMPCfg.InitSeqInfo.WriteSRAM = bFALSE; + AD5940_SEQInfoCfg(&AppAMPCfg.InitSeqInfo); + seq_cfg.SeqEnable = bTRUE; + AD5940_SEQCfg(&seq_cfg); /* Enable sequencer */ + AD5940_SEQMmrTrig(AppAMPCfg.InitSeqInfo.SeqId); + while(AD5940_INTCTestFlag(AFEINTC_1, AFEINTSRC_ENDSEQ) == bFALSE); + + /* Measurement sequence */ + AppAMPCfg.MeasureSeqInfo.WriteSRAM = bFALSE; + AD5940_SEQInfoCfg(&AppAMPCfg.MeasureSeqInfo); + +// seq_cfg.SeqEnable = bTRUE; +// AD5940_SEQCfg(&seq_cfg); /* Enable sequencer, and wait for trigger */ + AD5940_SEQCtrlS(bTRUE); /* Enable sequencer, and wait for trigger. It's disabled in initialization sequence */ + AD5940_ClrMCUIntFlag(); /* Clear interrupt flag generated before */ + + AD5940_AFEPwrBW(AppAMPCfg.PwrMod, AFEBW_250KHZ); + AppAMPCfg.AMPInited = bTRUE; /* AMP application has been initialized. */ + return AD5940ERR_OK; +} + +/* Modify registers when AFE wakeup */ +static AD5940Err AppAMPRegModify(int32_t * const pData, uint32_t *pDataCount) +{ + if(AppAMPCfg.NumOfData > 0) + { + AppAMPCfg.FifoDataCount += *pDataCount/4; + if(AppAMPCfg.FifoDataCount >= AppAMPCfg.NumOfData) + { + AD5940_WUPTCtrl(bFALSE); + return AD5940ERR_STOP; // Return STOP code + } + } + if(AppAMPCfg.StopRequired == bTRUE) + { + AD5940_WUPTCtrl(bFALSE); + return AD5940ERR_OK; + } + + return AD5940ERR_OK; +} + +/* Depending on the data type, do appropriate data pre-process before return back to controller */ +static AD5940Err AppAMPDataProcess(int32_t * const pData, uint32_t *pDataCount) +{ + uint32_t i, datacount; + datacount = *pDataCount; + float *pOut = (float *)pData; + for(i=0;i 10) /* Wakeup AFE by read register, read 10 times at most */ + return AD5940ERR_WAKEUP; /* Wakeup Failed */ + AD5940_SleepKeyCtrlS(SLPKEY_LOCK); + + *pCount = 0; + if(AD5940_INTCTestFlag(AFEINTC_0, AFEINTSRC_DATAFIFOTHRESH) == bTRUE) + { + FifoCnt = AD5940_FIFOGetCnt(); + AD5940_FIFORd((uint32_t *)pBuff, FifoCnt); + AD5940_INTCClrFlag(AFEINTSRC_DATAFIFOTHRESH); + + AD5940Err status = AppAMPRegModify(pBuff, &FifoCnt); /* If there is need to do AFE re-configure, do it here when AFE is in active state */ + AD5940_SleepKeyCtrlS(SLPKEY_UNLOCK); + //AD5940_EnterSleepS(); /* Manually put AFE back to hibernate mode. This operation only takes effect when register value is ACTIVE previously */ + + /* Process data */ + AppAMPDataProcess((int32_t*)pBuff,&FifoCnt); + *pCount = FifoCnt; + + if (status == AD5940ERR_STOP) return AD5940ERR_STOP; + return 0; + } + + return 0; +} + +/* Calculate voltage */ +float AppAMPCalcVoltage(uint32_t ADCcode) +{ + float kFactor = 1.835/1.82; + float fVolt = 0.0; + int32_t tmp = 0; + tmp = ADCcode - 32768; + switch(AppAMPCfg.ADCPgaGain) + { + case ADCPGA_1: + fVolt = ((float)(tmp)/32768)*(AppAMPCfg.ADCRefVolt/1)*kFactor; + break; + case ADCPGA_1P5: + fVolt = ((float)(tmp)/32768)*(AppAMPCfg.ADCRefVolt/1.5f)*kFactor; + break; + case ADCPGA_2: + fVolt = ((float)(tmp)/32768)*(AppAMPCfg.ADCRefVolt/2)*kFactor; + break; + case ADCPGA_4: + fVolt = ((float)(tmp)/32768)*(AppAMPCfg.ADCRefVolt/4)*kFactor; + break; + case ADCPGA_9: + fVolt = ((float)(tmp)/32768)*(AppAMPCfg.ADCRefVolt/9)*kFactor; + break; + } + return fVolt; +} +/* Calculate current in uA */ +float AppAMPCalcCurrent(uint32_t ADCcode) +{ + float fCurrent, fVoltage = 0.0; + fVoltage = AppAMPCalcVoltage(ADCcode); + fCurrent = fVoltage/AppAMPCfg.RtiaCalValue.Magnitude; + + return -fCurrent*1000000; +} \ No newline at end of file diff --git a/Amperometric.h b/Amperometric.h new file mode 100644 index 0000000..d1770cc --- /dev/null +++ b/Amperometric.h @@ -0,0 +1,95 @@ +// File: Amperometric.h +/*! + ***************************************************************************** + @file: Amperometric.h + @author: $Author: mlambe $ + @brief: Amperometric measurement header file. + @version: $Revision: 766 $ + @date: $Date: 2018-03-21 14:09:35 +0100 (Wed, 21 Mar 2018) $ + ----------------------------------------------------------------------------- + +Copyright (c) 2017-2019 Analog Devices, Inc. All Rights Reserved. + +This software is proprietary to Analog Devices, Inc. and its licensors. +By using this software you agree to the terms of the associated +Analog Devices Software License Agreement. + +*****************************************************************************/ +#ifndef _AMPEROMETRIC_H_ +#define _AMPEROMETRIC_H_ +#include "ad5940.h" +#include "stdio.h" +#include "string.h" +#include "math.h" + +#define DAC12BITVOLT_1LSB (2200.0f/4095) //mV +#define DAC6BITVOLT_1LSB (DAC12BITVOLT_1LSB*64) //mV +/* + Note: this example will use SEQID_0 as measurement sequence, and use SEQID_1 as init sequence. + SEQID_3 is used for calibration. +*/ + +typedef struct +{ +/* Common configurations for all kinds of Application. */ + BoolFlag bParaChanged; /* Indicate to generate sequence again. It's auto cleared by AppAMPInit */ + uint32_t SeqStartAddr; /* Initialaztion sequence start address in SRAM of AD5940 */ + uint32_t MaxSeqLen; /* Limit the maximum sequence. */ + uint32_t SeqStartAddrCal; /* Measurement sequence start address in SRAM of AD5940 */ + uint32_t MaxSeqLenCal; + +/* Application related parameters */ + BoolFlag ReDoRtiaCal; /* Set this flag to bTRUE when there is need to do calibration. */ + float SysClkFreq; /* The real frequency of system clock */ + float WuptClkFreq; /* The clock frequency of Wakeup Timer in Hz. Typically it's 32kHz. Leave it here in case we calibrate clock in software method */ + float AdcClkFreq; /* The real frequency of ADC clock */ + uint32_t FifoThresh; /* FIFO threshold. Should be N*4 */ + float AmpODR; /* in Hz. ODR decides the period of WakeupTimer who will trigger sequencer periodically.*/ + int32_t NumOfData; /* By default it's '-1'. If you want the engine stops after get NumofData, then set the value here. Otherwise, set it to '-1' which means never stop. */ + float RcalVal; /* Rcal value in Ohm */ + float ADCRefVolt; /* Measured 1.82 V reference*/ + uint32_t PwrMod; /* Control Chip power mode(LP/HP) */ + uint32_t ADCPgaGain; /* PGA Gain select from GNPGA_1, GNPGA_1_5, GNPGA_2, GNPGA_4, GNPGA_9 !!! We must ensure signal is in range of +-1.5V which is limited by ADC input stage */ + uint8_t ADCSinc3Osr; /* SINC3 OSR selection. ADCSINC3OSR_2, ADCSINC3OSR_4 */ + uint8_t ADCSinc2Osr; /* SINC2 OSR selection. ADCSINC2OSR_22...ADCSINC2OSR_1333 */ + uint32_t DataFifoSrc; /* DataFIFO source. FIFOSRC_SINC3, FIFOSRC_DFT, FIFOSRC_SINC2NOTCH, FIFOSRC_VAR, FIFOSRC_MEAN*/ + uint32_t LptiaRtiaSel; /* Use internal RTIA, select from RTIA_INT_200, RTIA_INT_1K, RTIA_INT_5K, RTIA_INT_10K, RTIA_INT_20K, RTIA_INT_40K, RTIA_INT_80K, RTIA_INT_160K */ + uint32_t LpTiaRf; /* Rfilter select */ + uint32_t LpTiaRl; /* SE0 Rload select */ + fImpPol_Type RtiaCalValue; /* Calibrated Rtia value */ + float Vzero; /* Voltage on SE0 pin and Vzero, optimumly 1100mV*/ + float SensorBias; /* Sensor bias voltage = VRE0 - VSE0 */ + BoolFlag ExtRtia; /* Use internal or external Rtia */ + float ExtRtiaVal; /* External Rtia value if using one */ + BoolFlag AMPInited; /* If the program run firstly, generated sequence commands */ + SEQInfo_Type InitSeqInfo; + SEQInfo_Type MeasureSeqInfo; + BoolFlag StopRequired; /* After FIFO is ready, stop the measurement sequence */ + uint32_t FifoDataCount; /* Count how many times impedance have been measured */ +/* End */ +}AppAMPCfg_Type; + +/** + * int32_t type Impedance result in Cartesian coordinate +*/ +typedef struct +{ + float Current; + float Voltage; +}fAmpRes_Type; + + + +#define AMPCTRL_START 0 +#define AMPCTRL_STOPNOW 1 +#define AMPCTRL_STOPSYNC 2 +#define AMPCTRL_SHUTDOWN 4 /* Note: shutdown here means turn off everything and put AFE to hibernate mode. The word 'SHUT DOWN' is only used here. */ + +AD5940Err AppAMPGetCfg(void *pCfg); +AD5940Err AppAMPInit(uint32_t *pBuffer, uint32_t BufferSize); +AD5940Err AppAMPISR(void *pBuff, uint32_t *pCount); +AD5940Err AppAMPCtrl(int32_t AmpCtrl, void *pPara); +float AppAMPCalcVoltage(uint32_t ADCcode); +float AppAMPCalcCurrent(uint32_t ADCcode); + +#endif \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index bcdd42b..06c85ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,8 @@ add_executable(EIS main.c ad5940.c Impedance.c + Amperometric.c + RampTest.c ) target_compile_definitions(EIS PRIVATE CHIPSEL_594X) diff --git a/Impedance.c b/Impedance.c index 6970e9a..291e878 100644 --- a/Impedance.c +++ b/Impedance.c @@ -1,10 +1,12 @@ -// File: Impedance.c +// Impedance.c #include "ad5940.h" #include #include "string.h" #include "math.h" #include "Impedance.h" +#define AD5940ERR_STOP 10 + /* Default LPDAC resolution (2.5V internal reference) */ #define DAC12BITVOLT_1LSB (2200.0f/4095) // mV #define DAC6BITVOLT_1LSB (DAC12BITVOLT_1LSB*64) // mV @@ -76,10 +78,10 @@ AppIMPCfg_Type AppIMPCfg = /* ----------------------------------------------------------------------- */ /** - * @brief Configures the Impedance Sweep parameters. - * @param start Start Frequency in Hz - * @param stop Stop Frequency in Hz - * @param ppd Points Per Decade (Resolution) +* @brief Configures the Impedance Sweep parameters. +* @param start Start Frequency in Hz +* @param stop Stop Frequency in Hz +* @param ppd Points Per Decade (Resolution) */ void AppIMPConfigureSweep(float start, float stop, float ppd) { if (start <= 0 || stop <= 0 || ppd <= 0) return; @@ -622,7 +624,7 @@ int32_t AppIMPRegModify(int32_t * const pData, uint32_t *pDataCount) { AD5940_WUPTCtrl(bFALSE); AD5940_SEQCtrlS(bFALSE); - return AD5940ERR_OK; + return AD5940ERR_STOP; // Return STOP code } } if(AppIMPCfg.StopRequired == bTRUE) @@ -752,7 +754,8 @@ int32_t AppIMPISR(void *pBuff, uint32_t *pCount) { AD5940_SEQCtrlS(bFALSE); - if (AppIMPRegModify(pBuff, &FifoCnt) == AD5940ERR_OK) + int32_t status = AppIMPRegModify(pBuff, &FifoCnt); + if (status == AD5940ERR_OK) { if(AppIMPCfg.FifoDataCount < AppIMPCfg.NumOfData || AppIMPCfg.NumOfData == -1) { @@ -780,6 +783,8 @@ int32_t AppIMPISR(void *pBuff, uint32_t *pCount) } else { *pCount = FifoCnt; } + + if (status == AD5940ERR_STOP) return AD5940ERR_STOP; } else { diff --git a/RampTest.c b/RampTest.c new file mode 100644 index 0000000..9196339 --- /dev/null +++ b/RampTest.c @@ -0,0 +1,583 @@ +// RampTest.c +#include "ad5940.h" +#include +#include "string.h" +#include "math.h" +#include "RampTest.h" + +#define AD5940ERR_STOP 10 + +AppRAMPCfg_Type AppRAMPCfg = +{ + .bParaChanged = bFALSE, + .SeqStartAddr = 0, + .MaxSeqLen = 0, + .SeqStartAddrCal = 0, + .MaxSeqLenCal = 0, + + .LFOSCClkFreq = 32000.0, + .SysClkFreq = 16000000.0, + .AdcClkFreq = 16000000.0, + .RcalVal = 10000.0, + .ADCRefVolt = 1820.0f, + .bTestFinished = bFALSE, + + .RampStartVolt = -500.0f, + .RampPeakVolt = +500.0f, + .VzeroStart = 1100.0f, + .VzeroPeak = 1100.0f, + .StepNumber = 100, + .RampDuration = 10000, + + .SampleDelay = 1.0f, + .LPTIARtiaSel = LPTIARTIA_4K, + .LpTiaRf = LPTIARF_20K, /* Default LPF */ + .ExternalRtiaValue = 20000.0f, + .AdcPgaGain = ADCPGA_1P5, + .ADCSinc3Osr = ADCSINC3OSR_2, + .FifoThresh = 4, + + .RAMPInited = bFALSE, + .StopRequired = bFALSE, + .RampState = RAMP_STATE0, + .bFirstDACSeq = bTRUE, + .bRampOneDir = bFALSE, +}; + +AD5940Err AppRAMPGetCfg(void *pCfg) +{ + if(pCfg) + { + *(AppRAMPCfg_Type **)pCfg = &AppRAMPCfg; + return AD5940ERR_OK; + } + return AD5940ERR_PARA; +} + +AD5940Err AppRAMPCtrl(uint32_t Command, void *pPara) +{ + switch (Command) + { + case APPCTRL_START: + { + WUPTCfg_Type wupt_cfg; + + if(AD5940_WakeUp(10) > 10) return AD5940ERR_WAKEUP; + if(AppRAMPCfg.RAMPInited == bFALSE) return AD5940ERR_APPERROR; + if(AppRAMPCfg.RampState == RAMP_STOP) return AD5940ERR_APPERROR; + + wupt_cfg.WuptEn = bTRUE; + wupt_cfg.WuptEndSeq = WUPTENDSEQ_D; + wupt_cfg.WuptOrder[0] = SEQID_0; + wupt_cfg.WuptOrder[1] = SEQID_2; + wupt_cfg.WuptOrder[2] = SEQID_1; + wupt_cfg.WuptOrder[3] = SEQID_2; + wupt_cfg.SeqxSleepTime[SEQID_2] = 4; + wupt_cfg.SeqxWakeupTime[SEQID_2] = (uint32_t)(AppRAMPCfg.LFOSCClkFreq * AppRAMPCfg.SampleDelay / 1000.0f) - 4 - 2; + wupt_cfg.SeqxSleepTime[SEQID_0] = 4; + wupt_cfg.SeqxWakeupTime[SEQID_0] = (uint32_t)(AppRAMPCfg.LFOSCClkFreq * (AppRAMPCfg.RampDuration / AppRAMPCfg.StepNumber - AppRAMPCfg.SampleDelay) / 1000.0f) - 4 - 2; + wupt_cfg.SeqxSleepTime[SEQID_1] = wupt_cfg.SeqxSleepTime[SEQID_0]; + wupt_cfg.SeqxWakeupTime[SEQID_1] = wupt_cfg.SeqxWakeupTime[SEQID_0]; + AD5940_WUPTCfg(&wupt_cfg); + break; + } + case APPCTRL_STOPNOW: + { + if(AD5940_WakeUp(10) > 10) return AD5940ERR_WAKEUP; + AD5940_WUPTCtrl(bFALSE); + AD5940_WUPTCtrl(bFALSE); + break; + } + case APPCTRL_STOPSYNC: + { + AppRAMPCfg.StopRequired = bTRUE; + break; + } + case APPCTRL_SHUTDOWN: + { + AppRAMPCtrl(APPCTRL_STOPNOW, 0); + AD5940_ShutDownS(); + break; + } + default: + break; + } + return AD5940ERR_OK; +} + +static AD5940Err AppRAMPSeqInitGen(void) +{ + AD5940Err error = AD5940ERR_OK; + const uint32_t *pSeqCmd; + uint32_t SeqLen; + AFERefCfg_Type aferef_cfg; + LPLoopCfg_Type lploop_cfg; + DSPCfg_Type dsp_cfg; + + AD5940_SEQGenCtrl(bTRUE); + AD5940_AFECtrlS(AFECTRL_ALL, bFALSE); + + aferef_cfg.HpBandgapEn = bTRUE; + aferef_cfg.Hp1V1BuffEn = bTRUE; + aferef_cfg.Hp1V8BuffEn = bTRUE; + aferef_cfg.Disc1V1Cap = bFALSE; + aferef_cfg.Disc1V8Cap = bFALSE; + aferef_cfg.Hp1V8ThemBuff = bFALSE; + aferef_cfg.Hp1V8Ilimit = bFALSE; + aferef_cfg.Lp1V1BuffEn = bTRUE; + aferef_cfg.Lp1V8BuffEn = bTRUE; + aferef_cfg.LpBandgapEn = bTRUE; + aferef_cfg.LpRefBufEn = bTRUE; + aferef_cfg.LpRefBoostEn = bFALSE; + AD5940_REFCfgS(&aferef_cfg); + + lploop_cfg.LpAmpCfg.LpAmpSel = LPAMP0; + lploop_cfg.LpAmpCfg.LpAmpPwrMod = LPAMPPWR_NORM; + lploop_cfg.LpAmpCfg.LpPaPwrEn = bTRUE; + lploop_cfg.LpAmpCfg.LpTiaPwrEn = bTRUE; + lploop_cfg.LpAmpCfg.LpTiaRf = AppRAMPCfg.LpTiaRf; // Use Configured LPF + lploop_cfg.LpAmpCfg.LpTiaRload = LPTIARLOAD_10R; + lploop_cfg.LpAmpCfg.LpTiaRtia = AppRAMPCfg.LPTIARtiaSel; + + lploop_cfg.LpAmpCfg.LpTiaSW = LPTIASW(5)|LPTIASW(2)|LPTIASW(4)|LPTIASW(12)|LPTIASW(13); + + lploop_cfg.LpDacCfg.LpdacSel = LPDAC0; + lploop_cfg.LpDacCfg.DacData12Bit = 0x800; + lploop_cfg.LpDacCfg.DacData6Bit = 0; + lploop_cfg.LpDacCfg.DataRst = bFALSE; + lploop_cfg.LpDacCfg.LpDacSW = LPDACSW_VBIAS2LPPA|LPDACSW_VBIAS2PIN|LPDACSW_VZERO2LPTIA|LPDACSW_VZERO2PIN; + lploop_cfg.LpDacCfg.LpDacRef = LPDACREF_2P5; + lploop_cfg.LpDacCfg.LpDacSrc = LPDACSRC_MMR; + lploop_cfg.LpDacCfg.LpDacVbiasMux = LPDACVBIAS_12BIT; + lploop_cfg.LpDacCfg.LpDacVzeroMux = LPDACVZERO_6BIT; + lploop_cfg.LpDacCfg.PowerEn = bTRUE; + AD5940_LPLoopCfgS(&lploop_cfg); + + AD5940_StructInit(&dsp_cfg, sizeof(dsp_cfg)); + dsp_cfg.ADCBaseCfg.ADCMuxN = ADCMUXN_LPTIA0_N; + dsp_cfg.ADCBaseCfg.ADCMuxP = ADCMUXP_LPTIA0_P; + dsp_cfg.ADCBaseCfg.ADCPga = AppRAMPCfg.AdcPgaGain; + + dsp_cfg.ADCFilterCfg.ADCSinc3Osr = AppRAMPCfg.ADCSinc3Osr; + dsp_cfg.ADCFilterCfg.ADCRate = ADCRATE_800KHZ; + dsp_cfg.ADCFilterCfg.BpSinc3 = bFALSE; + dsp_cfg.ADCFilterCfg.Sinc2NotchEnable = bTRUE; + dsp_cfg.ADCFilterCfg.BpNotch = bTRUE; + dsp_cfg.ADCFilterCfg.ADCSinc2Osr = ADCSINC2OSR_1067; + dsp_cfg.ADCFilterCfg.ADCAvgNum = ADCAVGNUM_2; + AD5940_DSPCfgS(&dsp_cfg); + + AD5940_SEQGenInsert(SEQ_STOP()); + + AD5940_SEQGenCtrl(bFALSE); + error = AD5940_SEQGenFetchSeq(&pSeqCmd, &SeqLen); + if(error == AD5940ERR_OK) + { + AD5940_StructInit(&AppRAMPCfg.InitSeqInfo, sizeof(AppRAMPCfg.InitSeqInfo)); + if(SeqLen >= AppRAMPCfg.MaxSeqLen) return AD5940ERR_SEQLEN; + + AppRAMPCfg.InitSeqInfo.SeqId = SEQID_3; + AppRAMPCfg.InitSeqInfo.SeqRamAddr = AppRAMPCfg.SeqStartAddr; + AppRAMPCfg.InitSeqInfo.pSeqCmd = pSeqCmd; + AppRAMPCfg.InitSeqInfo.SeqLen = SeqLen; + AppRAMPCfg.InitSeqInfo.WriteSRAM = bTRUE; + AD5940_SEQInfoCfg(&AppRAMPCfg.InitSeqInfo); + } + else + return error; + return AD5940ERR_OK; +} + +static AD5940Err AppRAMPSeqADCCtrlGen(void) +{ + AD5940Err error = AD5940ERR_OK; + const uint32_t *pSeqCmd; + uint32_t SeqLen; + uint32_t WaitClks; + ClksCalInfo_Type clks_cal; + + clks_cal.DataCount = 1; + clks_cal.DataType = DATATYPE_SINC3; + clks_cal.ADCSinc3Osr = AppRAMPCfg.ADCSinc3Osr; + clks_cal.ADCSinc2Osr = ADCSINC2OSR_1067; + clks_cal.ADCAvgNum = ADCAVGNUM_2; + clks_cal.RatioSys2AdcClk = AppRAMPCfg.SysClkFreq / AppRAMPCfg.AdcClkFreq; + AD5940_ClksCalculate(&clks_cal, &WaitClks); + + AD5940_SEQGenCtrl(bTRUE); + AD5940_SEQGpioCtrlS(AGPIO_Pin2); + AD5940_AFECtrlS(AFECTRL_ADCPWR, bTRUE); + AD5940_SEQGenInsert(SEQ_WAIT(16 * 250)); + AD5940_AFECtrlS(AFECTRL_ADCCNV, bTRUE); + AD5940_SEQGenInsert(SEQ_WAIT(WaitClks)); + AD5940_AFECtrlS(AFECTRL_ADCPWR | AFECTRL_ADCCNV, bFALSE); + AD5940_SEQGpioCtrlS(0); + AD5940_EnterSleepS(); + + error = AD5940_SEQGenFetchSeq(&pSeqCmd, &SeqLen); + AD5940_SEQGenCtrl(bFALSE); + + if(error == AD5940ERR_OK) + { + AD5940_StructInit(&AppRAMPCfg.ADCSeqInfo, sizeof(AppRAMPCfg.ADCSeqInfo)); + if((SeqLen + AppRAMPCfg.InitSeqInfo.SeqLen) >= AppRAMPCfg.MaxSeqLen) + return AD5940ERR_SEQLEN; + AppRAMPCfg.ADCSeqInfo.SeqId = SEQID_2; + AppRAMPCfg.ADCSeqInfo.SeqRamAddr = AppRAMPCfg.InitSeqInfo.SeqRamAddr + AppRAMPCfg.InitSeqInfo.SeqLen ; + AppRAMPCfg.ADCSeqInfo.pSeqCmd = pSeqCmd; + AppRAMPCfg.ADCSeqInfo.SeqLen = SeqLen; + AppRAMPCfg.ADCSeqInfo.WriteSRAM = bTRUE; + AD5940_SEQInfoCfg(&AppRAMPCfg.ADCSeqInfo); + } + else + return error; + return AD5940ERR_OK; +} + +static AD5940Err RampDacRegUpdate(uint32_t *pDACData) +{ + uint32_t VbiasCode, VzeroCode; + + if (AppRAMPCfg.bRampOneDir) + { + switch(AppRAMPCfg.RampState) + { + case RAMP_STATE0: + AppRAMPCfg.CurrVzeroCode = (uint32_t)((AppRAMPCfg.VzeroStart - 200.0f) / DAC6BITVOLT_1LSB); + AppRAMPCfg.RampState = RAMP_STATE1; + break; + case RAMP_STATE1: + if(AppRAMPCfg.CurrStepPos >= AppRAMPCfg.StepNumber / 2) + { + AppRAMPCfg.RampState = RAMP_STATE4; + AppRAMPCfg.CurrVzeroCode = (uint32_t)((AppRAMPCfg.VzeroPeak - 200.0f) / DAC6BITVOLT_1LSB); + } + break; + case RAMP_STATE4: + if(AppRAMPCfg.CurrStepPos >= AppRAMPCfg.StepNumber) + AppRAMPCfg.RampState = RAMP_STOP; + break; + case RAMP_STOP: + break; + } + } + else + { + switch(AppRAMPCfg.RampState) + { + case RAMP_STATE0: + AppRAMPCfg.CurrVzeroCode = (uint32_t)((AppRAMPCfg.VzeroStart - 200.0f) / DAC6BITVOLT_1LSB); + AppRAMPCfg.RampState = RAMP_STATE1; + break; + case RAMP_STATE1: + if(AppRAMPCfg.CurrStepPos >= AppRAMPCfg.StepNumber / 4) + { + AppRAMPCfg.RampState = RAMP_STATE2; + AppRAMPCfg.CurrVzeroCode = (uint32_t)((AppRAMPCfg.VzeroPeak - 200.0f) / DAC6BITVOLT_1LSB); + } + break; + case RAMP_STATE2: + if(AppRAMPCfg.CurrStepPos >= (AppRAMPCfg.StepNumber * 2) / 4) + { + AppRAMPCfg.RampState = RAMP_STATE3; + AppRAMPCfg.bDACCodeInc = AppRAMPCfg.bDACCodeInc ? bFALSE : bTRUE; + } + break; + case RAMP_STATE3: + if(AppRAMPCfg.CurrStepPos >= (AppRAMPCfg.StepNumber * 3) / 4) + { + AppRAMPCfg.RampState = RAMP_STATE4; + AppRAMPCfg.CurrVzeroCode = (uint32_t)((AppRAMPCfg.VzeroStart - 200.0f) / DAC6BITVOLT_1LSB); + } + break; + case RAMP_STATE4: + if(AppRAMPCfg.CurrStepPos >= AppRAMPCfg.StepNumber) + AppRAMPCfg.RampState = RAMP_STOP; + break; + case RAMP_STOP: + break; + } + } + + AppRAMPCfg.CurrStepPos ++; + if(AppRAMPCfg.bDACCodeInc) + AppRAMPCfg.CurrRampCode += AppRAMPCfg.DACCodePerStep; + else + AppRAMPCfg.CurrRampCode -= AppRAMPCfg.DACCodePerStep; + VzeroCode = AppRAMPCfg.CurrVzeroCode; + VbiasCode = (uint32_t)(VzeroCode * 64 + AppRAMPCfg.CurrRampCode); + if(VbiasCode < (VzeroCode * 64)) + VbiasCode --; + + if(VbiasCode > 4095) VbiasCode = 4095; + if(VzeroCode > 63) VzeroCode = 63; + *pDACData = (VzeroCode << 12) | VbiasCode; + return AD5940ERR_OK; +} + +static AD5940Err AppRAMPSeqDACCtrlGen(void) +{ +#define SEQLEN_ONESTEP 4L +#define CURRBLK_BLK0 0 +#define CURRBLK_BLK1 1 + AD5940Err error = AD5940ERR_OK; + uint32_t BlockStartSRAMAddr; + uint32_t DACData, SRAMAddr; + uint32_t i; + uint32_t StepsThisBlock; + BoolFlag bIsFinalBlk; + uint32_t SeqCmdBuff[SEQLEN_ONESTEP]; + + static BoolFlag bCmdForSeq0 = bTRUE; + static uint32_t DACSeqBlk0Addr, DACSeqBlk1Addr; + static uint32_t StepsRemainning, StepsPerBlock, DACSeqCurrBlk; + + if(AppRAMPCfg.bFirstDACSeq == bTRUE) + { + int32_t DACSeqLenMax; + StepsRemainning = AppRAMPCfg.StepNumber; + DACSeqLenMax = (int32_t)AppRAMPCfg.MaxSeqLen - (int32_t)AppRAMPCfg.InitSeqInfo.SeqLen - (int32_t)AppRAMPCfg.ADCSeqInfo.SeqLen; + if(DACSeqLenMax < SEQLEN_ONESTEP * 4) return AD5940ERR_SEQLEN; + DACSeqLenMax -= SEQLEN_ONESTEP * 2; + StepsPerBlock = DACSeqLenMax / SEQLEN_ONESTEP / 2; + DACSeqBlk0Addr = AppRAMPCfg.ADCSeqInfo.SeqRamAddr + AppRAMPCfg.ADCSeqInfo.SeqLen; + DACSeqBlk1Addr = DACSeqBlk0Addr + StepsPerBlock * SEQLEN_ONESTEP; + DACSeqCurrBlk = CURRBLK_BLK0; + + if (AppRAMPCfg.bRampOneDir) + { + AppRAMPCfg.DACCodePerStep = ((AppRAMPCfg.RampPeakVolt - AppRAMPCfg.RampStartVolt) / AppRAMPCfg.StepNumber) / DAC12BITVOLT_1LSB; + } + else + { + AppRAMPCfg.DACCodePerStep = ((AppRAMPCfg.RampPeakVolt - AppRAMPCfg.RampStartVolt) / AppRAMPCfg.StepNumber * 2) / DAC12BITVOLT_1LSB; + } + + if(AppRAMPCfg.DACCodePerStep > 0) + AppRAMPCfg.bDACCodeInc = bTRUE; + else + { + AppRAMPCfg.DACCodePerStep = -AppRAMPCfg.DACCodePerStep; + AppRAMPCfg.bDACCodeInc = bFALSE; + } + AppRAMPCfg.CurrRampCode = AppRAMPCfg.RampStartVolt / DAC12BITVOLT_1LSB; + + AppRAMPCfg.RampState = RAMP_STATE0; + AppRAMPCfg.CurrStepPos = 0; + + bCmdForSeq0 = bTRUE; + } + + if(StepsRemainning == 0) return AD5940ERR_OK; + bIsFinalBlk = StepsRemainning <= StepsPerBlock ? bTRUE : bFALSE; + if(bIsFinalBlk) + StepsThisBlock = StepsRemainning; + else + StepsThisBlock = StepsPerBlock; + StepsRemainning -= StepsThisBlock; + + BlockStartSRAMAddr = (DACSeqCurrBlk == CURRBLK_BLK0) ? DACSeqBlk0Addr : DACSeqBlk1Addr; + SRAMAddr = BlockStartSRAMAddr; + + for(i = 0; i < StepsThisBlock - 1; i++) + { + uint32_t CurrAddr = SRAMAddr; + SRAMAddr += SEQLEN_ONESTEP; + RampDacRegUpdate(&DACData); + SeqCmdBuff[0] = SEQ_WR(REG_AFE_LPDACDAT0, DACData); + SeqCmdBuff[1] = SEQ_WAIT(10); + SeqCmdBuff[2] = SEQ_WR(bCmdForSeq0 ? REG_AFE_SEQ1INFO : REG_AFE_SEQ0INFO, (SRAMAddr << BITP_AFE_SEQ1INFO_ADDR) | (SEQLEN_ONESTEP << BITP_AFE_SEQ1INFO_LEN)); + SeqCmdBuff[3] = SEQ_SLP(); + AD5940_SEQCmdWrite(CurrAddr, SeqCmdBuff, SEQLEN_ONESTEP); + bCmdForSeq0 = bCmdForSeq0 ? bFALSE : bTRUE; + } + + if(bIsFinalBlk) + { + uint32_t CurrAddr = SRAMAddr; + SRAMAddr += SEQLEN_ONESTEP; + RampDacRegUpdate(&DACData); + SeqCmdBuff[0] = SEQ_WR(REG_AFE_LPDACDAT0, DACData); + SeqCmdBuff[1] = SEQ_WAIT(10); + SeqCmdBuff[2] = SEQ_WR(bCmdForSeq0 ? REG_AFE_SEQ1INFO : REG_AFE_SEQ0INFO, (SRAMAddr << BITP_AFE_SEQ1INFO_ADDR) | (SEQLEN_ONESTEP << BITP_AFE_SEQ1INFO_LEN)); + SeqCmdBuff[3] = SEQ_SLP(); + AD5940_SEQCmdWrite(CurrAddr, SeqCmdBuff, SEQLEN_ONESTEP); + CurrAddr += SEQLEN_ONESTEP; + + SeqCmdBuff[0] = SEQ_NOP(); + SeqCmdBuff[1] = SEQ_NOP(); + SeqCmdBuff[2] = SEQ_NOP(); + SeqCmdBuff[3] = SEQ_STOP(); + AD5940_SEQCmdWrite(CurrAddr, SeqCmdBuff, SEQLEN_ONESTEP); + } + else + { + uint32_t CurrAddr = SRAMAddr; + SRAMAddr = (DACSeqCurrBlk == CURRBLK_BLK0) ? DACSeqBlk1Addr : DACSeqBlk0Addr; + RampDacRegUpdate(&DACData); + SeqCmdBuff[0] = SEQ_WR(REG_AFE_LPDACDAT0, DACData); + SeqCmdBuff[1] = SEQ_WAIT(10); + SeqCmdBuff[2] = SEQ_WR(bCmdForSeq0 ? REG_AFE_SEQ1INFO : REG_AFE_SEQ0INFO, (SRAMAddr << BITP_AFE_SEQ1INFO_ADDR) | (SEQLEN_ONESTEP << BITP_AFE_SEQ1INFO_LEN)); + SeqCmdBuff[3] = SEQ_INT0(); + AD5940_SEQCmdWrite(CurrAddr, SeqCmdBuff, SEQLEN_ONESTEP); + bCmdForSeq0 = bCmdForSeq0 ? bFALSE : bTRUE; + } + + DACSeqCurrBlk = (DACSeqCurrBlk == CURRBLK_BLK0) ? CURRBLK_BLK1 : CURRBLK_BLK0; + if(AppRAMPCfg.bFirstDACSeq) + { + AppRAMPCfg.bFirstDACSeq = bFALSE; + if(bIsFinalBlk == bFALSE) + { + error = AppRAMPSeqDACCtrlGen(); + if(error != AD5940ERR_OK) return error; + } + AppRAMPCfg.DACSeqInfo.SeqId = SEQID_0; + AppRAMPCfg.DACSeqInfo.SeqLen = SEQLEN_ONESTEP; + AppRAMPCfg.DACSeqInfo.SeqRamAddr = BlockStartSRAMAddr; + AppRAMPCfg.DACSeqInfo.WriteSRAM = bFALSE; + AD5940_SEQInfoCfg(&AppRAMPCfg.DACSeqInfo); + } + return AD5940ERR_OK; +} + +AD5940Err AppRAMPInit(uint32_t *pBuffer, uint32_t BufferSize) +{ + AD5940Err error = AD5940ERR_OK; + FIFOCfg_Type fifo_cfg; + SEQCfg_Type seq_cfg; + + if(AD5940_WakeUp(10) > 10) return AD5940ERR_WAKEUP; + + seq_cfg.SeqMemSize = SEQMEMSIZE_4KB; + seq_cfg.SeqBreakEn = bFALSE; + seq_cfg.SeqIgnoreEn = bFALSE; + seq_cfg.SeqCntCRCClr = bTRUE; + seq_cfg.SeqEnable = bFALSE; + seq_cfg.SeqWrTimer = 0; + AD5940_SEQCfg(&seq_cfg); + + if((AppRAMPCfg.RAMPInited == bFALSE) || (AppRAMPCfg.bParaChanged == bTRUE)) + { + if(pBuffer == 0) return AD5940ERR_PARA; + if(BufferSize == 0) return AD5940ERR_PARA; + + AppRAMPCfg.RAMPInited = bFALSE; + AD5940_SEQGenInit(pBuffer, BufferSize); + + error = AppRAMPSeqInitGen(); + if(error != AD5940ERR_OK) return error; + error = AppRAMPSeqADCCtrlGen(); + if(error != AD5940ERR_OK) return error; + AppRAMPCfg.bParaChanged = bFALSE; + } + + AD5940_FIFOCtrlS(FIFOSRC_SINC3, bFALSE); + fifo_cfg.FIFOEn = bTRUE; + fifo_cfg.FIFOSrc = FIFOSRC_SINC3; + fifo_cfg.FIFOThresh = AppRAMPCfg.FifoThresh; + fifo_cfg.FIFOMode = FIFOMODE_FIFO; + fifo_cfg.FIFOSize = FIFOSIZE_2KB; + AD5940_FIFOCfg(&fifo_cfg); + + AD5940_INTCClrFlag(AFEINTSRC_ALLINT); + + AppRAMPCfg.bFirstDACSeq = bTRUE; + error = AppRAMPSeqDACCtrlGen(); + if(error != AD5940ERR_OK) return error; + + AppRAMPCfg.InitSeqInfo.WriteSRAM = bFALSE; + AD5940_SEQInfoCfg(&AppRAMPCfg.InitSeqInfo); + + AD5940_SEQCtrlS(bTRUE); + AD5940_SEQMmrTrig(AppRAMPCfg.InitSeqInfo.SeqId); + while(AD5940_INTCTestFlag(AFEINTC_1, AFEINTSRC_ENDSEQ) == bFALSE); + AD5940_INTCClrFlag(AFEINTSRC_ENDSEQ); + + AppRAMPCfg.ADCSeqInfo.WriteSRAM = bFALSE; + AD5940_SEQInfoCfg(&AppRAMPCfg.ADCSeqInfo); + + AppRAMPCfg.DACSeqInfo.WriteSRAM = bFALSE; + AD5940_SEQInfoCfg(&AppRAMPCfg.DACSeqInfo); + + AD5940_SEQCtrlS(bFALSE); + AD5940_WriteReg(REG_AFE_SEQCNT, 0); + AD5940_SEQCtrlS(bTRUE); + AD5940_ClrMCUIntFlag(); + + AD5940_AFEPwrBW(AFEPWR_LP, AFEBW_250KHZ); + + AppRAMPCfg.RAMPInited = bTRUE; + return AD5940ERR_OK; +} + +static int32_t AppRAMPRegModify(int32_t *const pData, uint32_t *pDataCount) +{ + if(AppRAMPCfg.StopRequired == bTRUE) + { + AD5940_WUPTCtrl(bFALSE); + return AD5940ERR_OK; + } + return AD5940ERR_OK; +} + +static int32_t AppRAMPDataProcess(int32_t *const pData, uint32_t *pDataCount) +{ + uint32_t i, datacount; + datacount = *pDataCount; + float *pOut = (float *)pData; + float temp; + for(i = 0; i < datacount; i++) + { + pData[i] &= 0xffff; + temp = -AD5940_ADCCode2Volt(pData[i], AppRAMPCfg.AdcPgaGain, AppRAMPCfg.ADCRefVolt); + pOut[i] = temp / AppRAMPCfg.RtiaValue.Magnitude * 1e3f; /* Result unit is uA. */ + } + return 0; +} + +AD5940Err AppRAMPISR(void *pBuff, uint32_t *pCount) +{ + uint32_t BuffCount; + uint32_t FifoCnt; + BuffCount = *pCount; + uint32_t IntFlag; + + if(AD5940_WakeUp(10) > 10) return AD5940ERR_WAKEUP; + AD5940_SleepKeyCtrlS(SLPKEY_LOCK); + *pCount = 0; + IntFlag = AD5940_INTCGetFlag(AFEINTC_0); + + if(IntFlag & AFEINTSRC_CUSTOMINT0) + { + AD5940Err error; + AD5940_INTCClrFlag(AFEINTSRC_CUSTOMINT0); + error = AppRAMPSeqDACCtrlGen(); + if(error != AD5940ERR_OK) return error; + AD5940_SleepKeyCtrlS(SLPKEY_UNLOCK); + } + if(IntFlag & AFEINTSRC_DATAFIFOTHRESH) + { + FifoCnt = AD5940_FIFOGetCnt(); + AD5940_FIFORd((uint32_t *)pBuff, FifoCnt); + AD5940_INTCClrFlag(AFEINTSRC_DATAFIFOTHRESH); + AppRAMPRegModify((int32_t*)pBuff, &FifoCnt); + AD5940_SleepKeyCtrlS(SLPKEY_UNLOCK); + AppRAMPDataProcess((int32_t *)pBuff, &FifoCnt); + *pCount = FifoCnt; + return 0; + } + if(IntFlag & AFEINTSRC_ENDSEQ) + { + FifoCnt = AD5940_FIFOGetCnt(); + AD5940_INTCClrFlag(AFEINTSRC_ENDSEQ); + AD5940_FIFORd((uint32_t *)pBuff, FifoCnt); + AppRAMPDataProcess((int32_t *)pBuff, &FifoCnt); + *pCount = FifoCnt; + AppRAMPCtrl(APPCTRL_STOPNOW, 0); + + // Signal that we are done + return AD5940ERR_STOP; + } + return 0; +} \ No newline at end of file diff --git a/RampTest.h b/RampTest.h new file mode 100644 index 0000000..ee60777 --- /dev/null +++ b/RampTest.h @@ -0,0 +1,66 @@ +// RampTest.h +#ifndef _RAMPTEST_H_ +#define _RAMPTEST_H_ +#include "ad5940.h" +#include +#include "string.h" +#include "math.h" + +#define ALIGIN_VOLT2LSB 0 +#define DAC12BITVOLT_1LSB (2200.0f/4095) //mV +#define DAC6BITVOLT_1LSB (DAC12BITVOLT_1LSB*64) //mV + +typedef struct +{ + BoolFlag bParaChanged; + uint32_t SeqStartAddr; + uint32_t MaxSeqLen; + uint32_t SeqStartAddrCal; + uint32_t MaxSeqLenCal; + float LFOSCClkFreq; + float SysClkFreq; + float AdcClkFreq; + float RcalVal; + float ADCRefVolt; + BoolFlag bTestFinished; + float RampStartVolt; + float RampPeakVolt; + float VzeroStart; + float VzeroPeak; + uint32_t StepNumber; + uint32_t RampDuration; + float SampleDelay; + uint32_t LPTIARtiaSel; + uint32_t LPTIARloadSel; + uint32_t LpTiaRf; /* Added LPF Resistor Configuration */ + float ExternalRtiaValue; + uint32_t AdcPgaGain; + uint8_t ADCSinc3Osr; + uint32_t FifoThresh; + BoolFlag RAMPInited; + fImpPol_Type RtiaValue; + SEQInfo_Type InitSeqInfo; + SEQInfo_Type ADCSeqInfo; + BoolFlag bFirstDACSeq; + SEQInfo_Type DACSeqInfo; + uint32_t CurrStepPos; + float DACCodePerStep; + float CurrRampCode; + uint32_t CurrVzeroCode; + BoolFlag bDACCodeInc; + BoolFlag StopRequired; + enum _RampState{RAMP_STATE0 = 0, RAMP_STATE1, RAMP_STATE2, RAMP_STATE3, RAMP_STATE4, RAMP_STOP} RampState; + BoolFlag bRampOneDir; +}AppRAMPCfg_Type; + +#define APPCTRL_START 0 +#define APPCTRL_STOPNOW 1 +#define APPCTRL_STOPSYNC 2 +#define APPCTRL_SHUTDOWN 3 + +AD5940Err AppRAMPInit(uint32_t *pBuffer, uint32_t BufferSize); +AD5940Err AppRAMPGetCfg(void *pCfg); +AD5940Err AppRAMPISR(void *pBuff, uint32_t *pCount); +AD5940Err AppRAMPCtrl(uint32_t Command, void *pPara); + +#endif \ No newline at end of file diff --git a/host/src/GraphWidget.cpp b/host/src/GraphWidget.cpp index c85a31d..cc1c118 100644 --- a/host/src/GraphWidget.cpp +++ b/host/src/GraphWidget.cpp @@ -1,4 +1,4 @@ -// File: host/src/GraphWidget.cpp +// host/src/GraphWidget.cpp #include "GraphWidget.h" #include #include @@ -7,159 +7,322 @@ GraphWidget::GraphWidget(QWidget *parent) : QWidget(parent) { layout = new QVBoxLayout(this); plot = new QCustomPlot(this); - // Setup Layout layout->addWidget(plot); layout->setContentsMargins(0, 0, 0, 0); - // Setup Graphs - graph1 = plot->addGraph(); - graph2 = plot->addGraph(plot->xAxis, plot->yAxis2); - graph3 = plot->addGraph(plot->xAxis, plot->yAxis2); // Hilbert Trace + plot->setBackground(QBrush(QColor(25, 25, 25))); + + auto styleAxis = [](QCPAxis *axis) { + axis->setBasePen(QPen(Qt::white)); + axis->setTickPen(QPen(Qt::white)); + axis->setSubTickPen(QPen(Qt::white)); + axis->setTickLabelColor(Qt::white); + axis->setLabelColor(Qt::white); + axis->grid()->setPen(QPen(QColor(60, 60, 60), 0, Qt::DotLine)); + axis->grid()->setSubGridVisible(true); + axis->grid()->setSubGridPen(QPen(QColor(40, 40, 40), 0, Qt::DotLine)); + }; - // Style Graph 1 (Real - Blue) - QPen pen1(Qt::blue); + styleAxis(plot->xAxis); + styleAxis(plot->yAxis); + styleAxis(plot->yAxis2); + + // --- Setup Graphs --- + + // 1. Real / Raw Nyquist (Cyan) + graphReal = plot->addGraph(); + QPen pen1(QColor(0, 255, 255)); pen1.setWidth(2); - graph1->setPen(pen1); - graph1->setLineStyle(QCPGraph::lsLine); - graph1->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, 3)); - graph1->setName("Real"); + graphReal->setPen(pen1); + graphReal->setLineStyle(QCPGraph::lsLine); + graphReal->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, QColor(0, 255, 255), 3)); - // Style Graph 2 (Imaginary - Red) - QPen pen2(Qt::red); + // 2. Imaginary (Magenta) + graphImag = plot->addGraph(plot->xAxis, plot->yAxis2); + QPen pen2(QColor(255, 0, 255)); pen2.setWidth(2); - graph2->setPen(pen2); - graph2->setLineStyle(QCPGraph::lsLine); - graph2->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssTriangle, 3)); - graph2->setName("Imaginary"); + graphImag->setPen(pen2); + graphImag->setLineStyle(QCPGraph::lsLine); + graphImag->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssTriangle, QColor(255, 0, 255), 3)); - // Style Graph 3 (Hilbert - Green Dashed) + // 3. Hilbert (Green Dashed) + graphHilbert = plot->addGraph(plot->xAxis, plot->yAxis2); QPen pen3(Qt::green); pen3.setWidth(2); pen3.setStyle(Qt::DashLine); - graph3->setPen(pen3); - graph3->setLineStyle(QCPGraph::lsLine); - graph3->setName("Hilbert (Analytic)"); + graphHilbert->setPen(pen3); + + // 4. Corrected Nyquist (Orange) + graphNyquistCorr = plot->addGraph(plot->xAxis, plot->yAxis); + QPen pen4(QColor(255, 165, 0)); + pen4.setWidth(2); + graphNyquistCorr->setPen(pen4); + graphNyquistCorr->setLineStyle(QCPGraph::lsLine); + graphNyquistCorr->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCross, 4)); + graphNyquistCorr->setName("De-embedded (True Cell)"); + + // 5. Amperometric Graph (Lime) + graphAmp = plot->addGraph(); + QPen pen5(QColor(50, 255, 50)); + pen5.setWidth(2); + graphAmp->setPen(pen5); + graphAmp->setLineStyle(QCPGraph::lsLine); + graphAmp->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 3)); + graphAmp->setName("Current"); + + // 6. Extrapolated Point (Gold Star) + graphExtrapolated = plot->addGraph(plot->xAxis, plot->yAxis); + graphExtrapolated->setLineStyle(QCPGraph::lsNone); + graphExtrapolated->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssStar, QColor(255, 215, 0), 12)); + QPen pen6(QColor(255, 215, 0)); + pen6.setWidth(3); + graphExtrapolated->setPen(pen6); + graphExtrapolated->setName("Rs (Extrapolated)"); + + // 7. LSV Blank (Gray) + graphLSVBlank = plot->addGraph(); + QPen penBlank(QColor(150, 150, 150)); + penBlank.setWidth(2); + penBlank.setStyle(Qt::DashLine); + graphLSVBlank->setPen(penBlank); + graphLSVBlank->setName("Blank (Tap Water)"); + + // 8. LSV Sample (Yellow) + graphLSVSample = plot->addGraph(); + QPen penSample(Qt::yellow); + penSample.setWidth(2); + graphLSVSample->setPen(penSample); + graphLSVSample->setName("Sample (Bleach)"); + + // 9. LSV Diff (Cyan) + graphLSVDiff = plot->addGraph(); + QPen penDiff(Qt::cyan); + penDiff.setWidth(3); + graphLSVDiff->setPen(penDiff); + graphLSVDiff->setName("Diff (Chlorine)"); + + graphNyquistRaw = graphReal; - // Enable Right Axis plot->yAxis2->setVisible(true); plot->yAxis2->setTickLabels(true); - // Link Axes for Zooming connect(plot->yAxis, SIGNAL(rangeChanged(QCPRange)), plot->yAxis2, SLOT(setRange(QCPRange))); connect(plot->yAxis2, SIGNAL(rangeChanged(QCPRange)), plot->yAxis, SLOT(setRange(QCPRange))); - // Interactions plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); - // Legend plot->legend->setVisible(true); QFont legendFont = font(); legendFont.setPointSize(9); plot->legend->setFont(legendFont); - plot->legend->setBrush(QBrush(QColor(255, 255, 255, 230))); + plot->legend->setBrush(QBrush(QColor(40, 40, 40, 200))); + plot->legend->setBorderPen(QPen(Qt::white)); + plot->legend->setTextColor(Qt::white); - // Default Mode configureRawPlot(); } void GraphWidget::configureRawPlot() { - clear(); - - // X Axis: Frequency (Log) + // Only clear if explicitly requested, but here we just set visibility plot->xAxis->setLabel("Frequency (Hz)"); plot->xAxis->setScaleType(QCPAxis::stLogarithmic); QSharedPointer logTicker(new QCPAxisTickerLog); plot->xAxis->setTicker(logTicker); plot->xAxis->setNumberFormat("eb"); - // Y Axis 1: Real (Linear) plot->yAxis->setLabel("Real (Ohms)"); plot->yAxis->setScaleType(QCPAxis::stLinear); QSharedPointer linTicker(new QCPAxisTicker); plot->yAxis->setTicker(linTicker); plot->yAxis->setNumberFormat("f"); - // Y Axis 2: Imaginary (Linear) plot->yAxis2->setLabel("Imaginary (Ohms)"); plot->yAxis2->setScaleType(QCPAxis::stLinear); plot->yAxis2->setTicker(linTicker); plot->yAxis2->setNumberFormat("f"); plot->yAxis2->setVisible(true); - graph1->setName("Real"); - graph2->setName("Imaginary"); - graph2->setVisible(true); - graph3->setVisible(true); + graphReal->setName("Real"); + graphImag->setName("Imaginary"); + graphHilbert->setName("Hilbert"); + + graphReal->setVisible(true); + graphImag->setVisible(true); + graphHilbert->setVisible(true); + graphNyquistCorr->setVisible(false); + graphAmp->setVisible(false); + graphExtrapolated->setVisible(false); + + graphLSVBlank->setVisible(false); + graphLSVSample->setVisible(false); + graphLSVDiff->setVisible(false); plot->replot(); } void GraphWidget::configureNyquistPlot() { - clear(); - - // X Axis: Real (Z') plot->xAxis->setLabel("Real (Z')"); plot->xAxis->setScaleType(QCPAxis::stLinear); QSharedPointer linTicker(new QCPAxisTicker); plot->xAxis->setTicker(linTicker); plot->xAxis->setNumberFormat("f"); - // Y Axis 1: -Imaginary (-Z'') plot->yAxis->setLabel("-Imaginary (-Z'')"); plot->yAxis->setScaleType(QCPAxis::stLinear); plot->yAxis->setTicker(linTicker); plot->yAxis->setNumberFormat("f"); - // Disable Secondary Axis for Nyquist plot->yAxis2->setVisible(false); - graph2->setVisible(false); - graph3->setVisible(false); - - graph1->setName("Nyquist"); - graph1->setLineStyle(QCPGraph::lsLine); - graph1->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, 4)); + graphReal->setName("Measured (Raw)"); + graphReal->setLineStyle(QCPGraph::lsLine); + graphReal->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, 4)); + + graphReal->setVisible(true); + graphImag->setVisible(false); + graphHilbert->setVisible(false); + graphNyquistCorr->setVisible(true); + graphAmp->setVisible(false); + graphExtrapolated->setVisible(true); + + graphLSVBlank->setVisible(false); + graphLSVSample->setVisible(false); + graphLSVDiff->setVisible(false); + plot->replot(); } -void GraphWidget::addData(double x, double val1, double val2) { - // Mode Detection based on Axis Labels (Simple state check) - bool isNyquist = (plot->xAxis->label() == "Real (Z')"); +void GraphWidget::configureAmperometricPlot() { + plot->xAxis->setLabel("Sample Index"); + plot->xAxis->setScaleType(QCPAxis::stLinear); + QSharedPointer linTicker(new QCPAxisTicker); + plot->xAxis->setTicker(linTicker); + plot->xAxis->setNumberFormat("f"); - if (isNyquist) { - // For Nyquist: x = Real, val1 = Imaginary - // We plot Real vs -Imaginary - graph1->addData(x, -val1); - } else { - // Raw Plot: x = Freq, val1 = Real, val2 = Imag - if (plot->xAxis->scaleType() == QCPAxis::stLogarithmic && x <= 0) return; - - graph1->addData(x, val1); - graph2->addData(x, val2); + plot->yAxis->setLabel("Current (uA)"); + plot->yAxis->setScaleType(QCPAxis::stLinear); + plot->yAxis->setTicker(linTicker); + plot->yAxis->setNumberFormat("f"); + + plot->yAxis2->setVisible(false); + + graphAmp->setName("Current"); + graphAmp->setVisible(true); + + graphReal->setVisible(false); + graphImag->setVisible(false); + graphHilbert->setVisible(false); + graphNyquistCorr->setVisible(false); + graphExtrapolated->setVisible(false); + + graphLSVBlank->setVisible(false); + graphLSVSample->setVisible(false); + graphLSVDiff->setVisible(false); + + plot->replot(); +} + +void GraphWidget::configureLSVPlot() { + plot->xAxis->setLabel("Voltage (mV)"); + plot->xAxis->setScaleType(QCPAxis::stLinear); + QSharedPointer linTicker(new QCPAxisTicker); + plot->xAxis->setTicker(linTicker); + plot->xAxis->setNumberFormat("f"); + + plot->yAxis->setLabel("Current (uA)"); + plot->yAxis->setScaleType(QCPAxis::stLinear); + plot->yAxis->setTicker(linTicker); + plot->yAxis->setNumberFormat("f"); + + plot->yAxis2->setVisible(false); + + graphLSVBlank->setVisible(true); + graphLSVSample->setVisible(true); + graphLSVDiff->setVisible(true); + + graphReal->setVisible(false); + graphImag->setVisible(false); + graphHilbert->setVisible(false); + graphNyquistCorr->setVisible(false); + graphExtrapolated->setVisible(false); + graphAmp->setVisible(false); + + plot->replot(); +} + +void GraphWidget::addBodeData(double freq, double val1, double val2) { + if (plot->xAxis->scaleType() == QCPAxis::stLogarithmic && freq <= 0) return; + graphReal->addData(freq, val1); + graphImag->addData(freq, val2); + graphReal->rescaleAxes(false); + graphImag->rescaleAxes(false); + graphHilbert->rescaleAxes(false); + plot->replot(); +} + +void GraphWidget::addNyquistData(double r_meas, double i_meas, double r_corr, double i_corr, bool showCorr) { + graphNyquistRaw->addData(r_meas, -i_meas); + if (showCorr) { + graphNyquistCorr->addData(r_corr, -i_corr); } + graphNyquistRaw->rescaleAxes(false); + if (showCorr) { + graphNyquistCorr->rescaleAxes(true); + } + plot->replot(); +} - // Auto-scale - graph1->rescaleAxes(false); - if (!isNyquist) { - graph2->rescaleAxes(false); - graph3->rescaleAxes(false); +void GraphWidget::addAmperometricData(double index, double current) { + graphAmp->addData(index, current); + graphAmp->rescaleAxes(false); + plot->replot(); +} + +void GraphWidget::addLSVData(double voltage, double current, LSVTrace traceType) { + QCPGraph* target = nullptr; + switch(traceType) { + case LSV_BLANK: target = graphLSVBlank; break; + case LSV_SAMPLE: target = graphLSVSample; break; + case LSV_DIFF: target = graphLSVDiff; break; } - plot->replot(); + if (target) { + target->addData(voltage, current); + target->rescaleAxes(false); // Rescale to fit new data + plot->replot(); + } } void GraphWidget::addHilbertData(const QVector& freq, const QVector& hilbertImag) { - // Only valid for Raw Plot if (plot->xAxis->label() != "Frequency (Hz)") return; + graphHilbert->setData(freq, hilbertImag); + graphHilbert->rescaleAxes(false); + plot->replot(); +} - graph3->setData(freq, hilbertImag); - graph3->rescaleAxes(false); +void GraphWidget::setExtrapolatedPoint(double real, double imag) { + graphExtrapolated->data()->clear(); + graphExtrapolated->addData(real, -imag); plot->replot(); } void GraphWidget::clear() { - graph1->data()->clear(); - graph2->data()->clear(); - graph3->data()->clear(); + graphReal->data()->clear(); + graphImag->data()->clear(); + graphHilbert->data()->clear(); + graphNyquistCorr->data()->clear(); + graphAmp->data()->clear(); + graphExtrapolated->data()->clear(); + + // Note: We deliberately do NOT clear LSV data in generic clear() + // to allow Blank scans to persist across tab changes or mode switches + // until explicitly cleared. + plot->replot(); +} + +void GraphWidget::clearLSV(LSVTrace traceType) { + if (traceType == LSV_BLANK) graphLSVBlank->data()->clear(); + if (traceType == LSV_SAMPLE) graphLSVSample->data()->clear(); + if (traceType == LSV_DIFF) graphLSVDiff->data()->clear(); plot->replot(); } \ No newline at end of file diff --git a/host/src/GraphWidget.h b/host/src/GraphWidget.h index 16ebc64..ec7f1ce 100644 --- a/host/src/GraphWidget.h +++ b/host/src/GraphWidget.h @@ -1,4 +1,4 @@ -// File: host/src/GraphWidget.h +// host/src/GraphWidget.h #pragma once #include @@ -12,18 +12,46 @@ public: explicit GraphWidget(QWidget *parent = nullptr); // Data Handling - void addData(double freq, double val1, double val2); + void addBodeData(double freq, double val1, double val2); + void addNyquistData(double r_meas, double i_meas, double r_corr, double i_corr, bool showCorr); + void addAmperometricData(double index, double current); + + // Updated LSV Data handler + enum LSVTrace { LSV_BLANK, LSV_SAMPLE, LSV_DIFF }; + void addLSVData(double voltage, double current, LSVTrace traceType); + void addHilbertData(const QVector& freq, const QVector& hilbertImag); + + void setExtrapolatedPoint(double real, double imag); + void clear(); + void clearLSV(LSVTrace traceType); // Clear specific LSV trace // View Configurations void configureRawPlot(); void configureNyquistPlot(); + void configureAmperometricPlot(); + void configureLSVPlot(); private: QVBoxLayout *layout; QCustomPlot *plot; - QCPGraph *graph1; // Real - QCPGraph *graph2; // Imaginary - QCPGraph *graph3; // Hilbert (Analytic Imaginary) + + // Bode Graphs + QCPGraph *graphReal; + QCPGraph *graphImag; + QCPGraph *graphHilbert; + + // Nyquist Graphs + QCPGraph *graphNyquistRaw; + QCPGraph *graphNyquistCorr; + QCPGraph *graphExtrapolated; + + // Amperometric Graph + QCPGraph *graphAmp; + + // LSV Graphs + QCPGraph *graphLSVBlank; // The "Tap Water" baseline + QCPGraph *graphLSVSample; // The "Bleach" spike + QCPGraph *graphLSVDiff; // The calculated difference (Chlorine only) }; \ No newline at end of file diff --git a/host/src/MainWindow.cpp b/host/src/MainWindow.cpp index 137a999..89cb89f 100644 --- a/host/src/MainWindow.cpp +++ b/host/src/MainWindow.cpp @@ -1,6 +1,7 @@ -// File: host/src/MainWindow.cpp +// host/src/MainWindow.cpp #include "MainWindow.h" #include +#include #include #include #include @@ -13,9 +14,9 @@ #include #include #include +#include // Simple FFT Implementation (Cooley-Tukey) -// Note: Size must be power of 2 void fft(std::vector>& a) { int n = a.size(); if (n <= 1) return; @@ -39,11 +40,8 @@ void fft(std::vector>& a) { void ifft(std::vector>& a) { int n = a.size(); - // Conjugate for (int i = 0; i < n; i++) a[i] = std::conj(a[i]); - // Forward FFT fft(a); - // Conjugate again and scale for (int i = 0; i < n; i++) { a[i] = std::conj(a[i]); a[i] /= n; @@ -51,15 +49,19 @@ void ifft(std::vector>& a) { } MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { + settings = new QSettings("EISConfigurator", "Settings", this); + loadSettings(); + serial = new QSerialPort(this); connect(serial, &QSerialPort::readyRead, this, &MainWindow::handleSerialData); connect(serial, &QSerialPort::errorOccurred, this, &MainWindow::onPortError); + blinkTimer = new QTimer(this); + blinkTimer->setInterval(500); + connect(blinkTimer, &QTimer::timeout, this, &MainWindow::onBlinkTimer); + setupUi(); - // Delayed refresh to allow the window to render before auto-scanning QTimer::singleShot(1000, this, &MainWindow::refreshPorts); - - // Enable Swipe Gestures for Mobile Tab Switching grabGesture(Qt::SwipeGesture); } @@ -69,102 +71,222 @@ MainWindow::~MainWindow() { } } -void MainWindow::setupUi() { - // Central Layout: Splitter (Graphs Top, Log Bottom) - QSplitter *splitter = new QSplitter(Qt::Vertical, this); - setCentralWidget(splitter); +void MainWindow::loadSettings() { + cellConstant = settings->value("cellConstant", 1.0).toDouble(); +} - // Tab Widget for Graphs +void MainWindow::saveSettings() { + settings->setValue("cellConstant", cellConstant); +} + +void MainWindow::setupUi() { + QWidget *central = new QWidget(this); + setCentralWidget(central); + QVBoxLayout *mainLayout = new QVBoxLayout(central); + mainLayout->setContentsMargins(5, 5, 5, 5); + mainLayout->setSpacing(5); + + // --- Controls Container --- + QWidget *controlsContainer = new QWidget(this); + controlsContainer->setStyleSheet("background-color: #3A3A3A; border-radius: 5px;"); + QVBoxLayout *ctrlLayout = new QVBoxLayout(controlsContainer); + ctrlLayout->setContentsMargins(5, 5, 5, 5); + ctrlLayout->setSpacing(5); + + // Row 1 + QHBoxLayout *row1 = new QHBoxLayout(); + row1->setSpacing(8); + + portSelector = new QComboBox(this); + portSelector->setMinimumWidth(120); + connectBtn = new QPushButton("Connect", this); + QPushButton *refreshBtn = new QPushButton("Refresh", this); + + row1->addWidget(portSelector); + row1->addWidget(connectBtn); + row1->addWidget(refreshBtn); + + QFrame *sep1 = new QFrame(); sep1->setFrameShape(QFrame::VLine); sep1->setFrameShadow(QFrame::Sunken); + row1->addWidget(sep1); + + checkIdBtn = new QPushButton("Check ID", this); + calibrateBtn = new QPushButton("Calibrate HW", this); + row1->addWidget(checkIdBtn); + row1->addWidget(calibrateBtn); + + QFrame *sep2 = new QFrame(); sep2->setFrameShape(QFrame::VLine); sep2->setFrameShadow(QFrame::Sunken); + row1->addWidget(sep2); + + row1->addWidget(new QLabel("Start:")); + spinSweepStart = new QDoubleSpinBox(this); + spinSweepStart->setRange(0.1, 200000.0); spinSweepStart->setValue(1000.0); spinSweepStart->setSuffix(" Hz"); + row1->addWidget(spinSweepStart); + + row1->addWidget(new QLabel("Stop:")); + spinSweepStop = new QDoubleSpinBox(this); + spinSweepStop->setRange(0.1, 200000.0); spinSweepStop->setValue(200000.0); spinSweepStop->setSuffix(" Hz"); + row1->addWidget(spinSweepStop); + + row1->addWidget(new QLabel("PPD:")); + spinSweepPPD = new QSpinBox(this); + spinSweepPPD->setRange(1, 1000); spinSweepPPD->setValue(200); spinSweepPPD->setSuffix(" pts/dec"); + row1->addWidget(spinSweepPPD); + + sweepBtn = new QPushButton("Sweep", this); + row1->addWidget(sweepBtn); + + QFrame *sep3 = new QFrame(); sep3->setFrameShape(QFrame::VLine); sep3->setFrameShadow(QFrame::Sunken); + row1->addWidget(sep3); + + row1->addWidget(new QLabel("Shunt:")); + checkShunt = new QCheckBox("Enable", this); + row1->addWidget(checkShunt); + spinShuntRes = new QDoubleSpinBox(this); + spinShuntRes->setRange(1.0, 1000000.0); spinShuntRes->setValue(466.0); spinShuntRes->setSuffix(" Ω"); + row1->addWidget(spinShuntRes); + + QFrame *sep4 = new QFrame(); sep4->setFrameShape(QFrame::VLine); sep4->setFrameShadow(QFrame::Sunken); + row1->addWidget(sep4); + + row1->addWidget(new QLabel("Std Cond:")); + spinCondStd = new QDoubleSpinBox(this); + spinCondStd->setRange(0.0, 1000000.0); spinCondStd->setValue(1413.0); spinCondStd->setSuffix(" µS/cm"); + row1->addWidget(spinCondStd); + + btnCalCond = new QPushButton("Calibrate K", this); + row1->addWidget(btnCalCond); + + lblResultRs = new QLabel(" Rs: -- Ω", this); + lblResultRs->setStyleSheet("font-weight: bold; color: #FFD700; font-size: 14px; margin-left: 10px;"); + row1->addWidget(lblResultRs); + + row1->addStretch(); + ctrlLayout->addLayout(row1); + + // Row 2 + QHBoxLayout *row2 = new QHBoxLayout(); + row2->setSpacing(8); + + row2->addWidget(new QLabel("Range:")); + comboRange = new QComboBox(this); + comboRange->addItem("200 Ω", 200); + comboRange->addItem("1 kΩ", 1000); + comboRange->addItem("5 kΩ", 5000); + comboRange->addItem("10 kΩ", 10000); + comboRange->addItem("20 kΩ", 20000); + comboRange->addItem("40 kΩ", 40000); + comboRange->addItem("80 kΩ", 80000); + comboRange->addItem("160 kΩ", 160000); + comboRange->setCurrentIndex(0); + row2->addWidget(comboRange); + + QFrame *sepRange = new QFrame(); sepRange->setFrameShape(QFrame::VLine); sepRange->setFrameShadow(QFrame::Sunken); + row2->addWidget(sepRange); + + lblResultCond = new QLabel("Cond: -- µS/cm", this); + lblResultCond->setStyleSheet("font-weight: bold; color: #00FFFF; font-size: 14px; margin-right: 10px;"); + row2->addWidget(lblResultCond); + + QFrame *sep5 = new QFrame(); sep5->setFrameShape(QFrame::VLine); sep5->setFrameShadow(QFrame::Sunken); + row2->addWidget(sep5); + + row2->addWidget(new QLabel("Freq:")); + spinFreq = new QDoubleSpinBox(this); + spinFreq->setRange(0.1, 200000.0); spinFreq->setValue(1000.0); spinFreq->setSuffix(" Hz"); + row2->addWidget(spinFreq); + + measureBtn = new QPushButton("Measure", this); + row2->addWidget(measureBtn); + + QFrame *sep6 = new QFrame(); sep6->setFrameShape(QFrame::VLine); sep6->setFrameShadow(QFrame::Sunken); + row2->addWidget(sep6); + + row2->addWidget(new QLabel("Bias:")); + spinAmpBias = new QDoubleSpinBox(this); + spinAmpBias->setRange(-1100.0, 1100.0); spinAmpBias->setValue(0.0); spinAmpBias->setSuffix(" mV"); + row2->addWidget(spinAmpBias); + + row2->addWidget(new QLabel("LPF:")); + comboLPF = new QComboBox(this); + comboLPF->addItem("Bypass", 0); + comboLPF->addItem("20kΩ (8Hz)", 1); + comboLPF->addItem("100kΩ (1.6Hz)", 2); + comboLPF->addItem("200kΩ (0.8Hz)", 3); + comboLPF->addItem("400kΩ (0.4Hz)", 4); + comboLPF->addItem("600kΩ (0.26Hz)", 5); + comboLPF->addItem("1MΩ (0.16Hz)", 6); + comboLPF->setCurrentIndex(1); // Default 20k + row2->addWidget(comboLPF); + + ampBtn = new QPushButton("Start Amp", this); + row2->addWidget(ampBtn); + + QFrame *sep7 = new QFrame(); sep7->setFrameShape(QFrame::VLine); sep7->setFrameShadow(QFrame::Sunken); + row2->addWidget(sep7); + + // LSV Controls + row2->addWidget(new QLabel("LSV:")); + spinLsvStart = new QDoubleSpinBox(this); + spinLsvStart->setRange(-1100.0, 1100.0); spinLsvStart->setValue(800.0); spinLsvStart->setSuffix(" mV"); + row2->addWidget(spinLsvStart); + + spinLsvStop = new QDoubleSpinBox(this); + spinLsvStop->setRange(-1100.0, 1100.0); spinLsvStop->setValue(-200.0); spinLsvStop->setSuffix(" mV"); + row2->addWidget(spinLsvStop); + + spinLsvSteps = new QSpinBox(this); + spinLsvSteps->setRange(10, 4000); spinLsvSteps->setValue(200); spinLsvSteps->setSuffix(" pts"); + row2->addWidget(spinLsvSteps); + + spinLsvDuration = new QSpinBox(this); + spinLsvDuration->setRange(100, 600000); spinLsvDuration->setValue(10000); spinLsvDuration->setSuffix(" ms"); + row2->addWidget(spinLsvDuration); + + lsvBlankBtn = new QPushButton("Run Blank", this); + row2->addWidget(lsvBlankBtn); + + lsvSampleBtn = new QPushButton("Run Sample", this); + row2->addWidget(lsvSampleBtn); + + row2->addStretch(); + ctrlLayout->addLayout(row2); + + mainLayout->addWidget(controlsContainer); + + // --- Splitter for Graphs and Log --- + QSplitter *splitter = new QSplitter(Qt::Vertical, this); + tabWidget = new QTabWidget(this); - rawGraph = new GraphWidget(this); - rawGraph->configureRawPlot(); - nyquistGraph = new GraphWidget(this); + ampGraph = new GraphWidget(this); + lsvGraph = new GraphWidget(this); + + rawGraph->configureRawPlot(); nyquistGraph->configureNyquistPlot(); + ampGraph->configureAmperometricPlot(); + lsvGraph->configureLSVPlot(); tabWidget->addTab(rawGraph, "Raw Data"); tabWidget->addTab(nyquistGraph, "Nyquist Plot"); + tabWidget->addTab(ampGraph, "Amperometry"); + tabWidget->addTab(lsvGraph, "Voltammetry"); - // Log Widget logWidget = new QTextEdit(this); logWidget->setReadOnly(true); logWidget->setFont(QFont("Monospace")); logWidget->setPlaceholderText("Scanning for 0xCAFE EIS Device..."); + logWidget->setStyleSheet("background-color: #1E1E1E; color: #00FF00; border: 1px solid #444;"); QScroller::grabGesture(logWidget->viewport(), QScroller::TouchGesture); splitter->addWidget(tabWidget); splitter->addWidget(logWidget); - splitter->setStretchFactor(0, 2); + splitter->setStretchFactor(0, 3); splitter->setStretchFactor(1, 1); - // Toolbar Construction - toolbar = addToolBar("Connection"); - toolbar->setMovable(false); + mainLayout->addWidget(splitter); - portSelector = new QComboBox(this); - portSelector->setMinimumWidth(150); - connectBtn = new QPushButton("Connect", this); - QPushButton *refreshBtn = new QPushButton("Refresh", this); - - toolbar->addWidget(portSelector); - toolbar->addWidget(connectBtn); - toolbar->addWidget(refreshBtn); - toolbar->addSeparator(); - - checkIdBtn = new QPushButton("Check ID", this); - calibrateBtn = new QPushButton("Calibrate", this); - - toolbar->addWidget(checkIdBtn); - toolbar->addWidget(calibrateBtn); - toolbar->addSeparator(); - - // Sweep Configuration - QLabel *lblStart = new QLabel(" Start:", this); - spinSweepStart = new QDoubleSpinBox(this); - spinSweepStart->setRange(0.1, 200000.0); - spinSweepStart->setValue(1000.0); - spinSweepStart->setSuffix(" Hz"); - spinSweepStart->setToolTip("Sweep Start Frequency"); - - QLabel *lblStop = new QLabel(" Stop:", this); - spinSweepStop = new QDoubleSpinBox(this); - spinSweepStop->setRange(0.1, 200000.0); - spinSweepStop->setValue(200000.0); - spinSweepStop->setSuffix(" Hz"); - spinSweepStop->setToolTip("Sweep Stop Frequency"); - - QLabel *lblPPD = new QLabel(" PPD:", this); - spinSweepPPD = new QSpinBox(this); - spinSweepPPD->setRange(1, 1000); - spinSweepPPD->setValue(200); - spinSweepPPD->setSuffix(" pts/dec"); - spinSweepPPD->setToolTip("Points Per Decade"); - - sweepBtn = new QPushButton("Sweep", this); - - toolbar->addWidget(lblStart); - toolbar->addWidget(spinSweepStart); - toolbar->addWidget(lblStop); - toolbar->addWidget(spinSweepStop); - toolbar->addWidget(lblPPD); - toolbar->addWidget(spinSweepPPD); - toolbar->addWidget(sweepBtn); - toolbar->addSeparator(); - - // Measurement Control - QLabel *lblFreq = new QLabel(" Freq:", this); - spinFreq = new QDoubleSpinBox(this); - spinFreq->setRange(10.0, 200000.0); - spinFreq->setValue(1000.0); - spinFreq->setSuffix(" Hz"); - measureBtn = new QPushButton("Measure", this); - - toolbar->addWidget(lblFreq); - toolbar->addWidget(spinFreq); - toolbar->addWidget(measureBtn); - - // Initial State: Disabled + // Initial State checkIdBtn->setEnabled(false); calibrateBtn->setEnabled(false); sweepBtn->setEnabled(false); @@ -172,31 +294,37 @@ void MainWindow::setupUi() { spinSweepStart->setEnabled(false); spinSweepStop->setEnabled(false); spinSweepPPD->setEnabled(false); + ampBtn->setEnabled(false); + spinAmpBias->setEnabled(false); + btnCalCond->setEnabled(false); + comboRange->setEnabled(false); + comboLPF->setEnabled(false); + lsvBlankBtn->setEnabled(false); + lsvSampleBtn->setEnabled(false); + spinLsvStart->setEnabled(false); + spinLsvStop->setEnabled(false); + spinLsvSteps->setEnabled(false); + spinLsvDuration->setEnabled(false); - // Signal Connections + // Connections connect(connectBtn, &QPushButton::clicked, this, &MainWindow::connectToPort); connect(refreshBtn, &QPushButton::clicked, this, &MainWindow::refreshPorts); connect(checkIdBtn, &QPushButton::clicked, this, &MainWindow::checkDeviceId); connect(calibrateBtn, &QPushButton::clicked, this, &MainWindow::runCalibration); connect(sweepBtn, &QPushButton::clicked, this, &MainWindow::startSweep); connect(measureBtn, &QPushButton::clicked, this, &MainWindow::toggleMeasurement); + connect(ampBtn, &QPushButton::clicked, this, &MainWindow::toggleAmperometry); + connect(lsvBlankBtn, &QPushButton::clicked, this, &MainWindow::startLSVBlank); + connect(lsvSampleBtn, &QPushButton::clicked, this, &MainWindow::startLSVSample); + connect(btnCalCond, &QPushButton::clicked, this, &MainWindow::calibrateCellConstant); + connect(comboLPF, QOverload::of(&QComboBox::currentIndexChanged), this, &MainWindow::onLPFChanged); - #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) - toolbar->setIconSize(QSize(32, 32)); - QFont font = portSelector->font(); - font.setPointSize(14); - portSelector->setFont(font); - connectBtn->setFont(font); - refreshBtn->setFont(font); - checkIdBtn->setFont(font); - calibrateBtn->setFont(font); - sweepBtn->setFont(font); - measureBtn->setFont(font); - spinFreq->setFont(font); - spinSweepStart->setFont(font); - spinSweepStop->setFont(font); - spinSweepPPD->setFont(font); - #endif + connect(tabWidget, &QTabWidget::currentChanged, this, [this](int index){ + if (index == 0) rawGraph->configureRawPlot(); + else if (index == 1) nyquistGraph->configureNyquistPlot(); + else if (index == 2) ampGraph->configureAmperometricPlot(); + else if (index == 3) lsvGraph->configureLSVPlot(); + }); } void MainWindow::refreshPorts() { @@ -207,24 +335,17 @@ void MainWindow::refreshPorts() { for (const QSerialPortInfo &info : infos) { portSelector->addItem(info.portName()); - - // Check for 0xCAFE or "usbmodem" bool isCafe = (info.hasVendorIdentifier() && info.vendorIdentifier() == 0xCAFE); bool isUsbModem = info.portName().contains("usbmodem", Qt::CaseInsensitive); - if ((isCafe || isUsbModem) && !foundTarget) { targetPort = info.portName(); foundTarget = true; logWidget->append(">> Found Target Device: " + targetPort); } } - - // Auto-connect if found and not already connected if (foundTarget) { portSelector->setCurrentText(targetPort); - if (!serial->isOpen()) { - connectToPort(); - } + if (!serial->isOpen()) connectToPort(); } } @@ -240,8 +361,28 @@ void MainWindow::connectToPort() { spinSweepStart->setEnabled(false); spinSweepStop->setEnabled(false); spinSweepPPD->setEnabled(false); - isMeasuring = false; + ampBtn->setEnabled(false); + spinAmpBias->setEnabled(false); + btnCalCond->setEnabled(false); + comboRange->setEnabled(false); + comboLPF->setEnabled(false); + lsvBlankBtn->setEnabled(false); + lsvSampleBtn->setEnabled(false); + spinLsvStart->setEnabled(false); + spinLsvStop->setEnabled(false); + spinLsvSteps->setEnabled(false); + spinLsvDuration->setEnabled(false); + + isMeasuringImp = false; + isMeasuringAmp = false; + isSweeping = false; + lsvState = LSV_IDLE; + setButtonBlinking(nullptr, false); + measureBtn->setText("Measure"); + ampBtn->setText("Start Amp"); + lsvBlankBtn->setText("Run Blank"); + lsvSampleBtn->setText("Run Sample"); return; } @@ -260,6 +401,20 @@ void MainWindow::connectToPort() { spinSweepStart->setEnabled(true); spinSweepStop->setEnabled(true); spinSweepPPD->setEnabled(true); + ampBtn->setEnabled(true); + spinAmpBias->setEnabled(true); + btnCalCond->setEnabled(true); + comboRange->setEnabled(true); + comboLPF->setEnabled(true); + lsvBlankBtn->setEnabled(true); + lsvSampleBtn->setEnabled(true); + spinLsvStart->setEnabled(true); + spinLsvStop->setEnabled(true); + spinLsvSteps->setEnabled(true); + spinLsvDuration->setEnabled(true); + + // Sync LPF + onLPFChanged(comboLPF->currentIndex()); } else { logWidget->append(">> Connection Error: " + serial->errorString()); } @@ -277,63 +432,216 @@ void MainWindow::onPortError(QSerialPort::SerialPortError error) { spinSweepStart->setEnabled(false); spinSweepStop->setEnabled(false); spinSweepPPD->setEnabled(false); - isMeasuring = false; - measureBtn->setText("Measure"); + ampBtn->setEnabled(false); + spinAmpBias->setEnabled(false); + btnCalCond->setEnabled(false); + comboRange->setEnabled(false); + comboLPF->setEnabled(false); + lsvBlankBtn->setEnabled(false); + lsvSampleBtn->setEnabled(false); + spinLsvStart->setEnabled(false); + spinLsvStop->setEnabled(false); + spinLsvSteps->setEnabled(false); + spinLsvDuration->setEnabled(false); + setButtonBlinking(nullptr, false); } } void MainWindow::checkDeviceId() { - if (serial->isOpen()) { - logWidget->append(">> Checking ID (v)..."); - serial->write("v\n"); - } + if (serial->isOpen()) serial->write("v\n"); } void MainWindow::runCalibration() { if (serial->isOpen()) { - logWidget->append(">> Running Calibration (c)..."); + int rangeVal = comboRange->currentData().toInt(); + serial->write(QString("r %1\n").arg(rangeVal).toUtf8()); serial->write("c\n"); } } +void MainWindow::onLPFChanged(int index) { + if (serial->isOpen()) { + int val = comboLPF->itemData(index).toInt(); + serial->write(QString("f %1\n").arg(val).toUtf8()); + } +} + void MainWindow::startSweep() { if (!serial->isOpen()) return; + if (isSweeping) { + // Stop Sweep + serial->write("x\n"); + return; + } + + if (isMeasuringAmp) toggleAmperometry(); + if (lsvState != LSV_IDLE) stopLSV(); + if (isMeasuringImp) toggleMeasurement(); + rawGraph->clear(); nyquistGraph->clear(); - // Clear accumulated data sweepFreqs.clear(); sweepReals.clear(); + sweepImags.clear(); double start = spinSweepStart->value(); double stop = spinSweepStop->value(); int ppd = spinSweepPPD->value(); + int rangeVal = comboRange->currentData().toInt(); - // Send Firmware Sweep Command: s - logWidget->append(QString(">> Starting Sweep (s %1 %2 %3)...").arg(start).arg(stop).arg(ppd)); + logWidget->append(QString(">> Starting Sweep (Range: %1, s %2 %3 %4)...").arg(rangeVal).arg(start).arg(stop).arg(ppd)); + + serial->write(QString("r %1\n").arg(rangeVal).toUtf8()); serial->write(QString("s %1 %2 %3\n").arg(start).arg(stop).arg(ppd).toUtf8()); + + isSweeping = true; + sweepBtn->setText("Stop Sweep"); + setButtonBlinking(sweepBtn, true); + tabWidget->setCurrentIndex(1); } void MainWindow::toggleMeasurement() { if (!serial->isOpen()) return; + if (isMeasuringAmp) toggleAmperometry(); + if (lsvState != LSV_IDLE) stopLSV(); + if (isSweeping) startSweep(); // Stop sweep - if (isMeasuring) { - // Stop - logWidget->append(">> Stopping Measurement (x)..."); + if (isMeasuringImp) { serial->write("x\n"); measureBtn->setText("Measure"); - isMeasuring = false; + setButtonBlinking(nullptr, false); + isMeasuringImp = false; } else { - // Start double freq = spinFreq->value(); - logWidget->append(QString(">> Requesting Measure (m %1)...").arg(freq)); + int rangeVal = comboRange->currentData().toInt(); + + serial->write(QString("r %1\n").arg(rangeVal).toUtf8()); serial->write(QString("m %1\n").arg(freq).toUtf8()); + measureBtn->setText("Stop"); - isMeasuring = true; + setButtonBlinking(measureBtn, true); + isMeasuringImp = true; + tabWidget->setCurrentIndex(0); } } +void MainWindow::toggleAmperometry() { + if (!serial->isOpen()) return; + if (isMeasuringImp) toggleMeasurement(); + if (lsvState != LSV_IDLE) stopLSV(); + if (isSweeping) startSweep(); + + if (isMeasuringAmp) { + serial->write("x\n"); + ampBtn->setText("Start Amp"); + setButtonBlinking(nullptr, false); + isMeasuringAmp = false; + } else { + double bias = spinAmpBias->value(); + int rangeVal = comboRange->currentData().toInt(); + + serial->write(QString("r %1\n").arg(rangeVal).toUtf8()); + serial->write(QString("a %1\n").arg(bias).toUtf8()); + + ampBtn->setText("Stop Amp"); + setButtonBlinking(ampBtn, true); + isMeasuringAmp = true; + ampGraph->clear(); + tabWidget->setCurrentIndex(2); + } +} + +void MainWindow::startLSVBlank() { + if (!serial->isOpen()) return; + if (isMeasuringImp) toggleMeasurement(); + if (isMeasuringAmp) toggleAmperometry(); + if (lsvState != LSV_IDLE) stopLSV(); + if (isSweeping) startSweep(); + + double start = spinLsvStart->value(); + double stop = spinLsvStop->value(); + int steps = spinLsvSteps->value(); + int duration = spinLsvDuration->value(); + int rangeVal = comboRange->currentData().toInt(); + + logWidget->append(QString(">> Starting LSV Blank (Range: %1, %.1f to %.1f mV)...").arg(rangeVal).arg(start).arg(stop)); + + serial->write(QString("r %1\n").arg(rangeVal).toUtf8()); + serial->write(QString("l %1 %2 %3 %4\n").arg(start).arg(stop).arg(steps).arg(duration).toUtf8()); + + lsvBlankBtn->setText("Stop Blank"); + setButtonBlinking(lsvBlankBtn, true); + lsvState = LSV_RUNNING_BLANK; + + lsvGraph->clearLSV(GraphWidget::LSV_BLANK); + lsvGraph->clearLSV(GraphWidget::LSV_DIFF); // Clear old diff + lsvBlankData.clear(); + + tabWidget->setCurrentIndex(3); +} + +void MainWindow::startLSVSample() { + if (!serial->isOpen()) return; + if (isMeasuringImp) toggleMeasurement(); + if (isMeasuringAmp) toggleAmperometry(); + if (lsvState != LSV_IDLE) stopLSV(); + if (isSweeping) startSweep(); + + double start = spinLsvStart->value(); + double stop = spinLsvStop->value(); + int steps = spinLsvSteps->value(); + int duration = spinLsvDuration->value(); + int rangeVal = comboRange->currentData().toInt(); + + logWidget->append(QString(">> Starting LSV Sample (Range: %1, %.1f to %.1f mV)...").arg(rangeVal).arg(start).arg(stop)); + + serial->write(QString("r %1\n").arg(rangeVal).toUtf8()); + serial->write(QString("l %1 %2 %3 %4\n").arg(start).arg(stop).arg(steps).arg(duration).toUtf8()); + + lsvSampleBtn->setText("Stop Sample"); + setButtonBlinking(lsvSampleBtn, true); + lsvState = LSV_RUNNING_SAMPLE; + + lsvGraph->clearLSV(GraphWidget::LSV_SAMPLE); + lsvGraph->clearLSV(GraphWidget::LSV_DIFF); // Clear old diff + lsvSampleData.clear(); + + tabWidget->setCurrentIndex(3); +} + +void MainWindow::stopLSV() { + serial->write("x\n"); + lsvBlankBtn->setText("Run Blank"); + lsvSampleBtn->setText("Run Sample"); + setButtonBlinking(nullptr, false); + + // If we just finished a sample run, calculate the difference + if (lsvState == LSV_RUNNING_SAMPLE) { + calculateLSVDiff(); + } + + lsvState = LSV_IDLE; +} + +void MainWindow::calculateLSVDiff() { + if (lsvBlankData.isEmpty() || lsvSampleData.isEmpty()) return; + + lsvGraph->clearLSV(GraphWidget::LSV_DIFF); + + // Simple index-based subtraction (assumes same parameters used) + int count = std::min(lsvBlankData.size(), lsvSampleData.size()); + + for (int i = 0; i < count; i++) { + double v = lsvSampleData[i].voltage; // Use sample voltage + double diffCurrent = lsvSampleData[i].current - lsvBlankData[i].current; + lsvGraph->addLSVData(v, diffCurrent, GraphWidget::LSV_DIFF); + } + + logWidget->append(">> Calculated Difference Curve."); +} + void MainWindow::handleSerialData() { while (serial->canReadLine()) { QByteArray line = serial->readLine(); @@ -346,78 +654,328 @@ void MainWindow::handleSerialData() { if (str.startsWith("DATA,")) { parseData(str); + } else if (str.startsWith("AMP,")) { + parseData(str); + } else if (str.startsWith("RAMP,")) { + parseData(str); + } else if (str == "STOPPED") { + // Reset UI state + if (lsvState != LSV_IDLE) stopLSV(); + if (isSweeping) { + isSweeping = false; + sweepBtn->setText("Sweep"); + setButtonBlinking(nullptr, false); + } + if (isMeasuringImp) { + isMeasuringImp = false; + measureBtn->setText("Measure"); + setButtonBlinking(nullptr, false); + } + if (isMeasuringAmp) { + isMeasuringAmp = false; + ampBtn->setText("Start Amp"); + setButtonBlinking(nullptr, false); + } } } } void MainWindow::parseData(const QString &data) { - // Format: DATA,Freq,Mag,Phase,Real,Imag QStringList parts = data.split(','); - if (parts.size() < 6) return; + + if (parts[0] == "AMP" && parts.size() >= 3) { + bool okIdx, okCurr; + double index = parts[1].toDouble(&okIdx); + double current = parts[2].toDouble(&okCurr); + if (okIdx && okCurr) ampGraph->addAmperometricData(index, current); + return; + } - bool okF, okR, okI; - double freq = parts[1].toDouble(&okF); - double real = parts[4].toDouble(&okR); - double imag = parts[5].toDouble(&okI); + if (parts[0] == "RAMP" && parts.size() >= 3) { + bool okIdx, okCurr; + double index = parts[1].toDouble(&okIdx); + double current = parts[2].toDouble(&okCurr); + + if (okIdx && okCurr) { + // Calculate Voltage based on UI parameters (Host-side calculation) + double start = spinLsvStart->value(); + double stop = spinLsvStop->value(); + int steps = spinLsvSteps->value(); + + double voltage = start + (index * (stop - start) / steps); + + if (lsvState == LSV_RUNNING_BLANK) { + lsvGraph->addLSVData(voltage, current, GraphWidget::LSV_BLANK); + lsvBlankData.append({voltage, current}); + } else if (lsvState == LSV_RUNNING_SAMPLE) { + lsvGraph->addLSVData(voltage, current, GraphWidget::LSV_SAMPLE); + lsvSampleData.append({voltage, current}); + } + } + return; + } - if (okF && okR && okI) { - // Add to Raw Graph (Freq vs Real/Imag) - rawGraph->addData(freq, real, imag); - - // Add to Nyquist Graph (Real vs Imag) - nyquistGraph->addData(real, imag, 0); - - // Accumulate for Hilbert - sweepFreqs.append(freq); - sweepReals.append(real); - - // Update Hilbert periodically (every 10 points) to show progress - if (sweepReals.size() > 10 && sweepReals.size() % 10 == 0) { - computeHilbert(); + if (parts[0] == "DATA" && parts.size() >= 6) { + bool okF, okR, okI; + double freq = parts[1].toDouble(&okF); + double real = parts[4].toDouble(&okR); + double imag = parts[5].toDouble(&okI); + + if (okF && okR && okI) { + double real_plot = real; + double imag_plot = imag; + bool showCorr = false; + + if (checkShunt->isChecked()) { + showCorr = true; + double r_shunt = spinShuntRes->value(); + std::complex z_meas(real, imag); + std::complex z_shunt(r_shunt, 0.0); + std::complex denom = z_shunt - z_meas; + + if (std::abs(denom) > 1e-9) { + std::complex z_cell = (z_meas * z_shunt) / denom; + real_plot = z_cell.real(); + imag_plot = z_cell.imag(); + } else { + real_plot = 1e9; + imag_plot = 0; + } + } + + rawGraph->addBodeData(freq, real_plot, imag_plot); + nyquistGraph->addNyquistData(real, imag, real_plot, imag_plot, showCorr); + + sweepFreqs.append(freq); + sweepReals.append(real_plot); + sweepImags.append(imag_plot); + + if (sweepReals.size() > 10 && sweepReals.size() % 10 == 0) { + computeHilbert(); + performCircleFit(); + } } } } - void MainWindow::computeHilbert() { int n = sweepReals.size(); if (n == 0) return; - - // 1. Zero-pad to next power of 2 int n_fft = 1; while (n_fft < n) n_fft *= 2; std::vector> signal(n_fft); - for (int i = 0; i < n; i++) { - signal[i] = std::complex(sweepReals[i], 0.0); - } + for (int i = 0; i < n; i++) signal[i] = std::complex(sweepReals[i], 0.0); - // 2. FFT fft(signal); - - // 3. Create Analytic Signal in Frequency Domain - // H[0] = H[0] - // H[i] = 2*H[i] for 0 < i < N/2 - // H[N/2] = H[N/2] - // H[i] = 0 for N/2 < i < N - for (int i = 1; i < n_fft / 2; i++) { - signal[i] *= 2.0; - } - for (int i = n_fft / 2 + 1; i < n_fft; i++) { - signal[i] = 0.0; - } - - // 4. IFFT + for (int i = 1; i < n_fft / 2; i++) signal[i] *= 2.0; + for (int i = n_fft / 2 + 1; i < n_fft; i++) signal[i] = 0.0; ifft(signal); - // 5. Extract Imaginary Part (Hilbert Transform) to represent the analytical. QVector hilbertImag; - for (int i = 0; i < n; i++) { - hilbertImag.append(signal[i].imag()); + for (int i = 0; i < n; i++) hilbertImag.append(signal[i].imag()); + rawGraph->addHilbertData(sweepFreqs, hilbertImag); +} + +// Least Squares Circle Fit to find Rs (High Freq Intercept) +void MainWindow::performCircleFit() { + int n = sweepReals.size(); + if (n < 5) return; + + // --- Check for actual zero crossing first --- + double measuredRs = -1.0; + bool found = false; + + for (int i = 0; i < n - 1; i++) { + double img1 = sweepImags[i]; + double img2 = sweepImags[i+1]; + + // Check if we cross zero (one positive, one negative) or hit zero exactly + if ((img1 >= 0 && img2 < 0) || (img1 < 0 && img2 >= 0)) { + double r1 = sweepReals[i]; + double r2 = sweepReals[i+1]; + + // Avoid division by zero + if (std::abs(img2 - img1) < 1e-9) continue; + + // Linear Interpolation for Real Z where Imag Z = 0 + double fraction = (0.0 - img1) / (img2 - img1); + double crossingReal = r1 + fraction * (r2 - r1); + + // We want the smallest Real value (Rs is the left-most intercept) + if (!found || crossingReal < measuredRs) { + measuredRs = crossingReal; + found = true; + } + } } - // 6. Plot - rawGraph->addHilbertData(sweepFreqs, hilbertImag); + if (found && measuredRs > 0) { + lblResultRs->setText(QString(" Rs: %1 Ω (Meas)").arg(measuredRs, 0, 'f', 2)); + + double cond = (cellConstant / measuredRs) * 1000000.0; + lblResultCond->setText(QString(" Cond: %1 µS/cm").arg(cond, 0, 'f', 2)); + + nyquistGraph->setExtrapolatedPoint(measuredRs, 0); + return; // Skip extrapolation + } + + // Use last 1/3rd of points (High Frequency end) + int startIdx = n - (n / 3); + if (startIdx < 0) startIdx = 0; + int count = n - startIdx; + if (count < 3) return; + + double sum_x = 0, sum_y = 0, sum_x2 = 0, sum_y2 = 0; + double sum_xy = 0, sum_x3 = 0, sum_y3 = 0, sum_xy2 = 0, sum_x2y = 0; + + // Kasa Method for Circle Fit: Minimize sum((x^2 + y^2) - (Ax + By + C))^2 + // x = Real, y = -Imag (Nyquist convention) + + for (int i = startIdx; i < n; i++) { + double x = sweepReals[i]; + double y = -sweepImags[i]; // Nyquist Y is -Imag + + double x2 = x * x; + double y2 = y * y; + + sum_x += x; + sum_y += y; + sum_x2 += x2; + sum_y2 += y2; + sum_xy += x * y; + sum_x3 += x2 * x; + sum_y3 += y2 * y; + sum_xy2 += x * y2; + sum_x2y += x2 * y; + } + + // Solve Linear System for A, B, C + // | sum_x2 sum_xy sum_x | | A | | sum_x(x2+y2) | + // | sum_xy sum_y2 sum_y | | B | = | sum_y(x2+y2) | + // | sum_x sum_y N | | C | | sum(x2+y2) | + + double M11 = sum_x2, M12 = sum_xy, M13 = sum_x; + double M21 = sum_xy, M22 = sum_y2, M23 = sum_y; + double M31 = sum_x, M32 = sum_y, M33 = (double)count; + + double R1 = sum_x3 + sum_xy2; + double R2 = sum_x2y + sum_y3; + double R3 = sum_x2 + sum_y2; + + // Determinant of M + double det = M11*(M22*M33 - M23*M32) - M12*(M21*M33 - M23*M31) + M13*(M21*M32 - M22*M31); + + if (std::abs(det) < 1e-9) return; // Singular + + // Cramer's Rule for A, B, C + double detA = R1*(M22*M33 - M23*M32) - M12*(R2*M33 - M23*R3) + M13*(R2*M32 - M22*R3); + double detB = M11*(R2*M33 - M23*R3) - R1*(M21*M33 - M23*M31) + M13*(M21*R3 - R2*M31); + double detC = M11*(M22*R3 - R2*M32) - M12*(M21*R3 - R2*M31) + R1*(M21*M32 - M22*M31); + + double A = detA / det; + double B = detB / det; + double C = detC / det; + + // Circle Parameters + // Center (xc, yc) = (A/2, B/2) + // Radius r = sqrt(C + xc^2 + yc^2) + + double xc = A / 2.0; + // double yc = B / 2.0; // Not needed for intercept + double r_sq = C + (A*A)/4.0 + (B*B)/4.0; + + if (r_sq < 0) return; // Invalid fit + + // Find Intercepts with Real Axis (y = 0) + // Equation: x^2 + y^2 - Ax - By - C = 0 + // Set y=0: x^2 - Ax - C = 0 + // x = (A +/- sqrt(A^2 + 4C)) / 2 + + double disc = A*A + 4*C; + if (disc < 0) return; // No real intercept + + double x1 = (A - std::sqrt(disc)) / 2.0; + double x2 = (A + std::sqrt(disc)) / 2.0; + + // Rs is typically the smaller positive intercept (High Frequency) + // Rct + Rs is the larger intercept (Low Frequency) + double Rs = 0; + if (x1 > 0 && x2 > 0) Rs = std::min(x1, x2); + else if (x1 > 0) Rs = x1; + else if (x2 > 0) Rs = x2; + else Rs = std::max(x1, x2); // Fallback + + // Update UI + lblResultRs->setText(QString(" Rs: %1 Ω").arg(Rs, 0, 'f', 2)); + + // Calculate Conductivity: Cond = K / Rs + // K is in cm^-1, Rs in Ohms -> S/cm -> * 1e6 for uS/cm + double cond = (cellConstant / Rs) * 1000000.0; + lblResultCond->setText(QString(" Cond: %1 µS/cm").arg(cond, 0, 'f', 2)); + + // Update Graph Marker + nyquistGraph->setExtrapolatedPoint(Rs, 0); +} + +void MainWindow::calibrateCellConstant() { + // Parse current Rs from label (dirty but effective given the flow) + QString txt = lblResultRs->text(); + if (txt.contains("--")) { + QMessageBox::warning(this, "Calibration Error", "No valid Rs measurement found. Run a sweep first."); + return; + } + + // Extract number from string " Rs: 123.45 Ω" + QString numStr = txt.section(':', 1).section(QChar(0x03A9), 0, 0).trimmed(); // 0x03A9 is Omega + double measuredRs = numStr.toDouble(); + + if (measuredRs <= 0) return; + + double stdCond = spinCondStd->value(); // uS/cm + + // K = Rs * Cond + // Units: Ohm * (uS/cm) * 1e-6 = Ohm * S/cm * 1e-6 = (1/cm) * 1e-6 ??? + // Wait: Cond = K / Rs => K = Cond * Rs + // K [cm^-1] = (uS/cm * 1e-6) [S/cm] * Rs [Ohm] + + cellConstant = (stdCond * 1e-6) * measuredRs; + + saveSettings(); + + QMessageBox::information(this, "Calibration Success", + QString("Cell Constant (K) calibrated to: %1 cm⁻¹").arg(cellConstant, 0, 'f', 4)); + + // Re-run fit to update display with new K + performCircleFit(); +} + +void MainWindow::setButtonBlinking(QPushButton *btn, bool blinking) { + if (activeButton && activeButton != btn) { + // Reset previous button + activeButton->setStyleSheet(""); + } + + activeButton = btn; + + if (blinking) { + blinkTimer->start(); + } else { + blinkTimer->stop(); + if (activeButton) activeButton->setStyleSheet(""); + activeButton = nullptr; + } +} + +void MainWindow::onBlinkTimer() { + if (!activeButton) return; + + blinkState = !blinkState; + if (blinkState) { + activeButton->setStyleSheet("background-color: #FF0000; color: white; border: 1px solid #FF4444;"); + } else { + activeButton->setStyleSheet("background-color: #880000; color: white; border: 1px solid #AA0000;"); + } } bool MainWindow::event(QEvent *event) { diff --git a/host/src/MainWindow.h b/host/src/MainWindow.h index 39bbb97..405b815 100644 --- a/host/src/MainWindow.h +++ b/host/src/MainWindow.h @@ -1,4 +1,4 @@ -// File: host/src/MainWindow.h +// host/src/MainWindow.h #pragma once #include @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include "GraphWidget.h" class MainWindow : public QMainWindow { @@ -31,29 +33,49 @@ private slots: void connectToPort(); void refreshPorts(); void onPortError(QSerialPort::SerialPortError error); + void onBlinkTimer(); // Action Slots void checkDeviceId(); void runCalibration(); void startSweep(); void toggleMeasurement(); + void toggleAmperometry(); + + // LSV Slots + void startLSVBlank(); + void startLSVSample(); + void stopLSV(); + + void calibrateCellConstant(); + void onLPFChanged(int index); private: void setupUi(); + void loadSettings(); + void saveSettings(); void parseData(const QString &data); void handleSwipe(QSwipeGesture *gesture); void computeHilbert(); + void performCircleFit(); + void calculateLSVDiff(); + void setButtonBlinking(QPushButton *btn, bool blinking); QSerialPort *serial; + QSettings *settings; + QTimer *blinkTimer; + QPushButton *activeButton = nullptr; + bool blinkState = false; // Views - GraphWidget *rawGraph; // Raw Data (Freq vs Real/Imag) - GraphWidget *nyquistGraph; // Nyquist (Real vs -Imag) - QTextEdit *logWidget; // Serial Log + GraphWidget *rawGraph; + GraphWidget *nyquistGraph; + GraphWidget *ampGraph; + GraphWidget *lsvGraph; + QTextEdit *logWidget; // Layout QTabWidget *tabWidget; - QToolBar *toolbar; QComboBox *portSelector; QPushButton *connectBtn; QPushButton *checkIdBtn; @@ -62,14 +84,52 @@ private: QPushButton *measureBtn; QDoubleSpinBox *spinFreq; - // EIS + // EIS Configuration QDoubleSpinBox *spinSweepStart; QDoubleSpinBox *spinSweepStop; QSpinBox *spinSweepPPD; + QComboBox *comboRange; - bool isMeasuring = false; + // Shunt / De-embedding Configuration + QCheckBox *checkShunt; + QDoubleSpinBox *spinShuntRes; + + // Amperometry Configuration + QDoubleSpinBox *spinAmpBias; + QPushButton *ampBtn; + QComboBox *comboLPF; // New LPF Dropdown + + // LSV Configuration + QDoubleSpinBox *spinLsvStart; + QDoubleSpinBox *spinLsvStop; + QSpinBox *spinLsvSteps; + QSpinBox *spinLsvDuration; + QPushButton *lsvBlankBtn; + QPushButton *lsvSampleBtn; + + // Conductivity Calibration + QDoubleSpinBox *spinCondStd; + QPushButton *btnCalCond; + QLabel *lblResultRs; + QLabel *lblResultCond; - // Data Accumulation for Hilbert + double cellConstant = 1.0; + + bool isMeasuringImp = false; + bool isMeasuringAmp = false; + bool isSweeping = false; + + // LSV State + enum LSVState { LSV_IDLE, LSV_RUNNING_BLANK, LSV_RUNNING_SAMPLE }; + LSVState lsvState = LSV_IDLE; + + // Data Accumulation QVector sweepFreqs; - QVector sweepReals; + QVector sweepReals; + QVector sweepImags; + + // LSV Data Storage for Diff Calculation + struct LSVPoint { double voltage; double current; }; + QVector lsvBlankData; + QVector lsvSampleData; }; \ No newline at end of file diff --git a/host/src/main.cpp b/host/src/main.cpp index 48df579..0cb98dd 100644 --- a/host/src/main.cpp +++ b/host/src/main.cpp @@ -1,23 +1,67 @@ -// File: host/src/main.cpp +// host/src/main.cpp #include +#include #include "MainWindow.h" int main(int argc, char *argv[]) { + // High DPI Scaling + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + QApplication app(argc, argv); QApplication::setApplicationName("EIS Configurator"); QApplication::setApplicationVersion("1.0"); - // Dark Theme - QPalette p = app.palette(); - p.setColor(QPalette::Window, QColor(30, 30, 30)); + // --- Apply Dark Fusion Theme --- + app.setStyle(QStyleFactory::create("Fusion")); + + QPalette p; + p.setColor(QPalette::Window, QColor(53, 53, 53)); p.setColor(QPalette::WindowText, Qt::white); - p.setColor(QPalette::Base, QColor(15, 15, 15)); - p.setColor(QPalette::AlternateBase, QColor(45, 45, 45)); + p.setColor(QPalette::Base, QColor(25, 25, 25)); + p.setColor(QPalette::AlternateBase, QColor(53, 53, 53)); + p.setColor(QPalette::ToolTipBase, Qt::white); + p.setColor(QPalette::ToolTipText, Qt::white); p.setColor(QPalette::Text, Qt::white); - p.setColor(QPalette::Button, QColor(45, 45, 45)); + p.setColor(QPalette::Button, QColor(53, 53, 53)); p.setColor(QPalette::ButtonText, Qt::white); + p.setColor(QPalette::BrightText, Qt::red); + p.setColor(QPalette::Link, QColor(42, 130, 218)); + p.setColor(QPalette::Highlight, QColor(42, 130, 218)); + p.setColor(QPalette::HighlightedText, Qt::black); app.setPalette(p); + // --- Global Stylesheet for Modern Look --- + app.setStyleSheet( + "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }" + "QGroupBox { border: 1px solid #555; border-radius: 5px; margin-top: 10px; font-weight: bold; }" + "QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 3px; }" + "QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit { " + " background: #333; color: #FFF; border: 1px solid #555; padding: 4px; border-radius: 4px; " + " selection-background-color: #2A82DA; " + "}" + "QSpinBox::up-button, QDoubleSpinBox::up-button, QSpinBox::down-button, QDoubleSpinBox::down-button { " + " background: #444; width: 16px; border-radius: 2px;" + "}" + "QPushButton { " + " background-color: #444; border: 1px solid #555; border-radius: 4px; padding: 5px 15px; color: white; font-weight: bold;" + "}" + "QPushButton:hover { background-color: #555; }" + "QPushButton:pressed { background-color: #2A82DA; }" + "QPushButton:disabled { background-color: #333; color: #777; border: 1px solid #444; }" + "QTabWidget::pane { border: 1px solid #444; }" + "QTabBar::tab { background: #333; color: #AAA; padding: 8px 20px; border-top-left-radius: 4px; border-top-right-radius: 4px; }" + "QTabBar::tab:selected { background: #535353; color: #FFF; border-bottom: 2px solid #2A82DA; }" + "QScrollBar:vertical { background: #333; width: 12px; }" + "QScrollBar::handle:vertical { background: #555; min-height: 20px; border-radius: 6px; }" + "QLabel { color: #EEE; }" + ); + + // Set default font size + QFont font = app.font(); + font.setPointSize(12); + app.setFont(font); + MainWindow w; w.show(); diff --git a/main.c b/main.c index a70b824..318b28b 100644 --- a/main.c +++ b/main.c @@ -9,6 +9,15 @@ #include "hardware/watchdog.h" #include "ad5940.h" #include "Impedance.h" +#include "Amperometric.h" +#include "RampTest.h" + +#define AD5940ERR_STOP 10 + +// Fix for missing definition in some SDK versions +#ifndef LPTIARF_BYPASS +#define LPTIARF_BYPASS 0x2000 +#endif // --------------------------------------------------------------------------- // Hardware Definitions @@ -23,6 +32,72 @@ #define APPBUFF_SIZE 512 uint32_t AppBuff[APPBUFF_SIZE]; +// Application State +typedef enum { + MODE_IDLE, + MODE_IMPEDANCE, + MODE_AMPEROMETRIC, + MODE_RAMP +} AppMode; + +AppMode CurrentMode = MODE_IDLE; +float LFOSCFreq = 32000.0; // Default, updated by calibration + +// --------------------------------------------------------------------------- +// Range / RTIA Management +// --------------------------------------------------------------------------- + +// Nominal values for the 8 supported ranges +float RtiaCalibrationTable[8] = {200.0, 1000.0, 5000.0, 10000.0, 20000.0, 40000.0, 80000.0, 160000.0}; +uint32_t CurrentRtiaIndex = 0; // Default to 200 Ohm (Index 0) +uint32_t ConfigRtiaVal = 200; // Default Value +uint32_t CurrentLpTiaRf = LPTIARF_20K; // Default LPF + +// Map integer value to HSTIA Enum (Impedance) +uint32_t GetHSTIARtia(uint32_t val) { + switch(val) { + case 200: return HSTIARTIA_200; + case 1000: return HSTIARTIA_1K; + case 5000: return HSTIARTIA_5K; + case 10000: return HSTIARTIA_10K; + case 20000: return HSTIARTIA_20K; + case 40000: return HSTIARTIA_40K; + case 80000: return HSTIARTIA_80K; + case 160000: return HSTIARTIA_160K; + default: return HSTIARTIA_200; + } +} + +// Map integer value to LPTIA Enum (Amperometric/Ramp) +uint32_t GetLPTIARtia(uint32_t val) { + switch(val) { + case 200: return LPTIARTIA_200R; + case 1000: return LPTIARTIA_1K; + case 5000: return LPTIARTIA_4K; + case 10000: return LPTIARTIA_10K; + case 20000: return LPTIARTIA_20K; + case 40000: return LPTIARTIA_40K; + case 80000: return LPTIARTIA_85K; + case 160000: return LPTIARTIA_160K; + default: return LPTIARTIA_1K; + } +} + +// Helper to find index for calibration table +int GetRtiaIndex(uint32_t val) { + switch(val) { + case 200: return 0; + case 1000: return 1; + case 5000: return 2; + case 10000: return 3; + case 20000: return 4; + case 40000: return 5; + case 80000: return 6; + case 160000: return 7; + default: return 0; + } +} + // --------------------------------------------------------------------------- // Platform Interface Implementation // --------------------------------------------------------------------------- @@ -77,41 +152,80 @@ void setup_pins(void) { void AD5940ImpedanceStructInit(void) { AppIMPCfg_Type *pImpedanceCfg; - AppIMPGetCfg(&pImpedanceCfg); pImpedanceCfg->SeqStartAddr = 0; pImpedanceCfg->MaxSeqLen = 512; - pImpedanceCfg->RcalVal = 100.0; - pImpedanceCfg->RtiaVal = 200.0; + pImpedanceCfg->RtiaVal = RtiaCalibrationTable[CurrentRtiaIndex]; pImpedanceCfg->SinFreq = 1000.0; pImpedanceCfg->FifoThresh = 6; - pImpedanceCfg->DacVoltPP = 600.0; pImpedanceCfg->ExcitBufGain = EXCITBUFGAIN_0P25; pImpedanceCfg->HsDacGain = HSDACGAIN_0P2; - pImpedanceCfg->DswitchSel = SWD_CE0; pImpedanceCfg->PswitchSel = SWP_CE0; pImpedanceCfg->NswitchSel = SWN_SE0; pImpedanceCfg->TswitchSel = SWT_SE0LOAD; - - pImpedanceCfg->HstiaRtiaSel = HSTIARTIA_200; + pImpedanceCfg->HstiaRtiaSel = GetHSTIARtia(ConfigRtiaVal); pImpedanceCfg->BiasVolt = 0.0; - pImpedanceCfg->SweepCfg.SweepEn = bFALSE; pImpedanceCfg->SweepCfg.SweepStart = 100.0f; pImpedanceCfg->SweepCfg.SweepStop = 100000.0f; pImpedanceCfg->SweepCfg.SweepPoints = 50; pImpedanceCfg->SweepCfg.SweepLog = bTRUE; - pImpedanceCfg->PwrMod = AFEPWR_LP; pImpedanceCfg->ADCSinc3Osr = ADCSINC3OSR_4; pImpedanceCfg->DftNum = DFTNUM_16384; pImpedanceCfg->DftSrc = DFTSRC_SINC3; } +void AD5940AMPStructInit(void) +{ + AppAMPCfg_Type *pAMPCfg; + AppAMPGetCfg(&pAMPCfg); + pAMPCfg->WuptClkFreq = LFOSCFreq; + pAMPCfg->SeqStartAddr = 0; + pAMPCfg->MaxSeqLen = 512; + pAMPCfg->RcalVal = 100.0; + pAMPCfg->NumOfData = -1; + pAMPCfg->AmpODR = 1.0; + pAMPCfg->FifoThresh = 4; + pAMPCfg->SensorBias = 0; + pAMPCfg->LptiaRtiaSel = GetLPTIARtia(ConfigRtiaVal); + pAMPCfg->LpTiaRl = LPTIARLOAD_10R; + pAMPCfg->LpTiaRf = CurrentLpTiaRf; // Use configured LPF + pAMPCfg->Vzero = 1100; + pAMPCfg->ADCRefVolt = 1.82; +} + +void AD5940RampStructInit(void) +{ + AppRAMPCfg_Type *pRampCfg; + AppRAMPGetCfg(&pRampCfg); + + pRampCfg->SeqStartAddr = 0; + pRampCfg->MaxSeqLen = 1024; + pRampCfg->RcalVal = 100.0; + pRampCfg->ADCRefVolt = 1820.0f; + pRampCfg->FifoThresh = 4; + pRampCfg->SysClkFreq = 16000000.0f; + pRampCfg->LFOSCClkFreq = LFOSCFreq; + + pRampCfg->RampStartVolt = -500.0f; + pRampCfg->RampPeakVolt = +500.0f; + pRampCfg->VzeroStart = 1100.0f; + pRampCfg->VzeroPeak = 1100.0f; + pRampCfg->StepNumber = 100; + pRampCfg->RampDuration = 10000; + pRampCfg->SampleDelay = 1.0f; + + pRampCfg->LPTIARtiaSel = GetLPTIARtia(ConfigRtiaVal); + pRampCfg->LPTIARloadSel = LPTIARLOAD_10R; + pRampCfg->LpTiaRf = CurrentLpTiaRf; // Use configured LPF + pRampCfg->AdcPgaGain = ADCPGA_1P5; +} + static int32_t AD5940PlatformCfg(void) { CLKCfg_Type clk_cfg; @@ -121,7 +235,6 @@ static int32_t AD5940PlatformCfg(void) AD5940_HWReset(); AD5940_Initialize(); - // Use HFOSC (16MHz) for stability across all frequencies clk_cfg.ADCClkDiv = ADCCLKDIV_1; clk_cfg.ADCCLkSrc = ADCCLKSRC_HFOSC; clk_cfg.SysClkDiv = SYSCLKDIV_1; @@ -144,7 +257,7 @@ static int32_t AD5940PlatformCfg(void) AD5940_INTCCfg(AFEINTC_1, AFEINTSRC_ALLINT, bTRUE); AD5940_INTCClrFlag(AFEINTSRC_ALLINT); - AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_DATAFIFOTHRESH, bTRUE); + AD5940_INTCCfg(AFEINTC_0, AFEINTSRC_DATAFIFOTHRESH|AFEINTSRC_CUSTOMINT0, bTRUE); AD5940_INTCClrFlag(AFEINTSRC_ALLINT); gpio_cfg.FuncSet = GP0_INT; @@ -168,17 +281,28 @@ void ImpedanceShowResult(uint32_t *pData, uint32_t DataCount) { float mag = pImp[i].Magnitude; float phase = pImp[i].Phase; - float real = mag * cosf(phase); float imag = mag * sinf(phase); - printf("DATA,%.2f,%.4f,%.4f,%.4f,%.4f\n", - freq, - mag, - phase * 180.0f / MATH_PI, - real, - imag - ); + printf("DATA,%.2f,%.4f,%.4f,%.4f,%.4f\n", freq, mag, phase * 180.0f / MATH_PI, real, imag); + } +} + +void AmperometricShowResult(float *pData, uint32_t DataCount) +{ + static int index = 0; + for(int i=0;i> Calibrating LFOSC...\n"); - AppIMPCleanup(); - AppIMPCalibrateLFO(); - printf(">> LFOSC Calibrated.\n"); + if (CurrentMode == MODE_IMPEDANCE) AppIMPCleanup(); + else if (CurrentMode == MODE_AMPEROMETRIC) AppAMPCtrl(AMPCTRL_SHUTDOWN, 0); + else if (CurrentMode == MODE_RAMP) AppRAMPCtrl(APPCTRL_SHUTDOWN, 0); + + LFOSCMeasure_Type cal_cfg; + cal_cfg.CalDuration = 1000.0; + cal_cfg.CalSeqAddr = 0; + cal_cfg.SystemClkFreq = 16000000.0; + + if(AD5940_LFOSCMeasure(&cal_cfg, &LFOSCFreq) == AD5940ERR_OK) { + printf(">> LFOSC Calibrated: %.2f Hz\n", LFOSCFreq); + } else { + printf(">> LFOSC Calibration Failed.\n"); + } } void Routine_Measure(float freq) { + if (CurrentMode == MODE_AMPEROMETRIC) AppAMPCtrl(AMPCTRL_SHUTDOWN, 0); + else if (CurrentMode == MODE_RAMP) AppRAMPCtrl(APPCTRL_SHUTDOWN, 0); + CurrentMode = MODE_IMPEDANCE; + AppIMPCfg_Type *pCfg; AppIMPGetCfg(&pCfg); - AppIMPCleanup(); - AppIMPCalibrateLFO(); - + pCfg->WuptClkFreq = LFOSCFreq; + pCfg->HstiaRtiaSel = GetHSTIARtia(ConfigRtiaVal); + pCfg->RtiaVal = RtiaCalibrationTable[CurrentRtiaIndex]; pCfg->SweepCfg.SweepEn = bFALSE; pCfg->SinFreq = freq; pCfg->NumOfData = -1; - pCfg->RealDataCount = -1; // Disable filtering + pCfg->RealDataCount = -1; pCfg->bParaChanged = bTRUE; if(AppIMPInit(AppBuff, APPBUFF_SIZE) == AD5940ERR_OK) { @@ -214,23 +353,22 @@ void Routine_Measure(float freq) { } void Routine_Sweep(float start, float end, int steps) { + if (CurrentMode == MODE_AMPEROMETRIC) AppAMPCtrl(AMPCTRL_SHUTDOWN, 0); + else if (CurrentMode == MODE_RAMP) AppRAMPCtrl(APPCTRL_SHUTDOWN, 0); + CurrentMode = MODE_IMPEDANCE; + AppIMPCfg_Type *pCfg; AppIMPGetCfg(&pCfg); - AppIMPCleanup(); - AppIMPCalibrateLFO(); - + pCfg->WuptClkFreq = LFOSCFreq; + pCfg->HstiaRtiaSel = GetHSTIARtia(ConfigRtiaVal); + pCfg->RtiaVal = RtiaCalibrationTable[CurrentRtiaIndex]; pCfg->SweepCfg.SweepEn = bTRUE; pCfg->SweepCfg.SweepStart = start; pCfg->SweepCfg.SweepStop = end; - - // DUMMY POINT STRATEGY: - // Request steps + 1 from the engine, but tell the ISR to only report 'steps'. - // The artifact will happen on the +1 point, which is discarded. pCfg->SweepCfg.SweepPoints = steps + 1; pCfg->NumOfData = steps + 1; - pCfg->RealDataCount = steps; // Stop reporting after this count - + pCfg->RealDataCount = steps; pCfg->SweepCfg.SweepLog = bTRUE; pCfg->bParaChanged = bTRUE; @@ -241,13 +379,69 @@ void Routine_Sweep(float start, float end, int steps) { } } +void Routine_Amperometric(float bias_mv) { + if (CurrentMode == MODE_IMPEDANCE) AppIMPCleanup(); + else if (CurrentMode == MODE_RAMP) AppRAMPCtrl(APPCTRL_SHUTDOWN, 0); + CurrentMode = MODE_AMPEROMETRIC; + + printf(">> Starting Amperometry (Bias: %.1f mV, Range: %d)...\n", bias_mv, ConfigRtiaVal); + + AppAMPCfg_Type *pCfg; + AppAMPGetCfg(&pCfg); + AD5940AMPStructInit(); + pCfg->SensorBias = bias_mv; + pCfg->WuptClkFreq = LFOSCFreq; + pCfg->LptiaRtiaSel = GetLPTIARtia(ConfigRtiaVal); + pCfg->ReDoRtiaCal = bTRUE; + + if(AppAMPInit(AppBuff, APPBUFF_SIZE) == AD5940ERR_OK) { + AppAMPCtrl(AMPCTRL_START, 0); + } else { + printf("ERROR: AMP Init Failed\n"); + } +} + +void Routine_LSV(float start_mv, float end_mv, int steps, int duration_ms) { + if (CurrentMode == MODE_IMPEDANCE) AppIMPCleanup(); + else if (CurrentMode == MODE_AMPEROMETRIC) AppAMPCtrl(AMPCTRL_SHUTDOWN, 0); + CurrentMode = MODE_RAMP; + + printf(">> Starting LSV (%.1f to %.1f mV, %d steps, %d ms)...\n", start_mv, end_mv, steps, duration_ms); + + AppRAMPCfg_Type *pCfg; + AppRAMPGetCfg(&pCfg); + AD5940RampStructInit(); + + pCfg->RampStartVolt = start_mv; + pCfg->RampPeakVolt = end_mv; + pCfg->StepNumber = steps; + pCfg->RampDuration = duration_ms; + pCfg->LFOSCClkFreq = LFOSCFreq; + pCfg->LPTIARtiaSel = GetLPTIARtia(ConfigRtiaVal); + + // Use global calibration value + pCfg->RtiaValue.Magnitude = RtiaCalibrationTable[CurrentRtiaIndex]; + pCfg->RtiaValue.Phase = 0; + + pCfg->bRampOneDir = bTRUE; // Linear Sweep (not Cyclic) + pCfg->bParaChanged = bTRUE; + + if(AppRAMPInit(AppBuff, APPBUFF_SIZE) == AD5940ERR_OK) { + AppRAMPCtrl(APPCTRL_START, 0); + } else { + printf("ERROR: RAMP Init Failed\n"); + } +} + void Routine_CalibrateSystem(void) { + if (CurrentMode == MODE_AMPEROMETRIC) AppAMPCtrl(AMPCTRL_SHUTDOWN, 0); + else if (CurrentMode == MODE_RAMP) AppRAMPCtrl(APPCTRL_SHUTDOWN, 0); + CurrentMode = MODE_IMPEDANCE; + AppIMPCfg_Type *pCfg; AppIMPGetCfg(&pCfg); - AppIMPCleanup(); - // 1. ADC Calibration ADCPGACal_Type adcpga_cal; adcpga_cal.AdcClkFreq = 16000000.0; adcpga_cal.SysClkFreq = 16000000.0; @@ -261,18 +455,15 @@ void Routine_CalibrateSystem(void) { printf(">> Calibrating ADC Offset...\n"); AD5940_ADCPGACal(&adcpga_cal); - // 2. HSDAC Configuration HSDACCfg_Type hsdac_cfg; hsdac_cfg.ExcitBufGain = EXCITBUFGAIN_0P25; hsdac_cfg.HsDacGain = HSDACGAIN_0P2; hsdac_cfg.HsDacUpdateRate = 7; AD5940_HSDacCfgS(&hsdac_cfg); - // 3. RTIA Calibration HSRTIACal_Type hsrtia_cal; fImpPol_Type Res; memset(&hsrtia_cal, 0, sizeof(hsrtia_cal)); - hsrtia_cal.fFreq = 1000.0f; hsrtia_cal.AdcClkFreq = 16000000.0; hsrtia_cal.SysClkFreq = 16000000.0; @@ -283,15 +474,16 @@ void Routine_CalibrateSystem(void) { hsrtia_cal.HsTiaCfg.DiodeClose = bFALSE; hsrtia_cal.HsTiaCfg.HstiaBias = HSTIABIAS_1P1; hsrtia_cal.HsTiaCfg.HstiaCtia = 31; - hsrtia_cal.HsTiaCfg.HstiaRtiaSel = HSTIARTIA_200; + hsrtia_cal.HsTiaCfg.HstiaRtiaSel = GetHSTIARtia(ConfigRtiaVal); hsrtia_cal.HsTiaCfg.HstiaDeRtia = HSTIADERTIA_OPEN; hsrtia_cal.HsTiaCfg.HstiaDeRload = HSTIADERLOAD_OPEN; hsrtia_cal.DftCfg.DftNum = DFTNUM_16384; hsrtia_cal.DftCfg.DftSrc = DFTSRC_SINC3; - printf(">> Calibrating HSTIARTIA_200...\n"); + printf(">> Calibrating RTIA %d Ohm...\n", ConfigRtiaVal); if (AD5940_HSRtiaCal(&hsrtia_cal, &Res) == AD5940ERR_OK) { printf("Calibrated Rtia: Mag = %f Ohm, Phase = %f\n", Res.Magnitude, Res.Phase); + RtiaCalibrationTable[CurrentRtiaIndex] = Res.Magnitude; pCfg->RtiaVal = Res.Magnitude; } else { printf("Calibration Failed\n"); @@ -307,14 +499,38 @@ int input_pos = 0; void process_command() { char cmd = input_buffer[0]; - sleep_ms(10); if (cmd == 'v') { uint32_t id = AD5940_ReadReg(REG_AFECON_CHIPID); printf("CHIP_ID:0x%04X\n", id); } + else if (cmd == 'r') { + if (strlen(input_buffer) > 2) { + uint32_t val = atoi(input_buffer + 2); + ConfigRtiaVal = val; + CurrentRtiaIndex = GetRtiaIndex(val); + printf("RANGE_SET:%d\n", ConfigRtiaVal); + } + } + else if (cmd == 'f') { + if (strlen(input_buffer) > 2) { + int idx = atoi(input_buffer + 2); + switch(idx) { + case 0: CurrentLpTiaRf = LPTIARF_BYPASS; break; + case 1: CurrentLpTiaRf = LPTIARF_20K; break; + case 2: CurrentLpTiaRf = LPTIARF_100K; break; + case 3: CurrentLpTiaRf = LPTIARF_200K; break; + case 4: CurrentLpTiaRf = LPTIARF_400K; break; + case 5: CurrentLpTiaRf = LPTIARF_600K; break; + case 6: CurrentLpTiaRf = LPTIARF_1M; break; + default: CurrentLpTiaRf = LPTIARF_20K; break; + } + printf("LPF_SET:%d\n", idx); + } + } else if (cmd == 'c') { + Routine_CalibrateLFO(); Routine_CalibrateSystem(); } else if (cmd == 'm') { @@ -328,8 +544,23 @@ void process_command() { if (strlen(input_buffer) > 2) sscanf(input_buffer + 2, "%f %f %d", &start, &end, &steps); Routine_Sweep(start, end, steps); } + else if (cmd == 'a') { + float bias = 0.0f; + if (strlen(input_buffer) > 2) bias = atof(input_buffer + 2); + Routine_Amperometric(bias); + } + else if (cmd == 'l') { + // l + float start = -500.0f, end = 500.0f; + int steps = 100, duration = 10000; + if (strlen(input_buffer) > 2) sscanf(input_buffer + 2, "%f %f %d %d", &start, &end, &steps, &duration); + Routine_LSV(start, end, steps, duration); + } else if (cmd == 'x') { - AppIMPCleanup(); + if (CurrentMode == MODE_IMPEDANCE) AppIMPCleanup(); + else if (CurrentMode == MODE_AMPEROMETRIC) AppAMPCtrl(AMPCTRL_SHUTDOWN, 0); + else if (CurrentMode == MODE_RAMP) AppRAMPCtrl(APPCTRL_SHUTDOWN, 0); + CurrentMode = MODE_IDLE; printf("STOPPED\n"); } else if (cmd == 'z') { @@ -344,6 +575,10 @@ int main() { setup_pins(); AD5940PlatformCfg(); AD5940ImpedanceStructInit(); + AD5940AMPStructInit(); + AD5940RampStructInit(); + + Routine_CalibrateLFO(); printf("SYSTEM_READY\n"); @@ -361,13 +596,45 @@ int main() { if (gpio_get(PIN_INT) == 0) { uint32_t temp = APPBUFF_SIZE; - int32_t status = AppIMPISR(AppBuff, &temp); + int32_t status = 0; - if (status == AD5940ERR_FIFO) { - printf("ERROR: FIFO Overflow/Underflow. Stopping.\n"); - AppIMPCleanup(); - } else if(temp > 0) { - ImpedanceShowResult(AppBuff, temp); + if (CurrentMode == MODE_IMPEDANCE) { + status = AppIMPISR(AppBuff, &temp); + if (status == AD5940ERR_FIFO) { + printf("ERROR: FIFO Overflow/Underflow. Stopping.\n"); + AppIMPCleanup(); + CurrentMode = MODE_IDLE; + } else if(temp > 0) { + ImpedanceShowResult(AppBuff, temp); + } + } + else if (CurrentMode == MODE_AMPEROMETRIC) { + status = AppAMPISR(AppBuff, &temp); + if (status == AD5940ERR_FIFO) { + printf("ERROR: FIFO Overflow/Underflow. Stopping.\n"); + AppAMPCtrl(AMPCTRL_SHUTDOWN, 0); + CurrentMode = MODE_IDLE; + } else if(temp > 0) { + AmperometricShowResult((float*)AppBuff, temp); + } + } + else if (CurrentMode == MODE_RAMP) { + status = AppRAMPISR(AppBuff, &temp); + if (status == AD5940ERR_FIFO) { + printf("ERROR: FIFO Overflow/Underflow. Stopping.\n"); + AppRAMPCtrl(APPCTRL_SHUTDOWN, 0); + CurrentMode = MODE_IDLE; + } else if(temp > 0) { + RampShowResult((float*)AppBuff, temp); + } + } + + if (status == AD5940ERR_STOP) { + printf("STOPPED\n"); + if (CurrentMode == MODE_IMPEDANCE) AppIMPCleanup(); + else if (CurrentMode == MODE_AMPEROMETRIC) AppAMPCtrl(AMPCTRL_SHUTDOWN, 0); + else if (CurrentMode == MODE_RAMP) AppRAMPCtrl(APPCTRL_SHUTDOWN, 0); + CurrentMode = MODE_IDLE; } } }