Menggunakan volatile dalam pengembangan C yang disematkan

44

Saya telah membaca beberapa artikel dan jawaban Stack Exchange tentang penggunaan volatile kata kunci untuk mencegah kompiler menerapkan optimasi pada objek yang dapat berubah dengan cara yang tidak dapat ditentukan oleh kompiler.

Jika saya membaca dari ADC (sebut saja variabel adcValue), dan saya menyatakan variabel ini sebagai global, haruskah saya menggunakan kata kunci volatiledalam kasus ini?

  1. Tanpa menggunakan volatilekata kunci

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
  2. Menggunakan volatilekata kunci

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    volatile uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }

Saya mengajukan pertanyaan ini karena ketika debugging, saya dapat melihat tidak ada perbedaan antara kedua pendekatan meskipun praktik terbaik mengatakan bahwa dalam kasus saya (variabel global yang berubah langsung dari perangkat keras), maka menggunakan volatileadalah wajib.

Pryda
sumber
1
Sejumlah lingkungan debug (tentu saja gcc) tidak menerapkan optimisasi. Membangun produksi biasanya akan (tergantung pada pilihan Anda). Hal ini dapat menyebabkan perbedaan 'menarik' antara bangunan. Melihat peta output tautan adalah informatif.
Peter Smith
22
"dalam kasus saya (Variabel global yang berubah langsung dari perangkat keras)" - Variabel global Anda tidak diubah oleh perangkat keras tetapi hanya oleh kode C Anda, yang diketahui penyusunnya. - Register perangkat keras tempat ADC memberikan hasilnya, harus volatile, karena kompiler tidak dapat mengetahui apakah / kapan nilainya akan berubah (itu berubah jika / ketika perangkat keras ADC menyelesaikan konversi.)
JimmyB
2
Apakah Anda membandingkan assembler yang dihasilkan oleh kedua versi? Itu akan menunjukkan kepada Anda apa yang terjadi di bawah tenda
Mawg
3
@stark: BIOS? Pada mikrokontroler? Ruang I / O yang dipetakan oleh memori tidak akan dapat di-cache (jika arsitektur bahkan memiliki cache data di tempat pertama, yang tidak dijamin) dengan konsistensi desain antara aturan caching dan peta memori. Tetapi volatile tidak ada hubungannya dengan cache pengontrol memori.
Ben Voigt
1
@Davislor Standar bahasa tidak perlu mengatakan apa-apa lagi secara umum. Membaca ke objek yang mudah menguap akan melakukan beban nyata (bahkan jika kompiler baru-baru ini melakukan satu dan biasanya akan tahu apa nilainya) dan menulis ke objek tersebut akan melakukan toko nyata (bahkan jika nilai yang sama dibaca dari objek ). Jadi dalam if(x==1) x=1;penulisan dapat dioptimalkan untuk non volatile xdan tidak dapat dioptimalkan jika xvolatile. OTOH jika diperlukan instruksi khusus untuk mengakses perangkat eksternal, terserah Anda untuk menambahkannya (mis. Jika rentang memori perlu dibuat tuliskan melalui).
curiousguy

Jawaban:

87

Definisi dari volatile

volatilememberitahu kompiler bahwa nilai variabel dapat berubah tanpa diketahui kompiler. Oleh karena itu kompiler tidak dapat menganggap nilai tidak berubah hanya karena program C tampaknya tidak mengubahnya.

Di sisi lain, itu berarti bahwa nilai variabel mungkin diperlukan (baca) di tempat lain yang tidak diketahui oleh kompiler, oleh karena itu ia harus memastikan bahwa setiap tugas pada variabel benar-benar dilakukan sebagai operasi tulis.

Gunakan kasing

volatile diperlukan saat

  • mewakili register perangkat keras (atau I / O yang dipetakan memori) sebagai variabel - bahkan jika register tidak akan pernah dibaca, kompiler tidak boleh hanya melewati operasi penulisan dengan berpikir "Programmer bodoh. Mencoba untuk menyimpan nilai dalam variabel yang dia / dia tidak akan pernah membaca kembali. Dia bahkan tidak akan memperhatikan jika kita menghilangkan tulisannya. " Sebaliknya, bahkan jika program tidak pernah menulis nilai ke variabel, nilainya masih dapat diubah oleh perangkat keras.
  • berbagi variabel antara konteks eksekusi (mis. ISR / program utama) (lihat jawaban @ kkramo)

Efek dari volatile

Ketika sebuah variabel dideklarasikan volatile, kompiler harus memastikan bahwa setiap penugasan dalam kode program tercermin dalam operasi penulisan yang sebenarnya, dan bahwa setiap pembacaan dalam kode program membaca nilai dari memori (yang dipetakan).

Untuk variabel yang tidak mudah menguap, kompiler menganggap ia tahu jika / ketika nilai variabel berubah dan dapat mengoptimalkan kode dengan cara yang berbeda.

Untuk satu, kompiler dapat mengurangi jumlah baca / tulis ke memori, dengan menjaga nilai dalam register CPU.

Contoh:

void uint8_t compute(uint8_t input) {
  uint8_t result = input + 2;
  result = result * 2;
  if ( result > 100 ) {
    result -= 100;
  }
  return result;
}

Di sini, kompiler mungkin bahkan tidak akan mengalokasikan RAM untuk resultvariabel, dan tidak akan pernah menyimpan nilai-nilai perantara di mana pun kecuali dalam register CPU.

Jika resultvolatile, setiap kejadian resultdalam kode C akan memerlukan kompiler untuk melakukan akses ke RAM (atau port I / O), yang mengarah ke kinerja yang lebih rendah.

Kedua, kompiler dapat memesan ulang operasi pada variabel non-volatile untuk kinerja dan / atau ukuran kode. Contoh sederhana:

int a = 99;
int b = 1;
int c = 99;

bisa dipesan ulang

int a = 99;
int c = 99;
int b = 1;

yang dapat menyimpan instruksi assembler karena nilai 99 tidak harus dimuat dua kali.

Jika a, bdan cvolatile kompiler harus memancarkan instruksi yang menetapkan nilai-nilai dalam urutan yang tepat seperti yang diberikan dalam program.

Contoh klasik lainnya adalah seperti ini:

volatile uint8_t signal;

void waitForSignal() {
  while ( signal == 0 ) {
    // Do nothing.
  }
}

Jika, dalam kasus ini, signaltidak volatile, kompiler akan 'berpikir' bahwa while( signal == 0 )mungkin merupakan infinite loop (karena signaltidak akan pernah diubah oleh kode di dalam loop ) dan mungkin menghasilkan setara dengan

void waitForSignal() {
  if ( signal != 0 ) {
    return; 
  } else {
    while(true) { // <-- Endless loop!
      // do nothing.
    }
  }
}

Pertimbangan penanganan volatilenilai

Seperti yang dinyatakan di atas, volatilevariabel dapat memperkenalkan penalti kinerja ketika diakses lebih sering daripada yang sebenarnya diperlukan. Untuk mengurangi masalah ini, Anda bisa "membatalkan volatile" nilainya dengan menetapkan variabel yang tidak mudah menguap, seperti

volatile uint32_t sysTickCount;

void doSysTick() {
  uint32_t ticks = sysTickCount; // A single read access to sysTickCount

  ticks = ticks + 1; 

  setLEDState( ticks < 500000L );

  if ( ticks >= 1000000L ) {
    ticks = 0;
  }
  sysTickCount = ticks; // A single write access to volatile sysTickCount
}

Ini mungkin sangat bermanfaat di ISR ​​di mana Anda ingin secepat mungkin tidak mengakses perangkat keras atau memori yang sama beberapa kali ketika Anda tahu itu tidak diperlukan karena nilainya tidak akan berubah saat ISR Anda berjalan. Ini umum ketika ISR adalah 'penghasil' nilai untuk variabel, seperti sysTickCountpada contoh di atas. Pada AVR akan sangat menyakitkan untuk memiliki fungsi doSysTick()mengakses empat byte yang sama dalam memori (empat instruksi = 8 siklus CPU per akses ke sysTickCount) lima atau enam kali alih-alih hanya dua kali, karena programmer tahu bahwa nilainya tidak akan diubah dari beberapa kode lain saat doSysTick()menjalankannya.

Dengan trik ini, Anda pada dasarnya melakukan hal yang sama persis seperti yang dilakukan oleh kompiler untuk variabel non-volatil, yaitu membacanya dari memori hanya ketika harus, menyimpan nilai dalam register untuk beberapa waktu dan menulis kembali ke memori hanya ketika harus ; tetapi kali ini, Anda tahu lebih baik daripada kompiler jika / ketika membaca / menulis harus terjadi, jadi Anda membebaskan kompiler dari tugas optimasi ini dan melakukannya sendiri.

Keterbatasan volatile

Akses non-atom

volatiletidak tidak memberikan akses atom untuk variabel multi-kata. Untuk kasus-kasus tersebut, Anda harus memberikan pengecualian bersama dengan cara lain, selain menggunakan volatile. Pada AVR, Anda dapat menggunakan ATOMIC_BLOCKdari <util/atomic.h>atau cli(); ... sei();panggilan sederhana . Makro masing-masing bertindak sebagai penghalang memori juga, yang penting ketika datang ke urutan akses:

Perintah eksekusi

volatilemembebankan pesanan eksekusi ketat hanya sehubungan dengan variabel volatil lainnya. Ini artinya, misalnya

volatile int i;
volatile int j;
int a;

...

i = 1;
a = 99;
j = 2;

dijamin pertama menetapkan 1 untuk idan kemudian menetapkan 2 untuk j. Namun, itu tidak dijamin yang aakan ditugaskan di antara; kompiler dapat melakukan tugas itu sebelum atau setelah potongan kode, pada dasarnya setiap saat hingga pembacaan pertama (terlihat) a.

Jika bukan karena penghalang memori makro yang disebutkan di atas, kompiler akan diizinkan untuk menerjemahkan

uint32_t x;

cli();
x = volatileVar;
sei();

untuk

x = volatileVar;
cli();
sei();

atau

cli();
sei();
x = volatileVar;

(Demi kelengkapan, saya harus mengatakan bahwa hambatan ingatan, seperti yang tersirat oleh makro sei / cli, dapat benar-benar meniadakan penggunaan volatile, jika semua akses dihubungkan dengan hambatan-hambatan ini.)

JimmyB
sumber
7
Diskusi yang bagus tentang un-volatiling for performance :)
awjlogan
3
Saya selalu suka menyebutkan definisi volatile dalam ISO / IEC 9899: 1999 6.7.3 (6): An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Lebih banyak orang harus membacanya.
Jeroen3
3
Mungkin perlu disebutkan bahwa cli/ seiini solusi yang terlalu berat jika satu-satunya tujuan Anda adalah mencapai penghalang memori, bukan mencegah gangguan. Makro ini menghasilkan aktual cli/ seiinstruksi, dan juga memori clobber, dan clobbering inilah yang menghasilkan penghalang. Untuk hanya memiliki penghalang memori tanpa menonaktifkan interupsi Anda dapat mendefinisikan makro Anda sendiri dengan tubuh seperti __asm__ __volatile__("":::"memory")(yaitu kode perakitan kosong dengan memory clobber).
Ruslan
3
@NicHartley No. C17 5.1.2.3 §6 mendefinisikan perilaku yang dapat diamati : "Akses ke objek yang mudah menguap dievaluasi secara ketat sesuai dengan aturan mesin abstrak." Standar C tidak begitu jelas tentang di mana hambatan memori dibutuhkan secara keseluruhan. Pada akhir ekspresi yang menggunakan volatileada titik sekuens, dan segala sesuatu setelahnya harus "diurutkan setelah". Berarti ungkapan itu adalah semacam penghalang memori. Vendor kompiler memilih untuk menyebarkan semua jenis mitos untuk menempatkan tanggung jawab hambatan memori pada programmer tetapi itu melanggar aturan "mesin abstrak".
Lundin
2
@JimmyB Volatile lokal mungkin berguna untuk kode seperti volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Maciej Piechotka
13

Kata kunci yang mudah menguap memberi tahu kompiler bahwa akses ke variabel memiliki efek yang dapat diamati. Itu berarti setiap kali kode sumber Anda menggunakan variabel kompiler HARUS membuat akses ke variabel. Jadilah akses baca atau tulis.

Efeknya adalah setiap perubahan pada variabel di luar aliran kode normal juga akan diamati oleh kode. Misal jika interrupt handler mengubah nilainya. Atau jika variabel sebenarnya adalah register perangkat keras yang berubah sendiri.

Manfaat besar ini juga merupakan kerugiannya. Setiap akses tunggal ke variabel melewati variabel dan nilainya tidak pernah disimpan dalam register untuk akses yang lebih cepat untuk jumlah waktu berapa pun. Itu berarti variabel volatil akan lambat. Besarnya lebih lambat. Jadi hanya gunakan volatile yang sebenarnya diperlukan.

Dalam kasus Anda, sejauh yang Anda tunjukkan kode, variabel global hanya diubah ketika Anda memperbaruinya sendiri adcValue = readADC();. Kompiler tahu kapan ini terjadi dan tidak akan pernah memegang nilai adcValue dalam register di sesuatu yang mungkin memanggil readFromADC()fungsi. Atau fungsi apa pun yang tidak diketahuinya. Atau apapun yang akan memanipulasi pointer yang mungkin menunjuk adcValuedan semacamnya. Benar-benar tidak perlu bergejolak karena variabel tidak pernah berubah dengan cara yang tidak terduga.

Goswin von Brederlow
sumber
6
Saya setuju dengan jawaban ini tetapi "magnitude lebih lambat" terdengar terlalu mengerikan.
kkrambo
6
Register CPU dapat diakses dalam waktu kurang dari satu siklus cpu pada CPU superscalar modern. Di sisi lain, akses ke memori yang tidak di-cache yang sebenarnya (ingat beberapa perangkat keras eksternal akan mengubahnya, sehingga tidak ada cache CPU yang diizinkan) dapat berada dalam kisaran 100-300 siklus CPU. Jadi, ya, sangat besar. Tidak akan terlalu buruk pada AVR atau pengontrol mikro serupa tetapi pertanyaannya tidak menentukan perangkat keras.
Goswin von Brederlow
7
Dalam sistem embedded (mikrokontroler), penalti untuk akses RAM seringkali jauh lebih sedikit. AVR, misalnya, hanya mengambil dua siklus CPU untuk membaca dari atau menulis ke RAM (gerakan register-register membutuhkan satu siklus), sehingga penghematan menjaga hal-hal dalam pendekatan register (tetapi tidak pernah benar-benar mencapai) maks. 2 siklus jam per akses. - Tentu saja, secara relatif, menyimpan nilai dari register X ke RAM, kemudian segera memuat kembali nilai itu ke register X untuk perhitungan lebih lanjut akan mengambil 2x2 = 4 daripada 0 siklus (ketika hanya menyimpan nilai dalam X), dan karenanya tak terhingga lebih lambat :)
JimmyB
1
Ini 'magnitude lebih lambat' dalam konteks "menulis atau membaca dari variabel tertentu", ya. Namun dalam konteks program lengkap yang kemungkinan besar secara signifikan lebih dari membaca dari / menulis ke satu variabel berulang-ulang, tidak, tidak juga. Dalam hal ini, perbedaan keseluruhan cenderung 'kecil hingga dapat diabaikan'. Perhatian harus diambil, ketika membuat pernyataan tentang kinerja, untuk mengklarifikasi jika pernyataan terkait dengan satu operasi tertentu atau pada suatu program secara keseluruhan. Memperlambat operasi yang jarang digunakan dengan faktor ~ 300x hampir tidak pernah menjadi masalah besar.
Agustus
1
Maksud Anda, kalimat terakhir itu? Itu jauh lebih berarti dalam arti "optimasi prematur adalah akar dari semua kejahatan". Jelas Anda tidak boleh menggunakan volatilesegala sesuatu hanya karena , tetapi Anda juga tidak boleh menghindarinya dalam kasus-kasus di mana Anda berpikir itu secara sah diperlukan karena kekhawatiran kinerja preemptive, juga.
Agustus
9

Penggunaan utama kata kunci yang tidak stabil pada aplikasi C yang disematkan adalah untuk menandai variabel global yang ditulis ke dalam interrupt handler. Ini tentu bukan opsional dalam hal ini.

Tanpa itu, kompiler tidak dapat membuktikan bahwa nilai pernah ditulis setelah inisialisasi, karena tidak dapat membuktikan interrupt handler pernah dipanggil. Oleh karena itu ia berpikir dapat mengoptimalkan variabel dari keberadaan.

vicatcu
sumber
2
Tentu saja ada kegunaan praktis lainnya, tetapi ini adalah yang paling umum.
vicatcu
1
Jika nilainya hanya dibaca dalam ISR (dan diubah dari main ()), Anda berpotensi harus menggunakan volatile juga untuk menjamin akses ATOMIC untuk variabel multi-byte.
Rev1.0
15
@ Rev1.0 Tidak, volatile tidak menjamin aromisitas. Kekhawatiran itu harus ditangani secara terpisah.
Chris Stratton
1
Tidak ada pembacaan dari perangkat keras maupun gangguan dalam kode yang dipasang. Anda mengasumsikan hal-hal dari pertanyaan yang tidak ada. Itu tidak bisa dijawab dalam bentuk saat ini.
Lundin
3
+ msgstr "tandai variabel global yang ditulis dalam sebuah interrupt handler" nggak. Ini untuk menandai suatu variabel; global atau lainnya; bahwa itu dapat diubah oleh sesuatu di luar pemahaman kompiler. Interupsi tidak diperlukan. Itu bisa dibagi memori atau seseorang menempelkan penyelidikan ke dalam memori (yang terakhir tidak direkomendasikan untuk sesuatu yang lebih modern dari 40 tahun)
UKMonkey
9

Ada dua kasus di mana Anda harus menggunakan volatilesistem embedded.

  • Saat membaca dari register perangkat keras.

    Itu berarti, register yang dipetakan memori itu sendiri, bagian dari periferal perangkat keras di dalam MCU. Kemungkinan akan memiliki beberapa nama samar seperti "ADC0DR". Register ini harus didefinisikan dalam kode C, baik melalui beberapa peta register yang dikirimkan oleh vendor alat, atau oleh Anda sendiri. Untuk melakukannya sendiri, Anda harus melakukannya (dengan asumsi 16 bit mendaftar):

    #define ADC0DR (*(volatile uint16_t*)0x1234)

    di mana 0x1234 adalah alamat di mana MCU telah memetakan register. Karena volatilesudah menjadi bagian dari makro di atas, setiap akses ke sana akan memenuhi syarat volatile. Jadi kode ini baik-baik saja:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • Saat berbagi variabel antara ISR dan kode terkait menggunakan hasil ISR.

    Jika Anda memiliki sesuatu seperti ini:

    uint16_t adc_data = 0;
    
    void adc_stuff (void)
    {
      if(adc_data > 0)
      {
        do_stuff(adc_data);
      } 
    }
    
    interrupt void ADC0_interrupt (void)
    {
      adc_data = ADC0DR;
    }

    Maka kompiler mungkin berpikir: "adc_data selalu 0 karena tidak diperbarui di mana pun. Dan fungsi ADC0_interrupt () tidak pernah dipanggil, sehingga variabel tidak dapat diubah". Kompiler biasanya tidak menyadari bahwa interupsi dipanggil oleh perangkat keras, bukan oleh perangkat lunak. Jadi kompiler pergi dan menghapus kode if(adc_data > 0){ do_stuff(adc_data); }karena dianggap tidak pernah benar, menyebabkan bug yang sangat aneh dan sulit di-debug.

    Dengan mendeklarasikan adc_data volatile, kompiler tidak diperbolehkan untuk membuat asumsi seperti itu dan tidak diizinkan untuk mengoptimalkan akses ke variabel.


Catatan penting:

  • ISR harus selalu dinyatakan di dalam driver perangkat keras. Dalam hal ini, ADC ISR harus berada di dalam driver ADC. Tidak lain adalah pengemudi harus berkomunikasi dengan ISR - yang lainnya adalah pemrograman spageti.

  • Saat menulis C, semua komunikasi antara ISR dan program latar belakang harus dilindungi terhadap kondisi ras. Selalu , setiap saat, tanpa pengecualian. Ukuran bus data MCU tidak masalah, karena bahkan jika Anda melakukan satu salinan 8 bit dalam C, bahasa tidak dapat menjamin keaslian operasi. Tidak, kecuali Anda menggunakan fitur C11 _Atomic. Jika fitur ini tidak tersedia, Anda harus menggunakan beberapa cara semaphore atau menonaktifkan interupsi saat membaca dll. Assembler inline adalah pilihan lain. volatiletidak menjamin atomicity.

    Apa yang bisa terjadi adalah ini:
    Nilai -Load dari stack ke register
    -Interrupt terjadi
    -Gunakan nilai dari register

    Dan kemudian tidak masalah jika bagian "nilai pakai" adalah instruksi tunggal. Sayangnya, sebagian besar dari semua programmer sistem embedded tidak menyadari hal ini, mungkin menjadikannya bug sistem embedded paling umum yang pernah ada. Selalu terputus-putus, sulit diprovokasi, sulit ditemukan.


Contoh driver ADC yang ditulis dengan benar akan terlihat seperti ini (dengan asumsi C11 _Atomictidak tersedia):

adc.h

// adc.h
#ifndef ADC_H
#define ADC_H

/* misc init routines here */

uint16_t adc_get_val (void);

#endif

adc.c

// adc.c
#include "adc.h"

#define ADC0DR (*(volatile uint16_t*)0x1234)

static volatile bool semaphore = false;
static volatile uint16_t adc_val = 0;

uint16_t adc_get_val (void)
{
  uint16_t result;
  semaphore = true;
    result = adc_val;
  semaphore = false;
  return result;
}

interrupt void ADC0_interrupt (void)
{
  if(!semaphore)
  {
    adc_val = ADC0DR;
  }
}
  • Kode ini mengasumsikan bahwa interupsi tidak dapat diganggu dengan sendirinya. Pada sistem seperti itu, boolean sederhana dapat bertindak sebagai semaphore, dan itu tidak perlu atomik, karena tidak ada salahnya jika interupsi terjadi sebelum boolean diatur. Sisi bawah dari metode yang disederhanakan di atas adalah bahwa ia akan membuang pembacaan ADC ketika kondisi balapan terjadi, menggunakan nilai sebelumnya sebagai gantinya. Ini dapat dihindari juga, tetapi kemudian kode berubah lebih kompleks.

  • Di sini volatilemelindungi terhadap bug pengoptimalan. Itu tidak ada hubungannya dengan data yang berasal dari register perangkat keras, hanya bahwa data dibagikan dengan ISR.

  • staticmelindungi terhadap pemrograman spageti dan polusi namespace, dengan membuat variabel lokal ke pengemudi. (Ini bagus di aplikasi single-core, single-thread, tetapi tidak di yang multi-threaded.)

Lundin
sumber
Sulit untuk debug relatif, jika kode dihapus, Anda akan melihat bahwa kode Anda dihargai telah hilang - itu adalah pernyataan yang cukup berani bahwa ada sesuatu yang salah. Tapi saya setuju, mungkin ada efek yang sangat aneh dan sulit untuk debug.
Arsenal
@Arenal Jika Anda memiliki debugger yang bagus yang menghubungkan assembler dengan C, dan Anda tahu setidaknya sedikit asm, maka ya itu bisa mudah dikenali. Tetapi untuk kode kompleks yang lebih besar, sejumlah besar asm yang dihasilkan mesin tidak mudah untuk dilalui. Atau jika Anda tidak tahu ASM. Atau jika debugger Anda adalah omong kosong dan tidak menunjukkan asm (cougheclipsecough).
Lundin
Bisa jadi saya sedikit dimanjakan dengan menggunakan debuggers Lauterbach saat itu. Jika Anda mencoba menetapkan breakpoint dalam kode yang dioptimalkan, itu akan membuatnya berbeda dan Anda tahu ada sesuatu yang terjadi di sana.
Arsenal
@ Thomas Yep, jenis campuran C / asm yang bisa Anda dapatkan di Lauterbach sama sekali tidak standar. Sebagian besar debugger menampilkan asm di jendela terpisah, jika sama sekali.
Lundin
semaphoreseharusnya volatile! Bahkan, itu yang paling kasus penggunaan dasar Wich panggilan untuk volatile: Signal sesuatu dari satu konteks eksekusi yang lain. - Dalam contoh Anda, kompiler hanya bisa menghilangkan semaphore = true;karena 'melihat' bahwa nilainya tidak pernah dibaca sebelum ditimpa oleh semaphore = false;.
JimmyB
5

Dalam cuplikan kode yang disajikan dalam pertanyaan, belum ada alasan untuk menggunakan volatile. Tidak relevan bahwa nilai adcValueberasal dari ADC. Dan adcValuemenjadi global harus membuat Anda curiga apakah adcValueharus volatile tetapi itu bukan alasan dengan sendirinya.

Menjadi global adalah petunjuk karena membuka kemungkinan yang adcValuedapat diakses dari lebih dari satu konteks program. Konteks program mencakup penangan interupsi dan tugas RTOS. Jika variabel global diubah oleh satu konteks maka konteks program lainnya tidak dapat menganggap mereka tahu nilai dari akses sebelumnya. Setiap konteks harus membaca kembali nilai variabel setiap kali mereka menggunakannya karena nilainya mungkin telah diubah dalam konteks program yang berbeda. Konteks program tidak sadar ketika interupsi atau pengalihan tugas terjadi sehingga harus mengasumsikan bahwa variabel global apa pun yang digunakan oleh beberapa konteks dapat berubah di antara akses variabel apa pun karena kemungkinan pengalihan konteks. Ini adalah tujuan dari deklarasi volatile. Ini memberitahu kompiler bahwa variabel ini dapat berubah di luar konteks Anda jadi bacalah setiap akses dan jangan menganggap Anda sudah tahu nilainya.

Jika variabel dipetakan ke alamat perangkat keras, maka perubahan yang dilakukan oleh perangkat keras secara efektif adalah konteks lain di luar konteks program Anda. Jadi pemetaan memori juga merupakan petunjuk. Misalnya, jika readADC()fungsi Anda mengakses nilai yang dipetakan memori untuk mendapatkan nilai ADC maka variabel yang dipetakan memori mungkin harus berubah-ubah.

Jadi kembali ke pertanyaan Anda, jika ada lebih banyak kode Anda dan adcValuedapat diakses oleh kode lain yang berjalan dalam konteks yang berbeda, maka ya, adcValueharus volatile.

krambo
sumber
4

"Variabel global yang berubah langsung dari perangkat keras"

Hanya karena nilainya berasal dari beberapa register ADC perangkat keras, tidak berarti bahwa nilai tersebut "langsung" diubah oleh perangkat keras.

Dalam contoh Anda, Anda cukup memanggil readADC (), yang mengembalikan beberapa nilai register ADC. Ini bagus untuk kompiler, mengetahui bahwa adcValue diberi nilai baru pada saat itu.

Akan berbeda jika Anda menggunakan rutin interupsi ADC untuk menetapkan nilai baru, yang disebut ketika nilai ADC baru siap. Dalam hal itu, kompiler tidak akan memiliki petunjuk tentang kapan ISR yang sesuai dipanggil dan dapat memutuskan bahwa adcValue tidak akan diakses dengan cara ini. Di sinilah volatile akan membantu.

Rev1.0
sumber
1
Karena kode Anda tidak pernah "memanggil" fungsi ISR, Compiler melihat bahwa variabel hanya diperbarui dalam fungsi yang tidak dipanggil oleh siapa pun. Jadi kompiler mengoptimalkannya.
Swanand
1
Itu tergantung dari sisa kode, jika adcValue tidak dibaca di mana saja (seperti hanya membaca debugger), atau jika itu hanya dibaca sekali di satu tempat, kompiler kemungkinan akan mengoptimalkannya.
Damien
2
@Damien: Selalu "tergantung", tetapi saya bertujuan untuk menjawab pertanyaan aktual "Apakah saya harus menggunakan kata kunci yang tidak stabil dalam kasus ini?" sesingkat mungkin.
Rev1.0
4

Perilaku volatileargumen sangat tergantung pada kode Anda, kompiler, dan optimasi yang dilakukan.

Ada dua kasus penggunaan yang saya gunakan secara pribadi volatile:

  • Jika ada variabel yang ingin saya lihat dengan debugger, tetapi kompiler telah mengoptimalkannya (berarti telah menghapusnya karena ternyata tidak perlu memiliki variabel ini), menambahkan volatileakan memaksa kompiler untuk menyimpannya dan karenanya dapat dilihat pada debug.

  • Jika variabel dapat berubah "di luar kode", biasanya jika Anda memiliki beberapa perangkat keras yang mengaksesnya, atau jika Anda memetakan variabel secara langsung ke alamat.

Dalam embedded juga kadang-kadang ada beberapa bug di kompiler, melakukan optimasi yang sebenarnya tidak berfungsi, dan kadang-kadang volatilebisa menyelesaikan masalah.

Mengingat Anda variabel Anda dideklarasikan secara global, itu mungkin tidak akan dioptimalkan, selama variabel sedang digunakan pada kode, setidaknya ditulis dan dibaca.

Contoh:

void test()
{
    int a = 1;
    printf("%i", a);
}

Dalam hal ini, variabel mungkin akan dioptimalkan untuk printf ("% i", 1);

void test()
{
    volatile int a = 1;
    printf("%i", a);
}

tidak akan dioptimalkan

Yang lainnya:

void delay1Ms()
{
    unsigned int i;
    for (i=0; i<10; i++)
    {
        delay10us( 10);
    }
}

Dalam hal ini, kompiler dapat mengoptimalkan dengan (jika Anda mengoptimalkan kecepatan) dan dengan demikian membuang variabel

void delay1Ms()
{
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
}

Untuk kasus penggunaan Anda, "itu mungkin tergantung" pada sisa kode Anda, bagaimana adcValuedigunakan di tempat lain dan pengaturan versi / optimisasi yang Anda gunakan.

Terkadang menjengkelkan memiliki kode yang berfungsi tanpa optimasi, tetapi rusak begitu dioptimalkan.

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

Ini mungkin dioptimalkan untuk printf ("% i", readADC ());

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
  callAnotherFunction(adcValue);
}

-

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

void anotherFunction()
{
   // Do something with adcValue
}

Ini mungkin tidak akan dioptimalkan, tetapi Anda tidak pernah tahu "seberapa bagus kompilernya" dan mungkin berubah dengan parameter kompiler. Biasanya kompiler dengan optimasi yang baik dilisensikan.

Damien
sumber
1
Misalnya a = 1; b = a; dan c = b; kompiler mungkin berpikir tunggu sebentar, a dan b tidak berguna, mari kita letakkan 1 ke c secara langsung. Tentu saja Anda tidak akan melakukan itu dalam kode Anda, tetapi kompiler lebih baik daripada Anda menemukan ini, juga jika Anda mencoba untuk menulis kode yang dioptimalkan segera itu tidak akan terbaca.
Damien
2
Kode yang benar dengan kompiler yang benar tidak akan rusak dengan optimisasi dihidupkan. Kebenaran dari kompiler adalah sedikit masalah, tetapi setidaknya dengan IAR saya belum menemukan situasi di mana optimasi mengarah pada pemecahan kode di mana seharusnya tidak.
Arsenal
5
Banyak kasus di mana optimisasi memecahkan kode adalah ketika Anda menjelajah ke wilayah UB juga ..
pipe
2
Ya, efek samping dari volatile adalah ia dapat membantu debugging. Tapi itu bukan alasan yang baik untuk menggunakan volatile. Anda mungkin harus mematikan optimasi jika debugging mudah adalah tujuan Anda. Jawaban ini bahkan tidak menyebutkan interupsi.
kkrambo
2
Menambahkan ke argumen debugging, volatilememaksa kompiler untuk menyimpan variabel dalam RAM, dan memperbarui RAM itu segera setelah nilai ditugaskan ke variabel. Sebagian besar waktu, kompiler tidak 'menghapus' variabel, karena kita biasanya tidak menulis tugas tanpa efek, tetapi mungkin memutuskan untuk menyimpan variabel dalam beberapa register CPU dan kemudian atau tidak pernah menulis nilai register itu ke RAM. Debugger sering gagal menemukan register CPU tempat variabel disimpan dan karenanya tidak dapat menunjukkan nilainya.
JimmyB
1

Banyak penjelasan teknis tetapi saya ingin berkonsentrasi pada aplikasi praktis.

Kata volatilekunci memaksa kompiler untuk membaca atau menulis nilai variabel dari memori setiap kali digunakan. Biasanya kompiler akan mencoba untuk mengoptimalkan tetapi tidak membuat membaca dan menulis yang tidak perlu, misalnya dengan menyimpan nilai dalam register CPU daripada mengakses memori setiap kali.

Ini memiliki dua kegunaan utama dalam kode tertanam. Pertama digunakan untuk register perangkat keras. Register perangkat keras dapat berubah, mis. Register hasil ADC dapat ditulis oleh perangkat ADC. Register perangkat keras juga dapat melakukan tindakan saat diakses. Contoh umum adalah register data UART, yang sering membersihkan tanda interupsi ketika dibaca.

Kompiler biasanya akan mencoba untuk mengoptimalkan membaca berulang dan menulis register dengan asumsi bahwa nilai tidak akan pernah berubah sehingga tidak perlu terus mengaksesnya, tetapi volatilekata kunci akan memaksanya untuk melakukan operasi baca setiap kali.

Penggunaan umum kedua adalah untuk variabel yang digunakan oleh kode interrupt dan non-interrupt. Interupsi tidak dipanggil secara langsung, sehingga kompiler tidak dapat menentukan kapan mereka akan mengeksekusi, dan dengan demikian mengasumsikan bahwa setiap akses di dalam interupsi tidak pernah terjadi. Karena volatilekata kunci memaksa kompiler untuk mengakses variabel setiap saat, asumsi ini dihapus.

Penting untuk dicatat bahwa volatilekata kunci bukan solusi lengkap untuk masalah ini, dan harus berhati-hati untuk menghindarinya. Sebagai contoh, pada sistem 8 bit variabel 16 bit memerlukan dua akses memori untuk membaca atau menulis, dan bahkan jika kompiler dipaksa untuk membuat akses tersebut terjadi secara berurutan, dan dimungkinkan untuk perangkat keras untuk bertindak pada akses pertama atau interupsi terjadi antara keduanya.

pengguna
sumber
0

Dengan tidak adanya volatilekualifikasi, nilai objek dapat disimpan di lebih dari satu tempat selama bagian kode tertentu. Pertimbangkan, misalnya, diberikan sesuatu seperti:

int foo;
int someArray[64];
void test(void)
{
  int i;
  foo = 0;
  for (i=0; i<64; i++)
    if (someArray[i] > 0)
      foo++;
}

Pada hari-hari awal C, kompiler akan memproses pernyataan itu

foo++;

melalui langkah-langkah:

load foo into a register
increment that register
store that register back to foo

Kompiler yang lebih canggih, bagaimanapun, akan mengakui bahwa jika nilai "foo" disimpan dalam register selama loop, itu hanya perlu dimuat sekali sebelum loop, dan disimpan sekali setelah. Namun, selama loop, itu berarti bahwa nilai "foo" disimpan di dua tempat - di dalam penyimpanan global, dan di dalam register. Ini tidak akan menjadi masalah jika kompilator dapat melihat semua cara "foo" dapat diakses dalam loop, tetapi dapat menyebabkan masalah jika nilai "foo" diakses dalam beberapa mekanisme yang tidak diketahui kompilator ( seperti interrupt handler).

Mungkin ada kemungkinan bagi penulis Standar untuk menambahkan kualifikasi baru yang secara eksplisit akan mengundang kompiler untuk melakukan optimasi seperti itu, dan mengatakan bahwa semantik kuno akan berlaku jika tidak ada, tetapi kasus-kasus di mana optimasi sangat bermanfaat melebihi jumlah yang mana hal itu akan menimbulkan masalah, sehingga Standar malah memungkinkan penyusun untuk berasumsi bahwa optimisasi seperti itu aman tanpa adanya bukti bahwa mereka tidak. Tujuan volatilekata kunci adalah untuk menyediakan bukti tersebut.

Beberapa titik pertentangan antara beberapa penulis kompiler dan pemrogram terjadi dengan situasi seperti:

unsigned short volatile *volatile output_ptr;
unsigned volatile output_count;

void interrupt_handler(void)
{
  if (output_count)
  {
    *((unsigned short*)0xC0001230) = *output_ptr; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 1; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 0; // Hardware I/O register
    output_ptr++;
    output_count--;
  }
}

void output_data_via_interrupt(unsigned short *dat, unsigned count)
{
  output_ptr = dat;
  output_count = count;
  while(output_count)
     ; // Wait for interrupt to output the data
}

unsigned short output_buffer[10];

void test(void)
{
  output_buffer[0] = 0x1234;
  output_data_via_interrupt(output_buffer, 1);
  output_buffer[0] = 0x2345;
  output_buffer[1] = 0x6789;
  output_data_via_interrupt(output_buffer,2);
}

Secara historis, sebagian besar kompiler akan memungkinkan untuk kemungkinan bahwa menulis volatilelokasi penyimpanan dapat memicu efek samping yang sewenang-wenang, dan menghindari caching nilai apa pun di register di toko tersebut, atau mereka akan menahan diri dari caching nilai dalam register di seluruh panggilan ke fungsi yang tidak memenuhi syarat "inline", dan dengan demikian akan menulis 0x1234 ke output_buffer[0], mengatur hal-hal untuk menampilkan data, menunggu untuk menyelesaikan, kemudian menulis 0x2345 untuk output_buffer[0], dan melanjutkan dari sana. Standar tidak memerlukan implementasi untuk memperlakukan tindakan menyimpan alamatoutput_buffer menjadi avolatile-Penunjuk yang memenuhi syarat sebagai tanda bahwa sesuatu mungkin terjadi padanya melalui berarti kompiler tidak mengerti, namun, karena penulis pikir kompiler penulis kompiler yang ditujukan untuk berbagai platform dan tujuan akan mengenali ketika melakukan itu akan melayani tujuan-tujuan tersebut pada platform tersebut tanpa harus diberi tahu. Akibatnya, beberapa kompiler "pintar" seperti gcc dan dentang akan menganggap bahwa meskipun alamat output_bufferditulis ke pointer yang memenuhi syarat volatile antara dua toko output_buffer[0], itu bukan alasan untuk menganggap bahwa apa pun mungkin peduli tentang nilai yang dipegang pada objek di waktu itu.

Lebih lanjut, sementara pointer yang dilemparkan langsung dari bilangan bulat jarang digunakan untuk tujuan apa pun selain untuk memanipulasi hal-hal dengan cara yang tidak mungkin dipahami oleh penyusun, Standar lagi tidak mengharuskan penyusun untuk memperlakukan akses semacam itu sebagai volatile. Akibatnya, penulisan pertama *((unsigned short*)0xC0001234)dapat dihilangkan oleh kompiler "pintar" seperti gcc dan dentang, karena pengelola kompiler semacam itu lebih suka mengklaim bahwa kode yang mengabaikan kualifikasi hal-hal seperti volatile"rusak" daripada mengakui bahwa kompatibilitas dengan kode seperti itu berguna . Banyak file header yang disediakan vendor menghilangkan volatilekualifikasi, dan kompiler yang kompatibel dengan file header yang disediakan vendor lebih berguna daripada yang tidak.

supercat
sumber