Zeichenketten im myAVR C++ Framework
Zeichenketten, auch Strings genannt, sind dem Wesen nach Felder (Arrays) vom Typ char im Arbeitsspeicher (SRAM) des Controllers. Die String-Klasse ist aber eben nicht nur der eigentliche Speicher für die Zeichenketten, sondern repräsentiert auch eine Reihe von Operationen, die auf diese Speicher ausgeführt werden können. Dabei ist einer der wichtigsten Unterschiede zwischen einem Charakter-Array und einer String-Klasse die dynamische Speicherverwaltung. Das bedeutet, dass beim einfachen Array die Größe des benötigten Speichers zum Zeitpunkt des Kompilierens fest steht und sich dann nicht mehr ändert. Bei der String-Klasse wird erst zur Laufzeit der dafür benötigte Speicherplatz angefordert und bei String-Operationen auch zur Laufzeit der Anwendung vergrößert oder verkleinert. Gleichzeitig gibt es String-Operationen, die eleganten Operatoren wie +, += oder == zugeordnet sind und ein sehr modernes und komfortables Arbeiten erlauben. Leider gibt es das alles nicht zum Nulltarif. String-Klassen sollten nur benutzt werden, wenn ausreichend FLASH und SRAM zur Verfügung stehen. Der ATmega8 steckt das durchaus noch weg, aber so mancher Tiny eben nicht. Trotzdem kann auch auf kleineren Controllern noch objektorientiert gearbeitet werden, wenn man zum Beispiel auf den Komfort der String-Klasse verzichtet und wie im klassischen C mit Arrays arbeitet.
Erste Schritte mit der String-Klasse
Strings können wie jede andere Variable als Attribut der Klasse Controller oder lokal angelegt werden. Als Klassenattribut stehen diese in allen Operationen der Klasse zur Verfügung, belegen dafür aber zur gesamten Laufzeit Arbeitsspeicher (SRAM). Als lokale Variablen wird nur für die Dauer der Gültigkeit des Strings SRAM belegt.
class Application : public Controller { // Strings als Klassenattribute sind in allen Operationen stets verfügbar protected: String text1; ... public: void onWork() { // Strings als lokale Variablen benötigen nur kurzzeitig Speicher im SRAM String text2; } } app; // Anwendungsinstanz
Als erste Übung mit der Klasse String soll eine Zeichenkette einfach per UART an den PC gesendet werden. Der String ist als Klassenattribut zu realisieren. Wie gehabt formulieren wir unsere Entwurfsgedanken erst mal als Kommentare.
////////////////////////////////////////////////////////////////////// // ENTWURF Beispiel String1 ////////////////////////////////////////////////////////////////////// class Application : public Controller { // einen String anlegen // eine Instanz der UART anlegen public: void onStart() { // dem String einen Inhalt zuweisen // die UART initialisieren } public: void onWork() { // den String per UART an den PC senden // eine Sekunde warten } } app;
Bei der folgenden Realisierung kommt die Zuweisung des Inhaltes der Zeichenkette recht unspektakulär mit einer einfachen Wertzuweisung aus. Vergleichen Sie dies mit den Möglichkeiten von C eine Charakter-Array zur Laufzeit zu füllen oder kurz gesagt in C ist das zum Beispiel die Funktion strcpy(text, „Hallo“); Diese ist zwar noch überschaubar, aber sexy ist es nicht, denn der Entwickler möchte den Inhalt an dieser Stelle zuweisen und muss sich mit einer Kopierfunktion auseinandersetzen. Den Versuch in C einem Charter-Array seinen Inhalt dynamisch per Wertzuweisung zu geben, wird mit den lakonischen Fehlermeldungen invalid array assignment vom Compiler abgewiesen. Geschweige denn dass sich C darum kümmert, wenn das Array zu klein für die Zeichenkette ist.
////////////////////////////////////////////////////////////////////// // Beispiel String1 ////////////////////////////////////////////////////////////////////// class Application : public Controller { protected: String text; protected: Uart terminal; public: void onStart() { text="Hallo AVR C++ "; terminal.config(9600); } public: void onWork() { terminal.sendString(text); waitMs(1000); } } app; // Anwendungsinstanz
Bilden, übertragen und testen sie das Programm. Beachten Sie die Einstellungen im myAVR Controlcenter.
Es sollte folgendes Ergebnis zu sehen sein.
Zeichenketten manipulieren
Besonders bei der Interaktion mit dem Benutzer eines Systems ist es oft notwendig, textuelle Ausgaben zum Beispiel auf einem LC-Display aus verschiedenen Zeichenketten zusammenzusetzen bzw. die Strings in geeigneter Form anzupassen. Im Folgenden eine kleine Übersicht zu wichtigen Stringmanipulationen über Operatoren.
///////////////////////////////////////////////// // Übersicht zu den Operatoren der Klasse String ///////////////////////////////////////////////// // Zuweisen text="Hallo"; // Kopieren text1=text2; // Zusammensetzen text1="myAVR"; text2="Hallo "+text1+"!"; // Anfügen text1+=text2; // Vergleichen if (text1 == text2) { ... }
Das erste Beispiel für eine Stringmanipulation soll eine Erweiterung des vorangegangenen Beispiels sein. Es soll per Uart abwechselnd „Hallo myAVR“ und „Hallo AVR C++„ ausgegeben werden. Dabei ist der jeweils aktuelle Ausgabestring aus den drei Teilstrings „Hallo“, „myAVR“ und „AVR C++„ zusammenzusetzen. Zum Einsatz kommt dabei der Plus-Operator.
////////////////////////////////////////////////////////////////////// // ENTWURF Beispiel Stringmanipulation 1 ////////////////////////////////////////////////////////////////////// class Application : public Controller { // Insatnzen für den Ausgabe- und die Teilstrings sowie die Uart anlegen public: void onStart() { // Teilstrings und Uart initialisieren } public: void onWork() { // Ausgabestring 1 "Hallo myAVR" zusammensetzen // String per Uart senden // eine sekunde warten // Ausgabestring 2 "Hallo C++" zusammensetzen // String per Uart senden // eine sekunde warten } } app;
Nachdem wir uns ein Konzept per Kommentar zurechtgelegt und das ganze noch mal einem prüfenden Blick unterworfen haben, kann die Realisierung beginnen. Es soll jede Ausgabe auf einer neuen Zeile beginnen. Dazu schließen wir den String mit dem Sonderzeichen „\n“ ab. Im myAVR Controlcenter ist es möglich, die Einstellung für den Zeilenvorschub in der Textansicht anzupassen. In ASCII-Code ist der Zeilenvorschub (Line Feed LF) mit der Zahl 10 codiert oder in hexadezimaler Schreibweise 0x0A.
////////////////////////////////////////////////////////////////////// // Beispiel Stringmanipulation 1 ////////////////////////////////////////////////////////////////////// class Application : public Controller { protected: String text,part1,part2; protected: Uart terminal; public: void onStart() { part1="myAVR"; part2="AVR C++"; terminal.config(9600); } public: void onWork() { text="Hallo "+part1+"! \n"; terminal.sendString(text); waitMs(1000); text="Hallo "+part2+"! \n"; terminal.sendString(text); waitMs(1000); } } app;
Bilden, übersetzen und testen Sie das Programm. Es sollten mit myAVR Controlcenter abwechselnd die Zeilen „Hallo myAVR“ und „Hallo AVR C++„ erscheinen.
Die Formatfunktion
Richtig spannend wird es, wenn auf einem Display Messwerte angezeigt werden sollen. Die String-Klasse bietet verschiedene Möglichkeiten Binärwerte in druckbare Zeichen als ASCII-Code umzuwandeln. Die Formatfunktion der Klasse String entspricht vom Konzept der C Funktion printf.
Übersicht zu wichtigen Formatzeichen
- %d … gibt den Wert als vorzeichenbehafteten Dezimalwert aus
- %i … gibt den Wert als vorzeichenbehafteten Dezimalwert aus, entspricht %d
- %u … gibt den Wert als vorzeichenlosen Dezimalwert aus
- %x … gibt den Wert als Hexadezimalwert aus, Buchstaben sind klein
- %X … gibt den Wert als Hexadezimalwert aus, Buchstaben sind groß
- %% … gibt ein %-Zeichen aus
erweiterte Formatierung
- %04X … es werden führende Nullen erzwungen, die Zahl gibt an, wie viele Stellen
- %6d … ohne den Punkt werden die führenden Stellen mit Leerzeichen aufgefüllt
- \n … fügt das Sonderzeichen für einen Zeilenvorschub ein (new line)
- \t … fügt das Sonderzeichen für einen Tabulator ein
beachte:
- %f %e %g … Gleitkommaformatierungen sind in der vorkompilierten myAVR C++ Bibliothek abgeschaltet
Für den ersten Test zur Formatierung von Strings inkrementieren wir einfach eine Zahl und schauen mal, was sich mit den verschiedenen Formaten anstellen lässt. Zuerst wieder das Konzept.
//////////////////////////////////////////////////////////////// // ENTWURF Beispiel String-Formatierung //////////////////////////////////////////////////////////////// class Application : public Controller { // Instanzen für die zahl, den String und die Uart anlegen public: void onStart() { // Zahlenwert, Uart und ggf. den String initialisieren } public: void onWork() { // den String mit dem Zahlenwert formatieren // dann per Uart senden // eine Sekunde warten // zum Schluss der Wert hochzählen } } app;
Ich weiß, dass mit den Kommentaren fängt an zu nerven. Aber wann wollen wir Disziplin lernen, wenn nicht jetzt?
Erst nach dem Entwurf und dessen Review können wir mit der Realisierung beginnen. Lesen Sie ruhig
ein bischen etwas über die Funktion printf nach. Wichtig ist, dass ein % immer anzeigt, dass an dieser Stelle in der angegebenen Zeichenkette eine Formatierung eines Parameters erfolgt. Die Anzahl der folgenden Parameter muss unbedingt mit der Anzahl der Formatierungszeichen übereinstimmen. Desweiteren denken sie daran, dass wir die Funktion nicht auf dem PC mit schier unendlichen Ressourcen fahren, sondern auf einem 8 Bit Mikrocontroller. Also müssen wir ein paar Einschränkungen gegenüber den Möglichkeiten der Standard C Funktion akzeptieren.
//////////////////////////////////////////////////////////////// // Beispiel String-Formatierung //////////////////////////////////////////////////////////////// class Application : public Controller { protected: uint16 wert; protected: String text; protected: Uart terminal; public: void onStart() { terminal.config(9600); } public: void onWork() { text.format("wert = %u ist als HEX = %X \n",wert,wert); terminal.sendString(text); waitMs(1000); wert++; } } app; // Anwendungsinstanz
Bilden, übersetzen und testen Sie das Programm. Beachten Sie die Einstellungen für den Zeilenvorschub im myAVR Controlcenter. Variieren Sie die Formatierung. Im Folgenden einige Anregungen:
text.format("wert = %6u das ist als HEX %4X \n",wert,wert); text.format("wert = %06d das ist als HEX %04X \n",wert,wert); text.format("wert = %i das ist als HEX %x \n",wert,wert); text.format("wert = %6d = 0x%04X \n",wert,wert); ...
Kommas ohne float
Viele Mikrocontrolleranwendungen erfassen analoge Messwerte oder/und realisieren mehr oder weniger ausgefeilte Steuer- und Reglungskonzepte, bei denen mit einmal Kommazahlen ins Spiel kommen. Der AVR verfügt über keine Einheit für Fließkommaarithmetik (FPU, Floting Point Unit). Eine Berechnung von Kommazahlen muss dieser aufwendig in Softwareroutinen abbilden. Das frisst selbst in Assembler aber auch in C erheblich FLASH und Rechenzeit. Die Arithmetik mit ganzzahligen Werten erledigt ein AVR durchaus fix und speichereffizient. Für viele Anwendungsfälle genügt es, die gewünschte Berechnung einer Zahl mit Kommastellen in Integerarithmetik abzubilden. Ein einfaches Konzept Kommazahlen mit hinreichender Genauigkeit zu ermitteln ist, dass man einfach die gewünschten Operationen um eins, zwei oder drei Potenzen erhöht. Also nicht mit Metern und zwei Nachkommastellen rechen, sonder einfach in Zentimeter ohne Komma oder statt in Volt mit drei Kommastellen in Millivolt. Die Integerarithmetik unterstützt diese Vorgehensweise sehr gut und bietet Operatoren an, um auch mit der jeweils zugehöhrigen Kommazahl einigermaßen ordentlich umgehen zu können. Dabei werden die Vorkommastellen und die Nachkommastellen jedoch getrennt voneinander gehandhabt. Bei einer ganzzahligen Division erhält man als Ergebnis leider nur die Vorkommastellen. Der verbleibende Rest hinter dem Komma scheint verloren. Mit dem Modulo lässt sich dieser aber auch ermitteln, womit wir dann die Nachkommastellen in der Hand haben. Im Folgenden Beispiel soll die Ausgabe von Kommazahlen ohne die tatsächliche Anwendung von Gleikommaarithmetik demonstriert werden.
///////////////////////////////////////////////////////////////////// // Konzept für das Beispiel Kommazahlen ohne float ///////////////////////////////////////////////////////////////////// class Application : public Controller { // Annahme: ein Wert liegt in Tausendstel vor also milliWasauchimmer // Attribute für den Wert, ggf. Zwischenwerte und die Uart public: void onStart() { // Initialisierung der Uart und Startwert } public: void onWork() { // Formatierung des milli-Wertes und Ausgabe // Formatierung des dezi-Wertes *10 mit einer Kommastelle und Ausgabe // Formatierung des centi-Wertes *100 mit zwei Kommastellen und Ausgabe // Formatierung des Wertes *1000 mit drei Kommastellen und Ausgabe // etwas warten denn so schnell können wir nicht gucken // Wert weiterzählen } } app;
Damit wir die Übersicht behalten, soll die Ausgabe zeilenweise und tabellarisch erfolgen.
///////////////////////////////////////////////////////////////////// // Beispiel Kommazahlen ohne float ///////////////////////////////////////////////////////////////////// class Application : public Controller { // Annahme: Wert in Tausendstel also milliWasauchimmer protected: uint16 milliWert; protected: String text; protected: Uart terminal; public: void onStart() { terminal.config(9600); milliWert=900; } public: void onWork() { text.format("milli = %u\t",milliWert); terminal.sendString(text); text.format("centi = %u,%u\t",milliWert/10,milliWert%10); terminal.sendString(text); text.format("dezi = %u,%02u\t",milliWert/100,milliWert%100); terminal.sendString(text); text.format("Wert = %u,%03u\n",milliWert/1000,milliWert%1000); terminal.sendString(text); waitMs(100); milliWert++; } } app;
Bilden, übertragen und testen sie das Programm. Beachten sie die Einstellungen für den Zeilenvorschub im myAVR Controlcenter.