19.5.3.3. Run() 메서드 오버라이딩을 통한 uORB 업데이트 처리 로직 통합
앞선 19.5.3.2 단원에서 우리는 무식하고 더러웠던 C 스타일의 무한 루프(while)와 시스템 콜 배열 폴링(px4_poll)을 통째로 뜯어내 버리고, 그 빈자리에 px4::ScheduledWorkItem 상속과 uORB::SubscriptionCallbackWorkItem 객체를 융합한 가장 우아한 모던 C++ 이벤트 드리븐(Event-driven) 구독자 스켈레톤(뼈대)을 완벽히 세웠다.
이제 이 뼈대 아키텍처 위에서 남은 마지막 핵심 작업은, 내 SensorSubscriber 클래스의 심장부인 **Run() 메서드의 내부를 구현(Override)**하는 일이다.
과거 무한 루프 블록 안쪽에 지저분하게 뒤섞여 있던 지연 시간 제어, POLLIN 분기문 식별, 메모리 카피 및 비즈니스 데이터 처리 로직들이, 이제는 오직 Run()이라는 단 하나의 캡슐화된 메서드 룸 안으로 아주 정갈하고 치명적이게 역전배치되어 통합된다.
1. 이벤트 함수(Run) 내부의 다중-프레임 수확(Harvesting) 아키텍처
퍼블리셔 데몬이 데이터를 링 버퍼에 때려 넣고, OS 커널 인터럽트가 내 _sensor_sub 객체를 쳐서 알람을 울리면, 백그라운드 큐 스케줄러는 그 즉시 내 클래스의 Run() 함수 메모리 블록을 통째로 잡아채 백그라운드 워커 스레드 위에서 한 번 강제 실행시킨다. 이 찰나의 Run() 스코프 안에서 개발자는 반드시 **“내가 기절한 사이 버퍼 꼬리단에 고착된 모든 안 읽은 스냅샷 데이터들을 하나도 빠짐없이 모조리 뜯어내 소화(Digest)시키는 루프 로직”**을 구축해야만 한다.
void SensorSubscriber::Run()
{
// 1. [안전 방어선] 혹시 모를 Work Queue의 거짓 기상 알람(False Wake-up)이나 무효 스케줄링 방어
if (should_exit()) {
// 모듈이 종료(SIGTERM) 시그널을 맞았는데 스케줄러 큐에 찌꺼기처럼 남아있던 Run()이 늦게 호출된 경우
// 즉시 아무 짓도 하지 않고 큐 티켓을 반납하며 스레드를 자살(Terminate)시킨다.
ScheduleClear();
return;
}
// 2. [극한의 큐 수확 루프] 이 Run() 함수가 불렸다는 것은 "단 1개 이상의" 새 데이터가 확실히 있다는 뜻이다.
// 하지만 내가 기상하기까지 걸린 수십 us의 지연시간 동안, 미친 퍼블리셔가 데이터를 3~4번 연속으로 퍼부었을 수 있다!
// 따라서 updated() 함수가 false로 마를 때까지 계속 버퍼를 빨아들여야(Drain) CPU 큐 병목 현상이 발생하지 않는다.
while (_sensor_sub.updated()) {
// 3. VFS 링 버퍼에서 새 프레임 1개를 내 로컬 C++ 구조체 방 안으로 강제 딥 카피해 온다.
sensor_test_data_s sensor_data{};
_sensor_sub.copy(&sensor_data);
// 4. [치명적 코어 비즈니스 로직 타설 공간]
// 뽑아낸 데이터의 타임스탬프가 유효한지 거칠게 검증하고 물리 연산에 던진다.
if (sensor_data.timestamp > 0) {
PX4_INFO("Callback Fired & Data Drained! Subscribed Temp: %.2f", (double)sensor_data.temperature);
// ... 여기서 sensor_data.temperature 값으로 PID 게인 행렬을 업데이트하거나,
// 모터 이너 루프 믹서 알고리즘 함수를 호출하여 즉각적인 PWM 출력을 단행한다 ...
}
}
}
2. while (_sensor_sub.updated())의 무자비한 드레인(Drain) 철학
이 Run() 객체 지향 구현체에서 가장 경악스러운 최적화 포인트는 바로 내부에 존재하는 아주 짧은 while 루프이다. 초급 개발자들은 흔히 “어차피 Run()이 불렸다는 건 이벤트가 한 번 발생했다는 거니까, 그냥 if(_sensor_sub.updated()) 조건문으로 단 한 번만 copy()를 때리고 함수를 종료(return)하면 되지 않는가?“라고 오만하게 착각한다.
그것이 바로 EKF 추정기를 터뜨리고 비행기를 추락시키는 가장 끔찍한 아마추어 코딩이다.
글로벌 시스템 워크 큐(Work Queue)는 OS의 통합적인 렌더링 스케줄링을 받으므로, 내가 이벤트 우선순위를 받아 Run()으로 진짜 기상하기까지 아주 미세한 스케줄링 대기열 지연 시간(Queueing Latency, 예: 수 밀리초)이 반드시 누적된다. 그 찰나의 지연시간 동안 가속도계 보드가 400Hz의 맹렬한 속도로 링 버퍼 꼬리단에 무려 3개의 파편화된 스냅샷 덩어리를 연속 콤보로 갈겨 넣고 지나갔다면 어떻게 될까?
만약 당신의 코드가 if문으로 고작 가장 꼭대기에 있는 1개의 낡은 패킷만 카피해 빼먹고 Run() 함수를 쿨하게 닫아버린다면, 링 버퍼에는 여전히 내가 빼먹지 않은 2개의 데이터 찌꺼기가 적체되어 고여버린다. 이 처참한 잔여물은 그다음 내 Run()이 다시 소환되었을 때 비로소 읽히게 되며, 결국 내 모듈은 평생 현실 세계의 물리 센서 시간보다 몇 밀리초씩 끔찍하게 과거로 뒤처져서(Lagging) 데이터를 추종해 쫓아가는 물리적 위상 지연 폭주 상태에 빠지고 만다. 드론 제어 공학에서 10ms의 위상 지연(Phase Lag)은 곧장 시스템 제어 발산(Divergence) 추락으로 직결된다.
따라서 픽스호크 워크 큐의 절대적인 생존 철칙은, 스케줄러가 나를 깨워 함수 스코프(Run())에 단 한 번 진입시켜 주면, 그 안에서 while (updated()) 루프를 미친 듯이 돌려 링 버퍼의 밑바닥이 완전히 말라 비틀어져서 updated() 플래그가 false를 뱉을 때까지(Drain) 모든 버퍼 안의 적체된 배열 조각 데이터들을 한계까지 모조리 퍼올려 씹어 먹고 소화(copy)시켜 치워버려야 한다는 것이다.
이 극강의 드레인(Drain) 아키텍처를 통해서만, PX4 모듈은 진정한 1000Hz 동기화 위상 지연 0초의 완벽한 리얼타임 이너 루프 패권을 거머쥘 수 있게 된다.