Atomisches "__enable_irq () + WFI" / condition variablen für ARM

OP Persönliche Seite #7632103
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?

#7632110
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.

#7632203
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" :-(

#7632487
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/

OP Persönliche Seite #7632668
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!

OP Persönliche Seite #7761209
Lesenswert?

Weil ich da mal wieder drüber gestolpert bin habe ich mir ein Makro dafür gebastelt:

1
#define SLEEP_WAIT_COND(cond) do { \
2
  uint32_t sleepWaitCond_primaskState = __get_PRIMASK (); \
3
  __disable_irq (); \
4
  while (cond) { \
5
    __WFI (); \
6
    __enable_irq (); \
7
    __NOP (); \
8
    __disable_irq (); \
9
  } \
10
  if (sleepWaitCond_primaskState == 0) {\
11
    __enable_irq (); \
12
  } \
13
} while (0)

Dieses Makro aufzurufen wartet darauf, dass die angegebene Bedingung false wird. Funktional identisch zu

1
uint32_t sleepWaitCond_primaskState = __get_PRIMASK ();
2
while (cond);
3
if (sleepWaitCond_primaskState == 0) __enable_irq ();

Aber eben energieeffizienter weil der Sleep-Modus benutzt wird. Die Bedingung muss natürlich in einer ISR auf false gesetzt werden, und dafür werden natürlich zwischenzeitlich die Interrupts aktiviert. Der vorherige Interrupt-Zustand wird am Ende wiederhergestellt.

z.B. bei einem STM32Cube-Projekt kann man das in der syscalls.c so nutzen:

1
static int stdoutstate = 0;
2
__attribute__((weak)) int _write(int file, char *ptr, int len)
3
{
4
  (void)file;
5

6
  stdoutstate = 1;
7
  HAL_UART_Transmit_DMA (&huart2, (uint8_t*) ptr, len);
8

9
  SLEEP_WAIT_COND (stdoutstate != 0);
10

11
  return len;
12
}
13

14
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
15
  extern UART_HandleTypeDef huart2;
16

17
  if (huart == &huart2)
18
    stdoutstate = 0;
19
}

Dadurch werden printf() -Aufrufe dann energieeffizienter, weil der Prozessor im Sleep-Modus bleibt bis die Daten komplett abgeschickt sind (bis auf Interrupts natürlich).

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren