第二章第三节——ArduPilot线程

飞控开发

了解ArduPilot线程。

一旦你学会了ArduPilot的基本库,是时候让你知道ArduPilot的线程如何工作。这是从Arduino的继承setup()/loop()结构可使其看起来ArduPilot是一个单线程系统,但实际上却并非如此。
ArduPilot的线程取决于主板型号。有些主板(如APM1和APM2)不支持线程,所以凑合着用一个简单的定时器和回调。有些主板(PX4和Linux)支持丰富的实时Posix线程模式,而这些是由ArduPilot广泛使用。
有一些相关的线程的关键概念,你需要在ArduPilot理解:
 定时器回调。
 HAL特定线程。
 驱动特定线程。
 ardupilot驱动程序与平台驱动程序。
 平台特定线程和任务。
 AP_Scheduler任务调度系统。
 信号量。
 无锁数据结构。

1.定时器回调。

每个平台提供了AP_HAL一个1kHz的计时器。定时器回调通过hal.scheduler->register_timer_process(),举个例子:
hal.scheduler->register_timer_process(AP_HAL_MEMBERPROC(&AP_Baro_MS5611::_update));
这个例子是MS5611的一个气压计驱动。所述AP_HAL_MEMBERPROC()宏提供了一种方法来封装一个C ++成员函数作为回调参数。
如果一个更新函数的更新频率低于1K hz,在函数开始先判断当前时间和上次更新的时间差,如果下次更新的时间还没到,那么函数就立即返回。可以通过hal.scheduler->millis() 和hal.scheduler->micros()来获取当前毫秒和微秒时间。

2. HAL特定线程。

在支持实时线程平台AP_HAL该平台将创建多个线程支持基本操作。例如,PX4将创建下列HAL特定主题:
 UART线程,用于读取和写入的UART(和USB)
 计时器线程,支持上述1kHz的定时器功能。
 IO线程,支持写入microSD卡,EEPROM和FRAM。
每个硬件平台(apm,px4...)的实现都有Scheduler.cpp这么一个文件,去这个文件看看创建了哪些线程,以及这些线程的实时优先级。
你有一个Pixhawk那么你也应该现在安装调试控制台电缆连接到控制台NSH(serial5端口)。连接于57600。当你连接,请尝试“PS”命令的广告,你会得到这样的事情:
PID PRI SCHD TYPE NP STATE NAME
0 0 FIFO TASK READY Idle Task()
1 192 FIFO KTHREAD WAITSIG hpwork()
2 50 FIFO KTHREAD WAITSIG lpwork()
3 100 FIFO TASK RUNNING init()
37 180 FIFO TASK WAITSEM AHRS_Test()
38 181 FIFO PTHREAD WAITSEM <pthread>(20005400)
39 60 FIFO PTHREAD READY <pthread>(20005400)
40 59 FIFO PTHREAD WAITSEM <pthread>(20005400)
10 240 FIFO TASK WAITSEM px4io()
13 100 FIFO TASK WAITSEM fmuservo()
30 240 FIFO TASK WAITSEM uavcan()

在这个例子中,你可以看到“AHRS_Test”线程,这是运行在libraries/AP_AHRS/examples/AHRS_Test的例子。你还可以看到计时器线程(优先级181),UART的线程(优先级60)和IO线程(优先级59)。
此外,你可以看到px4io,fmuservo,uavcan,lpwork,hpwork很多空闲任务。
根据不同硬件平台的需要,创建的线程数也或多或少不一样。
一个共同的使用线程是提供驾驶的方式来安排缓慢任务,而不中断主自动驾驶飞行代码。例如,AP_Terrain库需要能够建立文件从IO端口到microSD卡(存储和检索地形数据)。通过调用函数实现hal.scheduler-> register_io_process()。
例子:
hal.scheduler->register_io_process(AP_HAL_MEMBERPROC(&AP_Terrain::io_timer));

3. 驱动特定线程。

可以创建驱动特定的线程,以支持在特定于一个驱动器异步处理。目前,您只能创建与平台相关的驱动特定线程,当你的驱动程序的目的是只在一种类型的自动驾驶仪板的运行,才是可以的。如果你想让它在多个AP_HAL平台运行,那么你有两个选择:
 可以使用register_io_process()和register_timer_process()调用调度来利用现有的定时器或IO线程
 可以添加一个新的HAL接口,提供了一个通用的方法来创建多个AP_HAL目标线程
驱动特定线程的一个例子是Linux移植的ToneAlarm线程。见AP_HAL_Linux/ ToneAlarmDriver.cpp

4. ardupilot驱动程序与平台驱动程序。

您可能会注意到一些ArduPilot驱动重复。例如,我们在libraries/AP_InertalSensor/AP_InertialSensor_MPU6000.cpp中有MPU6050的驱动,并在PX4Firmware/src/drivers/mpu6000中也有一个。
重复的原因是的PX4已经提供了一套行之有效的硬件的驱动程序。所以,当我们为PX4编译ArduPilot的时候,我们为PX4驱动编写一个小的“shim”的驱动,来兼容标准ArduPilot库接口。如果你看一下libraries/AP_InertialSensor/AP_InertialSensor_PX4.cpp你会看到一个小shim的驱动程序。这个驱动程序可以询问PX4板子上用的是什么IMU驱动程序,并自动使所有的程序都作为ArduPilot AP_InertialSensor库的一部分。
如果我们板子上有MPU6000,我们使用AP_InertialSensor_MPU6000.cpp驱动程序在非PX4平台,使用AP_InertialSensor_PX4.cpp驱动程序在PX4平台。
其它平台的移植也用同样的分开处理方式。例如,我们对一些linux平台的传感器使用Linux内核驱动程序。对于其他的传感器,我们使用通用AP_HAL I2C和SPI接口“in-tree”使用ArduPilot兼容多种平台的驱动程序。

5. 平台特定线程和任务。

在某些平台上启动进程会创建若干个基本任务和线程。这些都是平台相关的,所以为了这个教程,我将集中精力介绍在PX4平台的任务。
在shell下输入“PS”命令,我们看到一些任务和线程并不是由AP_HAL_PX4调度代码创建的。具体来说,他们分别是:
 idle task - 空闲时运行。
 init - 用于启动系统。
 px4io - 处理与PX4IO协处理器的通信。
 hpwork - 处理基于PX4驱动程序的线程(主要是I2C驱动程序)。
 lpwork - 处理基于低优先级的工作的线程(如:IO)。
 fmuservo - 处理在FMU板的PWM输出。
 uavcan - 处理uavcan CAN总线协议。
所有这些任务的启动是由PX4具体rc.APM脚本控制的。该脚本在PX4启动时运行,并负责检测我们使用的是什么样的PX4板,然后装入合适的任务和驱动程序。这是一个“NSH”的脚本,它类似于一个Bourne shell脚本(虽然NSH是更原始的)。
作为练习,尝试编辑rc.APM脚本并增加了一些sleep和echo命令。然后更新固件,并且在板子启动时连接调试控制台。您的echo命令应显示在控制台上。
探索PX4的启动的方式的另一个非常有用的方式是在没有插入microSD时启动板子。rcS脚本(运行在rc.APM之前)会检测是否插入一个microSD,如果检测到没有卡,会把信息通过USB端口上传到NSH控制台。然后您可以手动在控制台上运行rc.APM里所有的命令,以了解它是如何工作的。
在Pixhawk没有插microSD卡,并通过USB调试连接到主机的情况下,请尝试以下练习:
tone_alarm stop
uorb start
mpu6000 start
mpu6000 info
mpu6000 test
mount -t binfs /dev/null /bin
ls /bin
perf

尝试使用其他驱动程序。看看有什么可用的。大多数这些命令的源代码在PX4Firmware/src/drivers。通过看mpu6000驱动程序必须有所收获。
鉴于我们对线程和任务的话题,线程在PX4Firmware的git树的简要说明是值得一提的。如果你看一下在mpu6000驱动程序,你会看到这样一行:
hrt_call_every(&_call, 1000, _call_interval, (hrt_callout)&MPU6000::measure_trampoline, this);
这跟AP_HAL的hal.scheduler-> register_timer_process()函数一样,但是PX4灵活得多。它说,它希望PX4的HRT(高分辨率定时器)子系统每1000微秒调用MPU6000:: measure_trampoline函数。
hrt_call_every()是用于驱动程序中速度非常快的定期事件的常用方法,如SPI设备驱动程序的。该操作通常与禁止中断运行,并应采取只有几十微秒。
对比hmc5883驱动程序,你应该看下面:
work_queue(HPWORK, &_work, (worker_t)&HMC5883::cycle_trampoline, this, 1);
这个替代机制其适用于较慢的设备的定期事件,诸如I2C器件。

6. AP_Scheduler任务调度系统。

ArduPilot线程和任务的下一个要理解的是AP_Scheduler系统。AP_Scheduler系统提供了一些简单的机制来控制用于每个操作的时间(称为AP_Scheduler一个'任务')。
任务调度系统的工作方式是这样的,在loop()函数内为每个载体执行下面代码:
 等待一个新的IMU样本到达。
 调用一组任务的每一个IMU样本之间。
这是一个列表驱动调度器,每一个载体类型都具有AP_Scheduler::Task 列表。通过AP_Scheduler/examples/Scheduler_test.pde sketch来学习它。
在代码里面是下面这种格式。
static const AP_Scheduler::Task scheduler_tasks[] PROGMEM = {
{ ins_update, 1, 1000 },
{ one_hz_print, 50, 1000 },
{ five_second_call, 250, 1800 },
};
每个函数名后的第一个数字是调用频率,由ins.init()控制。在这个例子中ins.init()使用RATE_50HZ,所以每个调度为20ms。这意味着ins_update()每20ms被调用,则one_hz_print()函数每50周期调用一次(即每秒一次)和five_second_call()被每250周几调用一次(即每5秒钟一次)。
第三个数字是该函数预计将需要的最长时间。这是为了避免作出调用,除非有足够的时间在此留下调度运行以运行该函数。当scheduler.run()被调用它传递的时间长度(微秒)可用于运行任务,如果时间少于完成这个任务的最长时间,这个任务就不会被执行。
还有一点看仔细是ins.wait_for_sample()调用。那就是“节拍器”驱动在ArduPilot调度。可它会阻止执行的主要载体线程,直到一个新的IMU样本。 IMU样本之间的时间由参数的ins.init()调用控制。
注意,在表AP_Scheduler任务必须具有以下属性:
 它们不应该阻止(除了ins.update()调用)。
 飞行时,他们不应该调用睡眠功能。
 他们应该有可预见的最坏情况下的时间。
现在你应该去修改Scheduler_test例子并添加自己的任务运行。尝试添加执行以下任务:
 阅读气压计。
 阅读指南针。
 读取GPS。
 更新AHRS并打roll/pitch。
看一下例子sketches,你先前在本教程已了解如何使用每个传感器库。
copter_home_001-900x400.jpg

4 个评论

楼主辛苦了,以下几点语句有点别扭.
1)"当一段代码想要的东西发生低于1kHz时就应该保持它自己的“last_called”变量并立即返回,如果没有足够的时间已经过去了。"
这句感觉有点别扭。是不是这样的“如果一个更新函数的更新频率低于1K hz,在函数开始先判断当前时间和上次更新的时间差,如果下次更新的时间还没到,那么函数就立即返回。(实际上每个有update的类都有一个保存上次更新的时间成员变量,每次更新后,该变量被重新设为当前时间)”
2)“在Scheduler.cpp中看看每个AP_HAL里面创建的线程是什么,什么每个线程的优先级实时的。”
-->
“每个硬件平台(apm,px4...)的实现都有Scheduler.cpp这么一个文件,去这个文件看看创建了哪些线程,以及这些线程的实时优先级”
3)”其他AP_HAL端口都具有或多或少的线程取决于需要什么。“ 这里的port不是指端口,我们一般称为”移植“所以这句话意思是”根据不同硬件平台的需要,创建的线程数也或多或少不一样“
太感谢了 学习了
感觉很多话都像有道词典翻译出来的那样生硬。不过还是感谢楼主的辛苦。
学习了

要回复文章请先登录注册