Go To Ja Oder Nein / Beispiel Microcontroller
StartSeite | GoToJaOderNein/ | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
Im Rahmen der GoToJaOderNein Frage brachte HelmutSchellong dieses vertrackte Beispiel:
- Situation
- In einer Microcontroller Steuerung (3MB) existiert innerhalb einer Kommunikationsgruppe (100KB) eine Modemsteuerung (3KB). Zur Kommunikation mit dem Modem eine komplexe Funktion F1() aufgerufen. Aufgrund von diversen Interrupts, Timeouts und protokollbedingten Wartezeiten läuft diese Funktion bis zu 100 Sekunden und mehr.
- Problem
- Im Laufe des Projektes stellt sich heraus, dass die lange Wartezeit in der Hauptschleife des Microcontrollers sämtliche anderen Aufgaben des Systems blockiert. So werden keine Tastatureingaben mehr angenommen und die Anzeige wird nicht mehr erneuert.
Beispiel Microcontroller Hauptschleife:
| void main(void)
{
while (1) {
if (MU.req) Requests();
//...
F1(); //Modem-Kommunikation: CallBack,Anrufbereitschaft,OKAY-/FAIL-Meldung
//...
Display(HAUPT_MENU);
}
} |
|
|
- Aufgabe:Verändere F1 so, dass sie nach maximal 20us retourniert. Dabei darf sich für den angesteuerten UART und das daran angeschlossene Modem nichts ändern. Die neue Microcontroller Steuerung sorgt für die notwendige Aufrufhäufigkeit von F1().
Lösungsansätze | |
HelmutLeitner hatte so eine ähnliche Situation kürzlich bei der Steuerung eines komplexen mechanisch-elekronischen Spielautomaten. Seine Lösung war die Umstellung auf eine Zustandsmaschine, die durch Events und durch Timerroutinen gesteuert werden und deren Zustände und Übergänge in Konfigurationsdateien festgelegt werden können. Mit der Lösung war er recht glücklich, hat jedoch nicht das Gefühl, dass goto ihm dabei eine bessere Lösung ermöglicht hätte.
- Da die Modemkommunikation in einem größeren System eingebettet ist, ist die Implementierung einer weiteren Zustandsmaschine nicht indiziert. Die Integration in die globale Kommunikationszustandsmaschine wäre komplex.
Für Systeme mit einer einfachen Ereignisschleife schlägt HelmutLeitner folgende Technik vor:
Wenn die Grundstruktur so aussieht:
| ENDLOS {
F1();
ProcEvents();
} |
|
|
und in F1neu unakzeptable Wartezeiten gefordert werden, dann könnten diese so adaptiert werden:
| F1() {
....
// WAIT(50 sec); ersetzt
WaitProcEvents(50);
....
}
WaitProcEvents(sec) {
endtime=now()+sec;
while(now()<endtime) {
ProcEvents();
}
} |
|
|
- Original Kommentar
- Das ist natürlich sehr verkürzt. Aber im Prinzip. Wir haben so etwas ähnliches bei der Modularisierung der TWAIN-Schnittstelle gemacht, wo normalerweise die Haupt-Event-Loop einer Applikation verändert werden muss. Wir haben das nach unten verlagert, so dass alles in einem einzigen Aufruf image=ImageCreateScannerTwain(ausschnitt,auflösung, farbtiefe); passiert, wobei dort die Windows-Events genauso wie in der Haupt-Event-Loop abgearbeitet werden.
Alternativvorschlag | |
Lokale Variablen lassen sich nicht über zwei Aufrufe hinweg retten, du läufst nur Gefahr, versehentlich genau das anzunehmen. Stattdessen würde ich die große Funktion in kleinere zerlegen, und zwar genau dort, wo bei Helmut Labels stehen. Jede Funktion gibt einen Zeiger auf eine Funktion zurück, wo die Bearbeitung demnächst fortgesetzt wird, entweder auf sich selbst oder auf die nächste.
Der Gewinn der GOTOs liegt ja hier darin, dass du in eine Schleife mitten hinein springen kannst. Ich schlage vor, die Schleife zunächst "wegzutransformieren", die Funktion ist jetzt rekursiv. (Eigentlich ist das die strikte CpsTransformation?.)
Dann wird die Funktion in mehrere gegenseitig rekursive Funktionen zerlegt, und bei jedem solchen Aufruf kann man sich einfach die nächste Funktion und ihre Parameter merken, anstatt sofort dorthin zu springen. (In einer funktionalen Sprache merkt man sich einfach eine Funktion.) Die aufrufende Funktion sieht praktisch genua wie in der GOTO-Lösung aus.
Angenehmer Nebeneffekt: keine statischen Variablen und eine reentrante Funktion, was ein Riesengewinn ist, wenn in der nächsten Version ein zweiter UART angesteuert werden muss.
In einer ausrecheind mächtigen Sprache geht das sogar alles nahezu automatisch, etwa mit call/cc in SpracheScheme oder MonadCont? in SpracheHaskell.
HelmutSchellong hat der fraglichen Funktion mehrere return verpaßt, nämlich immer da, wo sie wartet. Sie wird dann mit einem numerischen Parameter aufgerufen, anhand dessen mitten rein gesprungen wird.
Vorher:
|
int ModemAT(cBYTE *at)
{
UNS sec=16*5;
BYTE msg=0;
int i, r=0;
Mo.bit.execat=1;
for (; ; ++at) {
switch (*at) {
default : DFL1:; Putc(*at); break;
case 0 :
case ' ':
case '\r':
case '\n': KZE:;
while (*at && at[1]<=' ') ++at;
Mo.t=MU.ul64ms;
Mo.msg=Mo.cmd=0;
Write("\r\n", 2);
AGAIN:;
while (Mo.msg==0&&Mo.cmd==0 && MU.ul64ms-Mo.t<(UNS4)sec) {
if (GetKeyCode()==kESC) return 1;
}
if (Mo.cmd) { Mo.cmd=0; goto AGAIN; }
if (Mo.msg==M_RING || Mo.ring) {
Mo.msg=Mo.ring=0; goto AGAIN;
}
if (Mo.msg) {
if (msg&&Mo.msg!=msg) { r=-1; goto RET; }
break;
}
if (msg&&!(Mo.Bit.cable&&Mo.Bit.autoansw)) {
r=-1; goto RET; //TIMEOUT
}
break;
case '%': switch (*++at) {
default :
case '%': goto DFL1;
case 0 : goto KZE;
case 'd': Delay(1000);
break;
case 'p': Delay( 250);
break;
case 't': for (sec=i=0; i<3 && DIGIT(at[1]); ++i) {
sec*= 10, sec+= *++at-'0';
}
if (sec>=100) sec+=63, sec>>=6;
else sec*=16;
if (sec<5) sec=5*1;
if (Mo.Bit.cable) {
if (!Mo.Bit.autoansw) {
if (sec>=16&&sec<16*15) sec=16*15;
}
else if (sec>16*30) sec=16*30;
}
break;
case '0': msg=0; break;
case 'C': msg=M_CONNECT; break;
case 'O': msg=M_OK; break;
case 'T': Putc(Mo.Bit.tonwahl?'T':'P');
Writes(Mo.telefnr);
break;
}
break;
}
if (*at==0) break;
}
RET:
Mo.bit.execat=0;
return (r);
}
|
|
|
Nachher:
|
#define JMP(n) Mo.jmp[1]=n; JMP##n:
int ModemAT(cBYTE *ats)
{
static UNS sec;
static BYTE msg;
static cBYTE *at;
int i, r=0;
/* Neuer Code: */
if (!(Mo.req&4)) Mo.req|=4, Mo.jmp[1]=0, Mo.bit.execat=1,
sec=16*5, msg=0, at=ats; /* static Init Start-Aufruf */
switch (Mo.jmp[1]) { /* je nach Status beim Verlassen, wieder einspringen */
case 1: goto JMP1;
case 2: goto JMP2;
case 3: goto JMP3;
}
/* :Neuer Code */
for (; ; ++at) {
switch (*at) {
default : DFL1:; Putc(*at); break;
case 0 :
case ' ':
case '\r':
case '\n': KZE:;
while (*at && at[1]<=' ') ++at;
Mo.t=MU.ul64ms;
Mo.msg=Mo.cmd=0;
Write("\r\n", 2);
JMP(1); /* <<== */
AGAIN:;
if (Mo.msg==0&&Mo.cmd==0 && MU.ul64ms-Mo.t<(UNS4)sec) {
return 1;
}
if (Mo.cmd) { Mo.cmd=0; goto AGAIN; }
if (Mo.msg==M_RING || Mo.ring) {
Mo.msg=Mo.ring=0; goto AGAIN;
}
if (Mo.msg) {
if (msg&&Mo.msg!=msg) { r=-1; goto RET; }
break;
}
if (msg&&!(Mo.Bit.cable&&Mo.Bit.autoansw)) {
r=-1; goto RET; //TIMEOUT
}
break;
case '%': switch (*++at) {
default :
case '%': goto DFL1;
case 0 : goto KZE;
case 'd': //Delay(1000);
Mo.t= MU.ul64ms;
JMP(2); /* <<== */
if (MU.ul64ms-Mo.t<16ul) return 1;
break;
case 'p': //Delay( 250);
Mo.t= MU.ul64ms;
JMP(3); /* <<== */
if (MU.ul64ms-Mo.t<4ul) return 1;
break;
case 't': for (sec=i=0; i<3 && DIGIT(at[1]); ++i) {
sec*= 10, sec+= *++at-'0';
}
if (sec>=100) sec+=63, sec>>=6;
else sec*=16;
if (sec<5) sec=5*1;
if (Mo.Bit.cable) {
if (!Mo.Bit.autoansw) {
if (sec>=16&&sec<16*15) sec=16*15;
}
else if (sec>16*30) sec=16*30;
}
break;
case '0': msg=0; break;
case 'C': msg=M_CONNECT; break;
case 'O': msg=M_OK; break;
case 'T': Putc(Mo.Bit.tonwahl?'T':'P');
Writes(Mo.telefnr);
break;
}
break;
}
if (*at==0) break;
}
RET:
Mo.bit.execat=0;
Mo.req&=~4;
return (r);
}
|
|
|
Fazit | |
Die Beschränkungen der Microcontroller Programmierung erfordern eine kompakte Umsetzung von großen Mengen sequentieller Ablaufsteuerungslogik, die mit Sonderfällen und anderen haarsträubenden Details der hardwarenahen Programmierung durchsetzt sind. Der Einsatz von gotos in diesem Kontext unterstützt die inkrementelle Anpassung an weitere Ausnahmen und Anforderungen. Wie Helmut Schellong in seiner Lösung demonstriert, kann man durch die gewonnene Flexibilität auch in ausweglos erscheinenender Lage noch praktikable Ergebnisse erzielen.
Weitere erschwerende Kriterien sind in diesem speziellen Fall die Einbettung in ein deutlich größeres System und der Mangel an Testcode und Testdaten. Dadurch ist eine großangelegte Umarbeitung der Funktionen sehr aufwendig und mit hohem Risiko verbunden.
Die hier dargestellte Funktion wird u. a. nämlich 13-mal in F1() aufgerufen, wie hier, mittels switch () { goto JMPn; ... }. Und F1() ist etwa 6-fach größer als die ModemAT?() hier.
Die Lösung mittels weiteren gotos ist also in diesem Falle der Weg des geringsten Risikos und des minimalen Aufwandes.
Fragen und Anmerkungen | |
Mit allem nötigen Respekt möchte ich nur anmerken: Der oben stehende Code ist so verwurschtelt und unverständlich für mich, dass ich mich ohne ausführliche TestSuite kaum trauen würde, auch nur einen Bezeichner-Namen zu ändern, geschweige denn die Struktur - zumal ich in der SpracheCee nicht unbedingt fit bin. -- IljaPreuß
Ich meine, das Beispiel ist nicht geeignet, als Beitrag für die ürsprüngliche Diskussion GoToJaOderNein zu dienen. Wenn jemand die drei im Nachher-Code hinzugekommenen Gotos (unter Wahrung mindestens ebenbürtiger Güte) eliminieren würde, wäre lediglich gezeigt, dass man Code statt mit 9 Gotos ebensogut mit 6 Gotos hinzuschreiben in der Lage ist. Der ürsprüngliche Punkt war aber nicht "Neun Gotos oder sechs Gotos", sondern GoToJaOderNein. Ein gut geeignetes Beispiel sähe so aus: Jemand hat Vorher-Code ohne Gotos. Dann gibt es eine kleine zusätzliche Forderung, die derjenige durch (wenige) Gotos meint gut gelöst zu bekommen (== Nachher-Code). Nun wäre es Aufgabe der übrigen Teilnehmer, zu zeigen, dass dem nicht so ist, sondern es ohne Gotos gleichwertig oder besser hinzubekommen ist. Wie gesagt, das Beispiel von HS leistet dies nicht. -- VolkerGlave
Also, ich kann mir nicht helfen, mir gefällt diese Lösung mit dem Macro. Nicht, dass ich es so schreiben würde, wenn ich in der Programmier-Situation ausreichend Zeit hätte. Aber als Hack ist es IMHO elegant. (Und natürlich muss man den Code vor Mitarbeitern und Studenten verstecken, damit keiner auf die Idee kommt, das nachzuahmen) -- HelmutLeitner
Könnte man das nicht analog zu DuffsDevice? mit einem switch realisieren?
- DuffsDevice? funktioniert in diesem Falle leider nicht, da es selbst auf switch/case aufsetzt und daher in einer Funktion mit switch/case-Konstrukten nicht verwendet werden kann. Hier ein Link zu diesem Thema: http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html -- JoachimStolberg?
KategorieProgrammierBeispiele
StartSeite | GoToJaOderNein/ | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
Text dieser Seite ändern (zuletzt geändert: 1. September 2005 13:18 (diff))