[하이브리드 항법 시스템: 제2편] PX4 자율 항법의 원리와 Gazebo SITL 시뮬레이터 세팅
안녕하세요! 드론 자율비행과 항법 기술을 연구하시는 대학원생, 그리고 연구원 여러분. 쿼드(QUAD) 드론연구소입니다.
지난 **<제1편>**에서는 GPS가 완전히 차단된(GNSS-Denied) 전장 환경에서도 수백 km를 날아갈 수 있도록, 제2차 세계대전 조종사들의 지혜를 빌린 **’3단계 하이브리드 무인 항법 시스템’**의 전체적인 청사진을 그려보았습니다.
하지만 아무리 훌륭한 알고리즘을 기획하더라도, 이를 실제 드론에 이식하려면 드론의 두뇌가 어떻게 세상을 인식하고 비행하는지 그 **’원리’**를 알아야 합니다. 또한, 값비싼 장비를 날리다 추락시키는 불상사를 막기 위해 마음껏 테스트할 수 있는 **’가상 환경(Simulator)’**이 필수적입니다.
이번 **<제2편>**에서는 전 세계 수많은 연구원들이 사용하는 오픈소스 비행 제어 스택인 PX4의 자율 항법 원리를 깊이 있게 파헤쳐 보고, 하드웨어 파손 없이 우리의 알고리즘을 검증할 수 있는 Gazebo SITL 시뮬레이터 세팅법과 Python 제어 코드까지 아주 친절하게 안내해 드리겠습니다. 자, 그럼 출발해 볼까요?
1. 자율 항법의 두뇌: PX4 EKF2의 원리와 메커니즘
사람이 눈을 감고 걸어갈 때 내장 기관(전정기관)의 감각만으로 방향과 거리를 짐작하듯, 드론 역시 다양한 센서 데이터를 융합하여 자신의 위치를 추정합니다. PX4 비행 제어기(Flight Controller)에서 이 역할을 담당하는 핵심 모듈이 바로 **EKF2 (Extended Kalman Filter 2)**입니다.

EKF2는 관성 측정 장치(IMU), 지자기 센서(나침반), 기압계, 대기속도계 등 다양한 센서에서 들어오는 노이즈 낀 데이터를 수학적으로 융합하여 드론의 **24가지 상태(States)**를 실시간으로 추정하는 확장 칼만 필터입니다.
이 24개의 상태 변수에는 다음이 포함됩니다:
- 자세 (Attitude): 4개의 쿼터니언(Quaternion) 상태
- 속도 (Velocity): 3축 (North, East, Down) 속도
- 위치 (Position): 3축 (North, East, Down) 위치
- 센서 바이어스 (Sensor Bias): 자이로스코프 및 가속도계 오프셋 (이 값이 장거리 비행 시 누적 오차를 결정합니다.)
- 지구 및 기체 자기장: 6개의 상태
- 풍속 (Wind Velocity): 북향, 동향 바람 속도 (2차 대전 조종사처럼 측풍을 극복하기 위해 필수적인 상태입니다!)
💡 지연된 시간축 (Delayed Time Horizon)의 마법
드론에 장착된 센서들은 데이터를 EKF로 보내는 지연 시간(Delay)이 각기 다릅니다. EKF2는 과거의 특정 시점인 ‘지연된 시간축(Fusion time horizon)’을 기준으로 데이터를 모아(FIFO 버퍼링) 융합 연산을 수행합니다. 이후 상보 필터(Complementary filter)를 사용해 IMU 데이터를 바탕으로 이 과거의 상태를 ‘현재 시간’으로 예측(Propagate)해 냅니다. 이를 통해 동적인 기동 중에도 수학적으로 일치하는 매우 정밀하고 안정적인 상태 추정이 가능해집니다.
📍 로컬(Local) 좌표계와 글로벌(Global) 좌표계
- 글로벌 위치 (Global Position): WGS84 타원체 모델을 기반으로 한 위도(Latitude), 경도(Longitude), 고도(Altitude)입니다.
- 로컬 위치 (Local Position): 드론이 시동을 켠 지점을 원점(0, 0, 0)으로 삼고, 북쪽(North), 동쪽(East), 아래쪽(Down) 방향을 미터(m) 단위로 나타내는 NED 좌표계입니다.
우리가 앞으로 개발할 ‘비-GPS 하이브리드 항법’에서는 글로벌 GPS 신호가 끊기기 때문에, 이 로컬 NED 좌표계 위에서 EKF2의 IMU 적분값만으로 자신의 위치를 파악하며 비행하게 됩니다.
2. 드론의 길잡이: 비행 모드와 Navigator, 그리고 Offboard 모드
EKF2가 “내가 지금 어디에 있고, 어느 방향으로 가고 있는지”를 알려준다면, Navigator 모듈은 “어디로 가야 할지” 궤적(Trajectory)을 생성합니다. PX4는 다양한 비행 모드를 제공합니다.
- Hold Mode (정지 모드): 현재 위치에 가만히 떠서 호버링(Hovering)합니다.
- Mission Mode (임무 모드): QGroundControl 등에서 사전에 입력한 GPS 웨이포인트(Waypoint)를 따라 자동으로 비행합니다.
- Offboard Mode (오프보드 모드): ⭐️우리가 가장 주목해야 할 모드입니다.⭐️

🚀 왜 Offboard 모드인가?
GPS가 없는 상황에서는 Mission Mode를 사용할 수 없습니다. 따라서 우리는 라즈베리파이나 젯슨 나노(Jetson Nano)와 같은 **컴패니언 컴퓨터(Companion Computer)**에 ROS 2(Robot Operating System 2)를 올려 딥러닝과 비전 알고리즘을 구동해야 합니다.
Offboard 모드는 PX4 내부의 Navigator를 무시하고, 외부에 있는 컴패니언 컴퓨터(ROS 2)가 직접 “X, Y, Z 위치로 가라!” 또는 **”이 속도로 비행해라!”**라는 명령(Setpoint)을 실시간으로 내릴 수 있게 해주는 특별한 모드입니다.
⚠️ Offboard 모드의 안전장치 (Failsafe)
외부 컴퓨터가 드론을 마음대로 조종하게 두는 것은 매우 위험합니다. 만약 ROS 2 프로그램이 에러를 일으켜 멈추면 어떻게 될까요? 이를 방지하기 위해 PX4는 **’생존 신호(Proof of life)’**를 요구합니다. ROS 2 노드는 1초에 최소 2번(2Hz) 이상 지속적으로 OffboardControlMode라는 메시지를 보내 자신이 정상 작동 중임을 증명해야 합니다. 이 신호가 끊어지면 PX4는 즉시 Offboard 모드를 해제하고, COM_OBL_RC_ACT 파라미터에 설정된 대로 제자리 착륙(Land)이나 고도 유지(Altitude) 등의 안전 모드(Failsafe)로 돌입합니다.
3. 안전한 실험실: Gazebo SITL 시뮬레이터 세팅
실제 드론을 야외로 들고 나가 코드를 테스트하다가 추락하면 수백만 원의 손실이 발생합니다. 연구원들에게는 랩실 컴퓨터 안에서 실제 드론과 100% 동일하게 동작하는 물리 엔진 기반 시뮬레이터, **SITL(Software In The Loop)**이 필수입니다.
Ubuntu 22.04 및 ROS 2 Humble 환경이 구축되어 있다고 가정하고 세팅을 시작하겠습니다.
1. PX4 소스코드 다운로드 및 빌드 준비
git clone https://github.com/PX4/PX4-Autopilot.git --recursive
cd PX4-Autopilot/
bash ./Tools/setup/ubuntu.sh
2. Gazebo 시뮬레이터 실행 명령어 한 줄이면 가상의 3D 월드에 쿼드콥터(x500)가 나타납니다.
make px4_sitl gz_x500
명령어를 실행하면 Gazebo 화면이 열리고 가상의 드론이 생성됩니다. 동시에 QGroundControl (QGC) 프로그램을 실행하면, 마치 실제 드론을 USB로 연결한 것처럼 시뮬레이터 속 드론과 자동으로 연동되어 상태를 모니터링할 수 있습니다.
3. Micro XRCE-DDS Agent 실행 PX4(내부 통신망인 uORB 사용)와 ROS 2(DDS 기반)가 서로 통신하려면 다리 역할을 하는 에이전트가 필요합니다. 새 터미널을 열고 에이전트를 실행합니다.
MicroXRCEAgent udp4 -p 8888
이제 가상의 드론과 우리의 Python 코드가 대화할 준비가 모두 끝났습니다!.

4. [실습] Python과 ROS 2를 이용한 Offboard 제어 맛보기
마지막으로, ROS 2 Python 노드를 작성하여 가상 드론을 Offboard 모드로 전환하고 이륙시킨 뒤 목표 위치로 이동시키는 예제 코드를 작성해 보겠습니다. (제공된 C++ 예제를 기반으로 Python 생태계에 맞추어 직관적으로 번역한 코드입니다.).
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
# PX4 통신을 위한 필수 메시지 임포트
from px4_msgs.msg import OffboardControlMode, TrajectorySetpoint, VehicleCommand
class OffboardControlNode(Node):
def __init__(self):
super().__init__('offboard_control_node')
# QoS 프로파일 설정 (통신 신뢰성 확보)
qos_profile = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=1
)
# Publisher 생성 [23, 25-27]
self.offboard_ctrl_mode_pub = self.create_publisher(OffboardControlMode, '/fmu/in/offboard_control_mode', qos_profile)
self.trajectory_setpoint_pub = self.create_publisher(TrajectorySetpoint, '/fmu/in/trajectory_setpoint', qos_profile)
self.vehicle_cmd_pub = self.create_publisher(VehicleCommand, '/fmu/in/vehicle_command', qos_profile)
# 타이머 설정 (100ms 주기 = 10Hz, PX4가 요구하는 2Hz 이상의 하트비트 충족) [27]
self.timer = self.create_timer(0.1, self.timer_callback)
self.setpoint_counter = 0
def timer_callback(self):
# 1. 생존 신호(Heartbeat) 및 위치 제어 활성화 알림 [23, 28]
mode_msg = OffboardControlMode()
mode_msg.position = True # 위치 제어 활성화
mode_msg.velocity = False
mode_msg.acceleration = False
mode_msg.attitude = False
mode_msg.body_rate = False
mode_msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
self.offboard_ctrl_mode_pub.publish(mode_msg)
# 2. 로컬 NED 좌표계 목표 위치(Setpoint) 전송 [23, 26]
# x(North)=0.0m, y(East)=0.0m, z(Down)=-5.0m (고도 5m로 상승)
sp_msg = TrajectorySetpoint()
sp_msg.position = [0.0, 0.0, -5.0]
sp_msg.yaw = 0.0
sp_msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
self.trajectory_setpoint_pub.publish(sp_msg)
# 3. 안전을 위해 초기 10번(1초) 이상 메시지를 먼저 보낸 후 Offboard 모드 진입 및 시동(Arming) [27, 29]
if self.setpoint_counter == 10:
self.publish_vehicle_command(VehicleCommand.VEHICLE_CMD_DO_SET_MODE, 1.0, 6.0) # Offboard 모드 전환
self.publish_vehicle_command(VehicleCommand.VEHICLE_CMD_COMPONENT_ARM_DISARM, 1.0) # 시동 (Arming)
self.get_logger().info('Offboard 모드 진입 및 시동 완료! 고도 5m로 이륙합니다.')
if self.setpoint_counter < 11:
self.setpoint_counter += 1
def publish_vehicle_command(self, command, param1=0.0, param2=0.0):
# PX4로 명령을 전송하는 함수 [23, 24]
cmd_msg = VehicleCommand()
cmd_msg.command = command
cmd_msg.param1 = param1
cmd_msg.param2 = param2
cmd_msg.target_system = 1
cmd_msg.target_component = 1
cmd_msg.source_system = 1
cmd_msg.source_component = 1
cmd_msg.from_external = True
cmd_msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
self.vehicle_cmd_pub.publish(cmd_msg)
def main(args=None):
rclpy.init(args=args)
offboard_control_node = OffboardControlNode()
rclpy.spin(offboard_control_node)
offboard_control_node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
이 코드를 실행(ros2 run <패키지명> <노드명>)하면, 시뮬레이터 속 드론이 시동을 걸고(Arming), 부드럽게 5미터 고도(NED 좌표계의 z = -5.0)로 수직 상승하여 호버링하는 모습을 볼 수 있습니다. 성공적으로 오프보드 제어를 달성하신 것을 축하드립니다!
마무리하며
이번 제2편에서는 드론의 뇌를 담당하는 PX4의 EKF2 상태 추정 원리와 글로벌/로컬 좌표계의 차이를 이해하고, 외부 컴퓨터가 제어권을 가져오는 Offboard 모드의 핵심 메커니즘을 파악했습니다. 더불어 파손 위험 없이 연구를 진행할 수 있는 Gazebo SITL 시뮬레이션 환경을 구축하고 Python을 통한 제어 실습까지 완료했습니다.
non-GPS 환경에서의 추측 항법(Dead Reckoning)을 구현하기 위한 모든 기초 공사가 끝났습니다. 다음 제3편에서는 저가형 센서가 장거리 비행 시 얼마나 오차가 발산하는지 그 태생적 한계를 알아보고, 이를 극복하기 위한 [하드웨어] 산업용 고성능 INS(관성항법장치) 기술에 대해 깊이 있게 다뤄보겠습니다. 다음 연재도 많은 기대 부탁드립니다!

Author: maponarooo, CEO of QUAD Drone Lab
Date: April 13, 2026
