Source file: /~heha/enas/Convac-Ätzer/Firmware-190517.zip/main.cpp

/* Firmware für den ATmega32U4 eines Arduino Micro
   für den Convac-Ätzer
   Henrik Haftmann, 161011
Aufgaben:
1. Ansteuerung des LC-Displays, Tastenabfrage, Tonerzeugung
2. Benutzerschnittstelle
3. Maschinenschnittstelle
4. Programmier-Interface für den Anwender

Mit USB, dient zur Programmierung sowie zum Backup/Restore des EEPROM-Inhalts)

Hardware (nach Ports sortiert; 44 Pins):
SS  8	PB0 i0	SS			D0	Bus:D0, LED RX (high-aktiv)
SC  9	PB1 i1	SCL			D1	Bus:D1
MO 10	PB2 i2	MOSI			D2	Bus:D2
MI 11	PB3 i3	MISO			D3	Bus:D3
 8 28	PB4 i4			ADC11	D4	Bus:D4
 9 29	PB5 i5	OC1A	!OC4B	ADC12	D5	Bus:D5
10 30	PB6 i6	OC1B	 OC4B	ADC13	D6	Bus:D6
11 12	PB7 i7	OC1C	 OC0A	!RTS	D7	Bus:D7

 5 31	PC6	OC3A	!OC4A			(PWM-Ausgang)
13 32	PC7	ICP3	 OC4A	CLK0		frei

 3 18	PD0 I0	SCL	 OC0B		A0	Bus:A0
 2 19	PD1 I1	SDA			A1	Bus:A1
RX 20	PD2 I2	RxD			A2	Bus:A2
TX 21	PD3 I3	TxD			A3	Bus:Select
 4 25	PD4	ICP1		ADC8	A4	Bus:Select
-- 22	PD5	XCK		!CTS	-	LED TX (high-aktiv), nicht herausgeführt
12 26	PD6	T1	!OC4D	ADC9	A5	Bus:Select
 6 27	PD7	T0	 OC4D	ADC10	TON	Display:Ton (getauscht mit PD6 170403)
						Der Piezo ist dummerweise selbstschwingend.

-- 33	PE2	!HWB			-	nicht herausgeführt, via 10 kΩ auf Masse gelegt
 7  1	PE6 I6			AIN0	A6	Bus:Select Enable

A5 41	PF0			ADC0	!WR	Bus: Schreiben
A4 40	PF1			ADC1	!IOSTB	Bus: I/O-Zyklus
A3 39	PF4	TCK		ADC4	X0	Bus:X0
A2 38	PF5	TMS		ADC5	X1	Bus:X1
A1 37	PF6	TDO		ADC6	X2	Bus:X2
A0 36	PF7	TDI		ADC7	!OE	Bus:!OUT/HALT (High = alle Ausgänge inaktiv)

    3	-	D-			D-	USB Data-
    4	-	D+			D+	USB Data+
    7	-	Ubus			Ubus	USB-Busspannung
RS 13	-	!Reset			-	Reset-Taste
   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

Die Buszuordnung ist wegen der Notwendigkeit der Subadressen
und kaum noch freien Pins fraglich geworden.
Beim nächsten Mal ein zusätzliches Adresslatch (74HC574) spart Pins,
erlaubt damit die Verwendung des billigeren Boards „Pro Micro“.
bisher	künftig
X0	A0	via '574 auf Bus
X1	A1	via '574 auf Bus
X2	A2	via '574 auf Bus
A0	A3	via '574 auf Bus
A1	A4	via '574 auf Bus
A2	A5	via '574 auf Bus
A3	A8	via '574 an Adressdekoder A0
A4	A9	via '574 an Adressdekoder A1
A5	A10	Portpin an Takteingang des '574 sowie an Adressdekoder A2
!OE	!OE	Portpin an !OE-Eingang des '574, an !CE des Adressdekoders
		sowie an !OUT/HALT auf Bus
Statt 11 werden nur 2 Portpins benötigt.
Der Adressbuss ist vom Arduino vollständig entkoppelt.

Verwendung der Timer:
0
1	Uhr 100 Hz	VT256	CTC 625
3
4	Tonausgabe

Aufteilung des Displays:
Die linke obere Ecke (52×11) ist für die Seitenbezeichnung vorgesehen.
Die rechte obere Ecke (52×11) ist für eine Uhr (36×11)
und Aktionsanzeige (16×11) vorgesehen.
Der untere Bereich (240×12) dient zur Anzeige der Belegung der F-Tasten,
mit 39×11 Pixeln für jedes der 6 Felder.
Die obere Mitte (136×11) dient zur Kontexthilfe
Verbleiben 240×41 Pixel für das restliche Bild.
Das genügt für 3 Zeilen Text und etwas unterstützende Grafik.
Das ursprüngliche Design verwendete 40×6 Zeichen.
Im Zoom-Modus entfallen alle Header und die F-Tastenbelegung,
und der mittlere Bereich dehnt sich auf die gesamte Displayfläche aus.

Menüsystem (F1..F6):
Die Taste „Set“ ist zum Setzen und Übernahme von Werten bestimmt (wie ENTER)
Die Taste „↲“ ist zum Verwerfen von Eingaben und zum Zurückgehen (wie ESC)
Hauptmenü
├─F1	System: Testseite und Backup/Restore; SET = Zeichenkode-Test
│	├─F1	Backup: Ausgabe 1 KByte auf USB CDC
│	├─F2	Restore: Emppfang 1 KByte von USB CDC, nach 2 s Timeout
│	├─F3	Beispiel-Rezeptliste aus Flash in EEPROM kopieren
│	├─F4	(frei)
│	├─F5	Debug: Bestimmte Eingangskanäle invertieren,
│	│	damit die Steuerung im ausgebauten Zustand nicht massiv Warnungen produziert
│	├─F6	Urlader starten für Firmware-Update
│	├─0..9	Ziffer ausgeben
│	└─SET	Zeichenkode-Test, jeder Tastendruck gibt das nächste Zeichen aus
├─F2	Start: Aktives Rezept starten (wie Startknopf am Gerät) oder stoppen
│	Wechselt zu Stopp, solange ein Rezept abgearbeitet wird
├─F3	Alarm-Editor: Listen-Anzeige
│	├─F1	▲ hoch
│	├─F2	▼ runter
│	├─F3	Ausgewählten Alarm löschen
│	├─F4	Alle Alarme löschen
│	├─F5	Refresh: Alarmlistenanzeige aktualisieren
│	├─F6	Zoom ein/aus
│	├─0..9	ohne Funktion
│	└─SET	ohne Funktion
├─F4	Rezept-Editor: Baum-Anzeige (= Derivat der Listen-Anzeige)
│	├─F1	▲ hoch
│	├─F2	▼ runter
│	├─F3	+/- Unteräste ein/ausblenden
│	├─F4	Edit: Weitere Funktionen (keine vertikale Bewegung möglich!)
│	│	├─F1	(nur Befehl): Klausel (Bedingung) einstellen
│	│	├─F2	(nur Befehl): Tätigkeit (Abbruch, Log) einstellen
│	│	├─F3	(nur Befehl): Kanal auswählen (mit Zahleneingabe)
│	│	│	├─F1	◄ Zahleneingabe: Cursor links
│	│	│	├─F2	► Zahleneingabe: Cursor rechts
│	│	│	├─F3	▲ hoch: Vorhergehende Kanalnummer auswählen
│	│	│	├─F4	▼ runter: Nächste Kanalnummer auswählen
│	│	│	├─F5	Umschalten Analog- oder Digitalkanal
│	│	│	└─F6	Zahleneingabe: Letzte Ziffer löschen
│	│	├─F1	(Rezept,Schritt): ▲▲ Tauschen mit vorher
│	│	├─F2	(Rezept,Schritt): ▼▼ Tauschen mit danach
│	│	├─F3	(Rezept,Schritt): ×2 Duplizieren
│	│	├─F4	Rezept-ID, Schritt-Dauer oder Befehls-Wert ändern (Zahleneingabe)
│	│	├─F5	Neu…
│	│	│	├─F1	Neues Rezept (erscheint nur wenn kontextbezogen erlaubt)
│	│	│	├─F2	Neuer Schritt (erscheint nur wenn kontextbezogen erlaubt)
│	│	│	└─F3	Neuer Befehl (erscheint nur wenn kontextbezogen erlaubt)
│	│	├─F6	⌫ Rezept, Schritt oder Aktion löschen
│	│	├─0..9	Erste Ziffer für Rezept-ID, Schritt-Zeit oder Befehls-Wert
│	│	└─SET	leitet Zahleneingabe für Rezept-ID, Schritt-Zeit oder Befehls-Wert ein
│	├─F5	Sicherheitsschritte anzeigen/verbergen
│	├─F6	Zoom ein/aus
│	├─0..9	Erste Ziffer für Rezept-ID, Schritt-Zeit oder Befehls-Wert
│	└─SET	wählt oder startet aktives Rezept
├─F5	Digital-Editor („Ports“)
│	├─F1	◄ links
│	├─F2	► rechts
│	├─F3	▲ Seite (= I/O-Karte) zurück
│	├─F4	▼ Seite (= I/O-Karte) vorwärts
│	├─F5	Alles 0: Setzt Ausgänge auf Null und schaltet Eingangsinvertierungen ab
│	├─F6	Anzeige Uhr oder Richtung
│	├─0..9	0 = Bit löschen, 1 = Bit setzen, 2..9 = ohne Funktion
│	└─SET	Bit kippen
├─F6	Analog-Editor: Listen-Anzeige
│	├─F1	▲ hoch
│	├─F2	▼ runter
│	├─F3	◄ Fokus nach links
│	├─F4	► Fokus nach rechts
│	├─F5	Alles 0
│	├─F6	Zoom ein/aus
│	├─0..9	Erste Ziffer einer Zahleneingabe
│	└─SET	Leitet Zahleneingabe ein
├─0..9	ohne Funktion
├─SET	ohne Funktion
└─↲	ohne Funktion
*/

#include "Convac.h"
#include <avr/eeprom.h>		// eeprom_read_block()
#include <avr/wdt.h>		// wdt_reset()
#include <avr/power.h>
#include <avr/interrupt.h>	// sei()
#include <avr/signature.h>
#include <avr/fuse.h>

FUSES={
 0xDE,	// USB-Urlader können diese Fuses nur überprüfen, nicht ändern
 0xDF,
 0xC3
};
// EEPROM-Vorgabewerte sind hier nicht enthalten, um mit Urladern
// kompatibel zu sein, die keinen EEPROM schreiben können. „ubaboot“ kann's.

/*======================================================================*
 * System (Grafik-Testseite sowie manuelles Backup/Restore via USB)	*
 *======================================================================*/

#define W(x) (x)&0xFF,(x)>>8
#define PL(n) (n)|0x80	// gefalteter Eintrag (zeigt PL=Plus an)

const PROGMEM byte Beispiel[]={
// Globale Sicherheit und externe Start/Halt-Schalter
   W(1000),PL(9),	// (implizit stets nur 1 „Schritt“)
    OP_D1|OP_CNT,0x00,// Eingang 0.0.0 abfragen und starten/nächster Schritt bei Flanke 0->1 (Taste Start)
    OP_D1|OP_BRK,0x04,	// Eingang 0.0.4 abfragen und Abbruch generieren solange 1 (Taste Stopp)
    OP_D0|OP_DNT,0x06,	// Eingang 0.0.6 (000 00 110) beim Start abfragen, muss 1 sein (Tür offen)
    OP_D0|OP_DNT,0x0A,	// Eingang 0.1.2 (000 01 010) beim Start abfragen, muss 1 sein (Absaugung fehlt)
    OP_D0|OP_DNT,0x0E,	// Eingang 0.1.6 (000 01 110) beim Start abfragen, muss 1 sein (Tankeinschub offen)
    OP_D0|OP_BRK|OP_LOG,0x1D,	// Eingang 0.3.5 (000 11 101), Abbruch solange 0 (Motortemperatur)
    OP_D0|OP_BRK|OP_LOG,0x1F,	// Eingang 0.3.7 (000 11 111), Abbruch solange 0 (Motorstörung)
    OP_D1|OP_BRK|OP_LOG,0x3C,	// Eingang 1.3.4 (001 11 100), Abbruch solange 1 (Überstrom an Ausgängen)
    OP_D1|OP_BRK|OP_LOG,0x5C,	// Eingang 2.3.4 (010 11 100) für die zweite Ausgabekarte
// Rezeptliste
 2,			// Anzahl Rezepte		Zeilenummer
  PL(5),		// Anzahl Schritte		1
   W(300),0,		// ID				2		
			// keine „lokale Sicherheit“
   W(100),PL(1),	// 1 s				3
    OP_D1|OP_OUT,0x38,	// Relais einschalten		4
    
   W(300),PL(1),	//				5
    OP_D1|OP_OUT,0x39,	// noch ein Relais		6
    
   W(200),PL(1),	//				7
    OP_D0|OP_OUT,0x39,	// wieder aus			8
    
   W(0),PL(2),		// Aufräum-Schritt Zeit = 0	9
    OP_D0|OP_OUT,0x39,	//				10
    OP_D0|OP_OUT,0x38,	//				11
    
  PL(11),		// Sequenz Ätzen (hypothetisch)
   W(200),PL(1),	// ID
    OP_D0|OP_DNT,0x14,	// Eingang 0.2.4 (000 10 100) Ätzer leer
    
   W(50),PL(1),		// 0,5 s
    OP_D1|OP_OUT,0x46,	// Ausgang 2.0.6 (010 00 110): Vakuum Chuck Ein
    
   W(20),PL(2),		// 0,2 s
    OP_D0|OP_BRK,0x08,	// Eingang 0.1.0 (000 01 000): Vakuum Chuck fehlt
    OP_D1|OP_OUT,0x38,	// Ausgang 1.3.0 (001 11 000): Spindelmotor (Relais)

   W(500),PL(1),	// 5 s
    OP_A16|OP_OUT,9,W(0x1000),// zum halben Maximum des D/A-Wandlers
    
   W(50),PL(1),		// 0,5 s
    OP_D1|OP_OUT,0x26,	// Ausgang 1.0.6 (001 00 110): Ätzer (laden??)
    
   W(40),PL(2),
    OP_D0|OP_OUT,0x26,	// Ausgang 1.0.6 (001 00 110): Ätzer Ende
    OP_D1|OP_OUT,0x27,	// Ausgang 1.0.7 (001 00 111): Ätzer zerstäuben
    
   W(1000),PL(1),	// 10 s auf Wafer verteilen
    OP_D0|OP_OUT,0x27,	// Ausgang 1.0.7 (001 00 111): Ätzer zerstäuben Ende

   W(100),PL(1),	// 1 s
    OP_D1|OP_OUT,0x28,	// Ausgang 1.0.7 (001 01 000): Ätzer spülen
   
   W(500),PL(2),	// 5 s
    OP_D0|OP_OUT,0x28,	// Ausgang 1.0.7 (001 01 000): Ätzer spülen Ende
    OP_A8|OP_OUT,9,0,	// runterfahren
   
   W(10),PL(3),		// 0,1 s
    OP_D0|OP_OUT,0x46,	// Ausgang 2.0.6 (010 00 110): Vakuum Chuck Ein Ende
    OP_D1|OP_OUT,0x47,	// Ausgang 2.0.7 (010 00 111): Vakuum Chuck Aus (belüften?)
    OP_D0|OP_OUT,0x38,	// Ausgang 1.3.0 (001 11 000): Spindelmotor Ende

   W(0),PL(1),
    OP_D0|OP_OUT,0x47,	// Ausgang 2.0.7 (010 00 111): Vakuum Chuck Aus Ende
#if 1
 0xFF,			// Debug-Endemarkierung
 0xFF,
#endif
};

#undef PL
#undef W

void scroll() {
 d.L=0;d.T=12;d.R=d.getXMax();d.B=50;
 d.scroll(0,d.getFontHeight());	// Vertikal scrollen
 d.T=d.B-(d.getFontHeight()-1);
 d.fillRect();
 d.X=0;
}
void melde(bool ok) {
 d.print(ok?F(" fertig."):F(" Zeitüberschreitung!"));
 beep(ok?80:160,10);
 beep(ok?70:180,10);
}
static void debugSetInv() {
 DIGITAL::setIo(0,0x40);	// DEBUG: Eingänge manipulieren dass es nicht
 DIGITAL::setIo(1,0x44);	// (ohne angeschlossene Hardware) andauernd piept
 DIGITAL::setIo(3,0xA0);
}

static void dlgSystem() {
 d.clear();
 d.print(F("Test Grafik + Tasten"));
 idle();
 d.fillRect(144,24,161,39);	// Grafikdemoseite und Tastentest
 d.drawRect(84,16,101,39);
 static const PROGMEM byte xhatch[8]={0x82,0x44,0x28,0x10,0x28,0x44,0x82,0x01};
 memcpy_P(d.brush,xhatch,8);
 d.patRect (24,14,51,39);
 d.flags=d.COLOR|d.R2_XORPEN;
 d.drawLine(17,24,229,38);
 d.flags=d.COOKED|d.COLOR|d.R2_COPYPEN;
 [](){}();	// Eine λ-Funktion, die nichts tut, aufrufen (Geigel! Nur um zu testen ob der C++-Compiler C++11 kann.)
/* 
 d.L=0;
 d.T=0;
 d.R=d.PIXEL_X-1;
 d.B=10;
 d.bitblt(ArialP12+4+0x80,1183,0);
 idle();
 
 d.T=12;
 d.B=22;
 d.bitblt(ArialP12+4+0x80,1183,d.PIXEL_X);
*/
 static byte cc=20;
menu:
 byte _usbCfg=usbCfg;
 byte mask=0x3F;
 if (!(_usbCfg&1)) mask=0x1C;	// Kein Backup, Restore und Urlader ohne USB.
 if (run.aktion) mask&=~2;	// Kein Restore während Prozesslauf
 paintMenu(F("Backup\0Restore\0Beispiel\0\0Debug\0Urlader"),mask);
 d.gotoXY(0,39);
 
 d.flags=d.R2_COPYPEN;
 for (;;) {
  byte c=getkey(50);
  switch (c) {
   case 0: if (_usbCfg!=usbCfg) goto menu; continue;
   case '\r': return;
   case '\n': c=cc++; break;	// Zeichen der Reihe nach (im Code) ausgeben
   case 0xF1: {
    scroll();
    d.print(F("copy com8 Datei"));
    melde(usbBackup());	// EEROM-Inhalt auf serielle Schnittstelle herausdonnern
   }continue;
   case 0xF2: {
    usbPurge();
    scroll();
    d.print(F("copy Datei com8"));
    melde(usbRestore());// Was von der Schnittstelle kommt geht zum RAM und schleichend zum EEPROM
   }continue;
   case 0xF3: {	// Beispielrezeptliste laden
    scroll();
    d.print(F("Beispielrezeptliste laden?"));
    if (yesno()) {
     memcpy_P(&eedata.gs,Beispiel,sizeof Beispiel);
#ifdef DEBUG
     if (eedata.gs.calcsizes()!=sizeof Beispiel-2) exit(-4);	// DEBUG: Fehler im Beispiel
#endif     
    }
   }goto menu;
   case 0xF4: beep(10,10); continue;	// funktionslos
   case 0xF5: {
    scroll();
    d.print(F("Eingangsinvertierungen aktivieren?"));
    if (yesno()) debugSetInv();
   }goto menu;
   case 0xF6: {
    scroll();
    d.print(F("Zum Atmel-Urlader springen?"));
    if (yesno()) {
     d.clear();
     bootstart();
    }
   }goto menu;
  }
  if (d.X+d.getTextExtent(c)>d.getXMax()) scroll();
  d.print(c);
 }
}

/*===========*
 * Hauptmenü *
 *===========*/

ITER giter;

static void mainscreen() __attribute__((noreturn));
static void mainscreen() {
vorn:
 d.clear();
 idle();
 d.print(F("EEPROM-Füllstand: "));
 d.print(floatstr((unsigned long)(sizeof(CONFIG)+gslen)*(1000U<<6)>>16,1));	// EEPROM-Länge: 1024 Bytes (2**6)
 d.print(F(" %"));
 idle();
 d.gotoXY(8,17);
 giter.ire=C.runrez;
 giter.re=*rl+giter.ire;		// Achtung! operator+
 giter.re->print(giter.ire);
 
 ALARM*a=ALARM::newest();
 if (a) {
  d.print(F("Letzter Fehler: "),0,29);
  d.Y=40; a->print();
 }
 paintMenu(F("System\0\0Alarme\0Rezepte\0Digital\0Analog"));
start:
 paintMenuItem(1*40,run.aktion?F("Stopp"):F("Start"),run.aktion?25:24);
 for(;;) {
// Fehler hier anzeigen!
  switch (getkey(255)) {
   case 0: goto start;
   case 0xF1: dlgSystem(); goto vorn;
   case 0xF2: if (run.aktion) run.next(run.re->n.c-1); else run.start(C.runrez); goto start;
   case 0xF3: dlgAlarm(); goto vorn;
   case 0xF4: dlgRezept(); goto vorn;
   case 0xF5: dlgDigital(); goto vorn;
   case 0xF6: dlgAnalog(); goto vorn;
   default: beep(80,20);
  }
 }
}

/***********************************************
 * Generelle Initialisierung und Hauptprogramm *
 ***********************************************/

static void setupHardware(void) {
 MCUCR = 0x80;	// JTAG deaktivieren (sonst gehen PF4..PF7 = ADC4..ADC7 nicht)
 MCUSR = 0;	// WDRF löschen, damit das nachfolgende Deaktivieren des Watchdogs klappt
 WDTCSR|=0x18;	// Watchdog aktivieren, legt u.a. !OUT/HALT auf High
 WDTCSR= 0x45;	// auf 500 ms
 wdt_reset();
 CLKPR = 0x80;
 CLKPR = 0x00;	// CLKDIV8-Fuse ist ab Werk gesetzt, beim Arduino Micro gelöscht
 SMCR  = 0x01;	// Sleep aktivieren
 PRR0  = 0x84;	// I²C und SPI aus, wird nicht benötigt
 PORTB = 0xFF;	// Pullups am Datenbus
 DDRC  = 0xC0;	// Ausgänge aktivieren
 DDRD  = 0xFF;
 PORTE = 0x40;
 DDRE  = 0x40;
 PORTF = 0x83;
 DDRF  = 0xF3;
 ACSR  = 0x80;	// Analogvergleicher deaktivieren
 ICR1  = 624;	// Zählumfang 0..624
 TCCR1B= 0x1C;	// CTC via ICR, Vorteiler 256, ergibt 100 Hz
 TIMSK1= 0x20;	// mit Interrupt
 TCCR4D= 0x01;	// Frequenz einstellbar am Timer4
 sei();
}

int main() {
 EEAR=0; EECR|=1; if (EEDR!=0xFF) {
  eeprom_read_block(&eedata,&eesave,sizeof(eedata));	// alles in einem Rutsch
 }
 setupHardware();
 usbInit();
 d.init();
 d.setFont(FP(ArialP12));
 if (eedata.gs.calcsizes()	// ausrechnen, sollte zunächst 1 ergeben
   > sizeof eedata-sizeof(CONFIG)) {
  memcpy_P(&eedata.gs,Beispiel,sizeof Beispiel);
  while (eeprom_update(&eedata.gs,&eesave.gs,sizeof Beispiel));
  return gslen;		// sterben mit Länge als Fehlerkode
 }
 DIGITAL::initOutputs();
 ANALOG::initOutputs();
 RUN::init();
 PORTF&=~0x80;		// !OUT/HALT aktivieren
#ifdef DEBUG
 debugSetInv();
#endif
 mainscreen();
}
Detected encoding: UTF-80