C언어 volatile 키워드 정리: 임베디드에서 왜 필요할까

카드뉴스

volatile은 안전장치가 아니라 컴파일러에게 주는 신호입니다

C언어에서 `volatile`을 처음 보면 “변하지 않는 값”처럼 오해하기 쉽지만 의미는 반대에 가깝습니다. 임베디드에서 volatile은 이 값이 코드 흐름 밖에서 바뀔 수 있으니 접근할 때마다 다시 읽거나 쓰라고 컴파일러에게 알려주는 표시입니다. 중심은 컴파일러 최적화입니다. 컴파일러는 프로그램을 빠르게 만들기 위해 불필요하다고 판단한 메모리 읽기와 쓰기를 줄일 수 있습니다. 일반 변수라면 도움이 되지만, 하드웨어 레지스터나 인터럽트 플래그에서는 문제가 될 수 있습니다.

예를 들어 메인 루프가 어떤 플래그가 1이 되기를 기다리고 있고, 그 플래그를 인터럽트 서비스 루틴에서 바꾼다고 생각해 볼 수 있습니다. 컴파일러가 메인 코드만 보고 “이 변수는 루프 안에서 바뀌지 않는다”고 판단하면 값을 한 번만 읽고 계속 같은 값으로 처리할 여지가 생깁니다. volatile은 이런 상황에서 매번 메모리에서 다시 읽으라는 의도를 전달합니다.

자주 쓰이는 곳은 ISR, 레지스터, DMA 상태값입니다

volatile이 자주 등장하는 대표적인 곳은 인터럽트에서 바뀌고 메인 루프에서 읽는 공유 플래그입니다. UART 수신 완료, 타이머 tick, 외부 입력 감지처럼 ISR이 짧게 상태만 남기고 실제 처리는 메인에서 하는 구조에서 사용됩니다. 또 하나는 메모리맵 하드웨어 레지스터입니다. 레지스터를 읽는 행위 자체가 하드웨어 상태 확인이고, 쓰는 행위가 주변장치 제어이므로 일반 변수처럼 최적화되면 안 됩니다.

DMA도 비슷합니다. CPU가 직접 쓰지 않았는데 DMA 컨트롤러가 버퍼나 완료 플래그를 바꿀 수 있습니다. 이때 CPU 코드만 기준으로 판단하면 값이 변하지 않는 것처럼 보일 수 있습니다. 다만 캐시가 있는 MCU나 MPU 환경에서는 volatile만으로 충분하지 않을 수 있고, cache invalidate, memory barrier 같은 별도 처리가 필요할 수 있습니다.

또 하나 확인할 점은 포인터가 가리키는 대상입니다. 하드웨어 레지스터 주소를 다룰 때는 보통 `volatile` 포인터를 통해 접근합니다. 이때 포인터 변수 자체가 volatile인지, 포인터가 가리키는 데이터가 volatile인지가 다를 수 있습니다. 레지스터 정의 헤더를 읽을 때 이 차이를 보면 주변장치 접근 코드가 더 명확해집니다.

volatile만 붙인다고 동기화 문제가 사라지지는 않습니다

가장 위험한 오해는 volatile이 원자성이나 thread-safe를 보장한다고 생각하는 것입니다. volatile은 “접근을 생략하지 말라”는 의미이지, 동시에 접근해도 안전하다는 의미가 아닙니다. 값을 읽고, 1을 더하고, 다시 쓰는 과정은 여러 명령으로 나뉠 수 있고 그 사이에 인터럽트가 끼어들 수 있습니다. 이런 경우에는 임계구역, 인터럽트 비활성화, atomic 연산, RTOS 동기화 도구를 별도로 검토해야 합니다.

따라서 volatile을 판단할 때는 질문을 분리하는 것이 좋습니다. “이 값이 현재 코드 밖에서 바뀔 수 있는가”는 volatile의 영역입니다. “동시에 접근해도 깨지지 않는가”는 동기화의 영역입니다. 두 문제를 섞지 않아야 임베디드 C 코드에서 불필요하게 volatile을 남발하지 않고, 필요한 곳에서는 빠뜨리지 않을 수 있습니다.

댓글 남기기