序列化调试自动驾驶系统

使用序列化软件组件调试自动驾驶系统

摘要

在汽车中软件密集型系统的开发过程中,例如自动驾驶系统,缺陷通常只有在实车试验期间才会被发现。与仿真环境不同,物理执行的操作无法暂停或对关键代码段进行调试,也无法复现和重复故障试验。此外,汽车内部的开发空间和能力有限。因此,最佳实践是在离线状态下分析物理执行过程中观察到的故障,并在仿真环境中复现故障试验。在仿真环境中进行重复试验虽然耗时,但对于推动软件组件进入曾出现错误行为的状态是必要的。本文提出了一种通过序列化软件系统的精确状态,在仿真环境中再次执行故障状态的方法,并总结了采用该方法获得的实际经验。

关键词 :故障检测、诊断、容错与消除;路径规划;高级驾驶辅助系统

1. 引言

在过去几年中,从高级驾驶辅助系统到高度自动化驾驶的发展在汽车行业中的重要性日益增加。这一趋势有望减少交通事故伤亡,并为汽车的驾驶员和乘客提供舒适性。然而,自动驾驶的复杂性 necessitates 越来越庞大的软件系统来应对所有可能的情况。不断增长的规模导致运行时可能出现更多的错误,从而增加了测试与调试所需的时间。

在开发此类系统时,通常的做法是首先在仿真环境中实现并测试一个软件组件的期望行为。首先,每个软件组件被单独考虑,然后测试所有必要模块之间的交互。这种方法允许在出现缺陷时随时暂停仿真,以便附加调试器并查看一段软件的内部状态。

不幸的是,在将系统部署到实车时,这种方法已不再可行。对于动态测试用例尤其如此,因为在这些情况下,情况无法简单停止,或者必须重复多次才能找到错误。此外,大多数情况下,完整系统的测试可能由不具备调试所有程序知识的开发人员完成。因此,有必要收集数据,以便专家能够在受控环境中复现故障,但问题仍然存在:需要多少数据?

示意图0

图1中描绘了许多可能导致缺陷产生的影响,而不仅仅是输入。一个软件组件。在非实时的运行时环境中(通常用于预开发),时序是可能改变内部状态的一个重要因素。例如,结果可能依赖于输入数据的顺序和时间。而该顺序和时间又会因车载计算机的操作系统的线程、文件访问等因素进一步受到影响。因此,需要一种方法来尽可能准确地复现缺陷发生时的条件,并消除车载计算机硬件和操作系统的影响。

我们提出一种框架,用于序列化软件组件的内部状态,以实现对大多数上述影响的解耦,这些影响会导致一个软件组件周期内的故障。

论文结构如下:首先,我们展示现有技术方法,并介绍来自re‐的方法。

2. 相关工作

在工业项目中,物理车辆上发生的故障主要通过记录测量的传感器值、执行器命令以及软件组件发出的调试信号来复现。以下将这些统称为信号轨迹。这些轨迹是使用预开发工具创建的,例如dSpace控制台¹、Elektrobit汽车数据和时间触发框架(EB Assist ADTF)中的硬盘记录仪²,或Bag文件机器人操作系统(ROS)的³。

参加DARPA城市挑战赛(美国国防高级研究计划局(2007年))的自动驾驶研究团队不得不应对实车中发生的故障。他们通过在故障发生期间记录通信数据,并通过重放数据来复现故障以解决问题。在十一支队伍中,有两支明确描述了如何复现故障(Bacha等人(2008年);Patz等人(2008年)),另有七支提到能够记录和回放通信数据(Chen等人(2008年);McBride等人(2008年);Rauskolb等人(2008年);Bohren等人(2008年);Leonard等人(2008年);Miller等人(2008年);Urmson等人(2008年)),这些功能很可能也用于故障复现。其余两支队伍未说明其如何处理故障(Montemerlo等人(2008年);Kammel等人(2008年))。

在更广泛的软件工程领域,已开发出多种用于复现故障的额外技术。许多方法也基于记录与回放通信数据。Clause和Orso(2007年)针对通信数据日志可能非常庞大且回放所需时间与记录时间相当的问题,提出了减小所需规模并提高回放速度的技术。Zamfir等人(2013年)处理的是数据中心应用中更大规模的数据,仅记录部分通信数据,同时解决了由网络和调度器时序引起的某些非确定性来源问题。这种非确定性在长时间回放场景中影响更为显著。Artzi等人(2008年)通过存储在带注释的Java程序中调用的所有方法的参数来消除该问题。如果方法主要依赖其参数,则此方法效果良好。Rößler(2013年)的方法不依赖于记录的数据,而是尝试基于进程转储和随机化测试方法来复现程序崩溃。最后,Yuan等人(2010年)通过将生成的日志消息与源代码中的行进行匹配,推断出有关执行路径的信息以复现故障。在后续的论文中(Yuan等人(2012年)),他们通过在源代码的日志消息中添加额外的变量来减少可能的执行路径数量。

记录和回放信号轨迹的主要缺点是,软件组件的输入和输出仅能暗示其内部状态。图1引言中提到的许多影响因素未被考虑。因此,由于随机性和其他影响因素,回放时的内部状态可能会偏离先前导致缺陷的运行状态。

相比之下,本文提出的方法:
- 允许在任何时间点精确复现一个软件组件的内部状态
- 仅需一个时间点的数据即可复现错误,因为无需重放至此时间点的执行序列
- 通过解耦通信与循环执行,避免了时序的影响
- 能够快速找到明显导致错误的执行过程
- 比捕获信号轨迹所需的磁盘空间和开销更少

3. 序列化和反序列化软件组件

将软件系统的状态转移到离线仿真环境的过程需要两个部分:一种用于存储和恢复软件系统状态的方法,以及用于分析存储的软件状态的离线仿真环境。

本节描述了存储和恢复仿真系统状态的方法。首先说明了所选序列化格式的动机。第3.2节介绍了如何对源代码进行注解以实现序列化,接着在第3.3节中讨论了开发过程是如何被采用的。最后,第3.4节描述了序列化的触发时机和方式。

3.1 选择序列化格式

存储一个软件组件的状态意味着将其序列化。序列化数据有多种广泛使用的格式。例如,Sumaray 和 Makki (2012) 列出了“XML、JSON、Thrift 和 ProtoBuf”。

对于这些格式,存在适用于不同编程语言的工具来创建序列化数据。本文所描述的调试方法已应用于用 C++ 编写的组件。因此,所选格式必须由 C++ 中可用的工具支持。

此外,整个软件组件比通常通过网络连接传输的数据更复杂。用于创建 XML 或 JSON 文件的典型工具需要显式设置或读取每个单独的数据项。此外,还必须实现用于读取和写入潜在隐藏信息的方法。Google Protocol Buffer⁴ 另外要求编写序列化数据的独立规范。这对于整个软件组件而言将难以维护。相比之下,boost serialization 支持复杂且嵌套的数据结构。它旨在用于持久化数据结构,并在“另一个程序上下文”⁵中重新创建它们。在本例中,所存储的数据结构就是本文所考虑的整个规划组件。

使用Boost序列化,任何C++类都可以通过以下命令进行序列化:

清单 1:序列化一个 C++类
1 std::stringstream ss;
2 boost::archive::binary_oarchive oa(ss);
3 oa << planning_component;

在此及以下示例中,“planning_component” 是类 “PlanningComponent” 的一个被序列化的实例。

为了使该命令生效,序列化软件组件需要定义Boost序列化方法。这些方法将在下一小节中进行说明。

3.2 注释源代码

Boost序列化为C++类提供了三种提供序列化方法的选项:
- 将序列化方法与类单独定义,
- 在头文件中定义序列化方法,或
- 在头文件中声明序列化方法,但在单独的文件中定义它们。

对于序列化规划组件,第三种方法最为合适。第一种方法无需更改原始C++类,但无法序列化私有数据成员,因此不能用于所有规划类。第二种方法实现工作量较小,但会显著增加编译时间和头文件长度,从而降低代码可读性,而这对于添加调试信息来说是不必要的。第三种方法在头文件中声明序列化方法,但在单独的文件中定义它们。这样只需添加三行代码,且编译时间增加几乎可以忽略不计。

因此,我们选择该方法用于序列化规划组件。

这三行代码包括包含一个头文件,使用boost宏声明要导出的类,以及使用可展开的自定义宏声明序列化方法:

清单2:要序列化的类的注释
1 友元类 boost::serialization::access;
2 模板<类Archive>
3 void 序列化(Archive & ar, const uint32_t version);

该方法随后可以在一个单独的cpp文件中实现。其主体基本上是已注解类的成员变量列表。

最后,软件系统中有一些组件不应被序列化,因为它们依赖于当前执行环境。对于我们的软件而言,这一点对于日志记录器和可视化也是如此。此外,对于时钟和通信中间件,部分信息会被序列化,但反序列化依赖于环境。

例如,在真实汽车上,时钟基于系统时钟。在序列化期间,时钟的当前时间被存储。当在仿真环境中进行反序列化时,时间会在模拟时钟中恢复。对于通信中间件,未处理的消息也会被序列化。

3.3 开发过程

创建初始的序列化代码后,必须在开发过程中对其进行维护。如果系统组件的类成员被添加或删除,则序列化方法也必须相应地进行调整。这带来了两个挑战:
- 规划系统不同版本之间的兼容性
- 不正确的序列化函数

Boost序列化通过版本属性解决了第一个挑战。该属性允许序列化组件的早期版本,并将其反序列化为后续版本。特别是,这对于检查在早期版本中引发错误行为的某些系统状态在组件的改进版本中是否仍然产生此行为非常有用。

应对的第二个挑战是不正确的序列化函数。序列化函数中的缺陷可以分为两类:
- 导致序列化功能崩溃的缺陷
- 导致不完整序列化的缺陷

第一类缺陷会自行暴露,因此只会导致日志记录数据的有限丢失。第二类缺陷更难以检测,因为唯一可见的结果可能是原始系统组件与其序列化后版本的行为差异。为了检测这些缺陷,测试套件可以比较自动驾驶系统在启用和不启用序列化时的行为。对于这两类缺陷,其影响通常是有限的,因为它们通常与待分析的现象无关。

3.4 触发序列化

在我们的项目中,可以设置三种不同的序列化触发方式:
- 序列化的手动触发,
- 每次记录错误消息时触发序列化
- 每个周期前序列化

手动触发可用于存储测试驾驶员认为值得进一步分析的状态。例如,汽车可能在没有明显原因的情况下拒绝继续行驶。相反,如果系统识别到某些功能异常,则可以将记录的错误消息用作触发条件。这对于查找已知错误模式的示例特别有用,例如规划组件无法找到路径的情况。在实践中,序列化在周期执行之前进行。如果没有错误发生,则在该周期中,序列化的数据会被丢弃。通过这种方式,可以重复错误发生时的确切情况。最后,每个周期都进行序列化意味着序列化数据永远不会被丢弃。此设置允许将故障的系统状态追溯到其变为故障的时间点。此设置会产生庞大数据量,但仍然少于信号跟踪。

在序列化的三种触发情况中,当软件工程师注意到车辆的某些行为需要进一步调查时,会在缺陷跟踪系统中提交一个工单。他们会将相应的序列化软件组件附加到这些工单中。

4. 离线仿真

序列化软件组件可以在独立环境中用于单个软件组件,或在完整的规划与控制系统仿真中进行重构。

4.1 轨迹规划器调试环境

我们主要使用独立环境来重构轨迹规划器的序列化软件组件。图2显示了该应用程序的截图。输入可以通过图形用户界面进行定义。在窗口的主区域中,轨迹规划器使用以下概念对其结果进行可视化。每个软件组件都通过日志记录和可视化接口来输出其计算相关信息。该可视化接口的实现方式取决于当前执行环境。独立环境为可视化接口提供了自己的实现。在独立环境中,用户可以精确控制轨迹规划器的输入,包括模拟时钟的时间,并触发下一个周期的执行。在真实汽车上,轨迹规划器通常使用速度优化的发布版本运行。而在调试环境中,相同的规划器则通过调试版本进行反序列化。由于处于受控环境并采用调试编译,软件工程师可以使用标准调试软件逐行执行轨迹规划器的代码。如果需要进一步分析的特定状态是由已记录的错误消息引起的,则查找问题尤为简单。在这种情况下,软件工程师可以在产生错误消息的相应代码行添加断点,并在调试器命中该断点时分析组件的状态。

4.2 完整仿真

某些情况需要在完整分析中执行特定场景。例如,如果规划路径与实际行驶路径之间的偏差异常大,则可能需要分析仿真环境中建模的传感器和执行器已知不准确性是否能够解释这些偏差。在这种情况下,整个规划系统将在仿真环境中被反序列化。像车辆本身这样的物理系统以及黑盒软件组件必须由足够精确的模型替代。Minnerup 等人 (2015) 基于 Minnerup 和 Knoll (2014) 发表的方法描述了这一过程。

示意图1

5. 实际经验

我们在与奥迪的一个项目中应用了本文提出的调试概念。在该项目中,我们开发了一个规划与控制系统,并将其集成到多个子项目中,例如自动泊车系统(Lenz 等人 (2014))。每个项目都有一个集成团队,该团队将我们团队开发的规划与控制系统与其他团队的组件一起运行。当集成团队遇到某些非期望行为时,会在缺陷跟踪工具中提交一个工单,并附上规划与控制系统生成的日志消息。通过基于序列化的规划组件重新执行问题情况,已解决多个问题。第5.2节描述了这样一个问题。第5.1节表明,序列化软件组件的规模小于ADTF信号轨迹的规模。对于某些工单,序列化的规划组件也有助于确认规划组件工作正常。

除了错误报告外,我们还在实车上进行自己的测试。对于每个测试日,我们都会存储序列化的规划组件,并分析异常行为。这种异常行为不仅包括错误行为,还包括我们未预料到且需要理解的模式。这有助于发现影响规划组件性能但并未导致明显错误行为的缺陷。

5.1 仿真结果

本文提出的概念验证所进行的首次测试是在仿真环境中运行。在仿真过程中,轨迹规划器每个周期都被序列化,且轨迹规划器的所有输入均使用ADTF硬盘记录器存储为信号跟踪。在七分钟的仿真中,轨迹规划器执行了多次泊车操作。在此期间,ADTF信号跟踪占用了812 MB的存储空间,而所有序列化的轨迹规划组件仅占用了10 MB。单个轨迹规划实例的最大规模为0.016 MB。因此,该实验表明,尽管序列化概念提供了更多信息,但其所需的存储空间仍少于信号跟踪用于调试的信息如下一小节所示。序列化局部规划器所需的计算时间始终小于1毫秒。

5.2 案例研究

图2‐6 展示了将软件故障追溯到根本原因并修复该故障的过程。在一次集成测试中,集成团队注意到汽车行驶的平顺性不如平常。因此,他们将相应的日志文件发送给负责开发规划与控制组件的团队以进行进一步分析。我们首先通过日志中包含的记录的加速度数据(如图3所示)识别出导致驾驶不舒适的情况。图中显示了测量加速度以及由规划组件计算出的加速度。根据系统内部知识,我们知道在标记的点处,规划的加速度与测量加速度应几乎一致,但实际情况并非如此。这一差异表明规划组件输出了错误的加速度。

示意图2

分析规划组件在检测到的时间点写入的日志消息表明,该规划组件假设了错误的初始加速度,如图4所示。与图3中突出显示的值之间的差异是由于插值引起的。

示意图3

从序列化的规划组件中,我们现在加载在发出可疑日志消息的周期执行之前被序列化的那个组件。该文件在图2所示的独立环境中反序列化。它可视化了 PlanningComponent的当前状态,包括其当前位置、当前参考路径以及未来几秒的规划。在这种情况下,可视化并未显示出任何问题。

使用调试工具逐步执行规划周期的执行,最终会到达图5所示的代码片段。所示的局部变量值表明插值函数未返回预期结果。预期的插值结果应位于两个输入值之间。

示意图4

在修正相应的代码片段后,可以使用修正后的 PlanningComponent执行相同的序列化状态。在相关位置,调试工具显示错误行为不再出现。

正如本例所示,我们已经能够将一个不准确的错误描述(车辆行驶舒适性差)追溯到规划器代码中的特定缺陷(插值函数的错误实现)。增加一个单元测试可以避免该段代码出现进一步问题。在此情况下,信号跟踪的作用会小得多:
- 规划组件需要花费显著更长的时间才能达到与车辆中完全相同的状态,
- 该错误与组件的时序密切相关:几毫秒级差异就会使问题消失,
- 信号跟踪可能会非常庞大,可能无法通过互联网传输

5.3 讨论

本文提出的概念对于具有显著状态信息的组件效果最佳。在这种情况下,它比信号跟踪更有用。如果周期频率相对于输入数据频率较低,则与信号跟踪相比的存储优势最大化。如果需要可视化变量的变化,信号轨迹仍然有用,这在控制器开发中经常是必要的。

6. 结论

在本论文中,我们展示了一种在离线测试环境中对实车进行道路测试时发生的故障进行调试的方法。通过使用 Boost C++ 库的序列化功能,我们能够完全保存和重新加载系统状态,从而复现观察到的错误,并在源代码中发现一个缺陷。

所展示的策略由我们的开发团队在工业级项目中开发并使用,该项目在多辆测试车辆上实现了多个自动驾驶场景。事实证明,为整个规划系统维护序列化代码的初始和持续开销是值得的,因为试驾主要可用于数据采集和参数优化,而缺陷修复则可以转移到办公室进行。

此外,开发周期得以加速,因为在道路测试期间持续保存了最大量信息,供专家进行后续分析,从而避免了额外的道路测试。甚至时序方面和内部系统状态也可以在事后进行分析。需要注意的是,正如案例研究5.2中所见,记录错误信息和记录特征信号可能具有积极作用。

目前,该序列化概念用于在类PC硬件上运行的自动驾驶车辆的规划组件。我们的目标是在快速原型实时硬件上执行的控制系统中实现类似的概念,但这会带来更严格的限制。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值