C mixen met 68HC11 assembler.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de THRijswijk.

Inleiding.

Sommige bewerkingen kunnen niet vanuit C gedaan worden. Zo kun je bijvoorbeeld in C niet controleren of na een rekenkundige bewerking een overflow is opgetreden. Dat kan wel in assemblercode (door de V flag in het CCR register van de 68HC11 te controleren). Soms is het ook mogelijk om in assembler snellere code te schrijven dan in C. Er zijn dus verscheidene redenen om assembler code op te nemen in een C programma. Dit kan op verschillende manieren:

Het asm keyword.

Het gebruik van het asm keyword is gcc specifiek. Het asm keyword is eenvoudig te gebruiken als we slechts een simpele 68HC11 assembler instructie willen aanroepen.

Voorbeeld: In het onderstaande programma worden alle maskeerbare interrupts geblokkeerd tijdens het vergelijken van de globale variabelen x en y. Het volledige programma kun je hier downloaden. Het blokkeren van alle maskeerbare interrupts gebeurt met behulp van de 68HC11 sei instructie die het I bit in het CCR register set. De interrupts worden weer vrijgegeven met de 68HC11 cli instructie.

int main() {
   *tmsk2|=0x40;
   *portb=0x00;
   while (1) {
      asm ("sei");
      if (x!=y)
         ++*portb;
      asm ("cli");
   }
   return 0;
}

Het gebruik van asm wordt al snel ingewikkeld en onoverzichtelijk. Zie GNU gcc documentatie voor meer informatie. Daarom is het beter om de assembler code in een aparte .s file op te nemen. Deze file kan dan met de GNU as assembler worden omgezet in machine code. Deze machine code kan met C code worden gelinkt met behulp van de GNU ld linker.

Gebruik van de GNU as assembler.

De GNU as assembler is een zogenaamde relocatable assembler. De in THRSim11 ingebouwde assembler (THRAss11, waar de E studenten in H1 mee gewerkt hebben) is een zogenaamde absolute assembler. Bij een absolute assembler geeft de programmeur in de assemblercode aan waar de betreffende code in het geheugen van de 68HC11 moet worden geladen (met behulp van de ORG directive). Bij een relocatable assembler bepaald de linker (in dit geval de GNU ld linker) waar het programma in het geheugen van de 68HC11 moet worden geladen (met behulp van een linker script). De GNU as assembler programmeur geeft alleen op of de code in ROM (met de directive .text) of in RAM (met de directive .data) moet worden geplaatst.

De GNU as assembler directives zijn anders dan de in THRAss11 gebruikte directives.

Hieronder is de C functie add_c.c gegeven die twee 16 bits getallen optelt en controleert of er een overflow is opgetreden. Als er een overflow optreedt zal het tekenbit van het resultaat niet correct zijn. In dit geval geeft de functie de waarde 1 terug. Als er geen overflow is opgetreden geeft de functie de waarde 0 terug.

short add(short a, short b, short* c) {
   *c=a+b;
   if ((a>0 && b>0 && *c<0) || (a<0 && b<0 && *c>0))
      return 1;
   return 0;
}

Deze functie kan vanuit het C programma test_overflow.c als volgt worden aangeroepen:

int main() {
   short add(short a, short b, short* c);
   // test add
   volatile short i=30000;
   volatile short j=10000;
   volatile short overflow=0;
   short resultaat;
   overflow=add(i, j, &resultaat);
   while (1);
   return 0;
}

De variabelen i, j en overflow zijn volatile gedefinieerd omdat anders de hele functieaanroep wordt weggeoptimaliseerd. Dit komt omdat het resultaat van de functie in het programma niet wordt gebruikt. De met gcc gecompileerde programma's test_overflow.c en add_c.c worden door de GNU ld linker gecombineerd tot één programma. Zoals beschreven in de makefile.

Het testen of een overflow is opgetreden kan echter in assembler veel eenvoudiger geïmplementeerd worden (door de V flag in het CCR register te testen).

De functie add kan als volgt in GNU as assembler geschreven worden (add_s.s):

	.global	add
	.text
add:	pshx
	tsx
	addd	4,x
	bvs	vset
	ldx	6,x
	std	0,x
	ldd	#0
	bra	return
vset:	ldd	#1
return:	pulx
	rts
	.end

De directive .global add zorgt ervoor dat het label add wordt doorgegeven aan de linker. Hierdoor kan de functie add vanuit een andere sourcefile aangeroepen worden. De directive  .text geeft aan dat alle code die volgt in het "text" segment van het uiteindelijke programma moet worden geplaatst. Het "text" segment wordt gebruikt voor het opslaan van programmacode en constanten. De inhoud van dit segment wordt uiteindelijk in ROM (Read Only Memory) geplaatst. De directive .end markeert het einde van de code. Zoals je ziet moeten labels als ze gedefinieerd worden zijn afgesloten met een :.

De GNU gcc compiler geeft de eerste parameter van een functie via het D register van de 68HC11 door. De overige parameters worden via de stack doorgegeven. De return waarde van een functie moet in het D register van de 68HC11 worden geplaatst.

Deze functie kan vanuit het C programma test_overflow.c nog steeds als volgt worden aangeroepen:

int main() {
   short add(short a, short b, short* c);
   // test add
   volatile short i=30000;
   volatile short j=10000;
   volatile short overflow=0;
   short resultaat;
   overflow=add(i, j, &resultaat);
   while (1);
   return 0;
}

Het met gcc gecompileerde programma test_overflow.c en het met as geassembleerde programma add_s.s worden door de GNU ld linker gecombineerd tot één programma. Zoals beschreven in de makefile.

Als de functie add vanuit C wordt aangeroepen zoals hierboven gegeven dan ziet de stack eruit zoals hiernaast is weergegeven. De waarde van de variabele i staat in het D register van de 68HC11. Bij binnenkomst in de functie add wordt eerst de waarde van het X register op de stack gezet en daarna wordt X gelijk gemaakt aan SP+1 (door middel van de TSX instructie) zodat X naar de oude waarde van X wijst. De parameter j kunnen we bereiken via 4,x en het adres van resultaat kunnen we bereiken via 6,x.

Hieronder worden de C versie en de assember versie van de functie add met elkaar vergeleken. Beide programma's zijn vertaald met de opties -O3 -fomit-frame-pointer -mshort.

Taal Size Speed
C

78

119

assembler

20

37

Size in bytes, Speed in clock-cycles

"Time write once" I/O Register.

De 68HC11 heeft een aantal I/O register bits die alleen tijdens de eerste 64 clock cycles na reset eenmaal veranderd kunnen worden. Dit zijn:

In het onderstaande programma probleem.c wordt geprobeerd om de PR1 en PR0 bits van het TMSK2 register op 1 te zetten.

int main() {
   /* probeer "time write once bits" te beschrijven */
   /* bijvoorbeeld TMSK2 bits PR1 en PR0 */
   typedef unsigned char byte;
   volatile byte* tmsk2=(byte*)0x1024;
   *tmsk2=0x03; /* prescale factor = 16 */

   while(1) {
      /* doe wat leuks */
   }
   return 0;
}

Als dit programma in de simulator wordt geladen en met single step (F7) wordt uitgevoerd zien we een probleem aankomen!

Zie jij het probleem ook?

Inderdaad, het initialiseren van de PR1 en PR0 bits van het TMSK2 register wordt niet binnen de eerste 64 clock cycles uitgevoerd en deze bits veranderen dus niet.

We kunnen dit probleem in dit geval oplossen door de gcc compiler te vertellen dat het programma zo snel mogelijk moet zijn. Dit gebeurt door in de makefile de regel:

GCC_OPTIONS =

te vervangen door:

GCC_OPTIONS = -O3 -fomit-frame-pointer

Het initialiseren van de PR1 en PR0 bits van het TMSK2 register wordt nu wel binnen de eerste 64 clock cycles uitgevoerd.

Maar deze oplossing werkt niet als bijvoorbeeld een globale variabele wordt gebruikt. Dit komt doordat deze globale variabele al voordat main wordt aangeroepen geïnitialiseerd wordt:

char globaal[]="Hallo";
int main() {
   /* probeer "time write once bits" te beschrijven */
   /* bijvoorbeeld TMSK2 bits PR1 en PR0 */
   typedef unsigned char byte;
   volatile byte* tmsk2=(byte*)0x1024;
   *tmsk2=0x03; /* prescale factor = 16 */

   while(1) {
      /* doe wat leuks */
   }
   return 0;
}

Het initialiseren van de PR1 en PR0 bits van het TMSK2 register wordt in dit geval niet binnen de eerste 64 clock cycles uitgevoerd en deze bits veranderen dus niet. Ook niet als in de makefile de regel:

GCC_OPTIONS = -O3 -fomit-frame-pointer

gebruikt wordt.

Om er zeker van te zijn dat de PR1 en PR0 bits van het TMSK2 register binnen de eerste 64 clock cycles geïnitialiseerd worden moet de C code met assembler code gecombineerd worden.

In het assembler programma init.s kan een section .install1 opgenomen worden. Alle code in deze section wordt meteen in het begin van het uiteindelijke programma geplaatst (na het initialiseren van de stackpointer, maar voor het initialiseren van globale variabelen).

	.sect	.install1
;	"time write once bits" kunnen hier beschreven worden
;	bijvoorbeeld TMSK2 bits PR1 en PR0
	.equ	tmsk2, 0x1024
	ldaa	#0x03
	staa	tmsk2	;prescale factor = 16

In het C programma oplossing.c kunnen we er nu vanuit gaan dat de PR1 en PR0 bits van het TMSK2 register al voordat main() aangeroepen wordt genitialiseerd zijn.

int main() {
   while(1) {
      /* doe wat leuks */
   }
   return 0;
}

In de makefile moeten beide files worden opgegeven:

OBJECTS    = init.o oplossing.o