本篇博客主要介绍Firmware固件中各功能模块的基本结构。
功能模块的编译
从上篇博客内容中的demo我们可以发现,如果我们需要给Pixhawk 模块新增一个功能模块,一般的做法是新建一个文件夹,所有这个功能模块需要处理的事情均在该文件夹下完成。这个文件夹的结构大致如下:
example
|--example.c/example.cpp/example.hpp/example.hpp
|--CMakeLists.txt
|--....
其中 example.c/example.cpp/example.hpp/.. 等是实现具体功能的程序文件,CMakeLists.txt 可以视为编译文件列表。以上已是新增功能模块的最低要求。为了能添加一些可以在地面站修改的参数,往往还会增加params.c 文件,即基本结构变为:
example
|--example.c/example.cpp/example.hpp/example.hpp
|--CMakeLists.txt
|--params.c
|--....
当一个功能模块里面还包含一些子功能模块的时候,也可以嵌套文件夹放入文件子功能模块文件,以 drivers 层下的 gps 模块为例,它的结构如下:
gps
|-- >device
|-- >src
|--ashtech.cpp/ashtch.h/mtk.cpp/mtk.h/....
|--CMakeLists.txt
|--gps.cpp
|--params.c
|--definitions.h
它包含了一个 device 文件夹,而该文件夹下又包含了一个 src 文件夹,之后才是代码文件。从文件夹的命名也可以推测出,可实现 gps定位功能的设备不限于一种,src 文件夹就是用于收录这些可实现卫星定位功能设备的驱动文件,实际实现时,可根据具体设备连接情况调用相应的文件执行。现在我们需要关注的是系统如何能编译到这些代码。
通过demo文件 ,我们知道,系统能够编译到新建的代码是因为 CMakeLists.txt 文件中的:
...
SRCS
my_example_app.c
...
据此,我们也可以推测,只要我们新建的 xx.c 文件或 xx.cpp 文件能够在 CMakeLists.txt 中出现即可让系统找到并编译。我们可以打开 gps 下的 CMakelists.txt 文件验证我们的猜想,发现果然有如下内容:
...
SRCS
gps.cpp
devices/src/gps_helper.cpp
devices/src/mtk.cpp
devices/src/ashtech.cpp
devices/src/ubx.cpp
devices/src/uwb.cpp
...
其中 devices/src/.. 是文件目录。由此,我们可以再次重申下我们的推测:
- 新建的.cpp或.c等源文件必须在
CMakeLists.txt 里申明。
对于头文件,源文件调用时,只需在文件开始的地方包含该头文件:#inlude "xxx.h" 或 #include "xx.hpp" 即可。
功能模块的入口函数
demo 实际上是独立运行的,就是说它不需要被别的文件调用,它有自己主文件。这也 Nuttx系统的工作特点之一,可以并行处理多进程任务。每一个进程有且仅有一个主文件 (xx.c 或.cpp ) 和若干个附属文件(也可以没有)。例如,demo 中的主文件就是
my_example_app.c
主文件中必须有一个入口函数,也就是进程开始执行的地方。入口函数要先用 __EXPORT 申明,然后再定义。以 demo 为例,其申明和定义的格式为:
...
__EXPORT int my_example_app_main(int argc, char *argv[]);//入口函数申明格式
...
int my_example_app_main(int argc, char *argv[])
//入口函数定义格式
{
...
}
....
arg c是 argument count 的缩写,表示传入 main 函数的参数个数;argv 是 argument vector 的缩写,*argv[] 表示传入入口函数的命令和参数序列,并且第一个参数argv[0] 一定是命令的名称。以 gps 进程的启动命令为例: gps start 则有,argc=2, *argv[0]="gps",*agrv[1]="start" 。
功能模块的基本结构
Pixhawk 中任何一个功能模块(如位置控制、姿态控制、各个传感器驱动模块等)或者说一个进程工作基本模式如下图所示:
图1 模块基本工作模式
下面以一个示例来分析如何实现图中功能模块的工作模式。
例1: 利用 Pixhawk 飞控和控制台实现加法功能,即控制台输入 加数 a和 被加数 b, 然后 飞控计算后,再返回结果到控制台。
为了能将该任务依照图2.6给定的方式完成,程序应当具备以下特点:
- 一共包含五个指令:进程启动指令、进程停止指令、进程状态指令、进程帮助指令以及用户操作指令。
- 用户操作指令的格式为:
my_example_app test -a num1 -b num2 ,其中 my_example_app 是指令的名称, -a num1 -b num2 均是指令参数。如一个正常命令为:
my_example_app test -a 100 -b 2
- 指令输入后,应当反馈计算的结果到控制台,然后系统继续等待新的指令, 比如上面 100 加 2 的例子,应返回:
The result is: 102
- 启动指令为:
my_example_app start , 输入后,若当前已启动,返回信息:"Task is already running, now is waiting for data to my_example_app "; 若当前未启动,则启动状态设为 true,并返回信息:"Task start successful! " - 停止指令为:my_example_app stop, 输入后,启动状态转化为 false。
- 状态指令为:
my_example_app status ,输入后,若当前未启动,返回信息:"Task is is not running, please start first "; 若当前已启动,则返回信息 Task is already running, now is waiting for data to my_example_app! - 帮助指令为:
my_example_app help ,输入后,返回以下信息:
This is my_example_app module, using for addition opreation. You can use the following command:
my_example_app start //---- start the module
my_example_app stop //---- stop the module
my_example_app status
my_example_app help
//---- show the current state of the module
//---- show what you can do in this module
my_example_app test -a num1 -b num2
//----caculate your data,
there is a limit that "a<=10000 and a>=-10000", and ’b’ has the same limitation.
command is not right, please check if it is one of the following command
:
my_example_app start //---- start the module
my_example_app stop //---- stop the module
my_example_app status
my_example_app help
//---- show the current state of the module
//---- show what you can do in this module
my_example_app test -a num1 -b num2
//----
caculate your data, there is a limit that "a<=10000 and a>=-10000", and ’b’ has the same limitation.
- 当输入数据超范围时,返回警告信息 “
Your data is beyond the limitation ”
实现: 考虑到源码中主要是用 C++ 进行编程,这里我们将 demo 用的 .c文件改为 .cpp文件,从而文件中需要创建一个类,我们创建如下:
#include <px4_posix.h>//包含了打印信息函数:PX4_INFO
#include <px4_defines.h>//含有OK等定义
#include <px4_getopt.h>//用于解析指令
#include <stdlib.h>//基本函数库,如字符串转浮点数据的函数: atof
#include <string.h>//用于字符串处理
extern "C" __EXPORT int my_example_app_main(int argc, char *argv[]);
class AddtionCall
{
public:
/**
* Constructor
*/
AddtionCall();//构造函数
/**
* Destructor, also kills task.
*/
~AddtionCall();//析构函数
int start();
int stop();
void status();
void help();
void my_example_app(int argc, char *argv[]);
private:
float _num1 ;//加数
float _num2;//被加数
float _result ;//结果
bool _status; //状态标志
};
如此,我们定义了一个名为Addtioncall 的类,里面包含了需要实现各个指令的公有函数,和用于运算与状态标记的变量。
C++中类的成员(即类里面定义的函数、变量等)可分类三种:public、protected、private;public 表明该数据成员、成员函数是对所有用户(类对象)开放的,所有用户都可以直接进行调用;private 表示私有,私有的意思就是除了 class 自己的成员(如类里的各种函数)可以使用之外,任何类对象都不可以直接使用;protected对于继承和友元类来说,就是 public 的,可以自由使用,没有任何限制,而对于其他的外部class,protected 就变成 private。
类函数具体的实现可参考C++相关资料, 这里主要以加法运算函数为例:
void AddtionCall::my_example_app(int argc, char *argv[])
{
bool error_flag=false;
int myoptind = 1;
int ch;
const char *myoptarg = nullptr;
while ((ch = PX4_getopt(argc, argv, "a:b:", &myoptind, &myoptarg)) != EOF){
switch (ch) {
case 'a':
_num1= atof(myoptarg);
break;
case 'b':
_num2 = atof(myoptarg);
break;
default:
PX4_WARN("unrecognized flag");
error_flag = true;
break;
}
}
if (error_flag){
;
}
else{
if (_num1<=10000&&_num1>=-10000&&_num2<=10000&&_num2>=-10000){
_result=_num1+_num2;
PX4_INFO("The result is: %f",(double)_result);
}
else{
PX4_WARN("Your data is beyond the limitation!");
}
}
}
该段函数比较关键的是对指令参数的提取,主要利用了头文件 #include<px4_getopt.h> 中的
int PX4_getopt(int argc, char *argv[], const char *options, int *myoptind,
const char **myoptarg)
函数来解析指令。如我们想传入某一个参数值,那么必须以-cmd_name cmd_value (指令名称和参数值,参数值有时候也可以没有)的形式传入,其中符号"-" 标识其后是一个指令名,这有利于函数PX4_getopt 在其字符串参数 *options 中对指令进行检索,如上述函数参数中:
*options="a:b:"//这里的冒号”:“表示指令后有参数值,需要提取到*myoptarg中,若没有冒号,表示该指令后不带参数值
而我们输入的命令是
my_example_app test -a num1 -b num2
那么函数PX4_getopt 根据符号"-" 检测到指令 a 和 b 并对照 *options , 发现确实有指令 a和 b,然后根据冒号提取后面的参数值 num1 和 num2 ,我们最后用 atof 函数将提取到的参数值转换为浮点型数据。
最后,我们打开 QGC 地面站的控制台,输入指令测试,得到我们期望的测试结果如下图所示。
图3 控制台运算结果
|