Zeitbasis-ISR-Parallelität

Dies ist eine Art "klassisches" Problem, und ich glaube, ich habe eine Lösung, aber ich möchte es mit dieser Community überprüfen. Ich baue ein Projekt mit dem Mikrocontroller ATtiny88 und programmiere in avr-gcc. Ich brauche es, um die folgenden Interrupts zu verarbeiten:

  • TWI (I2C)
  • Timer0-Überlauf
  • Timer1-Erfassung

Ich möchte den Timer0-Überlauf verwenden, um einen 32-Bit-Millisekunden-Zeitstempel beizubehalten (ähnlich dem millis()Konzept von Arduino). Für meine Anwendung kann dem TWI-Interrupt nichts im Wege stehen, da mein ATtiny88 als TWI-Slave fungiert und ich als solcher niemals Interrupts löschen kann. Die Timer ISRs müssen beide deklariert werden NO_BLOCK.

Da der ATtiny88 ein 8-Bit-Prozessor ist, dauert der Zugriff auf Variablen, die breiter als 8-Bit sind, mit Sicherheit mehrere Zyklen. Die Art und Weise, wie der Arduino-Kern dieses Dilemma handhabt, besteht darin, den Zugriff auf seine interne 32-Bit- timer0_millisVariable mit einem cli()innerhalb der millis()Funktion zu schützen. Dieser Ansatz ist für mich unangenehm, da er bedeutet, dass ich möglicherweise einen TWI-Interrupt verpasse, weil ich anrufe millis(). Es ist mir egal, ob gelegentlich ein "Tick" der Zeitbasis fehlt, weil ein TWI-Interrupt verarbeitet wird.

Also schrieb ich timebase.h / timebase.c in der Hoffnung, dieses Problem auf Kosten von etwas zusätzlichem Speicherplatz durch doppelte Pufferung zu umgehen .

timebase.h

/*
 * timebase.h
 *
 *  Created on: Dec 3, 2012
 *      Author: vic
 */

#ifndef TIMEBASE_H_
#define TIMEBASE_H_

// timer 0 is set up as a 1ms time base
#define TIMER0_1MS_OVERFOW_PRESCALER 3     // 8MHz / 64 = 125 kHz
#define TIMER0_1MS_OVERFLOW_TCNT     131   // 255 - 131 + 1 = 125 ticks

void timebase_init();
uint32_t timebase_now();

#endif /* TIMEBASE_H_ */

timebase.c

/*
 * timebase.c
 *
 *  Created on: Dec 3, 2012
 *      Author: vic
 */
#include <inttypes.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include "timebase.h"

// double buffered timestamp
volatile uint32_t timestamp_ms_buf[2] = {0, 1};
volatile uint8_t  timestamp_ms_buf_volatile_idx = 0;

void timebase_init(){
    // set up the timer0 overflow interrupt for 1ms
    TCNT0  = TIMER0_1MS_OVERFLOW_TCNT;
    TIMSK0 = _BV(TOIE0);

    // start timer0
    TCCR0A = TIMER0_1MS_OVERFOW_PRESCALER;
}

// fires once per millisecond, don't block
// can't miss TWI interrupts for anything
ISR(TIMER0_OVF_vect, ISR_NOBLOCK){
    TCNT0 = TIMER0_1MS_OVERFLOW_TCNT;

    // modify the volatile index value
    timestamp_ms_buf[timestamp_ms_buf_volatile_idx] += 2;

    // change the volatile index
    timestamp_ms_buf_volatile_idx = 1 - timestamp_ms_buf_volatile_idx; // always 0 or 1
}

uint32_t timebase_now(){
    uint8_t idx = timestamp_ms_buf_volatile_idx; // copy the current volatile index
    return timestamp_ms_buf[1 - idx];            // return the value from the non-volatile index
}

Meine Frage ist, löst der Code, den ich hier geschrieben habe, effektiv das Problem, das ich beschrieben habe? Habe ich den atomaren Zugriff auf die Zeitbasis erfolgreich implementiert, ohne Interrupts zu löschen? Wenn nein, warum nicht und wie kann ich meine Ziele erreichen?

Antworten (1)

Nach einem kurzen Blick darauf zu urteilen, sieht die doppelte Pufferung nach einem guten Ansatz aus. Ich glaube jedoch, dass es immer noch passieren kann, dass Sie einen "ungültigen" Wert zurückgegeben bekommen. Theoretisch könnte der T0-Interrupt mehrmals ausgelöst werden, während Sie auf den Zeitstempel in der timebase_now()Funktion zugreifen (wenn die Ausführung durch einen anderen ISR > 1 ms verzögert wird) und Ihre "doppelte" Pufferung unbrauchbar machen würde.

Sind Sie sicher, dass es überhaupt erforderlich ist, Ihren Timer0 ISR nicht blockierend zu machen? Da die Hardware alle Low-Level-TWI-Funktionen verarbeitet und die maximale Datenübertragungsgeschwindigkeit 400 kHz beträgt, sollte genügend Zeit für die Verarbeitung von TWI-Daten vorhanden sein. Die Aktualisierung der 4-Byte-Zeitstempelvariablen dauert nur wenige Taktzyklen. Unter welcher Annahme erwarten Sie, TWI-Interrupts zu verlieren?

Sie sagen, dass die Genauigkeit des Zeitstempels nicht kritisch ist. Eine andere Lösung könnte also darin bestehen, einfach ein Flag (oder einen 1-Byte-Zähler) in der T0-ISR zu setzen und die Aktualisierung des Zeitstempels in der Hauptschleife zu handhaben. Dies funktioniert jedoch nur, wenn timebase_now()von keiner ISR aus aufrufbar sein soll.

Das Update in der Hauptschleife basierend auf einem Flag durchzuführen, ist eine gute Idee ... Ich mag es. Angesichts der Tatsache, dass TWI asynchron ist, wird jede andere blockierende ISR einen TWI-Handler mit einer gewissen Wahrscheinlichkeit behindern, richtig?
Ich bin mit TWI nicht so vertraut, daher weiß ich nicht, wie schnell Sie reagieren müssen und wie oft ein Interrupt auftreten kann. Aber ich für mich selbst halte es für eine schlechte Praxis, verschachtelte Interrupts zuzulassen, da dies fast immer vermeidbar ist. Es ermutigt nur, faul zu werden und nicht auf das Gesamtverhalten/Timing des Codes zu achten. Ich würde versuchen, das Zeitfenster abzuschätzen, in dem der TWI-Handler ausgeführt werden muss, und dann prüfen, ob die Ausführungszeit der anderen ISRs innerhalb akzeptabler Grenzen liegt. Denken Sie daran, dass die Interrupt-Priorität wichtig sein kann, wenn Sie viele andere schnell wiederkehrende Interrupts haben.
Wenn ich 'hindern' sage, meine ich nicht 'im Gange unterbrechen' ... sondern ich meine 'verursachen, dass es vollständig übersehen wird'. Es besteht die Möglichkeit, wenn auch nur eine kleine, dass ein I2C-Datenempfangs-Interrupt auftritt, während eine Timer-ISR läuft. Wenn der Timer ISR Interrupts deaktiviert, wird dieses I2C-Datenempfangsereignis niemals behandelt, richtig?
Wenn ein ISR eingegeben wird, wird das globale Interrupt-Freigabe-Flag gelöscht, aber dies unterdrückt nicht, dass alle anderen Interrupts "erkannt" werden. Die Interrupt-Flags werden immer noch gesetzt, aber die Aufrufe der Interrupt-Service-Routine werden VERZÖGERT, bis das globale Interrupt-Flag wieder freigegeben wird. Wenn während der ISR-Ausführung mehrere Flags gesetzt sind, werden die Handler in der Reihenfolge ihrer Priorität ausgeführt. Das bedeutet, dass Sie keinen Interrupt verpassen, solange Sie ihm die Chance geben, verarbeitet zu werden, bevor er das nächste Mal auftritt.
Das klingt architekturabhängig, haben Sie eine Referenz, die darauf hindeutet, dass dies bei AVR der Fall ist? Ich weiß nicht, wo diese verzögerten Anrufe in der Hardware gepuffert würden ...
Ich ging zurück und las das Datenblatt erneut und es scheint, dass einige Interrupts einfach verzögert und in priorisierter Reihenfolge bedient werden. Es gibt jedoch eine Klasse von Interrupts, die verloren gehen könnten. Um das Datenblatt zu zitieren: „Die zweite Art von Interrupts wird ausgelöst, solange die Interrupt-Bedingung vorhanden ist. Diese Interrupts haben nicht unbedingt Interrupt-Flags. Wenn die Interrupt-Bedingung vor dem Interrupt verschwindet aktiviert ist, wird der Interrupt nicht ausgelöst." Wenn der Interrupt ein zugeordnetes Flag-Bit hat, wird er eventuell behandelt, solange Interrupts mit höherer Priorität nicht gesättigt sind.
Ich bezog mich auf dieses Verhalten "... wenn eine oder mehrere Interrupt-Bedingungen auftreten, während das Global Interrupt Enable-Bit gelöscht ist, werden die entsprechenden Interrupt-Flags gesetzt und gespeichert, bis das Global Interrupt Enable-Bit gesetzt ist, und werden es dann tun nach Priorität ausgeführt werden." Ich war mir der im Datenblatt erwähnten "zweiten Art von Interrupts" nicht bewusst, interessant. Das werde ich nachlesen. Vielleicht ist es einfach und nur die Art, wie einige Interrupts ausgelöst werden.