跳转至

启动流程、源码架构与数据流

读飞控源码时,不应从目录名猜功能,也不应从某个控制函数孤立地开始。更可靠的方法是同时追踪启动顺序、运行时调度和一条完整数据链。

在常见Arm Cortex-M目标上,主栈指针(MSP,Main Stack Pointer)给出当前主栈位置,向量表偏移寄存器(VTOR,Vector Table Offset Register)指定异常和中断向量表位置。具体处理器可能采用不同机制。

飞控启动与运行数据流

从复位到可运行系统

存在引导加载程序时,典型嵌入式启动链可能包含:

  1. 处理器复位后读取引导加载程序的向量表并进入其复位处理程序;
  2. 引导加载程序完成自身所需的最小启动初始化;
  3. 引导加载程序检查升级条件、板级标识和应用镜像;
  4. 选择有效应用后,设置应用所需的主栈指针和向量表偏移;
  5. 跳转到应用镜像的复位处理程序;
  6. 应用启动代码初始化自己的栈、数据段、未初始化数据段和运行环境;
  7. 应用建立芯片时钟、板级引脚、总线、存储和通信;
  8. 探测或启动传感器与执行器驱动;
  9. 加载并校验参数;
  10. 创建任务、调度表或消息通道;
  11. 启动状态估计、控制和日志模块;
  12. 进入未解锁状态并等待健康条件满足。

没有独立引导加载程序时,复位向量可以直接指向应用复位处理程序。具体处理器对主栈指针、向量表偏移和跳转状态的要求必须以芯片架构和项目实现为准。

不同项目会合并、调整或跳过其中部分阶段。实时操作系统上的飞控可能由启动脚本拉起多个模块;紧凑型固件可能在一个主循环中按调度表执行任务。

引导加载程序与应用固件

引导加载程序(Bootloader)和飞控应用通常是不同产物。

引导加载程序可能负责:

  • 初始化最少量硬件;
  • 进入USB、串口或其他升级模式;
  • 校验应用镜像;
  • 决定从哪个地址启动应用;
  • 提供失败恢复入口。

应用固件负责正常飞控功能。刷写地址、分区、板级标识或镜像格式错误,可能覆盖引导加载程序或让设备无法启动。

在修改链接脚本、闪存分区或启动代码前,必须确认恢复方式不依赖正在修改的同一条路径。

三种常见组织方式

单主循环与协作式调度

多个任务在一个主循环中按周期或事件运行。代码结构紧凑,但任一任务阻塞都可能拖延后续任务。

实时操作系统任务

实时操作系统(RTOS,Real-Time Operating System)可以提供任务、优先级、信号量、队列和定时机制。它不会自动保证实时性;优先级、锁、栈和最坏执行时间仍需设计和测量。

模块与消息总线

模块通过发布订阅或队列交换数据。模块边界清晰,但必须管理消息时间戳、队列深度、丢包、更新率和数据生命周期。

一个项目也可能同时使用以上三种方式。

用一条数据链理解源码

以陀螺仪角速度到电机命令为例,建立追踪表:

阶段 需要找到的对象 必须确认
设备驱动 原始寄存器或总线帧 量程、字节序、读取时间
驱动输出 带时间戳的物理量 单位、轴方向、有效标志
校准与滤波 偏置、比例和滤波状态 初始化与更新条件
状态估计 姿态或角速度状态 坐标系和延迟
目标生成 遥控或导航目标 模式、限幅和单位
控制器 误差与控制输出 周期、饱和和积分处理
控制分配 各轴需求到执行器 几何、优先级和限幅
输出驱动 协议帧或脉宽 通道映射、更新率和失效值

只找到同名变量不够。还要确认它在什么线程、以什么频率、由谁拥有、何时有效。

区分控制面和数据面

  • 控制面:参数修改、模式切换、解锁、任务命令和固件升级;
  • 数据面:传感器样本、估计状态、执行器指令、遥测和日志。

控制面通常低频,但权限和状态转换风险高;数据面可能高频,重点是时序、带宽和一致性。把配置消息直接当成实时控制数据,或允许任何通信端口无条件修改安全参数,都会破坏边界。

如何定位源码入口

先使用项目自身的构建文件和符号搜索:

rg "main\\(|setup\\(|init\\(" .
rg "scheduler|work_queue|task" .
rg "publish|subscribe|message|topic" .
rg "actuator|mixer|control_allocation|motor" .

搜索结果只是候选入口。继续从调用者、构建条件和板级配置确认该文件是否真的进入目标产物。

PX4 v1.17.0的可执行追踪例子

本专题使用SITL默认启用的hello示例说明“源码存在”如何变成“运行时命令”:

Kconfig是源自Linux内核生态的配置语言和菜单系统,PX4用它声明功能开关及依赖;它不是运行时参数。

证据 路径或命令 证明什么
源文件 src/examples/hello/hello_main.cpp 定义应用入口和只读输出
任务实现 src/examples/hello/hello_start.cpp 定义startstopstatus命令
模块构建 src/examples/hello/CMakeLists.txt px4_add_module()注册模块和源文件
功能开关 src/examples/hello/Kconfig 定义EXAMPLES_HELLO
SITL目标 boards/px4/sitl/default.px4board CONFIG_EXAMPLES_HELLO=y启用模块
构建 make px4_sitl sihsim_quadx 生成并启动包含该模块的SITL
运行 pxh>输入hello start 入口被注册并能够执行

成功时控制台出现hello、五次Doing work...goodbye。若命令不存在,应按上表从板级配置反向检查,不能只反复清理和重编译。

同一版本中,可以按下列目录继续追踪多旋翼主链:

src/modules/sensors
→ src/modules/ekf2
→ src/modules/mc_att_control
→ src/modules/mc_rate_control
→ src/modules/control_allocator

这些目录是v1.17.0的阅读入口,不表示每条数据都由目录顺序直接调用。PX4模块主要通过uORB主题交换数据,仍需结合订阅、发布、时间戳和目标配置确认实际路径。

反向确认编译关系

一个源文件存在,不代表当前目标会编译它。应查看:

  • 构建清单;
  • 条件编译宏;
  • 目标功能裁剪;
  • 链接映射文件;
  • 运行时模块列表;
  • 日志或调试输出。

画出自己的架构图

读完目标项目后,应能够画出:

启动入口
  → 板级初始化
  → 驱动与总线
  → 数据发布或缓存
  → 状态估计
  → 目标生成
  → 控制器
  → 控制分配
  → 输出驱动

并在旁边标出参数、日志、通信和故障监视如何跨越这些模块。无法画出数据所有权和时间关系时,不应直接重构控制链。

检查理解

  1. 引导加载程序和应用固件分别负责什么?
  2. 为什么实时操作系统不能自动保证实时性?
  3. 源文件存在为什么不代表它进入了目标固件?
  4. 沿数据链阅读源码时必须记录哪些契约?
  5. 控制面和数据面的主要风险有什么不同?

主要参考