ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

读懂ArduSub必先读懂ArduPilot:框架、调度与数据流全解析

读懂ArduSub必先读懂ArduPilot:框架、调度与数据流全解析 说实话我最早学ArduSub程序的时候干过一件特别容易劝退自己的事——直接打开ArduSub目录从入口函数开始逐行读。结果可想而知还没走到传感器初始化就先被HAL、AP_Scheduler、GCS_MAVLink这一堆抽象类绕晕了。后来我才意识到ArduSub并不是“一个普通单片机程序”而是一套运行在ArduPilot框架上的完整自动驾驶软件。ArduSub把飞控里最复杂的部分硬件适配、任务调度、状态估计、通信全部收进了框架里想真正读懂它第一件事不是盯着某一行代码看而是先把ardupilot框架的整体结构装进脑子里。这篇文章就是ArduSub程序学习系列的第一篇主要把ArduPilot框架的骨架拆开讲清楚。我会按实际读代码时遇到的顺序来讲先聊为什么框架思维这么重要再带你认识顶层目录、HAL和调度器、数据流、参数与MAVLink最后落到ArduSub相对ArduPilot的那些水下特性。想入门开源水下机器人程序、以及刚被ArduPilot庞大代码量劝退的开发者这篇文章能帮你少走很多弯路。1. 为什么从框架入手ArduSub学习路上我踩过的坑1.1 从入口函数开始读是最容易劝退的开局几乎每个刚接触ArduPilot的人都会犯同一个错误觉得读代码就该从程序入口开始像读小说一样一行行往后翻。但ArduPilot系列包括ArduSub的入口完全不同它在启动阶段会把大量初始化工作交给底层板级代码然后进入一个被调度器Scheduler管理的运行期循环。真正的功能代码既不在入口附近也不按顺序“跑一遍就完”而是以任务函数的形式被反复调用。我第一轮读入口读了两个小时最后只搞清楚了“这程序先初始化了串口”这种信息对整体运行还是一团雾水。更麻烦的是ArduSub的很多代码并不是顺序执行的而是由各种回调、消息、状态机驱动。今天你看到的一个函数明天可能被另一个模式调用你认为的“主流程”往往只是众多任务中的一条。如果你一上来就钻研某个具体模块很容易陷入“只见树木不见森林”的状态读着读着就忘了它和整个系统是什么关系。1.2 更合理的学习顺序先地图后细节我现在的习惯是任何大型开源项目都先找“地图”再开始导航。所谓地图就是搞清楚这个程序由哪些大的子系统组成、它们之间怎么通信、数据从哪里来到哪里去。对于ArduSub来说地图可以粗略画成硬件抽象层HAL在最底部往上依次是传感器驱动和状态估计、控制算法、模式管理、通信链路、日志和参数系统。这个顺序千万不要颠倒。我有段时间跳过框架直接研究姿态控制结果打开AC_AttitudeControl的头文件满屏都是四元数、角速度前馈、PID结构体我看了一晚上也没能把它和实际运行联系起来。后来换了个思路先回答一个很朴素的问题“从遥控器拨杆到推进器转动中间到底经历了哪些环节”顺着这条链路走下来才发现姿态控制只是其中一个节点它的输入输出、调用者、运行频率全都被框架规定好了框架知识反而帮助我更清楚地理解控制代码为什么这么写。ArduPilot还有一个特别好的点同一个框架同时支撑ArduCopter、ArduPlane、Rover、ArduSub等多个载具类型。也就是说只要把框架吃透之后看其他载具的代码会有一种“复读机”般的熟悉感唯一不同的是每个vehicle目录下那些个性化实现。这也是我选择先从框架整理下手的原因。2. 分而治之ArduPilot顶层目录与代码库地图2.1 顶层目录ArduSub在仓库里的位置先上一张ArduPilot仓库的顶层结构以我常用的release分支为例ardupilot/ ├── ArduSub/ # 水下机器人固件主目录 ├── ArduPlane/ # 固定翼 ├── ArduCopter/ # 多旋翼 ├── Rover/ # 无人车 ├── AntennaTracker/ # 天线跟踪器 ├── libraries/ # 所有载具共享的核心库 ├── modules/ # 外部依赖mavlink、lua等 ├── Tools/ # 编译烧录、日志分析等工具 ├── mk/ # 传统Makefile构建脚本 └── ...很多新手会误以为ArduSub是一套完全独立的代码其实它就躺在ArduPilot仓库的ArduSub/目录里。这个目录和ArduPlane/、ArduCopter/属于同一层级区别在于它实现的是“水下潜航器”这个载具类型。目录内部通常能找到类似config.h、Parameters.cpp、mode.cpp、motors.cpp、control.cpp这一系列文件不同版本文件名可能稍有变化但职责基本是一一对应的。libraries/才是整个框架的重头戏。像传感器驱动、数学库、任务调度、参数系统、MAVLink通信、日志系统这些与“具体载具”无关的能力全部放在这里。你可以把ArduSub/当成一个“使用框架的客户”把libraries/当成“框架本身”。这样理解之后你在读ArduSub代码时遇到不认识的功能第一反应就应该是去libraries/里找对应的库而不是在ArduSub目录里死磕。2.2 libraries库地图这些AP_xx各管什么活进入libraries/后你会看到一堆以AP_、AC_开头的目录命名基本反映了功能分类。我列一下ArduSub学习过程中最常打交道的部分库目录职责AP_Math四元数、旋转矩阵、向量、PID类等基础数学工具AP_InertialSensor陀螺仪、加速度计的读取、滤波与标定AP_Baro / AP_Depth气压计、深度计水下常用压力传感器AP_Compass磁罗盘校准与航向计算AP_GPS / AP_RangeFinderGPS定位、激光/声呐测距AP_Scheduler多任务调度决定各个任务多少Hz运行AP_Param全局参数系统运行时可修改、掉电保存AP_Logger飞行/航行日志记录GCS_MAVLink地面站通信负责收发MAVLink消息AP_Motors / AP_Motors_6DOF电机输出与混合控制AC_AttitudeControl姿态级联控制器这些库并不是每个都需要第一时间读完。AP_Math里的四元数公式你可以在需要时再翻AP_Logger的存储格式也可以遇到再查。但AP_Scheduler和AP_Param建议优先看因为它们决定了整个程序的“运行节奏”和“配置方法”。2.3 先抓住依赖方向别让库与库的关系淹没你ArduPilot头文件之间的引用关系很密如果从某个库的内部往里钻很容易被#include的层层套娃搞到头大。我的经验是先建立一个粗糙的依赖方向底层是最靠近硬件的HAL、传感器驱动、基础数学上层是控制、导航、通信、文件系统vehicle目录只负责把上层这些库“组装”成具体可用的控制逻辑。读代码时建议画一条主干HAL - 传感器数据 - 状态估计EKF- 控制指令 - 电机混合器 - PWM输出。其他所有库比如日志、参数、地面站通信都算是挂在主干上的分支。先记住主干再看分支心理压力会小很多。3. 硬核基石HAL与Scheduler如何撑起整个飞控3.1 HAL同一套逻辑跑在不同硬件的“翻译层”HAL的全称是Hardware Abstraction Layer在ArduPilot里这层抽象做得相当彻底。你的代码里写hal.rcout-write()时根本不需要关心底层是Pixhawk的原生PWM引脚还是SITL仿真环境里的虚拟通道——这些差异全部被HAL封装住了。实际项目中这个抽象带来的好处非常直接。同一套ArduSub固件可以直接编译到真实飞控板上也可以编译成本机跑SITLSoftware In The Loop模拟器。我经常用SITL在电脑上先调好一堆参数确认逻辑没有问题之后再刷进真实硬件。没有HAL这层“翻译官”这个工作流是不可能实现的。想移植ArduSub到自己的板子时重点就是新写一个HAL实现。很多做开源硬件的朋友拿到ArduSub第一反应是去改控制算法其实硬件兼容才是更底层的工程控制算法反而可以原样保留。所以学框架时我建议先翻一遍HAL相关的目录知道板卡要提供哪些能力比如串口、PWM输出、模拟量采集、定时器这些不需要看得特别细但要有整体印象。3.2 Scheduler决定“谁先跑、多久跑一次”的调度表ArduPilot不是用一个传统的大while循环顺序执行所有逻辑而是靠调度器管理一堆任务。调度表本质上一个数组每一项对应一个任务函数并声明这个任务希望多久执行一次、最长允许耗时多少。我简化一下大概是这种感觉static const AP_Scheduler::Task tasks[] { { update_sensors, 400, 2500 }, // 400Hz最大耗时才2500us { update_attitude, 400, 3000 }, // 姿态环400Hz { update_nav, 100, 5000 }, // 导航环100Hz { update_gcs, 50, 9000 }, // 地面站通信50Hz { update_logging, 10, 2000 }, // 日志10Hz };上面的频率和耗时不代表某个特定版本的准确值只是帮你看懂这个结构。数值本身不是关键关键是这种设计思路高频率、硬实时的控制任务比如姿态环和低频率、非实时的任务比如日志写入被放在了同一个框架里但调度器会保证慢任务不会阻塞快任务。为什么要设计成一个调度表而不是直接到处用定时器中断最大的原因是多样性和可维护性。不同载具对任务频率的要求不同固定翼、旋翼、ROV各有各的节奏。如果每个功能模块都自己开一个中断系统复杂度会上天。集中调度之后你只看一张表就能知道整个系统在忙什么某个任务跑飞了也能通过地面站的“任务超时统计”立刻看到问题。3.3 上电那一刻启动流程里的四步启动阶段我简化成四个阶段理解了它们排查问题会快很多板级初始化HAL初始化底层外设比如时钟、串口、PWM、I2C总线。参数加载从FRAM/文件系统把上次保存的AP_Param参数读进内存。传感器准备与校准等待IMU、罗盘、深度计等传感器健康首次使用通常需要做加速度计校准、罗盘校准。进入调度循环一切就绪后启动Scheduler按任务表周期性运行各功能。真机调试时我碰到过两次“上电后推进器毫无反应”的情况排查到最后都不是代码逻辑错误而是启动阶段卡在传感器等待上一次是深度计I2C地址不对一次是磁罗计一直没收到数据。ArduSub启动时会在串口控制台打印当前等待项这个信息比任何调试器都直接。所以我后来排查启动类问题第一件事就是看串口输出而不是直接上调试器打断点。4. 一条数据链从传感器到推进器怎么走完全程4.1 传感层原始测量到状态估计你可以把ArduSub的运行想象成一个人在水下操作ROV先“感知”当前姿态、深度、位置再根据操作指令“决定”怎么动最后“执行”推进器输出。感知层的第一步就是读取传感器数据。ArduSub里最常见的传感器组合包括IMU加速度计陀螺仪、磁罗盘、深度计压力传感器、测距声呐以及高端ROV/AUV上选配的DVL多普勒测速仪或外部USBL定位系统。这些数据不会直接被控制环使用而是先进入状态估计模块——ArduPilot里通常指EKF3——做数据融合。EKF把高频但会漂移的IMU数据、低频但绝对参考意义的罗盘/深度/DVL数据综合起来输出一份平滑可靠的姿态、位置和速度估计值。水下环境有个特点值得单独拿出来说没有GPS或GPS非常弱位置观测主要靠DVL/声呐/USBL这些声学设备而ROV电机工作时会产生强烈磁场干扰。这些都会直接影响状态估计质量。所以ArduSub的参数表里跟EKF相关的选项非常关键很多人一开始不重视真到了水下位置漂得离谱才回头调EKF参数。4.2 控制层模式切换里藏着一颗“目标生成器”控制层的核心概念是Mode。ArduSub支持多种模式比如MANUAL、STABILIZE、DEPTH_HOLD、POS_HOLD、AUTO、GUIDED这些。每个模式本质上是一个“目标生成器”它接收操作手的输入和当前状态估计生成一个控制目标再交给底层的姿态/深度/位置控制器去执行。举个例子更直观你切到DEPTH_HOLD模式把摇杆推到某个深度模式代码会把这个深度作为目标值存下来控制环读取当前深度计估值和这个目标值做差生成一个期望的垂向加速度然后这个垂向加速度进一步转换成垂直推进器的推力大小。整个过程外面看着是“定深”实际上内部是一套非常经典的控制链路。如果你以前接触过ArduCopter会发现很多概念类似但ArduSub水下控制的物理特性和空中完全不同。空中无人机重力方向和推力方向都相对简单水下则要考虑浮力、水流扰动、电缆拉力、姿态与推进器的耦合。这也是为什么ArduSub的控制参数往往不能直接从ArduCopter参数表里照搬必须针对水下动力学重新调整。4.3 输出层混合矩阵与推进器方向控制层算出来的还是“期望力和力矩”但推进器是实物必须把期望转成每路电机的PWM。这一步在ArduSub里由电机混合逻辑完成核心是一个混合矩阵根据每个推进器的安装角度和位置把六个方向的期望力和期望力矩映射到每一路推进器的推力百分比。这里要特别提醒新手ArduSub里的推进器布局不是你脑子里默认的“前后左右上下”那么简单。很多ROV为了机动性会采用斜置矢量布局每个推进器的出力方向都是倾斜的。混合矩阵负责把这些倾斜推进器的分力合成为系统需要的运动比如“前进”可能需要四路推进器同时按不同比例输出。我调试过一个八推进器的ROV电机方向参数设错之后表现是“推前进摇杆ROV却斜着漂”。那一次排查让我真正理解了混控矩阵的意义。所以改机架或换推进器时一定要认真核对电机方向和安装角度参数这部分配错控制层调得再好也是白搭。5. 参数与MAVLink让你既爱又恨的两个复杂子系统5.1 AP_Param参数系统一个全局配置中心ArduPilot里几乎所有的配置都放在AP_Param参数系统里。启动时会从飞控自带的FRAM或文件系统中把这些参数加载到内存运行过程中你可以通过地面站修改并写回存储。很多新手会问为什么不用普通配置文件因为飞控对稳定性和写寿命有要求参数存储要支持频繁读取、低频写入还要在恶劣环境断电时尽量不损坏数据。参数系统使用起来非常“声明式”代码里通过类似下面的宏把参数注册到全局参数表const AP_Param::GroupInfo AP_Motors_Class::var_info[] { AP_GROUPINFO(HOVER_THR, 0, AP_Motors_Class, hover_throttle, 50.0f), AP_GROUPINFO(VECTORED, 1, AP_Motors_Class, vectored, 0), AP_GROUPINFO(PWM_MIN, 2, AP_Motors_Class, pwm_min, 1100), AP_GROUPINFO(PWM_MAX, 3, AP_Motors_Class, pwm_max, 1900), AP_GROUPINFO(DIRECTION, 4, AP_Motors_Class, direction, 1), ... };这段话是示意性质具体字段含义不同版本会有些出入。你可以看到每个参数都有一个字符串名字、一个组内索引、一个变量地址和一个默认值。编译时ArduPilot会对所有这些参数建立索引地面站通过MAVLink的PARAM协议就能列出、读取、修改它们。从学习角度我建议你至少弄清参数前缀的命名习惯INS_表示惯性传感器相关MOT_表示电机相关PILOT_表示遥控器相关EK3_表示EKF状态估计相关。看到一份陌生参数表时先看前缀就能猜出它大概属于哪个子系统。5.2 MAVLink地面站与飞控之间的“电话线”MAVLink是一个为无人机/机器人通信设计的轻量级消息协议ArduSub与地面站、遥控器转发器、其他外设之间主要靠它通信。启动后飞控会周期性发送心跳包地面站发现心跳后才知道“有一台设备在线”。随后飞控会按配置发送姿态、状态、位置、传感器原始值等消息同时接收地面站下发的遥控指令、模式切换、参数读写请求。对于开发者来说MAVLink最有吸引力的地方在于你完全可以用脚本语言去“偷听”和“发话”。比如用pymavlink连接SITL或真机只需要几十行代码就能拿到实时姿态from pymavlink import mavutil m mavutil.mavlink_connection(udp:127.0.0.1:14550) m.wait_heartbeat() while True: msg m.recv_match(typeATTITUDE, blockingTrue) roll msg.roll pitch msg.pitch yaw msg.yaw print(fr{roll:.2f} p{pitch:.2f} y{yaw:.2f})这段代码特别适合用来验证飞控是否在正常工作在ArduSub学完框架之后也很有价值。网络上经常有人问“ardupilot用什么工具管理”“python怎么读取ardupilot数据”其实答案就在MAVLink这层地面站和pymavlink都是在和MAVLink打交道。5.3 实际开发中常打交道的工具链ArduSub日常开发和学习最常见的一套工具链大概是这样的QGroundControl或Mission Planner地面站用来刷固件、调参、看实时状态、回放日志。SITL MAVProxy没有真机也能在电脑上仿真运行ArduSub配合pymavlink或地面站一起使用。pymavlink写脚本读取MAVLink消息、发指令、批量改参数。这套组合基本能覆盖从“入门阅读”到“真机调试”的整个流程。我刚接触ArduSub时很大程度上也是靠SITL和pymavlink把框架里的各个模块跑起来再用地面站图形界面把看不到的数据可视化出来才逐渐在脑里建立起完整的数据链路。6. ArduSub在共享框架下的水下个性电机混控与模式系统6.1 从ArduPlane到ArduSub代码怎么“长”成水下版很多资料会说ArduSub是从ArduPlane派生出来的这一点从代码结构里能看出些许痕迹但更重要的是理解派生背后的原因。ArduPilot的框架把所有载具共通的部分全部抽到了libraries层vehicle层只需要实现自己的控制逻辑和模式管理因此Blue Robotics选择基于ArduPilot做ArduSub本质上是在成熟的框架里“填入”了水下载具特有的内容。实际打开ArduSub目录你会发现它里面有很多跟水下需求强相关的实现推力分配、定深控制、位置保持、对水下机械臂的支持、额外的辅助PWM通道控制灯光与机械爪等。这些都可以看作是对共享框架的“个性化定制”。所以你问“ArduSub和ArduPilot是什么关系”我更喜欢这样回答ArduSub是ArduPilot框架在“水下机器人”这个场景下的一套完整应用。6.2 水下电机映射从6自由度指令到每个推进器的PWM前面提到混合矩阵在ArduSub里这个思路尤其重要。水下机器人有六个自由度前后Surge、左右Sway、上下Heave三个平移自由度以及横滚Roll、俯仰Pitch、偏航Yaw三个旋转自由度。控制器最终输出的是一个“6维期望力/力矩向量”混合器要解决的问题是现在有N个推进器每个推进器能产生一个推力矢量如何给这N个推进器分配推力让它们的合力刚好等于控制器期望的那个6维向量。这本质是一个线性代数问题把每个推进器在安装位置、方向上产生推力的模型拼成一个矩阵然后用这个矩阵对期望向量做求解。推进器少了但方向安排合理同样能覆盖所有需要的自由度推进器再多混控矩阵也会自动分配好各路的输出比例。真实代码里还包含很多工程细节比如电机正反转死区、PWM饱和限制、单电机最大输出限制等都是为了在物理世界“安全运行”不得不考虑的。6.3 我的一些低门槛上手建议学ArduSub或者ArdubPilot这类大型开源项目我最大的心得是“别贪”。不要把所有的库、所有的函数都通读一遍那不现实也没有必要。我建议按这个顺序推进先把SITL跑起来用MAVProxy或QGroundControl连接上亲眼看到ArduSub在仿真环境里能响应模式切换、定深、位置保持。这相当于给后面的源码阅读提供一个“活的参照物”。然后回到框架层面把HAL、Scheduler、AP_Param、GCS_MAVLink这几个核心库的大体结构过一遍。等脑子里有了数据流的主干再挑选你最关心的模块深入比如电机混控、深度控制或EKF参数调优。我在实际学习中发现最有效的办法是把框架图打印出来贴在显示器旁边每读完一个源文件就在图上对应位置写一句注释。这样读了两三周后整张图上就布满了自己的笔记ArduSub和ArduPilot在我脑子里也从“一堆抽象类”变成了“一个有清晰分工的团队”。下一次再遇到陌生问题时我至少知道该去哪一层找答案。
返回列表