21.6.3.1.2. 실제 실행된 타임스탬프와의 편차(Jitter)를 계산하여 루프 실행 주기를 동적으로 보정하는 알고리즘

21.6.3.1.2. 실제 실행된 타임스탬프와의 편차(Jitter)를 계산하여 루프 실행 주기를 동적으로 보정하는 알고리즘

앞 단원에서 살펴봤듯, 운영체제의 스케줄러를 타는 이상 **예약된 시간(Expected Time)**과 실제 실행된 시간(Actual Time) 사이에는 필연적으로 미세한 지연(Latency, Jitter)이 발생할 수밖에 없다.
1,010,000us에 예약했던 우리 버스 화물이 여러 사정으로 인해 2ms 지각하여 1,012,000us에 간신히 Run() 함수 안으로 불려 들어왔다고 가정해 보자.

이제 Run() 함수가 자신의 연산을 1ms 만에 모두 끝마치고, 대망의 “다음번 예약 시간“을 잡아야 할 때이다. 이때 초보 개발자와 PX4 코어의 알고리즘은 극명하게 갈린다.

1. 초보자의 맹점: 누적되는 지연(Drifting)

초보자는 보통 코드 마지막에 단순하게 10ms(10,000us)를 현재 시간에 더해서 예약해 버린다.

// [X] 초보자의 잘못된 다음 루프 예약 (상대 시간)
hrt_abstime now = hrt_absolute_time(); // 현재 시간: 1,013,000 (시작 1012k + 연산 1k)
ScheduleAt(now + 10_ms);               // 예약 시간: 1,023,000

이 코드는 완전히 틀렸다. 원래 우리의 이상적인 100Hz 스톱워치 계획표에 따르면, 1차는 1,010k, 2차는 1,020k, 3차는 1,030k 에 알람이 울려야 했다.
하지만 한 번의 2ms 지각과 1ms 연산 시간이 더해지면서 2차 예약이 1,023k로 무려 3ms나 밀려버렸다. 이런 식으로 지연이 누적(Drifting)되면 드론은 1초에 100번이 아니라 95번, 80번으로 점점 맥박이 느려지다 결국 PID 무의식 상태에 빠지게 된다.

2. PX4의 해법: 지터(Jitter) 동적 보정 알고리즘

이 무시무시한 누적 지연 현상을 막기 위해, PX4의 ScheduledWorkItem 시스템은 보이지 않는 백그라운드에서 우리가 원래 뛰어야 했던 **이상적인 타임스탬프(Expected Timestamp)**를 끈질기게 추적하고 보정해 낸다.

ScheduleOnInterval()에 의해 영구 예약이 걸린 work_s 객체가 큐를 들락날락할 때, 커널은 내부적으로 다음과 같은 수식을 통해 다음 타깃 타임(Next Target Time)을 계산한다.

// px4::ScheduledWorkItem 내부 주기 보정 로직 (개념적 재구성)

uint32_t interval = _call_interval_us; // 우리가 예약한 주기 (10,000us)
hrt_abstime now = hrt_absolute_time(); // 현재 시각 (1,013,000)

// 1. 내가 원래 일어났어야 할 예약 시각을 기억해 둠 (1,010,000)
hrt_abstime expected_time = _expected_time; 

// 2. 다음 목표 시각은 무조건 '원래 예약 시각'에 주기를 더한 값이다!
// (지각과 연산 시간에 구애받지 않는 절대 시간표)
hrt_abstime target_time = expected_time + interval; // 1,020,000

// 3. 지터(Jitter) 보정의 핵심! 
// 만약 내가 너무 심각하게 늦잠을 잤거나(예: 15ms 지연), 스레드가 한참 멈췄다가 깨어나서
// 현재 시간(now)이 이미 다음 예약 시간(target_time)을 역전해 버렸다면 어떻게 할까?
if (now > target_time) {
    // 이미 100Hz 타이밍을 완벽히 놓쳤다! 
    // 밀린 숙제(스킵된 루프)를 소급해서 한꺼번에 하려 들지 말고, 
    // 깨끗하게 포기하고 현재 시간을 기준으로 스케줄 표를 아예 새로 짠다 (Reset).
    target_time = now + interval; 
}

// 4. 다음번 계산을 위해 이번의 목표 시간을 저장해 둔다.
_expected_time = target_time; 

// 5. 철저하게 계산된 새로운 목표 시간으로 큐에 화물을 던진다!
ScheduleAt(target_time); 

3. 동적 보정의 마술: 흔들리지 않는 100Hz

이 Jitter 보정 알고리즘 덕분에, 위 시나리오처럼 한 번 2ms(1,012k) 지각해서 들어왔더라도, 다음번 예약 시간은 현재 시간에 연동되지 않고 철저한 시간표에 따라 1,020,000us 에 정확하게 알람이 잡히게 된다.
결과적으로 한 번의 지연이 다음 루프까지 밀리지 않고(No Drifting), 드론은 찰나의 지터를 털어내고 다시 강철 같은 100Hz 주기로 빠르게 복귀하게 된다.

이토록 완벽하게 시간을 쪼개는 운영체제의 혜택을 누리면서도, 초보 개발자들의 어리설은 코딩 한 줄 때문에 이 아름다운 시스템이 통째로 블로킹(Blocking) 당해버리는 끔찍한 사태가 종종 발생한다.
대체 어떻게 코딩을 해야 이 실시간(Real-time)의 파이프라인을 부러뜨리지 않을 수 있는지, 다음 챕터 21.6.4장(블로킹 회피 메커니즘)에서 생존 코딩 가이드를 열어보자.