简介#
在真机部分的 上篇文章 中,我们完成了机载电脑的刷机。本文基于已有工作,利用机载电脑和底层飞控,跑通 ROS2 + PX4 的真机起飞流程。如果你是第一次接触 PX4 仿真,建议先从 ROS2+PX4 仿真环境开发教程 开始。
笔者使用阿木实验室(AMOVLAB)的 JCV-600 科研实验四旋翼飞机(底层飞控为 CodevDynamics 基于 PX4 的 Codev-autopilot),使用 Nvidia Jetson Orin NX 作为机载电脑进行 ROS2 控制。读者可以仿照本教程,利用自己的硬件设备进行相似实验。
需要的硬件设备(根据自己的情况调整):
- 无人机整机套件或自行刷入飞控的无人机
- 一对数传
- 本地电脑与机载电脑
- 相应的连接线若干(参考机载电脑与无人机 I/O 口的具体说明)
- 飞机相关接口的示意图/说明书
流程总述#
下图是本文采用的真机起飞流程的简要示意图。首先通过数传建立地面站与飞控的稳定连接,确保传感器数据、GPS 信号和遥控指令正常传输。随后,机载电脑通过串口与飞控通信,完成驱动安装、权限配置及波特率匹配。在地面站进行飞行前检查通过后,通过 SSH 远程连接机载电脑,启动 MAVROS2 节点,建立 ROS2 与 PX4 的通信链路。最后,使用 Offboard 模式控制无人机。整个过程需全程连接数传,关注地面站数据,随时准备手动接管,确保飞行安全。
与地面站建立连接#
在整个实验过程中,PX4 飞控严格要求与地面站的稳定连接。笔者使用一对 RFD900x 数传进行连接。无人机上的数传与飞控的 I/O 口连接(GND、RX-TX、TX-RX 交叉连接),飞控预留了 5V 电源口为数传供电,这是内部已经完成了电源分配。
在本地电脑中,打开 QGroundControl 地面站,在 “Application Settings” → “Comm Links” → “Add New Link” 中,Type 选择 “Serial”,Serial Port 选择含 “USB0” 的选项,波特率默认 57600。连接后,两个数传 “ACT” 灯常绿(供电正常),“COM” 灯闪烁(连接成功),地面站可以看到飞机各项状态。
机载电脑与飞控连接#
首先,用 USB-TypeC 线将飞控连接到电脑地面站,在参数页面为机载电脑添加 MAVLINK 实例(一般默认为 TELEM2 串口):
MAV_1_CONFIG = TELEM2
MAV_1_MODE = Onboard
MAV_1_RATE = 115200plaintext以下介绍三种连接方式。
方式一:USB-TTL 线#
使用 PX4 的 TELEM2 接口,用于连接机载电脑、数传等,有 VCC(5V)、TX、RX、PWM、GPIO、GND 六个引脚可供选择。将 USB 连接在机载电脑端,用杜邦转端子线交叉连接飞控的 GND、TX、RX,不要进行供电。机载电脑使用预留的 12V 供电口单独供电。
# 检查 USB 连接
lsusb
ls -l /dev/tty*
# 应出现 CH340 USB-Serial adapter 和 /dev/ttyS0
# 将用户加入 dialout 组
sudo usermod -a -G dialout $USERbash下载并编译 CH340 驱动:
sudo apt install build-essential git linux-headers-$(uname -r)
git clone https://github.com/juliagoda/CH341SER
cd CH341SER
make && sudo make install
sudo insmod ch341.ko
sudo cp ch341.ko /lib/modules/$(uname -r)/kernel/drivers/usb/serial/
sudo depmod -a
echo "ch341" | sudo tee /etc/modules-load.d/ch341.confbash创建 udev 规则 /etc/udev/rules.d/99-ch341.rules:
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", MODE="0666", SYMLINK+="ttyCH341"plaintextsudo udevadm control --reload-rules && sudo udevadm trigger
ls -l /dev/ttyCH341 # 验证驱动安装bash测试通信(如出现 MAVLINK 乱码则说明成功):
stty -F /dev/ttyUSB0 115200 raw -echo
cat /dev/ttyUSB0bash如果上述 USB-TTL 连接方式出现问题,可以参考以下链接进行驱动安装:https://blog.csdn.net/qq_52102933/article/details/126839474 ↗
方式二:UART 串口线#
Jetson Orin NX 有 40 针 UART 引脚,其中 Pin8(TX)、Pin10(RX) 为 UART1 默认调试口。我们使用 UART2 进行连接:Pin 14(RX)、Pin 12(TX)、Pin 6(GND),不连 VCC,单独供电。
# 查看可用 UART
ls /dev/ttyTHS*
# 如缺少 ttyTHS1,编辑 /boot/extlinux/extlinux.conf
# 在内核启动行末尾添加 console=ttyTHS1,115200
sudo reboot
# 配置权限和波特率
sudo usermod -aG dialout $USER
stty -F /dev/ttyTHS1 115200 raw -echo
cat /dev/ttyTHS1 # 测试通信bash方式三:USB-TypeC 线#
笔者的 JCV-600 的 TypeC 接口可直接连接机载电脑(仅供参考,不一定适用于所有设备)。
lsusb # 应出现 ID 26ac:0032 The Autopilot PX4 CODEV DP1000
sudo usermod -aG dialout $USER
stty -F /dev/ttyACM0 115200 raw -echo
cat /dev/ttyACM0 # 测试通信bash利用地面站做预检查#
在 “Vehicle Setup” 页面进行飞行前检查:
传感器校准:加速度计、陀螺仪、磁罗盘、水平校准;确认 GPS 信号质量(一般 6 颗星以上)。
参数检查:
SYS_COMPANION = 921600(ROS2 通信波特率)MAV_1_CONFIG = TELEM2(对应机载电脑连接的串口)BAT_*参数与实际电池规格匹配- 检查紧急停止开关功能正常
- 验证遥控器各通道映射正确
重要:在实机飞行中使用 Offboard 模式时,需随时准备用遥控器手动接管飞行,以确保安全。检查完成后,QGC 地面站应显示 “Ready for Fly”。
使用 SSH 远程连接机载电脑#
SSH(Secure Shell)是一种网络安全协议,通过加密和认证机制实现安全的访问和文件传输等业务。由于机载电脑连接在飞控上,并且直接向飞控实时发布 ROS 控制指令,与无人机一起飞行,因此我们不能够直接在机载电脑上发布控制指令。在这里我们使用 SSH 协议将本地电脑与远程的机载电脑进行连接,实现本地电脑远程控制机载电脑从而实现对飞控的控制。
在机载电脑的终端启用 SSH 并且设置开机自启:
sudo systemctl start ssh
sudo systemctl enable ssh
sudo systemctl status ssh # 应显示 active (running)bash确保两台电脑位于同一局域网内,使用个人热点避免公网的客户端隔离。查询 IP 地址:
ifconfig # 查找形如 inet 10.192.90.46 netmask 255.255.0.0 broadcast 10.192.255.255 的输出
# 其中 10.192.90.46 为 IPv4 地址,netmask 和 broadcast 分别为子网掩码和广播地址
# 验证两台电脑的 IPv4 地址,确保在同一大子网下
ping 10.192.90.46 # 在本地电脑 ping 机载电脑,验证网络连通bashSSH 连接:
ssh jetson@10.192.90.46 # 首次连接需输入 yes 确认,然后输入密码
# 终端用户名变为 jetson@ubuntu:~$ 说明连接成功
# 输入 exit 退出 SSH 连接bash使用 MAVROS 进行 Offboard 模式控制#
在仿真篇章中我们使用 MicroXRCEAgent 进行 ROS2 与 PX4 的通信。笔者的飞控固件中刷入了 MAVROS(而非 XRCE 中间件)。MAVROS 是连接 ROS 与 MAVLINK 协议的重要工具。针对 ROS2 的 MAVROS2 功能包仍在开发维护中,但对于简单任务已可使用。
后续,笔者会针对 XRCE 中间件的开发进行探索,这需要重新刷写 PX4 固件。已更新:ROS2+PX4 无人机编队实机(三)UXRCE-DDS 中间件的部署(以 Pixhawk 6C 为例)
安装 MAVROS2#
sudo apt install ros-humble-mavros ros-humble-mavros-msgs
# 安装 GeographicLib 地理数据集(必需依赖)
wget https://raw.githubusercontent.com/mavlink/mavros/master/mavros/scripts/install_geographiclib_datasets.sh
chmod +x install_geographiclib_datasets.sh
sudo ./install_geographiclib_datasets.shbash创建 Offboard 节点#
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python offboard_control \
--dependencies rclpy geometry_msgs mavros_msgs
cd offboard_control/offboard_control
touch simple_offboard.py && chmod +x simple_offboard.pybashsimple_offboard.py 完整源码(Offboard 起飞并悬停 5 米):
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.clock import Clock
from geometry_msgs.msg import PoseStamped
from mavros_msgs.msg import State
from mavros_msgs.srv import CommandBool, SetMode
class SimpleOffboard(Node):
def __init__(self):
super().__init__('simple_offboard')
# mavros
self.declare_parameter('mavros_ns', '/mavros')
self.mavros_ns = self.get_parameter('mavros_ns').get_parameter_value().string_value
# publishers
self.local_pos_pub = self.create_publisher(
PoseStamped,
f'{self.mavros_ns}/setpoint_position/local',
10
)
# subscribers
self.state_sub = self.create_subscription(
State,
f'{self.mavros_ns}/state',
self.state_callback,
10
)
# service clients
self.arming_client = self.create_client(
CommandBool,
f'{self.mavros_ns}/cmd/arming'
)
while not self.arming_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('mavros/arming service not available, waiting...')
self.set_mode_client = self.create_client(
SetMode,
f'{self.mavros_ns}/set_mode'
)
while not self.set_mode_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('mavros/set_mode service not available, waiting...')
self.current_state = State()
self.timer = self.create_timer(0.05, self.control_loop)
self.offboard_setpoint_counter = 0
self.last_call_time = self.get_clock().now()
self.target_altitude = 5.0
self.get_logger().info("Offboard Node Initialized. Waiting for MAVROS state...")
def state_callback(self, msg):
self.current_state = msg
def arm_drone(self):
self.get_logger().info("Attempting to ARM drone...")
arm_request = CommandBool.Request()
arm_request.value = True
future = self.arming_client.call_async(arm_request)
future.add_done_callback(self.arm_response_callback)
def arm_response_callback(self, future):
try:
response = future.result()
if response.success:
self.get_logger().info("Arming successful!")
else:
self.get_logger().warn(f"Arming failed: {response.result}")
except Exception as e:
self.get_logger().error(f"Arming service call failed: {str(e)}")
def set_offboard_mode(self):
self.get_logger().info("Attempting to set OFFBOARD mode...")
mode_request = SetMode.Request()
mode_request.custom_mode = "OFFBOARD"
future = self.set_mode_client.call_async(mode_request)
future.add_done_callback(self.set_mode_response_callback)
def set_mode_response_callback(self, future):
try:
response = future.result()
if response.mode_sent:
self.get_logger().info("OFFBOARD mode enabled!")
else:
self.get_logger().warn(f"Failed to set OFFBOARD mode.")
except Exception as e:
self.get_logger().error(f"SetMode service call failed: {str(e)}")
def control_loop(self):
now = self.get_clock().now()
self.get_logger().info(
f"[State] Connected: {self.current_state.connected}, "
f"Armed: {self.current_state.armed}, "
f"Mode: {self.current_state.mode}",
throttle_duration_sec=1.0
)
pose = PoseStamped()
pose.header.stamp = now.to_msg()
pose.header.frame_id = "map"
pose.pose.position.x = 0.0
pose.pose.position.y = 0.0
pose.pose.position.z = float(self.target_altitude)
self.local_pos_pub.publish(pose)
if not self.current_state.connected:
return
time_since_last_call = (now - self.last_call_time).nanoseconds / 1e9
if time_since_last_call > 5.0:
if not self.current_state.armed:
self.arm_drone()
self.last_call_time = now
elif self.current_state.mode != "OFFBOARD":
self.set_offboard_mode()
self.last_call_time = now
def main(args=None):
rclpy.init(args=args)
offboard_node = SimpleOffboard()
try:
rclpy.spin(offboard_node)
except KeyboardInterrupt:
offboard_node.get_logger().info('Offboard control interrupted by user.')
finally:
offboard_node.destroy_node()
rclpy.shutdown()
print("Offboard Node Shutdown.")
if __name__ == '__main__':
main()python在 setup.py 的 entry_points 中添加:
'console_scripts': [
'simple_offboard = offboard_control.simple_offboard:main',
],python编译:
cd ~/ros2_ws
colcon build --symlink-install --packages-select offboard_control
source install/setup.bashbash起飞#
所有软硬件准备完成,执行完整起飞流程:
- 通过数传将无人机与地面站连接
- 任选一种方式将机载电脑与飞控连接
- 地面站完成飞行前预检查
- 使用 SSH 将本地电脑与机载电脑远程连接
- 在 SSH 终端中执行:
# 激活 ROS2 环境
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
# 启动 MAVROS2 节点(串口和波特率需与飞控配置一致)
ros2 run mavros mavros_node --ros-args -p fcu_url:="serial:///dev/ttyACM0:115200"
# 出现 [INFO] [mavros]: FCU connection established 说明连接成功
# 启动 Offboard 控制节点
ros2 run offboard_control simple_offboardbash此时应看到无人机解锁、起飞、飞至 5 米高度。由于 Offboard 模式的危险性,需随时准备使用遥控器接管飞行。
总结#
勤能补拙,多动手尝试。本文给出了笔者自己的一种操作方式——从数传连接、机载电脑与飞控的三种硬件连接方式、飞行前检查、SSH 远程连接,到最终的 MAVROS2 Offboard 起飞。如有错漏请指正,感谢各位读者的耐心阅读!
参考资料#
- AMOVLAB JCV-600 使用手册 ↗
- PX4 官方文档 ↗
- ROS2+PX4 无人机编队实机(三)UXRCE-DDS 中间件的部署