Comme une sorte de prologue à cette réponse trop longue ...
Cette question m'a profondément captivé par le problème de la latence des interruptions, au point de perdre le sommeil dans les cycles de comptage au lieu des moutons. J'écris cette réponse davantage pour partager mes conclusions que pour simplement répondre à la question: la plupart de ce matériel peut en fait ne pas être à un niveau approprié pour une réponse correcte. J'espère que ce sera utile, cependant, pour les lecteurs qui débarquent ici à la recherche de solutions aux problèmes de latence. Les premières sections devraient être utiles à un large public, y compris l'affiche originale. Ensuite, il devient poilu en cours de route.
Clayton Mills a déjà expliqué dans sa réponse qu'il y avait une certaine latence dans la réponse aux interruptions. Ici, je vais me concentrer sur la quantification de la latence (qui est énorme lors de l'utilisation des bibliothèques Arduino), et sur les moyens de la minimiser. La plupart de ce qui suit est spécifique au matériel de l'Arduino Uno et des cartes similaires.
Minimiser la latence d'interruption sur l'Arduino
(ou comment passer de 99 à 5 cycles)
Je vais utiliser la question d'origine comme exemple de travail et reformuler le problème en termes de latence d'interruption. Nous avons un événement externe qui déclenche une interruption (ici: INT0 lors du changement de broche). Nous devons prendre des mesures lorsque l'interruption est déclenchée (ici: lire une entrée numérique). Le problème est: il y a un certain délai entre le déclenchement de l'interruption et la prise de l'action appropriée. Nous appelons ce délai " latence d'interruption ". Une longue latence est préjudiciable dans de nombreuses situations. Dans cet exemple particulier, le signal d'entrée peut changer pendant le retard, auquel cas nous obtenons une lecture erronée. Nous ne pouvons rien faire pour éviter ce retard: il est intrinsèque au fonctionnement des interruptions. Nous pouvons cependant essayer de le rendre aussi court que possible, ce qui devrait, espérons-le, minimiser les mauvaises conséquences.
La première chose évidente que nous pouvons faire est de prendre l'action critique dans le temps, à l'intérieur du gestionnaire d'interruption, dès que possible. Cela signifie appeler
digitalRead()
une fois (et une seule fois) au tout début du gestionnaire. Voici la version zéro du programme sur lequel nous allons construire:
#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
}
J'ai testé ce programme, et les versions suivantes, en lui envoyant des trains d'impulsions de largeurs variables. Il y a suffisamment d'espacement entre les impulsions pour garantir qu'aucun front ne soit manqué: même si le front descendant est reçu avant que l'interruption précédente ne soit effectuée, la deuxième demande d'interruption sera mise en attente et éventuellement traitée. Si une impulsion est plus courte que la latence d'interruption, le programme lit 0 sur les deux fronts. Le nombre signalé de niveaux ÉLEVÉS est alors le pourcentage d'impulsions correctement lues.
Que se passe-t-il lorsque l'interruption est déclenchée?
Avant d'essayer d'améliorer le code ci-dessus, nous examinerons les événements qui se déroulent juste après le déclenchement de l'interruption. La partie matérielle de l'histoire est racontée par la documentation Atmel. La partie logicielle, en démontant le binaire.
La plupart du temps, l'interruption entrante est réparée immédiatement. Il peut arriver, cependant, que le MCU (signifiant "microcontrôleur") se trouve au milieu d'une tâche critique en termes de temps, où le service d'interruption est désactivé. C'est généralement le cas lorsqu'il est déjà en train de traiter une autre interruption. Lorsque cela se produit, la demande d'interruption entrante est mise en attente et traitée uniquement lorsque cette section critique est terminée. Cette situation est difficile à éviter complètement, car il y a pas mal de ces sections critiques dans la bibliothèque principale Arduino (que j'appellerai " libcore"ci-dessous). Heureusement, ces sections sont courtes et ne s'exécutent que de temps en temps. Ainsi, la plupart du temps, notre demande d'interruption sera traitée immédiatement. Dans ce qui suit, je supposerai que nous ne nous soucions pas de ces quelques des cas où ce n'est pas le cas.
Ensuite, notre demande est traitée immédiatement. Cela implique encore beaucoup de choses qui peuvent prendre un certain temps. Tout d'abord, il existe une séquence câblée. Le MCU finira d'exécuter l'instruction en cours. Heureusement, la plupart des instructions sont à cycle unique, mais certaines peuvent prendre jusqu'à quatre cycles. Ensuite, le MCU efface un indicateur interne qui désactive la poursuite du service des interruptions. Ceci est destiné à empêcher les interruptions imbriquées. Ensuite, le PC est enregistré dans la pile. La pile est une zone de RAM réservée à ce type de stockage temporaire. Le PC (qui signifie " compteur de programmes "") est un registre interne contenant l'adresse de la prochaine instruction que le MCU est sur le point d'exécuter. C'est ce qui permet au MCU de savoir quoi faire ensuite, et l'enregistrer est essentiel car il devra être restauré pour que le principal programme à reprendre d'où il a été interrompu. Le PC est alors chargé avec une adresse câblée spécifique à la demande reçue, et c'est la fin de la séquence câblée, le reste étant contrôlé par logiciel.
Le MCU exécute maintenant l'instruction à partir de cette adresse câblée. Cette instruction est appelée " vecteur d'interruption " et est généralement une instruction de "saut" qui nous amènera à une routine spéciale appelée ISR (" Interrupt Service Routine "). Dans ce cas, l'ISR est appelé "__vector_1", alias "INT0_vect", ce qui est un terme impropre car il s'agit d'un ISR, pas d'un vecteur. Cet ISR particulier vient de libcore. Comme tout ISR, il commence par un prologue qui enregistre un tas de registres CPU internes sur la pile. Cela lui permettra d'utiliser ces registres et, une fois cela fait, de les restaurer à leurs valeurs précédentes afin de ne pas perturber le programme principal. Ensuite, il recherchera le gestionnaire d'interruptions enregistré avecattachInterrupt()
, et il appellera ce gestionnaire, qui est notre read_pin()
fonction ci-dessus. Notre fonction appellera alors digitalRead()
depuis libcore. digitalRead()
examinera certaines tables afin de mapper le numéro de port Arduino au port d'E / S matériel qu'il doit lire et le numéro de bit associé à tester. Il vérifiera également s'il y a un canal PWM sur cette broche qui devrait être désactivé. Il lira alors le port d'E / S ... et nous avons terminé. Eh bien, nous n'avons pas vraiment fini de réparer l'interruption, mais la tâche critique (lire le port d'E / S) est terminée, et c'est tout ce qui compte lorsque nous examinons la latence.
Voici un bref résumé de tout ce qui précède, ainsi que les retards associés dans les cycles CPU:
- séquence câblée: terminer l'instruction en cours, empêcher les interruptions imbriquées, enregistrer le PC, charger l'adresse du vecteur (≥ 4 cycles)
- exécuter le vecteur d'interruption: passer à l'ISR (3 cycles)
- Prologue ISR: sauvegarde des registres (32 cycles)
- Corps principal ISR: localiser et appeler la fonction enregistrée par l'utilisateur (13 cycles)
- read_pin: appeler digitalRead (5 cycles)
- digitalRead: trouvez le port et le bit à tester (41 cycles)
- digitalRead: lire le port d'E / S (1 cycle)
Nous supposerons le meilleur scénario, avec 4 cycles pour la séquence câblée. Cela nous donne une latence totale de 99 cycles, soit environ 6,2 µs avec une horloge de 16 MHz. Dans ce qui suit, j'explorerai quelques astuces qui peuvent être utilisées pour réduire cette latence. Ils viennent à peu près dans un ordre croissant de complexité, mais ils ont tous besoin que nous creusions d'une manière ou d'une autre dans les composants internes du MCU.
Utiliser un accès direct au port
Le premier objectif évident pour raccourcir la latence est digitalRead()
. Cette fonction fournit une belle abstraction au matériel MCU, mais elle est trop inefficace pour un travail à temps critique. Se débarrasser de celui-ci est en fait trivial: il suffit de le remplacer par digitalReadFast()
, de la
bibliothèque digitalwritefast . Cela réduit la latence de près de moitié au prix d'un petit téléchargement!
Eh bien, c'était trop facile pour être amusant, je vais plutôt vous montrer comment le faire à la dure. Le but est de nous lancer dans des choses de bas niveau. La méthode est appelée " accès direct au port " et est bien documentée sur la référence Arduino à la page sur les registres de ports . À ce stade, c'est une bonne idée de télécharger et de consulter la fiche technique ATmega328P . Ce document de 650 pages peut sembler quelque peu intimidant à première vue. Il est cependant bien organisé en sections spécifiques à chacun des périphériques et fonctionnalités du MCU. Et nous avons seulement besoin de vérifier les sections pertinentes à ce que nous faisons. Dans ce cas, il est la section du nom
des ports d' E / S . Voici un résumé de ce que nous apprenons de ces lectures:
- La broche Arduino 2 est en fait appelée PD2 (c'est-à-dire le port D, bit 2) sur la puce AVR.
- Nous obtenons tout le port D à la fois en lisant un registre MCU spécial appelé "PIND".
- Nous vérifions ensuite le bit numéro 2 en faisant une logique au niveau du bit et (l'opérateur C '&') avec
1 << 2
.
Voici donc notre gestionnaire d'interruption modifié:
#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++;
}
Maintenant, notre gestionnaire lira le registre d'E / S dès qu'il sera appelé. La latence est de 53 cycles CPU. Cette astuce simple nous a permis d'économiser 46 cycles!
Écrivez votre propre ISR
La prochaine cible pour la réduction de cycle est l'ISR INT0_vect. Cet ISR est nécessaire pour fournir la fonctionnalité de attachInterrupt()
: nous pouvons changer les gestionnaires d'interruption à tout moment pendant l'exécution du programme. Cependant, bien que agréable à avoir, ce n'est pas vraiment utile pour notre objectif. Ainsi, au lieu d'avoir l'ISR du libcore localiser et appeler notre gestionnaire d'interruption, nous économiserons quelques cycles en remplaçant l'ISR par notre gestionnaire.
Ce n'est pas aussi difficile qu'il y paraît. Les ISR peuvent être écrits comme des fonctions normales, il suffit de connaître leurs noms spécifiques et de les définir à l'aide d'une ISR()
macro spéciale de avr-libc. À ce stade, il serait bon de jeter un œil à la documentation de avr-libc sur les interruptions et à la section de la fiche technique intitulée Interruptions externes . Voici le bref résumé:
- Nous devons écrire un peu dans un registre matériel spécial appelé EICRA ( External Interrupt Control Register A ) afin de configurer l'interruption à déclencher à tout changement de la valeur de la broche. Cela se fera en
setup()
.
- Nous devons écrire un peu dans un autre registre matériel appelé EIMSK ( External Interrupt MaSK register ) afin de permettre l'interruption INT0. Cela se fera également en
setup()
.
- Nous devons définir l'ISR avec la syntaxe
ISR(INT0_vect) { ... }
.
Voici le code de l'ISR et setup()
, tout le reste est inchangé:
/* 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
}
Cela vient avec un bonus gratuit: puisque cet ISR est plus simple que celui qu'il remplace, il a besoin de moins de registres pour faire son travail, alors le prologue d'enregistrement de registre est plus court. Nous en sommes maintenant à une latence de 20 cycles. Pas mal vu que nous avons commencé près de 100!
À ce stade, je dirais que nous avons terminé. Mission accomplie. Ce qui suit est réservé à ceux qui n'ont pas peur de se salir les mains avec un assemblage AVR. Sinon, vous pouvez arrêter de lire ici, et merci d'être allé si loin.
Écrivez un ISR nu
Toujours là? Bien! Pour aller plus loin, il serait utile d'avoir au moins une idée très basique du fonctionnement de l'assemblage et de jeter un œil au livre
de recettes de l' assembleur en ligne de la documentation avr-libc. À ce stade, notre séquence d'entrée d'interruption ressemble à ceci:
- séquence câblée (4 cycles)
- vecteur d'interruption: passer à l'ISR (3 cycles)
- Prologue ISR: sauvegarde des regs (12 cycles)
- première chose dans le corps ISR: lire le port IO (1 cycle)
Si nous voulons faire mieux, nous devons déplacer la lecture du port dans le prologue. L'idée est la suivante: la lecture du registre PIND encombrera un registre CPU, nous devons donc enregistrer au moins un registre avant de le faire, mais les autres registres peuvent attendre. Nous devons ensuite écrire un prologue personnalisé qui lit le port d'E / S juste après avoir enregistré le premier registre. Vous avez déjà vu dans la documentation d'interruption avr-libc (vous l'avez lu, non?) Qu'un ISR peut être rendu
nu , auquel cas le compilateur n'émettra aucun prologue ou épilogue, nous permettant d'écrire notre propre version personnalisée.
Le problème avec cette approche est que nous finirons probablement par écrire l'ensemble de l'ISR en assembleur. Ce n'est pas grave, mais je préfère que le compilateur écrive ces ennuyeux prologues et épilogues pour moi. Voici donc la sale astuce: nous allons diviser l'ISR en deux parties:
- la première partie sera un court fragment d'assemblage qui
- enregistrer un seul registre dans la pile
- lire PIND dans ce registre
- stocker cette valeur dans une variable globale
- restaurer le registre à partir de la pile
- passer à la deuxième partie
- la deuxième partie sera du code C régulier avec un prologue et un épilogue générés par le compilateur
Notre précédent INT0 ISR est alors remplacé par ceci:
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++;
}
Ici, nous utilisons la macro ISR () pour avoir l'instrument de compilation
INT0_vect_part_2
avec le prologue et l'épilogue requis. Le compilateur se plaindra que "" INT0_vect_part_2 "semble être un gestionnaire de signal mal orthographié", mais l'avertissement peut être ignoré en toute sécurité. Maintenant, l'ISR a une seule instruction de 2 cycles avant la lecture du port réel, et la latence totale n'est que de 10 cycles.
Utilisez le registre GPIOR0
Et si nous pouvions avoir un registre réservé pour ce travail spécifique? Ensuite, nous n'aurions pas besoin de sauvegarder quoi que ce soit avant de lire le port. Nous pouvons en fait demander au compilateur de lier une variable globale à un registre . Cela, cependant, nous obligerait à recompiler tout le noyau Arduino et libc afin de nous assurer que le registre est toujours réservé. Pas vraiment pratique. D'un autre côté, l'ATmega328P se trouve avoir trois registres qui ne sont pas utilisés par le compilateur ni aucune bibliothèque, et sont disponibles pour stocker tout ce que nous voulons. Ils sont appelés GPIOR0, GPIOR1 et GPIOR2 ( Registres d'E / S à usage général ). Bien qu'ils soient mappés dans l'espace d'adressage d'E / S de la MCU, ils ne sont en fait pasRegistres d'E / S: ce ne sont que de la mémoire ordinaire, comme trois octets de RAM qui se sont en quelque sorte perdus dans un bus et se sont retrouvés dans le mauvais espace d'adressage. Ceux-ci ne sont pas aussi capables que les registres CPU internes, et nous ne pouvons pas copier PIND dans l'un d'eux avec l' in
instruction. GPIOR0 est intéressant, cependant, en ce qu'il est adressable par bit , tout comme PIND. Cela nous permettra de transférer les informations sans encombrer aucun registre CPU interne.
Voici l'astuce: nous nous assurerons que GPIOR0 est initialement nul (il est en fait effacé par le matériel au démarrage), puis nous utiliserons sbic
(Ignorer l'instruction suivante si un bit dans un registre d'E / S est effacé
) et le sbi
( Définissez à 1 un bit dans un registre d'E / S) comme suit:
sbic PIND, 2 ; skip the following if bit 2 of PIND is clear
sbi GPIOR0, 0 ; set to 1 bit 0 of GPIOR0
De cette façon, GPIOR0 finira par être 0 ou 1 selon le bit que nous voulions lire depuis PIND. L'instruction sbic prend 1 ou 2 cycles à exécuter selon que la condition est fausse ou vraie. De toute évidence, le bit PIND est accessible au premier cycle. Dans cette nouvelle version du code, la variable globale sampled_pin
n'est plus utile, car elle est fondamentalement remplacée par GPIOR0:
/* 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;
}
Il convient de noter que GPIOR0 doit toujours être réinitialisé dans l'ISR.
Maintenant, l'échantillonnage du registre d'E / S PIND est la toute première chose effectuée à l'intérieur de l'ISR. La latence totale est de 8 cycles. C'est à peu près le mieux que nous puissions faire avant d'être taché de coups terriblement coupables. C'est encore une bonne occasion d'arrêter de lire ...
Mettez le code critique dans le tableau vectoriel
Pour ceux qui sont encore là, voici notre situation actuelle:
- séquence câblée (4 cycles)
- vecteur d'interruption: passer à l'ISR (3 cycles)
- Corps ISR: lire le port IO (au 1er cycle)
Il y a évidemment peu de place pour l'amélioration. La seule façon de raccourcir la latence à ce stade est de remplacer le vecteur d'interruption lui-même par notre code. Soyez averti que cela devrait être extrêmement désagréable pour quiconque apprécie la conception de logiciels propres. Mais c'est possible, et je vais vous montrer comment.
La disposition de la table vectorielle ATmega328P se trouve dans la fiche technique, section Interruptions , sous-section Vecteurs d'interruption dans ATmega328 et ATmega328P . Ou en démontant tout programme pour cette puce. Voici à quoi ça ressemble. J'utilise les conventions de avr-gcc et avr-libc (__init est le vecteur 0, les adresses sont en octets) qui sont différentes de celles d'Atmel.
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
Chaque vecteur a un créneau de 4 octets, rempli d'une seule jmp
instruction. Il s'agit d'une instruction 32 bits, contrairement à la plupart des instructions AVR qui sont 16 bits. Mais un slot 32 bits est trop petit pour contenir la première partie de notre ISR: nous pouvons adapter les instructions sbic
et sbi
, mais pas les rjmp
. Si nous faisons cela, la table vectorielle finit par ressembler à ceci:
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
Lorsque INT0 se déclenche, PIND est lu, le bit correspondant est copié dans GPIOR0, puis l'exécution passe au vecteur suivant. Ensuite, l'ISR pour INT1 sera appelé, au lieu de l'ISR pour INT0. C'est effrayant, mais comme nous n'utilisons pas INT1 de toute façon, nous allons juste "détourner" son vecteur pour desservir INT0.
Il ne nous reste plus qu'à écrire notre propre table vectorielle personnalisée pour remplacer celle par défaut. Il s'avère que ce n'est pas si facile. La table vectorielle par défaut est fournie par la distribution avr-libc, dans un fichier objet appelé crtm328p.o qui est automatiquement lié à tout programme que nous construisons. Contrairement au code de bibliothèque, le code de fichier objet n'est pas censé être remplacé: essayer de faire cela donnera une erreur de l'éditeur de liens sur la table étant définie deux fois. Cela signifie que nous devons remplacer l'intégralité de crtm328p.o par notre version personnalisée. Une option consiste à télécharger le code source complet de avr-libc , à effectuer nos modifications personnalisées dans
gcrt1.S , puis à le créer en tant que libc personnalisée.
Ici, je suis allé pour une approche alternative plus légère. J'ai écrit un crt.S personnalisé, qui est une version simplifiée de l'original de avr-libc. Il lui manque quelques fonctionnalités rarement utilisées, comme la possibilité de définir un ISR "catch all", ou de pouvoir terminer le programme (ie geler l'Arduino) en appelant exit()
. Voici le code. J'ai découpé la partie répétitive de la table vectorielle afin de minimiser le défilement:
#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
Il peut être compilé avec la ligne de commande suivante:
avr-gcc -c -mmcu=atmega328p silly-crt.S
L'esquisse est identique à la précédente, sauf qu'il n'y a pas INT0_vect et INT0_vect_part_2 est remplacé par INT1_vect:
/* 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;
}
Pour compiler l'esquisse, nous avons besoin d'une commande de compilation personnalisée. Si vous avez suivi jusqu'à présent, vous savez probablement comment compiler à partir de la ligne de commande. Vous devez explicitement demander à silly-crt.o d'être lié à votre programme et ajouter l' -nostartfiles
option pour éviter de lier dans le crtm328p.o d'origine.
Maintenant, la lecture du port d'E / S est la toute première instruction exécutée après les déclencheurs d'interruption. J'ai testé cette version en lui envoyant des impulsions courtes à partir d'un autre Arduino, et il peut capter (mais pas de manière fiable) le niveau élevé d'impulsions aussi court que 5 cycles. Nous ne pouvons rien faire de plus pour raccourcir la latence d'interruption sur ce matériel.