Forum: Mikrocontroller und Digitale Elektronik ARM Cortex-M0: re-enter Interrupt


von Markus H. (traumflug)


Lesenswert?

Auf den AVRs hatte ich einen Interrupt alle 2 Millisekunden. Der hat 
zunächst mal ein paar Zähler auf den neuesten Stand gebracht, dann die 
Interrupts wieder freigegeben (da kann man ja nur keinen oder alle 
sperren) und erst dann eine längere Berechnung durchgeführt.

War der Prozessor zu viel mit anderen Interrupts beschäftigt, wurde 
dieser Interrupt durch sich selbst unterbrochen. Um das zu erkennen, 
wurde zu Beginn der langen Berechnung ein Flag gesetzt. Sah der 
Interrupt-Handler dieses Flag gesetzt, hat er die lange Berechnung 
ausgelassen. Das hat gut funktioniert, ich habe das hier mal mit einem 
Beispiel auf dem ARM LPC1114 nachgestellt:

1
void SysTick_Handler(void) {
2
  static uint8_t busy = 0, dot_written = 0;
3
  static uint32_t count = 0;
4
  static uint8_t minutes = 0, seconds = 0;
5
6
  count++;
7
  if (busy) {
8
    if ( ! dot_written) {
9
      serial_writechar('.');
10
      dot_written = 1;
11
    }
12
    return;
13
  }
14
15
  busy = 1;
16
  dot_written = 0;
17
18
  sei();  // = __enable_irq()
19
20
  if ( ! (count % 500)) {   // A full second.
21
    // Calculate clock.
22
    seconds++;
23
    if ( ! (seconds % 60)) {
24
      seconds = 0;
25
      minutes++;
26
    }
27
    sersendf_P(PSTR("%su:"), minutes);
28
    if (seconds < 10)
29
      serial_writechar('0');
30
    sersendf_P(PSTR("%su\n"), seconds);
31
32
    // Be busy sometimes.
33
    if ( ! (seconds % 5))
34
      delay_ms(2000);
35
  }
36
  busy = 0;
37
}

PSTR() u.Ä. einfach ignorieren, der Code ist auch für die AVRs. "busy" 
ist das Flag und delay_ms(2000) ist ein Ersatz für die lange Berechnung.

Ohne den Delay gibt der Code eine prima funktionierende Uhr. Mit dem 
Delay jedoch hält die Uhr alle 5 Sekunden kurz an (das ist noch OK), 
danach macht sie aber nicht den eigentlich gewollten Sprung in der 
Zählung. Das serial_writechar('.') wird auch nie ausgeführt.

Das bringt mich zu der Erkenntnis, dass das mit dem Re-entry des 
Interrupts nicht klappt. Während der Pause gehen die Interrupts 
offensichtlich verloren, der Zähler "count" stimmt nicht mehr mit der 
Zeit überein.

In der ARM-Doku ist das Re-Entry von Interrupts irgendwie nicht 
vorgesehen, da wird kein Wort dazu verloren. Weder dass es geht, noch, 
dass es nicht geht. Ausser hier:
http://infocenter.arm.com/help/topic/com.arm.doc.dui0471g/DUI0471G_developing_for_arm_processors.pdf
in Kapitel 6.12., doch das passt so gar nicht zur sonstigen 
Cortex-M0-Doku.

Irgendwas fehlt an der Geschichte noch. Hat jemand eine Idee, was das 
sein könnte? Oder wie man ein ähnliches Verhalten ohne Re-Entry gebacken 
bekommt? Die Berechnung muss in einen Interrupt, damit die noch 
unwichtigeren Sachen im Nicht-Interrupt-Bereich unterbrochen werden 
können. Ein zweiter Interrupt müsste irgendwie synchronisiert werden.

Besten Dank schon mal.

von Lothar (Gast)


Lesenswert?

1. ARM und re-entrante Interrupts

Nur Cortex-A und Cortex-R unterstützen re-entrante Interrupts, Cortex-M 
nicht. Tritt beim Cortex-M derselbe Interrupt nochmals auf, wird dieser 
Pending. Wenn Du deinen Code unbedingt so beibehalten willst, musst Du 
im Delay das Pending Bit löschen, sonst bleibt der SysTick stehen.

Allerdings wird die Sache beim Cortex-M üblicherweise anders gelöst: man 
lässt im SysTick einen globalen Counter laufen und macht die 
Ausgabefunktionen im Hauptprogramm oder einer Unterfunktion, abhängig 
von diesem Counter.

2. Das Thema wurde auch schon behandelt:

Beitrag "Cortex-M(0) - reentrant Funktion aus SysTick-Handler anspringen"

3. Wenn man auf dem Cortex-M unbedingt re-entrante Interrupts 
realisieren muss, gibt es diesen Trick (steht auch im Handbuch, ist aber 
nicht online verfügbar):

https://sites.google.com/site/sippeyfunlabs/embedded-system/how-to-run-re-entrant-task-scheduler-on-arm-cortex-m4

von Markus H. (traumflug)


Lesenswert?

Besten Dank, Lothar.

Das Pending-Bit zu löschen hilft nichts. Man müsste wohl das Active-Bit 
löschen, doch da kommt man direkt nicht dran.

Das mit dem Flag setzen und im Hauptprogramm abarbeiten verwende ich 
auch auf AVR schon, aber nur für diese ganz unwichtigen Sachen (z.B. 
Bedienung des Displays).

Das mit dem Stack neu schreiben, wie im Blog von Sippey Fun Labs 
beschrieben, habe ich mir verkniffen. Zumal dort auch der Beispielcode 
verschwunden ist, das also evtl. eine längere Odyssee geworden wäre.

@turboj hat hier eine gute Strategie:
Beitrag "Re: Reentrant IR auf Cortex-M3" Aus dem ersten 
Interrupt heraus einen Zweiten ohne eigene Interrupt-Quelle triggern. 
Das funktioniert prima und ist fast so gut wie ein Interrupt mit 
Reentry.

Das Beispiel habe ich entsprechend geändert, jetzt sieht es so aus und 
funktioniert:

1
void timer_init() {
2
  NVIC_SetPriority(SysTick_IRQn, 0);              // Highest priority.
3
4
  // SysTick defined in core_cm0.h.
5
  SysTick->LOAD  = TICK_TIME - 1;
6
  SysTick->VAL   = 0;                             // Actually load LOAD.
7
8
  SysTick->CTRL  = SysTick_CTRL_ENABLE_Msk        // Enable the ticker.
9
                 | SysTick_CTRL_TICKINT_Msk       // Enable interrupt.
10
                 | SysTick_CTRL_CLKSOURCE_Msk;    // Run at full CPU clock.
11
12
  NVIC_SetPriority(PendSV_IRQn, 1);               // Almost highest priority.
13
}
14
15
void SysTick_Handler(void) {
16
  static uint32_t count = 0;
17
  static uint8_t minutes = 0, seconds = 0;
18
19
  count++;
20
21
  if ( ! (count % 500)) {   // A full second.
22
    seconds++;
23
    if ( ! (seconds % 60)) {
24
      seconds = 0;
25
      minutes++;
26
    }
27
28
    sersendf_P(PSTR("%su:"), minutes);
29
    if (seconds < 10)
30
      serial_writechar('0');
31
    sersendf_P(PSTR("%su\n"), seconds);
32
33
    if ( ! (seconds % 5)) {
34
      SCB->ICSR = SCB_ICSR_PENDSVSET_Msk;         // Trigger PendSV_Handler().
35
    }
36
  }
37
}
38
39
void PendSV_Handler(void) {
40
  serial_writechar('a');
41
  delay_ms(2500);
42
  serial_writechar('e');
43
}

Für die, die kopieren wollen: sersendf_P() ist ein printf()-Ersatz und 
die anderen Funktionen tun das, was der Name andeutet. Diese Funktionen 
sind von der Teacup Firmware:

https://github.com/Traumflug/Teacup_Firmware

von Markus H. (traumflug)


Lesenswert?

P.S.:

- Ein paar von den NVIC_... Funktionen aus CMSIS' core_cm0.h kann man
  nicht verwenden, denn die sind nur für Interrupts mit positiven
  Nummern geeignet. Der PendSV hat die Nummer -2.

- Ein NVIC_EnableIRQ(PendSV_IRQn) braucht es offensichtlich nicht.

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.