C++ 임베디드 시스템 최적화 | 플래시·RAM·전력·실시간성 완벽 가이드 [#55-7]

C++ 임베디드 시스템 최적화 | 플래시·RAM·전력·실시간성 완벽 가이드 [#55-7]

이 글의 핵심

64KB 플래시에 안 들어가요? RAM 부족·배터리 소모가 심해요? 임베디드 C++ 최적화: 플래시 크기, RAM 사용량, 전력 소비, 실시간 제약 해결법. STM32F0(64KB 플래시, 8KB RAM)에 C++ 프로젝트를 올리려 했는데 빌드 이미지가 80KB를 넘어 플래시에 들어가지 않습니다. 배터리 구동 웨어러블에서는 전력 소모가 심하고, 1ms 주기 제어 루프에서는 지터가 발생해 데드라인을 놓칩니다.

들어가며: “64KB 플래시에 안 들어가요”

임베디드 개발자가 겪는 현실

STM32F0(64KB 플래시, 8KB RAM)에 C++ 프로젝트를 올리려 했는데 빌드 이미지가 80KB를 넘어 플래시에 들어가지 않습니다. 배터리 구동 웨어러블에서는 전력 소모가 심하고, 1ms 주기 제어 루프에서는 지터가 발생해 데드라인을 놓칩니다. 이런 상황에서 플래시 크기, RAM 사용량, 전력 소비, 실시간성을 동시에 최적화하는 방법을 다룹니다.

이 글에서 다루는 것:

  • 플래시 최적화: 링크 타임 최적화, 불필요 코드 제거, 섹션 배치
  • RAM 최적화: 스택·힙 제한, 정적 할당, 풀 allocator
  • 전력 최적화: 슬립 모드, 클럭 게이팅, DMA 활용
  • 실시간성: WCET 예측, 인터럽트 지연 최소화, 락프리 구조

요구 환경: C++17 이상, ARM Cortex-M 또는 유사 MCU

관련 글: 예외·RTTI 없이 임베디드 C++, volatile·메모리 맵 I/O.


실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

문제 시나리오: “이런 상황에서 막혔다”

시나리오 1: 플래시 크기 초과

"STM32F030(64KB 플래시)에 빌드했는데 이미지가 78KB예요. 플래시에 안 들어가요."
"예외·RTTI 끄고 -Os 했는데도 20KB 넘어요."

원인: 링크 타임 최적화(LTO) 미적용, 사용하지 않는 함수·데이터가 이미지에 포함, printf·iostream 등 대형 라이브러리 링크.

해결: -flto, -ffunction-sections -fdata-sections + --gc-sections, printf 대신 경량 로깅, 사용하지 않는 STL 제거.

시나리오 2: RAM 부족으로 스택 오버플로우

"런타임에 HardFault가 나요. 스택 포인터가 이상해요."
"큰 구조체를 지역 변수로 선언했더니 죽어요."

원인: 스택 크기 부족(기본 2KB 등), 재귀 깊이 과다, 지역 변수로 대형 버퍼 선언.

해결: 스택 크기 증가 또는 대형 데이터를 정적·풀에 배치, 재귀 제거·반복문 전환.

시나리오 3: 배터리 1일도 안 가요

"CR2032 배터리로 웨어러블 만들었는데 12시간만에 방전돼요."
"대기 모드 들어가도 전류가 2mA나 나와요."

원인: 주변기기(ADC, UART, 타이머) 클럭 미차단, 불필요한 폴링 루프, 슬립 진입 전 주변기기 비활성화 누락.

해결: 슬립 전 모든 불필요 주변기기 OFF, 이벤트 기반 웨이크업, DMA로 CPU 부하 감소.

시나리오 4: 1ms 제어 루프에서 지터 발생

"1ms 주기로 PWM 출력하는데 가끔 2ms 넘게 걸려요."
"인터럽트 안에서 뭔가 하면 제어가 불안정해요."

원인: ISR 내부에서 블로킹 호출, 동적 할당, 긴 연산, 인터럽트 비활성화 구간 과다.

해결: ISR은 플래그 설정만, 실제 처리는 메인 루프에서, 락프리 큐·링 버퍼 사용.

시나리오 5: 펌웨어 업데이트 후 부팅 실패

"OTA로 펌웨어 올렸는데 부팅이 안 돼요. 이전 버전으로 롤백해야 해요."
"플래시 쓰기 중 전원이 꺼지면 브릭될까 봐 걱정이에요."

원인: 업데이트 중 전원 차단, 검증 없이 점프, 부트로더·앱 영역 경계 오류.

해결: A/B 파티션, CRC/해시 검증, 원자적 스왑, 워치독으로 복구.

시나리오별 해결 방향 요약

시나리오특징권장 접근
플래시 초과이미지 크기LTO, gc-sections, 경량 라이브러리
RAM 부족스택/힙정적·풀 할당, 스택 크기 조정
전력 과다배터리 소모슬립 모드, 클럭 게이팅, DMA
실시간 지터데드라인 위반ISR 최소화, 락프리 구조
OTA 실패부팅 불가A/B 파티션, 검증, 원자적 업데이트

임베디드 최적화 흐름도

flowchart TB
    subgraph Flash[플래시 최적화]
        F1[-Os -flto] --> F2[--gc-sections]
        F2 --> F3[경량 printf/로깅]
        F3 --> F4[불필요 STL 제거]
    end

    subgraph RAM[RAM 최적화]
        R1[스택 크기 분석] --> R2[정적/풀 할당]
        R2 --> R3[힙 사용 최소화]
        R3 --> R4[const로 .rodata 이동]
    end

    subgraph Power[전력 최적화]
        P1[슬립 모드 진입] --> P2[주변기기 OFF]
        P2 --> P3[이벤트 웨이크업]
        P3 --> P4[DMA 활용]
    end

    subgraph Realtime[실시간성]
        RT1[ISR 최소화] --> RT2[락프리 큐]
        RT2 --> RT3[WCET 분석]
    end

    Flash --> RAM
    RAM --> Power
    Power --> Realtime

목차

  1. 플래시 크기 최적화
  2. RAM 사용량 최적화
  3. 전력 소비 최적화
  4. 실시간성 보장
  5. 완전한 임베디드 최적화 예제
  6. 자주 발생하는 에러와 해결법
  7. 모범 사례와 프로덕션 패턴
  8. 구현 체크리스트
  9. 정리

1. 플래시 크기 최적화

1.1 컴파일·링크 옵션

-Os: 크기 최적화. -O2보다 코드가 작아지고, 루프 언롤링 등이 줄어듭니다.

-flto: 링크 타임 최적화. 컴파일 단위 경계를 넘어 인라인·데드 코드 제거가 가능해집니다. 10~30% 크기 감소가 흔합니다.

-ffunction-sections -fdata-sections: 각 함수·데이터를 별도 섹션에 배치. 링커에서 --gc-sections와 함께 사용하면 참조되지 않는 코드를 최종 이미지에서 제거합니다.

// 예: 디버그용 함수 - 릴리즈에서 호출 안 하면 gc-sections로 제거됨
void debug_print(const char* msg) {
    // UART로 출력 - 프로덕션에서는 이 함수가 링크되지 않음
    (void)msg;
}

CMake 예시:

# 플래시 최적화를 위한 컴파일 옵션
add_compile_options(
    -Os
    -flto
    -ffunction-sections
    -fdata-sections
)
add_link_options(
    -flto
    -Wl,--gc-sections
    -Wl,--print-memory-usage
)

1.2 printf·iostream 제거

printfstd::cout은 포맷 문자열·부동소수점 지원 때문에 수 KB~수십 KB를 차지합니다. 임베디드에서는 경량 로깅으로 대체합니다.

// ❌ 나쁜 예: printf는 수 KB 이상
#include <cstdio>
printf("Value: %d\n", value);

// ✅ 좋은 예: 경량 UART 출력 (수십 바이트)
void uart_putchar(char c);
void log_u8(const char* label, uint8_t v) {
    while (*label) uart_putchar(*label++);
    uart_putchar('0' + (v / 100));
    uart_putchar('0' + ((v / 10) % 10));
    uart_putchar('0' + (v % 10));
    uart_putchar('\n');
}

1.3 const·constexpr로 .rodata 배치

const 데이터는 플래시(.rodata)에 배치되어 RAM을 사용하지 않습니다. 문자열· lookup 테이블은 반드시 const로 선언합니다.

// ✅ 상수는 플래시에, RAM 사용 0
const char* const ERROR_MSGS[] = {
    "OK", "Bus Error", "Timeout", "Invalid"
};

constexpr uint16_t CRC_TABLE[256] = {
    // CRC16 lookup table - 플래시에만 존재
    0x0000, 0x1021, 0x2042, /* ... */
};

1.4 플래시 사용량 측정

# ARM GCC에서 섹션별 크기 확인
arm-none-eabi-size -A firmware.elf
# 출력 예시
firmware.elf  :
section              size      addr
.text               45232   0x08000000
.rodata              2048   0x0800b0f0
.data                 256   0x20000000
.bss                 1024   0x20000100

2. RAM 사용량 최적화

2.1 스택 크기 설정

링커 스크립트에서 _estack 또는 Stack_Size를 설정합니다. 스택 사용량을 초과하면 HardFault가 발생합니다.

/* 링커 스크립트 예시 (STM32) */
_Min_Heap_Size = 0x200;   /* 512 bytes */
_Min_Stack_Size = 0x400;  /* 1KB - 필요에 따라 증가 */

. = ORIGIN(RAM) + LENGTH(RAM) - _Min_Stack_Size;
_estack = .;

스택 사용량 분석:

# -fstack-usage으로 함수별 스택 사용량 생성
arm-none-eabi-gcc -fstack-usage -c source.cpp -o source.o

2.2 지역 변수 대신 정적·풀 사용

큰 버퍼를 지역 변수로 선언하면 스택을 많이 사용합니다. 전역 정적 또는 풀 allocator로 옮깁니다.

// ❌ 나쁜 예: 256바이트가 스택에
void process_packet() {
    uint8_t buffer[256];  // 스택 256바이트
    receive(buffer, 256);
    parse(buffer);
}

// ✅ 좋은 예: 정적 버퍼 (RAM 고정, 스택 0)
void process_packet() {
    static uint8_t buffer[256];  // .bss에 256바이트, 스택 사용 없음
    receive(buffer, 256);
    parse(buffer);
}

주의: static 버퍼는 재진입 불가. 여러 곳에서 동시 호출되면 뮤텍스 또는 별도 버퍼 필요.

2.3 메모리 풀 (Pool Allocator)

동적 할당이 필요할 때 malloc 대신 고정 크기 풀을 사용하면 단편화를 피하고, 최악 실행 시간을 예측할 수 있습니다.

#include <cstddef>
#include <cstdint>

template <typename T, size_t N>
class Pool {
    alignas(T) uint8_t storage_[N * sizeof(T)];
    bool used_[N];

public:
    Pool() : used_{} {}

    T* allocate() {
        for (size_t i = 0; i < N; ++i) {
            if (!used_[i]) {
                used_[i] = true;
                return new (&storage_[i * sizeof(T)]) T();
            }
        }
        return nullptr;  // 풀 고갈
    }

    void deallocate(T* p) {
        for (size_t i = 0; i < N; ++i) {
            if (reinterpret_cast<T*>(&storage_[i * sizeof(T)]) == p) {
                p->~T();
                used_[i] = false;
                return;
            }
        }
    }
};

// 사용 예: 최대 4개 패킷 버퍼
struct Packet { uint8_t data[64]; };
Pool<Packet, 4> packet_pool;

2.4 RAM 사용량 요약

항목권장비고
스택1~2KB (기본)재귀·대형 지역 변수 있으면 증가
0 또는 최소malloc 사용 시 단편화 주의
.data최소화초기화된 전역 변수
.bss필요한 만큼초기화 안 된 전역 변수

3. 전력 소비 최적화

3.1 슬립 모드 진입

CPU를 정지시키고, 주변기기 클럭을 차단하면 전류를 크게 줄일 수 있습니다. ARM Cortex-M에서는 WFI(Wait For Interrupt)로 슬립 진입.

// Cortex-M 슬립 진입 (의사 코드)
void enter_sleep() {
    // 1. 슬립 전 불필요 주변기기 비활성화
    disable_adc();
    disable_uart_tx();
    disable_unused_timers();

    // 2. 웨이크업 소스 활성화 (GPIO, RTC 등)
    enable_wakeup_pin();
    enable_rtc_alarm();

    // 3. 슬립 모드 설정 (딥 슬립 등)
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;

    // 4. 인터럽트 대기 (WFI)
    __WFI();

    // 5. 웨이크업 후 복원
    restore_peripherals();
}

3.2 이벤트 기반 vs 폴링

폴링 루프는 CPU를 계속 돌리므로 전력 소모가 큽니다. 인터럽트 + 슬립으로 전환하면 대기 시 전류를 μA 단위로 낮출 수 있습니다.

// ❌ 나쁜 예: 폴링 - CPU 100% 사용
void main_loop_bad() {
    while (true) {
        if (button_pressed()) {
            do_work();
        }
        // 아무 일 없어도 CPU 계속 동작 → 전력 낭비
    }
}

// ✅ 좋은 예: 이벤트 기반 - 대기 시 슬립
volatile bool button_event = false;

void EXTI_IRQHandler() {
    button_event = true;  // ISR은 플래그만 설정
}

void main_loop_good() {
    while (true) {
        if (!button_event) {
            __WFI();  // 슬립, GPIO 인터럽트로 웨이크업
            continue;
        }
        button_event = false;
        do_work();
    }
}

3.3 DMA로 CPU 부하 감소

UART·ADC 등에서 DMA를 사용하면 CPU가 데이터 전송에 관여하지 않아도 됩니다. CPU는 전송 완료 인터럽트를 기다리는 동안 슬립 가능합니다.

// DMA로 UART 송신 - CPU는 시작만 하고 대기 시 슬립
void uart_send_dma(const uint8_t* data, size_t len) {
    dma_set_source(data);
    dma_set_dest(&UART->TDR);
    dma_set_count(len);
    dma_enable();
    // DMA 완료 인터럽트까지 CPU 슬립 가능
}

3.4 전력 소비 비교 (예시)

모드전류 (STM32L4 예시)비고
Run (48MHz)~3mA최대 성능
Sleep~100μARAM 유지
Stop~2μA대부분 클럭 정지
Standby~0.5μARAM 일부만 유지

4. 실시간성 보장

4.1 ISR 최소화 원칙

인터럽트 서비스 루틴(ISR) 안에서는 가능한 한 짧게 처리합니다. 블로킹 호출, 동적 할당, 긴 연산은 금지. 플래그 설정만 하고, 실제 처리는 메인 루프에서 합니다.

// 락프리 링 버퍼 (단일 생산자, 단일 소비자)
template <typename T, size_t N>
class LockFreeRingBuffer {
    static_assert((N & (N - 1)) == 0, "N must be power of 2");
    T buffer_[N];
    volatile size_t head_ = 0;
    volatile size_t tail_ = 0;

public:
    bool push(T v) {
        if ((tail_ + 1 - head_) % N == 0) return false;
        buffer_[tail_ % N] = v;
        tail_++;
        return true;
    }

    bool pop(T& v) {
        if (head_ == tail_) return false;
        v = buffer_[head_ % N];
        head_++;
        return true;
    }
};

// ISR: 데이터만 푸시
LockFreeRingBuffer<uint8_t, 64> uart_rx_buffer;

void UART_IRQHandler() {
    if (uart_rx_ready()) {
        uart_rx_buffer.push(uart_read_byte());
    }
}

// 메인 루프: 실제 처리
void process_uart() {
    uint8_t b;
    while (uart_rx_buffer.pop(b)) {
        handle_byte(b);
    }
}

4.2 WCET (최악 실행 시간) 예측

실시간 시스템에서는 최악의 경우 실행 시간을 예측해야 합니다. malloc, 무한 루프 가능성, 예외(사용 시)는 WCET 분석을 어렵게 합니다. 따라서:

  • 동적 할당 금지
  • 반복 횟수 상한이 있는 루프만 사용
  • 예외 비활성화 (-fno-exceptions)

5. 완전한 임베디드 최적화 예제

5.1 플래시 크기 최적화 예제

상황: 64KB 플래시 MCU에 펌웨어가 78KB로 들어가지 않음.

// main.cpp - 최적화된 임베디드 진입점
#include <cstdint>

// constexpr로 컴파일 타임 상수 - 런타임 RAM 사용 0
constexpr uint32_t APP_VERSION = 0x01020300;
constexpr uint32_t CONFIG_MAGIC = 0xDEADBEEF;

// const 데이터는 .rodata (플래시)
const char APP_NAME[] = "Firmware v1.2.3";

// 인라인으로 작은 함수는 호출 오버헤드 제거
inline void gpio_set(uint32_t pin) {
    *reinterpret_cast<volatile uint32_t*>(0x48000814) = (1u << pin);
}

inline void gpio_clear(uint32_t pin) {
    *reinterpret_cast<volatile uint32_t*>(0x48000818) = (1u << pin);
}

// 사용하지 않는 코드는 __attribute__((weak))로 gc-sections 대상
__attribute__((weak)) void debug_init() {}
__attribute__((weak)) void debug_print(const char*) {}

int main() {
    debug_init();
    gpio_set(5);
    // ...
    return 0;
}

빌드 옵션:

set(CMAKE_CXX_FLAGS_RELEASE "-Os -flto -ffunction-sections -fdata-sections")
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "-flto -Wl,--gc-sections -Wl,--print-memory-usage")

결과: 78KB → 52KB (약 33% 감소)

5.2 RAM 사용량 최적화 예제

상황: 8KB RAM에서 스택 오버플로우 발생.

// config.h
#pragma once
#include <cstddef>

// 스택 크기 (링커 스크립트와 일치)
constexpr size_t STACK_SIZE = 1024;

// 풀 크기
constexpr size_t MAX_PACKETS = 4;
constexpr size_t PACKET_SIZE = 64;

// 전역 풀 - .bss에 배치
struct Packet {
    uint8_t data[PACKET_SIZE];
    uint8_t len;
};

// 정적 풀 - 힙 사용 0
class PacketPool {
    static Packet pool_[MAX_PACKETS];
    static bool used_[MAX_PACKETS];

public:
    static Packet* alloc() {
        for (size_t i = 0; i < MAX_PACKETS; ++i) {
            if (!used_[i]) {
                used_[i] = true;
                pool_[i].len = 0;
                return &pool_[i];
            }
        }
        return nullptr;
    }

    static void free(Packet* p) {
        for (size_t i = 0; i < MAX_PACKETS; ++i) {
            if (&pool_[i] == p) {
                used_[i] = false;
                return;
            }
        }
    }
};

Packet PacketPool::pool_[MAX_PACKETS];
bool PacketPool::used_[MAX_PACKETS] = {};

메모리 레이아웃:

  • .bss: pool_ 256바이트 + used_ 4바이트 ≈ 260바이트
  • 스택: 1KB (대형 지역 변수 제거로 유지)

5.3 전력 소비 최적화 예제

상황: CR2032 배터리(220mAh)로 1주일 이상 동작 목표.

// power_manager.cpp
#include <cstdint>

// 하드웨어 추상화 (실제 레지스터는 MCU마다 다름)
struct PowerRegs {
    volatile uint32_t* const cr1;   // 제어 레지스터
    volatile uint32_t* const cr2;
};

void enter_stop_mode() {
    // 1. 모든 주변기기 비활성화
    disable_gpio_clocks();
    disable_uart();
    disable_adc();
    disable_timers_except_rtc();

    // 2. RTC 알람만 웨이크업 소스로
    rtc_set_alarm_1min();

    // 3. Stop 모드 진입
    __DSB();
    __WFI();
}

void main_low_power() {
    while (true) {
        if (sensor_data_ready()) {
            read_sensor();
            send_via_ble();
        }
        // 대기 시 Stop 모드 (μA 단위)
        enter_stop_mode();
    }
}

전류 예상:

  • 활성: 5mA × 10ms/분 ≈ 0.83μAh/분
  • Stop: 2μA × 59.99초 ≈ 120μAh/분
  • 합계: 약 121μAh/분 → 220mAh / (121μAh × 60 × 24) ≈ 12.5일

6. 자주 발생하는 에러와 해결법

문제 1: “region ‘FLASH’ overflowed”

arm-none-eabi-ld: firmware.elf section `.text' will not fit in region `FLASH'

원인: 코드+데이터 크기가 플래시 용량을 초과.

해결:

  1. -Os -flto --gc-sections 적용
  2. printf·iostream 제거
  3. 사용하지 않는 라이브러리 링크 해제
  4. -ffunction-sections -fdata-sections 확인

문제 2: “region ‘RAM’ overflowed”

region 'RAM' overflowed by 256 bytes

원인: .data + .bss + 스택이 RAM 용량 초과.

해결:

  1. 링커 스크립트에서 _Min_Stack_Size 줄이기 (위험)
  2. 대형 전역 변수 → const로 옮겨 .rodata로
  3. 지역 변수 → 정적 또는 풀으로
  4. -fstack-usage로 스택 사용량 분석

문제 3: HardFault after startup

프로그램 시작 후 곧바로 HardFault. 스택 포인터가 0x2000xxxx 근처.

원인: 스택 오버플로우, 또는 초기화되지 않은 포인터 역참조.

해결:

  1. 스택 크기 증가 (링커 스크립트)
  2. -fstack-usage로 큰 스택 사용 함수 찾기
  3. 지역 배열·구조체 크기 줄이기
  4. 스택 카나리(StackGuard)로 오버플로우 감지

문제 4: LTO 빌드 시 “undefined reference”

undefined reference to `some_function'

원인: LTO 시 인라인·제거로 심볼이 사라졌는데, 어셈블리·다른 언어에서 참조하는 경우.

해결:

  1. __attribute__((used))로 해당 함수 보존
  2. 또는 -fno-lto로 해당 파일만 제외

문제 5: 슬립 후 웨이크업 안 됨

__WFI() 진입 후 영원히 안 깨어남.

원인: 웨이크업 소스가 비활성화됐거나, 인터럽트가 마스크됨.

해결:

  1. 슬립 전 NVIC에서 웨이크업 인터럽트 활성화 확인
  2. __enable_irq() 호출
  3. 딥 슬립 시 일부 주변기기만 웨이크업 가능한지 데이터시트 확인

문제 6: 실시간 루프에서 가끔 지터

1ms 주기인데 가끔 3ms 걸림.

원인: ISR 내부에서 긴 처리, 또는 다른 인터럽트에 선점됨.

해결:

  1. ISR은 플래그/데이터 푸시만
  2. 인터럽트 우선순위 설정 (제어 루프가 가장 높게)
  3. 크리티컬 섹션 최소화

7. 모범 사례와 프로덕션 패턴

7.1 빌드 설정 체크리스트

# 프로덕션 임베디드 빌드
set(CMAKE_BUILD_TYPE Release)
add_compile_options(
    -Os
    -flto
    -ffunction-sections
    -fdata-sections
    -fno-exceptions
    -fno-rtti
)
add_link_options(
    -flto
    -Wl,--gc-sections
    -Wl,--print-memory-usage
)

7.2 메모리 맵 고정

/* 프로덕션: 부트로더 + 앱 파티션 */
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 60K   /* 부트로더 16K 제외 */
    RAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 8K
}

7.3 워치독 활용

// 메인 루프에서 주기적으로 워치독 리셋
void main_loop() {
    watchdog_init(2000);  // 2초 타임아웃
    while (true) {
        watchdog_feed();
        process_tasks();
        idle_sleep();
    }
}

7.4 OTA 안전 업데이트 패턴

flowchart LR
    subgraph OTA[OTA 업데이트]
        A[새 펌웨어 수신] --> B[플래시 B 파티션에 쓰기]
        B --> C[CRC/해시 검증]
        C --> D{검증 OK?}
        D -->|예| E[부트 플래그: B 부팅]
        D -->|아니오| F[재시도 또는 롤백]
        E --> G[재부팅]
        G --> H[부트로더: B에서 부팅]
    end
// 부트로더 의사 코드
void bootloader_main() {
    uint32_t active = read_boot_flag();
    uint32_t app_addr = (active == 0) ? PARTITION_A : PARTITION_B;

    if (!verify_firmware(app_addr)) {
        app_addr = (active == 0) ? PARTITION_B : PARTITION_A;
        if (!verify_firmware(app_addr)) {
            enter_recovery_mode();
        }
    }
    jump_to(app_addr);
}

7.5 로깅 전략

프로덕션에서는 printf 대신 레벨별 로깅조건부 컴파일을 사용합니다.

enum class LogLevel { None, Error, Warn, Info, Debug };

#ifndef LOG_LEVEL
#define LOG_LEVEL LogLevel::Warn
#endif

template <LogLevel L>
void log(const char* msg) {
    if constexpr (L <= LOG_LEVEL && LOG_LEVEL != LogLevel::None) {
        uart_send(msg);
    }
}

// 사용: LOG_LEVEL=Debug 빌드에서만 출력
log<LogLevel::Debug>("Sensor value: 42");

8. 구현 체크리스트

플래시 최적화

  • -Os -flto 적용
  • -ffunction-sections -fdata-sections + --gc-sections 적용
  • printf·iostream 제거 또는 경량 대체
  • -fno-exceptions -fno-rtti (가능한 경우)
  • arm-none-eabi-size -A로 섹션별 크기 확인

RAM 최적화

  • 스택 크기 분석 (-fstack-usage)
  • 대형 지역 변수 → 정적/풀
  • malloc 사용 최소화 또는 풀 allocator
  • const 데이터는 const 선언

전력 최적화

  • 슬립 모드 진입 경로 구현
  • 슬립 전 불필요 주변기기 비활성화
  • 폴링 → 이벤트 기반 전환
  • DMA 활용 검토

실시간성

  • ISR 내부 최소화 (플래그/데이터만)
  • 락프리 큐·링 버퍼 사용
  • 동적 할당 ISR/제어 루프에서 금지
  • 인터럽트 우선순위 설정

프로덕션

  • 워치독 설정
  • OTA 시 검증·롤백 전략
  • 부트로더·앱 파티션 분리
  • 로깅 레벨 조건부 컴파일

9. 정리

항목핵심 기법
플래시-Os, -flto, gc-sections, 경량 로깅, const
RAM스택 분석, 정적/풀 할당, 힙 최소화
전력슬립 모드, 이벤트 기반, DMA, 클럭 게이팅
실시간ISR 최소화, 락프리 구조, WCET 예측

핵심 원칙:

  1. 플래시: 사용하지 않는 코드 제거, 경량 라이브러리
  2. RAM: 스택·힙 사용 최소화, 정적·풀 할당
  3. 전력: 대기 시 슬립, 이벤트 기반, DMA
  4. 실시간: ISR 짧게, 블로킹 금지, 동적 할당 금지

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. IoT 디바이스, 자동차 ECU, 드론, 웨어러블 등 임베디드 시스템 개발 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

Q. LTO를 켜면 디버깅이 어려운데요?

A. 개발 시에는 -O0 -g로 LTO 없이 빌드하고, 릴리즈에서만 -Os -flto를 사용합니다. CMake에서 CMAKE_BUILD_TYPE으로 분리합니다.

Q. RAM이 2KB밖에 없는데요?

A. 스택 512B, 나머지를 .data/.bss와 풀에 할당. 동적 할당은 피하고, 모든 버퍼 크기를 상수로 고정해 분석합니다.

한 줄 요약: 플래시·RAM·전력·실시간성을 동시에 최적화하는 임베디드 C++ 기법을 마스터할 수 있습니다.


관련 글

  • C++ SIMD 최적화 실전 | SSE·AVX2·NEON 인트린직으로 4배 빠르게 [#51-2]
  • C++ 캐시 최적화 실전 | 캐시 친화적 구조·프리페치·False Sharing·AoS vs SoA 가이드
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3