Het gebruik van het C keyword volatile.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de Haagse Hogeschool - TH Rijswijk/Academie voor Engineering.

In het voorbeeldprogramma bij opdracht 1 wordt het C keyword volatile gebruikt. In de C standaard staat:

An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules.

In een verklarende voetnoot staat:

A volatile declaration may be used to describe an object corresponding to a memory-mapped input/output port or an object accessed by an asynchronously interrupting function. Actions on objects so declared shall not be ‘‘optimized out’’ by an implementation.

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 portb gebruikt die staat te wijzen naar een output poort:

uint8_t* portb=(uint8_t*)0x38;

Het type uint8_t kan gebruikt worden om een unsigned 8 bits variabele mee te definiëren. Een uint8_t* is dus een pointer naar een unsigned 8 bits variabele. 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 0x38. 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 ATmega16 zijn via memory adressering te bereiken. Het portb register kan bereikt worden via adres 0x38. Doordat de pointer portb naar het memory adres van het portb register wijst kan dit register via deze pointer worden beschreven en gelezen. Voor de constante 0x38 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.

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

*portb=~(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 uint8_t* portb=(uint8_t*)0x38;

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

Deze opties kun je in AVR Studio eenvoudig instellen via de menu optie Project, Configuraton Options:

De versie van de gcc compiler die wij nu gebruiken zal als je in het voorbeeld programma uit opdracht 1 het keyword volatile bij de pointer portb vergeet, bij het gebruik van een optimalisatie optie de LEDjes toch gewoon aansturen. Maar dat kan bij een nieuwe versie van gcc of een ander programma wel anders zijn!

Bij het onderstaande programma worden de ledjes niet correct aangestuurd als het keyword volatile bij de pointer portb wordt vergeten:

typedef unsigned char uint8_t;

typedef unsigned char uint8_t;

int main() {
    volatile uint8_t* ddrb=(uint8_t*)0x37;
    volatile uint8_t* portb=(uint8_t*)0x38;
    volatile uint8_t* pina=(uint8_t*)0x39;

    *ddrb=0xFF;
    *portb=0xFE;
    while (1) {
        while (*pina&0x01);  /* wacht tot SW0 ingedrukt wordt */
        --*portb;
        while (~*pina&0x01); /* wacht tot SW0 losgelaten wordt */
    }
    return 0;
}

Voorbeeld van het gebruik van volatile: Input.

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

uint8_t* pina=(uint8_t*)0x39;

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

waarde=*pina;

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 uint8_t* pina=(uint8_t*)0x39;

De versie van de gcc compiler die wij nu gebruiken zal, als je het keyword volatile bij de pointer pina in het bovenstaande programma vergeet, bij het gebruik van een optimalisatie optie het opnieuw inlezen van de input poort wegoptimaliseren!. Het programma werkt dan niet meer correct.

Voorbeeld van het gebruik van volatile: Tijdvertraging.

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

void wait(void) {
    int i;
    for (i=0; i<30000; ++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 int i;

De versie van de gcc compiler die wij nu gebruiken zal als je het keyword volatile bij de variabele i vergeet, bij gebruik van de optie -O2, -O3 of -Os de lus niet helemaal weggeoptimaliseren. De variabele i wordt bij het gebruik van deze optimalisatie opties echter telkens met 50 verlaagd in plaats van met 1. De waarde wordt verlaagd in plaats van verhoogd omdat de compiler de variabele i initialiseert met 29999 en dan telkens verlaagt tot i negatief wordt. Vraag me niet waarom de lus na optimalisatie steeds met 50 verlaagd wordt! De lus wordt dan dus maar 600x doorlopen (in plaats van 30000x). Het patroon op de LEDs verschuift zo snel dat het niet meer zichtbaar is (alle LEDs lijken continue zwak te branden).

Dit is de assembler listing die de compiler aanmaakt als de optie -Os gebruikt wordt:

void wait(void) {
  8e:	8f e2       	ldi	r24, 0x2F
  90:	95 e7       	ldi	r25, 0x75	; r25:r24 = 0x752F = 29999
	int i;
	for (i=0; i<30000; ++i) 
  92:	c2 97       	sbiw	r24, 0x32	; 50
  94:	97 ff       	sbrs	r25, 7
  96:	fd cf       	rjmp	.-6
  98:	08 95       	ret