我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
当前回答
在小型卫星的软件/固件开发和环境测试方面工作了大约4-5年,我想在这里分享我的经验。
*(小型卫星比大型卫星更容易发生单次事件干扰,因为其电子部件的尺寸相对较小且有限)
非常简洁和直接:没有机制可以从可检测到的错误中恢复过来软件/固件本身的情况,至少没有用于恢复目的的软件/固件的最低工作版本副本,以及支持恢复的硬件(功能)。
现在,这种情况通常在硬件和软件两级处理。在这里,根据您的要求,我将分享我们在软件级别可以做的事情。
…恢复目的。。。。提供在真实环境中更新/重新编译/刷新软件/固件的能力。这几乎是高度电离环境中任何软件/固件的必备功能。如果没有这一点,您可以拥有任意数量的冗余软件/硬件,但在某一点上,它们都会崩溃。所以,准备好这个功能!…最低工作版本。。。在您的代码中具有响应性、多个副本、最低版本的软件/固件。这类似于Windows中的安全模式。不要只拥有一个功能完整的软件版本,而是拥有软件/固件的最低版本的多个副本。最小副本通常比完整副本小得多,并且几乎总是只有以下两个或三个功能:能够监听来自外部系统的命令,能够更新当前软件/固件,能够监控基本操作的内务数据。…复制…某处。。。在某处安装冗余软件/固件。无论有无冗余硬件,您都可以尝试在ARM uC中使用冗余软件/固件。这通常是通过在单独的地址中有两个或多个相同的软件/固件来实现的,这些软件/固件将向彼此发送心跳信号,但一次只有一个处于活动状态。如果已知一个或多个软件/固件没有响应,请切换到其他软件/固件。使用这种方法的好处是,我们可以在发生错误后立即进行功能更换,而无需与负责检测和修复错误的任何外部系统/方进行任何联系(在卫星情况下,通常是任务控制中心(MCC))。严格来说,如果没有冗余硬件,这样做的缺点是实际上无法消除所有单点故障。至少,您仍然会有一个单一的故障点,那就是交换机本身(或者通常是代码的开头)。然而,对于高度电离环境中受尺寸限制的设备(如微微/毫微微卫星),在没有额外硬件的情况下将单点故障减少到一点仍然值得考虑。更重要的是,用于切换的代码肯定会比整个程序的代码少得多,从而显著降低了在其中出现单一事件的风险。但是,如果您没有这样做,您的外部系统中应该至少有一个副本,该副本可以与设备接触并更新软件/固件(在卫星情况下,它也是任务控制中心)。您还可以在设备的永久内存存储中保存副本,该副本可以被触发以恢复正在运行的系统的软件/固件…可检测到的错误情况。。该错误必须是可检测的,通常通过硬件纠错/检测电路或通过一小段纠错/检测代码来检测。最好将这些代码放得小、多,并且独立于主软件/固件。其主要任务仅用于检查/纠正。如果硬件电路/固件是可靠的(例如,它比其余的更抗辐射-或具有多个电路/逻辑),那么您可以考虑使用它进行错误校正。但如果不是,最好将其作为错误检测。可通过外部系统/设备进行校正。对于纠错,您可以考虑使用像Hamming/Golay23这样的基本纠错算法,因为它们可以更容易地在电路/软件中实现。但这最终取决于团队的能力。对于错误检测,通常使用CRC。…支持恢复的硬件现在是这个问题上最困难的方面。最终,恢复需要负责恢复的硬件至少能够正常工作。如果硬件永久损坏(通常发生在其总电离剂量达到一定水平后),则软件无法帮助恢复。因此,对于暴露在高辐射水平下的设备(如卫星)来说,硬件无疑是最重要的关注点。
除了上述预测固件错误的建议外,我还建议您:
子系统间通信协议中的错误检测和/或错误校正算法。这是另一个几乎必须具备的功能,以避免从其他系统接收到不完整/错误的信号过滤ADC读数。请勿直接使用ADC读数。通过中值过滤器、均值过滤器或任何其他过滤器对其进行过滤-切勿相信单个读数。多采样,而不是少采样-合理。
其他回答
使用C语言编写在这种环境中表现稳健的程序是可能的,但前提是大多数形式的编译器优化都被禁用。优化编译器旨在用“更高效”的编码模式替换许多看似冗余的编码模式,并且可能不知道当编译器知道x不可能保持任何其他值时,程序员测试x==42的原因是因为程序员想要阻止执行某些代码,而x保持某个其他值——即使在这样的情况下,它保持该值的唯一方法是系统接收到某种电气故障。
将变量声明为易失性通常很有用,但可能不是万能药。特别重要的是,注意安全编码通常需要操作具有需要多个步骤来激活的硬件联锁,并且使用以下模式编写代码:
... code that checks system state
if (system_state_favors_activation)
{
prepare_for_activation();
... code that checks system state again
if (system_state_is_valid)
{
if (system_state_favors_activation)
trigger_activation();
}
else
perform_safety_shutdown_and_restart();
}
cancel_preparations();
如果编译器以相对文字的方式翻译代码,并且如果全部在prepare_for_activation()之后重复对系统状态的检查,系统可以对几乎任何可能的单一故障事件具有鲁棒性,甚至那些会任意破坏程序计数器和堆栈的程序。如果在调用prepare_for_activation()之后发生了一个小故障,这意味着激活是合适的(因为没有其他原因prepare_for_activation()将在故障发生之前被调用)。如果故障导致代码不正确地到达prepare_for_activation(),但如果没有后续故障事件,则代码将无法在未通过验证检查或先调用cancel_preparies的情况下到达trigger_activation()[如果堆栈出现问题,则在调用prepare_for_activation()的上下文返回后,执行可能会继续到trigger_active()之前的某个位置,但调用cancel_preparations(从而使后者的调用无害。
这样的代码在传统的C语言中可能是安全的,但在现代的C编译器中却不安全。这种编译器在这种环境中可能非常危险,因为它们努力只包含通过某种定义良好的机制可能出现的情况下相关的代码,并且其结果也将得到很好的定义。在某些情况下,旨在检测和清理故障的代码可能会使情况变得更糟。如果编译器确定尝试的恢复在某些情况下会调用未定义的行为,则可能推断在这种情况下不可能出现需要恢复的条件,从而消除了检查这些条件的代码。
既然您专门要求软件解决方案,而且您使用的是C++,为什么不使用运算符重载来创建自己的安全数据类型呢?例如:
不要使用uint32_t(以及double、int64_t等),而是制作自己的SAFE_uint32-t,其中包含uint32/t的倍数(最小值为3)。重载您想要执行的所有操作(*+-/<<>>==!=等),并使重载的操作对每个内部值独立执行,即不要执行一次并复制结果。在之前和之后,检查所有内部值是否匹配。如果值不匹配,可以将错误的值更新为最常见的值。如果没有最常见的值,您可以安全地通知存在错误。
这样,即使ALU、寄存器、RAM或总线上发生损坏也无所谓,您仍然可以多次尝试并很好地捕获错误。然而,请注意,这只适用于您可以替换的变量-例如,堆栈指针仍然是易受影响的。
附带故事:我遇到了一个类似的问题,也是在一个旧的ARM芯片上。结果发现,这是一个使用旧版本GCC的工具链,与我们使用的特定芯片一起,在某些边缘情况下触发了一个错误,这会(有时)破坏传递到函数中的值。在将设备归咎于无线电活动之前,确保设备没有任何问题,是的,有时是编译器错误=)
NASA有一篇关于防辐射软件的论文。它描述了三个主要任务:
定期监控内存中的错误,然后清除这些错误,稳健的错误恢复机制,以及如果某些东西不再工作,重新配置的能力。
请注意,内存扫描速率应该足够频繁,很少发生多位错误,因为大多数ECC内存可以从单位错误而不是多位错误中恢复。
稳健的错误恢复包括控制流传输(通常在错误发生之前的某个点重新启动流程)、资源释放和数据恢复。
他们对数据恢复的主要建议是,通过将中间数据视为临时数据,避免数据恢复的需要,以便在错误发生之前重新启动也能将数据回滚到可靠状态。这听起来类似于数据库中的“事务”概念。
他们讨论了特别适用于面向对象语言(如C++)的技术。例如
用于连续内存对象的基于软件的ECC契约编程:验证先决条件和后决条件,然后检查对象以验证其是否仍处于有效状态。
而且,正是如此,美国宇航局(NASA)已将C++用于火星探测器等重大项目。
C++类抽象和封装支持多个项目和开发人员之间的快速开发和测试。
他们避免了某些可能产生问题的C++特性:
例外情况模板Iostream(无控制台)多重继承运算符重载(new和delete除外)动态分配(使用专用内存池并放置新的以避免系统堆损坏的可能性)。
使用循环调度程序。这使您能够增加定期维护时间,以检查关键数据的正确性。最常遇到的问题是堆栈损坏。如果您的软件是周期性的,您可以在周期之间重新初始化堆栈。不要为中断调用重用堆栈,请为每个重要的中断调用设置一个单独的堆栈。
与看门狗概念类似的是最后期限计时器。在调用函数之前启动硬件计时器。如果函数在截止时间计时器中断之前未返回,则重新加载堆栈并重试。如果在3/5次尝试后仍然失败,则需要从ROM重新加载。
将软件拆分为多个部分,并将这些部分隔离开来,以使用单独的内存区域和执行时间(尤其是在控制环境中)。示例:信号采集、预处理数据、主要算法和结果实现/传输。这意味着某一部分的失败不会导致整个程序的失败。因此,当我们修复信号采集时,其余任务将继续处理过时的数据。
一切都需要CRC。如果您在RAM中执行,甚至您的.txt也需要CRC。如果使用循环调度程序,请定期检查CRC。有些编译器(不是GCC)可以为每个部分生成CRC,有些处理器有专用硬件来进行CRC计算,但我想这将超出您的问题范围。检查CRC还会提示内存上的ECC控制器在出现问题之前修复单位错误。
使用看门狗进行启动,而不仅仅是一次操作。如果您的启动遇到问题,您需要硬件帮助。
您可能还对有关算法容错的丰富文献感兴趣。这包括旧的赋值:编写一个排序,当恒定数量的比较将失败时(或者,更糟糕的版本,当失败的比较的渐近数量为n次比较的log(n)时),正确地对其输入进行排序。
开始阅读黄和亚伯拉罕1984年的论文《矩阵运算的基于算法的容错》。他们的想法隐约类似于同态加密计算(但实际上并不相同,因为他们正在尝试在操作级别进行错误检测/纠正)。
该论文的一个较新的后代是Bosilca、Delmas、Dongarra和Langou的“基于算法的容错应用于高性能计算”。