我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
当前回答
如果你的硬件出现故障,你可以使用机械存储来恢复它。如果你的代码库很小,并且有一些物理空间,那么你可以使用一个机械数据存储。
材料表面不会受到辐射的影响。将有多个档位。机械读卡器将在所有齿轮上运行,并且可以灵活地上下移动。向下表示为0,向上表示为1。从0和1可以生成代码库。
其他回答
既然您专门要求软件解决方案,而且您使用的是C++,为什么不使用运算符重载来创建自己的安全数据类型呢?例如:
不要使用uint32_t(以及double、int64_t等),而是制作自己的SAFE_uint32-t,其中包含uint32/t的倍数(最小值为3)。重载您想要执行的所有操作(*+-/<<>>==!=等),并使重载的操作对每个内部值独立执行,即不要执行一次并复制结果。在之前和之后,检查所有内部值是否匹配。如果值不匹配,可以将错误的值更新为最常见的值。如果没有最常见的值,您可以安全地通知存在错误。
这样,即使ALU、寄存器、RAM或总线上发生损坏也无所谓,您仍然可以多次尝试并很好地捕获错误。然而,请注意,这只适用于您可以替换的变量-例如,堆栈指针仍然是易受影响的。
附带故事:我遇到了一个类似的问题,也是在一个旧的ARM芯片上。结果发现,这是一个使用旧版本GCC的工具链,与我们使用的特定芯片一起,在某些边缘情况下触发了一个错误,这会(有时)破坏传递到函数中的值。在将设备归咎于无线电活动之前,确保设备没有任何问题,是的,有时是编译器错误=)
这是一个非常广泛的主题。基本上,您无法真正从内存损坏中恢复,但至少可以尝试立即失败。以下是您可以使用的一些技巧:
校验和常量数据。如果有任何配置数据长期保持不变(包括已配置的硬件寄存器),请在初始化时计算其校验和并定期验证。当您看到不匹配时,应该重新初始化或重置。冗余存储变量。如果你有一个重要的变量x,把它的值写在x1、x2和x3中,然后读为(x1==x2)?x2:x3。实施程序流程监控。将全局标志与从主循环调用的重要函数/分支中的唯一值进行异或。在接近100%测试覆盖率的无辐射环境中运行程序,应为您提供循环结束时标志的可接受值列表。如果看到偏差,则重置。监视堆栈指针。在主循环的开头,将堆栈指针与其预期值进行比较。偏差复位。
使用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编译器中却不安全。这种编译器在这种环境中可能非常危险,因为它们努力只包含通过某种定义良好的机制可能出现的情况下相关的代码,并且其结果也将得到很好的定义。在某些情况下,旨在检测和清理故障的代码可能会使情况变得更糟。如果编译器确定尝试的恢复在某些情况下会调用未定义的行为,则可能推断在这种情况下不可能出现需要恢复的条件,从而消除了检查这些条件的代码。
以下是一些想法和想法:
更创造性地使用ROM。
在ROM中存储任何可以存储的东西。不要计算东西,而是将查找表存储在ROM中。(确保编译器将查找表输出到只读部分!在运行时打印内存地址以进行检查!)将中断向量表存储在RAM中。当然,运行一些测试以查看ROM与RAM相比的可靠性。
为堆栈使用最好的RAM。
堆栈中的SEU可能是最可能的崩溃源,因为它是索引变量、状态变量、返回地址和各种类型的指针通常存在的地方。
执行计时器滴答声和看门狗计时器例程。
您可以在每一次计时器计时时运行一个“健全性检查”例程,以及一个看门狗例程来处理系统锁定。您的主代码还可以周期性地增加一个计数器以指示进度,而健全性检查例程可以确保发生了这种情况。
在软件中执行纠错代码。
您可以为数据添加冗余,以便能够检测和/或纠正错误。这将增加处理时间,可能会使处理器长时间暴露在辐射中,从而增加出错的机会,因此您必须考虑权衡。
记住缓存。
检查CPU缓存的大小。您最近访问或修改的数据可能位于缓存中。我相信您可以禁用至少一些缓存(以较大的性能代价);你应该试试看缓存对SEU的敏感性。如果缓存比RAM更硬,那么您可以定期读取和重新写入关键数据,以确保它保留在缓存中并使RAM恢复正常。
巧妙地使用页面错误处理程序。
如果将内存页标记为不存在,CPU将在您尝试访问它时发出页面错误。您可以创建一个页面错误处理程序,在处理读取请求之前进行一些检查。(PC操作系统使用此功能透明地加载已交换到磁盘的页面。)
对关键的事情使用汇编语言(可能是所有事情)。
使用汇编语言,您知道寄存器中的内容和RAM中的内容;你知道CPU使用的是什么特殊的RAM表,你可以用迂回的方式来设计,以降低风险。
使用objdump实际查看生成的汇编语言,并计算每个例程占用的代码量。
如果你使用的是像Linux这样的大型操作系统,那么你就是在自找麻烦;有太多的复杂性和太多的事情要出错。
记住这是一场概率游戏。
一位评论者说
你为捕捉错误而编写的每一个例程都会因同样的原因而失败。
虽然这是真的,但检查例程正确运行所需的(例如)100字节代码和数据中发生错误的机会要比其他地方发生错误的几率小得多。如果你的ROM非常可靠,并且几乎所有的代码/数据都在ROM中,那么你的可能性就更大了。
使用冗余硬件。
使用具有相同代码的两个或更多相同硬件设置。如果结果不同,应触发重置。对于3个或更多设备,您可以使用“投票”系统来尝试确定哪一个已被破坏。
有一点似乎没有人提到。你说你在GCC中开发,并在ARM上交叉编译。你怎么知道你的代码中没有关于空闲RAM、整数大小、指针大小、执行某个操作需要多长时间、系统将持续运行多长时间等的假设?这是一个非常普遍的问题。
答案通常是自动单元测试。编写在开发系统上执行代码的测试线束,然后在目标系统上运行相同的测试线束。寻找差异!
还要检查嵌入式设备上的勘误表。您可能会发现“不要这样做,因为它会崩溃,所以启用编译器选项,编译器会解决它”。
简而言之,崩溃的最可能来源是代码中的错误。在你确定这不是事实之前,不要担心更深奥的故障模式。