printf-Alternative für AVR

Gast #3845538
Lesenswert?

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.
Gast #3846057
Lesenswert?

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.
Gast #3846307
Lesenswert?

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.
Gast #3846387
Lesenswert?

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?
#3846438
Lesenswert?

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.
#3846710
Lesenswert?

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(int i, int width, int digits)
2
{
3
#define INT_MAXWIDTH 7  // 5 digits + sign + dot
4
  if (width>INT_MAXWIDTH) width=INT_MAXWIDTH;
5
  static char buf[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
  else buf[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
  return p;
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.
Moderator Persönliche Seite #3846820
Lesenswert?

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
Gast #3846859
Lesenswert?

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?
#3846889
Lesenswert?

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.
Gast #3846895
Lesenswert?

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
Moderator #3846912
Lesenswert?

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.
Gast #3847418
Lesenswert?

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
Gast #3848002
Lesenswert?

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
const char* s;

Man könnte natürlich auch strcpy nutzen. Dafür müsste man aber erstmal 
Speicher reservieren, etwa so:
1
char s[10];
Macht das ganze aber nur umständlicher, langsamer und fehleranfälliger 
(wenn der String mal länger als der reservierte Speicher sein sollte).
Moderator #3848012
Lesenswert?

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
void sub1(unsigned char i, int x, int y, int z) {
2
  do{
3
    char *s;
4
    int *val;
5
    switch( i ){
6
      case 0: s = "BLA %d\n", val = &x; break;
7
      case 1: continue; // nothing to print
8
      case 2: s = "BLUB %d\n", val = &y; break;
9
      case 3: s = "PLOP %d\n", val = &z; break;
10
    }
11
    printf( s, *val );
12
  } while(0);
13
}
14

15
void sub2(unsigned char i, int x, int y, int z) {
16
  do{
17
    switch( i ){
18
      case 0: printf("BLA %d\n", x); break;
19
      case 1: continue; // nothing to print
20
      case 2: printf("BLUB %d\n", y); break;
21
      case 3: 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.
#3848071
Lesenswert?

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.
Persönliche Seite #3851379
Lesenswert?

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(int i, int width, int digits)
2
> {
3
> #define INT_MAXWIDTH 7  // 5 digits + sign + dot
4
>   if (width>INT_MAXWIDTH) width=INT_MAXWIDTH;
5
>   static char buf[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
>   else buf[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
>   return p;
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 (int i, int8_t width, int8_t digits)
6
{
7
#define INT_MAXWIDTH  8 /* -0.12345 */
8
    static char buf[INT_MAXWIDTH + 1];
9
    unsigned len, u = i;
10
    char *str, *end, sign = '+';
11
    int8_t len8;
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
        char c = ' ';
24
        if (--digits == -1)
25
            c = '.';
26
        else if (--len8 >= 0)
27
            c = *--end;
28
        else if (digits >= -2)
29
            c = '0';
30
        else if (sign)
31
            c = sign, sign = 0;
32
        *--str = c;
33
    } while (--width);
34
    
35
    return str;
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
static const __flash uint16_t pows10[] =
4
{
5
    10000, 1000, 100, 10
6
};
7

8
char* u16_to_string (char *str, uint16_t n)
9
{
10
    const __flash uint16_t *p = pows10;
11
    uint8_t not0 = 0;
12
    uint16_t pow10;
13

14
    do
15
    {
16
        char c = '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
    return str;
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...
Angehängte Dateien:
Gast #3851414
Lesenswert?

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.
Persönliche Seite #3851423
Lesenswert?

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:
 
1
;; char* s16_to_string (char *buf, int n)
2
s16_to_string:
3
  tst  r23
4
  brpl u16_to_string
5

6
  neg  r23
7
  neg  r22
8
  sbci r23, 0
9
  movw r26, r24
10
  ldi  r18, '-'
11
  st   X+, r18
12
  cpse 0, 0

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren