C 포인터로 MCU 레지스터 제어하기: 주소·volatile·구조체 이해

카드뉴스

MCU 레지스터 제어는 주소에서 시작한다

GPIO, UART, Timer 같은 주변장치는 CPU의 주소 공간 안에 놓인 레지스터로 제어한다. 출력 값을 바꾸거나 상태를 읽는 일도 결국 정해진 주소에 데이터를 쓰고 읽는 버스 접근이다. 코드에 보이는 `GPIOA` 같은 이름은 사람이 이해하기 쉽도록 붙인 것이며, CPU가 실제로 다루는 대상은 주소와 그곳의 값이다.

그래서 다른 보드의 예제를 가져올 때는 주소부터 확인해야 한다. 주변장치의 베이스 주소와 각 레지스터의 오프셋은 MCU 제품군에 따라 달라진다. 예제의 숫자를 그대로 옮기지 말고 사용 중인 칩의 메모리 맵과 레지스터 표를 기준으로 맞춰야 한다.

주소를 포인터로 바꾸고 `volatile`을 붙이는 이유

32비트 레지스터 주소를 C에서 사용하려면 `volatile uint32_t *` 형식의 포인터로 변환한 뒤 역참조한다. `*(volatile uint32_t *)address`라는 표현은 해당 주소를 32비트 레지스터로 읽거나 쓰겠다는 뜻이다. 이때 레지스터 폭과 주소 정렬이 하드웨어 정의에 맞아야 한다.

`volatile`은 코드 밖의 하드웨어가 값을 바꿀 수 있다는 사실을 컴파일러에 알린다. 상태 레지스터를 다시 읽는 동작이나 제어 레지스터에 쓰는 동작이 최적화 과정에서 생략되지 않게 하는 장치다. 다만 연산을 원자적으로 만들거나 인터럽트 경쟁과 동기화 문제까지 해결해 주지는 않는다.

구조체와 비트 마스크로 읽기 쉽게 만든다

주변장치 하나에는 여러 레지스터가 있으므로 실제 오프셋 순서에 맞춘 구조체를 사용한다. 베이스 주소를 그 구조체 포인터로 보면 `GPIOA->MODER`, `GPIOA->ODR`처럼 이름으로 접근할 수 있다. 멤버의 크기와 순서, 예약 영역이 어긋나면 전혀 다른 주소를 읽게 되므로 장치 헤더의 정의를 우선 확인하는 편이 안전하다.

특정 비트만 바꿀 때는 OR로 set, AND와 반전 마스크로 clear, XOR로 toggle한다. 읽기-수정-쓰기가 문제가 되는 레지스터라면 전용 set/reset 레지스터가 있는지도 살핀다. Bit-field와 union은 보기에는 편하지만 비트 배치가 컴파일러에 따라 달라질 수 있어, 명시적인 시프트와 마스크가 더 분명할 때가 많다.

접근 속성까지 읽어야 코드가 완성된다

레지스터 표의 R/W, RO, WO, W1C 표시는 단순한 주석이 아니다. 읽기 전용 레지스터에는 쓰지 말아야 하고, W1C 플래그는 1을 써서 지운다. 예약 비트도 매뉴얼이 지정한 값을 보존해야 한다.

레지스터 코드를 검토할 때는 베이스 주소, 오프셋, 접근 폭, `volatile`, 비트 마스크, 접근 속성을 차례로 대조하면 된다. 이 항목이 맞아야 C 포인터가 의도한 하드웨어 동작으로 이어진다.

댓글 남기기