存储和 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
功能。看看是否能发现任何错误,如果可以,请报告它们!