Forum: Mikrocontroller und Digitale Elektronik Atomisches "__enable_irq () + WFI" / condition variablen für ARM


von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Moin,

wie kann man beim Cortex-M3/4 (STM32) in einem Schritt die Interrupts 
aktivieren und den Sleep-Modus (WFI) betreten, ohne dass Interrupts 
dazwischen funken?

Das ist relevant bei solchen Konstrukten, wenn man den Sleep-Mode zur 
Reduktion der Leistungsaufnahme einsetzen möchte:
1
volatile int counter = 0;
2
3
void TIM2_IRQHandler (void) {
4
  ++counter;
5
}
6
7
int main () {
8
  HAL_TIM_Base_Start_IT(&htim2);
9
  ...
10
  while (1) {
11
    __disable_irq ();
12
    int c = counter;
13
    counter = 0;
14
    if (c == 0) {
15
      __enable_irq ();
16
      // Race Condition hier!!
17
      __WFI ();
18
    } else
19
      __enable_irq ();
20
    
21
    if (c != 0) {
22
      doSomething (c);
23
    }
24
  }
25
}

Der Interrupt signalisiert über die Variable, dass etwas zu tun ist. Die 
Funktion doSomething kann nicht direkt im Interrupt ausgeführt werden, 
weil sie langsam ist, Delays nutzt, Teil einer proprietären Bibliothek 
ist welche nicht geändert werden kann o.ä. Die Interruptsperre ist aber 
nötig um counter konsistent modifizieren zu können.

Die Tücke ist: Wenn genau zwischen dem __enable_irq() und dem WFI der 
Interrupt kommt und den counter inkrementiert, betritt der Controller 
trotzdem den Sleep-Modus und wacht ggf. nie wieder/zu spät wieder auf.

Man muss irgendwie sicherstellen dass zwischen Abfragen des counters und 
Betreten des Sleep-Mode kein Interrupt auftreten kann - oder die ISR 
muss irgendwie das Betreten des Sleepmode verhindern. Ersetzt man die 
Interrupt-Sperre durch Atomics, hat man übrigens das gleiche Problem, 
aber eben zwischen Abfrage des Atomic-Werts und WFI.

Gibt es dafür eine smarte Lösung?

Eine Möglichkeit wäre es WFE statt WFI zu nutzen und SEV im 
Interrupt-Handler bzw. SEVONPEND. Aber eigentlich ist dieser Mechanismus 
für Inter-Core-Kommunikation intendiert, in Multicore-Systemen würde man 
damit unnötig andere Kerne wecken.

Das Problem tritt sehr ähnlich auf wenn man Task-Queues für "normale" 
Multithreading-Systeme implementieren möchte, dort können Condition 
Variables helfen. So etwas bräuchte man hier auch, aber eben ohne 
Multithreading.

Mir ist eine relativ komplizierte Lösung mit SVC und SLEEPONEXIT 
eingefallen. Kennt jemand eine elegante kompakte Lösung? Wie macht man 
das bei Controllern welche kein WFE+SEV oder SLEEPONEXIT haben, wie z.B. 
AVR?

von Peter D. (peda)


Lesenswert?

Niklas G. schrieb:
> Wie macht man
> das bei Controllern welche kein WFE+SEV oder SLEEPONEXIT haben, wie z.B.
> AVR?

Beim AVR ist das elegant gelöst:
The instruction following SEI will be executed before any pending 
interrupts
D.h. der folgende Befehl kann noch nicht durch einen Interrupt 
unterbrochen werden. Die Sequenz SEI+Befehl ist also immer atomar und 
damit auch SEI+SLEEP.
Interessant wäre, ob das auch ohne vorheriges CLI funktioniert.

: Bearbeitet durch User
von Foobar (asdfasd)


Lesenswert?

peda schrieb:
> Beim AVR ist das elegant gelöst
> The instruction following SEI will be executed before any pending
> interrupts

Dummerweise im clib falsch umgesetzt.  "sei(); sleep_cpu();" soll das 
erledigen.  Allerdings sind das zwei getrennte asm-statements und der 
Compiler darf dazwischenpfuschen.  Habe erlebt, dass das sleep_cpu durch 
CSE verschoben wurde - im Assemblercode stand dann ein 
"sei+jmp_to_sleep" :-(

von Tassilo B. (big_t)


Lesenswert?

Niklas G. schrieb:
1
> if (c == 0) {
2
>       __enable_irq ();
3
>       // Race Condition hier!!
4
>       __WFI ();
5
>    } else
6
>
7
>     __enable_irq ();
Ändere das zu
1
if (c == 0) {
2
  __WFI ();
3
}
4
__enable_irq ();
und fertig. Das WFI kommt sofort wieder wenn ein aktivierter Interrupt 
'pending' ist, unabhängig davon, wie das globale INTs-enable gerade 
steht. Siehe auch
https://community.arm.com/support-forums/f/architectures-and-processors-forum/4039/wfe-wfi-and-pending-interrupts

und
https://developer.arm.com/documentation/dui0552/a/the-cortex-m3-instruction-set/miscellaneous-instructions/wfi?lang=en

oder
https://www.embedded.com/use-an-mcus-low-power-modes-in-foreground-background-systems/

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Tassilo B. schrieb:
> Das WFI kommt sofort wieder wenn ein aktivierter Interrupt
> 'pending' ist, unabhängig davon, wie das globale INTs-enable gerade
> steht.

Ahhh stimmt, der interessante Satz ist:

> WFI is a hint instruction that suspends execution until one of the following 
events occurs: [...]
> an interrupt masked by PRIMASK becomes pending

Damit ist es tatsächlich ganz einfach. Man sollte wohl nur direkt nach 
dem WFI die Interrupts wieder aktivieren damit diese schnell 
abgearbeitet werden.

Klasse, 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.