Hallo,
ich beschaeftige mich seit einiger Zeit mit AVRs, wobei ich bisher
hauptsaechlich in Assembler programmiert habe. Seit ueber einem Jahr bin
ich regelmaessig im Microkotroller Froum unterwegs, und alle meine
Fragen konnten mit Hilfe der SuFu beantwortet werden (Danke an dieser
Stelle fuer die zahlreichen kompetenten Beitraege). Jetzt stehe ich aber
irgendwie bei meinen ersten gcc-Versuchen auf dem Schlauch. Und zwar
moechte ich den Wert einer ADC-Wandlung in den Typ int16_t konvertieren,
der einer Spannung im Wertebereich von -maxVoltage bis +maxVoltage
(minus 1LSB) entspricht (die Null-Referenz der zu messenden Spannung
liegt ca. bei der Haelfte von Vref, die Zahl entspricht der Spannung in
hundertstel Volt). Um das Ganze ohne float zu realisieren, brauche ich
fuer die Zwischenrechnung den Typ int32_t, der nach der Division durch
512 (bei positiven Zahlen waere das 1 byte und 1 bit rechtsschieben)
wieder in int16_t umgewandelt werden kann. Hier der (fehlerhafte) Code:
static int16_t getVoltage()
{
// ADC output auf Wertebereich von -maxVoltage bis +maxVoltage (-1LSB)
mappen:
return (int16_t)((((int32_t)(ADC_Read(0)-512+ADCzero))*maxVoltage) /
512 );
}
maxVoltage liegt etwa bei 500 (Versorgungsspannung). Bei positiven
Zahlen funktioniert das wunderbar, bei negativen wird aber das MSB
(Minuszeichen) nicht richtig verarbeitet, so dass Datenmuell dabei
rauskommt. Einzige Loesung, die mir einfaellt, ist eine if/else
Verzweigung, um negative Zahlen gesondert zu behandeln. Gibt es da in C
nicht eine elegantere (und trotzdem effiziente) Vorgehensweise?
Vielen Dank!
Ingo.
Wenn ADC_Read() ein vorzeichenloses Resultat liefert, dann bleibt die
Zwischenrechnung innerhalb der Klammer vorzeichenlos und folglich die
ganze Rechnung. Also: (int16_t)ADC_Read(0). ADCzero sollte dann freilich
auch nicht vorzeichenlos sein, sonst hast du schon wieder verloren.
Ich muss mich wohl erstmal wieder an die Eigenarten der C-Typen
gewoehnen (ist schon eine Weile her). So funktioniert es dann:
uint16_t ADCVal;
ADCVal=ADC_Read(0);
return ((((int32_t)ADCVal-512+ADCzero)*maxVoltage) / 512 );
Geht es aber nicht ohne die Extra-Variable ADCVal?
Ingo.
Ingo B. schrieb:> return ((((int32_t)ADCVal-512+ADCzero)*maxVoltage) / 512 );
Besser ist:
return ((((int32_t)ADCVal-512+ADCzero)*maxVoltage) >> 9 )
Aber warum funktioniert das ueberhaupt? Wird das MSB (neg. Zahl) nicht
mitgeschoben?
Ingo B. schrieb:> Geht es aber nicht ohne die Extra-Variable ADCVal?
ich denke mal so:
return ((((int32_t)ADC_Read(0)-512+ADCzero)*maxVoltage) / 512 );
oder eben:
Ralf G. schrieb:> Müsste es nicht reichen, wenn ADC_Read() ein int32_t zurückgibt?Ingo B. schrieb:> Ich muss mich wohl erstmal wieder an die Eigenarten der C-Typen> gewoehnen
Hat damit nichts zu tun! Was ist denn das Ergebnis von ADC_Read(), was
ist der Datentyp von maxVoltage?
Du hast wahrscheinlich alles in 16 Bit gerechnet und erst 'als es zu
spät war' in 32 Bit gecastet.
Ingo B. schrieb:> Geht es aber nicht ohne die Extra-Variable ADCVal?
Doch. Aber es muss ein Vorzeichen in die Rechnung rein, sonst bleibt es
draussen. Ob du das mit int32_t oder int16_t machst ist aber egal.
Denk dran, dass bei 8/16-Bittern innerhalb einer 16-Bit Rechnung bereits
ein einziger vorzeichenloser Typ die ganze Rechnung vorzeichenlos macht.
Wenn du dann erst mit int32_t einen vorzeichenbehafteten Typ draus
machst, dann hast du den Stall erst abgesperrt, nachdem das Pferd schon
ausgebüxt ist.
Naja, ADC_Read gibt den Wert des ADC raus, der ist per se positiv (0 bis
1023). Es macht ja keinen Sinn, den in der Funktion ADC_Read in eine
signed zu verwandeln. Das ist erst notwendig, wenn die Zahl 512
subtrahiert werden soll.
Ralf G. schrieb:> return ((((int32_t)ADC_Read(0)-512+ADCzero)*maxVoltage) / 512 );
Hatte ich versucht, funktionierte nicht (Fehlermeldung). Jetzt aber
schon. Muss mich wohl vertippt haben. Vielen Dank!
Wenn jetzt noch die Frage wegen ">>9" beantwortet wird, loesen sich
meine Fragezeichen vollends auf!
Ingo B. schrieb:> Wenn jetzt noch die Frage wegen ">>9" beantwortet wird, loesen sich> meine Fragezeichen vollends auf!
Schreib mal lieber 512. Das ">>9" schiebt dir die Bits. Aber mit
Vorzeichen!
Arithmetisches Schieben gibt's beim AVR nicht (ist's ein AVR?). Falls du
einen µC verwendest, der das beherrscht (ich weiß nicht, ob es so einen
gibt), dann findet der Compiler solche 'Tricks' in der Regel selber
raus.
Ralf G. schrieb:> Schreib mal lieber 512. Das ">>9" schiebt dir die Bits. Aber mit> Vorzeichen!
Dachte ich auch, tut es aber nicht!
Bei MSDN heisst es:
For leftward shifts, the vacated right bits are set to 0. For rightward
shifts, the vacated left bits are filled based on the type of the first
operand after conversion. If the type is unsigned, they are set to 0.
Otherwise, they are filled with copies of the sign bit.
Also wird schon beruecksichtigt, dass die Zahl signed ist.
Ralf G. schrieb:> Arithmetisches Schieben gibt's beim AVR nicht (ist's ein AVR?). Falls du> einen µC verwendest, der das beherrscht (ich weiß nicht, ob es so einen> gibt), dann findet der Compiler solche 'Tricks' in der Regel selber> raus.
Ich verstehe nicht ganz, was arithmetishes Schieben heisst. Und wenn es
der Compiler kann, sollte es doch reichen, oder? Jedenfalls wird der
Code mit ">>9" um 70 Bytes kuerzer als mit "/512". Kann natuerlich sein,
dass die Optimierung nicht richtig eingestellt ist (steht auf -Os).
Ingo B. schrieb:> Bei MSDN heisst es:
Seit wann beschreibt MSDN einen Compiler für AVRs? Als universelle
C-Referenz ist diese Quelle ungeeignet, denn im Standard heisst es: "If
E1 has a signed type and a negative value, the resulting value is
implementation-defined."
A. K. schrieb:> Seit wann beschreibt MSDN einen Compiler für AVRs? Als universelle> C-Referenz ist diese Quelle ungeeignet, denn im Standard heisst es: "If> E1 has a signed type and a negative value, the resulting value is> implementation-defined."
OK, aber fuer den Hobbybereich ziehe ich die dann doch die deutlich
effizientere Variante vor. Wie gesagt, ich komme eigentlich aus dem
Assembler-Bereich, und da ist ohnehin nichts so richtig portierbar. Und
ich werde wohl bei AVR Studio bleiben, da sollte das MSDN-Zitat
anwendbar sein. Ein Hinweis als Kommentar im Programmkopf kann aber
nicht schaden.
A. K. schrieb:> NB: "/ 512" und ">> 9" liefern bei negativen Werten unterschiedliche> Ergebnisse, was die Rundung angeht.
Daran haette ich nicht gedacht, ist aber logisch. Danke fuer den
Hinweis!
Ingo B. schrieb:> A. K. schrieb:>> Seit wann beschreibt MSDN einen Compiler für AVRs? Als universelle>> C-Referenz ist diese Quelle ungeeignet, denn im Standard heisst es: "If>> E1 has a signed type and a negative value, the resulting value is>> implementation-defined.">> OK, aber fuer den Hobbybereich
Es geht aber nicht um 'Hobbybereich' oder nicht.
Es geht darum, dass >> ein falsches Ergebnis liefert.
Tu dir selbst einen Gefallen und vergiss diese ganzen Assembler-Tricks.
Wenn es möglich ist eine Division oder eine Multiplikation durch
Schieben zu ersetzen UND das dann auch noch effizienter ist, dann macht
der Compiler diese Optimierung für dich. So was haben Compiler seit 50
Jahren drauf. Sowas und noch viel mehr. Für dich lautet die Devise: Ich
schreib meinen Code so, dass ich in 2 Jahren den Code immer noch lesen
kann! Low-Level Optimierungen überlass ich dem Compiler.
A. K. schrieb:> Ralf G. schrieb:>> Arithmetisches Schieben gibt's beim AVR nicht>> Doch.
Stimmt. Den Befehl 'ASR' hab' ich tatsächlich immer überlesen :-(
Die Verwendung von '>>' wird, glaube ich, immer mit 'LSR' übersetzt. (?)
Ralf G. schrieb:> Die Verwendung von '>>' wird, glaube ich, immer mit 'LSR' übersetzt.
Ist genauso falsch. Weshalb sollte er das tun? Gleicher Aufwand.
Eine andere Idee: Wenn ADCzero positiv ist, kannst Du die Division auch
vorzeichenlos machen und die Subtraktion erst ganz am Schluss
vorzeichenbehaftet. Das sollte der Compiler dann zu einem Shift machen:
Nochmal >>: Interessant ist eine solche Frage effektiv nur bei
Prozessoren, die keinen Befehl dafür haben, wie etwa 8051 und die 8-Bit
PICs. Ich vermute freilich, dass viele oder alle Compiler auch da mit
Vorzeichen schieben, umständlicher eben. Einfach deshalb, weil das zwar
nicht vom Standard vorgeschrieben ist, sich aber eingebürgert hat.
A. K. schrieb:> Ralf G. schrieb:>> Die Verwendung von '>>' wird, glaube ich, immer mit 'LSR' übersetzt.>> Ist genauso falsch. Weshalb sollte er das tun? Gleicher Aufwand.
So'n Mist. Stimmt! (Wo hatte ich das bloß wieder her?)
Vielleicht kann man sich mal rantasten:
1
#include<avr/io.h>
2
#include<inttypes.h>
3
4
5
volatileuint8_ti;
6
7
8
intmain()
9
{
10
i=(uint8_t)PINB;
11
i=i/2;
12
return0;
13
}
Bei uintXX_t für 'i' wird geschoben. Bei intXX_t wird dividiert .
(Daher meine Fehlinterpretation, dass ASR fehlt. Hatte allerdings bis
jetzt immer nur unsigned zum Testen.)
Ersetzt man 'i=i/2' durch 'i=i >> 1' wird immer mathematisch korrekt
geschoben.
@xfr:
Guter Punkt! Dass 512/512=1 ist, haette ich auch sehen sollen ;-)
Zwar ist ADCzero u.U. auch negativ (und keine Konstante, sondern vom Typ
int8_t), aber man koennte es ja umdefinieren und einfach noch mal 512
abziehen:
1
int16_tADCzero;
2
...
3
ADCzero=0+512;// bleibt immer positiv, statt "0" der eigentliche Nullwert
Ausserdem habe ich den typecast weiter nach vorne gezogen, da die Summe
ja erstmal als 16bit-Zahl ausgerechnet werden kann.
Ist sogar noch kuerzer als die >>9 Variante. So kann ich auch guten
Gewissens auf die "Assembler-Tricks" verzichten. 70 Bytes (und etliche
Takte) extra fuer "/512" sind hingegen schwer zu akzeptieren...
Danke nochmal an alle, ich glaube jetzt bin ich am Ziel! Erstmal.
Karl Heinz Buchegger schrieb:> Low-Level Optimierungen überlass ich dem Compiler.
Blöd ist freilich, dass die Division bisweilen kürzer ist als der Shift.
In Bytes vom Aufruf der Laufzeitfunktion, nicht in Takten. Beim üblichen
-Os kann sich das martialisch bemerkbar machen.
Ingo B. schrieb:> Ausserdem habe ich den typecast weiter nach vorne gezogen, da die Summe> ja erstmal als 16bit-Zahl ausgerechnet werden kann.
Wenn die Subtraktion auch in 16 Bit reicht, kannst Du mit Verschieben
einer Klammer vielleicht noch ein paar Befehle sparen. :D
Edit: Du musst das sogar machen (oder in int32_t casten), denn sonst
rechnest Du die Subtraktion unsigned und castest erst ganz am Ende (was
sowieso implizit passieren würde, weil der Rückgabewert der Funktion ja
int16_t ist).
@Karl Heinz Buchegger (kbuchegg) (Moderator)
>Tu dir selbst einen Gefallen und vergiss diese ganzen Assembler-Tricks.
Jo, aber . . .
>Schieben zu ersetzen UND das dann auch noch effizienter ist, dann macht>der Compiler diese Optimierung für dich. So was haben Compiler seit 50>Jahren drauf.
Beim letzten Mal waren es erst 30. ;-) Vor 50 Jahren war von C noch
keine Rede, und Compiler waren bestenfalls Assembler. Alte Männer
erzählen vom Krieg.
>kann! Low-Level Optimierungen überlass ich dem Compiler.
Jain. Man muss wie immer wissen was man tut und ob es sich lohnt.
AVR-GCC-Codeoptimierung
xfr schrieb:> Edit: Du musst das sogar machen (oder in int32_t casten), denn sonst> rechnest Du die Subtraktion unsigned und castest erst ganz am Ende (was> sowieso implizit passieren würde, weil der Rückgabewert der Funktion ja> int16_t ist).
Hmm, sehe ich ein. Seltsamerweise funktioniert es, und es gibt keinen
Unterschied zwischen den beiden Klammer-Versionen in der Codegroesse.
Hat vielleicht damit zu tun, dass die uint32_t-Zahl ja quasi negativ
ist, da das MSB gesetzt wird (overflow bei der Subtraktion). Bei der
Konversion geht das dann halt trotz des Typkonflikts gut. Schoen ist es
aber nicht.
Dann hat der Compiler anscheinend auch bei der ersten Variante schon
erkannt, dass nur die unteren 16 Bit weiterverwendet werden und
entsprechend in 16 Bit gerechnet.
Dass bei der unsigned-Subtraktion und danach casten das gleiche
rauskommt, ist kein Wunder. Schließlich repräsentiert man negative
Zahlen in der Praxis ja genau deswegen im Zweiterkomplement, damit die
Rechenoperationen bei signed und unsigned die gleichen sind.
Nur garantiert C einem nicht, dass die Konvertierung von unsigned in
signed auf diese Weise abläuft, wenn der unsigned-Wert nicht in den
signed-Wert passt. Auf irgendeiner exotischen Maschine könnten negative
Zahlen ja anders repräsentiert sein. Daher ist es auf jeden Fall
sauberer, mit den richtigen Typen zu rechnen. Es kostet ja nichts.
Ralf G. schrieb:> {> i=(uint8_t)PINB;> i=i/2;> return 0;> }> Bei uintXX_t für 'i' wird geschoben. Bei intXX_t wird dividiert .
Nein, bei beiden wird dividiert, denn es steht eine Division da!
Ob die Division durch Schieben oder wie auch immer erledigt wird, ist
Sache des Compilers:
1
#include<stdint.h>
2
3
uint8_tudiv2(uint8_ti)
4
{
5
returni/2;
6
}
7
8
int8_tdiv2(int8_ti)
9
{
10
returni/2;
11
}
Dieses Beispiel von dir wird vom aktuellen avr-gcc zB so übersetzt:
> Da bastelt meine alte GCC-Version sozusagen was anderes draus.
Das das daran liegt, dass die neuere GCC Version mitlerweile auch für
Divisionen entsprechende Spezialbehandlungen mithat, dürftest du ja
schon gemerkt haben.
Das aber für int8_t eine Division durch 2 und einmal rechts schieben
nicht das gleiche Ergebnis bringt, kannst du ganz einfach feststellen,
wenn du i einfach mal den Wert -3 gibst. Im einen Fall kommt korrekt -1
raus, im anderen Fall kommt -2 raus.
Karl Heinz Buchegger schrieb:> Ralf G. schrieb:> [...]>> Da bastelt meine alte GCC-Version sozusagen was anderes draus.
Dann hast du nicht auf Geschwindigkeit optimiert, was du offenbar
willst.
Keine der von mir getesteten Versionsn (3.4, 4.3, 4.5, 4.6, 4.7, 4.8,
mehr hab ich net da) ruft bei Speed-Optimierung zur Division eine
Bibliotheksfunktion auf.
> Das das daran liegt, dass die neuere GCC Version mitlerweile auch für> Divisionen entsprechende Spezialbehandlungen mithat, dürftest du ja> schon gemerkt haben.
Wie gesagt, das macht schon avr-gcc 3.4 (ca. 7 Jahre alt), vorausgesetzt
man bedient ihn richtig ;-)
Johann L. schrieb:> Wie gesagt, das macht schon avr-gcc 3.4 (ca. 7 Jahre alt), vorausgesetzt> man bedient ihn richtig ;-)
Gut zu wissen, wenn der nächste meint, er muss da wieder selbst
'Handoptimieren'.
Ich hab da ehrlich gesagt noch nie im Assembler nachgeschaut. Denn wenn
ich 'durch 512' meine, dann schreib ich auch 'durch 512'. :-)
Karl Heinz Buchegger schrieb:> Johann L. schrieb:>>> Wie gesagt, das macht schon avr-gcc 3.4 (ca. 7 Jahre alt), vorausgesetzt>> man bedient ihn richtig ;-)>> Gut zu wissen, wenn der nächste meint, er muss da wieder selbst> 'Handoptimieren'.> Ich hab da ehrlich gesagt noch nie im Assembler nachgeschaut. Denn wenn> ich 'durch 512' meine, dann schreib ich auch 'durch 512'. :-)
Der übliche Denkfehler dabei ist, daß auf Größe optimiert wird, aber
Code erwartet wird, wie er bei Geschwindigkeits-Optimierung erzeugt
wird.
Erst ab avr-gcc 4.7 verhält sich der Compiler nicht ganz wie erwartet,
d.h. bei Optimierung auf Größe mittels -Os erzeugt er Code, der mehrere
Instruktionen größer sein kann als der optimale Code, dafür aber um
Größenodnungen schneller ist.
Die Frage ist, wo hier die Grenze zu ziehen ist; immerhin will der
Anwender bei -Os den kleinstmöglichen Code.
Andererseits sind Shifts so elementar und so weit verbreitet, daß mit
dieses Verhalten sinnvoll erschien. Konkret wird das erreicht, indem
Division teurer gemacht wird als sie gemäß der erzeugten Instruktionen
tatsächlich ist.
In den 4.7 Release Notes [1] ist das der Punkt
"Integer division by a constant"
obwohl der Code aus Sicht von -Os schlechter wird.
[1] http://gcc.gnu.org/gcc-4.7/changes.html#avr
Johann L. schrieb:> Die Frage ist, wo hier die Grenze zu ziehen ist; immerhin will der> Anwender bei -Os den kleinstmöglichen Code.
Für Mikrocontroller wäre imo ein Modus optimal, der den vorhandenen
Speicher bestmöglich ausnutzt. Also zuerst O3 und wenns zu voll wird
nach und nach Geschwindigkeitsoptimierungen gegen Größenoptimierungen
tauschen ... Und vielleicht noch anzeigen, was die kleinstmögliche Größe
gewesen wäre. Naja, man wird ja noch träumen dürfen ... ;-)
Johann L. schrieb:> Die Frage ist, wo hier die Grenze zu ziehen ist; immerhin will der> Anwender bei -Os den kleinstmöglichen Code.
Okay, ich habe das (int8_t) mit Version 4.3 und Optimierung -Os
getestet:
Code: 150Byte
mit -O3:
Code: 116Byte, und siehe da: jetzt wird auch 'asr' verwendet! ->
schneller und kürzer.
xfr schrieb:> Johann L. schrieb:>> Die Frage ist, wo hier die Grenze zu ziehen ist; immerhin will der>> Anwender bei -Os den kleinstmöglichen Code.>> Für Mikrocontroller wäre imo ein Modus optimal, der den vorhandenen> Speicher bestmöglich ausnutzt. Also zuerst O3 und wenns zu voll wird> nach und nach Geschwindigkeitsoptimierungen gegen Größenoptimierungen> tauschen ... Und vielleicht noch anzeigen, was die kleinstmögliche Größe> gewesen wäre. Naja, man wird ja noch träumen dürfen ... ;-)
Was glaubst du, wie die Leute rumweinern würden, wenn man das in den
Compiler einbaut? Nämlich spekulative Optimierung, die zu Compilezeiten
führen würden, wie du sie für ein Schachprogramm einplanen würdest, von
dem du erwartest, daß es auf GM-Niveau spielt: 1/4 Stündchen oder mehr
für die 100-Zeilen Quelle gefällig?
Sowas ist doch nur eine Fingerübung für Makefile-Autoren, wenn du sowas
willst brauchst haben musst, schreib dir entsprechende Makefiles
oder Skripte.