21.7.2.1. `SubscriptionCallbackWorkItem` 클래스의 이벤트 바인딩

21.7.2.1. SubscriptionCallbackWorkItem 클래스의 이벤트 바인딩

앞 장(21.6.1)에서 우리는 모듈을 Work Queue 버스에 태우기 위해 px4::ScheduledWorkItem 이라는 뼈대 클래스를 상속받았다. 그리고 이 클래스가 제공하는 ScheduleOnInterval() 함수로 무지성 알람(Polling)을 설정하는 법까지 배웠다.

이제 우리는 이 투박한 알람 시계를 부숴버리고, uORB 토픽이 도착하는 그 찰나의 순간에만 내 모듈을 깨우는 이벤트 기반 액셀러레이터, SubscriptionCallbackWorkItem 클래스를 뼈대에 이식해 볼 것이다.

1. 다중 상속: WorkItemCallback의 만남

가장 빠른 자세 제어기(mc_rate_control)를 짠다고 가정해 보자. 이 모듈은 자이로 센서 데이터(sensor_gyro)가 업데이트되는 순간 무조건 실행되어야 한다.
이를 위해 우리는 기존의 ScheduledWorkItem 상속 구조 안에 SubscriptionCallbackWorkItem 이라는 특별한 래퍼 객체를 멤버 변수로 심어둔다.

#include <px4_platform_common/ScheduledWorkItem.hpp>
#include <uORB/SubscriptionCallback.hpp> // 마법의 콜백 래퍼

class RateController : public px4::ScheduledWorkItem {
public:
    RateController() :
        ScheduledWorkItem(MODULE_NAME, px4::wq_configurations::rate_ctrl),
        // 핵심: 콜백 래퍼를 초기화할 때 '나 자신(this)'을 인자로 넘긴다!
        _sensor_gyro_sub(this, ORB_ID(sensor_gyro)) 
    {}

    // 더 이상 start() 함수에서 ScheduleOnInterval(10_ms)을 부르지 않는다!
    // 대신 uORB 콜백 스위치를 켠다.
    void start() {
        _sensor_gyro_sub.registerCallback(); 
    }

private:
    void Run() override;

    // 폴링(Polling)용 Subscription이 아니라, 
    // 나를 깨워줄 권한을 가진 Callback 전용 래퍼를 선언한다.
    uORB::SubscriptionCallbackWorkItem _sensor_gyro_sub;
};

위 코드의 하이라이트는 _sensor_gyro_sub 생성자에 this 포인터를 넘겨주는 부분이다.
콜백 래퍼는 “데이터가 오면 구체적으로 누구의 멱살을 잡고 깨워야 하는가?“를 알아야 하는데, this를 넘겨줌으로써 “자이로 데이터가 오면, 내 모듈(RateController)의 Work Item 객체를 강제로 큐에 꽂아 넣어줘!” 라고 배달 주소를 명확히 등록하는 것이다.

2. 콜백의 작동 원리 (Behind the Scenes)

registerCallback() 함수가 호출된 이후부터, 이 모듈의 운명은 커널의 uORB 매니저 손에 맡겨진다.

  1. 센서 드라이버가 I2C에서 데이터를 읽고 orb_publish(ORB_ID(sensor_gyro), data) 를 호출한다.
  2. 이 호출은 즉시 uORB 코어 스레드로 넘어가고, 토픽에 등록된 모든 콜백 리스트를 순회한다.
  3. 리스트에서 우리의 _sensor_gyro_sub 객체를 발견하면, 그 안에 저장해 두었던 this 포인터를 타고 올라가 this->ScheduleNow() 함수를 하드웨어 인터럽트(ISR)와 맞먹는 속도로 때려버린다.
  4. 결과적으로 Run() 함수가 즉각적으로 발동(Event-driven Trigger)된다.
void RateController::Run() {
    // 1. 깨어난 이유는 분명 100% 새 데이터가 왔기 때문이므로, if문 검사가 사실상 무의미하다.
    // 그래도 버퍼 복사를 위해 update()를 호출한다.
    sensor_gyro_s gyro;
    if (_sensor_gyro_sub.update(&gyro)) {
        
        // 2. 가장 신선한(방금 도착한) 데이터로 즉각 PID 연산 수행!
        calculate_rate_pid(gyro);
    }
}

이 강제 푸시(Push) 메커니즘 덕분에, 폴링(Polling) 방식에서 발생하던 9ms의 억울한 수면 지연(Data Age) 현상은 완전히 사라지게 된다.

그렇다면 센서 드라이버가 orb_publish()를 외치는 그 출발선에서는 어떤 일들이 벌어지고 있을까? 메모리 락(Lock) 없이 출판자와 구독자가 데이터를 뺏고 뺏기는 극한의 스피드전, 그 숨 막히는 Lock-Free Publication 메커니즘을 다음 섹션 21.7.3에서 마저 파헤쳐 보자.