Hi,
gibt es eine kleine Alternative zu printf, die man bei AVR verwenden
kann? Hat vielleicht jemand sogar eine Funktion gebastelt und auf Größe
optimiert?
Ideal wäre es, wenn die Funktion gar keine Floats verwenden würde,
sondern übergebene Variablen einfach als Vielfache einer Zehnerpotenz
interpretiert. Also man ruft sie z.B. so auf:
foobar(-12345, 2, 8) für 2 Nachkommastellen und Padding auf 8 Stellen
und die Funktion liefert den String " -123,45" zurück.
Wäre doch, denke ich, hilfreich.
Ich benutze sehr gern printf, in sämtlichen Variationen, zb vnprintf.
Die eierlegende Wollmilchsau unter den Funktionen!
Hast du Probleme mit Flash oder CPU-Last, oder warum willst du eine
Alternative?
Für nichtgenutzte Ressourcen gibts kein Geld zurück...
Herbert schrieb:> Ideal wäre es, wenn die Funktion gar keine Floats verwenden würde
Macht das avr-libc-printf() doch standardmäßig gar nicht.
Mich dünkt, du hast dir das einfach nur noch gar nicht angesehen und
willst es daher aus Prinzip nicht nehmen, oder?
Kann man dem TE, sofern er Anfänger ist, auch nicht verübeln.
Hier wird ja überall gepredigt printf() sei böse und der Untergang des
Abendlandes...
Und dann wird irgendwelcher Bastelcode vorgestellt der alles viel
umständlicher macht, unleserlich ist und trotzdem nur einen Bruchteil
der Features eines printf bietet.
Nein, es gibt in 95% der Fälle keinen Grund, sich selbst dieser
mächtigen Waffe zu berauben.
Ehrlich gesagt hat der Jörg sogar recht. ;) Ich habe nach "const char
*__fmt" schon aufgehört zu lesen. Ein Format-String ist etwas, das ich
nicht brauche und was die Funktion einfach aufblähen muss.
Aktuell verwende ich eine selbst gestrickte Geschichte, die dem letzten
Vorschlag ähnelt. Siehe auch die Parallelen beim Aufruf. Ich verwende
aber div und keine Subtraktionsschleife. Dachte nur, dass es noch
kleiner geht... eigentlich ist das doch etwas, was überall gebraucht
wird. Rechnen in Hundertsteln oder Tausendsteln ist doch üblich.
Bal schrieb:> Kann man dem TE, sofern er Anfänger ist, auch nicht verübeln.> Hier wird ja überall gepredigt printf() sei böse und der Untergang des> Abendlandes...
Das gleiche Spiel läuft ja auch immer wieder mit float ab ;-)
Herbert schrieb:> gibt es eine kleine Alternative zu printf, die man bei AVR verwenden
Welchen AVR verwendest Du denn?
Ist ein ATMEGA168 und Flash ist sehr knapp, obwohl schon stark
optimiert. Für zukünftige Funktionen wird immer etwas Platz gebraucht,
je mehr desto besser. Momentan sind noch 62 Bytes frei. ;)
Herbert schrieb:> Ist ein ATMEGA168 und Flash ist sehr knapp, obwohl schon stark> optimiert.
Wirklich?
Oft sieht man leider copy&paste Monster mit tausenden printf Aufrufen,
das kostet natürlich massig Flash.
Man kann da sehr leicht optimieren, wenn man bei switch/case nicht allen
100 cases sein eigenes printf spendiert, sondern einfach nur Pointer auf
Formatstring und Argumente setzt und danach ein einziges printf
plaziert.
Die cases ohne printf verläßt man z.B. mit return.
Herbert schrieb:> Ist ein ATMEGA168 und Flash ist sehr knapp, obwohl schon stark> optimiert.
Gerüchten zu Folge soll es einen ATmega328 geben, der doppelt soviel
Flash-Speicher hat.
Nein, ich bin an diesen Chip gebunden. Das Projekt stammt aus einer
Zeit, als der 328 noch selten war und die Baugruppen sind fertig und
weit verteilt.
Herbert schrieb:> Ideal wäre es, wenn die Funktion gar keine Floats verwenden würde,> sondern übergebene Variablen einfach als Vielfache einer Zehnerpotenz> interpretiert. Also man ruft sie z.B. so auf:> foobar(-12345, 2, 8) für 2 Nachkommastellen und Padding auf 8 Stellen> und die Funktion liefert den String " -123,45" zurück.
Ich als Arduino-Programmierer mache mir sowas ja einfach selbst, wenn
ich das brauche.
Beispielcode (sollte mit AVR GCC allgemein laufen, getestet habe ich es
aber nur mit Arduino) für "int" Wertebereich:
1
char*itoaDigits(inti,intwidth,intdigits)
2
{
3
#define INT_MAXWIDTH 7 // 5 digits + sign + dot
4
if(width>INT_MAXWIDTH)width=INT_MAXWIDTH;
5
staticcharbuf[INT_MAXWIDTH+1];// Room for formatted number and '\0'
6
char*p=buf+INT_MAXWIDTH;// points to terminating '\0'
7
if(i<0)
8
{
9
buf[0]='-';
10
i=-i;
11
}
12
elsebuf[0]='+';
13
do
14
{
15
*--p='0'+(i%10);
16
i/=10;
17
digits--;
18
if(digits==0)*--p='.';
19
}while((i!=0)||(digits>-1));
20
if(buf[0]=='-')*--p='-';
21
while(p>buf+INT_MAXWIDTH-width)*--p=' ';
22
returnp;
23
}
Denkbar wären aber auch Funktionsvarianten, denen man zur Formatierung
einen Pointer auf ein (ausreichend großes) bereits existierendes
char-Array übergibt, in dem die Formatierung erfolgt, falls man die 8
Bytes RAM-Verbrauch einsparen möchte.
Denkbar sind auch Variationen, z.B. dass positive Zahlen stets mit einem
führenden "+" Zeichen ausgegeben werden. Oder dass bei der Formatierung
das Vorzeichen immer "ganz links" statt "vor der ersten Ziffer" steht.
Und auch die Ausgabe mit "Dezimalkomma" statt "Dezimalpunkt" wäre ein
Klacks, wenn man es möchte.
Herbert schrieb:> Momentan sind noch 62 Bytes frei. ;)
Wenn du derartige Randbedingungen mal gleich ins Eröffnungsposting
geschrieben hättest, hätte sich die Hälfte des Threads erledigt.
Für derart spezifische Forderungen sehe ich nur zwei Varianten:
. Eigenbau (hast du ja wohl schon)
. itoa() oder ltoa() aus der Bibliothek (je nach Wertebereich) und
dann mit normalen String-Funktionen die beiden letzten Stellen
abtrennen für das Dezimalzeichen bzw. bei Bedarf so umkopieren,
dass linksseitig Leerzeichen gefüllt werden
Steht doch oben:
Herbert schrieb:> Hat vielleicht jemand sogar eine Funktion gebastelt und auf Größe> optimiert?
Jürgens Algorithmus ist gut und so ähnlich mache ich das auch. Jürgen
hat sogar den kleineren. Muss noch ergründen, warum.
Wenn man statt % und / gleich div() einsetzt, wird es noch etwas kleiner
(sofern man div() schon woanders im Einsatz hat).
Kann es sein, dass du implizierst, dass lokale Variablen mit 0 (ich sehe
kein \0) initialisiert sind?
Herbert schrieb:> Kann es sein, dass du implizierst, dass lokale Variablen mit 0 (ich sehe> kein \0) initialisiert sind?
Die "static char" variable "buf[INT_MAXWIDTH + 1]" ist als
static-Variable eigentlich keine richtige lokale Variable, weil sie
nicht auf dem Stack angelegt wird. Diese wird daher vor dem
Programmstart genau so ausgenullt wie alle anderen globalen und static
Variablen im Programm.
Unter AVR GCC kann man sicher sein, dass die ausgenullt ist.
Vom Algorithmus her muß bei meinem Code beim Aufruf der Funktion auch
nicht die ganze Variable ausgenullt zu sein (ist sie auch nicht, wenn
die Funktion vorher schon gelaufen ist), sondern es muss nur das
allerletzte Zeichen ein Nullzeichen sein.
Wenn der Code auch zu anderen Compilern außerhalb der AVR GCC Welt
kompatibel sein soll und man sich nicht sicher ist, ob "static char
buf[]" wirklich beim Start ausgenullt ist, kann man zur Sicherheit am
Anfang der Funktion das letzte Zeichen auf Nullzeichen setzen:
1
buf[INT_MAXWIDTH]='\0';
Aber bei AVR GCC ist das meines Erachtens nach immer überflüssig, auch
wenn man nicht mit der Arduino-IDE programmiert und diverse Parameter
und Optionen für Compiler und Linker anders setzen kann. Daher habe ich
es weggelassen.
Danke, habe das "static" eben erst bewusst wahrgenommen. Netter Trick.
Spart Speicher. Dann kann die Geschichte mit dem '+' aber auch weg.
Habe oben noch ein
1
char pad = ' ';
2
if (digits == -1) pad = '0';
eingebaut. Gibt man -1 als Digits an, kriegt man Zero-Padding, z.B. für
Uhrzeiten ganz nett. 12:03 sieht besser aus als 12: 3
Jürgen S. schrieb:> Wenn der Code auch zu anderen Compilern außerhalb der AVR GCC Welt> kompatibel sein soll und man sich nicht sicher ist, ob "static char> buf[]" wirklich beim Start ausgenullt ist,
Variablen mit "static storage duration" (dazu gehören auch lokale, als
"static" definierte Variablen) werden laut Standard vor dem Start des
eigentlichen C-Programms mit Nullen vorbelegt. Es ist also korrekt und
auch portabel, das explizite Schreiben des Nullzeichens wegzulassen.
Herbert schrieb:> Ein Format-String ist etwas, das ich> nicht brauche und was die Funktion einfach aufblähen muss.
Da ohne Formatstring nur Strings ausgegeben werden können suchst du
vermutlich etwas wie fputs.
Peter Dannegger schrieb:> an kann da sehr leicht optimieren, wenn man bei switch/case nicht allen> 100 cases sein eigenes printf spendiert, sondern einfach nur Pointer auf> Formatstring und Argumente setzt und danach ein einziges printf> plaziert.> Die cases ohne printf verläßt man z.B. mit return.
Kann dazu jemand ein Beispiel machen?
Habe den Kommentar nicht ganz verstanden!
LG
Oskar
Du schreibst einfach deine Strings in einen Puffer und hast nur einen
einzigen Funktionsaufruf ganz unten unter deinem switch-Gebilde. Ist
eigentlich eine ganz übliche Technik.
Herbert schrieb:> Du schreibst einfach deine Strings in einen Puffer und hast nur einen> einzigen Funktionsaufruf ganz unten unter deinem switch-Gebilde. Ist> eigentlich eine ganz übliche Technik.
Das macht der Compiler doch schon von sich aus, auch bei vielfacher
Wiederholung in jedem 'case'.
Scheinbar nicht immer, sonst wäre es nicht oben bemängelt worden. ;))
Der Compiler ist teils sehr gerissen, teils strunzdoof. Oft kann man
durch simples Vertauschen von Zeilen (wo keine Abhängigkeiten
bestehen!!!) etliche Bytes rausholen.
Mux Matzke schrieb:>>s = "BLA"> geht das? ohne strcpy?
Ja. s ist ein char-Pointer, also eine Variable, in der die
Speicheradresse eines Zeichens steht. Durch die Zuweisung enthält sie
die Adresse, an der das Stringliteral "BLA" im Speicher liegt.
Diese Adresse wird später an printf übergeben. Das ist nichts anderes,
als wenn man direkt schreiben würde:
1
printf("BLA");
Hier wird auch die Adresse des Literals "BLA" übergeben.
Man darf nur nicht versuchen, den Inhalt des Speichers zu ändern, in dem
das Stringliteral liegt. Deshalb wäre es so sauberer:
1
constchar*s;
Man könnte natürlich auch strcpy nutzen. Dafür müsste man aber erstmal
Speicher reservieren, etwa so:
1
chars[10];
Macht das ganze aber nur umständlicher, langsamer und fehleranfälliger
(wenn der String mal länger als der reservierte Speicher sein sollte).
Peter Dannegger schrieb:> Geht noch etwas kleiner:> do{> char *s;> int *val;> switch( i ){> case 0: s = "BLA %d\n", val = &x; break;> case 1: continue; // nothing to print> case 2: s = "BLUB %d\n", val = &y; break;> case 3: s = "PLOP %d\n", val = &z; break;> // ... usw.> }> printf( s, *val );> }while(0);
Man muss mit solchen Handptimierungen etwas vorsichtig sein. Sie können
in einigen Fällen Vorteile bringen, in anderen Fällen aber genau das
Gegenteil bewirken.
Folgendes Beispiel packt den obigen Code in die Funktion sub1, die
Variablen i, x, y und z werden als Funktionsargumente übergeben.
Die Funktion sub2 tut das Gleiche, aber mit jeweils einem printf in
jedem case-Zweig:
1
voidsub1(unsignedchari,intx,inty,intz){
2
do{
3
char*s;
4
int*val;
5
switch(i){
6
case0:s="BLA %d\n",val=&x;break;
7
case1:continue;// nothing to print
8
case2:s="BLUB %d\n",val=&y;break;
9
case3:s="PLOP %d\n",val=&z;break;
10
}
11
printf(s,*val);
12
}while(0);
13
}
14
15
voidsub2(unsignedchari,intx,inty,intz){
16
do{
17
switch(i){
18
case0:printf("BLA %d\n",x);break;
19
case1:continue;// nothing to print
20
case2:printf("BLUB %d\n",y);break;
21
case3:printf("PLOP %d\n",z);break;
22
}
23
}while(0);
24
}
Mit AVR-GCC 4.7.2 ist sub1 228 Bytes und sub2 112 Bytes groß. Die
vermeintliche Optimierung hat die Codegröße also mehr als verdoppelt.
Das liegt u.a. daran, dass in sub1 die Variablen x, y und z, die in
Registern übergeben werden, erst mühevoll ins RAM geschrieben werden
müssen, da nur so ein Pointer darauf erzeugt werden kann.
Auch in sub2 stellt der Compiler den printf-Aufruf inkl. dem Push des
zweiten Arguments und dem anschließenden Aufräumen des Stacks ans Ende
der Funktion.
Der Code in den einzelnen case-Zweigen ist in beiden Fällen gleich lang,
wenn auch unterschiedlich.
Aber selbst wenn der Compiler in sub2 drei separate printf-Aufrufe
erzeugen würde, wäre das Ergebnis in diesem Fall immer noch kürzer als
in sub1.
Wenn man Handoptimierungen macht, sollte man also immer den erzeugten
Assembler-Code vorher und nachher miteinander vergleichen.
P.S.: Warum ist val überhaupt ein Pointer? Nimmt man stattdessen eine
int-Variable und passt den Rest entsprechend an, schrumpft sub1 auf 128
Bytes und ist damit nur noch unwesentlich länger als sub2.
Bei nur 3 printf lohnt sich das natürlich noch nicht. Das Beispiel soll
ja nur das Prinzip verdeutlichen.
Und z.B. 10 Variablen übergibt man besser nicht als 10 Argumente,
sondern als Pointer auf eine Struct oder ein Array.
Yalu X. schrieb:> P.S.: Warum ist val überhaupt ein Pointer?
Einen Pointer laden kostet 2 Words.
Eine globale Variable laden aber 2 DWords.
Somit sparen wir bei 10 Cases nochmal 40 Byte.
Jürgen S. schrieb:>> Ich als Arduino-Programmierer mache mir sowas ja einfach selbst, wenn> ich das brauche.>> Beispielcode (sollte mit AVR GCC allgemein laufen, getestet habe ich es> aber nur mit Arduino) für "int" Wertebereich:>
1
>char*itoaDigits(inti,intwidth,intdigits)
2
>{
3
>#defineINT_MAXWIDTH7// 5 digits + sign + dot
4
>if(width>INT_MAXWIDTH)width=INT_MAXWIDTH;
5
>staticcharbuf[INT_MAXWIDTH+1];// Room for formatted number and
6
>'\0'
7
>char*p=buf+INT_MAXWIDTH;// points to terminating '\0'
8
>if(i<0)
9
>{
10
>buf[0]='-';
11
>i=-i;
12
>}
13
>elsebuf[0]='+';
14
>do
15
>{
16
>*--p='0'+(i%10);
17
>i/=10;
18
>digits--;
19
>if(digits==0)*--p='.';
20
>}while((i!=0)||(digits>-1));
21
>if(buf[0]=='-')*--p='-';
22
>while(p>buf+INT_MAXWIDTH-width)*--p=' ';
23
>returnp;
24
>}
25
>
hmmm... Zusammen mit den benötigten Bibliotheksroutinen belegt die
Routine rund 220 Bytes Flash (vergliechen mit einer Anwendung mit leerer
main, Compiler avr-gcc 4.9.2).
Verwendung von utoa + strlen erlaubt eine Routine, die inclusive
Bibliotheks-Geraffel und verglichen mit einer leeren main "nur" 200
Bytes lang ist:
1
#include<stdlib.h>
2
#include<stdint.h>
3
#include<string.h>
4
5
char*my(inti,int8_twidth,int8_tdigits)
6
{
7
#define INT_MAXWIDTH 8 /* -0.12345 */
8
staticcharbuf[INT_MAXWIDTH+1];
9
unsignedlen,u=i;
10
char*str,*end,sign='+';
11
int8_tlen8;
12
13
if(i<0)
14
u=-u,sign='-';
15
16
utoa(u,buf,10);
17
len8=len=strlen(buf);
18
end=buf+len;
19
str=buf+sizeof(buf);
20
21
do
22
{
23
charc=' ';
24
if(--digits==-1)
25
c='.';
26
elseif(--len8>=0)
27
c=*--end;
28
elseif(digits>=-2)
29
c='0';
30
elseif(sign)
31
c=sign,sign=0;
32
*--str=c;
33
}while(--width);
34
35
returnstr;
36
}
In anbetracht der oben genannten 62 Bytes freiem Flash gewinnt man aber
auch damit keinen Blumentopf...
Eigentlich sollte es nicht allzu schwer sein, in eine bestehende, kleine
utoa-ähnliche Routine ein '.' und evtl. ein paar Nullen reinzufutscheln.
Kramen im Eingemachten lieferte folgende Routine, die einen unsigned
analog zu utoa(.,10) in einen String umwandelt. Die Funktion liefert
die Adresse der abschließenden '\0' zurück, was sich bei
Weiterverarbeitung der Strings u.U. als günstig herausstellt:
1
#include<stdint.h>
2
3
staticconst__flashuint16_tpows10[]=
4
{
5
10000,1000,100,10
6
};
7
8
char*u16_to_string(char*str,uint16_tn)
9
{
10
const__flashuint16_t*p=pows10;
11
uint8_tnot0=0;
12
uint16_tpow10;
13
14
do
15
{
16
charc='0';
17
pow10=*p++;
18
19
while(n>=pow10)
20
not0=1,n-=pow10,c++;
21
22
if(not0)
23
*str++=c;
24
25
}while(!(pow10&2));// pow10 != 10
26
27
// Einer
28
*str++=n+'0';
29
*str='\0';
30
31
returnstr;
32
}
Transkripiert nach GNU-Assembler für den ATmega168 ergibt sich ein Code,
der noch 56 Bytes Flash belegt; siehe Anhang.
Dem TO verbleibt also die Hausaufgabe, zum Einfügen von Dezimalpunkt
sowie für Nullen und Leerzeichen zum Auffüllen nicht mehr als 3
Instruktionen zu verbraten. Zumindest unter diesem Aspekt erscheint die
Frage des TO schon recht trollig.
Ergo: Die Lösung des Problems liegt nicht (nur) in einer smarten
Konvertierung, sondern auch in Review und Straffung der Anwendung. Laut
Moores 2. Gesetz gibt es ja kein Programm, das nicht optimiert werden
kann!
Übrigens ist die Routine nicht nur klein sondern auch schnell: Weder die
signed- noch die unsigned-Variante dauern länger als 320 Ticks (incl.
CALL + RET).
Wem das langsam erscheint: Eine einzige Division durch 10 im o.g. Code
kostet mindestens 215 Ticks. Über die bis zu 5 Ziffern summiert sind
das zu schlappen 1000 Ticks aufwärts. Ausgabe- und Konvertierroutinen
müssen zwar nicht schnell sein, aber mancher µC hat mehr zu erledigen
als unentwegt zu dividieren...
Hi,
die 62 Bytes gelten für die jetzige Fassung, mit meiner eigenen
(größeren) Routine.
Wenn ich in die 62 Bytes noch eine Ausgabe-Routine einbauen müsste und
keinen Platz für die Aufrufe selbst mehr hätte, wär's ja auch irgendwie
merkwürdig. ;))
Ich wollte damit nur verdeutlichen, dass momentan jedes Byte zählt, was
irgendwie durch Optimierung frei wird.
Ich teste gerade noch ein bisschen. Es ist phänomenal, wie man durch
Umsortieren von nicht von einander abhängigen Programmzeilen kleineren
Code erhält.
Johann L. schrieb:> Transkripiert nach GNU-Assembler für den ATmega168 ergibt sich ein Code,> der noch 56 Bytes Flash belegt; siehe Anhang.
Und gleich nen Fehler in der signed-Version. Im Anhang oben fehlte das
abschließende cpse 0,0: