21.2.2.1.2. `STACK_MAIN` 속성에 따른 NuttX TCB(Task Control Block) 스택 사이즈 런타임 할당 로직

21.2.2.1.2. STACK_MAIN 속성에 따른 NuttX TCB(Task Control Block) 스택 사이즈 런타임 할당 로직

px4_add_module() 매크로 블록 안에서 초보 개발자들이 가장 큰 고뇌에 빠지는 파라미터가 하나 있다. 바로 **STACK_MAIN**이다.

px4_add_module(
    # ...
    MAIN custom_app
    STACK_MAIN 2000
    # ...
)

“내 모듈은 과연 스택(Stack) 메모리 크기를 얼마나 주어야 죽지 않을까?” 이 질문은 PX4 펌웨어가 램(SRAM) 고갈로 허덕이는 근본 원인이자, 반대로 기체 추락(Hard Fault)을 막는 최후의 보루이다. 21.1 단원에서 지겹도록 다루었듯, 이 숫자는 단순한 권장 사항이 아니라 OS 커널(NuttX)과 여러분의 모듈이 체결하는 **‘메모리 리스(Lease) 계약서’**의 절대 금액이다.

1. STACK_MAIN이 처리되는 생명주기 파이프라인

여러분이 CMakeLists.txt에 적어 넣은 숫자 2000은 모듈 컴파일 과정에서 마법처럼 커널의 테이블로 스며든다.

  1. 매크로 파싱 및 링커 심볼 주입: CMake는 px4_add_module을 해석하면서, 컴파일러에게 포워딩할 C 매크로를 내부적으로 생성한다. 이 과정에서 링커 심볼 테이블에 이 모듈 전용의 스택 크기 상수(Constant)가 박히게 된다.
  2. 명령어 분석기(Commander)와의 결합: NSH 콘솔이나 rc.mc_apps 부트 스크립트에서 사용자가 custom_app start를 치면, PX4 내부의 시스템 커맨드 파서가 작동한다. 파서는 1번 단계에서 구워진 앱 이름표와 스택 크기 정보(2000)를 메모리에서 매칭하여 꺼내온다.
  3. TCB (Task Control Block) 생성: 파서는 NuttX OS 커널의 task_spawn() 또는 task_create() API를 호출한다. 이때 커널은 전체 힙(Heap) 공간에서 연속된 2000 Bytes의 조각을 칼로 도려내어 가져온다.
  4. 스레드 생명 부여: 도려낸 2000바이트를 갓 태어난 스레드의 전용 스택 공간으로 매핑하고, 이것을 관리할 TCB(Task Control Block) 구조체를 생성하여 최종적으로 모듈을 백그라운드로 띄운다.

즉, STACK_MAIN은 코드를 번역할 때 적용되는 옵션이 아니라, 모듈이 런타임(Runtime)에 실행될 때마다(매번 start를 칠 때마다) OS 커널에게 “내 방 크기를 이만큼 떼어달라“고 요청하는 청구서의 금액이다.

2. 스택 크기를 결정하는 공학적 접근법

스택을 너무 작게 줘서 스레드가 선을 넘으면(Stack Overflow) 그 즉시 기체가 추락한다. 반대로 불안해서 무식하게 크게(예: 8000) 주면 다른 모듈이 띄워질 메모리가 부족해져(OOM) 펌웨어 부팅 자체가 멈춰버린다. 그렇다면 이 2000이라는 마법의 숫자는 도대체 어떻게 통찰해 내야 하는가?

감에 의존하는 찍기(Guesswork)를 멈추고 다음과 같은 아키텍처적 추론을 거쳐야 한다.

  1. 함수 호출 깊이(Call Stack Depth)의 추적:
    내 모듈의 main 함수가 호출하는 서브 함수가 얼마나 깊이 내려가는지 파악해야 한다. 함수가 한 번 호출될 때마다 CPU는 돌아올 주소(LR)와 상태 레지스터(수십 바이트)를 스택에 눌러 담는다(Push). 순환문(Recursion)을 쓰면 스택은 순식간에 폭발한다.
  2. 지역 변수(Local Variables) 버퍼 배열의 파악:
    가장 치명적인 부분이다. 함수 내부에서 float matrix[50][50]; (10,000 Bytes = 10KB) 같은 거대한 지역 변수를 함부로 선언하면, STACK_MAIN 2000은 호출 즉시 박살 난다. 이런 큰 버퍼는 클래스를 만들 때 newmalloc을 통해 힙(Heap)에서 미리 빌려오거나(물론 메모리 파편화를 고려해야 함), 모듈이 하나만 돈다면 static 배열로 데이터 영역(BSS)에 박아넣어 스택에서 빼내야 한다.
  3. 문맥 교환(Context Switching) 예비 공간:
    내가 짠 코드가 200바이트만 쓴다 해도 안심할 수 없다. 하드웨어 인터럽트(IRQ)가 터지거나 컨텍스트 스위칭이 일어날 때, 커널은 현재 상태 레지스터 백업을 위해 내 태스크 스택의 꼭대기 여백(보통 150~300 바이트)을 예고 없이 강제로 징발해 사용한다. 따라서 항상 수백 바이트의 마진(Margin)을 두어야 한다.

2.1 런타임 스택 추적 도구 (NSH 모니터링)

전문 PX4 엔지니어들은 모듈을 짤 때 일단 여유 있게(3000 정도) 빌드해 올린 뒤, 기체를 켜고 NSH 콘솔에 명시적으로 연결하여 테스트한다.
비행 중이거나 로직이 맹렬하게 도는 상황에서 top 명령어를 쳐보면 각 모듈별로 현재 **스택 마진(Stack Margin)**이 몇 바이트나 남았는지 실시간으로 출력된다. 만약 마진 버퍼가 100 이하로 위태롭게 남는다면 스택을 증설해야 하고, 마진이 2000 넘게 남아돈다면 공간 낭비이므로 CMake의 STACK_MAIN 숫자를 깎아내려 시스템 메모리를 절약하는 최적화 튜닝(Tuning) 과정을 반드시 거친다.