ich arbeite an einem C-Projekt, bei dem ich die variable Argumentliste (va_list) in einem Arduino Mega2560 verwenden muss. In meinem Code verwende ich die folgende Konstruktion:
// >>>>>>>>> was steht in arg drin, das ist die Frage
15
16
// Nächstes Argument aus der Argumentenliste holen
17
arg=va_arg(args,constchar*);
18
}
19
while(arg!=NULL);
20
21
va_end(args);// Argumentenliste aufräumen
22
}
Ich würde gerne wissen, welche Werte die va_list-Variablen args und num_args auf diesem spezifischen System zurückgeben.
Ich verwende das so:
1
voidCheck_MOD(uint16_tZeilenNr,uint8_tMOD_ix)
2
{
3
jprint("MOD RangeErr in ",ZeilenNr,": ",MOD_ix," ",4711);
4
}
und jprint fasst alle Argumente zusammen.
Kann mir jemand helfen, diese Rückgabewerte richtig zu verstehen für Argumente vom Typ char*, uint16_t und uint8_t und zu erklären, wie sie auf einem Arduino Mega2560 interpretiert werden können? Ich möchte dem Argument keine Info mitgeben, welchen Typ er hat.
Ich möchte dem Argument keine Info mitgeben, welchen Typ er hat.
Das funktioniert nicht. va_arg() funktioniert nur richtig, wenn Du den Typ des Arguments korrekt mitteilst. printf() und Konsorten machen das auch, nämlich durch den Format-String.
Im Kern geht es um die Frage: Was steht in arg?
So, wie Du das aufrufst: Müll. Spätestens bei der Verarbeitung des Arguments 4711, was kein C-String ist.
Gib mal bei Google "man va_arg" ein und lies erstmal das Manual.
ein paar Bits. Ob das als uint16_t oder Pointer übergeben wurde, kann man innerhalb der Funktion nicht mehr feststellen. Insbesondere kann man NULL (das Ende der Liste) nicht von MOD_ix=0 unterscheiden. Eine Notlösung wäre, immer abwechselnd char * und int zu übergeben. Dann wäre der Typ aller Parameter fest und NULL am Ende wieder eindeutig. Trotzdem könnte die Liste beliebig lang sein.
Nachdem die Ähnlichkeit mit printf() nicht zu übersehen ist: was ist der Vorteil von dieser Konstruktion?
ich arbeite an einem C-Projekt, bei dem ich die variable Argumentliste
(va_list) in einem Arduino Mega2560 verwenden muss.
Arduino-Code ist C++-Code, nicht C.
jprint("MOD RangeErr in ",ZeilenNr,": ",MOD_ix," ",4711);
Warum schreibst Du nicht einfach C++:
1
Stringstr=(String)"MOD RangeErr in "+ZeilenNr+": "+MOD_ix+" "+4711;
2
Serial.write(str);// oder irgendeine andere Ausgabefunktion
Du kannst auch auf die Variable str verzichten und das Ganze rechts vom Gleichheitszeichen direkt Deiner Ausgabefunktion übergeben - vorausgesetzt, sie kann mit einem C++-String als Argument umgehen. Notfalls, wenn Du wirklich noch mit "char *" arbeitest, musst Du den C++-String in einen C-String zurückwandeln.
Eine
Notlösung wäre, immer abwechselnd char * und int zu übergeben. Dann wäre
der Typ aller Parameter fest und NULL am Ende wieder eindeutig. Trotzdem
könnte die Liste beliebig lang sein.
Das ist vielleicht eine Idee.
Nachdem die Ähnlichkeit mit printf() nicht zu übersehen ist: was ist der
Vorteil von dieser Konstruktion?
printf() hat einen ziemlich großen Overhead, und das ist auf einem Arduino nicht so toll. Ich programmiere auch auf dem Uno und Nano.
Kann ich printf() auch benutzen, um nur den Ausgabestring zu erzeugen, ohne ihn auszugeben? Also in ein char output[100] ablegen?
Ich möchte dem Argument keine Info mitgeben, welchen Typ er hat.
Wie schon gesagt wurde, geht das mit varargs nicht. Du musst in deiner Funktion beim Aufruf von va_arg() angeben, welchen Typ das Argument hat. Es liegt also an dir, den Typ zu kennen und korrekt anzugeben. Wie du an den Typ kommst, ist dir überlassen.
In C++ könnte man sowas mit einem variadischen Template machen.
Den hat es vor allem deshalb, weil es eben "allen Furz und Feuerstein" unterstützen muss und erst zur Laufzeit weiß, was davon wirklich gebraucht wird.
Daher gibt es in der avr-libc auch noch Varianten ohne Gleitkomma-Support sowie völlig minimalistische, bei denen noch einige andere Features weggelassen wurden.
Gerade auf dem ATmega2560 würde ich mich aber an deiner Stelle nicht verrenken und erst einmal so lange printf (oder sprintf) benutzen bis klar ist, dass wirklich die Säge klemmt und auch diese zu viel sind. Davor lohnt sich der Bau eigener Ersatzfunktionen eigentlich eher nicht, zumal du ja immer das Risiko eingehst, statt relativ gut debuggter Software eigene neue Fehler einzubauen.
Für das Beispiel aus dem ersten Beitrag würde auch etwas wie itoa()
ausreichen.
Würde es? Soweit ich verstehe, zeigt es eine Ausgabefunktion im Stile von printf (nur ohne Formatstring), die entsprechend auch mit mehreren Argumenten von unterschiedlichem Typ zurecht kommen und das dann zu einem Gesamtstring zusammenbauen soll.
In C++ wäre es ziemlich einfach mit variadischen templates. Hier gezeigt mit Unterstützung für signed Integers, unsigned Integers, char, char*, String Literale. Man kann beliebig eigene "convert" Funktionen hinzufügen für weitere Typen (z.B. float oder auch Klassen). Es wird automatisch auf Buffer Overflows geprüft. Die Ausgabe erfolgt testweise auf stdout, kann natürlich beliebig geändert werden. Es ist potentiell auch effizienter als sprintf, weil kein Parsen eines Format-Strings und Laufzeitunterscheidung nötig ist sowie keine komplizierte Stack-Akrobatik (va_arg).
Das Atmel Studio kann auch C++, du musst vermutlich nur die Datei mit der Endung .cpp speichern.
Für das Beispiel aus dem ersten Beitrag würde auch etwas wie itoa()
ausreichen.
Würde es? Soweit ich verstehe, zeigt es eine Ausgabefunktion im Stile
von printf (nur ohne Formatstring), die entsprechend auch mit mehreren
Argumenten von unterschiedlichem Typ zurecht kommen und das dann zu
einem Gesamtstring zusammenbauen soll.
Naja, wenn va_list für die ursprüngliche Idee funktionieren würde, brauchte man doch kaum mehr als itoa() und strncat(). Aber snprintf() ist wirklich vernünftiger, besonders, wenn es abgespeckte Varianten gibt.
Wenn die immer noch zu groß sind, würde ich eher einen eigenen snprintf()-Ersatz bauen, kleiner, aber voll kompatibel. Schon alleine, weil der gcc dann nette Warnungen ausgeben kann
Aber ob das Ergebnis dann wirklich ressourcenschonender wird als
snprintf()?
Schon möglich. Hängt davon ab wie gut der Compiler es optimiert bekommt (inlining & Co). Eventuell wird bei zahlreichen Aufrufen der Code länger, aber die Ausführungszeit dürfte kürzer sein. Man könnte alternativ auch den Format-String per templates automatisch generieren und dann snprintf aufrufen ;-)
An den Integer-Convert-Funktionen kann man bestimmt auch noch was optimieren, hab da einfach die erstbeste Implementierung hin geschrieben. Aber das ist ein gelöstes Problem
ohne snprintf: Program Memory Usage : 33284 bytes
Data Memory Usage : 6690 bytes
mit snprintf: Program Memory Usage : 34796 bytes
Data Memory Usage : 6714 bytes
Ich habe das Makro P() dazugenommen. P() ist ein Makro, das den String im PROGMEM platziert und einen const char* zurückgibt. Ich verwende es so:
1
snprintf(jou,sizeof(jou),P("MOD RangeErr in %d: %d"),ZeilenNr,MOD_ix);
Dadurch reduziert sich der Verbrauch des RAM, und das ist ja der Knackpunkt beim Arduino.
mit P(): Program Memory Usage : 34810 bytes
Data Memory Usage : 6690 bytes
Der String in jou ist "MOD RangeErr in 1945: 1", so wie ich es möchte.
Davor lohnt sich der Bau eigener Ersatzfunktionen eigentlich eher nicht,
zumal du ja immer das Risiko eingehst, statt relativ gut debuggter
Software eigene neue Fehler einzubauen.
Ja, das habe ich auch nur früher gemacht, wo Atmel noch recht geizig mit Flash und RAM war (AT90S2313, AT90S4433, ATtiny15, ATtiny26).
Dann musst du aber auch snprintf_P() benutzen. Das ist die Variante der
Funktion, die den Format-String im Progmem erwartet.
Habe ich getestet: verbraucht mehr RAM als meine Version.
Dann solltest du, statt hier irgendwie herum zu stochern, das genau analysieren.
Format-String im Flash braucht die _P-Funktionen, sonst kommt Käse raus. Die normale s[n]printf-Funktion hat schlicht keine Idee, dass der ihr übergebene Zeiger in den Flash zeigen soll, sodass sie an der gleichen Adresse im RAM stattdessen liest.
Intern geht das eh alles auf das gleiche Backend (vfprintf), das wird nur mit unterschiedlichen Flags aufgerufen je nach dem benutzten Frontend. Kannst du gut erkennen, wenn du dir den Sourcecode ansiehst:
1
% ls -sk
2
total 367
3
5 Files.am 5 fscanf.c 5 sprintf_p.c
4
45 Makefile 5 fscanf_p.c 5 sscanf.c
5
5 Makefile.am 5 fwrite.c 5 sscanf_p.c
6
49 Makefile.in 5 getc.S 5 stdio_private.h
7
5 Rules.am 5 getchar.c 9 ultoa_invert.S
8
5 clearerr.c 5 gets.c 5 ungetc.c
9
5 fclose.c 5 iob.c 21 vfprintf.c
10
9 fdevopen.c 5 printf.c 5 vfprintf_p.c
11
5 feof.c 5 printf_p.c 25 vfscanf.c
12
5 ferror.c 5 putc.S 5 vfscanf_p.c
13
5 fgetc.c 5 putchar.c 5 vprintf.c
14
5 fgets.c 5 puts.c 5 vscanf.c
15
5 fprintf.c 5 puts_p.c 5 vsnprintf.c
16
5 fprintf_p.c 5 scanf.c 5 vsnprintf_p.c
17
5 fputc.c 5 scanf_p.c 5 vsprintf.c
18
5 fputs.c 5 snprintf.c 5 vsprintf_p.c
19
5 fputs_p.c 5 snprintf_p.c 5 xtoa_fast.h
20
5 fread.c 5 sprintf.c
Gut zu erkennen, dass der komplette Code für stdio in den beiden Implementierungen für vpfprintf.c und vfscanf.c steckt. Alles andere sind Wrapper darum.
Ich habe nur versucht nachzuvollziehen, was du hier erzählt hast, und ich weiß, wie der stdio-Code gestrickt ist, da ich insbesondere diesen Teil mit den vfprintf/vfscanf-Backends und den diversen Wrappern selbst entworfen habe (auch wenn Dmitry Xmelkov dann in den Backends vieles optimiert hat).
Bei mir funzt es bestens
Dann machst du was anderes als was ich deinen Ausführungen entnommen habe.
Das braucht aber trotzdem mehr RAM als die Lösung mit sprintf_P() – nur auf dem Stack, dadurch fällt dir das nicht direkt auf. Es gibt aber eigentlich keinen Grund, das erst in den RAM umzukopieren, da eben vfprintf() in der Lage ist, den Format-String auch gleich direkt aus dem Flash zu lesen.
Warum??? Es ist umständlich, gefährlich, langsam, braucht zusätzlichen (Stack-/)Speicher. Einen stdio-Stream zu erstellen ist so trivial (Beispiel-wrapper für uart_putc/getc):
fehlerhaft - es liefert eine Adresse, die beim Erreichen der "}" ungültig wird. Dazu kommt noch, dass str mehrmals ausgewertet wird. Und lokale var-arrays ohne Längenbeschränkung sind (insb auf Systemen ohne dynamischen Stack, wie Mikrocontrollern) eh Buggy.
PS: ne +1 fehlt auch noch.
PPS: Zu meinen, allein durch Benutzung von "n"-Funktionen (snprintf, strncpy, etc), "sicher" zu programmieren, ist ein Irrtum - man verschiebt nur den Angriffsvektor: von Buffer overflow nach String truncation.
Wie ich weiter oben schrieb: eigene Versuche produzieren meist eine höhere Bug-Wahrscheinlichkeit, als wenn man gleich die "gut abgehangene" fertige Implementierung benutzt. Allein der zusätzliche RAM-Bedarf für das Umkopieren des Strings frisst ja den durch den im Flash platzierten Format-String angestrebten Vorteil zur Hälfte schon wieder auf.
Ich finde ja das Konzept der generic Pointer beim Keil C51 genial.
Der Programmierer muß keinerlei Rücksicht darauf nehmen, in welchem Segment eine Variable angelegt wurde. Ein zusätzliches Byte des Pointers merkt sich den Segmenttyp und es wird automatisch zu dem richtigen Befehl compiliert. Läßt sich das zur Compilezeit noch nicht auflösen, wird eben eine Unterfunktion aufgerufen, die dann das Typ-Byte prüft und entsprechend den Zugriff ausführt.
Ohne irgendwelche Sonderlocken schreiben zu müssen, wird der Formatstring des printf automatisch im Flash abgelegt.
Ich finde ja das Konzept der generic Pointer beim Keil C51 genial.
Außer halt, dass es Overhead kostet, sowohl im Zeiger selbst als auch für die Auswertung zur Laufzeit. Ist aber wurscht: hat beim AVR keiner implementiert und wird auch keiner mehr implementieren.
Außer halt, dass es Overhead kostet, sowohl im Zeiger selbst als auch
für die Auswertung zur Laufzeit.
Das hatte selbst bei 12MHz / 12 = 1MIPS nicht sonderlich gestört. Die
Ableserate des Menschen ist oftmals der Flaschenhals.
In diesem Falle schon, aber generic pointer waren ja ein allgemeines Konzept, nicht nur hinsichtlich stdio, oder?
Aber wie schon geschrieben: wird bei AVR keiner mehr machen. Zu viel Sonderlocke, und die Domäne für größere Flashs geht eh klar in Richtung ARM oder RISC-V. Bei kleineren Flashs wiederum gibt es mittlerweile die Option, den Flash in den normalen Adressbereich einzublenden, da braucht man solche Verrenkungen dann auch nicht mehr.
Zumindest kennt der Compiler __memx, außer für reduced Tiny.
Müsste sich aber trotzdem jemand hinsetzen, dafür die Bibliotheksfunktionen zu machen. Da fände ich 64-bit Gleitkomma in libm deutlich interessanter. ;)
Ist aber wurscht: hat beim AVR keiner
implementiert und wird auch keiner mehr implementieren.
Ja, für die ollen 8-Bitter macht keiner mehr was.
Du musst mir die Worte nicht im Mund rumdrehen. Microchip konstruiert nach wie vor neue AVRs am laufenden Band, aber sie müssen natürlich Dinge produzieren, die sich auch verkaufen lassen. Da haben sie wohl erkannt, dass es jenseits von 128 KiB Flash (die man noch ohne irgendwelche Verrenkungen adressieren kann) keinen Sinn hat, auf 8-Bitter zu setzen.
Microchip konstruiert
nach wie vor neue AVRs am laufenden Band
Ja, das habe ich auch nicht verstanden, die 0-, 1-, 2-Serie mit nur minimalen Unterschieden in schneller Folge rauszuhauen. Ich vermute darin öfteren Führungswechsel, wo jeder seine Duftmarke setzen will.
Ich werde schon schief angesehen, wenn ich noch was mit dem AVR machen will. Bisher eingesetzte Typen haben Bestandsschutz, weitere anzulegen ist verboten.
Für das aktuelle Projekt ist ein i.MX RT1176 vorgesehen.
https://www.embeddedartists.com/products/imx-rt1176-ucom/
Ich vermute darin öfteren Führungswechsel, wo jeder seine Duftmarke
setzen will.
Ich vermute, dass deine Vermutung reichlich daneben ist.
Die Entwicklung eines IC kostet Millionen – selbst, wenn man vielleicht auf eine Art „Baukastensystem“ für die IP-Blöcke zurück greifen kann. Diese Millionen muss der Chip dann auch erstmal einspielen, ansonsten wäre ziemlich schnell Schicht im Schacht.
Das Maxim-Prinzip (viele Ideen auf MPWs und dann erst gucken, was sich verkaufen lässt, den Rest wieder einstampfen) scheint bei Microchip nun eher nicht vorzuherrschen.
Da wundert es mich aber doch, warum sie in kurzer Folge die 8-Pinner ATtiny12, 15, 22, 13, 25-85 rausgebracht haben. Ich habe sie alle gekauft und mich geärgert, als die besseren verfügbar waren. Nun liegen sie rum, außer dem ATtiny25, der alle Vorgänger ersetzen kann.
Und mit den neuen Serien geht dieses Spielchen ja munter weiter.
Nur der ATmega328PB ist irgendwie ein Ausreißer, da ist recht viel hinzugekommen.
Da wundert es mich aber doch, warum sie in kurzer Folge die 8-Pinner
ATtiny12, 15, 22, 13, 25-85 rausgebracht haben.
Da schmeißt du aber einige Jahre, wenn nicht Jahrzehnte in einen Topf.
25 … 85 (die letztlich eine IC-Entwicklung erstmal sind) ist eine Art Nachfolger / Ablösung des 15 gewesen, mehr oder weniger aufwärtskomptabibel.
12 (und dessen Bruder 11) waren wohl die ersten Tinys überhaupt, mit dem 13 hat man dann ein deutlich universelleres Teil nachgeliefert, das bis heute noch Bestand hat.
22 … keine Ahnung. Ich glaube, ich habe mal einen in den Fingern gehabt. Möglich, dass der eher ein Verlustgeschäft für Atmel war damals. Auch möglich, dass da ein einzelner großer Kunde als Auftraggeber dahinter stand, sowas ist nicht ungewöhnlich in der IC-Industrie.
Man kann nicht mal so dumm denken wie manche Führungsetage ist!
Frage vom Abteilungsleiter:
"kennen sie sich aus mit Commodore mit kleiner Tastatur? (PET2001 mit Datasette)"
Anwort vom Bewerber "ja (und meinte den Commodore Taschenrechner)"
Vom 6502 und Innenleben hatte er NULL Ahnung, auch nicht das man beim Programmieren ENTER drückt und nicht mit den Cursortasten in die nächste Zeile geht.
Ein anderer Entscheider stolz wie Bolle, er hat seinen ersten Dr.Ing eingestellt, einer der Prüfautomaten und Programme baute die auch ohne angeschlossenen Prüfling gut testeten!
Keine Jahrzehnte, aber schon einige Jahre dazwischen. Bedenke auch, dass die die Anfangszeit der AVRs war. Der AT90S1200 hatte das Feld ausgelotet (kurz danach der ATmega103), ab da konnte man dann Feedback von potentiellen und realen Kunden sammeln.
Dass eine Entwicklung einer kompletten MCU-Serie "from scratch" zu Anfang defizitär ist, dürfte normal sein.
Ich würde gerne wissen, welche Werte die va_list-Variablen args und
num_args auf diesem spezifischen System zurückgeben.
num_args kann so nicht verwendet werden, und eine Dokumentation / Beispiel, die das so verwendet, kann du getrost in die Tonne kloppen. Der Typ von args wird durch den 2. Parameter von va_arg festgelegt.
Am einfachsten verwendet man in solchen Fehlerausgaben die printf Funktionen die Libc:
Der Format-String von error() muss dabei ein gültiges Argument für PSTR() darstellen. Oder man lässt das PSTR für den Format-String weg, aber dann belegt der String i.d.R. RAM.
[v]fprintf ist ein zielmlicher Brummer, und bei Platzproblemen -- die man auf einem ATmega2560 eher weniger hat -- implementiert man ienfach eine abgespeckte Version, die nur das nötogste mitbringt wie %s, %S, %d, %u und %x.
implementiert man einfach eine abgespeckte Version, die nur das nötigste
mitbringt wie %s, %S, %d, %u und %x.
Wobei es ja schon eine abgespeckte Variante gibt, die man erstmal
stattdessen probieren kann. ;-)
Ja, aber das printf aus der Libc ist immer noch ziemlich fett. Das liegt nicht zuletzt auch dran, dass viele Spezialitäten wie %*, %+, %-, %0 etc unterstützt werden, die man meist nicht braucht; schon gar nicht bei Fehlerausgabe.
Ich bin kein Freund von Logging per printf/fprintf/snprintf…
Sie kosten einiges an Platz und die Formatierung zur Laufzeit braucht Zeit und ist fehleranfällig (wobei es ja Compiler gibt, die Forrmierstring Fehler erkennen.)
Wenn möglich würde ich (wie oben schon empfohlen) eher auf C++ variadische Template umsteigen. Das ist flexibler und ressourcenschonender.
Unter C11 kann man so etwas auch per Makros und _Generic() nachbauen, geht dann aber nur mit C-Compiler die C11 unterstützen und sieht irgendwie gemurkst aus….
Unter C11 kann man so etwas auch per Makros und _Generic() nachbauen,
geht dann aber nur mit C-Compiler die C11 unterstützen und sieht
irgendwie gemurkst aus….
Unter C11 kann man so etwas auch per Makros und _Generic() nachbauen,
geht dann aber nur mit C-Compiler die C11 unterstützen und sieht
irgendwie gemurkst aus….
Ich bin kein Freund von Logging per printf/fprintf/snprintf…
Sie kosten einiges an Platz und die Formatierung zur Laufzeit braucht
Zeit [...]
Mit dem Platz ist relativ: bei vielen libc-stdio-Implementation zieht die Benutzung von sprintf gleich das gesamte stdio-Subsystem mit rein - bei shared-libs kein Problem, bei statischen Builds wird's eins, ja. Der meiste Platz dürfte aber für die Float-to-String conversion drauf gehen (eine qualitativ hochwertig Implementation frißt mehrere dutzend kB, wenns schnell sein soll auch mal deutlich über 100kB).
Die avr-libc ist eine mMn sehr gute Implementation für µC - die Grundroutinen sind schlank, sie ziehen keinen Rattenschwanz an abhängigen Funktionen rein und die FP-version ist optional.
Und meist sieht es ja so aus: anfangs nur einen String ausgeben - wozu printf, einfach println o.ä. Aber je mehr das Projekt wächst, umso mehr steigen die Anforderungen und man schreibt sich immer mehr Hilfsroutinen (für signed/unsigned dezimal-Zahlen, hex-Zahlen, fix-width, etc) und bevor man sich versieht, hat man sprintf neu erfunden.
Und was die Geschwindigkeit angeht: printf ist nicht langsam! Das Bottleneck ist üblicherweise die anschließende Ausgabe, nicht die Formatierung (bei Ausgabe über UART sowieso).
Wenn möglich würde ich (wie oben schon empfohlen) eher auf C++
variadische Template umsteigen. Das ist flexibler und
ressourcenschonender.
Flexibler mag sein, das mit dem ressourcenschonender möchte ich bezweifeln. Statt in einer zentralen Formatierungsfunktion findet bei den Templates die Formatierung bei jedem einzelnen Aufrufer statt. Sieht kurz auf, es wird aber für jedes Argument extra Code generiert. Das läppert sich ... Gewinnen kann man da nur, wenn die Argumente statisch sind, so dass der Compiler das wegoptimieren kann.
Und meist sieht es ja so aus: anfangs nur einen String ausgeben - wozu
printf, einfach println o.ä. Aber je mehr das Projekt wächst, umso mehr
steigen die Anforderungen und man schreibt sich immer mehr Hilfsroutinen
(für signed/unsigned dezimal-Zahlen, hex-Zahlen, fix-width, etc) und
bevor man sich versieht, hat man sprintf neu erfunden.
Flexibler mag sein, das mit dem ressourcenschonender möchte ich
bezweifeln. Statt in einer zentralen Formatierungsfunktion findet bei
den Templates die Formatierung bei jedem einzelnen Aufrufer statt.
Das kommt auf die Implementierung an. Wenn für jeden Parameter ein (überladener) Funktionsaufruf genutzt wird, ist der Speicherplatz nicht viel größer als bei ...printf Übergabe, man spart aber die Dekodierung des Formatstrings und ist typsicher. Klar man hat mehrere Funktionsaufrufe,
aber das parsen des Formatstrings kostet auch Zeit.