Source file: /~heha/ewa/Ofen/prozess.zip/avr/o1/o1.cpp

/* Firmware für den ATmega32U4 eines Arduino Pro Micro
   Zuständig: Fabian Lorenz, 32482
   Henrik Haftmann
 190715	erstellt
+191104	WebUSB/WinUSB-Interface (neben HID)
*191114 Klarstellung der Leistungsberechnung
-191115	Überall Regler-Begrenzung
*191115 Änderung der Konstanten, erfordert Änderung der Applikation
+191115	Konfigurierbare Tabellenlängen
+191115	Cookies = Freiraum für Browser
+191115	Historiendaten im RAM (1800 Werte) mit automatischer Ausdünnung
-191121	Historiendaten inklusive „NaN-Differenzen“ (0x80)
Aufgaben:
1. Heizungssteuerung für Ofen, mit Thermoelement Typ K
2. (optional) digitale Ausgänge zur Gassteuerung; Startknopf
Die Steuerung erfordert zumindest zum Einrichten einen PC
mit zugehörigem Windows- oder WebUSB-Programm.
Das Starten und Fahren der Temperaturkurve geht auch ohne PC, dann unbeobachtet.
Die Reglerkennwerte (PID) sind frei einstellbar.

Alle hier verwendeten Strukturen müssen dem Applikationsprogramm bekannt sein;
nur die Struktur des Message-Reports ist im Report-Deskriptor abgelegt.

Hardware:
24-V-Netzteil und Vierphasen-Schütz im Hutschienengehäuse.
Dazu Platine mit 5-V-Tiefsetzsteller, dem Arduino (mit ubaboot),
einem ULN2003 sowie einem Thermoelement-Modul mit MAX6675.

--  8	PB0	SS			(!LED RX)
15  9	PB1	SCL			MAX6675: CLK
16 10	PB2	MOSI			(Ausgang)
14 11	PB3	MISO			MAX6675: DO
 8 28	PB4			ADC11	Start/Stopp-Taste mit LED
 9 29	PB5	OC1A	!OC4B	ADC12	Digitaler Eingang B5
10 30	PB6	OC1B	 OC4B	ADC13	Digitaler Eingang B6
-- 12	PB7	OC1C	 OC0A	!RTS	-

 5 31	PC6	OC3A	!OC4A		Ausgang zum Schütz
-- 32	PC7	ICP3	 OC4A	CLK0	-

 3 18	PD0	SCL	 OC0B		Digitaler Ausgang D0
 2 19	PD1	SDA			Digitaler Ausgang D1
RX 20	PD2	RxD			Digitaler Ausgang D2
TX 21	PD3	TxD			Digitaler Ausgang D3
 4 25	PD4	ICP1		ADC8	Digitaler Ausgang D4
-- 22	PD5	XCK		!CTS	(!LED TX)
-- 26	PD6	T1	!OC4D	ADC9	-
 6 27	PD7	T0	 OC4D	ADC10	Digitaler Ausgang D7

-- 33	PE2	!HWB			-
 7  1	PE6			AIN0	MAX6675: !CS

-- 41	PF0			ADC0	-
-- 40	PF1			ADC1	-
A3 39	PF4	TCK		ADC4	frei
A2 38	PF5	TMS		ADC5	frei
A1 37	PF6	TDO		ADC6	frei
A0 36	PF7	TDI		ADC7	frei

--  3	-	D-			USB Data-
--  4	-	D+			USB Data+
--  7	-	Ubus			USB-Busspannung
RS 13	-	!Reset			Reset-Eingang
-- 16	-	XTAL2			16-MHz-Quarz
-- 17	-	XTAL1			16-MHz-Quarz
-- 42	-			AREF	Kondensator
--  6	-	UCAP			Kondensator
2,34,14,44,24	UUcc,Ucc,AUcc		5P
5,15,23,35,43	UGND,GND		GND

Verwendung der Timer:
0	frei
1	250-ms-Zeitgeber für Listenabarbeitung, Temperaturmessung und Regler
3	Langsame PWM für Schütz
4	frei
*/

#include <avr/io.h>
#include <avr/eeprom.h>
#include <avr/pgmspace.h>	// PSTR
#include <avr/signature.h>
#include <util/delay.h>
#include "usb.h"
#include <string.h>

// limitierende Funktion, Wrap-Around ist tödlich für jede Art von Regler
// (siehe Ariane 5)
int toint(long v) {
 if (v>32766) return 32766;
 if (v<-32766) return -32766;
 return int(v);
}

/**************
 * PID-Regler *
 **************/

struct PID{
 int Kp,Ki,Kd;		// Ki, Kd bezogen auf Abtastrate 2,5 Hz
};

static struct regler{
 int esum,ealt;
 int regle(int,const PID&);
 int regle(int,int);
 void reset() {esum=ealt=0;}
}regler;

int regler::regle(int e,const PID&pid) {
 esum=toint((long)esum+e);	// Alle Überläufe begrenzen!
 int delta=toint((long)e-ealt);
 ealt=e;
 long y=(long)pid.Kp*e + (long)pid.Ki*esum + (long)pid.Kd*delta;
 return toint(y>>8);
}

// Teperaturspezifische PID-Werte sowie Korrekturwerte
struct TPID:public PID{
 int hold;		// Y-Wert zum Halten der Temperatur
 int f_up;		// Y-Faktor für Temperaturanstieg
 int f_dn;		// Y-Faktor für Temperaturabfall
 int T;			// Temperaturwert für Lookup-Tabelle
 static const TPID*findEntry(int T);
 static bool allValid();
 void approx(TPID&pid) const;
 const TPID&next() const {return *(this+1);}
};

void TPID::approx(TPID&tpid) const{
// Vorgabe: tpid.T = Isttemperatur
 int f=tpid.T-T;
 int n=next().T-T;
// Alles außer T (= 6 Werte) linear approximieren
 for (const int*p=&Kp;p<&T;p++) {
  *(&tpid.Kp+(p-&Kp))=p[0]+(p[sizeof*this/sizeof*p]-p[0])*(long)f/n;
 }
}

int regler::regle(int e, int T) {
 const TPID*p=TPID::findEntry(T);
 TPID tpid;		// Neue TPID-Struktur
 tpid.T=T;		// Eingabe für approx()
 p->approx(tpid);	// Kp bis f_dn linear approximieren aus Tabelle
 long y=regle(e,tpid);	// PID-Regler
 long yc=y+tpid.hold;	// Korrektur: Offset
 yc+=y*(y<0?tpid.f_dn:tpid.f_up)>>8;	// Korrektur: Faktor
 return toint(yc);
}

/*********************
 * Heizungssteuerung *
 *********************/

// Die Schwingungspaketsteuerung mit einer Paketlänge von 4 s
// kann von 0 (aus) bis 400 (volle Heizleistung) „regeln“.
// Da der Wert direkt vom Regler kommt, bewirkt eine Änderung der Paketlänge
// eine Änderung des Regelverhaltens; bei gleichem Regelverhalten
// sollten die PID-Werte mitskaliert werden.
// Das erspart Division in der Firmware; das soll die Applikation machen!

struct burstcontrol{
 static void set(int,int);
 static void off()	{OCR3A = 0;}
 static constexpr word len(word len10ms) {	// Umrechnen 10-ms-Schritte in OCR-Wert
  return len10ms*10000UL>>6;
 }
 static void setLen(word len10ms) {
  ICR3 = len(len10ms)-1;
 }
};

void burstcontrol::set(int y, int burstlen) {
 if (y<0) y=0;
 else if (y>burstlen) y=burstlen;
 OCR3A=burstcontrol::len(y);	// Schütz steuern
}

/*******************
 * Prozessschritte *
 *******************/

struct STEP{
 word time;	// absolute Zielzeit, in s (max. 18 h)
 int temp;	// Zieltemperatur, in 1/4 °C (0..1000 °C)
 byte digo,digc;	// Ausgänge: Wert und Änderungsmaske
 static const STEP*findEntry(word t); 
 static bool allValid();
 int solltemp(word t) const;
 void setOutputs() const;
 const STEP&next() const {return *(this+1);}
};

// lineare Temperaturkurve zwischen 2 Schritten
// this.time <= t <= next().time!
int STEP::solltemp(word t) const{
 word a=t-time;
 word b=next().time-time;	// a/b = Lage im Intervall = 0..1
 int c=next().temp-temp;	// c = Temperaturdifferenz
 return temp+int(c*(long)a/b);
}

void STEP::setOutputs() const{
 PORTD=PORTD&~digc^digo;
}

/*****************
 * EEPROM-Inhalt *
 *****************/

static struct{
 byte repid2;
 byte ntpid;		// Anzahl TPIDs, mindestens 2
 TPID tpids[4];		// max. 4 Einträge (Regelparameter)
 byte repid3;
 int burstlen;		// Länge der PWM in 10-ms-Schritten
 word fullheat;		// Cookie: Volle Heizleistung in W (von Firmware ungenutzt)
 word fullcool;		// Cookie: Volle Kühlleistung in W (von Firmware ungenutzt)
 byte repid4;
 byte nstep;		// Anzahl Schritte, mindestens 2
 STEP steps[10];	// max. 10 Einträge (Prozessschritte)
 byte repid14;
 byte cookie[COOKIESIZE];	// Plotfarben, Plotattribute u.ä.
}cfg;			// Der erste Schritt sollte t=0 haben
bool ee_dirty;

// Liefert Zeiger auf den ersten bis maximal vorletzten Eintrag zur Interpolation,
// niemals <nullptr>
const TPID*TPID::findEntry(int T) {
 byte cnt=cfg.ntpid-1;
 const TPID*p=cfg.tpids;
 do{
  if (T<(++p)->T) return --p;
 }while(--cnt);
 return --p;
}

bool TPID::allValid() {
 byte cnt=cfg.ntpid;
 if (cnt<2) return false;		// Mindestens 2 Elemente
 if (cnt>elemof(cfg.tpids)) return false;
 const TPID*p=cfg.tpids;
 int T=p->T;
 --cnt;
 do{
  if ((++p)->T<=T) return false;	// Temperatur muss streng monoton steigen
  T=p->T;
 }while(--cnt);
 return true;
}

// Liefert Zeiger auf ersten bis letzten Eintrag, <nullptr> wenn <t> zu groß
// Der letzte Eintrag wird nur bei _exaktem_ Treffer geliefert.
const STEP*STEP::findEntry(word t) {
 byte cnt=cfg.nstep-1;
 const STEP*s=cfg.steps;
 do{
  if (t<(++s)->time) return --s;	// Nachfolgender mit größerer Zeit: Diesen zurückgeben
 }while(--cnt);
 if (t==s->time) return s;
 return 0;		// Zeit >= Eintrag im letzten Listenelement = Programmende
}

bool STEP::allValid() {
 byte cnt=cfg.nstep;
 if (cnt<2) return false;
 if (cnt>elemof(cfg.steps)) return false;
 const STEP*s=cfg.steps;
 word t=s->time;
 if (t) return false;	// Erste Zeit muss 0 sein
 --cnt;
 do{
  if ((++s)->time<=t) return false;	// Zeit muss streng monoton steigen
  t=s->time;
 }while(--cnt);
 return true;
}

static bool burstlen_valid() {
 return 100<=cfg.burstlen && cfg.burstlen<=400;
}

// Asynchrones Update: Wartet nicht sondern stößt EEPROM-Schreibvorgang nur an
// Liefert FALSE wenn's nichts zu updaten gibt
static bool eeprom_update(const void*ram, void*eeprom, size_t len) {
 if (EECR&0x02) return true;
 const byte*src=(const byte*)ram;
 byte*dst=(byte*)eeprom;
 for (;len;src++,dst++,len--) {
  EEAR=(word)dst;
  EECR|=1;
  if (EEDR!=*src) {
   EEDR=*src;	// Adresse steht noch in EEAR
   EECR|=0x04;
   EECR|=0x02;
   return true;
  }
 }
 return false;
}

void bootstart() __attribute__((naked,noreturn));
void bootstart() {
 USBCON=0x20;
 UDCON =0x01;
 SMCR  =0;
 DDRB  =0;
 DDRC  =0;
 DDRD  =0;
 DDRE  =0;
 PORTB =0;
 PORTC =0;
 PORTD =0;
 PORTE =0;
 asm("jmp 0x7E00");	// Adresse des Urladers (hier: ubaboot)
}

// Infinity und Not-a-Number werden hier bei Integerzahlen behandelt:
static const int iNaN=-0x8000;
static const int iInf= 0x7FFF;
static const char cNaN=-128;
static const char cInf= 127;
// Die Schönheit dieser Festlegung (aus: Schrittmotorsteuerung) ist:
// abs(-iInf) == iInf bzw. abs(-cInf) == cInf sowie -iNaN == iNaN bzw. -cNaN == cNaN,
// damit die Symmetrie der Wertebereiche
// Schade dass es dafür (noch) keine Prozessoren / ALUs gibt.

// liefert cInf, cNaN, -cInf (als Klassifizierung) oder 0
static char isNanInf(int v) {
 v-=iInf;
 if (unsigned(v)<3) return char(v+cInf);
 return 0;
}
static char isNanInf(char v) {
 return byte(v-127)<3 ? v : 0;
}
static char zeroNanInf(char v) {
 return byte(v-127)<3 ? 0 : v;
}

/*************************
 * Historiendaten im RAM *
 *************************/
// Gespeichert wird nur die MAXIMALE Temperatur in Differenzen
template<typename T,word L>struct History{
 byte repid;		// (0) 13
 byte maxdivisor;	// (1) 0 == 256, nur 2^n erlaubt
 word fill;		// (2) Füllstand, 0..1800
 byte divisor;		// (4) 0 == 256 (= 64 Sekunden), macht 32 Stunden!
 byte ticker;		// (5) um jeden n-ten Messwert aufzunehmen
 T firstvalue;		// (6) Erster Nicht-NanInf-Wert
 T lastvalue;		// (8) (zur Kontrolle für den Report-Empfänger)
 char diffs[L];		// (10) Index 0 ist zumeist 0, manchmal Inf/NaN
 void push_back(T);
 void clear() {divisor=1; fill=0; ticker=0; firstvalue=iNaN;}
 void reduce();
 void shift(word);
 static constexpr char limit(T v) {return v>126 ? 126 : v<-126 ? -126 : (char)v;}
};

// Abspeichern als Startwert oder Differenz; Überläufe werden beachtet
// NaN/Inf werden beachtet und direkt (nicht als Differenz) gespeichert
template<typename T,word L> void History<T,L>::push_back(T value) {
 if (++ticker!=divisor) return;
 ticker=0;
 char d=isNanInf(value);
 if (!d && isNanInf(firstvalue)) firstvalue=lastvalue=value;
 if (fill==L) reduce();
 if (!d) {
  d=limit(value-lastvalue);
  lastvalue+=d;	// Bei Überlauf tun wir so als könnte die Temperatur nicht um mehr als 32 K springen
 }
 diffs[fill]=d;
 fill++;
}

// Halbiert den Datenbestand durch Zusammenfassung zweier Nachbarn
// fill muss gerade sein! Typischerweise == 1800
// Auf Überläufe wird geachtet: Temperatursprünge werden „in die Länge gezogen“
// Ab jetzt kann diffs[0] !=0 sein; lastvalue kann sich bei Temperatursprüngen ändern
// firstvalue ändert sich nicht.
template<typename T,word L> void History<T,L>::reduce() {
 if (divisor==maxdivisor) {shift(fill>>1); return;}
 T v=firstvalue,n=v;		// n = gelesener Temperaturwert
 for (word i=0; i<fill; i+=2) {		// zuerst diffs[0]=diffs[0]+diffs[1],
  char a=diffs[i],b=diffs[i+1],d=a;	// dann   diffs[1]=diffs[2]+diffs[3]
  if (!isNanInf(a) || !isNanInf(b)) {	// bis    diffs[899]=diffs[1798]+diffs[1799]
   n+=zeroNanInf(a);
   n+=zeroNanInf(b);
   d=limit(n-v);	
   v+=d;	// v folgt n so gut es geht: Werden 2 Differenzen zu groß hinkt v nach
  }
  diffs[i>>1]=d;
 }	
 lastvalue=v;
 fill>>=1;	// wird 900
 divisor<<=1;	// Teiler verdoppeln = Abspeicherrate halbieren
}

// Bei Maximalteiler (oder auch eher) Inhalt schieben, nicht ausdünnen
template<typename T,word L> void History<T,L>::shift(word t) {
 T v=firstvalue;
 for (word i=0; i<t; i++) v+=zeroNanInf(diffs[i]);
 firstvalue=v;
 memcpy(diffs,diffs+t,fill-=t);	// Das Verhalten von memcpy bei überlappendem Speicher ist hier bekannt
}

History<int,HISTLEN> history NOINIT;

/*********************
 * Temperaturmessung *
 *********************/
struct max6675{
 static int readTemp();	// 1/4 °C
 static int readVal();
private:
 static byte readByte() {SPDR=0;while(!(SPSR&0x80));return SPDR;}
};

int max6675::readVal() {
 PORTE&=~0x40;		// PE6 = !CS = Low
 union{
  int i;
  byte b[2];
 }r;
 r.b[1]=readByte();	// Ergebnisbyte
 r.b[0]=readByte();
 PORTE|= 0x40;		// PE6 = !CS = High
 return r.i;		// Bit 2 = offener Eingang
}

int max6675::readTemp() {
 int i=readVal();
 if (i&4) return iNaN;
 return i>>3;
}

/*******************************************
 * Regelmäßiges Nachrichtenbündel für Host *
 *******************************************/
struct msg_t{	// Länge: 16 Bytes
 byte repid1;	// 1
 byte flags;	// Zustände (in Klammern zugehörige Report-IDs zum Setzen)
		// (56) Bit 0: Regler läuft
		// (57) Bit 1: Programm läuft
		// (58) Bit 2: Programm erfolgreich beendet
		// (59) Bit 3: Taste gedrückt
		// (60) Bit 4: Reglerparameter OK
		// (61) Bit 5: PWM-Parameter OK
		// (62) Bit 6: Programm OK
		// (63) Bit 7: EEPROM OK
 byte relays;	// (64) Zustand der Relais
 byte inputs;	// (65) Zustand digitaler Eingänge (2 vorhanden)
 word runTime;	// (66) Zeit in s seit Programmstart
 int heatPower;	// (67) Heizleistung = Ausgangswert des PID-Reglers, in W*
 int tempSoll;	// (68) Temperatursollwert aus Liste, linear interpoliert
 int tempIst[3];// (69..71) Temperaturistwert von bis zu 3 Thermoelementen, in 1/4 °C
 void start();	// (in Klammern die zugeordneten Usage-Werte bzw. Report-IDs)
 void advance();// Alle int-Werte sind mit 0x7FFF = +INF, 0x8000 = NaN, 0x8001 = -INF
 void stop();
 void setHeatPower(int);
 void measure();
 static bool keypress();
}msg;

//* Die Leistung in W errechnet sich folgendermaßen:
//	Der Füllgrad der Schwingungspaketsteuerung ergibt sich zu
//	heatPower/burstlen, wobei burstlen (100..400) vom Report 3 kommt.
//	Ist die Angabe < 0 oder > 1, liegt Regelüberlauf vor; muss begrenzt werden.
//	Anschließend wird diese Angabe mit der Maximalleistung multipliziert, fertig.
// Derzeit ist die Ofensteuerungs-Firmware nicht zum Kühlen vorbereitet.
// Dafür wäre bspw. ein zweiter PWM-Ausgang vonnöten, der den Kühler ansteuert.

void msg_t::start() {
 byte f=flags;
 if ((f&0x70)==0x70) {	// Regler OK, PWM OK und Programm OK?
  f|=3;			// Regler und Programmablauf EIN
  f&=~4;		// „Erfolgreiches Ende“ AUS
  flags=f;
  runTime=0;
  regler.reset();
  history.clear();
 }
}
void msg_t::advance() {	// Aufruf alle 250 ms
 static byte subcount;	// Unterzähler für runTime
 if (!(subcount+=0x40) && flags&2) {
  const STEP*s=STEP::findEntry(++runTime);
  if (s) {
   tempSoll=s->solltemp(runTime);
   if (s->time==runTime) s->setOutputs();
   PINB |= 0x10;	// Tasten-LED blinken lassen
  }else{
   stop();
   flags|=4;		// Erfolgreiches Ende
  }
 }
}
void msg_t::stop() {
 flags&=~2;
 tempSoll=0;
 burstcontrol::off();
}
void msg_t::setHeatPower(int p) {
 heatPower=p;
 if (msg.flags&0x20) burstcontrol::set(p,cfg.burstlen);
}

void msg_t::measure() {
 relays=PIND&0x9F;	// immer aktualisieren
 inputs=PINB&0x60;
 int tempmax=tempIst[0]=max6675::readTemp(); // alle 250 ms lesen, nicht öfter!
 history.push_back(tempmax);
 if (flags&1) setHeatPower(regler.regle(tempSoll-tempmax,tempSoll));
}
bool msg_t::keypress() {
 DDRB &=~0x10;	// LED+Taste auf Eingang
 byte outstate=PORTB;
 PORTB|= 0x10;	// Pullup aktivieren
 ADCSRB= 0x60;	// ADC-Multiplexer auf Analogvergleicher-Minus
 ACSR  = 0x40;	// 1,1 V auf Analogvergleicher-Plus
 ADMUX = 0x03;	// ADC11 auf Analogvergleicher-Minus
 _delay_us(70);
 byte keystate=ACSR;	// Bit 5 = 1 wenn Taste gedrückt
 ACSR  = 0x80;	// wieder aus
 PORTB = outstate;	// LED-Zustand wiederherstellen
 DDRB |= 0x10;	// LED+Taste auf Ausgang
 byte f=msg.flags;
 if (!(keystate&0x20) && f&8) {	// Taste losgelassen
  f&=~8;
 }else if (keystate&0x20 && !(f&8)) {	// Taste gedrückt
  f|=8;
  msg.flags=f;
  return true;	// liefert einmalig <true> beim Drücken der Taste
 }
 msg.flags=f;
 return false;
}

/*****************************
 * Host-Anfragen beantworten *
 *****************************/
void onEp0GetReport(byte reportID) {
#define W(x) byte(x),byte((x)>>8)
 static const PROGMEM byte reportK[1+KONSTLEN]={
   15,		// Report-ID
   NETZFREQ*2,	// Doppelte Netzfrequenz in Hz
   W(250),	// Zeitschritt in ms für Regler
   W(1000),	// Zeitschritt in ms für Prozess
   W(250),	// Temperaturschritt in mK
   W(256),	// Divisor beim PID-Regler
   W(256),	// Divisor bei den beiden Reglerfaktoren
   MAXPID,	// maximale Regler-Einträge
   MAXSTEP,	// maximale Prozess-Schritte
   W(COOKIESIZE),
   W(HISTLEN)};
#undef W
 switch (reportID) {
  case 1: usbEp0Send(&msg,sizeof msg); break;
  case 2: usbEp0Send(&cfg.repid2,2+sizeof cfg.tpids); break;	// Reglerparameter
  case 3: usbEp0Send(&cfg.repid3,1+6); break;			// PWM-Periode
  case 4: usbEp0Send(&cfg.repid4,2+sizeof cfg.steps); break;	// Ablauf
  case 13: usbEp0Send(&history,sizeof history); break;		// Historiendaten
  case 14: usbEp0Send(&cfg.repid14,1+COOKIESIZE); break;	// Cookie (bspw. Plotfarben)
  case 15: usbEp0Send(FP(reportK),sizeof reportK); break;	// Konstanten
 }
}

void onEp0SetReport(byte reportID) {
 static byte outrep[3];
 switch (reportID) {
  case 2: {	// Feature: Reglerparameter
   usbEp0Recv(&cfg.repid2,1+1+sizeof cfg.tpids);
   byte f=msg.flags&~0x90;
   if (TPID::allValid()) f|=0x10;
   msg.flags=f;
  }break;
  case 3: {	// Feature: Schwingungspaketsteuerungs-Periode und Maximalleistungen
   usbEp0Recv(&cfg.repid3,1+6);
   byte f=msg.flags&~0xA0;
   if (burstlen_valid()) {
    f|=0x20;
    burstcontrol::setLen(cfg.burstlen);	// Periodenlänge sofort ändern
   }
   msg.flags=f;
  }break;
  case 4: {	// Feature: Programmschritte (Ablauf)
   usbEp0Recv(&cfg.repid4,1+1+sizeof cfg.steps);
   byte f=msg.flags&~0xC0;
   if (!(f&2)) {
    f&=~4;		// „Erfolgreiches Ende“ weg
   }
   if (STEP::allValid()) f|=0x40;
   msg.flags=f;
  }break;
  case 12: {	// Historiendaten werden nur gelöscht
   usbEp0Recv(outrep,2);
   history.clear();
   history.maxdivisor=outrep[1];	// Bonus: maxdivisor kann gesetzt werden!
  }break;
  case 14: {
   usbEp0Recv(&cfg.repid14,1+COOKIESIZE);
  }break;
  case 56: {	// Out: Start/Stopp Regler
   usbEp0Recv(outrep,2);
   if (outrep[1]&1) {
    if (!(msg.flags&1)) regler.reset();
    if (msg.flags&0x10) msg.flags|=1;	// Regler ein wenn Reglerparameter OK
   }else if (!(msg.flags&2)) {		// Wenn kein Prozess läuft:
    msg.flags&=~1;			// Regler aus
    msg.setHeatPower(0);		// (sicherheitshalber) nullsetzen
   }
  }break;
  case 57: {	// Out: Start/Stopp Prozess
   usbEp0Recv(outrep,2);		// schaltet Ablauf ein/aus
   if (outrep[1]&1) msg.start();
   else msg.stop();
  }break;
  case 64: {	// Out: Relais (nur sinnvoll wenn Prozess NICHT läuft)
   usbEp0Recv(outrep,2);
   PORTD = outrep[1];			// Relais (ohne Programm) steuern
  }break;
  case 66: { 	// Out: Prozesszeit beeinflussen (nur sinnvoll wenn Prozess läuft)
   usbEp0Recv(outrep,3);		// vor- oder zurückspulen
   msg.runTime=*(int*)(outrep+1);
  }break;
  case 67: {	// Out: Heizleistung setzen (nur sinnvoll Regler nicht läuft)
   usbEp0Recv(outrep,3);
   msg.setHeatPower(*(int*)(outrep+1));	// einfach heizen
  }break;
  case 68: {	// Out: Solltemperatur setzen (nur sinnvoll wenn Prozess NICHT läuft UND Regler läuft)
   usbEp0Recv(outrep,3);
   msg.tempSoll=*(int*)(outrep+1);	// Regelung (ohne Programm) nutzen
  }break;
  case 99: {	// Out: Urlader anspringen (ohne Daten) = Hintertür (geht das??)
   usbEp0Recv(outrep,2);
   if (outrep[1]&1) bootstart();
  }break;
  default: UECONX=0x21; return;		// stall
 }
 UEINTX=~1;
}

/*************************************
 * Initialisierung und Hauptprogramm *
 *************************************/
static void setupHardware(void) {
 MCUCR = 0x80;	// JTAG deaktivieren (sonst gehen PF4..PF7 = ADC4..ADC7 nicht)
 CLKPR = 0x80;
 CLKPR = 0x04;	// Takt auf 1 MHz drosseln
 PORTB = 0xEC;	// Pullups an Eingängen, Low an Ausgängen
 DDRB  = 0x17;	// LED RX = !SS und MAX6675::CLK = Ausgang, Low
 PORTC = 0x80;
 DDRC  = 0x40;	// Schütz = Ausgang
 PORTD = 0;
 DDRD  = 0xFF;	// alles Ausgänge, auch LED TX
 PORTE = 0x44;
 DDRE  = 0x40;	// MAX6675::!CS = Ausgang, High
 PORTF = 0xFF;
 SPCR  = 0x51;	// SPI-Freigabe als Master, CPOL=CPHA=0, 1 MHz
 ICR1  = 15624+15625;
 TCCR1A= 0x02;	// WGM-Modus: 14 = Fast PWM mit ICR1 als TOP-Wert
 TCCR1B= 0x1A;	// Vorteiler 256, Zählumfang 15625 = 250 ms = 4 Hz
 burstcontrol::setLen(cfg.burstlen);
 TCCR3A= 0x82;	// WGM-Modus: 14 = Fast PWM mit ICR3 als TOP-Wert
 TCCR3B= 0x1B;	// Vorteiler 1024, Zählumfang bis 4 Sekunden
}

static void idle() {
 if (!(msg.flags&0x80) && !eeprom_update(&cfg,0,sizeof cfg)) msg.flags|=0x80;
 if (TIFR1&0x01) {	// Überlauf Zähler 1? (250 ms)
  TIFR1 = 0x01;		// löschen
  PIND |= 0x20;		// TX-LED blinken lassen
  if (!(msg.flags&2)) PINB|=0x10;	// Tasten-LED aufblitzen lassen (= Aktivität)
  if (msg.keypress()) {
   if (msg.flags&2) {	// Prozess läuft?
    msg.stop();
   }else if (msg.flags&4) {	// Prozessende OK?
    msg.flags&=~4;
    PORTB&=~0x10;	// Nur Prozessende-Flag löschen
   }else msg.start();
  }else msg.advance();
  msg.measure();
  static_assert(sizeof msg<=16,"Passt nicht in Endpoint");
  usbEp1Send(&msg,sizeof msg);	// HID informieren
  usbEp3Send(&msg,sizeof msg);	// WebUSB informieren
  if (msg.flags&4) PORTB|=0x10;			// LED ein
  else if (!(msg.flags&2)) PORTB&=~0x10;	// LED aus
 }
 if (TIFR3&0x01) {	// Überlauf Zähler 3? (einstellbar 1..4 s)
  TIFR3=0x01;		// löschen
  PINB|=0x01;		// RX-LED blinken lassen
 }
 usbPoll();
}

int main() {
 msg.repid1=1;		// Report-IDs (als einziges !=0) initialisieren
 cfg.repid2=2;
 cfg.repid3=3;
 cfg.repid4=4;
 cfg.repid14=14;
 cfg.ntpid=2;
 cfg.tpids[0].Kp=256;	// Nur Proportionalwerte initialisieren
 cfg.tpids[1].Kp=256;
 cfg.tpids[1].T=4000;	// 1000 °C
 cfg.burstlen=400;	// 4-Sekunden-Schwingungspaketsteuerung (hier: Relais)
 if (eeprom_read_byte(0)==2)
   eeprom_read_block(&cfg,0,sizeof cfg);	// alles in einem Rutsch
 if (TPID::allValid()) msg.flags|=0x10;
 if (burstlen_valid()) msg.flags|=0x20;
 if (STEP::allValid()) msg.flags|=0x40;
 *((byte*)&msg.tempIst+3)=0x80;	// mit NaN initialisieren
 *((byte*)&msg.tempIst+5)=0x80;
 history.repid=13;
 history.maxdivisor=0;
 history.clear();
 setupHardware();
 usbInit();
 for(;;) idle();
}

/************************************************
 * Sag was du liest (oder Englisch ist Scheiße) *
 ************************************************/
/*	    gh	  o	     t     i	  Ergebnis:
ghoti:	enough	 women	   nation	  fish	(Fisch)
ghoti:	  night	people	ballet	business  ÷	(stumm)
*/
Detected encoding: UTF-80