/*
50 Hz Frequenzgenerator mit 0,001 Hz Auflösung im Bereich 45,000 - 55,000 Hz

Ein ATmega48/88 erzeugt über Timer1 am Ausgang PB1 ein symmetrisches, quarzgenaues Rechtecksignal
mit 1 mHz Auflösung. Dies passiert in der ISR(TIMER1_COMPA_vect).

Die Frequenzeinstellung erfolgt per Potentiometer (Joystick) welches sich in Mittenstellung neutral
verhält. Abhängig von Drehrichtung und proportional zum Drehwinkel wird die aktuelle Frequenz verändert.
Dazu wird über OCR1B von Timer1 eine Frequenz erzeugt, die die Einstellgeschwindigkeit passend zum Drehwinkel
vorgibt, indem die Variable 'offset' erhöht wird.
Die Geschwindigkeitsstufen werden mit der Tabelle uint16_t inc[] vorgegeben und liegen bei 1, 2, 4, 8 ... Hz.
Die Änderung von 'offset' wird im Hauptprogramm ausgewertet und als 'neue_periode' der ISR für OCR1A
übergeben.
Damit die Drehung des Potis zügig erkannt wird, wird die Stellfrequenz um den F_FAKTOR (typ. 10) erhöht.

Die eingestellte Frequenz wird über eine 5-stellige 7-Segmentanzeige angezeigt, worauf hier nicht näher
eingegangen werden soll.
Beim Einschalten wird die zuletzt eingestellte Frequenz erzeugt.

Das Schaltbild findet sich hier:
http://mino-elektronik.de/bilder/Generator_50Hz/Generator_50Hz.png

2016-12-29
http://www.mino-elektronik.de
Alle Angaben ohne Gewaehr !
*/


#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/eeprom.h>
#include <stdlib.h>



#ifndef EEAR
#define EEAR _SFR_IO16(0x21)            // für Mega88 Kompatibilität
#endif

#ifndef BODS                            // falls keine P-Typ verwendet wird
#define BODS      6                     // dummy Bits verwenden
#define BODSE     5             
#endif
 
#define BIT(x)  (1<<x)

// Pegel je nach Anzeige/Treibern anpassen
// ggf. auch dynamisch mit PortPin umschalten
#define SEGM_PEGEL   1                  // 1-Pegel für aktives Segment
#define DIGIT_PEGEL  0                  // 0-Pegel für aktives Digit

#define F_CPU         12000000L         // 12 MHz ggf. anpassen
#define LED_STELLEN     5               // angezeigte Stellen

#define EE_ADR_F_SOLL   2               // Platz für Korrekturwert

#define ADC_MAX       1023.0            // bei 10 Bit
#define FILTER        5
#define F_MIN         45.000            // in Hz
#define F_MAX         55.000            // in Hz
#define SKAL_FAKTOR   1000.0            // für mHz Auflösung
#define POTI_STUFEN   10                // Drehwinkel in Stufen aufteilen
#define F_FAKTOR      10                // zu tiefe Stellfrequenzen vermeiden

#define POTI        6                   // ADC-Eingang 6

volatile uint8_t  neue_periode_fertig,  // zur Übernahme der neuen Frequenz
                  neue_periode_poti, poti_sperre, richtung;
volatile uint32_t neue_periode, offset, periode_poti; // errechneter Wert für Periodendauer

volatile uint8_t t1_ovf_sync;           // wird alle 5,5 ms gesetzt

// tabelle für Stellfrequenz
uint16_t inc[POTI_STUFEN] = {0,1,2,4,8,16,32,64,128,256};


// Segmente an PORTD
#define AUSGABE_PORT    PORTD
#define AUSGABE_DDRX    DDRD
#define DIGIT_PORT      PORTC
#define DIGIT_DDRX      DDRC


// Digit-Ausgabe an PORTC: PC0 - PC4
#define DIGIT1          BIT(4)
#define DIGIT2          BIT(3)
#define DIGIT3          BIT(2)
#define DIGIT4          BIT(1)
#define DIGIT5          BIT(0)
#define DIGIT_LED       BIT(3)          // an PortB

uint8_t stellen[]  = {DIGIT1, DIGIT2, DIGIT3, DIGIT4, DIGIT5, DIGIT_LED};

// bei abweichendem Layout Segmente entsprechend zuordnen
#define SEGA    0x01
#define SEGB    0x02
#define SEGC    0x04
#define SEGD    0x08
#define SEGE    0x10
#define SEGF    0x20
#define SEGG    0x40
#define SEGP    0x80
#define DP_SEGM         BIT(5)        // auf PC5

#define ZAHL8   0xff-SEGP             // ALLE SEGMENTE AUSSER '.'
#define ZAHL0   ZAHL8-SEGG

uint8_t zeichensatz[] = {
    ZAHL8-SEGG,             // 0
    SEGB+SEGC,              // 1
    ZAHL8-SEGF-SEGC,        // 2
    ZAHL8-SEGE-SEGF,        // 3
    ZAHL8-SEGA-SEGD-SEGE,   // 4
    ZAHL8-SEGB-SEGE,        // 5
    ZAHL8-SEGB,             // 6
    SEGA+SEGB+SEGC,         // 7
    ZAHL8,                  // 8
    ZAHL8-SEGE,             // 9
    ZAHL8-SEGD,             // 'A'
    ZAHL8-SEGA-SEGB,        // 'b'
    ZAHL8-SEGB-SEGC-SEGG,   // 'C'
    ZAHL8-SEGA-SEGF,        // 'd'
    ZAHL8-SEGB-SEGC,        // 'E'
    ZAHL8-SEGB-SEGC-SEGD    // 'F'
};

uint8_t ausgabe_register[LED_STELLEN+1];        // register zur formatierten Ausgabe
uint8_t mpx_register[LED_STELLEN+1];            // aktuell angezeigte Digits


void led_ausgabe(void)                          // ausgabe_register[] -> mpx_register[]
{
uint8_t n;
  cli();
  for(n=0; n <= LED_STELLEN; n++)
    mpx_register[n] = ausgabe_register[n];      // segmente 1:1 kopieren
  sei();
}


void loesche_ausgabe_register(void)             // löscht nur das ausgabe_register[]
{
uint8_t n;
  for(n=0; n <= LED_STELLEN; n++)
    ausgabe_register[n] = 0; 
}

void loesche_anzeige(void)                      // Anzeige direkt löschen
{
  loesche_ausgabe_register();                   // ausgabe_register[] löschen
  led_ausgabe();                                // und Anzeige auffrischen
}


void init_led(uint8_t muster)
{
uint8_t n;
  for(n=0; n < LED_STELLEN; n++) {
    ausgabe_register[n] = muster;               // Muster nach Vorgabe
  }
  led_ausgabe();                                // und anzeigen
}


// Routine zur Wandlung und Ausgabe eines Meßwertes
void zeige_x(float x)
{
int8_t i,j,k,dimension;
  k=0,dimension=3;
  if(x!=0) {
    while(x >= 1e3) {x*=0.001;dimension--;}
    while(x<1.0) {x*=1000;dimension++;}
// x liegt im Bereich 1.0 - 999.9
    while(x>=10.0) {
      x*=0.1;
      k++;
    }
// x liegt im Bereich 1.0 - 9.999
    x += 5e-5;                                  // aufrunden
    if(x >= 10.0) {                             // und ggf. Bereich korrigieren
      x *= 0.1;
      k++;
      if(k > 2) {
        dimension--;
        k=0;
      }
    }
  }

  for(i = 0; i < LED_STELLEN; i++) {
    j = x;
    if(i == k) {                                // Dez.-Punkt ergänzen
      ausgabe_register[i] = (zeichensatz[j] | SEGP);    
    } else {
      ausgabe_register[i] = zeichensatz[j];     // ohne punkt
    }
    x-=j;
    x*=10.0;
  }
  led_ausgabe();
}


void init_timer(void)
{
  TCCR1B = 0;                                   // stoppen
  TCNT1  = 0;                             
  TCCR1B = BIT(CS10);                           // freilaufend mit vorteiler /1
  TCCR1A = BIT(COM1A1);
  DDRB |=  BIT(1);                              // Ausgang PB1
  OCR1A  = 0;
  OCR1B  = 0; 
  TIFR1  = BIT(OCF1A) | BIT(OCF1B);             // OCF1x flag loeschen
  TIMSK1 |= BIT(TOIE1) | BIT(OCIE1A) | BIT(OCIE1B);
  ADCSRA = BIT(ADEN) + 0x07;                    // ADC einschalten mit Teiler /128  

  TCCR0B = BIT(CS01) + BIT(CS00);
  TIMSK0 = BIT(TOIE0);                          // OVFL Int-enable
}


// Ausgangssignal an PB1(OC1A) erzeugen
ISR(TIMER1_COMPA_vect)
{
static uint32_t nachlade_temp, pw_nachlade_temp;
static uint8_t trigger_pw = 1;
static uint32_t periode  =  F_CPU/50L;          // default Startwerte
static uint32_t pulsweite = F_CPU/100L;         // für 50 Hz
uint32_t temp = 0xffff;

  if(trigger_pw) {
    if(pw_nachlade_temp <= temp) temp = pw_nachlade_temp;         
    else if(pw_nachlade_temp < 0x1ffff) temp = pw_nachlade_temp/2; 
    pw_nachlade_temp -= temp;                   // laengere PW-Intervalle stueckweise abarbeiten
    OCR1A += temp;
    if(pw_nachlade_temp == 0) {                 // Pulsweite macht den letzten Zyklus
      TCCR1A &= ~BIT(COM1A0);                   // danach OC1A synchron loeschen wenn TCNT1 == OCR1A
      nachlade_temp = periode - pulsweite;      // Restwert für Periodendauer verwenden
      trigger_pw = 0;
    }
    return;                                     // Pulsweite ist noch aktiv
  }

  if(nachlade_temp <= temp) temp = nachlade_temp;         
  else if(nachlade_temp < 0x1ffff) temp =nachlade_temp/2; 
  nachlade_temp -= temp;                        // laengere Intervalle stueckweise abarbeiten
  OCR1A += temp;
  if(nachlade_temp) return;                     // Intervall noch nicht abgelaufen
  trigger_pw = 1;                               // Ausgangsimpuls im nächsten Zyklus ausgeben
  TCCR1A |= BIT(COM1A0);                        // beim nächsten Interrupt OC1A synchron setzen
  pw_nachlade_temp = pulsweite;
  if(neue_periode_fertig) {                     // update synchronisieren
    periode = neue_periode;
    pulsweite = neue_periode/2;
    neue_periode_fertig = 0;
  }
}


// Einstellgeschwindigkeit abhängig vom Poti
ISR(TIMER1_COMPB_vect)
{
static uint32_t nachlade_temp;
static uint32_t periode =  F_CPU;               // default Startwert
uint32_t temp = 0xffff;

  if(nachlade_temp <= temp) temp = nachlade_temp;         
  else if(nachlade_temp < 0x1ffff) temp =nachlade_temp/2; 
  nachlade_temp -= temp;                        // laengere Intervalle stueckweise abarbeiten
  OCR1B += temp;
  if(nachlade_temp) return;                     // Intervall noch nicht abgelaufen

  if(!poti_sperre) {
    offset++;                                   // Frequenzänderung
  }
  nachlade_temp = periode;
  if(neue_periode_poti) {                       // update synchronisieren
    periode = periode_poti;
    neue_periode_poti = 0;
  }
}

// Multiplexen der Anzeige
ISR(TIMER0_OVF_vect)                            // wird mit MPX_FREQUENZ aufgerufen
{
static uint8_t digit_nr;
uint8_t temp;
  sei();                                        // und gleich wieder freigeben

// ab hier unkritisches Timing
// zunächst alle Treiber abschalten
  if(SEGM_PEGEL == 1) {                         // alle Segmente ausschalten
    AUSGABE_PORT = 0x80;                        // auf 0 aber Pullup eingeschaltet lassen
    PORTC &= ~DP_SEGM;  
  } else {
    AUSGABE_PORT = 0xff;                        // oder auf 1
    PORTC |= DP_SEGM;
  }
  if(DIGIT_PEGEL == 1) {                        // alle Digit-Treiber ausschalten
    DIGIT_PORT &= ~(DIGIT1 | DIGIT2 | DIGIT3 | DIGIT4 | DIGIT5); // alle auf 0
    PORTB &= ~DIGIT_LED;
  } else {
    DIGIT_PORT |= DIGIT1 | DIGIT2 | DIGIT3 | DIGIT4 | DIGIT5;    // oder auf 1
    PORTB |= DIGIT_LED;
  }


// neue Segemente einschalten
  temp = mpx_register[digit_nr];                // Zugriff verkuerzen
  if(SEGM_PEGEL == 1) {
    if(temp & SEGP) PORTC |= DP_SEGM;
    AUSGABE_PORT = temp | 0x80;                 // 1:1
  } else {
    if(temp & SEGP) PORTC &= ~DP_SEGM;
    AUSGABE_PORT = (temp ^ 0xff) | 0x80;        // oder invertiert
  }

// aktuellen Digit Ausgang aktivieren
  if(digit_nr < LED_STELLEN) {
    if(DIGIT_PEGEL == 1)           
      DIGIT_PORT |= stellen[digit_nr];          // mit '1'
    else 
      DIGIT_PORT &= ~stellen[digit_nr];         // oder mit '0'
  } else {
    if(DIGIT_PEGEL == 1)           
      PORTB |= DIGIT_LED;                       // mit '1'
    else    
      PORTB &= ~DIGIT_LED;                      // oder mit '0'
  }


  digit_nr++;                                   // nächste Stelle vorbereiten
  if(digit_nr > LED_STELLEN) digit_nr = 0;

}

ISR(TIMER1_OVF_vect)            
{
  t1_ovf_sync = 1;
}


void t1_ovf_wait(uint16_t time)                 // time * 5,5 ms
{
  while(time--) {   
    t1_ovf_sync = 0;                            // bis Zeit abgelaufen
    while(!t1_ovf_sync);                        // 5,5 ms abwarten
  }    
}

void t1_sync()
{
    while(!t1_ovf_sync);                        // auf Überlauf warten
    t1_ovf_sync = 0;                            // Bit wieder loeschen
}

uint8_t ee_read_byte(uint16_t adr)
{
  while(EECR & BIT(EEPE));      // abwarten
  EEAR  = adr;
  EECR |= BIT(EERE);            // lesebit setzen
  return(EEDR);
}

void ee_write_byte(uint8_t data, uint16_t adr)
{
  if(data != ee_read_byte(adr)) {
    cli();
    EEAR = adr;
    EEDR = data;
    EECR  = BIT(EEMPE);         // master schreibbit setzen, EEPMx löschen
    EECR |= BIT(EEPE);          // schreibbit setzen
    sei();
  }
}

void ee_write_word(uint16_t data, uint16_t adr)
{
  ee_write_byte((uint16_t)(data)%256, adr);
  ee_write_byte((uint16_t)(data)/256, adr+1);
}

uint16_t ee_read_word(uint16_t adr)
{
uint16_t temp;
  temp = ee_read_byte(adr);
  temp += ee_read_byte(adr+1) * 256;
  return(temp);
}


void init_ports(void)
{
  if(SEGM_PEGEL == 1) {                         // alle Segmente ausschalten
    AUSGABE_PORT = 0;                           // auf 0
    PORTC &= ~DP_SEGM;
  } else {
    AUSGABE_PORT = 0xff;                        // auf 1
    PORTC |= DP_SEGM;
  }
  AUSGABE_PORT |= 0x80;                         // pullup fuer AIN1
  AUSGABE_DDRX = 0x7f;                          // auf Ausgang für alle Segemente
  PCMSK0 |= BIT(PORTB0);                        // Flankenerkennung von Vorteiler freigeben

  if(DIGIT_PEGEL == 1) {                        // alle Digit-Treiber ausschalten
    DIGIT_PORT &= ~(DIGIT1 | DIGIT2 | DIGIT3 | DIGIT4 | DIGIT5);  // alle auf 0
  } else {                                                        // oder 
    DIGIT_PORT |= (DIGIT1 | DIGIT2 | DIGIT3 | DIGIT4 | DIGIT5);   // alle auf 1
  }
  DIGIT_DDRX = (DIGIT1 | DIGIT2 | DIGIT3 | DIGIT4 | DIGIT5);      // als Ausgänge
  DDRC |= DP_SEGM;   
  DDRB |= DIGIT_LED;
  init_led(SEGG);                               // "-----" ins mpx-register
}

uint16_t lese_adc(uint8_t kanal)
{
  ADMUX =  BIT(REFS0)| kanal;                   // Vcc als Vref, rechtsbuendig
  ADCSRA |= BIT(ADSC);                          // starten
  while(ADCSRA & BIT(ADSC));                    // warten
  return(ADC);                                  // fertig  
}

int32_t filter_poti(int32_t neuer_wert)         // zu schnelle Aenderungen vermeiden
{
static int32_t summe = ADC_MAX/2 * FILTER;
   summe -= summe/FILTER;
   summe += neuer_wert;
   return summe/FILTER;
}

// Hauptprogramm
int main(void)
{
float f_soll = 50.0, temp_offset;
int32_t temp;

  CLKPR = 0x80; CLKPR = 0x0;                  // CKDIV auf 0; voller CPU-Takt
  init_ports();
  init_timer();
  temp_offset = ee_read_word(EE_ADR_F_SOLL)/SKAL_FAKTOR;  // letzten Wert lesen und auf Hz skalieren
  if(temp_offset >= F_MIN && temp_offset <= F_MAX) f_soll = temp_offset;
  neue_periode = F_CPU/f_soll;                // eff. Periodendauer
  neue_periode_fertig = 1;                    // letzte Frequenz verwenden
  zeige_x(f_soll);                            // und anzeigen
  sei();

  for(;;) {
    t1_sync();                                // Ausführung für Poti-Filterung ein wenig bremsen
// Berechnung für Poti-> Frequenz
    if(!neue_periode_poti) { 
      temp = filter_poti(lese_adc(POTI));     // gemittelter ADC-Wert
      temp *= POTI_STUFEN;                    // Drehwinkel in Stufen einteilen
      temp -= ADC_MAX*POTI_STUFEN/2;          // mit Nullpunkt in Poti-Mitte
      temp = temp / (ADC_MAX/2);              // bei rechts-links auf 0 -> +/-POTI_STUFEN normieren
      richtung = 0;
      if(temp >= 0) richtung = 1;             // Richtung-Signal abhaengig vom Vorzeichen
      temp = inc[labs(temp)] * F_FAKTOR;      // eff. Stellfrequenz aus Tabelle
//    zeige_x(temp); 
      if(!temp) {                             // bei Poti Mittenstellung
        poti_sperre = 1;                      // keine Veränderung
        temp = F_FAKTOR;                      // und kleinste Gechwindigkeit als Minimum
      } else 
      poti_sperre = 0;                        // Stellfrequenz wieder einschalten
      periode_poti = F_CPU/temp;              // eff. Periodendauer
      neue_periode_poti = 1;                  // und Stellfrequenz übergeben
    }

// Aenderung der Ausgangsfrequenz
    if(!neue_periode_fertig) { 
      if(offset > F_FAKTOR) {
        cli();
        temp_offset = offset/SKAL_FAKTOR/F_FAKTOR; // Änderungen in 0,001 Hz Schritten
        offset = 0;                           // Zähler wieder löschen
        sei();
        if(richtung) f_soll += temp_offset;   // Frequenzänderungen aufwärts
        else f_soll -= temp_offset;           // oder abwärts
        if(f_soll < F_MIN) {
          f_soll = F_MIN;                     // nach unten begrenzen
        }
        if(f_soll > F_MAX) {
          f_soll = F_MAX;                     // nach oben begrenzen
        }
        neue_periode = F_CPU/f_soll;          // eff. Periodendauer
        neue_periode_fertig = 1;              // neue Frequenz verwenden
        zeige_x(f_soll);                      // und anzeigen
        ee_write_word(f_soll*1000, EE_ADR_F_SOLL); // in mHz speichern
      }  
    } 

  } // for
}
