MICB2 opdracht 1.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de THRijswijk groep EH2.

Knight Rider.

Z'n 20 jaar geleden was de TV serie Knight Rider populair. In deze serie werd de hoofdrol gespeeld door een sprekende auto KITT genaamd. Deze auto was aan de voorkant voorzien van een rijtje rode LED's (lampjes) die in een bepaald patroon aan en uitgeschakeld werden. Deze rij LED's moesten een scanner voorstellen waarmee KITT de omgeving verkende. 

In deze practicumopgave gaan we zelf een rijtje LED's aansturen met een bepaald patroon met behulp van de HC11 microcontroller.

Voorbeeldprogramma.

THRSim11 versie 5.22c installeren (indien nodig).

Het voorbeeld programma ophalen.

Het voorbeeld programma compileren, linken en omzetten in het juiste formaat.

Het voorbeeld programma simuleren.

Het voorbeeld programma uitvoeren op de EVM.

Extra informatie.

In het programma wordt het C/C++ keyword volatile gebruikt. In de C++ standaard staat:

volatile is a hint to the implementation to avoid aggressive optimization involving the object because the value of the object might be changed by means undetectable by an implementation. See intro.execution for detailed semantics. In general, the semantics of volatile are intended to be the same in C++ as they are in C.

Het woord volatile betekent "vluchtig" en wordt dus gebruikt om aan te geven dat de variabele ook buiten het programma om veranderd kan worden. Dit keyword zorgt ervoor dat de compiler geen optimalisaties toepast die ervan uitgaan dat de waarde van een variabele nog hetzelfde is als die variabele door het programma zelf niet veranderd is.

Voorbeeld van het gebruik van volatile: Output.

In het voorbeeld programma wordt een pointer p gebruikt die staat te wijzen naar een output poort:

byte* p=(byte*)0x1004;

Het type byte is in het begin van het programma als volgt gedefinieerd:

typedef unsigned char byte;

In de for lus van main() wordt telkens een nieuwe waarde naar deze output poort geschreven:

*p=c1|c2;

Een sterk optimaliserende compiler kan nu "denken": "De waarde die ik via de pointer wegschrijf wordt nooit meer gelezen dus ik kan dat schrijven ook wel achterwege laten." Dat is natuurlijk niet de bedoeling, wij willen de juiste LEDjes wel zien branden! De waarde waar de pointer naar wijst moet dus als vluchtig (volatile) worden gequalificeerd:

volatile byte* p=(byte*)0x1004;

Bij het compileren met gcc kun je een programma op verschillende manieren optimaliseren, zie hier, Bijvoorbeeld:

De versie van de gcc compiler die wij nu gebruiken (3.3.5) zal als je het keyword volatile bij de pointer p vergeet, bij het gebruik van de optie -Os of -O3 de LEDjes toch gewoon aansturen. Maar dat kan bij een nieuwe versie van gcc anders zijn!

Voorbeeld van het gebruik van volatile: Input.

Je kunt ook een pointer naar een input poort definiëren:

byte* p=(byte*)0x....;

De waarde van deze input poort kun je dan als volgt inlezen:

waarde=*p;

Als dit meerdere malen achter elkaar gebeurt (bijvoorbeeld in een lus) kan een sterk optimaliserende compiler "denken": "De waarde die ik via de pointer heb ingelezen heb ik zo meteen weer nodig. Ik kan deze waarde dus bewaren (bijvoorbeeld in een register) en hoef deze waarde dan de tweede keer niet opnieuw uit het geheugen te lezen." Dat is natuurlijk niet de bedoeling, wij willen de nieuwe waarde van de inputpoort inlezen! De waarde waar de pointer naar wijst moet dus als vluchtig (volatile) worden gequalificeerd:

volatile byte* p=(byte*)0x....;

De versie van de gcc compiler die wij nu gebruiken (3.3.5) zal als je het keyword volatile bij de pointer p vergeet, bij het gebruik van de optie -Os de input poort toch gewoon opnieuw inlezen. Bij het gebruik van de opties -O3 wordt het opnieuw inlezen van de input poort wel weggeoptimaliseerd!.

Voorbeeld van het gebruik van volatile: Tijdvertraging.

In het voorbeeldprogramma wordt een "lege" for lus gebruikt om een tijdvertraging te realiseren.

void wait() {
    word i;
    for (i=0; i<10000; ++i)
        /*empty*/;
}

Een sterk optimaliserende compiler kan nu "denken": "De waarde van i wordt toch niet gebruikt de hele for lus kan dus weggeoptimaliseerd worden (en vervolgens kan de hele functie wait() weggeoptimaliseerd worden)." Dat is natuurlijk niet de bedoeling, wij willen dat de functie wait() een tijdvertraging opleverd! De variabele i moet dus als vluchtig (volatile) worden gequalificeerd:

volatile word i;

De versie van de gcc compiler die wij nu gebruiken (3.3.5) zal als je het keyword volatile bij de variabele i vergeet, bij gebruik van de optie -Os of -O3 de lus niet helemaal weggeoptimaliseren. De variabele i wordt bij het gebruik van de optie -Os of -O3 echter telkens met 625 verhoogd in plaats van met 1. Vraag me niet waarom! De lus wordt dan dus maar 8x doorlopen (in plaats van 10000x).

Programmeren in C++.

Het voorbeeldprogramma is geschreven in C. Je kunt echter ook C++ programma's compileren en uitvoeren op de 68HC11. Het voorbeeld programma in C++ kun je vinden in de file: opdr1.cpp. Gcc kijkt dus naar de extensie van het bestand om te bepalen op de C of de C++ compiler aangeroepen moet worden.

Opdracht 1a.

Timing.

In de vorige opdracht werd de tijd tussen het verschuiven van het patroontje bepaald door de volgende wachtlus.

void wait() {
    volatile word i;
    for (i=0; i<10000; ++i)
        /*empty*/;
}

Het is moeilijk om op deze manier een exacte tijdvertraging te realiseren. Als je een bepaalde tijd wilt wachten kun je beter de real-time clock die in de 68HC11 is ingebouwd gebruiken (Zie paragraaf 7.7 in het boek van Miller.).

Opdracht 1b.

Pas het programma uit opgave 1a zodanig aan dat het verschuiven van het patroontje op de LED's wordt aangestuurd met behulp van een Real-Time Clock. Het patroontje moet telkens na 500 ms opschuiven. Maak gebruik van polling (telkens wachten tot RTIF 1 wordt).

Let op! Als je dit programma een andere naam wilt geven, bijvoorbeeld opdr1b.c dan moet je een nieuw directory aanmaken en ook het linkerscript evm.ld en de makefile makefile in deze directory kopiëren. In de makefile moet je vervolgens de regel:

OBJECTS    = opdr1.o

veranderen in:

OBJECTS    = opdr1b.o

Opdracht 1c.

In opdracht 1b heb je gebruik gemaakt van polling, dit wordt "busy waiting" genoemd. Je kunt op deze manier als je staat te wachten niets anders doen. Pas het programma zodanig aan dat het verschuiven van het patroontje op de LED's wordt aangestuurd met behulp van een Real-Time Clock interrupt. Het patroontje moet weer telkens na 500 ms opschuiven. Maak daarbij gebruik van de in de les behandelde voorbeeldprogramma.

typedef unsigned char byte;
volatile byte* tmsk2=(byte*)0x1024;
volatile byte* tflg2=(byte*)0x1025;

void rtti_isr(void) __attribute__((interrupt));

void rtti_isr(void) {
    /* ... */
    *tflg2=0x40;
}

int main() {
    *tmsk2|=0x40;
    /* ... */
    return 0;
}

typedef void (*isr)(void);

#define E (isr)0xffff 

#ifdef __cplusplus
extern "C" void _start(void);
#else
extern void _start(void);
#endif

isr vectors[32] __attribute__ ((section (".vector"))) = {
    E, E, E, E, E, E, E, E,
    E, E, E, E, E, E, E, E,
    E, E, E, E, E, E, E, E,
    rtti_isr, E, E, E, E, E, E, _start
};

Extra uitleg.

Als de 68HC11 een interrupt krijgt dan wordt eerst de huidige instructie afgemaakt en daarna worden alle registers op de stack geplaatst. Vervolgens wordt naar de ISR (Interrupt Service Routine) gesprongen. Aan het einde van de ISR moet een RTI (ReTurn from Interrupt) instructie worden gebruikt zodat alle registers weer van de stack worden verwijderd.

De 68HC11 "weet" van tevoren niet wanneer de interrupt zal optreden. Op de een of andere manier moet de programmeur opgeven welke ISR uitgevoerd moet worden bij het optreden van een bepaalde interrupt. Omdat de programmeur ook niet van tevoren weet op welke plaats het programma onderbroken zal worden door een ISR kan de ISR niet worden aangeroepen met een "normale" functieaanroep.

In een zogenaamde interrupt vector tabel geeft de programmeur voor elke gebruikte interrupt het adres van de ISR op (de plaats waar het adres van de ISR moet worden ingevuld wordt de interrupt vector genoemd). Bij de 68HC11 bevindt deze interrupt vector tabel zich in het geheugen op de adressen $FFC0 t/m $FFFF. In de documentatie van de 68HC11 kun je vinden welk adres voor een bepaalde interrupt wordt gebruikt.

Een ISR kan in C als volgt gedeclareerd worden:

void rtti_isr(void) __attribute__((interrupt));

Een ISR is dus een "soort" functie die aangeroepen wordt als de betreffende interrupt optreed. Omdat je niet weet wanneer de interrupt zal optreden kun je geen argumenten meegeven en ook geen return type gebruiken. De __attribute__((interrupt)) is nodig om de compiler te vertellen dat de functie moet eindigen met een RTI (ReTurn from Interrupt) instructie in plaats van met de RTS (ReTurn from Subroutine) instructie waarmee een gewone functie eindigt.

In de interrupt vector tabel moet het startadres van de ISR worden ingevuld. In C is dat een pointer naar de ISR functie. Om dit te kunnen doen is het type isr gedefinieerd als een pointer naar een functie waaraan je niets meegeeft en die zelf ook niets teruggeeft:

typedef void (*isr)(void);

De interrupt vector tabel is in C niets anders dan een array gevuld met deze pointers:

isr vectors[32]

De __attribute__ ((section (".vector"))) is nodig om de array "te plaatsen" op de adressen $FFC0 t/m $FFFF.

Deze array wordt meteen geïnitialiseerd met de juiste waarden:

isr vectors[32] __attribute__ ((section (".vector"))) = {
    E, E, E, E, E, E, E, E,
    E, E, E, E, E, E, E, E,
    E, E, E, E, E, E, E, E,
    rtti_isr, E, E, E, E, E, E, _start
};

De naam van een functie (rtii_isr) kan in C worden toegekend aan een pointer naar een functie. Je zou ook &rtti_isr mogen gebruiken om aan te geven dat je het adres van de functie in de tabel invult. In de documentatie van de 68HC11 kun je vinden dat de RTTI (Real Time Timer Interrupt) de interrupt vector op adres $FFF0 en $FFF1 gebruikt. Omdat de 68HC11 een 16 bits adresbus heeft en dus 16 bits adressen gebruikt is een pointer (die een adres bevat) in C code voor de 68HC11 ook 16 bits. Elke plaats in de array vectors neemt dus 16 bits (= 2 bytes) in gebruik. De waarde E is gedefinieerd als:

#define E (isr)0xffff 

Om aan te kunnen geven dat een interrupt vector leeg is.

Het is de bedoeling dat je zelf voor elke interrupt van de 68HC11 kunt bepalen op welke plaats in de array je de naam van de betreffende ISR moet invullen!

Extra informatie.

Door de reset vector te definiëren willen we het programma bij een harware reset op het juiste adres laten (her)starten. We zouden het programma bij een reset naar de functie main() kunnen laten "springen". Dit is echter onjuist! Voordat in een C of C++ programma de functie main() wordt aangeroepen moeten er al eerst allerlei actie plaatsvinden. De stackpointer moet van de juiste waarde worden voorzien, globale variabelen moeten worden geïnitialiseerd en in C++ moeten de constructors van alle globale objecten worden aangeroepen. De code die moet worden uitgevoerd voordat main() kan beginnen wordt de opstartcode genoemd. Deze opstartcode word automatisch door de GNU linker meegelinkt met de rest van het programma. Aan het begin van de opstartcode wordt de identifier (het label) _start gedefinieerd. De opstart code staat in de file C:\Program Files\THRSim11\gcc\lib\gcc-lib\m6811-elf\3.3.5-m68hc1x-20050129\crt1.o. De inhoud van deze file kun je bekijken door in het command window van THRSim11 het volgende commando in te typen:

!objdump -x -S "C:\Program Files\THRSim11\gcc\lib\gcc-lib\m6811-elf\3.3.5-m68hc1x-20050129\crt1.o"

Als we het label _start in de vector tabel zetten:

typedef void (*isr)(void);

#define E (isr)0xffff 

extern void _start(void);

isr vectors[32] __attribute__ ((section (".vector"))) = {
    E, E, E, E, E, E, E, E,
    E, E, E, E, E, E, E, E,
    E, E, E, E, E, E, E, E,
    rtti_isr, E, E, E, E, E, E, _start
};

Dan werkt dit goed onder C.

Als we dezelfde code als C++ code compileren krijgen we een linker fout:

"C:\Program Files\THRSim11"\gcc\bin\m6811-elf-gcc.exe -g -c -m68hc11 -Wall  opdr1c.cpp
"C:\Program Files\THRSim11"\gcc\bin\m6811-elf-gcc.exe -m68hc11 -T evm.ld opdr1c.o
opdr1c.o: undefined reference to `_start()'
collect2: ld returned 1 exit status

Dit komt doordat in C++ name-mangling wordt gebruikt om function overloading (meerdere functies met dezelfde naam) mogelijk te maken. De functie: void f(void) krijgt door name-mangling bijvoorbeeld de naam _Z1fv. De overloaded functie void _start(int) bijvoorbeeld krijgt de naam _Z1fi. Op deze manier kan de linker deze 2 overloaded functies uit elkaar houden. Je kunt de mangled names van een in THRSim11 geladen C++ programma zichtbaar maken door in het command window van THRSim11 het volgende commando in te typen:

!nm a.out

Je kunt de orginele namen zichtbaar maken met het commando:

!nm -C a.out

We kunnen dit probleem in C++ oplossen door de functie _start als volgt te definiëren:

extern "C" void _start(void);

Deze syntax zorgt ervoor dat de name-mangling wordt uitgeschakeld en is speciaal bedoeld om C functies vanuit C++ aan te roepen. Maar deze syntax kun je weer niet in C gebruiken.

De oplossing is conditionele compilatie te gebruiken:

#ifdef __cplusplus
extern "C" void _start(void);
#else
extern void _start(void);
#endif