APM官方文档翻译

线程

理解ArduPilot线程

一旦您了解了ArduPilot库的基本知识,就该了解ArduPilot如何处理线程了。从arduino继承的setup()/loop()结构可能使ArduPilot看起来像是一个单线程系统,但实际上不是。ArduPilot中的线程处理方法依赖于它为之构建的板。有些板(例如APM1和APM2)不支持线程,因此只能使用简单的计时器和回调。一些板(PX4和Linux)支持具有实时优先级的富Posix线程模型,ArduPilot广泛使用这些优先级。

ArduPilot中的线程处理方法依赖于它为之构建的板。有些板(例如APM1和APM2)不支持线程,因此只能使用简单的计时器和回调。一些板(PX4和Linux)支持具有实时优先级的富Posix线程模型,ArduPilot广泛使用这些优先级。

在ArduPilot中,有许多与线程相关的关键概念需要理解:

  • 定时器回调函数
  • HAL特定线程
  • 驱动程序特定线程
  • ardupilot驱动程序与平台驱动程序
  • 平台特定的线程和任务
  • AP调度程序系统
  • 信号量
  • 无锁的数据结构

定时器回调函数

每个平台在AP HAL中提供一个1kHz定时器。ArduPilot中的任何代码都可以注册一个定时器函数,然后以1kHz调用该函数。所有注册的定时器函数都是按顺序调用的。之所以使用这种非常原始的机制,是因为它具有极强的可移植性,而且非常有用。你可以像这样注册一个定时器并回调hal.scheduler->register_timer_process():

hal.scheduler->register_timer_process(AP_HAL_MEMBERPROC(&AP_Baro_MS5611::_update));

这个特殊的例子来自MS5611气压计驱动程序。AP HAL_MEMBERPROC()宏提供了一种将c++成员函数封装为回调参数的方法(将对象上下文与函数指针绑定)

当一段代码想要某事发生在小于1kHz,那么它应该维护自己的“last called”变量,并立即返回,如果没有足够的时间过去。您可以使用half .scheduler->millis()和half .scheduler->micros()函数来获取自启动以来的时间(毫秒和微秒),以实现目的。

您现在应该去修改一个已有的示例草图(或者创建一个新的)并添加一个定时器回调。让定时器增加一个计数器,然后在loop()函数中打印计数器每秒时刻下的值。修改函数,使计数器每25毫秒增加一次。

HAL特定线程

在支持实际线程的平台上,该平台的AP-_HAL将创建大量线程来支持基本操作。例如:在Pixhawk上创建了如下的HAL线程:UART的线程。阅读和书写UARTs(和USB)定时器线程,它支持上面描述的:

  • UART线程,用于读写UARTs(和USB)
  • 定时器线程,它支持上面描述的1kHz定时器功能
  • IO线程,它支持写入到microSD卡,EEPROM和FRAM

如果您现在有一个Pixhawk,那么您现在应该设置一条调试控制台并附加到nsh控制台(serial5端口)。连接在57600。当你已经连接,尝试“ps”命令ad,你会得到这样的东西:

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”线程,它正在运行库/AP_AHRS/examples/AHRS_Test中的示例。您还可以看到计时器线程(优先级181)、UART线程(优先级60)和10线程(优先级59)。

此外,您可以看到px4io, fmuservo。uavcan lpwork。hpwork和idle任务。稍后会详细介绍。

其他的AP HAL端口有或多或少的线程,这取决于需要什么。

线程的一个常见用途是为驱动程序提供一种方法,在不中断main autopilot flight code 的情况下安排slow tasks。例如:AP Terrain(地形库)需要能够对microSD卡执行文件IO(以存储和检索地形数据)。它这样做的方式是调用函数hal.scheduler->register_io_process(),如下:

hal.scheduler->register_io_process(AP_HAL_MEMBERPROC(&AP_Terrain::io_timer));

设置AP地形:io定时器函数定期调用。那被称为在板子内的IO线程,意味着它是一个低实时优先级,适合存储的IO任务。重要的是,像这样的慢lO任务不要在定时器线程上调用,因为它们会在高速传感器数据的更重要的处理中造成延迟。

驱动程序特定线程

也可以创建特定于驱动程序的线程,以特定于某个驱动程序的方式支持异步处理。目前,您只能以一种依赖于平台的方式创建驱动程序特定的线程;因此,这只适用于您的驱动程序只打算在一种类型的自动驾驶板上运行。如果你想让它运行在多个AP HAL目标,然后你有两个选择:

  • 您可以使用register io process()和register timer
    process()调度器调用来使用现有的计时器或io线程
  • 您可以添加一个新的HAL接口,它提供了在多个AP_HAL目标上创建线程的通用方法(请提供补丁)

驱动程序特定线程的一个例子是Linux端口中的ToneAlarm线程。参考:AP HAL_Linux/ToneAlarmDriver.cpp

ArduPilot驱动程序与平台驱动程序

您可能会在ArduPilot中注意到一些重复的驱动程序。例如。我们在libraries/AP_InertalSensor/AP_InertialSensor_MPU6000.cpp,还有一个MPU6000的驱动在PX4Firmware/src/drivers/mpu6000

造成这种重复的原因是,PX4项目已经为Pixhawk主板提供了一套经过良好测试的硬件驱动程序,在开发和增强这些驱动程序方面,我们与PX4团队有着良好的合作关系。因此,当我们为PX4构建ArduPilot时,我们通过编写小的“shim”驱动程序来利用PX4驱动程序,这些驱动程序使用标准的ArduPilot library接口来呈现PX4驱动程序。如果你查看libraries/AP InertialSensor/AP InertialSensor PX4.cpp,您会看到一个小的shim驱动,它会询问PX4在这个板上有哪些IMU驱动程序可用,并自动使所有这些驱动程序作为ArduPilot AP惯性传感器库的一部分可用。所以如果我们有一个MPU6000板,我们使用AP InertialSensor MPU6000.cpp驱动在非Pixhawk/NuttX平台和基于NuttX平台的AP InertialSensor PX4.cpp驱动程序。其他AP_HAL端口也类似。例如,我们可以为Linux板上的一些传感器使用Linux内核驱动程序;对于其他传感器,我们使用通用的AP HAL I2C和SPl接口,以使用ArduPilot“in-tree”驱动程序,使得工作可以跨越不同的硬件电路板。

平台特定的线程和任务

在一些平台上,启动进程将创建许多基本任务和线程。这些都是特定于平台的,因此对于本教程,我将集中讨论基于PX4板上使用的任务。

在上面的“ps”输出中,我们看到了许多没有由AP HAL PX4调度程序代码启动的任务和线程。具体来说:

  • idle任务 – 在没有其他东西运行
  • init-用于启动系统
  • px4io - handle与PX4IO的协同处理器
  • hpwork-handle线程基于PX4驱动程序(主要是I2C驱动程序)
  • lpwork-handle线程基于低优先级的工作(例如:IO)
    fmuservo - 处理talking的auxillary PWM输出上的FMU(原文:fmuservo - handle talking to the auxillary PWM outputs on the FMU
  • uavcan - 处理CANBUS协议

所有这些任务的启动都由PX4 specifc rc控制。该脚本在PX4引导时运行,负责检测我们使用的PX4板的类型,然后为该板加载正确的任务和驱动程序。它是一个“nsh”脚本,类似于bourne shell脚本(尽管nsh更原始)。

作为练习,试着编辑rc.APM脚本,并添加一些sleep和echo命令。然后上传一个新的固件并在主板启动时连接到调试控制台。您的echo命令应该显示在控制台上。

探索PX4启动的另一种非常有用的方法是引导槽中没有microSD卡。rcS script,它在rc.APM之前运行。检测microSD是否被插入,并在USB端口上提供一个裸nsh控制台(如果不是)。然后您可以手动运行rc的所有步骤。自己在USB控制台上APM,了解它是如何工作的。

在启动没有microSD卡的Pixhawk并连接到USB控制台后,请尝试以下练习:

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驱动程序,了解一下所涉及的内容。

既然我们讨论的主题是线程和任务,那么对px4固件git树中的线程的简要描述就值得一提了。如果你查看mpu6000驱动程序,你会看到这样一行:

hrt_call_every(&_call, 1000, _call_interval, (hrt_callout)&MPU6000::measure_trampoline, this);

这相当于AP_HAL中的hall .scheduler->register_timer_process()函数,但它是特定于PX4的,而且更加灵活。意思是,它希望PX4的HRT(高分辨率定时器)子系统每1000微秒调用MPU6000:measure_trampoline函数。

如果您将此与hmc5883驱动程序进行比较,您将看到这样的一行:

work_queue(HPWORK, &_work, (worker_t)&HMC5883::cycle_trampoline, this, 1);

它为常规事件使用了一种替代机制,这种机制适合于较慢的设备,比如I2C设备。它所做的是将cycle_trampoline函数添加到上文您看到的hpwork线程中的一个工作队列中,在hpwork workers中发出的调用应该在启用中断的情况下运行,可能会花费几百微秒的时间。对于需要花费比LPWORK工作队列更长时间的任务,它在较低优先级的LPWORK线程中运行它们。

AP调度程序系统

要理解ArduPilot线程和任务的下一个方面是AP调度器系统。AP调度程序库用于分配主vehicle线程的时间,同时提供一些简单的机制来控制每个操作(称为“tasks”) in AP_Scheduler)。

它的工作方式是,每个vehicle实现的loop()函数包含以下代码:

  • 等待新的IMU采样到达
  • 在每个IMU采样之间调用一组任务

它是一个table驱动调度程序,并且每种vehicle类型都有一个 AP_Scheduler::Task table。要了解其工作原理,请查看AP_Scheduler/examples/Scheduler_test.cpp sketch.

如果您查看该文件内部,您将看到一个包含3 scheduling tasks。与每个任务相关的是两个数字。表格是这样的:

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次(每秒1次)。和five- seconds _call()每250次调用一次(即:每5秒一次)。
原文:The first number after each function name is the call frequency, in units controlled by the ins.init() call. For this example sketch the ins.init() uses RATE_50HZ, so each scheduling step is 20ms. That means the ins_update() call is made every 20ms, the one_hz_print() function is called every 50 times (ie. once a second) and the five_second_call() is called every 250 times (ie. once every 5 seconds).
对这段话的理解:ins_update调用一次是需要20ms,即50hz的频率运行ins_update;而one_hz_print的50是表示相对于ins_update的20ms;因此;one_hz_print的运行周期的1s一次;同理,five_second_call的250是相对于ins_update的20ms而言,所以原文档才说是每五秒运行一次。不知道有没有理解错误,如果有错,望各位大佬指出,谢谢!

第三个数字是函数预期花费的最大时间。这用于避免调用,除非调度运行中还有足够的时间来运行函数。当调用scheduler.run()时,将传递可用于运行任务的时间量(以us为单位),如果该任务的最坏情况时间意味着在时间耗尽之前无法满足要求,则不会调用该任务。

另一个需要密切关注的是ins.wait_for_sample()调用。这就是在ArduPilot中驱动调度的“metronome”(软件翻译为:节拍器)。它阻塞主vehicle线程的执行,直到有一个新的IMU sample可用为止。IMU sample之间的时间间隔由ins.init()调用的参数控制。

注意, AP_Scheduler tables (AP调度表)中的任务必须有以下属性:

  • 它们不应该阻塞(除了in .update()调用)
  • 它们绝不应该在飞行时调用sleep functions(这就像真正的飞行员一样,自动驾驶仪在飞行时也不应入睡)
  • 它们应该有可预测的最坏情况时机

现在,您应该修改Scheduler_test示例,并添加您自己要运行的任务。尝试添加以下任务:

  • 读指气压计
  • 读指南针
  • 读取GPS
  • 更新AHRS并打印 roll/pitch

查看本教程前面使用的每个库的示例草图,以了解如何使用每个传感器库。

信号量(Semaphores)

当您有多个线程(或定时器回调)时,您需要确保由两个逻辑执行线程共享的数据结构以防止损坏的方式更新。在ardupilot中有三种基本的实现方式 – 信号量、无锁数据结构和PX4 ORB。

AP HAL信号量只是特定平台上可用的信号量系统的包装器。并提供一个简单的互斥机制。例如:I2C驱动程序可以要求l2C总线信号量,以确保每次只使用一个I2C设备。

在库/AP Compass/AP Compass_HMC5843.cpp中查看hmc5843驱动程序,并查找getsemaphore()的调用;看看所有使用它的地方,看看你是否能找出为什么需要它。

无锁的数据结构

ArduPilot代码还包含使用无锁数据结构以避免需要信号量的示例;这比信号量更有效。

ArduPilot中无锁定数据结构的两个例子是:

  • the_shared_data结构在/AP_InertialSensor/AP_InertialSensor_MPU9250.cpp
    ring缓冲器在许多地方使用。一个很好的例子是库/数据闪光/DataFlash_File.cpp
    库中的_shared_data结构/AP_InertialSensor/AP_InertialSensor_MPU9250.cpp
    环缓冲器在许多地方使用。
    一个很好的例子是libraries/DataFlash/DataFlash_File.cpp

看看这两个例子,并证明它们对于并发访问是安全的。
对于DataFlash_File,请查看_writebuf_head和_writebuf_tail变量的使用情况。

最好创建一个通用的环形缓冲区类,它可以在ArduPilot的几个地方使用,而不是单独的ringbuffer实现。如果你想贡献,那么请做一个pull request!

PX4 ORB

这种机制的另一个例子是PX4 ORB,ORB(对象请求代理)是一种将数据从系统的一个部分提供给另一个部分的方法。(例如:设备驱动程序->车辆代码)使用发布/订阅模型,在多线程环境中是安全的。

ORB提供了一种很好的机制来声明将以这种方式共享的结构(全部在PX4Firmware/src/modules/uORB/中定义)。然后,代码可以将数据“发布”到这些主题中的一个,这些主题由其他代码片段获取。

一个示例是发布执行器值,以便在Pixhawk上使用uavcan ESCs。看一下AP HAL PX4/RCOutput.cpp中的publish actuators()函数。您将看到它改变了一个“执行器直接”主题。其中包含每个ESC所需的速度。uavcan在PX4Firmware / src / modules / uavcan / uavcan_main.cpp中为这些监视代码编写了有关此主题的更改,并将新值输出到uavcan ESC。

与PX4驱动程序进行通信的其他两种常见机制是:

  • ioctl调用(参见AP HAL PX4/RCOutput.cpp中的示例)
  • /dev/xxxread/write调用(参见定时器tickin AP HAL PX4/RCOutput.cpp)

如果您不确定为新代码使用哪种机制,请在ArduPilot Gitter General Chat 上与ardupilot开发团队进行沟通。

上一篇:PX4 四元数旋翼姿态控制修正


下一篇:Webstorm less watcher 配置