MICPRG opdracht 1.

Harry Broeders (met aanpassingen van Ben Kuiper)

Deze pagina is bestemd voor studenten van de Haagse Hogeschool - Academie voor Technology, Innovation & Society Delft groep EQ1.2.

Knight Rider.

Z'n 30 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 ATmega32 microcontroller. We beginnen eerst met een voorbeeldprogramma en daarna...

Voorbeeldprogramma.

Aanmaken nieuw project

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

Het voorbeeld programma uitvoeren op de ATmega32 op het STK500 bord m.b.v. de JTAGICE mkII.


De volgende paragraaf beschrijft hoe je het programma thuis kan testen en uitvoeren. Dit moet je dan ook thuis doen! Tijdens het practicum kun je hier verder gaan.

Het voorbeeld programma thuis simuleren.

Stap voor stap verklaring van de werking van het programma.

De I/O registers van de ATmega32 zijn 8 bits breed. In een C programma dat deze I/O registers gaat lezen en/of schrijven moeten dus vaak 8 bits brede variabelen worden gebruikt. In C89 (de eerste versie van ANSI C) is het enige geschikte datatype het type char. Omdat we 8 bits getallen zonder teken in deze variabelen willen opslaan maken we gebruik van het datatype unsigned char. In de eerste regel van het programma wordt met behulp van een typedef de identifier uint8_t gedefinieerd als een alias voor het type unsigned char.

typedef unsigned char uint8_t;

Daarna wordt de functie wait gedefinieerd.

void wait(void) {

    volatile int i;

    for (i = 0; i < 30000; ++i)

        /*empty*/;

}

Deze functie is als tijdvertraging bedoeld. De variabele i wordt geïnitialiseerd op 0 en telkens met 1 verhoogd tot de waarde 30000 is bereikt. Dit duurt ongeveer 0,25 seconde op een ATmega32 als een klokfrequentie van 3.686 MHz gebruikt wordt. Dit is proefondervindelijk vastgesteld. Het gebruik van het keyword volatile wordt hier uitgelegd.

Vervolgens wordt de hoofdfunctie main gedefinieerd.

int main(void) {

    void wait(void);

    uint8_t c1, c2, i;

    volatile uint8_t* ddrb = (uint8_t*)0x37;

    volatile uint8_t* portb = (uint8_t*)0x38;



    *ddrb = 0xFF;

    while (1) {

        c1 = 0x80;

        c2 = 0x01;

        for (i = 0; i < 4; i++) {

            wait();

            *portb = ~(c1 | c2);

            c1 >>= 1;

            c2 <<= 1;

        }

    }

    return 0;

}

De eerste regel in main is het prototype (de declaratie) van de functie wait. Dit is in dit geval niet noodzakelijk omdat de functie wait al gedefinieerd is (boven main). Als de functie wait echter onder de functie main wordt geplaatst dan is de declaratie van wait wel noodzakelijk.

Vervolgens worden de variabelen c1, c2 en i gedefinieerd. Dit zijn alledrie variabelen van het type uint8_t (een alias voor het standaardtype unsigned char). Deze variabelen kunnen dus een 8 bits unsigned waarde bevatten.

Vervolgens worden 2 pointers gedefinieerd.

    volatile uint8_t* ddrb = (uint8_t*)0x37;

    volatile uint8_t* portb = (uint8_t*)0x38;

De variabele ddrb is dus een pointer naar een variabele van het type uint8_t. Een pointer is een variabele die staat te wijzen naar een plaats in het geheugen (normaal gesproken een andere variabele). In dit geval wordt de pointer geïnitialiseerd met de waarde 0x37. Als een getal in een C programma met 0x begint, dan betekent dit dat de constante in het hexadecimale talstelsel is opgegeven. De I/O registers van de ATmega32 zijn via memory adressering te bereiken. Het ddrb register (data direction register port b) kan bereikt worden via adres 0x37. Doordat de pointer ddrb naar het memory adres van het ddrb register wijst kan dit register via deze pointer worden beschreven en gelezen. Voor de constante 0x37 staat een zogenaamde cast operatie (uint8_t*) dit zorgt ervoor dat de compiler begrijpt dat het echt de bedoeling is om een uint8_t* variabele te vullen met een integer constante. Als de cast operatie wordt weggelaten geeft de compiler de volgende warning:

../opdr1b.c:12: warning: initialization makes pointer from integer without a cast

Het gebruik van het keyword volatile wordt hier uitgelegd.

Op de volgende regel wordt de pointer ddrb gebruikt om alle pinnen van poort B als outputs te configureren. Dit wordt gedaan door 8 enen (0xFF) te schrijven in het ddrb register. De notatie *ddrb betekent: "waar de pointer ddrb naar wijst" (= geheugenlokatie 0x37 = data direction register port b).

    *ddrb = 0xFF;

De rest van het programma bestaat uit een oneindige lus:

    while (1) {

        ...

    }

De voorwaarde van de while is namelijk altijd waar (1). Veel programma's voor microcontrollers hebben z'n oneindige lus omdat het programma moet blijven draaien zolang de microcontroller aan staat.

In de lus worden de variabelen c1 en c2 geïnitialiseerd met een waarde.

        c1 = 0x80;

        c2 = 0x01;

c1 wordt geïnitialiseerd met de binaire waarde 10000000 en c2 met de binaire waarde 00000001. Omdat we in standaard C constanten niet in het binaire talstelsel kunnen opgeven wordt de hexadecimale notatie gebruikt.

Voor iemand die alles wil weten: Als je gebruik maakt van de GNU C compiler (die gebruikt wordt door AVR Studio) dan is het wel mogelijk om binaire constanten op te geven met de prefix 0b. Dus bijvoorbeeld:

        c1 = 0b10000000;

De compiler geeft dan wel de volgende warning:

../opdr1b.c:17:12: warning: binary constants are a GCC extension

Het is echter beter om alleen standaard C te gebruiken voor het geval we later een andere C compiler willen gaan gebruiken.

Na het initialiseren van de variabelen c1 en c2 volgt een for lus die 4x wordt uitgevoerd voor i = 0 tot 4.

        for (i = 0; i < 4; i++) {

            wait();

            *portb = ~(c1 | c2);

            c1 >>= 1;

            c2 <<= 1;

        }

In de for lus wordt eerst de functie wait aangeroepen om even te wachten. Daarna wordt de bitwise or bepaald van de variabelen c1 en c2. Deze waarde wordt geïnverteerd naar het portb register weggeschreven. Meer informatie over bitwise bewerkingen in C kun je hier vinden.

Vervolgens wordt de inhoud van de variabele c1 één plaatsje naar rechts geschoven met de schuifinstructie c1 >>= 1. De inhoud van c2 wordt één plaatsje naar links geschoven met de schuifinstructie c2 <<= 1. Meer informatie over schuifbewerkingen in C kun je hier vinden.

Voor iemand die alles wil weten: Na de while lus volgt nog de instructie:

    return 0;

Deze instructie wordt echter nooit uitgevoerd omdat de while lus nooit eindigt. Als we de instructie weglaten geeft de compiler echter de volgende warning:

../opdr1b.c:27: warning: control reaches end of non-void function

Misschien denk je nu: Waarom vertel je de compiler dan niet dat de functie main niets teruggeeft door het returntype void te gebruiken in plaats van int. Volgens de C89 standaard moet main echter een int returntype hebben. De compiler geeft als je void gebruikt de volgende warning:

../opdr1b.c:9: warning: return type of 'main' is not `int'

Als we het programma zonder warnings willen laten compileren is de return 0 dus noodzakelijk.

Alternatieve versie.

In het bovenstaande programma hebben we geschreven naar de I/O registers van de ATmega32 door gebruik te maken van een pointer variabele.

    volatile uint8_t* ddrb = (uint8_t*)0x37;

    *ddrb = 0xFF;

Het is echter ook mogelijk om naar de I/O registers te schrijven door het gebruik van een soort "tijdelijke" pointer.

    (*(volatile uint8_t*)0x37) = 0xFF;

Er wordt in dit geval een "tijdelijke" pointer aangemaakt naar geheugenlokatie 0x37. Vervolgens wordt de waarde 0xFF weggeschreven naar de geheugenlokatie waar deze pointer naar wijst. Als we het aanmaken van de tijdelijke pointer definiëren als de identifier DDRB met behulp van een #define dan is het net of het ddrb register een gewone C variabele is.

#define DDRB (*(volatile uint8_t*)0x37)



    DDRB = 0xFF;

Als we gebruik maken van deze tijdelijke pointers ziet het programma er als volgt uit:

typedef unsigned char uint8_t;



#define DDRB (*(volatile uint8_t*)0x37)

#define PORTB (*(volatile uint8_t*)0x38)



void wait(void) {

    volatile int i;

    for (i = 0; i < 30000; ++i)

        /*empty*/;

}



int main(void) {

    void wait(void);

    uint8_t c1, c2, i;



    DDRB = 0xFF;

    while (1) {

        c1 = 0x80;

        c2 = 0x01;

        for (i = 0; i < 4; i++) {

            wait();

            PORTB = ~(c1 | c2);

            c1 >>= 1;

            c2 <<= 1;

        }

    }

    return 0;

}

Dit programma kun je hier downloaden: opdr1b.c

Maak gebruik van include files.

In de include file avr/io.h zijn onder andere #define's opgenomen zodat alle registers van de gebruikte AVR microcontroller als C variabelen gebruikt kunnen worden. In de file avr/io.h wordt gekeken naar het ingestelde type AVR microcontroller om te bepalen welke registernamen aan welke adressen moeten worden gekoppeld. Het is dus van groot belang om bij het aanmaken van het project het juiste AVR type te selecteren.

In de include file stdint.h zijn onder andere de zogenaamde fixed width (vaste breedte) integer datatypes int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t en uint64_t gedefinieerd. Het is dan niet meer nodig om deze types zelf met typedef te definiëren. Deze file is in 1999 in de ANSI/ISO C standaard (C99) opgenomen.

In de include file util/delay.h zijn onder andere 2 functies gedefinieerd waarmee wachtlussen geprogrammeerd kunnen worden.

void _delay_ms(double ms); /* ms is maximaal 262.14 / F_CPU * 1000000 */

void _delay_us(double us); /* us is maximaal 768 / F_CPU * 1000000 */

De maximale waarde van de parameter ms en us is afhankelijk van de klokfrequentie van de microcontroller (F_CPU). De klokfrequentie van de ATmega32 op het practicum is ingesteld op 3.686 MHz.

F_CPU=
1000000
F_CPU=
3686000
F_CPU=
8000000
F_CPU=
16000000
ms max

262.14

71.11

32.76

16.38

us max

768.00

208.35

96.00

48.00

De delay functies zijn afhankelijk van de gebruikte klokfrequentie. Het is dus van groot belang om deze juist in te stellen in het project dat je hebt gemaakt. Dit kun je doen door allereerst Project -> Properties (Alt+F7) en vervolgens het tabblad Toolchain te selecteren. Klik vervolgens op Symbols. Je dient hier een zgn. nieuwe symbol (m.b.v. Add Item) aan te maken die bestaat uit F_CPU=3686000UL.

Voor iemand die alles wil weten: UL staat voor Unsigned Long. Je geeft hier immers een getal op dat mogelijk niet in een integer past en daarnaast is het getal altijd positief.

Er is overigens ook de mogelijkheid om in je code #define F_CPU 3686000UL te zetten en dit doet effectief hetzelfde. Het is wel belangrijk deze regel voor de include te zetten van header-files die deze frequentie gebruiken (zoals util/delay.h).

Als we gebruik maken van bovenstaande include files ziet het programma er als volgt uit:

#include <avr/io.h>

#include <stdint.h>

#include <util/delay.h>



void wait(void) {

    uint8_t i;

    for (i = 0; i < 10; ++i)

        _delay_ms(25);

}



int main(void) {

    void wait(void);

    uint8_t c1, c2, i;



    DDRB = 0xFF;

    while (1) {

        c1 = 0x80;

        c2 = 0x01;

        for (i = 0; i < 4; i++) {

            wait();

            PORTB = ~(c1 | c2);

            c1 >>= 1;

            c2 <<= 1;

        }

    }

    return 0;

}

Dit programma kun je hier downloaden: opdr1c.c

Als je de simulator gebruikt dan kun je ook de klokfrequencie van de simulator instellen. Vreemd genoeg neemt de simulator niet automatisch de bij de project opties ingestelde frequentie over. Je kunt deze simulator opties alleen maar instellen als de simulator gestart en op pauze staat door een break-point of door op het drukken op Break (Ctrl+F5).

Je kunt de ingestelde klokfreqentie zien in de Processor view:

Door op de Frequency te klikken kun je deze ook vervolgens veranderen. Helaas kun je maar 4 decimalen opgeven! Daardoor kun je niet exact de frequentie van 3,6864 MHz invoeren en dit zorgt ervoor dat de stopwatch niet exact meer klopt.

Opdracht.