13.5.2.3. PX4 GPS 드라이버(src/drivers/gps/gps.cpp): 백그라운드 워크 큐(Work Queue) 기반 스케줄링 및 물리적 UART/CAN 포트로의 Non-blocking 파일 디스크립터(File Descriptor) 쓰기(Write) POSIX 호출
MAVLink 파서가 쪼개낸 RTCM 보정 데이터 조각들이 uORB 미들웨어의 gps_inject_data 토픽 링 버퍼에 안착했다면, 이제 이 바이트 덩어리들을 건져내어 실제 드론 보드(Pixhawk)에 꽂혀 있는 GPS 모듈의 하드웨어 핀(UART/CAN)으로 밀어 넣는 최후의 임무가 남았다. 이 무식하지만 가장 크리티컬(Critical)한 입출력(I/O) 작업을 전담하는 주체가 바로 PX4 펌웨어의 gps 데몬 스레드(src/drivers/gps/gps.cpp)이다.
본 절에서는 GPS 드라이버 프로세스가 거대한 EKF2 메인 루프를 방해하지 않으면서도, 어떻게 백그라운드 워크 큐(Work Queue) 스케줄러 위에서 비동기적(Asynchronous)으로 깨어나(Wake-up), OS 레벨의 최소 단위인 파일 디스크립터(File Descriptor) 포직스(POSIX) API를 통해 하드웨어 포트에 데이터를 무정지(Non-blocking)로 때려 박는지(Write) 그 심층 로직을 해부한다.
1. 워크 큐(Work Queue) 기반의 백그라운드 스케줄링 아키텍처
PX4 운영체제(NuttX RTOS) 안에서 GPS 드라이버는 무한 루프 스레드(while(1))를 뱅뱅 도는 원시적인 방식 대신, 고도로 최적화된 워크 큐(Work Queue) 스케줄링 모델을 따른다.
1.1 ScheduledWorkItem 상속
GPS 클래스는 내부적으로 px4::ScheduledWorkItem (또는 과거 WorkItem)을 상속받아 구현된다.
평상시 이 워크 아이템(메서드)은 메모리 귀퉁이에서 잠을 자며(Sleep) CPU 점유율 0\%를 유지한다. 그러다 uORB 토픽 버스에 새로운 gps_inject_data 패킷이 도착하거나, 센서 폴링(Polling) 타임아웃 주기가 도래했을 때만 잠시 깨어나(Run) 신속하게 작업을 마치고 다시 휴면에 들어간다.
// src/drivers/gps/gps.cpp (워크 큐 스케줄링 유사 로직)
void GPS::Run()
{
// 워크 큐에 의해 스케줄러가 이 함수를 깨웠을 때 실행
// 1. 센서 포트로부터 UBX/NMEA 데이터 읽어오기 (Read)
bool reading_done = gps_read();
// 2. uORB에서 RTCM 주입(Injection) 데이터 확인 (Poll)
bool injection_done = inject_rtcm();
// 3. 다음 실행을 위한 스케줄 재등록 (예: 5ms 뒤에 다시 날 깨워줘)
ScheduleDelayed(5_ms);
}
이러한 워크 큐 기반 스케줄링은 수십 개의 센서 드라이버(IMU, Barometer, Mag)가 제한된 코어 하나(STM32 등)에서 경쟁 충돌(Race) 없이 조화롭게 팽이를 돌듯이 멀티플렉싱(Multiplexing)될 수 있는 비결이다.
2. uORB 폴링(Polling)과 inject_rtcm() 메서드 버퍼 팝(Pop)
GPS::Run() 루프 내부의 inject_rtcm 로직으로 들어가면, 드라이버가 어떻게 uORB 링 버퍼에서 대기 중인 RTCM 조각들을 뽑아(Pop/Copy)내는지 볼 수 있다.
// RTCM 데이터 주입 메서드 내부 흐름 (유사 코드)
void GPS::inject_rtcm()
{
// uORB 구독자(Subscriber) 파일 디스크립터에서 업데이트 여부 체크
bool updated = false;
orb_check(_gps_inject_data_sub, &updated);
if (updated) {
gps_inject_data_s inject_data{};
// 1. uORB 링 버퍼에서 데이터 조각 하나를 내 메모리로 복사 (Copy)
orb_copy(ORB_ID(gps_inject_data), _gps_inject_data_sub, &inject_data);
// 2. 포직스(POSIX) write 시스템 콜을 통한 하드웨어 쓰기
// _serial_fd 는 오픈(open)된 하드웨어 직렬 포트(예: /dev/ttyS3)
ssize_t written = ::write(_serial_fd, inject_data.data, inject_data.len);
// (3. 쓰기 실패 시 예외 처리 등)
}
}
이 코드는 놀랍도록 단순하지만, 하부(Low-level)를 들여다보면 운영체제의 핵심 철학이 숨어 있다. orb_copy 함수는 앞서 언급한 뮤텍스(Mutex)를 아주 찰나의 시간(\text{ns} 단위) 동안만 쥐고 데이터를 넘겨받음으로써, MAVLink가 다음 배열 칸에 데이터를 푸시(Push)하는 프로세스를 방해(Blocking)하지 않는다.
3. 포직스(POSIX) write() 와 Non-blocking(논블로킹) 하드웨어 제어
가장 아찔한 병목 지점은 바로 OS 시스템 콜인 ::write(_serial_fd, ...) 이다.
RTCM 데이터 180\text{ bytes}를 보드레이트 115200\text{ bps}의 시리얼 포트로 전송하는 데에는 물리적으로 약 15 \sim 20\text{ ms}가 소요된다. 만약 이 write() 함수가 “데이터가 구리선을 타고 다 나갈 때까지 대기(Blocking)“하는 방식이라면, GPS::Run() 메서드가 여기서 20\text{ms} 동안 멈춰버리고 만다.
FC(비행 제어기) 커널 스케줄러가 20\text{ms} 동안 하나의 드라이버에 발이 묶인다면, 그 사이 읽어 들였어야 할 그 초정밀 가속도계(IMU) 스레드가 밀려 기체의 자이로 축이 무너져 내리는(Crash) 대형 사고가 터진다.
3.1 O_NONBLOCK 오픈(Open)의 기적
GPS 드라이버가 초기화(init)될 때, NuttX 시스템은 하드웨어 디바이스 노드(/dev/ttyS*)를 열면서 O_NONBLOCK 이라는 포직스 전용 비동기 입출력 플래그를 삽입한다.
// UART 초기화 개방 예 (논블로킹 탑재)
_serial_fd = ::open(port_name, O_RDWR | O_NOCTTY | O_NONBLOCK);
이 NONBLOCK 마법의 플래그 덕분에, ::write() 함수는 180\text{ bytes}짜리 바이트 배열 덩어리를 하드웨어 칩(UART 레지스터와 DMA 버퍼)에 휙 던져만 놓고 실행 제어권(Return)을 단 0.1\text{ms} 만에 돌려준다(Fire-and-Forget). 실제 직렬 포트 핀에서 전압(TX Line)이 파도치듯 구리선을 빠져나가는 과정은, 메인 CPU가 아닌 비행 제어기 마이크로컨트롤러(STMicroelectronics 등) 내부에 탑재된 DMA (Direct Memory Access) 하드웨어 컨트롤러가 알아서 백그라운드 처리한다.
결과적으로 PX4의 GPS 드라이버 스레드는 write() 호출이 끝난 직후 즉시 워크 큐 스케줄러를 Run() 메서드 바깥으로 빠져나가게 해 주어, EKF2 프로세스나 IMU 폴링 스레드가 조금도 방해받지 않고 정해진 타이밍 수류(Stream)에 맞춰 칼같이 돌아가도록 막강한 **시분할 유연성(Time-slicing Flexibility)**을 제공하는 구조적 승리를 이룩하고 있다.