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_array
Werte gibt wie: 111
oder 000
.
Wenn ich die CHANGE
Option in der attachInterrupt()
Funktion verwende, sollte die 0101010101
Datensequenz nach meinem Verständnis immer ohne Wiederholung sein.
Die Daten ändern sich sehr schnell, da sie von einem Funkmodul stammen.
arduino-uno
c
isr
user277820
quelle
quelle
pin
,x
undtest_array
Definition sowieloop()
Verfahren; Auf diese Weise können wir feststellen, ob dies beim Zugriff auf vontest_func
.if (digitalRead(pin) == HIGH) ... else ...;
oder noch besser diese einzeilige ISR :test_array[x++] = digitalRead(pin);
.Antworten:
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: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 wurde
attachInterrupt()
und es wird diesen Handler nennen, der unsereread_pin()
Funktion oben ist. Unsere Funktion wird danndigitalRead()
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:
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 einendigitalReadFast()
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:
1 << 2
.Hier ist unser modifizierter Interrupt-Handler:
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:setup()
.setup()
.ISR(INT0_vect) { ... }
.Hier ist der Code für den ISR und
setup()
alles andere bleibt unverändert: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:
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:
Unser vorheriger INT0 ISR wird dann durch diesen ersetzt:
Hier verwenden wir das ISR () - Makro, um das Compiler-Instrument
INT0_vect_part_2
mit 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
in
Anweisung 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
sbic
Anweisung (Nächste Anweisung überspringen, wenn ein Bit in einem E / A-Register gelöscht ist) und diesbi
( Setzen Sie in einigen E / A-Registeranweisungen die folgenden Anweisungen auf 1 Bit: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_pin
nicht mehr nützlich, da sie grundsätzlich durch GPIOR0 ersetzt wird: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:
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.
Jeder Vektor hat einen 4-Byte-Schlitz, der mit einem einzelnen
jmp
Befehl 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 Anweisungensbic
undsbi
anpassen, aber nicht dierjmp
. Wenn wir das tun, sieht die Vektortabelle folgendermaßen aus: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:Es kann mit der folgenden Befehlszeile kompiliert werden:
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:
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
-nostartfiles
Option 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.
quelle
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.quelle