Source file: /~heha/mb-iwp/Bergwerk/fba-fw-230825.zip/i2c.cpp

/* (Tabweite: 8, was denn sonst!! 4 war eine Microsoft-Erfindung, 3 von Borland)
Als SCL-Takt sind 400 kHz zulässig.
Der Master muss slave-seitiges Clock-Stretch unterstützen, d.h. akzeptieren.
Die maximale Transferlänge beträgt 64 Bytes.
Die meisten Daten sind int8_t,
für Mehrbytedaten ist little-endian festgelegt weil bequemer.
Das I²C-Interface ist schichtweise gebaut. 
Der Mikrocontroller meldet sich I²C-Adresse 0x5D.
Schreib- und Lesedaten werden mit einer 2-Byte-Subadresse spezifiziert.
Der Mikrocontroller garantiert dass ein gelesener Satz zusammenhängend ist,
d.h. eine One-Time-Kopie von (ansonsten veränderlichen) Daten übertragen werden.
Es ist egal ob getrennte I²C-Rahmen zum Schreiben und Lesen verwendet werden
oder ob zwischen Schreiben (Adresse setzen) und Lesen ein START ohne STOP kommt.

NEU 230405: Lesedaten über die volle oder verbleibende Blocklänge der Schicht
bekommen ein OneWire-CRC8-Byte angehangen.
IMMER SCHON: Über das Ende der Blocklänge reichende Lesedaten
(Master taktet weiter, macht kein STOP) werden mit 0xFF beantwortet.

Das erste Byte spezifiert die Schicht:
(High-Nibble = 6 oder 7, Low-Nibble = Schicht, derzeit 0..7).
Das Bit 4 schaltet ggf. eine Sonderfunktion.
Ohne Angabe = 0x60.

Das zweite Byte spezifiziert den Schreib/Leseoffset.
Bei höheren Schichten muss dieser 0 sein.
Ohne Angabe = 0x00.

Das erste Byte steuert außerdem Direktzugriffe:
0x00: 		SFR lesen/schreiben
0x01..0x08	RAM lesen/schreiben (2 KByte)
0x20..0x23	EEPROM lesen (1 KByte)
0x40..0x7F	Schichtzugriff wie oben beschrieben
0x80..0xFF	Flash lesen
Dann ist das zweite Byte der Low-Teil der von-Neumannisierten Adresse.
Das ist vor allem zum Debuggen gedacht.

*******************************************
* Schicht 0, Subadresse 0x60: Echtzeituhr *
*******************************************
[RW]	Abfrage oder Setzen der Echtzeituhr, Datentyp: uint64_t, Sekunden÷64K
	Subadresse = Byteadresse
*Beispiel:*
START 0xBA 0x60 2 START 0xBB secB0 secB1 secB2 secB3 STOP
Eine Batteriestütze ermöglicht eine Gangreserve über einige Jahre.
So kommt der Raspberry auch ohne USV und NTP zu einer Echtzeituhr.
Es erscheint sinnvoll, diese Uhr in Weltzeit (UTC) zu betreiben.

Die Echtzeituhr ist ein Nebenprodukt der Schaltung /ohne/ Baudratenquarz.

********************************************
* Schicht 1, Subadresse 0x61: EEPROM-Daten *
********************************************
[RW]	Fahrzeugkonstanten, Struktur siehe Eedata.
	Subadresse = Byteadresse
*Beispiel:*
START 0xBA 0x60 START 0xBB byte0 byte1 usw. STOP

Die EEPROM-Daten werden im RAM gehalten und
im Hintergrund zum EEPROM aktualisiert.

******************************************************
* Schicht 2, Subadresse 0x62: Servosteuerung via PWM *
******************************************************
[W]	Setzen von PWM-Ausgängen, Datentyp: int8_t Wertebreich ±125, 0x80 = cNaN = skip
	Subadresse mit Auto-Inkrement:
	0	Lenken
	1	Fahrmotor
	2	Heben (Arm oder Ladefläche)
	3	Löffel (nur Radlader)
	4	(Hydraulikpumpe)
	5	Zündung/Hupe
	6	Licht
	7	Rundumleuchte
	8	(Fahrgeräusch)
Diese Reihenfolge ergibt sich aus der Zuordnung innerhalb der 
Originalfernbedienung, wobei die ersten vier Kanäle unveränderlich sind.
*Beispiel:* Lenken, Heben, Löffel vorgeben:
START 0xBA 0x62 0 25 0x80 50 -25 STOP
Lenken 20%, Fahren unverändert weil cNaN, Heben mit 40%, Löffel mit -20%
Die Hydraulikpumpe läuft dann automatisch mit 20% + 40% + 20% = 80%.
Exzessive Werte (jenseits Index 8) werden ignoriert und mit NAK beantwortet.
Werte = cNaN = 0x80 = -128 werden übersprungen, nicht geschrieben.
Werte außerhalb ±125 werden in den zulässigen Wertebereich limitiert.

Das Setzen der PWM-Ausgänge 0..3 schaltet den zugeordneten Regler (Schicht 5) ab.
Das Setzen des PWM-Ausgangs 1 bricht alle Fahraufträge (Schicht 7) ab.
Das Setzen von PWM-Ausgängen sperrt die Fernbedienung für 1? s
*Beispiel:* Abbruch einer Zielfahrt und Anhalten:
START 0xBA 0x61 1 0 STOP

[R]	Abfrage von PWM-Ausgängen
	Subadresse: Wie oben
Beispielsweise: Alle 3 Hydraulikventil-Servos abfragen:
START 0xBA 0x61 START 0xBB 20 0 40 -20 STOP
(Das Setzen von Offset 0 ist nicht erforderlich.)
Die gelieferte 0 für den Fahrmotor wäre zu ignorieren.

***************************************************************************
* Schicht 3, Subadresse 0x63: Belauschen oder Übernahme der Fernbedienung *
***************************************************************************
[W]	Nur Subadresse setzen, Bit 4 = für 1 s entreißen

[R]	Abfrage der Fernbedienung, Datentyp: int8_t mit Wertebereich ±125
	Bedeutung der Bytes wie bei Subadresse 0xC2, 0x80 = cNaN = kein Wert (bspw. Fernbedienung tot)
	Die Originalfernbedienung unterstützt via IBUS 14 Kanäle.
	Die Werte werden per Rechtsschieben auf 8 Bit gestutzt
	(original liefert diese 10 Bit: -500 .. 500 = Mikrosekunden Pulsvariation).
*Beispiel:* Alles abfragen und entreißen:
START 0xBA 0x72 START 0xBB b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 STOP
*Beispiel:* Fernbedienung nur abfragen
(d.h. zum Mitschneiden, bspw. zur späteren Wiederholung)
START 0xBA 0x62 START 0xBB b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 STOP

Die Fernbedienbefehle können von der Originalfernbedienung
oder von der Pampelmuse kommen.
Nach 1 s Abfragepause (50 PWM-Zyklen) übernimmt die Fernbedienung wieder
die Kontrolle über das Fahrzeug.
Das Not-Aus der Pampelmuse wird dadurch /nicht/ unwirksam gemacht.

*****************************************************
* Schicht 4, Subadresse 0x64: A/D-Wandler, Sensoren *
*****************************************************
[W]	Nur Subadresse setzen
	
[R]	Potenziometer usw. abfragen, das meiste int16_t
	0..13	= 7 Istwerte linearisiert, zur Feststellung des (erreichten) Regelziels:
		  0 = Lenkposition in 0,01 Grad, linearisiert mittels Eedata.lenkung
		  2 = Fahrgeschwindigkeit, umgerechnet mittels Eedata.onestep
		  4 = Hebung in 0,01 Grad, linearisiert mittels Eedata.hubarm
		  6 = Löffel/Verwindung in 0,01 Grad, linearisiert mittels Eedata.loeffel
		  8 = Bordspannung in 0,01 V
		 10 = Chiptemperatur in 0,1 °C umgerechnet mittels Eedata.t0 und Eedata.ts
		 12 = Ultraschallsensor: Entfernung in mm
	14	Flags: Not-Aus, Anfahr-Schalter, Zielfahrt-Flag
	15	ungenutzt
	16..25	= 5 Werte vom A/D-Wandler (int16_t, 32× summiert, 0..32736)
		 16 = ADC0 (ratiometrisch) = Lenkpoti
		 18 = ADC1 (ratiometrisch) = Heben
		 20 = ADC2 (ratiometrisch) = Löffel/Verwindung
		 22 = ADC3 (3,3 V ×4 = 13,2 V) = Bordspannung
		 24 = ADC8 (1,1 V) = Chiptemperatur
		Diese werden für Kalibriervorgänge benötigt.
	26..31	= Sonstige Roh-Werte
		 26 = Ultraschall-Messzeit
		 28 = Zeitdifferenz
		 30 = Anzahl Pulse
		 31 = Anzahl pulsfreie 20-ms-Rahmen, max. 255
	32..35	= Tacho als int32, zählt vorzeichenlos Impulse vom Motor.
	
*Beispiel:* Bordspannung abfragen
START 0xBA 0x64 8 START 0xBB {840} STOP
liefert 8,40 V Bordspannung (vom Fahrakku)
*Beispiel:* Alles abfragen
START 0xBA 0x64 START 0xBB {lenkpos} {speed} {hubarm} {loeffel} ... STOP

Die Umstellung auf 16 Bit wurde zur Verfeinerung des Reglers vollzogen.
Die Angabe in Hundertstel für die Anzeige auf dem Original-Controller.

**************************************
* Schicht 5, Subadresse 0x65: Regler *
**************************************
[W]	Setzen der Regler, Datentyp: int8_t, 0x80 = cNaN = skip
	Subadresse mit Auto-Inkrement:
	0	Lenkposition, Angabe in Grad
	1	Fahrgeschwindigkeit, Angabe in mm/s(?)
	2	Heben, Angabe in Grad. 0 = Bodenlage; Muldenkipper: abgesenkt
	3	Löffel, Angabe in Grad. 0 = waagerecht. Dummy beim Muldenkipper
	4,5,6,7	Regelgeschwindigkeit (schont den Servo; Beschleunigung bei Fahrgeschwindigkeit)
		in Grad pro 20 ms
*Beispiel:* Ladefläche Muldenkipper auf 70° ankippen und per Regler halten
START 0xBA 0x64 2 70 STOP
Das Setzen eines Reglers bricht Fahraufträge (Schicht 7) ab.
Eine geringe Fahrgeschwindigkeit (nahe 0) kann nicht
auf Hanglagen geregelt werden!
Weil die aktuelle Bewegungsrichtung des Fahrzeugs nicht erfasst werden kann
und womöglich nicht an der Phasenleitung abgreifbar ist,
kann der Regler in die falsche Richtung „regeln“ und ausbrechen!
Daher wird der Regler bei Fahrgeschwindigkeit 0 kurzerhand deaktiviert.
Abhilfe schafft nur ein erheblicher Eingriff ins Fahrzeug, entweder mechanisch
(Inkrementalgeber, Resolver) oder elektronisch (BLDC-Controller mit Rückmeldung).

[R]	Regler abfragen
	Subadresse wie oben, zusätzlich
	0,1,2,3	= Reglersoll wie oben
	4,5,6,7	= Regelgeschwindigkeit wie oben

Die Reglerparameter befinden sich in den EEPROM-Daten ab Offset 0x3C

*****************************************
* Schicht 6, Subadresse 0x66: Odometrie *
*****************************************
[W]	Setzen der Position (des Rechenpunktes) und der Winkellage des Fahrzeugs
	Position x/y im Linkssystem, int32
	
	Bit 4 der Subadresse ist Addier-Flag:
	Übermittelte Werte werden addiert, nicht kopiert
	lNaN-Werte für X und Y werden übersprungen und ignoriert.
	Hinweis: Das Setzen der Position löst keine Lenk- oder Fahrbewegung aus!
	Es überschreibt lediglich die Odometriedaten.

[R]	Abfrage der Lage des Rechenpunkts (x/y) und der Winkellage (α) des
	Fahrzeugs, jeweils int32. Weg-Einheit: 0,1 mm.
	Winkel-Einheit: -2^31 = -128π, 0 = 0, +2^31-1 ≈ +128π.
	(Das heißt die Winkellage steckt in den unteren 24 Bit,
	und die oberen 8 Bit zählen vzb. die ganzen Umdrehungen.)
	Dazu Winkel-Addierwert q (int16) in 1/2^23π
	und Lenkeinschlag λ (int16) in 0,01°.
	Zum Thema „Was ist der Rechenpunkt“ siehe Webseite „Bergwerk“.
	Kurz und knapp: Bei Lenkbewegungen im Stand ändert sich die Lage
	des Rechenpunktes nicht.
	Er liegt bei geradeausgerichtetem Fahrzeug (λ≡0) zwischen den
	Achsmitten im /umgekehrten/ Verhältnis der Achsabstände zum Knick.
*Beispiel:* Vollständige Positionsermittlung
START 0xBA 0x66 START 0xBB x0 x1 x2 x3 y0 y1 y2 y3 α0 α1 α2 α3 q0 q1 λ0 λ1 STOP
Die Art der Interruptbehandlung im Mikrocontroller garantiert
einen konsistenten Datensatz:
Der Schnappschuss wird im Moment von "START 0xBB" erstellt.

Die Firmware verändert die Position anhand von Lenk- und Fahrbewegungen
und benötigt dazu die Fahrzeugkonstanten im EEPROM ab Offset 0x4C.
Der Radausbruch bei Kurvenfahrt wird ignoriert, d.h. der odometrische
Fahrweg ist etwas länger als der des Rechenpunktes.
(Das macht bei knickgelenkten Fahrzeugen viel weniger aus als bei Kfz
mit Vorderrad-Umlaufmessung.)

********************************************
* Schicht 7, Subadresse 0x67: Fahraufträge *
********************************************
[W]	Gleichzeitige, koordinierte Steuerung aller (4) geregelten Kanäle
	(Das was jeder Baggerfahrer nach einiger Übung ohne Computer tut.)
	Das _Löschen_ _aller_ Fahraufträge erfolgt durch Beschreiben der Regler in Schicht 5,
	mittelbar durch Schreiben auf die Servokanäle 0..3 in Schicht 2
	sowie durch Eingriff mit den Hebeln der Fernbedienung.
		int8 Lenkung regeln in Grad; cNaN = nichts tun
		int8 Fahrgeschwindigkeit regeln*; cNaN = nichts tun
		int8 Hubarm bzw. Ladefläche regeln in Grad; cNaN = nichts tun
		int8 Löffel regeln in Grad; cNaN = nichts tun; Verhalten undefiniert beim Muldenkipper
		int8 Geschwindigkeit des Lenkens; cNaN, ≤0, cInf = Defaultwert
		int8 Fahr-Beschleunigung*; cNaN, ≤0, cInf = Defaultwert
		int8 Geschwindigkeit des Hubarms / der Ladefläche; cNaN, ≤0, cInf = Defaultwert
		int8 Geschwindigkeit des Löffels; cNaN, ≤0, cInf = Defaultwert
		int32 Fahrstecke in Tachoticks; 0 oder lNaN = keine „Regelung“ auf Fahrstrecke
		int16 Warten in 20 ms; 0 oder iNaN = kein Warten, max. 3 Minuten
	Die FIFO-Tiefe beträgt 16.
	* Ist die „Fahrstrecke in Tachoticks“ (Byteoffset 8) vorgegeben,
	  fungiert Offset 1 als (Maximal-)Geschwindigkeit und Offset 5
	  als Anfahr- und Bremsbeschleunigung des Trapezprofils:
	  Die „Regelung der Fahrgeschwindigkeit“ erfolgt in diesem Fall
	  vom Trapezprofilgenerator.
	Ein Verketten vorgegebener Fahrstrecken (wie beim 3D-Drucker mit
	Marlin-Firmware oder bei NC-Fräsmaschinen) ist derzeit nicht implementiert,
	aufeinanderfolgende Fahraufträge (mit gegebener Fahrstrecke ≠ 0)
	werden mit Zwischen-Geschwindigkeit = 0 ausgeführt.
	So etwas würde für Langsam- und Schnellfahrstrecken gebraucht.
	Rückwärtsfahren erfolgt durch negative Tackoticks ODER durch
	negative Geschwindigkeitsangabe.

[R]	Abfrage des Füllstands der Fahrauftrag-Queue (max. 16), 1 Byte

Hinweis: Die Fahrt führt /nicht/ zu einem bestimmten (x/y/α)-Ziel
	auf der Spielwiese! Darum muss sich der Planer kümmern
	und ggf. Lenkbewegungen einschieben.
Im Prinzip muss der „Auftraggeber“ das Bewegen zur Zielkoordinate
in eine knickfreie Turtlegrafik umwandeln.

*************************
* Schicht 8: Regler-Log *
*************************
Die vier Regler (der Schicht 5) schreiben alle 20 ms in einen 16 Einträge
tiefen Ringpuffer mit vier 16-Bit-Werten:
	- Regelabweichung
	- Begrenzte Integrationssumme
	- Geglättete letzte Regelabweichung
	- Ungespreizter und unlimitierter Regelausgang
Mit einem Zeithorizont von 300 ms kann so ein Oszillogramm vom Regelvorgang
genommen werden; Kauderwelsch wenn mehrere Regler gleichzeitig arbeiten.

**************************
* Schicht 9: Timing-Info *
**************************
Information über die Auslastung des AVR, wie
	- Maximale ISR-Abarbeitungszeit = System-Latenz (8 Bit genügen)
	- Gesamte Zeit in ISRs = „Kernel“-Auslastung
	- Anzahl ISR-Aufrufe (zwecks Kompensation des Zeiterfassungs-Kodes)
	- Gesamte Zeit im Schlafmodus
	- Anzahl Schlafmodus-Aufrufe
	- Evtl. längste und kürzeste Zeit im Schlafmodus
Daraus folgend kann 'loadavg' = CPU-Auslastung, „Kernel“-Auslastung
und Energieverbrauch berechnet werden.
Dazu sind (per bedingte Compilierung) sämtliche ISRs
sowie der Kode rund um die sleep_cpu()-Anweisung
mit Zeiterfassungs-Prolog und -Epilog ausgestattet.
Kurze ISRs mit fixer Laufzeit werden zweckmäßigerweise gesondert gezählt
und dann verrechnet?
Da der Schlafmodus stets von einer ISR beendet wird,
erfolgt die Zeitnahme unter Zuhilfenahme des ISR-Prologs.
Als Timer dient der umlaufender Timer1 (mit Vorteiler 8, im Speicher-Adressraum).
Die Verrechnung is alle 20 ms geplant, sodass 16-Bit-Akkumulatoren nicht überlaufen.
Die Abfrage ist alle 100..300 ms in 32-Bit-Speicher vorgesehen.

************************************
* Schicht 10..15: Nicht ausgedacht *
************************************

Es ist so, dass die I²C-Master-Hardware des Raspberry
nicht korrekt auf Clock-Stretch reagiert.
Es muss der I²C-Master-Software-Treiber installiert werden.
Auch gibt es immer wieder Fälle von „Verklemmern“,
die nunmehr per Polling und OCR2B oder Watchdog aufgelöst werden:
SDA wird vom AVR auf Low gehalten, während der Raspberry
eine Stoppsequenz generieren will.

*****************************
* Was macht die Pampelmuse? *
*****************************
Die Pampelmuse bekommt das gleiche Protokoll wie der I²C-Bus,
allerdings über HID / WebHID + WebUSB.

Wobei das zweite Fahrzeug die I²C-Adresse 0xBC[W] / 0xBD[R] bekommt.
Das bedeutet, dass das Fahrzeug ggf. die Originalfernbedienungssignale
zur Pampelmuse „forwardet“ (= zwei Funkstrecken).
*/

#include "fba.h"
#include <avr/wdt.h>	// wdt_reset()
#include <util/delay.h>

struct TwiBuf{
 byte lvl,ofs;		// Softwareschicht (0..7), Byteoffset
 byte buf[64];		// Schreib- oder Lesedaten
private:
 byte crcspace;
}twiBuf NOINIT;
byte*twiPtr NOINIT;
byte twiLen;
byte spiBuf[64] NOINIT;
byte spiIdx,spiLen;

void twiOnRead();	// Assemblerroutine

/*
static constexpr bool BigEndian=false;

// Wie memcpy aber mit Bytevertauschung, len==0 meint 256
// Das Byte an der Adresse 0 wird zuletzt geschrieben
static byte*swpcpy(byte*dst,byte const*src,byte len) {
 dst+=len;
 do *--dst=*src++; while(--len);
 return dst;
}

static char getofs(char o, char sz) {
 o-=twiBuf.ofs;
 byte len = sizeof twiBuf.buf-twiLen;	// Netto-Schreibdaten
 if (o+sz > len) o=-1;			// Hinten fehlt 'was
 return o;				// o<0: Vorn fehlt 'was
}

static float getFloat(byte const*buf) {
 if (BigEndian) {
  float ret;
  swpcpy((byte*)&ret,buf,4);
  return ret;
 }
 return *(float*)buf;
}

static void copyFloat(char o, float&dst) {
 o=getofs(o,sizeof(float));
 if (o<0) return;
 float f=getFloat(twiBuf.buf+o);
 if (!isnan(f)) {
  if (twiBuf.lvl&0x10) f+=dst;
  dst=f;
 }
}


static long getInt32(byte const*buf) {
 if (BigEndian) {
  long ret;
  swpcpy((byte*)&ret,buf,4);
  return ret;
 }
 return *(long*)buf;
}

static void copyInt32(byte o, long&dst) {
 o=getofs(o,sizeof(long));
 if (o<0) return;
 long l=getInt32(twiBuf.buf+o);
 if (l!=lNaN) {
  if (twiBuf.lvl&0x10) l+=dst;
  dst=l;
 }
}

static int getInt16(byte const*buf) {
 if (BigEndian) {
  int ret;
  swpcpy((byte*)&ret,buf,2);
  return ret;
 }
 return *(int*)buf;
}

static void copyInt16(byte o, int&dst) {
 o=getofs(o,sizeof(int));
 if (o<0) return;
 int i=getInt16(twiBuf.buf+o);
 if (i!=iNaN) {
  if (twiBuf.lvl&0x10) i+=dst;
  dst=i;
 }
}
*/
namespace OneWire{
 byte crc8(const byte*,byte);	// in Assembler
}

static void copybuf(void const*src,byte len,byte ofs) {
 byte l = len > ofs ? len - ofs : 0;
 auto s=reinterpret_cast<byte const*>(src);
 if (l>sizeof twiBuf.buf) l=sizeof twiBuf.buf;	// Nicht mehr als 64 Bytes (betrifft eedata!)
 memcpy(twiPtr=twiBuf.buf,s+ofs,l);
 twiBuf.buf[l]=~OneWire::crc8(twiBuf.buf,l);	// CRC über Antwort zum Prüfen beim Empfänger
 twiLen=l+1;
}

void Regler::Log::savecurrent() {
 byte wi = writeidx, ri=readidx;
 if (++wi>=elemof(items)) wi=0;
 if (ri==wi) {
  if (++ri>=elemof(items)) ri=0;	// Ungelesene Einträge verwerfen
  readidx=ri;
 }
 writeidx=wi;
}

byte Regler::Log::emit(byte*dst, byte buflen) {
 byte wi=writeidx, ri=readidx, l=0;
 byte ll = wi>=ri ? wi-ri : elemof(items)-ri;
 static_assert(sizeof*items==8);
 buflen>>=3;	// ganze Items
 if (ll) {
  if (ll>buflen) ll=buflen;
  memcpy(dst,items+ri,ll*sizeof*items);	// Oberer oder einziger Teilbereich
  l+=ll;
  buflen-=ll;
  ri+=ll;
  if (ri>=elemof(items)) {
   ri=0;
   dst+=ll*sizeof*items;
   ll=wi;
   if (ll>buflen) ll=buflen;
   if (ll) {
    memcpy(dst,items,ll*sizeof*items);	// Unterer Teilbereich
    l+=ll;
   }
  }
 }
 readidx=ri;
 return l;
}

void I2C::init() {
 TWAR = 0xBA;	// 1011101r (0x5D)
 TWCR = 0xC5;	// aktivieren mit Interrupt
}

static void onTwiInput() {
 byte lvl = twiBuf.lvl;
 byte ofs = twiBuf.ofs;
 byte l = sizeof twiBuf-1-twiLen;	// Bis dahin empfangene Bytes (-1: keine CRC)
// 230327: Während dieser Zeit (mit TWCR Bit 7 gesetzt)
// ist der I²C-Slavecontroller taub!
// Auf eine eingehende Leseadresse (0b10111011) antwortet dieser mit NAK,
// d.h. überhaupt nicht.
// Erwartet wurde meinerseits, dass der AVR mit ACK und Clock-Stretch antwortet.
// Es kann aber auch daran liegen, dass Clock-Stretch nur nach
// verarbeiteter Startsequenz funktioniert; das ist hier nicht der Fall.
// Der Raspberry-Softwaretreiber versucht es unmittelbar danach (ca. 35µs) nochmal,
// und (bei vorhergehendem 1-Byte-Schreiben) klappt das auch.
 if (l<=2) return;	// nichts zu tun
 l-=2;		// mindestens 1 Byte Nutzdaten
 if ((lvl&0xC0)==0x40) switch (lvl&0xF) {	// 01ff llll
  case 0: if (ofs<sizeof uhr) {
   if (l>sizeof uhr-ofs) l=sizeof uhr-ofs;
// TODO: mit Bit 4 addieren
   memcpy(uhr+ofs,twiBuf.buf,l);
  }return;
  case 1: if (ofs<sizeof eedata) {
   if (l>sizeof eedata-ofs) l=sizeof eedata-ofs;
   memcpy(eedata+ofs,twiBuf.buf,l);	// EEPROM-Kopie ändern
  }return;
  case 2: {
   char*src=(char*)twiBuf.buf;
   do{
    if (ofs>=sizeof l2.pwms) break;
    l2.set(ofs|lvl&L2::fAdd,*src++);	// über Setzfunktion gehen
    ++ofs;
   }while (--l);
  }return;
  case 3: return;	// nichts, Daten verwerfen
  case 4: {	// Hintertür: Autopulse generieren lassen
// Das geht zwar auch via 0x62 aber da muss man sich selber
// zeitlich um die Rückflanke kümmern. Hier tut das der Mikrocontroller.
// Wenn kein Notaus aktiv ist werden die 4 Zustandszähler mit durchgezählt,
// d.h. reflektieren den tatsächlichen Zustand der Zündung / Rundumleuchte / Lampen.
   byte*src=twiBuf.buf, add=0;
   for (;l && ofs<sizeof L2::autopuls; l--, ofs++) {
    char c = *src++;	// Anzahl der Autopulse (typ. 1)
    if (c!=cNaN) {
     if (c) {
      if (!l2.pwmok) {
       l2.set(7,-125);// PWM aktivieren (Null auf allen anderen Kanälen)
       add=7;		// 7x Null-Längen-Pulse ausgeben
      }
      L2::autopuls[ofs]+=(c<<4)+add;
     }else{	// Weitere Hintertür: Bei Nullwert Zähler nullsetzen,
      byte flags = l4.flags, lamps = l4.lamps;
      switch (ofs) {
       case 0: flags&=~(1<<3);break;	// K6-	Zündung aus
       case 1: lamps&=0xF8; break;	// K7-	Licht Fahrzeug
       case 2: lamps&=0x1F; break;	// K7+	Licht Fahrerhaus
       case 3: lamps&=0xE7; break;	// K7--	Blinken
       case 4: flags&=0xEF; break;	// K7++	Blinkgeräusch
       case 5: flags&=0x1F; break;	// K8+	Rundumleuchte
      }
      l4.flags=flags; l4.lamps=lamps;
     }
    }
   }
   if (ofs==sizeof L2::autopuls && l) {	// Offset 6
    char c = *src++;
    if (c!=cNaN && c) {	// Zwangs-Notaus toggeln (Pampelmusen-Notaus beachten)
     if ((l4.flags&0x06)!=0x04 && notaus::forced()) notaus::release();
     else notaus::force();
    }
    ofs++; l--;
   }
   if (ofs==sizeof L2::autopuls+1 && l) {	// Offset 7
    char c = *src++;
    if (c!=cNaN && c) l4.kmz = 0;	// Kilometerzähler zurücksetzen
   }
  }return;
  case 5: {	// Regler: Schreiben ganz anders als Lesen! Ziel, gefolgt von Speed
   char const*src=(char const*)twiBuf.buf;
   for(;ofs<4 && l;++ofs) {	// Offset in Strukturen aber Länge in Bytes: Suboptimal!
    byte k=ofs | lvl&Regler::fAdd;
    char so = *src++; --l;
    char sp = cNaN;
    if (l) {sp = *src++; --l;}
    Regler::set(k,so,sp);	// hier: NaN = Regelung beenden
    L7::purge(k);		// Zielfahrt beenden, Warteschlange leeren
   }
  }return;
  case 6: {
   if (l>sizeof odo-ofs) l=sizeof odo-ofs;
   memcpy((byte*)&odo+ofs,twiBuf.buf,l);
  }return;
  case 7: {
   L7*l7 = L7::prepare();
   if (!l7) break;	// Kein Platz!
   if (l>sizeof*l7-ofs) l=sizeof*l7-ofs;
   memcpy(((byte*)l7)+ofs,twiBuf.buf,l);
   L7::push();
  }return;
 }else memcpy((byte*)(lvl<<8|ofs),twiBuf.buf,l);
// RAM/SFR beschreiben (und gucken wie die Kuh davonfliegt)
}

// Nochmal zur Wiederholung: Wann passiert was?
// * Beim Eintrudeln der Schreibadresse wird Subadresse 0x60 und Offset 0 gesetzt
//   und der Empfang weiterer Daten (max. 2 + 64 Bytes) vorbereitet.
//	(Das passiert in der ISR ohne nennenswerten Clock-Stretch.)
//   Nach dem 66. Byte kommt NAK. Was auch immer der Master damit anstellt.
// * Bei STOP oder REPEATSTART nach Schreiben werden die
//   bis dahin empfangenen Daten „en bloc“ übernommen.
// * Beim Eintrudeln der Leseadresse wird {entweder — oder, je nach Subadresse}:
//   - der Puffer mit den zu sendenden Daten (max. 64 Bytes) gefüllt
//   - der Pufferzeiger auf RAM, EEPROM oder Flash umgebogen (von-neumannisiert)
//	(TODO: Letzteres darf in der ISR passieren)
//   NAKs vom Master werden ignoriert; der Master muss mit STOP terminieren.
// Bei beiden Aktionen kommt es zu einem nennenswerten Clock-Stretch,
// denn die Verarbeitung erfolgt (wegen der notwendigen Synchronisierung
// der Daten mit anderen Programmabschnitten) in der Hauptschleife.
// Mehr passiert nicht! Datenbytes werden in der ISR verarztet,
// ohne sichtbaren (aber dennoch vorhandenen) Clock-Stretch.
// Das bedeutet, dass man, wenn man permanent von /einer/ Subadresse
// lesen möchte, zwischendurch keine Schreibadresse senden braucht.
// Es handelt sich dabei /nicht/ um eine Fortsetzung.

void I2C::poll() {
 if (GPIOR0&1<<6) {GPIOR0&=~(1<<6); eedata.debug[17]++;}
 if (GPIOR0&1<<7) {GPIOR0&=~(1<<7); eedata.debug[18]++;}
#if 1
 if (PINC&1<<4) {	// High = in Ordnung
// Es ist SDA das klemmt, nicht SCL! Warum eigentlich??
//https://de.wikipedia.org/wiki/System_Management_Bus
  OCR2B = TCNT2 + byte(F_CPU/1024*35E-3);	// 35 ms (knapp eine ganze Periode!)
  TIFR2|=1<<2;		// Interruptflag löschen
 }else if (TIFR2&1<<2) {	// Verklemmer?
//  PORTD|=1<<6;		// DEBUG
  eedata.debug[14]++;
  TWCR = 0x80;		// ausschalten
// Noch ein Problem: Verklemmer löst sich dadurch nicht (umgehend) auf!
  _delay_us(100);
  init();		// einschalten mit ACK und Interrupt
//  PORTD&=~(1<<6);	// DEBUG
 }
#elif 1
 if (PINC&1<<4) {	// High = in Ordnung
  wdt_reset();
 }else if (GPIOR0&1<<4) {	// Verklemmer 250 ms lang?
  GPIOR0&=~(1<<4);
  wdt_reset();
  eedata.debug[14]++;
  TWCR = 0x80;		// ausschalten
  _delay_us(50);
  init();		// einschalten mit ACK und Interrupt
 }
#endif
 if (TWCR&1) return;	// nichts zu tun, ISR kümmert sich
 byte twsr = TWSR/*&0xF8*/;
 switch (twsr) {
#if 0	// Schreibadresse empfangen: Wird von ISR verarbeitet
  case 0x60: {
   TWCR=0xC5;	// ACK
   twiBuf.lvl = 0x60;
   twiBuf.ofs = 0;
   twiPtr=&twiBuf.lvl; twiLen=sizeof twiBuf-1; 
  }return;
  // Byte empfangen: Wird in der ISR verarbeitet
  case 0x80:
  case 0x88: {
   byte b=TWDR;
   if (twiLen) {
    TWCR=0xC5;	// ACK
    *twiPtr++=b;
    --twiLen;
   }else TWCR=0x85;	// NAK
  }return;
#endif
  // STOP oder RepeatStart: Paket empfangen
  // Byte 0 = Level, Byte 1 = Offset
  // Bei Level>7: Byte 0 = High-Adresse, Byte 1 = Low-Adresse
  // Bei Anzahl Bytes == 0: Level 0 (Echtzeituhr)
  // Bei Anzahl Bytes == 1: Offset 0
  case 0xA0: {
   onTwiInput();
   TWCR=0xC5;	// ACK (jetzt erst, im Hintergrund könnten nun neue Bytes kommen)
  }return;
  
  // Leseadresse empfangen
  case 0xA8: {		// Noch kein ACK!
   byte lvl = twiBuf.lvl;
   byte ofs = twiBuf.ofs;
   if ((lvl&0xC0)==0x40) switch (lvl&0xF) {	// 01ff llll
    case 0: copybuf(uhr,sizeof uhr,ofs); break;
    case 1: copybuf(eedata,sizeof eedata,ofs); break;
    case 2: copybuf(&l2,sizeof l2,ofs);
    if (!l2.pwmok && sizeof l2.pwms>ofs) {
     memset(twiBuf.buf,cNaN,sizeof l2.pwms-ofs);
     byte l=twiLen-1;
     twiBuf.buf[l]=~OneWire::crc8(twiBuf.buf,l); // CRC korrigieren
    }
    break;	// Gesondert NaN liefern bei deaktiviertem Pulsgenerator
    case 3: copybuf(&l3,sizeof l3,ofs); if (lvl&0x10) L3::fbaus=250; break;
    case 4: copybuf(&l4,sizeof l4,ofs); break;	// ADC + Linearisierung
    case 5: copybuf(regler,sizeof regler,ofs); break;	// Alle Reglerdaten
    case 6: copybuf(&odo,sizeof odo,ofs); break;
    case 7: copybuf(&L7::füllstand(),1,ofs); break;
    case 8: if (!ofs) {
     byte l=Regler::log.emit(twiBuf.buf);	// max. 60 Bytes
     twiPtr=&twiBuf.ofs;	// 1 davor
     twiBuf.ofs = l;	// Sonderfall "variable Länge" muss vom I²C-Empfänger gesondert verarbeitet werden
     twiBuf.buf[l]=~OneWire::crc8(twiBuf.buf,l); // Längenbyte nicht in CRC
     twiLen=l+2;
    }break;
    default: twiLen = 0;	// nichts (bzw. 0xFF) liefern
   }else{
    twiLen = 0xFF;	// Kode für unendlich (nur der I²C-Master bricht das Lesen ab)
    twiPtr = (byte*)(lvl<<8|ofs);	// Adresse bauen
   }
   twiOnRead();	// TWDR füttern: Hier ist das ACK
  }break;
  case 0xF8: {	// kommt gelegentlich: Kein Fehler
   eedata.debug[15]++;
  }break;
  default:
  eedata.debug[12]++;
  eedata.debug[13]=twsr;
  TWCR = 0xC5;	// dürfte nie vorkommen
 }
}
Detected encoding: ASCII (7 bit)8