/* 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-8 | 0
|