การใช้สารระเหยในการพัฒนา C แบบฝัง


44

ฉันได้อ่านบทความและคำตอบของการแลกเปลี่ยนกองซ้อนเกี่ยวกับการใช้volatileคำสำคัญเพื่อป้องกันไม่ให้คอมไพเลอร์จากการใช้การเพิ่มประสิทธิภาพใด ๆ บนวัตถุที่สามารถเปลี่ยนแปลงในวิธีที่ไม่สามารถกำหนดโดยคอมไพเลอร์

หากฉันกำลังอ่านจาก ADC (เรียกว่าตัวแปรadcValue) และฉันประกาศว่าตัวแปรนี้เป็นโกลบอลฉันควรใช้คำหลักvolatileในกรณีนี้หรือไม่?

  1. โดยไม่ต้องใช้volatileคำสำคัญ

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. การใช้volatileคำสำคัญ

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

ฉันถามคำถามนี้เพราะเมื่อทำการดีบั๊กฉันไม่เห็นความแตกต่างระหว่างทั้งสองวิธีแม้ว่าแนวทางปฏิบัติที่ดีที่สุดจะบอกว่าในกรณีของฉัน (ตัวแปรทั่วโลกที่เปลี่ยนแปลงโดยตรงจากฮาร์ดแวร์) จากนั้นจึงใช้volatileเป็นสิ่งจำเป็น


1
จำนวนของสภาพแวดล้อมการแก้ปัญหา (แน่นอน gcc) ใช้ไม่มีการเพิ่มประสิทธิภาพ การสร้างการผลิตตามปกติจะ (ขึ้นอยู่กับตัวเลือกของคุณ) สิ่งนี้สามารถนำไปสู่ความแตกต่าง 'ที่น่าสนใจ' ระหว่างงานสร้าง ดูที่แผนที่เอาท์พุทลิงเกอร์เป็นข้อมูล
ปีเตอร์สมิ ธ

22
"ในกรณีของฉัน (ตัวแปรโกลบอลที่เปลี่ยนแปลงโดยตรงจากฮาร์ดแวร์)" - ตัวแปรโกลบอลของคุณไม่ได้เปลี่ยนจากฮาร์ดแวร์แต่ใช้รหัส C ของคุณเท่านั้นซึ่งคอมไพเลอร์รับรู้ - การลงทะเบียนฮาร์ดแวร์ที่ ADC ให้ผลลัพธ์นั้นจะต้องมีความผันผวนเนื่องจากคอมไพเลอร์ไม่สามารถรู้ได้ว่าเมื่อใดค่าของมันจะเปลี่ยนแปลง (มันเปลี่ยนถ้า / เมื่อฮาร์ดแวร์ ADC เสร็จสิ้นการแปลง)
JimmyB

2
คุณเปรียบเทียบแอสเซมเบลอร์ที่สร้างโดยทั้งสองเวอร์ชันหรือไม่ ที่ควรแสดงให้คุณเห็นสิ่งที่เกิดขึ้นภายใต้ประทุน
Mawg

3
@stark: BIOS? บนไมโครคอนโทรลเลอร์หรือไม่? พื้นที่ I / O ที่แมปหน่วยความจำจะไม่สามารถแคชได้ (หากสถาปัตยกรรมยังมีแคชข้อมูลในสถานที่แรกซึ่งไม่มั่นใจ) โดยการออกแบบที่สอดคล้องกันระหว่างกฎการแคชและแผนผังหน่วยความจำ แต่สารระเหยไม่เกี่ยวข้องกับแคชของตัวควบคุมหน่วยความจำ
Ben Voigt

1
@Davislor มาตรฐานภาษาไม่จำเป็นต้องพูดอะไรเพิ่มเติมโดยทั่วไป การอ่านไปยังวัตถุระเหยจะดำเนินการโหลดจริง (แม้ว่าคอมไพเลอร์เพิ่งจะทำอย่างใดอย่างหนึ่งและมักจะรู้ว่าค่าคืออะไร) และการเขียนไปยังวัตถุดังกล่าวจะดำเนินการจัดเก็บจริง (แม้ว่าค่าเดียวกันถูกอ่านจากวัตถุ ) ดังนั้นในif(x==1) x=1;การเขียนอาจจะปรับให้เหมาะสมสำหรับความไม่ระเหยxและไม่สามารถปรับให้เหมาะสมหากxมีความผันผวน OTOH หากต้องการคำแนะนำพิเศษในการเข้าถึงอุปกรณ์ภายนอกมันขึ้นอยู่กับคุณที่จะเพิ่มคำสั่งเหล่านั้น (f.ex. ถ้าช่วงหน่วยความจำต้องทำการเขียนผ่าน)
curiousguy

คำตอบ:


87

ความหมายของ 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และจากนั้นกำหนด j2 อย่างไรก็ตามไม่รับประกันว่าaจะได้รับมอบหมายระหว่าง; คอมไพเลอร์อาจจะได้รับมอบหมายว่าก่อนหรือหลังข้อมูลโค้ดโดยทั่วไปในเวลาใด ๆ ขึ้นกับครั้งแรก (มองเห็น) aอ่าน

หากไม่ใช่เพราะสิ่งกีดขวางหน่วยความจำของมาโครที่กล่าวมาคอมไพเลอร์จะได้รับอนุญาตให้แปล

uint32_t x;

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

ไปยัง

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

หรือ

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

(เพื่อประโยชน์ของความสมบูรณ์ฉันต้องบอกว่าอุปสรรคหน่วยความจำเช่นเดียวกับที่ส่อให้เห็นโดยมาโคร sei / cli จริง ๆ แล้วอาจยกเลิกการใช้งานvolatileหากการเข้าถึงทั้งหมดถูก จำกัด ด้วยอุปสรรคเหล่านี้)


7
การอภิปรายที่ดีของการยกเลิกการ volatiling สำหรับประสิทธิภาพ :)
awjlogan

3
ฉันมักจะพูดถึงคำจำกัดความของสารระเหยใน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. ผู้คนจำนวนมากควรอ่าน
Jeroen3

3
มันอาจจะคุ้มค่าที่จะกล่าวถึงว่าcli/ seiเป็นวิธีการแก้ปัญหาที่หนักเกินไปหากเป้าหมายเดียวของคุณคือการบรรลุสิ่งกีดขวางหน่วยความจำไม่ใช่ป้องกันการขัดจังหวะ มาโครเหล่านี้สร้างcli/ seiคำสั่งจริงและหน่วยความจำแบบปิดบังเพิ่มเติมและนี่คือการปิดบังซึ่งส่งผลให้เกิดสิ่งกีดขวาง ในการมีเพียงกำแพงกั้นหน่วยความจำโดยไม่ปิดใช้งานการขัดจังหวะคุณสามารถกำหนดแมโครของคุณเองด้วยเนื้อความ__asm__ __volatile__("":::"memory")(เช่นรหัสแอสเซมบลีว่างเปล่าที่มีตัวบล็อกหน่วยความจำ)
Ruslan

3
@NicHartley หมายเลข C17 5.1.2.3 §6กำหนดพฤติกรรมที่สังเกตได้ : "การเข้าถึงวัตถุที่ระเหยได้ถูกประเมินอย่างเคร่งครัดตามกฎของเครื่องนามธรรม" มาตรฐาน C ยังไม่ชัดเจนว่าต้องใช้อุปสรรคหน่วยความจำโดยรวมในลักษณะใด ในตอนท้ายของการแสดงออกที่ใช้volatileมีจุดลำดับและทุกอย่างหลังจากนั้นจะต้อง "ลำดับหลังจาก" ความหมายที่แสดงออกนั้นเป็นอุปสรรคหน่วยความจำแปลก ๆ ผู้ค้าคอมไพเลอร์เลือกที่จะเผยแพร่ตำนานทุกประเภทเพื่อรับผิดชอบเรื่องอุปสรรคของหน่วยความจำบนโปรแกรมเมอร์ แต่มันก็ละเมิดกฎของ "เครื่องจักรนามธรรม"
Lundin

2
@JimmyB volatile data_t data = {0}; set_mmio(&data); while (!data.ready);ท้องถิ่นผันผวนอาจจะมีประโยชน์สำหรับรหัสเช่น
Maciej Piechotka

13

คำสำคัญระเหยบอกคอมไพเลอร์ว่าการเข้าถึงตัวแปรมีผลที่สังเกตได้ นั่นหมายความว่าทุกครั้งที่ซอร์สโค้ดของคุณใช้ตัวแปรคอมไพเลอร์ต้องสร้างการเข้าถึงตัวแปร เป็นการเข้าถึงแบบอ่านหรือเขียน

ผลของสิ่งนี้คือการเปลี่ยนแปลงของตัวแปรภายนอกโค้ดโฟลว์ปกติจะถูกตรวจสอบโดยโค้ด เช่นถ้าตัวจัดการขัดจังหวะการเปลี่ยนแปลงค่า หรือถ้าตัวแปรเป็นจริงลงทะเบียนฮาร์ดแวร์บางอย่างที่มีการเปลี่ยนแปลงในตัวมันเอง

ประโยชน์ที่ยอดเยี่ยมนี้ยังมีข้อเสีย การเข้าถึงตัวแปรทุกครั้งจะต้องผ่านตัวแปรและค่าจะไม่ถูกเก็บไว้ในการลงทะเบียนเพื่อการเข้าถึงที่รวดเร็วขึ้นในทุกช่วงเวลา นั่นหมายถึงตัวแปรระเหยจะช้า ขนาดช้าลง ดังนั้นควรใช้สารระเหยที่จำเป็นจริงๆเท่านั้น

ในกรณีของคุณตราบใดที่คุณแสดงรหัสตัวแปรโกลบอลจะเปลี่ยนเฉพาะเมื่อคุณอัปเดตด้วยตัวเองadcValue = readADC();เท่านั้น คอมไพเลอร์ทราบเมื่อเกิดเหตุการณ์นี้ขึ้นและจะไม่ถือคุณค่าของ adcValue ในการลงทะเบียนในบางสิ่งที่อาจเรียกใช้readFromADC()ฟังก์ชัน หรือฟังก์ชั่นใด ๆ ที่มันไม่รู้เกี่ยวกับ หรืออะไรก็ตามที่จะจัดการกับพอยน์เตอร์ที่อาจชี้ไปยังadcValueและเช่นนั้น ไม่จำเป็นต้องมีความผันผวนเนื่องจากตัวแปรไม่เคยเปลี่ยนแปลงด้วยวิธีที่ไม่สามารถคาดการณ์ได้


6
ฉันเห็นด้วยกับคำตอบนี้ แต่ "ช้าลง" ฟังดูน่ากลัวเกินไป
kkrambo

6
การลงทะเบียน CPU สามารถเข้าถึงได้ในรอบน้อยกว่ารอบ cpu ของซีพียูรุ่นใหม่ ในทางกลับกันการเข้าถึงหน่วยความจำที่ไม่ได้ใช้งานจริง (จำไว้ว่าฮาร์ดแวร์ภายนอกบางตัวจะเปลี่ยนแปลงสิ่งนี้ดังนั้นจึงไม่อนุญาตให้ใช้แคช CPU) ในช่วง 100-300 รอบของ CPU ใช่แล้ว จะไม่เลวใน AVR หรือไมโครคอนโทรลเลอร์ที่คล้ายกัน แต่คำถามไม่ได้ระบุฮาร์ดแวร์
Goswin von Brederlow

7
ในระบบฝังตัว (ไมโครคอนโทรลเลอร์) บทลงโทษสำหรับการเข้าถึง RAM มักจะน้อยกว่ามาก ตัวอย่างเช่น AVR ใช้เวลาเพียงสองรอบของ CPU สำหรับการอ่านหรือเขียนไปยัง RAM (การย้ายรีจิสเตอร์ลงทะเบียนใช้เวลาหนึ่งรอบ) ดังนั้นการประหยัดของการรักษาสิ่งต่าง ๆ ในวิธีการลงทะเบียน (แต่ไม่ถึงจริง) สูงสุด 2 รอบนาฬิกาต่อการเข้าถึง - แน่นอนว่าการพูดค่อนข้างประหยัดค่าจาก register X เป็น RAM จากนั้นโหลดซ้ำค่านั้นใน register X สำหรับการคำนวณเพิ่มเติมจะใช้ 2x2 = 4 แทน 0 รอบ (เมื่อเพียงเก็บค่าใน X) และเป็นอนันต์ ช้าลง :)
JimmyB

1
มัน 'ขนาดช้าลง' ในบริบทของ "การเขียนหรือการอ่านจากตัวแปรเฉพาะ" ใช่ อย่างไรก็ตามในบริบทของโปรแกรมที่สมบูรณ์ที่น่าจะมีความหมายมากกว่าอ่าน / เขียนถึงตัวแปรหนึ่งครั้งแล้วครั้งเล่าไม่ใช่ไม่ใช่จริงๆ ในกรณีนั้นความแตกต่างโดยรวมน่าจะเป็น 'เล็กน้อยถึงเล็กน้อย' ควรใช้ความระมัดระวังเมื่อทำการยืนยันเกี่ยวกับประสิทธิภาพเพื่อชี้แจงว่าการยืนยันนั้นเกี่ยวข้องกับ op ที่เฉพาะเจาะจงหรือโปรแกรมโดยรวม การชะลอการใช้งาน op-infrequently โดยปัจจัย ~ 300x แทบจะไม่เป็นเรื่องใหญ่
aroth

1
คุณหมายถึงประโยคสุดท้ายนั่นเหรอ? นั่นหมายถึงมากขึ้นในแง่ของ "การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากของความชั่วร้ายทั้งหมด" เห็นได้ชัดว่าคุณไม่ควรใช้volatileทุกอย่างเพียงเพราะแต่คุณไม่ควรอายในกรณีที่คุณคิดว่ามันถูกกฎหมายเพราะความกังวลเรื่องประสิทธิภาพการทำงาน
aroth

9

การใช้คำหลักที่ระเหยได้ในแอพพลิเคชั่น C แบบฝังคือการทำเครื่องหมายตัวแปรทั่วโลกที่เขียนไว้ในตัวจัดการอินเตอร์รัปต์ มันไม่ใช่ตัวเลือกในกรณีนี้อย่างแน่นอน

หากไม่มีคอมไพเลอร์จะไม่สามารถพิสูจน์ได้ว่าค่านั้นเคยถูกเขียนไปหลังจากการเตรียมใช้งานเพราะมันไม่สามารถพิสูจน์ได้ว่า interrupt handler นั้นถูกเรียก ดังนั้นจึงคิดว่าสามารถปรับค่าตัวแปรให้เหมาะสม


2
แน่นอนว่ามีการใช้งานจริงอื่น ๆ อยู่ แต่ imho นี่เป็นเรื่องธรรมดาที่สุด
vicatcu

1
หากค่าถูกอ่านใน ISR เท่านั้น (และเปลี่ยนจาก main ()) คุณอาจต้องใช้ความผันผวนเช่นกันเพื่อรับประกันการเข้าถึง ATOMIC สำหรับตัวแปรหลายไบต์
Rev1.0

15
@ Rev1.0 ไม่ระเหยไม่รับประกัน aromicity ข้อกังวลนั้นต้องได้รับการแก้ไขแยกต่างหาก
Chris Stratton

1
ไม่มีการอ่านจากฮาร์ดแวร์หรือการขัดจังหวะใด ๆ ในรหัสที่โพสต์ คุณกำลังสมมติสิ่งต่าง ๆ จากคำถามที่ไม่มี ไม่สามารถตอบได้จริงในรูปแบบปัจจุบัน
Lundin

3
"ทำเครื่องหมายตัวแปรส่วนกลางที่เขียนไว้ในตัวจัดการขัดจังหวะ" ไม่ มันคือการทำเครื่องหมายตัวแปร; ทั่วโลกหรือมิฉะนั้น; ว่าอาจมีการเปลี่ยนแปลงโดยสิ่งที่อยู่นอกเหนือความเข้าใจของคอมไพเลอร์ ไม่จำเป็นต้องขัดจังหวะ อาจเป็นหน่วยความจำที่ใช้ร่วมกันหรือมีคนติดโพรบไว้ในหน่วยความจำ (ไม่แนะนำสำหรับอะไรที่ทันสมัยกว่า 40 ปี)
UKMonkey

9

มีอยู่สองกรณีที่คุณต้องใช้volatileในระบบฝังตัว

  • เมื่ออ่านจากการลงทะเบียนฮาร์ดแวร์

    นั่นหมายถึงการแมปหน่วยความจำลงทะเบียนตัวเองเป็นส่วนหนึ่งของอุปกรณ์ต่อพ่วงฮาร์ดแวร์ภายใน MCU อาจเป็นไปได้ว่าจะมีชื่อที่มีความลับเช่น "ADC0DR" การลงทะเบียนนี้จะต้องกำหนดในรหัส C ไม่ว่าจะผ่านแผนที่การลงทะเบียนที่จัดส่งโดยผู้จำหน่ายเครื่องมือหรือด้วยตัวเอง หากต้องการทำด้วยตัวเองคุณต้องทำ (สมมติว่ามีการลงทะเบียน 16 บิต):

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

    โดยที่ 0x1234 เป็นที่อยู่ที่ MCU ทำแผนที่การลงทะเบียน เนื่องจากvolatileเป็นส่วนหนึ่งของแมโครข้างต้นการเข้าถึงใด ๆ จะมีคุณสมบัติที่เปลี่ยนแปลงได้ ดังนั้นรหัสนี้ใช้ได้:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • เมื่อแชร์ตัวแปรระหว่าง ISR และรหัสที่เกี่ยวข้องโดยใช้ผลลัพธ์ของ ISR

    หากคุณมีสิ่งนี้:

    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;
    }

    จากนั้นคอมไพเลอร์อาจคิดว่า: "adc_data เสมอ 0 เพราะไม่ได้อัพเดททุกที่และไม่เคยเรียกใช้ฟังก์ชัน ADC0_interrupt () ดังนั้นตัวแปรไม่สามารถเปลี่ยนแปลงได้" คอมไพเลอร์มักจะไม่ทราบว่าอินเตอร์รัปต์นั้นถูกเรียกโดยฮาร์ดแวร์ไม่ใช่โดยซอฟต์แวร์ ดังนั้นคอมไพเลอร์จึงไปและลบรหัสออกif(adc_data > 0){ do_stuff(adc_data); }เพราะคิดว่ามันไม่สามารถเป็นจริงได้ก่อให้เกิดบั๊กที่แปลกและยากต่อการดีบัก

    ด้วยการประกาศadc_data volatileคอมไพเลอร์ที่ไม่ได้รับอนุญาตให้สมมติฐานใด ๆ ดังกล่าวและจะไม่ได้รับอนุญาตให้ออกไปเพิ่มประสิทธิภาพการเข้าถึงตัวแปร


หมายเหตุสำคัญ:

  • ISR จะถูกประกาศภายในไดรเวอร์ฮาร์ดแวร์เสมอ ในกรณีนี้ ADC ISR ควรอยู่ในไดรเวอร์ ADC ไม่มีอื่นนอกจากไดรเวอร์ควรสื่อสารกับ ISR - ทุกอย่างอื่นคือการเขียนโปรแกรมสปาเก็ตตี้

  • เมื่อเขียน C การสื่อสารทั้งหมดระหว่าง ISR และโปรแกรมแบ็คกราวน์จะต้องได้รับการปกป้องจากสภาพการแข่งขัน เสมอทุกครั้งที่ไม่มีข้อยกเว้น ขนาดของบัสข้อมูล MCU ไม่สำคัญเพราะแม้ว่าคุณจะทำสำเนา 8 บิตใน C ภาษาก็ไม่สามารถรับประกันการทำงานของอะตอมมิก _Atomicไม่เว้นแต่คุณใช้คุณลักษณะ C11 หากคุณสมบัตินี้ไม่พร้อมใช้งานคุณต้องใช้สัญญาณบางอย่างหรือปิดการใช้งานอินเตอร์รัปต์ระหว่างอ่านเป็นต้นแอสเซมเบลอร์อินไลน์เป็นอีกตัวเลือกหนึ่ง volatileไม่รับประกันอะตอมมิก

    สิ่งที่สามารถเกิดขึ้นได้คือ:
    - โหลดค่าจากสแต็กเข้าสู่รีจิสเตอร์ -
    อินเทอรัปต์เกิดขึ้น
    - ใช้ค่าจากรีจิสเตอร์

    และมันก็ไม่สำคัญว่าส่วน "ค่าการใช้" เป็นคำสั่งเดียวในตัวมันเอง น่าเศร้าที่ส่วนสำคัญของโปรแกรมเมอร์ระบบฝังตัวทั้งหมดไม่สนใจสิ่งนี้และอาจทำให้ระบบฝังตัวเป็นข้อบกพร่องที่พบบ่อยที่สุด ไม่ต่อเนื่องเสมอยากที่จะกระตุ้นและหายาก


ตัวอย่างของไดรเวอร์ ADC ที่เขียนอย่างถูกต้องจะมีลักษณะเช่นนี้ (สมมติว่า C11 _Atomicใช้งานไม่ได้):

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;
  }
}
  • รหัสนี้สมมติว่าการขัดจังหวะไม่สามารถขัดจังหวะในตัวเอง บนระบบดังกล่าวบูลีนแบบง่ายสามารถทำหน้าที่เป็นเซมาฟอร์และไม่จำเป็นต้องเป็นอะตอมเนื่องจากไม่มีอันตรายใด ๆ หากอินเทอร์รัปต์เกิดขึ้นก่อนที่บูลีนจะถูกตั้งค่า ด้านล่างของวิธีง่าย ๆ ข้างต้นคือมันจะทิ้ง ADC อ่านเมื่อสภาพการแข่งขันเกิดขึ้นโดยใช้ค่าก่อนหน้าแทน สามารถหลีกเลี่ยงได้เช่นกัน แต่รหัสจะซับซ้อนขึ้น

  • ที่นี่volatileป้องกันข้อบกพร่องการเพิ่มประสิทธิภาพ มันไม่มีส่วนเกี่ยวข้องกับข้อมูลที่มาจากการลงทะเบียนฮาร์ดแวร์เพียงว่าข้อมูลนั้นถูกแชร์กับ ISR

  • staticป้องกันการเขียนโปรแกรมสปาเก็ตตี้และมลภาวะเนมสเปซด้วยการทำให้ตัวแปรท้องถิ่นกับไดรเวอร์ (สิ่งนี้ใช้ได้ในแอพพลิเคชั่นแบบ Single-Core แบบเธรดเดียว แต่ไม่ได้อยู่ในแบบมัลติเธรด)


การดีบักยากที่จะสัมพันธ์กันถ้ารหัสถูกลบคุณจะสังเกตเห็นว่ารหัสที่มีค่าของคุณได้หายไป - นั่นคือคำสั่งตัวหนาสวยที่สิ่งที่ผิดพลาด แต่ฉันเห็นด้วยอาจมีลักษณะแปลกและยากที่จะตรวจแก้จุดบกพร่อง
อาร์เซนอล

@Arsenal หากคุณมีบั๊กที่ดีที่ inlines ประกอบกับ C และคุณรู้ว่าอย่างน้อยนิด asm เล็ก ๆ น้อย ๆ แล้วใช่มันสามารถจะง่ายต่อการจุด แต่สำหรับโค้ดที่ซับซ้อนที่มีขนาดใหญ่ขึ้น asm ที่สร้างด้วยเครื่องจักรขนาดใหญ่นั้นไม่น่าจะผ่านไปได้ หรือถ้าคุณไม่รู้แอส หรือถ้าดีบักเกอร์ของคุณเป็นอึและไม่แสดง asm (ไอ)
Lundin

เป็นไปได้ไหมที่ฉันจะรู้สึกเสียบ้างโดยใช้ debuggers ของ Lauterbach หากคุณพยายามตั้งค่าเบรกพอยต์ในรหัสที่ปรับให้เหมาะสมมันจะตั้งค่าไว้ที่อื่นและคุณรู้ว่ามีบางอย่างเกิดขึ้นที่นั่น
Arsenal

@Arsenal Yep, ชนิดของ C ผสม / asm ที่คุณจะได้รับใน Lauterbach นั้นไม่ได้เป็นมาตรฐาน เครื่องมือดีบั๊กส่วนใหญ่จะแสดง asm ในหน้าต่างแยกต่างหากถ้าหากทั้งหมด
Lundin

semaphoreควรจะเป็นอย่างแน่นอนvolatile! ในความเป็นจริงมันเป็นกรณีการใช้งานขั้นพื้นฐานที่สุดชเรียกร้องให้: บางสิ่งบางอย่างสัญญาณจากบริบทการดำเนินการอย่างใดอย่างหนึ่งไปยังอีก - ในตัวอย่างของคอมไพเลอร์ก็สามารถละเว้นเพราะเห็นว่าค่าของมันจะไม่เคยอ่านมาก่อนจะได้รับการเขียนทับโดย volatilesemaphore = true;semaphore = false;
JimmyB

5

ในตัวอย่างรหัสที่แสดงในคำถามยังไม่มีเหตุผลที่จะใช้สารระเหย มันไม่เกี่ยวข้องที่ค่าของadcValueมาจาก ADC และadcValueการเป็นคนทั่วโลกควรทำให้คุณสงสัยว่าadcValueควรจะมีความผันผวนหรือไม่

การเป็นโกลบอลเป็นเงื่อนงำเพราะเปิดโอกาสให้adcValueสามารถเข้าถึงได้จากบริบทของโปรแกรมมากกว่าหนึ่งรายการ. บริบทของโปรแกรมประกอบด้วยตัวจัดการขัดจังหวะและงาน RTOS หากตัวแปรโกลบอลถูกเปลี่ยนโดยบริบทหนึ่งบริบทอื่น ๆ ของโปรแกรมจะไม่สามารถสันนิษฐานได้ว่าพวกเขารู้ค่าจากการเข้าถึงก่อนหน้านี้ แต่ละบริบทต้องอ่านค่าตัวแปรใหม่ทุกครั้งที่ใช้เนื่องจากค่าอาจเปลี่ยนแปลงในบริบทโปรแกรมที่แตกต่างกัน บริบทของโปรแกรมไม่ได้รับรู้เมื่อมีการขัดจังหวะหรือสลับงานเกิดขึ้นดังนั้นจึงต้องสมมติว่าตัวแปรโกลบอลใด ๆ ที่ใช้โดยหลายบริบทอาจเปลี่ยนไประหว่างการเข้าถึงตัวแปรใด ๆ เนื่องจากการสลับบริบทที่เป็นไปได้ นี่คือสิ่งที่การประกาศที่ผันผวนนั้นมีไว้สำหรับ มันบอกคอมไพเลอร์ว่าตัวแปรนี้สามารถเปลี่ยนแปลงนอกบริบทของคุณดังนั้นอ่านมันทุกการเข้าถึงและไม่คิดว่าคุณรู้ค่าแล้ว

หากตัวแปรถูกแม็พหน่วยความจำกับฮาร์ดแวร์แอดเดรสดังนั้นการเปลี่ยนแปลงที่ทำโดยฮาร์ดแวร์จะเป็นอีกบริบทหนึ่งที่มีประสิทธิภาพนอกบริบทของโปรแกรมของคุณ ดังนั้นหน่วยความจำที่แมปยังเป็นเบาะแส ตัวอย่างเช่นหากreadADC()ฟังก์ชันของคุณเข้าถึงค่าที่แม็พหน่วยความจำเพื่อรับค่า ADC ดังนั้นตัวแปรที่แม็พหน่วยความจำควรจะเปลี่ยนแปลงได้

ดังนั้นกลับไปที่คำถามของคุณหากมีมากกว่ารหัสของคุณและadcValueเข้าถึงได้โดยรหัสอื่นที่ทำงานในบริบทที่แตกต่างกันใช่แล้วadcValueควรจะผันผวน


4

"ตัวแปรทั่วโลกที่เปลี่ยนแปลงโดยตรงจากฮาร์ดแวร์"

เพียงเพราะค่ามาจากการลงทะเบียน ADC ของฮาร์ดแวร์บางตัวไม่ได้หมายความว่ามันจะถูกเปลี่ยนแปลงโดยฮาร์ดแวร์โดยตรง

ในตัวอย่างของคุณคุณเพียงแค่เรียก readADC () ซึ่งจะส่งกลับค่าการลงทะเบียน ADC สิ่งนี้ใช้ได้กับคอมไพเลอร์โดยทราบว่า adcValue ได้รับการกำหนดค่าใหม่ ณ จุดนั้น

มันจะแตกต่างกันถ้าคุณใช้รูทีนการอินเตอร์รัปต์ ADC เพื่อกำหนดค่าใหม่ซึ่งจะถูกเรียกเมื่อค่า ADC ใหม่พร้อม ในกรณีนั้นคอมไพเลอร์จะไม่มีเงื่อนงำเกี่ยวกับเมื่อเรียก ISR ที่สอดคล้องกันและอาจตัดสินใจว่า adcValue จะไม่สามารถเข้าถึงด้วยวิธีนี้ นี่คือที่ระเหยจะช่วย


1
เมื่อโค้ดของคุณไม่ "เรียกใช้" ฟังก์ชัน ISR คอมไพเลอร์จะเห็นว่าตัวแปรนั้นได้รับการปรับปรุงในฟังก์ชันที่ไม่มีใครเรียกเท่านั้น ดังนั้นคอมไพเลอร์ปรับให้เหมาะสม
Swanand

1
มันขึ้นอยู่กับส่วนที่เหลือของรหัสถ้า adcValue ไม่ได้ถูกอ่านที่ใด (เช่นอ่านผ่านดีบักเกอร์) หรือถ้ามันอ่านเพียงครั้งเดียวในที่เดียวคอมไพเลอร์จะปรับให้เหมาะสม
ดาเมียน

2
@ Damien: มันเสมอ "ขึ้นอยู่กับ" แต่ฉันตั้งใจที่จะตอบคำถามที่แท้จริง "ฉันควรใช้คำสำคัญระเหยในกรณีนี้หรือไม่?" สั้นที่สุด
Rev1.0

4

พฤติกรรมของการvolatileโต้แย้งส่วนใหญ่ขึ้นอยู่กับรหัสของคุณคอมไพเลอร์และการเพิ่มประสิทธิภาพที่ทำ

มีอยู่สองกรณีที่ฉันใช้เป็นการส่วนตัวvolatile:

  • หากมีตัวแปรที่ฉันต้องการดูด้วยดีบักเกอร์ แต่คอมไพเลอร์ได้ทำการปรับปรุงให้ดีที่สุด (หมายถึงลบออกเพราะพบว่าไม่จำเป็นต้องมีตัวแปรนี้) การเพิ่มvolatileจะบังคับให้คอมไพเลอร์เก็บไว้และด้วยเหตุนี้ สามารถเห็นได้ในการแก้ปัญหา

  • หากตัวแปรอาจเปลี่ยน "out of the code" โดยทั่วไปหากคุณมีฮาร์ดแวร์บางอย่างที่เข้าถึงมันหรือหากคุณจับคู่ตัวแปรกับที่อยู่โดยตรง

ในการฝังตัวยังมีข้อบกพร่องบางอย่างในคอมไพเลอร์การเพิ่มประสิทธิภาพที่ใช้งานได้จริงและบางครั้งvolatileสามารถแก้ปัญหาได้

ให้ตัวแปรของคุณถูกประกาศไปทั่วโลกมันอาจจะไม่ได้รับการปรับให้เหมาะสมตราบใดที่ตัวแปรนั้นถูกใช้กับโค้ดอย่างน้อยก็เขียนและอ่านได้

ตัวอย่าง:

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

ในกรณีนี้ตัวแปรอาจถูกปรับให้เหมาะกับ printf ("% i", 1);

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

จะไม่ถูกปรับให้เหมาะสม

อีกหนึ่ง:

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

ในกรณีนี้คอมไพเลอร์อาจปรับให้เหมาะสมโดย (ถ้าคุณปรับให้เหมาะสมสำหรับความเร็ว) และทิ้งตัวแปร

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

สำหรับกรณีการใช้งานของคุณ "มันอาจขึ้นอยู่กับ" ในส่วนที่เหลือของรหัสของคุณวิธีadcValueการใช้งานที่อื่นและการตั้งค่าเวอร์ชั่น / การปรับแต่งคอมไพเลอร์ที่คุณใช้

บางครั้งอาจเป็นเรื่องที่น่ารำคาญที่จะมีรหัสที่ทำงานโดยไม่มีการปรับให้เหมาะสม

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

สิ่งนี้อาจปรับให้เหมาะกับ 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
}

สิ่งเหล่านี้อาจจะไม่ได้รับการปรับปรุง แต่คุณไม่เคยรู้ว่า "คอมไพเลอร์ดีแค่ไหน" และอาจเปลี่ยนแปลงได้ด้วยพารามิเตอร์คอมไพเลอร์ โดยทั่วไปแล้วคอมไพเลอร์ที่มีการเพิ่มประสิทธิภาพที่ดีจะได้รับลิขสิทธิ์


1
ตัวอย่างเช่น a = 1; ข = a; และ c = b; คอมไพเลอร์อาจจะคิดว่าเดี๋ยวก่อน a และ b ไร้ประโยชน์เรามาวาง 1 ถึง c โดยตรง แน่นอนว่าคุณจะไม่ทำอย่างนั้นในโค้ดของคุณ แต่คอมไพเลอร์ดีกว่าคุณในการค้นหาสิ่งเหล่านี้และถ้าคุณพยายามที่จะเขียนโค้ดที่ได้รับการปรับปรุงให้ดีที่สุดในทันทีมันจะไม่สามารถอ่านได้
ดาเมียน

2
รหัสที่ถูกต้องพร้อมกับคอมไพเลอร์ที่ถูกต้องจะไม่แตกเมื่อเปิดใช้การปรับให้เหมาะสม ความถูกต้องของคอมไพเลอร์เป็นปัญหาเล็กน้อย แต่อย่างน้อยกับ IAR ฉันไม่ได้พบกับสถานการณ์ที่การเพิ่มประสิทธิภาพนำไปสู่การทำลายรหัสที่ไม่ควร
Arsenal

5
หลายกรณีที่การเพิ่มประสิทธิภาพแบ่งรหัสคือเมื่อคุณกำลังเข้าไปในดินแดนUBด้วย ..
ท่อ

2
ใช่ผลข้างเคียงของสารระเหยคือสามารถช่วยแก้ไขข้อบกพร่องได้ แต่นั่นไม่ใช่เหตุผลที่ดีที่จะใช้สารระเหย คุณควรปิดการปรับให้เหมาะสมถ้าการดีบักอย่างง่ายเป็นเป้าหมายของคุณ คำตอบนี้ไม่ได้พูดถึงการขัดจังหวะ
kkrambo

2
การเพิ่มอาร์กิวเมนต์การดีบักvolatileบังคับให้คอมไพเลอร์เก็บตัวแปรใน RAM และอัปเดต RAM นั้นทันทีที่กำหนดค่าให้กับตัวแปร ส่วนใหญ่คอมไพเลอร์ไม่ได้ตัวแปร 'ลบ' เพราะเรามักจะไม่เขียนการมอบหมายโดยไม่มีผล แต่มันอาจตัดสินใจที่จะเก็บตัวแปรไว้ใน CPU register บางตัวและอาจภายหลังหรือไม่เขียนค่าลงทะเบียนของ RAM นั้น ตัวแก้ไขมักจะล้มเหลวในการค้นหาการลงทะเบียน CPU ซึ่งตัวแปรนั้นถูกเก็บไว้และดังนั้นจึงไม่สามารถแสดงค่าของมัน
JimmyB

1

คำอธิบายทางเทคนิคมากมาย แต่ฉันต้องการมีสมาธิกับการใช้งานจริง

volatileกองกำลังคำหลักคอมไพเลอร์จะอ่านหรือเขียนค่าของตัวแปรจากความทรงจำครั้งที่มีการใช้ทุก โดยทั่วไปคอมไพเลอร์จะพยายามปรับให้เหมาะสม แต่ไม่ทำการอ่านและเขียนที่ไม่จำเป็นเช่นการเก็บค่าในการลงทะเบียน CPU แทนที่จะเข้าถึงหน่วยความจำทุกครั้ง

มีสองการใช้งานหลักในรหัสที่ฝังตัว ประการแรกมันถูกใช้สำหรับการลงทะเบียนฮาร์ดแวร์ การลงทะเบียนฮาร์ดแวร์สามารถเปลี่ยนแปลงได้เช่นการลงทะเบียนผล ADC สามารถเขียนได้โดยอุปกรณ์ต่อพ่วง ADC การลงทะเบียนฮาร์ดแวร์ยังสามารถดำเนินการได้เมื่อเข้าถึง ตัวอย่างทั่วไปคือการลงทะเบียนข้อมูลของ UART ซึ่งมักจะล้างการอินเตอร์รัปต์แฟล็กเมื่ออ่าน

คอมไพเลอร์จะพยายามเพิ่มประสิทธิภาพการอ่านและเขียนซ้ำของการลงทะเบียนบนสมมติฐานที่ว่าค่าจะไม่เปลี่ยนแปลงดังนั้นจึงไม่จำเป็นต้องเข้าถึงต่อไป แต่volatileคำสำคัญจะบังคับให้ดำเนินการอ่านทุกครั้ง

การใช้งานทั่วไปที่สองสำหรับตัวแปรที่ใช้โดยทั้งรหัสขัดจังหวะและไม่ขัดจังหวะ อินเทอร์รัปต์ไม่ได้ถูกเรียกโดยตรงดังนั้นคอมไพเลอร์จึงไม่สามารถกำหนดได้ว่าจะให้ใช้เมื่อใดและจะถือว่าการเข้าถึงใด ๆ ภายในอินเทอร์รัปต์ไม่เกิดขึ้น เนื่องจากvolatileคีย์เวิร์ดบังคับให้คอมไพเลอร์เข้าถึงตัวแปรทุกครั้งข้อสมมติฐานนี้จะถูกลบออก

เป็นสิ่งสำคัญที่จะต้องทราบว่าvolatileคำหลักนั้นไม่สามารถแก้ไขปัญหาเหล่านี้ได้อย่างสมบูรณ์และต้องระมัดระวังไม่ให้เกิดปัญหา ตัวอย่างเช่นในระบบ 8 บิตตัวแปร 16 บิตต้องการหน่วยความจำสองตัวในการเข้าถึงเพื่ออ่านหรือเขียนและแม้ว่าคอมไพเลอร์จะถูกบังคับให้ทำการเข้าถึงเหล่านั้นจะเกิดขึ้นตามลำดับและเป็นไปได้ที่ฮาร์ดแวร์จะสามารถเข้าถึงการเข้าถึงครั้งแรกหรือ การขัดจังหวะที่จะเกิดขึ้นระหว่างคนทั้งสอง


0

ในกรณีที่ไม่มีตัวระบุvolatileอาจมีการเก็บค่าของวัตถุไว้ในสถานที่มากกว่าหนึ่งแห่งระหว่างรหัสบางส่วน ลองพิจารณายกตัวอย่างเช่น:

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

ในวันแรกของ C คอมไพเลอร์จะประมวลผลคำสั่ง

foo++;

ผ่านขั้นตอน:

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

คอมไพเลอร์ที่ซับซ้อนมากขึ้นจะรับรู้ว่าถ้าค่าของ "foo" ถูกเก็บไว้ในรีจิสเตอร์ระหว่างลูปจะต้องโหลดหนึ่งครั้งก่อนลูปและเก็บไว้หนึ่งครั้ง อย่างไรก็ตามในระหว่างการวนรอบนั่นหมายความว่าค่าของ "foo" จะถูกเก็บไว้ในสองแห่ง - ภายในที่เก็บข้อมูลส่วนกลางและภายในการลงทะเบียน นี่จะไม่เป็นปัญหาหากคอมไพเลอร์สามารถมองเห็นวิธีการทั้งหมดที่ "foo" อาจเข้าถึงได้ภายในลูป แต่อาจทำให้เกิดปัญหาได้หากเข้าถึง "foo" ในกลไกบางอย่างที่คอมไพเลอร์ไม่ทราบ ( เช่นตัวจัดการขัดจังหวะ)

อาจเป็นไปได้สำหรับผู้สร้างมาตรฐานที่จะเพิ่มตัวระบุใหม่ที่จะเชิญคอมไพเลอร์เพื่อทำการปรับให้เหมาะสมดังกล่าวอย่างชัดเจนและบอกว่าซีแมนทิกส์สมัยเก่าจะใช้ในกรณีที่ไม่มี แต่กรณีที่การเพิ่มประสิทธิภาพมีประโยชน์มากกว่า สิ่งที่มันจะเป็นปัญหาดังนั้นมาตรฐานจึงอนุญาตให้คอมไพเลอร์คิดว่าการเพิ่มประสิทธิภาพดังกล่าวปลอดภัยในกรณีที่ไม่มีหลักฐานว่าพวกเขาไม่ใช่ วัตถุประสงค์ของvolatileคำสำคัญคือการจัดหาหลักฐานดังกล่าว

ความขัดแย้งสองสามอย่างระหว่างนักเขียนคอมไพเลอร์และโปรแกรมเมอร์เกิดขึ้นกับสถานการณ์เช่น:

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);
}

ในอดีตคอมไพเลอร์ส่วนใหญ่จะอนุญาตให้มีความเป็นไปได้ว่าการเขียนที่volatileเก็บข้อมูลอาจทำให้เกิดผลข้างเคียงโดยพลการและหลีกเลี่ยงการแคชค่าใด ๆ ในการลงทะเบียนในร้านค้าดังกล่าวหรือมิฉะนั้นพวกเขาจะละเว้นจากค่าแคชในรีจิสเตอร์ ไม่มีคุณสมบัติ "อินไลน์" และจะเขียน 0x1234 ไปที่output_buffer[0]ตั้งค่าสิ่งต่างๆเพื่อส่งออกข้อมูลรอให้เสร็จสมบูรณ์จากนั้นเขียน 0x2345 ถึงoutput_buffer[0]และดำเนินการต่อจากที่นั่น มาตรฐานไม่จำเป็นต้องมีการใช้งานเพื่อปฏิบัติต่อการจัดเก็บที่อยู่ของoutput_bufferลงในvolatile- ตัวบ่งชี้ที่มีคุณสมบัติเป็นสัญญาณว่ามีบางสิ่งที่อาจเกิดขึ้นกับมันผ่านหมายความว่าคอมไพเลอร์ไม่เข้าใจ แต่เนื่องจากผู้เขียนคิดว่าคอมไพเลอร์ผู้เขียนคอมไพเลอร์ที่มีไว้สำหรับแพลตฟอร์มและวัตถุประสงค์ต่างๆจะรับรู้เมื่อทำเช่นนั้น โดยไม่ต้องบอกต่อ ดังนั้นคอมไพเลอร์ "ฉลาด" บางอย่างเช่น gcc และเสียงดังกราวจะสันนิษฐานว่าแม้ว่าที่อยู่ของoutput_bufferจะถูกเขียนไปยังตัวชี้ที่มีคุณสมบัติในการระเหยระหว่างสองร้านไปถึงoutput_buffer[0]นั่นคือเหตุผลที่จะไม่คิดว่าอะไรก็ตามที่สนใจคุณค่าของวัตถุ เวลานั้น.

volatileนอกจากนี้ในขณะที่ตัวชี้ที่ถูกโยนโดยตรงจากจำนวนเต็มจะไม่ค่อยมีใครใช้เพื่อวัตถุประสงค์อื่นใดนอกเหนือจากที่จะจัดการกับสิ่งที่อยู่ในรูปแบบที่คอมไพเลอร์ไม่น่าจะเข้าใจมาตรฐานอีกครั้งไม่จำเป็นต้องใช้คอมไพเลอร์ในการรักษาเช่นการเข้าถึง ดังนั้นการเขียนครั้งแรก*((unsigned short*)0xC0001234)อาจถูกละเว้นโดยคอมไพเลอร์ "ฉลาด" เช่น gcc และเสียงดังกราวเพราะผู้ดูแลของคอมไพเลอร์ดังกล่าวค่อนข้างจะอ้างว่ารหัสที่ละเลยการมีคุณสมบัติเช่นvolatileนี้ "แตก" กว่าจำได้ว่าเข้ากันได้กับรหัสดังกล่าวเป็นประโยชน์ . ไฟล์ส่วนหัวที่ผู้จำหน่ายระบุไว้จะไม่ใช้ตัวvolatileระบุและคอมไพเลอร์ที่เข้ากันได้กับไฟล์ส่วนหัวที่ผู้จำหน่ายระบุจะมีประโยชน์มากกว่าไฟล์ที่ไม่ได้เป็น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.