Menghindari variabel global saat menggunakan interupsi dalam sistem tertanam


13

Apakah ada cara yang baik untuk mengimplementasikan komunikasi antara ISR dan program lainnya untuk sistem embedded yang menghindari variabel global?

Tampaknya pola umum adalah memiliki variabel global yang dibagi antara ISR dan program lainnya dan digunakan sebagai bendera, tetapi penggunaan variabel global ini bertentangan dengan saya. Saya telah menyertakan contoh sederhana menggunakan ISR gaya avr-libc:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

Saya tidak bisa melihat apa yang sebenarnya merupakan masalah pelingkupan; variabel apa saja yang dapat diakses oleh ISR dan program lainnya harus bersifat global, tentunya? Meskipun demikian, saya sering melihat orang mengatakan hal-hal seperti "variabel global adalah salah satu cara untuk menerapkan komunikasi antara ISR dan program lainnya" (penekanan tambang), yang tampaknya menyiratkan bahwa ada metode lain; jika ada metode lain, apa itu?



1
Itu tidak selalu benar bahwa SEMUA program akan memiliki akses; jika Anda mendeklarasikan variabel sebagai statis, hanya file tempat variabel itu dideklarasikan akan melihatnya. Sama sekali tidak sulit untuk memiliki variabel yang terlihat dalam keseluruhan satu file, tetapi tidak seluruh program dan itu dapat membantu.
DiBosco

1
di samping, flag harus dinyatakan volatile, karena Anda menggunakan / mengubahnya di luar aliran program normal. Ini memaksa kompiler untuk tidak mengoptimalkan baca / tulis untuk menandai, dan melakukan operasi baca / tulis yang sebenarnya.
next-hack

@ next-hack Ya itu benar sekali, maaf saya hanya mencoba untuk membuat contoh dengan cepat.

Jawaban:


18

Ada cara standar de facto untuk melakukan ini (dengan asumsi pemrograman C):

  • Interupsi / ISR adalah level rendah dan oleh karena itu hanya boleh diterapkan di dalam driver yang terkait dengan perangkat keras yang menghasilkan interupsi. Mereka tidak boleh ditempatkan di tempat lain tetapi di dalam pengemudi itu.
  • Semua komunikasi dengan ISR dilakukan oleh pengemudi dan pengemudi saja. Jika bagian lain dari program memerlukan akses ke informasi itu, ia harus memintanya dari driver melalui fungsi setter / getter atau sejenisnya.
  • Anda tidak boleh mendeklarasikan variabel "global". Variabel lingkup file makna global dengan hubungan eksternal. Yaitu: variabel yang dapat dipanggil dengan externkata kunci atau hanya karena kesalahan.
  • Sebagai gantinya, untuk memaksa enkapsulasi pribadi di dalam driver, semua variabel seperti itu yang dibagikan antara driver dan ISR harus dinyatakan static. Variabel seperti itu tidak bersifat global tetapi terbatas pada file yang dideklarasikan.
  • Untuk mencegah masalah optimisasi kompiler, variabel tersebut juga harus dinyatakan sebagai volatile. Catatan: ini tidak memberikan akses atomik atau menyelesaikan re-entrancy!
  • Beberapa cara mekanisme re-entrancy sering diperlukan dalam driver, jika ISR menulis ke variabel. Contoh: interrupt disable, global interrupt mask, semaphore / mutex atau jaminan atomic reads.

Catatan: Anda mungkin harus mengekspos prototipe fungsi ISR ​​melalui header, untuk menempatkannya dalam tabel vektor yang terletak di file lain. Tapi itu bukan masalah selama Anda mendokumentasikan bahwa itu adalah interupsi dan tidak boleh dipanggil oleh program.
Lundin

Apa yang akan Anda katakan, jika kontra-argumen adalah peningkatan overhead (dan kode tambahan) menggunakan fungsi setter / mendapatkan? Saya sudah membahas ini sendiri, memikirkan standar kode untuk perangkat 8 bit yang tertanam.
Leroy105

2
@ Leroy105 Bahasa C telah mendukung fungsi sebaris untuk selamanya sekarang. Meskipun bahkan penggunaan inlinemenjadi usang, karena kompiler menjadi lebih pintar dan lebih pintar dalam mengoptimalkan kode. Saya akan mengatakan bahwa mengkhawatirkan overhead adalah "optimasi pra-matang" - dalam kebanyakan kasus overhead tidak masalah, jika itu sama sekali bahkan ada dalam kode mesin.
Lundin

2
Yang sedang berkata, dalam hal menulis driver ISR, sekitar 80-90% dari semua programmer (tidak melebih-lebihkan di sini) selalu mendapatkan sesuatu di dalamnya salah. Hasilnya adalah bug halus: flag yang tidak beres dengan benar, optimisasi kompiler salah dari volatile yang hilang, kondisi balapan, performa real-time yang buruk, stack overflow dll. semakin meningkat. Berfokuslah pada penulisan driver-bebas bug sebelum mengkhawatirkan hal-hal yang menarik, seperti jika setter / getter memperkenalkan sedikit overhead.
Lundin

10
penggunaan variabel global ini bertentangan dengan saya

Inilah masalah sebenarnya. Lupakan saja.

Sekarang sebelum lutut-jerker segera berteriak-teriak tentang bagaimana ini najis, izinkan saya sedikit memenuhi syarat itu. Tentu saja ada bahaya dalam menggunakan variabel global secara berlebihan. Tetapi, mereka juga dapat meningkatkan efisiensi, yang terkadang penting dalam sistem terbatas sumber daya yang kecil.

Kuncinya adalah memikirkan kapan Anda bisa menggunakannya secara masuk akal dan tidak akan membuat masalah, dibandingkan bug yang menunggu untuk terjadi. Selalu ada pengorbanan. Walaupun umumnya menghindari variabel global untuk mengkomunikasikan antara kode interrupt dan foreground adalah pedoman yang bisa dilakukan, mengambilnya, seperti kebanyakan pedoman lainnya, pada ekstrem agama adalah kontra-produktif.

Beberapa contoh di mana saya kadang-kadang menggunakan variabel global untuk menyampaikan informasi antara kode interrupt dan foreground adalah:

  1. Penghitung centang jam dikelola oleh interupsi jam sistem. Saya biasanya memiliki interupsi jam berkala yang berjalan setiap 1 ms. Itu sering berguna untuk berbagai waktu dalam sistem. Salah satu cara untuk mendapatkan informasi ini dari rutinitas interupsi ke tempat sisa sistem dapat menggunakannya adalah dengan menjaga penghitung centang jam global. Rutin interupsi menambah penghitung setiap clock tick. Kode latar depan dapat membaca konter kapan saja. Seringkali saya melakukan ini selama 10 ms, 100 ms, dan bahkan 1 detik ticks.

    Saya memastikan kutu 1 ms, 10 ms, dan 100 ms memiliki ukuran kata yang dapat dibaca dalam satu operasi atom. Jika menggunakan bahasa tingkat tinggi, pastikan untuk memberi tahu kompiler bahwa variabel-variabel ini dapat berubah secara tidak sinkron. Dalam C, Anda menyatakan mereka sebagai volatile eksternal , misalnya. Tentu saja ini adalah sesuatu yang dimasukkan ke dalam file kaleng kalengan, jadi Anda tidak perlu mengingatnya untuk setiap proyek.

    Saya kadang-kadang membuat penghitung centang 1 s penghitung waktu berlalu total, jadi buat lebar 32 bit. Itu tidak dapat dibaca dalam operasi atom tunggal pada banyak mikro kecil yang saya gunakan, sehingga tidak dibuat mendunia. Alih-alih, disediakan rutin yang membaca nilai multi-kata, menangani kemungkinan pembaruan di antara bacaan, dan mengembalikan hasilnya.

    Tentu saja ada bisa menjadi rutinitas untuk mendapatkan yang lebih kecil 1 ms, 10 ms, dll, centang counter juga. Namun, itu benar-benar sangat sedikit untuk Anda, menambahkan banyak instruksi untuk membaca satu kata, dan menggunakan lokasi tumpukan panggilan lainnya.

    Apa kekurangannya? Saya kira seseorang dapat membuat kesalahan ketik yang secara tidak sengaja menulis ke salah satu konter, yang kemudian dapat mengacaukan waktu lain dalam sistem. Menulis ke konter dengan sengaja tidak masuk akal, jadi bug semacam ini perlu sesuatu yang tidak disengaja seperti kesalahan ketik. Tampaknya sangat tidak mungkin. Saya tidak ingat pernah terjadi di lebih dari 100 proyek mikrokontroler kecil.

  2. Nilai A / D yang difilter dan disesuaikan akhir. Hal umum yang harus dilakukan adalah memiliki pembacaan pegangan rutin interupsi dari A / D. Saya biasanya membaca nilai analog lebih cepat dari yang diperlukan, kemudian menerapkan sedikit penyaringan low-pass. Sering ada juga penskalaan dan offset yang diterapkan.

    Misalnya, A / D mungkin membaca output 0 hingga 3 V dari pembagi tegangan untuk mengukur pasokan 24 V. Banyak bacaan dijalankan melalui beberapa pemfilteran, kemudian diskalakan sehingga nilai akhir dalam milivolt. Jika persediaan di 24.015 V, maka nilai akhirnya adalah 24015.

    Sisanya dari sistem hanya melihat nilai pembaruan langsung yang menunjukkan tegangan suplai. Itu tidak tahu atau tidak perlu peduli kapan persisnya itu diperbarui, terutama karena itu diperbarui jauh lebih sering daripada waktu penyelesaian filter low pass.

    Sekali lagi, rutin antarmuka dapat digunakan, tetapi Anda hanya mendapat sedikit manfaat dari itu. Hanya menggunakan variabel global setiap kali Anda membutuhkan tegangan catu daya jauh lebih sederhana. Ingatlah bahwa kesederhanaan bukan hanya untuk mesin, tetapi lebih sederhana juga berarti lebih sedikit kemungkinan kesalahan manusia.


Saya telah menjalani terapi, dalam minggu yang lambat, benar-benar mencoba untuk memperbaiki kode saya. Saya melihat poin Lundin tentang membatasi akses variabel, tetapi saya melihat sistem saya yang sebenarnya dan berpikir itu adalah kemungkinan yang sangat jauh. SETIAP ORANG akan benar-benar memperdaya variabel global sistem yang kritis. Fungsi Getter / Setter akhirnya membebani biaya overhead Anda dibandingkan hanya menggunakan global dan menerima ini adalah program yang cukup sederhana ...
Leroy105

3
@ Leroy105 Masalahnya bukan "teroris" sengaja menyalahgunakan variabel global. Polusi Namespace bisa menjadi masalah dalam proyek yang lebih besar, tetapi itu bisa diselesaikan dengan penamaan yang baik. Tidak, masalah sebenarnya adalah programmer mencoba menggunakan variabel global sebagaimana dimaksud, tetapi gagal melakukannya dengan benar. Entah karena mereka tidak menyadari masalah kondisi ras yang ada dengan semua ISR, atau karena mereka mengacaukan penerapan mekanisme perlindungan wajib, atau hanya karena mereka memuntahkan penggunaan variabel global di seluruh kode, menciptakan penggandengan yang ketat dan kode tidak dapat dibaca.
Lundin

Poin Anda adalah Olin yang valid, tetapi bahkan dalam contoh-contoh ini, mengganti extern int ticks10msdengan inline int getTicks10ms()akan sama sekali tidak membuat perbedaan dalam rakitan yang dikompilasi, sementara di sisi lain itu akan membuat sulit untuk secara tidak sengaja mengubah nilainya di bagian lain dari program, dan juga memungkinkan Anda untuk cara untuk "menghubungkan" ke panggilan ini (misalnya untuk mengejek waktu selama pengujian unit, untuk mencatat akses ke variabel ini, atau apa pun). Bahkan jika Anda berpendapat bahwa kesempatan seorang programmer san mengubah variabel ini menjadi nol, tidak ada biaya bagi pengambil inline.
Groo

@ Goo: Itu hanya benar jika Anda menggunakan bahasa yang mendukung fungsi inlining, dan itu berarti definisi fungsi pengambil harus terlihat oleh semua. Sebenarnya ketika menggunakan bahasa tingkat tinggi, saya menggunakan fungsi getter lebih banyak dan variabel global lebih sedikit. Dalam perakitan, jauh lebih mudah untuk mengambil nilai variabel global daripada repot dengan fungsi pengambil.
Olin Lathrop

Tentu saja, jika Anda tidak bisa sebaris, maka pilihannya tidak begitu sederhana. Saya ingin mengatakan bahwa dengan fungsi sebaris (dan banyak kompiler pra C99 sudah mendukung ekstensi inlining), kinerja tidak dapat menjadi argumen terhadap pengambil. Dengan kompilator pengoptimal yang masuk akal, Anda harus berakhir dengan perakitan yang sama.
Groo

2

Setiap gangguan tertentu akan menjadi sumber daya global. Namun kadang-kadang, mungkin berguna untuk meminta beberapa interupsi berbagi kode yang sama. Misalnya, sistem mungkin memiliki beberapa UART, yang semuanya harus menggunakan logika kirim / terima yang serupa.

Pendekatan yang baik untuk menangani itu adalah menempatkan hal-hal yang digunakan oleh pengendali interupsi, atau petunjuk kepada mereka, dalam objek struktur, dan kemudian memiliki pengendali interrupt hardware sebenarnya menjadi sesuatu seperti:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

Objek uart1_info, uart2_infodll. Akan menjadi variabel global, tetapi mereka akan menjadi satu - satunya variabel global yang digunakan oleh pengendali interupsi. Segala hal lain yang akan disentuh pawang akan ditangani di dalamnya.

Perhatikan bahwa apa pun yang diakses oleh interrupt handler dan oleh kode jalur utama harus memenuhi syarat volatile. Mungkin paling sederhana untuk menyatakan sebagai volatilesegala sesuatu yang akan digunakan sama sekali oleh pengendali interupsi, tetapi jika kinerjanya penting seseorang mungkin ingin menulis kode yang menyalin informasi ke nilai sementara, mengoperasikannya, dan kemudian menulisnya kembali. Misalnya, alih-alih menulis:

if (foo->timer)
  foo->timer--;

menulis:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

Pendekatan yang pertama mungkin lebih mudah dibaca dan dipahami, tetapi akan kurang efisien daripada yang terakhir. Apakah itu masalah akan tergantung pada aplikasi.


0

Inilah tiga ide:

Deklarasikan variabel flag sebagai statis untuk membatasi cakupan ke satu file.

Jadikan variabel flag sebagai pribadi dan gunakan fungsi pengambil dan penyetel untuk mengakses nilai bendera.

Gunakan objek pensinyalan seperti semaphore dan bukan variabel flag. ISR akan mengatur / memposting semafor.


0

Interrupt (yaitu, vektor yang menunjuk ke handler Anda) adalah sumber daya global. Jadi, bahkan jika Anda menggunakan beberapa variabel di stack atau di heap:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

atau kode berorientasi objek dengan fungsi 'virtual':

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

... langkah pertama harus melibatkan variabel global aktual (atau setidaknya statis) untuk mencapai data lainnya.

Semua mekanisme ini menambah tipuan, jadi ini biasanya tidak dilakukan jika Anda ingin memeras siklus terakhir dari interrupt handler.


Anda harus mendeklarasikan flag sebagai volatile int *.
next-hack

0

Saya sedang mengkode Cortex M0 / M4 saat ini dan pendekatan yang kami gunakan dalam C ++ (tidak ada tag C ++, jadi jawaban ini mungkin di luar topik) adalah sebagai berikut:

Kami menggunakan kelas CInterruptVectorTableyang berisi semua rutinitas layanan interupsi yang disimpan dalam vektor interupsi aktual dari pengontrol:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

Kelas CInterruptVectorTablemengimplementasikan abstraksi vektor interupsi, sehingga Anda dapat mengikat berbagai fungsi ke vektor interupsi selama runtime.

Antarmuka kelas itu terlihat seperti ini:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

Anda perlu membuat fungsi-fungsi yang disimpan dalam tabel vektor statickarena pengontrol tidak dapat memberikan this-poiner karena tabel vektor bukan objek. Jadi untuk mengatasi masalah itu kita memiliki pThispointer- statis di dalam CInterruptVectorTable. Setelah memasukkan salah satu fungsi interupsi statis, ia dapat mengakses pThis-pointer untuk mendapatkan akses ke anggota satu objek CInterruptVectorTable.


Sekarang dalam program, Anda dapat menggunakan SetIsrCallbackfunctionuntuk menyediakan pointer fungsi ke staticfungsi yang akan dipanggil ketika interupsi terjadi. Pointer disimpan di InterruptVectorTable_t virtualVectorTable.

Dan implementasi fungsi interupsi terlihat seperti ini:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

Sehingga akan memanggil staticmetode kelas lain (yang bisa private), yang kemudian dapat berisi static this-pointer lain untuk mendapatkan akses ke variabel anggota objek itu (hanya satu).

Saya kira Anda bisa membangun dan antarmuka suka IInterruptHandlerdan menyimpan pointer ke objek, sehingga Anda tidak perlu- static thispointer di semua kelas tersebut. (mungkin kita mencobanya di iterasi arsitektur kita selanjutnya)

Pendekatan lain berfungsi dengan baik bagi kami, karena satu-satunya objek yang diizinkan untuk mengimplementasikan pengendali interupsi adalah yang ada di dalam lapisan abstraksi perangkat keras, dan kami biasanya hanya memiliki satu objek untuk setiap blok perangkat keras, sehingga bekerja dengan- static thispointer. Dan lapisan abstraksi perangkat keras menyediakan abstraksi lain untuk menyela, yang disebut ICallbackyang kemudian diimplementasikan dalam lapisan perangkat di atas perangkat keras.


Apakah Anda mengakses data global? Tentu Anda melakukannya, tetapi Anda dapat menjadikan sebagian besar data global yang diperlukan bersifat pribadi seperti this-poiner dan fungsi interupsi.

Ini bukan antipeluru, dan itu menambah overhead. Anda akan kesulitan menerapkan tumpukan IO-Link menggunakan pendekatan ini. Tetapi jika Anda tidak terlalu ketat dengan pengaturan waktu, ini bekerja cukup baik untuk mendapatkan abstraksi fleksibel dari interupsi dan komunikasi dalam modul tanpa menggunakan variabel global yang dapat diakses dari mana saja.


1
"jadi kamu bisa mengikat berbagai fungsi ke vektor interupsi selama runtime" Ini kedengarannya seperti ide yang buruk. "Kompleksitas siklomatik" dari program hanya akan melewati atap. Semua kombinasi use case harus diuji sehingga tidak ada waktu atau konflik penggunaan stack. Banyak sakit kepala untuk fitur dengan kegunaan IMO yang sangat terbatas. (Kecuali jika Anda memiliki case bootloader, itu cerita lain) Secara keseluruhan ini berbau pemrograman meta.
Lundin

@Lundin Saya tidak begitu mengerti maksud Anda. Kami menggunakannya untuk mengikat misalnya interupsi DMA ke penangan interupsi SPI jika DMA digunakan untuk SPI dan penangan interupsi UART jika digunakan untuk UART. Kedua penangan harus diuji, tentu, tetapi tidak masalah. Dan itu pasti tidak ada hubungannya dengan pemrograman meta.
Arsenal

DMA adalah satu hal, penugasan run-time vektor interupsi adalah sesuatu yang sama sekali berbeda. Masuk akal untuk membiarkan pengaturan driver DMA menjadi variabel, dalam jangka waktu. Tabel vektor, tidak terlalu banyak.
Lundin

@Lundin Saya kira kita memiliki pandangan yang berbeda tentang itu, kita dapat memulai obrolan tentang hal itu, karena saya masih tidak melihat masalah Anda dengannya - jadi mungkin jawaban saya ditulis dengan buruk, sehingga seluruh konsepnya disalahpahami.
Arsenal
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.