Guten Tag!
Ich schaue mir gerade an, wie die millis()-Funktion bei Arduino
implementiert ist. Diese liefert die Anzahl der ms seit dem Start des
uC's zurück. In
1
/arduino-0017hardware/cores/arduino/wiring.c
findet sich dessen Implementierung:
1
volatileunsignedlongtimer0_overflow_count=0;
2
volatileunsignedlongtimer0_millis=0;
3
staticunsignedchartimer0_fract=0;
4
5
SIGNAL(TIMER0_OVF_vect)
6
{
7
// copy these to local variables so they can be stored in registers
8
// (volatile variables must be read from memory on every access)
9
unsignedlongm=timer0_millis;
10
unsignedcharf=timer0_fract;
11
12
m+=MILLIS_INC;
13
f+=FRACT_INC;
14
if(f>=FRACT_MAX){
15
f-=FRACT_MAX;
16
m+=1;
17
}
18
19
timer0_fract=f;
20
timer0_millis=m;
21
timer0_overflow_count++;
22
}
23
24
25
unsignedlongmillis()
26
{
27
unsignedlongm;
28
uint8_toldSREG=SREG;
29
30
// disable interrupts while we read timer0_millis or we might get an
31
// inconsistent value (e.g. in the middle of a write to timer0_millis)
32
cli();
33
m=timer0_millis;
34
SREG=oldSREG;
35
36
returnm;
37
}
Prinzipiell wenig überraschendes, Timer0 läuft beim Arduino durch und
setzt u.a. den Wert für m bzw. timer0_millis hoch. Das würde meine
Recherche ja befriedigen, allerdings wundere ich mich warum die Jungs
die volatiles nochmal explizit zwischenspeichern:
1
// copy these to local variables so they can be stored in registers
2
// (volatile variables must be read from memory on every access)
3
unsignedlongm=timer0_millis;
4
unsignedcharf=timer0_fract;
Meinem Wissen nach, weist volatile den Compiler ja an, die Variable
jedes mal ausm SRAM zu holen.
In meinem C-Buch ( Goto Reihe, Addison-Wesley) steht nur :
"[...] volatile ist ein Hinweis an den Compiler, bei jedem Zugriff auf
die Variable die zugehörige Speicherstelle auszulesen, selbst wenn die
Variable bereits in einem schnellen internen Register steht."
Ist meine Interpretation falsch oder sind die Jungs von Arduino/Wiring
nur (über)vorsichtig?
Vielen Dank!
Robert F. schrieb:
> Meinem Wissen nach, weist volatile den Compiler ja an, die Variable> jedes mal ausm SRAM zu holen.
genau das ist ja das Problem, das ist langsam. Wenn du sie ein eine
lokale Variabel speicherst dann kann der Compieler sie in einem Register
speichern. Damit geht der Zugriff schneller.
ganz klar um im Interrupt Rechenzeit zu sparen. Nach der Zuweisung sind
sie im Register und muss nicht immer mit Adressbefehlen erst herangeholt
werden.
Das volatile ist ja nur wegen dem Anwendungsprogramm drin damit das
nicht mit alten daten arbeitet.
Also wenn man sie mehrfach in einer Methode .. ähh Funktion verwendet,
sollte man sie Zwischenspeichern, klar.
Aber in den o.g. Schnipseln erfolgt der Zugriff ja nur 1x ... Also kein
Performanzgewinn...
Robert F. schrieb:
> Also kein> Performanzgewinn...
Aber ein "Atomizitätsgewinn". Es könnte ja sein, dass wenn du millis()
ausliest, gerade mittendrin der Timer-Interrupt kommt und dir den Wert
verändert, den du schon zur Hälfte ausgelesen hast.
Robert F. schrieb:
> Also wenn man sie mehrfach in einer Methode .. ähh Funktion verwendet,> sollte man sie Zwischenspeichern, klar.
Das kann man so pauschal auch wieder nicht sagen.
Das bringt nur hier etwas, weil es sich um eine ISR handelt. D.h. wir
wissen an dieser Stelle, dass sich die Variablen sowieso nicht verändern
können! Wir sind ja gerade in der Funktion, die die Veränderung
durchführt.
> Aber in den o.g. Schnipseln erfolgt der Zugriff ja nur 1x ... Also kein> Performanzgewinn...
Schau nochmal genau
Wobei: Der Performance Gewinn ist an dieser Stelle sicherlich nicht
signifikant. Aber es geht ja ums Prinzip.
Deine Überlegungen sind nicht ganz richtig.
statt:
1
m+=MILLIS_INC;
2
f+=FRACT_INC;
3
if(f>=FRACT_MAX){
4
f-=FRACT_MAX;
5
m+=1;
6
}
könnten die ja auch
1
timer0_millis+=MILLIS_INC;
2
timer0_fract+=FRACT_INC;
3
if(timer0_fract>=FRACT_MAX){
4
timer0_fract-=FRACT_MAX;
5
timer0_millis+=1;
6
}
schreiben, richtig? NEIN!
Falls du Assembler kannst, dann schau dir mal die unterschiedlichen
Assemblerlistings an.
Ansonsten:
volatile besagt, dass die Variable IMMER aus dem SRAM gelesen werden
muss. Demzufolge muss sie auch augenblicklich wieder dothin zurück
geschrieben werden. D.h. im obigen Beispiel würden die Variablen immer
vom SRAM in ein Register gelesen werden und das Ergebnis würde sofort
wieder ins SRAM geschrieben, auch wenn die Variable später nochmal
gebraucht wird (dann wird sie halt wieder ausdem SRAM gelesen). Das
erzeugt einen Overhead, der (zumindest in der ISR) nicht benötigt wird.
Das Zwischenspeichern in die lokale Variable (=laden in ein Register)
hält die volatile Variable im Register. Alle weiteren Berechnungen
finden mit diesem Register statt.
Es wird bei beiden Codes das gleiche rauskommen, aber mit
Registerzwischenspeicher gehts deutliche schneller.
EDIT: uuups war zu langsam
Peter schrieb:
> Robert F. schrieb:>> ber in den o.g. Schnipseln erfolgt der Zugriff ja nur 1x ... Also kein>> Performanzgewinn...>> ich zähle 3 bzw 4 zugriffe.
das "1x" bezog sich auf millis() ...
in der ISR zähle ich ( wenn man sich die Zwischenspeicherung weg denkt)
einen Zugriff beim incrementieren, einen bedingten, also 1 oder 2.
in der Summe komme ich dann auf 2-3 ... habe ich mich verzählt/ etwas
übersehen?
spacedog schrieb:
> Aber ein "Atomizitätsgewinn". Es könnte ja sein, dass wenn du millis()> ausliest, gerade mittendrin der Timer-Interrupt kommt und dir den Wert> verändert, den du schon zur Hälfte ausgelesen hast.
naja dafür ist ja das SREG-save&resore + cli() Konstrukt da (hier im
Bsp.)
1
// (volatile variables must be read from memory on every access)
^ Dieser Kommentar ist dann aber stark irreführend...
Karl heinz Buchegger schrieb:
>> Wobei: Der Performance Gewinn ist an dieser Stelle sicherlich nicht> signifikant. Aber es geht ja ums Prinzip.
... also - an dieser Stelle / in diesem Bsp. - Übervorsicht?
Daniel V. schrieb:
> EDIT: uuups war zu langsam
^ hehe im Forum braucht man auch volatile, die Threads werden während
der Ansicht extern verändert ;)
Robert F. schrieb:
> arl heinz Buchegger schrieb:>>>> Wobei: Der Performance Gewinn ist an dieser Stelle sicherlich nicht>> signifikant. Aber es geht ja ums Prinzip.>> ... also - an dieser Stelle / in diesem Bsp. - Übervorsicht?
teste doch mal beides und schau die den asm code an, aber ich würde fast
sagen das diese Version doppelt so schnell ist. Auch wenn es in der
summe nur nur im µs geht.
Peter schrieb:
> teste doch mal beides und schau die den asm code an, aber ich würde fast> sagen das diese Version doppelt so schnell ist. Auch wenn es in der> summe nur nur im µs geht.
Die o.g. Implementierung (Zwischenspeichern) *zusammen mit dem
Kommentar*
1
// (volatile variables must be read from memory on every access)
hat mich stutzig gemacht.
Daher wollte ich nachfragen, ob ich einen Denkfehler bzgl. volatile habe
("Meinem Wissen nach, weist volatile den Compiler ja an, die Variable
jedes mal ausm SRAM zu holen.")
Also ich wollte jetzt keine Performanzdiskursion oder über ein
Memory/Speed Trade Off Diskutieren...
Peter schrieb:
> Robert F. schrieb:>> arl heinz Buchegger schrieb:>>>>>> Wobei: Der Performance Gewinn ist an dieser Stelle sicherlich nicht>>> signifikant. Aber es geht ja ums Prinzip.>>>> ... also - an dieser Stelle / in diesem Bsp. - Übervorsicht?>> teste doch mal beides und schau die den asm code an, aber ich würde fast> sagen das diese Version doppelt so schnell ist.
Nicht ganz.
timer0_fract ist sowieso schon mal nicht volatile und von daher ist ein
Laden in lokale Variablen Luxus (den der Compiler höchst wahrscheinlich
sowieso auf eigene Faust breeinigt)
Und timer0_millis wird nur dann ein zweites mal angerührt, wenn die
Fraction überläuft.
Nun kennen wir natürlich nicht die exakten Werte für die ISR-Frequenz,
FRACT_MAX und FRACT_INT, aber doppelt so schnell ist die optimierte
Variante sicherlich nicht.
Karl heinz Buchegger schrieb:
> Nun kennen wir natürlich nicht die exakten Werte für die ISR-Frequenz,> FRACT_MAX und FRACT_INT, aber doppelt so schnell ist sie sicherlich> nicht.
stimmt, ich hatte die lese und schreibaktionen der volatile variabel
unterschlagen, sie müssen ja mindests einmal gemacht werden.
Da sehen ich übringens als ein grossen Nachteil von volatil an. Gibt es
nicht eine Möglichkeit eine Variable normal zu deklarieren und dann an
der stelle wo es entscheident ist dem Compiler mitzuteilen das er bitte
den aktuellen stand auslesen soll.
while ( read_volatil( var ) == 1 )
{
mach was..
}
Robert F. schrieb:
> Peter schrieb:>> teste doch mal beides und schau die den asm code an, aber ich würde fast>> sagen das diese Version doppelt so schnell ist. Auch wenn es in der>> summe nur nur im µs geht.>> Die o.g. Implementierung (Zwischenspeichern) *zusammen mit dem> Kommentar*>
1
> // (volatile variables must be read from memory on every access)
2
>
> hat mich stutzig gemacht.>> Daher wollte ich nachfragen, ob ich einen Denkfehler bzgl. volatile habe> ("Meinem Wissen nach, weist volatile den Compiler ja an, die Variable> jedes mal ausm SRAM zu holen.")>>
Das meinte ich mit "Deine Überlegungen sind nicht ganz richtig.".
Vielleicht wäre "...nicht ganz vollständig." besser?
In der ISR möchte man natürlich vermeiden, dass die volatile Variable
ständig vom SRAM gelesen werden muss, weil die ISR nicht unterbrochen
wird.
Deswegen der Hinweis seitens den Typen.
Daniel V. schrieb:
[...]
> Das meinte ich mit "Deine Überlegungen sind nicht ganz richtig.".> Vielleicht wäre "...nicht ganz vollständig." besser?> In der ISR möchte man natürlich vermeiden, dass die volatile Variable> ständig vom SRAM gelesen werden muss, weil die ISR nicht unterbrochen> wird.> Deswegen der Hinweis seitens den Typen.
Ah ok, vielen Dank.
Beim scharfen Hinsehen auf millis() viel mir auf, dass man um eine
Zwischenspeicherung nicht herrumkommt:
* timer0_millis ist long -> beim Zugriff kann einem ein ISR dazwischen
pfuschen
* => SREG save&restore nötig
* return timer0_millis geht hier nicht, da sonst SREG = oldSREG; fehlen
würde
-> also wird es in millis() nicht aus Übervorsicht, oder aus Performanz
sondern, wie spacedog bereits bemerkte wgn der atomität bzw. weil es
nicht anders geht ;)
Peter schrieb:
> Da sehen ich übringens als ein grossen Nachteil von volatil an. Gibt es> nicht eine Möglichkeit eine Variable normal zu deklarieren und dann an> der stelle wo es entscheident ist dem Compiler mitzuteilen das er bitte> den aktuellen stand auslesen soll.>> while ( read_volatil( var ) == 1 )> {> mach was..> }
Hmm. Interessant Idee bzw. Fragestellung
1
while(*((volatileuint8_t*)&var)==1)
müsste das eigentlich machen. Also einfach das volatile dazucasten :-)
Der Nachteil ist natürlich, dass hier der eigentliche Datentyp von var
vorkommt und man sich so Probleme bei einer Datentypänderung einhandelt.
Aber das Problem hat man im Grunde mit jedem Cast der etwas mit Pointern
zu tun hat.
Ohne Gewähr:
1
while(*((volatiletypeof(var)*)&var)==1)
und dann den ganzen Klimbim in einem Makro verstecken.
Robert F. schrieb:
> Daniel V. schrieb:> [...]>> Das meinte ich mit "Deine Überlegungen sind nicht ganz richtig.".>> Vielleicht wäre "...nicht ganz vollständig." besser?>> In der ISR möchte man natürlich vermeiden, dass die volatile Variable>> ständig vom SRAM gelesen werden muss, weil die ISR nicht unterbrochen>> wird.>> Deswegen der Hinweis seitens den Typen.>> Ah ok, vielen Dank.>>> Beim scharfen Hinsehen auf millis() viel mir auf, dass man um eine> Zwischenspeicherung nicht herrumkommt:>> * timer0_millis ist long -> beim Zugriff kann einem ein ISR dazwischen> pfuschen> * => SREG save&restore nötig> * return timer0_millis geht hier nicht, da sonst SREG = oldSREG; fehlen> würde>> -> also wird es in millis() nicht aus Übervorsicht, oder aus Performanz> sondern, wie spacedog bereits bemerkte wgn der atomität bzw. weil es> nicht anders geht ;)
Hmm... ich verstehe deinen Gedankengang nicht ganz. Meiner Meinung nach
wirfst du verschiedene Dinge in einen Topf.
Zur SREG-Verwendung sei auf
http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Programmieren_mit_Interrupts
verwiesen. Das hat nix mit ULONG oder volatile zu tun.
Die Verwendung von millis() ist auch ohne 'volatile' notwendig, weil
eben ein Interrupt dazwischen funken kann.
Volatile wird nur verwendet, um sicher zu stellen, dass der Compiler
keine Zugriffsoptimierungen bei dieser Variablen macht. Im Grunde
genommen braucht mandas bei jeder Variablen, die in einem Interrupt
geändert werden kann. Das kann der Compilernämlich nicht vorhersehen.
Daniel V. schrieb:
[..]
>> Hmm... ich verstehe deinen Gedankengang nicht ganz. Meiner Meinung nach> wirfst du verschiedene Dinge in einen Topf.> Zur SREG-Verwendung sei auf> http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Programmieren_mit_Interrupts> verwiesen. Das hat nix mit ULONG oder volatile zu tun.> [...]
Ich habe mich in der Tat unklar ausgedrückt, sry.
Klar hat das Statusregister nichts mit long oder volatile zutun ;)
Vieleicht sollte ich das laut-denken im Forum unterlassen - mein letztes
Post bitte überlesen ;)
Zwischenvariablen im Interrupt machen den Code auch nicht gerade besser
lesbar.
Es gibt aber einen Trick, ohne volatile auszukommen.
Man macht den Zugriff auf Main-Level mit folgenden Macros:
1
#define vu8(x) (*(volatile uint8_t*)&(x))
2
#define vs8(x) (*(volatile int8_t*)&(x))
3
#define vu16(x) (*(volatile uint16_t*)&(x))
4
#define vs16(x) (*(volatile int16_t*)&(x))
5
#define vu32(x) (*(volatile uint32_t*)&(x))
6
#define vs32(x) (*(volatile int32_t*)&(x))
Der Trick dabei, eine Variable läßt sich nicht auf volatile casten, ein
Pointer aber schon.
Das hängt wohl damit zusammen, daß das volatile erst nachträglich in C
reingestrickt wurde, also kein richtiger Typ ist.
Die C-Erfinder hatten es nicht so mit Hardware naher Programmierung.
Bei mehrbytigen Zugriffen muß man außerdem noch atomar kapseln.
Bei Verwendung des ATOMIC-Macros könnte man auch das volatile ganz
weglassen, der Zugriff wird nicht wegoptimiert.
Allerdings kann dann Code von außerhalb mit in das ATOMIC-Macro
hineingezogen werden. Bei einer Division sind dann also Interrupts für
lange Zeit gesperrt.
Der Vernachlässigung der volatile und atomic Problematik ist bestimmt
für 90% der Bluescreens und Abstürze verantwortlich.
Peter
Peter Dannegger schrieb:
> Der Trick dabei, eine Variable läßt sich nicht auf volatile casten, ein> Pointer aber schon.
Danke soetwas hatte ich schon lange mal gesucht.
Peter Dannegger schrieb:
> Der Trick dabei, eine Variable läßt sich nicht auf volatile casten, ein> Pointer aber schon.> Das hängt wohl damit zusammen, daß das volatile erst nachträglich in C> reingestrickt wurde, also kein richtiger Typ ist.
Eher damit, daß ein Cast niemals irgendwas am Original verändert,
sondern immer eine (temporäre) Kopie erzeugt, die dann den Zieltyp hat.
Das bringt einem aber natürlich nichts, wenn man auf die
Original-Variable zugreifen will, also nimmt man einen Zeiger darauf und
castet den. Mit volatile hat das nichts zu tun, es gilt vielmehr für
jeden Cast, wo man ein Objekt irgendeines Typs als anderen Typ
interpretieren will. Da man nicht auf eine Kopie, sondern auf das
Original-Objekt zugreifen will, muß man seine Adresse nehmen und die
casten.
Übrigens:
Ich finde es aber eher besser, die Variable generell volatile zu machen
und dann mit der lokalen Variablen zu arbeiten, wenn mehrere Zugriffe
nötig sind.
Das finde ich klarer, als irgendwo vereinzelte volatile-Zugriffe
reinzufummeln. Und ich bin auch sicher, daß mein Zugriff nur einmal
stattfindet. Bei deiner Variante bin ich darauf angewiesen, daß der
Compiler die anderen Zugriffe wegoptimiert.