Forum: Mikrocontroller und Digitale Elektronik va_args in Funktion übergeben


von Alexander M. (a_lexander)


Lesenswert?

Hallo Zusammen,

Vielleicht will mir jemand helfen, ich finde die Lösung leider nicht...

Folgendes Codebeispiel:
1
void print_test(char *fmt, ...)
2
{
3
  va_list vl;
4
  va_start(vl, fmt);
5
  vprintf(fmt, vl);
6
  va_end(vl);
7
}
8
9
int main(void)
10
{
11
12
print_test("Teststring %d", 11);
13
14
...
15
}

Ich bekomme dort keinen Output, warum?

Danke ;)

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


Lesenswert?

Alexander M. schrieb:
> Ich bekomme dort keinen Output, warum?

Kann man so nicht beantworten.

Das Beispiel sieht korrekt aus.

Vielleicht mal mit vsprintf() probieren und dann im Debugger schauen, 
was passiert?

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Alexander M. schrieb:
> Ich bekomme dort keinen Output, warum?

Bei mir funktioniert Dein Codebeispiel, wenn man noch stdarg.h mit 
einbindet.

Das einzige, was mir auffällt: Du gibst in Deinem Format kein \n an. 
printf (bzw. stdio) buffert im Allgemeinen, bis ein Newline ausgegeben 
wird.

Ich weiß ja nicht, was sich hinter "...." in Deinem Code verbirgt. Kann 
aber sein, dass der Output niemals geflusht wird, weil Du vielleicht 
danach in einer Endlosschleife verweilst.

Probiere daher mal:
1
print_test("Teststring %d\n", 11);    // mit \n
oder
1
fflush (stdout);                      // wenn kein \n gewuenscht

nach Aufruf von print_test().

von Alexander M. (a_lexander)


Lesenswert?

Vielen Dank :)

vsprintf hat alles prima funktioniert, dadurch habe ich den Fehler auch 
gefunden:

Am Ende des Strings hat das "\n\r" gefehlt...

Ich dachte bei fehlendem "\n\r" verweilt der Prozessor, bis er diese 
Eingabe bekommen hat, anscheinend "überspringt" er aber einfach diesen 
String ohne eine Ausgabe...

Danke ;)

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Alexander M. schrieb:
> Am Ende des Strings hat das "\n\r" gefehlt...

Wenn, dann eher "\r\n".

Und meist wird '\n' allein schon auf \r\n gemappt. Von welchem System 
sprechen wir denn? Probiere erstmal '\n'. Erst wenn das nicht reicht, 
dann "\r\n".

> Ich dachte bei fehlendem "\n\r" verweilt der Prozessor, bis er diese
> Eingabe bekommen hat, anscheinend "überspringt" er aber einfach diesen
> String ohne eine Ausgabe...

Nein, er überspringt nicht die Ausgabe, sondern speichert sie zwischen, 
bis:

- ein Newline ausgegeben wird
- das Programm beendet wird.

Du scheinst aber das Programm niemals zu beenden, das nahm ich oben 
schon an, als ich "...." in Deinem Code las.

von Alexander M. (a_lexander)


Lesenswert?

Sehr interessant, danke für die Informationen :)

Verwendeter uC: ATSAMG55

Wo kann ich denn nachlesen, ob automatisch schon ein "\r\n" gemappt 
wird?

Exakt, nach der main kommt noch eine Endlosschleife... ;)
1
....
2
     /* Branch to main function */
3
     main();
4
5
     /* Infinite loop */
6
     while (1);
7
}

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Alexander M. schrieb:
> Wo kann ich denn nachlesen, ob automatisch schon ein "\r\n" gemappt
> wird?

Ausprobieren. Wo gibt das printf() denn seine Ausgabe aus? Auf einem 
UART? Dann wird wahrscheinlich nicht gemappt und Du solltest "\r\n" 
schreiben.

Man kann es aber auch prüfen: Sollte es tatsächlich ein UART sein, dann 
PuTTY starten und schauen, ob nach Ausgabe der Test-Zeichenkette gefolgt 
von '\n' der Cursor

1) nur eine Zeile nach unten
2) eine Zeile nach unten und zusätzlich an den Anfang der Zeile

springt.

Wenn 1: Du solltest immer "\r\n" schicken
Ween 2: Es reicht, ein "\n" zu schicken, wird gemappt auf "\r\n"

Merke:

\r - Carriage Return = Wagenrücklauf -> Cursor an den Anfang der Zeile
\n - Newline - Neue Zeile -> Cursor eine Zeile tiefer

> Exakt, nach der main kommt noch eine Endlosschleife..

Wenn Du die Ausgabe ohne Newline erzwingen willst, kannst Du auch
1
  fflush (stdout);
verwenden. Das forciert die Ausgabe aller zwischengespeicherten Zeichen. 
Siehe auch oben, da hatte ich das schon mal erwähnt.

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


Lesenswert?

Frank M. schrieb:
> Auf einem UART? Dann wird wahrscheinlich nicht gemappt und Du solltest
> "\r\n" schreiben.

Alternative: im UART-Treiber selbst \n auf \r\n mappen. Machen wir hier 
so, es sei denn, der Stream wird mit O_BINARY geöffnet ("wb" statt "w" 
im fopen()).

Funktioniert prima, und man muss nicht quer durch den gesamten Code \r\n 
schreiben. Entspricht letztlich dem ONLCR-Feature von Posix.

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Jörg W. schrieb:
> Alternative: im UART-Treiber selbst \n auf \r\n mappen. Machen wir hier
> so, es sei denn, der Stream wird mit O_BINARY geöffnet ("wb" statt "w"
> im fopen()).

Sehr gut gelöst.

> Funktioniert prima, und man muss nicht quer durch den gesamten Code \r\n
> schreiben. Entspricht letztlich dem ONLCR-Feature von Posix.

Jepp. Absolut sinnvoll.

von Alexander M. (a_lexander)


Lesenswert?

Danke ;)

von Rolf M. (rmagnus)


Lesenswert?

Frank M. schrieb:
> 1) nur eine Zeile nach unten
> 2) eine Zeile nach unten und zusätzlich an den Anfang der Zeile
>
> springt.
>
> Wenn 1: Du solltest immer "\r\n" schicken
> Ween 2: Es reicht, ein "\n" zu schicken, wird gemappt auf "\r\n"
>
> Merke:
>
> \r - Carriage Return = Wagenrücklauf -> Cursor an den Anfang der Zeile
> \n - Newline - Neue Zeile -> Cursor eine Zeile tiefer

Das ist systemabhängig. Windows, Linux und Mac machen das alle drei 
verschieden. Normalerweise wird das dann aber in den I/O-Funktionen 
gemappt, so dass man bei printf() immer nur ein \n braucht. Genau das 
macht ja auch die von Jörg genannte Variante.

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


Lesenswert?

Rolf M. schrieb:
> Windows, Linux und Mac machen das alle drei verschieden.

Mac aber nur bis MacOS 9.

Seit OSX (und damit nun auch schon einige Jahre lang) sind sie da auch 
unixoid.

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Rolf M. schrieb:
> Normalerweise wird das dann aber in den I/O-Funktionen gemappt, so dass
> man bei printf() immer nur ein \n braucht.

Korrekt, das gilt allgemein für Betriebssysteme. Das war schon in den 
80er Jahren unter VAX/VMS so. Aber hier geht es um einen µC. Da wird 
normalerweise nix gemappt.

von Rolf M. (rmagnus)


Lesenswert?

Du hast offenbar den letzten Satz meines Postings übersehen.

Frank M. schrieb:
> Da wird normalerweise nix gemappt.

Je nachdem, denn:

Rolf M. schrieb:
> Genau das macht ja auch die von Jörg genannte Variante.

von foobar (Gast)


Lesenswert?

Zwei Dinge:

1) Die CR/LF-Konvertierung sollte nicht im Mikrocontroller und erst 
recht nicht im C-Lib gemacht werden.  Das von C verlangte einzelne 
Zeichen (\n) als Zeilentrenner, hat sich bewährt - auch als systemweite 
Konvention.  Warum sollte man den CR/LF-Quatsch (den ja sogar Microsoft 
inzw bereut) auch auf dem Mikrocontroller einführen?  Wenn konvertiert 
werden muß, dann so spät wie möglich.  Im typischen Mikrocontrollerfall 
heißt das: im Terminalprogramm - stellt das passend ein und fertig.

2) Dass deine Ausgabe ohne \n nicht erscheint, liegt, wie oben schon 
erwähnt, am Output-Buffering, das deine stdio-Routinen machen.  Du 
kannst das auf unbuffered umschalten (z.B. mittels setbuf(stdout, 
NULL);).  Allerdings solltest du überlegen, ob du in deinem Bootloader 
wirklich full-featured stdio-Routinen mit Buffering brauchst[1].  Sie 
fressen deutlich mehr RAM und ROM als triviale, unbuffered-Versionen 
(wie z.B. AVR-libc, um 1.5kB ROM, ein paar Bytes RAM).



[1] Das Buffering ist bei Betriebssystemen, bei denen der Aufruf der 
Ausgaberoutinen sehr teuer ist (Systemcall, viele 1000 Taktzyklen) 
sinnvoll.  Bei einem Mikrocontroller, bei dem das ein einfacher 
Funktionsaufruf ist, ist es meist unnötig.

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


Lesenswert?

foobar schrieb:

> 1) Die CR/LF-Konvertierung sollte nicht im Mikrocontroller und erst
> recht nicht im C-Lib gemacht werden.  Das von C verlangte einzelne
> Zeichen (\n) als Zeilentrenner, hat sich bewährt

Hat es.

Aber wenn es sich sooo sehr "bewährt" hat – warum dann konvertieren 
Unixe (von denen die Konvention mit \n ja stammt) dann eigentlich bei 
der Terminal-Ein-/Ausgabe und nicht im Terminal selbst?

> [1] Das Buffering ist bei Betriebssystemen, bei denen der Aufruf der
> Ausgaberoutinen sehr teuer ist (Systemcall, viele 1000 Taktzyklen)
> sinnvoll.  Bei einem Mikrocontroller, bei dem das ein einfacher
> Funktionsaufruf ist, ist es meist unnötig.

Ob das Buffering sinnvoll ist oder nicht, hängt nicht von der 
Anwesenheit eines Betriebssystems ab. Gerade erst wieder durch hier: 
Ausgabenachrichten im Binärformat bestehen aus einer Sync-Sequenz, einem 
Header, einer Nachricht und einer CRC. Jedes dieser Teile wird mehr oder 
weniger separat erzeugt. In der ersten Version wurde jeweils danach 
schnell noch ein fflush(stdout) gemacht. Solange das nur auf 'ne UART 
ging, war das noch einigermaßen vertretbar, aber als stattdessen HS-USB 
als Transport heran sollte, ist die Wirkung katastrophal. Da wurden dann 
teilweise einzelne Zeichen in einem kompletten USB-Paket versendet.

Umbau auf fully buffered und fflush() erst am Ende der Nachricht: jetzt 
kommen die ersten xxx KiB alle schön in 512-Byte-Paketen, nur das letzte 
ist kürzer.

Auch alles ohne Betriebssystem … aber man sollte drüber nachdenken. Die 
paar Byte für den stdio-Buffer stören auf einem größeren Mikrocontroller 
kaum. Auf einem ATtiny13 würde ich das natürlich anders sehen.

von Rolf M. (rmagnus)


Lesenswert?

foobar schrieb:
> Zwei Dinge:
>
> 1) Die CR/LF-Konvertierung sollte nicht im Mikrocontroller und erst
> recht nicht im C-Lib gemacht werden.

Solange sie nicht beim Aufruf von printf() gemacht wird. Da gehört es am 
allerwenigsten hin.

> Das von C verlangte einzelne Zeichen (\n) als Zeilentrenner, hat sich
>  bewährt - auch als systemweite Konvention.

C verlangt es allerdings nur im Programm. In was das beim Rausschreiben 
konvertiert wird, ist C egal.

> Warum sollte man den CR/LF-Quatsch (den ja sogar Microsoft inzw bereut)
> auch auf dem Mikrocontroller einführen?

Es schadet auf anderen Systemen nicht.

Jörg W. schrieb:
> Aber wenn es sich sooo sehr "bewährt" hat – warum dann konvertieren
> Unixe (von denen die Konvention mit \n ja stammt) dann eigentlich bei
> der Terminal-Ein-/Ausgabe und nicht im Terminal selbst?

Was meinst du damit? Unixe konvertieren da doch nix.

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


Lesenswert?

Rolf M. schrieb:
> Unixe konvertieren da doch nix.

Selbstverständlich tun sie das.

stty onlcr icrnl

Sie erledigen das tief unten im seriellen Treiber, aber noch generisch 
oberhalb der Hardware.

Bei der von mir beschriebenen Variante erfolgt es dagegen tatsächlich 
erst im Hardware-Treiber (UART bzw. USB/CDC).

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Jörg W. schrieb:
> Aber wenn es sich sooo sehr "bewährt" hat – warum dann konvertieren
> Unixe (von denen die Konvention mit \n ja stammt) dann eigentlich bei
> der Terminal-Ein-/Ausgabe und nicht im Terminal selbst?

Das hat historische Gründe. Die damaligen Teletypes (TTYs, 
schreibmaschinenähnlich) konnten schon immer zwischen Wagenrücklauf (CR) 
und Zeilenvorschub (LF, auch NL) unterscheiden - was auch sinnvoll war. 
So konnte man auch von der aktuellen Spaltenposition wesentlich 
schneller eine Zeile tiefer springen, ohne den Wagenrücklauf zu 
verwenden. Denn dann hätte man noch eventuell Dutzende von SPACEs 
"drucken" müssen, um an dieselbe Spalte zu gelangen.

Die von DEC erfundenen nachfolgenden Terminals wie VT100 bzw. Varianten 
haben diese Tradition fortgeführt. Das passte auch gut zu deren 
Betriebssystemen RSX11 und VMS. Die damit ausgestatteten Rechner PDP11 
bzw. VAX arbeiteten nicht nur auf TTY-Basis, sondern auch bei 
Text-Dateien grundsätzlich mit CR+LF.

Erst UNIX hat das einfache Newline '\n' eingeführt. Daher das ONLCR, mit 
dem man das Mapping für TTYs dann einstellen kann.

EDIT:
Für die nicht-unixoiden Betriebssysteme wurde übrigens erst nachträglich 
das Binary-Flag für fopen() wie "rb" und "wb" eingeführt - um das 
Mapping von '\n' für Binärdateien unterdrücken zu können. UNIX kennt 
dieses Mapping sowieso nicht für Dateien, nur für TTYs. Später wurde das 
Binary-Flag dann auch aus Kompatibilitätsgründen für UNIX/Linux 
eingeführt, obwohl es da überhaupt keinen Effekt hat.

von foobar (Gast)


Lesenswert?

Jörg schrieb:
> Aber wenn es sich sooo sehr "bewährt" hat – warum dann konvertieren
> Unixe (von denen die Konvention mit \n ja stammt) dann eigentlich bei
> der Terminal-Ein-/Ausgabe und nicht im Terminal selbst?

Wegen der nicht perfekten Welt ;-)  Nicht alle Geräte waren frei 
konfigurierbar (einige hatten ja nicht mal Elektronik drin).  Deshalb 
hat man über dem Interface-Layer (UART-Treiber) einen weiteren Layer 
eingeschoben (line-discipline), der einen Teil der gerätespezifischen 
(nicht interfacespezifischen) Eigenschaften wegabstrahierte.  Dazu 
gehörte unter anderem die CR/LF-Konvertierung, aber auch z.B. 
Verzögerungen nach einem 
Wagenrücklauf/Zeilenvorschub/Tabulator/Backspace, 
(XON/XOFF-)Flow-Control, oder der Line-Mode (Minizeileneditor) und 
lokales Echo.

Von dem ganzen bekommt das Programm nichts mit. Es schickt seine \n 
raus, und egal wo die nun hingehen (Datei, Pipe, Drucker, Terminal), es 
geschieht das passende.  Ähnlich beim Input (inkl eventuellen 
Zeileneditor).

> Ob das Buffering sinnvoll ist oder nicht, hängt nicht von der
> Anwesenheit eines Betriebssystems ab. Gerade erst wieder durch hier:
> Ausgabenachrichten im Binärformat bestehen aus einer Sync-Sequenz, einem
> Header, einer Nachricht und einer CRC.

Du weißt aber schon, dass das Verhalten von stdio bezgl des Bufferings 
extrem implementationsabhängig ist; von gar kein Buffering über kleine, 
große bis zu dynamischen Buffern oder gar ein Mix-Betrieb.

> Da wurden dann teilweise einzelne Zeichen in einem kompletten USB-Paket
> versendet.

Dann fix den USB-Treiber: Daten sammeln bis ein Paket voll ist oder ein 
Time-out eintritt.  Der USB-Treiber weiß viel besser, was und wieviel er 
wielange Buffern muß als der stdio-Layer.


Rolf schrieb:
>> Das von C verlangte einzelne Zeichen (\n) als Zeilentrenner, hat sich
>>  bewährt - auch als systemweite Konvention.
>
> C verlangt es allerdings nur im Programm. In was das beim Rausschreiben
> konvertiert wird, ist C egal.

Schon klar, deshalb schrieb ich ja extra, dass das sich aber auch als 
systemweite Konvention bewährt hat.

>> Warum sollte man den CR/LF-Quatsch (den ja sogar Microsoft inzw bereut)
>> auch auf dem Mikrocontroller einführen?
>
> Es schadet auf anderen Systemen nicht.

Das muß von einem Windows-Nutzer sein ;-)

Was heißt schon schaden, es nervt, ist unnötig und an anderer Stelle 
besser zu behandeln.

von S. R. (svenska)


Lesenswert?

foobar schrieb:
> Der USB-Treiber weiß viel besser, was und wieviel er
> wielange Buffern muß als der stdio-Layer.

Der USB-Treiber weiß wesentlich schlechter, wie das zugrundeliegende 
Protokoll funktioniert als der Protokoll-Treiber (der das via stdio 
kommuniziert, weil er von USB nichts weiß).

Es ist nicht Aufgabe der unteren Schichten, mangelhafte Abstraktionen 
zu "reparieren". Denn damit zieht man sich furchtbare 
Kreuzabhängigkeiten in das gesamte System rein und kann sich die 
Abstraktionen irgendwann ganz schenken.

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


Lesenswert?

S. R. schrieb:
> foobar schrieb:
> Der USB-Treiber weiß viel besser, was und wieviel er
> wielange Buffern muß als der stdio-Layer.
>
> Der USB-Treiber weiß wesentlich schlechter, wie das zugrundeliegende
> Protokoll funktioniert als der Protokoll-Treiber (der das via stdio
> kommuniziert, weil er von USB nichts weiß).

So ist es.

stdio macht das Buffering in den verwendeten Bibliotheken (newlib auf 
ARM) gut und erprobt genug, als dass ich mir nicht stattdessen woanders 
neue Fehler einbauen muss.

Wenn ich deren dynamisch allozierten Puffer nicht mag, kann ich allemal 
noch mit setvbuf() einen eigenen statischen hinterlegen.

von W.S. (Gast)


Lesenswert?

Frank M. schrieb:
> Du solltest immer "\r\n" schicken...

Dem schließe ich mich ausdrücklich an. Es ist die einzige wirklich 
saubere Lösung - wenngleich auch etwas Disziplin erfordernd.

All das Gefasel von Zwischenpuffern in stdio und Konvertierungen in 
Layern, die eigentlich nur übertragen sollen und gefälligst NICHTS 
eigenmächtig an dem Datenstrom herumändern dürfen, mach die Dinge nur 
noch komplizierter als sie tatsächlich sind.



S. R. schrieb:
> Es ist nicht Aufgabe der unteren Schichten, mangelhafte Abstraktionen
> zu "reparieren".

Darum geht es beim USB ja auch gar nicht. Der USB ist blockorientiert 
und serielle Kanäle a la stdio sind zeichenorientiert.

Wenn nun ein USB-Treiber nach außen hin zeichenorientiert sich 
darstellen soll (VCP), dann muß er die Umsetzung in den internen 
Blockverkehr irgendwie organisieren.

Also selber zwischenpuffern und dann blockweise übertragen und das 
Hängenbleiben von Zeichen im Zwischenpuffer in geeigneter Weise 
verhindern.



foobar schrieb:
>>> Warum sollte man den CR/LF-Quatsch (den ja sogar Microsoft inzw bereut)
>>> auch auf dem Mikrocontroller einführen?
>>
>> Es schadet auf anderen Systemen nicht.
>
> Das muß von einem Windows-Nutzer sein ;-)
>
> Was heißt schon schaden, es nervt, ist unnötig und an anderer Stelle
> besser zu behandeln.

Tja, frühere Fehler in den Fundamenten haben eben Spätfolgen. Ätsch.

Man hätte damals eben irgend ein anderes Zeichen als Zeilen-Ende sich 
aussuchen sollen. RS ($1E) zum Beispiel.

Aber nein, man mußte ja die direkten Hardware-Codes zum Auslösen des 
Wagenrücklaufs und des Walzendrehens verwenden.

Aber das sind 2 Zeichen - und jeder macht es anders, die einen nur CR, 
die anderen nur LF und nur DOS/Windows macht es eigentlich richtig, 
nämlich beides korrekterweise zu verwenden. CR würde ja nur auf den 
Zeilenanfang setzen und LF würde die Spaltenposition beibehalten.

Es gab ja in den 80er Jahren mal den Versuch, RS als Zeilenende 
einzuführen - aber die superklugen Programmierer haben es verschmäht. 
Ganz schön blöd gewesen.

Und jetzt jammert ihr darüber.

Traurig, traurig, traurig.

W.S.

von W.S. (Gast)


Lesenswert?

Jörg W. schrieb:
> Alternative: im UART-Treiber selbst \n auf \r\n mappen. Machen wir hier
> so, es sei denn, der Stream wird mit O_BINARY geöffnet ("wb" statt "w"
> im fopen()).

oder es sei denn, die Anwendungsschicht will dediziert kein Editieren 
des Ausgabe-Streams im Lowlevel-Treiber, oder.. oder.. oder..

Wenn ihr das bei euch so macht, dann ist das eure Sache, aber es ist 
Pfusch. Ein UART-Treiber soll Zeichen transportieren und nicht 
interpretieren und auch nicht editieren.

W.S.

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

W.S. schrieb:
> Wenn ihr das bei euch so macht, dann ist das eure Sache, aber es ist
> Pfusch. Ein UART-Treiber soll Zeichen transportieren und nicht
> interpretieren und auch nicht editieren.

Wo ist das Problem?
1
     Anwendung
2
         |
3
         v
4
       IO-LIB
5
         |
6
         V
7
     UART-Treiber

Die Anwendung schickt "\n", die I/O-Lib sieht: "Oh, das geht auf ein 
TTY!" und macht "\r\n" draus. Der UART-Treiber bekommt "\r\n" und 
schickt "\r\n".

Ich halte es für durchaus legitim, 2 Schichten in einem kleinen und 
überschaubaren System auch innerhalb eines C-Moduls zu erschlagen. Wenn 
die IO-Lib zum Beispiel sowieso nichts anderes zu tun hat, als 
ausschließlich den UART-Treiber zu bedienen, kann man diese auch 
zusammenfassen, ohne etwas an Funktionalität zu verlieren.

Von daher verstehe ich Deine Aufregung nicht.

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.