Von Zeit zu Zeit muss ich in meinen Projekten printf in Verbindung mit einer seriellen Schnittstelle verwenden (UART oder eine Abstraktion über USB, die eine serielle Schnittstelle nachahmt). Und wie immer vergeht viel Zeit zwischen den Anwendungen, und es gelingt mir, alle Nuancen, die berücksichtigt werden müssen, vollständig zu vergessen, damit es in einem großen Projekt normal funktioniert.
In diesem Artikel habe ich meine eigenen Top-Nuancen zusammengestellt, die bei der Verwendung von printf in Programmen für Mikrocontroller auftreten, sortiert nach Beweisen von den offensichtlichsten bis zu den völlig nicht offensichtlichen.
Kurze Einführung
Tatsächlich reicht es aus, printf in Programmen für Mikrocontroller zu verwenden:
- Fügen Sie die Header-Datei in den Projektcode ein.
- Definieren Sie die _write-Systemfunktion neu, um sie an die serielle Schnittstelle auszugeben.
- Beschreiben der Stubs von Systemaufrufen, die der Linker benötigt (_fork, _wait und andere).
- Verwenden Sie den printf-Aufruf im Projekt.
In der Tat ist nicht alles so einfach.
Beschreiben Sie alle Stubs, nicht nur gebrauchte.
Das Vorhandensein einer Reihe von vagen Links im Layout des Projekts ist zunächst überraschend, aber nach einigem Lesen wird klar, was und warum. In all meinen Projekten verbinde ich dieses
Submodul . Daher definiere ich im Hauptprojekt nur die Methoden neu, die ich benötige (in diesem Fall nur _write), und der Rest bleibt unverändert.
Es ist wichtig zu beachten, dass alle Stubs C-Funktionen sein müssen. Nicht C ++ (oder in externes "C" eingeschlossen). Andernfalls schlägt das Layout fehl (denken Sie an die Namensänderung während der Assembly mit G ++).
In _write kommt 1 Zeichen
Trotz der Tatsache, dass der Prototyp der _write-Methode ein Argument hat, das die Länge der Ausgabenachricht übergibt, hat er den Wert 1 (tatsächlich werden wir ihn selbst immer auf 1 setzen, aber dazu später mehr).
int _write (int file, char *data, int len) { ... }
Im Internet kann man oft
genau eine solche Implementierung dieser Methode sehen:
Häufige Implementierung der Funktion _write int uart_putc( const char ch) { while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); {} USART_SendData(USART2, (uint8_t) ch); return 0; } int _write_r (struct _reent *r, int file, char * ptr, int len) { r = r; file = file; ptr = ptr; #if 0 int index; for(index=0; index<len; index++) { if (ptr[index] == '\n') { uart_putc('\r'); } uart_putc(ptr[index]); } #endif return len; }
Eine solche Implementierung hat die folgenden Nachteile:
- geringe Produktivität;
- Streaming-Unsicherheit;
- Unfähigkeit, die serielle Schnittstelle für andere Zwecke zu verwenden;
Geringe Leistung
Eine langsame Leistung ist auf das Senden von Bytes mithilfe von Prozessorressourcen zurückzuführen: Sie müssen das Statusregister überwachen, anstatt denselben DMA zu verwenden. Um dieses Problem zu lösen, können Sie den Puffer für das Senden im Voraus vorbereiten und beim Empfangen des Zeichens am Ende der Zeile (oder beim Füllen des Puffers) senden. Diese Methode erfordert einen Pufferspeicher, verbessert jedoch die Leistung bei häufigem Senden erheblich.
Beispielimplementierung von _write mit einem Puffer #include "uart.h" #include <errno.h> #include <sys/unistd.h> extern mc::uart uart_1; extern "C" { // uart. static const uint32_t buf_size = 254; static uint8_t tx_buf[buf_size] = {0}; static uint32_t buf_p = 0; static inline int _add_char (char data) { tx_buf[buf_p++] = data; if (buf_p >= buf_size) { if (uart_1.tx(tx_buf, buf_p, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } buf_p = 0; } return 0; } // Putty \r\n // . static inline int _add_endl () { if (_add_char('\r') != 0) { return -1; } if (_add_char('\n') != 0) { return -1; } uint32_t len = buf_p; buf_p = 0; if (uart_1.tx(tx_buf, len, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } return 0; } int _write (int file, char *data, int len) { len = len; // . if ((file != STDOUT_FILENO) && (file != STDERR_FILENO)) { errno = EBADF; return -1; } // // \n. if (*data != '\n') { if (_add_char(*data) != 0) { return -1; } } else { if (_add_endl() != 0) { return -1; } } return 1; } }
Hier ist das uart-Objekt uart_1 für das direkte Senden mit dma verantwortlich. Das Objekt verwendet FreeRTOS-Methoden, um den Zugriff von Drittanbietern auf das Objekt zum Zeitpunkt des Sendens von Daten aus dem Puffer (Aufnehmen und Zurückgeben von Mutex) zu blockieren. Daher kann niemand das uart-Objekt verwenden, während er von einem anderen Thread sendet.
Einige Links:
- _Schreiben Sie hier den Funktionscode als Teil eines realen Projekts
- Die Uart-Klassenschnittstelle ist hier
- Implementierung der Uart-Klassenschnittstelle unter stm32f4 hier und hier
- Instanziierung der Uart-Klasse als Teil des Projekts hier
Streaming-Unsicherheit
Diese Implementierung bleibt ebenfalls ungeschützt, da sich niemand im benachbarten FreeRTOS-Stream die Mühe macht, eine weitere Zeile an printf zu senden und dadurch den aktuell gesendeten Puffer zu mahlen (Mutex innerhalb des Uarts schützt das Objekt vor der Verwendung in verschiedenen Streams, aber die nicht an sie übertragenen Daten ) Wenn das Risiko besteht, dass printf eines anderen Threads aufgerufen wird, muss ein Layer-Objekt implementiert werden, das den Zugriff auf printf vollständig blockiert. In meinem speziellen Fall interagiert nur ein Thread mit printf, sodass zusätzliche Komplikationen nur die Leistung beeinträchtigen (ständige Erfassung und Freigabe von Mutex innerhalb der Ebene).
Unfähigkeit, die serielle Schnittstelle für andere Zwecke zu verwenden
Da wir erst senden, nachdem die gesamte Zeichenfolge empfangen wurde (oder der Puffer voll ist), können Sie anstelle des uart-Objekts die Konvertermethode für die nachfolgende Paketübertragung an eine Schnittstelle der obersten Ebene aufrufen (z. B. Zustellung mit einer Garantie gemäß dem Übertragungsprotokoll ähnlich wie bei Paketen Transaktionsmodbus). Auf diese Weise können Sie einen Uart sowohl zum Anzeigen von Debugging-Informationen als auch zum Beispiel für die Benutzerinteraktion mit der Verwaltungskonsole verwenden (sofern einer auf dem Gerät verfügbar ist). Es reicht aus, einen Dekomprimierer auf der Empfängerseite zu schreiben.
Standardmäßig funktioniert die Float-Ausgabe nicht
Wenn Sie newlib-nano verwenden, unterstützen printf (sowie alle ihre Derivate wie sprintf / snprintf ... und andere) standardmäßig die Ausgabe von float-Werten nicht. Dies lässt sich leicht lösen, indem dem Projekt die folgenden Linker-Flags hinzugefügt werden.
SET(LD_FLAGS -Wl,-u,vfprintf; -Wl,-u,_printf_float; -Wl,-u,_scanf_float; "_")
Die vollständige Liste der Flags finden Sie
hier .
Das Programm friert irgendwo im Darm von printf ein
Dies ist ein weiterer Fehler in den Linker-Flags. Damit die Firmware mit der gewünschten Version der Bibliothek konfiguriert werden kann, müssen Sie die Prozessorparameter explizit angeben.
SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) SET(LD_FLAGS ${HARDWARE_FLAGS} "_")
Die vollständige Liste der Flags finden Sie
hier .
printf zwingt den Mikrocontroller zu einem schweren Fehler
Es kann mindestens zwei Gründe geben:
- Stapelprobleme;
- Probleme mit _sbrk;
Stapelprobleme
Dieses Problem tritt wirklich auf, wenn Sie FreeRTOS oder ein anderes Betriebssystem verwenden. Das Problem ist die Verwendung des Puffers. Der erste Absatz besagte, dass in _write jeweils 1 Byte kommt. Dazu müssen Sie die Verwendung von Pufferung in Ihrem Code verbieten, bevor Sie printf zum ersten Mal verwenden.
setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0);
Aus der Beschreibung der Funktion folgt, dass einer der folgenden Werte auf die gleiche Weise eingestellt werden kann:
#define _IOFBF 0 #define _IOLBF 1 #define _IONBF 2
Dies kann jedoch zu einem Überlauf des Taskstapels führen (oder zu Unterbrechungen, wenn Sie plötzlich eine sehr schlechte Person sind, die printf von Interrupts aus aufruft).
Rein technisch ist es möglich, Stapel für jeden Stream sehr sorgfältig anzuordnen, aber dieser Ansatz erfordert eine sorgfältige Planung und es ist schwierig, die darin enthaltenen Fehler zu erfassen. Eine viel einfachere Lösung besteht darin, jeweils ein Byte zu empfangen, es in einem eigenen Puffer zu speichern und es dann in dem zuvor analysierten erforderlichen Format auszugeben.
Probleme mit _sbrk
Dieses Problem war für mich persönlich das impliziteste. Und was wissen wir über _sbrk?
- Ein weiterer Stub, der implementiert werden muss, um einen erheblichen Teil der Standardbibliotheken zu unterstützen.
- erforderlich, um Speicher auf dem Heap zuzuweisen;
- wird von allen Arten von Bibliotheksmethoden wie malloc verwendet, kostenlos.
Persönlich verwende ich in meinen Projekten in 95% der Fälle FreeRTOS mit neu definierten Methoden new / delete / malloc, die eine Reihe von FreeRTOS verwenden. Wenn ich also Speicher zuordne, bin ich sicher, dass sich die Zuweisung auf dem FreeRTOS-Heap befindet, der eine vorgegebene Menge an Speicher im bss-Bereich beansprucht. Sie können die Ebene
hier ansehen. Rein technisch sollte es also kein Problem geben. Eine Funktion sollte einfach nicht aufgerufen werden. Aber denken wir mal, wenn sie anruft, wo wird sie dann versuchen, sich zu erinnern?
Erinnern Sie sich an das Layout des RAM des "klassischen" Projekts für Mikrocontroller:
- .data;
- .bss;
- leerer Raum
- Anfangsstapel.
In Daten haben wir die Anfangsdaten globaler Objekte (Variablen, Strukturen und andere globale Projektfelder). In bss globale Felder mit einem anfänglichen Nullwert und sorgfältig eine Reihe von FreeRTOS. Es ist nur ein Array im Speicher. mit denen dann die Methoden aus der Datei heap_x.c arbeiten. Als nächstes kommt der leere Raum, nach dem (oder besser gesagt vom Ende) der Stapel ist. Weil FreeRTOS wird in meinem Projekt verwendet, dann wird dieser Stapel nur verwendet, bis der Scheduler startet. Daher ist seine Verwendung in den meisten Fällen auf Kollobyte beschränkt (in der Tat normalerweise eine 100-Byte-Grenze).
Aber wo wird dann mit _sbrk Speicher zugewiesen? Schauen Sie sich an, welche Variablen sie aus dem Linker-Skript verwendet.
void *__attribute__ ((weak)) _sbrk (int incr) { extern char __heap_start; extern char __heap_end; ...
Jetzt finden wir sie im Linker-Skript (mein Skript unterscheidet sich geringfügig von dem, das st bereitstellt, aber dieser Teil ist dort ungefähr gleich):
__stack = ORIGIN(SRAM) + LENGTH(SRAM); __main_stack_size = 1024; __main_stack_limit = __stack - __main_stack_size; ... flash, ... .bss (NOLOAD) : ALIGN(4) { ... . = ALIGN(4); __bss_end = .; } >SRAM __heap_start = __bss_end; __heap_end = __main_stack_limit;
Das heißt, es wird Speicher zwischen dem Stapel (1 KB von 0x20020000 nach unten mit 128 KB RAM) und bss verwendet.
Verstanden. Aber er hatte eine Neudefinition der Methoden malloc, free und anderer. Verwenden Sie _sbrk schließlich ist nicht notwendig? Wie sich herausstellte, ein Muss. Darüber hinaus verwendet diese Methode nicht printf, sondern die Methode zum Festlegen des
Puffermodus -
setvbuf (oder besser _malloc_r, das in der Bibliothek nicht als schwache Funktion deklariert ist. Im Gegensatz zu malloc, das leicht ersetzt werden kann).

Da ich sicher war, dass sbrk nicht verwendet wurde, platzierte ich eine Reihe von FreeRTOS (bss-Abschnitt) in der Nähe des Stapels (weil ich sicher wusste, dass der Stapel zehnmal weniger als erforderlich verwendet wurde).
Lösungen zu Problem 3:
- Einzug zwischen bss und dem Stapel;
- _malloc_r überschreiben, damit _sbrk nicht aufgerufen wird (eine Methode von der Bibliothek trennen);
- sbrk via malloc neu schreiben und kostenlos.
Ich habe mich für die erste Option entschieden, da es nicht erfolgreich war, den Standard _malloc_r (der sich in libg_nano.a (lib_a-nano-mallocr.o) befindet) zu ersetzen (die Methode wurde nicht als __attribute__ ((schwach) deklariert), sondern nur eine einzige Funktion aus der Bi-Bibliothek auszuschließen Es ist mir nicht gelungen, eine Verknüpfung herzustellen. Ich wollte sbrk wirklich nicht für einen Anruf umschreiben.
Die endgültige Lösung bestand darin, separate Partitionen im RAM für den anfänglichen Stapel und _sbrk zuzuweisen. Dies stellt sicher, dass Abschnitte während der Einrichtungsphase nicht übereinander gestapelt werden. Innerhalb von sbrk gibt es auch einen Scheck für das Verlassen des Abschnitts. Ich musste eine kleine Korrektur vornehmen, damit der Fluss beim Erkennen eines Übergangs ins Ausland in einer while-Schleife hängen bleibt (da die Verwendung von sbrk nur in der Anfangsphase der Initialisierung erfolgt und in der Phase des Debuggens des Geräts verarbeitet werden sollte).
Geändertes mem.ld MEMORY { FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1M CCM_SRAM (RW) : ORIGIN = 0x10000000, LENGTH = 64K SRAM (RW) : ORIGIN = 0x20000000, LENGTH = 126K SBRK_HEAP (RW) : ORIGIN = 0x2001F800, LENGTH = 1K MAIN_STACK (RW) : ORIGIN = 0x2001FC00, LENGTH = 1K }
Änderungen an section.ld __stack = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK); __heap_start = ORIGIN(SBRK_HEAP); __heap_end = ORIGIN(SBRK_HEAP) + LENGTH(SBRK_HEAP);
Sie können
mem.ld und
section.ld in meinem Sandbox-Projekt
in diesem Commit anzeigen .
UPD 07/12/2019: Die Liste der Flags für die Arbeit mit printf mit Gleitkommawerten wurde korrigiert. Ich habe den Link zu den funktionierenden CMakeLists mit korrigierten Kompilierungs- und Layout-Flags korrigiert (es gab Nuancen mit der Tatsache, dass die Flags einzeln und durch ";" aufgelistet werden sollten, während es in einer Zeile oder in verschiedenen Zeilen keine Rolle spielt).