Docs » Learning the ArduPilot Codebase » Threading

来源:互联网 发布:vscode颜色主题 编辑:程序博客网 时间:2024/06/05 21:16

原文链接:http://ardupilot.org/dev/docs/learning-ardupilot-threading.html

本页介绍如何安排您编写的一些新代码, 以便间歇运行。

Threading

Understanding ArduPilot threading

  一旦您了解了 ArduPilot 库的基本知识, 您就该了解 ArduPilot 如何处理线程。从 arduino 继承的 setup()/loop() 结构可能使 ArduPilot 看起来像是一个单线程系统, 但实际上它不是。
  ArduPilot 中的线程处理方法取决于它所构建的主板。某些主板 (如 APM1 和 APM2) 不支持线程, 因此请使用简单的计时器和回调。一些主板 (PX4 和 Linux) 支持一个具有实时优先级的丰富的 Posix 线程模型, 它们被 ArduPilot 广泛使用。
  在 ArduPilot 中, 有许多与线程相关的关键概念需要了解:

  • 定时器回调
  • HAL 特定线程
  • driver 特定线程
  • ardupilot 驱动程序与平台驱动程序
  • platform 特定的线程和任务
  • AP_Scheduler 系统
  • semaphores
  • lockless 数据结构

The timer callbacks

每个平台在 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” 变量, 并立即返回, 如果没有足够的时间已经过去了。您可以使用 hal.scheduler->millis() 和 hal.scheduler->micros() 函数以获得时间, 因为启动以毫秒和微秒来支持这一点。
现在, 您应该去修改现有的示例sketch (或创建一个新的sketch ) 并添加一个计时器回调。使计时器递增一个计数器, 然后在loop() 函数中每秒打印计数器的值。修改函数, 使计数器每25毫秒递增一次。

HAL specific threads

在支持实际线程的平台上, 该平台的 AP_HAL 将创建许多支持基本操作的线程。例如, 在 PX4 上, 将创建以下 HAL 特定线程:

  • UART 线程, 读写 uart (和 USB)
  • 定时器线程, 支持上面描述的1kHz 定时器功能
  • IO 线程, 支持写入 microSD 卡、EEPROM 和电存储器

在每个 AP_HAL 实现中查看 Scheduler.cpp, 查看创建的线程以及每个线程的实时优先级。
如果您有一个 Pixhawk, 那么您现在还应该设置一个debug console将杜邦线连接到 nsh console(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” 线程, 它正在运行来自库/AP_AHRS/示例/AHRS_Test 的示例草图。您还可以看到计时器线程 (优先级 181)、UART 线程 (优先级 60) 和 IO 线程 (优先级 59)。
此外, 您还可以看到 px4io、fmuservo、uavcan、lpwork、hpwork 和闲置任务。更多关于那些之后介绍。
其他 AP_HAL 端口或多或少有线程, 具体取决于所需的内容。
线程的一个常见用途是为驱动程序提供一种在不中断主自动驾驶仪飞行代码的情况下调度慢速任务的方法。例如, AP_Terrain 库需要能够对 microSD 卡执行文件 IO (存储和检索地形数据)。它的方式是它调用函数 hal.scheduler->register_io_process() 像这样:

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

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

Driver specific threads

还可以创建驱动程序特定的线程, 以特定于一个驱动程序的方式支持异步处理。目前, 您只能以与平台相关的方式创建驱动程序特定的线程, 因此仅当您的驱动程序只在一种自动驾驶仪板上运行时才适用。如果您希望它在多个 AP_HAL 目标上运行, 那么您有两个选择:

  • 可以使用 register_io_process () 和 register_timer_process () 调度程序调用来使用现有的计时器或 io 线程
  • 可以添加一个新的 HAL 接口, 它提供了在多个 AP_HAL 目标上创建线程的通用方法 (请将修补程序返回)

驱动程序特定线程的一个示例是 Linux 端口上的 ToneAlarm 线程。请看AP_HAL_Linux/ToneAlarmDriver.cpp

ArduPilot drivers versus platform drivers

ArduPilot 驱动程序与平台驱动程序
您可能会注意到 ArduPilot 中的一些驱动程序重复。例如, 我们在库/AP_InertalSensor/AP_InertialSensor_MPU6000. cpp 中有一个 MPU6000 驱动程序, 在 PX4Firmware/src/drivers/mpu6000 还有另一个 MPU6000 驱动程序。
造成这种重复的原因是, PX4 项目已经提供了一套经过测试的驱动程序, 用于 PX4 主板附带的硬件, 我们与 PX4 项目开发和增强这些驱动程序的合作关系很好。因此, 当我们建立 ArduPilot 的 PX4, 我们利用 PX4 驱动程序编写小 “填充” 驱动程序, 提出 PX4 驱动程序与标准的 ArduPilot 库接口。如果你看看libraries/AP_InertialSensor/AP_InertialSensor_PX4. cpp, 你会看到一个小的填充驱动程序, 询问 PX4 在这个主板上有哪些惯性驱动程序, 并自动使它们都可以作为 ArduPilot AP_InertialSensor 库的一部分使用。
因此, 如果我们有一个 MPU6000 在板上, 我们使用 AP_InertialSensor_MPU6000 的 non-PX4 平台上的 cpp 驱动程序, 以及 AP_InertialSensor_PX4 的基于 PX4 的平台上的 cpp 驱动程序。
其他 AP_HAL 端口也会发生相同类型的拆分。例如, 我们可以对 linux 主板上的一些传感器使用 linux 内核驱动程序。对于其他传感器, 我们使用通用的 AP_HAL I2C 和 SPI 接口来使用 ArduPilot “in-tree” 驱动程序, 它可以跨多种主板工作。

Platform specific threads and tasks

特定于平台的线程和任务
在某些平台上, 将有许多基本任务和线程将由启动过程创建。这些都是非常特定的平台, 因此, 为了本教程, 我将集中在基于 PX4 的主板上使用的任务。
在上面的 “ps” 输出中, 我们看到了 AP_HAL_PX4 调度程序代码未启动的许多任务和线程。具体地说, 它们是:

  • idle task - 当没有别的可以运行时调用
  • init - 用于启动系统
  • px4io - 处理与 PX4IO 处理器的沟通
  • hpwork - 处理基于 PX4 驱动程序的线程(主要是 I2C 驱动程序)
  • lpwork - 处理基于低优先级工作的线程 (eg.io)
  • fmuservo - 处理在FMU上的辅助 PWM输出
  • uavcan - 处理 uavcan 总线协议

所有这些任务的启动由 PX4 特定的rc.APM script脚本控制。该脚本在PX4 启动时运行, , 负责检测我们使用的 PX4 板的种类, 然后为该板加载正确的任务和驱动程序。这是一个 “nsh” 脚本, 这是类似于bourne 的脚本 (虽然 nsh 是更原始的)。
作为练习, 请尝试编辑 rc.APM 脚本, 并添加一些睡眠和echo命令。然后在主板启动时上载新固件并连接到调试控制台,您的echo 命令会显示在控制台上。
另一种探索启动 PX4 的非常有用的方法是在插槽中没有 microSD 卡的情况下启动。rcS 脚本, 它在 rc.APM之前运行。检测是否插入了 microSD, 并在 USB 端口上为您提供一个裸 nsh 控制台 (如果没有)。然后, 您可以手动运行 rc.APM的所有步骤。自己在 USB 控制台上, 了解它是如何工作的。
在引导没有 microSD 卡的 Pixhawk 并连接到 USB 控制台之后, 请尝试以下练习:

tone_alarm stopuorb startmpu6000 startmpu6000 infompu6000 testmount -t binfs /dev/null /binls /binperf

尝试与其他驱动程序玩。看看有什么可用。大多数这些命令的源代码都在 PX4Firmware/src/drivers 中。查看一下 mpu6000 驱动程序, 了解所涉及的内容。
考虑到线程和任务的主题, PX4Firmware git 树中的线程的简短描述值得一提。如果您查看 mpu6000 驱动程序, 您将看到如下所示的行:

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

这相当于 hal.scheduler->register_timer_process()函数在 AP_HAL, 但 PX4 具体, 也更灵活。据说, 希望 PX4 的 HRT (高分辨率定时器) 子系统每1000微秒调用一次 MPU6000::measure_trampoline 函数。
使用 hrt_call_every () 是在运行速度非常快的驱动程序 (如 SPI 设备驱动程序) 中用于常规事件的常用方法。这些操作通常是在禁用中断的情况下运行的, 最多只需要几十微秒。
如果将其与 hmc5883 驱动程序进行比较, 您将看到这样的一行:

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

它使用了适用于较慢设备 (如 I2C 设备) 的常规事件的替代机制。它所做的是将 cycle_trampoline 函数添加到您在上面看到的 hpwork 线程中的工作队列中。在 HPWORK 的工作人员中发出的呼叫应该在启用中断的时候运行, 并且可能需要几百微秒。对于需要使用比 LPWORK 工作队列更长的任务, 在较低优先级 LPWORK 线程中运行它们。

The AP_Scheduler system

ArduPilot 线程和任务要理解的下一个方面是 AP_Scheduler 系统。AP_Scheduler 库用于在主vehicle线程中划分时间, 同时提供一些简单的机制来控制每个操作所用的时间 (在 AP_Scheduler 中称为 “任务”)。
它的工作方式是, 每个vehicle实现的loop() 函数包含一些代码, 这样做:

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

它是一个驱动调度程序表,每种vehicle都有一个AP_Scheduler::Task table。要了解它的工作原理, 看看 AP_Scheduler/examples/Scheduler_test.cpp sketch。
如果您查看该文件, 您将看到一个小表, 其中有一组3计划任务。与每个任务关联的是两个数字。该表类似于:

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 中驱动调度的 “节拍器”。它阻止执行主vehicle线程, 直到一个新的IMU采样可用。IMU采样之间的时间由 ins.init()调用的参数决定。
请注意, AP_Scheduler 表中的任务必须具有以下属性:

  • 他们不应该被阻止(除了ins.update()的调用)
  • 他们不应该在飞行时调用睡眠功能 (自动驾驶仪, 像一个真正的飞行员, 不应该在飞行时睡觉)
  • 他们应该有可预知的最坏情况的时候

现在, 您应该去修改 Scheduler_test 示例, 并将自己的任务添加到运行中。请尝试添加执行下列操作的任务:

  • read the barometer
  • read the compass
  • read the GPS
  • update the AHRS and print the roll/pitch

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

Semaphores

当您有多个线程 (或计时器回调) 时, 需要确保以防止损坏的方式更新由两个逻辑执行线程共享的数据结构。在ArduPilot 中有3种原理方法:信号、lockless 数据结构和 PX4 ORB 中 。
AP_HAL 信号量只是在特定平台上可用的任何信号系统的封装, 并提供了一种简单的互斥机制。例如, I2C 驱动程序可以要求 I2C 总线信号量, 以确保一次只使用一个 I2C 设备。
去看看libraries/AP_Compass/AP_Compass_HMC5843.cpp , 并寻找 get_semaphore () 调用。看看它所使用的所有地方, 看看你是否能找出为什么需要它。

Lockless Data Structures

ArduPilot 代码还包含使用 lockless 数据结构以避免信号量需要的示例。这可能比信号量更有效。
ArduPilot 中的 lockless 数据结构的两个示例是:

  • 在libraries/AP_InertialSensor/AP_InertialSensor_MPU9250.cpp里的_shared_data 结构
  • 在许多地方使用的环形缓冲器。一个很好的例子是libraries/DataFlash/DataFlash_File.cpp

去看看这两个例子, 并证明给自己看, 它们对并发访问是安全的。DataFlash_File 看 _writebuf_head 和 _writebuf_tail 变量的使用。
创建一个可以使用的通用环缓冲区类将是很好的, 而不是在 ArduPilot的几个地方单独的 实现ringbuffer。如果你想贡献, 那么请做一个拉请求!

The PX4 ORB

这类机制的另一个例子是 PX4 ORB。ORB (对象请求代理) 是一种使用在多线程环境中安全的发布/订阅模型提供从系统的一个部分到另一个 (eg. 设备驱动程序->> vehicle代码) 的数据的方法。
ORB 提供了一个很好的机制, 用于声明将以这种方式共享的结构 (在 PX4Firmware/src/modules/uORB/中定义)。然后, 代码可以将数据 “发布” 到这些主题之一, 这是由其他代码片段拾取的。
一个例子是发布执行器的值, 所以 uavcan ESCs可用于 Pixhawk。看看 AP_HAL_PX4/RCOutput. cpp 中的 _publish_actuators () 函数。您将看到它公布了一个 “actuator_direct” 主题, 其中包含每个 ESC 所需的速度。uavcan 代码在PX4Firmware/src/modules/uavcan/uavcan_main.cpp基础上更改,并将新值输出到 uavcan ESCs。
与 PX4 驱动程序通信的其他两种常见机制有:

  • ioctl 调用 (请参见 AP_HAL_PX4/RCOutput. cpp 中的示例)
  • /dev/xxx read/write调用 (请参见中的AP_HAL_PX4/RCOutput.cpp中的_timer_tick )

如果您不确定新代码使用哪种机制, 请与无人机的 ardupilot 开发小组讨论邮件列表。

原创粉丝点击