21.5.3.1. `parameter_update_s` 토픽 구독 모델 구현

21.5.3.1. parameter_update_s 토픽 구독 모델 구현

PX4의 비동기 파라미터 업데이트를 수신하려면, 우리는 PX4의 핵심 통신망인 uORB 네트워크에 라디오 주파수를 맞추듯 ‘구독자(Subscriber)’ 객체를 심어 넣어야 한다.

이 역할을 전담하는 메신저가 바로 uORB::SubscriptionInterval 또는 uORB::Subscription 클래스이며, 우리가 기다려야 하는 전보의 이름은 parameter_update_s 이다.

1. 헤더 파일에서의 구독자 선언

CustomApp 클래스의 헤더(.h) 파일을 열어 private 영역에 아래와 같이 uORB 구독자 멤버 변수를 살포시 선언해 준다.

#include <uORB/SubscriptionInterval.hpp>
#include <uORB/topics/parameter_update.h>

class CustomApp : public ModuleBase<CustomApp>, public ModuleParams
{
// ...
private:
    // 파라미터 서버가 쏘는 업데이트 알림 방송(Topic)을 들을 라디오 수신기
    uORB::SubscriptionInterval _parameter_update_sub{ORB_ID(parameter_update), 1_s};
};

여기서 SubscriptionInterval 객체를 선언하면서 초기화 리스트로 두 개의 놀라운 인자를 쥐여주었다.

  1. ORB_ID(parameter_update): “내가 들을 라디오 주파수는 파라미터 동기화 채널이다!”
  2. 1_s: “아무리 파라미터 서버가 1초에 수십 번 갱신 명령을 난사해도, 나는 1초에 딱 1번만 그 메시지를 체크(Interval)하겠다!” (극강의 Throttling 최적화 기법)

이 1초 인터벌 최적화가 들어간 이유는 무엇일까? QGC에서 마우스 휠을 한 번 돌릴 때마다 파라미터가 드르륵 갱신되며 uORB 메시지가 융단폭격처럼 쏟아지기 때문이다. 만약 모든 메시지에 런루프가 일일이 대응해서 배열을 다시 할당한다면 CPU는 터져버릴 것이다. 우리는 이 여과망(Interval)을 통해 가장 마지막 최신 상태만 1초 주기로 부드럽게 가져오게 된다.

2. updated()를 통한 런타임 낚아채기

이제 메인 제어 루프가 돌고 있는 Run() 메서드 한복판으로 들어가, 이 라디오 수신기가 울벨을 울렸는지(업데이트가 발생했는지) 확인하는 코드를 삽입하자.

void CustomApp::Run()
{
    // ... 시작 전 초기화 로직 ...

    while (!should_exit()) { // 무한 런루프 시작

        // 1. 라디오 수신기에 메시지가 도착했는가? (Non-blocking 폴링)
        if (_parameter_update_sub.updated()) {
            
            // 2. 메시지가 도착했다면, 껍데기 변수에 그 알림장을 읽어 들인다(Clear).
            // (이 작업을 안 하면 updated()가 평생 true인 무한루프 지옥에 빠짐)
            parameter_update_s param_update;
            _parameter_update_sub.copy(&param_update);

            // 3. 자, 이제 내 모듈의 변수들을 글로벌 서버와 동기화시켜라!
            updateParams(); 
            
            // 4. (분기 선택) 만약 모터 출력이나 필터 계수가 바뀌었다면 다시 계산해 두도록 한다.
            recalculate_filters();
        }

        // 5. 핵심 제어 로직 수행...
        // 이 시점에서는 항상 "최신 파라미터" 임이 보장된다.
        controller_step();
    }
}

위의 로직에서 가장 빛나는 문장은 단연 _parameter_update_sub.updated() 이다.

1000Hz 속도로 도는 런루프 안에서 이 함수는 매번 호출되지만, 이 함수의 내부는 시스템 콜(System Call)이나 IPC 메모리 복사 같은 무거운 작업을 수행하지 않는다. 그저 단일 비트 플래그(Boolean)를 메모리에서 스윽 읽어오고 끝나는 **초경량 오퍼레이션(Lightweight Operation)**이다.

이 엄청나게 빠른 updated() 플래그 검사가 어떻게 단 1클럭 수준의 포인터 락-프리 검사로 끝날 수 있는지, 비트 마스킹(Bit Masking)과 세마포어의 흑마법적인 속도전(Speed War)을 다음 단원(21.5.3.1.1)에서 살벌하게 뜯어보자.