/*
PWM-Spannungssteller mit ATtiny25/45/85 mit mechanischen Einzeltastern
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 zwei Tasten SW_UP und
SW_DOWN 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 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.

Entprellung und Dekodierung der angeschlossenen Taster liefern einen Ausgangswert in der 
Variablen 'taste'. Eine gedrückte gehaltene Taste liefert zudem ja nach Tabelleneintrag einen Repeat-Wert,
der auch gesondert ausgewertet werden könnte. Beispielsweise Wertänderung mit +/- 5. Hier
wird nur die schnellere Änderung um +/- 1 verwendet.
Als 3. Funktion kann ein Signal an SW_ZERO den Ausgangswert auf einen Sollwert nach Bedarf
einstellen. Hier wird der PWM-Wert auf 0 gesetzt.

Die Funktion 'dekodiere_taste()' kann bis zu 32 Tasten erweitert werden und wertet immer nur 
eine einzelne Taständerung aus. Um auch Schalter entprellt auszulesen, kann der Tabelleneintrag
in 'tast_tab[]' leer ' ' bleiben und der aktuelle Zustand in 'entprellte_tasten' gelesen werden.

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

#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdlib.h>
#include <avr/eeprom.h>

#define BIT(x)  (1<<x)

#define SW_UP           0         // PortB 0
#define SW_DOWN         1         // PortB 1
#define SW_ZERO         2         // PortB 2
#define PWM_OUT         4         // PortB 4 OC1B
#define TASTEN  (BIT(SW_UP) | BIT(SW_DOWN) | BIT(SW_ZERO))  // Bitmuster

#define DEF_CPU_CLOCK 8000000
#define T0_MAX_WERT 77            // fuer 100Hz bei 8 MHz Takt und 1024 Vorteiler

#define MAX_BITS        8         // max. nur ein Port
#define ENTPRELL_ZEIT   5         // 5 Zyklen -> 50ms entprellen
#define FIRST_REPEAT    50        // 0,5 Sek. Pause bei gedrueckter Taste
#define NEXT_REPEAT     10        // danach Wiederholung alle 0,12 Sek.

#define MAX_STELLWERT   50        // VCC
#define MIN_STELLWERT   5         // 0,1 * VCC
#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

#define TASTENWERT      uint8_t   // wenn mehr als 8 Tasten auch short oder long
TASTENWERT entprellte_tasten;     // zur externen Verwendung
TASTENWERT taste, temp_tasten, letzte_aenderung;

uint8_t stellwert;
char rep_flag, entprell_ctr, rep_ctr;

uint8_t     tast_tab[] = "ABCDEFGH"; // 1. Tastencode für 8-Bit Port
uint8_t rep_tast_tab[] = "ab      "; // repeatcode teilweise zulassen

char lese_taste()
{
char temp;
  TIMSK &= ~BIT(OCIE0A);          // T0-COMPA-interrupt kurz verbieten
  temp = taste;                   // Tastencode lesen
  taste=0;                        // und loeschen
  TIMSK |= BIT(OCIE0A);           // T0-COMPA-interrupt und wieder freigeben
  return(temp);
}

// bei Änderung neue Taste bewerten, bei gedrückter Taste Repeat-Code erzeugen
void dekodiere_taste()
{
uint8_t n;
TASTENWERT tasten_schalter, aenderung;

// in 'tasten_schalter' muss das Eingangsbitmuster stehen hier nur PortA
  tasten_schalter = ((PINB ^ TASTEN) & TASTEN);  // PortA bit0 + 2 aktiv-low Eingang

  if(rep_ctr) rep_ctr--;                   // repeat-counter dekrementieren
  entprell_ctr++;                          // Entprell-counter inkrementieren
  if(tasten_schalter != temp_tasten) {     // Aenderung der Bits ?
    temp_tasten = tasten_schalter;         // ja, speichern
    entprell_ctr=0;                        // und neu entprellen
    return;                                // mehr nicht
  }
  if(entprell_ctr < ENTPRELL_ZEIT) return; // Entprellzeit abwarten

// Tasten sind entprellt; jetzt auf positive Aenderungen testen
  aenderung = (temp_tasten ^ entprellte_tasten) & temp_tasten;
  entprellte_tasten = temp_tasten;        // und letzten Zustand merken
  entprell_ctr=0;                         // Entprellen der Tasten neu starten

  if(aenderung) {                         // falls neue Tasten aktiviert
    letzte_aenderung = aenderung;         // Aenderung merken
    rep_ctr = FIRST_REPEAT;               // und 1.Repeat aktivieren
    rep_flag = 0;
  }
  else if(rep_ctr == 0 && rep_flag) {     // keine neue Taste aber repeat aktiv
    aenderung = entprellte_tasten & letzte_aenderung; // letzte Aenderung
    rep_ctr = NEXT_REPEAT;                // wiederholen, mit kurzer repeat-Zeit
  }

  n=0;                                    // Auswertung der Tasten
  do {                                    // bit suchen
    if(aenderung & 1) {                   // bit gefunden
      if(aenderung == 1) {                // und nur eine Taste erlaubt
        if(!rep_flag) {                   // kein repeat: normale Codes verwenden
          n = tast_tab[n];
          rep_flag = 1;                   // aber beim naechsten Mal repeat !
        } else
          n = rep_tast_tab[n];            // repeat ist schon aktiv
        if(n != ' ') taste = n;           // nur gueltigen Code speichern
        else rep_flag = 0;                // kein repeat, wenn keine gueltige taste
      }
      return;                             // taste erkannt, dekodierung fertig
    }
    n++;
    aenderung >>= 1;
  } while(n < MAX_BITS);
// keine taste gefunden
}


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
ISR(TIM0_COMPA_vect)
{
  dekodiere_taste();
}  

// 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);             // OC1B als Ausgang aktivieren
  PLLCSR |= BIT(PLLE);              // 64 MHZ PLL einschalten
  while(!PLLCSR & BIT(PLOCK));      // Start abwarten
  PLLCSR |= BIT(PCKE);              // und als T1-Takt verwenden
}

// Beispielprogramm
int main(void)
{
char temp;
  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) {
    OCR1B = stellwert*STELLWERT_FAKTOR;     // aktuellen Wert als PWM-Wert
    do {
      temp = lese_taste();
    } while(!temp);      
    switch(temp) {
      case 'A':                             // aufwärts
      case 'a':                             // Repeat-Funktion dazu
        if(stellwert < MAX_STELLWERT) stellwert++;
        break;
      case 'B':                             // abwärts
      case 'b':                             // Repeat-Funktion dazu
        if(stellwert > MIN_STELLWERT) stellwert--;
        break;
      case 'C':                             // auf 0 setzen
        stellwert = 0;                      // oder Funktion nach Bedarf ausführen
        break;
    }
    ee_write_byte(stellwert, EE_STELLWERT); // neuen Wert sofort ins EEPROM legen
  }
}

