Forum: Mikrocontroller und Digitale Elektronik FreeRTOS seltsames Verhalten bei SemaphoreTake


von Sven (Gast)


Angehängte Dateien:

Lesenswert?

Hi zusammen

Kurz und einfach gesagt: Ich habe hier ein ganz seltsames Verhalten beim 
Debuggen eines SPI Treibers aud FreeRTOS basierend.

Wichtigster Code-Ausschnitt (vollständige Datei im Anhang):

```c
static volatile SemaphoreHandle_t spiMutex = NULL;
static volatile StaticSemaphore_t spiMutexStatic;

static inline error_t spi_mutexTake()
{
    if (spiMutex == NULL) // <- Break-Point 1
        spiMutex = 
xSemaphoreCreateMutexStatic((StaticSemaphore_t*)&spiMutexStatic);
    if (xSemaphoreTake(spiMutex, SPI_CONFIG_PORT_DELAY) != pdTRUE) // <- 
Break-Point 2
        return SPI_ERROR_MUTEX_TAKE_FAILED;
    else
        return SPI_ERROR_SUCCESS;
}

static inline error_t spi_mutexGive()
{
    if (xSemaphoreGive(spiMutex) != pdTRUE)
        return SPI_ERROR_MUTEX_GIVE_FAILED;
    else
        return SPI_ERROR_SUCCESS;
}

error_t spi_run(/* params */)
{
    ON_ERROR_RETURN(spi_mutexTake());
    spi_doStuff(); // ...
    ON_ERROR_RETURN(spi_mutexGive());
}
```

Ich habe nun 2 tasks laufen. Task 1 greift als erstes auf den SPI 
Treiber zu. Er sendet und empfängt wenige bytes indem zwei mal 
nacheinander `spi_run()` aufgerufen wird. Hier läuft alles einwandfrei, 
beim ersten aufruf von `spi_mutexTake()` wird das statische Semaphore 
initialisiert und `spiMutex` enthält ab Break-Point 2 die Adresse 
`0x02f0`.
Nun wird zu Task 2 gewechselt, welcher ebenfalls `spi_run()` aufruft. 
Doch hier geschiet was ganz interessantes. `spi_mutexTake()` wird 
aufgerufen und ist zu Break-Point 1 noch adresse `0x02f0` (die Bedingung 
`if (spiMutex == NULL)` müsste also false sein), doch ab Break-Point 2 
zeigt dieser ins völlige Nirwana von `0x2001` ausserhalb des Speichers 
und nach dem Aufruf von `spi_mutexGive()` zeigt spiMutex sogar auf 
`NULL` (hier wird aber glücklicherweise auch 
`SPI_ERROR_MUTEX_GIVE_FAILED` zurückgegeben).

Ich habe leider wenig Ahnung, was der Compiler alles hier versucht zu 
(über-)optimieren, doch beim besten Willen kann ich mir nicht erklären, 
was da geschiet.

Ausserhalb von `spi_MutexTake()` findet nie eine Zuweisung für spiMutex 
statt. Darum habe ich auch keinen Schimmer, wieso sich dessen Wert auf 
einmal ändert.

Falls jemand einen Tipp hat, gerne her damit...

von foobar (Gast)


Lesenswert?

Hab keine Ahnung von FreeRTOS, aber wo ist die Mutex, die den Zugriff 
auf spiMutex schützt?  Die volatiles bringen nichts.

Sieh lieber zu, dass spiMutex initialisiert wird, bevor du die Tasks 
startest.

von Sven (Gast)


Lesenswert?

foobar schrieb:
> Hab keine Ahnung von FreeRTOS, aber wo ist die Mutex, die den Zugriff
> auf spiMutex schützt?  Die volatiles bringen nichts.

Die volatiles sind nur dazu da damit ich das zeug anständig debuggen 
kann.

Im Anhang siehst du das ursprünglich `taskENTER_CRITICAL()` dabei ist im 
Makro `_SPI_MUTEX_TAKE(m,s,e)`. Das wäre der Schutz vor dem Zugriff.

foobar schrieb:
> Sieh lieber zu, dass spiMutex initialisiert wird, bevor du die Tasks
> startest.

Hat bisher eigentlich immer ganz gut so geklappt, aber in der aktuellen 
Situation tatsächlich gar nicht verkehrt. Probier ich später gleich aus, 
danke!

von foobar (Gast)


Lesenswert?

> taskENTER_CRITICAL ... Das wäre der Schutz vor dem Zugriff.

Hmmm... ich denke, das reicht nicht.  Laut Doku sperrt das nur 
Interrupts und dadurch das preemptive Scheduling, nicht das explizite. 
Bei SMP reicht das eh nicht, da braucht man Spinlocks (keine Ahnung, ob 
die dann in taskXX_Critical drin sind, Doku äußert sich nicht dazu).

Insb steht da auch noch: "FreeRTOS API functions must not be called from 
within a critical section." ...

von Sven (Gast)


Lesenswert?

foobar schrieb:
> Insb steht da auch noch: "FreeRTOS API functions must not be called from
> within a critical section." ...

Stimmt, weil das nämlich in den APIs selbst aufgerufen wird (ist mir 
gerade bei genaueren debuggen aufgefallen).
Die API functions sind also bereits selbst thread-safe. (Erklärt auch 
warum in den Docs in keinem Beispiel irgend ein mechanismus dazu 
verwendet wird)

Wenn das preemptive Scheduling nämlich deaktiviert ist und die API 
Funktion selbst keinen Taskwechsel erzeugt, dann muss der aktuelle Task 
zwangsweise die Funktion vollständig auführen ohne eine Gefahr...

Den Fehler selbst habe ich derweil gefunden. Man sollte nicht zu sparsam 
sein bei der Stackzuweisung, auch wenn dieser AVR nicht gerade der 
speicherreichste ist. Der Stackoverflow hat hat perfekt den angrenzenden 
Speicherbereich überschrieben, in dem gerade der Semaphore-Pointer 
versorgt war. Mein Fehler...

@foobar danke dir trotzdem: Die enter und exit critical sections hab ich 
jetzt raus genommen. Sie sind nicht unbedingt gefährlich, aber nach der 
Überprüfung zumindest tatsächlich überflüssig an dieser Stelle.
Gefährlich wird es erst wenn man die critical section tatsächlich 
benötigt, denn wenn man darin eine API aufruft, dann hebt diese API die 
critical section auf (deswegen auch "FreeRTOS API functions must not be 
called from
within a critical section.").

von foobar (Gast)


Lesenswert?

Wie gesagt, hab bisher nichts mit FreeRTOS gemacht, aber:

> Gefährlich wird es erst wenn man die critical section tatsächlich
> benötigt, denn wenn man darin eine API aufruft, dann hebt diese API
> die critical section auf

Laut der Doku sind Critical Sections (CS) verschachtelbar und damit 
heben sie die äußere CS nicht auf.  Das Verbot dürfte andere Gründe 
haben, z.B. dass innerhalb einer CS keine blockierenden Funktionen 
aufgerufen werden dürfen.

von Sven (Gast)


Lesenswert?

foobar schrieb:
> Laut der Doku sind Critical Sections (CS) verschachtelbar und damit
> heben sie die äußere CS nicht auf.  Das Verbot dürfte andere Gründe
> haben, z.B. dass innerhalb einer CS keine blockierenden Funktionen
> aufgerufen werden dürfen.

Ups, da hast du recht... Habs gerade selbst durchgelesen.
Dann wird es ganz sicher daran liegen, danke dir!

von Peter D. (peda)


Lesenswert?

Typisch implementiert man Interfaces als Stream.
Benötigt eine Task ein Interface, wird es mit fopen belegt und nach Ende 
mit fclose wieder freigegeben.

Eine andere Möglichkeit ist, keine Task darf das Interface direkt 
benutzen. Es werden entsprechend viele Puffer angelegt, die ein 
gemeinsamer Interrupthandler der Reihe nach abarbeitet. Jede Task 
schreibt und liest dann ihren Puffer.

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.