/*
 * Copyright 2016 Mario Richter
 * This file is part of NeMo.
 *
 * NeMo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * NeMo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NeMo.  If not, see <http://www.gnu.org/licenses/>.
 */

/*!
 * \file adc.c
 * \ingroup adc
 * \brief Routinen für den AD-Wandler (Temperatur- und Spannungsmessung)
 */

/*!
 * \defgroup adc Routinen zur Ansteuerung des ADC
 */

#include "main.h"

static uint8_t  adcStat;
uint16_t        adcResults[4];

char            u5v[5];
char            u3v3[5];
char            uLed[5];
char            NTCtemp[5];

#define NTCcalcPoints     10                  //!< Anzahl Stützstellen
const uint16_t NTCcalc[NTCcalcPoints][2] =
  //  Temperatur*10     ADC-Wert
  { { 0,                672 },
    { 100,              710 },
    { 200,              748 },
    { 250,              767 },
    { 300,              786 },
    { 400,              823 },
    { 500,              859 },
    { 600,              895 },
    { 700,              930 },
    { 800,              964 }
};

/*!
 * \ingroup adc
 * \brief Initialisierung des AD-Wandler
 *
 * Diese Funktion initialisiert den AD-Wandler für die Messung der Endstufentemperatur
 * und der Betriebsspannungen (3.3V, 5V, Eingangsspannung).
 *
 * \sa adcScheduler
 */

void adcInit(void)
{
  // Interne 2.56V-Referenz, Kanal ADC0 (5V) auswählen
  ADMUX = (1 << REFS1) | (1 << REFS0);

  // ADC einschalten, Prescaler 128: 16MHz / 128 = 125kHz
  ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);

  // Digitale Eingangsbuffer für ADC0..3 deaktivieren (spart Strom)
  DIDR0 = (1 << ADC3D) | (1 << ADC2D) | (1 << ADC1D) | (1 << ADC0D);

  // Starte Dummy-Wandlung
  ADCSRA |= (1 << ADSC);

  // Status der State Machine auf IDLE setzen
  adcStat = ADC_STAT_IDLE;
}


/*!
 * \ingroup adc
 * \brief Start eines neuen Wandlerzyklus (Wandeln aller Eingänge)
 *
 * Diese Routine startet einen neuen Wandlerzyklus, das heißt, die Wandlung aller
 * definierten Eingänge für die Messung des Endstufen-NTCs und der Betriebsspannungen.
 * Der eigentliche Ablauf erfolgt dann im adcScheduler().
 *
 * \return 0: Zyklus wurde gestartet, 1: ADC nicht bereit
 *
 * \sa adcScheduler
 */

uint8_t adcStartCycle(void)
{
  // Prüfe, ob ADC bereit für neue Wandlung
  if(adcStat != ADC_STAT_IDLE)
    return 1;

  adcStat = ADC_STAT_START;
  return 0;
}


/*!
 * \ingroup adc
 * \brief Scheduler für den AD-Wandler
 *
 * Hierbei handelt es sich um den Scheduler für den AD-Wandler. Er startet die einzelnen
 * Wandlungen, holt die Wandlungsergebnisse ab und trägt sie in die Variablen ein.
 *
 * \sa adcInit, adcStartCycle
 */

void adcScheduler(void)
{
  switch(adcStat)
  {
    // AD-Wandler im Leerlauf: es gibt nichts zu tun
    case ADC_STAT_IDLE:

      break;

    // Starte neuen Wandlerzyklus
    case ADC_STAT_START:

      if((ADCSRA & (1 << ADSC)) == 0)                 // Prüfe, ob ADC bereit ist
      {
        ADMUX = (1 << REFS1) | (1 << REFS0);           // Referenz 2,56V und Eingang ADC0 wählen
        ADCSRA |= (1 << ADSC);
        adcStat = ADC_STAT_WAIT1;
      }
      break;

    // Warte auf 1. Wandlung
    case ADC_STAT_WAIT1:

      if((ADCSRA & (1 << ADSC)) == 0)                 // Prüfe, ob ADC fertig ist
      {
        adcResults[0] = ADC;                          // Ergebnis holen
        ADMUX = (1 << REFS1) | (1 << REFS0) | (1 << MUX0);       // Eingang ADC1 wählen
        ADCSRA |= (1 << ADSC);
        adcStat = ADC_STAT_WAIT2;
      }
      break;

    // Warte auf 2. Wandlung
    case ADC_STAT_WAIT2:

      if((ADCSRA & (1 << ADSC)) == 0)                 // Prüfe, ob ADC fertig ist
      {
        adcResults[1] = ADC;                          // Ergebnis holen
        ADMUX = (1 << REFS1) | (1 << REFS0) | (1 << MUX1);       // Eingang ADC2 wählen
        ADCSRA |= (1 << ADSC);
        adcStat = ADC_STAT_WAIT3;
      }
      break;

    // Warte auf 3. Wandlung
    case ADC_STAT_WAIT3:

      if((ADCSRA & (1 << ADSC)) == 0)                 // Prüfe, ob ADC fertig ist
      {
        adcResults[2] = ADC;                          // Ergebnis holen
        ADMUX = (1 << REFS1) | (1 << REFS0) | (1 << MUX1) | (1 << MUX0);       // Eingang ADC3 wählen
        ADCSRA |= (1 << ADSC);
        adcStat = ADC_STAT_WAIT4;
      }
      break;

    // Warte auf 4. Wandlung
    case ADC_STAT_WAIT4:

      if((ADCSRA & (1 << ADSC)) == 0)                 // Prüfe, ob ADC fertig ist
      {
        adcResults[3] = ADC;                          // Ergebnis holen
        adcStat = ADC_STAT_IDLE;                      // Zyklus ist fertig
      }

      adcVoltageCalc();                               // Spannungen berechnen
      adcTemperatureCalc();                           // Endstufentemperatur berechnen
      break;

    default:

      adcStat = ADC_STAT_IDLE;
      break;
  }
}


/*!
 * \ingroup adc
 * \brief Spannungsberechnungen aus den ADC-Werten
 *
 * Diese Routine berechnet aus den ADC-Werten die korrespondierenden Spannungen direkt als String.
 * Hierbei wird die Inkrementen-Methode benutzt, d.h. vom ADC-Wert werden solange nacheinander die
 * (vorab berechneten) Inkremente für 10V, 1V und 0,1V abgezogen, bis der Wert kleiner als das
 * jeweilige Inkrement ist. Die Anzahl der Subtraktionen ergibt die Ziffer der jeweiligen Stelle.
 *
 * \sa adcScheduler
 */

void adcVoltageCalc(void)
{
  uint16_t tmp;

  // Spannungsberechnung für 5V
  // **************************

  // Einerstelle
  u5v[0] = 0x30;
  tmp = adcResults[0];
  while(tmp >= ADC_5V_INKR_1V)
  {
    u5v[0]++;
    tmp -= ADC_5V_INKR_1V;
  }

  // Zehntel-Stelle
  u5v[1] = '.';
  u5v[2] = 0x30;
  while(tmp >= ADC_5V_INKR_0V1)
  {
    u5v[2]++;
    tmp -= ADC_5V_INKR_0V1;
  }

  // Hundertstel-Stelle
  u5v[3] = 0x30;
  while(tmp >= ADC_5V_INKR_0V01)
  {
    u5v[3]++;
    tmp -= ADC_5V_INKR_0V01;
  }
  u5v[4] = 0;

  // Spannungsberechnung für 3.3V
  // **************************

  // Einerstelle
  u3v3[0] = 0x30;
  tmp = adcResults[1];
  while(tmp >= ADC_3V3_INKR_1V)
  {
    u3v3[0]++;
    tmp -= ADC_3V3_INKR_1V;
  }

  // Zehntel-Stelle
  u3v3[1] = '.';
  u3v3[2] = 0x30;
  while(tmp >= ADC_3V3_INKR_0V1)
  {
    u3v3[2]++;
    tmp -= ADC_3V3_INKR_0V1;
  }

  // Hundertstel-Stelle
  u3v3[3] = 0x30;
  while(tmp >= ADC_3V3_INKR_0V01)
  {
    u3v3[3]++;
    tmp -= ADC_3V3_INKR_0V01;
  }
  u3v3[4] = 0;

  // Spannungsberechnung für VLED
  // **************************

  // Einerstelle
  uLed[0] = 0x30;
  tmp = adcResults[2];
  while(tmp >= ADC_VLED_INKR_10V)
  {
    uLed[0]++;
    tmp -= ADC_VLED_INKR_10V;
  }

  // Zehntel-Stelle
  uLed[1] = 0x30;
  while(tmp >= ADC_VLED_INKR_1V)
  {
    uLed[1]++;
    tmp -= ADC_VLED_INKR_1V;
  }

  // Hundertstel-Stelle
  uLed[2] = '.';
  uLed[3] = 0x30;
  while(tmp >= ADC_VLED_INKR_0V1)
  {
    uLed[3]++;
    tmp -= ADC_VLED_INKR_0V1;
  }
  uLed[4] = 0;
}


/*!
 * \ingroup adc
 * \brief Temperaturberechnung aus den ADC-Werten
 *
 * Diese Routine berechnet aus dem ADC-Wert des NTC-Spannungsteilers die zugehörige
 * Temperatur direkt als String.
 *
 * \sa adcScheduler
 */

void adcTemperatureCalc(void)
{
  uint8_t i;
  uint16_t result;
  uint32_t tmp;

  // Minimaltemperatur
  if(adcResults[3] <= NTCcalc[0][1])
  {
    NTCtemp[0] = ' ';
    NTCtemp[1] = '0';
    NTCtemp[2] = '.';
    NTCtemp[3] = '0';
    NTCtemp[4] = 0;
  }
  // Maximaltemperatur
  else if(adcResults[3] >= NTCcalc[NTCcalcPoints-1][1])
  {
    NTCtemp[0] = '8';
    NTCtemp[1] = '0';
    NTCtemp[2] = '.';
    NTCtemp[3] = '0';
    NTCtemp[4] = 0;
  }
  // Dazwischen erfolgt Interpolation
  else
  {
    i = 0;

    // Suche die beiden Einträge in der LUT, zwischen denen interpoliert werden muss:
    while((adcResults[3] > NTCcalc[i+1][1]) && (i < (NTCcalcPoints - 2)))
    {
      i++;
    }

    // Der ADC-Wert liegt jetzt in der Tabelle zwischen Eintrag i und Eintrag i+1.
    // Es wird interpoliert nach der Formel:
    // Temperatur = Temp,i + (Temp,i+1 - Temp,i) * (ADC-Wert - ADC,i) / (ADC,i+1 - ADC,i)
    // Für höhere Genauigkeit wird intern mit uint32_t gerechnet.

    tmp = ((uint32_t)adcResults[3] - (uint32_t)NTCcalc[i][1]) << 16;
    tmp = tmp / ((uint32_t)NTCcalc[i+1][1] - (uint32_t)NTCcalc[i][1]);
    tmp = tmp * (NTCcalc[i+1][0] - NTCcalc[i][0]);

    result = NTCcalc[i][0] + (uint16_t)(tmp >> 16);

    // Jetzt können wir die Temperatur in einen String wandeln:
    // Zehnerstelle
    NTCtemp[0] = 0x30;
    while(result >= 100)
    {
      result -= 100;
      NTCtemp[0]++;
    }

    NTCtemp[1] = 0x30;
    while(result >= 10)
    {
      result -= 10;
      NTCtemp[1]++;
    }

    NTCtemp[2] = '.';
    NTCtemp[3] = 0x30;
    while(result >= 1)
    {
      result--;
      NTCtemp[3]++;
    }

    NTCtemp[4] = 0;
  }
}


