출처 카페 > 임베디드 시스템(Device.. / 별빛
원본 http://cafe.naver.com/kucira/484

먼저 다음 물음에 답해보자.


혹시 당신이 C로짠 임베디드 코드에서 다음과 같은 경우를 경험한 적이 있는가?


* 옴티마이즈 옵션을 켜기 전까지는 코드가 잘 동작한다.
* 어떤 인터럽트를 disable 시킨 동안에는 코드가 잘 동작한다.
* RTOS가 탑재된 멀티태스킹 시스템에서 어떤 태스크(TASK)가 enable 되기 전까지는 태스크가 잘 동작한다.


만약 위의 물음에 "네(yes)"라고 대답한다면 그것은 바로 당신이 volatile라는 C keyword를 사용하지 않았기 때문이다. 이건 비단 당신혼자만의 문제는 아니다. 많은 프로그래머들이 volatile 키워드에 대해서 어설프게 잘못 알고 있거나 제대로 사용하지 않고 있다. 이건 그리 놀랄만한 일이 아닌데 그건 바로 많은 C 책이 이점에 관해서 너무 하리만큼 무심하기 때문이다.


volatile은 변수를 선언할 때 같이 사용하는 키워드이다. volatile을 사용함으로 인해 컴파일러에게 volatile과 함께 선언된 변수는 언제 어느 때든지 값이 바뀔수 있다는 것을 말한다. 구체적인 사용 예를 들기 전에 먼저 volatile에 대한 문법적인 사항을 알아보자. volatile 변수를 사용하기 위해 volatile 키워드를 정의된 변수의 데이터 타입 앞 또는 뒤에 명시하면 된다. 다음과 같이 말이다.

       

volatile int foo;
int volatile foo;


자, 그럼 포인터에서는 어떻게 될까? 포인터에서 뭔가 특별한 점이 있다고 생각하는가? 포인터라 해서 별반 다를게 없다. 포인터 역시 다음과 같이 하면된다.


volatile int *foo;
int volatile *foo;


마지막으로 volatile을 struct나 union에 적용시켜버리면 struct나 union의 모든 내용들은 volatile이다. 만약 위와 같은 경우를 원하지 않는다면 어떻게 할것인가? struct나 union 멤버에게 개별적으로 사용하면 된다. 자, 그럼 본격적인 사용법을 알아보자.


어떤 변수들이 예고 없이 값이 바뀔 수 있을 가능성이 있는 경우에는 volatile로 선언해야 한다. 사실상 다음의 3가지 타입의 변수들이 바뀔 수 있다.


* memory-mapped periherral registers
* 인터럽트 서비스 루틴에 의해 수정되는 전역변수
* 멀티 태스킹 또는 멀티 쓰레드에서 사용되는 전역변수


그럼 먼저 첫번째 항목에 대해서 좀더 자세히 알아보자.


임베디드 시스템에서는 진보되고 복잡한 실질적인 주변 디바이스(peripheral)를 포함하게 된다. 이런 peripheral들은 프로그램 흐름과 비동기적으로 값들이 변하는 레지스터들을 가지고 있는 경우가 대부분이다. 매우 간단한 예로 0x1234 address에 위치한 8비트 status 레지스터가 있다고 가정하고 생각해보자. 만약 이 레지스터가 0이 아닌 값을 가질때까지 이 레지스터를 폴링(polling)한다고 가정해보자. 그럼 당신은 분명히 다음과 같이 코드를 작성할것이다.


INT8U *ptr = (INT8U *)0x1234; //wait for register to become non-zero
while (*ptr == 0); //Do something else


만약 당신이 옴티마이즈 옵션을 켰다면 위의 코드는 제대로 동작하지 않을 확률이 굉장히 높다. 왜냐하면 컴파일러는 당신이 작성한 코드에 대해서 다음과 같은 어셈블러를 생성할 것이다. 반드시 유심히 보길 바란다. 중요하다.


move ptr, #0x1234
move a, @ptr
loop bz loop


자, 한번 분석해보자. 컴파일러는 굉장히 똑똑하게 어셈블리 코드를 생성한 것을 볼 수 있다. 첨에 한번반 0x1234를 액세스해서 값을 로딩한 이후로 두번 다시는 0x1234를 억세스 하지 않는다. 두 번째 코드에서 볼 수 있듯이 값은 accumulator에 이미 로딩이 되있기 때문에 값을 재 로딩할 필요가 없다고 컴파일러는 판단하기 때문이다. 왜냐하면 값은 항상 같다고 보기 때문이다. 그러므로 3 번째 라인에 의해 당신이 작성한 코드는 무한 루프에 빠지게 된다. 정작 우리가 원하는 동작을 하기 위해서는 위의 코드를 다음과 같이 수정해야 한다.


INT8U volatile * ptr = (INT8U volatile *)0x1234;


그럼 컴파일러는 이제 다음과 같이 어셈블러를 생성할 것이다.


mov ptr, #0x1234
loop mov a, @ptr
bz loop


자, 어떤가? 드디어 당신이 원하는 결과를 얻게 될 것이다.


자, 그럼 인터럽트 서비스 루틴의 경우에 대해서 생각해보자. 종종 인터럽트 서비스 루틴은 메인함수에서 테스트하는 변수를 셋팅하게된다. 예를 들어 시리얼 포트 인터럽트는 각각에 수신한 캐릭터에 대해 ETX 캐릭터가 수신됐는지를 테스트한다고 가정해보자. 만약 ETX가 수신되면 인터럽트 서비스 루틴은 전역 플래그를 셋팅할 것이다. 불완전한 코드를 다음에 보이겠다.


int ETXRcvd = FALSE;


void main (void)
{
        ...
        while (!ETXRcvd)

        {
                //what
        }
        ...
}


interrupt void RxISR (void)
{
        ...


        if (rx_char == ETX)

        {
                ETXRcvd = TRUE;
        }

        ...
}


옵티마이즈 옵션을 꺼논 동안에는 코드가 올바르게 작동할 것이다. 그러나, 그렇지 않을 경우에는? 문제는 컴파일러는 ETXRcvd가 인터럽트 서비스 루틴에 의해서 값이 바꼈을 경우 이를 알 수 없는 경우가 생긴다. 위에 peripheral 예에서 들었듯이 !EXTRcvd는 항상 참이기 때문에 while 루프를 절대 벗어날 수 없는 경우가 생길 수도 있다. 게다가 심지어는 이런 이유로 인해 루프 이후에 코드들은 옵티마이즈에 의해 제거 되어버릴 수도 있다. 만약 당신이 운 좋은 놈이라면 당신의 컴파일러는 이런 문제에 대해서 경고 메세지를 보내게 될것이다. 그렇지 않고 당신이 운좋은 놈이 아니거나 컴파일러가 제공하는 경고 메세지가 때로는 얼마나 무서운 것인지를 경험해보지 못했다면 어떻게 될까? 말안해도 알리라 본다. 마지막으로 멀티 쓰레드또는 멀티 태스킹 어플리케이션 경우를 생각해 봐야 한다.

by sminchoi 2007. 5. 15. 09:29