ความหมายของ volatile
volatile
บอกคอมไพเลอร์ว่าค่าของตัวแปรอาจเปลี่ยนแปลงได้โดยที่ไม่รู้ว่าคอมไพเลอร์ ดังนั้นคอมไพเลอร์ไม่สามารถสันนิษฐานได้ว่าค่าไม่เปลี่ยนแปลงเพียงเพราะโปรแกรม C ดูเหมือนว่าจะไม่เปลี่ยนแปลง
ในทางกลับกันก็หมายความว่าอาจจำเป็นต้องใช้ค่าของตัวแปร (อ่าน) ที่อื่นที่คอมไพเลอร์ไม่ทราบดังนั้นจึงต้องตรวจสอบให้แน่ใจว่าการมอบหมายให้กับตัวแปรนั้นเป็นการดำเนินการเขียน
ใช้กรณี
volatile
จำเป็นเมื่อ
- เป็นตัวแทนของฮาร์ดแวร์รีจิสเตอร์ (หรือหน่วยความจำที่แมป I / O) เป็นตัวแปร - แม้ว่าการลงทะเบียนจะไม่สามารถอ่านได้คอมไพเลอร์ต้องไม่เพียงแค่ข้ามการดำเนินการเขียนความคิด "โปรแกรมเมอร์ Stupid พยายามเก็บค่าในตัวแปรที่เขา / เธอ จะไม่เคยอ่านกลับมาเขา / เธอจะไม่แม้แต่สังเกตเห็นถ้าเราไม่เขียน " ในทางกลับกันแม้ว่าโปรแกรมไม่เคยเขียนค่าลงในตัวแปรค่าของมันอาจยังคงมีการเปลี่ยนแปลงโดยฮาร์ดแวร์
- การแชร์ตัวแปรระหว่างบริบทการดำเนินการ (เช่น ISRs / โปรแกรมหลัก) (ดูที่คำตอบของ @ kkramo)
ผลของการ volatile
เมื่อมีการประกาศตัวแปรvolatile
คอมไพเลอร์ต้องตรวจสอบให้แน่ใจว่าการกำหนดทุกอย่างในรหัสโปรแกรมสะท้อนให้เห็นในการดำเนินการเขียนจริงและการอ่านในรหัสโปรแกรมทุกครั้งจะอ่านค่าจากหน่วยความจำ (mmapped)
สำหรับตัวแปรที่ไม่ลบเลือนคอมไพเลอร์ถือว่ามันรู้ว่า / เมื่อค่าของตัวแปรเปลี่ยนแปลงและสามารถปรับโค้ดให้เหมาะสมในวิธีที่ต่างกัน
สำหรับหนึ่งคอมไพเลอร์สามารถลดจำนวนการอ่าน / เขียนไปยังหน่วยความจำโดยการรักษาค่าในการลงทะเบียน CPU
ตัวอย่าง:
void uint8_t compute(uint8_t input) {
uint8_t result = input + 2;
result = result * 2;
if ( result > 100 ) {
result -= 100;
}
return result;
}
ที่นี่คอมไพเลอร์อาจจะไม่จัดสรร RAM ให้กับresult
ตัวแปรและจะไม่เก็บค่ากลางไว้ที่ใดก็ได้ แต่ใน CPU register
หากresult
มีความผันผวนเกิดขึ้นทุกครั้งresult
ในรหัส C จะต้องรวบรวมเพื่อดำเนินการเข้าถึง RAM (หรือพอร์ต I / O) นำไปสู่ประสิทธิภาพที่ลดลง
ประการที่สองคอมไพเลอร์อาจสั่งซื้อใหม่การดำเนินการกับตัวแปรที่ไม่ลบเลือนสำหรับประสิทธิภาพและ / หรือขนาดรหัส ตัวอย่างง่ายๆ:
int a = 99;
int b = 1;
int c = 99;
สามารถสั่งซื้ออีกครั้งเพื่อ
int a = 99;
int c = 99;
int b = 1;
ซึ่งอาจบันทึกคำสั่งแอสเซมเบลอร์ได้เนื่องจากค่า99
ไม่จำเป็นต้องโหลดสองครั้ง
ถ้าa
, b
และc
มีความผันผวนคอมไพเลอร์จะต้องเปล่งคำแนะนำที่กำหนดค่าในลำดับที่แน่นอนที่พวกเขาจะได้รับในโปรแกรม
ตัวอย่างคลาสสิกอื่น ๆ เป็นเช่นนี้:
volatile uint8_t signal;
void waitForSignal() {
while ( signal == 0 ) {
// Do nothing.
}
}
หากในกรณีนี้signal
ไม่ได้volatile
คอมไพเลอร์จะ 'คิด' ซึ่งwhile( signal == 0 )
อาจเป็นอนันต์ลูป (เพราะsignal
จะไม่มีการเปลี่ยนแปลงด้วยโค้ดภายในลูป ) และอาจสร้างเทียบเท่า
void waitForSignal() {
if ( signal != 0 ) {
return;
} else {
while(true) { // <-- Endless loop!
// do nothing.
}
}
}
คำนึงถึงการจัดการvolatile
คุณค่า
ตามที่ระบุไว้ข้างต้นvolatile
ตัวแปรสามารถแนะนำการปรับประสิทธิภาพเมื่อเข้าถึงได้บ่อยกว่าที่ต้องการจริง ในการลดปัญหานี้คุณสามารถ "ยกเลิกการลบ" ค่าโดยกำหนดให้กับตัวแปรที่ไม่ลบเลือนเช่น
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
}
สิ่งนี้อาจเป็นประโยชน์โดยเฉพาะอย่างยิ่งใน ISR ที่คุณต้องการให้เร็วที่สุดเท่าที่จะทำได้ไม่เข้าถึงฮาร์ดแวร์หรือหน่วยความจำเดียวกันหลาย ๆ ครั้งเมื่อคุณรู้ว่าไม่จำเป็นเพราะค่าจะไม่เปลี่ยนแปลงในขณะที่ ISR ของคุณกำลังทำงาน นี่เป็นเรื่องปกติเมื่อ ISR คือ 'ผู้สร้าง' ของค่าสำหรับตัวแปรเช่นเดียวกับsysTickCount
ในตัวอย่างข้างต้น ใน AVR มันจะเจ็บปวดโดยเฉพาะอย่างยิ่งที่จะให้ฟังก์ชั่นdoSysTick()
เข้าถึงสี่ไบต์เดียวกันในหน่วยความจำ (สี่คำสั่ง = 8 CPU รอบต่อการเข้าถึงsysTickCount
) ห้าหรือหกครั้งแทนที่จะเป็นเพียงสองครั้งเพราะโปรแกรมเมอร์รู้ว่าค่าจะไม่ เปลี่ยนจากรหัสอื่นในขณะที่เขา / เธอdoSysTick()
ทำงาน
ด้วยเคล็ดลับนี้คุณจำเป็นต้องทำสิ่งเดียวกันกับที่คอมไพเลอร์ทำกับตัวแปรที่ไม่ลบเลือนเช่นอ่านจากหน่วยความจำก็ต่อเมื่อต้องเก็บค่าไว้ในรีจิสเตอร์บางครั้งและเขียนกลับไปที่หน่วยความจำก็ต่อเมื่อ ; แต่คราวนี้คุณรู้ดีกว่าคอมไพเลอร์หาก / เมื่ออ่าน / เขียนต้องเกิดขึ้นดังนั้นคุณจึงต้องลดความซับซ้อนของคอมไพเลอร์จากงานเพิ่มประสิทธิภาพนี้และทำมันเอง
ข้อ จำกัด ของ volatile
การเข้าถึงที่ไม่ใช่อะตอม
volatile
ไม่ได้ให้การเข้าถึงแบบอะตอมมิกกับตัวแปรหลายคำ สำหรับกรณีที่คุณจะต้องให้การยกเว้นร่วมกันด้วยวิธีอื่น ๆนอกเหนือvolatile
จากการใช้ บน AVR คุณสามารถใช้ATOMIC_BLOCK
จาก<util/atomic.h>
หรือcli(); ... sei();
โทรง่าย ๆ มาโครที่เกี่ยวข้องทำหน้าที่เป็นกำแพงกั้นหน่วยความจำเช่นกันซึ่งเป็นสิ่งสำคัญเมื่อพูดถึงลำดับการเข้าถึง:
คำสั่งดำเนินการ
volatile
กำหนดลำดับการดำเนินการที่เข้มงวดเฉพาะกับตัวแปรที่เปลี่ยนแปลงได้อื่น ๆ ซึ่งหมายความว่าตัวอย่างเช่น
volatile int i;
volatile int j;
int a;
...
i = 1;
a = 99;
j = 2;
รับประกันได้ว่าจะเป็นครั้งแรกกำหนด 1 i
และจากนั้นกำหนด j
2 อย่างไรก็ตามไม่รับประกันว่าa
จะได้รับมอบหมายระหว่าง; คอมไพเลอร์อาจจะได้รับมอบหมายว่าก่อนหรือหลังข้อมูลโค้ดโดยทั่วไปในเวลาใด ๆ ขึ้นกับครั้งแรก (มองเห็น) a
อ่าน
หากไม่ใช่เพราะสิ่งกีดขวางหน่วยความจำของมาโครที่กล่าวมาคอมไพเลอร์จะได้รับอนุญาตให้แปล
uint32_t x;
cli();
x = volatileVar;
sei();
ไปยัง
x = volatileVar;
cli();
sei();
หรือ
cli();
sei();
x = volatileVar;
(เพื่อประโยชน์ของความสมบูรณ์ฉันต้องบอกว่าอุปสรรคหน่วยความจำเช่นเดียวกับที่ส่อให้เห็นโดยมาโคร sei / cli จริง ๆ แล้วอาจยกเลิกการใช้งานvolatile
หากการเข้าถึงทั้งหมดถูก จำกัด ด้วยอุปสรรคเหล่านี้)