Forum: Mikrocontroller und Digitale Elektronik STM32: effizienter UART-Empfang mit DMA-Ringpuffer


von drama (Gast)


Lesenswert?

Moin,

ich bin gerade dabei, auf einem STM32 eine möglichst effiziente 
Implementierung für den Empfang von Daten über mehrere UARTs 
auszuarbeiten. Es sind insgesamt drei UARTs aktiv, die mit hoher 
Geschwindigkeit (mind. 230400 Baud) Daten empfangen und senden.

Um die Daten ohne andauernde Aktivierung der CPU zu empfangen, nutze ich 
DMA im circular mode, d.h. als Ringpuffer. Das funktioniert prinzipiell 
sehr gut. Die eigentlichen Empfänger der Daten sind mehrere 
FreeRTOS-Prozesse.

Nun habe ich allerdings das Problem, dass ich den Prozessen mitteilen 
muss, wenn neue Daten angekommen sind. Grundsätzlich nutze ich dafür 
Semaphoren. Das heißt, die Prozesse lesen so viel wie möglich aus den 
Ringpuffern (entsprechend dem NDTR-Register des DMA-Streams), und wenn 
das nicht mehr möglich ist, warten sie auf einen Semaphor, der die 
Ankunft neuer Daten ankündigt. Damit kann man auch Timeouts sehr einfach 
realisieren.

Ich kann nun die Semaphoren mit Interrupts freigeben, wenn neue Daten 
empfangen werden. Dafür nutze ich momentan "transfer complete", 
"transfer half complete" des DMA-Streams und "idle" des UARTs. Das 
heißt, immer wenn der Ringpuffer halb voll bzw voll ist oder der UART 
keine neuen Daten mehr empfängt, wird der Semaphor freigegeben.

Ich habe nun das Problem, dass damit die CPU zu selten aufgeweckt wird 
und nicht genügend Zeit bleibt, die neuen Daten zu verarbeiten. Es kommt 
zum overrun! :(

Wenn ich alternativ den RXNE-Interrupt des UART verwende, um den 
Semaphor freizugeben, gibt es das Problem nicht mehr. Aber dann gibt es 
eine Flut an Interrupts, einer pro empfangenen Zeichen, und ich brauche 
sehr viel CPU-Zeit. :(

Ich brauche also idealerweise eine Möglichkeit, per Interrupt regelmäßig 
(bspw. alle n Zeichen) den Semaphor freizugeben.

Hat jemand eine Idee?

von drama (Gast)


Lesenswert?

Relevanter Code:

Empfang von Daten im Prozess:
1
static void read_fifo(struct uart_rtos *u, char *ptr, int len)
2
{
3
    do {
4
        if ((UART_FIFO_SIZE - u->dmarx_handle.Instance->NDTR) != u->read_ptr) {
5
            /* read a byte if we can */
6
            *ptr++ = u->rxbuf[u->read_ptr++];
7
            u->read_ptr %= UART_FIFO_SIZE;
8
            len--;
9
        } else {
10
            /* or wait for new data */
11
            xSemaphoreTake(u->rx_mutex, portMAX_DELAY);
12
        }
13
    } while (len > 0);
14
}

Interrupts, die den Semaphor bei "idle" freigeben:
1
static void usart_fifo_irq(struct uart_rtos *u)
2
{
3
    BaseType_t woken;
4
5
    __HAL_UART_CLEAR_IDLEFLAG(&u->uart_handle);
6
    xSemaphoreGiveFromISR(u->rx_mutex, &woken);
7
    HAL_UART_IRQHandler(&u->uart_handle);
8
    portYIELD_FROM_ISR(woken);
9
}

Interrupts für complete / half complete:
1
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
2
{
3
    struct uart_rtos *uart = (struct uart_rtos *)huart;
4
    BaseType_t woken;
5
6
    xSemaphoreGiveFromISR(uart->rx_mutex, &woken);
7
    uart->error = 0;
8
    portYIELD_FROM_ISR(woken);
9
}
10
11
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
12
{
13
    struct uart_rtos *uart = (struct uart_rtos *)huart;
14
    BaseType_t woken;
15
16
    xSemaphoreGiveFromISR(uart->rx_mutex, &woken);
17
    uart->error = 0;
18
    portYIELD_FROM_ISR(woken);
19
}

Das ist nicht vollständig, sollte aber reichen um eine Idee davon zu 
bekommen, wie es zur Zeit funktioniert.

von Stefan (Gast)


Lesenswert?

Wie groß sind Deine DMA-Puffer?
Wie stellst Du den Overrun fest?

Gruß, Stefan

von drama (Gast)


Lesenswert?

Stefan schrieb:
> Wie groß sind Deine DMA-Puffer?

z.Z. 64 Byte. Ich kann die auch größer machen, dann dauert es nur etwas 
länger, bis ein Overrun auftritt.

> Wie stellst Du den Overrun fest?

Noch gar nicht explizit, ich merk es nur daran, dass beim Empfang Mist 
herauskommt und Bytes fehlen. Es sollte sich aber leicht daran 
feststellen lassen, wenn der write pointer den read pointer "überholt".

von Stefan (Gast)


Lesenswert?

Bist Du Dir sicher, daß die Tasks rechtzeitig bedient werden? Bei 64 
Byte großen Puffern hast Du ab half-full und 230400 Baud noch 32 / 
2304000 * 10 = ca. 1,4ms Zeit, bis der Puffer überläuft. Diese Zeit muß 
für Deinen verarbeitenden Task und alle höherprioren ausreichen. Ggf. 
diesen Task mal auf die höchste Prio setzen und schauen, ob das Problem 
noch auftritt.

Wenn Du jedes Zeichen einzeln verarbeitest, dann gewinnst Du maximal 
eine Verdopplung der Verarbeitungszeit. Das kannst Du genauso durch eine 
Verdopplung des Eingangspuffers erreichen.

Die Möglichkeit "Interrupt nach n Zeichen" gibt es meines Wissens nicht. 
Hätte ich auch schon gut gebrauchen können ;-)

Viele Grüße, Stefan

von Gerd E. (robberknight)


Lesenswert?

Du könntest mit einem auf die Baudraten angepassten Timer regelmäßig den 
Füllstand des Puffers auslesen. Wenn der sich in einem gewissen Fenster 
bewegt, gibst Du die Semaphore zum Auslesen frei.

von Martin K. (martinko)


Lesenswert?

Ping Pong spielen könnte helfen.

Du kannst am DMA Kontroller 2 Puffer anmelden.
Wenn ein Puffer voll ist kommt ein Interrupt. Der DMA Kontroller meldet 
dann welcher der beiden Puffer voll wurde, mit dem kannst Du dann machen 
was Du willst da der DMA Kontroller jetzt in dem anderen rum wickelt.
"Machen was Du willst" geht sogar so weit, dass Du den Zeiger auf den 
gerade nicht vom DMA verwendeten Puffer auch ändern kannst. Damit wird 
eine Puffer immer vom DMA verwendet und der andere steht ihm direkt im 
Anschluss nahtlos zur Verfügung.

Meistens reichen diese 2 Puffer, der Inhalt muss ja auch irgendwann 
abgearbeitet werden. Du kannst aber auch zwischen beliebig vielen 
Puffern hin- und herschalten und die jeweils vollen z.B. an eine Message 
Queue übergeben.

Wichtig ist dabei immer: Den Interrupt so schnell wie möglich 
verarbeiten und die eigentliche Arbeit einen Thread machen lassen.

Gruß Martin

: Bearbeitet durch User
von (prx) A. K. (prx)


Lesenswert?

Martin K. schrieb:
> Ping Pong spielen könnte helfen.

Eine ähnliche Rolle spielt bereits ein optionaler DMA-Interrupt des 
STM32, der nach der Hälfte der programmierten Bytes auftritt. Der er 
auch verwendet.

von drama (Gast)


Lesenswert?

Stefan schrieb:
> Bist Du Dir sicher, daß die Tasks rechtzeitig bedient werden? Bei 64
> Byte großen Puffern hast Du ab half-full und 230400 Baud noch 32 /
> 2304000 * 10 = ca. 1,4ms Zeit, bis der Puffer überläuft. Diese Zeit muß
> für Deinen verarbeitenden Task und alle höherprioren ausreichen. Ggf.
> diesen Task mal auf die höchste Prio setzen und schauen, ob das Problem
> noch auftritt.

Guter Punkt, das wird tatsächlich recht knapp.

Gerd E. schrieb:
> Du könntest mit einem auf die Baudraten angepassten Timer regelmäßig den
> Füllstand des Puffers auslesen. Wenn der sich in einem gewissen Fenster
> bewegt, gibst Du die Semaphore zum Auslesen frei.

Das klingt nach einem interessanten Ansatz, der sowas Ähnliches wie 
"Interrupt nach n empfangenen Zeichen" umsetzt. Da habe ich nun aber 
eine IMHO bessere Lösung für: ich lasse das Warten auf den Semaphor 
einfach nach n Ticks auslaufen und probiere dann das Lesen. Praktisch 
also eine Kombination mit Polling.

Letztendlich war das eigentliche Problem aber auch ein ganz anderes: 
ich habe jedes gelesene Zeichen mit einer blockierenden Methode (durch 
einen Semaphor für Transmit) wieder ausgegeben. Da das Schreiben so 
mindestens so lange braucht wie das Lesen, tendenziell aber etwas länger 
durch Overhead, konnte das auf Dauer natürlich nicht klappen und der 
Puffer lief dann garantiert über.

Klarer Fall von PEBKAC also.

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.