21.7.2.1. SubscriptionCallbackWorkItem 클래스의 이벤트 바인딩
앞 장(21.6.1)에서 우리는 모듈을 Work Queue 버스에 태우기 위해 px4::ScheduledWorkItem 이라는 뼈대 클래스를 상속받았다. 그리고 이 클래스가 제공하는 ScheduleOnInterval() 함수로 무지성 알람(Polling)을 설정하는 법까지 배웠다.
이제 우리는 이 투박한 알람 시계를 부숴버리고, uORB 토픽이 도착하는 그 찰나의 순간에만 내 모듈을 깨우는 이벤트 기반 액셀러레이터, SubscriptionCallbackWorkItem 클래스를 뼈대에 이식해 볼 것이다.
1. 다중 상속: WorkItem과 Callback의 만남
가장 빠른 자세 제어기(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 매니저 손에 맡겨진다.
- 센서 드라이버가 I2C에서 데이터를 읽고
orb_publish(ORB_ID(sensor_gyro), data)를 호출한다. - 이 호출은 즉시 uORB 코어 스레드로 넘어가고, 토픽에 등록된 모든 콜백 리스트를 순회한다.
- 리스트에서 우리의
_sensor_gyro_sub객체를 발견하면, 그 안에 저장해 두었던this포인터를 타고 올라가this->ScheduleNow()함수를 하드웨어 인터럽트(ISR)와 맞먹는 속도로 때려버린다. - 결과적으로
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에서 마저 파헤쳐 보자.