Forum: Compiler & IDEs Verständnisfrage GCC bare-metal, Syscalls.c, _write() & __io_putchar() Funktionen


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Ralf (Gast)


Bewertung
0 lesenswert
nicht lesenswert
Hallo,

ich habe eine Verständnisfrage zu den _read()/_write() Funktionen, wie 
sie im GCC in der Syscalls.c Sourcedatei verwendet werden.
Für den STM32 i.V.m. CubeMX wird eine syscalls.c Minimalimplementierung 
bei der Projekterstellung zur Verfügung gestellt.

Die Funktionen _read() und _write() werden im allgemeinen bei allen 
Funktionen, welche auf Dateien zugreifen aufgerufen. printf() greift 
ebenfalls auf _write zu, scanf() analog auf _read(). printf()/scanf() 
benutzen die eindeutig festgelegten Dateinummern für stdout (0) und 
stdin (1), welche an _write() und _read() übergeben werden. Für 
Fehlerausgaben ist die Dateinummer 2 vorgesehen. Alle darüber hinaus 
gehenden Dateinummern hängen vom verwendeten Betriebs- und/oder 
Dateisystem (sofern vorhanden) ab, ist das allgemein soweit korrekt?

Welche Rolle spielen hier nun die Funktionen __io_putchar() und 
__io_getchar(), wie sie in der Minimalimplementierung auftauchen?
int _write(int file, char *ptr, int len) {
  int DataIdx;

  for (DataIdx = 0; DataIdx < len; DataIdx++)
  {
    __io_putchar(*ptr++);
  }
  return len;
}
Dass ich hier mittels einer eigenen __io_putchar() Funktion die Ausgabe 
bspw. auf den UART umleiten kann ist mir klar. Mit der oben gezeigten 
Standardimplementierung würden dann alle Schreibzugriffe, egal mit 
welcher Dateinummer auf dem UART landen.
Mir fehlt das Verständnis, ob die __io_putchar() Funktion nur für die 
Ausgabe auf stdout vorgesehen ist. Ich habe weitere Implementierungen 
der _read()/_write() Funktionen gefunden, welche zumindest mal die 
übergebene Dateinummer auf stdout, stdin und stderr abfragen und bei 
darüber hinausgehenden Dateinummern auf einem bare-metal-System keine 
Ein-/Ausgabe vornehmen. Also eine etwas sauberere Implementierung. Und 
den Aufruf der __io...() Funktionen könnte man sich ja sparen und direkt 
vom UART einlesen bzw. drauf ausgeben.
Ist also der Zweck der __io_putchar()/__io_getchar() Funktionen die 
Ein-/Ausgabe auf einem dedizierten "Medium"? Auf einem Rechner als 
Konsolenprogramm ist die Standardein-/Ausgabe ja eben genau die Konsole. 
D.h. Systeme mit Betriebs-/Dateisystem verwenden in den _read()/_write 
Funktionen für Dateinummern >2 eine eigene Ein-/Ausgabefunktion, bspw. 
eine __file_putchar() Funktion?

Ralf

von Jim M. (turboj)


Bewertung
1 lesenswert
nicht lesenswert
Ralf schrieb:
> D.h. Systeme mit Betriebs-/Dateisystem verwenden in den _read()/_write
> Funktionen für Dateinummern >2 eine eigene Ein-/Ausgabefunktion, bspw.
> eine __file_putchar() Funktion?

Die o.g. _read() und _write() werden nur auf Architekturen ohne 
Betriebssystem verwendet.

Hat man ein Betriebssystem, sind read() und write() eignene System 
Calls, und der OS (-Kern? -Treiber?) kümert sich um die korrekte 
Zuordnung der Daten zum File / Ausgabedevice.

Im minimalen Beispiel oben werden deshalb die File Nummern 
weggeschmissen, weil es (z.B. bei einem UART) ohnehin nur einen 
Eingabe/Ausgabe Kanal gibt.

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


Bewertung
1 lesenswert
nicht lesenswert
Ralf schrieb:
> D.h. Systeme mit Betriebs-/Dateisystem verwenden in den _read()/_write
> Funktionen für Dateinummern >2 eine eigene Ein-/Ausgabefunktion, bspw.
> eine __file_putchar() Funktion?

Kann man selbst für die Deskriptoren 0, 1 und 2 machen, wenn man will. 
Dann muss man nur ein passendes freopen() am Anfang von main() haben, 
damit kann man dann in Abhängigkeit vom Dateinamen ziemlich generisch 
intern arbeiten. Wir interpretieren in unserer Firwmare bspw. Strings 
mit Doppelpunkt am Ende als Gerätetreiber, dabei geht dann auch sowas 
wie "uart0@38400:" oder auch "nul:" :.), alle anderen Dateinamen gehen 
auf die SD-Karte. Das sieht dann bspw. so aus:
int main(void)
{
   // hardware init ...

   // stdin/stdout für Binärdaten (keine CR/LF Wandlung)
   freopen("uart0@38400:", "rb", stdin);
   freopen("uart0@38400:", "wb", stdout);
   // stderr zum Debugging ("\n" -> CR/LF)
#ifdef DEBUG
   freopen("uart1@9600", "w", stderr);
#else
   freopen("nul:", "w", stderr);
#endif

Man muss natürlich dann ein passendes _open() implementieren und intern 
irgendwie über die zurück gegebenen file handles Buch führen.

: Bearbeitet durch Moderator
von Ralf (Gast)


Bewertung
0 lesenswert
nicht lesenswert
Jim M. schrieb:
> Die o.g. _read() und _write() werden nur auf Architekturen *ohne*
> Betriebssystem verwendet.
Widerspricht das...

> Hat man ein Betriebssystem, sind read() und write() eignene System
> Calls, und der OS (-Kern? -Treiber?) kümert sich um die korrekte
> Zuordnung der Daten zum File / Ausgabedevice.
...nicht dieser Aussage? Oben sagst du, sie werden nur ohne OS 
verwendet, und hier werden sie doch wieder verwendet, wobei das OS dann 
eben die Dateinummer etc. abhandelt.
Oder meinst du damit, dass das OS ohne eine eigene Implementierung der 
read()/write() Funktionen entscheiden kann, was wie wohin wandert?

Jörg W. schrieb:
> Kann man selbst für die Deskriptoren 0, 1 und 2 machen, wenn man will.
> Dann muss man nur ein passendes freopen() am Anfang von main() haben,
> damit kann man dann in Abhängigkeit vom Dateinamen ziemlich generisch
> intern arbeiten. Wir interpretieren in unserer Firwmare bspw. Strings
> mit Doppelpunkt am Ende als Gerätetreiber, dabei geht dann auch sowas
> wie "uart0@38400:" oder auch "nul:" :.), alle anderen Dateinamen gehen
> auf die SD-Karte. Das sieht dann bspw. so aus:
>
>> [snip]
>> ...
>> [snap]
Das heisst, in eurer Implementierung gibt's "den" (einen) Dateinamen an 
sich nicht, sondern freopen() ist selbst implementiert und interpretiert 
den Dateinamen und wenn er korrekt umgesetzt werden kann, wird der UART 
passend konfiguriert?

> Man muss natürlich dann ein passendes _open() implementieren und intern
> irgendwie über die zurück gegebenen file handles Buch führen.
Mmmh, okay. Das heisst, freopen() ruft _open() auf? Das sind Funktionen 
welche ich bis dato noch gar nicht beäugt habe, weil ich bisher auch 
kein OS oder Dateisystem verwende.

Danke schonmal an euch beide, ein bisschen ist es jedenfalls schon 
klarer geworden.

Das ganze ist doch die Mechanik der C-Bibliothek, oder? Gibt's dazu 
Literatur, in der man das ganze Prozedere "am Stück" nachlesen kann? Ich 
habe gerade das libc-Handbuch auf gnu.org gefunden, vielleicht hat 
jemand noch weitere Literaturvorschläge?

Ralf

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


Bewertung
1 lesenswert
nicht lesenswert
Ralf schrieb:
> Mmmh, okay. Das heisst, freopen() ruft _open() auf? Das sind Funktionen
> welche ich bis dato noch gar nicht beäugt habe, weil ich bisher auch
> kein OS oder Dateisystem verwende.

Mit dem ARM-GCC benutzt man auf Controllern (also arm-none-eabi) 
typischerweise die newlib. Diese ist ein Konglomerat diverser 
nicht-GPL-lizensierter (sondern liberalerer lizensierter) 
Implementierungsteile, vorrangig aus dem BSD-Umfeld. Naturgemäß setzen 
diese nun auf dem Posix-Syscall-Modell auf, denn das implementiert in 
einem unixoiden System das zugrunde liegende OS bereits. Daher rufen 
fopen() oder auch freopen() halt open() auf (je nach Gusto auch mal mit 
einem vorangestellten Unterstrich, _open()), von denen man nun wiederum 
Posix-Semantik erwartet. Die einzige Erweiterung gegenüber dem 
Posix-Modell ist, dass (wie ich oben angedeutet habe) das f[re]open-Flag 
"b" dort als O_BINARY mit nach unten weiter gereicht wird. Daher kann 
man dieses Flag günstig auch in eigenen Implementierungen verwenden, um 
bspw. im UART-Treiber eine Umwandlung von "\n" nach <CR><LF> 
vorzunehmen. (Unter Posix würde man das ja über einen deutlich 
aufwändigeren Terminaltreiber mit termios realisieren.)

Ähnlich bspw. ist das auch bei malloc() gelagert: die Implementierung 
der Bibliothek ruft hier sbrk() (oder _sbrk()) auf, von der erwartet 
wird, dass sie das "Datensegment" vergrößert. Dort kann man dann einfach 
selbst implementieren, was auch immer man als Heap realisieren möchte.

von Ralf (Gast)


Bewertung
0 lesenswert
nicht lesenswert
Vielen Dank für die Erläuterungen. Sieht aus, als ob ich da noch viel zu 
lernen hab :D

> Mit dem ARM-GCC benutzt man auf Controllern (also arm-none-eabi) typischerweise 
die newlib.
Guter Hinweis mit newlib, ich werd mal in der Richtung nach Lektüre 
gucken. Danke.

Grüße

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [avrasm]AVR-Assembler-Code[/avrasm]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.