Arduino Multitasking - Teil zwei
Wenn Sie Projekte durchführen, gibt es oft Zeiten, in denen Sie möchten, dass der Prozessor eine weitere Sache erledigt. Aber wie bringt man ihn dazu, wenn er mit anderen Aufgaben beschäftigt ist?
Mann-Orchester, Foto um 1865, Quelle Wikipedia.
In diesem Leitfaden bauen wir auf den Techniken auf, die wir in Teil 1 gelernt haben.
Wir werden lernen, wie man die Stoppuhr-Interrupts zähmt, damit alles wie am Schnürchen läuft. Wir werden auch entdecken, wie man externe Interrupts verwendet, um uns über externe Ereignisse zu informieren.
Verbindung
Alle Beispiele in diesem Handbuch verwenden das folgende Anschlussdiagramm:
Was ist ein Interrupt?
Ein Interrupt ist ein Signal, das der CPU mitteilt, alles sofort zu stoppen und sich mit der Verarbeitung mit hoher Priorität zu befassen. Es heißt Interrupt-Handling.
Der Interrupt-Handler ist eine weitere leere Funktion. Wenn Sie einen schreiben und an einen Interrupt anhängen, wird er immer dann aufgerufen, wenn das Interrupt-Signal ausgelöst wird. Wenn Sie diese Funktion deaktivieren, nimmt der Prozessor seine vorherige Funktion wieder auf.
Woher kommen die Interrupts?
Interrupts können aus mehreren Quellen generiert werden:
- Durch Arduino -Stopper verursachte Zeitunterbrechungen.
- Externe Interrupts, die durch eine Statusänderung eines der externen Interrupt-Pins verursacht werden.
- Pin-Wechsel-Interrupts, die durch eine Statusänderung eines beliebigen Pins in der Gruppe verursacht werden.
Wofür sind sie nützlich?
Durch die Verwendung von Interrupts müssen Sie keine Schleifen mehr in Ihren Code schreiben, um ständig den Zustand von Interrupts mit hoher Priorität zu überprüfen. Dank langlaufender Unterprogramme müssen Sie sich keine Gedanken über langsame Reaktionen machen oder vergessen, eine Taste zu drücken.
Während eines Interrupts stoppt der Prozessor automatisch alle Operationen und ruft den Interrupt-Handler auf. Sie müssen nur Code schreiben, der auf jeden Interrupt reagiert.
Die Zeit unterbricht
Rufen Sie uns nicht an, wir rufen Sie an
In Teil 1 haben wir gelernt, wie man die Funktion millis() in Verzögerungen verwendet. Damit es funktionierte, mussten wir es nach jeder Schleife abrufen, um zu sehen, ob etwas getan werden musste. Diese Funktion mehr als jede Millisekunde aufzurufen, nur um herauszufinden, dass sich die Zeit nicht geändert hat, war eine Verschwendung. Es wäre besser, wenn diese Überprüfung nur jede Millisekunde erfolgen würde.
Timer und Timer ermöglichen uns genau das. Wir können die Stoppuhr so einstellen, dass sie uns einmal pro Millisekunde unterbricht. Dann teilt er uns selbst mit, wann wir die Uhrzeit prüfen sollen!
Arduino-Stopper
Es gibt 3 Arten von Arduino Uno-Stoppuhren: Stopwatch0, Stopwatch1 und Stopwatch2. Ersteres ist bereits so eingestellt, dass es einen Millisekunden-Interrupt generiert, Dadurch wird der von der Funktion millis () gemeldete Millisekundenzähler aktualisiert. Wir werden auch Stopwatch0 verwenden.
Häufigkeit und Zählung
Timer sind einfache Timer, die mit einer bestimmten Frequenz laufen, die von der 16-MHz-Systemuhr abgeleitet wird. Sie können den Taktteiler einstellen, um die Frequenz und andere verschiedene Zählmodi zu variieren. Sie können sie auch so einrichten, dass sie Interrupts erstellen, wenn der Timer eine bestimmte Zahl erreicht.
Stopwatch0 hat 8 Bits und kann von 0 bis 255 zählen und generiert außerdem bei jedem Überlauf Interrupts. Standardmäßig verwendet es einen Taktteiler von 64, um eine Interrupt-Rate von 976,5625 Hz zu liefern. Wir werden die Frequenz von Stopwatch0 nicht ändern, da dies die Funktion millis () ändern würde!
Vergleichende Register
Arduino-Stopper haben eine Reihe von Konfigurationsregistern. Sie können mit Sonderzeichen gelesen oder geschrieben werden, die in der Arduino IDE angegeben sind.
Wir werden ein Komparatorregister für Stopwatch 0 einrichten (dieses Register ist als OCR0A bekannt), um den nächsten Interrupt zu erzeugen, während die Zählung im Gange ist. Bei jedem Tick wird der Timer-Zähler mit einem Vergleichsregister verglichen, und wenn sie gleich sind, wird ein Interrupt erzeugt.
Der folgende Code erstellt einen 'TIMER0_COMPA'-Interrupt, wenn der Zähler der Werte 0xAF überschreitet.
// Timer0 wird bereits für millis() verwendet - wir unterbrechen einfach irgendwo
// in der Mitte und rufe unten die Funktion "Compare A" auf
OCR0A = 0xAF;
TIMSK0 | = _BV (OCIE0A);
Als nächstes definieren wir einen Interrupt-Handler für den als "TIMER0_COMPA_vect" bekannten Zeit-Interrupt-Vektor. In diesem Interrupt-Handler werden wir alles tun, was wir in der Schleife getan haben.
// Interrupt wird einmal pro Millisekunde aufgerufen,
SIGNAL (TIMER0_COMPA_vect)
{
unsigned long currentMillis = millis ();
Sweeper1.Update (aktuelleMillis);
// wenn (digitalRead (2) == HIGH)
{
Sweeper2.Update (aktuelleMillis);
led1.Update (aktuelleMillis);
}
led2.Update (aktuelleMillis);
led3.Update (aktuelleMillis);
}
Dies lässt uns mit einer völlig leeren Schleife zurück.
leere Schleife ()
{
}
Jetzt können Sie alles in Ihrer Schleife tun. Verwenden Sie sogar die Funktion dealy () ohne Konsequenzen! An der Funktionsweise von Flashern und Sweepern ändert sich dadurch nichts.
Quellcode:
Hier ist der gesamte Code, einschließlich Flasher und Sweeper :
#includeKlasse Blinker
{
// Klassenmitgliedsvariablen
// Diese werden beim Start initialisiert
int ledPin; // die Nummer des LED-Pins
lange OnTime; // Millisekunden Einschaltzeit
lange OffTime; // Millisekunden Auszeit
// Diese behalten den aktuellen Zustand bei
int ledState; // ledState zum Setzen der LED
unsigned long previousMillis; // speichert das letzte Mal, als die LED aktualisiert wurde // Konstruktor - erstellt einen Flasher
// und initialisiert die Mitgliedsvariablen und den Zustand
öffentlich:
Flasher (int pin, lang an, lang aus)
{
ledPin = Stift;
PinMode (ledPin, AUSGANG);
OnTime = ein;
OffTime = aus;
ledState = NIEDRIG;
vorherigeMillis = 0;
} void Update (unsigned long currentMillis)
{
if ((ledState == HIGH) && (currentMillis - previousMillis> = OnTime))
{
ledState = NIEDRIG; // Schalte es aus
vorherigeMillis = aktuelleMillis; // Denk an die Zeit
digitalWrite (ledPin, ledState); // Aktualisieren Sie die tatsächliche LED
}
else if ((ledState == LOW) && (currentMillis - previousMillis> = OffTime))
{
ledState = HOCH; // Mach es an
vorherigeMillis = aktuelleMillis; // Denk an die Zeit
digitalWrite (ledPin, ledState); // Aktualisieren Sie die tatsächliche LED
}
}
};
Klasse Kehrmaschine
{
Servo-Servo; // das Servo
intpos; // aktuelle Servoposition
int-Inkrement; // Inkrement zum Bewegen für jedes Intervall
int updateInterval; // Intervall zwischen Aktualisierungen
unsigned long lastUpdate; // letzte Positionsaktualisierung öffentlich:
Sweeper (int-Intervall)
{
updateInterval = Intervall;
Schrittweite = 1;
}
void Anhängen (int pin)
{
servo.attach (Stift);
}
void Trennen ()
{
servo.detach ();
}
void Update (unsigned long currentMillis)
{
if ((currentMillis - lastUpdate)> updateInterval) // Zeit zum Aktualisieren
{
lastUpdate = millis ();
Pos + = Inkrement;
servo.write (pos);
if ((pos> = 180) || (pos <= 0)) // Ende des Sweeps
{
// umgekehrte Richtung
Inkrement = -Inkrement;
}
}
}
};
Led1 Blinker (11, 123, 400);
Led2 Blinker (12, 350, 350);
LED3-Blinker (13.200.222);
Kehrmaschine Kehrmaschine1 (25);
Kehrmaschine Kehrmaschine2 (35);
Void-Setup ()
{
Kehrmaschine1.Befestigen (9);
Kehrmaschine2.Befestigen (10);
// Timer0 wird bereits für millis() verwendet - wir unterbrechen einfach irgendwo
// in der Mitte und rufe unten die Funktion "Compare A" auf
OCR0A = 0xAF;
TIMSK0 | = _BV (OCIE0A);
}
// Interrupt wird einmal pro Millisekunde aufgerufen, um die LEDs zu aktualisieren
// Sweeper2 wird nicht aktualisiert, wenn die Taste auf Digital 2 gedrückt wird.
SIGNAL (TIMER0_COMPA_vect)
{
unsigned long currentMillis = millis ();
Sweeper1.Update (aktuelleMillis);
if (digitalRead (2) == HIGH)
{
Sweeper2.Update (aktuelleMillis);
led1.Update (aktuelleMillis);
}
led2.Update (aktuelleMillis);
led3.Update (aktuelleMillis);
}
leere Schleife ()
{
}
Externe Interrupts
Wann Sie Schleifen nicht verwenden sollten
Im Gegensatz zu Zeitinterrupts werden externe Interrupts durch externe Ereignisse ausgelöst. Zum Beispiel nach dem Drücken einer Taste oder nach dem Empfang eines Impulses von einem Drehgeber. Sie müssen jedoch nicht ständig prüfen, ob die GPIO-Pins geändert werden müssen, wie dies bei den Zeitinterrupts der Fall war.
Der Arduino UNO hat 2 externe Interrupt-Pins. In diesem Beispiel verbinden wir eine Taste mit einem dieser Pins und verwenden sie, um unsere Sweeper zurückzusetzen. Zuerst fügen wir der Sweeper -Klasse eine "reset ()"-Funktion hinzu. Diese Funktion setzt die Position auf 0 und positioniert das Servo sofort dort:
ungültig zurücksetzen ()
{
Pos = 0;
servo.write (pos);
Inkrement = abs (Inkrement);
}
Dann fügen wir einen Aufruf an AttachInterrupt() hinzu, um den externen Interrupt mit dem Handler-Code zu verknüpfen.
In UNO ist Interrupt 0 an den digitalen Pin 2 gebunden. Wir befehlen ihm, die FALLENDE Flanke des Signals dieses Pins zu finden. Wenn der Taster gedrückt wird, "fällt" das Signal von HIGH auf LOW und der Interrupt-Handler "Reset" wird aufgerufen.
PinMode (2, INPUT_PULLUP);
AttachInterrupt (0, Reset, FALLING);
Und hier ist der Interrupt-Handler "Reset". Es ruft die Sweeper- Reset-Funktionen auf:
ungültig Zurücksetzen ()
{
Sweeper1.reset ();
Sweeper2.reset ();
}
Von da an hören die Servos bei jedem Tastendruck auf zu arbeiten und beginnen sofort mit der Suche nach der Nullposition.
Quellcode:
Hier ist eine vollständige Skizze mit Stoppern und äußeren Unterbrechungen:
#includeKlasse Blinker
{
// Klassenmitgliedsvariablen
// Diese werden beim Start initialisiert
int ledPin; // die Nummer des LED-Pins
lange OnTime; // Millisekunden Einschaltzeit
lange OffTime; // Millisekunden Auszeit
// Diese behalten den aktuellen Zustand bei
flüchtig int ledState; // ledState zum Setzen der LED
volatile unsigned long previousMillis; // speichert das letzte Mal, als die LED aktualisiert wurde
// Konstruktor - erstellt einen Flasher
// und initialisiert die Mitgliedsvariablen und den Zustand
öffentlich:
Flasher (int pin, lang an, lang aus)
{
ledPin = Stift;
PinMode (ledPin, AUSGANG);
OnTime = ein;
OffTime = aus;
ledState = NIEDRIG;
vorherigeMillis = 0;
}
void Update (unsigned long currentMillis)
{
if ((ledState == HIGH) && (currentMillis - previousMillis> = OnTime))
{
ledState = NIEDRIG; // Schalte es aus
vorherigeMillis = aktuelleMillis; // Denk an die Zeit
digitalWrite (ledPin, ledState); // Aktualisieren Sie die tatsächliche LED
}
else if ((ledState == LOW) && (currentMillis - previousMillis> = OffTime))
{
ledState = HOCH; // Mach es an
vorherigeMillis = aktuelleMillis; // Denk an die Zeit
digitalWrite (ledPin, ledState); // Aktualisieren Sie die tatsächliche LED
}
}
};
Klasse Kehrmaschine
{
Servo-Servo; // das Servo
int updateInterval; // Intervall zwischen Aktualisierungen
flüchtig int pos; // aktuelle Servoposition
volatile unsigned long lastUpdate; // Letzte Aktualisierung der Position
flüchtiges int-Inkrement; // Inkrement zum Bewegen für jedes Intervall
öffentlich:
Sweeper (int-Intervall)
{
updateInterval = Intervall;
Schrittweite = 1;
}
void Anhängen (int pin)
{
servo.attach (Stift);
void Trennen ()
{
servo.detach ();
} void zurücksetzen ()
{
Pos = 0;
servo.write (pos);
Inkrement = abs (Inkrement);
}
void Update (unsigned long currentMillis)
{
if ((currentMillis - lastUpdate)> updateInterval) // Zeit zum Aktualisieren
{
lastUpdate = aktuelleMillis;
Pos + = Inkrement;
servo.write (pos);
if ((pos> = 180) || (pos <= 0)) // Ende des Sweeps
{
// umgekehrte Richtung
Inkrement = -Inkrement;
}
}
}
};
Led1 Blinker (11, 123, 400);
Led2 Blinker (12, 350, 350);
LED3-Blinker (13.200.222);
Kehrmaschine Kehrmaschine1 (25);
Kehrmaschine Kehrmaschine2 (35);
Void-Setup ()
{
Kehrmaschine1.Befestigen (9);
Kehrmaschine2.Befestigen (10);
// Timer0 wird bereits für millis() verwendet - wir unterbrechen einfach irgendwo
// in der Mitte und rufe unten die Funktion "Compare A" auf
OCR0A = 0xAF;
TIMSK0 | = _BV (OCIE0A);
PinMode (2, INPUT_PULLUP); AttachInterrupt (0, Reset, FALLING);
}
ungültig Zurücksetzen ()
{
Sweeper1.reset ();
Sweeper2.reset ();
}
// Interrupt wird einmal pro Millisekunde aufgerufen,
SIGNAL (TIMER0_COMPA_vect)
{
unsigned long currentMillis = millis ();
Sweeper1.Update (aktuelleMillis); // wenn (digitalRead (2) == HIGH)
{
Sweeper2.Update (aktuelleMillis);
led1.Update (aktuelleMillis);
}
led2.Update (aktuelleMillis);
led3.Update (aktuelleMillis);
}
leere Schleife ()
{
}
Bibliotheken
Mehr über Stoppuhren
Timer können so konfiguriert werden, dass sie mit unterschiedlichen Frequenzen und in einer Vielzahl von Modi arbeiten. Neben der Erzeugung von Interrupts werden sie auch zur Überprüfung von PWM-Pins verwendet.
Stoppuhr-Bibliotheken
Es gibt eine Reihe von Arduino-Timer-Bibliotheken, die im Internet verfügbar sind. Viele von ihnen überwachen die Funktion millis() und erfordern eine ständige Überprüfung, wie es in Teil 1 der Fall war. Aber es gibt auch einige, mit denen Sie Timer konfigurieren und Interrupts generieren können.
TimerOne- und TimerThree-Bibliotheken |
Pin-Wechsel unterbricht
Wenn 2 nicht genug ist
Arduino UNO hat nur 2 externe Interrupt-Pins. Aber was, wenn Sie mehr brauchen? Glücklicherweise unterstützt Arduino UNO "Pin Change"-Interrupts auf allen Pins.
Die Pinwechsel-Interrupts ähneln den externen Interrupts. Der einzige Unterschied besteht darin, dass einer von ihnen für eine Zustandsänderung gemacht wird auf einem der 8 verbundenen Pins. Diese sind etwas komplizierter zu handhaben, da Sie den letzten bekannten Zustand aller 8 Pins verfolgen müssen, um herauszufinden, welcher den Interrupt verursacht hat.
Stoppuhr und Interrupt-Label
Die Unterbrechungen sind wie die Schlangen im Supermarkt. Seien Sie vorsichtig und wählen Sie 10 Artikel oder weniger und es wird alles glatt laufen.
Wenn alles wichtig ist, dann ist nichts wichtig.
Interrupt-Handler sollten nur zur Verarbeitung der wichtigsten zeitbezogenen Ereignisse verwendet werden. Denken Sie daran, dass Interrupts im Interrupt-Handler deaktiviert sind. Wenn Sie versuchen, zu viel auf der Interrupt-Ebene zu tun, verschlechtern Sie die Antwort auf andere Interrupts.
Eine Unterbrechung nach der anderen.
In der ISR sind Interrupts gesperrt. Dies hat zwei sehr wichtige Konsequenzen:
- Die Arbeit im ISR sollte kurz gehalten werden, um keine Unterbrechungen zu verpassen.
- Der Code in der ISR sollte nichts aufrufen, was Interrupts zum Arbeiten erfordert (z. B. eine delay()-Funktion oder irgendetwas, das den I2C-Bus verwendet). Wenn dies jedoch der Fall ist, stürzt Ihr Programm ab.
Lange Verarbeitung bis zur Schleife verzögern
Wenn Sie als Reaktion auf einen Interrupt eine detaillierte Verarbeitung benötigen, verwenden Sie den Interrupt-Handler, um nur das Notwendige zu tun. Setzen Sie die schwebende Zustandsvariable später, um anzuzeigen, dass eine Nachbearbeitung erforderlich ist. Wenn Sie die Update-Funktion aus einer Schleife aufrufen, überprüfen Sie den Status der Variablen, um festzustellen, ob eine weitere Verarbeitung erforderlich ist.
Überprüfen Sie dies, bevor Sie die Stoppuhr neu konfigurieren
Stoppuhren sind eine begrenzte Quelle. UNO hat nur 3 davon und sie werden für viele Dinge verwendet. Wenn Sie die Einstellungen der Stoppuhr ändern, funktionieren einige andere Dinge möglicherweise nicht mehr. Zum Beispiel im Arduino UNO:
- Stopoper0 - wird in den Funktionen millis (), micros (), delay () und in PWM an den Pins 5 und 6 verwendet
- Stopwatch1 – verwendet in Serves, WaveHC-Bibliothek und PWM an den Pins 9 und 10
- Stopwatch2 – Wird von Tone und PWM an den Pins 11 und 13 verwendet
Daten sicher austauschen
Wir müssen beim Datenaustausch zwischen dem Interrupt-Handler und dem Code in der Schleife vorsichtig sein, da ein Interrupt jede CPU-Aktivität zum Absturz bringen wird, um weiterzuarbeiten.
Variablen ändern
Der Compiler versucht manchmal, Ihren Code auf Geschwindigkeit zu optimieren. Manchmal behalten diese Änderungen eine Kopie der am häufigsten verwendeten Variablen in der Registrierung für den schnellen Zugriff bei. Das Problem besteht darin, dass einige dieser Variablen vom Interrupt-Handler und dem Schleifencode gemeinsam genutzt werden. Einer von ihnen verwendet möglicherweise eine ältere Version anstelle der neuesten. Markieren Sie Variablen als sich ändernd, um dem Compiler mitzuteilen, dass er diese potenziell gefährlichen Optimierungen nicht durchführen soll.
Schutz größerer Variablen
Selbst zu sagen, dass sich eine Variable ändert , reicht nicht aus, wenn sie größer als eine ganze Zahl ist (z. B. Strings, Tabellen, Strukturen usw.). Größere Variablen erfordern zum Aktualisieren mehrere Sätze von Anweisungen. Wenn während der Aktualisierung ein Interrupt auftritt, können die Daten beschädigt werden. Wenn Sie größere Variablen oder Strukturen haben, die mit Interrupt-Handlern ausgetauscht werden, sollten Sie Interrupts beim Aktualisieren von der Schleife ausschließen. (Interrupts sind standardmäßig in der Interrupt-Behandlung deaktiviert).
Quelle: https://learn.adafruit.com/multi-tasking-the-arduino-part-2