Einzelnen Beitrag anzeigen

Der_Unwissende

Registriert seit: 13. Dez 2003
Ort: Berlin
1.756 Beiträge
 
#15

Re: UART Terminal-Programm zur Kommunikation mit ATmega8 µC

  Alt 6. Feb 2007, 12:20
Vorwort:
Ok, bin gerade fertig geworden und glaube es ist doch etwas umfangreicher. Deswegen vorab die Warnung, lass Dich nicht davon beirren, dass ich etwas viel geschrieben habe. An sich ist ein guter Teil mehr der Vollständigkeit halber erwähnt, als dass er wirklich direkt mit Deinem Problem zu tun hat (wirst schon merken wann was zutrifft).
Versuche einfach nicht zusehr an Details hängen zu bleiben, dann sollte alles gut klappen. Bei Problemen frag einfach nochmal gezielt nach.
Ende Vorwort

Zitat von Manado:
ich hab noch ein doofes Problem mit dem Aufruf des OnRXChar (oder Flag) EREIGNISSES.
--> Was ist ein "Ereignis". Wie wird das, was im Aufruffall passieren sollte, deklariert?

(Ich bin Hobbyelektroniker, und eher der Bedarfs-informatiker, deswegen muss man mir meine Fragen verzeihen ...µC - Assembler ist sooo schön einfach .)
Nichts zu entschuldigen, ich sehe nicht warum ein Vollzeit-Informatiker nicht die gleiche Frage stellen sollte?! Ist doch schön, dass Du wenigstens auch nachfragst (und damit ja auch dein Interesse untermauerst) und nicht nur eine fertige funktionierende Lösung forderst (soll's ja auch geben).
Aber da Du fragst und ausgerechnet ich antworte, musst Du halt mit etwas mehr Text rechnen!

Jedenfalls zum Problem der Ereignisse und die Frage was das ist. Ereignisse in Delphi entsprechen erstmal dem, was man gemeinhin als Ereignis versteht, es passiert irgendwas. Soweit so klar. Jetzt gibt es zwei Möglichkeiten zu merken, dass etwas passiert ist:
  1. Polling - Man fragt ständig nach ob etwas passiert ist
  2. Benachrichtigung - Eine spezielle Behandlung wird genau dann aufgerufen, wenn etwas bestimmtes passiert ist

Beides findest Du, z.B. auch bei µC. Gerade bei diesen legt man viel Wert darauf, dass man nicht pollt. Polling impliziert natürlich, dass man ständig etwas tut, nicht sehr energiesparend. Viel schöner ist es, wenn man alles (insbesondere auch die CPU) schlafen legt und nur einen minimalen Teil laufen lässt. Dieser hat einfach die Möglichkeit einen Interrupt auszulösen, der dann die CPU wieder aufweckt, die das Ereignis (den Interrupt) behandelt.
Bei einer CPU wird das über eine Interrupt Service Routine (ISR) gemacht. Zu jedem Interrupt kann hier ein Routine registriert werden, die direkt angesprungen wird, wenn ein Interrupt eintritt (kennst Du vielleicht schon?).
An sich ist das auch schon die Grundlage dessen was hinter einem Ereignis in Delphi steckt. Du bekommst ein Signal (im Beispiel die Unterbrechung) und gehst in die Behandlung (die ISR).

In Delphi wäre im Prinzip das gleiche denkbar. Auch die normale x86-CPU verwendet schließlich Interrupts. Das Problem ist aber, dass jedes Interrupt die CPU auch echt unterbricht. Dann gibt es auch sehr viele Programme, mit sehr vielen Ereignissen, da müsste man schon sehr sehr viele verschiedene Interrupts anbieten und einen Mechanismus, der die eindeutig zuordnet. Schließlich möchtest Du nicht das jmd. deine Behandlung überschreibt und Du über dein Ereignis nie informiert wirst.
Viel schöner (und leichter) ist es, wenn Du Dich gar nicht soweit runter begeben musst, sondern das ganze in der Software löst. Die Idee bleibt aber die gleiche, Du verwendest ein spezielles (Software-)Signal, dass das Eintreten eines Ereignisses anzeigt. Deine Behandlung findet dann ebenfalls in der SW statt, hier kannst Du ja leicht eine Tabelle erstellen, die jedem solchen Signal eine Routine zuordnet. Wie eine Komponente feststellt ob ein Ereignis eintritt kann nicht pauschal gesagt werden, hängt natürlich stark von der Komponente ab.

Für einen einfachen Fall könnten wir ja annehmen, dass Du eine Komponente zum Laden von AZB -Dateien baust. In einer Methode Load wird das Format ausgelesen. Sagen wir jetzt, dass das Auslesen asynchron in einem eigenen Prozess stattfindet. Die Prozedur Load kehrt sofort zurück, bevor die ganze Datei geladen ist. Der Prozess füllt in Ruhe eine Datenstruktur und löst sobald er fertig ist einfach ein Ereignis aus (Fertig).
Ok, ist nicht unbedingt ein sinnvolles Beispiel, aber ich hoffe Du siehst dass hier ein Ereignis ausgelöst werden sollte (sonst weiß der Benutzer nie wann das Laden fertig ist).
Was Du in dem nebenläufigen Thread machen würdest ist einfach das normale Laden der Datei. Bist Du damit fertig weißt Du das und musst das jetzt auch nach außen weiter reichen. Hier kommt das Signal ins Spiel. Für die Signalisierung hast Du aber gleich wieder mehrere Möglichkeiten.
Fangen wir mit der Standardmöglichkeit in Delphi an: Funktionszeiger.

Ja, Du magst Assembler aber keine Zeiger? Interessant
Die Idee eines Zeigers ist sicherlich klar, Du speicherst darin einfach eine Adresse. Der Nutzen einer Adresse ist auch sehr einfach zu erklären. Ein Zeiger ist immer ein CPU-Wort (im Moment 32 Bit) breit (was eine ideale Größe für ein Datum darstellt!). Hast Du eine große Datenstruktur (z.B. ein Record der Größe 128 Byte), dann würde diese bei Übergabe Call-By-Value auch kopiert werden. Das gilt (um es kompliziert zu machen) aber nur für statische Arrays (Arrays fester Länge) und Records. Klassen und dyn. Arrays werden eh anders übergeben (nur der Vollständigkeit halber erwähnt).
Statt 128 Byte zu kopieren kann man auch einfach die Adresse des Records (4 Byte) übergeben. Da die Adresse auf das Datum im Speicher "zeigt" und Du weißt was für einen Typen Du an dieser Adresse erwartest hast Du so Zugriff auf die selben Daten. Veränderst Du jetzt auch diese Daten, so wird der Unterschied auch nochmal deutlich. Beim Aufruf Call-By-Value wird eine echte Kopie übergeben. Änderungen finden nur auf der Kopie statt, das Original merkt nichts davon.
Übergibst Du die Adresse, so arbeitest Du direkt auf dem Speicher des Originals, Änderungen werden also sofort übernommen.

So, genug über allgemeine Zeiger, Sinn und Nutzen kannst Du sicherlich an verschiedenen Stellen (zum Beispiel hier in der DP ) nachlesen.
Was also wird ein Funktionszeiger sein? Richtig, es handelt sich um die Adresse einer Funktion. An sich werden wir im folgenden eher Methodenzeiger betrachten. Zwischen beiden besteht ein wichtiger Unterschied, auch wenn sie sich sehr ähneln! Von einer Methode spricht man immer nur im Zusammenhang mit einer Klasse. Alle Prozeduren/Funktionen, die zu einer Klasse gehören werden als Methoden bezeichnet.

Delphi-Quellcode:
type
  TFoo = class(TObject)
    public
      procedure doFoo();
  end;

....

procedure doFoo(); // <-- normale Funktion, hat nichts mit TFoo zu tun!
begin
 //...
end;

procedure TFoo.doFoo(); // <-- Methode der Klasse TFoo!
begin
 //...
end;
Ein Funktionszeiger kann nicht auf eine Methode zeigen und ein Methodenzeiger kann nur auf Methoden zeigen! Das alles (jede Variable, Methode, Funktion, ...) eine eigene Adresse hat ist sicherlich klar. Hinter jedem deiner normalen Funktionsaufrufe steckt auch nichts anderes, als dass man die Adresse der Funktion nimmt und den Code an dieser Stelle ausführt. Das ist Dir aber normalerweise völlig egal (zurecht).

Jetzt fehlt nur noch die Zusammenführung von Methodenzeigern mit Ereignissen (die Du vielleicht schon siehst/erahnst). Es ist auch hier wieder sehr einfach, Du gibst der Komponente einfach die Adresse einer Deiner Methoden, die immer dann aufgerufen werden soll, wenn das entsprechende Ereignis auftritt. Diese Adresse speichert die Komponente als Variable. Der Typ der Variable ist eben ein Methodenzeiger. Tritt das Ereignis ein, so kann die Komponente prüfen ob die Variable gesetzt wurde (Adresse <> nil). Ist dies der Fall, wird einfach der Code an dieser Adresse ausgeführt. Man spricht hier von einem Call-Back (sollte klar sein warum).
Das wichtige ist, dass Du hier eine beliebige Rückrufadresse übergeben kannst, Du also selbst eine beliebige Behandlung erstellen kannst. Das gilt mit einer wichtigen Einschränkung, der Zeiger ist immer typisiert, die Signatur der Methode (Parameter und Rückgabetyp) muss also mit der deklarierten übereinstimmen.

Das ganze klingt jetzt etwas kompliziert, ist es aber eigentlich gar nicht. Als erstes überlegst Du Dir, was für Ereignisse Du hast, die eintreten können. Hier schaust Du Dir dann an, was für Informationen Du weiterreichen möchtest. Nimm hier ruhig Ereignisse, die es schon gibt. Beim einem Maus-Click reicht es Dir zu wissen, was angeklickt wurde und welche Taste das Ereignis auslöste. Bei einer Mausbewegung sieht das schon anders aus, hier kann jede Taste gedrückt sein (oder nicht), die Position ist wichtig, ...
Für alle Informationen, die jmd. interessieren könnten legst Du einfach einen Parameter fest und erstellst Methodenzeiger für jede Art von Benachrichtigung. So wirst Du die selben Informationen für das Drücken oder Loslassen eines Buttons übergeben, hier sind also nicht zwei Typen nötig!

Ich mache an dieser Stelle der Einfachheit mit der Datei AZB weiter, hier legen wir einfach zwei mögliche Ereignistypen fest. Der eine informiert über den Fortschritt, der andere über das Fertigwerden/Beginnen des Laden. Es wird immer die Datei (Typ TAzb) übergeben, die das Ereignis auslöste. Beim Fortschritt kommt zusätzlich ein Integer Wert hinzu, der den prozentualen Fortschritt angibt.
In Delphi sieht das dann so aus:

[delphi]
type
TAzbEvent = procedure(const Sender: TAzb) of Object;

TAzbProgressEvent = procedure(const Sender: TAzb; const Progress: Integer) of Object;
[/quote]

An sich ist das wie gesagt ganz einfach. Du hast auf der Linken-Seite des Gleichheitszeichen wie immer den Namen des neuen Datentyp. Rechts kommt zunächst ein prozedure (oder eben function). Das zeigt an, dass es sich hier um einen Funktions/Methodenzeiger handelt. Diesem Schlüsselwort folgen direkt die Argumente (oder nichts), bei einer Funktion kommt wie gewohnt dahinter noch ein : Rückgabetyp. Würdest Du es dabei belassen, hast Du einen Funktionszeiger. Das of Object hingegen zeigt an, dass es sich um den Zeiger auf eine Methode handelt.
Die beiden Datentypen können jetzt wie jeder andere Datentyp auch behandelt werden. Du kannst ganz normale Variablen von diesem Typen anlegen.

[pre]
type
TAzb = class(TObject)
....
public
OnBegin: TAzbEvent;
OnFinish: TAzbEvent;
....
end;
[/pre]

Wird jetzt eine Datei geladen, kannst Du ganz einfach prüfen ob OnBegin <> nil ist und ggf. die Methode dahinter aufrufen

[pre]
procedure TAzb.start(const fileName: String);
begin
// Start signalisieren
if assigned(self.OnBegin) then
begin
self.OnBegin(self);
end;

// ....
end;
[/pre]

Das assigned ist nichts anderes als <> nil. Der Aufruf der Methode findet so statt, wie bei jeder anderen Methode auch (geht auch ohne qualifizierendes self.). Als Parameter möchte die Methode das auslösende TAzb-Objekt bekommen. Nun ja, da es sich um ein Callback handelt ist ja das Objekt selbst auch der Auslöser dieser Benachrichtigung. Also übergibt es sich selbst. Beim Fortschritt würde es sich selbst und den eigenen Fortschritt beim Laden übergeben.
Das wirkt vielleicht etwas ungewohnt, aber Du musst nur im Hinterkopf behalten, dass OnBegin auf eine Methode zeigt, die eben nicht im gleichen TAzb-Objekt liegen muss.

Jetzt bleibt zuletzt noch die Frage, wie man jetzt dieser Variablen einen Wert zuweißt. Aber auch das ist wiederum einfach. Du erstellst einfach eine Klasse, die eine Methode mit der vorgegebenen Signatur enthält. Diese kannst Du dann übergeben:
[pre]
type
TAzbEventHandlerClass = class(TObject)
private
azb: TAbz;
protected
procedure OnAzbBegin(const sender: TAzb);
...
end;

// z.B. im Constructor
....
begin
azb := TAzb.Create;

// hier jetzt die eigentliche Zuweisung
azb.OnBegin := self.OnAzbBegin;
...
end;

procedure OnAzbBegin(const sender: TAzb);
begin
// eigentliche Behandlung
// Wird immer aufgerufen, wenn azb's OnBegin aufgerufen wird
// Aber erst nachdem azb.OnBegin := self.OnAzBegin aufgerufen wurde!
end;

[/pre]

Wie Du hier siehst ist der Name der Prozedur und die Sichtbarkeit (usw.) völlig egal. Wichtig ist nur, dass die die gleichen Parameter besitzt. Wie Du auch siehst muss man nicht auf jedes Ereignis reagieren. Die Abfrage assigned(X) wird schon prüfen, ob eine Behandlung registriert wurde oder nicht (ohne hättest Du sonst ein Problem).

Ok, ich hoffe so ganz grob ist Dir die Idee jetzt klar. Der Auslöser einer Nachricht legt einen Mehtodenzeiger fest. Über diese Art von Methode wird das Eintreten eines Ereignisses signalisiert. Informationen an den "Informierten" können über die Parameter dieser Methode übergeben werden.
Der Auslöser besitzt für jedes Ereignis eine Variable, die die Adresse einer Methode speichern kann. Tritt ein Ereignis ein (über das Informiert werden soll), schaut der Auslöser nach, ob die Adresse <> nil ist und benachrichtigt ggf. die Methode an der gespeicherten Adresse. Dem Auslöser ist nur die Adresse bekannt, mehr interessiert den nie!
Auf der anderen Seite gibt es jmd. der sich benachrichtigen lassen möchte. Dieser jmd. legt einfach eine Methode an, deren Signatur der des Ereignisses entspricht, über das man sich informieren lassen möchte. Diese Methode wird dann einfach der Eigenschaft/Variable des Auslösers zugewiesen, die für die Benachrichtigung zuständig ist.
Das ist dann alles!

Ja, wie gesagt, es gibt noch andere Wege. Dieser hier ist der Standard-Delphi-Weg. Auf die anderen werde ich deswegen nicht ganz so detailliert eingehen (außerdem hast Du ja auch noch was vor die Woche! gut, ich auch!).
Das was jetzt alles vermeintlich etwas kompliziert wirkt geht natürlich auch leichter. Du hast doch die TComPort Komponente installiert. D.h. Du findest die irgendwo in deiner Palette? Nimm Dir dort so ein Exemplar raus und platzier die auf dem Formular (wie ein Button). Dann gehst Du in den ObjectInspector. Der hat zwei Tabs, Eigenschaften und Ereignisse. Hier wählst Du Ereignisse aus und findest u.A. OnRxChar. Daneben ist ein leeres weißes Feld, da klickst Du doppelt rein und Delphi erstellt für Dich automatisch eine Behandlungmethode. Die hat dann schon die richtige Signatur und ist auch sofort beim TComPort als Call-Back eingetragen.

Ja, wie gesagt, es gibt noch andere Mechanismen um Ereignisse zu signalisieren. Der nächste Weg, der auch in Delphi eingesetzt wird ist etwas Windows-spezifischer. Es handelt sich dabei um die Windows-Botschaften. Windows basiert grob gesagt auf zwei Dingen, Fenster und Fenster-Botschaften. Ein Fenster befindet sich eigentlich nur in einer Endlosschleife, in der auf die nächste Nachricht gewartet wird. Wird eine solche Nachricht empfangen, wird diese entsprechende behandelt. Danach wird wieder auf die nächste Nachricht gewartet. Alle Ineraktionen finden über solche Botschaften statt. Wird ein Fenster verschoben, neu gezeichnet, die Maus gedrückt, eine Taste auf der Tastur gedrückt, ... alles löst eine Nachricht aus. Auf die Details möchte ich hier nicht weiter eingehen, wichtig ist nur zu wissen, dass es diese Botschaften gibt und dass die verschickt werden können. In Delphi kannst Du Methoden so deklarieren, dass diese aufgerufen werden, sobald eine Nachricht ankommt. Zudem kannst Du natürlich auch eigene Nachrichten erzeugen (gilt nicht nur für Delphi!).

Der dritte Weg der mir noch einfällt ist dem ersten nochmal sehr ähnlich. Es gibt noch das Observer-Pattern. Dabei handelt es sich um ein Designpattern (Nähreres kannst Du ergooglen). Das Muster ist sehr einfach, Du hast etwas, das Beobachtet wird (das Observable) und einen oder mehrere Beobachter (Observer). Das Observable bietet die Möglichkeit, dass sich Beobachter bei ihm registrieren (und natürlich auch wieder deregistrieren). Die Observer implementieren einfach ein bestimmtes Interface. Damit sichern die Observer zu, dass sie eine Methode mit einer bestimmten Signatur (Parameter, Rückgabetyp aber auch Namen!) besitzen, die öffentlich ist. Was genau die Methode macht bleibt hinter der Schnittstelle verborgen, man weiß nur, dass die Methode(n) vorhanden ist(/sind).
Tritt das Ereignis ein, so wird das Observable einfach die entsprechende Methode aller registrierten Observer aufrufen (wie ein Call-Back). Anders als beim Methodenzeiger (es gibt genau einen) können hier also beliebig viele Observer benachrichtigt werden.
Dieser Ansatz entspricht übrigens dem Objekt Orientierten Ansatz. Dazu muss ich aber sagen, dass das Observer-Pattern unabhängig von der OOP ist. Design-Pattern können in beliebiger Weise implementiert werden. Sie beschreiben nur ein Problem (Beobachten eines Ereignisses durch mehrere Beobachter) und dessen Lösung (Registrierte Observer mit bestimmter Schnittstelle). Ob es sich bei den Observern um Objekte handelt oder Module, Komponenten oder Methodenzeiger, egal, das gehört nicht mehr zum Pattern.

Methodenzeiger kann man zwar für OOP halten, zumal eine Methode nur im Zusammenhang mit Objekten auftauchen, aber da würdest Du jetzt falsch liegen. Klassen/Objekte sind keineswegs der OOP vorbehalten. OOP ist nur ein Paradigma, es gibt gewisse Dinge, die man zusichern möchte. Dazu werden bestimmte Vorraussetzungen getroffen, die ein paar Dinge leichter möglich machen. In streng OOen Sprachen würdest Du nur mit Klassen und Schnittstellen arbeiten können und kannst damit sehr viel leichter einen Teil der Eigenschaften aufrecht halten. Die wenigsten Sprachen sind aber streng OO, insbesondere sind es nicht C++, Java, Delphi,... Eine der bekannten "echten" OO Sprachen wäre hier eher SmallTalk.
An sich versucht man aber in der OOP explizite Zeiger zu vermeiden. Objekte werden hier immer als Referenz übergeben, wobei eine Referenz nur ein impliziter Zeiger ist. Die Arbeit mit einer Referenz entspricht dem transparenten Arbeiten mit Zeigern, Du merkst hier nichts von den Nachteilen/Problemen mit Zeigern, musst Dich auch nicht um das Dereferenzieren kümmern!
Lange Rede, kurzer Sinn, dass was Du als OO eingestuft hattest fällt nicht darunter

Ja, das wär's dann auch,
Gruß Der Unwissende
  Mit Zitat antworten Zitat