Wie ich chinesischem Code das Teilen lehrte

Lasst mich euch heute mal von einem einschneidenden Erlebnis rund um die Arduino-Entwicklung erzählen, das mir vorhin passiert ist.

Nach längerer Pause wollte ich heute mal wieder an der zweiten Version der Terrariums-Steuerung weiterbasteln. Die erste Version hatte ja, ähnlich wie der erste FrostCube einige Mängel, sowohl in der Hard- als auch in der Software. Beiden Steuerungen liegt ein Arduino zugrunde, jedoch ärgerte es mich schon länger, dass die normale Arduino IDE zwar aus dem Karton heraus einwandfrei funktioniert, allerdings doch sehr (sehr) eingeschränkt ist und eben speziell auf Anfänger zugeschnitten.

Daher hatte ich mich entschlossen Sloeber, dem Arduino-Plugin für Eclipse, eine Chance zu geben. Eclipse finde ich eine sehr angenehme IDE, auch wenn sie für die Entwicklung unter C nun nicht unbedingt die erste Wahl wäre. In der Java-Welt zumindest kam ich immer außerordentlich gut mit Eclipse klar.

Das ist nun alles schon einige Zeit her. Trotzdem wollte ich kurz mal den technischen Rahmen anreißen, unter dem die folgenden Absurditäten ihren Lauf nehmen.

Wie so oft in der Softwarewelt kommt es vor, dass man ein Projekt einige Zeit liegen lässt, nur um es dann Monate später auszugraben und zu erweitern. So auch hier. Die Hardware für mein Nachfolgeprojekt des TerraControl, jetzt TerraDuino genannt, war eigentlich schon länger fertig. Es fehlte nur noch die passende Software dazu und an diese wollte ich mich heute wieder mal ran setzen.

Die Ansteuerung des Displays, des Rotary-Encoders und des Relais-Board war bereits umgesetzt. Es fehlte nur noch die RTC, bei der ich diesmal zu einem Modul mit DS3232 griff, statt wie zuvor zu einem DS1307. Im Grunde sind beide Module ähnlich. Sie werden über I²C bzw. Two-Wire angesteuert und brauchen damit nur zwei Datenpins plus Masse und Versorgungsspannung. Ich stellte es mir also denkbar einfach vor das Teil einzubinden und getrennt von allen anderen Bausteinen anzusteuern. Tja, unterschätze nie den guten Murphy!

Nachdem ich die entsprechende Library eingebunden hatte, die logischerweise für die Nutzung von I²C auch TwoWire (bzw. Wire.h) braucht, dachte ich mir es könne ja nicht schaden an dieser Stelle den Code einfach schonmal zu kompilieren und laufen zu lassen. Es hatte sich ja zu vorher nichts verändert – außer, dass ich die beiden Libs eingebunden hatte.

Puff! Nichts geht mehr. Kein Magic smoke, nein. Ganz so schlimm nun doch nicht. Dafür bleibt das LCD einfach dunkel. Es springt beim Initialisieren kurz an, danach Schwärze.

Ein kurzes Debuggen zeigt, dass der Arduino problemlos läuft. Der Code läuft, es passiert das, was passieren soll, nur das Display bleibt aus. Aha! Es scheint so, als grätsche uns wohl irgendetwas in die Übertragung zum Display. Der Gedanke lag nahe, da es schon zuvor recht lange dauerte das LCD überhaupt in Betrieb zu nehmen und alle drei Milliarden Pins richtig zu stecken. Das Display nutzt nämlich kein normalsterbliches I²C oder SPI, sondern fummelt sich seine Daten irgendwie selbst zurecht. Mit in der „Packung“ war damals eine von den netten Chinesen selbst auf dieses spezielle Display (ILI9481) angepasste Version der Adafruit „TFTLCD“, sowie deren „GFX“ Library. Diese Tatsache solltet ihr auch auch für später noch im Hinterkopf behalten.

Meine Vermutungen gingen nun in verschiedene Richtungen. Entweder es war irgendeine Form der Abhängigkeiten unter den Pins. Bei der Arduino-Plattform muss man ja immer dreimal hinsehen, welcher Pin noch sonst fdür irgendwelche PWMs, Interrupts oder sonstigen Krempel verwendet wird, der nur in einer kalten Nacht im Juni bei Glatteis aktiv wird. Aber nur Dienstags. Nach kurzem Nachprüfen schied diese Vermutung aus. Alle Module nutzen glücklicherweise eine andere Übertragungsform zum Arduino.

Meine zweite Vermutung war nun, dass in den Libraries irgendetwas sich nicht verträgt. Ich hatte ja an meinem Code zu vorher nichts geändert und hatte noch garnicht damit angefangen die eingebundene Library für die RTC zu nutzen.

Also entfernte ich die Library für den DS3232 aus dem Projekt und ließ es erneut laufen. Und siehe da: Immer noch kaputt. Damit schien es an der Arduino-eigenen TwoWire Library zu liegen. Sobald ich diese entfernte, ging auch mein Display wieder an!

„Komisch“, hör ich euch jetzt rufen. „Steckt da denn ein Fehler drin? Das ergibt doch gar keinen Sinn. Das wäre bestimmt jemandem aufgefallen. Das muss an etwas anderem liegen.“ Und damit werdet ihr auch Recht behalten. Es kommt jedoch anders, als man denkt.

Da das bloße Einbetten der TwoWire Lib dazu führte, dass sich irgendetwas in meinem Programm anders verhielt, lag der Verdacht nahe, dass innerhalb der Lib trotzdem etwas beim Start passiert. Irgendein Konstruktor, irgendetwas statisches vielleicht? Initialisierung? Jedoch zunächst Fehlanzeige.

Dann fiel mir auf, dass im Code der TwoWire Lib (genauer gesagt in „twi.c“) ganz unten ein Interrupt gesetzt wird! Nicht durch attachInterrupt, wie man es im Code machen kann, sondern durch das Makro ISR(), welches quasi direkt in der Interrupttabelle den entsprechenden Eintrag setzt. Da hätten wir also die Modifikation, die allein die Einbindung der Library mit sich zu ziehen scheint. Wer sich das Ganze ansehen möchte, hier der Source zur genannten Datei. Ganz unten solltet ihr die Funktion ISR(TWI_vect) finden.

Der nächste Schritt war mal probehalber die ISR auszukommentieren und zu prüfen, ob sich etwas ändert. Gesagt getan. Ein /* */ darum gesetzt und neu kompiliert. Und siehe da: Es lief wieder!

Und nun Hand auf’s Herz: Wer hätte an dieser Stelle nicht gedacht, dass das Problemn eindeutig einen Bezug zum Interrupt hat? Aber der Spuk ist ja noch lange nicht vorbei. Wartet es nur ab.

Nun also weiter herantasten. Interrupt-Deklaration wieder einkommentiert, diesmal nur den Code innerhalb draußen lassen. Funktioniert! Aha. Es ist also nicht der Interrupt selbst, der mir irgendwie die Kommunikation mit dem Display vermurkst. Also tasten wir uns weiter. PRaktischerweise besteht die ganze ISR nur aus einem switch mit unzähligen cases. Also habe ich testweise einfach mal eine Hälfte der cases auskommentiert und neu kompiliert. Siehe da, es lief wieder!

Es folgte sogleich jedoch die Ernüchterung. Völlig egal ob ich die erste oder die zweite Hälfte der cases drin ließ, das Display lief wieder ohne Probleme. Sobald die komplette ISR kompiliert wurde, wieder Schwärze. Da stinkt doch etwas gewaltig!

Mein nächster Versuch bestand darin, der ISR direkt zu Beginn ein return unterzujubeln – mal sehen, was passiert. Und es lief wieder. Mit komplettem Code, der zwar nie ausgeführt werden konnte, aber immerhin vorhanden war. Durch ein lustiges LED-Blinken in der ISR versuchte ich herauszufinden, ob sie denn jemals überhaupt aufgerufen würde. Und siehe da, die Antwort war: Nein. Kein einziges Mal.

An dieser Stelle war nun also klar: Es war weder die Deklaration des Interrupt selbst, noch was er im Code tut. Es wurde schließlich ja nie ausgeführt. Aber lediglich den Code in das Binary zu integrieren (statt auszukommentieren) schien die Kommunikation zum Display lahm zu legen. Mit einem return zu Beginn der ISR lief es allerdings ja auch. Dabei scheint der Compiler jedoch einfach den restlichen Code bis zum Ende der Funktion wegzuoptimieren, liegt ja auch Nahe. Damit hätten wir den gleichen Fall, wie beim Auskommentieren, was den gleichen Ausgang des Experiments erklärt.

Ich konnte mir jedoch darüber hinaus keinen Reim auf das Verhalten machen. Von eventuellen Compilerfehlern bis hin zu Pointerproblemen wegen der Codegröße spukten mir alle Ansätze im Kopf herum. Mit letzterem sollte ich jedoch gar nicht so falsch liegen, wie sich bald herausstellen wird.

Während der verschiedenen Experimente hatte ich immer mal wieder beim Kompilieren die erzeugte Größe des Binarys im Auge behalten. Das schwankte so ungefähr zwischen 49,5 und 50,5 kB, je nachdem, ob ich die ISR gerade drin ließ oder auskommentierte. (Anmerkung: Das ganze Projekt läuft auf einem Arduino Mega, daher ist die kompilierte Größe für mich nicht von höchster Priorität).

Obwohl ich selbst nicht so recht daran glaube, erschien mir der Zufall zu groß, dass es tatsächlich beim Schrittweisen Auskommentieren der einzelnen cases in der ISR exakt beim Überschreiten der 50.000 Bytes plötzlich nicht mehr lief. Von 49.984 auf 50.002 Bytes schmierte das Display plötzlich ab. Aber das konnte doch nur Zufall sein!?

Mir fiel auf, dass die Adafruit Libraries an diversen Stellen von pgm_read_byte und Konsorten gebrauch machen. Vielleicht reicht ja für die Adressierung der entsprechenden Stelle ein 16-Bit einfach nicht aus? Daher ersetzte ich kurzerhand alle Aufrufe durch die „far“-Variante und probierte es erneut. Das Display blieb schwarz. Keine Verbesserung also Fehlanzeige. Mist!

In diesem Zuge wollte ich der ganzen Geschichte nun aber doch mal ordentlich auf den Zahn fühlen. Eine kurze Recherche zum Dekompilieren von Arduino-Code führte zu Tage, dass man dazu hervorragend das Tool avr-objdump nutzen kann, welches direkt mit der Arduino-Installation kommt.

Wählt man die beim Kompilieren generierten ELF-Dateien aus, so generiert das Tool auch gleich beim Dekompilieren noch passende Kommentare dazu. Also jagte ich mein Binary mit avr-objdump.exe -S „TerraDuino.elf“ > „TerraDuino.txt“ durch das Tool und in der erzeugten Textdatei kann man sich wunderschön den kompletten Assemblycode ansehen, der auf dem Arduino ausgeführt wird. Dazu komme ich allerdings gleich erst.

Mir kam die Idee, auch die Initialisierung des Displays einmal näher zu betrachten. Das passiert in der innerhalb von Adafruit_TFTLCD::begin. Darin wird anhand der übergebenen „Display-ID“ entschieden mit welchen Registerwerten das Display anzusteuern und zu initialisieren ist. Die Werte werden über das genannte pgm_read_byte aus einem vorher definierten Array ILI9481_regValues geholt und ausgewertet. Dieses ist wie folgt definiert:

static const uint16_t ILI9481_regValues[] PROGMEM = {	
	0x11, 0,
	TFTLCD_DELAY, 50,
	0xD0, 3, 0x07, 0x42, 0x18,
	0xD1, 3, 0x00, 0x07, 0x10,
	0xD2, 2, 0x01, 0x02,
	0xC0, 5, 0x10, 0x3B, 0x00, 0x02, 0x11,
	0xC5, 1, 0x03,
	0x36, 1, 0x0A,
	0x3A, 1, 0x55,
	0x2A, 4, 0x00, 0x00, 0x01, 0x3F,
	0x2B, 4, 0x00, 0x00, 0x01, 0xE0,
	TFTLCD_DELAY, 50,
	0x29, 0,
	0x2C, 0,
	TFTLCD_DELAY, 50,
};

In meinem Fall wurde die Library von den netten Chinesen um die entsprechenden Einträge für den ILI9481 ergänzt. Es gibt bei mir also eine zusätzliche Abfrage in der begin-Funktion für die ID 0x9481. Das Ganze sieht bei mir (ähnlich zu den anderen IDs) folgendermaßen aus:

} else if (id == 0x9481) {
    driver = ID_9341;
    CS_ACTIVE;
	
    while(i < sizeof(ILI9481_regValues)) {
        uint8_t r = pgm_read_byte(&ILI9481_regValues[i++]);
        uint8_t len = pgm_read_byte(&ILI9481_regValues[i++]);

        if (r == TFTLCD_DELAY) {
            delay(len);
        } else {
            //Serial.print("Register $"); Serial.print(r, HEX);
            //Serial.print(" datalen "); Serial.print(len);

            CS_ACTIVE;
            CD_COMMAND;
            write8(r);
            CD_DATA;
			
            for (uint8_t d = 0; d < len; d++) {
                uint8_t x = pgm_read_byte(&ILI9481_regValues[i++]);
                write8(x);
            }
			
            CS_IDLE;
        }
    }
    return;
}

Nach dem Dekompilieren liegen die genannten regValues im Binary so vor:

00000140 <_zl17ili9481_regvalues>:
     140:	11 00 00 00 ff 00 32 00 d0 00 03 00 07 00 42 00     ......2.......B.
     150:	18 00 d1 00 03 00 00 00 07 00 10 00 d2 00 02 00     ................
     160:	01 00 02 00 c0 00 05 00 10 00 3b 00 00 00 02 00     ..........;.....
     170:	11 00 c5 00 01 00 03 00 36 00 01 00 0a 00 3a 00     ........6.....:.
     180:	01 00 55 00 2a 00 04 00 00 00 00 00 01 00 3f 00     ..U.*.........?.
     190:	2b 00 04 00 00 00 00 00 01 00 e0 00 ff 00 32 00     +.............2.
     1a0:	29 00 00 00 2c 00 00 00 ff 00 32 00                 )...,.....2.

Die Datensektion sieht OK aus. Wenn wir das mit der Definition im Code oben vergleichen, wird klar, dass die Werte exakt kompiliert wurden. Wie sollte es auch anders sein? Es wird außerdem ersichtlich, dass auch die Umstellung des Auslesens auf pgm_read_byte_far (mit 24-Bit Adressraum statt 16-Bit Adressraum) keine Änderung hat bringen können. die referenzierten Einträge starten bei 0x140 und liegen damit (wohl absichtlich) am Anfang des Binaries, gefolgt von anderen Werten und anschließend erst der Code-Section.

Was wissen wir nun aktuell also? Nur durch den Größenunterschied des Binarys wird das Display anders initialisiert! Das ist Fakt. Nun gilt es also herauszufinden, was genau dafür verantwortlich ist.

Praktischerweise gibt es im obigen Code zur Initialisierung des Displays schon einige auskommentierte Logs. Diese geben die Einzelschritte beim Initialisieren des Displays aus, jeweils in welches Register wieviele Datensätze geschrieben werden. Die sind in der Deklaration oben erkennbar. In jeder Zeile steht zunächst das Register, dann die Anzahl der Werte, dann die Werte selbst. Ein TFTLCD_DELAY wird gesondert gehandhabt und führt nur dazu, dass der folgende Wert in Millisekunden gewartet wird.

Also, zack, Log-Ausgaben angeschaltet und die beiden Varianten (Display geht, Display geht nicht) miteinander verglichen. Zusätzlich hab ich noch den aktuellen Index „i“ und das sizeof(ILI9481_regValues) ausgeben lassen.

Display geht:

Register $11 datalen 0 i=2 size=108
Register $D0 datalen 3 i=6 size=108
Register $D1 datalen 3 i=11 size=108
Register $D2 datalen 2 i=16 size=108 
Register $C0 datalen 5 i=20 size=108
Register $C5 datalen 1 i=27 size=108
Register $36 datalen 1 i=30 size=108
Register $3A datalen 1 i=33 size=108
Register $2A datalen 4 i=36 size=108 
Register $2B datalen 4 i=42 size=108
Register $29 datalen 0 i=50 size=108
Register $2C datalen 0 i=52 size=108
Register $0 datalen 0 i=56 size=108
Register $0 datalen 91 i=58 size=108 

Display geht nicht:

Register $11 datalen 0 i=2 size=108
Register $D0 datalen 3 i=6 size=108
Register $D1 datalen 3 i=11 size=108
Register $D2 datalen 2 i=16 size=108 
Register $C0 datalen 5 i=20 size=108
Register $C5 datalen 1 i=27 size=108
Register $36 datalen 1 i=30 size=108 
Register $3A datalen 1 i=33 size=108
Register $2A datalen 4 i=36 size=108
Register $2B datalen 4 i=42 size=108 
Register $29 datalen 0 i=50 size=108
Register $2C datalen 0 i=52 size=108
Register $0 datalen 1 i=56 size=108
Register $0 datalen 0 i=59 size=108 
Register $B datalen 1 i=61 size=108
Register $3 datalen 6 i=64 size=108
Register $9 datalen 245 i=72 size=108
Register $3 datalen 3 i=63 size=108 
Register $B datalen 0 i=68 size=108
Register $10 datalen 6 i=70 size=108 
Register $8 datalen 10 i=78 size=108
Register $0 datalen 67 i=90 size=108

Nanu! Dem Display werden bei beiden Varianten verschiedene Initialwerte in die Register geschoben! Interessant ist, dass nicht das Auslesen selbst durch pgm_read_byte kaputt zu sein scheint, da beide Varianten zunächst das gleiche tun, dann aber unterschiedlich weiterarbeiten. Bei der Variante, in der das Display nicht anspringt, wird noch irgendein Murks hinterhergeschoben!

Sehen wir und noch einmal die Deklaration von ILI9481_regValues an. Das letzte Register, das vor dem abschließenden TFTLCD_DELAY beschrieben werden soll, ist 0x2C mit Länge 0. Das passiert in beiden Varianten. Spannend wird nun, dass selbst in der funktionierenden Variante („Display initialisiert“) hinterher noch zweimal das Register $0 nachgeschoben wird, sogar einmal mit 91 Datenbytes! In der kaputten Variante („Display initialisiert nicht“) kommt danach noch komplett anderer Müll rein, bis die Funktion sich irgendwann entscheidet es gut sein zu lassen. Durch die Ausgabe des Zählers i und dem Rückgabewert des sizeof der „regValues“ ist klar, dass er über das eigentliche Array hinaus liest!

Dafür nun noch einmal ein Blick in die dekompilierten Teile beider Varianten. Und siehe da, sie unterscheiden sich an dieser Stelle! Auf die oben beschriebene Definition der regValues bei 0x140 folgt:

In der funktionierenden Variante (ohne ISR):

000001ac <_zl4font>:
     1ac:	00 00 00 00 00 3e 5b 4f 5b 3e 3e 6b 4f 6b 3e 1c     .....>[O[>>kOk>.
     1bc:	3e 7c 3e 1c 18 3c 7e 3c 18 1c 57 7d 57 1c 1c 5e     >|>..< ~<..W}W..^
     1cc:	7f 5e 1c 00 18 3c 18 00 ff e7 c3 e7 ff 00 18 24     .^...<.........$
     1dc:	18 00 ff e7 db e7 ff 30 48 3a 06 0e 26 29 79 29     .......0H:..&)y)
...

In der kaputtenVariante (mit ISR):

000001ac <_zl15roboto_14glyphs .lto_priv.65>:
     1ac:	00 00 01 01 04 00 00 01 00 01 0b 04 01 f5 03 00     ................
     1bc:	03 03 06 01 f5 05 00 08 0b 0a 00 f5 10 00 06 0d     ................
     1cc:	09 01 f5 1a 00 09 0b 0c 01 f5 27 00 08 0b 0a 01     ..........'.....
     1dc:	f5 32 00 01 03 04 01 f5 33 00 04 10 06 01 f4 3b     .2......3......;
...

Aha! Da liegt also ein Unterschied im Binary! Wir erinnern uns: Die Adafruit Lib liest Werte händisch über aus den regValues. Das sieht mir doch alles stark nach einer fehlerhaften Abbruchbedingung aus, die dazu führt, dass die Funktion zum Initialisieren des Displays aus dem Datenbereich der ILI9481_regValues einfach frisch-fröhlich über die Werte hinaus liest!

Diese Abbruchbedingung sieht in der modifizierten Adafruit Lib, wie oben schon kopiert, so aus:

while(i < sizeof(ILI9481_regValues))

Moment mal! Hier vergleichen wir doch Äpfel mit Birnen. Wir vergleichen den Index "i" mit der Byte-Größe der ILI9481_regValues. Das scheint zunächst zwar logisch, ist aber eigentlich Quatsch. "i" wird nach jedem Auslesen durch pgm_read_byte um eins erhöht, spiegelt aber ja den aktuellen Index des Arrays wieder, nicht das aktuelle Offset in Bytes! Das würde auch erklären, weshalb direkt nach dem Schreiben ins Register $2C noch nicht Schicht im Schacht ist. Hier sollte der Initialisierungsprozess eigentlich fertig sein, jedoch ist "i" gerade einmal halb so groß, wie "size". Das ist ja auch logisch, wenn wir den Index um eins erhöhen, aber intern ein Array aus uint16_t-Werten vorliegen haben.

Was passiert hier also? Durch die falsche Abbruchbedingung liest die Funktion zum Initialisieren einfach über die Werte aus den ILI9481_regValues hinaus ein und schiebt sie zum Display rüber! Durch Zufall interpretiert er die nachfolenden Bytes aus "_ZL4font" vermeintlich funktionierenden Variante als Register $0 und datalen 91, was wohl dazu führt, dass das Display trotzdem korrekt startet, obwohl auch hier bereits Müll geschrieben wird! In der nicht funktionierenden Variante werden nach der korrekten Initialisierung (runter bis Register $2C) noch andere Register mit Müll aus dem Code von "_ZL15Roboto_14Glyphs.lto_priv.65" beschrieben. Das führt offenbar dazu, dass das Display nicht startet (oder einfach wieder ausgeht). Irgendwas geht jedenfalls kaputt. Deshalb wurde auch nichts angezeigt.

Was ist also die Lösung? Die Lösung ist, in der Abbruchbedingung nicht den Index mit der Größe in Bytes zu vergleichen. Dazu müssen wir einfach das sizeof(ILI9481_regValues) noch durch sizeof(uint16_t) teilen. Damit erhalten wir wieder die eigentliche Anzahl an Einträgen im Array, statt der Größe in Bytes.

Man mag es kaum glauben, aber die Änderung der Zeile in...

while(i < sizeof(ILI9481_regValues) / sizeof(uint16_t))

...führt dazu, dass in beiden Varianten (mit oder ohne TwoWire Lib und aktivierter ISR) das Display jetzt korrekt initialisiert!

Auch die Debug-Ausgabe ist nun bei beiden Varianten identisch und nun auch korrekt, ohne die beiden letzten Zeilen mit Murks:

Register $11 datalen 0 i=2 size=108
Register $D0 datalen 3 i=6 size=108
Register $D1 datalen 3 i=11 size=108
Register $D2 datalen 2 i=16 size=108 
Register $C0 datalen 5 i=20 size=108
Register $C5 datalen 1 i=27 size=108
Register $36 datalen 1 i=30 size=108 
Register $3A datalen 1 i=33 size=108
Register $2A datalen 4 i=36 size=108
Register $2B datalen 4 i=42 size=108 
Register $29 datalen 0 i=50 size=108
Register $2C datalen 0 i=52 size=108

So wie das Leben sein sollte!

Und was lernen wir daraus? Wenn wir etwas lernen, dann wohl: Vertraue nie dem Code anderer Leute, selbst wenn die Library von noch so dubiosen Chinesen für dein neues LCD handgeklöppelt wurde!

Nun versteht ihr hoffentlich auch den doch sehr merkwürdigen Titel dieses doch sehr lang gewordenen Blogeintrags. 😀

Und damit euch noch eine gute Nacht und vielen Dank für's Lesen!

Bookmark the permalink.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.