volatile
. © Harry Broeders.
In de opdracht voor week 1 en 2 voor KOF01 wordt gevraagd om een wachtlus te
implementeren. Daarbij komt het C keyword volatile
goed van pas. In de C standaard
(ISO/IEC 9899:1999 p.108) 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 rulesof the abstract machine.
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.
volatile
: Tijdvertraging. In het onderstaande voorbeeldprogramma wordt een "lege" for
lus
gebruikt om een tijdvertraging te realiseren.
void wait(void) {
int i;
for (i = 0; i < 30000; ++i) {
/*empty*/ }
}
Een 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;
Bij het compileren met gcc kun je een programma op verschillende manieren optimaliseren, zie hier, Bijvoorbeeld:
-Os
optimaliseer voor minimaal geheugengebruik. -O3
optimaliseer voor maximale snelheid. Deze opties kun je in AVR Studio eenvoudig instellen via de menu optie Project, Properties...:
De versie van de gcc compiler die wij nu gebruiken (Atmel Studio 7) zal als
je het keyword volatile
bij de variabele i
vergeet, bij gebruik van de optie -O1
, -O2
,
-O3
of -Os
de functie wait
helemaal
weggeoptimaliseren.
volatile
: Output.Als we geen gebruik maken van de include file avr/io.h
dan
kunnen we de I/O registers gebruiken met behulp van pointers. We kunnen dan het
PORTB register bereiken met de pointer portb
die staat te wijzen naar geheugen adres
0x25
:
uint8_t* portb = (uint8_t*)0x25;
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 0x25
. 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
ATmega328P zijn via memory adressering te bereiken. Het register PORTB kan
bereikt worden via adres 0x25. Doordat de pointer portb
naar het
memory adres van het register PORTB wijst kan dit register via deze pointer
worden beschreven en gelezen. Voor de constante 0x25
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.
We kunnen nu als volgt een nieuwe waarde naar deze output poort schrijven:
*portb = 0x20;
Een 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 dat de
waarde 0x20
in het PORTB register wordt geschreven. De waarde waar
de pointer naar wijst moet dus als vluchtig (volatile
) worden gequalificeerd:
volatile uint8_t* portb = (uint8_t*)0x25;
In de include file avr/io.h
wordt, als de ATmega328P is
geslecteerd in het project, de file avr/iom328p.h
geïnclude. In
deze file wordt (bijvoorbeeld) PORTB
via een aantal macro's als
volgt gedefinieerd:
#define PORTB (*(volatile uint8_t *)(0x25))
Je ziet dat hier ook het keyword volatile
is gebruikt om aan te geven dat de
compiler deze code niet mag optimaliseren.
volatile
: Input. Je kunt ook een pointer naar een input poort definiëren:
uint8_t* pinb = (uint8_t*)0x23;
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*)0x23;
In de include file avr/io.h
wordt, als de ATmega328P is
geslecteerd in het project, de file avr/iom328p.h
geïnclude. In
deze file wordt (bijvoorbeeld) PINB
via een aantal macro's als
volgt gedefinieerd:
#define PINB (*(volatile uint8_t *)(0x23))
Je ziet dat hier ook het keyword volatile
is gebruikt om aan te geven dat de
compiler deze code niet mag optimaliseren.
volatile
: globale variabelen bij
interrupts. Om dit deel van deze pagina te kunnen begrijpen moet je weten wat interrupts zijn en hoe je die in C gebruikt. Dit wordt in de lessen van KOF01 behandeld.
Als voorbeeld kijken we naar een programma dat een waarde vanuit de ISR
(Interrupt Service Routine) moet doorgeven aan het hoofdprogramma
(main
). De code waar het om gaat is hieronder weergegeven. Er
staat natuurlijk meer code in de ISR en in main
maar die is in dit
verband niet relevant. In dit voorbeeldprogramma wordt timer/counter1 zo
ingesteld dat elke seconde een interrupt optreed. In de ISR wordt een globale
secondeteller (sec
) bijgewerkt. In het hoofdprogramma wordt deze
globale variabele gebruikt.
#include <avr/io.h> #include <stdint.h> #include <avr/interrupt.h>volatile uint32_t sec = 0;
ISR(
TIMER1_OVF_vect) {
/* deze ISR wordt 1x per seconde aangeroepen */ //...sec = sec + 1;
//...}
int main(void)
{
//initialiseren Timercounter 1: //... sei(); /* interups aanzetten */ while (1) { //... min = sec / 60; //... }return 0;
}
Je ziet dat er gebruik wordt gemaakt van een globale variabele en dat doen
we liever niet (waarom ook alweer?). Maar dit is de "uitzondering" waarbij we
echt niet om een globale variabele heen kunnen. Het is namelijk niet
mogelijk om de waarde van sec
via een returnwaarde of een
call-by-reference parameter door te geven omdat we de ISR namelijk niet
zelf aanroepen (want een ISR wordt door de hardware aangeroepen).
Waarom is het nodig om de globale variabele volatile
te kwalificeren? Als in het
bovenstaande programma het keyword volatile
wordt weggelaten en een optimalisatie
optie (b.v. -O1
) wordt gebruikt dan blijft de interrupt correct
werken maar wordt de waarde de variabele min
niet meer
bijgewerkt in main
.
De optimaliserende compiler "denkt" nu dat de variabele sec
niet elke keer opnieuw ingelezen hoeft te worden omdat deze variabele in de
while
lus niet aangepast wordt. Dat deze
while
lus onderbroken kan worden door de
ISR (die door de hardware van de timer wordt aangeroepen) "weet" de
optimaliserende compiler niet.
Je hebt het keyword volatile
dus
nodig om aan te geven dat de compiler de waarde van de variabele
sec
in de while
lus steeds
opnieuw moet inlezen omdat de waarde (zonder dat de compiler dat weet) kan
veranderen. Of anders geformuleerd: omdat de variabele "vluchtig" is.