ARM Cortex-M3 아키텍처의 핵심 원리와 실전 응용

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의 설계 원리를 이해하는 것은 깊이 있는 임베디드 개발의 출발점이 된다.

태그: ARM Cortex-M3 NVIC Thumb-2 실시간 시스템 임베디드 아키텍처

7월 2일 02:33에 게시됨