/**************************************
Project:     OpAmp Tester
File:        TestOpAmp
Version:     3.0 - May 2026
Written by:  diy-electronic.ru
***************************************
Version history
  1.0  Mar 2025 Initial version
  2.0  May 2025 Minor changes and bug
                fixes
  2.1  Dec 2025 Voltage variation
                calculation and display
  3.0  May 2026 New timeline. Vos thermal
                drift measurement 
***************************************/
uint16_t progressBarPos; // Progress bar position

// Voltmeter full scale (in ADC steps) for low resolution mode (+/-15V)
uint16_t FullScaleLow;
// the same for high resolution mode (less than +/-1.85V)
uint16_t FullScaleHigh;

#ifdef __DEBUG__
  int Start;  // Test start time in milliseconds
#endif


//------------ Global functions ---------------

/**********************************************/
/*                                            */
/*   OpAmp parameters measurement procedure   */
/*                                            */
/**********************************************/
void testOpAmp() {
  // Local variables
  float V02pre, V02a, V02b, V02c, V02d, V02e, V02f; // Voltage measurement results
  float V02begin;    // V02a value on test start for voltage drift calculation

#ifdef __DEBUG__
  Start = millis();  // Store start time
#endif

  // Reset progress bar position
  progressBarPos = 0;

  // Set all flags to true
  IbValid   = true;
  IosValid  = true;
  AolValid  = true;
  CMRRValid = true;
  PSRRValid = true;

  // Calculate voltmeter full scale
  FullScaleLow = (uint16_t)(30.0 * ActiveSettings.voltageLowGain + ActiveSettings.voltageLowZero);
  // the same for high resolution mode
  FullScaleHigh = (uint16_t)(3.75 * ActiveSettings.voltageHighGain + ActiveSettings.voltageHighZero);

#ifdef __DEBUG__
  Serial.println("\n======= TEST START =======");
  timestamp();
  Serial.println("Test started.");
#endif

  showTestInProgressPage();
  // Turn on power supply for OpAmp under test
  opAmpSupplyVoltage(12.0, 12.0, 2.5, 2.5);
  delay(400);    // Initial warming

  //++++++++++++++++++++++++++++++++++++++++++++++++++//
  // 1. Preliminary Input Offset Voltage measurement. //
  //++++++++++++++++++++++++++++++++++++++++++++++++++//
  // Keep initial V02a voltage for the calculation 
  // of Vos temperature drift
  V02begin = measureVoltage("V02a");

  //+++++++++++++++++++++++++++++++++//
  // 2. Measure quescent current, Iq //
  //+++++++++++++++++++++++++++++++++//
  uint16_t ADCData = 0, FullScale;
  switch (Voltage) {
    case voltageLow:
      // Full scale is 10mA
      FullScale = (uint16_t)(10 * ActiveSettings.currentLowGain + ActiveSettings.currentLowZero);
      // Read result from ADC channel 2 with 50 iterations
      ADCData = getAdcCodeSingleEnded(2, 30, FullScale);
      Iq = (ADCData - ActiveSettings.currentLowZero) / ActiveSettings.currentLowGain;
      break;
    case voltageStandard:
      FullScale = (uint16_t)(10 * ActiveSettings.currentStdGain + ActiveSettings.currentStdZero);
      ADCData = getAdcCodeSingleEnded(2, 30, FullScale);
      Iq = (ADCData - ActiveSettings.currentStdZero) / ActiveSettings.currentStdGain;
      break;
  }
  
#ifdef __DEBUG__
  timestamp();
  Serial.printf("ADC2: [%d]  Iq=", ADCData);
  Serial.print(Iq, 4);
  Serial.print("mA var=");
  Serial.print(variation, 2);
  Serial.println("%");
#endif

  // If Iq is too small (less than 5uA) - raise an error and stop further measurement
  if (Iq < 0.005) {
    handleError(1);
    return;
  }

  // If Iq is too big (more than 12mA) - raise fatal error
  if (Iq > 12.0) {
    handleError(2);
    return; // Stop further measurement
  }

  incProgressBar();
  pause(1500);
  pause(1500);

  //++++++++++++++++++++++++++++++++++++++//
  // 3. Input Offset Voltage measurement. //
  //++++++++++++++++++++++++++++++++++++++//
  V02pre =  measureVoltage("V02a");

  // If measured voltage is out of the measurement range
  if (abs(V02pre) > 10.0) {
    handleError(4);   // Raise fatal error and
    return;           // stop further measurement
  }
  
  //+++++++++++++++++++++++++++++++++++++++++++++++//
  // 4. Input Bias Current at Inverting input, Ib- //
  //+++++++++++++++++++++++++++++++++++++++++++++++//
  setRelays(LOW, HIGH, LOW);
  pause(700);
  V02b = measureVoltage("V02b");
 
  setRelays(LOW, LOW, LOW);   // Reset relays
  pause(700);
  V02a = measureVoltage("V02a");  
  
  //----- IMPORTANT! ------
  // Here and in all similar calculations below we use arithmetic mean of the previous and 
  // current V02a values as V02a. This is related with themperature drift of Vos.
  Ibminus = (V02b-(V02pre + V02a) / 2) * 10; // Ibminus in nA is equal to (V02b-V02a) multiplied
                                             // by 10 because we divide it by R1 (100000 Ohm)
                                             // first then divide by feedback factor G (1000) and 
                                             // finally multiply by 1000000000 to get result in nA 
  V02pre = V02a;
#ifdef __DEBUG__
  Serial.print("  Ib- = ");
  Serial.print(Ibminus, 1);
  Serial.println("nA");
#endif

  // Check if measured voltage is within the measurement range
  if (abs(V02b) > 12.0) {
    if(handleOutOfRange("Ib-")) {  // Skip button is pressed - continue
      IbValid = false;             // but mark Ib and Ios as invalid
      IosValid = false;
    }
    else {
      return;  // Stop button is pressed - cancel further measurement
    }
  }
//+++++++++++++++++++++++++++++++++++++++++++++++++++++//
  // 5. Input Bias Current at Non-inverting input, Ib+ //
  //+++++++++++++++++++++++++++++++++++++++++++++++++++//
  setRelays(LOW, LOW, HIGH);
  pause(700);
  V02c = measureVoltage("V02c");
  setRelays(LOW, LOW, LOW);   // Reset relays
  pause(700);
  V02a = measureVoltage("V02a");  
  Ibplus = ((V02pre + V02a) / 2 - V02c) * 10; // Ibplus in nA is equal to (V02a-V02c) multiplied
                                              // by 10 because we divide it by R3 (100000 Ohm) 
                                              // first then divide by feedback factor G (1000) and
                                              //  finally multiply by 1000000000 to get result in nA 
  V02pre = V02a;
  // Calculate Input Bias Current and Input Offset Current
  Ib = (Ibminus + Ibplus) / 2;
  Ios = Ibplus - Ibminus;

#ifdef __DEBUG__
  Serial.print("  Ib+ = ");
  Serial.print(Ibplus, 1);
  Serial.println("nA");
  
  Serial.print("  Ib  = ");
  Serial.print(Ib, 1);
  Serial.println("nA");
  
  Serial.print("  Ios = ");
  Serial.print(Ios, 1);
  Serial.println("nA");
#endif

  // Check if measured voltage is within the measurement range
  if (abs(V02c) > 12.0) {
    if(handleOutOfRange("Ib+")) {  // Skip button is pressed - continue
      IbValid = false;             // but mark Ib and Ios as invalid
      IosValid = false;
    }
    else {
      return;  // Stop button is pressed - cancel further measurement
    }
  }
  //++++++++++++++++++++++++//
  // 6. Open-Loop Gain, Aol //
  //++++++++++++++++++++++++//
  setRelays(HIGH, LOW, LOW);
  pause(700);
  V02d = measureVoltage("V02d");
  setRelays(LOW, LOW, LOW);   // Reset relays
  pause(700);
  V02a = measureVoltage("V02a");  
  float Vref = (Voltage == voltageLow) ? VrefLow : VrefStd;

  Aol = Vref * 1000 / ((V02pre + V02a) / 2 - V02d + 0.032); // Aol is equal to Vref multiplied by
                                                            // G (1000) and divided by (V02a - V02d)
                                                            // =====  0.032V - MAGIC CORRECTION =====
  Aol = ((int)(Aol / 100 + 0.5)) * 100;   // Round Aol for better readability
  V02pre = V02a;
#ifdef __DEBUG__
  Serial.print("  Aol = ");
  Serial.println(Aol, 0);
#endif

  // Check if measured voltage is within the measurement range and
  // calculated Aol value is adequate
  uint32_t LowLimit;                                // Aol measurement range lower limit 
  LowLimit = (Voltage == voltageLow) ? 200 : 600 ;  // differs for low votage OpAmps
  if ((abs(V02d) > 12.0) || (Aol > 1500000) || (Aol < LowLimit)) {
    if(handleOutOfRange("Aol")) {  // Skip button is pressed - continue but mark Aol as 
      AolValid = false;            // invalid and assign zero value to Aol if it is too big
      Aol = (abs(Aol) < 9999999) ? Aol : 0; 
    }
    else {
      return;  // Stop button is pressed - cancel further measurement
    }
  }
  
  //++++++++++++++++++++++++++++++++++++++//
  // 7. Common mode rejection ratio, CMRR //
  //++++++++++++++++++++++++++++++++++++++//
  opAmpSupplyVoltage(13.0, 11.0, 3.5, 1.5); // Alter both supply voltages in the same direction by 1V
  pause(700);
  V02e = measureVoltage("V02e");
  opAmpSupplyVoltage(12.0, 12.0, 2.5, 2.5);  // Return supply voltages to normal values
  pause(700);
  V02a = measureVoltage("V02a");  
  CMRR = 1000 / abs((V02pre + V02a) / 2 - V02e); // CMRR is equal to power supply voltage shift (+1V)
                                                 // multiplied by G (1000) and divided by (V02a - V02e)
  CMRR = ((int)(20 * log10(CMRR)));  // Convert to dB 
  V02pre = V02a;
#ifdef __DEBUG__
  Serial.print(" CMRR = ");
  Serial.print(CMRR, 0);
  Serial.println("dB");
#endif

  // Check if measured voltage is within the measurement range and
  // calculated CMRR value is adequate
  if ((abs(V02e) > 12.0) || (CMRR > 150) || (CMRR < 40)) {
    if(handleOutOfRange("CMRR")) {          // Skip button is pressed - continue
      CMRRValid = false;                    // but mark CMRR as invalid and assign
      CMRR = (abs(CMRR) < 999) ? CMRR : 0;  // zero value to CMRR if it is too big
    }
    else {
      return;  // Stop button is pressed - cancel further measurement
    }
  }
  //+++++++++++++++++++++++++++++++++++++++//
  // 8. Power supply rejection ratio, PSRR //
  //+++++++++++++++++++++++++++++++++++++++//
  opAmpSupplyVoltage(13.0, 13.0, 3.0, 3.0); // Alter both supply voltages in opposite directions 
  pause(700);
  V02f = measureVoltage("V02f");
  opAmpSupplyVoltage(12.0, 12.0, 2.5, 2.5); // Return supply voltages to normal values
  pause(700);
  V02a = measureVoltage("V02a");  
  int deltaV = (Voltage == voltageStandard) ? 2 : 1;
  PSRR = 1000 * deltaV / abs(V02f - (V02pre + V02a) / 2);  // PSRR is equal to power supply voltage variation
                                                           // multiplied by G (1000) and divided by (V02f - V02a)
  PSRR = ((int)(20 * log10(PSRR)));  // Convert to dB
#ifdef __DEBUG__
  Serial.print(" PSRR = ");
  Serial.print(PSRR, 0);
  Serial.println("dB");
#endif
  
  // Check if measured voltage is within the measurement range and
  // calculated PSRR value is adequate
  if ((abs(V02f) > 12.0) || (PSRR > 150) || (PSRR < 40)) {
    if(handleOutOfRange("PSRR")) {          // Skip button is pressed - continue
      PSRRValid = false;                    // but mark PSRR as invalid and assign
      PSRR =  (abs(PSRR) < 999) ? PSRR : 0; // zero value to PSRR if it is too big
    }
    else {
      return;  // Stop button is pressed - cancel further measurement
    }
  }
  //+++++++++++++++++++++++++++++++//
  // 9. Keep last V02a measurement //
  // as final Vos value            //
  //+++++++++++++++++++++++++++++++//
  Vos = -V02a;          // Vos in millivolts is numerically equal to V02a in volts
                        // because feedback factor G = (R16+R13)/R13 = 1000
#ifdef __DEBUG__
  Serial.print("  Vos = ");
  Serial.print(Vos, 4);
  Serial.println("mV");
#endif

  // Turn power off
  opAmpSupplyVoltage(0, 0, 0, 0);

  //+++++++++++++++++++++++++++++++++++++++++++++++++++++//
  // 10. Offset Voltage drift calculation - experimental //
  //  feature included into release since version 3.0    //
  //+++++++++++++++++++++++++++++++++++++++++++++++++++++//
  float power = Iq * ((Voltage == voltageStandard) ? 24 : 5) / 1000; // in Watts for Standard voltage 
                                                                     // +/-12V or Low voltage +/-2.5V
  float dT = 150 * power * 0.6;             // DIP-8 thermal resistance 150 °C/Wt, 60% of final temperature
  float dVos = abs(V02a - V02begin) * 1000; // Drift: last measured Vos minus first measured Vos in uV
  Drift = dVos/dT;
  
  // Display results
  showResultsPage();

#ifdef __DEBUG__
  Serial.print("   dT = ");
  Serial.print(dT, 2);
  Serial.print("°C  dVos = ");
  Serial.print(dVos, 2);
  Serial.print("uV  dVos/dT = ");
  Serial.print(Drift, 2);
  Serial.println("uV/°C");

  Serial.printf("Test completed in %i ms.\n", millis() - Start);

  // Print out all measurement and calculation results as a tab
  // separated string to simplify COPY-PASTE export to MS Excel
  Serial.println("Iq\tIb\tIos\tAol\tCMRR\tPSRR\tVos\tDrift\tV02a\tV02b\tV02c\tV02d\tV02e\tV02f\tV02a");
  // Calculated parameter values:
  Serial.print(Iq, 2);
  Serial.print("\t");
  Serial.print(Ib, 2);
  Serial.print("\t");
  Serial.print(Ios, 2);
  Serial.print("\t");
  Serial.print(Aol, 0);
  Serial.print("\t");
  Serial.print(CMRR, 0);
  Serial.print("\t");
  Serial.print(PSRR, 0);
  Serial.print("\t");
  Serial.print(Vos, 2);
  Serial.print("\t");
  Serial.print(Drift, 2);
  Serial.print("\t");
  // Measured voltage values:
  Serial.print(V02begin, 3);
  Serial.print("\t");
  Serial.print(V02b, 3);
  Serial.print("\t");
  Serial.print(V02c, 3);
  Serial.print("\t");
  Serial.print(V02d, 3);
  Serial.print("\t");
  Serial.print(V02e, 3);
  Serial.print("\t");
  Serial.print(V02f, 3);
  Serial.print("\t");
  Serial.println(V02a, 3);
  Serial.println("======== TEST END ========");
#endif

  // Wait for EXIT button click
  waitButtonClick(leftButton);
}

//----------------------------------//
// Measure voltage at the auxiliary //
// OpAmp output.                    //
// Returns result in Volts          //
//----------------------------------//
float measureVoltage(String ParamName) {
  float Vout = 0;
  // Measure voltage at ADCchannel 0-1 with 20 iterations and gain = 2 (low resolution)
#ifdef __DEBUG__
  timestamp();
  Serial.println("ADC0: measuring...");
#endif
  int16_t ADCData = getAdcCodeDiff(GAIN_TWO, 20, FullScaleLow);
  Vout = (ADCData - ActiveSettings.voltageLowZero) / ActiveSettings.voltageLowGain;
#ifdef __DEBUG__
  timestamp();
  Serial.printf("%s: [%d] ", ParamName.c_str(), ADCData);
  Serial.print(Vout, 4);
  Serial.print("V var=");
  Serial.print(variation,2);
  Serial.print("%");
#endif
  if (abs(Vout) < 1.85) { // If voltage is low then switch to high resolution mode and
    ADCData = getAdcCodeDiff(GAIN_SIXTEEN, 20, FullScaleHigh); // re-measure with gain = 16 
    Vout = (ADCData - ActiveSettings.voltageHighZero) / ActiveSettings.voltageHighGain;
#ifdef __DEBUG__
    Serial.printf(" [%d] ", ADCData);
    Serial.print(Vout, 4);
    Serial.print("V var=");
    Serial.print(variation,2);
    Serial.print("%");
#endif
  }
#ifdef __DEBUG__
  Serial.println("");
#endif
  return Vout;
}

//---------------------------------//
// Read code from ADC differential //
// input A0-A1 with averaging      //
//---------------------------------//
int16_t getAdcCodeDiff(adsGain_t gain, uint16_t iterations, uint16_t FullScale) {
  int16_t  resultU, maxU = -32767, minU = 32767;
  int32_t sumV = 0;
  ads.setGain(gain);
  ads.setDataRate(RATE_ADS1115_64SPS); // The lower the rate, the higher 50 Hz noise suppression
  for (int i=0; i<iterations; i++) {
    resultU = ads.readADC_Differential_0_1();
    sumV += resultU;
      if (resultU > maxU) {maxU = resultU;}
      if (resultU < minU) {minU = resultU;} 
  }
  variation = abs((float)(maxU - minU) / FullScale) * 100; //Voltage variation relative to full scale in %
  return (int16_t)(sumV / iterations + 0.5);
}

//---------------------------------//
// Read code from ADC single-ended //
// input with averaging            //
//---------------------------------//
uint16_t getAdcCodeSingleEnded(uint16_t ADCchannel, uint16_t iterations, uint16_t FullScale) {
  uint16_t  resultU, maxU = 0, minU = 32767;
  uint32_t sumV = 0;
  ads.setGain(GAIN_ONE);
  ads.setDataRate(RATE_ADS1115_128SPS);
  for (int i=0; i<iterations; i++) {
    resultU = ads.readADC_SingleEnded(ADCchannel);
    sumV += resultU;
    if (resultU > maxU) {maxU = resultU;}
    if (resultU < minU) {minU = resultU;} 
  }
  variation = abs((float)(maxU - minU) / FullScale) * 100; //Voltage variation relative to full scale in %
  return (uint16_t)(sumV / iterations + 0.5);
}

//------------ Local functions ---------------

//---------------------------------------------//
// Wait until the integration process is       //
// completed and stable voltage is established //
// at the auxiliary OpAmp output.              //
//---------------------------------------------//
void pause(unsigned long Interval) {
#ifdef __STOP_ON_EACH_PARAM__
  waitAnyButtonClick();
#endif
  delay(Interval);
  incProgressBar();
}

//------------------------//
// Increment progress bar //
//------------------------//
void incProgressBar() {
  progressBarPos++;
  progressBarSet(progressBarPos);
}

//-------------------------------------//
// Turn relays on main board on or off //
//-------------------------------------//
void setRelays(uint16_t Relay1State, uint16_t Relay2State, uint16_t Relay3State) {
    digitalWrite(relay1,Relay1State);
    digitalWrite(relay2,Relay2State);
    digitalWrite(relay3,Relay3State);
#ifdef __DEBUG__
    timestamp();
    Serial.print(String("RELs: K1=") + ((Relay1State == HIGH) ? "On" : "Off"));
    Serial.print(String(" K2=") + ((Relay2State == HIGH) ? "On" : "Off"));
    Serial.println(String(" K3=") + ((Relay3State == HIGH) ? "On" : "Off"));
#endif
}

//-------------------------------//
// Set or change supply voltages //
// for OpAmp under test          //
//-------------------------------//
void opAmpSupplyVoltage(
      float stdVoltagePlus, 
      float stdVoltageMinus,
      float lowVoltagePlus,
      float lowVoltageMinus) {
  switch (Voltage) {
    case voltageStandard:
#ifdef __DEBUG__
      timestamp();
#endif
      ppsSetVoltage(channelPlus, stdVoltagePlus, false);
#ifdef __DEBUG__
      timestamp();
#endif
      ppsSetVoltage(channelMinus, stdVoltageMinus, false);
      break;
    case voltageLow:
#ifdef __DEBUG__
      timestamp();
#endif
      ppsSetVoltage(channelPlus, lowVoltagePlus, false);
#ifdef __DEBUG__
      timestamp();
#endif
      ppsSetVoltage(channelMinus, lowVoltageMinus, false);
      break;
  }
}

//----------------------------//
// Invoke warning screen with //
// Stop and Skip buttons      //
//----------------------------//
bool handleOutOfRange(String ParamName) {
  int Button;
  showOutOfRangePage(ParamName);
  while (true) {
    Button = waitAnyButtonClick();
    if (Button == leftButton) { // Stop button is pressed
      ppsReset(false);
      return false;
    }
    if (Button == middleButton) { // Skip button is pressed
      showTestInProgressPage();
      progressBarSet(progressBarPos);
      return true;
    }
  }
} 

//----------------------------------------//
// Turn power off, invoke fatal error     //
// message and wait for Exit button press //
//----------------------------------------//
void handleError(uint16_t errID) {
  ppsReset(false);
  showErrorPage(errID);
  waitButtonClick(leftButton);
} 

//---------------------------------------//
// Print a number of milliseconds passed //
// since test start to Serial Monitor    //
// (in DEBUG mode only)                  //
//---------------------------------------//
#ifdef __DEBUG__
void timestamp() {
  Serial.printf("%.5i   ", millis() - Start);
}
#endif
