ich bin noch neu auf dem Gebiet der STM32 Programmierung. Aktuell habe ich ein kleines Evalboard mit dem STM32L051, welches ich bereits etwas Leben eingehaucht habe (Clock source auf externen Quarz und PLL umgestellt, GPIO initialisiert und UART initialisiert). Programmiert wird bare metal, sprich ohne jegliche HAL (nur die CMSIS) wird benutzt. Meine nächsten Schritte sind die anderen Peripherals (I2C und SPI) zum Laufen zu bringen.
Aktuelles Thema ist jedoch die UART. Die UART Übertragung funktioniert bei mir so, dass ich einen RX-Interrupt aktiviere und in der UART ISR dann auf bestimmte Zeichen abfrage und entsprechende Aktionen auslöse (z.B. LED ein/aus, String senden). Das funktioniert auch, sprich die Basisfunktionalität ist gegeben.
Anstatt des UART Tests möchte ich jedoch in serial.write Arduino-Manier einfach Log - Messages während der Entwicklung über die UART ausgeben und da komme ich irgendwie nicht weiter. Meine Idee war einfach printf zu benutzen, da man hier alle Formatierungsoptionen hat. Dazu muss man aber wohl eine Funktion (_write) umbiegen, damit printf die UART als standard output verwendet. Das scheint doch alles komplex zu werden - für eine "triviale" Aufgabe. Generell: ist es eine gute Idee mit UART RX / TX Interrupts zu arbeiten oder ist einfaches Polling völlig in Ordnung?
Anbei der Code zur ISR und zur TX-Routine, der ursprünglich von R. Jesse stammt:
void USART2_IRQHandler(void)
{
uint16_t received = 0;
Meine Idee war einfach printf zu benutzen, da man hier alle
Formatierungsoptionen hat. Dazu muss man aber wohl eine Funktion
(_write) umbiegen, damit printf die UART als standard output verwendet.
Das scheint doch alles zu komplex zu sein.
Das ist gar nicht schwierig. Dort ist ein Beispiel (ohne Sendepuffer):
Wenn du das mit einem Puffer machen willst, würde ich aber nicht '\0' als Ende-Markierung benutzen, denn das funktioniert nur mit Strings in denen das Zeichen nicht vorkommt. Ich würde das eher mit einem Ringpuffer machen. Wie das geht, wurde hier schon ziemlich oft diskutiert. Du findest die Beiträge bestimmt mit der Suchfunktion.
Generell: ist es eine gute
Idee mit UART RX / TX Interrupts zu arbeiten oder ist einfaches Polling
vällig in Ordnung?
Also generell ist es schonmal gut, wenn Du mit Bare Metal anfängst, um Dir einen etwas tieferen Einblick in den Microcontroller zu verschaffen.
Allerdings haben wir 'zu meiner Zeit' immer erstmal versucht einen 8 Bit Controller (ATmega8) zum laufen zu bringen. Das ist dann doch etwas einfacher.
Was den UART bzw. die Interrupts betrifft ist es keine gute Idee, darin die Daten auszuwerten. Du sollstest einfach einen globulen Byte Array nutzen und diesen mit den empfangenen Daten füllen (z.B. uartDataRx[i] = ...). Dann kannst du noch ein (globales ) Flag sezten, z.B. uartReceived = true.
In der main() wird das ganze dann ausgewertet und ggf. auch was zurückgeschickt.
ich bin noch neu auf dem Gebiet der STM32 Programmierung. Aktuell habe
ich ein kleines Evalboard mit dem STM32L051, welches ich bereits etwas
Leben eingehaucht habe (Clock source auf externen Quarz und PLL
umgestellt, GPIO initialisiert und UART initialisiert).
Das Zeug im Zipfile ist zwar für den STM32F103C8T6, aber zum einen ist vieles darin plattformunabhängig (läuft also auch auf z.B. Fujitsu-Controllern) und zum anderen sind die Peripheriecores bei den diversen STM32 oftmals ziemlich ähnlich, sodaß sich auch Lowlevel-Treiber ohne allzu große Hürden auf einen anderen Chip umschreiben lassen.
Vielen Dank Stefan für den Link. Ich kenne die Seite eigentlich, mmh. scheinbar habe ich den Artikel nicht gefunden ...
In der _write Funktion nutzt Du normales Polling, wenn ich das richtig sehe (Abfrage Bit USART_ISR_TXE).
In der UART Initialisierungsroutine werden sowohl TX und RX Interrupt gleichzeitig aktiviert. Ist das gut so? Sollte TX Interrupt nicht erst frigegeben werden, wenn etwas im TX Sendebuffer steht?
Allerdings haben wir 'zu meiner Zeit' immer erstmal versucht einen 8 Bit
Controller (ATmega8) zum laufen zu bringen. Das ist dann doch etwas
einfacher.
ich kenne die 8bit AVR Controller recht gut und habe schon einiges programmiert. Nun wollte ich einfach den Schritt gehen und auf die STM32 umschwenken. Mein Ziel ist wirklich ein harter Wechsel - sonst beschäftige ich mich nur halb mit dem STM32 ;-)
Das Zeug im Zipfile ist zwar für den STM32F103C8T6, aber zum einen ist
vieles darin plattformunabhängig (läuft also auch auf z.B.
Fujitsu-Controllern) und zum anderen sind die Peripheriecores bei den
diversen STM32 oftmals ziemlich ähnlich, sodaß sich auch
Lowlevel-Treiber ohne allzu große Hürden auf einen anderen Chip
umschreiben lassen.
Danke für den Link! Das werde ich mir bei Gelegenheit zu Gemüte führen. Zu den Peripherie-Cores und der Ähnlichkeit kann ich nur sagen - es stimmt nur bedingt. Die Familien sind doch sehr unterschiedlich und man kommt um das Studium des Referenzmanuals nicht herum. Beispiel: ich habe das Buch von R. Jesse "STM32 ARM Mikrocontroller programmieren - das umfassende Handbuch" (keine Werbung!!). Hier wird ein STM32F446 verwendet und die Bare Metal Beispiele muss man häufig auf den STM32L0x1 umschreiben, da die Registernamen anders sind. ImPrinzip sind diese aber schon sehr ähnlich, da gebe ich Dir völlig Recht.
Sollte TX Interrupt nicht erst
frigegeben werden, wenn etwas im TX Sendebuffer steht?
Ähem... naja. Also bei vielen Chips ist es so, daß der Interrupt statisch ist, also solange ansteht, bis entweder etwas zum Senden in den Sender geschrieben wurde oder bis man ihn sperrt. Das bedeutet, daß der Sendeinterrupt immer dann von der ISR gesperrt werden muß, wenn sie gerade nix zum Senden vorfindet. Und von der Routine, die ein Zeichen zum Senden entgegennimmt und in einen Ringpuffer stopft, wird der Interrupt freigegeben, damit die ISR loslegen oder ihn wieder sperren kann (wenn es nix zu senden gibt).
In der UART Initialisierungsroutine werden sowohl TX und RX Interrupt
gleichzeitig aktiviert. Ist das gut so? Sollte TX Interrupt nicht erst
frigegeben werden, wenn etwas im TX Sendebuffer steht?
Sorry Stefan, da habe ich Deinen Code auf Deiner Seite falsch gelesen.
// Enable transmitter, receiver and receive-interrupt of USART1
USART1->CR1 = USART_CR1_UE + USART_CR1_TE + USART_CR1_RE + USART_CR1_RXNEIE;
Du schaltest NUR den RX Interrupt frei. TX benutzt keinen Interrupt und damit nur Polling, richtig? Ist das so die "gängige" Methode?
Hallo Stefan, Dein Code gefällt mir! kurz und prägnant. Meine Fragen hierzu.
Du verwendet puts() statt printf(). Benötigt puts() weniger Speicher oder was ist der Hintergrund für die Verwendung von puts? Wenn ich das richtig verstanden habe, kennt puts() keine format specifiers.
In der Regel will man beim Loggen ja einen kleinen erklärenden Text und dann (Roh)Daten ausgeben - also eigentlich schon das was printf() bietet. Wäre eine Möglichkeit statt printf() die Kombination von sprintf() und puts() zu verwenden? Dann würde man erst einmal den String zusammensetzen und dann als String per puts() ausgeben.
By the way. mein Code wird bei Verwendung von printf() ca. 5 kB gegenüber der Nichtverwendung aufgebläht...
Das Senden läuft bei diesem Beispielprogramm wie gesagt ohne Puffer, also auch ohne Interrupt.
Mir ging es darum, dir ein einfaches verständliches Beispiel zu geben, wie man die _write() Funktion implementiert. Puffern ist ein davon unabhängiges Thema.
Du verwendet puts() statt printf(). Benötigt puts() weniger Speicher
oder was ist der Hintergrund für die Verwendung von puts?
Printf ist zum formatieren von Ausgaben gedacht. Puts ist für einfache Strings.
Zumindest der avr-gcc ist allerdings so schlau, ein printf("...\n") durch puts("...") zu ersetzen. Ob der arm-gcc auch so clever ist, weiß ich nicht.
Wäre eine Möglichkeit statt printf() die Kombination von
sprintf() und puts() zu verwenden?
Kannst du auch machen. Effizienter ist das am Ende aber vermutlich nicht, als gleich printf() zu verwenden.
By the way. mein Code wird bei Verwendung von printf()
ca. 5 kB gegenüber der Nichtverwendung aufgebläht...
Das steht auch auf meiner Seite: http://stefanfrings.de/stm32/stm32l0.html#gccopt
Die ganzen Formatier-Funktionen von printf bekommt man halt nicht umsonst. Und ja, ich weiß dass printf() beim AVR deutlich schlanker ist.
Danke an alle für die Infos. Leider gibt mein printf auf dem Terminal nichts aus. Ich habe es so gemacht wie Stefan auf seiner Webseite implementiert hat:
// Redirect standard output to the serial port
int _write(int file, char *ptr, int len)
{
for (int i=0; i<len; i++)
{
while(!(USART1->ISR & USART_ISR_TXE));
USART1->TDR = *ptr++;
}
return len;
}
Hast du die Zeile mit \n abgeschlossen? Das ist bei ARM wichtig, weil
die Bibliothek Ausgaben bis zum Zeilenumbruch zurück hält.
Danke Stefan, das war die Lösung! Ich hatte das \n im printf vergessen. Dieses Verhalten war mir nicht bewusst. Der Unterschied zu Deiner Implementierung war, dass Du das printf("Hello World") in der Endlosschleife hattest und ich davor. Meine Endlosschleife war leer.
Interessant ist auch, dass OHNE abschliessendes \n die Code Size um gut 1.5kByte grösser ist als mit dem \n.
Anbei der jetzt so funktionierende (Minimal-)Code:
int main(void)
{
SystemCoreClockInit(); // PLL using HSE with 32MHz clock out
GpioInit(); // GPIO configuration
Usart2Init();
// GPIOC->ODR |= GPIO_ODR_OD13; // GPIOC: PC13 -> 1
// GPIOC->BSRR = GPIO_BSRR_BS_13; // GPIOC: PC13 -> 1; w/o read-modify-write
printf("Hello World\n");
LED_PC13_ON;
1
while(1)
2
{
3
4
}
}
ISR (IRQ Handler) und _write Funktion sind identisch zu Deiner Implementierung, ausser dass ich USART1 durch USART2 ersetzt habe.
Das steht übrigens auch auf der Webseite. Am besten liest du sie mal komplett durch. Ich habe da alle Knackpunkte aufgeschrieben, auf die ich beim Wechsel von AVR nach STM32 gestoßen war.
Du schreibst selbst auch über dem Quelltext: "Das folgende Beispielprogramm sendet regemäßig "Hello World!" aus." -> Beschreibung passt zum Code für mich.
Du schreibst selbst auch über dem Quelltext: "Das folgende
Beispielprogramm sendet regemäßig "Hello World!" aus." -> Beschreibung
passt zum Code für mich.
Da sehe ich jetzt aber einen Fehler. Die Ausgabe lautet "Hello", nicht "Hello World!".
Was den UART bzw. die Interrupts betrifft ist es keine gute Idee, darin
die Daten auszuwerten. Du sollstest einfach einen globulen Byte Array
nutzen und diesen mit den empfangenen Daten füllen (z.B. uartDataRx[i] =
...). Dann kannst du noch ein (globales ) Flag sezten, z.B. uartReceived
= true.
Das finde ich eine gute Idee. Frage meinerseits: wie gross dimensioniert man so ein Array? Hast Du Code-Beispiele für mich (oder einen Link) für eine entsprechende Implementierung? Mir geht es nur um den Ansatz und die grobe Struktur.
Beim Thema Buffer bin ich ehrlich gesagt auch Newbie. Das Prinzip des Ring-Buffers habe ich verstanden, habe aber im Moment keine Idee, das kompakt in C umzusetzen ...
Beim Empfangen musst du dir überlegen, wie viele Bytes sich maximal im Puffer aufstauen, während dein µC gerade nicht empfangsbereit ist weil er anderweitig beschäftigt ist. Das hängt auch von der Quelle der Daten ab.
Beim Senden musst du dir überlegen, ob du überhaupt puffern willst. Wenn ja, dann würde ich mir überlegen, wie viele Daten dein Programm quasi am Stück erzeugt, die dann aber später nach und nach gesendet werden.
Viele Programme warten nach dem Senden, bis das letzte Byte raus ist, bevor sie (mit was auch immer) weiter machen. In diesem Fall mach ein Puffer die Sache bloß unnötig kompliziert.
In anderen Programm kann ein Sendepuffer aber hilfreich sein, damit dein Programm nach dem Erzeugen einer Ausgabe sofort weiter arbeiten kann, ohne auf die relativ langsame serielle Kommunikation warten zu müssen.
Debugausgaben sollte man gar nicht puffern oder dabei Funktionen
benutzen die das doch tun. Warum das so ist, da solltest du
selber drauf kommen.
Debugausgaben sollten ohne Interrupt auskommen.
Global den Interrupt sperren ist dann natuerlich noetig.
Es kann lohnenswert sein, Debugausgaben mit einem Soft-UART
auszugeben. Der kann u.U. ein mehrfaches der Geschwindigkeit
eines HW-UART erreichen. Warum das fuers Debugging gut ist,
auch darauf solltest du selber kommen.
printf hat nun gar nichts von "bare metal".
Mehr als Zeichen, (kurze) Zeichenketten, und Hex und Dezimal in
Byte, 16/32 bit-Word braucht man da eigentlich nicht.
Ich kannte mal jemanden, der versuchte eine *zeitkritische"
Software mit solchem "printf"-Germurkse zum Laufen zu bringen.
Dabei haben schon die Debugausgaben sein Programm zeitlich
so ruiniert, dass das nicht mehr funktionierte.
Sagen wir's mal direkt: mit printf wird Übersetzungsarbeit vom Übersetzungszeitpunkt in die Laufzeit des µC verlagert. Das printf ist im Wesentlichen ein Textinterpreter, der zum Übersetzen des Formatstrings gebraucht wird und da er nicht ahnen kann, was der jeweilige Programmierer dort hineingeschrieben hat, muß er obendrein auch noch alle möglichen (und unmöglichen) Ausgabe-Konverter vorhalten. Das macht ihn so dick. Aber als Programmierer der Firmware weiß man eigentlich, was man an Ausgabekonvertern braucht und kann diese dann auch selbst aufrufen, ohne dafür einen Textinterpreter zu benötigen.
Es kann lohnenswert sein, Debugausgaben mit einem Soft-UART
auszugeben. Der kann u.U. ein mehrfaches der Geschwindigkeit
eines HW-UART erreichen.
Ok, gibt es dafür bereits Beispiel-Implementierungen? So ganz ist mir auch nicht klar, warum eine Soft-UART viel schneller ist als eine HW UART sein sollte.
Ich stimme zu, das printf nicht unbedingt zu bare metal passt. Wie bewerkstelligt man denn nun konkret die Ausgaben auf die serielle Schnittstelle zu Debug-Zwecken?
Was mich etwas irritiert ist, dass es für ein solches Standardthema (Log Ausgaben via UART) scheinbar keine vernünftige Lösung gibt.
Ok, gibt es dafür bereits Beispiel-Implementierungen? So ganz ist mir
auch nicht klar, warum eine Soft-UART viel schneller ist als eine HW
UART sein sollte.
Löse dich einfach mal von der Vorstellung, daß man für alles, was irgendwie 'debug' genannt werden kann, dieselbe Signalisierung und dann noch per genormten Parametern machen kann. Ich hab selbe auch schon Einpin-Ausgaben gemacht, die dazu da sind, per Oszi angeschaut zu werden. Sowas ist sinnvoll an Stellen, wo noch kein definierter Takt vorliegt, also bei der Grundkonfiguration mittendrin, wo die PLL noch nicht fertig meldet. Ich hab auch schon eine zweistufige Debug-Ausgabe gemacht, zum Beispiel in der ISR des USB. Zuerst alles in einen RAM-Puffer und später dann von dort aus gemütlich per UART auf die Reise geschickt. Es kommt eben immer darauf an, was man wo machen will.
Ich stimme zu, das printf nicht unbedingt zu bare metal passt. Wie
bewerkstelligt man denn nun konkret die Ausgaben auf die serielle
Schnittstelle zu Debug-Zwecken?
Es sind ja nicht nur Debug-Zwecke, sondern die Kommunikation insgesamt. Dazu sollte man sich eine vernünftige für den µC geeignete Schnittstelle zwischen Lowlevel-Treiber und den anderen Teilen der Firmware machen. Sowas wie printf gehört nicht dazu. Ich halte es so, daß der Treiber sowas wie char_out(char C) anbietet, wo dann je nach Bedarf ein String_Out(...) oder ein Long_Out(...) oder ein Float_Out(...) aufsetzen können. Eben je nachdem, was man tatsächlich braucht.
Was mich etwas irritiert ist, dass es für ein solches Standardthema (Log
Ausgaben via UART) scheinbar keine vernünftige Lösung gibt.
Es gibt (s.o.) keine Standard-Methode für's Debuggen und folglich auch keine Standard-Mitteilungs-Methode. Und für ein langsames (da durch den Menschen am Terminalprogramm auf dem PC zu beobachten) Verfahren reichen die bereits genannten auf Char_Out(...) aufsetzenden Methoden völlig aus. Aber die Programmier-Kinder, die nix anderes als printf gezeigt bekommen haben und selbst keine Lust oder keine Fähigkeit haben, sich etwas effizienteres selbst auszudenken oder gar von der Hochmut-Kuh gebissen sind (neulich: "ich programmiere NICHT auf Registerebene"), schielen eher darauf, als Ersatz für printf irgendwas aus der C++ Kiste nehmen zu wollen. Ohne auch nur einen einzigen Gedanken darauf zu verwenden, was sie sich damit einbrocken. Hauptsache, es gibt da eine fertige Lib für.
Ich hab selbe auch schon
Einpin-Ausgaben gemacht, die dazu da sind, per Oszi angeschaut zu
werden. Sowas ist sinnvoll an Stellen, wo noch kein definierter Takt
vorliegt, also bei der Grundkonfiguration mittendrin, wo die PLL noch
nicht fertig meldet.
Den ersten Teil kenne ich als HW Entwickler, das ist absolut Low-Level und geht, ist aber mühsam.
Ich hab auch schon eine zweistufige Debug-Ausgabe
gemacht, zum Beispiel in der ISR des USB. Zuerst alles in einen
RAM-Puffer und später dann von dort aus gemütlich per UART auf die Reise
geschickt.
Das finde ich eine gute Idee. Dazu fehlt mir allerdings die Erfahrung so etwas zu implementieren.
Es sind ja nicht nur Debug-Zwecke, sondern die Kommunikation insgesamt.
Dazu sollte man sich eine vernünftige für den µC geeignete Schnittstelle
zwischen Lowlevel-Treiber und den anderen Teilen der Firmware machen.
Sowas wie printf gehört nicht dazu. Ich halte es so, daß der Treiber
sowas wie char_out(char C) anbietet, wo dann je nach Bedarf ein
String_Out(...) oder ein Long_Out(...) oder ein Float_Out(...) aufsetzen
können. Eben je nachdem, was man tatsächlich braucht.
Mit dem Ansatz gehe ich durchaus mit. printf() hat halt den "Charme", das man alles mit einer Funktion erschlägt. Ich habe mir auch schon andere Tools / Instrumentierungen angeschaut, wie z.B. Trice (https://github.com/rokath/trice). Leider habe ich das nicht zum Laufen bekommen ...
Ein letzter Aspekt: Debug via UART ist für mich Mittel zum Zweck - ich will z.B. ADC RAW Daten sehen. Ich möchte da ungern ein eigenes Projekt daraus machen.
Ok, gibt es dafür bereits Beispiel-Implementierungen? So ganz ist mir
auch nicht klar, warum eine Soft-UART viel schneller ist als eine HW
UART sein sollte.
Weil ein HW-UART das Signal gerne N-fach ueberabtasten will.
Das gilt zwar eigentlich nur fuer den RX-Teil, trifft aber
den TX des UARTs genauso.
Die Senderoutine eines Soft-UART in Assembler(!) zu schreiben,
ist eine gute Fingeruebung. Bereits 8-bitter schaffen bei
einem internen CPU-Takt von 2 MHz Geschwindigkeiten von 230 kbit.
Wer will, kann bei einem Soft-UART auch gleich Fast-IRDA
auf eine (IR-)LED ausgeben. Da ist dann Potentialtrennung gleich
inklusiv.
Sicher gibt es "Beispiel-Implementierungn" die aber fuer
diesen Zweck genau alle nicht taugen, weil sie interruptgesteuert
sind. Und natuerlich viel zu langsam.
Wenn man sich nicht an die Standardbaudraten halten will,
kann den seriellen Text ja auch im LA mitlesen.
Viele serielle USB-Adapter gestatten auch die Einstellung
arbitraerer Baudraten. Wenn es "ueberall" funtionieren soll,
sind die Standardbaudraten schon im Vorteil.
Ueber
Debugausgaben sollte man gar nicht puffern
und
Debugausgaben sollten ohne Interrupt auskommen.
scheinst du ja auch noch nicht nachgedacht zu haben.
Ein Debugprint ist auch noch etwas anderes als ein Debuglog.
Ich möchte da ungern ein eigenes Projekt daraus machen.
Ein anderer schreibt dazu immer: "Keine Arme, keine Kekse."