Rutin layanan interupsi AVR tidak mengeksekusi secepat yang diharapkan (instruksi overhead?)

8

Saya mengembangkan penganalisis logika kecil dengan 7 input. Perangkat target saya adalah ATmega168dengan clock rate 20MHz. Untuk mendeteksi perubahan logika saya menggunakan interupsi perubahan pin. Sekarang saya mencoba mencari tahu laju sampel terendah yang dapat saya deteksi perubahan pin ini. Saya menentukan nilai minimum 5,6 μs (178,5 kHz). Setiap sinyal di bawah angka ini saya tidak bisa menangkap dengan benar.

Kode saya ditulis dalam C (avr-gcc). Rutinitas saya terlihat seperti:

ISR()
{
    pinc = PINC; // char
    timestamp_ll = TCNT1L; // char
    timestamp_lh = TCNT1H; // char
    timestamp_h = timerh; // 2 byte integer
    stack_counter++;
}

Perubahan sinyal yang saya tangkap terletak di pinc. Untuk melokalkannya saya memiliki nilai cap waktu yang panjang 4 byte.

Dalam datasheet saya membaca layanan rutin interupsi membutuhkan 5 jam untuk melompat dan 5 jam untuk kembali ke prosedur utama. Saya mengasumsikan setiap perintah di saya ISR()mengambil 1 jam untuk dieksekusi; Jadi singkatnya harus ada overhead 5 + 5 + 5 = 15jam. Durasi satu jam harus sesuai dengan laju jam 20MHz 1/20000000 = 0.00000005 = 50 ns. Total overhead dalam detik harus kemudian: 15 * 50 ns = 750 ns = 0.75 µs. Sekarang saya tidak mengerti mengapa saya tidak bisa menangkap apa pun di bawah 5,6 μs. Adakah yang bisa menjelaskan apa yang terjadi?

arminb
sumber
mungkin 5 jam untuk mengirim kode ISR, yang mencakup penyimpanan konteks dan mengembalikan epilog / prolog yang tidak Anda lihat di sumber C. Juga, apa yang dilakukan perangkat keras ketika interupsi mati? Apakah dalam kondisi tidur. (Saya tidak tahu AVR, tetapi secara umum, mengganggu proses negara-negara tertentu dapat memakan waktu lebih lama.)
Kaz
@arminb Lihat juga pertanyaan ini untuk ide-ide lain tentang cara menangkap peristiwa eksternal dengan ketelitian yang lebih tinggi. Juga [catatan aplikasi ini] (www.atmel.com/Images/doc2505.pdf) mungkin menarik.
angelatlarge

Jawaban:

10

Ada beberapa masalah:

  • Tidak semua perintah AVR membutuhkan 1 jam untuk dijalankan: jika Anda melihat bagian belakang lembar data, ia memiliki jumlah jam yang diperlukan untuk setiap instruksi yang akan dieksekusi. Jadi, misalnya ANDadalah instruksi satu jam, MUL(berlipat ganda) membutuhkan dua jam, sedangkan LPM(memuat memori program) adalah tiga, dan CALLadalah 4. Jadi, sehubungan dengan pelaksanaan instruksi, itu benar-benar tergantung pada instruksi.
  • 5 jam untuk melompat dan 5 jam untuk kembali bisa menyesatkan. Jika Anda melihat kode yang dibongkar, Anda akan menemukan bahwa selain lompatan dan RETIinstruksi, kompiler menambahkan semua jenis kode lain, yang juga membutuhkan waktu. Misalnya Anda mungkin perlu variabel lokal yang dibuat di stack dan harus dibuka, dll. Hal terbaik yang harus dilakukan untuk melihat apa yang sebenarnya terjadi adalah dengan melihat pembongkaran.
  • Terakhir, ingatlah bahwa ketika Anda berada dalam rutinitas ISR Anda, interupsi Anda tidak memicu. Ini berarti bahwa Anda tidak akan bisa mendapatkan jenis kinerja yang Anda cari dari penganalisa logika Anda, kecuali Anda tahu bahwa tingkat sinyal Anda berubah pada interval yang lebih lama daripada yang diperlukan untuk melayani gangguan Anda. Untuk menjadi jelas, setelah Anda menghitung waktu yang diperlukan untuk menjalankan ISR Anda, ini memberi Anda batas atas seberapa cepat Anda dapat menangkap satu sinyal . Jika Anda perlu menangkap dua sinyal, maka Anda mulai mengalami masalah. Untuk terlalu rinci tentang ini pertimbangkan skenario berikut:

masukkan deskripsi gambar di sini

Jika xwaktu yang dibutuhkan untuk memperbaiki interupsi Anda, maka sinyal B tidak akan pernah ditangkap.


Jika kami mengambil kode ISR Anda, masukkan ke dalam rutin ISR (saya menggunakan ISR(PCINT0_vect)) rutin, deklarasikan semua variabel volatile, dan kompilasi untuk ATmega168P, kode yang dibongkar terlihat seperti berikut (lihat jawaban @ jipple untuk info lebih lanjut) sebelum kita sampai ke kode bahwa "melakukan sesuatu" ; dengan kata lain prolog ke ISR Anda adalah sebagai berikut:

  37                    .loc 1 71 0
  38                    .cfi_startproc
  39 0000 1F92              push r1
  40                .LCFI0:
  41                    .cfi_def_cfa_offset 3
  42                    .cfi_offset 1, -2
  43 0002 0F92              push r0
  44                .LCFI1:
  45                    .cfi_def_cfa_offset 4
  46                    .cfi_offset 0, -3
  47 0004 0FB6              in r0,__SREG__
  48 0006 0F92              push r0
  49 0008 1124              clr __zero_reg__
  50 000a 8F93              push r24
  51                .LCFI2:
  52                    .cfi_def_cfa_offset 5
  53                    .cfi_offset 24, -4
  54 000c 9F93              push r25
  55                .LCFI3:
  56                    .cfi_def_cfa_offset 6
  57                    .cfi_offset 25, -5
  58                /* prologue: Signal */
  59                /* frame size = 0 */
  60                /* stack size = 5 */
  61                .L__stack_usage = 5

jadi, PUSHx 5, inx 1, clrx 1. Tidak seburuk vars 32-bit jipple, tapi masih tidak ada apa-apanya.

Sebagian dari ini perlu (perluas diskusi dalam komentar). Jelas, karena rutin ISR dapat terjadi kapan saja, ia harus mempra-register terlebih dahulu register yang digunakannya, kecuali Anda tahu bahwa tidak ada kode di mana interupsi dapat terjadi menggunakan register yang sama dengan rutin interupsi Anda. Misalnya baris berikut dalam ISR yang dibongkar:

push r24

Apakah ada karena semuanya berjalan r24: Anda pincdimuat di sana sebelum masuk ke memori, dll. Jadi Anda harus memilikinya terlebih dahulu. __SREG__dimasukkan ke dalam r0dan kemudian didorong: jika ini bisa melewati r24maka Anda bisa menyelamatkan diri Anda sendiri aPUSH


Beberapa solusi yang mungkin:

  • Gunakan loop polling ketat seperti yang disarankan oleh Kaz di komentar. Ini mungkin akan menjadi solusi tercepat, apakah Anda menulis loop di C atau assembly.
  • Tuliskan ISR Anda dalam rakitan: dengan cara ini Anda dapat mengoptimalkan penggunaan register sedemikian rupa sehingga jumlah paling sedikit dari mereka perlu disimpan selama ISR.
  • Nyatakan rutinitas ISR Anda ISR_NAKED , meskipun ini lebih merupakan solusi herring merah. Ketika Anda mendeklarasikan rutinitas ISR ISR_NAKED, gcc tidak menghasilkan kode prolog / epilog, dan Anda bertanggung jawab untuk menyimpan register apa pun yang dimodifikasi oleh kode Anda, serta menelepon reti(kembali dari interupsi). Sayangnya, tidak ada cara menggunakan register di avr-gcc C langsung (jelas Anda dapat dalam perakitan), namun, apa yang dapat Anda lakukan adalah variabel mengikat untuk register tertentu dengan register+ asmkata kunci, seperti ini: register uint8_t counter asm("r3");. Jika Anda melakukannya, untuk ISR Anda akan tahu register apa yang Anda gunakan di ISR. Masalahnya kemudian adalah bahwa tidak ada cara untuk menghasilkan pushdanpopuntuk menyimpan register yang digunakan tanpa perakitan inline (lih. poin 1). Untuk memastikan Anda harus menyimpan lebih sedikit register, Anda juga dapat mengikat semua variabel non-ISR ke register spesifik, namun, tidak, Anda tidak mengalami masalah yang menggunakan register gcc untuk mengocok data ke dan dari memori. Ini berarti bahwa kecuali Anda melihat pembongkaran, Anda tidak akan tahu register apa yang digunakan kode utama Anda. Jadi jika Anda mempertimbangkan ISR_NAKED, Anda mungkin juga menulis ISR dalam pertemuan.
angelatlarge
sumber
Terima kasih, jadi kode C saya membuat overhead besar? Apakah akan lebih cepat jika saya menulisnya di assembler? Tentang hal kedua, saya sadar akan hal itu.
arminb
@arminb: Saya tidak cukup tahu untuk menjawab pertanyaan itu. Asumsi saya adalah bahwa kompiler cukup pintar, dan melakukan apa yang dilakukannya karena suatu alasan. Setelah mengatakan bahwa saya yakin bahwa jika Anda menghabiskan waktu dengan perakitan Anda bisa memeras beberapa siklus jam lagi dari rutinitas ISR Anda.
angelatlarge
1
Saya pikir jika Anda menginginkan respons tercepat, Anda biasanya menghindari interupsi dan menyurvei pin dalam loop yang ketat.
Kaz
1
Dengan tujuan spesifik dalam pikiran adalah mungkin untuk mengoptimalkan kode dengan menggunakan assembler. Misalnya kompilator dimulai dengan mendorong semua register yang digunakan ke stack, kemudian mulai menjalankan rutinitas aktual. Jika Anda memiliki pengaturan waktu untuk hal-hal penting, Anda dapat memindahkan beberapa dorongan dan menariknya ke depan. Jadi ya Anda bisa mengoptimalkan dengan menggunakan assembler, tetapi kompiler itu sendiri juga cukup pintar. Saya suka menggunakan kode yang dikompilasi sebagai titik awal dan memodifikasinya secara manual untuk persyaratan spesifik saya.
jippie
1
Jawaban yang sangat bagus. Saya akan menambahkan bahwa kompiler menambahkan semua jenis penyimpanan register dan pemulihan sesuai dengan kebutuhan sebagian besar pengguna. Dimungkinkan untuk menulis interrupt handler Anda sendiri - jika Anda tidak membutuhkan semua overhead itu. Beberapa kompiler bahkan mungkin menawarkan opsi untuk membuat interupsi "cepat", meninggalkan banyak "pembukuan" kepada programmer. Saya tidak perlu langsung ke loop ketat tanpa ISR jika saya tidak dapat memenuhi jadwal saya. Pertama saya akan mempertimbangkan UC lebih cepat, dan kemudian saya mencari tahu apakah saya bisa menggunakan semacam perangkat keras lem, seperti kait dan RTC.
Scott Seidman
2

Ada banyak register PUSH'ing dan POP'ing untuk ditumpuk terjadi sebelum ISR Anda yang sebenarnya dimulai, yaitu di atas siklus 5 jam yang Anda sebutkan. Lihatlah pembongkaran kode yang dihasilkan.

Bergantung pada rantai alat yang Anda gunakan, membuang daftar yang kami lakukan dengan berbagai cara. Saya bekerja pada baris perintah Linux dan ini adalah perintah yang saya gunakan (membutuhkan file .elf sebagai input):

avr-objdump -C -d $(src).elf

Lihatlah sniplet kode yang baru-baru ini saya gunakan untuk ATtiny. Beginilah bentuk kode-C:

ISR( INT0_vect ) {
        uint8_t myTIFR  = TIFR;
        uint8_t myTCNT1 = TCNT1;

Dan ini adalah kode perakitan yang dihasilkan untuk itu:

00000056 <INT0_vect>:
  56:   1f 92           push    r1
  58:   0f 92           push    r0
  5a:   0f b6           in      r0, SREG        ; 0x3f
  5c:   0f 92           push    r0
  5e:   11 24           eor     r1, r1
  60:   2f 93           push    r18
  62:   3f 93           push    r19
  64:   4f 93           push    r20
  66:   8f 93           push    r24
  68:   9f 93           push    r25
  6a:   af 93           push    r26
  6c:   bf 93           push    r27
  6e:   48 b7           in      r20, TIFR       ; uint8_t myTIFR  = TIFR;
  70:   2f b5           in      r18, TCNT1      ; uint8_t myTCNT1 = TCNT1;

Sejujurnya, rutin C saya menggunakan beberapa variabel lebih banyak yang menyebabkan semua push dan pop ini, tetapi Anda mendapatkan idenya.

Memuat variabel 32 bit terlihat seperti ini:

  ec:   80 91 78 00     lds     r24, 0x0078
  f0:   90 91 79 00     lds     r25, 0x0079
  f4:   a0 91 7a 00     lds     r26, 0x007A
  f8:   b0 91 7b 00     lds     r27, 0x007B

Meningkatkan variabel 32 bit dengan 1 terlihat seperti ini:

  5e:   11 24           eor     r1, r1
  d6:   01 96           adiw    r24, 0x01       ; 1
  d8:   a1 1d           adc     r26, r1
  da:   b1 1d           adc     r27, r1

Menyimpan variabel 32-bit terlihat seperti ini:

  dc:   80 93 78 00     sts     0x0078, r24
  e0:   90 93 79 00     sts     0x0079, r25
  e4:   a0 93 7a 00     sts     0x007A, r26
  e8:   b0 93 7b 00     sts     0x007B, r27

Maka tentu saja Anda harus memunculkan nilai-nilai lama setelah Anda meninggalkan ISR:

 126:   bf 91           pop     r27
 128:   af 91           pop     r26
 12a:   9f 91           pop     r25
 12c:   8f 91           pop     r24
 12e:   4f 91           pop     r20
 130:   3f 91           pop     r19
 132:   2f 91           pop     r18
 134:   0f 90           pop     r0
 136:   0f be           out     SREG, r0        ; 0x3f
 138:   0f 90           pop     r0
 13a:   1f 90           pop     r1
 13c:   18 95           reti

Menurut ringkasan instruksi dalam lembar data, sebagian besar instruksi adalah siklus tunggal, tetapi PUSH dan POP adalah siklus ganda. Anda mendapat ide dari mana keterlambatan itu berasal?

jippie
sumber
Terima kasih atas jawaban anda! Sekarang saya sadar tentang apa yang terjadi. Terutama terima kasih atas perintahnya avr-objdump -C -d $(src).elf!
arminb
Luangkan waktu beberapa saat untuk memahami instruksi perakitan yang avr-objdumpdimuntahkan, mereka dijelaskan secara singkat dalam lembar data di bawah Instruksi Ringkasan. Menurut pendapat saya itu adalah praktik yang baik untuk membiasakan diri dengan mnemonik karena dapat banyak membantu ketika men-debug kode C Anda.
jippie
Bahkan, pembongkaran berguna sebagai bagian dari standar Anda Makefile: jadi setiap kali Anda membangun proyek Anda dibongkar secara otomatis juga sehingga Anda tidak perlu memikirkannya, atau ingat bagaimana melakukannya secara manual.
angelatlarge