Forum: Mikrocontroller und Digitale Elektronik STM32F030x08 > UART > IRQ und Puffer Betrieb


von Jan H. (janiiix3)


Lesenswert?

Hallo @ Community,

ich hätte mal eine Frage zum Thema: "UART und senden via. IRQ mit einem 
Puffer".

Wie ist denn der grobe Ablauf?
Ich beschreibe meinen Puffer (z.B. Ringpuffer) und nach dem beschreiben 
sage ich ihm das er ein Interrupt feuern soll und dort werden dann die 
Daten aus dem Puffer gelesen und in das Ausgaberegister geschrieben?.

von Stefan F. (Gast)


Lesenswert?

Anders herum.

Der UART ruft die ISR auf, wenn er bereit ist, das nächste Byte zu 
senden. Die ISR schiebt bei jedem Aufrauf ein Byte auf dem Puffer in das 
TDR Register des UART.

Ganz am Anfang muss das erste Byte am Puffer vorbei gesendet werden, 
indem man es direkt ins TDR Register schreibt.

Wenn alle gepufferten Bytes gesendet wurden, hat die letzte ISR nichts 
mehr zu tun. Es wird nichts mehr ins TDR Register geschrieben. Weiter 
geht es dann ggf. bei "Ganz am Anfang...".

von Jan H. (janiiix3)


Lesenswert?

Stefan ⛄ F. schrieb:
> Ganz am Anfang muss das erste Byte am Puffer vorbei gesendet werden,
> also ohne auf einen Interrupt zu warten. Gleiches gilt auch, nachdem
> alle gepufferten Zeichen gesendet wurden und man später etwas neues
> senden will.

Du meinst also erstmal die Daten in den Puffer schreiben,
dann einmal..
1
xx->TDR = data
und in der ISR dann quasie Byte für Byte aus dem Puffer holen?.

von Stefan F. (Gast)


Lesenswert?

Jan H. schrieb:
> Du meinst also erstmal die Daten in den Puffer schreiben,
> dann einmal..xx->TDR = data
> und in der ISR dann quasie Byte für Byte aus dem Puffer holen?.

Nein! Das erste Byte wird nicht gepuffert.

Das allererste Byte musst du direkt ins TDR Register schreiben. Erst 
danach löst der UART den ersten Interrupt aus. Der Interrupt Handler 
und der Puffer ist als nur für das zweite und alle folgenden Bytes 
zuständig.

Bis es nichts mehr zu senden gibt, dann fängst du wieder von vorne an, 
mit direktem Zugriff auf TDR.

von Jan H. (janiiix3)


Lesenswert?

> Nein!
>
> Das allererste Byte musst du direkt ins TDR Register schreiben. Erst
> danach (wenn es gesendet wurde) löst der UART einen Interrupt aus. Der
> Interrupt Handler ist als nur für das zweite und alle folgenden Bytes
> zuständig.
>
> Bis es nichts mehr zu senden gibt, dann fängst du wieder von vorne an,
> mit direktem Zugriff auf TDR.
1
/*
2
 * BETA
3
 */
4
FIFO_ERROR_CODE_t  __USARTSend( USART_TypeDef *pUsart, char *pcData )
5
{
6
  FIFO_ERROR_CODE_t eError = FIFO_OK;
7
8
  pUsart->TDR = *pcData++;
9
  
10
  for( ; *pcData != '\0'; pcData++ )
11
  {
12
      eError |= FIFOPush( *pcData );
13
  }
14
15
  return eError;
16
}
1
void USART1_IRQHandler(void)
2
{
3
  if ( ( USART1->ISR & USART_ISR_RXNE ) == USART_ISR_RXNE )
4
  {
5
    FIFOPush( USART1->RDR );
6
7
    GPIOTogglePin( GPIOC, GPIO_PIN_9 );
8
9
    USART1->RQR |= USART_RQR_RXFRQ; // clear flag ( write 1 to clear )
10
  }
11
12
13
  if ( ( USART1->ISR & USART_ISR_TXE ) == USART_ISR_TXE )
14
  {
15
    uint8_t uiByte;
16
    FIFO_ERROR_CODE_t eFIFO = FIFO_OK;
17
18
    eFIFO = FIFOPull( &uiByte );
19
    if ( eFIFO != FIFO_OK )
20
    {
21
      USART1->CR1 &= ~ (USART_CR1_TXEIE | USART_CR1_TCIE );
22
    }else
23
    {
24
        USART1->TDR = uiByte;
25
    }
26
  }
27
}

Meinst Du das so?

von W.S. (Gast)


Lesenswert?

Jan H. schrieb:
> Wie ist denn der grobe Ablauf?

Eigentlich ganz einfach (sofern eine Ähnlichkeit besteht mit dem 
STM32F103):

Es gibt zwei Funktionen, die von außen her (also von main.. her) gerufen 
werden. Das ist zum einen das Lesen eines empfangenen Zeichens und zum 
anderen das Schreiben eines zu sendenden Zeichens.
Die Funktion zum Senden stopft das Zeichen in einen Ringpuffer und gibt 
dann bei jedem eintrudelnden Zeichen den Sende-Interrupt des UART-Cores 
frei.

Der Sendezweig des UARTs erzeugt immer einen Interrupt, wenn noch ein 
weiteres Zeichen in den Sender geschoben werden kann. Die 
Interupt-Routine testet, ob im Ringpuffer ein Zeichen herumliegt und 
wenn ja, dann stopft sie das Zeichen in den Sender. Wenn nicht, dann 
schaltet sie den Sende-Interrupt wieder aus.

Damit wird das Senden bei jedem Zeichen wieder angestoßen, das dem 
UART-Treiber zum Senden übergeben wird. Wenn alles gesendet ist, dann 
wird der Interrupt wieder gesperrt (ist wichtig!) und es passiert nix 
bis zum nächsten Zeichen.

Nochwas: Es sollte noch einige weitere Funktionen geben, und zwar für:
- Abfragen, ob ein empfangenes Zeichen vorliegt
- Abfragen, ob der Sendepuffer noch ein weitees Zeichen vertragen kann

W.S.

Beitrag #6966021 wurde von einem Moderator gelöscht.
von Stefan F. (Gast)


Lesenswert?

Jan, ich muss mich bei dir entschuldigen, habe da zwei Mikrocontroller 
miteinander verwechselt, so dass eine irreführende Antwort dabei heraus 
kam. WS hat es korrekt beschrieben.

Die ISR wird beim STM32 nicht nur 1x aufgerufen, wenn der USART bereit 
ist etwas zu senden, sondern sie wird immer wieder aufgerufen, solange 
er bereit ist. Die Konsequenz daraus ist, dass man den Interrupt 
abschalten muss, wenn man nichts mehr zu senden hat. Deine ISR ist also 
insofern stimmig.

Beim Senden ist es in der Tat am einfachsten, erst alles in den Puffer 
zu schreiben (so wie du das vor hattest) und dann die Interrupts 
einzuschalten. Dann ist ausschließlich die ISR für das Senden der 
gepufferten Daten zuständig.

Du kannst auch das erste Byte direkt (am Puffer vorbei) senden und 
danach die Interrupts einschalten. Dadurch entfällt ein Interrupt, man 
spart ein kleines bisschen Zeit. Allerdings wird der Code in 
__USARTSend() komplexer, vermutlich lohnt sich der Aufwand nicht.

Deine __USARTSend() musst du nochmal anpassen. Schau dir die Funktion 
Usart1Put() auf 
https://embedds.com/programming-stm32-usart-using-gcc-tools-part-2/ an.

Sorry, dass ich Verwirrung gestiftet habe.

von Jan H. (janiiix3)


Lesenswert?

Stefan ⛄ F. schrieb:
> Jan, ich muss mich bei dir entschuldigen, habe da zwei
> Mikrocontroller miteinander verwechselt, so dass eine irreführende
> Antwort dabei heraus kam. WS hat es korrekt beschrieben.
> Die ISR wird beim STM32 nicht nur 1x aufgerufen, wenn der USART bereit
> ist etwas zu senden, sondern sie wird immer wieder aufgerufen, solange
> er bereit ist. Die Konsequenz daraus ist, dass man den Interrupt
> abschalten muss, wenn man nichts mehr zu senden hat. Deine ISR ist also
> insofern stimmig.
> Beim Senden ist es in der Tat am einfachsten, erst alles in den Puffer
> zu schreiben (so wie du das vor hattest) und dann die Interrupts
> einzuschalten. Dann ist ausschließlich die ISR für das Senden der
> gepufferten Daten zuständig.
> Du kannst auch das erste Byte direkt (am Puffer vorbei) senden und
> danach die Interrupts einschalten. Dadurch entfällt ein Interrupt, man
> spart ein kleines bisschen Zeit. Allerdings wird der Code in
> __USARTSend() komplexer, vermutlich lohnt sich der Aufwand nicht.
> Deine __USARTSend() musst du nochmal anpassen. Schau dir die Funktion
> Usart1Put() auf
> https://embedds.com/programming-stm32-usart-using-gcc-tools-part-2/ an.
> Sorry, dass ich Verwirrung gestiftet habe.


Alles gut.
Bei dem verlinkten Beispiel, packt er ja nur Byte für Byte in den Fifo 
und aktiviert dann direkt den interrupt…
Sollte man nicht erstmal den Fifo ein wenig füllen und dann senden? Was 
ist effektiver?

von Stefan F. (Gast)


Lesenswert?

Jan H. schrieb:
> Sollte man nicht erstmal den Fifo ein wenig füllen und dann senden? Was
> ist effektiver?

Ich glaub nicht dass das einen wesentlichen Unterschied macht. So ein 
einfacher Registerzugriff ist ja nicht teuer.

Falls du es doch machst berücksichtige den Fall dass die zu sendenden 
Daten größer sein können, las der Sendepuffer. Nicht dass du dann in 
einer Warteschleife hängen bleibst weil der Interrupt noch nicht 
aktiviert ist.

von W.S. (Gast)


Lesenswert?

Jan H. schrieb:
> Sollte man nicht erstmal den Fifo ein wenig füllen und dann senden? Was
> ist effektiver?

Nö, denn dein UART-Treiber weiß ja nicht, ob und wann und wieviele 
Zeichen demnächst bei ihm hereintrudeln werden. Deshalb ist jeder 
Gedanke daran, erstmal den Fifo etwas zu füllen, bevor man mit der 
Arbeit beginnt, gründlich falsch. Der Fifo dient letztlich für 2 Dinge:
- Entkopplung der ISR-Welt von der maín()-Welt
- Puffern von nicht allzu langen Ausgaben, so daß bei kürzeren Ausgaben 
der µC ohne Wartezeiten weitermachen kann. Wenn man die gesamte Bibel 
senden will, dann nützt das aber nix. Die ist zu umfänglich, weswegen 
man dann auf das Senden doch wieder warten muß.

W.S.

von Olaf (Gast)


Lesenswert?

Ich denke das Problem besteht darin das es nicht die eine
optimale Loesung gibt. Es haengt leider immer auch etwas
von der Anwendung und den Daten ab.
Das zeigt alleine schon die Existenz von Befehlen wie "flush()"


Olaf

von Klaus S. (kseege)


Lesenswert?

Olaf schrieb:
> Ich denke das Problem besteht darin das es nicht die eine
> optimale Loesung gibt. Es haengt leider immer auch etwas
> von der Anwendung und den Daten ab.
> Das zeigt alleine schon die Existenz von Befehlen wie "flush()"

Aus meiner Sicht ist das eher eine Hardwarefrage. Hat man eine CPU, bei 
der der Kontextwechsel (wie z.B. der Interrupt) nur wenig CPU-Zeit 
kostet, puffert man nur zwischen Anwendung und ISR und schickt jedes 
Zeichen einzeln zum UART. So wie es auch W.S. beschrieben hat.

Hat man dagegen sowas wie x86 (mit laaaang dauerndem Kontextwechsel) und 
einen UART mit FIFO, dann spart man sich 9 Kontextwechsel und sendet 10 
Bytes auf einmal zum UART-FIFO. Erst dann braucht man flush(), um die 
letzten <10 im Puffer hängenden Bytes, die die Nachricht 
vervollständigen, auch noch auf die Reise zu schicken. Und nur dann 
lohnt es sich, nicht gleich loszubrettern.

Just my 2 cents

von Jan H. (janiiix3)


Lesenswert?

Ich werde die Lösung später mal ausprobieren.

von W.S. (Gast)


Lesenswert?

Olaf schrieb:
> Das zeigt alleine schon die Existenz von Befehlen wie "flush()"

Ach nein. Sowas wie "flush()" zeigt lediglich an, daß derjenige, der 
sowas geschrieben hat, mit seinem Latein am Ende war und dies als einen 
Notbehelf dann geschrieben hat. Ein serieller Kanal wie ein UART 
benötigt so etwas von Haus aus nicht (weil er auch sendet, wenn der 
Empfänger abgestöpselt ist), lediglich für den Übergang zwischen seriell 
und blockorientiert (wie eben beim virtellen COM-Port) kommen da 
Probleme auf, die eigentlich gelöst sein wollen und nicht mit einem 
flush() umgangen.

W.S.

von W.S. (Gast)


Lesenswert?

Klaus S. schrieb:
> Hat man dagegen sowas wie x86 (mit laaaang dauerndem Kontextwechsel) und
> einen UART mit FIFO, dann spart man sich...

Nanana, das Retten von CPU-Registern dauert auch beim 80x86 nicht lang. 
Und wenn der HW-Fifo im Sendezweig eines UART groß ist, dann kann man 
den auch mit allen anderen Architekturen nutzen. Aber bei gar vielen 
Chips hat man als UART-Core nur einen, der ein Senderegister und danach 
das eigentliche Schieberegister hat. Also ein "Fifo" mit nur 2 Plätzen. 
Das spart vermutlich einiges an Chipfläche und der Aufwand, beim Anfang 
der ISR die CPU-Register zu retten, ist dagegen vergleichsweise gering.

W.S.

von Stefan F. (Gast)


Lesenswert?

Ich kenne flush() von zahlreichen Bibliotheken mit unterschiedlichen 
Bedeutungen:

a) Bei Schnittstellen, die Daten Paketweise oder in Intervallen senden, 
dient der Befehl dazu, jetzt sofort zu senden, auch wenn das aktuelle 
Paket/Intervall noch nicht voll ist.

b) Es dient dazu, alle gepufferten Daten jetzt sofort blockierend zu 
senden. Nach Ausführung des Befehls ist der Sendepuffer leer.

c) Er dient dazu, den Inhalt des Puffers zu verwerfen. Kann sowohl den 
Sende-Puffer als auch (häufiger) den Empfangs-Puffer betreffen.

> daß derjenige, der sowas geschrieben hat, mit seinem Latein
> am Ende war und dies als einen Notbehelf dann geschrieben hat.

Sehe ich nicht so. Du pauschalisierst das zu sehr, bzw. neigst dazu 
deinen persönlichen Programmierstil anderen aufzudrängen.

von Klaus S. (kseege)


Lesenswert?

W.S. schrieb:
> Nanana, das Retten von CPU-Registern dauert auch beim 80x86 nicht lang.

Lang ist relativ. Für einen Kernphysiker sind 10 hoch -22 Sekunden lang 
und 10 hoch -26 Sekunden kurz. Wer bietet weniger?

Ich bin kein CPU-Kenner, aber ich weiß, daß sowohl Realtime-Linux als 
auch QNX nur Kontextwechselzeiten von ca.10 Mikrosekunden auf einem 
3GHz-Prozessor hinkriegen, das sind dreißigtausend Takte. Das nenne ich 
lang und das wird einen handfesten Grund haben, da sitzen ja keine 
Forumstrolle. Und nur deshalb gibt es mMn UARTs mit FIFO und das schon 
seit dem 286er.

Im Übrigen ging es mir nur darum, die beiden Endsituationen zu 
skizzieren, unabhängig davon, wie lang nun lang ist. Jeder kann sich 
mit FIFOs beliebiger Größe dazwischen positionieren. Der x86 war nur der 
mir grade geläufigste Typ. Keine Ahnung, wie "big iron" das erledigt. 
IBMs JCL schickt mWn auch immer ganze Zeilen, hat also auch keine Lust, 
Character einzeln zu übertragen.

Und wer unter Linux oder Windows den "canonical mode" des UART-Treibers 
benutzt (aus welchen Gründen auch immer), der braucht eben auch manchmal 
flush(), um zum Ziel zu kommen. Wer den "cm" nie benutzt, der braucht 
auch kein flush() und muß nix drüber wissen, da bin ich mit Dir einig. 
Es sollte dann aber klar sein, daß es maximal zu Halbwahrheiten reicht. 
Keine Ahnung, wie ich das freundlicher ausdrücken kann, weil ich Dich 
und Dein Wissen schätze.

Gruß Klaus

von W.S. (Gast)


Lesenswert?

Klaus S. schrieb:
> Ich bin kein CPU-Kenner, aber ich weiß, daß sowohl Realtime-Linux als
> auch QNX nur Kontextwechselzeiten von ca.10 Mikrosekunden auf einem
> 3GHz-Prozessor hinkriegen, das sind dreißigtausend Takte.

Das sind aber auch ganz andere Randbedingungen. Ein Kontextwechsel bei 
PC-Betriebssystemen umfaßt nicht nur die paar Register einer CPU in 
einem µC, sondern auch die Zustände im Gleitkomma-Prozessor, sämtliche 
taskabhängige Dinge wie Setzen von Rechten, Einrichten der ggf. 
vorhandenen CPU-Kerne, des Heaps usw. Das ist dramatisch mehr, als das, 
womit wir es hier zu tun haben.

Und bei manchen Dingen wie vor allem bei zusammengesetzten Datenwegen - 
seriell->paketweise->dateiartig, wie man es z.B. bei Windows hat, wenn 
man Daten seriell von einem µC per VCP/USB an ein Programm im PC senden 
will - gibt es an allen Stellen eben Stoßstellen, an denen man abwägen 
muß. Wer (aus was für Gründen auch immer) per VCP nur voll gefüllte 
Blöcke übertragen will, der hat auf der Sendeseite große Probleme, denn 
er kann nicht vorhersagen, ob und wann auf einem asynchronen Inputkanal 
wie viele Zeichen in Zukunft hereinkommen werden. Da ist man mit seinem 
Latein am Ende. In so einem Falle gibt es nur 3 Wege:
1. den Anspruch aufgeben, nur volle Blöcke übertragen zu wollen.
2. den Anspruch der Asynchronität des Inputs aufgeben.
3. die Forderung aufstellen, daß das aufrufende Programm sowas wie eine 
Putzfrau namens flush() hinterherschicken soll. Damit wird das Problem 
auf die aufrufenden Programme abgeschoben.

Da muß sich jeder entscheiden. Wie auch immer.

W.S.

von Klaus S. (kseege)


Lesenswert?

W.S. schrieb:
> Das ist dramatisch mehr, als das, womit wir es hier zu tun haben.

Genau. Den Grund dafür hatte ich ja schon beschrieben.

Die folgende Schauergeschichte über einen Vollpfosten gehört für mich 
zwar eher in die Literaturabteilung. Hat mir aber viel Vergnügen gemacht 
und ist ein schönes Beispiel dafür, wohin es führt, wenn man allzu 
platten Annahmen nachgeht.

Danke :-)))

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.