Weg von Keil, hin zu SDCC

Migrationshinweise. In englisch: Von Infineon, von 2007.

Im allgemeinen geht es um das Problem, dass der Keil-Linker BL51.exe bzw. XL51.exe ein Kode-Limit von 2 KiByte (im Bundle mit Cypress 4 KiByte) hat. Sicherlich findet man irgendwo einen Crack, aber damit verbaut man sich die Möglichkeit, Kode zu veröffentlichen. Als Ersatz muss SDCC herhalten. Leider optimiert SDCC noch einiges schlechter als Keil-C, sodass man im Projekt mehr Speicher einplanen muss. Von Vorteil ist bei SDCC der zuverlässigere Compiler, der IMHO besser Warnungen und Fehler findet, und nicht bei Konstrukten wie if (0<i) abstürzt.

Im folgenden geht es darum, einen vorhandenen umfangreichen Quelltext von Keil-C auf SDCC umzusetzen und dabei die Übersetzbarkeit mit beiden Compilern aufrechtzuerhalten. Beide Compiler sind kode-bank-fähig; 8051-Programme mit mehr als 64 KiByte Umfang lassen sich damit erstellen.

Schlüsselwörter

Beide Compiler unterstützen „benannte“ Speicherblöcke und -zeiger, die schließlich von verschiedenen Assemblerbefehlen angesprochen werden. Man behalte stets im Hinterkopf, dass es beim 8051 die Adresse 0 gibt. Für den Linker sogar , die Bitadressen.

Schlüsselwörter
Keil-CSDCCErklärungKompatibilitätsheader
code__codeROM- bzw. Flash-Speicher. Für Programmkode und Konstanten: (Tabellen, USB-Deskriptoren und Strings). Wird ausschließlich indirekt mit movc a,@a+dptr angesprochen. Von Vorteil ist die leichtere Adressierung von Strukturkomponenten mit a, wird aber (zumindest) von SDCC verschmäht. Objekte und Zeiger sind automatisch const. #define code __code
data__data Je nach Speichermodell wie idata (256 Byte indirekt adressierbarer RAM) oder xdata (64 KiByte „externer“ RAM). Für Variablen aller Art. Beim SDCC ist __data nicht ganz dasselbe wie __idata! Sondern (beim Speichermodell small) nur die direkt adressierbaren Variablen von 0x30 bis 0x7F, das sind 80 Bytes. Achtung! Zeiger ohne Typspezifikation sind generische Zeiger, wobei ein drittes Byte den Speicherbereich kodiert! Die Verwendung solcher Zeiger bläht den Kode auf! Datenstrukturen ohne Typspezifikation landen in data. Man sollte immer eine geeignete Typspezifikation verwenden. #define data __data
idata__idata Der indirekt adressierbare Speicher von 0x80 bis 0xFF, 128 Byte. Der Zugriff erfolgt ausschließlich indirekt mov r/m,@r[0:1]; mov @r[0:1],r/m. Wer echten idata hat, platziert den Stapelzeiger am besten da hinein, hinter allen anderen idata-Variablen. Denn der Stapel wächst beim 8051 „nach oben“. Belässt man den Stapelzeiger bei seinem Reset-Wert 0x07, überschreibt der Stapel die wertvollen, direkt und indirekt adressierbaren Bank-Register ab Adresse 0x08. #define idata __idata
xdata__xdata Der „externe“ RAM, häufig eingebaut, bietet bis zu 64 KiByte Daten. Früher war er wie der Programmspeicher über den Busanschluss angebunden. Kann nur mit movx @dptr,a bzw. movx a,@dptr angesprochen werden. Bei einigen Controllern ist der Datenspeicher „von-neumannisiert“, und Kode und Daten können/müssen sich den RAM teilen. Dann und nur dann ist ein Typecast zwischen xdata und code folgenlos. Und der Lesezugriff auf Strukturkomponenten ist mit movc a,@a+dptr günstiger. Beim Ablegen von (konstanten) Objekten ist jedoch ein großer Unterschied anzumerken: xdata-Objekte müssen vom Startupkode vom Flash (meistens hinter dem Kode) in den XRAM geschaufelt werden oder können gar nicht vorinitialisiert werden. #define xdata __xdata
pdata__pdata Eine Seite von 256 Byte im „externen“ RAM, Vorgabe 0..0xFF. Wird etwas günstiger mit movx @r[1:0],a bzw. movx a,@r[1:0] angesprochen. Damit stehen 2 Datenzeiger zur Verfügung, die 8 Bit umfassen. Früher wurde der externe RAM durch Laden von PortC page-adressiert, heutzutage tut das ein SFR, beim EZUSB MPAGE. Da beim FX2 viele Register im xdata-Bereich ab 0xE600 liegen, ist es zweckmäßig, diese Register als pdata zu deklarieren. Nicht vergessen, in main() am Anfang MPAGE = 0xE6; hinzuschreiben! pdata-Zeiger können in xdata-Zeiger überführt werden, indem MPAGE als High-Teil hinzugefügt wird. #define pdata __pdata
bdatakeine Entsprechung Bit-adressierbare Daten im Bereich 0x20 - 0x2F (16 Byte). Diese sind direkt, indirekt und bitweise-direkt ansprechbar und nach den Registern der wertvollste Datenbereich. Beim SDCC muss man sich mit __at()-Zuweisungen behelfen. Dass dieser Speicherbereich nicht nebenher von automatischen Zuweisungen überschrieben wird, darum muss man sich mit Linkerschaltern kümmern: -Wl -bBSEG=0x21. Muss aufgelöst werden:
__data unsigned char __at(0x20) bits;
  __bit __at(0x00) bit0;
  /* usw. */
  __bit __at(0x07) bit7;
bit__bit Losgelöste bit-adressierbare Daten im Bereich 0x20 - 0x2F (16 Byte = 128 Bit) Wird vom Compiler / Linker auf die (noch) freien Bits verteilt. Als Funktionsargumente und Rückgabetypen ??? Als lokale Variablen werden diese, wenn nicht rekursiv, wie static behandelt. #define bit __bit
sfr__at() (Nur) Direkt adressierbare Special-Function-Register im Bereich 0x80..0xFF. Können nur gemeinsam mit = bzw. __at() verwendet werden. Defaultwertzuweisung nicht möglich.
#ifdef SDCC
# define SFRBYTE(name,addr) \
 volatile data BYTE __at(addr) name
#else
# define SFRBYTE(name,addr) \
 sfr name = addr
#endif
sbit__sbit __at() Bits im Bereich der Special-Function-Register definieren. Können nur gemeinsam mit = bzw. __at() verwendet werden. Defaultwertzuweisung nicht möglich.
#ifdef SDCC
# define SFRBIT(name,addr) \
 __sbit __at(addr) name
#else
# define SFRBIT(name,addr) \
 sbit name = addr
#endif

Automatische Variablen

Keiner der 8051-Compiler legt die automatischen Variablen standardmäßig auf den Stack! Das heißt, sie sind effektiv static und die Funktionen sind nicht reentrant. Während Keil-C eine Liste hält, welche Funktionen sich wie gegenseitig aufrufen und bei entsprechender Eignung automatische Variablen „übereinander“ legt, tut der SDCC alle nebeneinander, sodass der Platz bei __data sehr schnell knapp wird. Beim SDCC wird nur der erste Parameter in (DPH:)DPL übergeben, alles andere über solche static-Speicherplätze. Man tut also gut, die Länge der Parameterliste klein zu halten und weitere Parameter über globale Variablen, wie beim Uralt-BASIC, zu übergeben. Ein guter Kandidat dafür ist (DPH2:)DPL2 (Zweiter Datapointer) der 8052-Architekturen sowie die beiden Autopointer beim CY7C68013A. Bei umfangreichen Datenstrukturen innerhalb der Funktion legt man diese mittels idata oder xdata „zu Fuß“ in entsprechende Speicherbereiche.

Inline-Assembler

Inline-Assembler (?)
Keil-CSDCC
#pragma ASM
djnz r7,$
#pragma ENDASM
__asm
djnz r7,$
__endasm

Externer Assembler

Die Art der Parameterübergabe unterscheidet sich stark zwischen Keil-C und SDCC. Leider.

Übergabe an nicht-rekursive Funktionen
ParameterKeil-CSDCCErklärung
1.8,16bit: R6:R7
32bit: R4:R5:R6:R7
*: R3:R2:R1
A:B:DPH:DPLJedes SFR v.r.n.l. je nach Bitbreite
2.8,16bit: R4:R5 (wenn verfügbar)
*: R3:R2:R1
In funktionsspezifischen, direkt adressierbaren Speicherzellen, Segment UDATA?
3.8,16bit: R2:R3 (wenn verfügbar)
*: R3:R2:R1
weitereIn direkt adressierbaren Speicherzellen
Returnwert8-32bit:R4:R5:R6:R7
*: R3:R2:R1
A:B:DPH:DPL

Das bedeutet, dass es für SDCC zweckmäßig ist, Zeigerargumente als ersten Parameter anzuordnen, dann ist dieser sogleich in DPTR. Weiterhin sind Funktionen mit mehr als 1 Parameter per se nicht rekursiv verwendbar, sonst muss umständlicher über den Stack gegangen werden.

Structs

IMHO können im aktuellen SDCC structs kopiert werden. Besser ist es jedoch, mit spezialisierten memcpy()-Routinen (von code nach idata, von xdata nach xdata, von idata nach xdata usw.) das Problem ohne generische Zeiger zu lösen.