理解ArduPilot线程
当你学习了ArduPilot库的基础结构,你应该了解ArduPilot是如何处理线程的了。从Arduino中继承的setup()/loop()结构可能是ArduPilot看起来是单线程的,但是实际上并不是;
ArduPilot中的线程方式依赖与它所运行的板子;有一些板子(像APM1和APM2)不支持线程,所以使用一个简单的timer和callbacks。一些板子(像PX4和Linux)支持带有实时优先级的Posix线程模型,并且广泛的被ArduPilot所使用;
POSIX:可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准。
以下是你需要理解的关于线程的一些关键概念:
- The timer callbacks
- HAL specific threads
- driver specific threads
- ardupilot drivers versus platform drivers
- platform specific threads and tasks
- the AP_Scheduler system
- semaphores
- lockless data structures
The timer callbacks
每个平台在AP_HAL中提供了一个1kHz的定时器;在ArduPilot中的任何代码都可以注册一个定时器函数,然后以1kHz的频率被调用;所有被注册的定时器函数都可以被频繁的调用;这是一种十分原始的机制因为它是十分轻便的也是十分有用的;你可以通过调用hal.scheduler->register_timer_process()注册一个时钟回调函数:
这个例子来自于MS5611压力传感器驱动;AP_HAL_MEMBERPROC()宏将C++成员函数压缩为一个回调参数(将对象内容捆绑为一个函数指针)
在新版的ArduPilot项目中没有找到上述程序,应该是更新之后重写的这个宏定义,如下所示,道理应该是一样的:
当程序想以小于1kHz的频率运行时,那么它应该定义一个属于它自己的“last_called”变量,并且如果在没有经过足够时间的时候,立即返回;
使用hal.scheduler->millis()和hal.scheduler->micros()函数以毫秒和微秒为单位获取从程序启动开始的时间;
你可以修改一个sketch(或者自己创建一个)来添加一个时钟回调函数;使定时器增加然后在loop()函数中每分钟打印一个计数器的值;
修改你的函数使计数器每25毫秒增加一次;
HAL特定线程
在支持线程的平台上,平台的AP_HAL将会创建多个线程支持基础操作;例如,在Pixhawk平台上,将创建以下线程:
- UART线程:读写UART(和USB)
- 定时器线程:支持像上面描述的1kHz的定时器功能
- IO线程:支持向microSD、EEPROM、FRAM写数据操作
去看一下在每个AP_HAL中的Scheduler.cpp文件,了解被创建的线程和每个线程的优先级;
在libraries/AP_HAL_ChibiOS/Scheduler.h中可以看见在ChibiOS平台上所定义的线程和优先级;
//优先级定义
#define CHIBIOS_SCHEDULER_MAX_TIMER_PROCS 8
#define APM_MONITOR_PRIORITY 183
#define APM_MAIN_PRIORITY 180
#define APM_TIMER_PRIORITY 181
#define APM_RCIN_PRIORITY 177
#define APM_UART_PRIORITY 60
#define APM_STORAGE_PRIORITY 59
#define APM_IO_PRIORITY 58
#define APM_STARTUP_PRIORITY 10
#define APM_SCRIPTING_PRIORITY LOWPRIO
...
//创建的线程
static void _timer_thread(void *arg);
static void _rcin_thread(void *arg);
static void _io_thread(void *arg);
static void _storage_thread(void *arg);
static void _uart_thread(void *arg);
static void _monitor_thread(void *arg);
...
可以使用debug工具查看进程,此部分暂时没有硬件平台,不做叙述;线程的一种通用的用途是提供一种驱动方式来调度速度更缓慢的任务而不用中断飞行程序;例如,AP_Terrain库需要使用IO接口将文件读取到microSD卡上(存储和获取地形数据);使用线程来处理此类需要可以调用以下函数进行:
通过以上程序,AP_Terrain::io_timer函数可以被规则的调用;这个被板子的IO线程所调用,这意味着它使用更低的优先级,适用于存储的IO任务;像这样更慢的IO任务不使用timer线程调用是重要的,因为它将导致更重要的处理高速传感器数据的任务变慢;
驱动特指线程
它是可能的创建驱动特指线程,来支持特指驱动的异步处理;当前你可以根据相关平台创建驱动指定线程,如果你想要驱动仅仅运行在一个类型的平台上,这种方式是方便的;如果你想要运行在多个平台上,你有以下两种选择:
- 你可以使用register_io_process()和register_timer_process()来使用timer和io线程;
-
你可以添加新的硬件抽象层接口来提供一种通用的方式在多个平台上创建新的线程(请贡献补丁)
AP_HAL_Linux/ToneAlarmDriver.cpp提供了一个驱动指定线程的例子;
ArduPilot驱动相对于平台驱动
你可能注意到一些驱动的副本存在于ArduPilot中;例如:有一个MPU6000驱动在libraries/AP_InertialSensor/AP_InertialSensor_MPU6000.cpp,还有一个MPU6000驱动在PX4Firmware/src/drivers/mpu6000.
存在一个副本的原因是PX4项目在Pixhawk板子上已经提供了一系列测试好的驱动程序;并且我们和PX4团队有着一个良好的合作关系在开发和强化驱动的工作上;所以当我们在PX4平台上构建ArduPilot的时候,我们使用PX4驱动通过写
一个小的shim驱动表示带有标准ArduPilot库接口的PX4驱动;在libraries/AP_InertialSensor/AP_InertialSensor_PX4.cpp中,你可以看见一个小shim驱动访问在这个板子上可获得的PX4IMU驱动并且自动的使它们成为ArduPilot AP_InertialSensor库中的一部分;
这里博主暂时理解为调用关系,大部分的驱动程序被PX4所开发,ArduPilot程序调用已经实现好的PX4驱动,在新版程序中,已经不存在这种结构,Pixhawk板子的驱动程序被完成的写在ChibiOS目录下;
平台指定线程和任务
在一些平台上,通过启动程序,将有很多的基础任务和线程被创建;这是和平台类型十分相关的,在此教程中,将主要专注于基于PX4板子类型的任务;
在使用debug工具的"ps"命令输出内容中,我们可以看到一些不是被AP_HAL_PX4调度程序所启动的线程;如下所示:
idle task//当没有其他任务运行的时候被调用
init//启动系统
px4io//处理与PX4协处理器之间的通信
hpwork//处理基于PX4驱动线程(主要是I2C驱动)
lpwork//处理低优先级线程(IO)
fmuservo//处理在FMU上与辅助PWM输出间的通信
uavcan//处理uavcan CANBUS协议
这些线程的启动被PX4指定的rc.APM脚本所控制;当PX4启动时,运行这个脚本,这个脚本检测所使用的PX4板子的类型并且加载正确的任务和驱动;它是个“nsh”脚本,和"bourne shell"脚本相似;
作为练习,你可以尝试编辑rc.APM脚本并且添加一些sleep和echo命令;然后加载到固件上并且启动时连接debug控制台,你可以看见echo命令将显示在控制台上;
学习PX4启动过程的另一种方式是不插microSD卡启动;rcS脚本在rc.APM脚本之前启动,它的功能是检测是否有microSD卡插入,如果没有SD卡插入,只提供给你一个nsh控制台;你可以在USB控制台上手动运行rc.APM中的步骤学习它是如何工作的;
连接USB控制台,在Pixhawk启动以后,尝试以下练习:
tone_alarm stop
uorb start
mpu6000 start
mpu6000 info
mpu6000 test
mount -t binfs /dev/null /bin
ls /bin
perf
尝试运行一下其他驱动,在/bin中查看可获得的驱动;这些命令的源码在PX4Firmware/src/drivers中。
如果你查看mpu6000驱动中的内容,你可以看见如下内容:
这个和AP_HAL中的hal.scheduler->register_timer_process()函数内容是相同的,但是是PX4中特有的内容并且是更灵活的(所以在Pixhawk中是不是没卵用?)
对于在驱动中运算很快的周期性事件,使用hrt_call_every()是一种通用方法;这个运算的特点是不可中断,至多执行几十微秒(SPI设备驱动)
和hmc5883驱动相比,你可以看以下内容:
是另外一种应用于周期性事件的机制,适用于运算更慢以下的设备,像I2C设备;以上程序将cycle_trampoline函数添加到hpwork线程的工作队列中;hpwork中调用的内容可以中断,执行时间大约为几百微秒;对于执行时间更长的任务,应该使用lpwork工作队列,在更低优先级的lpwork线程中运行;
AP_Scheduler系统
ArduPilot线程和任务的下一方面是理解AP_Scheduler系统;AP_lirary库用于分割机器人主线程的时间,提供了一个简单的机制控制每个运算所使用的时间(一个运算即是调用一个任务)
这种工作方式可以表示为:在每个人机器人实现中的loop()函数中包含一些程序执行以下内容:
- 等待新的IMU样本到达
-
在每个IMU样本中调用一组任务
维护Scheduler的是一张表,每个机器人类型都有一个AP_Scheduler::Task表;阅读AP_Scheduler/examples/Scheduler_test.cpp例子学习Scheduler如何工作;
你可以看见有一个小的表中有三个被调度的程序,每个任务后有两个数值,如下所示:
/*
scheduler table - all regular tasks are listed here, along with how
often they should be called (in 20ms units) and the maximum time
they are expected to take (in microseconds)
*/
const AP_Scheduler::Task SchedTest::scheduler_tasks[] = {
SCHED_TASK(ins_update, 50, 1000),
SCHED_TASK(one_hz_print, 1, 1000),
SCHED_TASK(five_second_call, 0.2, 1800),
};
每个函数名后面的第一个数值表示调用频率,在这里例子中ins_update()使用50Hz的频率,这意味着每次调用使用20ms时间;