Forum: Compiler & IDEs arm-gcc, newlib und iostreams


von Tobias P. (hubertus)


Lesenswert?

Hallo zusammen

ich verwende den arm-none-eabi-gcc um mein Programm zu kompilieren für 
meinen STM32F407. Ich benutze streams für Input und Output auf 
verschiedenen UARTs.
Konkret habe ich den stdout, stdin und stderr auf dem UART1, sodass bei 
der Verwendung von printf() und Konsorten eine Ausgabe von Text über den 
UART1 passiert. Soweit so gut. Das geschieht über die read() und write() 
Syscalls:
1
_ssize_t _write_r(struct _reent *r, int file, const void *ptr, size_t len)
2
{
3
  switch(file)
4
  {
5
    case STDOUT_FILENO:
6
    case STDERR_FILENO:
7
    {
8
      const unsigned char *p = (const unsigned char*)ptr;
9
      for(int i = 0; i < len; i++)
10
      {
11
        if(*p == '\n')
12
        {
13
          txchar('\r');
14
        }
15
        txchar(*p++);
16
      }
17
      return len;
18
    }
19
20
    default:
21
    {
22
      r->_errno = EBADF;
23
      return -1;
24
    }
25
  }
26
}
27
28
_ssize_t _read_r(struct _reent *r, int file, void *ptr, size_t len)
29
{
30
  switch(file)
31
  {
32
    case STDIN_FILENO:
33
    {
34
      extern int kbhit(void);
35
      int numread;
36
      char* dest = (char*)ptr;
37
38
      for(numread = 0; numread < len; numread++)
39
      {
40
        int rxchar = kbhit();
41
        if(rxchar >= 0)
42
        {
43
          dest[numread] = (char)rxchar;
44
        }
45
        else
46
        {
47
          r->_errno = EIO;
48
        }
49
        break;
50
      }
51
      return numread;
52
    }
53
54
    default:
55
    {
56
      r->_errno = EBADF;
57
      return -1;
58
    }
59
  }
60
  return 0;
61
}

Jetzt möchte ich aber auf dem UART2 ebenfalls Daten ausgeben, und dazu 
wenn möglich auch die C Standardfunktionen nutzen. Ich habe das jetzt 
wie folgt gelöst:
1
FILE* uart2 = fdopen(UART2_FD, "w");
2
setvbuf(uart2, NULL, _IONBF, 0);
3
fprintf(uart2, "This message goes to UART2");
4
printf("This message goes to UART1");

die write()-Funktion habe ich entsprechend angepasst:
1
_ssize_t _write_r(struct _reent *r, int file, const void *ptr, size_t len)
2
{
3
  switch(file)
4
  {
5
    case STDOUT_FILENO:
6
    case STDERR_FILENO:
7
    {
8
      const unsigned char *p = (const unsigned char*)ptr;
9
      for(int i = 0; i < len; i++)
10
      {
11
        if(*p == '\n')
12
        {
13
          txchar('\r');
14
        }
15
        txchar(*p++);
16
      }
17
      return len;
18
    }
19
20
    case UART2_FD:
21
    {
22
      return uart2_tx(ptr, len);
23
    }
24
25
    default:
26
    {
27
      r->_errno = EBADF;
28
      return -1;
29
    }
30
  }
31
}

Das funktioniert wunderbar, aber es würde mich interessieren ob meine 
Vorgehensweise mit fdopen() richtig ist, oder ob man das anders machen 
muss. Wie macht man es "richtig" ?

von Bauform B. (bauformb)


Lesenswert?

Tobias P. schrieb:
> wenn möglich auch die C Standardfunktionen nutzen

Dann möchte man das ja so schreiben können
1
FILE* uart2 = fopen("/dev/usart2", "w");
was ja praktisch das gleiche ist wie
1
int  uart2_fd = open ("/dev/usart2", O_WRONLY, 0);
2
FILE *uart2 = fdopen (uart2_fd, "w");
Normalerweise wird dabei fd vom open() fortlaufend vergeben, kann also 
nicht konstant sein. Aber man soll es ja nicht übertreiben. Genauso gut 
kann open() einfach den Devicenamen 1:1 zu einem festen fd mappen.

Die UARTs müssen in jedem Fall initialisiert werden, also brauchst du 
immer irgendwo irgendeine Art von open(). Der Mehraufwand für ein 
"echtes" open() scheint mir nicht allzu groß zu sein, nur ein paar 
strcmp und das passende int abliefern. Dafür kann man dann ganz normal 
fopen() statt fdopen() benutzen. Ein Bayer würde sagen "G'scheit oder 
garnicht" ;)

Was mich mehr irritiert ist die total unterschiedliche Behandlung von 
UART1 und UART2 im write(). Mit einem Pointer auf USART1 oder eben 
USART2 könnte der ganze Rest für beide komplett identisch sein. 
Einfaches Senden und Empfangen funktioniert für alle UART, USART und 
LPUART gleich.

von Tobias P. (hubertus)


Lesenswert?

Hallo,

Bauform B. schrieb:
> Was mich mehr irritiert ist die total unterschiedliche Behandlung von
> UART1 und UART2 im write(). Mit einem Pointer auf USART1 oder eben
> USART2 könnte der ganze Rest für beide komplett identisch sein.
> Einfaches Senden und Empfangen funktioniert für alle UART, USART und
> LPUART gleich.

da hast du natürlich vollkommen recht und das ist auch so angedacht. 
Mein Code ist im Moment noch nicht komplett auf einem sauberen Stand: 
ich arbeite immernoch an einem GPSDO, mit dessen Software ich vor ca. 2 
Jahren begonnen habe, daher sind da noch alte Sachen drin.

Ich frage mich grade, ob z.B. die Initialisierung von stdout vielleicht 
noch besser so zu lösen wäre, dass der zugehörige UART mittels einer 
Funktion mit
1
__attribute__((constructor))

gelöst werden sollte. Dann sind nämlich stdin und stdout schon bereit, 
bevor main() startet.
Oder wie würdest du denn stdout behandeln? denn da geht die C-Umgebung 
ja einfach davon aus, dass es "funktioniert" und nicht erst noch 
initialisiert werden muss. Wenn aber stdout über einen UART gehen soll, 
muss man da sehr wohl zuerst noch einiges initialisieren.

von Bauform B. (bauformb)


Lesenswert?

Tobias P. schrieb:
> Ich frage mich grade, ob z.B. die Initialisierung von stdout vielleicht
> noch besser so zu lösen wäre, dass der zugehörige UART mittels einer
> Funktion mit __attribute__((constructor))
> gelöst werden sollte. Dann sind nämlich stdin und stdout schon bereit,
> bevor main() startet.

Muss man sich überhaupt darum kümmern? Das muss doch alles von newlib, 
libgloss, gcc, libgcc erledigt werden. Oder stelle ich mir das viel zu 
einfach vor?

Für C++ müssen vor main() so viele Dinge richtig zusammen spielen, dass 
ich mich strikt an die Gebrauchsanweisung der newlib halten würde. 
Demnach muss man doch nur ein paar genau definierte syscalls¹ wie open() 
und write() bereit stellen und sonst nichts. Und auf der Ebene muss doch 
auch ANSI-C ausreichen, also write() statt write_r()?

Für stdout wird dieses open() (hoffentlich) von einer lib vor main() 
aufgerufen und initialisiert daraufhin UART1. Später kann man es genauso 
für UART2 usw. benutzen.

Dieses open() muss ziemlich viel Hardware initialisieren. Das UART läuft 
dann mit 9600, 8N1 mit dem Takt vom HSI16. Die Umschaltung auf einen 
anderen Takt wird ja traditionell erst in main() gemacht. Danach stimmt 
dann die Baudrate nicht mehr, aber die muss ja sowieso mit ioctl() o.ä. 
einstellbar sein.


1) https://sourceware.org/newlib/libc.html#Syscalls

: Bearbeitet durch User
von Tobias P. (hubertus)


Lesenswert?

Bauform B. schrieb:
> Muss man sich überhaupt darum kümmern? Das muss doch alles von newlib,
> libgloss, gcc, libgcc erledigt werden. Oder stelle ich mir das viel zu
> einfach vor?

Funktionen, die du mit __attribute__((constructor)) kennzeichnest, 
werden (korrektes Linkerscript vorausgesetzt) in das preinit_array rein 
geschrieben. Der Startupcode, welcher für die Newlib nötig ist, muss vor 
dem Aufruf von main() schauen, ob es im preinit_array Einträge drin hat, 
und wenn ja, muss er die alle aufrufen. Das geschieht über die funktion 
__libc_init_array(). Wenn du einen UART initialisieren willst, dann 
kannst du entweder die Initialisierungsroutine aus main() heraus 
aufrufen, oder sie als constructor deklarieren, dann wird sie 
automatisch aufgerufen, bevor main() dran kommt. Oder, eben, wie du 
sagst, man könnte die Initialisierung auch im open() Aufruf machen.

Bauform B. schrieb:
> Für C++ müssen vor main() so viele Dinge richtig zusammen spielen, dass
> ich mich strikt an die Gebrauchsanweisung der newlib halten würde.

Wo findet man die eigentlich? was man vor main() alles aufrufen muss, 
und wie und welche Daten kopiert oder auf 0 gesetzt werden müssen und 
welche Sektionen es im Linkerscript dafür alle braucht, habe ich mir 
mühsam zusammen suchen müssen, ich habe nirgends an einer zentralen 
Stelle eine gute Dokumentation dazu gefunden. Ich habe aber das mit C++ 
begonnen genauer anzuschauen und teste grade einen C++ Code auf dem 
STM32. Konstruktoren, Destruktoren, globale Instanzen von Klassen, das 
alles funktioniert bisher gut, aber nur wenn man vor main() die 
__libc_init_array aufruft. Ob man noch mehr tun muss, weiss ich nicht.

Bauform B. schrieb:
> Und auf der Ebene muss doch
> auch ANSI-C ausreichen, also write() statt write_r()?

Soweit ich das herausgefunden habe bisher ist write_r nur eine Kapselung 
von write, und die C-Library ruft auf jeden Fall write_r auf. Wenn man 
das mit einem RTOS verwenden möchte, ist das glaube ich keine dumme 
Idee, aber sicher weiss ich es eben auch nicht.

von Bauform B. (bauformb)


Lesenswert?

Tobias P. schrieb:
> Wo findet man die eigentlich? was man vor main() alles aufrufen muss,
> und wie und welche Daten kopiert oder auf 0 gesetzt werden müssen...

Naja, wozu Dokumentation schreiben, wenn es kaum einen interessiert ;) 
Die Linker-Scripte, die ich so gefunden habe, sehen auch alle genauso 
aus.

> Ich habe aber das mit C++ begonnen genauer anzuschauen und teste
> grade einen C++ Code auf dem STM32. Konstruktoren, Destruktoren,
> globale Instanzen von Klassen, das alles funktioniert bisher gut

Wahnsinn, ich bin schon froh, dass ich es für simples C ansatzweise 
verstanden habe.

Tobias P. schrieb:
> Bauform B. schrieb:
>> Und auf der Ebene muss doch
>> auch ANSI-C ausreichen, also write() statt write_r()?
>
> Soweit ich das herausgefunden habe bisher ist write_r nur eine Kapselung
> von write, und die C-Library ruft auf jeden Fall write_r auf.

Ich glaube auch; die libc braucht das für Threads, aber der syscall ganz 
innen drin darf ein einfaches write() sein.

> Wenn man das mit einem RTOS verwenden möchte, ist das glaube ich
> keine dumme Idee, aber sicher weiss ich es eben auch nicht.

Kommt drauf an, ob man fette Prozesse oder schlanke Threads mag. Mit 
genug RAM und einem "richtigen" RTOS hat jeder Prozess sein eigenes RAM 
(stack, data, bss). Dann kann man eine libc ohne Threads und ohne die _r 
Funktionen benutzen. Umgekehrt braucht man kein RTOS. Und Mittelwege 
sind auch noch denkbar.

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.