穿线

了解 ArduPilot 线程

在学习了 ArduPilot 库的基本知识后,就该了解 ArduPilot 如何处理线程问题了。从 arduino 继承而来的 setup()/loop() 结构可能会让人觉得 ArduPilot 是一个单线程系统,但事实上并非如此。

ArduPilot 中的线程方法取决于它所适用的电路板。有些板(如 APM1 和 APM2)不支持线程,因此只能使用简单的定时器和回调。有些电路板(PX4 和 Linux)支持具有实时优先级的丰富 Posix 线程模型,ArduPilot 广泛使用这些模型。

在 ArduPilot 中,您需要了解一些与线程相关的关键概念:

  • 定时器回调

  • HAL 专用线程

  • 驱动程序专用线程

  • ardupilot 驱动程序与平台驱动程序的比较

  • 平台特定线程和任务

  • AP_Scheduler 系统

  • 信号灯

  • 无锁数据结构

定时器回调

每个平台都在 AP_HAL 中提供一个 1kHz 的定时器。ArduPilot 中的任何代码都可以注册一个定时器函数,然后以 1kHz 的频率调用该函数。所有注册的定时器函数都会被顺序调用。使用这种非常原始的机制是因为它非常便携,而且非常有用。您可以像这样调用 hal.scheduler->register_timer_process() 来注册定时器回调函数:

.调度->;注册定时器进程(ap_hal_memberproc(及样品;AP_Baro_MS5611::更新));

该示例来自 MS5611 气压计驱动程序。AP_HAL_MEMBERPROC() 宏提供了一种将 C++ 成员函数封装为回调参数的方法(将对象上下文与函数指针捆绑在一起)。

当一段代码希望以小于 1kHz 的频率发生某些事情时,它应该维护自己的 "last_called "变量,并在时间不够时立即返回。您可以使用 hal.scheduler->millis() 和 hal.scheduler->micros() 函数以毫秒和微秒为单位获取启动后的时间,以支持这一点。

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

HAL 专用线程

在支持真正线程的平台上,该平台的 AP_HAL 将创建若干线程来支持基本操作。例如,在 Pixhawk 上会创建以下特定于 HAL 的线程:

  • UART 线程,用于读写 UART(和 USB)

  • 定时器线程支持上述 1kHz 定时器功能

  • IO 线程,支持写入 microSD 卡、EEPROM 和 FRAM

请查看 AP_HAL 实现中的 Scheduler.cpp,了解创建了哪些线程以及每个线程的实时优先级。

如果您有 Pixhawk,那么现在还应该设置一条调试控制台电缆,并将其连接到 nsh 控制台(串行 5 端口)。连接速度为 57600。连接完成后,尝试使用 "ps "命令,您将得到如下结果:

PID PRI SCHD 类型 NP 国家 姓名
 0 0 先进先出 任务 准备就绪 闲置 任务()
 1 192 先进先出 KTHREAD WAITSIG hpwork()
 2 50 先进先出 KTHREAD WAITSIG lpwork()
 3 100 先进先出 任务 跑步 启动()
 37 180 先进先出 任务 WAITSEM AHRS_Test()
 38 181 先进先出 PTHREAD WAITSEM <;pthread>;(20005400)
 39 60 先进先出 PTHREAD 准备就绪 <;pthread>;(20005400)
 40 59 先进先出 PTHREAD WAITSEM <;pthread>;(20005400)
 10 240 先进先出 任务 WAITSEM px4io()
 13 100 先进先出 任务 WAITSEM fmuservo()
 30 240 先进先出 任务 WAITSEM uavcan()

在本例中,您可以看到 "AHRS_Test "线程,它正在运行 libraries/AP_AHRS/examples/AHRS_Test 中的示例草图。还可以看到定时器线程(优先级 181)、UART 线程(优先级 60)和 IO 线程(优先级 59)。

此外,您还可以看到 px4io、fmuservo、uavcan、lpwork、hpwork 和空闲任务。稍后将详细介绍这些任务。

其他 AP_HAL 端口的线程数量有多有少,这取决于需要什么。

线程的一个常见用途是为驱动程序提供一种方法,在不中断主自动驾驶飞行代码的情况下安排慢速任务。例如,AP_Terrain 库需要能够对 microSD 卡进行文件 IO(存储和检索地形数据)。其方法是像这样调用函数 hal.scheduler->register_io_process():

.调度->;register_io_process(ap_hal_memberproc(及样品;AP_Terrain::io_timer));

设置 AP_Terrain::io_timer 函数定期调用。该函数在电路板 IO 线程内调用,这意味着它的实时优先级较低,适用于存储 IO 任务。重要的是,像这样的慢速 IO 任务不能在定时器线程中调用,因为它们会导致更重要的高速传感器数据处理延迟。

驱动程序专用线程

还可以创建特定于驱动程序的线程,以特定于一个驱动程序的方式支持异步处理。目前,您只能以一种依赖于平台的方式创建特定于驱动程序的线程,因此只有当您的驱动程序只打算在一种类型的自动驾驶板上运行时,才适合创建特定于驱动程序的线程。如果希望在多个 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 中还有另一个 MPU6000 驱动程序。

造成这种重复的原因是 PX4 项目已经为 Pixhawk 电路板附带的硬件提供了一套经过良好测试的驱动程序,我们与 PX4 团队在开发和增强这些驱动程序方面保持着良好的合作关系。因此,当我们为 PX4 构建 ArduPilot 时,我们通过编写小型 "垫片 "驱动程序来利用 PX4 驱动程序,这些驱动程序将 PX4 驱动程序与标准 ArduPilot 库接口相连。如果查看 libraries/AP_InertialSensor/AP_InertialSensor_PX4.cpp,您就会看到一个小的 shim 驱动程序,它会询问 PX4 板上有哪些 IMU 驱动程序可用,并自动将所有这些驱动程序作为 ArduPilot AP_InertialSensor 库的一部分。

因此,如果电路板上有 MPU6000,我们在非 Pixhawk/NuttX 平台上使用 AP_InertialSensor_MPU6000.cpp 驱动程序,在基于 NuttX 的平台上使用 AP_InertialSensor_PX4.cpp 驱动程序。

其他 AP_HAL 端口也可能出现相同类型的拆分。例如,我们可以使用 Linux 内核驱动程序来处理 Linux 电路板上的某些传感器。对于其他传感器,我们使用通用的 AP_HAL I2C 和 SPI 接口来使用 ArduPilot "树内 "驱动程序,这些驱动程序适用于各种电路板。

平台特定线程和任务

在某些平台上,启动过程会创建一些基本任务和线程。这些任务和线程与平台密切相关,因此在本教程中,我将重点介绍基于 PX4 的板卡上使用的任务。

在上面的 "ps "输出中,我们看到了一些未被 AP_HAL_PX4 调度程序代码启动的任务和线程。具体来说,它们是

  • 空闲任务 - 在没有其他任务运行时调用

  • init - 用于启动系统

  • px4io - 处理与 PX4IO 协处理器的通信

  • hpwork - 处理基于线程的 PX4 驱动程序(主要是 I2C 驱动程序)

  • lpwork - 处理基于线程的低优先级工作(如 IO)

  • fmuservo - 与 FMU 上的辅助 PWM 输出对话

  • uavcan - 处理 uavcan CANBUS 协议

所有这些任务的启动都由 PX4 控制。 rc.APM 脚本.该脚本在 PX4 启动时运行,负责检测我们使用的是哪种 PX4 板,然后为该板加载正确的任务和驱动程序。这是一个 "nsh "脚本,类似于伯恩 shell 脚本(不过 nsh 要原始得多)。

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

探索 PX4 启动的另一个非常有用的方法是在插槽中不插入 microSD 卡的情况下启动。启动 rcS 脚本在 rc.APM 之前运行,它会检测是否插入了 microSD,如果没有,则在 USB 端口上提供一个裸 nsh 控制台。然后,你就可以在 USB 控制台上手动运行 rc.APM 的所有步骤,了解它是如何工作的。

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

警报音 停止
uorb 启动
mpu6000 启动
mpu6000 信息
mpu6000 测试
挂载 -t binfs /设计/无效 /箱柜
ls /箱柜
敷衍

试试其他驱动程序。看看 /bin 文件中有哪些可用命令。大部分命令的源代码都在 PX4Firmware/src/drivers.请查看 mpu6000 驱动程序,了解其中涉及的内容。

既然说到了线程和任务,那么对 PX4Firmware git 树中线程的简要描述就值得一提了。在 mpu6000 驱动程序中,你会看到这样一行内容:

hrt_call_every(及样品;调用, 1000, 调用间隔, (hrt_callout)及样品;MPU6000::测量蹦床, );

该函数等同于 AP_HAL 中的 hal.scheduler->register_timer_process() 函数,但专门针对 PX4,而且更加灵活。它要求 PX4 的 HRT(高分辨率定时器)子系统每 1000 微秒调用一次 MPU6000::measure_trampoline 函数。

使用 hrt_call_every(),是 SPI 设备驱动程序等运行速度非常快的驱动程序中处理常规事件的常用方法。这些操作通常在禁用中断的情况下运行,最多只需几十微秒。

如果将其与 hmc5883 驱动程序进行比较,就会看到这样一行:

工作队列(HPWORK, 及样品;工作, (工人_t)及样品;HMC5883::蹦床, , 1);

它使用另一种机制来处理常规事件,适用于速度较慢的设备,如 I2C 设备。这样做的目的是将 cycle_trampoline 函数添加到上述 hpwork 线程的工作队列中。在 HPWORK 工作线程内进行的调用应在启用中断的情况下运行,耗时可能长达几百微秒。对于耗时较长的任务,应使用 LPWORK 工作队列,在优先级较低的 lpwork 线程中运行。

AP_Scheduler 系统

要了解 ArduPilot 线程和任务的下一个方面是 AP_Scheduler 系统。AP_Scheduler 库用于划分主飞行器线程内的时间,同时提供一些简单的机制来控制每个操作(AP_Scheduler 中称为 "任务")使用的时间。

它的工作原理是,每种载具实现的 loop() 函数都包含一些执行此功能的代码:

  • 等待新的 IMU 样本到达

  • 在每个 IMU 样本之间调用一组任务

这是一个表格驱动的调度程序,每种载具类型都有一个 AP_Scheduler::Task 表格。要了解其工作原理,请查看 AP_Scheduler/examples/Scheduler_test.cpp 草图

如果你查看该文件,就会看到一个包含 3 个调度任务的小表格。每个任务都有两个数字。表格如下

天电  AP_Scheduler::任务 调度任务[] PROGMEM = {
 { ins_update, 1, 1000 },
 { one_hz_print, 50, 1000 },
 { 五秒呼叫, 250, 1800 },
};

每个函数名称后的第一个数字是调用频率,单位由 ins.init() 调用控制。在本示例草图中,ins.init() 使用 RATE_50HZ,因此每个调度步长为 20 毫秒。这意味着 ins_update() 每 20 毫秒调用一次,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 并打印滚动/螺距

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

Semaphores

当您有多个线程(或定时器回调)时,您需要确保两个逻辑执行线程共享的数据结构以防止损坏的方式更新。在 ArduPilot 中,有 3 种主要方法可以做到这一点,即 semaphores、无锁数据结构和 PX4 ORB。

AP_HAL 信号系统只是对特定平台上可用的信号系统的封装,并提供了一种简单的互斥机制。例如,I2C 驱动程序可以要求使用 I2C 总线信号,以确保一次只能使用一个 I2C 设备。

查看 libraries/AP_Compass/AP_Compass_HMC5843.cpp 中的 hmc5843 驱动程序,查找 get_semaphore() 调用。查看所有用到它的地方,看看能否找出需要它的原因。

无锁数据结构

ArduPilot 代码还包含使用无锁数据结构的示例,以避免使用信号传递器。这比信号传递器的效率要高得多。

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

  • libraries/AP_InertialSensor/AP_InertialSensor_MPU9250.cpp 中的 _shared_data 结构

  • 的环形缓冲区。一个很好的例子是 libraries/DataFlash/DataFlash_File.cpp

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

如果能创建一个通用的环形缓冲器类,以取代 ArduPilot 中多个地方的独立环形缓冲器实现,那将会非常好。如果您想参与其中,请提交拉取请求!

PX4 ORB

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

ORB 提供了一个很好的机制,用于声明将以这种方式共享的结构(所有结构都定义在 PX4Firmware/src/modules/uORB/).然后,代码就可以将数据 "发布 "到这些主题之一,由其他代码接收。

例如,发布执行器值,以便在 Pixhawk 上使用 uavcan ESC。请查看 AP_HAL_PX4/RCOutput.cpp 中的 _publish_actuators() 函数。您会看到它公布了一个 "actuator_direct "主题,其中包含每个电调所需的速度。uavcan 代码会在 PX4Firmware/src/modules/uavcan/uavcan_main.cpp并将新值输出到 uavcan ESC。

与 PX4 驱动程序通信的另外两种常见机制是

  • ioctl 调用(参见 AP_HAL_PX4/RCOutput.cpp 中的示例)

  • /dev/xxx 读/写调用(参见 AP_HAL_PX4/RCOutput.cpp 中的 _timer_tick)

请与 ardupilot 开发团队在 ArduPilot 开发人员讨论区 如果您不确定新代码应使用哪种机制。