/*
PWM-Spannungssteller mit ATtiny25/45/85 und mechanischen Drehgeber
Der µC erzeugt per PWM eine Ausgangsspannung in Abhängigkeit von der eigenen Versorgungs-
spannung und einem eingestellten PWM-Wert. Dieser PWM-Wert wird mit einem mechanischen 
Drehgeber im Bereich MIN_STELLWERT - MAX_STELLWERT eingestellt, wobei MAX_STELLWERT 100%
der Versorgungsspannung entspricht. Die Differenz MIN_STELLWERT - MAX_STELLWERT gibt die
Anzahl der Stufen vor.

Beispielsweise werden hier 5 und 50 für die Stellwerte verwendet, was Ausgangsspannungen 
von 0,1 * VCC -> VCC erzeugt. Bei 5 V Versorgung: 0,5 V - 5 V, wobei eine Stufe 0,1 V entspricht.
Das PWM-Signal wird an Ausgang PB4 (OC1B) ausgegeben, wobei der PWM-Wert per STELLWERT_FAKTOR 
so skaliert wird, dass möglichst hohe Auflösung verwendet wird. Um das PWM-Signal mit möglichst kurzer
Einschwingzeit per Filter in eine stabile Ausgangsspannung wandeln zu können, läuft die PWM mit intern 64 MHz,
wodurch die Ausgangsfrequenz >= 250 kHz beträgt. Für hochohmige Ansteuerung reicht ein RC-Glied
als Filter; werden Ströme im mA-Bereich benötigt, ist ein LC-Glied notwendig, was bei Belastung die
Ausgangsspannung kaum verändert. Bei 5 mA liefert der PWM-Ausgang des µC nicht mehr die 
volle Amplitude, was die Ausgangsspannung um ca. 1 % einbrechen läßt. Ob das tolerabel ist
muß je nach Anwendung entschieden werden.

Einschränkungen beim PWM-Signal:
Der ATtiny25 ist leider nicht in der Lage, die letzte PWM-Stufe "sauber" zu erzeugen. Bei
Betrieb mit 64 MHz müßte bei fast 100% Tastverhältnis der letzte Lücke (negativer Impuls)
ca. 15 ns betragen. Im gewählten Modus mit OCR1C als Endwert sind dies jedoch 30 ns. 
Das bedeutet, dass der obere Spannungswert nicht exakt erreicht wird.

Die Drehgeberauswertung wird in einer Schleife aufgerufen, die bei geändertem Zählerstand verlassen
wird und den Ausgangswert anpasst. 
Als weitere Funktion kann ein Signal an SW_ZERO (für Drehgeber mit Tastfunktion) den Ausgangswert 
auf einen Sollwert nach Bedarf einstellen. Hier wird der PWM-Wert auf 0 gesetzt.

Michael Nowak
www.mino-elektronik.de
2022-01-05
Schaltung: http://mino-elektronik.de/bilder/up_down_analog/up_down_analog_dg.png
 */ 

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdlib.h>
#include <avr/eeprom.h>

#define BIT(x)  (1<<x)

#define DG_PHA          0         // PortB 0
#define DG_PHB          1         // PortB 1
#define DG_ZERO         2         // PortB 2
#define PWM_OUT         4         // PortB 4 OC1B
#define PWM_OUTN        3         // PortB 3 OC1Bn; invertiertes Signal
#define DG_PINS (BIT(DG_PHA) | BIT(DG_PHB))  // Bitmuster
#define ENTPRELL_ZEIT   5         // 5 Zyklen -> 50ms entprellen

#define DEF_CPU_CLOCK 8000000
#define T0_MAX_WERT 77            // fuer 100Hz bei 8 MHz Takt und 1024 Vorteiler

#define MAX_STELLWERT   50        // VCC
#define MIN_STELLWERT   5         // 0,5 V
#define MAX_PWM_WERT    255       // per Hardware vorgegeben
#define STELLWERT_FAKTOR  (MAX_PWM_WERT/MAX_STELLWERT) // für max. Auflösung; eff. PWM-Werte 25 - 250
#define EE_STELLWERT    0x10      // Adresse für Stellwertablage


int16_t stellwert;

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)) { // nur geänderte Werte schreiben
    cli();
    EEAR = adr;
    EEDR = data;
    EECR  = BIT(EEMPE);           // master schreibbit setzen, EEPMx löschen
    EECR |= BIT(EEPE);            // schreibbit setzen
    sei();
  }
}

// Tastendekodierung wird mit 100Hz aufgerufen
// hier nur DG_ZERO auswerten
ISR(TIM0_COMPA_vect)
{
static uint8_t alter_wert, entprell_cntr;
uint8_t neuer_wert;
  neuer_wert = PINB & BIT(DG_ZERO);
  neuer_wert ^= BIT(DG_ZERO);         // neg. Signalpegel => aktiver Tastendruck
  if(alter_wert != neuer_wert) {
    entprell_cntr++;
    if(entprell_cntr >= ENTPRELL_ZEIT) {
      if((alter_wert ^ neuer_wert) & neuer_wert) {  // bei positiver Änderung
        stellwert = MIN_STELLWERT;                  // kleinsten Wert einstellen
      }         
      alter_wert = neuer_wert;
      entprell_cntr = 0;
    }
  }                    
}  

// T0 für 100 Hz Interrupts, T1 als PWM-Generator
void init_timer(void)
{
uint16_t temp;  
  TCCR0A = BIT(WGM01);              // CTC modus max = OCRA
  OCR0A = T0_MAX_WERT;              // ca. 100Hz
  TCCR0B = 5;                       // 1024 Vorteiler
  TIFR = BIT(OCIE0A);               // flag loeschen
  TIMSK |= BIT(OCIE0A);             // und wieder freigeben
  
// Timer1 für PWM-Signal  
  temp = MAX_STELLWERT*STELLWERT_FAKTOR+1;      // obere PWM-Grenze
  if(temp > MAX_PWM_WERT) temp = MAX_PWM_WERT;  // muss unbedingt begrenzt werden 
  OCR1C = temp;
  TCCR1 = BIT(CS10);                // T1 ohne Vorteiler starten
  GTCCR = BIT(PWM1B) + BIT(COM1B0); // PWM starten
  DDRB |= BIT(PWM_OUT) + BIT(PWM_OUTN); // OC1B und OC1Bn als Ausgang aktivieren
//  DT1B = 0x11;                        // bei Bedarf noch Totzeiten einfügen
  PLLCSR |= BIT(PLLE);              // 64 MHZ PLL einschalten
  while(!PLLCSR & BIT(PLOCK));      // Start abwarten
  PLLCSR |= BIT(PCKE);              // und als Timer1-Takt verwenden
}

void bewerte_dg()
{
static uint8_t alter_wert, init;
uint8_t muster, wechsel, neuer_wert;
  neuer_wert = PINB & DG_PINS;
  if(!init) {                       // direkt nach Reset 
    init = 1;                       // aktuelle Pegel als Ausgangswert
    alter_wert = neuer_wert;
  }    
  wechsel = alter_wert ^ neuer_wert;
  alter_wert = neuer_wert;
  muster = neuer_wert;
  if(wechsel & BIT(DG_PHA)) {
    if(muster == !(BIT(DG_PHA) | BIT(DG_PHB)) || muster == (BIT(DG_PHA) | BIT(DG_PHB)))
      stellwert++;
    else
     stellwert--;
  }
  
#ifdef VIERFACH_AUSWERTUNG        // nur bei Bedarf
  if(wechsel & BIT(DG_PHB)) {
    if(muster == BIT(DG_PHA) || muster == BIT(DG_PHB))
     stellwert++;
    else
     stellwert--;
  }
#endif
}  


// Beispielprogramm
int main(void)
{
int16_t alter_wert = 0;
  CLKPR = 0x80; CLKPR = 0x0;                // Vorteiler 8 MHz erzwingen
  PORTB |=  0xff;                           // alle pullup-Widerstände aktivieren

  stellwert = ee_read_byte(EE_STELLWERT);   // alten Wert lesen und ggf. auf Sollbereich anpassen
  if(stellwert > MAX_STELLWERT || stellwert < MIN_STELLWERT)
    stellwert = MIN_STELLWERT;
  init_timer();
  sei();

  while(1) {
    alter_wert = stellwert;
    OCR1B = stellwert*STELLWERT_FAKTOR;     // aktuellen Wert als PWM-Wert
    do {
      bewerte_dg();
    } while(alter_wert == stellwert);       // auf Änderung warten
    if(stellwert >= MAX_STELLWERT)          // im vorgebenen Rahmen halten
      stellwert = MAX_STELLWERT;
    if(stellwert <= MIN_STELLWERT)
      stellwert = MIN_STELLWERT;  
    ee_write_byte(stellwert, EE_STELLWERT); // neuen Wert sofort ins EEPROM legen
  }
}
