Cortex-M3: 실시간 임베디드 시스템의 설계 철학을 이해하다
임베디드 개발에서 HardFault가 발생했을 때 스택 추적이 의미 없어 보였던 경험, 혹은 인터럽트 처리 중 예기치 않게 데이터가 손상된 적이 있는가? 이런 문제들은 코드의 문법적 오류라기보다는 프로세서 코어의 동작 메커니즘에 대한 깊은 이해 부족에서 비롯된다. ARM Cortex-M3는 오늘날까지도 실시간 제어 시스템의 교과서로 통하는 이유가 바로 여기에 있다.
성능 면에서는 최신 코어들에 뒤지지만, M3는 ARMv7-M 아키텍처의 첫 번째 완전한 구현체로서, 고성능, 실시간성, 개발 편의성을 한 차원 높인 설계를 선보였다. 특히 NVIC, 자동 컨텍스트 저장, Thumb-2 명령어 세트 등은 이후 모든 Cortex-M 계열의 기반을 형성했다.
통합 아키텍처: CPU 이상의 가치
Cortex-M3는 단순한 프로세서 코어를 넘어서, 다음과 같은 하위 시스템들이 유기적으로 결합된 형태로 제공된다:
- CPU 코어: 3단계 파이프라인, 하버드 구조 기반
- NVIC (Nested Vectored Interrupt Controller): 인터럽트 우선순위 및 중첩 관리
- SysTick 타이머: 운영체제 시간 기반 제공
- PPB (Private Peripheral Bus): 내부 레지스터 접근 영역
- 메모리 맵핑 I/O: 외부장치도 메모리처럼 접근
이러한 구성은 결정론적(deterministic) 반응성을 요구하는 모터 제어, 산업용 통신, 센서 허브 등에 이상적인 환경을 제공한다.
Thumb-2 명령어 세트: 효율성의 정점
M3는 전용 상태 전환이 없다. 모든 코드는 Thumb-2 상태에서 실행되며, 이는 16비트와 32비트 명령어를 혼용할 수 있는 하이브리드 방식이다. 예를 들어:
MOV R1, #100 ; 16비트 명령어, 짧고 빠름
LDR R2, [R3, #0x400] ; 32비트 명령어, 큰 오프셋 지원
이 방식은 순수 ARM 32비트 명령어 대비 약 30% 적은 플래시 사용량을 달성하면서도 성능 저하는 5% 미만으로 억제한다. GCC, Keil, IAR 등의 컴파일러는 자동으로 최적의 명령어 길이를 선택하므로 개발자는 로직에 집중할 수 있다.
하버드 아키텍처와 파이프라인: 병렬 처리의 기반
M3는 명령어 Fetch와 데이터 접근을 위한 별도의 버스(I-Code, D-Code)를 사용함으로써, 하나의 클록 사이클 내에 명령어 가져오기와 메모리 읽기/쓰기를 동시에 수행할 수 있다. 여기에 3단계 파이프라인(Fetch → Decode → Execute)이 결합되어 이론적으로 1 사이클당 1개 명령어(1 IPC) 처리가 가능하다.
72MHz 클록 기준으로 약 90 DMIPS의 처리 능력을 가지며, 이는 전통적인 8비트 MCU 대비 10배 이상의 효율을 의미한다.
NVIC: 지능형 인터럽트 관리
NVIC는 단순 인터럽트 컨트롤러를 넘어, 실시간 반응성을 극대화하는 핵심 요소다. 주요 특징은 다음과 같다:
인터럽트 우선순위 설정
IRQ 인터럽트는 최대 240개까지 지원하며, 각각 독립된 우선순위를 가진다. 우선순위 값은 부호 있는 8비트 수로, 값이 작을수록 우선순위가 높다. 그룹 분할은 AIRCR 레지스터로 설정 가능하다:
// 4비트 전부를 프리엠션티(preemption) 우선순위로 사용
SCB->AIRCR = (0x5FA << 16) | (0b100 << 8);
초고속 인터럽트 응답
M3는 인터럽트 발생 후 최소 12클록 사이클 만에 서비스 루틴 진입이 가능하다. 이를 가능하게 하는 기술은 다음과 같다:
- 하드웨어 기반 컨텍스트 저장: R0~R3, R12, LR, PC, xPSR을 자동으로 스택에 저장
- Tail-Chaining: 연속된 인터럽트 간 불필요한 스택 복원을 생략하여 지연을 최소화
- Late Arrival Handling: 이미 인터럽트 처리 중이라도 더 높은 우선순위 인터럽트가 도착하면 즉시 전환
이러한 메커니즘은 고주파 인터럽트 환경에서도 일관된 반응 시간을 보장한다.
이중 스택 포인터: RTOS의 안정성 기반
M3는 두 개의 스택 포인터를 제공한다:
- MSP (Main Stack Pointer): 초기화, 예외 처리, 시스템 서비스에 사용
- PSP (Process Stack Pointer): 사용자 태스크 전용
RTOS는 각 태스크에 독립된 PSP를 할당하고, 인터럽트 발생 시 자동으로 MSP로 전환함으로써 태스크 간 스택 오염을 방지한다. FreeRTOS와 uC/OS-II는 이 기능을 활용해 효율적인 컨텍스트 스위칭을 구현한다.
__set_PSP(task_stack_top); // 태스크 스택 설정
__enable_irq(); // 인터럽트 활성화
비트 밴딩(Bit-Banding): 원자성 보장 기술
공유 자원을 조작할 때 "읽기-수정-쓰기" 과정에서 인터럽트나 다중 태스크 경쟁이 발생할 수 있다. M3는 SRAM 및 외부 레지스터 영역의 각 비트를 별도의 32비트 주소로 매핑함으로써, 단일 쓰기 명령어로 특정 비트를 조작할 수 있도록 한다.
SRAM 영역에서의 주소 변환 공식:
AliasAddr = 0x22000000 + ((&variable - 0x20000000) * 32) + (bit_number * 4)
예시 코드:
#define BITBAND_SRAM(addr, bit) \
(*(volatile uint32_t*)((0x22000000) + (((uint32_t)&(addr)) - 0x20000000)*32 + (bit)*4))
// 사용 예
BITBAND_SRAM(status_flag, 0) = 1; // bit 0 설정 (원자적)
이 기능은 GPIO 제어나 공유 플래그 동기화에 매우 유용하다.
4GB 메모리 맵: 통합 주소 공간
Cortex-M3는 전체 4GB 주소 공간을 아래와 같이 구성한다:
0x0000_0000 ───▶ 부트 플래시 또는 RAM 미러링 0x2000_0000 ───▶ 내부 SRAM (전역 변수, 스택) 0x4000_0000 ───▶ AHB/APB 외부장치 (GPIO, USART 등) 0x6000_0000 ───▶ 외부 메모리 인터페이스 (FSMC) 0xE000_0000 ───▶ PPB 영역 (NVIC, SysTick 등 내부 레지스터)
모든 외부장치는 메모리처럼 포인터로 직접 접근 가능하며, 이는 드라이버 개발을 크게 단순화한다.
벡터 테이블 재배치(VTOR): 유연한 부트 전략
기본 벡터 테이블은 0x0000_0000에 위치하지만, 부트로더 사용 시에는 다른 위치로 이동이 필요하다. VTOR 레지스터를 통해 벡터 테이블의 시작 주소를 변경할 수 있다:
SCB->VTOR = 0x08010000; // 64KB 이후부터 벡터 테이블 시작
이 기능은 OTA 업데이트, 이중 플래시 뱅크 전환 등에 필수적이다.
SysTick 타이머: RTOS의 시간 기준
24비트 감속 카운터인 SysTick은 OS 틱을 생성하는 데 사용된다. 예를 들어 72MHz에서 1ms 주기 설정:
void init_systick() {
SysTick->LOAD = 71999; // 72MHz / 1000 = 72,000
SysTick->VAL = 0;
SysTick->CTRL =
SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
인터럽트 핸들러에서 글로벌 카운터를 증가시키면, 지연 함수 및 태스크 스케줄링 기반을 마련할 수 있다.
실습 예제: 온도 모니터링 시스템
1초마다 ADC로 온도를 측정하고 UART로 전송하는 시스템을 구성해보자.
int main() {
clock_init_72mhz();
gpio_init();
adc_init_with_irq();
usart_init_with_dma();
timer_init_for_1s_trigger();
init_systick();
__enable_irq();
for (;;) {
__WFI(); // 인터럽트 대기, 절전 모드 진입
}
}
인터럽트 처리:
void TIM2_IRQHandler() {
if (TIM2->SR & TIM_SR_UIF) {
TIM2->SR &= ~TIM_SR_UIF;
ADC1->CR2 |= ADC_CR2_SWSTART;
}
}
void ADC1_IRQHandler() {
if (ADC1->SR & ADC_SR_EOC) {
uint16_t raw = ADC1->DR;
float temp = convert_temp(raw);
uart_send_async(&temp, sizeof(temp));
}
}
주의 사항 및 모범 사례
- 스택 크기 부족: MSP는 최소 1KB 이상 확보하고, PSP는 태스크 호출 깊이를 고려해 설계해야 한다.
- 인터럽트 우선순위 혼란: 핵심 인터럽트(HardFault, NMI)는 가장 높은 우선순위로, 나머지는 계층적으로 배치한다.
- 인터럽트 플래그 미처리: 인터럽트 핸들러 종료 전 반드시 플래그를 명시적으로 해제해야 무한 반복을 방지할 수 있다.
절전 모드 활용
배터리 구동 장치에서는 WFI(Wait For Interrupt) 명령어를 활용해 유휴 상태에서 전력 소모를 최소화한다:
while (1) {
background_task();
__DSB(); // 메모리 동기화
__WFI(); // 인터럽트 발생 시까지 대기
}
RTC 알람이나 외부 인터럽트를 통해 깨울 수 있으며, 평균 전류 소모를 μA 수준으로 낮출 수 있다.
결론: M3가 남긴 유산
Cortex-M3는 단순한 MCU 코어를 넘어, 다음과 같은 설계 철학을 확립했다:
- 자동화: 수동 작업을 최소화하여 오류 가능성 감소
- 결정론성: 실시간 시스템의 신뢰성 확보
- 하드웨어와의 긴밀한 통합: CMSIS 표준을 통해 이식성과 제어력을 동시에 제공
최신 Cortex-M4/M7을 다루더라도, M3의 설계 원리를 이해하는 것은 깊이 있는 임베디드 개발의 출발점이 된다.