임베디드 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 하드웨어가 변환을 완료하면
변경됨

2
두 버전에서 생성 된 어셈블러를 비교 했습니까? 그것은 후드 아래에서 무슨 일이 일어나고 있는지 보여줄 것입니다
Mawg

3
@stark : BIOS? 마이크로 컨트롤러에서? 캐싱 규칙과 메모리 맵 간의 설계 일관성에 의해 메모리 매핑 된 I / O 공간은 캐시 할 수 없습니다 (아키텍처가 처음부터 데이터 캐시를 가지고있는 경우에도 보장되지 않음). 그러나 휘발성은 메모리 컨트롤러 캐시와 관련이 없습니다.
벤 Voigt

1
@Davislor 언어 표준은 일반적으로 더 이상 말할 필요가 없습니다. 휘발성 객체에 대한 읽기는 실제로드를 수행하고 (컴파일러가 최근에 수행 한 값과 일반적으로 값을 알고있는 경우에도) 해당 객체에 대한 쓰기는 실제 저장을 수행합니다 (오브젝트에서 동일한 값을 읽었더라도) ). 따라서 if(x==1) x=1;쓰기에서 비 휘발성에 대해 최적화 될 수 있으며 휘발성 인 x경우 최적화 될 수 없습니다 x. OTOH 외부 장치에 액세스하는 데 특별한 지침이 필요한 경우 해당 장치를 추가해야 할 책임은 사용자에게 있습니다 (예 : 메모리 범위를 기록해야하는 경우).
curiousguy

답변:


87

의 정의 volatile

volatile컴파일러에 알리지 않고 변수 값이 변경 될 수 있음을 컴파일러에 알립니다. 따라서 컴파일러는 C 프로그램이 값을 변경하지 않은 것만으로 값이 변경되지 않았다고 가정 할 수 없습니다.

반면에, 컴파일러가 알지 못하는 곳에서는 변수의 값이 필요 (읽기) 될 수 있으므로 변수에 대한 모든 할당이 실제로 쓰기 작업으로 수행되도록해야합니다.

사용 사례

volatile 필요한 경우

  • 하드웨어 레지스터 (또는 메모리 매핑 된 I / O)를 변수로 나타냄-레지스터를 읽지 않더라도 컴파일러는 "Stupid programmer. 다시 읽지 않을 것입니다. 글을 생략해도 눈치 채지 못할 것입니다. " 반대로, 프로그램이 변수에 값을 쓰지 않더라도 하드웨어에 의해 값이 변경 될 수 있습니다.
  • 실행 컨텍스트간에 변수 공유 (예 : ISR / 메인 프로그램) (@ kkramo의 답변 참조)

의 효과 volatile

변수가 선언 volatile되면 컴파일러는 프로그램 코드에서 변수에 대한 모든 할당이 실제 쓰기 작업에 반영되고 프로그램 코드에서 읽을 때마다 (매핑 된) 메모리에서 값을 읽도록해야합니다.

비 휘발성 변수의 경우, 컴파일러는 변수의 값이 변경되는시기와시기를 알고 다른 방식으로 코드를 최적화 할 수 있다고 가정합니다.

하나의 경우, 컴파일러는 CPU 레지스터에 값을 유지함으로써 메모리에 대한 읽기 / 쓰기 수를 줄일 수 있습니다.

예:

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

여기서 컴파일러는 아마도 result변수에 RAM을 할당하지 않을 것이고 중간 값을 CPU 레지스터 이외의 어디에도 저장하지 않을 것입니다.

result일시적인 경우 resultC 코드에서 발생 하면 컴파일러가 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이 실행되는 동안 변경되지 않기 때문에 필요하지 않은 것을 알고있다. 이는 sysTickCount위의 예 와 같이 ISR이 변수 값의 '생산자'인 경우에 일반적 입니다. AVR에서는 함수 doSysTick()가 메모리에서 동일한 4 바이트 (4 개의 명령 = 액세스 당 8 개의 CPU 사이클 sysTickCount)에 두 번이 아니라 5 번 또는 6 번 액세스하는 것이 특히 고통 스럽습니다 . 프로그래머는 값이 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;

에 보장 처음 에 할당 한 i다음 2에 할당 j. 그러나 사이에 할당되는 것은 보장 되지 않습니다a . 컴파일러는 코드 스 니펫 이전 또는 이후에 해당 할당을 수행 할 수 있습니다 a.

위에서 언급 한 매크로의 메모리 장벽이 아닌 경우 컴파일러는 번역 할 수 있습니다

uint32_t x;

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

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

또는

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

(완전성을 위해 모든 액세스가 이러한 장벽으로 묶여 volatile있으면 sei / cli 매크로에 의해 암시 된 것과 같은 메모리 장벽이 실제로 사용을 제거 할 수 있다고 말해야합니다 .)


7
성능 저하를위한
unvolatiling에

3
ISO / IEC 9899 : 1999 6.7.3 (6) : volatile 정의를 항상 언급하고 싶습니다 . 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 No. C17 5.1.2.3 §6은 관찰 가능한 동작을 정의합니다 . "휘발성 개체에 대한 액세스는 추상 기계의 규칙에 따라 엄격하게 평가됩니다." C 표준은 메모리 장벽이 전체적으로 필요한 곳을 실제로 명확하지 않습니다. 사용하는 표현식의 끝에는 volatile시퀀스 포인트가 있으며 그 이후의 모든 항목은 "시퀀스 이후"여야합니다. 표현 일종의 기억 장벽 이라는 것을 의미합니다 . 컴파일러 공급 업체는 프로그래머에게 메모리 장벽의 책임을 부여하기 위해 모든 종류의 신화를 퍼 뜨리기로 선택했지만 "추상 기계"의 규칙을 위반합니다.
Lundin

2
@JimmyB Local volatile은 같은 코드에 유용 할 수 volatile data_t data = {0}; set_mmio(&data); while (!data.ready);있습니다.
Maciej Piechotka

13

volatile 키워드는 컴파일러에게 변수에 대한 액세스가 관찰 가능한 효과가 있음을 알려줍니다. 즉, 소스 코드가 변수를 사용할 때마다 컴파일러는 변수에 대한 액세스를 작성해야합니다. 읽기 또는 쓰기 권한이 있어야합니다.

그 결과 정상적인 코드 흐름 외부의 변수에 대한 변경 사항도 코드에 의해 관찰됩니다. 예를 들어, 인터럽트 핸들러가 값을 변경하면 또는 변수가 실제로 자체적으로 변경되는 일부 하드웨어 레지스터 인 경우.

이 큰 장점은 단점이기도합니다. 변수에 대한 모든 단일 액세스는 변수를 통과하며 값은 어떤 시간 동안 더 빠른 액세스를 위해 레지스터에 유지되지 않습니다. 그것은 휘발성 변수가 느리다는 것을 의미합니다. 크기가 느려집니다. 따라서 실제로 필요한 곳에만 휘발성을 사용하십시오.

귀하의 경우 코드를 표시하는 한 전역 변수는로 직접 업데이트 할 때만 변경됩니다 adcValue = readADC();. 컴파일러는 이러한 상황이 언제 발생하는지 알고 있으며 readFromADC()함수를 호출 할 수있는 항목에서 레지스터의 adcValue 값을 보유하지 않습니다 . 또는 모르는 기능. 또는 가리킬 수있는 포인터를 조작하는 모든 것 adcValue. 변수가 예측할 수없는 방식으로 변하지 않기 때문에 실제로 휘발성이 필요하지 않습니다.


6
이 답변에 동의하지만 "크기가 느립니다"소리가 너무 심합니다.
kkrambo

6
최신 슈퍼 스칼라 CPU에서 CPU 사이클 미만으로 CPU 레지스터에 액세스 할 수 있습니다. 반면에 실제 캐시되지 않은 메모리에 대한 액세스 (일부 외부 하드웨어는이를 변경하므로 CPU 캐시는 허용되지 않음)는 100-300 CPU주기 범위에있을 수 있습니다. 예, 크기입니다. AVR 또는 유사한 마이크로 컨트롤러에서는 그렇게 나쁘지는 않지만 하드웨어를 지정하지는 않습니다.
Goswin von Brederlow

7
임베디드 (마이크로 컨트롤러) 시스템에서 RAM 액세스에 대한 패널티는 훨씬 적습니다. 예를 들어, AVR은 RAM에서 읽기 또는 쓰기에 2 개의 CPU 주기만 사용하므로 (레지스터-레지스터 이동은 한주기가 소요됨) 레지스터에 유지하는 비용을 최대한 절약합니다 (그러나 실제로 도달하지는 않음). 액세스 당 2 클럭 사이클. -물론 상대적으로 말해서 레지스터 X에서 RAM으로 값을 저장 한 다음 추가 계산을 위해 해당 값을 레지스터 X로 즉시 다시로드하면 0 사이클 대신 2x2 = 4가 소요됩니다 (X 값을 유지하는 경우). 따라서 무한합니다.
slow

1
"특정 변수에 쓰거나 읽습니다"라는 맥락에서 '크기가 느립니다'. 그러나 완전한 프로그램의 맥락에서 하나의 변수를 반복해서 읽거나 쓰는 것보다 훨씬 더 많은 것을 할 가능성이 큰 것은 아닙니다. 이 경우 전체적인 차이는 '작거나 무시할 수 있습니다'. 성능에 대한 주장을 할 때 주장이 하나의 특정 op 또는 프로그램 전체와 관련이 있는지를 명확히하기 위해주의를 기울여야합니다. ~ 300x의 계수로 자주 사용하지 않는 op를 늦추는 것은 결코 큰 문제가 아닙니다.
aroth

1
마지막 문장인가요? 그것은 "조기 최적화는 모든 악의 근원"이라는 의미에서 훨씬 더 의미가 있습니다. 분명히 당신은 단지volatile 모든 이유로 사용 해서는 안되지만 선제 적 성능 걱정 때문에 합법적으로 요구된다고 생각되는 경우에도 부끄러워해서는 안됩니다.
aroth

9

임베디드 C 애플리케이션에서 volatile 키워드의 주요 용도 는 인터럽트 핸들러에 기록 되는 글로벌 변수를 표시하는 것입니다 . 이 경우에는 선택 사항이 아닙니다.

그것이 없으면 컴파일러는 인터럽트 핸들러가 호출되었음을 증명할 수 없기 때문에 초기화 후에 값이 쓰여졌다는 것을 증명할 수 없습니다. 따라서 존재하지 않는 변수를 최적화 할 수 있다고 생각합니다.


2
확실히 다른 실제적인 용도가 존재하지만 이것이 가장 일반적입니다.
vicatcu

1
값이 ISR에서만 읽히고 main ()에서 변경된 경우 다중 바이트 변수에 대한 ATOMIC 액세스를 보장하기 위해 잠재적으로 휘발성을 사용해야합니다.
Rev1.0

15
@ Rev1.0 아니요, 휘발성 Aromicity를 보장 하지 않습니다 . 이러한 문제는 별도로 해결해야합니다.
Chris Stratton

1
게시 된 코드에서 하드웨어를 읽거나 인터럽트를 읽을 수 없습니다. 당신은 존재하지 않는 질문에서 물건을 가정하고 있습니다. 현재의 형태로는 실제로 대답 할 수 없습니다.
Lundin

3
"인터럽트 핸들러에 기록 된 글로벌 변수를 표시하십시오"아니오. 변수를 표시하는 것입니다. 글로벌 또는 기타; 컴파일러 외부에서 이해하는 것으로 인해 변경 될 수 있습니다. 인터럽트가 필요하지 않습니다. 메모리를 공유하거나 프로브를 메모리에
꽂는 사람

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과 백그라운드 프로그램 간의 모든 통신은 경쟁 조건으로부터 보호 되어야합니다 . 항상 예외는 없습니다. C에서 단일 8 비트 사본을 수행하더라도 언어가 작동의 원 자성을 보장 할 수 없기 때문에 MCU 데이터 버스의 크기는 중요하지 않습니다. C11 기능을 사용하지 않는 한 아닙니다 _Atomic. 이 기능을 사용할 수 없으면 세마포어 방식을 사용하거나 읽기 등 동안 인터럽트를 비활성화해야합니다. 인라인 어셈블러는 다른 옵션입니다. volatile원 자성을 보장하지는 않습니다.

    무엇 일어날 수있는 것은 이것이다 :
    레지스터에 스택에서 -로드 값
    -Interrupt가 발생
    레지스터로부터 -use 값을

    "사용 가치"부분 자체가 단일 명령인지 여부는 중요하지 않습니다. 안타깝게도, 모든 임베디드 시스템 프로그래머의 상당 부분이이를 잊어 버려서 아마도 가장 일반적인 임베디드 시스템 버그 일 것입니다. 항상 간헐적이고 자극하기가 어렵고 찾기가 어렵습니다.


올바르게 작성된 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변수를 드라이버에 로컬로 만들어 스파게티 프로그래밍 및 네임 스페이스 오염으로부터 보호합니다. (단일 코어, 단일 스레드 응용 프로그램에서는 문제가 없지만 다중 스레드 응용 프로그램에서는 문제가되지 않습니다.)


디버깅하기 어려운 것은 상대적입니다. 코드를 제거하면 소중한 코드가 사라 졌음을 알 수 있습니다. 이는 뭔가 잘못되었다는 대담한 진술입니다. 그러나 나는 매우 이상하고 디버그하기 어려운 효과가있을 수 있음에 동의합니다.
아스날

@Arsenal C로 어셈블러를 인라인하는 멋진 디버거가 있고 최소한 약간의 asm을 알고 있다면 쉽게 알 있습니다. 그러나 더 큰 복잡한 코드의 경우, 대량의 머신 생성 asm은 쉽지 않습니다. 또는 당신이 asm을 모른다면. 또는 디버거가 엉망이고 asm을 표시하지 않는 경우 (cougheclipsecough).
Lundin

Lauterbach 디버거를 사용하면 약간 망칠 수 있습니다. 최적화 된 코드에서 중단 점을 설정하려고하면 다른 곳에서 설정되고 무언가가 진행되고 있음을 알 수 있습니다.
아스날

Lauterbach에서 얻을 수있는 혼합 C / asm의 종류 인 @Arsenal Yep은 결코 표준이 아닙니다. 대부분의 디버거는 asm을 별도의 창에 표시합니다.
Lundin

semaphore반드시 있어야합니다 volatile! 사실, 그건 요구 느릅 나무 가장 기본적인 유스 케이스 다른 하나의 실행 컨텍스트에서 신호 뭔가 :. -귀하의 예에서 컴파일러 는 값을 덮어 쓰기 전에 값을 읽지 않는다는 것을 알기 때문에 생략 할 수 있습니다 . volatilesemaphore = true;semaphore = false;
JimmyB

5

질문에 제시된 코드 스 니펫에는 아직 휘발성을 사용해야 할 이유가 없습니다. 의 가치가 adcValueADC에서 나온다는 것은 중요하지 않습니다 . 그리고 adcValue글로벌 한 것은 당신 adcValue이 휘발성이어야 하는지 의심스러워 할 것이지만 그 자체로는 이유가 아닙니다.

글로벌이라는 것은 adcValue하나 이상의 프로그램 컨텍스트 에서 액세스 할 수있는 가능성을 열어주는 단서입니다.. 프로그램 컨텍스트에는 인터럽트 처리기와 RTOS 작업이 포함됩니다. 글로벌 변수가 한 컨텍스트에 의해 변경되면 다른 프로그램 컨텍스트는 이전 액세스의 값을 알 수 있다고 가정 할 수 없습니다. 값은 다른 프로그램 컨텍스트에서 변경되었을 수 있으므로 각 컨텍스트는 변수 값을 사용할 때마다 다시 읽어야합니다. 프로그램 컨텍스트는 인터럽트 또는 작업 전환이 발생할 때 인식하지 못하므로 여러 컨텍스트에서 사용되는 전역 변수가 컨텍스트 전환으로 인해 변수의 액세스간에 변경 될 수 있다고 가정해야합니다. 이것이 휘발성 선언의 목적입니다. 컴파일러 에게이 변수가 컨텍스트 외부에서 변경 될 수 있다고 알려주므로 모든 액세스를 읽고 값을 이미 알고 있다고 가정하지 마십시오.

변수가 하드웨어 주소에 메모리 매핑 된 경우 하드웨어에 의한 변경은 사실상 프로그램 컨텍스트 외부의 다른 컨텍스트입니다. 따라서 메모리 매핑도 단서입니다. 예를 들어 readADC()함수가 메모리 매핑 된 값에 액세스하여 ADC 값을 얻는 경우 해당 메모리 매핑 된 변수는 휘발성이어야합니다.

따라서 코드에 더 많은 정보가 adcValue있고 다른 컨텍스트에서 실행되는 다른 코드에 의해 액세스되면 질문에 다시 대답 adcValue해야합니다. 그렇습니다 .


4

"하드웨어에서 직접 변경되는 전역 변수"

값이 일부 하드웨어 ADC 레지스터에서 나온다고해서 하드웨어에 의해 "직접"변경되는 것은 아닙니다.

이 예제에서는 readADC ()를 호출하여 일부 ADC 레지스터 값을 반환합니다. adcValue에 해당 시점에 새로운 값이 할당된다는 것을 알고 컴파일러에 대해서는 괜찮습니다.

ADC 인터럽트 루틴을 사용하여 새 값을 할당하는 경우에는 달라집니다. 새 ADC 값이 준비되면 호출됩니다. 이 경우 컴파일러는 해당 ISR이 언제 호출되는지에 대한 실마리가 없으며 adcValue에 이러한 방식으로 액세스하지 않을 것이라고 결정할 수 있습니다. 휘발성이 도움이되는 곳입니다.


1
코드가 ISR 함수를 "호출"하지 않으므로 컴파일러는 변수가 아무도 호출하지 않는 함수에서만 업데이트되는 것을 확인합니다. 따라서 컴파일러가 최적화합니다.
Swanand

1
adcValue가 어디에서나 읽지 않는 경우 (디버거를 통해서만 읽거나) 한 곳에서 한 번만 읽으면 컴파일러가 코드를 최적화 할 수 있습니다.
Damien

2
@Damien : 항상 "의존적"이지만 실제 질문 인 "이 경우 키워드를 휘발성으로 사용해야합니까?" 가능한 한 짧게
Rev1.0

4

volatile인수 의 동작은 코드, 컴파일러 및 수행 된 최적화에 크게 좌우됩니다.

개인적으로 사용하는 두 가지 사용 사례가 있습니다 volatile.

  • 디버거로보고 싶은 변수가 있지만 컴파일러가 최적화 한 경우 (이 변수를 가질 필요가 없다는 것을 알았 기 때문에 삭제 한 것을 의미 함) 추가 volatile하면 컴파일러가 변수 를 유지하도록 강요합니다. 디버그에서 볼 수 있습니다.

  • 변수가 "코드 외부"로 변경 될 수있는 경우, 일반적으로 변수에 액세스하는 하드웨어가 있거나 변수를 주소에 직접 매핑하는 경우.

임베디드에는 컴파일러에 버그가있어 실제로 작동하지 않는 최적화를 수행하고 때로는 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; b = a; 및 c = b; 컴파일러는 잠시 기다렸다 고 생각할 수 있습니다. a와 b는 쓸모가 없습니다. 1을 c에 직접 넣으십시오. 물론 코드에서 그렇게하지는 않지만 컴파일러는 이것을 찾는 것보다 낫습니다. 또한 최적화 된 코드를 즉시 작성하려고하면 읽을 수 없습니다.
Damien

2
올바른 컴파일러를 사용하는 올바른 코드는 최적화가 설정되어 있어도 중단되지 않습니다. 컴파일러의 정확성은 약간의 문제이지만 적어도 IAR에서는 최적화로 인해 코드가 손상되어서는 안되는 상황이 발생하지 않았습니다.
아스날

5
최적화가 코드를 깨뜨리는 많은 경우는 UB 영역으로도 이동할 때입니다 .
pipe

2
휘발성의 부작용은 디버깅에 도움이 될 수 있다는 것입니다. 그러나 이것이 휘발성을 사용하는 좋은 이유는 아닙니다. 쉬운 디버깅이 목표라면 최적화를 해제해야합니다. 이 답변에는 인터럽트에 대한 언급조차 없습니다.
kkrambo

2
디버깅 인수에 추가 volatile하면 컴파일러에서 변수를 RAM에 저장하고 값이 변수에 할당되는 즉시 해당 RAM을 업데이트합니다. 대부분의 경우 컴파일러는 변수를 '삭제'하지 않습니다. 일반적으로 할당없이 효과를 쓰지 않지만 일부 CPU 레지스터에 변수를 유지하기로 결정하고 나중에 해당 레지스터의 값을 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을 통해 무언가가 일어날 수 있다는 표시로 -qualified 포인터는 컴파일러가 이해하지 못한다는 것을 의미합니다. 저자는 컴파일러가 다양한 플랫폼과 목적으로 설계된 컴파일러 작성자가 컴파일러가 플랫폼에서 그러한 목적을 수행 할 때 인식 할 것이라고 생각했기 때문에 컴파일러를 이해하지 못함을 의미합니다 말할 필요없이. 결과적으로 gcc 및 clang과 같은 일부 "영리한"컴파일러는의 주소가 output_buffer두 저장소 사이의 휘발성 정규 포인터에 쓰여지 더라도 output_buffer[0]어떤 객체에서 보유한 값에 관심이 있다고 가정 할 이유가 없습니다. 그때.

또한, 정수에서 직접 캐스트되는 포인터는 컴파일러가 이해하지 못하는 방식으로 사물을 조작하는 것 이외의 다른 목적으로 거의 사용되지 않지만 표준은 컴파일러에게 이러한 액세스를 처리하도록 요구하지 않습니다 volatile. 결과적으로 첫 번째 쓰기 *((unsigned short*)0xC0001234)는 gcc 및 clang과 같은 "영리한"컴파일러에 의해 생략 될 수 있습니다. 왜냐하면 그러한 컴파일러의 관리자는 오히려 volatile그러한 코드와의 호환성이 유용하다는 것을 인식하는 것보다 "깨진"것으로 자격을 부여하는 것을 무시하는 코드를 주장하기 때문 입니다 . 많은 공급 업체 제공 헤더 파일은 volatile한정자를 생략 하고 공급 업체 제공 헤더 파일과 호환되는 컴파일러는 그렇지 않은 것보다 더 유용합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.