© Harry Broeders.
Deze pagina is bestemd voor studenten van de THRijswijk.
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:
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.
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
.
Size in bytes, Speed in clock-cycles |
![]() |
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:
PR1
en PR0
in het TMSK2
register.
Hiermee wordt de prescaler van de free running timer ingesteld.
CR1
en CR0
in het OPTION
register.
Hiermee wordt de COP timer timeout ingesteld.
IRQE
in het OPTION
register. Hiermee kan de
IRQ interrupt ingang flankgevoelig worden gemaakt.
DLY
in het OPTION
register. Hiermee kan de
delay die na een STOP
instructie optreed worden uitgezet.
INIT
register. Hiermee kun je het start adres
van de internal RAM en internal I/O registers aanpassen.
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