存储和 EEPROM 管理

ArduPilot 支持的每块电路板都有某种形式的持久存储。它用于保存用户参数、航点、集结点、地形数据和许多其他有用的东西。ArduPilot 有四种基本机制来访问这些存储空间:

  • AP_HAL::Storage 对象,访问方式为 hal.storage

  • StorageManager 库为 hal.storage 提供了一个更高层次的抽象层。

  • 数据闪存,用于存储到机载记录区

  • 在支持 Posix IO 功能的电路板上,将 Posix IO 功能应用于传统文件系统(例如 microSD 卡上的 VFAT

其他需要持久存储的库和函数都建立在这些基本系统之上。例如,AP_Param 库(用于处理用户可设置的参数)建立在 StorageManager 之上,而 StorageManager 又建立在 AP_HAL::Storage 之上。AP_Terrain 库(用于处理地形数据)建立在用于保存地形数据库的 Posix IO 函数之上。

AP_HAL::Storage 库

AP_HAL::Storage 对象适用于所有平台。在 ArduPilot 支持的板上,通过此接口提供的最小存储空间为 4096 字节。有些电路板提供的空间更大,例如 PX4v1 有 8k 的 EEPROM,Pixhawk 有 16k 的 FRAM。所有这些都隐藏在 AP_HAL::Storage API 之后。

hal.storage API 非常简单。它只有 3 个函数

  • init() 启动存储子系统

  • read_block() 读取字节块

  • write_block() 写入一个字节块

之所以使用这个非常简单的 API,是因为我们鼓励开发人员使用 StorageManager API 而不是 hal.storage。只有在调用新电路板或调试时,您才需要深入研究 hal.storage。

可用存储空间的大小在 AP_HAL/AP_HAL_Boards.h 宏 HAL_STORAGE_SIZE 中。这意味着我们还不支持动态确定该接口的可用存储空间大小。如果需要动态确定存储空间大小,目前需要使用 Posix IO。

我们没有 AP_HAL::Storage API 的示例草图,因此这是您编写示例草图的机会。如果您已经在 ArduPilot 教程中学习了这么多,那么您应该已经看过足够多的示例草图,知道如何从头开始编写一个。因此,请编写一个 libraries/AP_HAL/examples/Storage 示例,计算 hal.storage 数据全部内容的 8 位 XOR,并将其打印到控制台。然后 将示例作为补丁提交 到 ArduPilot github,并注意遵守补丁提交指南。

我很想知道,在我把这个练习添加到教程中多久后,我们才会收到提交的 ....。

存储管理程序库

简单的 hal.storage API 可以轻松将 ArduPilot 移植到新电路板上,但在实际使用可用存储空间时并不方便。为此,我们有了 StorageManager。StorageManager 库提供的 API 将存储抽象为具有指定用途的伪连续数据块。因此,我们将可用的存储空间分割成多个区域,以提供以下功能:

  • 参数

  • 栅栏点

  • 航点

  • 拉力点

  • 签名密钥

  • RC 绑定信息

请阅读 libraries/StorageManager/StorageManager.cpp.请特别查看顶部的表格。请注意,对于存储量较大的系统,每种类型都定义了多个区域。这种将多个非连续存储区域合并为一个逻辑存储区域的能力是添加 StorageManager 的主要原因。它使我们能够从在所有板卡上使用 4k 存储空间发展到使用每个板卡上的全部可用存储空间,而无需要求用户重新加载所有参数或重新加载航点。

在 ArduPilot 中,避免用户重新配置自动驾驶板是一个常见的主题。我们希望用户能更新到最新固件,并且之前设置的所有功能都能继续使用,同时还能获得新功能。有时,这意味着作为开发人员,我们必须做更多的工作才能实现这一点--例如在 StorageManager 中发生的情况。

StorageManager API 还提供了读写整数等变量的便捷函数。这就是 AP_Mission 等库用来保存和恢复航点的 API。

现在去看看 libraries/StorageManager/examples/StorageTest.cpp.这是对 StoageManager 层的压力测试,因此也是对 AP_HAL::Storage 对象的压力测试。它以随机偏移和随机长度进行随机 IO。这意味着它进行的 IO 会跨越 FRAM 或 EEPROM 中单个参数值可能不连续的边界。该测试草图旨在对 StorageManager API 进行压力测试,但在将 ArduPilot 移植到新电路板时也非常有用,因为它能很好地对 EEPROM 访问功能进行压力测试。

请在您的电路板上尝试 StorageTest,但请注意这是一项破坏性测试。它不会破坏你的板子,但会清除你所有的参数和航点。因此,如果您要在您最喜欢的飞机上进行板载测试,请备份您的配置。

作为练习,为 StorageTest 添加一些剖析功能,以便打印以千字节/秒为单位的总 IO 速率以及读取和写入的 IO 速率。你是否注意到读写速率之间的差异?你是否注意到写入已设置在该地址的存储值的速度?看看能否在 StorageManager 中找到代码来解释你的观察结果。然后 提交补丁 将剖析输出添加到 ArduPilot github。

接下来搜索 ArduPilot 库,找出所有使用 StorageManager 的地方。您要找的是 StorageAccess 句柄,如下所示:

存储访问 AP_Param::存储(存储管理器::存储参数);

声明了一个名为 AP_Param::_storage 的变量,该变量提供了对该电路板上存储空间的 StorageParam 区域的访问。哪些库使用 StorageAccess?

DataFlash 库

(飞行)控制器需要的另一种存储方式是机载日志。对于 ArduPilot 而言,这是由 DataFlash 库提供的。这个库在很多方面都很奇怪。首先,它的名字就很奇怪,因为它最初是围绕 APM1 的 DataFlash 芯片设计的。它是一个硬件设备驱动程序库,后来逐渐演变成一个通用日志系统。从内部结构上看,该库从几个方面(而且不是好的方面!)展示了这段历史。

如今,DataFlash应用程序接口是围绕日志基础结构模型设计的。它允许您为单个日志信息定义自描述数据结构--例如记录来自 GPS 传感器的数据的 "GPS "信息。它能以高效的方式将数据记录到持久存储中,还能为其他库提供应用程序接口,以便在飞行结束后用户希望下载日志文件时将数据取回。

如果您在下载日志时看到过 ArduPilot 现在使用的 "*.bin "文件,那么您就会看到 ArduPilot 用来存储日志信息的格式。这是一种 "自描述 "格式,这意味着地面站无需使用某种通用方案,就能确定日志文件中的信息格式。每个日志文件的前面都有一组 FMT 信息,这些信息具有众所周知的格式,并描述了后面信息的格式。

去看看 libraries/AP_FlashStorage/examples/FlashTest/FlashTest.cpp.你会看到顶部的一个小表,它定义了我们要写的日志信息,本例中的 "TEST "信息包含 4 个无符号 16 位整数和 2 个有符号 32 位整数(这就是 "HHHHHii "的意思)。它还给出了这 6 个变量的名称(巧妙地标记为 V1 至 V4 以及 L1 和 L2)。

在 loop() 函数中,你会看到这样一个相当奇怪的调用:

数据闪存.获取日志边界(log_num, 启动, 最后);

这是 DataFlash 库隐藏电路板实际存储日志文件方式的公共 API。在具有 Posix IO 的系统中(如 Pixhawk 或 Linux),日志文件以单独文件形式存储在 microSD 卡的 "LOGS "目录中。用户可以拔出 microSD 卡并将其插入个人电脑,直接复制这些文件。

在 APM2 这样的电路板上,事情就没那么简单了。APM2 在 DataFlash 芯片上有 4 兆字节的存储空间,可通过 SPI 接口访问。接口本身是面向页面的,因此您需要填充一个 512 字节(也可能是 528 字节!)的页面,然后告诉芯片将该页面复制到持久存储区,同时填充下一个页面。在 DataFlash 上进行随机 IO 并不好--它是为需要连续写入的代码设计的,而这正是日志记录时会发生的情况。与(飞行)控制器喜欢记录的数据量相比,4 兆字节的大小并不算大,因此我们还需要处理数据填满时的包装问题。

所有这些复杂性都隐藏在一个 API 背后,该 API 提出了 "日志编号 "的概念,而日志编号只是(飞行)控制器在一次飞行中写入的一串字节。APM1 和 APM2 上的 DataFlash 实现在每个页面的前端使用小标记字节来说明正在写入的日志编号。这些日志编号与用户要求检索日志时下载的日志编号相对应。

Posix IO

ArduPilot 支持的一些自动驾驶板基于具有类似 Posix API 的操作系统。例如,Linux 端口有非常好的 Posix 子系统,用于 PX4 的 NuttX 操作系统(如 Pixhawk)也有相当合理的 Posix 层。您可以在 ArduPilot 的库中利用这一点,只要您不依赖它来做任何必须在所有板上都能工作的事情。

保存地形数据的 AP_Terrain 库就是一个很好的例子。地形数据非常庞大,EEPROM 无法容纳,而且它是随机存取的,因此不适合使用 DataFlash。它对于(飞行)控制器的基本功能也不是必不可少的,因此非常适合使用 Posix IO 来实现。

我们使用 Posix IO 的方法是,首先检查电路板是否支持 Posix IO,方法是检查电路板上的 HAVE_OS_POSIX_IO 宏。 AP_HAL_Boards.h.然后,为了知道应该将数据存储在文件系统的哪个位置,需要在 AP_HAL_Boards.h 中添加一个数据特定宏,该宏给出了应该放置此类数据的目录路径。例如,宏 HAL_BOARD_TERRAIN_DIRECTORY 用于定义地形数据的存放目录。

有了这两样东西之后,你就可以使用普通的 Posix IO 函数(即打开、关闭、读取、写入等),不过也有一些注意事项:

  • 只能从 IO 定时器或自己的低优先级线程调用 IO 函数。

  • 切勿从库中可直接访问的 API 中调用任何 IO 函数。即使是 stat() 这样的简单函数也不行。

  • 尽量对慢速存储卡友好,以合理大小的块进行 IO,并尽可能避免寻道

这些规则非常重要。在 Pixhawk 上对 microSD 卡进行一次简单的 IO 可能需要长达一秒钟的时间。这段时间足以让您宝贵的四旋翼飞行器倒着飞向地面。Pixhawk microSD 卡的平均 IO 时间很短(几毫秒),但偶尔也会出现慢速 IO 的情况,因为 microSD 卡需要花一些时间重新读取 SD 卡规格并计算 Pi。不要因为大部分时间看起来很快,就想偷偷进行一些操作。

但有一个例外,那就是只有在载具启动或解除警报时才能调用的初始化功能。这时候稍微延迟一下就可以了。

现在去读一读 libraries/AP_Terrain/TerrainIO.cpp 看看它是如何使用 Posix IO 的。请注意它用来处理所有 IO 的小状态机,所有 IO 都是从 AP_Terrain::io_timer 功能。看看是否能发现任何错误,如果可以,请报告它们!