Software Hier geht es dort hin.
Aus Wiki
Inhaltsverzeichnis |
Vorwort
Beim Erstellen der Software stand die leichte Benutzbarkeit und die Wiederverwertbarkeit des Programmcodes um Vordergrund. Bei der Benutzbarkeit stand vor allem im Vordergrund, dass auch ein wenig geübter Anfänger sich einarbeiten kann. Denn die schwierigste Phase beim Lernen einer neuen Programmiersprache ist das Erstellen des ersten "Hallo Welt" Programms. Hat man dieses Stück Programm verstanden, geht man auf Entdeckungsreise und probiert mehr und mehr aus. Interessanter und schwieriger ist das ganze, wenn man sich in den Kopf gesetzt hat auch gleich was mit Netzwerk oder Internet zu machen. Auf Mikrocontrollern ist das um so schwieriger, da es ja hier keinen Bildschirm gibt, wo man das erste "Hallo Welt" oder andere Ergebnisse direkt wie am gewohnten PC bewundern kann. Dafür bietet das OpenMCP-Projekt schon die meisten Grundbausteine, um mal eben ein "Hallo Welt" zu programmieren oder mit dem Netzwerk zu spielen. Es richtet z.B. eine Uhr ein, initialisiert die RS232-Schnittstelle und das Netzwerk und viele andere kleine Dinge. Auf die vielen anderen kleinen Dinge, die das Leben leichter machen, kommen wir später noch zu sprechen.
Bei der Wiederverwertbarkeit war der Grundgedanke, dass man das ganze Projekt ja auch später noch auf andere Mikrocontroller und/oder andere Boards portieren kann, denn auch die Entwicklung bleibt hier nicht stehen. Mikrocontroller werden ja auch besser, schneller, schöner und kleiner. Um die Wiederverwertbarkeit sicher zu stellen, ist eine Unterteilung der Software nötig. Denn meist müssen bei der Anpassung an einen neuen Mikrocontroller nur die Treiber für die Hardware angepasst werden, das wäre der Idealfall und liegt auch relativ dicht an der Realität. Das Portieren des OpenMCP von einen ATmega2561 auf den AVR NET-IO hat nur 2 Tage gedauert, wobei eigentlich nur Änderungen an den Hardwaretreibern und Funktionen statt fanden, abgesehen von einigen Änderungen in Bezug auf den knappen RAM des ATmega644.
Alle nachfolgenden Beispiele beziehen sich auf das AVR NET-IO von Pollin, bestückt mit einen ATmega644 mit 16MHz, sofern nicht anderes gekennzeichnet. Und damit der folgende Text auch verstanden und zu Erfolgen - sprich Programmen - führt sollte man ein bisschen Hardware und Software am Start haben. Zu nennen sind da wenn man Windows benutzt:
- AVR-NET-IO-Board von Pollin mit mindestens ATmega644, besser ATmega1284 wenn mehr Speicher benötigt wird.
- ein Programmieradapter, vorzugsweise den AVRISP mkII
- AVRStudio in aktueller Fassung
- Toolchain WinAVR oder ATMEL-Toolchain in aktueller Fassung
- eine aktuelle Fassung des OpenMCP-Sourcecodes
Unter Linux sollte man den aktuellen AVR-gcc haben, averdude und die schon genannte Hardware. Dazu einen Texteditor nach Wahl, oder besser gleich eine integrierte Entwicklungsumgebung (IDE). Zu beachten wäre noch das das makefile noch für das jeweilige Betriebssystem angepasst werden muss, denn Windows benutzt andere Pfade als Linux. In der Datei makefile sieht man was man auskommentieren/ändern muss anhand der Kommentare.
Aufteilung der Software
Wie eben schon angedeutet, erleichtert die Aufteilung der Software in Module das Portieren auf andere System enorm. Ein weiterer Vorteil liegt in der einfacheren Wartbarkeit der Software. Denn wenn alle sich daran halten, müssen Fehler von Treibern oder Funktionen nur an einer Stelle geändert werden. Dann reicht ein einfaches Neuübersetzen, und zack, alle benutzen die bereinigten Treiber oder Funktionen. Die Aufteilung der Software gliedert sich deshalb in drei Bereiche:
- Hardwaretreiber/funktionen
- Systemtreiber/funktionen
- Applikationen
Bei den Hardwaretreibern handelt es sich um Treiber oder Funktionen, die direkt mit der Hardware zu tun haben, auf diese zugreifen oder eben diese verwalten. Als Beispiel sei hier die UART oder auch RS232-Schnittstelle genannt.
Applikationen sollten sich idealerweise selbst erklären, hier kommt das eigentliche Programm samt geistigen Errungenschaften des Programmierers rein.
Unter Systemtreibern befinden sich Treiber oder Funktionen, die nicht direkt auf die Hardware zugreifen. Ein gutes Beispiel für einen solchen Treiber/Funktion ist die Uhr und deren Verwaltung. Die Uhr bietet eine Menge Funktionen, sie liefert nicht nur die aktuelle Zeit. Man kann mit ihr aber auch Zeit stoppen oder Ereignisse in bestimmten zeitlichen Abständen ausführen. Kritiker werden jetzt sagen, dass die Uhr etwas mit der Hardware zu tun haben muss, denn sie braucht ja einen Takt der ihr sagt, wie viel Zeit vergangen ist. Das ist so auch richtig - aber eben nicht ganz. Denn die Uhr benutzt Treiber aus dem Hardwarebereich, die Timer. Ein Timer wird so eingerichtet, dass er in bestimmten Intervallen ein Ereignis auslöst, einen Interrupt. In diesem Interrupt wird dann die Uhr aufgerufen, die dann schaut, was gemacht werden muss. Müsste die Uhr mitrechnen, wieviel Zeit vergangen ist, würde die ganze Rechenzeit damit vertrödelt, um nachrechnen wieviel Zeit vergangen ist. Der Timer dient in diesen Fall als Zeitbasis. Da die Uhr die Abstände zwischen den Intervallen kennt, kann sie einfach im Hintergrund mitzählen wieviel Zeit seit ihrem letzten Aufruf vergangen ist. Aber wie sagt man dem Timer, dass dieser in bestimmten Intervallen die Uhr aufrufen soll? Das erkläre ich im folgenden Kapitel. (Was sind Callbacks ?)
Ein bisschen Vorwissen
Da die Anzahl der vorhanden Funktionen und Treiber doch in der Zwischenzeit relativ groß ist, sollten noch ein paar Dinge geklärt werden, bevor es an die Arbeit geht. Damit man einige Sachen besser versteht hier noch einige Erklärungen zu Methoden wie etwas funktioniert, oder wozu es da ist oder warum es so gemacht ist. Ich werde versuchen dieses Kapitel verständlich zu erklären und es bei Bedarf zu erweitern.
Parameterübergabe an Funktionen
Funktionen sollen meist immer recht flexibel sein, damit man sie später auch in anderen Projekten verwenden kann. Um dies zu erreichen kann man den meisten Funktionen noch Parameter übergeben. Diese Parameter werden in Klammern nach dem Funktionsnamen geschrieben. Dabei wird genau definiert, in welcher Reihenfolge die Parameter übergeben werden und welchen Datentyp sie besitzen. Hier ein Beispiel, wie so eine Funktion deklariert sein könnte:
void FooBar( char Parameter1, int Parameter2 );
Aufrufen würde man die Funktion so:
FooBar( 3, 3 );
Wir sehen, dass die Funktion mit zwei Parametern aufgerufen werden will, Paramter1 und Paramter2. Gleichzeitig sieht man, welche Reihenfolge die Parameter haben müssen und welchen Datenyp sie besitzen. Zu den Datentypen später mehr. Funktionen/Treiber, die keine Parameter brauchen, sehen so aus:
void FooBar( void );
Aufrufen würde man die Funktion so:
FooBar();
Void stellt in diesen Fall keinen Datentyp dar, ober besser gesagt keinen Parameter.
Returnwert
Um auch Ergebnisse einer Funktion, die vielleicht eine Berechnung durchführt hat, zu erhalten, kann eine Funktion einen Rückgabewert beim Verlassen zurückgeben. Dabei ist zu beachten, dass nur 1 Rückgabewert möglich ist. Auch hier ist wieder der Datentyp festzulegen. Void würde hier keinem Rückgabewert entsprechen. Ein Beispiel ohne Rückgabewert hatten wir oben schon. Hier ein Beispiel, welches einen Rückgabewert vom Datentyp int liefern würde.
int FooBar( char Parameter1, int Parameter2 );
Dieser Rückgabewert kann dann im Programm, welches die Funktion aufgerufen hat, weiter verarbeitet werden. Man könnte den Rückgabewert z.B. in einer Variablen speichern, das sieht so aus:
MeineVariable = FooBar( 3, 3 );
Zu achten ist wie immer auf den Datentyp des Rückgabewertes! Da im Beispiel der Rückgabewert den Datentyp int hat, sollte auch die Variable MeineVariable diesen Datentyp haben.
Was sind Callbacks ?
Callback heißt eigentlich nichts anderes als zurückrufen. Mehr macht die Funktion eigentlich auch nicht. Um das Beispiel mit der Uhr aufzugreifen kann man jetzt mit diesem Wissen sagen, dass ein Timer in bestimmten Intervallen die Uhr aufrufen soll. So einfach ist das. Wie das aussehen könnte, zeigt folgender kleiner Code-Schnipsel:
#include "hardware/timer0.h" void foobar(void); void uhr(void); void foobar(void) { // Der Timer0 soll 100 mal in der Sekunde ausgelöst werden timer0_init( 100, 0 ); // Ruf dann die Funktion Uhr auf timer0_RegisterCallbackFunction( Uhr ); } void uhr(void) { // mach was sinnloses, aber bitte 100 mal in der Sekunde }
Dieses Beispiel zeigt sehr gut den Einsatz von Callback. Damit kann man Hardwaretreiber programmieren, Ereignisse bearbeiten und später gesagt bekommen, was sie dann eigentlich tun sollen. Das hat den Vorteil, dass man nicht unnötig die Hardwaretreiber mit der restlichen Software verknotet. Es ist nicht ratsam, wenn man schon im Hardwaretreiber sagen würde was er später tun soll. Das wäre zu unflexibel weil man den Hardwaretreiber dann nicht so leicht in andere Projekte einbinden könnte.
Ein interessanter Hardwaretreiber, der Callback-Funktionen benutzt, sind die Treiber für externe Interrupts. Dort kann man einfach sagen, ich will den externen Interrupt 4 benutzen, und das er bei steigender Flanke die Funktion XY ausführen soll.
Was sind Handles ?
Ein wichtiges Mittel um Funktionen oder Programme flexibel zu halten sind Handles. Handles sind Nummern oder Strukturen. Dazu ein bisschen Vorgeschichte. Vielleicht kennt der eine oder andere den Effekt, dass man mehrere Dateien am Computer gleichzeitig öffnen kann. Um eine Datei zu öffnen benutzt ein Programm Funktionen, die das Betriebssystem bereitstellt. Um jetzt viele gleichzeitig geöffnete Dateien unterscheiden zu können, ohne jedes mal mit Dateinamen hantieren zu müssen, erhält man für jede geöffnete Datei eine ID oder auch Handle genannt. Diese ID ist so lange gültig, wie diese Datei geöffnet ist. Mit Hilfe dieser ID kann das Programm später dann zwischen Dateien wechseln. Auch sei erwähnt, dass es das Betriebssystem so einfacher hat, weil die Zuordung eindeutig ist. Wenn ein Programm sagt, öffne Datei XY, so bekommt es als Antwort darauf die ID bzw. ein Handle zurück. Mit diesem Handle kann es Aktionen ausführen wie Lesen oder Schreiben in eine Datei. Als kleines Beispiel, wie das Kopieren einer Datei in dieser Machart aussehen könnte:
ID1 = öffne Datei XY ID2 = lege Datei YZ an
Lies von ID1 ein Byte und hänge dieses an ID2 an und mach das solange bis du alle Bytes von ID1 gelesen hast.
Danach schließe ID1 und ID2.
Man sieht, das vereinfacht die Arbeit enorm. Richtig schön wird es, wenn man diese Machart auf Netzwerkfunktionen bezieht. Dort kann man dann sagen, ich will eine Verbindung zum Rechner mit der IP XY auf Port YZ und erhält wiederum ein Handle, falls der Verbindungsaufbau erfolgreich war. Mit diesem Handle kann man die geöffnete Verbindung bearbeiten, ohne bei jedem Senden oder Empfangen eines Bytes sagen zu müssen, dass der Rechner mit der IP XY und Port YZ gemeint ist. Die ID reicht, den Rest übernimmt das Betriebssystem.
Jetzt aber ran an die main.c
Nachdem wir nun einige theoretische Kleinigkeiten geklärt haben und diese hoffentlich im Hinterkopf hängen geblieben sind, wollen wir auch was sehen oder besser noch programmieren. Dazu ist es ratsam - wenn man in C noch nicht so bewandert ist - folgende Seite in einem anderen Browserfenster oder Tab offen zu haben, um einen Überblick über Datentypen zu haben (char, int, long etc.), wovon es in C ja einige gibt. Aber nun ran an die ersten Experimente :-). Ich versuche mal vorne anzufangen, und das ist die Init-Phase der OpenMCP-Software.
Die Initphase, was passiert da eigentlich ?
In der Init-Phase werden alle Treiber, Hardware- als auch Softwaretreiber, initialisiert. In der Regel bedeutet das:
- FIFO werden eingerichtet
- UART wird gestartet mit 57600 8N1 und Ausgaben werden auf diese umgeleitet
- Das Zeitsystem wird gestartet mit 1/100 s Auflösung
- Config im EEmem wird gesucht und gelesen
- GPIO wird eingerichtet
- ADC wird eingerichtet
- SPI wird eingerichtet
- Ethernet wird gestartet
- ARP, IP, ICMP, UDP und TCP werden gestartet
- Es wird versucht, eine IP per DHCP zu holen, wenn dies in der Config so eingestellt ist
- Es wird versucht die Uhrzeit von einem NTP-Server zu holen, wenn dies in der Config so eingestellt ist
Danach werden jene Dienste/Programme gestartet, die permanent laufen wie z.B. Telnet, Webserver und Crondienst. Diese werden in einer Threadliste eingetragen und zyklisch nacheinander aufgerufen. Dabei handelt es sich um kooperatives Multitasking, wenn man das so nennen kann. Jedes augerufene Programm schaut nach, ob es was zu tun hat und gibt dann die Kontrolle wieder ab. Dabei wurde darauf geachtet, dass diese möglicst nicht blockierend sind. Wenn alle Dienste/Programme initialisiert sind, werden einzelne Module initialisiert. Das sind im Fall des Webserver die einzelnen CGIs oder bei Telnet die einzelnen Befehle. Wenn das alles passiert ist, wird in einer Endlossschleife die Thread-Funktion aufgerufen, die die Threadliste abarbeitet.
Komplett könnte eine main.c im einfachsten Fall dann so aussehen:
#include "system/init.h" #include "apps/apps_init.h" #include "apps/modul_init.h" #include "system/init.h" int main( void ) { // System initialisieren init(); // Applikationen initialisieren (http, telnet, cron, .... ) apps_init(); // Module initialisieren ( cmd, cgi .... ) modul_init(); // die mainloop, hier wird alles abgearbeitet while(1) { THREAD_mainloop(); } }
Mein erstes "Hallo Welt" mit OpenMCP
Kommen wir nun zu den ersten Schritten mit OpenMCP. Dazu entfernen wir erst mal einige Programmteile, die wir jetzt nicht brauchen, indem wir die main.c auf die Initialisierung beschränken. Dann sollte die main.c so aussehen.
#include "system/init.h" #include <stdio.h> #include <avr/pgmspace.h> int main( void ) { // System initialisieren init(); // Endlosschleife while(1); }
Dort ist leicht zu erkennen, dass nach dem Initialisieren nichts weiter passiert, zu sehen am while(1);. Eine einfache Endlosschleife die nichts weiter macht. Aber was ist zu sehen wenn wir nur das machen? Lediglich die RS232 gibt beim Starten ein paar interessante Dinge aus. Sehen wir uns die mal an:
Microwebserver build on AVR-libc version: 1.6.2/20080402 $Id: main.c 113 2009-0$ UART initialisiert STDOUT initialisiert CLOCK initialisiert GPIO initialisiert ADC initialisiert Config gefunden (Pos: 0, Len: 2047, Offset: 13) ENC28j60 initialisiert ( HW-Add: 00:03:6f:55:1c:c8 ) -+-> ARP initialisiert |-> UDP initialisiert |-> TCP (Hurrican-engine) initialisiert |-> Versuche DHCP-Config zu holen. DHCP-Config geholt | IP : 192.168.2.195 | Netmask: 255.255.255.0 | Gateway: 192.168.2.1 | DNS : 192.168.2.1
Jetzt läuft das OpenMCP System. Der Controller lässt sich jetzt anpingen. Aber er macht erst mal nichts weiter. Das wollen wir jetzt mal ändern. Am einfachsten ist das wenn wir einfach ein printf hinzufügen. Die Ausgaben von printf sollten dann im Anschluss der Init zu sehen sein. Dazu müssen nur einige wenige Zeilen eingefügt werden, so das unsere main.c jetzt so aussieht.
#include "system/init.h" #include <stdio.h> #include <avr/pgmspace.h> int main( void ) { // System initialisieren init(); printf_P( PSTR("Hallo Welt!\r\n")); // Endlosschleife while(1); }
Jetzt bekommt man nach dem Übersetzen wieder die Init-Meldungen zu sehen und als extra im Anschluss unser "Hallo Welt!". Hui. Wichtig ist in diesen Zusammenhang auch die Einbindung der stdio.h und avr/pgmspace.h per #include. Diese stellen uns die Funktionen für printf_P bereit, die benötigt werden.
Das ist aber auf Dauer etwas langweilig. Zumal das ja nur einmal passiert. Also versuchen wir mal unsere erste Schleife in Gang zu bekommen. Die sicherlich einfachste Variante ist eine Endlosschleife, diese ist unter Programmierern gefürchtet, da sie bei Anfängern doch die häufigste Fehlerursache ist. Hier basteln wir aber mal mit Absicht eine. Dazu ändern wir die Zeile mit dem printf_P wie folgt um:
while(1)
{
printf_P( PSTR("Hallo Welt!\r\n"));
}
Jetzt bekommt man die Meldung "Hallo Welt!" ständig zu sehen, ob man will oder nicht. Auch hier stellt man wieder fest: llaaannngggweilig. Aber interessant ist, das der Controller, obwohl er eigentlich mit unseren "Hallo Welt!" beschäftigt ist, noch anpingbar ist. Das liegt daran, dass der Stack weitestgehend im Hintergrund arbeitet. Aber nehmen wir uns mal vor das "Hallo Welt!" nur 10 mal erscheinen soll. Dann sieht das wie folgt aus.
char i;
for( i = 0 ; i < 10 ; i++ )
{
printf_P( PSTR("Hallo Welt!\r\n"));
}
Jetzt sehen wir "Hallo Welt!" nur 10 mal. Hier sei noch angemerkt, dass jetzt der Blick auf die Seite zu den Datentypen interessant ist und wie die for-Schleife arbeitet. Als erstes sehen wir, dass eine Variable Namens i vom Typ char definiert wurde. Beim Typ char handelt es sich um einen 8-Bit breiten vorzeichenbehafteten Typ, oder auch ein Byte. Der Wertebereich dieser Variable ist -128 bis +127. Die Variable kommt in der for-Schleife für das Zählen zum Einsatz. Auch hier hilft experimentieren weiter. Aber es empfiehlt sich auf jeden Fall den Aufbau der for-Schleife genauer unter die Lupe zu nehmen. Sehen wir uns den Befehl mal ohne Schnickschnack an.
for ( .. ; .. ; .. ) { .. }
Nach dem for kommen die in den Klammern eingefassten Argumente, 3 an der Zahl und durch Semikolon getrennt. Das erste Argument ist die Initialisierung, z.B. i = 0. Dieses Argument wird nur einmal ausgeführt. Das zweite Argument ist die Verarbeitungsbedingung wann der Schleifenkörper in den geschweiften Klammern ausgeführt wird. Ausgeführt wird dieser immer dann, wenn die Verarbeitungsbedingung wahr ist. Und als drittes Argument die Iterationsbedingung, was soviel heißt wie wiederholen, oder vereinfacht: "Was soll ich nach dem Abarbeiten des Schleifenkörpers machen?". In unseren Beispiel i um 1 erhöhen, oder einfach geschrieben i++, man könnte auch i = i + 1 schreiben, sieht aber für faule Programmierer doof aus. Aber das geht auch in die andere Richtung sehr gut mit i--, oder i = i - 1, macht aber nur Sinn, wenn auch die Initialisierung und die Verarbeitungsbedingung dazu passt. Man sollte mal wie oben geschrieben ein bisschen mit der for-Schleife spielen, kaputt gehen kann hier noch nichts, nur abstürzen. Insbesondere sollte man mal andere Datentypen ausprobieren. Kleiner Tip, der Typ int beinhaltet beim AVR-GCC 16Bit und hat einen Wertebereich von -32768 bis +32767.
So, nach dem wir jetzt mit den ersten C-Befehlen und Funktionen Kontakt hatten und vielleicht sogar Freundschaft geschlossen haben, wollen wir uns auf die restlichen stürzen. Den mit wichtigsten Befehl hatten wir eben schon, die for-Schleife, aber es gibt noch andere Abarten davon die der for-Schleife ähneln ( while(..){}, do{}while(..) aber dazu vielleicht später mehr ). Der zweite wichtige Befehl ist die if-Bedingung. Mit diesem Befehl kann man in Programmen verzweigen. Erweitern wir unser kleines Progrämmchen mal.
char i;
for( i = 0 ; i < 10 ; i++ )
{
if ( i == 0 )
{
printf_P( PSTR("Hallo Welt!\r\n"));
}
else
{
printf_P( PSTR("langweilig!\r\n"));
}
}
Und was passiert jetzt nach dem Starten? Gewissenhafte Leser kennen die Lösung bestimmt schon. Richtig, es wird einmal "Hallo Welt!" ausgegeben und dann immer 9 mal "langweilig". Wie kommt das? Das liegt am if-Befehl. Grob kann man sagen, dass diese Konstruktion in etwa so aussieht:
if( .. ) {..} else {..}
Das Argument in den Klammern nach dem if stellt die Ausführungsbedingung der ersten geschweiften Klammern dar, wobei für deren Ausführung die Bedingung wahr sein sollte. Mit dem else könnte man sagen, was er machen soll, wenn die Bedingung nicht erfühlt worden ist. Es aber keine Pflicht ein else zu verwenden, denn dann gibt es halt einfach nix was er machen soll, wenn die Bedingung nicht erfüllt worden ist. Nach dieser Erklärung versteht man auch das obige Beispiel. Denn nur beim ersten Schleifendurchlauf ist i gleich 0, sprich wahr. Also wird die erste geschweifte Klammer ausgeführt. Und wenn die Bedingung nicht wahr ist, kommt das else zu tragen. Einige werden sich jetzt wundern, dass in der Bedingung zwei Gleichheitszeichen stehen. Das hat einen bestimmten Grund. Der Unterschied lässt sich gut erklären, wenn man beide Schreibweisen genauer betrachtet. Denn i = 0 ist was anderes als i == 0. i = 0 ist eine Wertzuweisung, und i == 0 ist ein Vergleich. Das ist wichtig, damit der Compiler unterscheiden kann, was wir eigentlich machen wollen. Einen Wert zuweisen oder vergleichen. Das ist ein beliebter Fehler bei angehenden Programmierern, denn ein if( i = 0 ) bringt einfach nichts, weil so ein Wert zugewiesen wird und nicht verglichen wird. Also immer brav drauf achten, i = 0 != i == 0 um es mal in C zu schreiben. Und siehe da, eben hatten wir schon wieder was Unbekanntes. Das != dient nicht zur Verwirrung und ist auch nicht Böse, sondern heißt nur "ungleich". Beispiel hier für Mathematiker 1 != 2, sagt soviel aus wie: "1 ungleich 2". Betrachtet man dann wieder i = 0 != i == 0 ist das eine nette Aussage. Und für Leute, die noch mehr vergleichen wollen, es gibt noch >,<, <= und >=, der Sinn sollte sich - denke ich mal - auch ohne Erklärung erschliessen.
Und jetzt der Rest
So, nachdem wir jetzt einen kleinen Grundstock haben, mit dem man schon etwas programmieren kann, wollen wir uns mal an die ersten Dinge rantrauen, die das OpenMCP zu bieten hat. Wichtigste Hilfe ist beim Programmieren, das Treibersystem des OpenMCP. Es soll helfen, Aufgaben umzusetzen, ohne in die Komplexität des Controllers und dessen Hardwareeinstellungen einsteigen zu müssen. Im Folgenden werde ich versuchen, die wichtigsten Treiber mit kleinen Beispielen zu veranschaulichen. Nach dem Durcharbeiten können eigene Ziele umgesetzt werden. Danach sollte es möglich sein, eigene CGIs mit dem CGI-System zu entwickeln oder eigene Treiber zu schreiben, die spezielle Aufgaben erfüllen.
Hardware
Anfängern macht bei der Programmierung mit Mikrocontrollernzunächst am meisten Spaß, wenn etwas zu sehen ist. Daher versuche ich als erstes die einzelnen Treiber für die Hardwarefunktionen zu erklären und wie sie benutzt werden. Ich werde hier aber nicht zu erklären versuchen, wie die Treiber im einzelnen arbeiten. Das kann man sich nach etwas Übung beim Programmieren im Sourcecode selber ansehen. Ein wichtiges Hilsemittel beim Benutzen der Treiber ist die Dokumentation zu lesen. Dort kann nochmal genau nachgelesen werden, wie Funktionen aufgerufen und benutzt werden. Incomplete
GPIO
Zu den schönsten Dingen gehört, wie schon gesagt, wenn man ein Ergebnis sehen kann, am besten als Blinken, Leuchten usw.. Am einfachsten geht das mit einem Pin am Controller, der sich vom Programm ein- oder ausschalten lässt. Um einen einzelnen Pin zu steuern, ist der gpio_in und gpio_out Treiber gedacht. Um diese Treiber nutzen zu können, müssen dem Programm die Funktionsnamen und Aufrufweisen bekannt gemacht werden. Das macht man mit der #include Anweisung. Diese Anweisung sagt dem Compiler, wie die Funktionen aufzurufen sind, bevor ihm der eigentliche Funktionsablauf in der Funktion bekannt ist. Diese speziellen Dateien, die das beschreiben, nennt man Header-Dateien. Sie haben die Endung .h. Die Dateien, in den die eigentliche Funktion und deren interner Ablauf beschrieben ist, sind normale Dateien mit der Endung .c, dort befindet sich das passende Programm zum Funktionsnnamen. Das ist eine wichtige Unterscheidung, auf die ich im Verlauf noch eingehen werde. Alle folgenden Beispiele sollten erstmal in der Datei main.c ausprobiert werden.
Ändern wir zunächst die Datei main.c
#include "system/init.h" void main( void ) { init(); }
Das ist der Ausgangspunkt für alle folgenden Beispiele. Um jetzt die GPIO-Treiber nutzen zu können, fügen wir die passenden include Anweisungen hinzu.
#include "system/init.h" #include "hardware/gpio/gpio_in.h" #include "hardware/gpio/gpio_out.h" void main( void ) { init(); }
Danach können wir die Funktionen dieses Treibers nutzen. Um einen Pin einzuschalten genügt jetzt ein einfacher Aufruf der Funktion
gpio_out_set( Pinnummer );
Wobei jetzt die Pinnummer durch die entsprechende Nummer zu ersetzen ist. Nehmen wir mal den Pin 0. Dann sieht das komplette Beispiel so aus:
#include "system/init.h" #include "hardware/gpio/gpio_in.h" #include "hardware/gpio/gpio_out.h" void main( void ) { init(); gpio_out_set( Pinnummer ); }
Wenn man jetzt am entsprechenden Pin eine LED mit einem Vorwiderstand gegen Masse (GND) anschließen würden, würde die LED nach dem Starten des Controllers und der Init-Phase aufleuchten. Bleibt die Frage, welcher Pin ist Pin 0? Dazu ist ein Blick in die Dokumentation nötig. Wenn das OpenMCP auf einem AVR-NET-IO benutzt wird, wäre das der zweite Pin am 25-poligen Stecker. Also schnell eine LED und einen Vorwiderstand angelötet/angesteckt von Pin 2 nach GND (Pin 18,19,20,21,22,23,24 oder 25). Wenn alles funktioniert leuchtet diese nach dem Starten. Ausschalten könnte man sie, indem man das gpio_out_set( Pinnummer ); durch ein gpio_out_clear( Pinnummer ); ersetzt. Damit es mehr Spass macht, könnten wir sie im Sekundentakt blinken lassen. Dazu brauchen wir aber eine Zeitquelle, Clock genannt. Die Funktionen dazu werden mit einem #include "system/clock/clock.h" bekannt gemacht. Auf alle Funktionen der [Clock] gehe ich weiter unten ein, jetzt soll erst mal nur eine bestimmte Funktionen ohne Erklärung dafür herhalten. Und damit sie blinkt brauchen wir eine Schleife. Das geht mit der for-Schleife, die wir schon kennen. Ein Beispiel könnte so aussehen:
#include "system/init.h" #include "system/clock/clock.h" #include "hardware/gpio/gpio_in.h" #include "hardware/gpio/gpio_out.h" void main( void ) { init(); for ( ; ; ) { GPIO_out_set( 0 ); CLOCK_delay( 1000 ); GPIO_out_clear( 0 ); CLOCK_delay( 1000 ); } }
Tataaa. Wenn alles geklappt hat blinkt die LED im Sekundentakt. Incomplete
ADC
Mit dem ADC (Analog-to-Digital-Converter) lassen sich analoge Werte (z.B. Spannung) erfassen. Ein ATmega644 kann 8 verschiedene Kanäle abfragen. Auf dem Pollinboard sind aber nur 4 Pins mit ADC richtig nach außen geführt. Das Einlesen erfolgt in OpenMCP relativ einfach mit
int ADC_GetValue( char Channel );
Das Beispiel für das Einlesen des Kanals 3 sieht dann so aus:
int temp; temp = ADC_GetValue( 3 );
Danach steht in der Variablen temp der eingelesene Wert dieses Kanals. Jetzt hat man aber immer noch Probleme zu wissen, welche Spannung das nun darstellt. Hierfür gibt es die Funktion
int ADC_mVolt( int ADC_Wert, int ADC_mVoltmax );
Diese Funktion rechnet uns den eingelesenen Wert in mV um, unter Angabe der maximalen Spannung in mV dieam Eingang anliegen kann . Zu beachten ist hier, dass am Eingang maximal 5V anliegen dürfen. Will man größere Spannungen messen, wird das über einen Spannungsteiler gemacht, so dass am Eingang maximal 5V auftreten können. Der Funktion wird die Spannung vor dem Spannungsteiler mitgeteilt.
int temp; int mVolt; temp = ADC_GetValue( 3 ); mVolt = ADC_mVolt( temp, 5000 );
Das Beispiel liest uns den Eingang 3 ein und rechnet den Wert in mV um in Bezug auf maximal 5V bei Vollausschlag am Eingang.
UART/RS232
Externe Interrupts
Eine schöne Möglichkeit auf externe Ereignisse zu reagieren sind externe Interrupts.
SPI
1-Wire
LED
Die Hauptdatei zur Ansteuerung der LEDs ist led_core.c. Zum Schalten der LEDs wird die gpio_core.c verwendet. Damit bekannt ist, welche LED wo angeschlossen ist, wird dies im LED_DATA Array in led_core.c bekanntgegeben:
//z.B. für den AVRNETIO const char LED_DATA[] PROGMEM = { PORTD_2 , PORTD_4 , PORTD_6 };
Je nachdem, ob die LEDs bei HIGH oder LOW Pegel leuchten, wird der LED_INVERT definiert. (D.h wenn die LEDs zwischen VCC und dem Pin am Mikrocontroller hängen, wird eine 0 (LOW) statt einer 1 (HIGH) benötigt, damit die LED leuchtet.
Im Code können die LEDs mit einfachen Befehlen geschaltet werden.
//Initialisierung LED (siehe init.c) LED_init(); ... //irgendwo im Code (apps, module, etc.) LED_on(1);//Led 1 einschalten (an PORTD,Pin4) LED_off(0);//Led 0 ausschlaten (an PORTD,Pin2) LED_toggle(2);//Led 2 Zustand wechseln (an PORTD,Pin6)
Timer
VS10xx
System
Ein/Ausgabe
Das Ein/Ausgabemodul ist eines der interessantesten Module. Mit diesem Modul kann die Ausgabe von printf auf andere Geräte umgeleitet werden oder auf andere Funktionen. Sinn ist es, dass man Ausgaben über printf machen kann, und die dann auf eine Netzwerkverbindung, RS232 etc. umgeleitet werden. Eingaben werden derzeit noch nicht unterstützt, ist aber für die Zukunft geplant und auch schon soweit vorbereitet.
Wie biege ich die Ein/Ausgabe um ?
Ganz einfach mit
STDOUT_set( Typ , Devicenummer );
Angewandt auf eine TCP-Verbindung könnte das dann so aussehen:
STDOUT_set( _TCP , Socketnummer );
Das biegt uns die Ausgabe von printf auf eine TCP-Verbindung um, und alle Ausgaben werden per TCP versendet. Es sind auch andere Typen möglich:
- RS232
- _TCP
- NONE
Aber bevor man seine Ausgabe umbiegt sollte man die alte Ausgabe sichern, um sie dann wieder herzustellen, nachdem man seine Ausgaben gemacht hat. Das stellt den Programmablauf anderer Ausgaben sicher, so das diese nicht gestört werden.
#include "system/stdout/stdout.h" // Struktur anlegen für STDOUT struct STDOUT oldstream; // alte Struktur sichern STDOUT_save( &oldstream ); // Neue Ausgabe einstellen, z.B. eine TCP-Verbindung STDOUT_set( _TCP , SOCKET ); // mach mal ein paar andere Sachen foorbar(); STDOUT_restore( &oldstream );
Wozu das ganze?
Damit kann man bequem Ausgaben per printf erledigen. Zunächst ist der Programmieraufwand höher, aber später erleichtert es das Programmieren sehr, und Programme, die Ausgaben mit printf machen, lassen sich so auch an anderer Stelle oder mehrfach verwenden. So können z.B. Telnetkommandos auch ihre Ausgaben auf die RS232 ausgeben, oder nach einen Aufruf aus dem CGI-Modul auch dort. Ein gutes Beispiel findet sich auf dem Webinterface unter Netzwerk/Infos. Dort werden Telnet-Kommandos für die Ausgabe benutzt.
Die Zeit
Wozu Puffer?
Wie Werte dauerhaft speichern ?
Ich will ins Netz!
Die IP
Das UDP-Modul
Das TCP-Modul
Das NTP-Modul
Das NTP-Modul dient zum Holen der aktuellen Uhrzeit von einem NTP-Server. Die Funktion sieht wie folgt aus: unsigned int NTP_GetTime( unsigned long IP, unsigned char * dnsbuffer, long timedif ). Es kann eine IP oder ein Hostname übergeben werden, und zusätzlich auch noch die Zeitdifferenz, die aufaddiert werden soll. Auch die Zeitzone kann angegeben werden.
char dnsname[] = "ntp.foo.bar"; if ( NTP_GetTime( 0, dnsname, 0 ) == NTP_ERROR ) { printf_P( PSTR("Fehler, konnte Zeit nicht holen\r\n")); } else { printf_P( PSTR("Ok, Zeit geholt\r\n")); }
Das DNS-Modul
DNS dient dazu Hostnamen zu IP-Adressen aufzulösen. Die Funktion unsigned long DNS_ResolveName( char * HOSTNAME ) tut genau dieses. Dazu muss die dns.h auf dem Verzeichniss system/net/ eingbunden werden. Anzuwenden ist sie wie Folgt:
#include "system/net/dns.h" unsigned long = IP; char dnsname[] = "foo.bar"; IP = DNS_ResolveName( dnsname ); if ( IP == DNS_NO_ANSWER ) { printf_P( PSTR("Fehler beim auflösen\r\n")); } else { printf_P( PSTR("Dns-Name erfolgreich aufgelöst\r\n")); }
Applikationen
Telnet
Jeder kennt es bestimmt. Telnet, die Konsole zur Welt. Dort lassen sich Kommandos absetzen und zu einem entfernten Rechner übertragen. Telnet arbeitet auf Port 23. Jedes Betriebssystem bringt meist ein Telnet-Client mit, sogar Windows. Der Telnet-Server ist unter apps/telnet zu finden und erstreckt sich über mehrere Dateien. Die wichtigste Datei ist telnet.c, die anderen Dateien beginnen mit cmd und folgen den Schema cmd_*.c. Dort sind die eigentlichen Kommandos implementiert. Die telnet.c kümmert sich nur um den Verbindungsaufbau und das Aufbereiten der eintreffenden Daten und setzt die STDOUT richtig, damit Ausgaben per printfauch dort landen wo sie sollen. So zerlegt es z.B. eine Befehlszeile in seine Bestandteile und ordnet es einem Kommando zu, ruft dieses dann auf, und übergibt ihm zusätzlich noch Argumente.
Webserver
Die einfachste Möglichkeit mit dem Controller in Kontakt zu treten ist neben dem einfachen Ping der Aufruf von Webseiten über das Netzwerk. So beherrscht auch OpenMCP diese Methode. Dabei lauscht der Webserver auf dem Port 80, wie die meisten Webserver.
Statische Webseiten
Der Webserver beherrscht das Ausliefern sowohl von statischen als auch von dynamischen Webseiten. Wobei das Erzeugen von dynamischen Webseiten zu den interessanteren Fähigkeiten des Webservers gehört.
Die file_data.h
Um statische Webseiten einzurichten wird die Datei files_data.h im apps/httpd/ benötigt. Um eine Seite abzulegen sind 3 Dinge nötig. Als erstes der Name der Datei, der als String im Flash anzulegen ist:
const char foobar[] PROGMEM = "foobar.html";
Als zweites wird der HTML-Code der Webseite benötigt:
const char foobar_file[] PROGMEM = { "<HTML>\r\n" "<HEAD>\r\n" "<TITLE>OpenMCP - Welcome on Microcontroller Board</TITLE>\r\n" " </HEAD>\r\n" " <body>\r\n" " <p>Hallo Welt</p>\r\n" " </body>\r\n" "</HTML>\r\n" "\r\n" };
Jetzt haben wir zwei Strings angelegt. Foobar welcher den Namen "foobar.html" festlegt und foobar_file welcher den Inhalt der Webseite darstellt. Jetzt müssen wir nur noch den Zusammenhang zwischen diesen beiden Strings festlegen, dazu gibt es am Ende der file_data.h eine Tabelle, welche den Zusammenhang schafft und den Dateityp festlegt. Ein Beispiel:
FILES files[] = { { files1, data1, TEXT, sizeof( data1 ) - 1 }, { files2, data2, TEXT, sizeof( data2 ) - 1 }, { files3, data3, TEXT, sizeof( data3 ) - 1 }, { files4, data4, TEXT, sizeof( data4 ) - 1 }, { files5, data5, TEXT, sizeof( data5 ) - 1 }, { files6, data6, TEXT, sizeof( data6 ) - 1 }, { files7, data7, TEXT, sizeof( data7 ) - 1 }, { files8, data8, TEXT, sizeof( data8 ) - 1 }, { 0,0,0,0 } };
Dort wird zuerst der Name eingetragen, dann die Webseite, danach kommt der Typ, möglich sind dort JPEG, TEXT, PNG. Und zuletzt die Länge der Daten in Bytes, das ist wichtig. Bei normalen Strings/Text wird die Länge durch eine NULL festgelegt. Bei Bildern oder anderen binären Daten kann aber genau diese NULL auch so vorkommen, deshalb muss die Länge genau angeben werden. Wenn wir nun unser Beispiel eintragen sollte die Tabelle wie folgt aussehen:
FILES files[] = { { files1, data1, TEXT, sizeof( data1 ) - 1 }, { files2, data2, TEXT, sizeof( data2 ) - 1 }, { files3, data3, TEXT, sizeof( data3 ) - 1 }, { files4, data4, TEXT, sizeof( data4 ) - 1 }, { files5, data5, TEXT, sizeof( data5 ) - 1 }, { files6, data6, TEXT, sizeof( data6 ) - 1 }, { files7, data7, TEXT, sizeof( data7 ) - 1 }, { files8, data8, TEXT, sizeof( data8 ) - 1 }, { foobar, foobar_file, TEXT, sizeof( foobar ) -1 ) }, { 0,0,0,0 } };
Wichtig bei dieser Tabelle ist der letzte Eintrag. Dort muss 0 eingetragen werden, das signalisiert dem Programm das die Tabelle hier zu Ende ist. Wenn alles richtig eingetragen ist, lässt sich das Projekt ohne Probleme übersetzen und die Datei foobar.html abrufen.
Dynamische Webseiten (CGI) (Veraltet)
Am schönsten ist es, wenn es interaktiv wird. Am besten natürlich per Webbrowser. Um das zu bewerkstelligen benötigen wir CGI. CGI hat bestimmt schon jeder mal gehört. Das sind vereinfacht gesagt Webseiten, die Parameter entgegen nehmen und mit diesen arbeiten können. Als Ergebnis liefern diese CGIs wiederum Webseiten aus, die das Ergebnis enthalten oder uns darüber informieren. Um eine eigene CGI zu schreiben bedienen wir uns der Datei cgi-bin.c im Verzeichnis Apps/httpd/. Dort sind alle CGIs zusammengeführt. Um uns die Arbeit zu erleichtern nimmt uns der Webserver schon einiges ab. Er zerlegt Anfragen, filtert Parameter, zerlegt diese in Parametername und deren Inhalt. Und zu guter Letzt speichert er das ganze in einer Struktur Namens HTTP_REQUEST. Das CGI muss also beim Aufruf eigentlich nur wissen, wo diese Struktur sich befindet. Deshalb bekommt sie das auch beim Starten mit auf den Weg. Zusätzlich wird die Ausgabe von printf vorher umgebogen so das alles, was wir ausgeben wollen beim Betrachter der Webseite landet, ohne das wir uns weiter darum kümmern müssen.
Jetzt aber zum CGI. Das CGI muss einige Bedingungen erfüllen, damit es aufgerufen werden kann. Das erste wäre, wie die Funktion aufgerufen wird und mit welchem Parameter.
void cgi_foobar( void * pStruct );
So sollte die Funktionsdeklaration aussehen, und sie sollte mit am Anfang der cgi-bin.c stehen.
Die Definition der Funktion, also die eigentliche Funktion, steht weiter unten und sollte an die bestehenden Funktionen angehängt werden. Um jetzt in unserem CGI auf die Parameter u.s.w. zuzugreifen, die uns vom Client mit auf den Weg gegeben worden sind, brauchen wir noch die Struktur und einen HTML-Header. Unserer CGI sieht dann wie folgt aus:
void cgi_foobar( void * pStruct ) { struct HTTP_REQUEST * http_request; http_request = (struct HTTP_REQUEST *) pStruct; printf_P( PSTR( "<HTML>\r\n" " <HEAD>\r\n" " </HEAD>\r\n" " <BODY>\r\n")); // Hier kommen die eigenen Ausgaben hin. printf_P( PSTR( " </BODY>\r\n" "</HTML>\r\n" "\r\n")); }
So könnte dann unser Rumpf aussehen für unser erstes CGI. Aber es passiert noch nichts außer einer leeren Seite, und mit welchen Namen wird es im Webbrowser aufgerufen wird steht auch noch nicht fest. Und woher weiß unser Webserver, dass wir ein neues CGI hinzugefügt haben? Er weiß es noch nicht, aber das kann man ändern. Dazu befindet sich im oberen Teil der cgi-bin.c eine Struktur/Tabelle in der die Namen und die passende Funktion hinterlegt sind:
// Funktionsdeklarationen der CGI void cgi_stream( void * pStruct ); void cgi_dio_out( void * pStruct ); void cgi_aio( void * pStruct ); void cgi_stats( void * pStruct ); void cgi_reset( void * pStruct ); void cgi_network( void * pStruct ); void cgi_ntp( void * pStruct ); void cgi_eemem( void * pStruct ); void cgi_dio_in( void * pStruct ); // Namen der CGI const char cmd1[] PROGMEM = "dio_out.cgi"; const char cmd2[] PROGMEM = "stats.cgi"; const char cmd3[] PROGMEM = "reset.cgi"; const char cmd4[] PROGMEM = "network.cgi"; const char cmd5[] PROGMEM = "aio.cgi"; const char cmd6[] PROGMEM = "ntp.cgi"; const char cmd7[] PROGMEM = "eemem.cgi"; const char cmd8[] PROGMEM = "dio_in.cgi"; // Tabelle wo alles zusammen kommt CGIBIN cgibin[ ] = { { cgi_dio_out, cmd1 }, { cgi_stats, cmd2 }, { cgi_reset, cmd3 }, { cgi_network, cmd4 }, { cgi_aio, cmd5 }, { cgi_ntp, cmd6 }, { cgi_eemem, cmd7 }, { cgi_dio_in, cmd8 }, { NULL , NULL } };
Als erstes sieht man die Funktionsdeklarationen, danach folgen die Namen unter denen die CGI aufgerufen werden sollen. Diese Namen werden mittels PROGMEM im Flash gespeichert, da RAM zu wertvoll dafür ist. Als letztes folgt die Tabelle, in der der Zusammenhang zwischen Name und Funktion geschaffen wird. Zuerst wird die Funktion eingetragen und danach der Name. Abgeschlossen wird die Tabelle mit NULL in beiden Feldern, als Markierung dass hier Ende ist. Wenn wir unser CGI jetzt hinzufügen sollte es in etwa so aussehen:
// Funktionsdeklarationen der CGI void cgi_stream( void * pStruct ); void cgi_dio_out( void * pStruct ); void cgi_aio( void * pStruct ); void cgi_stats( void * pStruct ); void cgi_reset( void * pStruct ); void cgi_network( void * pStruct ); void cgi_ntp( void * pStruct ); void cgi_eemem( void * pStruct ); void cgi_dio_in( void * pStruct ); void cgi_foobar( void * pStruct ); // <<--- Unsere Funktionsdeklaration // Namen der CGI const char cmd1[] PROGMEM = "dio_out.cgi"; const char cmd2[] PROGMEM = "stats.cgi"; const char cmd3[] PROGMEM = "reset.cgi"; const char cmd4[] PROGMEM = "network.cgi"; const char cmd5[] PROGMEM = "aio.cgi"; const char cmd6[] PROGMEM = "ntp.cgi"; const char cmd7[] PROGMEM = "eemem.cgi"; const char cmd8[] PROGMEM = "dio_in.cgi"; const char cmd9[] PROGMEM = "foobar.cgi"; // <<-- Unser Name für das CGI // Tabelle wo alles zusammen kommt CGIBIN cgibin[ ] = { { cgi_dio_out, cmd1 }, { cgi_stats, cmd2 }, { cgi_reset, cmd3 }, { cgi_network, cmd4 }, { cgi_aio, cmd5 }, { cgi_ntp, cmd6 }, { cgi_eemem, cmd7 }, { cgi_dio_in, cmd8 }, { cgi_foobar, cmd9 }, // <<-- Unser Eintrag in die Struktur/Tabelle { NULL , NULL } }; // Hier kommen die anderen Funktionen void cgi_foobar( void * pStruct ) { struct HTTP_REQUEST * http_request; http_request = (struct HTTP_REQUEST *) pStruct; printf_P( PSTR( "<HTML>\r\n" " <HEAD>\r\n" " </HEAD>\r\n" " <BODY>\r\n")); // Hier kommen die eigenen Ausgaben hin. printf_P( PSTR( " </BODY>\r\n" "</HTML>\r\n" "\r\n")); }
Fassen wir zusammen. Um ein CGI zu schreiben/einzubinden brauchen wir 4 Dinge.
- Funktionsdeklaration
- Namen unter dem das CGI aufgerufen wird
- Eintrag in der Tabelle, um Namen und Funktion zuzuordnen
- Die Funktion selber
Wenn wir das alles beachten sollte sich jetzt alles übersetzen lassen und unser CGI verrichtet seine Arbeit. Ein Aufruf im Browser sollte dann so aussehen:
http://x.x.x.x/foobar.cgi
Das CGI-System
Aber schnell werden wir merken, dass hier ja nichts außer einer total leeren Seite erscheint. Wie ich vorhin schon angemerkt hatte, werden alle Ausgaben von printf zum Client umgeleitet. Was hindert uns daran jetzt auch per printf eigene Dinge auszugeben. Versuchen wir es mal mit der Uhrzeit. Dazu müssen wir in der cgi-bin.c noch das Headerfile system/clock/clock.h einfügen, falls nicht schon geschehen.
#include "system/clock/clock.h"
Wenn wir nun die Uhrzeit ausgeben wollen, brauchen wir noch Platz um die Uhrzeit zu ermitteln. Dazu legen wir uns die Struktur Zeit vom Typ TIME an. Danach holen wir die Zeit mit der Funktion CLOCK_GetTime ( ) und geben danach die Uhrzeit per printf aus. Das Beispiel könnte dann so aussehen:
void cgi_foobar( void * pStruct ) { struct HTTP_REQUEST * http_request; http_request = (struct HTTP_REQUEST *) pStruct; // Struktur anlegen struct TIME Zeit; // Zeit holen und in Time speichern CLOCK_GetTime ( &Zeit ); printf_P( PSTR( "<HTML>\r\n" " <HEAD>\r\n" " </HEAD>\r\n" " <BODY>\r\n")); printf_P( PSTR( "Zeit: %02d:%02d:%02d.%02d"), Zeit.hh, Zeit.mm, Zeit.ss, Zeit.ms ); printf_P( PSTR( " </BODY>\r\n" "</HTML>\r\n" "\r\n")); }
Wenn wir jetzt das CGI mit
http://x.x.x.x/foobar.cgi
aufrufen sehen wir die Uhrzeit. Mit anderen Ausgaben kann man genauso verfahren. Man könnte ja auch die Werte des ADC ausgeben lassen per printf. Alles möglich.
Die cgi-bin.c/h
Aber so richtig interaktiv ist es noch nicht, da unser CGI ja noch keine Parameter verarbeitet. Wie ich vorhin schon andeutete greift der Webserver uns hier schon unter die Arme und sortiert und speichert die Parameter und deren Inhalt für uns schon in einer Struktur ab, deren Ort wir schon am Anfang des CGI mitgeteilt bekommen haben. Wir können so über http_request auslesen was wir haben wollen. Um abzufragen, ob überhaupt ein Parameter übertragen wurde kann man mit
foo = http_request->argc;
abfragen, wie viele Parameter es gibt. Wenn http_request->argc 0 ist gibt es keine Parameter, die es auszuwerten gibt. Wenn http_request->argc größer 0 ist sind Parameter vorhanden. Um jetzt einen bestimmten Parameter abzufragen, kann man die Funktion PharseCheckName_P ( http_request, Parametername) ) benutzten. Diese gibt uns falls ein Parameter mit den gesuchten Namen vorhanden ist 1 (wahr) zurück. Ist kein Parameter mit diesen Namen vorhanden 0. Man kann so if Abfragen gestalten, um auf einen bestimmten Parameter zu testen. Ein Beispiel.
if ( PharseCheckName_P( http_request, PSTR("Zeit") ) ) { // Mach was }
Fügen wir sowas mal in unser CGI ein:
void cgi_foobar( void * pStruct ) { struct HTTP_REQUEST * http_request; http_request = (struct HTTP_REQUEST *) pStruct; // Struktur anlegen struct TIME Zeit; // Zeit holen und in Time speichern CLOCK_GetTime ( &Zeit ); printf_P( PSTR( "<HTML>\r\n" " <HEAD>\r\n" " </HEAD>\r\n" " <BODY>\r\n")); if ( PharseCheckName_P( http_request, PSTR("Zeit") ) ) { printf_P( PSTR( "Ein Paramter wurde uebertragen\r\n")); } else { printf_P( PSTR( "Zeit: %02d:%02d:%02d.%02d"), Zeit.hh, Zeit.mm, Zeit.ss, Zeit.ms ); } printf_P( PSTR( " </BODY>\r\n" "</HTML>\r\n" "\r\n")); }
Wenn wir jetzt wieder unser CGI aufrufen sehen wir wieder unsere Uhrzeit. Wenn wir aber einen Parameter mit übertragen mit
http://x.x.x.x/foobar.cgi?Zeit=1000
sehen wir an der Ausgabe, dass ein Parameter übertragen wurde und nicht mehr unsere Uhrzeit. Jetzt wollen wir aber auch sehen was der Parameter Zeit beinhaltet. Dazu müssen wir rausbekommen, das wievielte Argument es in der Struktur http_request ist. Das sagt uns die Funktion PharseGetValue_P ( http_request, Parametername ). Nachdem wir nun die Nummer des Argumentes haben holen wir uns nun deren Inhalt mit
http_request->argvalue[ PharseGetValue_P ( http_request, Parametername ) ]
Das liefert uns die Adresse (Pointer) auf den Inhalt des Parameters. Darin enthalten in ein String. Die Adresse kann man auch printf mit Hilfe des %s mit auf den weg geben. Printf gibt dann den String an dieser Adresse aus. Ein Blick in die Dokumentation der http2_pharse.c/.h und httpd2.h gibt einige nette Informationen zum Aufbau und Inhalt der Struktur http_request zum besten. Findige Programmierer werden damit auf jeden Fall ihren Spass haben. Ich will aber an dieser Stelle nochmal darauf hinweisen, dass sich hier noch was ändern kann, da die Funktionen noch nicht ganz optimal sind.
Eingebaut in unser CGI sieht das dann so aus:
void cgi_foobar( void * pStruct ) { struct HTTP_REQUEST * http_request; http_request = (struct HTTP_REQUEST *) pStruct; // Struktur anlegen struct TIME Zeit; // Zeit holen und in Time speichern CLOCK_GetTime ( &Zeit ); printf_P( PSTR( "<HTML>\r\n" " <HEAD>\r\n" " </HEAD>\r\n" " <BODY>\r\n")); if ( PharseCheckName_P( http_request, PSTR("Zeit") ) ) { printf_P( PSTR( "Ein Paramter wurde uebertragen\r\n")); printf_P( PSTR( "Parameterinhalt ist: %s" ), http_request->argvalue[ PharseGetValue_P ( http_request, PSTR("Zeit") ) ] ); } else { printf_P( PSTR( "Zeit: %02d:%02d:%02d.%02d"), Zeit.hh, Zeit.mm, Zeit.ss, Zeit.ms ); } printf_P( PSTR( " </BODY>\r\n" "</HTML>\r\n" "\r\n")); }
Wenn wir jetzt wieder
http://x.x.x.x/foobar.cgi?Zeit=1000
aufrufen erhalten wir die Ausgabe, dass ein Parameter übertragen wurde und einen Inhalt, sofern vorhanden. Es sei noch erwähnt, dass wenn der Parameter keinen Inhalt hat, der Inhalt gleich dem Parameter ist. Einfach mal mit
http://x.x.x.x/foobar.cgi?Zeit
testen.

