Hallo,
ich programmiere einen STM32F0 mit CooCox und erhalte ein (für mich)
merkwürdiges Verhalten.
Da meine Anwendung sehr zeitbegrenzt ist und ich auf jede µs achten muss
bin ich gerade dabei meinen Code zu Optimieren.
Das merkwürdige Verhalten zeigt sich bei einer Rechnung:
1
v=((Buffer[0]*K)/Buffer[1]);
Diese errechnet mir eine Spannung aus den Werten meines AD-Wandlers.
Die Datentypen sind uint16_t Buffer[2], uint32_t K=5029, int16_t v;
Was ist das merkwürdige Verhalten: Sobald meine Spannung über 1 Volt
geht, so dauert die Berechnung 0,5µs länger und ich verstehe einfach
nicht warum.
Das gleiche Verhalten zeigt sich, wenn ich den Nenner konstant halte und
nur den Zähler ändere.
Konkret bedeutet das: Spannung<1V Buffer[0]= 1296
Spannung>1V Buffer[0]=1810
Setze ich diese beiden zahlen ein und halte den Nenner konstant so
erhalte ich auch dieses Verhalten. Ich kann es mir einfach nicht
erklären. Ein Überlauf passiert ja nicht, da K als unsigned long int
deklariert wurde.
Vielleicht hat jemand einen Tipp.
Meinen Assembler-Code poste ich noch:
1
137 v=((1810*K)/Buffer[1]); //Berechnete Spannung im Q3.12 Format
Der Cortext M0 hat keine Hardware-Division. Weshalb sie wie hier gezeigt
in der Library-Routine "__udivsi3" stattfindet. Interessant ist also
dessen Code.
Dass die Laufzeit von Divisionen generell von den zu dividierenden
Werten abhängen kann ist nicht neu.
Versuche mal als festen Wert nicht die 1810 sondern die 1812 ;-)
Btw..: DMA1_Channel1_IRQHandler - in einer ISR macht man solche
Berechnungen nicht. Der STM32F0 hat kein HW Divisor - richtig?!
A. K. schrieb:> Dass die Laufzeit von Divisionen generell von den zu dividierenden> Werten abhängen kann ist nicht neu.
Okay, aber wie ist das verhalten zu erklären?
Weil der Compiler, bei bestimmten Werten die Division nicht mit einer
Schiebeoperation ersetzen kann?
Rene K. schrieb:> Versuche mal als festen Wert nicht die 1810 sondern die 1812 ;-)
Die 1810 habe ich nur zu Testzwecken eingesetzt, damit ich zwei Werte
habe die unterschiedliches Verhalten zeigen
Oliver S. schrieb:> Hast du dir denn mal __udivsi3 angeschaut, was das so macht?
Habe es noch nicht gefunden. Muss ich erstmal suchen nach der Funktion
Matthias H. schrieb:> so dauert die Berechnung 0,5µs länger
Das ist aber nun keine Zeit, die ins Gewicht fallen sollte.
Wenn es Dir nicht gelingt, durch etwas geschicktere Programmierung die
Laufzeit zu optimieren, takte den µC einen Tick schneller.
Vielleicht bringt ein anderer Compiler bessere Ergebnisse.
Wenn das Programm kurz ist, könnte eine Demoversion von IAR oder Keil
ausreichen.
Matthias H. schrieb:> Rene K. schrieb:>> Versuche mal als festen Wert nicht die 1810 sondern die 1812 ;-)>> Die 1810 habe ich nur zu Testzwecken eingesetzt, damit ich zwei Werte> habe die unterschiedliches Verhalten zeigen
Ja, dann setze mal die 1812 ein - und schau nach deiner Zeit. Überleg
einfach mal was die "0" in irgendeiner Berechnung macht - und was ein
Compiler aus solch einer Berechnung anstellt.
Rene K. schrieb:> Ja, dann setze mal die 1812 ein - und schau nach deiner Zeit. Überleg> einfach mal was die "0" in irgendeiner Berechnung macht - und was ein> Compiler aus solch einer Berechnung anstellt.
Hat sich trotzdem nichts verändert.
Bei <1296 ist es immernoch 0,5µs kürzer als bei >1810
Und was macht 1816? Kann es sein das Werte über 1296 einfach in einen
anderen Wertebereich fallen und somit die Berechnung halt länger dauert?
Aber wie gesagt - ohne zu wissen was __udivsi3 macht - alles
Rätselraten.
Matthias H. schrieb:> Weil der Compiler, bei bestimmten Werten die Division nicht mit einer> Schiebeoperation ersetzen kann?
Nur Divisionen durch konstante Zweierpotenzen sind durch eine
Schiebeoperation ersetzbar. Ist hier aber keine Zweierpotenz, im
Original nicht einmal eine Konstante, weshalb keine Schiebeoperation
möglich ist.
Wenn man durch eine Konstante dividiert, kann man u.U. mit einer
Multiplikation und etwas Zusatzkram auskommen. Das macht ein Compiler
aber nicht zwangsläufig selber, zumal dabei eine Rundung unter die
Räder kommen kann.
Übrigens ist auch die Laufzeit der ab Cortex M3 vorhandenen
Hardware-Division von den Werten abhängig.
Rene K. schrieb:> ohne zu wissen was __udivsi3 macht - alles> Rätselraten.
Wo kann ich denn udivsi3 finden?
Im internet steht dazu nur
"These functions return the negation of a.
Runtime Function: unsigned int __udivsi3 (unsigned int a, unsigned int
b)"
Wenn ich eine Funktion mit unterschiedlichen Werten aufrufe, dann
erwarte ich idR. auch ein unterschiedliches Ergebnis. Dass zum Ermitteln
des Ergebnisses unterschiedliche Abläufe greifen, erschient mir dabei
als normales Verhalten.
Schreibe (und optimiere hinterher) einfach selber mal so eine
Divisionsroutine, dass wirst du sehen, dass du abhängig vom Bitmuster
andere Strategien und Wege gehen musst.
Matthias H. schrieb:> Weil der Compiler, bei bestimmten Werten die Division nicht mit einer> Schiebeoperation ersetzen kann?
Er kann nur in ganz, ganz seltenen Fällen die Division durch ein simples
Schieben ersetzen. Lass die Division (die ist doch unsigned, oder?) mal
so laufen 1810/510 und mal so 1810/512. Was kommt dabei raus?
Matthias H. schrieb:> Bei <1296 ist es immernoch 0,5µs kürzer als bei >1810
Und was ist daran schlimm?
Du kannst dich niemals drauf verlassen, dass ein Funktionsaufruf in der
Zeit 0 oder wenigstens immer gleich schnell abläuft.
Matthias H. schrieb:> Im internet steht dazu nur> "These functions return the negation of a.
Nö. Darunter gucken, nicht darüber. "These functions return the quotient
of the unsigned division of a and b."
Alle mir bekannten Divisionsroutinen waren als Schleife ausgeführt.
Subsequent wurde da, abhängig davon ob ein Bit gesetzt ist, gearbeitet,
oder ein Teil übersprungen.
Das hat natürlich zur Folge, dass je nach "Bitmuster" die Zeit, die zum
Grübeln benötigt wird, variiert.
Ist alles logisch, wenn man sich mal daran erinnert, wie man ohne
Taschenrechner dividiert. Ham' wir alle mal - anno Tobak - gelernt.
> Okay, aber wie ist das verhalten zu erklären?
Dividiere doch mal handschriftlich auf Papier. Du wirst sehen, daß der
Zeitaufwand auch hier von den konkreten Werten abhängt.
Beim Computer ist der Grund prinzipiell der selbe.
Amateur schrieb:> Alle mir bekannten Divisionsroutinen waren als Schleife ausgeführt.
Da kenne ich noch andere Varianten. Wobei in diesem Fall die Schleife
entrollt ist.
Amateur schrieb:> Alle mir bekannten Divisionsroutinen waren als Schleife ausgeführt.> Subsequent wurde da, abhängig davon ob ein Bit gesetzt ist, gearbeitet,> oder ein Teil übersprungen.> Das hat natürlich zur Folge, dass je nach "Bitmuster" die Zeit, die zum> Grübeln benötigt wird, variiert.>> Ist alles logisch, wenn man sich mal daran erinnert, wie man ohne> Taschenrechner dividiert. Ham' wir alle mal - anno Tobak - gelernt.Stefan U. schrieb:> Dividiere doch mal handschriftlich auf Papier. Du wirst sehen, daß der> Zeitaufwand auch hier von den konkreten Werten abhängt.>> Beim Computer ist der Grund prinzipiell der selbe.
Ja das würde ich ja verstehen.
Was ich aber nicht verstehe, dass es unter einem bestimmten Wert weniger
lang dauert wie über einem bestimmten wert. Eurer theorie nach mpsste es
ja in beiden bereichen(drüber,drunter) unterschiedlich lange dauern...
Welche Genauigkeit brauchst Du?
Ggf. kannst Du die Division durch eine Lookup-Tabelle ersetzen und damit
um ein Vielfaches schneller werden. Natürlich brauchst Du dafür - je
nach Genauigkeit - Einiges an Flash und es sollten nicht mehrere solche
Berechnungen optimiert werden müssen.
Der Faktor k / Buffer[1] würde dabei in eine Tabelle ausgelagert.
Als Berechnung hast Du dann nur noch:
v = Buffer[0] * lookupTable[Buffer[1]];
Viele Grüße, Stefan
Matthias H. schrieb:> Was ich aber nicht verstehe, dass es unter einem bestimmten Wert weniger> lang dauert wie über einem bestimmten wert.
Die oben gezeigte Routine überspringt abhängig von den Werten einige
Iterationen der entrollten Schleife.
Matthias H. schrieb:> Ja das würde ich ja verstehen.> Was ich aber nicht verstehe, dass es unter einem bestimmten Wert weniger> lang dauert wie über einem bestimmten wert. Eurer theorie nach mpsste es> ja in beiden bereichen(drüber,drunter) unterschiedlich lange dauern...
Dividiere mal von Hand 10.000 / 10 und 10.000 / 11 und messe nach, wie
lange Du für jede dieser Divisionen brauchst.
Egal ob Du eine Division per Hand oder per Software ausführst: die Dauer
wird massgeblich von den Werten abhängen.
Viele Grüße, Stefan
PS: Geh doch einfach mal per single step durch die Divisionsroutine
durch. Einmal mit den schnellen und einmal mit den langsamen Werten. Und
verfolge dabei, welchen Weg die diversen bedingten Sprünge darin nehmen.
Stefan K. schrieb:> Der Faktor k / Buffer[1] würde dabei in eine Tabelle ausgelagert.> Als Berechnung hast Du dann nur noch:>> v = Buffer[0] * lookupTable[Buffer[1]];
Stefan, das ist eine super Idee.
Allerdings hängt mein Faktor K von der Kalibrierung des AD-Wandlers zu
Beginn ab und kann sich somit ändern. Eine Berechnung der LookupTable
würde zu lange dauern, da Buffer[1] um 100 variiert...
Nimm als Beispiel die schriftliche Division wie sie in Europa in den
Schulen gelehrt wird.
Dann tritt es auf, dass der Rest eines Zwischenschrittes, kleiner als
der Divisor ist. Man nimmt also eine weitere Stelle hinzu um wieder eine
Zahl zu erhalten, die grösser/gleich dem Divisor ist. Manchmal reicht
das nicht und noch eine Ziffer muss dazu genommen werden. Dieses
vergleichen von Rest und Divisor geht recht schnell und im Kopf, auch
das hinzunehmen von weiteren Ziffern. Es geht aber langsamer als die
eigentliche Division der soweit betrachteten Zahl.
Bei diesem Verfahren hängt die Geschwindigkeit nicht davon ab, wie groß
der Divisor absolut ist oder wie groß er im Vergleich zum Dividend ist.
Klar, soweit?
Das ist nur mal eine prinzipielle Betrachtung. Wie das bei der
Implementierung ist, hängt vom Verfahren ab. Auf dem Rechner gibt es
mehrere Möglichkeiten eine Division effizient zu implementieren.
Nun noch eine Bemerkung zu diesen beiden Sätzen, wenn Du gestattest:
> ...dass es unter einem bestimmten Wert weniger
lang dauert wie über einem bestimmten wert.
> Eurer theorie nach müsste es ja in beiden Bereichen (drüber,drunter)> unterschiedlich lange dauern ...
sagen das selbe aus, widersprechen sich gegenseitig nicht.
Matthias H. schrieb:> Allerdings hängt mein Faktor K von der Kalibrierung des AD-Wandlers zu> Beginn ab und kann sich somit ändern. Eine Berechnung der LookupTable> würde zu lange dauern, da Buffer[1] um 100 variiert...
Du musst die Lookup-Table doch nur einmal nach der Kalibrierung rechnen.
Und wenn Buffer[1] sich nur um den Faktor 100 ändert, dann bleibt die
Lookup-Table im RAM auch überschaubar klein.
Viele Grüße, Stefan
Dezimal Binär
529 1000010001 (3 Einser)
415 0110011111 (7 Einser)
Die Laufzeit der Division hat nur beschränkt etwas mit der Größe der
Zahlen zu tun.
Bei der schrittweisen Abarbeitung von 529 wird nur Dreimal gearbeitet
und Neunmal ein Teil übersprungen.
Bei 415 sieht das Ganze etwas anders aus. Obwohl, als Zahl gesehen
dieser Wert kleiner ist als der Vorherige, wird hier Siebenmal
gearbeitet und nur Dreimal etwas übersprungen.
Das Ganze ist zwar etwas vereinfacht, aber wenn du im oberen Falle mal
die Laufzeiten ins Auge fasst, wirst Du sehen, dass die "größere" Zahl
schneller bearbeitet wird wie die kleinere.
Oder Umgekehrt: Die Annahme, das größere Zahlen länger brauchen, wie
kleinere, ist nicht richtig.
Der Mensch ist halt auf das dezimale System getrimmt.
Für ihn ist 500 eine gerade Zahl. Computy sieht das aber ganz anders.
Für ihn ist 512 sehr gerade. 0111110100 (500d) : 1000000000 (512d).
Amateur schrieb:> Bei der schrittweisen Abarbeitung von 529 wird nur Dreimal gearbeitet> und Neunmal ein Teil übersprungen.
In
08001c5a: bcc.n 0x8001c60 <__udivsi3+216>
08001c5c: lsls r3, r1, #3
08001c5e: subs r0, r0, r3
wird entweder gesprungen oder es werden 2 Eintakter ausgeführt. Je nach
Tempo des nichtsequentiellen Zugriffs kann das deutlich oder
laufzeitneutral ausfallen. Der Unterschied kann daher auch davon
abhängen, ob Code in RAM oder Flash.
Matthias H. schrieb:> so dauert die Berechnung 0,5µs länger
Wieviel Takte sind das?
Solche Division sind in der Regel nur nötig wenn du den Wert anzeigen
willst.
Rechne einfach mit der Auflösung des ADC weiter.
Es ist ja schon eine Spannung in Volt. Halt mit einer unschönen
Auflösung.
Deine anderen Spannungswerte z.B Grenzen für Signal Range Check musst
du halt auch in dieser Auflösung angeben.
Veränderung der Auflösung innerhalb des Programms ist nur ein Komfort
für den Programmierer. Wenn Laufzeit kritisch ist sollte man sie nicht
dafür verschwenden.
Umgerechnet wird beim Senden oder Ausgeben.
Ja solangsam dämmerts mir.
Auch der Schritt für Schritt durchgang von udivsi hat licht ins dunkle
gebracht. Ich werden jetzt erstmal mit einer Lookup Table arbeiten, was
mir wirklich Zeit einspart.
Danke für die vielen Erklärungen und Tipps!
Wirklich klasse Diskussion hier
Matthias H. schrieb:> Stefan U. schrieb:>> Dividiere doch mal handschriftlich auf Papier. Du wirst sehen, daß der>> Zeitaufwand auch hier von den konkreten Werten abhängt.>> Beim Computer ist der Grund prinzipiell der selbe.>> Ja das würde ich ja verstehen.> Was ich aber nicht verstehe, dass es unter einem bestimmten Wert weniger> lang dauert wie über einem bestimmten wert. Eurer theorie nach mpsste es> ja in beiden bereichen(drüber,drunter) unterschiedlich lange dauern...
Das ist auch so. Tatsächlich hängt die Laufzeit der Division nicht nur
vom Divisor ab (weil du dich so an verschiedene Werte des Divisors
klammerst) sondern sowohl vom Divisor als auch vom Dividenden.
Das ist ganz normal und sollte jedem Programmierer auch bekannt sein.
Wenn dein Programm nur dann funktioniert, wenn die Laufzeit der Division
immer gleich ist, dann funktioniert es nicht. Schreib es anders.
Volle22 schrieb:> Solche Division sind in der Regel nur nötig wenn du den Wert anzeigen> willst.> Rechne einfach mit der Auflösung des ADC weiter.>> Es ist ja schon eine Spannung in Volt. Halt mit einer unschönen> Auflösung.>> Deine anderen Spannungswerte z.B Grenzen für Signal Range Check musst> du halt auch in dieser Auflösung angeben.>> Veränderung der Auflösung innerhalb des Programms ist nur ein Komfort> für den Programmierer. Wenn Laufzeit kritisch ist sollte man sie nicht> dafür verschwenden.>> Umgerechnet wird beim Senden oder Ausgeben.
Ja aber ich habe eine Offsetkorrektur. Z.B will ich von dem Wert 2,5V
abziehen. Woher weiß ich jetzt welcher digitale Zahlenwert 2,5V
entspricht?
Mein VDDA, sprich die Referenz, varriert nämlich
Axel S. schrieb:> Das ist ganz normal und sollte jedem Programmierer auch bekannt sein.> Wenn dein Programm nur dann funktioniert, wenn die Laufzeit der Division> immer gleich ist, dann funktioniert es nicht. Schreib es anders.
Wenn man in obigem __udivsi Code die Anfangstests weglässt, das also auf
die klassische Divisions-Iteration reduziert, und ihn auf einem Cortex
M0 ohne Waitstates ausführt (z.B. im RAM), ist der Code laut ARM-Doku
laufzeitneutral. Der ausgeführte Sprung der entrollten Iteration dauert
genauso lang wie der nicht ausgeführte Sprung plus die beiden Eintakter.
Eine sichere Bank für alle Lebenslagen ist das natürlich nicht.
>Ich werden jetzt erstmal mit einer Lookup Table arbeiten, was>mir wirklich Zeit einspart.
Vorsicht bissiger Hund!
Sind der FLASH-Speicher oder das RAM beschränkt, so bekommst Du bei der
Verwendung von Tabellen sehr schnell dicke Backen (sehr Platzintensiv).
Vor allem, wenn Du den Wertebereich nicht ordentlich einschränken
kannst.
Amateur schrieb:> Vorsicht bissiger Hund!
Und wenn man diesen Code dann auf einem ARM mit (Flash-, oder CPU-)
Cache für diese Tabellenzugriffe laufen lässt, dann ists wieder Essig
mit der exakten Reproduzierbarkeit der Laufzeit. Nur hängt das dann
nicht nur von den Werten selbst ab, sondern auch noch von Wasserstand
und Luftdruck.
> Ja das würde ich ja verstehen.> Was ich aber nicht verstehe, dass es unter einem bestimmten Wert weniger> lang dauert wie über einem bestimmten wert. Eurer theorie nach mpsste es> ja in beiden bereichen(drüber,drunter) unterschiedlich lange dauern...
Programmiere doch mal selber eine Division in Assembler. Hinweis:
Schriftliche Division wie in der 3. Klasse. Dann wird klar, warum die
Regenzeit der Division unterschiedlich lang wird.