MMIO Register 추상화 — 타입 안전한 비트 필드 접근
#한 줄 요약
“Register는 typed pointer와 bit operation의 결합입니다.” magic number 매크로 대신 type-safe wrapper를 씁니다.
#어떤 문제를 푸는가
전통적인 C 매크로 방식입니다.
#define GPIOA_ODR (*(volatile uint32_t*)0x40020014)#define GPIO_PIN_5 (1 << 5)
GPIOA_ODR |= GPIO_PIN_5;문제는 다음과 같습니다.
- magic number만 있고 type 정보가 없습니다
- wrong register에 wrong value를 쓸 수 있습니다
- 디버거에서 의미를 알 수 없습니다
- pin과 register 매칭이 보이지 않습니다
C++ type abstraction은 다음과 같습니다.
template<uintptr_t Address>struct Register { static volatile uint32_t& ref() { return *reinterpret_cast<volatile uint32_t*>(Address); } static void set(uint32_t mask) { ref() |= mask; } static void clear(uint32_t mask) { ref() &= ~mask; } static uint32_t read() { return ref(); }};
using GpioA_ODR = Register<0x40020014>;GpioA_ODR::set(1 << 5);컴파일러가 인라인해 매크로와 동일한 어셈블리가 나옵니다. type-safe하고 디버깅도 가능합니다.
#volatile의 필수
memory-mapped register는 컴파일러 최적화를 회피해야 합니다.
// 잘못 — volatile 없음uint32_t* reg = reinterpret_cast<uint32_t*>(0x40020014);*reg = 0xFF;*reg = 0xFF; // 컴파일러가 중복 제거 — 단 한 번만 실행
// 올바름 — volatilevolatile uint32_t* reg = reinterpret_cast<volatile uint32_t*>(0x40020014);*reg = 0xFF;*reg = 0xFF; // 두 번 모두 실행 (hardware sequencing 필요)volatile은 매 access를 강제합니다. peripheral과의 정확한 통신에 필수입니다.
#단순 wrapper
template<uintptr_t Address>class Reg32 {public: static void write(uint32_t value) { ptr() = value; }
static uint32_t read() { return ptr(); }
static void set_bits(uint32_t mask) { ptr() |= mask; }
static void clear_bits(uint32_t mask) { ptr() &= ~mask; }
static void modify(uint32_t clear_mask, uint32_t set_mask) { uint32_t v = ptr(); v = (v & ~clear_mask) | set_mask; ptr() = v; }
static bool is_set(uint32_t mask) { return (ptr() & mask) != 0; }
private: static volatile uint32_t& ptr() { return *reinterpret_cast<volatile uint32_t*>(Address); }};
using GPIOA_ODR = Reg32<0x40020014>;using GPIOA_BSRR = Reg32<0x40020018>;using USART2_DR = Reg32<0x40004404>;
GPIOA_ODR::set_bits(1 << 5); // Pin 5 highGPIOA_BSRR::write(1 << 5); // Atomic set (HW supported)각 register가 별도 type을 갖습니다. 서로 섞이지 않습니다.
#Bit field — 명명된 비트
32-bit MMIO 레지스터는 보통 여러 비트 필드로 쪼개져 있습니다. 예를 들어 STM32의 USART_CR1은 다음처럼 enable 비트, mode 비트, parity 비트 등이 한 워드에 모여 있습니다.
매직 넘버 대신 타입으로 명명된 비트를 두면 컴파일러가 검사하고 가독성이 살아납니다.
template<int Bit>struct BitField { static constexpr uint32_t mask = 1u << Bit;};
namespace gpio_moder { struct Mode0 { static constexpr uint32_t mask = 0b11 << 0; }; struct Mode1 { static constexpr uint32_t mask = 0b11 << 2; }; struct Mode2 { static constexpr uint32_t mask = 0b11 << 4; }; // ... struct Mode5 { static constexpr uint32_t mask = 0b11 << 10; };}
// 사용template<typename Field>void set_gpio_mode_output(int pin) { constexpr uint32_t mask = Field::mask; constexpr uint32_t output = 0b01; // output mode GPIOA_MODER::modify(mask, output << (pin * 2));}복잡합니다. 각 field에 enum class를 쓰는 편이 더 깔끔합니다.
enum class GpioMode : uint32_t { Input = 0b00, Output = 0b01, Alt = 0b10, Analog = 0b11,};
template<int Pin>void set_mode(GpioMode mode) { constexpr uint32_t shift = Pin * 2; constexpr uint32_t clear_mask = 0b11u << shift; uint32_t set_mask = static_cast<uint32_t>(mode) << shift; GPIOA_MODER::modify(clear_mask, set_mask);}
set_mode<5>(GpioMode::Output);Pin이 컴파일 타임 상수이므로 shift도 컴파일 타임에 계산됩니다.
#CMSIS-style — 구조체 매핑
ARM CMSIS의 표준 패턴입니다. struct를 register block에 매핑합니다.
struct GpioRegs { volatile uint32_t MODER; // 0x00 volatile uint32_t OTYPER; // 0x04 volatile uint32_t OSPEEDR; // 0x08 volatile uint32_t PUPDR; // 0x0C volatile uint32_t IDR; // 0x10 volatile uint32_t ODR; // 0x14 volatile uint32_t BSRR; // 0x18 volatile uint32_t LCKR; // 0x1C volatile uint32_t AFR[2]; // 0x20-0x24};
static_assert(sizeof(GpioRegs) == 0x28);static_assert(offsetof(GpioRegs, ODR) == 0x14);
#define GPIOA (reinterpret_cast<GpioRegs*>(0x40020000))#define GPIOB (reinterpret_cast<GpioRegs*>(0x40020400))
GPIOA->ODR |= 1 << 5;장점은 다음과 같습니다.
- struct member로 register 접근이 명확합니다
- 디버거가 모든 register를 보여줍니다
- offset이 자동으로 계산됩니다
단점은 다음과 같습니다.
- raw pointer와 macro 조합이라 type safety가 약합니다
- 수정 시 wrong register를 건드릴 수 있습니다
#Type-safe peripheral 객체
CMSIS struct를 wrapping합니다.
class Gpio { GpioRegs* regs_;
public: explicit Gpio(GpioRegs* regs) : regs_(regs) {}
void set_mode(int pin, GpioMode mode) { uint32_t v = regs_->MODER; v &= ~(0b11u << (pin * 2)); v |= static_cast<uint32_t>(mode) << (pin * 2); regs_->MODER = v; }
void set_pin(int pin) { regs_->BSRR = 1u << pin; } void clear_pin(int pin) { regs_->BSRR = 1u << (pin + 16); } bool read_pin(int pin) const { return (regs_->IDR >> pin) & 1; }};
static Gpio gpio_a(GPIOA);static Gpio gpio_b(GPIOB);
gpio_a.set_mode(5, GpioMode::Output);gpio_a.set_pin(5);method 호출이 명확합니다. 디버거에서 step into도 가능합니다.
#Template 기반 register
가장 type-safe하고 zero-cost한 방식입니다.
template<uintptr_t Address>struct GpioPort { static constexpr uintptr_t base = Address;
static GpioRegs& regs() { return *reinterpret_cast<GpioRegs*>(base); }
template<int Pin> static void set_mode(GpioMode mode) { constexpr uint32_t shift = Pin * 2; constexpr uint32_t mask = 0b11u << shift; uint32_t v = regs().MODER; v = (v & ~mask) | (static_cast<uint32_t>(mode) << shift); regs().MODER = v; }
template<int Pin> static void set_pin() { regs().BSRR = 1u << Pin; }
template<int Pin> static void clear_pin() { regs().BSRR = 1u << (Pin + 16); }};
using GpioA = GpioPort<0x40020000>;using GpioB = GpioPort<0x40020400>;
GpioA::set_mode<5>(GpioMode::Output);GpioA::set_pin<5>();GpioA::clear_pin<5>();Address와 Pin이 모두 template parameter로 들어가 완전히 컴파일 타임에 처리됩니다. 어셈블리 출력은 다음과 같습니다.
GpioA::set_pin<5>: ldr r3, =0x40020018 movs r2, #32 ; 1 << 5 str r2, [r3] bx lr매크로 출력과 완전히 동일합니다. zero-cost이면서 type-safe하고 IntelliSense도 받을 수 있습니다.
#임베디드 — UART register 추상화
template<uintptr_t Address>class Uart { struct Regs { volatile uint32_t SR; volatile uint32_t DR; volatile uint32_t BRR; volatile uint32_t CR1; volatile uint32_t CR2; volatile uint32_t CR3; volatile uint32_t GTPR; };
static Regs& regs() { return *reinterpret_cast<Regs*>(Address); }
public: static void init(uint32_t baud, uint32_t clock_hz) { regs().BRR = clock_hz / baud; regs().CR1 = (1 << 13) | (1 << 3) | (1 << 2); // UE | TE | RE }
static void send(uint8_t b) { while (!(regs().SR & (1 << 7))); // TXE regs().DR = b; }
static uint8_t receive() { while (!(regs().SR & (1 << 5))); // RXNE return regs().DR; }};
using Uart2 = Uart<0x40004400>;
Uart2::init(115200, 168'000'000);Uart2::send('H');peripheral별로 type이 갈립니다. 각 UART는 별도 인스턴스가 아니라 별도 type입니다.
#Bit operations
C++의 bit operation은 C와 동일하지만 type safety가 더 강합니다.
// 옛 C 방식 — magic numberGPIOA->MODER &= ~(0b11 << 10); // pin 5 clearGPIOA->MODER |= (0b01 << 10); // pin 5 output
// type-safe (위 패턴)GpioA::set_mode<5>(GpioMode::Output);#RAII로 peripheral lifecycle 관리
template<typename Uart>class UartGuard {public: UartGuard() { Uart::init(115200, 168'000'000); }
~UartGuard() { Uart::shutdown(); // 자동 power down }};
void burst_log() { UartGuard<Uart2> uart; // turn on Uart2::send_string("hello"); // 자동 power down at function exit}power saving이 자연스럽게 됩니다. 자세한 내용은 Part 2-01 RAII에서 다룹니다.
#자주 보는 함정과 안티패턴
#1. volatile 누락
uint32_t* reg = (uint32_t*)0x40020014; // volatile 없음*reg = 0xFF; // 컴파일러 최적화로 사라질 수 있음항상 volatile을 붙입니다.
#2. Bit field struct 사용
struct Reg { uint32_t bit0 : 1; uint32_t bit1 : 1; // ...};ABI에 의존합니다(endian, packing, ordering). portability가 깨집니다. 명시적 비트 마스크를 권장합니다.
#3. Read-Modify-Write를 atomic으로 가정
GPIOA->ODR |= mask; // RMW — ISR이 끼어들면 race대안은 BSRR을 사용하거나(HW atomic) critical section을 두는 것입니다.
#4. Magic number 매크로
#define GPIO_MODE 0b01enum class를 활용해 type safety를 확보합니다.
#5. Template parameter overflow
GpioA::set_mode<32>(/* */); // Pin 32? — 컴파일 OK but invalidstatic_assert(Pin < 16, "Invalid pin")을 추가합니다.
#6. Wrong alignment
64-bit register를 32-bit access하면 bus fault가 납니다. 알맞은 type을 사용합니다.
#측정 — 매크로 vs Template
같은 GPIO blink 코드 비교입니다.
# C 매크로GPIOA_ODR |= (1 << 5);GPIOA_ODR &= ~(1 << 5);
어셈블리: 8 bytes per call
# C++ TemplateGpioA::set_pin<5>();GpioA::clear_pin<5>();
어셈블리: 8 bytes per call (동일)완전히 동일합니다. 다만 C++는 추가로 다음을 얻습니다.
- type safety
- 디버거에서 함수 이름이 보입니다
- IntelliSense를 받을 수 있습니다
#정리
- Memory-mapped register는 volatile과 typed pointer의 결합으로 표현합니다.
- 템플릿으로 Address와 Pin을 모두 compile-time에 결정하면 zero-cost가 됩니다.
- CMSIS struct를 wrapper class로 감싸는 것이 표준 패턴입니다.
- enum class로 bit value를 type-safe하게 다룹니다.
- RAII로 peripheral lifecycle을 관리합니다.
- Bit field struct는 ABI 위험이 있으므로 명시적 마스크를 권장합니다.
#관련 항목
- Part 4-05: Type-safe Flags — bit flag
- Part 5-02: GPIO 추상화
- Part 5-03: Peripheral 추상화
- Part 2-06: Templates 기초
#다음 글
Part 5-02: GPIO 추상화 — GPIO pin을 template 기반 type으로 다루며 컴파일 타임 검증을 적용합니다.
Embedded C++ for Real Systems · 38 of 41
- 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
- 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
- 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
- 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
- 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
- 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
- 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
- 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
- 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
- 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
- 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
- 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
- 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
- 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
- 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
- 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
- 17CRTP 패턴 분석 — vtable 없는 정적 다형성
- 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
- 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
- 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
- 21Custom Allocator 기초 — std::allocator 인터페이스 분석
- 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
- 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
- 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
- 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
- 26std::expected 분석 — C++23 결과 타입과 에러 전파
- 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
- 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
- 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
- 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
- 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
- 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
- 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
- 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
- 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
- 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
- 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
- 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
- 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
- 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
- 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
관련 글
GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
GPIO pin = type — 컴파일 타임에 핀 설정 검증, runtime 비용 0.
임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
타입 안전한 상태 머신 — enum + switch부터 std::variant, etl::fsm까지.
Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
enum class + bit operators — type-safe 비트 플래그. 의도하지 않은 변환 차단.
이 글을 참조하는 글 (7)
- 임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교— Embedded C++ for Real Systems
- Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계— Embedded C++ for Real Systems
- GPIO 추상화 패턴 — Template·Concept으로 보드 독립성— Embedded C++ for Real Systems
- Singleton 대안 패턴 — Service Locator·Static Init·Phantom— Embedded C++ for Real Systems
- Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag— Embedded C++ for Real Systems
- constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성— Embedded C++ for Real Systems
- constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용— Embedded C++ for Real Systems