适用读者:有 STM32 裸机/RTOS 开发经验的嵌入式工程师,初次接触 PX4
PX4 版本:v1.13.x(本仓库版本)
目标:从 STM32 开发者视角,彻底搞懂 PX4 源码架构,并能独立二次开发


目录

  1. PX4 是什么
  2. PX4 vs STM32 开发——核心概念类比
  3. 源码目录结构总览
  4. 构建系统详解
  5. 启动流程——从开机到起飞
  6. uORB 消息总线——PX4 的"神经系统"
  7. 参数系统——PX4 的"配置文件"
  8. 核心模块详解
  9. 完整数据流——从传感器到电机
  10. 二次开发实战
  11. 调试技巧
  12. 常用资源速查

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 为例):

组件芯片作用
主 MCUSTM32F765 / STM32H743 (Cortex-M7)跑 PX4 主固件
协处理器STM32F103 (Cortex-M3)PX4IO——PWM 输出、RC 遥控器输入
双 IMUICM-20689 + BMI055加速度计 + 陀螺仪(冗余)
气压计MS5611 / BMP388高度测量
磁力计RM3100 / IST8310航向参考
GPSu-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_datauORB 消息 vehicle_attitude_s发布/订阅模式传递数据
Flash 存配置参数参数系统(params)自动持久化到 SD 卡/Flash
HAL_SPI_Transmit()platforms/nuttx/src/ 底层 SPIPX4 封装了一层,但底层还是 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 开发者,你需要适应什么

  1. 放弃 “全局变量传数据” 的习惯 → 学会用 uORB 发布/订阅
  2. 放弃 “一个 while(1) 控制一切” 的思维 → 每个模块独立运行,通过消息协作
  3. 放弃 “编译下载调试” 的 IDE 工作流 → 学会命令行 make upload + NSH 终端
  4. 放弃 “改代码就改 .h 里的 #define” → 学会用参数系统,很多参数地面站在线改
  5. 学会看 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, 配置标签=default
  • cubepilot_cubeorange_default → CubePilot 的 CubeOrange
  • holybro_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_attitudevehicle_attitude_sekf2mc_att_control, fw_att_control, mavlink, logger
vehicle_local_positionvehicle_local_position_sekf2mc_pos_control, navigator, mavlink, logger
vehicle_angular_velocityvehicle_angular_velocity_ssensorsmc_rate_control, ekf2
sensor_combinedsensor_combined_ssensorsekf2
actuator_controls_0actuator_controls_smc_rate_controlcontrol_allocator
actuator_motorsactuator_motors_scontrol_allocatorpwm_out, logger
manual_control_setpointmanual_control_setpoint_smanual_controlmc_att_control, fw_att_control
vehicle_statusvehicle_status_scommandermavlink, ekf2, navigator, logger
battery_statusbattery_status_sbattery_statuscommander, mavlink, logger
vehicle_commandvehicle_command_smavlink (收到地面站命令)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_AUTOSTART0★ 机架类型(决定了飞控认为自己是四轴还是固定翼等)
SYS_MC_EST_GROUP2估计器选择(2=EKF2,3=Q 估计器)
MC_ROLL_P6.5滚转 P 增益
MC_ROLLRATE_P0.15滚转角速率 P 增益
MC_PITCH_P6.5俯仰 P 增益
MC_PITCHRATE_P0.15俯仰角速率 P 增益
MC_YAWRATE_P0.2偏航角速率 P 增益
MPC_Z_P1.0高度 P 增益
MPC_Z_VEL_P0.4垂直速度 P 增益
MPC_XY_P0.95水平位置 P 增益
MPC_XY_VEL_P0.09水平速度 P 增益
EKF2_AID_MASK1EKF2 辅助传感器掩码
BAT1_N_CELLS3电池电芯数(3S/4S/6S…)
COM_RC_IN_MODE0RC 输入协议(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 模块:

  1. 检查每个 IMU 的数据是否有效(没有超时、没有读错误)
  2. 检查数据内部一致性(加速度模长 ~9.8?陀螺仪静止时接近 0?)
  3. 投票选出最健康的一个 IMU 作为主 IMU
  4. 对主 IMU 做温度补偿
  5. 发布 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] → 闭环!

关键理解

  1. 每一步都在不同模块中异步执行
  2. EKF 处理的频率最高(1000Hz),位置环最低(50Hz)
  3. 所有数据带时间戳,模块间不阻塞
  4. 日志模块(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 — 降落模式

添加新模式的大致步骤

  1. flight_mode_manager/tasks/ 下创建新任务类
  2. 实现 update() 方法——输出 vehicle_local_position_setpoint
  3. FlightModeManager.cpp 中注册新模式
  4. commander 中加模式切换入口
  5. 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/rcSPX4 的 “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 常用术语速查表

术语全称中文含义
FMUFlight Management Unit飞管单元飞控主处理器(跑 PX4)
IOInput/Output输入输出协处理器(PWM 输出、RC 输入)
uORBmicro Object Request Broker微对象请求代理内部消息总线
EKF2Extended Kalman Filter v2扩展卡尔曼滤波器 v2主姿态/位置估计器
MAVLinkMicro Air Vehicle Link微型飞行器链路飞控与地面站通信协议
SITLSoftware In The Loop软件在环纯软件仿真(电脑模拟飞控)
HITLHardware In The Loop硬件在环硬件在环仿真(真飞控+仿真传感器)
QGCQGroundControlQ 地面站官方地面站软件
NSHNuttShellNuttShellNuttX 的命令行终端
ROMFSROM File SystemROM 文件系统固件内的只读文件系统(存启动脚本)
MCMultiCopter多旋翼四轴/六轴/八轴等
FWFixed Wing固定翼常规布局/飞翼
VTOLVertical Take-Off and Landing垂直起降倾转旋翼/尾座式等
UGV/RoverUnmanned Ground Vehicle地面无人车
UUVUnmanned Underwater Vehicle水下无人潜航器
PIDProportion-Integral-Derivative比例-积分-微分经典控制算法
RTLReturn To Launch返航自动返回起飞点
QNHQuery: Nautical Height海平面修正气压气压高度计修正值
PWMPulse Width Modulation脉宽调制电调/舵机控制信号
DShotDigital Shot数字协议比 PWM 更快的数字电调协议
UAVCANUnmanned Aerial Vehicle CAN无人机 CAN 总线飞控与电调/GPS 间的高速数字总线

附录 B:STM32 开发者常见问题

Q1: PX4 的 main() 在哪?

PX4 没有暴露给开发者的 main()。NuttX 内核的 os_start() 是真正的入口,然后执行 rcS 脚本。对 STM32 开发者来说,rcS 就是你的 main()

Q2: 怎么看变量值?我用惯了 MDK 的 Watch 窗口。

三个办法:

  1. NSH 终端 listener <topic名> — 实时看 uORB 消息(最常用)
  2. 代码里 PX4_INFO("x=%.2f", x) — 打印到串口(最直接)
  3. 飞完看 .ulg 日志 — 用 PlotJuggler 打开,能看到所有 uORB 数据的时间曲线(最全面)

Q3: 中断服务函数在哪写?

底层中断(SPI、I²C、UART)由 NuttX 内核处理,不需要你写 IRQHandler。你在驱动代码的 Run() 里读数据,NuttX 在中断上下文中已经把数据搬运好了(通过 DMA)。

但是,如果你确实需要写自定义中断处理,可以参考 src/drivers/px4io/ 里对 PX4IO 串口中断的处理。

Q4: 怎么加新的外设?比如我想接一个 I²C 温度传感器。

  1. 硬件连接:把传感器焊到飞控的 I²C 总线上
  2. board_config.h 里确认 I²C 总线已启用
  3. src/drivers/ 下写驱动(参考 10.4 节)
  4. 在启动脚本里加一行启动命令

Q5: 代码改了怎么生效?

make px4_fmu-v5_default    # 编译
make px4_fmu-v5_default upload  # 上传
# 飞控自动重启 → 生效

比 Keil 多了一步命令行,本质一样。

Q6: 怎么移植到我的 STM32 板子?

  1. boards/ 下创建你的厂商/型号目录
  2. board_config.h(引脚映射)—— 和你的 STM32 工程里 gpio.h 一样的思路
  3. default.px4board(模块开关)—— 决定要哪些模块
  4. nuttx-config/nsh/defconfig(内核配置)—— 类似 STM32CubeMX 生成的配置
  5. 写启动脚本 —— 你的板子上有哪些传感器就启动哪些驱动

参考 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 看数据流,然后改几个参数看看效果

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐