27.2.2.2. 스케줄링 로직: 작업 항목(WorkItem) 인터페이스를 통한 주기적 호출 방식

27.2.2.2. 스케줄링 로직: 작업 항목(WorkItem) 인터페이스를 통한 주기적 호출 방식

소프트웨어 공학에서 주기적인 연산 루프를 구현하는 가장 고전적인 방법은 무한 루프 내에 sleep()(혹은 usleep()) 지연 함수를 배치하는 것이다. 그러나 하드 리얼타임(Hard Real-time)이 요구되는 PX4의 EKF2 메인 추정기 루틴에서 이러한 폴링(Polling) 방식은 CPU의 컨텍스트 스위칭(Context Switching) 오버헤드를 극심하게 낭비하는 설계 결함으로 작용한다.

이러한 문제를 극복하기 위해 ekf2 모듈은 PX4 고유의 비동기 스케줄링 패러다임인 작업 항목(px4::WorkItem) 인터페이스를 상속받아 극도로 효율적인 이벤트 드리븐(Event-driven) 방식으로 구동된다. 본 절에서는 EKF2가 스케줄러 위에서 어떻게 CPU 점유율을 최소화하며 주기적 호출을 달성하는지 분석한다.


1. 전통적인 스레드 모델의 한계와 Work Queue의 도입

만약 EKF2 모듈이 독자적인 운영체제 스레드(NuttX의 pthread 등) 위에서 while(1) { ... } 형태로 250Hz 루프를 돈다면, CPU 스케줄러는 초당 250번씩 기존에 실행 중이던 스레드(예: MAVLink 통신 스레드)의 레지스터 상태를 저장하고 EKF 스레드로 문맥 전환을 시도해야 한다.
모듈 개수가 50개를 넘어가는 현대 PX4 아키텍처에서 모든 모듈이 이런 식으로 독자 스레드를 가진다면, 기체 보드는 순전히 스위칭 오버헤드만으로도 연산 한계에 직면하게 된다.

1.1 px4::WorkItem 상속을 통한 자릿세 내기

이를 해결하기 위해 EKF2 클래스는 독자적인 스레드를 파생(Spawn)시키는 대신, px4::WorkItem이라는 가상 인터페이스를 상속받는다.

class EKF2 : public px4::WorkItem {
    // ...
protected:
    void Run() override; // 실제 EKF 수학 연산 및 uORB 통신 콜백 함수
};

이 상속 구조의 물리적 의미는 “EKF2 전용 스레드를 만들지 말고, 이미 OS가 만들어둔 거대한 공용 스레드(Work Queue) 위에서 내 Run() 함수만 잠깐 실행시키고 빠지겠다” 는 선언이다.

실제로 EKF2는 PX4의 가장 높은 우선순위를 가지는 nav_and_controllers (내비게이션 및 제어) 워크 큐 스레드 풀(Thread Pool)에 자기 자신(Task)을 밀어 넣은 형태로 스케줄링된다.


2. 인터럽트 기반의 Run() 메소드 트리거 메커니즘

그렇다면 EKF2Run() 함수는 누구에 의해, 언제 호출될까?
폴링(Polling) 방식이 아니기 때문에, EKF2 클래스 스스로 시계를 보며 “지금 실행해야지” 하고 일어나는 것이 아니다. 정답은 철저한 이벤트 기반 콜백(Event-based Callback) 에 있다.

2.1 IMU 데이터 도달 주기와의 동기화

EKF 상태 추정 알고리즘의 심박수(Heartbeat)는 항상 자이로 및 가속도 센서인 IMU의 측정 주기(샘플링 레이트)와 일치해야 한다.
따라서 EKF2 래퍼 객체는 sensor_selection (또는 sensor_combined) uORB 토픽을 구독할 때, “해당 토픽의 새로운 데이터 버퍼가 갱신되면 내 Run() 함수를 Work Queue 스케줄러에 예약(Schedule)해 달라” 고 커널에 콜백 바인딩을 걸어놓는다.

이 처리 과정의 흐름은 다음과 같다.

  1. 하드웨어 인터럽트: SPI 버스를 통해 IMU 센서(예: ICM-42688-P)에서 새로운 물리적 측정값이 도착하여 DMA(Direct Memory Access) 인터럽트가 발생한다.
  2. uORB 발행(Publish): 센서 드라이버 모듈이 이 데이터를 읽어 uORB 토픽으로 발행한다.
  3. 스케줄러 기상(Wake up): 0초의 지연(Zero Delay)으로 커널 스케줄러가 EKF2 객체가 대기 중인 nav_and_controllers 워크 큐를 깨운다.
  4. Run() 호출: EKF2::Run() 함수가 단 한 번(Tick) 즉각적으로 실행된다.
  5. 다시 수면(Sleep): 상태 추정 계산과 결과 발행이 끝나면 함수는 즉시 반환(Return)되고, 스레드는 다음 IMU 인터럽트가 발생할 때까지 완전히 대기 상태(Blocked)로 전환되어 CPU 점유율을 0%로 떨어뜨린다.

3. 요약: 비동기 논블로킹(Non-blocking) 아키텍처의 완성

결과적으로 src/modules/ekf2/EKF2.cpp의 소스 코드 내부 어디를 뒤져봐도 강제적인 지연 함수(usleep, px4_usleep)나 무한 루프 블록은 존재하지 않는다. 오직 이벤트가 트리거 되었을 때 위에서 아래로 한 번 흘러가고 종료되는, 지극히 함수형 반응형 프로그래밍(Functional Reactive Programming)에 가까운 Run() 메서드의 논블로킹(Non-blocking) 파이프라인만이 존재할 뿐이다.

이러한 WorkItem 스케줄링 최적화 덕분에, PX4 펌웨어는 168MHz에 불과한 저사양 STM32F4 기반 컨트롤러 위에서도 24차원의 방대한 EKF2 수학 행렬을 400Hz의 초고속 스피드로 돌리면서 여유로운 CPU 유휴 시간(Idle Time)을 확보할 수 있게 되었다.

다음 절에서는 이러한 비동기 이벤트 트리거의 핵심이 되는 uORB 토픽 감지 메커니즘, 즉 SubscriptionCallback을 활용하여 수많은 비동기 센서 폴링 레이트(Polling Rate)를 어떻게 통합하여 통제하는지 그 세부 브릿지 구조를 살펴본다.