PX4 飞控源码详解与二次开发指南
适用读者:有 STM32 裸机/RTOS 开发经验的嵌入式工程师,初次接触 PX4
PX4 版本:v1.13.x(本仓库版本)
目标:从 STM32 开发者视角,彻底搞懂 PX4 源码架构,并能独立二次开发
目录
- PX4 是什么
- PX4 vs STM32 开发——核心概念类比
- 源码目录结构总览
- 构建系统详解
- 启动流程——从开机到起飞
- uORB 消息总线——PX4 的"神经系统"
- 参数系统——PX4 的"配置文件"
- 核心模块详解
- 完整数据流——从传感器到电机
- 二次开发实战
- 调试技巧
- 常用资源速查
1. PX4 是什么
1.1 一句话
PX4 是一个开源飞控固件,运行在 ARM Cortex-M7/H7 等 MCU(以 STM32 为主)的飞控硬件上,底层跑 NuttX 实时操作系统。
PX4 = NuttX RTOS + 传感器驱动层 + uORB 消息中间件 + EKF2 估计器 + 多机型控制器 + MAVLink 通信协议
1.2 硬件上跑的是什么
典型的 PX4 飞控硬件(以 Pixhawk 5X / CUAV X7 为例):
| 组件 | 芯片 | 作用 |
|---|---|---|
| 主 MCU | STM32F765 / STM32H743 (Cortex-M7) | 跑 PX4 主固件 |
| 协处理器 | STM32F103 (Cortex-M3) | PX4IO——PWM 输出、RC 遥控器输入 |
| 双 IMU | ICM-20689 + BMI055 | 加速度计 + 陀螺仪(冗余) |
| 气压计 | MS5611 / BMP388 | 高度测量 |
| 磁力计 | RM3100 / IST8310 | 航向参考 |
| GPS | u-blox NEO-M8N / M9N | 定位 + 速度 |
| TF 卡 | microSD | 日志存储 |
| 接口 | UART × 7, SPI × 4, I²C × 2, CAN × 2, USB | 外设连接 |
和普通 STM32 开发板的区别:飞控上的传感器多得多、精度高得多、而且关键传感器(IMU)有 2-3 个做冗余。
1.3 PX4 支持哪些飞行器
- 多旋翼(Multicopter / MC):四轴、六轴、八轴、共轴
- 固定翼(Fixed Wing / FW):常规布局、飞翼
- 垂直起降(VTOL):倾转旋翼、尾座式、标准 VTOL
- 地面无人车(Rover / UGV)
- 水下无人潜航器(UUV)
- 飞艇(Airship)
所有机型共用同一套代码,只是启动时加载不同的控制器和混控器(mixer)。
2. PX4 vs STM32 开发——核心概念类比
这是本文档最重要的一节。用你熟悉的 STM32 概念来理解 PX4,事半功倍。
2.1 概念一一对应表
| STM32 裸机/FreeRTOS 开发 | PX4 飞控 | 说明 |
|---|---|---|
main() 函数 | ROMFS/.../init.d/rcS 启动脚本 | PX4 没有 main(),启动逻辑在 shell 脚本里 |
while(1) 主循环 | ScheduledWorkItem::Run() 回调 | 每个模块有自己的周期性回调,没有大一统的循环 |
TIM3_IRQHandler 定时器中断 | WorkQueue + ScheduledWorkItem | 硬件定时器 → NuttX 工作队列 → 模块的 Run() |
全局变量 g_imu_data | uORB 消息 vehicle_attitude_s | 发布/订阅模式传递数据 |
| Flash 存配置参数 | 参数系统(params) | 自动持久化到 SD 卡/Flash |
HAL_SPI_Transmit() | platforms/nuttx/src/ 底层 SPI | PX4 封装了一层,但底层还是 HAL |
HAL_GPIO_WritePin() | board_config.h 宏 + px4_arch_gpioset() | GPIO 操作 |
xTaskCreate() (FreeRTOS) | px4_task_spawn_cmd() | NuttX 任务创建 |
| Keil/IAR 工程 | CMake + make 命令行构建 | 没有 IDE 工程文件 |
printf 串口调试 | PX4_INFO/WARN/ERR 宏 + NSH 终端 | 飞控有内置命令行 |
.h 文件定义寄存器 | board_config.h + nuttx-config/defconfig | 引脚映射 + 内核配置 |
2.2 最重要的一行类比——你的 main() 去哪了?
STM32 的 main():
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init(); // 初始化 GPIO
MX_SPI1_Init(); // 初始化 SPI(接 IMU)
MX_I2C1_Init(); // 初始化 I²C(接磁力计)
MX_USART1_UART_Init(); // 初始化串口(接 GPS)
IMU_Init(); // 传感器初始化
PID_Init(); // 控制器初始化
Motor_Init(); // 电调初始化
while (1)
{
IMU_Read(&accel, &gyro); // 读传感器
AttitudeEstimate(&attitude); // 姿态解算
ControllerUpdate(&attitude, &rc); // PID 控制
MotorOutput(&motor_pwm); // 输出 PWM
HAL_Delay(2); // 2ms 一个周期
}
}
PX4 的等价物——rcS 启动脚本(摘录):
# ROMFS/px4fmu_common/init.d/rcS
# === 硬件初始化(等价于 HAL_Init、MX_xxx_Init)===
# NuttX 内核已经做了
# === 传感器启动(等价于 IMU_Init、SPI_Init...)===
. ${R}etc/init.d/rc.sensors # 启动所有传感器驱动
# === 姿态估计启动 ===
ekf2 start & # 等价于你的 AttitudeEstimate 线程
# === 控制器启动(等价于 PID_Init + while 循环里的控制代码)===
mc_rate_control start # 角速率环
mc_att_control start # 姿态环
mc_pos_control start # 位置环
# === 通信启动 ===
mavlink start # 地面站通信
# === 输出启动(等价于 Motor_Init)===
pwm_out start # PWM 输出
核心差异不是功能不同,而是组织方式不同:STM32 把所有代码塞一个 main(),PX4 拆成独立模块,用脚本编排启动。
2.3 设计思想对比
| 维度 | STM32 裸机 | PX4 |
|---|---|---|
| 代码组织 | 所有逻辑在一个/几个文件 | 50+ 独立模块,各管各的 |
| 数据流动 | 全局变量直接读写 | uORB 发布/订阅,谁需要谁订阅 |
| 执行方式 | 轮询 + 中断 | 事件/回调驱动 |
| 配置方式 | #define 宏 | 运行时参数(地面站在线改) |
| 可扩展性 | 加功能要改主循环 | 加一个新模块,不影响现有代码 |
| 调试方式 | 串口 printf / JTAG | 内置 NSH 终端 + ulog 日志 |
2.4 如果你是 STM32 开发者,你需要适应什么
- 放弃 “全局变量传数据” 的习惯 → 学会用 uORB 发布/订阅
- 放弃 “一个 while(1) 控制一切” 的思维 → 每个模块独立运行,通过消息协作
- 放弃 “编译下载调试” 的 IDE 工作流 → 学会命令行
make upload+ NSH 终端 - 放弃 “改代码就改 .h 里的 #define” → 学会用参数系统,很多参数地面站在线改
- 学会看 shell 脚本 → 启动流程在
rcS脚本里,不是 C 代码里
3. 源码目录结构总览
3.1 顶层目录一览
PX4源码/
├── src/ # ★★★ 核心源代码(你最常打交道的地方)
│ ├── drivers/ # ★ 硬件驱动层(IMU、GPS、电调等)
│ ├── modules/ # ★★ 应用模块层(控制器、导航、通信)
│ ├── lib/ # ★ 共享库(数学、PID、参数、混控器)
│ ├── systemcmds/ # 系统命令(param、reboot、top...)
│ ├── examples/ # ★★ 示例代码(新手学习入口)
│ └── templates/ # ★★ 模块模板(新建模块从这里开始)
│
├── msg/ # ★★ uORB 消息定义(178 个 .msg 文件)
│ # 每个文件定义一个模块间通信的消息结构体
│
├── ROMFS/ # ★★★ ROM 文件系统(启动脚本 + 混控器)
│ └── px4fmu_common/
│ ├── init.d/ # ★★★ 启动脚本(理解启动流程的核心入口)
│ │ ├── rcS # 总启动脚本(PX4 的 "main 函数")
│ │ ├── rc.sensors # 传感器启动
│ │ ├── rc.mc_apps # 多旋翼应用(ekf2、控制器)
│ │ ├── rc.fw_apps # 固定翼应用
│ │ ├── rc.vehicle_setup # 根据 VEHICLE_TYPE 分发
│ │ ├── rc.interface # PWM/混控器配置
│ │ └── airframes/ # 99 个机架配置文件
│ └── mixers/ # 混控器文件(定义电机/舵机如何映射)
│
├── boards/ # ★ 板级支持包(BSP)
│ └── px4/fmu-v5/ # 以 Pixhawk 4 为例
│ ├── default.px4board # ★★ Kconfig 开关(决定编译哪些模块)
│ ├── src/board_config.h # ★ GPIO/SPI/I²C 引脚定义
│ ├── src/i2c.cpp # I²C 总线初始化
│ ├── src/spi.cpp # SPI 总线初始化
│ ├── init/ # 板级启动脚本
│ └── nuttx-config/ # ★ NuttX 内核配置
│ └── nsh/defconfig # 使能 SPI/I²C/SDIO/USB...
│
├── platforms/ # 平台抽象层
│ ├── nuttx/ # ★ NuttX RTOS 层(真机飞控跑这个)
│ │ ├── NuttX/ # NuttX 内核源码
│ │ ├── src/px4/common/ # PX4 平台初始化(px4_init.cpp)
│ │ ├── src/bootloader/ # 主引导加载程序
│ │ └── cmake/ # 工具链配置
│ ├── posix/ # Linux/SITL 仿真层(电脑模拟)
│ └── common/ # 通用平台代码
│ ├── uORB/ # uORB 消息总线实现
│ ├── px4_work_queue/ # 工作队列实现
│ └── include/ # 平台无关的 API 头文件
│
├── cmake/ # CMake 构建模块
│ ├── px4_config.cmake # 板卡配置发现
│ ├── px4_add_module.cmake # 模块编译宏
│ └── kconfig.cmake # Kconfig 解析器
│
├── Tools/ # 工具脚本(编译、仿真、上传)
├── Makefile # 顶层 Makefile
└── CMakeLists.txt # 顶层 CMake
3.2 新手必看 vs 偶尔看
| 优先级 | 目录/文件 | 原因 |
|---|---|---|
| ★★★ 必看 | ROMFS/.../init.d/rcS | 整个启动流程,PX4 的"main 函数" |
| ★★★ 必看 | src/modules/ | 飞控核心逻辑 |
| ★★ 必看 | src/examples/ | 最简单模块,学习怎么写模块 |
| ★★ 必看 | msg/ | 查有哪些消息可用,理解模块间通信 |
| ★★ 常用 | src/lib/parameters/ | 理解参数怎么定义和读取 |
| ★ 偶尔 | src/drivers/ | 添加新传感器驱动 |
| ★ 偶尔 | boards/ | 移植到新硬件 |
3.3 看看真实文件(边看边理解)
打开 src/modules/,你会看到这些目录:
commander/— 系统状态机(解锁/上锁/模式切换)ekf2/— 卡尔曼滤波器(融合 IMU+GPS+磁力计)mc_att_control/— 多旋翼姿态控制器mc_pos_control/— 多旋翼位置控制器mc_rate_control/— 多旋翼角速率控制器navigator/— 任务导航mavlink/— 地面站通信logger/— 数据记录(SD 卡)sensors/— 传感器数据融合投票
每个目录都是一个独立的"小程序",有自己的 main 入口、自己的参数、自己的 uORB 输入输出。它们之间不直接调用,全部通过 uORB 消息通信。这就是 PX4 模块化的核心。
4. 构建系统详解
4.1 基本命令
# 在仓库根目录执行(你的 Ubuntu 环境)
# 列出所有支持的板卡
make list_config_targets
# 编译 SITL 仿真(强烈推荐新手先用这个)
make px4_sitl_default
# 编译 Pixhawk 4 (FMUv5)
make px4_fmu-v5_default
# 编译并自动上传到飞控(USB 连接)
make px4_fmu-v5_default upload
# 清理
make clean
4.2 命名规则:厂商_型号_label
px4_fmu-v5_default→ 厂商=px4, 型号=fmu-v5, 配置标签=defaultcubepilot_cubeorange_default→ CubePilot 的 CubeOrangeholybro_durandal-v1_default→ Holybro Durandal
4.3 构建流程——从 make 到固件(和 STM32 类比)
STM32 Keil 的编译过程:
选择 Target (px4_fmu-v5) → 选择源文件(Options → C/C++ → 哪些 .c 参与编译)
→ 选择宏定义 → 编译每个 .c → 链接 .o → 生成 .hex/.bin → 下载
PX4 的 make 过程:
make px4_fmu-v5_default
│
├─[1] CMake 读取 boards/px4/fmu-v5/default.px4board
│ → 解析里面的 Kconfig 变量(CONFIG_DRIVERS_IMU_INVENSENSE=y ...)
│ → 确定哪些驱动、模块需要编译 → 生成 config_module_list
│
├─[2] 确定平台 = nuttx, MCU = stm32f7
│ → 加载 platforms/nuttx/cmake/px4_impl_os.cmake
│ → 设置交叉编译工具链 arm-none-eabi-gcc
│
├─[3] 依序编译:
│ ├─ src/lib/ (基础库:数学、参数、混控器...)
│ ├─ platforms/nuttx/ (NuttX 内核)
│ ├─ src/drivers/ (按 default.px4board 选择启用的驱动)
│ ├─ src/modules/ (按 default.px4board 选择启用的模块)
│ └─ ROMFS 打包 (把 init.d/ 脚本和 mixers/ 打包进固件)
│
└─[4] 链接 → 生成 px4_fmu-v5_default.px4 (等价于 .bin)
4.4 关键配置文件详解
(a) boards/px4/fmu-v5/default.px4board — 模块开关
这个文件决定哪些代码参与编译,等价于你在 Keil 里勾选哪些 .c 文件。
# 摘录——看到这些就能理解编译控制
CONFIG_BOARD_PX4_FMU_V5=y # 板卡型号
CONFIG_TOOLCHAIN_ARM_EABI=y # 用 ARM 工具链
CONFIG_ARCH_BOARD_IMXRT=n # 不是 i.MX RT 芯片
CONFIG_ARCH_BOARD_STM32F7=y # 是 STM32F7
# 驱动开关(每个传感器都是一个驱动)
CONFIG_DRIVERS_IMU_INVENSENSE=y # ★ InvenSense IMU(ICM20689 等)
CONFIG_DRIVERS_IMU_BOSCH=y # ★ Bosch IMU(BMI055 等)
CONFIG_DRIVERS_BAROMETER_MS5611=y # MS5611 气压计
CONFIG_DRIVERS_MAGNETOMETER_RM3100=y # RM3100 磁力计
CONFIG_DRIVERS_GPS=y # ★ GPS 驱动
CONFIG_DRIVERS_PWM_OUT=y # ★ PWM 输出
# 模块开关
CONFIG_MODULES_EKF2=y # ★ EKF2 估计器
CONFIG_MODULES_COMMANDER=y # ★ 系统状态机
CONFIG_MODULES_MC_ATT_CONTROL=y # ★ 多旋翼姿态控制
CONFIG_MODULES_MC_POS_CONTROL=y # ★ 多旋翼位置控制
CONFIG_MODULES_MAVLINK=y # ★ MAVLink 地面站通信
CONFIG_MODULES_LOGGER=y # ★ 日志记录
# 系统命令
CONFIG_SYSTEMCMDS_PARAM=y # param 命令
CONFIG_SYSTEMCMDS_REBOOT=y # reboot 命令
CONFIG_SYSTEMCMDS_TOP=y # top 命令
(b) boards/px4/fmu-v5/nuttx-config/nsh/defconfig — NuttX 内核配置
这个是 NuttX RTOS 自己的配置文件,等价于 STM32CubeMX 生成的那些 HAL 配置。决定:
- SPI1/SPI2/SPI3/SPI4 是否使能
- I²C1/I²C2 是否使能
- SDIO 是否使能
- USB CDC ACM 是否使能
- 多少个串口
- 内存布局(FLASH/RAM 分配)
- 调度器策略
© boards/px4/fmu-v5/src/board_config.h — 硬件引脚定义
这是最接近你 STM32 开发经验的部分——定义哪个 GPIO 连什么外设。
// 摘录自 boards/px4/fmu-v5/src/board_config.h
// === GPIO 定义(等价于你的 gpio.h) ===
#define GPIO_nARMED (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_SET|GPIO_PORTB|GPIO_PIN2)
#define GPIO_VDD_BRICK_VALID (GPIO_INPUT|GPIO_PULLUP|GPIO_PORTE|GPIO_PIN2)
#define GPIO_VBUS_VALID (GPIO_INPUT|GPIO_PULLUP|GPIO_PORTE|GPIO_PIN4)
#define GPIO_nPOWER_INPUT (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_SET|GPIO_PORTE|GPIO_PIN9)
#define GPIO_nSPI_EN_SENSORS (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_SET|GPIO_PORTA|GPIO_PIN0)
#define GPIO_BTN_SAFETY (GPIO_INPUT|GPIO_FLOAT|GPIO_PORTE|GPIO_PIN7)
// === SPI 总线定义 ===
#define PX4_SPI_BUS_SENSORS 1 // SPI1 接传感器
#define PX4_SPI_BUS_RAMTRON 2 // SPI2 接 FRAM
#define PX4_SPI_BUS_EXT 4 // SPI4 外部 SPI
// 等价于你的: SPI_HandleTypeDef hspi1, hspi2, hspi4;
// === I²C 总线定义 ===
#define PX4_I2C_BUS_EXPANSION 1 // I²C1 接扩展
#define PX4_I2C_BUS_EXTERNAL1 2 // I²C2 外部
// === 串口定义 ===
#define PX4_NUMBERED_UARTS 8 // 8 个串口
// 串口映射: GPS=/dev/ttyS0, TELEM1=/dev/ttyS1, TELEM2=/dev/ttyS2...
4.5 编译输出在哪
编译完成后,固件在 build/px4_fmu-v5_default/ 目录下:
px4_fmu-v5_default.px4— 最终固件(包含 NuttX + PX4 + ROMFS)px4_fmu-v5_default.elf— ELF 文件(用于 GDB 调试)*.bin— 裸二进制(用于通过 bootloader 烧录)
5. 启动流程——从开机到起飞
这是 PX4 源码阅读中最重要的章节。PX4 没有 main() 函数(藏在 NuttX 内核里),所有启动逻辑在 shell 脚本 中。
5.1 为什么要用 shell 脚本而不是 C 代码启动?
因为灵活。改启动顺序不需要重新编译 C 代码,改一下脚本就行。而且板级定制(不同飞控的传感器不同)只需要换一个不同的 rc.board_sensors 脚本。
5.2 完整启动时序
===== 硬件上电 =====
│
▼
NuttX 内核启动 (os_start → nsh_main)
│ 做了:时钟初始化、外设初始化、挂载 ROMFS
│ 类比:HAL_Init() + SystemClock_Config() + MX_xxx_Init()
│
▼
执行 ROMFS 中的 /etc/init.d/rcS ← ★ PX4 的"主函数"从此开始
│
├─[1] 设置默认环境变量
│ set VEHICLE_TYPE none # 还没确定飞行器类型
│ set MIXER none # 还没确定混控器
│ set SDCARD_AVAILABLE no # SD 卡还没检测
│
├─[2] 挂载 microSD 卡
│ if mount /dev/mmcsd0 /fs/microsd → SDCARD_AVAILABLE=yes
│ 如果卡没格式化 → 自动格式化
│ ★ 类比:f_mount(&SDFatFS, "0:", 1);
│
├─[3] 加载参数
│ param select /fs/microsd/params # 选 SD 卡上的参数文件
│ param import # 导入
│ ★ 类比:从 Flash 读配置到全局变量
│
├─[4] 加载板级默认脚本
│ . rc.board_defaults # 每个板卡自己的额外配置
│ . rc.board_arch_defaults # 架构级默认值
│ ★ 类比:board 级的 HAL_MspInit()
│
├─[5] 启动基础服务
│ tone_alarm start # 蜂鸣器
│ dataman start # 数据管理器(航点存储)
│ load_mon start # CPU 负载监控
│ rgbled start # RGB LED
│
├─[6] ★☆☆ 选择机架(AUTOSTART)
│ 根据参数 SYS_AUTOSTART 的值(比如 4011),加载对应的机架脚本
│ → ROMFS/px4fmu_common/init.d/airframes/4011_holybro_qav250
│ 机架脚本里设定:
│ set VEHICLE_TYPE mc # 飞行器类型 = 多旋翼
│ set MIXER quad_x # 混控器 = X 型四轴
│ # 设定默认 PID 参数、机型参数...
│
├─[7] 检测/更新 PX4IO 协处理器固件
│ if px4io checkcrc → IO_PRESENT=yes
│ 否则尝试更新 IO 固件
│ ★ PX4IO 是 STM32F103,负责 PWM 输出和 RC 接收
│ 类比 STM32 的双 MCU 架构(一个主控一个协控)
│
├─[8] ★★★ 启动传感器
│ rc_update start # RC 遥控器通道映射
│ manual_control start # 手动控制输入处理
│
│ # 检测是否为 HITL(硬件在环仿真)
│ if SYS_HITL > 0:
│ sensors start -h # 仿真传感器
│ else:
│ . rc.board_sensors # 板级传感器脚本
│ . rc.sensors # ★ 标准传感器启动
│ → 启动 IMU 驱动(icm20689 start, bmi055 start...)
│ → 启动气压计(ms5611 start...)
│ → 启动磁力计(rm3100 start...)
│ → 启动 GPS(gps start...)
│ sensors start # ★ 传感器融合模块(投票、温度补偿)
│ commander start # ★ 系统状态机
│ ★ 类比:IMU_Init() + Baro_Init() + Mag_Init() + GPS_Init()
│
├─[9] ★★★ 根据飞行器类型启动控制器
│ . rc.vehicle_setup # 根据 VEHICLE_TYPE 分发
│ │
│ ├── if VEHICLE_TYPE = mc (多旋翼)
│ │ . rc.interface # 1. 启动 PWM 输出 + 加载混控器
│ │ . rc.mc_apps # 2. 启动 MC 控制器
│ │ ├── ekf2 start # ★ 姿态/位置估计器
│ │ ├── mc_rate_control start # ★ 角速率控制(内环)
│ │ ├── mc_att_control start # ★ 姿态控制(外环)
│ │ ├── mc_pos_control start # ★ 位置控制(最外环)
│ │ ├── flight_mode_manager start # 飞行模式管理
│ │ └── land_detector start # 着陆检测
│ │ ★ 类比:你的 while(1) 里的 PID 控制部分
│ │
│ ├── if VEHICLE_TYPE = fw (固定翼)
│ │ . rc.fw_apps # 固定翼专用控制器
│ │
│ ├── if VEHICLE_TYPE = vtol (垂直起降)
│ │ . rc.vtol_apps # VTOL 专用控制器
│ │
│ └── if VEHICLE_TYPE = rover (无人车)
│ . rc.rover_apps # 无人车专用控制器
│
├─[10] 启动其他模块
│ navigator start # 任务导航
│ gimbal start # 云台(如果启用了)
│ gyro_fft start # 陀螺仪 FFT 振动分析
│ gyro_calibration start # 陀螺仪在线标定
│
├─[11] 启动通信
│ mavlink start # ★ MAVLink(地面站通信)
│ # 等价于你的数传串口收发线程
│
├─[12] 启动日志
│ . rc.logging # logger start(SD 卡记录 uORB 数据)
│
└─[13] ★ 就绪!
mavlink boot_complete # 通知地面站:"飞控启动完毕"
5.3 以一个具体机架为例:4011 Holybro QAV250
文件:ROMFS/px4fmu_common/init.d/airframes/4011_holybro_qav250
#!/bin/sh
#
# @name Holybro QAV250
# @type Multi-rotor
# @class Racing
# @maintainer Lorenz Meier <lorenz@px4.io>
# ★ 设定飞行器类型
set VEHICLE_TYPE mc
# ★ 设定混控器(决定了电机布局怎么配)
set MIXER quad_x4
# quad_x4 = X 型四轴,4 个电机
# ★ 设定 PWM 输出映射
set PWM_OUT 1234
# 使用了 MAIN1-4 四个输出通道
# ★ 设定默认参数(这些值会覆盖参数系统的默认值)
param set-default IMU_GYRO_RATEMAX 800 # 陀螺仪最大速率 800Hz
param set-default MC_ROLL_P 5.5 # 滚转 P 增益
param set-default MC_PITCH_P 5.5 # 俯仰 P 增益
param set-default MC_ROLLRATE_P 0.08 # 滚转角速率 P 增益
param set-default MC_PITCHRATE_P 0.08 # 俯仰角速率 P 增益
param set-default MC_YAWRATE_P 0.15 # 偏航角速率 P 增益
这个文件非常短,但它告诉 PX4:你要飞的是 X 型四轴,用哪些输出通道,用哪组默认 PID。
5.4 类比:如果 STM32 也这么设计
想象一下,如果你的 STM32 四轴也用 shell 脚本启动:
# 你想象的 "STM32 版 rcS"
set VEHICLE_TYPE quad_x
imu_start icm20689 # 启动 IMU
pid_start 5.5 0.08 # 启动 PID (P=5.5, D=0.08)
motor_start 1234 # 启动 1-4 号电机
这就是 PX4 的设计哲学——所有模块是可插拔的,启动顺序由脚本编排。
6. uORB 消息总线——PX4 的"神经系统"
6.1 这是什么?(用 STM32 的方式理解)
STM32 传输 IMU 数据的方式:
// === 方式 1: 全局变量 ===
float g_imu_accel_x, g_imu_accel_y, g_imu_accel_z;
float g_imu_gyro_x, g_imu_gyro_y, g_imu_gyro_z;
// IMU 驱动里写(中断或主循环):
g_imu_accel_x = accel_data[0];
// ...
// 控制代码里读:
float ax = g_imu_accel_x;
// ...
// 问题: 谁都能改,不知道谁在什么时候改了,多线程不安全
// === 方式 2: FreeRTOS 消息队列 ===
QueueHandle_t imu_queue = xQueueCreate(10, sizeof(imu_data_t));
// IMU 驱动:
xQueueSend(imu_queue, &imu_data, 0);
// 控制代码:
xQueueReceive(imu_queue, &imu_data, portMAX_DELAY);
// 问题: 只能一对一,多个模块想看同一个数据要多个队列
PX4 的 uORB 方式:
// IMU 驱动发布数据(publish):
sensor_accel_s accel_data{};
accel_data.x = 0.1f;
accel_data.y = 0.2f;
accel_data.z = 9.8f;
accel_data.timestamp = hrt_absolute_time();
_accel_pub.publish(accel_data);
// 驱动不关心谁来读
// 控制模块订阅(subscribe):
sensor_accel_s accel_data;
if (_accel_sub.update(&accel_data)) {
float ax = accel_data.x; // 拿到最新数据
}
// 日志模块也订阅:
// (完全不需要修改 IMU 驱动的代码)
本质:uORB = 一个带时间戳的全局黑板。任何人往黑板贴纸条(发布),任何人可以看(订阅)。
6.2 uORB 的特性
| 特性 | 说明 | 类比你熟悉的 |
|---|---|---|
| 发布/订阅 | 发布者不关心谁在订阅 | 全局变量,但有线程安全保护 |
| 带时间戳 | 每条消息都有微秒级时间戳 | HAL_GetTick() 时间戳 |
| 多实例 | 多个 IMU 发布到同一 topic,实例 0,1,2… | 同名结构体数组 |
| 队列缓冲 | 可设置缓冲深度(默认 1,只保留最新) | FreeRTOS 消息队列 |
| 自动生成代码 | .msg 文件 → sensor_accel_s 结构体 | 省得你手写结构体定义 |
| 无锁读取 | 订阅者拷贝数据,不阻塞发布者 | memcpy 安全拷贝 |
6.3 uORB 核心 API 详解
// ========== C++ API(推荐在模块中使用) ==========
// --- 发布者 ---
#include <uORB/Publication.hpp>
#include <uORB/topics/vehicle_attitude.h>
// 创建发布者(构造函数自动注册)
uORB::Publication<vehicle_attitude_s> _att_pub{ORB_ID(vehicle_attitude)};
// 发布数据
vehicle_attitude_s att{};
att.timestamp = hrt_absolute_time(); // ★ 时间戳是必须的
att.q[0] = q0; att.q[1] = q1; att.q[2] = q2; att.q[3] = q3;
_att_pub.publish(att);
// --- 订阅者(轮询方式) ---
#include <uORB/Subscription.hpp>
uORB::Subscription _sub{ORB_ID(vehicle_attitude)};
// 在周期性回调中调用
vehicle_attitude_s att;
if (_sub.update(&att)) {
// 有新数据, att 是最新的
// update() 返回 true 表示有新数据
float roll = atan2f(2.0f*(att.q[0]*att.q[1] + att.q[2]*att.q[3]),
1.0f - 2.0f*(att.q[1]*att.q[1] + att.q[2]*att.q[2]));
}
// --- 订阅者(回调方式——更高效,IMU 数据推荐这样) ---
#include <uORB/SubscriptionCallback.hpp>
uORB::SubscriptionCallbackWorkItem _sub{this, ORB_ID(sensor_combined)};
// 当 sensor_combined 有新数据时,会自动调用 this->Run()
// 不需要轮询,CPU 效率高
6.4 C API(老代码中常见,了解即可)
#include <uORB/uORB.h>
// 发布
orb_advert_t pub = orb_advertise(ORB_ID(vehicle_attitude), &att_data);
orb_publish(ORB_ID(vehicle_attitude), pub, &att_data);
// 订阅
int sub = orb_subscribe(ORB_ID(vehicle_attitude));
bool updated;
orb_check(sub, &updated);
if (updated) {
orb_copy(ORB_ID(vehicle_attitude), sub, &att_data);
}
6.5 如何定义一个新消息
步骤 1:创建 msg/my_data.msg
# msg/my_data.msg
uint64 timestamp # 时间戳(微秒),每个消息必须第一个字段
float32 temperature # 温度值
float32 humidity # 湿度值
int32 sensor_id # 传感器 ID
bool is_valid # 数据是否有效
uint8[16] name # 传感器名称(字节数组)
步骤 2:添加到 msg/CMakeLists.txt(文件末尾的列表中加 my_data.msg)
步骤 3:编译后自动生成的结构体:
// 自动生成的 msg/my_data.h 中:
struct my_data_s {
uint64_t timestamp; // 8 字节
float temperature; // 4 字节
float humidity; // 4 字节
int32_t sensor_id; // 4 字节
bool is_valid; // 1 字节
uint8_t name[16]; // 16 字节
// 自动 padding 到对齐
};
6.6 常用 uORB 消息速查
| Topic 名称 | 结构体 | 发布者 | 订阅者(谁在用) |
|---|---|---|---|
vehicle_attitude | vehicle_attitude_s | ekf2 | mc_att_control, fw_att_control, mavlink, logger |
vehicle_local_position | vehicle_local_position_s | ekf2 | mc_pos_control, navigator, mavlink, logger |
vehicle_angular_velocity | vehicle_angular_velocity_s | sensors | mc_rate_control, ekf2 |
sensor_combined | sensor_combined_s | sensors | ekf2 |
actuator_controls_0 | actuator_controls_s | mc_rate_control | control_allocator |
actuator_motors | actuator_motors_s | control_allocator | pwm_out, logger |
manual_control_setpoint | manual_control_setpoint_s | manual_control | mc_att_control, fw_att_control |
vehicle_status | vehicle_status_s | commander | mavlink, ekf2, navigator, logger |
battery_status | battery_status_s | battery_status | commander, mavlink, logger |
vehicle_command | vehicle_command_s | mavlink (收到地面站命令) | commander, navigator |
7. 参数系统——PX4 的"配置文件"
7.1 为什么需要参数系统?
STM32 方式改 PID:
// 在代码里改
#define PID_ROLL_P 5.5f
// 改完 → 重新编译 → 重新下载 → 重启
// 不能在空中调参!
PX4 方式改 PID:
# 地面站(QGC)连上飞控 → 参数页面找到 MC_ROLL_P → 拖滑块改 5.5 为 6.0
# 改完立即生效!不需要重编译、不需要重下载!
参数系统让 PX4 的绝大多数配置变成了运行时可变的,存储在 SD 卡或 Flash 中,掉电不丢失。
7.2 参数由什么组成
每个参数有三个核心属性:
// 伪代码表示一个参数
struct Parameter {
const char *name; // "MC_ROLL_P"(全大写,用下划线)
float default_value; // 6.5(出厂默认值)
float min; // 0.0(最小值)
float max; // 20.0(最大值)
const char *description; // "Roll P gain"(描述)
};
7.3 如何定义一个参数
参数文件:src/modules/mc_att_control/mc_att_control_params.c
/**
* Roll P gain.
*
* 滚转角的比例增益。决定了飞控对滚转角度误差的响应强度。
* 值越大响应越快,但太大导致振荡。
*
* @unit 1/s ← 单位
* @min 0.0 ← 最小值
* @max 12.0 ← 最大值
* @decimal 1 ← 小数位数
* @increment 0.1 ← 步进
* @group Multicopter Attitude Control
*/
PARAM_DEFINE_FLOAT(MC_ROLL_P, 6.5f);
在模块中使用:
class MulticopterAttitudeControl : public ModuleParams
{
DEFINE_PARAMETERS(
(ParamFloat<px4::params::MC_ROLL_P>) _param_mc_roll_p,
(ParamFloat<px4::params::MC_ROLLRATE_P>) _param_mc_rollrate_p,
(ParamFloat<px4::params::MC_PITCH_P>) _param_mc_pitch_p,
// ...
)
// 更新参数(在 Run() 开始调用)
void parameters_update() {
_param_mc_roll_p.update(); // 从参数系统读取最新值
// 之后直接用 _param_mc_roll_p.get() 取值
}
};
7.4 参数操作命令(NSH 终端)
# 列出所有参数(很多!可以加过滤)
param show
param show MC_* # 只看 MC_ 开头的
# 查看某个参数
param show MC_ROLL_P
# 修改参数(立即生效)
param set MC_ROLL_P 7.0
# 保存到 SD 卡(否则断电丢失)
param save
# 从 SD 卡加载
param load
# 重置为默认值
param reset MC_ROLL_P
# 导出参数到 SD 卡文件
param dump /fs/microsd/params_export.txt
7.5 常用参数速查
| 参数 | 默认值 | 含义 |
|---|---|---|
SYS_AUTOSTART | 0 | ★ 机架类型(决定了飞控认为自己是四轴还是固定翼等) |
SYS_MC_EST_GROUP | 2 | 估计器选择(2=EKF2,3=Q 估计器) |
MC_ROLL_P | 6.5 | 滚转 P 增益 |
MC_ROLLRATE_P | 0.15 | 滚转角速率 P 增益 |
MC_PITCH_P | 6.5 | 俯仰 P 增益 |
MC_PITCHRATE_P | 0.15 | 俯仰角速率 P 增益 |
MC_YAWRATE_P | 0.2 | 偏航角速率 P 增益 |
MPC_Z_P | 1.0 | 高度 P 增益 |
MPC_Z_VEL_P | 0.4 | 垂直速度 P 增益 |
MPC_XY_P | 0.95 | 水平位置 P 增益 |
MPC_XY_VEL_P | 0.09 | 水平速度 P 增益 |
EKF2_AID_MASK | 1 | EKF2 辅助传感器掩码 |
BAT1_N_CELLS | 3 | 电池电芯数(3S/4S/6S…) |
COM_RC_IN_MODE | 0 | RC 输入协议(0=自动,1=PPM,2=SBUS…) |
8. 核心模块详解
8.1 模块到底是什么?
STM32 的视角:模块就是一个独立的 C++ 类,它:
- 订阅一些 uORB 消息(输入)
- 发布一些 uORB 消息(输出)
- 有一个周期性的
Run()方法(循环体) - 有一组可配置的参数
// 一个最小化的 PX4 模块
class MyModule : public ModuleBase, public ScheduledWorkItem
{
public:
// 构造函数:注册输入输出、设定运行频率
MyModule() :
ModuleParams(nullptr),
ScheduledWorkItem(MODULE_NAME, px4::wq_configurations::nav_and_controllers),
_sub_input(ORB_ID(vehicle_attitude)),
_pub_output(ORB_ID(vehicle_rates_setpoint))
{}
// ★ 这就是你的周期性循环体
void Run() override {
if (should_exit()) { exit_and_cleanup(); return; }
// 1. 读输入
vehicle_attitude_s att;
_sub_input.update(&att);
// 2. 你的算法
// ...
// 3. 输出结果
_pub_output.publish(result);
}
static MyModule *instantiate(int argc, char *argv[]) {
return new MyModule();
}
private:
uORB::Subscription _sub_input;
uORB::Publication<vehicle_rates_setpoint_s> _pub_output;
};
int my_module_main(int argc, char *argv[]) {
return MyModule::main(argc, argv);
}
8.2 EKF2 模块(src/modules/ekf2/)——大脑
作用:融合所有传感器数据,输出高精度姿态和位置
通俗类比:
- 你只有 IMU(加速度计 + 陀螺仪),可以算出姿态,但会漂移
- 你加上 GPS,可以修正漂移,但 GPS 更新慢(10Hz)且不准
- 你加上磁力计,可以修正航向漂移
- 你加上气压计,可以修正高度漂移
EKF2 就是那个把各路传感器数据用数学最优的方式融合在一起的模块。
输入(订阅的消息):
sensor_combined ← IMU(加速度 + 角速度),最快 1000Hz
vehicle_gps_position ← GPS 位置,1-10Hz
vehicle_magnetometer ← 磁力计,~100Hz
vehicle_air_data ← 气压高度,~100Hz
distance_sensor ← 测距仪(低空用),~50Hz
optical_flow ← 光流(室内定位),~50Hz
输出(发布的消息):
vehicle_attitude ← 姿态四元数(你可以转成欧拉角)
vehicle_local_position ← 局部位置(相对起飞点)
vehicle_global_position ← 全球位置(GPS 坐标 + 高度)
estimator_status ← 滤波器健康状态
wind ← 风速估计
核心算法(了解即可):24 状态扩展卡尔曼滤波器
- 10 个导航状态:姿态 (4) + 速度 (3) + 位置 (3)
- 6 个 IMU 偏置:陀螺仪偏置 (3) + 加速度计偏置 (3)
- 8 个其他状态:磁力计偏置 (3) + 风速 (2) + 地形 (1) + …
代码文件结构:
src/modules/ekf2/
├── EKF2.cpp / EKF2.hpp # 主类:订阅传感器、发布估计结果
├── EKF2Selector.cpp # 多实例管理(多 IMU 时用哪个)
├── EKF/ # ★ 卡尔曼滤波器核心实现
│ ├── ekf.cpp # predict + update 主循环
│ ├── control.cpp # 控制逻辑(何时融合什么)
│ ├── covariance.cpp # 协方差矩阵
│ ├── gps_fusion.cpp # GPS 数据融合
│ ├── mag_fusion.cpp # 磁力计数据融合
│ ├── vel_pos_fusion.cpp # 速度/位置融合
│ ├── height_fusion.cpp # 高度融合(气压计/测距仪)
│ ├── airspeed_fusion.cpp # 空速融合(固定翼用)
│ ├── drag_fusion.cpp # 多旋翼阻力融合
│ ├── sideslip_fusion.cpp # 侧滑融合
│ ├── optflow_fusion.cpp # 光流融合
│ ├── terrain_estimator.cpp# 地形估计
│ ├── gps_yaw_fusion.cpp # GPS 航向融合
│ └── utils.cpp # 工具函数
├── Utility/
│ └── PreFlightChecker.cpp # 飞行前检查
└── ekf2_params.c # EKF 相关参数(几十个可调参数)
8.3 Sensors 模块(src/modules/sensors/)——传感器"投票官"
作用:多个同类型传感器时,选出最优的数据
场景:Pixhawk 5X 上有 3 个 IMU(ICM-20689、BMI055、ICM-20948),sensors 模块:
- 检查每个 IMU 的数据是否有效(没有超时、没有读错误)
- 检查数据内部一致性(加速度模长 ~9.8?陀螺仪静止时接近 0?)
- 投票选出最健康的一个 IMU 作为主 IMU
- 对主 IMU 做温度补偿
- 发布
sensor_combined(各控制器和 EKF 用的是这个,而不是直接读原始 IMU 数据)
处理的数据类型:
vehicle_angular_velocity← 从多个陀螺仪选最优vehicle_acceleration← 从多个加速度计选最优vehicle_magnetometer← 从多个磁力计选最优vehicle_air_data← 气压计数据处理vehicle_gps_position← GPS 数据处理(质量检查)
8.4 Commander(src/modules/commander/)——大管家
作用:整个飞控的状态机,管理:
- 解锁/上锁(Arm/Disarm)
- 飞行模式切换(Manual → Altitude → Position → Mission…)
- 故障检测和处理(遥控器丢失→RTL,GPS 丢失→降级…)
- 传感器标定流程触发
核心状态转换(简化版):
上电 → [上锁/Disarmed]
│ │
│ 安全检查通过(GPS 锁星、IMU 标定完成、电池 OK...)
│ │
│ ▼
│ [解锁/Armed] ←→ [各种飞行模式]
│ │ Manual
│ │ Altitude (定高)
│ │ Position (定点)
│ │ Mission (任务)
│ │ RTL (返航)
│ │ Land (降落)
│ │ ...
代码位置:
Commander.cpp— 主状态机逻辑(3000+ 行)state_machine_helper.h/cpp— 状态转换辅助函数Arming/PreFlightCheck/PreFlightCheck.cpp— 解锁前检查failure_detector/— 故障检测
8.5 多旋翼控制——三级串联 PID
遥控器摇杆(期望角度/速度)
│
▼
┌─────────────────────────────────────────────────────┐
│ mc_pos_control (位置控制, ~50Hz) │
│ ├─ 位置环 P:位置误差 → 期望速度 │
│ ├─ 速度环 PID:速度误差 → 期望推力 + 期望角度 │
│ └─ 输出: vehicle_local_position_setpoint │
│ vehicle_attitude_setpoint (期望角度) │
│ vehicle_thrust_setpoint (期望推力) │
└────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ mc_att_control (姿态控制, ~250Hz) │
│ ├─ 姿态环 P:角度误差 → 期望角速率 │
│ ├─ 前馈:期望角速率的导数 │
│ └─ 输出: vehicle_rates_setpoint (期望角速率) │
└────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ mc_rate_control (角速率控制, ~1000Hz, 最快) │
│ ├─ 角速率环 PID:角速率误差 → 力矩 │
│ ├─ 直接把陀螺仪数据当反馈(EKF 估计值) │
│ └─ 输出: actuator_controls_0 (三轴力矩 + 油门) │
└────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ control_allocator (混控器) │
│ ├─ 把 (roll_torque, pitch_torque, yaw_torque, thrust)│
│ │ 转换为 4 个电机的 PWM 值 │
│ │ 公式: motor1 = thrust + roll_torque + pitch_torque - yaw_torque│
│ │ motor2 = thrust - roll_torque + pitch_torque + yaw_torque│
│ │ ...(由 mixer 几何文件定义) │
│ └─ 输出: actuator_motors (16 个电机的 PWM 值) │
└────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ pwm_out (PWM 输出驱动) │
│ ├─ 定时器输出 PWM 到电调 │
│ └─ 电调 → 电机 → 螺旋桨 → 产生力矩 → 四轴姿态变化 │
└─────────────────────────────────────────────────────┘
为什么分三级?
- 内环(rate control)最快(1kHz),直接对抗扰动
- 中环(attitude control)次快(250Hz),跟踪期望角度
- 外环(position control)最慢(50Hz),位置和速度响应本来就是低频
这和你在 STM32 上写的四轴控制级联 PID 完全一样,只是 PX4 把每一级拆成了独立模块。
8.6 mc_rate_control 核心代码(最内环,最接近你 STM32 里写的)
位置:src/modules/mc_rate_control/RateControl/RateControl.cpp
// 简化后的角速率控制核心逻辑
void RateControl::update(const vehicle_rates_setpoint_s &rate_sp,
const vehicle_angular_velocity_s &rate,
const float dt)
{
// 1. 误差 = 期望角速率 - 当前角速率
float roll_rate_error = rate_sp.roll - rate.xyz[0];
float pitch_rate_error = rate_sp.pitch - rate.xyz[1];
float yaw_rate_error = rate_sp.yaw - rate.xyz[2];
// 2. PID 计算
float roll_torque = _pid_rate[0].update(roll_rate_error, dt);
float pitch_torque = _pid_rate[1].update(pitch_rate_error, dt);
float yaw_torque = _pid_rate[2].update(yaw_rate_error, dt);
// 3. 发布力矩指令
_actuators.control[0] = roll_torque; // PX4 内部:actuator_controls[0]=滚转
_actuators.control[1] = pitch_torque; // actuator_controls[1]=俯仰
_actuators.control[2] = yaw_torque; // actuator_controls[2]=偏航
_actuators.control[3] = thrust; // actuator_controls[3]=油门
_actuators_pub.publish(_actuators);
// STM32 里你写的可能是:
// motor_pwm[0] = throttle + roll_pid + pitch_pid - yaw_pid;
// motor_pwm[1] = throttle - roll_pid + pitch_pid + yaw_pid;
// ...
// PX4 把这步交给了 control_allocator/mixer 来做!
}
8.7 MAVLink(src/modules/mavlink/)——通信
作用:和地面站(QGroundControl、Mission Planner)通信
通信链路:
飞控 UART (TELEM1/TELEM2) ──→ 数传电台 ──→ 地面端数传 ──→ USB ──→ QGC
飞控 USB (CDC ACM) ──→ 直接连电脑 ──→ QGC
MAVLink 做哪些事:
- 心跳包:1Hz,告诉地面站"我还活着,当前状态是 xxx"
- 参数同步:地面站列出所有参数,用户修改后下发到飞控
- 命令:地面站发
MAV_CMD_NAV_TAKEOFF→ commander 收到 → 执行起飞 - 数据流:周期性发送姿态、位置、电池等(可配置频率)
- 文件传输:通过 MAVLink FTP 上传/下载任务文件
- 日志流:实时传输 uORB 数据给地面站分析
9. 完整数据流——从传感器到电机
以一个 1kHz(1ms 周期)的多旋翼控制为例,追踪数据流动:
[时间点 0ms] 硬件 SPI 中断触发
│ NuttX 驱动层完成 SPI 读取(DMA)
│
├── ICM-20689 IMU 驱动程序收到数据
│ ├── 原始 ADC 值 → 转换为 m/s² 和 rad/s
│ ├── 发布 sensor_accel (3 轴加速度, 时间戳)
│ └── 发布 sensor_gyro (3 轴角速度, 时间戳)
│
├── BMI055 IMU 驱动程序也同时运行(第 2 个 IMU)
│ └── 同样发布 sensor_accel 和 sensor_gyro(实例 1)
│
[时间点 ~0.5ms] sensors 模块收到数据
│ (sensors 模块以 1kHz 频率运行)
│
├── 检查两个 IMU 的数据有效性
├── 投票选出健康的 IMU 作为主 IMU
├── 应用温度补偿
├── 传感器融合 → 发布:
│ ├── sensor_combined (融合后的 IMU 数据)
│ ├── vehicle_angular_velocity (角速度)
│ └── vehicle_acceleration (加速度)
│
[时间点 ~1ms] ekf2 模块收到 sensor_combined
│ (ekf2 以 IMU 数据频率运行,~500-1000Hz)
│
├── Predict 步骤: 用 IMU 数据预测新状态
│ ├── 积分角速度 → 更新姿态四元数
│ ├── 积分加速度 → 更新速度和位置
│ └── 更新协方差矩阵(表示你对自己估计的信心)
│
├── (如果有 GPS 数据到达)Update 步骤:
│ ├── gps_fusion.cpp: 用 GPS 修正位置和速度
│ ├── mag_fusion.cpp: 用磁力计修正航向
│ ├── height_fusion.cpp: 用气压计修正高度
│ └── 卡尔曼增益 = 传感器精度 vs 预测精度 → 加权融合
│
└── 发布:
├── vehicle_attitude (姿态: 四元数 q[4], 角速度)
├── vehicle_local_position (局部位置: x, y, z, vx, vy, vz)
└── vehicle_global_position (全球位置: lat, lon, alt)
│
[时间点 ~2ms] 遥控器数据到达
│ (manual_control 模块处理)
│
├── 解析遥控器通道值(roll, pitch, yaw, throttle sticks)
├── 应用死区、曲线(expo)
├── 摇杆值 → 期望角度: 摇杆 50% → 期望 pitch 30°
└── 发布 manual_control_setpoint
│
[时间点 ~3ms] 控制器级联计算
│
├── mc_pos_control (~50Hz, 即每 20 个周期跑一次)
│ ├── 输入: vehicle_local_position (当前位置)
│ ├── position_setpoint (期望位置, 来自遥控器/任务)
│ ├── 位置环: (pos_sp - pos) * P → 期望速度
│ ├── 速度环: (vel_sp - vel) * PID → 期望推力 + 期望角度
│ └── 发布: vehicle_attitude_setpoint, vehicle_thrust_setpoint
│
├── mc_att_control (~250Hz, 即每 4 个周期跑一次)
│ ├── 输入: vehicle_attitude (当前姿态)
│ ├── vehicle_attitude_setpoint (期望姿态, 来自 pos_control)
│ ├── 姿态环: (quat_sp - quat) → 期望角速率
│ └── 发布: vehicle_rates_setpoint
│
├── mc_rate_control (~1000Hz, 每个周期都跑)
│ ├── 输入: vehicle_angular_velocity (当前角速率, 直接来自陀螺仪)
│ ├── vehicle_rates_setpoint (期望角速率)
│ ├── 误差 = rates_sp - rates
│ ├── 角速率环 PID 计算 → 三轴力矩
│ └── 发布: actuator_controls_0 (roll_torque, pitch_torque, yaw_torque, thrust)
│
[时间点 ~4ms] control_allocator (混控器)
│
├── 输入: actuator_controls_0 (三轴力矩 + 油门)
├── 根据 mixer 几何文件 (quad_x.toml) 计算:
│ motor[0] = thrust + roll_torque*0.7 + pitch_torque*0.7 - yaw_torque
│ motor[1] = thrust - roll_torque*0.7 + pitch_torque*0.7 + yaw_torque
│ motor[2] = thrust - roll_torque*0.7 - pitch_torque*0.7 - yaw_torque
│ motor[3] = thrust + roll_torque*0.7 - pitch_torque*0.7 + yaw_torque
│ (系数 0.7 是因为 X 型配置 cos(45°) ≈ 0.707)
├── 值归一化到 [0, 1] 或 PWM 脉宽 [1000μs, 2000μs]
└── 发布: actuator_motors
│
[时间点 ~5ms] pwm_out 驱动
│
├── 输入: actuator_motors (PWM 值数组)
├── 设置定时器比较寄存器: TIMx->CCR1 = motor[0]
├── 定时器自动输出 PWM 到 GPIO 引脚
└── 电调收到 PWM → 调节电机转速
│
[时间点 ~6ms] 物理世界反馈
│ 电机转速变化 → 力矩变化 → 四轴角速度变化
│ → IMU 检测到变化 → 回到 [时间点 0ms] → 闭环!
关键理解:
- 每一步都在不同模块中异步执行
- EKF 处理的频率最高(1000Hz),位置环最低(50Hz)
- 所有数据带时间戳,模块间不阻塞
- 日志模块(logger)平行运行,从 uORB 总线"偷看"所有消息,不影响飞行
10. 二次开发实战
10.0 准备工作——SITL 仿真环境
为什么要先用 SITL?
- 在电脑上模拟飞控,不需要真机
- 修改代码后可以立即测试,不担心炸机
- 可以用 GDB 调试
# 你的 Ubuntu 环境
cd ~/PX4-Autopilot # 你的 PX4 仓库路径
make px4_sitl_default jmavsim
# 会弹出 jMAVSim 界面,一个四轴在地上
# 在 NSH 终端(启动的终端)可以输入:
# commander takeoff → 起飞到默认高度
# commander land → 降落
# listener vehicle_attitude → 实时看姿态数据
10.1 场景一:添加一个新的 uORB 消息
假设你要加一个传感器数据消息。
步骤 1:创建 msg/my_custom_data.msg
uint64 timestamp
float32 temperature
float32 humidity
int32 sensor_id
bool is_valid
步骤 2:在 msg/CMakeLists.txt 中,找到消息文件列表(约在文件末尾),添加 my_custom_data.msg
步骤 3:重新编译
make px4_sitl_default
编译后会在 build/px4_sitl_default/uORB/topics/ 目录下生成:
my_custom_data.h→ 包含my_custom_data_s结构体uORBTopics.h→ 包含ORB_ID(my_custom_data)的 ID 定义
步骤 4:在代码中使用
#include <uORB/topics/my_custom_data.h>
// 发布
uORB::Publication<my_custom_data_s> pub{ORB_ID(my_custom_data)};
my_custom_data_s data{};
data.timestamp = hrt_absolute_time();
data.temperature = 25.5f;
data.is_valid = true;
pub.publish(data);
10.2 场景二:创建一个全新的 PX4 模块
这是最常见的二次开发需求——你要写一个自己的算法模块。
步骤 1:复制模板
cp -r src/templates/template_module src/modules/my_sensor_module
cd src/modules/my_sensor_module
步骤 2:修改 CMakeLists.txt
# 改了模块名就行, 模板已经设好了基本结构
px4_add_module(
MODULE modules__my_sensor_module # ★ 模块 ID(用双下划线分隔路径)
MAIN my_sensor_module # ★ main 函数名
SRCS
my_sensor_module.cpp # ★ 源文件
DEPENDS
)
步骤 3:写核心代码
// src/modules/my_sensor_module/my_sensor_module.cpp
#include <px4_platform_common/module.h>
#include <px4_platform_common/px4_work_queue/ScheduledWorkItem.hpp>
#include <px4_platform_common/log.h>
#include <uORB/Subscription.hpp>
#include <uORB/Publication.hpp>
#include <uORB/topics/vehicle_attitude.h>
#include <uORB/topics/vehicle_angular_velocity.h>
#include <uORB/topics/my_custom_data.h>
#include <drivers/drv_hrt.h>
#include <mathlib/mathlib.h>
// ★ 你的模块类
class MySensorModule : public ModuleBase, public px4::ScheduledWorkItem
{
public:
// 构造函数
MySensorModule() :
ModuleBase(nullptr),
// ★ ScheduledWorkItem 让你定时运行
// nav_and_controllers 是系统预定义的工作队列,优先级较高
ScheduledWorkItem(MODULE_NAME, px4::wq_configurations::nav_and_controllers),
// ★ 声明你订阅的消息
_att_sub(ORB_ID(vehicle_attitude)),
_gyro_sub(ORB_ID(vehicle_angular_velocity)),
// ★ 声明你发布的消息
_custom_pub(ORB_ID(my_custom_data))
{}
// ★ PX4 框架要求的工厂方法
static MySensorModule *instantiate(int argc, char *argv[])
{
return new MySensorModule();
}
// ★★★ 核心!周期性回调——等价于你的 while(1) 循环体 ★★★
void Run() override
{
// 退出检查(必须的!)
if (should_exit()) {
exit_and_cleanup();
return;
}
// ---- 第 1 步: 读取输入消息 ----
vehicle_attitude_s att;
bool att_updated = _att_sub.update(&att);
vehicle_angular_velocity_s gyro;
bool gyro_updated = _gyro_sub.update(&gyro);
// ---- 第 2 步: 你的算法 ----
my_custom_data_s output{};
output.timestamp = hrt_absolute_time(); // ★ 时间戳必须填
if (att_updated) {
// 四元数转欧拉角(roll, pitch, yaw)
Eulerf euler(matrix::Quatf(att.q));
output.temperature = math::degrees(euler.phi()); // roll(度)
output.humidity = math::degrees(euler.theta()); // pitch(度)
output.sensor_id = 1;
output.is_valid = true;
// 打印日志(调试用,记得生产环境去掉)
PX4_INFO("Roll: %.1f deg, Pitch: %.1f deg",
(double)output.temperature, (double)output.humidity);
}
if (gyro_updated) {
// 陀螺仪数据也能用
}
// ---- 第 3 步: 发布输出 ----
_custom_pub.publish(output);
}
private:
// ★ 输入:uORB 订阅
uORB::Subscription _att_sub;
uORB::Subscription _gyro_sub;
// ★ 输出:uORB 发布
uORB::Publication<my_custom_data_s> _custom_pub;
};
// ★★★ 模块入口函数——启动脚本中 "my_sensor_module start" 会调用这个 ★★★
int my_sensor_module_main(int argc, char *argv[])
{
return MySensorModule::main(argc, argv);
}
步骤 4:在板卡配置中启用模块
编辑 boards/px4/fmu-v5/default.px4board,添加:
CONFIG_MODULES_MY_SENSOR_MODULE=y
步骤 5:在启动脚本中启动
编辑 ROMFS/px4fmu_common/init.d/rc.mc_apps,在合适位置添加:
my_sensor_module start
步骤 6:编译测试
# 先在 SITL 中测试
make px4_sitl_default
# SITL 可以后,编译真机固件
make px4_fmu-v5_default
10.3 场景三:修改控制算法
这是最常见的二次开发场景。以修改多旋翼高度控制为例:
代码位置:src/modules/mc_pos_control/PositionControl/PositionControl.cpp
// 原代码(简化版)
void PositionControl::_positionControl()
{
// 高度误差 → 期望垂直速度
float z_error = _pos_sp(2) - _pos(2);
_vel_sp(2) = _param_mpc_z_p.get() * z_error;
// 期望速度 = P 增益 × 高度误差
// 等价于你 STM32 代码里的 alt_pid_output = ALT_P * (target_alt - current_alt)
}
// 你的自定义版本——比如改成非线性控制
void PositionControl::_positionControl()
{
float z_error = _pos_sp(2) - _pos(2);
// 小误差:软响应;大误差:硬响应(非线性增益)
if (fabsf(z_error) < 0.5f) {
_vel_sp(2) = _param_mpc_z_p.get() * z_error; // 正常 P
} else {
_vel_sp(2) = sqrtf(fabsf(z_error)) * sign(z_error) * 2.0f; // 非线性
}
// 加上速度前馈(如果遥控器油门杆在移动)
// ...
}
10.4 场景四:添加新传感器驱动
以添加一个新的 I²C 测距传感器为例:
步骤 1:在 src/drivers/distance_sensor/ 下创建新目录
mkdir src/drivers/distance_sensor/my_new_rangefinder
步骤 2:写驱动代码(参考 vl53l1x/ 或 tfmini/ 的实现)
驱动的核心结构:
// 继承 CDev 可以注册为 /dev 设备节点
// 继承 ScheduledWorkItem 可以周期性读取
class MyRangefinder : public cdev::CDev, public px4::ScheduledWorkItem
{
public:
MyRangefinder(int i2c_bus, uint8_t i2c_addr) :
CDev("my_rangefinder"), // 设备名
ScheduledWorkItem(...),
_i2c_bus(i2c_bus),
_i2c_addr(i2c_addr)
{}
// 初始化
int init() {
// 1. 打开 I²C 设备
// 2. 发送配置命令给传感器
// 3. 注册为 uORB 发布者
// 4. 启动 ScheduledWorkItem(开始周期读取)
return PX4_OK;
}
// 周期读取(由 ScheduledWorkItem 调度)
void Run() override {
// 1. 通过 I²C 读取距离值
uint16_t distance_mm = read_i2c_register(REG_DISTANCE);
// 2. 发布 distance_sensor 消息
distance_sensor_s report{};
report.timestamp = hrt_absolute_time();
report.current_distance = distance_mm / 1000.0f; // mm → m
report.min_distance = 0.02f; // 20mm 最小量程
report.max_distance = 4.0f; // 4m 最大量程
_pub.publish(report);
}
private:
int _i2c_bus;
uint8_t _i2c_addr;
uORB::Publication<distance_sensor_s> _pub{ORB_ID(distance_sensor)};
};
10.5 场景五:修改/添加飞行模式
飞行模式的核心在 src/modules/flight_mode_manager/ 和 src/modules/navigator/。
现有模式示例(在 flight_mode_manager/tasks/ 下):
ManualPositionSmooth— 手动定点模式ManualAltitudeSmooth— 手动定高模式Auto— 任务模式Orbit— 绕圈模式Descend— 降落模式
添加新模式的大致步骤:
- 在
flight_mode_manager/tasks/下创建新任务类 - 实现
update()方法——输出vehicle_local_position_setpoint - 在
FlightModeManager.cpp中注册新模式 - 在
commander中加模式切换入口 - 在
navigator中加模式对应的导航逻辑
10.6 开发工作流(最佳实践)
[1. 编码]
└── 在 VS Code / Vim 中写代码
[2. SITL 仿真测试] ← 最快、最安全、迭代最快
└── make px4_sitl_default jmavsim
└── 在 NSH 终端手动发命令测试
└── QGC 连接 localhost:14550 看状态
└── 用 listener 命令看 uORB 数据
[3. SITL + GDB 调试(如果有 bug)]
└── make px4_sitl_default debug
└── 设断点、单步运行
[4. 编译真机固件]
└── make px4_fmu-v5_default
[5. 上传飞控]
└── make px4_fmu-v5_default upload
└── (需要 USB 连接飞控)
[6. 飞行测试]
└── 先在室内 / 系留飞行测试
└── 下载 SD 卡日志分析
11. 调试技巧
11.1 NSH 终端——最强调试工具
飞控通过 USB 连接电脑后会暴露一个串口(/dev/ttyACM0 或类似),上面跑的是 NSH(NuttShell)——一个类似 Linux shell 的命令行。
# 连接 NSH
screen /dev/ttyACM0 57600
# === 最常用的 NSH 命令 ===
help # 列出所有可用命令
# -- 查看系统状态 --
top # 进程列表 + CPU 使用率(类似 Linux top)
dmesg # 内核日志(启动日志、错误信息)
ver all # 版本信息
# -- 查看 uORB 数据(实时) --
listener vehicle_attitude # ★ 实时打印姿态数据
listener vehicle_local_position # ★ 实时打印位置数据
listener sensor_accel # 查看加速度计原始数据
listener sensor_gyro # 查看陀螺仪原始数据
listener -n 10 vehicle_attitude # 只打印 10 条
# -- 参数操作 --
param show # 列出所有参数
param show MC_ROLL_P # 查看某个参数
param set MC_ROLL_P 7.5 # ★ 在线修改参数!
param save # ★ 保存到 SD 卡
# -- 传感器测试 --
sensors status # ★ 查看所有传感器状态
actuator_test # ★ 电机/舵机测试(千万不要装桨!)
# -- 系统控制 --
reboot # 重启飞控
shutdown # 关机
11.2 SITL 仿真调试
# 启动 SITL
make px4_sitl_default jmavsim
# 在 SITL 的 NSH 终端中可以:
listener vehicle_attitude # 和真机一样的命令
commander takeoff # 自动起飞
commander land # 自动降落
param set MC_ROLL_P 8.0 # 在线调参
# GDB 调试 SITL
make px4_sitl_default debug
# 然后可以设断点、单步...
# 非常适合调试复杂逻辑
11.3 日志分析——事后分析
下载日志:
- QGC → 分析 → 日志下载 → 选 .ulg 文件下载
- 或者拔 SD 卡直接读
分析工具:
- PX4 Flight Review — 在线,上传 .ulg 文件即可
- PlotJuggler — 本地分析工具,功能强大,推荐
- Ulog Viewer — QGC 内置的简易查看器
日志中能看到什么:
- 每一个 uORB 消息的时间序列(姿态、位置、加速度、陀螺仪…)
- 每个控制器的输入输出
- 每个传感器的原始数据
- EKF 的状态和每个融合步骤的 innovation(新息 = 预测 - 观测)
- 电池电压、CPU 负载、内存使用…
11.4 printf 调试
// PX4 的日志宏——比 printf 好用
PX4_INFO("温度: %.1f 度", (double)temp);
// 输出: [module_name] 温度: 25.3 度
PX4_WARN("GPS 信号弱, 卫星数: %d", num_sats);
// 输出: [module_name] WARN: GPS 信号弱, 卫星数: 5
PX4_ERR("传感器初始化失败!");
// 输出: [module_name] ERROR: 传感器初始化失败!
// 生产环境记得删掉或者改成 PX4_DEBUG(编译时可以关掉)
11.5 常见问题排查
| 问题 | 排查方法 |
|---|---|
| 飞控不启动 | dmesg 看启动日志, sensors status 看传感器 |
| 无法解锁 | commander check 看解锁拒绝原因 |
| 飞行不稳/振荡 | 检查 PID 参数, 分析日志看角速率振荡频率 |
| GPS 不能定位 | listener sensor_gps 看 GPS 输出 |
| 电机不转 | actuator_test 测试, 检查 PWM 输出 |
| SD 卡不识别 | dmesg 看 mount 日志 |
12. 常用资源速查
12.1 PX4 源码中最重要的 10 个文件
| 文件 | 为什么要看 | 优先级 |
|---|---|---|
ROMFS/px4fmu_common/init.d/rcS | PX4 的 “main 函数”, 理解启动流程 | ★★★ |
ROMFS/px4fmu_common/init.d/rc.mc_apps | 多旋翼应用启动(ekf2 + 三级控制器) | ★★★ |
src/examples/px4_simple_app/px4_simple_app.c | 最简单的模块示例, 学习怎么写模块 | ★★★ |
src/modules/ekf2/EKF2.cpp | 核心估计器主类 | ★★ |
src/modules/mc_att_control/mc_att_control_main.cpp | 姿态控制入口 | ★★ |
src/modules/mc_rate_control/RateControl/RateControl.cpp | 角速率控制(和你 STM32 写的 PID 最像) | ★★ |
src/modules/mc_pos_control/PositionControl/PositionControl.cpp | 位置控制算法 | ★★ |
src/modules/commander/Commander.cpp | 状态机 | ★★ |
src/lib/parameters/param.h | 参数系统 API | ★ |
msg/vehicle_attitude.msg | 最核心的消息结构 | ★ |
12.2 官网链接
12.3 推荐 5 天学习路线
┌─ 第 1 天: 跑起来 ────────────────────────────────┐
│ • 编译 SITL: make px4_sitl_default │
│ • 启动: make px4_sitl_default jmavsim │
│ • 看 QGC 连接, 手动解锁, 在仿真里飞 │
│ • 读 rcS 启动脚本, 理解 MIXER 和 VEHICLE_TYPE │
│ • 搞懂: 为什么改 SYS_AUTOSTART 会改变飞行器类型 │
└─────────────────────────────────────────────────┘
┌─ 第 2 天: 理解 uORB ─────────────────────────────┐
│ • 读几个 msg/*.msg 文件, 看看消息长什么样 │
│ • 在 NSH 终端: listener vehicle_attitude │
│ • 在 NSH 终端: listener sensor_accel │
│ • 在 NSH 终端: listener actuator_motors │
│ • 跑 src/examples/px4_simple_app 的代码 │
│ • 自己写一个小模块, 订阅姿态, 打印 roll angle │
└─────────────────────────────────────────────────┘
┌─ 第 3 天: 理解控制架构 ──────────────────────────┐
│ • 画一张图: sensors→ekf2→rate_control→att_control→pos_control→mixer |
│ • 读 mc_rate_control 的 PID 代码 │
│ • 改 MC_ROLLRATE_P 参数, 在 SITL 中观察效果 │
│ • 读 control_allocator 看看混控怎么算的 │
│ • 读 quad_x.toml 几何文件 │
└─────────────────────────────────────────────────┘
┌─ 第 4 天: 理解 EKF2 ─────────────────────────────┐
│ • 读 EKF2.cpp 的 Run() 方法 │
│ • 调出 estimator_status 看哪些传感器正在被融合 │
│ • 理解 innovation(新息) 是什么意思 │
│ • 理解 predictor-corrector 模式 │
└─────────────────────────────────────────────────┘
┌─ 第 5 天: 实战二次开发 ──────────────────────────┐
│ • 创建自己的模块 (参考 10.2 节) │
│ • 在 SITL 中测试 │
│ • 编译真机固件: make px4_fmu-v5_default │
│ • 上传: make px4_fmu-v5_default upload │
│ • 飞控正常运行, 你的模块也在运行! │
└─────────────────────────────────────────────────┘
附录 A:PX4 常用术语速查表
| 术语 | 全称 | 中文 | 含义 |
|---|---|---|---|
| FMU | Flight Management Unit | 飞管单元 | 飞控主处理器(跑 PX4) |
| IO | Input/Output | 输入输出 | 协处理器(PWM 输出、RC 输入) |
| uORB | micro Object Request Broker | 微对象请求代理 | 内部消息总线 |
| EKF2 | Extended Kalman Filter v2 | 扩展卡尔曼滤波器 v2 | 主姿态/位置估计器 |
| MAVLink | Micro Air Vehicle Link | 微型飞行器链路 | 飞控与地面站通信协议 |
| SITL | Software In The Loop | 软件在环 | 纯软件仿真(电脑模拟飞控) |
| HITL | Hardware In The Loop | 硬件在环 | 硬件在环仿真(真飞控+仿真传感器) |
| QGC | QGroundControl | Q 地面站 | 官方地面站软件 |
| NSH | NuttShell | NuttShell | NuttX 的命令行终端 |
| ROMFS | ROM File System | ROM 文件系统 | 固件内的只读文件系统(存启动脚本) |
| MC | MultiCopter | 多旋翼 | 四轴/六轴/八轴等 |
| FW | Fixed Wing | 固定翼 | 常规布局/飞翼 |
| VTOL | Vertical Take-Off and Landing | 垂直起降 | 倾转旋翼/尾座式等 |
| UGV/Rover | Unmanned Ground Vehicle | 地面无人车 | |
| UUV | Unmanned Underwater Vehicle | 水下无人潜航器 | |
| PID | Proportion-Integral-Derivative | 比例-积分-微分 | 经典控制算法 |
| RTL | Return To Launch | 返航 | 自动返回起飞点 |
| QNH | Query: Nautical Height | 海平面修正气压 | 气压高度计修正值 |
| PWM | Pulse Width Modulation | 脉宽调制 | 电调/舵机控制信号 |
| DShot | Digital Shot | 数字协议 | 比 PWM 更快的数字电调协议 |
| UAVCAN | Unmanned Aerial Vehicle CAN | 无人机 CAN 总线 | 飞控与电调/GPS 间的高速数字总线 |
附录 B:STM32 开发者常见问题
Q1: PX4 的 main() 在哪?
PX4 没有暴露给开发者的 main()。NuttX 内核的 os_start() 是真正的入口,然后执行 rcS 脚本。对 STM32 开发者来说,rcS 就是你的 main()。
Q2: 怎么看变量值?我用惯了 MDK 的 Watch 窗口。
三个办法:
- NSH 终端
listener <topic名>— 实时看 uORB 消息(最常用) - 代码里
PX4_INFO("x=%.2f", x)— 打印到串口(最直接) - 飞完看 .ulg 日志 — 用 PlotJuggler 打开,能看到所有 uORB 数据的时间曲线(最全面)
Q3: 中断服务函数在哪写?
底层中断(SPI、I²C、UART)由 NuttX 内核处理,不需要你写 IRQHandler。你在驱动代码的 Run() 里读数据,NuttX 在中断上下文中已经把数据搬运好了(通过 DMA)。
但是,如果你确实需要写自定义中断处理,可以参考 src/drivers/px4io/ 里对 PX4IO 串口中断的处理。
Q4: 怎么加新的外设?比如我想接一个 I²C 温度传感器。
- 硬件连接:把传感器焊到飞控的 I²C 总线上
- 在
board_config.h里确认 I²C 总线已启用 - 在
src/drivers/下写驱动(参考 10.4 节) - 在启动脚本里加一行启动命令
Q5: 代码改了怎么生效?
make px4_fmu-v5_default # 编译
make px4_fmu-v5_default upload # 上传
# 飞控自动重启 → 生效
比 Keil 多了一步命令行,本质一样。
Q6: 怎么移植到我的 STM32 板子?
- 在
boards/下创建你的厂商/型号目录 - 写
board_config.h(引脚映射)—— 和你的 STM32 工程里gpio.h一样的思路 - 写
default.px4board(模块开关)—— 决定要哪些模块 - 写
nuttx-config/nsh/defconfig(内核配置)—— 类似 STM32CubeMX 生成的配置 - 写启动脚本 —— 你的板子上有哪些传感器就启动哪些驱动
参考 boards/mro/ctrl-zero-f7/ 或 boards/omnibus/f4sd/ 这些第三方板卡的实现,它们是比较好的移植参考。
Q7: 为什么用 NuttX 而不是 FreeRTOS?
历史原因 + 功能原因。NuttX 有完整 POSIX 兼容(open/read/write/ioctl 设备模型),这让 PX4 可以在 NuttX(STM32 真机)和 Linux(SITL 仿真)之间共用大量代码。FreeRTOS 没有这套 POSIX API。
Q8: 学 PX4 是不是要学 NuttX?
不需要深入学。NuttX 对 PX4 开发者来说就是个"高级版 HAL 库"——你只需要知道:
- 怎么创建任务(有封装好的
px4_task_spawn_cmd()) - 怎么用设备文件(
open("/dev/ttyS0")打开串口,就像 Linux 编程) - 怎么用工作队列(
ScheduledWorkItem—— PX4 已经封装好了)
最后的最后:PX4 看起来庞大,但你只需要关注
src/modules/下的几个核心模块 +msg/里的消息定义 +rcS脚本的启动流程。三样东西加起来就是 PX4 的"操作系统"——你写的每一个模块都是往这个系统里插一个组件。不用一次性读懂所有代码,先跑起来 SITL,在 NSH 里listener看数据流,然后改几个参数看看效果。
更多推荐



所有评论(0)