... oder wie man sich auf einem Arduino in den Fuß schießt

In der Sommer-Computerschule verwenden wir
einen alten Computer, der von uns selbst hergestellt wurde, um die Spieleentwicklung zu unterrichten.
Jetzt verfügt es über eine Arduino Mega-Karte mit einem ATmega2560-Prozessor, in dem bis zu 256 Kilobyte Flash-Speicher gespeichert sind. Es wurde angenommen, dass dies sehr lange dauern würde, da die Spiele einfach sind (der Bildschirm ist nur 64x64 Pixel groß). In der Realität traten einige Probleme auf, als die Firmware ungefähr 128 Kilobyte erreichte.
Im Programmspeicher werden trotz des Namens neben dem ausführbaren Code der Spiele alle Arten von unveränderten Daten wie Sprites und Level-Tabellen gespeichert. Diese Daten sind nicht so sehr.
Aber als wir
den YM2149F-Soundchip an unseren Computer angeschlossen und ein paar Dutzend Musikstücke in denselben Programmspeicher heruntergeladen haben, begannen Probleme.
Das Präfix stürzte ab, wenn versucht wurde, eine Melodie abzuspielen, oder es wurde Müll in das Spielmenü gezogen. Es war nicht klar, wie dies überhaupt zu debuggen ist, da der Prozessor nicht nur die Logik des Spiels behandelt, sondern auch Bild und Ton anzeigt. Als Ergebnis stellte sich heraus, dass der gcc-avr-Compiler Variablen mit einer Größe von zwei Bytes zum Speichern von Zeigern verwendet. Es ist jedoch unmöglich, 256 Kilobyte mit nur zwei Bytes zu adressieren! Wie kommt er raus?
Code-Zeiger
Erstens können Funktionsaufrufanweisungen und Sprünge Drei-Byte-Adressen verwenden. Daher reicht es für den Linker aus, die vollständige Adresse in einer solchen Anweisung zu ersetzen, und es wird funktionieren. Wenn die Adresse der Funktion über einen Zeiger übergeben wird, wird eine solche Nummer nicht übergeben - da wir einen Doppelbyte-Zeiger haben.
In dieser Situation fügt gcc ein „Sprungbrett“ in die unteren 64 KB ein - den Befehl jmp, der auf die gewünschte Funktion umschaltet. Dann fungiert die Adresse dieses Sprungbretts als Adresse der Funktion, die in der Variablen gespeichert werden muss - schließlich wird sie in zwei Bytes platziert. Und wenn es angerufen wird, wird es bei Bedarf einen Übergang geben.
Datenzeiger
Wir speichern aber nicht nur den ausführbaren Code im Programmspeicher. Die Sprünge werden hier also nicht helfen - wir dereferenzieren Zeiger, gehen aber nicht zu ihnen.
Die AVR-Bibliothek verfügt sogar über Funktionen / Makros wie pgm_read_byte_far (addr), um den vollständigen Zeiger zu dereferenzieren (ihnen werden Vier-Byte-Werte übergeben). Aber gcc weiß nicht, wie man diese Zeiger mit der C-Sprache bekommt.
Glücklicherweise gibt es ein Makro pgm_get_far_address (var), um die vollständige Adresse einer Variablen abzurufen. Dies erfolgt mit dem integrierten Assembler (der Fall, wenn der Assembler schlauer als der Compiler ist).
Der gesamte Code, der die Daten im ROM verwendet, muss noch neu geschrieben werden. Das heißt, ein Musik-Player, der Sprites rendert, Text ausgibt, ... Keine sehr angenehme Erfahrung. Ja, und der Code wird hemmender, und für Grafiken ist er sehr kritisch. Daher ist
Wir verteilen Daten auf ROM
Linker ist sehr bemüht, Daten für den Programmspeicher in den unteren 64 KB zu speichern. Dies funktioniert nicht, wenn zu viele Daten vorhanden sind. Aber die größten Daten, die wir haben, sind Musikdateien. Wenn Sie also nur sie entfernen, passt alles andere in den unteren Speicher und Sie müssen den Hauptteil des Codes nicht wiederholen.
Dazu nutzen wir die Funktionen des Linker-Skripts. Einer der letzten Abschnitte, die der Linker im ROM platziert, heißt .fini7. Speichern Sie alle Musik-Arrays in diesem Abschnitt:
#define MUSICMEM __attribute__((section(".fini7"))) const uint8_t tetris2[] MUSICMEM = { ... };
Jetzt sagt uns avr-nm, dass alles in Ordnung ist - die Daten mit Sprites und Levels befinden sich unten im ROM und die Musik oben.
00002f9c t _ZL10level_menu 00002e0f t _ZL10rope_lines 000006de t _ZL10ShipSprite 00023a09 t tetris2 00024714 T the_last_v8
Es bleibt, den Player zu wiederholen, um Vier-Byte-Zeiger zu verwenden, und anstatt einen Zeiger auf ein Array mit dem Melodiecode zu verwenden, verwenden Sie die Funktion, um seine Adresse abzurufen. Funktionen sind erforderlich, da wir eine Player-Anwendung haben, mit der Sie alle Musikstücke Ihrer Wahl anhören können. Es speichert jetzt Zeiger auf Funktionen dieser Art:
00006992 <_Z12tetris2_addrv>: 6992: 61 ef ldi r22, 0xF1 ; 241 6994: 7a e3 ldi r23, 0x3A ; 58 6996: 82 e0 ldi r24, 0x02 ; 2 6998: 99 27 eor r25, r25 699a: 08 95 ret
Das Ende der Welt wird verzögert, bis die Sprites die unteren 64k erreichen. Dies ist unwahrscheinlich, da immer noch mehr Code als Sprites vorhanden ist, was bedeutet, dass der Speicher wahrscheinlich im Allgemeinen endet.
Bonus
Diesen Sommer haben wir ein Spiel im Stil von Sokoban geschrieben. Einige Level erwiesen sich als ziemlich schwierig. Versuchen Sie zum Beispiel, diese zu bestehen:

Referenzen
- Github-Projektseite
- Arduino und LED-Anzeige
- Arduino und der musikalische Stein des
Philosophen - Die letzten Spiele des letzten Jahres