Ardupilot移植经验分享(1)

前言

笔者写这篇文章的重点是分享移植方法。Ardupilot代码在不断的演化更新,一篇非常细致的讲解移植步骤的文章总会过时,只有掌握了方法才能随机应变。必要的移植步骤是有的,不过在此之前,笔者会讲解一些辅助性的内容,比如搭建高效阅读源码的环境,移植Ardupilot会遇到哪些困难。

如果你对Ardupilot很好奇,想深入研究一些东西,但是又畏惧其复杂庞大的代码工程,那么笔者强烈推荐你耐心阅读本文,相信一定会对你有所帮助。

背景

两年前接了个小项目,使用TI公司的单片机做飞控。这是学校的实验项目,主要是演示、推广之类的用途,要求不高。接触Ardupilot数年矣,一直想搞点大动作,正好借着这次机会,来一次真正的移植。

硬件平台为TM4C1294XL LaunchPad,软件平台为RT-Thread操作系统,基于Copter-3.5.7版本进行移植。
图片来源:TI使用手机
在这里插入图片描述
先放个截图,源码获取方式在文章结尾。
在这里插入图片描述

为什么写这篇文章

在10年前,四轴飞行器还是个冷门的东西,玩的人并不多,而接触过飞控开发的人就更少了。2013年全国大学生电子设计竞赛首次引入飞控类的赛题,越来越多的学生接触了飞控代码。许多开源飞控如雨后春笋般诞生,飞控开发的难度和门槛逐渐降低。

然而,历史悠久的Ardupilot的开发门槛却并未降低,个人感觉反而更高了。
图片来源:ardupilot.org
上图展示了Ardupilot的发展历史,2009年第一版硬件使用的是Ardunio Mega单片机,2013年使用了性能更高资源更丰富的Pixhawk(168 Mhz/256 KB RAM/2 MB Flash)。时至今日,Ardupilot支持众多的开源闭源硬件。
图片来源:ardupilot.org
Ardupilot能支持众多硬件平台,归功于其优秀的代码框架。业务、算法与驱动相分离,层次分明,从而具有强大的可扩展性。然而优秀的代码框架并不意味学习成本低,相反,需要花费一定的时间去了解它的层次和脉络。

学习成本高,导致Ardupilot令人望而生畏。笔者在决定移植Ardupilot之前,对其已有一定的了解,数年前还做过一次小移植(后面会细说)。纵使如此,也并无百分百的把握,是强烈的兴趣驱使我接下了这个项目。断断续续做了好几个月,经历了许多困难,中途都后悔了哈哈。

虽然困难,但Ardupilot是值得研究的。其包含的诸多功能,不仅仅可以用于飞行器。实际上,Ardupilot项目也不只有飞行器,其支持四种载具:

  • Copter(旋翼机)
  • Plane(固定翼)
  • Rover(小车)
  • Sub(潜艇)

随便挑个功能,下图是Ardupilot支持的无源导航(即无GPS信号时)方式,有3D摄像头,光流,UWB定位,外置摄像头捕捉等等。
图片来源:ardupilot.org
因此,笔者决定分享自己的移植经验,让更多对Ardupilot感兴趣的同学有所参考。

网上关于Ardupilot的代码分析和移植的文章有很多,笔者这篇文章的重点是分享方法。笔者移植的是Copter-3.5.7,而现在最新版本是Copter-4.0.3,代码肯定发生了很多变化。细节是死的,方法是活的。掌握了方法,才能灵活应变。

分析Ardupilot代码框架也是必不可少的,不过笔者不会讲解每一处需要修改的代码,比如关于外设驱动移植,会挑一些有代表性的来讲解。除此之外,笔者会详细地分享一些有助于成功移植的方法,比如如何加速源码的下载,命令行编译,搭建高效阅读源码的环境。

移植Ardupilot的方法有两种

Ardupilot的代码框架
图片来源:ardupilot.org
ArduPilot代码顶层目录
在这里插入图片描述

Ardupilot代码分为5个部分:

  • 载具代码:APMrover2, ArduCopter, AntennaTracker, ArduPlane, ArduSub。对于飞行器项目来说就是飞控代码,是业务层的代码,比如飞行器初始化,飞行模式控制。
  • 共享库:libraries中除以AP_HAL开头的所有子目录。包含传感器驱动,姿态位置估算与控制。
  • 硬件抽象层(HAL):libraries中以AP_HAL开头子目录,如AP_HAL, AP_HAL_PX4, AP_HAL_Linux。
  • 工具目录:Tools。
  • 外部支持代码:modules。具体硬件平台的代码,如操作系统,中间件。

要重点讲的是HAL。libraries/ AP_HAL中有一个顶级AP_HAL,用于定义其余代码与特定板功能之间的接口,然后每种硬件平台都有一个AP_HAL_XXX子目录,例如,用于基于AVR的板的AP_HAL_AVR,用于Pixhawk板的AP_HAL_PX4和用于基于Linux板的AP_HAL_Linux。
在这里插入图片描述
换句话说,AP_HAL定义板级驱动接口,业务代码使用这些接口,而AP_HAL_XXX对应的硬件代码实现这些接口。这样一来,业务层不依赖于硬件代码,硬件代码也不依赖业务层,它们都依赖于HAL定义的接口。正是这种面向接口的设计模式,使得ArduPilot可以被移植到多种不同的平台。

至于AP_HAL中的接口具体是什么样子的,在之后讲驱动代码时再说。这里介绍Ardupilot的代码构架的目的是引出移植的两个方法。

每种硬件平台都有一个AP_HAL_XXX子目录,而我们要移植到新平台的话,就是添加一个AP_HAL_XXX,比如AP_HAL_TI,并且实现所有HAL接口。这就是第一种方法,从底层适配。

与之相对应的是提取应用层代码,将飞控业务和算法代码提取出来,并做适当的修改,以适配自己的硬件。

下面详细分析下两种方法的特点和优劣。

底层适配

底层适配,即实现所有HAL接口。libraries/AP_HAL目录中的文件如下:
在这里插入图片描述
分为如下几类:

  • 纲领型文件:AP_HAL.h, HAL.h, HAL.cpp,定义命名空间和HAL类,被其他文件所引用。
  • 通信类驱动:CAN.h, Device.h, UARTDriver.h, SPIDevice.h,等等,都是些通信类外设的驱动。
  • RC类驱动:RCInput.h, RCOutput.h,负责接收遥控信号,输出电机信号。
  • 操作系统接口:Semaphores.h, system.h,提供操作系统相关的接口,如线程,信号量,睡眠等功能。
  • 其他驱动:AnalogIn.h, GPIO.h。

下图最左边的分支,就是笔者TI板子,使用RT-Thread操作系统来提供AP_HAL所需要的线程等功能。
在这里插入图片描述
不过笔者偷了个懒,并没有创建新的AP_HAL_TiLaunchPad,而是在AP_HAL_PX4上修改,偷梁换柱。如下图,AP_HAL_PX4中的文件都被笔者修改了,比如SPIDevice.cpp中的do_transfer函数,改成了调用RT-Thread的spi接口。
在这里插入图片描述

提取应用层代码

在这里插入图片描述
上图是飞控框架图的一小部分,此为飞控代码以及与具体硬件平台无关的共享库,可提取出包含如下功能的代码:

  • 传感器驱动
  • 姿态位置解算与控制
  • 飞行模式控制
  • 导航(路径规划)

还可以更简单些,只提取姿态解算与控制,不管Ardupilot用了什么传感器,而是使用自己的传感器与电机驱动(比如pixhawk板子上使用的是L3GD20H和LSM303D,而你自己用mpu9250),可以实现最基础的飞行功能。笔者所说的数年前的小移植,就是这么干的,当时移植的是ArduCopter-2.9.1b。下图是移植出的姿态解算与控制代码的工程,提取并且转换成了C语言代码。
在这里插入图片描述
以fast_loop函数为例,给大家看下移植前后的区别。

原始代码

// Main loop - 100hz
static void fast_loop()
{
    // IMU DCM Algorithm
    // --------------------
    read_AHRS();

    // reads all of the necessary trig functions for cameras, throttle, etc.
    // --------------------------------------------------------------------
    update_trig();

    // run low level rate controllers that only require IMU data
    run_rate_controllers();

    // write out the servo PWM values
    // ------------------------------
    set_servos_4();

    // Inertial Nav
    // --------------------
    read_inertia();

    // optical flow
    // --------------------
#if OPTFLOW == ENABLED
    if(g.optflow_enabled) {
        update_optical_flow();
    }
#endif  // OPTFLOW == ENABLED

    // Read radio and 3-position switch on radio
    // -----------------------------------------
    read_radio();
    read_control_switch();

    // custom code/exceptions for flight modes
    // ---------------------------------------
    update_yaw_mode();
    update_roll_pitch_mode();

    // update targets to rate controllers
    update_rate_contoller_targets();

    // agmatthews - USERHOOKS
#ifdef USERHOOK_FASTLOOP
    USERHOOK_FASTLOOP
#endif

}

移植后

void arducopter_fast_loop(void)
{
	// IMU DCM Algorithm
	// --------------------
	read_AHRS();

	// custom code/exceptions for flight modes
	// ---------------------------------------
	update_yaw_mode();
	update_roll_pitch_mode();

	// update targets to rate controllers
	update_rate_contoller_targets();

	//control the motor
	run_rate_controllers();
	set_servos_4();
}

移植后的arducopter_fast_loop分为如下几个部分:

  • read_AHRS:从传感器读取角速度与加速度,使用DCM(方向余弦矩阵)计算姿态角。
  • update_yaw_mode和update_roll_pitch_mode:根据飞行模式,当前姿态,目标姿态等来计算目标角速度(大地坐标系)。
  • update_rate_contoller_targets:目标角速度转换(大地坐标系–>机体坐标系)。
  • run_rate_controllers:根据目标角速度计算并输出电机信号。

可以看出,这实现的是非常基础的飞行功能。其实笔者这里讲述几个函数的作用,并不是真想展开来讲飞控算法,而仅仅是想让大家体会一下:这种移植需要对飞控算法进行深入的了解

两种方法对比

底层适配需要深入了解HAL框架,了解每个HAL接口的功能并且实现它。这种方法,基本不用在应用层做改动,好处是:不需要深入研究应用层的代码,比如各种飞行模式,姿态解算与控制算法。再说直白点,就算你对飞控算法一无所知,只要你有扎实的嵌入式软件功底,就能完成移植。什么,软件功底也不是很厚实?不用担心,这正是我写这篇文章的目的啊。

相反,提取应用层代码则需要对飞控算法有更多的了解,并且需要深入分析Ardupilot的应用层代码,提取出你所需要的模块。好处也是与底层适配相反的,就是不需要了解HAL啦。你可以随意的修改应用层代码,包括调用驱动的地方。

应该选择哪一种,要看应用场景。

如果你只是想使用Ardupilot的一个小模块,比如姿态解算。Ardupilot将陀螺仪,加速度计,磁力计,GPS的数据进行融合,从而计算出稳定准确的姿态数据。把这些代码提取出来,应用到其他的项目中,是不错的想法。

如果你是想做一款飞行器,并且想使用到Ardupilot的诸多功能,如各种各样的飞行模式(定点悬停,路径规划),使用UWB进行无源导航,物体追踪,那就不宜提取应用层代码,而是做底层适配。否则,你可能要提取太多的代码,做太多的改动。

准备阅读源码

在开始移植之前,有两个必要的步骤:

  1. 熟练使用一款Ardupilot飞控,比如Pixhawk。
  2. 搭建源码的阅读、编译、调试环境。

熟练的意思是,会配置、校准飞控,装到四轴上,能操控它进行基础的飞行。

虽然我们的最终目标是将飞控代码移植到新的平台上,不过搭建参考平台的源码环境非常重要。得先在这个参考平台上把源码读懂,当遇到疑问时可稍微修改下源码并调试验证。笔者的参考平台是第一代Pixhawk,如下图:

图片来源:ardupilot.org

阅读官方开发者wiki

官方开发者wiki是最应该仔细阅读的资料,建议先阅读下面两个章节,其他的章节可根据需求选读。Building这章,只看自己使用的平台即可,比如Linux/Ubuntu

  • Building the code
  • Learning the code

选择编译平台

ardupilot支持以下平台,详见Building the code

  • Linux/Ubuntu
  • Windows(Cygwin或WSL)
  • MacOSX

笔者当初移植时,是在VMWare虚拟机中跑Ubuntu。在虚拟机中跑系统,最大的问题就是卡顿。这次写文章,笔者试了下WSL。WSL的全称是Windows10 Subsystem for Linux,就是在Windows10下直接跑Linux,不需要虚拟机。Setting up the Build Environment on Windows10 using WSL1 or WSL2推荐使用WSL2,不过笔者的Windows10版本不支持,因此使用了WSL1。

笔者使用的WSL1,也是跑ubuntu,大部分操作没有区别,有区别时会说明。WSL1的安装,可以参考Win10安装Ubuntu子系统及图形化界面详细教程

下载编译源代码

关于下载编译的步骤,Setting up the Build Environment (Linux/Ubuntu)说得非常清楚,ardupilot工程里面还包含了安装编译环境的脚本(Tools/environment_install/install-prereqs-ubuntu.sh,仅限于在Debian/Ubuntu系统中使用),笔者整理如下:

友情提示,这是下载并编译官方最新的源码,而并非是笔者移植所基于的3.5.7版本源码。

下载源码

git clone https://github.com/ArduPilot/ardupilot.git
cd ardupilot
git submodule update --init --recursive

安装编译环境

# 当前目录为工程根目录,即ardupilot
Tools/environment_install/install-prereqs-ubuntu.sh -y

重启终端(wsl)或者重新登录(ubuntu),或者执行

. ~/.profile

编译

# Ardupilot使用了Waf来管理项目编译。Waf是类似于make的项目构建系统,有Linux开发经验的同学肯定对make很熟悉。
./waf configure --board Pixhawk1
./waf copter 

下载源码、安装编译环境和执行编译,总共10来行命令。大家实际操作时,可能会遇到几个问题

切换版本的正确方式

git clone https://github.com/ArduPilot/ardupilot.git

上述命令下载的源码,默认处在master分支,若想切换到指定版本,需要使用git checkout来切换。比如切换到3.5.7版本并拉出一个新的分支,以便在其上做修改并且提交。

git checkout -b Copter-3.5-test Copter-3.5.7
# 切换了分支后,需要同步对子模块的引用。
git submodule update --init --recursive

ardupilot不仅使用git来管理自己的源码,还使用git的submodule功能来跟踪其所依赖的子模块。对于不熟悉git的同学,切不可在github下载源码的压缩包,这样无法下载其子模块,也无法进行编译。

还有一点要注意,之前提到的install-prereqs-ubuntu.sh,在3.5.7版本时是不存在的。可以在master分支上运行它来安装环境,安装完后再切换分支

编译老版本

Tools/environment_install/install-prereqs-ubuntu.sh是官方为Debian/Ubuntu系统提供的一键安装编译调试环境的脚本,有兴趣的同学可以阅读下这个脚本,其内容非常简单:下载一些python库和编译器,下载的编译器为gcc-arm-none-eabi-6-2017-q2-update,位于/opt目录中。

用其编译最新源码是没问题的,若编译老版本代码,比如3.5.7,则会报错,比如笔者遇到的错误:

/opt/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1/bits/basic_string.h: In function 'float std::__cxx11::stof(const string&, size_t*)':
/opt/gcc-arm-none-eabi-6-2017-q2-update/arm-none-eabi/include/c++/6.3.1/bits/basic_string.h:5454:31: error: 'strtof' is not a member of 'std'
   { return __gnu_cxx::__stoa(&std::strtof, "stof", __str.c_str(), __idx); }
                               ^~~
compilation terminated due to -Wfatal-errors.
make[3]: *** [src/platforms/nuttx/CMakeFiles/platforms__nuttx.dir/px4_nuttx_impl.cpp.obj] Error 1
make[2]: *** [src/platforms/nuttx/CMakeFiles/platforms__nuttx.dir/all] Error 2
make[1]: *** [src/firmware/nuttx/CMakeFiles/build_firmware_px4fmu-v2.dir/rule] Error 2
make: *** [build_firmware_px4fmu-v2] Error 2

需要替换成老的编译器,有如下几步:

  1. 手动下载编译器。
  2. 执行Tools/environment_install/install-prereqs-ubuntu.sh。
  3. 修改PATH配置:将PATH变量中的编译器路径改为老版本编译器的路径。
  4. 配置ubuntu以支持运行32位程序。
  5. 切换到目标分支,编译

手动下载编译器

下载老版本编译器gcc-arm-none-eabi-4_9-2015q3,密码vyqb

这链接还包含了最新的编译器,这将两个编译器都安装在/opt目录。之后执行install-prereqs-ubuntu.sh时,其检测到新编译器已存在,就不会再去官方网站下载(从官网下载速度极慢)。

以gcc-arm-none-eabi-4_9-2015q3-20150921-linux.tar.bz2为例,将其放至/opt目录下,并执行:

cd /opt
sudo tar xvf gcc-arm-none-eabi-4_9-2015q3-20150921-linux.tar.bz2
sudo rm gcc-arm-none-eabi-4_9-2015q3-20150921-linux.tar.bz2

修改PATH配置

修改~/.profile,在最下面找到PATH变量,将gcc-arm-none-eabi-6-2017-q2-update改为gcc-arm-none-eabi-4_9-2015q3。
在这里插入图片描述
重启终端(wsl)或者重新登录(ubuntu)以重新加载~/.profile,查看PATH变量中,看其中的编译器是否已改为老编译器。
在这里插入图片描述

配置ubuntu以支持运行32位程序

新编译器是64位的,老的是32位。默认情况下,64位ubuntu不支持运行32位程序,会报如下错误:

$ arm-none-eabi-gcc
-bash: /opt/gcc-arm-none-eabi-4_9-2015q3/bin/arm-none-eabi-gcc: cannot execute binary file: Exec format error

若是跑ubuntu,则执行如下操作即可:

sudo dpkg --add-architecture i386
sudo apt update
sudo apt install libc6:i386 libncurses5:i386 libstdc++6:i386

若是跑WSL(据说WSL2是没问题的,与ubuntu的操作一致),由于其内核就不支持32位程序,只能曲线救国,使用qemu来运行编译器:

sudo apt install qemu-user-static
sudo update-binfmts --install i386 /usr/bin/qemu-i386-static --magic '\x7fELF\x01\x01\x01\x03\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x03\x00\x01\x00\x00\x00' --mask '\xff\xff\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xff\xff\xff\xff\xff\xf8\xff\xff\xff\xff\xff\xff\xff'
sudo service binfmt-support start

使用qemu的缺点是,编译速度比虚拟机还慢。。。。。。也许确实该用WSL2,笔者这次算是跳坑里了。

现在就可以运行老编译器了,no input files是因为我们没传源码文件路径,这个不用管。

$ arm-none-eabi-gcc
arm-none-eabi-gcc: fatal error: no input files
compilation terminated.

如果关闭了所有WSL窗口,貌似WSL就关机了。再次启动WSL时,需要再次执行:

sudo service binfmt-support start

老版本的编译命令

之前的编译命令第一步如下,这是在配置硬件类型。

./waf configure --board Pixhawk1

笔者在编译3.5.7版本时,选择的是px4-v2。其实Pixhawk1和px4-v2对应的都是第1代Pixhawk,是一样的硬件,不同的是底层的软件平台。

  • px4-v2:基于PX4Firmware和Nuttx,这不是Ardupilot开发的,而是Pixhawk硬件厂商开发。
  • Pixhawk1:Ardupilot团队使用ChibiOS重新开发了底层软件。

笔者的编译命令:

./waf configure --board px4-v2
./waf copter 

下载太慢或者失败

本节原来是放在此处写的,结果发现内容较多,导致篇幅太大,故后续单独出一篇文章:Ardupilot移植经验分享(1)–加速下载ardupilot工程

建立源码阅读环境

多年前,一个学弟研究Ardupilot的方法让我印象深刻,他将整个工程代码打印在纸上以进行阅读。

那时使用的是Ardunio版飞控,apm2.5,用Ardunio官方IDE来编译固件。IDE长这个样子:
图片来源:ardupilot.org
这个IDE的代码浏览功能非常简陋,连函数跳转功能都没有。用它来学习庞大的Ardupilot代码简直是恶梦。对于习惯了Keil和IAR的同学来说,可能并不了解其他好用的代码浏览工具,这就是那个学弟打印出来看的原因。

其实Ardupilot开发团队肯定也不是用Ardunio IDE来开发,顶多是当成编译工具,肯定各有各自熟悉的开发利器,Editors and IDEs推荐了一些IDE。

笔者使用Eclipse阅读ardupilot源码,之后移植时用的是TI公司的Code Composer Studio,它也是基于Eclipse开发的。如果你并不打算使用Eclipse,“创建Eclipse工程”一节也值得一看,笔者将展示如何创建出与编译系统匹配的IDE工程,此方法是通用的,并不仅限于Eclipse

使用Eclipse

笔者之所以钟爱Eclipse,是因为它的代码编译和浏览功能很强大,可以极大的提高开发效率,也有助于高效地阅读一个全新的并且庞大的工程。

简单介绍几个功能。

全貌

  • 左侧是项目浏览器视图,主要提供文件列表功能。
  • 右侧是大纲视图,显示当前文件中的符号表。
  • 中间是代码编辑器视图。

在这里插入图片描述

跳转到定义

多种方法触发:

  • Ctrl+鼠标左击
  • F3
  • 通过右键菜单

必备功能啊,不太好

选中符号高亮显示

当前光标在current_loc上(截屏捕捉到光标),所有的current_loc都高亮显示了。此功能有助于阅读复杂流程时关注某个变量或函数的访问情况。
在这里插入图片描述

显示函数调用栈

要想知道上图中的read_inertia函数被谁调用了。

  • 右键菜单
  • Ctrl+Alt+H

在这里插入图片描述
结果如下,其被fast_loop调用了。
在这里插入图片描述
这就完了吗?当然不是,点击fast_loop左边的小三角,可查看fast_loop被谁调用了。此过程可反复进行,直到根节点。
最上层的是Ardupilot_main,找程序入口是不是超简单。
在这里插入图片描述

快速导航

可使用大纲视图在当前文件中进行导航,跳转到某个函数或变量。不过还有更快的方法,如果你记得目标符号的名称,或者记得一部分,都可以使用弹出式大纲(Ctrl+O)快速导航。
在这里插入图片描述
使用Ctrl+Shift+T可在整个项目空间中导航。
在这里插入图片描述
想快速查找文件?没问题,Ctrl+Shift+R
在这里插入图片描述

快速重命名

如果对某个函数或者变量的名称不满意,但是它被多处引用了。没关系,Shift+Alt+R就触发重构功能,只需要修改一处,其他地方自动更新。
在这里插入图片描述

记不住快捷键?

刚才在介绍功能时,有的仅给出了快捷键的触发方式。不必担心,所有的功能都有相应的菜单。比如说各种导航功能,都可以通过右键菜单或者菜单栏中的“导航”找到。

创建Eclipse工程

笔者还是以3.5.7版本为便进行讲解,方法是通用的

选择IDE是一个问题,如何使用IDE来浏览ardupilot代码是另一个问题。Ardupilot工程不像大家平时用的Keil工程。如果你拿到一个可以编译的Keil工程,那么其所用到的代码自然就在工程配置之中。而Ardupilot使用Waf来编译,当我们选择了一个IDE后,需要自己来创建对应的工程。

笔者分享自己创建Eclipse工程的方法,从Waf提供的编译信息中提取相关信息以构建Eclipse工程,而且这是由代码自动完成。

为了方便说明,再次将ardupilot框架图放出来:
图片来源:ardupilot.org
笔者要阅读的是中间的部分,即FlightCode,包含:

  • Vehicle specific flight code(对应Arducopter)
  • Shared Libraries(对应AP_HAL,AP_HAL_PX4)
  • Hardware Abstraction Layer(对应libraries中的其他文件)
.
├── ArduCopter
└── libraries
            ├──AP_HAL
            ├──AP_HAL_PX4
            └──others

需要获取哪些信息

我们的目标是创建一个工程,并将这些代码包含在工程的文件列表之类。ArduCopter和libraries目录中的所有文件并不都是需要的,有的并没有参与编译,所以我们需要知道哪些文件参与了编译。不仅如此,还需要获取到编译时用到的:

  • 宏定义
  • 头文件路径

如下函数的行为受FRAME_CONFIG(机架类型配置)所控制,FRAME_CONFIG选择某一分支的代码参与编译。Eclipse有一项非常好用的功能,那就是如下图所示,其将宏未选中的代码灰化,使得用户可专注于选中的代码
在这里插入图片描述
当然,这个前提是,得在Eclipse工程中配置相关的宏
在这里插入图片描述
应用层代码(尤其是AP_HAL_PX4)总是会调用底层代码的,比如下面的应用层代码,调用了底层的hrt_call_after函数,图中还有一个子窗口,显示出了该函数的声明和注释。
在这里插入图片描述
如果再深入一下,比如查看hrt_abstime:
在这里插入图片描述
虽然笔者并不需要了解底层的具体实现,但是像刚才一样了解底层接口是有必要的。若想在IDE中实现上述的查看,就必须让IDE能找到相关的头文件。应用层代码通过<drivers/drv_hrt.h>来引用头文件。
在这里插入图片描述
显然drivers目录并不在应用层代码的根目录,<drivers/drv_hrt.h>是一个相对路径,因此需要配置路径前缀
在这里插入图片描述

如何获取工程配置信息

从上节我们明确了目标,需要如下信息:

  • 代码文件列表
  • 宏定义
  • 头文件路径

不仅我们需要这些信息,编译器更需要。ardupilot工程使用Waf来管理构建任务,所以,这些信息都在Waf的相关脚本文件之中,由其在调用编译器时进行传递。

不过笔者不了解Waf,也仅仅是在ardupilot中使用Waf,所以并不想花费额外的时间去深入了解它。笔者有一条以不变应万变的捷径,那就是从编译指令中提取

gcc -I. -Ilibraries -DHELLO library.cpp -c -o library.o
gcc -I. -Ilibraries -DHELLO main.cpp -c -o main.o
gcc library.o main.o -o hello_world

上面是我随便写2条编译命令和1条链接命令,对于单片机开发者来说,若无linux开发经验,可能不太熟悉这些指令。笔者简单介绍下几个参数的意义:

  • -I添加头文件路径,这个路径+include中的路径构成完整的路径。比如前面那个drv_hrt.h的路径是由/mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/PX4Firmware/src + rivers/drv_hrt.h。.表示当前路径。
  • -D添加宏定义。

ardupilot的指令远比上面要复杂,参数比上面要多,但是格式是一样的。只要我们想办法得到ardupilot的编译指令,那就可获得我们所需要的信息。

笔者在创建linux内核,RT-Thread之类的复杂系统的IDE工程时,是通过在命令中加入verbose之类的参数,从而让构建系统打印出详细的编译日志,从而提取相关信息。下图是RT-Thread的编译:
 RT-Thtread编译图

而ardupilot直接提供了相关文件,这是笔者在默认级别的日志中发现的:
在这里插入图片描述
compile_commands.json是用json格式存储的ardupilot应用层代码的编译指令,下图是其中一条,用于编译libraries/AP_NavEKF2/AP_NavEKF2_MagFusion.cpp。
在这里插入图片描述

浓缩一下:

{
  "directory": "/mnt/g/ardupilot/src/ardupilot/build/px4-v2",
  "file": "../../libraries/AP_NavEKF2/AP_NavEKF2_MagFusion.cpp",
  "command": "此处省略1千字"
}
  • directory:编译时的工作目录
  • file:代码文件路径,是相对于directory的路径。
  • command:编译指令,包含了头文件路径和宏定义的信息。

其实command和这条编译main.cpp的指令是一样的,只不过它有更多的宏定义和头文件路径参数,另外,还有些别的用于控制编译行为的参数。

gcc -I. -Ilibraries -DHELLO main.cpp -c -o main.o

笔者来拆分下这条超长的command指令给大家看。

编译器

/opt/gcc-arm-none-eabi-4_9-2015q3/bin/arm-none-eabi-g++

宏定义

-DCONFIG_ARCH_BOARD_PX4FMU_V2
-D__STDC_FORMAT_MACROS
-D__PX4_NUTTX
-D__DF_NUTTX 
-DCONFIG_WCHAR_BUILTIN
-D__CUSTOM_FILE_IO__ 
-DCONFIG_HAL_BOARD=HAL_BOARD_PX4
-DHAVE_OCLOEXEC=0
-DHAVE_STD_NULLPTR_T=0
-DUAVCAN_CPP_VERSION=UAVCAN_CPP03
-DUAVCAN_NO_ASSERTIONS=1
-DUAVCAN_NULLPTR=nullptr 
'-DSKETCHBOOK=\"/mnt/g/ardupilot/src/ardupilot\"'

头文件路径

-Include visibility.h 
-Include ap_config.h
-Ilibraries
-Ilibraries/GCS_MAVLink
-Imodules/uavcan/libuavcan/include/dsdlc_generated
-I.
-I../../libraries
-I../../libraries/AP_Common/missing
-I../../modules/uavcan/libuavcan/include
-I../../modules/PX4Firmware/src
-Imodules/PX4Firmware
-Imodules/PX4Firmware/src
-I../../modules/PX4Firmware/src/modules
-I../../modules/PX4Firmware/src/include
-I../../modules/PX4Firmware/src/lib
-I../../modules/PX4Firmware/src/platforms
-I../../modules/PX4Firmware/src/drivers/boards/px4fmu-v2
-Imodules/PX4Firmware/src/modules/px4_messages
-Imodules/PX4Firmware/src/modules
-I../../modules/PX4Firmware/mavlink/include/mavlink
-I../../modules/PX4Firmware/src/lib/DriverFramework/framework/include
-Isrc/lib/matrix
-I../../src/lib/matrix
-Imodules/PX4Firmware/px4fmu-v2/NuttX/nuttx-export/include
-Imodules/PX4Firmware/px4fmu-v2/NuttX/nuttx-export/include/cxx
-Imodules/PX4Firmware/px4fmu-v2/NuttX/nuttx-export/arch/chip
-Imodules/PX4Firmware/px4fmu-v2/NuttX/nuttx-export/arch/common 

其他的编译参数,这在移植时可能用到,不过目前,我们无需关心。

-g -fno-exceptions -fno-rtti -std=gnu++0x -fno-threadsafe-statics
-Wall -Werror -Wextra -Wno-sign-compare -Wfloat-equal -Wpointer-arith 
-Wmissing-declarations -Wno-unused-parameter -Werror=format-security 
-Werror=array-bounds -Wfatal-errors -Werror=unused-variable 
-Werror=reorder -Werror=uninitialized -Werror=init-self -Wframe-larger-than=1024 
-Werror=unused-but-set-variable -Wformat=1 -Wdouble-promotion -Werror=double-promotion 
-Wno-missing-field-initializers -Os -fno-strict-aliasing -fomit-frame-pointer 
-funsafe-math-optimizations -ffunction-sections -fdata-sections 
-fno-strength-reduce -fno-builtin-printf -fvisibility=hidden
-mcpu=cortex-m4 -mthumb -march=armv7e-m -mfpu=fpv4-sp-d16 -mfloat-abi=hard 
-nodefaultlibs -nostdlib -std=gnu++11 -fdata-sections -ffunction-sections -fno-exceptions 
-fsigned-char -Wall -Wextra -Wformat -Wshadow -Wpointer-arith -Wcast-align -Wundef 
-Wno-unused-parameter -Wno-missing-field-initializers -Wno-reorder -Wno-redundant-decls 
-Wno-unknown-pragmas -Werror=format-security -Werror=array-bounds -Werror=uninitialized 
-Werror=init-self -Werror=switch -Wfatal-errors -Werror=unused-but-set-variable -Wno-error=cast-align 
-Wlogical-op -Wframe-larger-than=1300 -fsingle-precision-constant -Wno-error=double-promotion 
-Wno-error=missing-declarations -Wno-error=float-equal -Wno-error=undef -Wno-error=cpp

代码文件

../../libraries/AP_NavEKF2/AP_NavEKF2_MagFusion.cpp 

编译成.o

-c -o/mnt/g/ardupilot/src/ardupilot/build/px4-v2/libraries/AP_NavEKF2/AP_NavEKF2_MagFusion.cpp.0.o

拆分完后再看,是不是非常清晰了。宏定义,头文件路径,代码路径,都有了。当然,这只是一条编译指令。我们需要解析compile_commands.json中的所有几百条指令。

补充个注意事项,前面在说编译命令时,使用的是:

./waf copter

其不仅编译出多旋翼(四轴)的固件,还会编译直升机的。这两个固件有一个不同的宏定义,即上文提到的FRAME_CONFIG。

~/ardupilot/build/px4-v2/bin$ tree
.
├── arducopter
├── arducopter-heli
├── arducopter-heli.px4
└── arducopter.px4
  • 四轴:FRAME_CONFIG=MULTICOPTER_FRAME
  • 直升机:FRAME_CONFIG=HELI_FRAME

笔者试的情况是,其先编译四轴,再编译直升机,最终的compile_commands.json中留的是直升机的编译指令。如果你需要四轴的,使用下面这条指令,仅编译四轴:

./waf --targets bin/arducopter

自动化创建工程

由于文件众多,一个个去手工解析是不现实的。我们可以写个程序来自动提取这些信息,笔者使用Java来解析:
在这里插入图片描述
解析命令时,使用正则表达式将非常方便,比如下面的正则可将编译命令拆分成几个部分,以便进一步解析。

/**
   * used to parse compile command
    * 
    * for example(ignore some parameter):
    * 		/opt/gcc-arm-none-eabi-4_9-2015q3/bin/arm-none-eabi-g++ -DCONFIG_ARCH_BOARD_PX4FMU_V2
    * 		-include visibility.h -mcpu=cortex-m4 -mthumb -march=armv7e-m
    * 		-I../../modules/PX4Firmware/src/lib/DriverFramework/framework/include 
    * 		../../libraries/AP_NavEKF2/AP_NavEKF2_MagFusion.cpp 
    * 		-c -o/home/dongbowen/work/ardupilot/ardupilot_gitee/ardupilot/build/px4-v2/libraries/AP_NavEKF2/AP_NavEKF2_MagFusion.cpp.0.o
    * 
    *  group(1) is the command which don't include "-c -o..."
    *  group(2) is the compiler:  /opt/gcc-arm-none-eabi-4_9-2015q3/bin/arm-none-
    *  group(3) is the parameters 
    */
   private static final Pattern COMPILE_CMD_PATTERN = 
           Pattern.compile("^\\s*((\\S+-)[^-\\s]+\\s+(.*))\\s+-c\\s+-o");

其实这几百条指令的宏定义和头文件路径基本是一致,笔者解析时得出他们的并集,并且打印出每条重复的次数(在方括号之中)。

头文件路径如下,大部分的重复次数是429,这正是代码文件的个数,即它们在每条编译指令中都有。不过也有一些重复次数小于429的,最少的次数是81。这说明了自动化解析的重要性。如果仅手工解析第一条编译指令,那就会漏掉一些在后续编译指令中才出现的条目

[0429] /mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/src
[0348] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/src/lib/matrix
[0429] /mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/src/lib
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/PX4Firmware/src/modules/px4_messages
[0429] /mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/src/drivers/boards/px4fmu-v2
[0081] /mnt/g/ardupilot/src/ardupilot/ArduCopter/src/lib/matrix
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/PX4Firmware/px4fmu-v2/NuttX/nuttx-export/include/cxx
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/PX4Firmware/px4fmu-v2/NuttX/nuttx-export/include
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/libraries
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/PX4Firmware/src/modules
[0429] /mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/mavlink/include/mavlink
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/PX4Firmware/px4fmu-v2/NuttX/nuttx-export/arch/common
[0429] /mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/src/lib/DriverFramework/framework/include
[0429] /mnt/g/ardupilot/src/ardupilot/modules/uavcan/libuavcan/include
[0348] /mnt/g/ardupilot/src/ardupilot/src/lib/matrix
[0429] /mnt/g/ardupilot/src/ardupilot/libraries/AP_Common/missing
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/.
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/uavcan/libuavcan/include/dsdlc_generated
[0429] /mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/src/platforms
[0429] /mnt/g/ardupilot/src/ardupilot/libraries
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/PX4Firmware
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/PX4Firmware/px4fmu-v2/NuttX/nuttx-export/arch/chip
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/libraries/GCS_MAVLink
[0429] /mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/src/modules
[0429] /mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/src/include
[0081] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/ArduCopter/src/lib/matrix
[0429] /mnt/g/ardupilot/src/ardupilot/build/px4-v2/modules/PX4Firmware/src

宏定义的情况也一样,有少部分宏的重复次数小于429。

[0081] APM_BUILD_DIRECTORY=APM_BUILD_ArduCopter
[0429] __PX4_NUTTX
[0429] __CUSTOM_FILE_IO__
[0429] UAVCAN_NO_ASSERTIONS=1
[0429] __STDC_FORMAT_MACROS
[0429] UAVCAN_NULLPTR=nullptr
[0081] SKETCH="ArduCopter"
[0429] CONFIG_WCHAR_BUILTIN
[0081] SKETCHNAME="ArduCopter"
[0429] HAVE_OCLOEXEC=0
[0429] __DF_NUTTX
[0429] UAVCAN_CPP_VERSION=UAVCAN_CPP03
[0429] CONFIG_ARCH_BOARD_PX4FMU_V2
[0429] HAVE_STD_NULLPTR_T=0
[0065] FRAME_CONFIG=HELI_FRAME
[0429] SKETCHBOOK="/mnt/g/ardupilot/src/ardupilot"
[0429] CONFIG_HAL_BOARD=HAL_BOARD_PX4

由于大部分都是重复的,所以它们的并集并没有多大。由程序解析出宏定义和头文件路径后,手工配置到Eclipse工程中是可行的。不过,别忘记,还有那几百个文件呢,如果手工把它们一个个加进工程,那是很费事儿的,搞不好还会遗漏些文件。

笔者不仅用程序进行解析,还使用程序来创建Eclipse工程,这就是所谓的自动化创建工程

Eclipse的工程文件是以XML文本的形式存储,Keil也是如此。只要我们了解下其文本的格式,自动创建工程并非难事。

我们先看下Eclipse工程文件中宏定义、头文件路径和代码路径中格式。Eclipse工程目录下有两个文件,.project和.cproject。

  • .cproject存储配置,包含了宏定义和头文件路径。
  • .project存储代码文件路径。

宏定义
在这里插入图片描述
其中__PX4_NUTTX对应的是

<listOptionValue builtIn="false" value="&quot;__PX4_NUTTX&quot;"/>

头文件路径
在这里插入图片描述
其中/mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/src对应的是

<listOptionValue builtIn="false" value="&quot;/mnt/g/ardupilot/src/ardupilot/modules/PX4Firmware/src&quot;"/>

代码路径
在这里插入图片描述
宏定义和头文件路径的格式非常简单,代码路径涉及到父目录的问题稍微复杂些。不过,很明显,写程序去追加条目是可行的。

笔者原本打算详细介绍自动化创建工程的步骤,不过突然发现这并不是一件简单的事情。需要准备更多的素材,需要更大的篇幅才能讲清楚讲透。因此就不在这里展开了,毕竟本文的宗旨是讲方法。如果大家感兴趣的话,笔者后续会专门出一篇文章来讲自动化创建工程。当然,ardupilot移植分享系列文件也不是仅此一篇,笔者规划的是至少3遍文章,第2篇分析代码,第3篇讲移植的细节。在这里向大家求一波三连支持,大家的认可是我创作的动力

对了,项目源码,请观众笔者的公众号,回复:ardupilot-ti,即可获取。
在这里插入图片描述

别急,方法还未讲完。

除了宏定义、头文件路径和代码路径,工程中还有许多其他的内容。我们并不需要去了解这所有的内容,也不需要凭空生成工程文件。笔者使得的方法是,先创建一个模版工程,具体的步骤是:

  1. 使用Eclipse创建空工程。
  2. 使用文本编辑器打开工程文件,在宏定义、头文件路径和代码路径的位置上加上特殊的字符串标记,这就是模版工程。
  3. 写程序自动创建工程时,先组装好宏定义、头文件路径和代码路径的内容,然后替换模版工程中对应的字符串标记即可。

下图是.cproject文件中的三个标记,分别对应宏、头文件路径、额外的头文件。
在这里插入图片描述
这是.project中的标记,用于添加代码文件。
在这里插入图片描述
使用特殊的字符串标记是一个取巧的方法,不过它有一个缺点:若后续再用Eclipse打开模版工程做修改的话,因为Eclipse并不认识这个标记,所以它将会消失。

这次真结束了,源码分析和移植,有待下回分解。

  • 8
    点赞
  • 41
    收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 4

打赏作者

wenbodong

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值