Arduino-Unterbrechung (bei Pinwechsel)

8

Ich benutze die Interrupt-Funktion, um ein Array mit Werten zu füllen, die von empfangen wurden digitalRead().

 void setup() {
      Serial.begin(115200);
       attachInterrupt(0, test_func, CHANGE);
    }

    void test_func(){
      if(digitalRead(pin)==HIGH){
          test_array[x]=1;  
        } else if(digitalRead(pin)==LOW){
          test_array[x]=0;  
        }
         x=x+1;
    }

Das Problem ist, dass es beim Drucken test_arrayWerte gibt wie: 111oder 000.

Wenn ich die CHANGEOption in der attachInterrupt()Funktion verwende, sollte die 0101010101Datensequenz nach meinem Verständnis immer ohne Wiederholung sein.

Die Daten ändern sich sehr schnell, da sie von einem Funkmodul stammen.

user277820
quelle
1
Interrupts entprellen die Schaltfläche nicht. Verwenden Sie Hardware-Debouncing?
Ignacio Vazquez-Abrams
Bitte senden Sie den vollständigen Code, einschließlich pin, xund test_arrayDefinition sowie loop()Verfahren; Auf diese Weise können wir feststellen, ob dies beim Zugriff auf von test_func.
jfpoilpret
2
Sie sollten digitalRead () nicht zweimal im ISR verwenden: Überlegen Sie, was passieren würde, wenn Sie beim ersten Aufruf LOW und beim zweiten HIGH erhalten. Stattdessen if (digitalRead(pin) == HIGH) ... else ...;oder noch besser diese einzeilige ISR : test_array[x++] = digitalRead(pin);.
Edgar Bonet
@ EdgarBonet schön! +1 zu diesem Kommentar. Ich hoffe, es macht Ihnen nichts aus, dass ich meiner Antwort etwas hinzugefügt habe, um das aufzunehmen, was Sie hier erwähnt haben. Auch wenn Sie sich dazu entschließen, Ihre eigene Antwort einschließlich dieses Details zu verfassen, werde ich meinen Zusatz entfernen und Ihre Stimme abgeben, damit Sie den Repräsentanten dafür erhalten.
Clayton Mills
@ Clayton Mills: Ich bereite eine (zu lange und geringfügig tangentiale) Antwort vor, aber Sie können Ihre Bearbeitung beibehalten, sie ist für mich vollkommen in Ordnung.
Edgar Bonet

Antworten:

20

Als eine Art Prolog zu dieser zu langen Antwort ...

Diese Frage hat mich tief in das Problem der Unterbrechungslatenz hineingezogen, bis ich den Schlaf in Zählzyklen anstelle von Schafen verlor. Ich schreibe diese Antwort eher, um meine Ergebnisse zu teilen, als nur um die Frage zu beantworten: Der größte Teil dieses Materials befindet sich möglicherweise nicht auf einem Niveau, das für eine richtige Antwort geeignet ist. Ich hoffe jedoch, dass es für Leser nützlich sein wird, die hier auf der Suche nach Lösungen für Latenzprobleme landen. Es wird erwartet, dass die ersten Abschnitte für ein breites Publikum nützlich sind, einschließlich des Originalplakats. Dann wird es auf dem Weg haarig.

Clayton Mills erklärte bereits in seiner Antwort, dass es eine gewisse Latenz gibt, auf Interrupts zu reagieren. Hier werde ich mich auf die Quantifizierung der Latenz konzentrieren (die bei Verwendung der Arduino-Bibliotheken sehr groß ist ) und auf die Mittel, um sie zu minimieren. Das meiste, was folgt, ist spezifisch für die Hardware des Arduino Uno und ähnlicher Boards.

Minimierung der Interrupt-Latenz auf dem Arduino

(oder wie man von 99 auf 5 Zyklen herunterkommt)

Ich werde die ursprüngliche Frage als Arbeitsbeispiel verwenden und das Problem in Bezug auf die Interrupt-Latenz wiederholen. Wir haben ein externes Ereignis, das einen Interrupt auslöst (hier: INT0 beim Pinwechsel). Wir müssen etwas unternehmen, wenn der Interrupt ausgelöst wird (hier: Lesen Sie einen digitalen Eingang). Das Problem ist: Es gibt eine gewisse Verzögerung zwischen dem Auslösen des Interrupts und dem Ergreifen der entsprechenden Maßnahmen. Wir nennen diese Verzögerung " Interrupt-Latenz ". Eine lange Latenz ist in vielen Situationen nachteilig. In diesem speziellen Beispiel kann sich das Eingangssignal während der Verzögerung ändern. In diesem Fall erhalten wir einen fehlerhaften Messwert. Wir können nichts tun, um die Verzögerung zu vermeiden: Sie hängt von der Art und Weise ab, wie Interrupts funktionieren. Wir können jedoch versuchen, es so kurz wie möglich zu halten, was hoffentlich die schlimmen Folgen minimieren sollte.

Das erste, was wir tun können, ist, die zeitkritischen Maßnahmen innerhalb des Interrupt-Handlers so schnell wie möglich zu ergreifen. Dies bedeutet, dass Sie digitalRead()am Anfang des Handlers einmal (und nur einmal) aufrufen müssen . Hier ist die nullte Version des Programms, auf der wir aufbauen werden:

#define INT_NUMBER 0
#define PIN_NUMBER 2    // interrupt 0 is on pin 2
#define MAX_COUNT  200

volatile uint8_t count_edges;  // count of signal edges
volatile uint8_t count_high;   // count of high levels

/* Interrupt handler. */
void read_pin()
{
    int pin_state = digitalRead(PIN_NUMBER);  // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (pin_state == HIGH) count_high++;
}

void setup()
{
    Serial.begin(9600);
    attachInterrupt(INT_NUMBER, read_pin, CHANGE);
}

void loop()
{
    /* Wait for the interrupt handler to count MAX_COUNT edges. */
    while (count_edges < MAX_COUNT) { /* wait */ }

    /* Report result. */
    Serial.print("Counted ");
    Serial.print(count_high);
    Serial.print(" HIGH levels for ");
    Serial.print(count_edges);
    Serial.println(" edges");

    /* Count again. */
    count_high = 0;
    count_edges = 0;  // do this last to avoid race condition
}

Ich habe dieses Programm und die nachfolgenden Versionen getestet, indem ich ihm Impulsfolgen unterschiedlicher Breite gesendet habe. Zwischen den Impulsen ist genügend Abstand, um sicherzustellen, dass keine Flanke übersehen wird: Selbst wenn die fallende Flanke vor dem vorherigen Interrupt empfangen wird, wird die zweite Interrupt-Anforderung angehalten und schließlich bedient. Wenn ein Impuls kürzer als die Interrupt-Latenz ist, liest das Programm an beiden Flanken 0. Die gemeldete Anzahl von HIGH-Pegeln ist dann der Prozentsatz der korrekt gelesenen Impulse.

Was passiert, wenn der Interrupt ausgelöst wird?

Bevor wir versuchen, den obigen Code zu verbessern, werden wir uns die Ereignisse ansehen, die sich unmittelbar nach dem Auslösen des Interrupts entfalten. Der Hardware-Teil der Geschichte wird in der Atmel-Dokumentation erzählt. Der Software-Teil durch Zerlegen der Binärdatei.

Meistens wird der eingehende Interrupt sofort bedient. Es kann jedoch vorkommen, dass sich die MCU (was "Mikrocontroller" bedeutet) mitten in einer zeitkritischen Aufgabe befindet, bei der die Interrupt-Wartung deaktiviert ist. Dies ist normalerweise der Fall, wenn bereits ein anderer Interrupt bedient wird. In diesem Fall wird die eingehende Interrupt-Anforderung nur dann angehalten und bearbeitet, wenn dieser zeitkritische Abschnitt abgeschlossen ist. Diese Situation ist schwer vollständig zu vermeiden, da es einige dieser kritischen Abschnitte in der Arduino- Kernbibliothek gibt (die ich " libcore " nennen werde"im Folgenden). Glücklicherweise sind diese Abschnitte kurz und werden nur gelegentlich ausgeführt. Daher wird unsere Interrupt-Anforderung die meiste Zeit sofort bearbeitet. Im Folgenden gehe ich davon aus, dass uns diese wenigen nicht wichtig sind Fälle, in denen dies nicht der Fall ist.

Dann wird unsere Anfrage sofort bearbeitet. Dies beinhaltet immer noch eine Menge Dinge, die eine ganze Weile dauern können. Erstens gibt es eine festverdrahtete Sequenz. Die MCU beendet die Ausführung des aktuellen Befehls. Glücklicherweise sind die meisten Anweisungen ein Zyklus, aber einige können bis zu vier Zyklen dauern. Dann löscht die MCU ein internes Flag, das die weitere Wartung von Interrupts deaktiviert. Dies soll verschachtelte Interrupts verhindern. Dann wird der PC im Stapel gespeichert. Der Stapel ist ein RAM-Bereich, der für diese Art der temporären Speicherung reserviert ist. Der PC (bedeutet " Programmzähler "") ist ein internes Register, das die Adresse des nächsten Befehls enthält, den die MCU ausführen soll. Auf diese Weise kann die MCU wissen, was als nächstes zu tun ist, und es ist wichtig, es zu speichern, da es für den Hauptbefehl wiederhergestellt werden muss Programm, um an der Stelle fortzufahren, an der es unterbrochen wurde. Der PC wird dann mit einer festverdrahteten Adresse geladen, die für die empfangene Anforderung spezifisch ist. Dies ist das Ende der festverdrahteten Sequenz, der Rest wird softwaregesteuert.

Die MCU führt nun den Befehl von dieser festverdrahteten Adresse aus. Dieser Befehl wird als " Interrupt-Vektor " bezeichnet und ist im Allgemeinen ein "Sprung" -Befehl, der uns zu einer speziellen Routine bringt, die als ISR (" Interrupt Service Routine ") bezeichnet wird. In diesem Fall heißt der ISR "__vector_1", auch bekannt als "INT0_vect". Dies ist eine Fehlbezeichnung, da es sich um einen ISR und nicht um einen Vektor handelt. Diese spezielle ISR stammt von libcore. Wie bei jedem ISR beginnt es mit einem Prolog , der eine Reihe interner CPU-Register auf dem Stapel speichert. Auf diese Weise können diese Register verwendet und anschließend auf ihre vorherigen Werte zurückgesetzt werden, um das Hauptprogramm nicht zu stören. Anschließend wird nach dem Interrupt-Handler gesucht, bei dem registriert wurdeattachInterrupt()und es wird diesen Handler nennen, der unsere read_pin()Funktion oben ist. Unsere Funktion wird dann digitalRead()von libcore aufgerufen. digitalRead()In einigen Tabellen wird die Arduino-Portnummer dem zu lesenden Hardware-E / A-Port und der zu testenden zugehörigen Bitnummer zugeordnet. Es wird auch geprüft, ob sich an diesem Pin ein PWM-Kanal befindet, der deaktiviert werden müsste. Es liest dann den E / A-Port ... und wir sind fertig. Nun, wir sind noch nicht wirklich damit fertig, den Interrupt zu warten, aber die zeitkritische Aufgabe (Lesen des E / A-Ports) ist erledigt, und es ist alles, was zählt, wenn wir die Latenz betrachten.

Hier ist eine kurze Zusammenfassung aller oben genannten Punkte zusammen mit den damit verbundenen Verzögerungen in den CPU-Zyklen:

  1. festverdrahtete Sequenz: Aktuelle Anweisung beenden, verschachtelte Interrupts verhindern, PC speichern, Adresse des Vektors laden (≥ 4 Zyklen)
  2. Interrupt-Vektor ausführen: Sprung zum ISR (3 Zyklen)
  3. ISR-Prolog: Register speichern (32 Zyklen)
  4. ISR-Hauptteil: Vom Benutzer registrierte Funktion suchen und aufrufen (13 Zyklen)
  5. read_pin: digitalRead aufrufen (5 Zyklen)
  6. digitalRead: Finden Sie den relevanten Port und das zu testende Bit (41 Zyklen).
  7. digitalRead: Lesen Sie den E / A-Port (1 Zyklus)

Wir nehmen das beste Szenario mit 4 Zyklen für die festverdrahtete Sequenz an. Dies ergibt eine Gesamtlatenz von 99 Zyklen oder etwa 6,2 µs bei einem 16-MHz-Takt. Im Folgenden werde ich einige Tricks untersuchen, mit denen diese Latenz verringert werden kann. Sie kommen ungefähr in aufsteigender Reihenfolge der Komplexität, aber sie alle brauchen uns, um irgendwie in die Interna der MCU zu graben.

Verwenden Sie den direkten Portzugriff

Das offensichtliche erste Ziel zur Verkürzung der Latenz ist digitalRead(). Diese Funktion bietet eine schöne Abstraktion für die MCU-Hardware, ist jedoch für zeitkritische Arbeiten zu ineffizient. Diesen loszuwerden ist eigentlich trivial: Wir müssen ihn nur durch einen digitalReadFast()aus der digitalwritefast- Bibliothek ersetzen . Dies halbiert die Latenz auf Kosten eines kleinen Downloads fast um die Hälfte!

Nun, das war zu einfach, um Spaß zu haben. Ich werde Ihnen lieber zeigen, wie man es auf die harte Tour macht. Der Zweck ist es, uns in Low-Level-Sachen zu bringen. Das Verfahren wird als „ direkter Port - Zugriff “ und an der Seite auf auf die Arduino Referenz gut dokumentiert Port - Register . An dieser Stelle empfiehlt es sich, das Datenblatt ATmega328P herunterzuladen und einen Blick darauf zu werfen . Dieses 650-seitige Dokument mag auf den ersten Blick etwas entmutigend wirken. Es ist jedoch gut in Abschnitte unterteilt, die für die einzelnen MCU-Peripheriegeräte und -Funktionen spezifisch sind. Und wir müssen nur die Abschnitte überprüfen, die für das, was wir tun, relevant sind. In diesem Fall handelt es sich um den Abschnitt mit dem Namen E / A-Ports . Hier ist eine Zusammenfassung dessen, was wir aus diesen Lesungen lernen:

  • Der Arduino-Pin 2 wird auf dem AVR-Chip tatsächlich als PD2 (dh Port D, Bit 2) bezeichnet.
  • Wir erhalten den gesamten Port D auf einmal, indem wir ein spezielles MCU-Register namens "PIND" lesen.
  • Wir überprüfen dann Bit Nummer 2, indem wir ein bitweises logisches und (den C '&' Operator) mit ausführen1 << 2 .

Hier ist unser modifizierter Interrupt-Handler:

#define PIN_REG    PIND  // interrupt 0 is on AVR pin PD2
#define PIN_BIT    2

/* Interrupt handler. */
void read_pin()
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Jetzt liest unser Handler das E / A-Register, sobald es aufgerufen wird. Die Latenz beträgt 53 CPU-Zyklen. Dieser einfache Trick hat uns 46 Zyklen erspart!

Schreiben Sie Ihren eigenen ISR

Das nächste Ziel für das Trimmen des Zyklus ist der ISR INT0_vect. Dieser ISR wird benötigt, um die folgenden Funktionen attachInterrupt()bereitzustellen: Wir können Interrupt-Handler jederzeit während der Programmausführung ändern. Obwohl es schön zu haben ist, ist dies für unseren Zweck nicht wirklich nützlich. Anstatt den ISR des libcore unseren Interrupt-Handler lokalisieren und aufrufen zu lassen, sparen wir einige Zyklen, indem wir den ISR durch unseren Handler ersetzen .

Das ist nicht so schwer, wie es sich anhört. ISRs können wie normale Funktionen geschrieben werden. Wir müssen nur ihre spezifischen Namen kennen und sie mit einem speziellen ISR()Makro von avr-libc definieren. An dieser Stelle sollten Sie sich die Dokumentation von avr-libc zu Interrupts und den Datenblattabschnitt mit dem Namen Externe Interrupts ansehen . Hier ist die kurze Zusammenfassung:

  • Wir müssen ein Bit in ein spezielles Hardwareregister namens EICRA ( External Interrupt Control Register A ) schreiben, um den Interrupt so zu konfigurieren, dass er bei jeder Änderung des Pin-Werts ausgelöst wird. Dies wird in durchgeführt setup().
  • Wir müssen ein Bit in ein anderes Hardwareregister namens EIMSK ( External Interrupt MaSK Register ) schreiben , um den INT0-Interrupt zu aktivieren. Dies wird auch in durchgeführt setup().
  • Wir müssen den ISR mit der Syntax definieren ISR(INT0_vect) { ... }.

Hier ist der Code für den ISR und setup()alles andere bleibt unverändert:

/* Interrupt service routine for INT0. */
ISR(INT0_vect)
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

void setup()
{
    Serial.begin(9600);
    EICRA = 1 << ISC00;  // sense any change on the INT0 pin
    EIMSK = 1 << INT0;   // enable INT0 interrupt
}

Dies ist mit einem kostenlosen Bonus verbunden: Da dieser ISR einfacher ist als der, den er ersetzt, benötigt er weniger Register, um seine Arbeit zu erledigen, und der Prolog zum Speichern von Registern ist kürzer. Jetzt haben wir eine Latenz von 20 Zyklen. Nicht schlecht, wenn man bedenkt, dass wir fast 100 angefangen haben!

An dieser Stelle würde ich sagen, dass wir fertig sind. Mission erfüllt. Was folgt, ist nur für diejenigen, die keine Angst haben, sich mit einer AVR-Baugruppe die Hände schmutzig zu machen. Andernfalls können Sie hier aufhören zu lesen und sich dafür bedanken, dass Sie so weit gekommen sind.

Schreibe einen nackten ISR

Immer noch hier? Gut! Um fortzufahren, wäre es hilfreich, zumindest eine sehr grundlegende Vorstellung davon zu haben, wie die Montage funktioniert, und sich das Inline Assembler-Kochbuch aus der avr-libc-Dokumentation anzusehen . Zu diesem Zeitpunkt sieht unsere Interrupt-Eingabesequenz folgendermaßen aus:

  1. festverdrahtete Sequenz (4 Zyklen)
  2. Interrupt-Vektor: Sprung zum ISR (3 Zyklen)
  3. ISR-Prolog: Regs speichern (12 Zyklen)
  4. Das erste im ISR-Body: Lesen Sie den IO-Port (1 Zyklus).

Wenn wir es besser machen wollen, müssen wir die Lesung des Ports in den Prolog verschieben. Die Idee ist die folgende: Das Lesen des PIND-Registers blockiert ein CPU-Register, daher müssen wir mindestens ein Register speichern, bevor wir dies tun, aber die anderen Register können warten. Wir müssen dann einen benutzerdefinierten Prolog schreiben, der den E / A-Port direkt nach dem Speichern des ersten Registers liest. Sie haben bereits in der avr-libc-Interrupt-Dokumentation gesehen (Sie haben es gelesen, richtig?), Dass ein ISR nackt gemacht werden kann. In diesem Fall gibt der Compiler keinen Prolog oder Epilog aus, sodass wir unsere eigene benutzerdefinierte Version schreiben können.

Das Problem bei diesem Ansatz ist, dass wir wahrscheinlich den gesamten ISR in Assembly schreiben werden. Keine große Sache, aber ich möchte lieber, dass der Compiler diese langweiligen Prologe und Epiloge für mich schreibt. Also, hier ist der schmutzige Trick: Wir werden den ISR in zwei Teile teilen:

  • Der erste Teil wird ein kurzes Baugruppenfragment sein
    • Speichern Sie ein einzelnes Register im Stapel
    • Lesen Sie PIND in dieses Register
    • Speichern Sie diesen Wert in einer globalen Variablen
    • Stellen Sie das Register vom Stapel wieder her
    • springe zum zweiten Teil
  • Der zweite Teil wird regulärer C-Code mit vom Compiler generiertem Prolog und Epilog sein

Unser vorheriger INT0 ISR wird dann durch diesen ersetzt:

volatile uint8_t sampled_pin;    // this is now a global variable

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    push r0                \n"  // save register r0
    "    in r0, %[pin]          \n"  // read PIND into r0
    "    sts sampled_pin, r0    \n"  // store r0 in a global
    "    pop r0                 \n"  // restore previous r0
    "    rjmp INT0_vect_part_2  \n"  // go to part 2
    :: [pin] "I" (_SFR_IO_ADDR(PIND)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Hier verwenden wir das ISR () - Makro, um das Compiler-Instrument INT0_vect_part_2mit dem erforderlichen Prolog und Epilog zu erhalten. Der Compiler wird sich beschweren, dass "'INT0_vect_part_2' ein falsch geschriebener Signalhandler zu sein scheint", aber die Warnung kann sicher ignoriert werden. Jetzt hat der ISR einen einzelnen 2-Zyklus-Befehl vor dem eigentlichen Port-Lesen, und die Gesamtlatenz beträgt nur 10 Zyklen.

Verwenden Sie das GPIOR0-Register

Was wäre, wenn wir ein Register für diesen bestimmten Job reservieren könnten? Dann müssten wir nichts speichern, bevor wir den Port lesen. Wir können den Compiler tatsächlich bitten , eine globale Variable an ein Register zu binden . Dies würde jedoch erfordern, dass wir den gesamten Arduino-Kern und libc neu kompilieren, um sicherzustellen, dass das Register immer reserviert ist. Nicht wirklich praktisch. Auf der anderen Seite verfügt der ATmega328P über drei Register, die weder vom Compiler noch von einer Bibliothek verwendet werden und zum Speichern von beliebigen Inhalten zur Verfügung stehen. Sie heißen GPIOR0, GPIOR1 und GPIOR2 ( General Purpose I / O Registers ). Obwohl sie im E / A-Adressraum der MCU zugeordnet sind, sind dies tatsächlich keineE / A-Register: Sie sind nur einfacher Speicher, wie drei Bytes RAM, die irgendwie in einem Bus verloren gegangen sind und im falschen Adressraum gelandet sind. Diese sind nicht so leistungsfähig wie die internen CPU-Register, und wir können PIND mit der inAnweisung nicht in eines dieser Register kopieren . GPIOR0 ist interessant, obwohl, dass es bitadressierbaren , genau wie PIND. Auf diese Weise können wir die Informationen übertragen, ohne ein internes CPU-Register zu beschädigen.

Hier ist der Trick: Wir werden sicherstellen, dass GPIOR0 anfänglich Null ist (es wird beim Booten tatsächlich von der Hardware gelöscht), dann werden wir die sbicAnweisung (Nächste Anweisung überspringen, wenn ein Bit in einem E / A-Register gelöscht ist) und die sbi( Setzen Sie in einigen E / A-Registeranweisungen die folgenden Anweisungen auf 1 Bit:

sbic PIND, 2   ; skip the following if bit 2 of PIND is clear
sbi GPIOR0, 0  ; set to 1 bit 0 of GPIOR0

Auf diese Weise wird GPIOR0 0 oder 1, abhängig von dem Bit, das wir aus PIND lesen wollten. Die Ausführung des sbic-Befehls dauert 1 oder 2 Zyklen, je nachdem, ob die Bedingung falsch oder wahr ist. Offensichtlich wird beim ersten Zyklus auf das PIND-Bit zugegriffen. In dieser neuen Version des Codes ist die globale Variable sampled_pinnicht mehr nützlich, da sie grundsätzlich durch GPIOR0 ersetzt wird:

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    sbic %[pin], %[bit]    \n"
    "    sbi %[gpio], 0         \n"
    "    rjmp INT0_vect_part_2  \n"
    :: [pin]  "I" (_SFR_IO_ADDR(PIND)),
       [bit]  "I" (PIN_BIT),
       [gpio] "I" (_SFR_IO_ADDR(GPIOR0)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Es ist zu beachten, dass GPIOR0 im ISR immer zurückgesetzt werden muss.

Das Abtasten des PIND-E / A-Registers ist nun das allererste, was innerhalb des ISR durchgeführt wird. Die Gesamtlatenz beträgt 8 Zyklen. Dies ist ungefähr das Beste, was wir tun können, bevor wir mit schrecklich sündigen Klumpen befleckt werden. Dies ist wieder eine gute Gelegenheit, mit dem Lesen aufzuhören ...

Fügen Sie den zeitkritischen Code in die Vektortabelle ein

Für diejenigen, die noch hier sind, ist hier unsere aktuelle Situation:

  1. festverdrahtete Sequenz (4 Zyklen)
  2. Interrupt-Vektor: Sprung zum ISR (3 Zyklen)
  3. ISR-Body: Lesen Sie den IO-Port (im 1. Zyklus).

Es gibt offensichtlich wenig Raum für Verbesserungen. Die einzige Möglichkeit, die Latenz an dieser Stelle zu verkürzen, besteht darin , den Interrupt-Vektor selbst durch unseren Code zu ersetzen . Seien Sie gewarnt, dass dies für jeden, der Wert auf sauberes Software-Design legt, immens unangenehm sein sollte. Aber es ist möglich und ich werde Ihnen zeigen, wie.

Das Layout der ATmega328P-Vektortabelle finden Sie im Datenblatt, Abschnitt Interrupts , Unterabschnitt Interrupt-Vektoren in ATmega328 und ATmega328P . Oder indem Sie ein Programm für diesen Chip zerlegen. So sieht es aus. Ich verwende die Konventionen von avr-gcc und avr-libc (__init ist Vektor 0, Adressen sind in Bytes), die sich von denen von Atmel unterscheiden.

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  jmp __vector_1   a.k.a. INT0_vect
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

Jeder Vektor hat einen 4-Byte-Schlitz, der mit einem einzelnen jmpBefehl gefüllt ist . Dies ist ein 32-Bit-Befehl, im Gegensatz zu den meisten 16-Bit-AVR-Befehlen. Ein 32-Bit-Steckplatz ist jedoch zu klein, um den ersten Teil unseres ISR aufzunehmen: Wir können die Anweisungen sbicund sbianpassen, aber nicht die rjmp. Wenn wir das tun, sieht die Vektortabelle folgendermaßen aus:

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  sbic PIND, 2     the first part...
 0x0006  sbi GPIOR0, 0    ...of our ISR
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

Wenn INT0 ausgelöst wird, wird PIND gelesen, das relevante Bit wird in GPIOR0 kopiert und die Ausführung fällt dann zum nächsten Vektor durch. Dann wird der ISR für INT1 anstelle des ISR für INT0 aufgerufen. Das ist gruselig, aber da wir INT1 sowieso nicht verwenden, werden wir nur seinen Vektor "entführen", um INT0 zu warten.

Jetzt müssen wir nur noch unsere eigene benutzerdefinierte Vektortabelle schreiben, um die Standardtabelle zu überschreiben. Es stellt sich heraus, dass es nicht so einfach ist. Die Standardvektortabelle wird von der avr-libc-Distribution in einer Objektdatei namens crtm328p.o bereitgestellt, die automatisch mit jedem von uns erstellten Programm verknüpft wird. Im Gegensatz zum Bibliothekscode soll der Objektdateicode nicht überschrieben werden. Wenn Sie dies versuchen, wird ein Linkerfehler über die zweimal definierte Tabelle ausgegeben. Dies bedeutet, dass wir die gesamte crtm328p.o durch unsere benutzerdefinierte Version ersetzen müssen. Eine Möglichkeit besteht darin, den vollständigen Quellcode von avr-libc herunterzuladen , unsere benutzerdefinierten Änderungen in gcrt1.S vorzunehmen und diesen dann als benutzerdefinierte libc zu erstellen .

Hier habe ich mich für einen leichteren, alternativen Ansatz entschieden. Ich habe ein benutzerdefiniertes crt.S geschrieben, eine vereinfachte Version des Originals von avr-libc. Es fehlen einige selten verwendete Funktionen, wie die Möglichkeit, einen "catch all" ISR zu definieren oder das Programm durch Aufrufen zu beenden (dh das Arduino einzufrieren) exit(). Hier ist der Code. Ich habe den sich wiederholenden Teil der Vektortabelle gekürzt, um das Scrollen zu minimieren:

#include <avr/io.h>

.weak __heap_end
.set  __heap_end, 0

.macro vector name
    .weak \name
    .set \name, __vectors
    jmp \name
.endm

.section .vectors
__vectors:
    jmp __init
    sbic _SFR_IO_ADDR(PIND), 2   ; these 2 lines...
    sbi _SFR_IO_ADDR(GPIOR0), 0  ; ...replace vector_1
    vector __vector_2
    vector __vector_3
    [...and so forth until...]
    vector __vector_25

.section .init2
__init:
    clr r1
    out _SFR_IO_ADDR(SREG), r1
    ldi r28, lo8(RAMEND)
    ldi r29, hi8(RAMEND)
    out _SFR_IO_ADDR(SPL), r28
    out _SFR_IO_ADDR(SPH), r29

.section .init9
    jmp main

Es kann mit der folgenden Befehlszeile kompiliert werden:

avr-gcc -c -mmcu=atmega328p silly-crt.S

Die Skizze ist identisch mit der vorherigen, außer dass kein INT0_vect vorhanden ist und INT0_vect_part_2 durch INT1_vect ersetzt wird:

/* Interrupt service routine for INT1 hijacked to service INT0. */
ISR(INT1_vect)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Zum Kompilieren der Skizze benötigen wir einen benutzerdefinierten Kompilierungsbefehl. Wenn Sie bisher gefolgt sind, wissen Sie wahrscheinlich, wie Sie über die Befehlszeile kompilieren. Sie müssen explizit anfordern, dass dummes-crt.o mit Ihrem Programm verknüpft wird, und die -nostartfilesOption hinzufügen , um das Verknüpfen im ursprünglichen crtm328p.o zu vermeiden.

Das Lesen des E / A-Ports ist nun der allererste Befehl, der ausgeführt wird, nachdem der Interrupt ausgelöst wurde. Ich habe diese Version getestet, indem ich kurze Impulse von einem anderen Arduino gesendet habe, und sie kann (wenn auch nicht zuverlässig) den hohen Impulspegel von nur 5 Zyklen erfassen. Wir können nichts mehr tun, um die Interrupt-Latenz auf dieser Hardware zu verkürzen.

Edgar Bonet
quelle
2
Gute Erklärung! +1
Nick Gammon
6

Der Interrupt wird so eingestellt, dass er bei einer Änderung ausgelöst wird, und Ihre test_func wird als Interrupt Service Routine (ISR) festgelegt, die aufgerufen wird, um diesen Interrupt zu bedienen. Der ISR druckt dann den Wert der Eingabe.

Auf den ersten Blick würden Sie erwarten, dass der Output so ist, wie Sie es gesagt haben, und abwechselnd hohe Tiefs, da er nur bei einer Änderung zum ISR gelangt.

Was uns jedoch fehlt, ist, dass die CPU eine gewisse Zeit benötigt, um einen Interrupt zu bedienen und zum ISR zu verzweigen. Während dieser Zeit hat sich die Spannung am Pin möglicherweise wieder geändert. Insbesondere, wenn der Stift nicht durch Hardware-Entprellen oder ähnliches stabilisiert wird. Da der Interrupt bereits markiert ist und noch nicht gewartet wurde, wird diese zusätzliche Änderung (oder viele davon, da sich ein Pins-Pegel relativ zur Taktrate sehr schnell ändern kann, wenn er eine geringe parasitäre Kapazität aufweist) übersehen.

Im Wesentlichen haben wir ohne irgendeine Form des Entprellens keine Garantie dafür, dass, wenn sich der Eingang ändert und der Interrupt für die Wartung markiert wird, der Eingang immer noch den gleichen Wert hat, wenn wir seinen Wert im ISR lesen.

Als allgemeines Beispiel enthält das auf dem Arduino Uno verwendete ATmega328-Datenblatt die Interruptzeiten in Abschnitt 6.7.1 - "Interrupt-Antwortzeit". Für diesen Mikrocontroller wird angegeben, dass die Mindestzeit für die Verzweigung zu einem ISR zur Wartung 4 Taktzyklen beträgt, jedoch länger sein kann (zusätzlich, wenn ein Mehrzyklusbefehl bei Unterbrechung ausgeführt wird, oder 8 + Schlaf-Wachzeit, wenn sich die MCU im Ruhezustand befindet).

Wie @EdgarBonet in den Kommentaren erwähnt hat, kann sich der Pin auch während der ISR-Ausführung ändern. Da der ISR zweimal vom Pin liest, würde er dem test_array nichts hinzufügen, wenn er beim ersten Lesen auf ein LOW und beim zweiten auf ein HIGH stößt. Aber x würde immer noch inkrementieren und diesen Slot im Array unverändert lassen (möglicherweise als nicht initialisierte Daten, je nachdem, was zuvor mit dem Array gemacht wurde).

Sein einzeiliger ISR von test_array[x++] = digitalRead(pin);ist eine perfekte Lösung dafür.

Clayton Mills
quelle