Saya menggunakan fungsi interupsi untuk mengisi array dengan nilai yang diterima dari 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;
}
Masalahnya adalah bahwa ketika saya mencetak test_array
ada nilai-nilai seperti: 111
atau 000
.
Seperti yang saya pahami, jika saya menggunakan CHANGE
opsi dalam attachInterrupt()
fungsi, maka urutan data harus selalu 0101010101
tanpa berulang.
Data berubah cukup cepat karena berasal dari modul radio.
arduino-uno
c
isr
pengguna277820
sumber
sumber
pin
,x
dantest_array
definisi, dan jugaloop()
metode; itu akan memungkinkan kita untuk melihat apakah ini bisa menjadi masalah konkurensi ketika mengakses variabel yang diubah olehtest_func
.if (digitalRead(pin) == HIGH) ... else ...;
atau, lebih baik lagi, ini single-line ISR:test_array[x++] = digitalRead(pin);
.Jawaban:
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: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 terdaftar
attachInterrupt()
, dan itu akan memanggil penangan itu, yang merupakanread_pin()
fungsi kita di atas. Fungsi kami kemudian akan memanggildigitalRead()
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:
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 dengandigitalReadFast()
, 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:
1 << 2
.Jadi, inilah penangan interrupt kami yang dimodifikasi:
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:setup()
.setup()
.ISR(INT0_vect) { ... }
.Berikut adalah kode untuk ISR dan yang
setup()
lainnya tidak berubah: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:
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:
INT0 ISR kami sebelumnya kemudian digantikan oleh ini:
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) dansbi
( Setel ke 1 bit dalam beberapa instruksi I / o register) sebagai berikut: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: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:
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.
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 memuatsbic
dansbi
instruksinya, tetapi tidakrjmp
. Jika kita melakukan itu, tabel vektor akhirnya tampak seperti ini: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:Itu dapat dikompilasi dengan baris perintah berikut:
Sketsa itu identik dengan yang sebelumnya kecuali bahwa tidak ada INT0_vect, dan INT0_vect_part_2 diganti oleh INT1_vect:
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.
sumber
Interupsi sedang diatur untuk menjalankan perubahan, dan test_func Anda ditetapkan sebagai Interrupt Service Routine (ISR), dipanggil ke layanan yang mengganggu. ISR kemudian mencetak nilai input.
Pada pandangan pertama Anda akan mengharapkan output menjadi seperti yang Anda katakan, dan bergantian set posisi terendah tinggi, karena hanya sampai ke ISR pada perubahan.
Tetapi yang kami lewatkan adalah bahwa ada sejumlah waktu yang dibutuhkan CPU untuk melayani interupsi dan bercabang ke ISR. Selama waktu ini tegangan pada pin mungkin telah berubah lagi. Terutama jika pin tidak distabilkan oleh perangkat keras yang terpental atau serupa. Karena interupsi sudah ditandai dan belum diservis, perubahan tambahan ini (atau banyak dari mereka, karena level pin dapat berubah sangat cepat relatif terhadap kecepatan clock jika memiliki kapasitansi parasit rendah) akan terlewatkan.
Jadi pada dasarnya tanpa beberapa bentuk de-bouncing, kami tidak memiliki jaminan bahwa ketika input berubah, dan interupsi ditandai untuk diperbaiki, bahwa input akan tetap pada nilai yang sama ketika kita membaca nilainya di ISR.
Sebagai contoh umum, ATmega328 Datasheet yang digunakan pada Arduino Uno merinci waktu interupsi di bagian 6.7.1 - "Interrupt Response Time". Ini menyatakan untuk mikrokontroler ini waktu minimum untuk bercabang ke ISR untuk servis adalah 4 siklus clock, tetapi bisa lebih (tambahan jika menjalankan instruksi multi-siklus saat interupsi atau 8 + waktu bangun tidur jika MCU sedang tidur.)
Seperti @EdgarBonet disebutkan dalam komentar, pin juga bisa berubah selama eksekusi ISR. Karena ISR membaca dari pin dua kali, itu tidak akan menambah apa pun ke test_array jika menemui RENDAH pada bacaan pertama dan TINGGI pada yang kedua. Tetapi x masih akan bertambah, membiarkan slot di array tidak berubah (mungkin sebagai data yang tidak diinisialisasi tergantung pada apa yang dilakukan pada array sebelumnya).
Satu baris ISR-nya
test_array[x++] = digitalRead(pin);
adalah solusi sempurna untuk ini.sumber