Hello, Sky! - 从 PX4 开发者的第一个程序聊起

今天聊聊px4学习者的第一个程序

  • 版本:1.8.2
  • 成文时间:2018.12.13

px4学习者的第一个程序一般都是px4_simple_app,虽然examples到现在的最新版1.8.2已经有了11个例程,算上新加入的templates已经有了12个。但这个例程仍是px4官方文档唯一进行了讲解的例程。

这个例程的优点就是「simple」,基本上覆盖到了最基础的px4开发所需的知识点。但我觉得,他的缺点也是「simple」,在细节上缺乏得太多,导致入门的开发者看懂了这个程序,能大概了解这个系统的架构,却看不懂其他的modules,达不到基本入门px4的开发的水平。

如何把一个大象装进冰箱里?
官方文档:分三步。第一步,把冰箱门打开;第二步,把大象放进去;第三步,把冰箱门关上。

我在这篇文章中补充了很多我在入门过程中摸索了很久的细节,希望看完这篇文章,大家能够对这个经典的入门例程胸有成竹,快速上手。

Hello, Sky!

__EXPORT int px4_simple_app_main(int argc, char *argv[]);

EXPORT声明了这个Module的接口,也就是说当飞控板启动该Module的时候,他会首先进入这个函数。

PX4_INFO("Hello Sky!");

PX4_INFO相当于在PX4 shell里面printf,是一个标准的log函数。log函数还包括了PX4_INFO,PX4_WARN,PX4_ERR,PX4_DEBUG。他们代表了不同级别的日志。其中警告和错误还会添加到系统日志中并显示在Flight Review上。

养成使用Log函数的好习惯,才能在对飞行日志进行分析的时候做到心中有数,不至于说炸机了还搞不清为什么。

  • uorb的简单应用

对于Log的学习,一行程序加两句解释其实也就够了,但我个人觉得,这个例程对于uorb的使用所做的示范完全不够。

聊接下来的内容之前,还是先对uorb的机制做一个介绍。

什么是Uorb?

先用之前讲过的那个例子复习一遍。别嫌烦,成为一个高手首先一定要有明晰的概念,UORB是PX4的一个核心,多重复几次是很有益的。

  • PX4 程序就像一个聋哑公司

PX4的整个程序就像一个公司,公司里的员工都又聋又哑,他们之间无法直接交流。于是他们平时的工作是这样进行的:

一、员工从公司的某个黑板上读取数据。

二、读到数据后他们对这些数据进行处理,并得到了一些结果。

三、将其中一些黑板上的数据擦掉,写上自己的数据。

另外该公司还有这样的规定:每个黑板只能写特定内容的数据,并且要写入新数据,首先要擦除旧数据。

UORB是PX4内部的一套异步信息传输机制,公司里的这些黑板实际上相当于一个个缓存区,在实际的程序中这些缓存区是一个个总线。黑板的特定内容在真正的程序中是通过topic(主题)规定的。我们可以在Firmware/msg目录下看到系统自带的各种主题。

自己定义一个主题

你同样也可以自己建立主题,格式仿照其他主题就行,但一定要记得在Firmware/msg下的CmakeLists下进行注册,否则系统是不会对你的主题进行编译的。

  • 趋势:记得给你的时间戳赋值

讲一句题外话,我看了github上比1.8.2还新的,最新的源码,在新版本里面所有的主题都要加一个timestamp,也就是时间戳数据。时间戳就是系统从启动到现在经过的时间,这个数据在现在的版本中是会自动编译进去的,这导致很多人在自定义主题的时候没有意识到还有这个数据。

想让你的代码比别人更规范一点,记得给你新主题的「时间戳」赋值。一般来讲,我们直接用系统的定时函数hrt_abosolute_time()给他赋值就行。

  • 科普:异步通信与同步通信

刚刚说到UORB是一个异步信息传输机制,这个「异步信息传输机制」听起来很厉害,那他实际是什么意思呢?这其实是通信学科中的一个概念。虽然我不是通信专业的,但我一直觉得通信这个学科很有趣。

和「异步通信」对应的是「同步通信」。异步通信意味着我想发就发,想收就收。而同步通信则意味着,我发的时候你就要收,你发的时候我就要收。如果同步通信是面对面的讲话,那么异步通信就是你给家里人留的便签。这两种通信方式各有各的好处,我会在以后的博客中穿插讲讲。

用UORB进行数据的发送和读取

言归正传,我们回到对px4_simple_app的解读上,要看懂接下来的代码,首先要了解用UORB进行数据的发送和读取的方法。

  • 发送数据:避免多次公告

大略得讲,发送数据分两步:先公告(orb_advertise)再发布(orb_publish)。

发送数据要注意的点是只能公告一次,否则公告会报错。因此最好直接把公告放到类的构造函数里面去。

  • 读取数据:巧用check和阻塞等待

概括来讲,读取数据也分两步,先订阅(orb_subscribe)再复制(orb_copy),同样地,把订阅放在类的构造函数里面是个很聪明的做法。

不同的是,读取数据除了这两大步之外,在中间还有一小步,那就是检查数据有没有更新,检查更新的方法有两种:阻塞等待和check。

//返回一个句柄,用在后面的程序中
int sensor_sub_fd = orb_subscribe(ORB_ID(sensor_combined));
//初始化一个结构体数组,把句柄和输入标识符放到里面
px4_pollfd_struct_t fds[] = {
    { .fd = sensor_sub_fd,   .events = POLLIN },
};
/*
*poll_ret>0:阻塞等待成功
*poll_ret=0:阻塞等待超时
*poll_ret=-1:阻塞等待失败。
*等待1个主题,超时时间1000ms。
*/
int poll_ret = px4_poll(fds, 1, 1000); 

通俗地讲,阻塞等待(px4_poll)就是我今晚啥都不干了,我就盯着这个网页等高考成绩出来,你不出来我就不干别的了。当然如果太晚了,比如等到了半夜三点成绩还没出来,阻塞等待就会报一个超时,表示我困得实在不行了,先睡觉,明早继续等成绩。

这个比喻也有不恰当的地方,因为阻塞等待在等待的时候是不占用CPU的,但等成绩却很费心力。

//这两行代码例程里没有,我从别的地方摘过来的
bool update = false;
//如果数据更新了,就把update设为true
orb_check(sensor_sub_fd, &update);

而check(orb_check)则是一种比较随意的方式,可能我忽然想看小说了,我就看一眼它更新没,更新了我就把它看完,没更新我就继续干别的。

这两种方式我们一般混合使用,因为阻塞等待可能会比较耗时,我们往往只将其用在一个非常重要的消息上,而对一些不那么重要的消息使用check的方式。(其实阻塞等待也可以等待多个主题,但比较少见。)

读取数据:set_interval用法

orb_set_interval(sensor_sub_fd, 200);
//限制每200ms只能复制一次

例程在订阅后用了一个orb_set_interval,这个函数可以限制数据的读取频率。

这个函数其实不怎么用到,但如果你遇到了以下的场景,可以考虑用它:

1.你的资源紧张,想优化自己的程序。

2.传感器的数据可能一秒更新一次,但你却一秒check它两百遍,这样效率就会很低,不如限制一下吧。

3.你阻塞等待的数据更新得过快,你并不需要那么快得更新数据,就可以限制一下更新频率。

用这个函数要注意,把间隔设置得过大,会导致数据丢失。如果你的资源充沛,那其实可以不用管这个函数。

利用类的特性进行公告、订阅、取消公告和取消订阅。

把公告和订阅写在类的构造函数中,取消公告和取消订阅写在类的折构函数中。在程序开始时通过new一个实例来自动订阅和公告,在程序结束时通过delete这个实例来自动取消公告和取消订阅。

养成取消订阅(orb_unsubscribe)和取消公告(orb_unadvertise)的好习惯很重要,这可以消除程序出错的隐患。

我在上面讲的有些内容在px4_simple_app中是看不出来的,这也是为什么我说这个例程过于simple的原因,它同样也没有教大家用c++进行编程时的很多技巧。

大家可以以modules/attitude_estimator_q为参考,结合我说的看一看。这个程序难度适中,适合进阶学习。

回到源码

讲到这里,理解这个程序接下来所做的事就很简单了,这个程序接下来通过阻塞等待读取了sensor_combined的数据,然后把这些数据写入log,并发布到了vehicle_attitude。

最后再讲一点小细节吧,POLLIN的值是1,阻塞等待成功后输出标识符fds[0].revents也是1,因此两者按位作与运算还是等于1。

if (fds[0].revents & POLLIN)

把文章拉到最下面点击阅读原文,对照着这篇文章理解一下源码吧。

写在后面

为什么是写在后面不是写在前面?因为我估计很多人看不到最后一行。

在这个碎片化的信息时代里,人们的阅读能力已经大大地被抖音、头条等各种垃圾信息削弱了,如果你能耐心地把这有篇有些枯燥的技术长文看完,恭喜你,你的阅读能力已经超过很多人了。

这篇文章应该算得上讲解px4_simple_app比较详细的一篇。

原创不易,如果你认真看完了这文章,觉得还不错,可以关注我的个人公众号「我不是药丸」,看我的其他原创文章。