Sebagai semacam prolog untuk jawaban yang terlalu panjang ini ...
Pertanyaan ini membuat saya sangat terpesona dengan masalah latensi interupsi, sampai tidak bisa tidur dalam menghitung siklus, bukan domba. Saya menulis respons ini lebih untuk membagikan temuan saya daripada hanya menjawab pertanyaan: sebagian besar materi ini mungkin sebenarnya tidak pada tingkat yang sesuai untuk jawaban yang tepat. Saya harap ini akan bermanfaat, bagi pembaca yang datang ke sini untuk mencari solusi untuk masalah latensi. Beberapa bagian pertama diharapkan bermanfaat bagi khalayak luas, termasuk poster aslinya. Kemudian, itu menjadi berbulu di sepanjang jalan.
Clayton Mills sudah menjelaskan dalam jawabannya bahwa ada beberapa latensi dalam menanggapi interupsi. Di sini saya akan fokus pada mengukur latensi (yang sangat besar ketika menggunakan perpustakaan Arduino), dan pada cara untuk meminimalkannya. Sebagian besar yang berikut ini khusus untuk perangkat keras Arduino Uno dan papan serupa.
Meminimalkan latensi interupsi pada Arduino
(atau cara mendapatkan dari 99 hingga 5 siklus)
Saya akan menggunakan pertanyaan asli sebagai contoh yang berfungsi, dan menyatakan kembali masalah dalam hal interupsi latensi. Kami memiliki beberapa peristiwa eksternal yang memicu interupsi (di sini: INT0 pada perubahan pin). Kita perlu mengambil tindakan ketika interupsi dipicu (di sini: baca input digital). Masalahnya adalah: ada beberapa penundaan antara interupsi yang dipicu dan kami mengambil tindakan yang sesuai. Kami menyebut penundaan ini " interupsi latensi ". Latensi yang panjang merugikan dalam banyak situasi. Dalam contoh khusus ini, sinyal input dapat berubah selama penundaan, dalam hal ini kita mendapatkan pembacaan yang salah. Tidak ada yang bisa kita lakukan untuk menghindari keterlambatan: itu intrinsik dengan cara interupsi bekerja. Namun, kita dapat mencoba membuatnya sesingkat mungkin, yang semoga dapat meminimalkan konsekuensi buruknya.
Hal nyata pertama yang dapat kita lakukan adalah mengambil tindakan kritis waktu, di dalam interrupt handler, sesegera mungkin. Ini berarti memanggil
digitalRead()
sekali (dan hanya sekali) di awal pawang. Berikut adalah versi nol dari program yang akan kami bangun:
#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
}
Saya menguji program ini, dan versi berikutnya, dengan mengirimkannya kereta pulsa dengan lebar yang berbeda-beda. Ada jarak yang cukup di antara pulsa untuk memastikan bahwa tidak ada tepi yang terlewatkan: bahkan jika tepi jatuh diterima sebelum interupsi sebelumnya dilakukan, permintaan interupsi kedua akan ditunda dan akhirnya diservis. Jika pulsa lebih pendek dari latensi interupsi, program membaca 0 pada kedua sisi. Jumlah level TINGGI yang dilaporkan kemudian adalah persentase pulsa yang dibaca dengan benar.
Apa yang terjadi ketika interupsi dipicu?
Sebelum mencoba memperbaiki kode di atas, kita akan melihat peristiwa yang terjadi segera setelah interupsi dipicu. Bagian perangkat keras dari cerita ini diceritakan oleh dokumentasi Atmel. Bagian perangkat lunak, dengan membongkar biner.
Sebagian besar waktu, interupsi yang masuk dilayani segera. Akan tetapi, mungkin terjadi bahwa MCU (artinya "mikrokontroler") ada di tengah-tengah beberapa tugas yang sangat penting waktu, di mana servis interupsi dinonaktifkan. Ini biasanya terjadi ketika sudah melayani gangguan lain. Ketika ini terjadi, permintaan interupsi yang masuk ditunda dan dilayani hanya ketika bagian yang kritis waktu itu dilakukan. Situasi ini sulit dihindari sepenuhnya, karena ada beberapa bagian kritis di perpustakaan inti Arduino (yang akan saya sebut " libcore"sebagai berikut). Untungnya, bagian-bagian ini pendek dan hanya berjalan sesekali. Jadi, sebagian besar waktu, permintaan interupsi kami akan segera dilayani. Berikut ini, saya akan berasumsi bahwa kami tidak peduli dengan beberapa orang itu contoh saat ini tidak terjadi.
Kemudian, permintaan kami segera dilayani. Ini masih melibatkan banyak hal yang bisa memakan waktu cukup lama. Pertama, ada urutan bawaan. MCU akan selesai menjalankan instruksi saat ini. Untungnya, sebagian besar instruksi adalah siklus tunggal, tetapi beberapa dapat memakan waktu hingga empat siklus. Kemudian, MCU membersihkan bendera internal yang menonaktifkan layanan interupsi lebih lanjut. Ini dimaksudkan untuk mencegah interupsi bersarang. Kemudian, PC disimpan ke dalam tumpukan. Tumpukan adalah area RAM yang disediakan untuk penyimpanan sementara semacam ini. PC (artinya " Program Counter") adalah register internal yang menyimpan alamat instruksi selanjutnya yang akan dieksekusi MCU. Inilah yang memungkinkan MCU untuk mengetahui apa yang harus dilakukan selanjutnya, dan menyimpannya adalah penting karena harus dipulihkan agar dapat program untuk melanjutkan dari tempat itu terputus. PC kemudian dimuat dengan alamat bawaan khusus untuk permintaan yang diterima, dan ini adalah akhir dari urutan bawaan, sisanya dikendalikan oleh perangkat lunak.
MCU sekarang menjalankan instruksi dari alamat bawaan tersebut. Instruksi ini disebut " vektor interupsi ", dan umumnya merupakan instruksi "lompatan" yang akan membawa kita ke rutinitas khusus yang disebut ISR (" Interrupt Service Routine "). Dalam hal ini, ISR disebut "__vector_1", alias "INT0_vect", yang keliru karena merupakan ISR, bukan vektor. ISR khusus ini berasal dari libcore. Seperti ISR apa pun, ini dimulai dengan prolog yang menyimpan banyak register CPU internal pada stack. Ini akan memungkinkannya untuk menggunakan register-register itu dan, ketika sudah selesai, kembalikan ke nilai sebelumnya agar tidak mengganggu program utama. Kemudian, ia akan mencari penangan interrupt yang terdaftarattachInterrupt()
, dan itu akan memanggil penangan itu, yang merupakan read_pin()
fungsi kita di atas. Fungsi kami kemudian akan memanggil digitalRead()
dari libcore. digitalRead()
akan melihat beberapa tabel untuk memetakan nomor port Arduino ke port I / O perangkat keras yang harus dibaca dan nomor bit yang terkait untuk diuji. Ini juga akan memeriksa apakah ada saluran PWM pada pin yang perlu dinonaktifkan. Ini kemudian akan membaca port I / O ... dan kita selesai. Yah, kita tidak benar-benar selesai melayani interupsi, tetapi tugas kritis waktu (membaca port I / O) selesai, dan itu yang penting ketika kita melihat latensi.
Berikut ini adalah ringkasan singkat dari semua hal di atas, bersama dengan penundaan terkait dalam siklus CPU:
- urutan bawaan: selesaikan instruksi saat ini, cegah gangguan terputus, simpan PC, muat alamat vektor (≥ 4 siklus)
- mengeksekusi interrupt vector: lompat ke ISR (3 siklus)
- Prolog ISR: simpan register (32 siklus)
- Badan utama ISR: temukan dan panggil fungsi yang didaftarkan pengguna (13 siklus)
- read_pin: panggil digitalRead (5 siklus)
- digitalRead: temukan port dan bit yang relevan untuk diuji (41 siklus)
- digitalRead: baca port I / O (1 siklus)
Kami akan mengasumsikan skenario kasus terbaik, dengan 4 siklus untuk urutan bawaan. Ini memberi kami latensi total 99 siklus, atau sekitar 6,2 μs dengan clock 16 MHz. Berikut ini, saya akan mengeksplorasi beberapa trik yang dapat digunakan untuk menurunkan latensi ini. Mereka datang secara kasar dengan urutan kompleksitas yang semakin meningkat, tetapi mereka semua membutuhkan kita untuk menggali bagian dalam MCU.
Gunakan akses port langsung
Target pertama yang jelas untuk memperpendek latensi adalah digitalRead()
. Fungsi ini memberikan abstraksi yang bagus untuk perangkat keras MCU, tetapi terlalu tidak efisien untuk pekerjaan yang membutuhkan waktu. Menyingkirkan yang ini sebenarnya sepele: kita hanya harus menggantinya dengan digitalReadFast()
, dari
perpustakaan digitalwritefast . Ini memotong latensi hampir setengahnya dengan biaya unduhan kecil!
Yah, itu terlalu mudah untuk bersenang-senang, saya lebih suka menunjukkan kepada Anda bagaimana melakukannya dengan cara yang sulit. Tujuannya adalah untuk membuat kita memulai hal-hal tingkat rendah. Metode ini disebut " akses port langsung " dan didokumentasikan dengan baik pada referensi Arduino di halaman tentang Port Register . Pada titik ini, sebaiknya unduh dan lihat lembar data ATmega328P . Dokumen setebal 650 halaman ini mungkin terlihat agak menakutkan pada pandangan pertama. Namun demikian, diatur dengan baik ke dalam bagian-bagian khusus untuk masing-masing periferal dan fitur MCU. Dan kita hanya perlu memeriksa bagian yang relevan dengan apa yang kita lakukan. Dalam hal ini, itu adalah bagian bernama
I / O port . Berikut adalah ringkasan dari apa yang kami pelajari dari bacaan tersebut:
- Pin Arduino 2 sebenarnya disebut PD2 (yaitu port D, bit 2) pada chip AVR.
- Kami mendapatkan seluruh port D sekaligus dengan membaca register MCU khusus yang disebut "PIND".
- Kami kemudian memeriksa bit nomor 2 dengan melakukan bitwise logical dan (operator C '&') dengan
1 << 2
.
Jadi, inilah penangan interrupt kami yang dimodifikasi:
#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++;
}
Sekarang, pawang kami akan membaca register I / O segera setelah dipanggil. Latensi adalah 53 siklus CPU. Trik sederhana ini menyelamatkan kita dari 46 siklus!
Tulis ISR Anda sendiri
Target berikutnya untuk pemangkasan siklus adalah INT0_vect ISR. ISR ini diperlukan untuk menyediakan fungsionalitas attachInterrupt()
: kita dapat mengubah penangan interupsi kapan saja selama eksekusi program. Namun, meskipun menyenangkan untuk dimiliki, ini tidak benar-benar berguna untuk tujuan kita. Dengan demikian, alih-alih menempatkan ISR libcore dan memanggil penangan interrupt kami, kami akan menghemat beberapa siklus dengan mengganti ISR dengan penangan kami.
Ini tidak sesulit kedengarannya. ISR dapat ditulis seperti fungsi normal, kita hanya harus mengetahui nama spesifiknya, dan mendefinisikannya menggunakan ISR()
makro khusus dari avr-libc. Pada titik ini akan lebih baik untuk melihat dokumentasi avr-libc tentang interupsi , dan pada bagian lembar data bernama Interupsi Eksternal . Berikut ini ringkasan singkatnya:
- Kita harus menulis sedikit dalam register perangkat keras khusus yang disebut EICRA ( External Interrupt Control Register A ) untuk mengkonfigurasi interupsi yang akan dipicu pada setiap perubahan nilai pin. Ini akan dilakukan di
setup()
.
- Kita harus menulis sedikit di register perangkat keras lain yang disebut EIMSK ( Eksternal Mauping register ) untuk mengaktifkan interupsi INT0. Ini juga akan dilakukan di
setup()
.
- Kita harus mendefinisikan ISR dengan sintaksisnya
ISR(INT0_vect) { ... }
.
Berikut adalah kode untuk ISR dan yang setup()
lainnya tidak berubah:
/* 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
}
Ini datang dengan bonus gratis: karena ISR ini lebih sederhana daripada yang diganti, ISR membutuhkan lebih sedikit register untuk melakukan tugasnya, maka prolog hemat register lebih pendek. Sekarang kita ke latensi 20 siklus. Tidak buruk mengingat kami mulai mendekati 100!
Pada titik ini saya akan mengatakan kita sudah selesai. Misi selesai. Berikut ini hanya untuk mereka yang tidak takut mengotori tangan dengan beberapa perakitan AVR. Kalau tidak, Anda bisa berhenti membaca di sini, dan terima kasih sudah membaca sejauh ini.
Tuliskan ISR telanjang
Masih di sini? Baik! Untuk melangkah lebih jauh, akan sangat membantu untuk memiliki setidaknya beberapa ide yang sangat mendasar tentang cara kerja perakitan, dan untuk melihat Inline Assembler Cookbook
dari dokumentasi avr-libc. Pada titik ini, urutan entri interupsi kami terlihat seperti ini:
- urutan bawaan (4 siklus)
- interrupt vector: lompat ke ISR (3 siklus)
- Prolog ISR: save regs (12 cycle)
- hal pertama dalam tubuh ISR: baca port IO (1 siklus)
Jika kita ingin melakukan yang lebih baik, kita harus memindahkan pembacaan port ke prolog. Idenya adalah sebagai berikut: membaca register PIND akan mengalahkan satu register CPU, jadi kita perlu menyimpan setidaknya satu register sebelum melakukan itu, tetapi register lain dapat menunggu. Kami kemudian perlu menulis prolog khusus yang membaca port I / O setelah menyimpan register pertama. Anda telah melihat dalam dokumentasi interrupt avr-libc (Anda telah membacanya, kan?) Bahwa ISR dapat dibuat
telanjang , dalam hal ini kompiler tidak akan memancarkan prolog atau epilog, yang memungkinkan kami untuk menulis versi kustom kami sendiri.
Masalah dengan pendekatan ini adalah bahwa kita mungkin akan berakhir menulis seluruh ISR dalam pertemuan. Bukan masalah besar, tapi saya lebih suka kompiler menulis prolog dan epilog membosankan untuk saya. Jadi, inilah trik kotornya: kita akan membagi ISR menjadi dua bagian:
- bagian pertama akan menjadi bagian perakitan pendek yang akan
- simpan satu register ke stack
- baca PIND ke dalam register itu
- menyimpan nilai itu ke dalam variabel global
- pulihkan register dari tumpukan
- lompat ke bagian kedua
- bagian kedua adalah kode C reguler dengan prolog dan epilog yang dibuat oleh compiler
INT0 ISR kami sebelumnya kemudian digantikan oleh ini:
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++;
}
Di sini kita menggunakan makro ISR () untuk memiliki instrumen kompiler
INT0_vect_part_2
dengan prolog dan epilog yang diperlukan. Kompiler akan mengeluh bahwa "'INT0_vect_part_2' tampaknya menjadi pengendali sinyal yang salah eja", tetapi peringatan itu dapat diabaikan dengan aman. Sekarang ISR memiliki instruksi 2 siklus tunggal sebelum port aktual dibaca, dan total latensi hanya 10 siklus.
Gunakan daftar GPIOR0
Bagaimana jika kita dapat memiliki register yang disediakan untuk pekerjaan khusus ini? Maka, kita tidak perlu menyimpan apa pun sebelum membaca port. Kita sebenarnya bisa meminta kompiler untuk mengikat variabel global ke register . Ini, bagaimanapun, akan mengharuskan kita untuk mengkompilasi ulang seluruh inti Arduino dan libc untuk memastikan register selalu dicadangkan. Sangat tidak nyaman. Di sisi lain, ATmega328P memiliki tiga register yang tidak digunakan oleh kompiler atau pustaka apa pun, dan tersedia untuk menyimpan apa pun yang kita inginkan. Mereka disebut GPIOR0, GPIOR1 dan GPIOR2 ( General I / O Register ). Meskipun mereka dipetakan di ruang alamat I / O MCU, ini sebenarnya tidakRegister I / O: mereka hanya memori biasa, seperti tiga byte RAM yang entah bagaimana tersesat di bus dan berakhir di ruang alamat yang salah. Ini tidak mampu seperti register CPU internal, dan kami tidak dapat menyalin PIND ke salah satu dari ini dengan in
instruksi. GPIOR0 menarik, karena itu bit-addressable , seperti PIND. Ini akan memungkinkan kami untuk mentransfer informasi tanpa mengganggu register CPU internal apa pun.
Inilah triknya: kita akan memastikan bahwa GPIOR0 awalnya nol (sebenarnya dihapus oleh perangkat keras saat boot), maka kita akan menggunakan
sbic
(Lewati instruksi berikutnya jika beberapa Bit dalam beberapa register I / o Jelas) dan sbi
( Setel ke 1 bit dalam beberapa instruksi I / o register) sebagai berikut:
sbic PIND, 2 ; skip the following if bit 2 of PIND is clear
sbi GPIOR0, 0 ; set to 1 bit 0 of GPIOR0
Dengan cara ini, GPIOR0 akan menjadi 0 atau 1 tergantung pada bit yang ingin kita baca dari PIND. Instruksi sbic membutuhkan 1 atau 2 siklus untuk dijalankan tergantung pada apakah kondisinya salah atau benar. Jelas, bit PIND diakses pada siklus pertama. Dalam versi kode yang baru ini, variabel global sampled_pin
tidak berguna lagi, karena pada dasarnya digantikan oleh 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;
}
Perlu dicatat bahwa GPIOR0 harus selalu diatur ulang di ISR.
Sekarang, sampling dari register PIND I / O adalah hal pertama yang dilakukan di dalam ISR. Total latensi adalah 8 siklus. Ini adalah yang terbaik yang bisa kita lakukan sebelum ternoda oleh lumpur yang sangat berdosa. Sekali lagi ini adalah kesempatan bagus untuk berhenti membaca ...
Masukkan kode waktu-kritis dalam tabel vektor
Bagi mereka yang masih di sini, inilah situasi kita saat ini:
- urutan bawaan (4 siklus)
- interrupt vector: lompat ke ISR (3 siklus)
- Badan ISR: baca port IO (pada siklus 1)
Jelas ada sedikit ruang untuk perbaikan. Satu-satunya cara kita dapat mempersingkat latensi pada titik ini adalah dengan mengganti vektor interupsi itu sendiri dengan kode kita. Berhati-hatilah bahwa ini harus sangat tidak disukai bagi siapa pun yang menghargai desain perangkat lunak yang bersih. Tapi itu mungkin, dan saya akan menunjukkan caranya.
Tata letak tabel vektor ATmega328P dapat ditemukan di lembar data, bagian Interupsi , subbagian Interrupt Vektor di ATmega328 dan ATmega328P . Atau dengan membongkar program apa pun untuk chip ini. Berikut ini tampilannya. Saya menggunakan konvensi avr-gcc dan avr-libc (__init adalah vektor 0, alamat dalam byte) yang berbeda dari 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
Setiap vektor memiliki slot 4-byte, diisi dengan satu jmp
instruksi. Ini adalah instruksi 32-bit, tidak seperti kebanyakan instruksi AVR yang 16-bit. Tetapi slot 32-bit terlalu kecil untuk menampung bagian pertama dari ISR kami: kami dapat memuat sbic
dan sbi
instruksinya, tetapi tidak rjmp
. Jika kita melakukan itu, tabel vektor akhirnya tampak seperti ini:
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
Ketika INT0 menyala, PIND akan dibaca, bit yang relevan akan disalin ke GPIOR0, dan kemudian eksekusi akan jatuh ke vektor berikutnya. Kemudian, ISR untuk INT1 akan dipanggil, bukan ISR untuk INT0. Ini menyeramkan, tetapi karena kita tidak menggunakan INT1, kita hanya akan "membajak" vektornya untuk melayani INT0.
Sekarang, kita hanya perlu menulis tabel vektor kustom kita sendiri untuk menimpa yang default. Ternyata itu tidak mudah. Tabel vektor default disediakan oleh distribusi avr-libc, dalam file objek bernama crtm328p.o yang secara otomatis ditautkan dengan program apa pun yang kami buat. Tidak seperti kode perpustakaan, kode objek-file tidak dimaksudkan untuk diganti: mencoba melakukan itu akan memberikan kesalahan linker tentang tabel yang didefinisikan dua kali. Ini berarti kami harus mengganti seluruh crtm328p.o dengan versi khusus kami. Salah satu opsi adalah mengunduh kode sumber avr-libc penuh , lakukan modifikasi khusus kami di
gcrt1.S , lalu buat ini sebagai libc khusus.
Di sini saya mencari pendekatan alternatif yang lebih ringan. Saya menulis custom crt.S, yang merupakan versi asli dari avr-libc yang disederhanakan. Ini tidak memiliki beberapa fitur yang jarang digunakan, seperti kemampuan untuk mendefinisikan "catch all" ISR, atau untuk dapat menghentikan program (yaitu membekukan Arduino) dengan menelepon exit()
. Ini kodenya. Saya memangkas bagian berulang tabel vektor untuk meminimalkan bergulir:
#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
Itu dapat dikompilasi dengan baris perintah berikut:
avr-gcc -c -mmcu=atmega328p silly-crt.S
Sketsa itu identik dengan yang sebelumnya kecuali bahwa tidak ada INT0_vect, dan INT0_vect_part_2 diganti oleh 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;
}
Untuk mengkompilasi sketsa, kita memerlukan perintah kompilasi khusus. Jika Anda telah mengikuti sejauh ini, Anda mungkin tahu cara mengkompilasi dari baris perintah. Anda harus secara eksplisit meminta konyol-crt.o untuk ditautkan ke program Anda, dan menambahkan -nostartfiles
opsi untuk menghindari menautkan di crtm328p.o asli.
Sekarang, pembacaan port I / O adalah instruksi pertama yang dijalankan setelah pemicu interupsi. Saya menguji versi ini dengan mengirimkannya pulsa pendek dari Arduino lain, dan dapat menangkap (walaupun tidak andal) pulsa tingkat tinggi sesingkat 5 siklus. Tidak ada lagi yang bisa kita lakukan untuk mempersingkat latensi interupsi pada perangkat keras ini.