volatile
.
© Harry Broeders.
Deze pagina is bestemd voor studenten van de Haagse Hogeschool - Academie voor Technology, Innovation & Society Delft.
In het voorbeeldprogramma bij opdracht 1 wordt
het C keyword
volatile
gebruikt. 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 rules of 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 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;
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 Atmel Studio eenvoudig instellen via Project -> Properties en dan het tabblad Toolchain. Selecteer vervolgens in de lijst onder het onderdeel AVR/GNU C compiler het item Optimization:
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 = 29999int
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
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 ATmega32 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(void)
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;
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;
int main(void) {
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;
}
In de include file avr/io.h wordt, als de ATmega32 is geslecteerd in het
project, de file avr/iom32.h geïnclude. In deze file wordt (bijvoorbeeld)
PORTB
via een aantal macro's als volgt gedefinieerd:
#define PORTB (*(volatile uint8_t *)(0x38))
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* 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.
In de include file avr/io.h wordt, als de ATmega32 is geslecteerd in het
project, de file avr/iom32.h geïnclude. In deze file wordt (bijvoorbeeld)
PINA
via een aantal macro's als volgt gedefinieerd:
#define PINA (*(volatile uint8_t *)(0x39))
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 les 3 van MICPRG 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 worden diverse
acties uitgevoerd (niet weergegeven in het onderstaande programma) en wordt
de globale variabel ready
op 1
gezet. In het
hoofdprogramma wordt telkens als de variabele ready
de waarde
1
heeft gekregen de waarde van een secondenteller
(sec
) op het LCD afgedrukt en ready
weer op
0
gezet. Snap je waarom de auteur van het programma dit schrijven
naar de LCD in main
doet en niet in de ISR? Het schrijven naar
de LCD duurt erg lang (een aantal ms) en als dit in de ISR wordt gedaan dan
kun je al die tijd geen andere interrupts verwerken. Dit kan een probleem
zijn (of kan in de toekomst een probleem worden) als dit programma nog andere
interrupts gebruikt (of in de toekomst andere interrupts gaat gebruiken).
#include <avr/io.h> #include <stdint.h> #include <stdio.h> #include <avr/interrupt.h> #include "lcd.h"
volatile uint8_t ready = 0;
ISR(
TIMER1_OVF_vect) {
/* deze ISR wordt 1x per seconde aangeroepen */ //...ready = 1;
//...}
int main(void)
{
int sec = 0; char buffer[17]; // initialiseren LCD: lcd_init(); lcd_cursor(false, false); /* cursor uit */ //initialiseren Timercounter 1: //... sei(); /* interups aanzetten */ while (1) { if (ready == 1) { sec = sec + 1; lcd_cls(); snprintf(buffer, sizeof buffer, "Seconden =%6d", sec); lcd_puts(buffer); ready = 0; } }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 ready
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. -O2
) wordt gebruikt dan blijft de
interrupt correct werken maar wordt de waarde van de secondenteller
niet meer op het LCD afgedrukt.
De optimaliserende compiler "denkt" nu dat als de variabele
ready
in de while
lus
niet de waarde 1
heeft dat deze variabele dan niet meer
gecontrolleerd hoeft te worden omdat deze variabele in de
while
lus niet op 1
gezet
kan worden. 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 ready
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.