포스트

임베디드 스터디 - volatile과 const

임베디드 스터디 - volatile과 const

이번 글 참고자료:
EmbeddedInterviewlab

Volatile의 역할

  • volatile은 임베디드 소프트웨어에서 세 가지 역할을 수행한다.
  • volatile 변수는 원자적(Atomic)인 동작을 수행하지 않는다.
    • 원자적 연산(Atomic Operation) : 모든 명령의 순서가 일관적으로 유지되며 중간 상태를 가지지 않고, 하위 기계어 명령어로 분해되지 않는 명령어

      1. 레지스터 캐싱 방지

  • volatile이 없는 변수
    • 선언 이후 변수의 변화가 없는 경우, 컴파일러는 CPU 레지스터에 해당 변수를 저장한 후 더 이상 변수의 변화를 확인하지 않도록 빌드한다.
  • volatile 변수
    • 선언 이후 변화가 없어도, 컴파일러는 해당 변수를 매번 호출할 때마다 RAM에서 읽도록 빌드한다.
1
2
3
4
5
6
7
/* Without volatile: compiler may read flag once and loop forever */
uint8_t flag = 0;          /* set by ISR */
while (flag == 0) { }      /* compiler: "flag is 0, it never changes in this loop" */

/* With volatile: compiler re-reads flag from RAM every iteration */
volatile uint8_t flag = 0; /* set by ISR */
while (flag == 0) { }      /* compiler: "flag is volatile, I must re-read it" */

2. Read/Write 재배치

  • volatile이 없는 변수
    • CPU 동작 최적화를 위해 컴파일러에서 읽기/쓰기 동작의 순서를 재배치하는 경우도 있다.
  • volatile 변수
    • CPU 동작 효율성과 관계없이 컴파일러가 무조건 순차적으로 명령을 실행하도록 빌드한다.
    • 단, CPU 하드웨어 reordering(out-of-order execution, store buffer)은 막지 못하며, 이를 위해서는 별도의 memory barrier(__DMB(), __DSB() 등)가 필요함.
1
2
/* volatile prevents compiler reordering only.
   For hardware ordering (multi-core / cache), use __DMB() / __DSB(). */

3. Dead-Store 제거 방지

  • volatile이 없는 변수
    • 컴파일러가 하나의 변수가 읽기 명령 없이 연속적으로 데이터가 변하는 경우, 이전 쓰기 명령을 삭제한다.
  • volatile 변수
    • 컴파일러가 하나의 변수에 연속적인 쓰기 명령이 있어도 명령을 유지시킨다.

Volatile의 활용

하드웨어 레지스터 접근

  • Peripheral register는 메모리에 고정 주소로 매핑되는데, 해당 주소의 읽기, 쓰기 동작이 곧 해당 장치의 명령, 혹은 상태를 확인하 수 있는 요소이다. 따라서 해당 변수는 컴파일러에 의해 최적화되면 안된다.
1
2
3
4
5
6
7
8
9
10
11
/* Typical register pointer: volatile pointer-to-volatile data, const address */
#define GPIOA_ODR  (*(volatile uint32_t *)0x40020014)  /* output data reg */
#define GPIOA_IDR  (*(volatile uint32_t *)0x40020010)  /* input data reg  */

void toggle_led(uint8_t pin) {
    GPIOA_ODR ^= (1U << pin);   /* read-modify-write: must hit hardware */
}

uint8_t read_button(uint8_t pin) {
    return (GPIOA_IDR >> pin) & 1U;  /* must read hardware, not a cached copy */
}
  • 각 칩 제조사에서 제공하는 HAL과 같이 struct 타입 안에 각각의 변수를 volatile로 선언하면 훨씬 깔끔하다.
1
2
3
4
5
6
7
8
9
10
typedef struct {
    volatile uint32_t MODER;   /* mode register        */
    volatile uint32_t OTYPER;  /* output type register  */
    volatile uint32_t OSPEEDR; /* output speed register */
    volatile uint32_t PUPDR;   /* pull-up/pull-down     */
    volatile uint32_t IDR;     /* input data register   */
    volatile uint32_t ODR;     /* output data register  */
} GPIO_TypeDef;

#define GPIOA ((GPIO_TypeDef *)0x40020000)

ISR-Shared 변수

  • ISR(Interrupt Service Routine)를 사용할 경우, main()에서 해당 변수의 동작 확인을 위해 무조건 volatile 변수로 선언해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
volatile uint8_t rx_complete = 0;  /* set by UART ISR */

void UART_IRQHandler(void) {
    /* ... read data from DR ... */
    rx_complete = 1;               /* signal main loop */
}

int main(void) {
    while (1) {
        if (rx_complete) {         /* compiler must re-read every iteration */
            rx_complete = 0;
            process_rx_data();
        }
    }
}

Volatile Pointer vs Pointer to volatile

  • 포인터 변수의 선언 방식에 따라 변수 특징이 달라진다
    • volatilePTR : 데이터는 volatile하지만, 포인터 자체는 volatile이 아님. 해당 변수의 주소는 레지스터에 저장됨.
    • PTRvolatile : 포인터는 volatile하지만, 데이터 자체는 volatile이 아님. 즉, 외부로부터 포인터가 변경될 수도 있음.
1
2
3
4
5
6
7
8
9
volatile uint32_t *p;       /* pointer to volatile data (the DATA is volatile)      */
                            /* p itself can be cached in a register                  */

uint32_t * volatile p;      /* volatile pointer to non-volatile data (the PTR is volatile) */
                            /* rarely useful -- the pointer itself changes externally */

volatile uint32_t * const p = (volatile uint32_t *)0x40020014;
/* const pointer to volatile data -- most common for register pointers:             */
/* the address never changes, but the data at that address can change any time      */

Const의 역할

  • 변수의 쓰기 명령 방지
  • 플래시 메모리와 RAM이 별도로 있는 MCU에서 const 변수는 .rodata(플래시 메모리)에 저장함.
    즉, RAM의 저장공간 확보와 부트타임 개선이 가능함
1
2
3
4
5
/* .rodata (flash) -- zero RAM cost, survives power cycles */
const uint16_t sin_lut[256] = { 0, 402, 804, /* ... */ };

/* .data (RAM, copied from flash at boot) -- costs RAM + boot time */
uint16_t mutable_lut[256] = { 0, 402, 804, /* ... */ };
  • 함수의 입력 변수로 사용하여 컴파일러를 통한 에러 확인이 가능함
1
2
3
4
5
/* Caller knows this function will not modify the buffer */
void transmit(const uint8_t *data, size_t len);

/* Without const, caller cannot be sure their buffer is safe */
void transmit(uint8_t *data, size_t len);

Pointer-Const 조합

  • 총 네 가지로 구분하여 조합할 수 있다.
DeclarationRead right-to-leftWhat is const?Embedded use case
int *p“p is a pointer to int”NothingGeneral mutable pointer
const int *p“p is a pointer to const int”The dataRead-only access to a buffer or LUT
int * const p“p is a const pointer to int”The pointerFixed-address hardware register (writable)
const int * const p“p is a const pointer to const int”BothFixed-address read-only register

Right-to-Left Rule
const나 volatile로 선언된 변수의 경우, 영어로 해석할 때 변수명부터 왼쪽으로 읽어서 해석한다.
volatile uint32_t * const reg : “reg is a const pointer to volatile uint32_t.”

Volatile const

  • volatile const는 센서의 출력이나, Read-Only 레지스터에 대해 사용한다.
    • volatile가 없으면 하드웨어에서 변수를 수정해도 main()에서 변수 변동에 대해 확인을 못한다.
    • const가 없으면 main()에서 해당 레지스터를 수정할 수 있다.

MISRA C 규정

  • MISRA C Rule 8.13 : 변수를 수정하지 않는 포인터는 무조건 const로 선언한다.
  • MISRA C Rule 2.2 : volatile로 선언된 변수는 읽지 않아도 Dead-Code로 취급하지 않는다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.