Vermeiden globaler Variablen bei Verwendung von Interrupts in eingebetteten Systemen

13

Gibt es eine gute Möglichkeit, die Kommunikation zwischen einem ISR und dem Rest des Programms für ein eingebettetes System zu implementieren, das globale Variablen vermeidet?

Es scheint, dass das allgemeine Muster darin besteht, eine globale Variable zu haben, die zwischen dem ISR und dem Rest des Programms geteilt und als Flag verwendet wird, aber diese Verwendung globaler Variablen widerspricht mir. Ich habe ein einfaches Beispiel mit ISRs im avr-libc-Stil beigefügt:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

Ich kann nicht wegsehen, was im Wesentlichen ein Scoping-Problem ist. Müssen Variablen, auf die sowohl der ISR als auch der Rest des Programms zugreifen können, von Natur aus global sein? Trotzdem habe ich oft Leute gesehen, die Dinge im Sinne von "Globale Variablen sind eine Möglichkeit, die Kommunikation zwischen ISRs und dem Rest des Programms zu implementieren" (Schwerpunkt Mine) gesagt haben, was zu implizieren scheint, dass es andere Methoden gibt. Wenn es andere Methoden gibt, welche?

p0llard
quelle
1
Es ist nicht unbedingt wahr, dass der gesamte Rest des Programms Zugriff haben würde; Wenn Sie die Variable als statisch deklariert haben, wird sie nur von der Datei angezeigt, in der die Variable deklariert wurde. Es ist überhaupt nicht schwer, Variablen zu haben, die in der gesamten Datei sichtbar sind, aber nicht im Rest des Programms, und das kann helfen.
DiBosco
1
Außerdem muss das Flag als flüchtig deklariert werden, da Sie es außerhalb des normalen Programmflusses verwenden / ändern. Dies zwingt den Compiler, kein Lese- / Schreib-to-Flag zu optimieren und die eigentliche Lese- / Schreiboperation auszuführen.
Next-Hack
@ next-hack Ja, das ist absolut richtig. Tut mir leid, ich habe nur versucht, schnell ein Beispiel zu finden.
p0llard

Antworten:

19

Es gibt einen De-facto-Standardweg, um dies zu tun (vorausgesetzt, C-Programmierung):

  • Interrupts / ISRs sind Low-Level-Interrupts und sollten daher nur im Treiber implementiert werden, der sich auf die Hardware bezieht, die den Interrupt generiert. Sie sollten sich nur in diesem Treiber befinden.
  • Die gesamte Kommunikation mit dem ISR erfolgt nur durch den Fahrer und den Fahrer. Wenn andere Teile des Programms Zugriff auf diese Informationen benötigen, müssen sie diese über Setter / Getter-Funktionen oder ähnliches vom Treiber anfordern.
  • Sie sollten keine "globalen" Variablen deklarieren. Gültigkeitsbereichsvariablen mit globaler Bedeutung und externer Verknüpfung. Das heißt: Variablen, die mit einem externSchlüsselwort oder einfach versehentlich aufgerufen werden können .
  • Um stattdessen eine private Kapselung innerhalb des Treibers zu erzwingen, müssen alle diese Variablen, die zwischen dem Treiber und dem ISR geteilt werden, deklariert werden static. Eine solche Variable ist nicht global, sondern auf die Datei beschränkt, in der sie deklariert ist.
  • Um Probleme bei der Compileroptimierung zu vermeiden, sollten solche Variablen auch als deklariert werden volatile. Hinweis: Dies ermöglicht keinen atomaren Zugriff und löst keinen Wiedereintritt!
  • Im Treiber wird häufig eine Art Wiedereintrittsmechanismus benötigt, falls der ISR in die Variable schreibt. Beispiele: Interrupt-Deaktivierung, globale Interrupt-Maske, Semaphor / Mutex oder garantierte atomare Lesevorgänge.
Lundin
quelle
Hinweis: Möglicherweise müssen Sie den ISR-Funktionsprototyp über einen Header verfügbar machen, um ihn in einer Vektortabelle in einer anderen Datei zu platzieren. Dies ist jedoch kein Problem, solange Sie dokumentieren, dass es sich um einen Interrupt handelt und nicht vom Programm aufgerufen werden sollte.
Lundin
Was würden Sie sagen, wenn das Gegenargument der erhöhte Aufwand (und der zusätzliche Code) für die Verwendung der Setter- / Get-Funktionen wäre? Ich habe selbst darüber nachgedacht und über Codestandards für unsere 8-Bit-Embedded-Geräte nachgedacht.
Leroy105
2
@ Leroy105 Die Sprache C unterstützt seit einiger Zeit Inline-Funktionen. Obwohl selbst die Verwendung von inlineveraltet ist, werden Compiler bei der Optimierung von Code immer intelligenter. Ich würde sagen, dass die Sorge um den Overhead eine "vorzeitige Optimierung" ist - in den meisten Fällen spielt der Overhead keine Rolle, wenn er überhaupt im Maschinencode vorhanden ist.
Lundin
2
Abgesehen davon verstehen 80-90% aller Programmierer (hier nicht übertrieben) beim Schreiben von ISR-Treibern immer etwas falsch. Das Ergebnis sind subtile Fehler: falsch gelöschte Flags, falsche Compileroptimierung aufgrund fehlender Volatilität, Rennbedingungen, miese Echtzeitleistung, Stapelüberläufe usw. usw. Falls der ISR nicht richtig im Treiber eingekapselt ist, besteht die Möglichkeit solcher subtiler Fehler weiter erhöht. Konzentrieren Sie sich darauf, einen fehlerfreien Treiber zu schreiben, bevor Sie sich über Dinge von peripherem Interesse Gedanken machen, z. B. wenn Setter / Getter ein wenig Overhead verursachen.
Lundin
10
Diese Verwendung globaler Variablen widerspricht mir

Das ist das eigentliche Problem. Komm darüber hinweg.

Bevor die Knie-Ruckler sofort darüber schimpfen, wie unrein das ist, lassen Sie mich das ein wenig näher erläutern. Es besteht sicherlich die Gefahr, dass globale Variablen übermäßig verwendet werden. Sie können aber auch die Effizienz steigern, was manchmal bei kleinen Systemen mit begrenzten Ressourcen von Bedeutung ist.

Der Schlüssel ist, darüber nachzudenken, wann Sie sie vernünftigerweise verwenden können und es unwahrscheinlich ist, dass Sie in Schwierigkeiten geraten, im Gegensatz zu einem Fehler, der nur darauf wartet, passiert zu werden. Es gibt immer Kompromisse. Während das generelle Vermeiden globaler Variablen für die Kommunikation zwischen Interrupt- und Vordergrundcode eine verständliche Richtlinie ist, ist es kontraproduktiv, sie wie die meisten anderen Richtlinien auf ein extremes Religionsprinzip zu übertragen.

Einige Beispiele, bei denen ich manchmal globale Variablen verwende, um Informationen zwischen Interrupt- und Vordergrundcode zu übergeben, sind:

  1. Takt-Tick-Zähler, die vom Systemuhr-Interrupt verwaltet werden. Normalerweise habe ich einen periodischen Taktinterrupt, der alle 1 ms ausgeführt wird. Dies ist häufig für verschiedene Zeitabläufe im System nützlich. Eine Möglichkeit, diese Informationen aus der Interrupt-Routine herauszuholen, wo der Rest des Systems sie verwenden kann, besteht darin, einen globalen Taktgeber zu führen. Die Interruptroutine erhöht den Zähler bei jedem Takt. Der Vordergrundcode kann den Zähler jederzeit lesen. Oft mache ich das für 10 ms, 100 ms und sogar 1 Sekunde Ticks.

    Ich stelle sicher, dass die Ticks von 1 ms, 10 ms und 100 ms eine Wortgröße haben, die in einer einzelnen atomaren Operation gelesen werden kann. Wenn Sie eine Hochsprache verwenden, müssen Sie dem Compiler mitteilen, dass sich diese Variablen asynchron ändern können. In C deklarieren Sie sie beispielsweise als extern flüchtig . Dies ist natürlich etwas, das in eine vordefinierte Include-Datei aufgenommen wird, sodass Sie sich das nicht bei jedem Projekt merken müssen.

    Ich mache manchmal den 1 s Tick Counter zum Zähler für die gesamte verstrichene Zeit, also mache diesen 32 Bit breit. Das kann bei vielen der von mir verwendeten kleinen Mikrogeräte nicht in einer einzigen atomaren Operation gelesen werden, sodass dies nicht global gemacht wird. Stattdessen wird eine Routine bereitgestellt, die den Mehrwortwert liest, mögliche Aktualisierungen zwischen den Lesevorgängen behandelt und das Ergebnis zurückgibt.

    Natürlich hätte es auch Routinen geben können, um die kleineren Tick-Zähler von 1 ms, 10 ms usw. zu erhalten. Das tut Ihnen jedoch sehr wenig, fügt viele Anweisungen hinzu, anstatt ein einzelnes Wort zu lesen, und verbraucht einen anderen Aufrufstapel.

    Was ist der Nachteil? Ich nehme an, jemand könnte einen Tippfehler machen, der versehentlich auf einen der Zähler schreibt, was dann das andere Timing im System durcheinander bringen könnte. Das absichtliche Schreiben an einen Schalter würde keinen Sinn ergeben, daher müsste diese Art von Fehler etwas Unbeabsichtigtes wie ein Tippfehler sein. Scheint sehr unwahrscheinlich. Ich erinnere mich nicht, dass dies jemals in weit über 100 kleinen Mikrocontroller-Projekten passiert ist.

  2. Endgültige gefilterte und angepasste A / D-Werte. Es ist üblich, dass eine Interrupt-Routine Messwerte von einem A / D verarbeitet. Normalerweise lese ich analoge Werte schneller als nötig und wende dann eine kleine Tiefpassfilterung an. Oft werden auch Skalierungen und Offsets angewendet.

    Beispielsweise kann der A / D den 0 bis 3 V-Ausgang eines Spannungsteilers lesen, um die 24 V-Versorgung zu messen. Die vielen Messwerte werden durch eine Filterung durchlaufen und dann so skaliert, dass der Endwert in Millivolt angegeben wird. Wenn die Versorgung bei 24,015 V liegt, beträgt der Endwert 24015.

    Der Rest des Systems sieht nur einen aktualisierten Live-Wert, der die Versorgungsspannung anzeigt. Es weiß nicht und muss sich auch nicht darum kümmern, wann genau das aktualisiert wird, zumal es viel häufiger aktualisiert wird als die Einschwingzeit des Tiefpassfilters.

    Wieder eine Schnittstellenroutine könnte verwendet werden, aber man bekommt sehr wenig Nutzen davon. Die Verwendung der globalen Variablen, wann immer Sie die Versorgungsspannung benötigen, ist viel einfacher. Denken Sie daran, dass Einfachheit nicht nur für die Maschine gilt, sondern dass Einfachheit auch weniger menschliches Versagen bedeutet.

Olin Lathrop
quelle
Ich bin in einer langsamen Woche zur Therapie gegangen und habe wirklich versucht, meinen Code zu picken. Ich sehe Lundins Argument, den Zugriff auf Variablen einzuschränken, aber ich schaue auf meine tatsächlichen Systeme und denke, dass dies eine so entfernte Möglichkeit ist, dass JEDE PERSON tatsächlich eine systemkritische globale Variable ruckeln würde. Die Getter / Setter-Funktionen kosten Sie am Ende Overhead im Vergleich zur Verwendung eines globalen Programms und akzeptieren, dass dies ziemlich einfache Programme sind ...
Leroy105
3
@ Leroy105 Das Problem ist nicht, dass "Terroristen" die globale Variable absichtlich missbrauchen. Die Verschmutzung von Namespaces könnte bei größeren Projekten ein Problem sein, das jedoch mit einer guten Benennung gelöst werden kann. Nein, das wahre Problem ist, dass der Programmierer versucht, die globale Variable wie beabsichtigt zu verwenden, dies jedoch nicht korrekt tut. Entweder weil sie das Problem der Race-Bedingungen, das bei allen ISRs besteht, nicht erkennen oder weil sie die Implementierung des obligatorischen Schutzmechanismus durcheinander bringen, oder einfach, weil sie die Verwendung der globalen Variablen im gesamten Code ausspucken und eine enge Kopplung schaffen unlesbarer Code.
Lundin
Ihre Punkte sind gültig, Olin, aber selbst in diesen Beispielen macht das Ersetzen extern int ticks10msdurch inline int getTicks10ms()absolut keinen Unterschied in der kompilierten Baugruppe, während es andererseits schwierig ist, ihren Wert in anderen Teilen des Programms versehentlich zu ändern, und Sie dies auch zulassen eine Möglichkeit, sich an diesen Aufruf zu "binden" (z. B. die Zeit während des Komponententests zu verspotten, den Zugriff auf diese Variable zu protokollieren oder was auch immer). Selbst wenn Sie argumentieren, dass die Wahrscheinlichkeit, dass ein San-Programmierer diese Variable auf Null ändert, keine Kosten für einen Inline-Getter entstehen.
Groo
@Groo: Das gilt nur, wenn Sie eine Sprache verwenden, die Inlining-Funktionen unterstützt, und dies bedeutet, dass die Definition der Getter-Funktion für alle sichtbar sein muss. Wenn ich eine Hochsprache verwende, verwende ich mehr Getter-Funktionen und weniger globale Variablen. In der Assembly ist es viel einfacher, den Wert einer globalen Variablen zu ermitteln, als sich mit einer Getter-Funktion zu beschäftigen.
Olin Lathrop
Wenn Sie nicht inline können, ist die Auswahl natürlich nicht so einfach. Ich wollte sagen, dass bei Inline-Funktionen (und vielen Compilern vor C99, die bereits Inlining-Erweiterungen unterstützen) die Leistung kein Argument gegen Getter sein kann. Mit einem vernünftigen optimierenden Compiler sollten Sie dieselbe produzierte Assembly erhalten.
Groo
2

Jeder bestimmte Interrupt ist eine globale Ressource. Manchmal kann es jedoch nützlich sein, mehrere Interrupts denselben Code zu verwenden. Beispielsweise kann ein System mehrere UARTs haben, die alle eine ähnliche Sende- / Empfangslogik verwenden sollten.

Ein guter Ansatz besteht darin, die vom Interrupt-Handler verwendeten Dinge oder Zeiger darauf in einem Strukturobjekt zu platzieren und dann die eigentlichen Hardware-Interrupt-Handler so aussehen zu lassen:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

Die Objekte uart1_info, uart2_infowürde usw. globale Variablen sein, aber sie würden die sein nur globale Variablen durch die Interrupt - Handler verwendet. Alles andere, was die Handler berühren werden, wird in diesen behandelt.

Beachten Sie, dass alles, auf das sowohl der Interrupt-Handler als auch der Hauptleitungscode zugreifen, qualifiziert sein muss volatile. Es mag am einfachsten sein, einfach volatilealles zu deklarieren, was vom Interrupt-Handler überhaupt verwendet wird. Wenn jedoch die Leistung wichtig ist, möchten Sie möglicherweise Code schreiben, der Informationen in temporäre Werte kopiert, diese bearbeitet und dann zurückschreibt. Zum Beispiel anstatt zu schreiben:

if (foo->timer)
  foo->timer--;

schreiben:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

Der erstere Ansatz mag leichter zu lesen und zu verstehen sein, ist jedoch weniger effizient als der letztere. Ob dies ein Problem darstellt, hängt von der Anwendung ab.

Superkatze
quelle
0

Hier sind drei Ideen:

Deklarieren Sie die Flag-Variable als statisch, um den Bereich auf eine einzelne Datei zu beschränken.

Machen Sie die Flag-Variable privat und verwenden Sie Getter- und Setter-Funktionen, um auf den Flag-Wert zuzugreifen.

Verwenden Sie ein Signalisierungsobjekt wie ein Semaphor anstelle einer Flagvariablen. Der ISR würde das Semaphor setzen / posten.

kkrambo
quelle
0

Ein Interrupt (dh der Vektor, der auf Ihren Handler zeigt) ist eine globale Ressource. Selbst wenn Sie eine Variable auf dem Stapel oder auf dem Heap verwenden:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

oder objektorientierter Code mit einer 'virtuellen' Funktion:

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

… Der erste Schritt muss eine tatsächliche globale (oder zumindest statische) Variable beinhalten, um diese anderen Daten zu erreichen.

Alle diese Mechanismen fügen eine Indirektion hinzu, sodass dies normalerweise nicht erfolgt, wenn Sie den letzten Zyklus aus dem Interrupt-Handler herausdrücken möchten.

CL.
quelle
Sie sollten flag als volatile int * deklarieren.
Next-Hack
0

Ich codiere derzeit für Cortex M0 / M4 und der Ansatz, den wir in C ++ verwenden (es gibt kein C ++ - Tag, daher ist diese Antwort möglicherweise nicht zum Thema gehörend), lautet wie folgt:

Wir verwenden eine Klasse, CInterruptVectorTabledie alle Interrupt-Serviceroutinen enthält, die im tatsächlichen Interrupt-Vektor der Steuerung gespeichert sind:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

Die Klasse CInterruptVectorTableimplementiert eine Abstraktion der Interruptvektoren, sodass Sie zur Laufzeit verschiedene Funktionen an die Interruptvektoren binden können.

Die Schnittstelle dieser Klasse sieht folgendermaßen aus:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

Sie müssen die Funktionen erstellen, die in der Vektortabelle gespeichert sind, staticda der Controller keinen thisZeiger bereitstellen kann, da die Vektortabelle kein Objekt ist. Um dieses Problem zu pThisumgehen, haben wir den statischen Zeiger in der CInterruptVectorTable. Bei Eingabe einer der statischen Interrupt-Funktionen kann auf den pThisZeiger zugegriffen werden, um Zugriff auf Mitglieder des einen Objekts von zu erhalten CInterruptVectorTable.


Jetzt können Sie im Programm SetIsrCallbackfunctionmit den einen Funktionszeiger auf eine staticFunktion setzen, die aufgerufen werden soll, wenn ein Interrupt auftritt. Die Zeiger werden in der gespeichert InterruptVectorTable_t virtualVectorTable.

Und die Implementierung einer Interrupt-Funktion sieht folgendermaßen aus:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

Das ruft also eine staticMethode einer anderen Klasse auf (die es sein kann private), die dann einen anderen static thisZeiger enthalten kann, um Zugriff auf Mitgliedsvariablen dieses Objekts zu erhalten (nur eine).

Ich denke, Sie könnten ähnliche IInterruptHandlerZeiger auf Objekte erstellen und mit diesen static thisverknüpfen und speichern, sodass Sie den Zeiger in all diesen Klassen nicht benötigen . (Vielleicht versuchen wir das in der nächsten Iteration unserer Architektur)

Der andere Ansatz funktioniert gut für uns, da die einzigen Objekte, die einen Interrupt-Handler implementieren dürfen, diejenigen innerhalb der Hardware-Abstraktionsschicht sind und wir normalerweise nur ein Objekt für jeden Hardware-Block haben, so dass es gut funktioniert, mit static this-zeigern zu arbeiten. Und die Hardware-Abstraktionsschicht bietet noch eine weitere Abstraktion für Interrupts, ICallbackdie dann in der Geräteschicht über der Hardware implementiert wird.


Greifen Sie auf globale Daten zu? Sicher, aber Sie können die meisten benötigten globalen Daten wie die thisZeiger und die Interrupt-Funktionen privat machen .

Es ist nicht kugelsicher und erhöht den Overhead. Mit diesem Ansatz haben Sie Schwierigkeiten, einen IO-Link-Stack zu implementieren. Wenn Sie jedoch nicht sehr zeitlich begrenzt sind, funktioniert dies recht gut, um eine flexible Abstraktion von Interrupts und Kommunikation in Modulen zu erhalten, ohne globale Variablen zu verwenden, auf die von überall aus zugegriffen werden kann.

Arsenal
quelle
1
"So können Sie zur Laufzeit verschiedene Funktionen an die Interrupt-Vektoren binden" Das klingt nach einer schlechten Idee. Die "zyklomatische Komplexität" des Programms würde nur durch das Dach gehen. Alle Anwendungsfallkombinationen müssten getestet werden, damit weder Timing- noch Stack-Nutzungskonflikte auftreten. Viele Kopfschmerzen für eine Funktion mit sehr eingeschränkter Nützlichkeit IMO. (Es sei denn, Sie haben einen Bootloader-Fall, das ist eine andere Geschichte.) Insgesamt riecht dies nach Metaprogrammierung.
Lundin
@Lundin Ich verstehe deinen Standpunkt nicht wirklich. Wir verwenden es, um beispielsweise den DMA-Interrupt an den SPI-Interrupt-Handler zu binden, wenn der DMA für den SPI verwendet wird, und an den UART-Interrupt-Handler, wenn er für den UART verwendet wird. Beide Handler müssen zwar getestet werden, sind aber kein Problem. Und es hat sicherlich nichts mit Metaprogrammierung zu tun.
Arsenal
DMA ist eine Sache, die Laufzeitzuweisung von Interruptvektoren ist etwas ganz anderes. Es ist sinnvoll, ein DMA-Treiber-Setup zur Laufzeit variabel zu lassen. Eine Vektortabelle, nicht so sehr.
Lundin
@Lundin Ich denke, wir haben unterschiedliche Ansichten dazu, wir könnten einen Chat darüber beginnen, weil ich Ihr Problem immer noch nicht sehe - also könnte es sein, dass meine Antwort so schlecht geschrieben ist, dass das gesamte Konzept missverstanden wird.
Arsenal