/***************************************************
Project:     OpAmp Tester
File:        Main
Version:     3.0 - Dec 2026
Written by:  Alexander Butovsky - diy-electronic.ru
****************************************************
Hardware:
  - STM32F103CBT6
  - TFT display 240*320 px on SPI bus
  - Programmable power supply (PPS)
    with two MCP4725 modules inside
  - ADS1115 module as voltmeter-ammeter
****************************************************
Version history
  1.0 Mar 2025 Initial version with LCD2004 display 
               and INA219 for Iq measurement
  1.1 Apr 2025 INA219 is replaced with schunt and 
               diff amplifier for Iq measurement.
  2.0 May 2025 LCD2004 is replaced with color TFT
               display 240*320 px, ADS1115 module 
               added instead of STM32 built-in ADC
  2.1 Dec 2025 Calibration procedures are added.
               STM32F103CBT6 with 128kB flash used.
  3.0 May 2026 Vos temperature drift calculation is
               added.
***************************************************/

// Uncomment following line to turn debugging mode on

#define __DEBUG__

// Uncomment following line to suspend measurement of each parameter.
// If this mode is activated then user will have to press any key on
// each measurement step to continue operation.
           
 // #define __STOP_ON_EACH_PARAM__

// Uncomment following line to reset EEPROM content: all values 
// stored in EEPROM will be set to the following default values

// #define __EEPROM_RESET__

// Default values for the settings to be stored in EEPROM:
#define ppsPlusZeroDefault          8.0
#define ppsPlusGainDefault        295.0
#define ppsMinusZeroDefault         9.0
#define ppsMinusGainDefault       293.2 
#define voltageLowZeroDefault    -188.0
#define voltageLowGainDefault    1740.0
#define voltageHighZeroDefault  -1501.0
#define voltageHighGainDefault  13836.2
#define currentLowZeroDefault   65461.0
#define currentLowGainDefault  -29090.3
#define currentStdZeroDefault     542.0
#define currentStdGainDefault    2351.5
#define themeNumDefault  0
#define voltageDefault   voltageStandard

#include <Adafruit_ADS1X15.h>

// Analog-digital converter I2C address = 72
#define ADC_I2C_ADDR 0x48

// Pushbutton pins
#define leftButton   PB12
#define middleButton PB13
#define rightButton  PB14

//Relay pins
#define relay1 PB8
#define relay2 PB9
#define relay3 PB10

// Custom type for Programmable Power 
// Supply (PPS) channels enumeration.
typedef enum {
  channelPlus,
  channelMinus
} ppsChannel;

// Custom type for OpAmp supply voltage:
typedef enum {
  voltageStandard, // +/-12V
  voltageLow       // +/-2.5V
} opAmpVoltage;

// Current OpAmp voltage
opAmpVoltage Voltage = voltageDefault;

// Current color theme number 0...2
uint16_t themeNum = 0;

// Main board test point TP6 reference voltage (in Volts) for Aol measurement 
float VrefStd = 7.150; // For standard voltage OpAmps
float VrefLow = 1.490; // For low voltage OpAmps

// Type declaration for the ActiveSettings storage. Keeps
// a number of float values used to calculate voltages in
// PPS and in voltmeter-ammeter. Declared as a structure
// with named fields and as an array at the same time.
// Hence we can read/write values either by field name:
//      float myVar = ActiveSettings.ppsMinusZero
// or by array member index (convenient for bulk operations):
//      float myVar = ActiveSettings.array[2]
typedef struct {
  union {
    struct {
      // Settings for PPS (MCP4725-based DACs in PPS module)
      float ppsPlusZero;     // Used for the conversion of voltage
      float ppsPlusGain;     // to voltageCode for V+ PPS channel.
      float ppsMinusZero;    // Used for the conversion of voltage
      float ppsMinusGain;    // to voltageCode for V- PPS channel.
      // Settings for voltage measurement (ADS1115-based ADC channels A0-A1)
      float voltageLowZero;  // Used for the conversion of ADC input A0-A1
      float voltageLowGain;  // code to voltage (Aux OpAmp output voltage) 
                             // with low resolution - PGA GAIN_TWO
      float voltageHighZero; // Used for the conversion of ADC input A0-A1
      float voltageHighGain; // code to voltage (Aux OpAmp output voltage)
                             // with high resolution - PGA GAIN_SIXTEEN
      // Settings for quiescent current measurement (ADS1115-based ADC channel A2)
      float currentLowZero;  // Used for the conversion of ADC input A2 code
      float currentLowGain;  // to current (OpAmp current consumption) for
                             // low voltage mode +/- 2.5 V
      float currentStdZero;  // Used for the conversion of ADC input A2 code
      float currentStdGain;  // to current (OpAmp current consumption) for
                             // standard voltage mode +/- 12 V
    };
    struct {
      float array[12];
    };
  };
} settings;

// ActiveSettings structure keeps all calibration 
// parameters for PPS and voltmeter-ammeter:
settings ActiveSettings;

//ADC object
Adafruit_ADS1115 ads;

// OpAmp parameters (calculated values)
float Iq,      // Power supply current consumed by IC, mA
      Vos,     // Input offset voltage, mV
      Drift,   // Vos temperature drift, uV/°C
      Ibminus, // Inverting input bias current (display as "Ib-"), nA
      Ibplus,  // Non-inverting input bias current (display as "Ib+"), nA
      Ib,      // Input Bias Current (Ibplus + Ibminus) / 2, nA
      Ios,     // Input Offset Current (Ibplus - Ibminus), nA
      Aol,     // Open loop gain
      CMRR,    // Common Mode Rejection Ratio, dB
      PSRR;    // Power Supply Rejection Ratio, dB

bool IbValid = true,    // Flags indicating parameters validity. If the
     IosValid = true,   // measusrement of certain parameter caused out
     AolValid = true,   // of range error then corresponding flag is
     CMRRValid = true,  // being set to false. Used when showing results
     PSRRValid = true;  // page to highlight invalid parameters in red.

//Instability of measured voltage in % relative fo full scale
float variation = 0.0;

/******************************************
**                                       **
**  PROGRAM EXECUTION STARTS FROM HERE   **
**                                       **
******************************************/
void setup() {
  // Pushbutton pins init
  pinMode(leftButton,   INPUT_PULLUP);
  pinMode(middleButton, INPUT_PULLUP);
  pinMode(rightButton,  INPUT_PULLUP);

  // Relay pins init
  pinMode(relay1, OUTPUT);
  pinMode(relay2, OUTPUT);
  pinMode(relay3, OUTPUT);

#ifdef __DEBUG__
  // Serial port init
  Serial.begin(9600);
  Serial.println("******************************");
  Serial.println("*     OpAmp Tester v.3.0     *");
  Serial.print("*        ");
  Serial.print(__DATE__);
  Serial.println("         *");
  Serial.println("******************************");
#endif

  //Retrieve settings stored in EEPROM
#ifndef __EEPROM_RESET__
  eepromRetrieveSettings();
#endif

  if(!isnan(ActiveSettings.ppsMinusGain) && (ActiveSettings.ppsMinusGain > 200.0) && (themeNum < 3)) {
#ifdef __DEBUG__
    Serial.println("Settings were successfully retrieved from EEPROM.\n");
#endif
  }
  else {
      ActiveSettings.ppsPlusZero  = ppsPlusZeroDefault;  // For the very first launch or when
      ActiveSettings.ppsPlusGain  = ppsPlusGainDefault;  // EEPROM reset mode is activated. In
      ActiveSettings.ppsMinusZero = ppsMinusZeroDefault; // these cases  we can't read from  
      ActiveSettings.ppsMinusGain = ppsMinusGainDefault; // EEPROM - assign default values and...
      ActiveSettings.voltageLowZero   = voltageLowZeroDefault;
      ActiveSettings.voltageLowGain   = voltageLowGainDefault;
      ActiveSettings.voltageHighZero  = voltageHighZeroDefault;
      ActiveSettings.voltageHighGain  = voltageHighGainDefault;
      ActiveSettings.currentLowZero  = currentLowZeroDefault;
      ActiveSettings.currentLowGain  = currentLowGainDefault;
      ActiveSettings.currentStdZero  = currentStdZeroDefault;
      ActiveSettings.currentStdGain  = currentStdGainDefault;
      themeNum = themeNumDefault;
      Voltage = voltageDefault;
      eepromStoreSettings();                             // ...store them in EEPROM
  }

  // Programmable power supply init
  ppsInit();   // Init DACs in both PPS channels and set
               // voltage at V+ and V- outputs to 0

  // ADC init
#ifdef __DEBUG__
  Serial.print("ADC : init ");
  if(ads.begin(ADC_I2C_ADDR)) {
    Serial.println("success.");
  }
  else {
    Serial.println("failure.");
  }
#else
  ads.begin(ADC_I2C_ADDR);
#endif
  ads.setGain(GAIN_TWO); // Activate low resolution mode

	// Display init
  displayInit();

  // If left button is pressed during startup -
  // - invoke PPS calibration procedure
  if (!digitalRead(leftButton)) {
    showCalibrationPage("PPS");
    waitButtonRelease(leftButton);
    if(calibrationPPS()) {
      calibrationFinalize("PPS"); // PPS calibration succeeded
    }
    else {
      calibrationFailure("PPS");  // PPS calibration failed
    }
  }
  
  // If middle button is pressed during startup -
  // - invoke voltmeter calibration procedure
  if (!digitalRead(middleButton)) {
    showCalibrationPage("Voltmeter");
    waitButtonRelease(middleButton);
    calibrationVoltmeter();
    calibrationFinalize("Voltmeter");
  }
  
  // If right button is pressed during startup -
  // - invoke ammeter calibration procedure
  if (!digitalRead(rightButton)) {
    showCalibrationPage("Ammeter");
    waitButtonRelease(rightButton);
    calibrationAmmeter();
    calibrationFinalize("Ammeter");
  }

  //Greeting screen
  showTitle();
  // Time to display greeting screen
  delay(2000);

}

/*************************************
**                                  **
**        PROGRAM MAIN LOOP         **
**                                  **
*************************************/
void loop() {
  showInsertOpAmpPage(); // Buttons: START, MODE, SETUP
  int Button = waitAnyButtonClick();
  switch (Button) {
    case leftButton:
      testOpAmp();
      break;
    case middleButton:
      selectVoltage();
      break;
    case rightButton:
      selectTheme();
      break;
  }
}

//------------------------------------------//
//           Voltage setup dialog           //
//------------------------------------------//
void selectVoltage() {
  uint16_t oldVoltage = (uint16_t)Voltage;
  showSelectVoltagePage(); // Buttons: +/-12V, +/-2.5V
  int button = waitAnyButtonClick();
  switch (button) {
    case leftButton:
      Voltage = voltageStandard;
      break;
    case middleButton:
      Voltage = voltageLow;
      break;
  }
  if ((uint16_t)Voltage != oldVoltage) {
    eepromStoreSettings();
  }
}

//------------------------------------------//
//        Color theme setup dialog          //
//------------------------------------------//
void selectTheme() {
  uint16_t oldThemeNum = themeNum;
  showSelectThemePage(); // Buttons: GREEN, BLUE, BROWN
  int Button = waitAnyButtonClick();
  switch (Button) {
    case leftButton:
      themeNum = 0;
      break;
    case middleButton:
      themeNum = 1;
      break;
    case rightButton:
      themeNum = 2;
      break;
  }
  if (themeNum != oldThemeNum) {
    eepromStoreSettings();
    displayTheme();
  }
}

//--------------------------------//
// Wait until button is clicked   //
//--------------------------------//
void waitButtonClick(int button) {
  while (digitalRead(button)) {delay(20);}
  waitButtonRelease(button);
}

//----------------------------//
// Wait until at least one of //
// three buttons is clicked   //
// Returns button clicked     //
//----------------------------//
int waitAnyButtonClick() {
  int button = waitButtonPress();
  waitButtonRelease(button);
  return button;
}

int waitButtonPress() {
  while (true) {
    delay(20);
    if (!digitalRead(leftButton)) {
      return leftButton;
    }
    if (!digitalRead(middleButton)) {
      return middleButton;
    }
    if (!digitalRead(rightButton)) {
      return rightButton;
    }
  } 
}

void waitButtonRelease(int button) {
  while (!digitalRead(button)) {delay(20);}
}
