Gangguan Arduino (perubahan pin)

8

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_arrayada nilai-nilai seperti: 111atau 000.

Seperti yang saya pahami, jika saya menggunakan CHANGEopsi dalam attachInterrupt()fungsi, maka urutan data harus selalu 0101010101tanpa berulang.

Data berubah cukup cepat karena berasal dari modul radio.

pengguna277820
sumber
1
Interupsi tidak menghilangkan tombol. Apakah Anda menggunakan debouncing perangkat keras?
Ignacio Vazquez-Abrams
Silakan kirim kode lengkap, termasuk pin, xdan test_arraydefinisi, dan juga loop()metode; itu akan memungkinkan kita untuk melihat apakah ini bisa menjadi masalah konkurensi ketika mengakses variabel yang diubah oleh test_func.
jfpoilpret
2
Anda tidak boleh digitalRead () dua kali di ISR: pikirkan apa yang akan terjadi jika Anda mendapatkan RENDAH pada panggilan pertama dan TINGGI pada yang kedua. Sebaliknya, if (digitalRead(pin) == HIGH) ... else ...;atau, lebih baik lagi, ini single-line ISR: test_array[x++] = digitalRead(pin);.
Edgar Bonet
@ EdgarBonet bagus! Beri +1 pada komentar itu. Saya harap Anda tidak keberatan saya telah menambahkan sesuatu ke jawaban saya untuk memasukkan apa yang Anda sebutkan di sini. Juga jika Anda memutuskan untuk memberikan jawaban Anda sendiri termasuk detail ini maka saya akan menghapus tambahan saya dan memberikan suara Anda sehingga Anda mendapatkan perwakilan untuk itu.
Clayton Mills
@Clayton Mills: Saya sedang menyiapkan jawaban (terlalu panjang dan sedikit singgung), tetapi Anda dapat menyimpan hasil edit Anda, tidak masalah bagi saya.
Edgar Bonet

Jawaban:

21

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:

  1. urutan bawaan: selesaikan instruksi saat ini, cegah gangguan terputus, simpan PC, muat alamat vektor (≥ 4 siklus)
  2. mengeksekusi interrupt vector: lompat ke ISR (3 siklus)
  3. Prolog ISR: simpan register (32 siklus)
  4. Badan utama ISR: temukan dan panggil fungsi yang didaftarkan pengguna (13 siklus)
  5. read_pin: panggil digitalRead (5 siklus)
  6. digitalRead: temukan port dan bit yang relevan untuk diuji (41 siklus)
  7. 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:

  1. urutan bawaan (4 siklus)
  2. interrupt vector: lompat ke ISR (3 siklus)
  3. Prolog ISR: save regs (12 cycle)
  4. 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_2dengan 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 ininstruksi. 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_pintidak 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:

  1. urutan bawaan (4 siklus)
  2. interrupt vector: lompat ke ISR (3 siklus)
  3. 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 jmpinstruksi. 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 sbicdan sbiinstruksinya, 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 -nostartfilesopsi 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.

Edgar Bonet
sumber
2
Penjelasan yang bagus! +1
Nick Gammon
6

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.

Clayton Mills
sumber