Som en slags prolog til dette alt for lange svar ...
Dette spørgsmål fik mig dybt betaget af problemet med interruptlatency, til det punkt at miste søvn ved tælling cykler i stedet for får. Jeg skriver dette svar mere for at dele min opdagelse end bare at besvare spørgsmålet: det meste af dette materiale er muligvis ikke på et niveau, der passer til et ordentligt svar. Jeg håber, det vil dog være nyttigt for læsere, der lander her på jagt efter løsninger til forsinkelsesproblemer. De første par sektioner forventes at være nyttige for et bredt publikum, inklusive den originale plakat. Derefter bliver det behåret undervejs.
Clayton Mills forklarede allerede i sit svar, at der er noget ventetid, der reagerer på afbrydelser. Her vil jeg fokusere på at kvantificere forsinkelsen (som er enorm når du bruger Arduino-bibliotekerne) og på temaerne for at minimere det. Det meste af det følgende er specifikt for hardware til Arduino Uno og lignende kort.
Minimering af afbrydelsestiden på Arduino
(eller hvordan man kommer fra 99 ned til 5 cyklusser)
Jeg bruger det originale spørgsmål som et fungerende eksempel og gentager problemet med hensyn til afbrydelse af ventetid. Vi har en ekstern begivenhed, der udløser en afbrydelse (her: INT0 om pin-skift). Vi er nødt til at tage en handling, når afbrydelsen udløses (her: læs et digitalt input). Problemet er: der er en vis forsinkelse mellem, at afbrydelsen udløses, og at vi tager den passende handling. Vi kalder denne forsinkelse " interruptlatency ". En lang ventetid er skadelig i mange situationer. I dette særlige eksempel kan indgangssignalet ændre sig under forsinkelsen, i hvilket tilfælde vi får en fejlbehæftet læsning. Der er intet, vi kan gøre for at undgå forsinkelsen: det er iboende for den måde, hvorpå arbejdet afbrydes. Vi kan dog forsøge at gøre det så kort som muligt, hvilket forhåbentlig bør minimere de dårlige konsekvenser.
Den første åbenlyse ting, vi kan gøre, er at tage den tidskritiske handling,
inde i afbryderen så hurtigt som muligt. Dette betyder at ringe til digitalRead ()
en gang (og kun en gang) helt i starten af håndtereren. Her er den nul version af programmet, hvorpå vi vil bygge:
#define INT_NUMBER 0 # define PIN_NUMBER 2 // interrupt 0 er på pin 2 # definer MAX_COUNT 200 flygtige uint8_t count_edges; // optælling af signalkanter flygtige uint8_t count_high; // antal høje niveauer / * Afbryd handler. * / ugyldigt read_pin () {int pin_state = digitalRead (PIN_NUMBER); // gør dette først! hvis (count_edges > = MAX_COUNT) returnerer; // vi er færdige count_edges ++; hvis (pin_state == HIGH) count_high ++;} ugyldig opsætning () {Serial.begin (9600); attachInterrupt (INT_NUMBER, read_pin, CHANGE);} ugyldig sløjfe () {/ * Vent på, at interrupthandleren tæller MAX_COUNT kanter. * / while (count_edges < MAX_COUNT) {/ * vent * /} / * Rapportresultat. * / Serial.print ("Counted"); Serial.print (count_high); Serial.print ("HIGH levels for"); Serial.print (count_edges); Serial.println ("kanter"); / * Tæl igen. * / count_high = 0; count_edges = 0; // gør dette sidst for at undgå race-tilstand}
Jeg testede dette program og de efterfølgende versioner ved at sende det tog af pulser af varierende bredde. Der er tilstrækkelig afstand mellem pulsen for at sikre, at der ikke går glip af nogen kant: selvom den faldende kant modtages, før den forrige afbrydelse er færdig, vil den anden afbrydelsesanmodning blive sat i venteposition og til sidst serviceret. Hvis en puls er kortere end interrupt latency, læser programmet 0 i begge kanter. Det rapporterede antal HIGH niveauer er derefter procentdelen af korrekt læste impulser.
Hvad sker der, når afbrydelsen udløses?
Før vi prøver at forbedre koden ovenfor, vil vi se på begivenhederne, der udfolder sig lige efter afbrydelsen, udløses. Den hårde del af historien fortælles af Atmel-dokumentationen. Softwaredelen,
ved at adskille den binære.
Det meste af tiden serviceres den indgående afbrydelse med det samme. Det kan dog ske, at MCU (betyder "mikrokontroller") er i midten af en tidskritisk opgave, hvor afbrydelse af service er deaktiveret. Dette er typisk tilfældet, når den allerede servicerer en anden afbrydelse. Når dette sker, indsendes den indgående afbrydelsesanmodning kun, når den tidskritiske sektion er færdig. Denne situation er svær at undgå helt, fordi der er ganske få af disse kritiske sektioner i Arduino-kernebiblioteket (som jeg vil kalde " libcore "i det følgende). Heldigvis er disse sektioner kort og kører kun så ofte. Således vil vores afbrydelsesforespørgsel oftest blive serviceret med det samme. I det følgende vil jeg antage, at vi ikke er ligeglade med de få tilfælde, hvor dette ikke er tilfældet.
Derefter betjenes vores anmodning straks. Dette involverer stadig en masse ting, der kan tage et stykke tid. For det første er der en hardwired sekvens. MCU vil afslutte udførelsen af den aktuelle instruktion. Heldigvis er de fleste instruktioner encyklus, men nogle kan tage op til fire cyklusser. MCU rydder derefter et internt flag, der deaktiverer yderligere service af afbrydelser. Dette er beregnet til at forhindre indlejrede afbrydelser. Derefter gemmes pc'erne i stakken. Stakken er et område med RAM, der er reserveret til denne slags midlertidig lagring. PC'en (betyder " Programtæller ") er et internt register, der indeholder adressen på den næste instruktion, som MCU'en skal udføre. Dette er det, der gør det muligt for MCU at vide, hvad de skal gøre næste gang, og det er vigtigt at gemme det, fordi det skal gendannes, for at hovedprogrammet kan genoptages, hvorfra det blev afbrudt. PC'en er derefter fyldt med en hardwired adresse, der er specifik for den modtagne anmodning, og dette er slutningen af den hardwired-sekvens, resten er software-styret.
MCU'en udfører nu instruktionen fra den hardwired-adresse. Dette
instruktion kaldes en " afbrydelsesvektor " og er generelt en "spring" instruktion, der bringer os til en særlig rutine kaldet en ISR (" Interrupt Service Routine "). I dette tilfælde kaldes ISR "__ vector_1", også kaldet "INT0_vect", hvilket er en misvisende betegnelse, fordi det er en ISR, ikke en vektor. Denne særlige ISR kommer fra libcore. Som enhver ISR starter det med en prolog der gemmer en masse interne CPU-registre på stakken. Dette giver det mulighed for at bruge disse registre og, når det er gjort, gendanne dem til deres tidligere værdier for ikke at forstyrre hovedprogrammet. Derefter ser det efter afbryderehåndteringen, der blev registreret med attachInterrupt ()
, og den kalder den håndterer, som er vores read_pin ()
-funktion ovenfor. Vores funktion kalder derefter digitalRead ()
fra libcore. digitalRead ()
vil se intosome-tabeller for at kortlægge Arduino-portnummeret til den hardware-I / Oport, den skal læse, og det tilknyttede bitnummer, der skal testes. Det vil også kontrollere, om der er en PWM-kanal på den pin, der skal være handicappet. Derefter læses I / O-porten ... og vi er færdige. Nå, vi er ikke rigtig færdige med at afbryde afbrydelsen, men den tidskritiske opgave (læsning af I / O-porten) er færdig, og det er alt, hvad der betyder noget, når vi ser på latens.
Her er en kort resumé af alle ovenstående sammen med de tilknyttede forsinkelser i CPU-cyklusser:
- hardwired-sekvens: afslut nuværende instruktion, forhindrer indlejrede afbrydelser, gem pc, belastningsadresse for vektor (≥ 4 cyklusser)
- udfør afbrydelsesvektor: spring til ISR (3 cyklusser)
- ISR-prolog: gem registre (32 cyklusser)
- ISR-hoveddel: lokaliser og ring til brugerregistreret funktion (13 cyklusser)
- read_pin: call digitalRead (5 cycles)
- digitalRead: find den relevante port og bit, der skal testes (41 cyklusser)
- digitalRead: læs I / O-port (1 cyklus)
Vi antager det bedste tilfælde med 4 cyklusser for
hardwired sekvens. Dette giver os en samlet latenstid på 99 cyklusser, udløbende 6,2 µs med et 16 MHz ur. I det følgende vil jeg udforske nogle tricks, der kan bruges til at sænke denne ventetid. De kommer i stigende rækkefølge af kompleksitet, men de har alle brug for, at vi på en eller anden måde graver ind i MCU'ens indvendige dele.
Brug direkte portadgang
Det åbenlyse første mål for at forkorte latenstiden er digitalRead () . Denne funktion giver en flot abstraktion til MCU-hardware, men den er ikke ineffektiv til tidskritisk arbejde. At slippe af med denne er faktisk trivielt: vi skal bare erstatte den med digitalReadFast ()
fra biblioteket digitalwritefast. Dette reducerer latenstiden næsten med det halve på bekostning af en smalldownload!
Nå, det var for let til at være sjovt, jeg vil hellere vise dig, hvordan du gør det på den hårde måde. Formålet er at få os i gang med ting på lavt niveau. Metoden kaldes " direkte portadgang " og er pænt dokumenteret på Arduino-referencen på siden på PortRegisters. På dette tidspunkt er det en god ide at downloade og kigge på ATmega328Pdatasheet. Dette dokument på 650 sider kan virke noget skræmmende ved første kig. Det er dog godt organiseret i sektioner, der er specifikke for hver af MCUperipherals og funktioner. Og vi behøver kun at kontrollere de sektioner, der er relevante for, hvad vi laver. I dette tilfælde er det sektionen med navnet I / O-porte . Her er et resumé af, hvad vi lærer af disse målinger:
- Arduino pin 2 kaldes faktisk PD2 (dvs. port D, bit 2) på AVR-chippen.
- Vi få hele porten D på én gang ved at læse et specielt MCU-register kaldet "PIND".
- Vi kontrollerer derefter bit nummer 2 ved at gøre en bitvis logisk og (C '&' operator) med
1 << 2
.
Så her er vores modificerede interrupt handler:
#define PIN_REG PIND // interrupt 0 er på AVR pin PD2 # definer PIN_BIT 2
/ * Afbryd handler. * / ugyldigt read_pin () {uint8_t sampled_pin = PIN_REG; // gør dette først! hvis (count_edges > = MAX_COUNT) returnerer; // vi er færdige count_edges ++; if (sampled_pin & (1 << PIN_BIT)) count_high ++;}
Nu vil vores handler læse I / O-registret, så snart det kaldes. Ventetid er 53 CPU-cyklusser. Dette enkle trick sparede os 46 cyklusser!
Skriv din egen ISR
Det næste mål for cyklus-trimning er INT0_vect ISR. Denne ISR er nødvendig for at levere funktionaliteten til attachInterrupt ()
: vi kan til enhver tid skifte interrupt-handlers under programudførelse. Men selvom det er rart at have, er dette ikke rigtig nyttigt til vores formål. Så i stedet for at få libcores ISR til at lokalisere og kalde vores interrupthandler, gemmer vi et par cyklusser ved at udskifte ISR med vores håndterer.
Dette er ikke så hårdt som det lyder. ISR'er kan skrives som normale funktioner, vi skal bare være opmærksomme på deres specifikke navne og definere dem ved hjælp af en speciel ISR ()
makro fra avr-libc. På dette tidspunkt ville det være godt at se på avr-libc's dokumentation om afbrydelser og i databladets sektion med navnet Eksterne afbrydelser . Her er kort opsummering:
- Vi er nødt til at skrive lidt i et specielt hardware-register kaldet EICRA ( Eksternt afbrydelsesregister A ) for at konfigurere afbrydelsen, der skal udløses på enhver ændring af pinværdien. Dette vil være bedone i
setup()
. - Vi er nødt til at skrive lidt i et andet hardware-register kaldet EIMSK ( Eksternt interrupt MaSK-register ) for at aktiver INT0interrupt. Dette gøres også i
setup()
. - Vi er nødt til at definere ISR med syntaksen
ISR (INT0_vect) {...}
.
Her er koden til ISR og setup ()
, alt andet er ikke ændret:
/ * Afbryd servicerutine for INT0. * / ISR (INT0_vect) {uint8_t sampled_pin = PIN_REG; // gør dette først! hvis (count_edges > = MAX_COUNT) returnerer; // vi er færdige count_edges ++; hvis (sampled_pin & (1 << PIN_BIT)) count_high ++;} ugyldig opsætning () {Serial.begin (9600); EICRA = 1 << ISC00; // fornem enhver ændring på INT0-pin EIMSK = 1 << INT0; // aktiver INT0 interrupt}
Dette kommer med en gratis bonus: da denne ISR er enklere end den, den erstatter, har den brug for færre registre for at udføre sit job, så er den registerbesparende prolog kortere . Nu er vi nede på en ventetid på 20 cykler. Ikke dårligt i betragtning af at vi startede tæt på 100!
På dette tidspunkt vil jeg sige, at vi er færdige. Mission fuldført. Whatfollows er kun for dem, der ikke er bange for at få deres hænder beskidte med en eller anden AVR-samling. Ellers kan du stoppe med at læse her, og tak for at du er kommet så langt.
Skriv en nøgen ISR
Stadig her? Godt! For at gå videre vil det være nyttigt at have mindst en meget grundlæggende idé om, hvordan montering fungerer, og at se på Inline AssemblerCookbook fra avr-libc-dokumentationen. På dette tidspunkt ser vores interrupt-indgangssekvens sådan ud:
- hardwired-sekvens (4 cyklusser)
- afbrydelsesvektor: spring til ISR (3 cyklusser)
- ISR-prolog: gem regs (12 cyklusser)
- første ting i ISR-kroppen: læs IO-porten (1 cyklus)
Hvis vi vil gøre det bedre, vi er nødt til at flytte læsningen af porten ind i prologen. Ideen er følgende: at læse PIND-registret vil klatre et CPU-register, så vi er nødt til at gemme mindst et register, før du gør det, men de andre registre kan vente. Vi har derefter brug for at skrive en brugerdefineret prolog, der læser I / O-porten lige efter at have gemt det første register. Du har allerede set i avr-libc-afbrydelsen
dokumentation (du har læst det, ikke?), at en ISR kan gøres nøgen , i hvilket tilfælde compileren ikke udsender nogen prolog eller epilog, så vi kan skrive vores egen brugerdefinerede version.
Problemet med denne tilgang er, at vi sandsynligvis ender med at skrive hele ISR i forsamling. Ikke en big deal, men jeg vil hellere have kompilatoren til at skrive de kedelige prologer og epiloger til mig. Så her er det beskidte trick: vi deler ISR i to dele:
- den første del vil være et kort forsamlingsfragment, der
- gemmer et enkelt register i stakken
- læs PIND i det register
- gem den værdi i en global variabel
- gendan registeret fra stakken
- spring til det andet del
- anden del vil være almindelig C-kode med compiler-genereret prolog og epilog
Vores tidligere INT0 ISR erstattes derefter af denne :
flygtig uint8_t sampled_pin; // dette er nu en global variabel / * Afbryd servicerutine for INT0. * / ISR (INT0_vect, ISR_NAKED) {asm flygtig ("skub r0 \ n" // gem register r0 "i r0,% [pin] \ n" // læs PIND i r0 "st sampled_pin, r0 \ n" // gem r0 i en global "pop r0 \ n" // gendan tidligere r0 "rjmp INT0_vect_part_2 \ n" // gå til del 2 :: [pin] "I" (_SFR_IO_ADDR (PIND)));} ISR (INT0_vect_part_2) { hvis (count_edges > = MAX_COUNT) returnerer; // vi er færdige count_edges ++; hvis (sampled_pin & (1 << PIN_BIT)) count_high ++;}
Her bruger vi makroen ISR () til at have kompilatorinstrumentet INT0_vect_part_2
med den krævede prolog og epilog. Compileren klager over, at "'INT0_vect_part_2' ser ud til at være en forkert stavet signalhåndterer", men advarslen kan ignoreres sikkert. Nu har ISR en enkelt 2-cyklus instruktion før den faktiske portlæsning og summen
latenstid er kun 10 cyklusser.
Brug GPIOR0-registret
Hvad hvis vi kunne have et register reserveret til dette specifikke job? Derefter behøver vi ikke gemme noget, før vi læser havnen. Vi kan faktisk bede kompilatoren om at binde en global variabel til aregister. Dette vil dog kræve, at vi kompilerer hele Arduino-kernen og libc for at sikre, at registret altid er reserveret. Ikke rigtig praktisk. På den anden side har ATmega328P tilfældigvis treregistreringer, der ikke bruges af compileren eller noget bibliotek, og som er tilgængelige til opbevaring af det, vi ønsker. De kaldes GPIOR0, GPIOR1og GPIOR2 ( I / O-registre til almindeligt formål ). Selvom de er kortlagt i I / O-adresseområdet på MCU'en, er disse faktisk ikke I / Oregistrere: de er bare almindelig hukommelse, som tre byte RAM, der på en eller anden måde gik tabt i en bus og endte i det forkerte adresseområde. Disse er ikke så kapable som de interne CPU-registre, og vi kan ikke kopierePIND til en af disse med i
instruktion. GPIOR0 er dog interessant, fordi den er bitadresserbar ligesom PIND. Dette gør det muligt for os at overføre oplysningerne uden at spærre nogen intern CPU-registrering.
Her er tricket: vi sørger for, at GPIOR0 oprindeligt er nul (det ryddes faktisk af hardware ved opstartstidspunktet), så bruger vi sbic
(Spring næste instruktion over, hvis noget Bit i noget I / o-register er klart) og sbi
(Indstil til 1 noget Bit i noget I / o-register) instruktionerne følger:
sbic PIND, 2; spring over følgende, hvis bit 2 i PIND er clearsbi GPIOR0, 0; indstillet til 1 bit 0 i GPIOR0
På denne måde ender GPIOR0 med at være 0 eller 1 afhængigt af den bit, vi ønskede at læse fra PIND. Sbic-instruktionen tager 1 eller 2 cyklusser at udføre, afhængigt af om tilstanden er falsk eller sand. Det er klart, at der åbnes PINDbit i den første cyklus. I denne nye version af koden er
global variabel sampled_pin
er ikke nyttig mere, da den grundlæggende erstattes af GPIOR0:
/ * Afbryd servicerutine til INT0. * / ISR (INT0_vect, ISR_NAKED) {asm flygtig ("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 ++; hvis (GPIOR0) count_high ++; } GPIOR0 = 0;}
Det skal bemærkes, at GPIOR0 altid skal nulstilles i ISR.
Nu er samplingen af PIND I / O-registret er den allerførste ting, der er gjort inden for ISR. Samlet ventetid er 8 cyklusser. Dette handler om det bedste, vi cando, før vi bliver farvet med frygtelig syndige kludges. Dette er igen en god mulighed for at stoppe læsningen ...
Sæt den tidskritiske kode i vektortabellen
For dem der stadig er her, her er vores nuværende situation:
- hardwired-sekvens (4 cyklusser)
- afbrydelsesvektor: spring til ISR (3 cyklusser)
- ISR-krop: læs IO-porten (i 1. cyklus)
Der er åbenbart lidt plads til forbedring. Den eneste måde, vi kunne forkorte latenstiden på dette tidspunkt er ved at erstatte afbrydelsesvektoren selv med vores kode. Vær advaret om, at dette skal være uhyre distastfuldt for enhver, der værdsætter rent softwaredesign. Men det er muligt, og jeg vil vise dig hvordan.
Layoutet af ATmega328P vektortabellen kan findes i databladet, sektion Interrupts , underafsnit Interrupt Vectors in ATmega328 andATmega328P . Eller ved at adskille ethvert program til denne chip. Sådan ser det ud. Jeg bruger avr-gcc og avr-libc's konventioner (__initis-vektor 0, adresser er i byte), som adskiller sig fra Atmel's.
adresse │ instruktion Kommentar
─ ─ - ─ ─ ┼ ─ ─ ─ ─ x │ jmp __init │ nulstillingsvektor 0x0004 │ jmp __vector_1 │ aka INT0_vect 0x0008 │ jmp __vector_2 │ aka INT1_vect 0x000c │ jmp __vector_3 │ aka PCINT0_vect ... 0x0064 │ jmp __vector_25 │ aka \ SPect_M har en 4-byte slot, fyldt med en enkelt jmp
instruktion. Dette er en 32-bit instruktion, i modsætning til de fleste AVR-instruktioner, der er 16-bit. Men en 32-bit slot er for lille til at rumme den første del af vores ISR: vi kan tilpasse instruktionerne sbic
og sbi
, men ikke rjmp
.Hvis vi gør det, ender vektortabellen med at se sådan ud: adresse │ instruktion │ kommentar────────── ───────────────── 0── 0xxmpmpmpmpmp mpmp in in ininit │ reset vektor 0x0004 │ sbic PIND, 2 │ den første del ... 0x0006 │ sbi GPIOR0, 0 │ ... af vores ISR 0x0008 │ jmp __vector_2 │ aka INT1_vect 0x000c │ jmp __vector_3 │ aka PCINT0_vect ... 0x0064 │ jmp __vector_25 │ aka SPM_ >
Når INT0 udløses, læses PIND, den relevante bit kopieres til GPIOR0, og derefter falder udførelsen videre til næste vektor. Derefter kaldes ISR for INT1 i stedet for ISR til INT0. Dette er uhyggeligt, men da vi alligevel ikke bruger INT1, vil vi bare "kapre" dens vektor for at servicere INT0.
Nu skal vi bare skrive vores egen brugerdefinerede vektortabel for at tilsidesætte standardindstillingen. Det viser sig, at det ikke er så let. Standardvektortabellen leveres af avr-libc-distributionen, i en objektfil kaldet crtm328p.o, der automatisk linkes til ethvert program, vi bygger. I modsætning til bibliotekkode er objektfilkoden ikke beregnet til at blive tilsidesat: forsøg på at gøre det vil give en linkerfejl om, at tabellen defineres to gange. Dette betyder, at vi er nødt til at erstatte hele crtm328p.o med vores tilpassede version. En mulighed er at downloade den fulde avr-libc sourcecode, gør vores
tilpassede ændringer i gcrt1.S, bygg derefter dette som en brugerdefineret libc.
Her gik jeg for en lettere, alternativ tilgang. Jeg skrev en customcrt.S, som er en forenklet version af originalen fra avr-libc. Det mangler et par sjældent anvendte funktioner, som evnen til at definere en "catchall" ISR eller at kunne afslutte programmet (dvs. fryse theArduino) ved at ringe til exit ()
. Her er koden. Jeg trimmede gentagelsesdelen af vektortabellen for at minimere rulning:
#include <avr / io.h>.weak __heap_end.set __heap_end, 0. makrovektornavn. svagt \ navn. sæt \ navn, __vektorer jmp \ navn.endm.afsnit .vektorer__vektorer: jmp __init sbic _SFR_IO_ADDR (PIND), 2; disse 2 linjer ... sbi _SFR_IO_ADDR (GPIOR0), 0; ... udskift vector_1 vektor __vector_2 vektor __vector_3 [... og så videre indtil ...] vektor __vector_25.sektion .init2__init: clr r1 ud _SFR_IO_ADDR (SREG), r1 ldi r28, lo8 (RAMEND) ldi r29, hi8 (RAMEND ) ud _SFR_IO_ADDR (SPL), r28 ud _SFR_IO_ADDR (SPH), r29.afsnit .init9 jmp main
Det kan kompileres med følgende kommandolinje:
avr-gcc -c -mmcu = atmega328p fjollet-crt.S
Skitsen er identisk med den foregående bortset fra at der ikke er noINT0_vect , og INT0_vect_part_2 erstattes af INT1_vect:
/ * Afbryd servicerutine for INT1 kapret til service INT0. * / ISR (INT1_vect) {if (count_edges < MAX_COUNT) {count_edges ++; hvis (GPIOR0) count_high ++; } GPIOR0 = 0;}
For at kompilere skitsen har vi brug for en brugerdefineret kompileringskommando. Hvis du har fulgt indtil videre, ved du sandsynligvis, hvordan du kompilerer fra kommandolinjen. Du skal eksplicit anmode dumt-crt.o om at blive linket til dit program, og tilføj indstillingen -nostartfiles
for at undgå at linke i originalen
crtm328p.o.
Nu er læsningen af I / O-porten den allerførste instruktion, der udføres efter afbrydelsesudløserne. Jeg testede denne version ved at sende den korte impulser fra en anden Arduino, og den kan (dog ikke pålideligt) fange det høje niveau af impulser så korte som 5 cyklusser. Der er ikke noget mere, vi kan gøre for at forkorte afbrydelsesforsinkelsen på denne hardware.