Forum: Mikrocontroller und Digitale Elektronik C: format+va_list für printf zwischenspeichern und (viel) später abarbeiten?


von Alexander I. (daedalus)


Lesenswert?

Hallo,

in einem Embedded-Betriebssystem gibt es eine API-Funktion, die Text auf 
einer Debug-Schnittstelle (RS232) ausgeben kann. Damit das ganze 
möglichst komfortabel abläuft, bekommt der Entwickler von der OS-API 
eine Art printf() an die Hand gegeben, mit dem er seine Debugausgaben in 
gewohnter Weise formatieren kann. Das klappt soweit auch alles 
wunderbar. Der Applikationsentwickler kann überall und jederzeit eine 
Debug-Ausgabe erzeugen, z.B. auch innerhalb einer ISR. Da gibt es 
natürlich ein großes Problem: Die Performance wird unterirdisch 
schlecht,
wenn...

1. der String zunächst aufwändig mit printf() formatiert werden muss.
2. der formatierte String laanngsam über die Serielle rausklickert.

Problem 2 ist kein Problem, denn ich kann ja einfach statt printf() ein 
sprintf() nutzen und die gepufferten und formatierten Daten zu einem 
späteren Zeitpunkt von einen niederprioren Debug-Task über die Serielle 
rausschreiben lassen, wenn das Betriebssystem gerade keine anderen 
wichtigen Dinge zu tun hat und/oder einen RS232-Treiber schreiben, der 
über DMA die Daten selbständig rausschaufelt.

Der Grund meines Postings ist Problem 1:
Wie kann ich erreichen, dass die Formatierung erst durch den Debug-Task 
ausgeführt wird und dieser sich aus einem Puffer den Format-String und 
die va_list nimmt und dann erst printf() anwendet und nicht etwa 
innerhalb einer ISR oder einem zeitkritischen Task? Also quasi so, dass 
erstmal alle relevanten Daten für die Debugausgabe (format + va_list) in 
der zeitkritischen Task oder ISR nur zwischengepuffert werden und zu 
einem späteren Zeitpunkt vom Debug-Task mit printf ausgewertet werden.

Ich hab bis jetzt keine Möglichkeit gefunden, die Argumente für printf 
irgendwie zwischenzuspeichern und zu einem beliebigen späteren Zeitpunkt 
zu verwenden? Ich glaube ich suche so eine Art
1
// Dumpen einer va_list in einen Puffer damit das Programm schnell weiterlaufen kann
2
char buffer[100];
3
char outputbuffer[100];
4
Dump_va_list(buffer, va_list args)
5
// (...) viele Instruktionen später in einem weniger zeitkritischen Moment
6
// va_list aus einem Puffer heraus erzeugen
7
Get_va_list(char* buffer, va_list args)
8
// Gepuffertes format + va_list auf printf anwenden und formatierten Text in outputbuffer schreiben.
9
cnt = vsprintf(outputbuffer, format, args);
10
// Formatierten String über RS232 ausgeben.
11
RS232_Write(outpufbuffer, cnt);

Gibt's da was?

Vielen Dank im Vorraus

von Peter (Gast)


Lesenswert?

gar nicht (denke ich)

Das Problem ist ja wenn jemand soetwas macht

char* s = "irgend ein string"

printf("%s", s );

und die willst jetzt die parameter zwischen speicher, dann musst du ja 
auch s komplett kopieren, denn es könnte ja später nicht mehr da sein. 
Dafür musst du aber erstmal den Typvon s ermitteln. Dafür musst du aber 
den Formatstring parsen.

von Εrnst B. (ernst)


Lesenswert?

Ich seh keine Möglichkeit die deutlich schneller wäre als ein sprintf.
allein schon ein
1
printf("String: %s\nPointer: 0x%x\n",charpointer, charpointer);
ist ein Problem: du hast zweimal denselben wert im va_args, einmal musst 
du aber ein strcopy (Wohin?) laufen lassen, einmal nur einen Pointer 
kopieren.

d.H. dein Dump_va_list muss den Format-String kennen, parsen, auswerten.
Damit ist aber der Vorteil gegenüber einem sprintf nur noch minimal, 
wenn überhaupt.

Edit: Peter war schneller ;)

von Andreas F. (aferber)


Lesenswert?

Alexander I. schrieb:
> Gibt's da was?

Nein. Du müsstest den Formatstring dazu parsen und die va_list 
entsprechend auswerten. Die Liste selbst wird beim Beenden der Funktion, 
in der sie erzeugt wurde, ungültig. Um den Inhalt der Liste dumpen zu 
können, bräuchtest du aber Informationen über die Elemente, die bekommst 
du bei printf() aber nur durch den Formatstring. Wenn du den aber 
sowieso parst, kannst du auch gleich die Formatierung fertig machen.

Ich würde in den zeitkritischen Pfaden auf die volle Flexibilität von 
printf() verzichten und mir Debug-Funktionen schreiben, die einen 
Formatstring und fest (z.B.) immer drei Integer-Argumente erhalten (wenn 
eins nicht gebraucht wird dann eben 0 angeben):
1
void rt_dbg_print(const char *fmt, int p1, int p2, int p3);

Der Formatstring entspricht printf(), es können aber eben nur eine 
bestimmte Anzahl Parameter mit vorausbestimmten Typen verwendet werden. 
Diese Parameter können dann ohne weiteres für später abgespeichert 
werden. Nur Vorsicht bei etwaigen Zeigern auf lokale Variablen!

Andreas

von Klaus W. (mfgkw)


Lesenswert?

Zu Peter:
das ist das eine, das andere Problem ist, daß man eine
irgendwie gerettete Parameterliste nicht wieder zu einer
va_list zusammenbauen kann, zumindest kenne ich keinen
halbwegs legalen Weg.

Zumal der Nutzen auch nicht besonders groß wäre, weil
das Parsen und Retten kaum weniger aufwändig wäre als
gleich printf laufen zu lassen.

Ich würde in so einem Fall eher auf printf verzichten.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Unportable Methoden, die auf eine bestimmte Umgebung zugeschnitten
sind, könnte ich mir in gewissem Maße vorstellen: man müsste die
Parameterliste bspw. aus dem Stack in einen Speicherbereich "zur
Seite" legen, wobei man halt so viel auf Verdacht kopieren muss,
dass der maximal mögliche Fall abgedeckt ist.

Dazu muss man aber zuvor erst einmal analysieren, wie die betreffende
C-Implementierung derartige Listen überhaupt hinterlegt.

Wirklich schön ist das trotzdem nicht.

von Stephan M. (stephanm)


Lesenswert?

Alexander I. schrieb:
> Ich hab bis jetzt keine Möglichkeit gefunden, die Argumente für printf
> irgendwie zwischenzuspeichern und zu einem beliebigen späteren Zeitpunkt
> zu verwenden?

Ich denke eine va_list kann man außer mit Assembler-Tricksereien nicht 
einfach so zwischenspeichern. Je nach ABI kann ein Teil der Parameter in 
Registern übergeben werden. Um einen Eindruck von dem, was va_start(), 
va_end() und va_arg() machen kannst Du ja mal einen Blick in die 
aktuelle Manualpage zu stdarg werfen.

Ein möglicher Ausweg wäre, den Formatstring "grob" zu parsen und die 
Argumente in einen "Variant"-Datentyp zu kopieren. In C-ähnlichem 
Pseudocode meine ich etwa sowas:

enum {
  VT_ptr,
  VT_int,
  VT_sonstwas
};

struct VARIANT {
  int type; /* One of the VT_xxx constants */
  union {
    int ival;
    char *ptrval;
    type *someval;
  };
};

struct DELAYED_PRINTF_DATA {
  char *fmt;
  ARRAY_OF_VARIANT args;
};

void fast_debug_printf(const char *fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);
  DELAYED_PRINTF_DATA data;

  while (*fmt) {
    VARAINT current_arg;

    if (*fmt == '%') {
      /* Format spec can be e.g. "%02d". Skip the "%02" stuff to
         get to the conversion specifier (the 'd' in this example) */
      skip_formatting_qualifiers;

      if (*fmt == 'd' || *fmt == 'x' || *fmt == ...) {
        current_arg.type = VT_int;
        current_arg.ival = va_arg(ap, int);
        VARIANT_ARRAY_append(data.args, current_arg);
      } else if (*fmt == 's') {
        currrent_arg.type = VT_ptr;
        current_arg.ptrval = va_arg(ap, char *);
        VARIANT_ARRAY_append(data.args, current_arg);
      } else ........
    };

    fmt++;
  };

  DELAYED_PRINTF_DATA_add_to_round_robin_list(&data);
  return;
};

Stephan

von Grrrr (Gast)


Lesenswert?

Das sehe ich wie meine Vor-"schreiber". Der Aufwand für die Aufbereitung 
ist genauso gross, wie der, wenn Du printf/sprintf direkt aufrufst.
Also kannst Du grundsätzlich nur die Ausgabe verzögern.

von Alexander I. (daedalus)


Lesenswert?

Sowas in der Richtung habe ich schon geahnt. Vor allem eure Beispiele 
mit den Zeigern sind einleuchtend...

Bisher hab ich tatsächlich ein paar vorgefertigte Funktionen definiert 
mit ein paar Integer-Platzhaltern etc. damit man ohne format-parsen 
auskommt.

Ich denke, ich werde die printf-Funktionalität weiterhin lassen aber 
einen deutlichen Hinweis setzen, dass dieser ungewollte Verzögerungen 
verursachen kann. Da es neben mir nur 1-2 andere Entwickler gibt, die 
das nutzen, kann ich die Echtzeit-Verantwortung auch an die Entwickler 
abgeben...

Wo wir gerade beim Thema printf sind: Gibt's ein <xy>sprintf, wo man 
eine Puffer-Größe mit angibt und der Rest einfach abgeschnitten wird 
bevor es einen Pufferüberlauf gibt?

z.B.
1
char buffer[100];
2
ssprintf(buffer, sizeof(buffer), format, args);

von Peter (Gast)


Lesenswert?

Alexander I. schrieb:
> Gibt's ein <xy>sprintf, wo man
> eine Puffer-Größe mit angibt und der Rest einfach abgeschnitten wird
> bevor es einen Pufferüberlauf gibt?
ja gibt es, snprintf

von Alexander I. (daedalus)


Lesenswert?

Danke!

von Knut G. (Gast)


Lesenswert?

Ich habe diese Funktion mit Abstrichen implementiert.
Tatsächlich ist das Umkopieren und Parsen des Formatstrings auf meine 
Plattform (QNX 6.3/PPC) sehr viel schneller als die direkte Fomratierung 
mit snprintf.
Die Einzelzeile benötigt etwa 200 Mikrosekunden für die Formatierung.
Die Umspeicherung benötigt weniger als 20 Mikrosekunden.

Die Ausgabe erfolgt in einem zweiten Thread. Dadurch werden die 
Logausgaben serialisiert ohne das andere Threads lange Warten müssen und 
die Zeit für das Schreiben auf Ramdisk (oder Syslog) fällt dann an, wenn 
der niedrigpriore Prozess die Zeit bekommt.

Bei mehreren Cores kann man im Echtzeitthread ohne ein zu enges 
Formatierungskorsett Meldungen rausschicken, die dann halt ein paar 
hundert Millisekunden später raus geschrieben werden. Das ist bei der 
Fehlersuche sehr hilfreich, da im Falle des Falles Material zu dem was 
geschehen ist vorliegt. Oft lassen sich halt Ereignisse am Testplatz 
nicht hinreichend gut nachstellen. BTW: Klick-Klick-Debuggen ist nicht. 
Das FPGA will 2000mal pro Sekunde ausgelesen werden, sonst gibt es halt 
Fehlerflags und andere annormale Ereignisse.

Die Abstriche: Da sich kein va_args generieren läßt, können nur 
32Bit-Werte konvertieren. Schon bei 64Bit-Integer muß ein 
plattformspezifischer Trick helfen. Double und Float funktionieren 
nicht, da sie in anderer Form übergeben werden.

von Markus F. (mfro)


Lesenswert?

Die va_list ist letztendlich ein Stackformat. Mit (halbwegs) legalen 
C-Mitteln kommt zur Sicherung nur ein setjmp()/longjmp() in Frage, dazu 
müssten aber auch alle über Zeiger referenzierten Variablen gesichert 
werden (sonst sind die möglicherweise anschließend verschwunden).

Ich will nicht ausschließen, daß es geht, aber ich möchte das nicht 
programmieren müssen.

von Rufus Τ. F. (rufus) Benutzerseite


Lesenswert?

Knut G. schrieb im Beitrag #4371546:
> Ich habe diese Funktion mit Abstrichen implementiert.

Du hast Dich auf jeden Fall an die Betreffzeile gehalten:

und (viel) später abarbeiten

Das Thema war vor fünfeinhalb Jahren aktuell. Ob va_arg & Co. so lange mitmachen?

von Eric B. (beric)


Lesenswert?

Alexander I. schrieb:
> in einem Embedded-Betriebssystem gibt es eine API-Funktion, die Text auf
> einer Debug-Schnittstelle (RS232) ausgeben kann. Damit das ganze
> möglichst komfortabel abläuft, bekommt der Entwickler von der OS-API
> eine Art printf() an die Hand gegeben, mit dem er seine Debugausgaben in
> gewohnter Weise formatieren kann. Das klappt soweit auch alles
> wunderbar. Der Applikationsentwickler kann überall und jederzeit eine
> Debug-Ausgabe erzeugen, z.B. auch innerhalb einer ISR.

Ich würde erstens die Benutzung des Debug-printf's in der ISR 
unterbinden. Wenn eine Debugausgabe wirklich gebraucht wird, dann:
- In der ISR die auszugebene Variablen zwischenspeichern und ein Flag 
setzen
- In der main-Loop (oder in einem separaten Task/Thread) Flag abfragen 
und erst dann ausgeben.

Zweitens kann man sich fragen ob eine printf()-ähnliche Debugausgabe im 
embedded-Umfeld überhaupt notwendig ist. Ich bin in vielen Projekten 
schon wunderbar zurechtgekommen mit fest vordefinierte Debugmeldungen. 
Auf der serielle Console wurden nur (binär) Task- oder Modul-Id, 
Funktion Id und  Funktion sub-Id und ggf noch eine Wert mit feste Größe 
(zB 4 Bytes) übertragen.
Am PC lief dann ein kleiner Skript, dass diese Werte ausgelesen hat und 
benutzerfreundlich dargestellt. Damit wird der ganze printf()-Kram am uC 
überflüssig.

Also etwa so:
1
void DBG_Out(uint16 module_id, uint8 func_id, uint8 subid, uint16 param);
2
{
3
  serialOutWord(module_id);
4
  serialOutWord((func_id << 8) | subid);
5
  serialOutWord(param);
6
}
Und dann:
1
// Module ID - identifiziert C-Datei oder OS-Task.
2
#define MODULE_BLAH_ID 0x0123
3
4
// jede Funktion kriegt eine eindeutige ID
5
#define BLAH_FOO_ID    0x01
6
#define BLAH_BAR_ID    0x02
7
8
void foo()
9
{
10
  DBG_Out(MODULE_BLAH_ID, BLAH_FOO_ID, 0u, 0u);
11
}
12
13
void bar(char param)
14
{
15
  if(param < 23)
16
  {
17
     DBG_Out(MODULE_BLAH_ID, BLAH_BAR_ID, 0u, param);
18
  }
19
  else
20
  {
21
     DBG_Out(MODULE_BLAH_ID, BLAH_BAR_ID, 1u, param);
22
  }
23
}
Mehr braucht man nicht. Wirklich.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Eric B. schrieb:
> Alexander I. schrieb:

Nochmal: Alexander schrieb das vor fünf(!) Jahren!

von Eric B. (beric)


Lesenswert?

Rufus Τ. F. schrieb:
> Das Thema war vor fünfeinhalb Jahren aktuell. Ob va_arg & Co. so lange
> mitmachen?

Hm, könnte man Threads nicht irgendwann (zB. nach einer gewissen Zeit 
von Inaktivität, oder nach max 1 Jahr) automatisch schliessen?  Das 
würde solche Ausgrabungen deutlich einbinden :-I

EDIT:
> Nochmal: Alexander schrieb das vor fünf(!) Jahren!

Ja ja, ist mir erst nach deiner Post aufgefallen. Mea maxima culpa etc 
:-)

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

GCC bzw. GNU-C bietet die Möglichkeit, Funktionsaufrufe zu konstruieren:

http://gcc.gnu.org/onlinedocs/gcc-5.2.0/gcc/Constructing-Calls.html

von Klaus R. (klausro)


Lesenswert?

Jörg W. schrieb:
> Nochmal: Alexander schrieb das vor fünf(!) Jahren!

Also, irgendwie verstehe ich euch (Moderatoren) nicht:

1) Wenn ihr nicht wollt, dass man Leichen ausgräbt, dann verhindert dass 
doch einfach! 6 Monate, 1 Jahr, 2 Jahre nach dem letzten Post ist der 
Thread dann einfach Read-Only.

2) Da schreibt jemand eine "neue" Lösung zu einem älteren Problem. Warum
sollte dass nicht interessant sein? Sicher nicht für den ursprünglichen 
TO. Dann kommen noch neue Informationen von Johann und Eric. Finde ich 
gut!
Mal wieder was über den gcc gelernt.

PS: Ich bin sonst wirklich der Meinung, dass ihr einen tollen Job macht! 
Aber das Thema Leichenfledderei von Threads ist einfach nur inkonsequent 
umgesetzt.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Klaus R. schrieb:
> Da schreibt jemand eine "neue" Lösung zu einem älteren Problem. Warum
> sollte dass nicht interessant sein?

Deshalb haben wir sie ja stehen lassen.

Aber es ist jetzt nicht sinnvoll, dem TE noch vermeintlich gute
Ratschläge auf das Problem zu geben, welches er vor 5 Jahren hatte.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.