我们正在编译一个嵌入式 C++ 应用程序,该应用程序部署在受 ionizing radiation 轰炸的环境中的屏蔽设备中。我们正在为 ARM 使用 GCC 和交叉编译。部署后,我们的应用程序会生成一些错误数据,并且崩溃的频率超出了我们的预期。硬件是为这个环境设计的,我们的应用程序已经在这个平台上运行了好几年。
我们是否可以对代码进行更改,或者可以进行编译时改进以识别/纠正由 single event upsets 引起的 soft errors 和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
在 miniaturized satellites* 的软件/固件开发和环境测试方面工作了大约 4-5 年,我想在这里分享我的经验。
*(小型卫星比大型卫星更容易发生单事件扰动,因为其电子元件的尺寸相对较小且有限)
非常简洁和直接:没有机制可以通过软件/固件本身从可检测的错误情况中恢复,而至少没有一份软件/固件的最低工作版本的副本用于恢复目的 - 并且硬件支持恢复(功能)。
现在,这种情况通常在硬件和软件层面都得到处理。在这里,应您的要求,我将分享我们在软件层面可以做些什么。
...恢复目的....提供在真实环境中更新/重新编译/重新刷新您的软件/固件的能力。对于高度电离环境中的任何软件/固件来说,这几乎是必备的功能。如果没有这个,您可以拥有任意数量的冗余软件/硬件,但在某一时刻,它们都会爆炸。所以,准备这个功能! ...最低工作版本...在您的代码中具有响应式、多副本、最低版本的软件/固件。这就像 Windows 中的安全模式。不要只拥有一个功能齐全的软件版本,而是拥有软件/固件最低版本的多个副本。最小副本通常比完整副本的大小要小得多,并且几乎总是只有以下两个或三个功能:能够侦听来自外部系统的命令,能够更新当前的软件/固件,能够监控基本操作的内务处理数据。 ...复制...某处...在某处有冗余软件/固件。无论有没有冗余硬件,您都可以尝试在 ARM uC 中安装冗余软件/固件。这通常是通过在不同的地址中拥有两个或多个相同的软件/固件来完成的,这些软件/固件相互发送心跳 - 但一次只有一个处于活动状态。如果已知一个或多个软件/固件无响应,请切换到其他软件/固件。使用这种方法的好处是我们可以在发生错误后立即进行功能更换 - 无需与负责检测和修复错误的任何外部系统/方有任何联系(在卫星情况下,通常是任务控制中心( MCC))。严格来说,如果没有冗余硬件,这样做的缺点是您实际上无法消除所有单点故障。至少,您仍然会遇到一个单点故障,即开关本身(或者通常是代码的开头)。尽管如此,对于在高度电离环境中受尺寸限制的设备(例如 pico/femto 卫星),在没有额外硬件的情况下将单点故障减少到一个点仍然值得考虑。此外,用于切换的一段代码肯定会比整个程序的代码少得多——显着降低了其中出现单事件的风险。但是,如果您不这样做,您的外部系统中应该至少有一个副本,它可以与设备联系并更新软件/固件(在卫星情况下,它又是任务控制中心)。您还可以将副本保存在设备的永久内存存储中,可以触发该副本以恢复正在运行的系统的软件/固件……可检测到的错误情况。错误必须是可检测到的,通常由硬件纠错/检测电路或通过一小段代码进行纠错/检测。最好将此类代码小而多,并独立于主要软件/固件。它的主要任务只是检查/纠正。如果硬件电路/固件是可靠的(例如它比其余的更耐辐射 - 或具有多个电路/逻辑),那么您可能会考虑使用它进行纠错。但如果不是,最好将其作为错误检测。可以通过外部系统/设备进行校正。对于纠错,您可以考虑使用 Hamming/Golay23 等基本纠错算法,因为它们可以更容易地在电路/软件中实现。但这最终取决于您团队的能力。对于错误检测,通常使用 CRC。 ...支持恢复的硬件 现在,谈到这个问题上最困难的方面。最终,恢复需要负责恢复的硬件至少可以正常工作。如果硬件永久损坏(通常在其总电离剂量达到一定水平后发生),那么(遗憾地)软件无法帮助恢复。因此,对于暴露在高辐射水平的设备(例如卫星)来说,硬件无疑是最重要的问题。
除了上述由于单事件扰动而导致固件错误的建议之外,我还建议您:
子系统间通信协议中的错误检测和/或纠错算法。这是另一个几乎必须具备的功能,以避免在您的 ADC 读数中从其他系统过滤器接收到不完整/错误的信号。不要直接使用 ADC 读数。通过中值过滤器、均值过滤器或任何其他过滤器对其进行过滤 - 永远不要相信单个读数值。采样更多,而不是更少 - 合理。
NASA 有 a paper on radiation-hardened 软件。它描述了三个主要任务:
定期监控内存中的错误,然后清除这些错误,强大的错误恢复机制,以及在某些东西不再有效时重新配置的能力。
请注意,内存扫描速率应该足够频繁,以至于很少发生多位错误,因为大多数 ECC 内存可以从单位错误中恢复,而不是从多位错误中恢复。
强大的错误恢复包括控制流转移(通常在错误前的某个点重新启动进程)、资源释放和数据恢复。
他们对数据恢复的主要建议是避免需要它,通过将中间数据视为临时数据,以便在错误之前重新启动也将数据回滚到可靠状态。这听起来类似于数据库中“事务”的概念。
他们讨论了特别适用于 C++ 等面向对象语言的技术。例如
用于连续内存对象的基于软件的 ECC 合同编程:验证前置条件和后置条件,然后检查对象以验证它是否仍处于有效状态。
而且,碰巧的是,NASA 已将 C++ 用于大型项目,例如 Mars Rover。
C++ 类抽象和封装支持在多个项目和开发人员之间进行快速开发和测试。
他们避免了某些可能产生问题的 C++ 特性:
异常模板 Iostream(无控制台) 多重继承 运算符重载(除了 new 和 delete) 动态分配(使用专用内存池和放置 new 以避免系统堆损坏的可能性)。
以下是一些想法和想法:
更有创意地使用 ROM。
将您可以存储的任何内容存储在 ROM 中。无需计算,而是将查找表存储在 ROM 中。 (确保您的编译器将您的查找表输出到只读部分!在运行时打印出内存地址以进行检查!)将您的中断向量表存储在 ROM 中。当然,运行一些测试来看看你的 ROM 与你的 RAM 相比有多可靠。
为堆栈使用最好的 RAM。
堆栈中的 SEU 可能是最有可能导致崩溃的来源,因为它通常存在诸如索引变量、状态变量、返回地址和各种指针之类的东西。
实现定时器滴答和看门狗定时器例程。
您可以在每个计时器滴答声中运行一个“健全性检查”例程,以及一个看门狗例程来处理系统锁定。您的主代码还可以定期增加一个计数器以指示进度,并且健全性检查例程可以确保这已经发生。
在软件中实现 error-correcting-codes。
您可以为数据添加冗余,以便能够检测和/或纠正错误。这会增加处理时间,可能会使处理器暴露在辐射下的时间更长,从而增加出错的机会,因此您必须考虑权衡。
记住缓存。
检查 CPU 缓存的大小。您最近访问或修改的数据可能会在缓存中。我相信您至少可以禁用一些缓存(以很大的性能成本);你应该试试这个,看看缓存对 SEU 的敏感程度。如果缓存比 RAM 更硬,那么您可以定期读取和重写关键数据,以确保它保留在缓存中并使 RAM 恢复正常。
巧妙地使用页面错误处理程序。
如果您将内存页面标记为不存在,CPU 将在您尝试访问它时发出页面错误。您可以创建一个页面错误处理程序,在处理读取请求之前进行一些检查。 (PC 操作系统使用它来透明地加载已交换到磁盘的页面。)
对关键的事情(可能是一切)使用汇编语言。
使用汇编语言,您可以知道寄存器中的内容和 RAM 中的内容;您知道 CPU 正在使用哪些特殊的 RAM 表,并且您可以通过迂回的方式设计事物以降低风险。
使用 objdump
实际查看生成的汇编语言,并计算出每个例程占用了多少代码。
如果您使用的是像 Linux 这样的大型操作系统,那么您就是在自找麻烦。有太多的复杂性和太多的问题。
请记住,这是一个概率游戏。
一位评论者说
您编写的每个用于捕获错误的例程都将因相同的原因而失败。
虽然这是真的,但检查例程正常运行所需的(例如)100 字节代码和数据中出现错误的可能性远小于其他地方出现错误的可能性。如果您的 ROM 非常可靠并且几乎所有代码/数据实际上都在 ROM 中,那么您的几率会更高。
使用冗余硬件。
使用 2 个或更多具有相同代码的相同硬件设置。如果结果不同,则应触发重置。对于 3 台或更多设备,您可以使用“投票”系统来尝试确定哪一台已被入侵。
您可能还对有关算法容错主题的丰富文献感兴趣。这包括旧的分配:编写一个排序,当恒定数量的比较将失败时正确排序其输入(或者,稍微更邪恶的版本,当失败比较的渐近数量缩放为 log(n)
用于 n
比较)。
可以从 Huang 和 Abraham 1984 年的论文“Algorithm-Based Fault Tolerance for Matrix Operations”开始阅读。他们的想法有点类似于同态加密计算(但实际上并不相同,因为他们正在尝试在操作级别进行错误检测/纠正)。
该论文的最新后代是 Bosilca、Delmas、Dongarra 和 Langou 的“Algorithm-based fault tolerance applied to high performance computing”。
为放射性环境编写代码与为任何关键任务应用程序编写代码并没有什么不同。
除了已经提到的内容之外,这里还有一些其他提示:
使用应该存在于任何半专业嵌入式系统上的日常“面包和黄油”安全措施:内部看门狗、内部低电压检测、内部时钟监视器。这些东西甚至不需要在 2016 年提及,它们是几乎所有现代微控制器的标准配置。
如果您有一个安全和/或面向汽车的 MCU,它将具有某些看门狗功能,例如给定的时间窗口,您需要在该时间窗口内刷新看门狗。如果您有一个关键任务实时系统,这是首选。
一般来说,使用适合这类系统的 MCU,而不是你在一包玉米片中收到的一些通用的主流绒毛。现在几乎每个 MCU 制造商都有专门为安全应用设计的 MCU(TI、飞思卡尔、瑞萨、ST、英飞凌等)。它们具有许多内置的安全功能,包括锁步内核:这意味着有 2 个 CPU 内核执行相同的代码,并且它们必须彼此一致。
重要提示:您必须确保内部 MCU 寄存器的完整性。所有可写的硬件外设控制和状态寄存器都可能位于 RAM 内存中,因此很容易受到攻击。为了保护自己免受寄存器损坏,最好选择具有内置寄存器“一次写入”功能的微控制器。此外,您需要将所有硬件寄存器的默认值存储在 NVM 中,并定期将这些值复制到您的寄存器中。您可以以相同的方式确保重要变量的完整性。注意:始终使用防御性编程。这意味着您必须设置 MCU 中的所有寄存器,而不仅仅是应用程序使用的寄存器。您不希望某些随机硬件外围设备突然唤醒。
有各种方法可以检查 RAM 或 NVM 中的错误:校验和、“行走模式”、软件 ECC 等。现在最好的解决方案是不使用其中任何一种,而是使用具有内置 ECC 的 MCU 和类似的检查。因为在软件中执行此操作很复杂,并且错误检查本身可能会因此引入错误和意外问题。
使用冗余。您可以将易失性和非易失性内存存储在两个相同的“镜像”段中,它们必须始终相同。每个段都可以附加一个 CRC 校验和。
避免使用 MCU 外部的外部存储器。
为所有可能的中断/异常实现默认中断服务例程/默认异常处理程序。即使是你不使用的那些。默认例程除了关闭自己的中断源之外什么都不做。
理解并接受防御性编程的概念。这意味着您的程序需要处理所有可能的情况,即使是理论上不可能发生的情况。例子。高质量的关键任务固件会检测尽可能多的错误,然后以安全的方式处理或忽略它们。
永远不要编写依赖于指定不当行为的程序。这种行为可能会随着辐射或 EMI 引起的意外硬件变化而发生巨大变化。确保您的程序摆脱此类垃圾的最佳方法是使用 MISRA 之类的编码标准以及静态分析器工具。这也将有助于防御性编程和清除错误(为什么您不想检测任何类型的应用程序中的错误?)。
重要提示:不要实现对静态存储持续时间变量默认值的任何依赖。也就是说,不要相信 .data 或 .bss 的默认内容。从初始化点到实际使用变量的点之间可能有任何时间,可能有足够的时间让 RAM 损坏。相反,编写程序以便在运行时从 NVM 设置所有此类变量,就在第一次使用此类变量的时间之前。在实践中,这意味着如果变量在文件范围内声明或声明为静态变量,则永远不应使用 = 来初始化它(或者可以,但它没有意义,因为无论如何您都不能依赖该值)。总是在运行时设置它,就在使用之前。如果可以从 NVM 重复更新这些变量,那么就这样做。同样在 C++ 中,不要依赖于静态存储持续时间变量的构造函数。让构造函数调用公共“设置”例程,您也可以稍后在运行时直接从调用者应用程序调用该例程。如果可能,请完全删除初始化 .data 和 .bss(并调用 C++ 构造函数)的“向下复制”启动代码,以便在编写依赖于此类的代码时出现链接器错误。许多编译器可以选择跳过这个,通常称为“最小/快速启动”或类似的。这意味着必须检查任何外部库,以便它们不包含任何此类依赖。
为程序实施并定义一个安全状态,在出现严重错误时您将恢复到该状态。
实施错误报告/错误日志系统总是有帮助的。
.text
部分中翻转一个位,从而更改操作码或类似内容。
可以使用 C 编写在此类环境中表现稳健的程序,但前提是大多数形式的编译器优化都被禁用。优化编译器旨在将许多看似冗余的编码模式替换为“更高效”的编码模式,并且可能不知道当编译器知道 x
不可能保存任何其他内容时程序员测试 x==42
的原因是因为程序员想要阻止某些代码的执行,而 x
持有一些其他值——即使在它持有该值的唯一方法是系统收到某种电子故障的情况下也是如此。
将变量声明为 volatile
通常很有帮助,但可能不是灵丹妙药。特别重要的是,请注意,安全编码通常要求危险操作具有需要多个步骤才能激活的硬件联锁,并且使用以下模式编写代码:
... 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_preparations 的情况下将无法随后到达 trigger_activation()
[如果堆栈故障,在调用 prepare_for_activation()
的上下文返回之后,执行可能会继续执行到 trigger_activation()
之前的某个位置,但对 cancel_preparations()
的调用将发生在对 prepare_for_activation()
和 trigger_activation()
的调用之间,从而使后者的调用无害。
这样的代码在传统 C 中可能是安全的,但在现代 C 编译器中则不然。这样的编译器在那种环境中可能是非常危险的,因为它们咄咄逼人地努力只包含在可能通过某些明确定义的机制出现的情况下相关的代码,并且其结果也将得到明确定义。在某些情况下,旨在检测和清理故障的代码最终可能会使事情变得更糟。如果编译器确定尝试的恢复在某些情况下会调用未定义的行为,它可能会推断在这种情况下需要这种恢复的条件不可能发生,从而消除了检查它们的代码。
-O0
或等效开关? 如果你允许它,GCC 会做很多奇怪的事情,但如果你要求它不要这样做,它通常也可以是相当字面意思的。
-O2
恢复速度。
-O0
是一个坏主意的第二个原因是它发出了更多无用的指令。示例:非内联调用包含保存寄存器、进行调用、恢复寄存器的指令。所有这些都可能失败。不存在的指令不会失败。
-O0
是一个坏主意的另一个原因是:它倾向于将变量存储在内存中而不是寄存器中。现在还不确定内存是否更容易受到 SEU 的影响,但传输中的数据比静止数据更容易受到影响。应避免无用的数据移动,-O2
对此有所帮助。
v1=v2+0xCAFEBABE
并且对这两个变量的所有更新都已完成......
这是一个非常广泛的主题。基本上,您无法真正从内存损坏中恢复,但您至少可以尝试立即失败。以下是您可以使用的一些技术:
校验和常量数据。如果您有任何长时间保持不变的配置数据(包括您已配置的硬件寄存器),请在初始化时计算其校验和并定期验证。当您看到不匹配时,是时候重新初始化或重置了。
以冗余方式存储变量。如果您有一个重要的变量 x,请将其值写入 x1、x2 和 x3 并将其读取为 (x1 == x2) ? x2:x3。
实施程序流监控。 XOR 在从主循环调用的重要函数/分支中具有唯一值的全局标志。在测试覆盖率接近 100% 的无辐射环境中运行程序应该会在循环结束时为您提供可接受的标志值列表。如果发现偏差,请重置。
监视堆栈指针。在主循环开始时,将堆栈指针与其期望值进行比较。偏差复位。
watchdog 可以帮助您。看门狗在 1980 年代广泛用于工业计算。那时硬件故障更为常见——另一个答案也指那个时期。
看门狗是一种组合的硬件/软件功能。硬件是一个简单的计数器,可以从一个数字(比如 1023)倒数到零。 TTL 或其他逻辑可以使用。
该软件的设计使得一个程序可以监控所有基本系统的正确运行。如果此例程正确完成 = 发现计算机运行良好,则将计数器设置回 1023。
总体设计是为了在正常情况下,软件防止硬件计数器达到零。如果计数器达到零,计数器的硬件将执行其唯一的任务并重置整个系统。从计数器的角度来看,零等于 1024,并且计数器继续向下计数。
这个看门狗确保连接的计算机在很多很多失败的情况下重新启动。我必须承认,我不熟悉能够在当今计算机上执行此类功能的硬件。与外部硬件的接口现在比以前复杂得多。
看门狗的一个固有缺点是,从系统出现故障到看门狗计数器达到零 + 重新启动时间,系统都不可用。虽然该时间通常比任何外部或人为干预短得多,但受支持的设备将需要能够在该时间范围内无需计算机控制即可继续运行。
这个答案假设您关心的是拥有一个正常工作的系统,而不是拥有一个成本最低或速度快的系统;大多数玩放射性物品的人更看重正确性/安全性而不是速度/成本
有几个人建议您可以进行硬件更改(很好 - 答案中已经有很多好东西,我不打算重复所有这些),其他人建议冗余(原则上很好),但我不认为任何人都建议过这种冗余如何在实践中发挥作用。你如何故障转移?你怎么知道什么时候“出了问题”?许多技术都是在一切正常的基础上工作的,因此失败是一件棘手的事情。然而,一些为规模设计的分布式计算技术预计会失败(毕竟规模足够大,单个节点的任何 MTBF 都不可避免地出现多个节点中的一个节点的故障);您可以将其用于您的环境。
这里有一些想法:
确保您的整个硬件被复制 n 次(其中 n 大于 2,最好是奇数),并且每个硬件元素都可以与其他硬件元素通信。以太网是一种显而易见的方法,但还有许多其他更简单的路由可以提供更好的保护(例如 CAN)。尽量减少常用组件(甚至电源)。例如,这可能意味着在多个位置对 ADC 输入进行采样。
确保您的应用程序状态位于一个位置,例如在有限状态机中。这可以完全基于 RAM,但不排除稳定的存储。因此它将被存储在几个地方。
采用仲裁协议来更改状态。例如,参见 RAFT。当您在 C++ 中工作时,有一些众所周知的库。只有当大多数节点同意时,才会对 FSM 进行更改。为协议栈和仲裁协议使用一个已知好的库,而不是自己滚动一个,否则当仲裁协议挂断时,你所有的冗余工作都将被浪费。
确保您对 FSM 进行校验和(例如 CRC/SHA),并将 CRC/SHA 存储在 FSM 本身中(以及在消息中传输,并对消息本身进行校验和)。让节点定期根据这些校验和检查它们的 FSM,校验传入消息,并检查它们的校验和是否与仲裁的校验和匹配。
在您的系统中构建尽可能多的其他内部检查,使检测到自身故障的节点重新启动(如果您有足够的节点,这比进行一半工作要好)。尝试让他们在重新启动期间彻底将自己从仲裁中移除,以防他们不再出现。在重新启动时,让他们对软件映像(以及他们加载的任何其他内容)进行校验和,并在将自己重新引入仲裁之前进行完整的 RAM 测试。
使用硬件来支持你,但要小心。例如,您可以获取 ECC RAM,并定期通过它读/写以纠正 ECC 错误(如果错误无法纠正,则恐慌)。但是(从内存中)静态 RAM 比 DRAM 更能容忍电离辐射,因此使用静态 DRAM 可能会更好。请参阅“我不会做的事情”下的第一点。
假设您在一天内任何给定节点有 1% 的故障几率,假设您可以使故障完全独立。如果有 5 个节点,您需要 3 个在一天内失败,这是 0.00001% 的机会。有了更多,好吧,你明白了。
我不会做的事情:
低估一开始就没有问题的价值。除非重量是一个问题,否则你设备周围的一大块金属将是一个比程序员团队想出的更便宜、更可靠的解决方案。 EMI 输入的同上光耦合是一个问题,等等。无论如何,在采购您的组件时尝试采购那些额定最能抵抗电离辐射的组件。
推出自己的算法。人们以前做过这种事情。使用他们的工作。容错和分布式算法很难。尽可能使用其他人的工作。
天真地使用复杂的编译器设置希望您检测到更多故障。如果幸运的话,您可能会发现更多故障。更有可能的是,您将在编译器中使用经过较少测试的代码路径,特别是如果您自己滚动它。
使用在您的环境中未经测试的技术。大多数编写高可用性软件的人必须模拟故障模式来检查他们的 HA 是否正常工作,并因此错过许多故障模式。您处于按需频繁失败的“幸运”位置。因此,测试每种技术,并确保其应用程序实际将 MTBF 提高的数量超过了引入它的复杂性(复杂性带来了错误)。尤其是将此应用于我对仲裁算法等的建议。
既然您特别要求软件解决方案,而且您正在使用 C++,为什么不使用运算符重载来创建自己的安全数据类型呢?例如:
不要使用 uint32_t
(和 double
、int64_t
等),而是制作自己的 SAFE_uint32_t
,其中包含 uint32_t 的倍数(至少为 3)。重载您想要执行的所有操作 (* + - / << >> = == != 等),并使重载的操作在每个内部值上独立执行,即不要执行一次并且复制结果。在之前和之后,检查所有内部值是否匹配。如果值不匹配,您可以将错误的值更新为最常见的值。如果没有最常见的值,您可以安全地通知存在错误。
这样,ALU、寄存器、RAM 或总线上是否发生损坏都无关紧要,您仍然可以进行多次尝试,并且很有可能发现错误。但是请注意,尽管这仅适用于您可以替换的变量 - 例如,您的堆栈指针仍然很容易受到影响。
一个小故事:我遇到了类似的问题,也是在旧的 ARM 芯片上。结果证明这是一个使用旧版本 GCC 的工具链,与我们使用的特定芯片一起,在某些边缘情况下触发了一个错误,该错误会(有时)破坏传递给函数的值。在将设备归咎于放射性之前,请确保您的设备没有任何问题,是的,有时它是编译器错误 =)
免责声明:我不是放射性专业人士,也不为此类应用工作。但我为关键数据的长期存档处理软错误和冗余,这在某种程度上是相关的(同样的问题,不同的目标)。
在我看来,放射性的主要问题是放射性可以转换位,因此放射性可以/将篡改任何数字记忆。这些错误通常称为 soft errors、bit rot 等。
那么问题来了:当你的内存不可靠时,如何可靠地计算?
要显着降低软错误率(以计算开销为代价,因为它主要是基于软件的解决方案),您可以:
依靠良好的旧冗余方案,更具体地说是更有效的纠错码(目的相同,但算法更聪明,这样您就可以用更少的冗余恢复更多位)。这有时(错误地)也称为校验和。使用这种解决方案,您必须随时将程序的完整状态存储在主变量/类(或结构?)中,计算 ECC,并在执行任何操作之前检查 ECC 是否正确,如果不,修复田地。但是,此解决方案并不能保证您的软件可以正常工作(只是在可以正常工作时它会正常工作,否则会停止工作,因为 ECC 可以告诉您是否有问题,在这种情况下,您可以停止您的软件,以便您不要得到虚假的结果)。
或者您可以使用有弹性的算法数据结构,在一定程度上保证您的程序即使在存在软错误的情况下仍能给出正确的结果。这些算法可以看作是普通算法结构与原生混合的 ECC 方案的混合,但这比这更有弹性,因为弹性方案与结构紧密绑定,因此您不需要编码额外的程序检查ECC,通常它们要快得多。这些结构提供了一种方法来确保您的程序可以在任何条件下运行,直至达到软错误的理论界限。您还可以将这些弹性结构与冗余/ECC 方案混合以提高安全性(或将最重要的数据结构编码为弹性,其余的,您可以从主要数据结构重新计算的消耗性数据,作为具有位 ECC 或计算速度非常快的奇偶校验)。
如果您对弹性数据结构(这是算法和冗余工程中最近但令人兴奋的新领域)感兴趣,我建议您阅读以下文档:
弹性算法数据结构介绍 Giuseppe F.Italiano, Universita di Roma "Tor Vergata"
Christiano, P., Demaine, ED, & Kishore, S. (2011)。具有附加开销的无损容错数据结构。在算法和数据结构中(第 243-254 页)。施普林格柏林海德堡。
Ferraro-Petrillo, U.、Grandoni, F. 和 Italiano, GF (2013)。数据结构对内存故障的弹性:字典的实验研究。实验算法杂志 (JEA), 18, 1-6。
意大利人,GF(2010)。弹性算法和数据结构。在算法和复杂性中(第 13-24 页)。施普林格柏林海德堡。
如果您有兴趣了解有关弹性数据结构领域的更多信息,可以查看 Giuseppe F. Italiano 的作品(并按照自己的方式阅读 refs)和 Faulty-RAM 模型(在 Finocchi 中引入)等人 2005;Finocchi 和 Italiano 2008)。
/ EDIT:我说明了主要针对RAM内存和数据存储的软错误的预防/恢复,但我没有谈论计算(CPU)错误。其他答案已经指出在数据库中使用原子事务,因此我将提出另一个更简单的方案:冗余和多数投票。
这个想法是,您只需为需要执行的每个计算执行 x 次相同的计算,并将结果存储在 x 个不同的变量中(x >= 3)。然后,您可以比较您的 x 变量:
如果他们都同意,那么根本没有计算错误。
如果他们不同意,那么您可以使用多数票来获得正确的值,并且由于这意味着计算已部分损坏,您还可以触发系统/程序状态扫描以检查其余部分是否正常。
如果多数票无法确定获胜者(所有 x 值都不同),那么这是触发故障保护程序(重新启动、向用户发出警报等)的完美信号。
与 ECC 相比,这种冗余方案非常快(实际上是 O(1)),当您需要故障保护时,它会为您提供清晰的信号。多数票也(几乎)保证永远不会产生损坏的输出,也可以从轻微的计算错误中恢复,因为 x 次计算给出相同输出的概率是无限小的(因为有大量可能的输出,几乎不可能随机获得 3 次相同,如果 x > 3) 则机会更少。
因此,通过多数票,您可以避免损坏的输出,并且在冗余 x == 3 的情况下,您可以恢复 1 个错误(使用 x == 4,它将是 2 个可恢复的错误,等等 - 确切的等式是 nb_error_recoverable == (x-2)
其中 x是计算重复的次数,因为您需要至少 2 次同意的计算才能使用多数票恢复)。
缺点是您需要计算 x 次而不是一次,因此您有额外的计算成本,但线性复杂度如此渐近,您不会因为获得的好处而损失太多。进行多数表决的一种快速方法是计算数组的众数,但您也可以使用中值滤波器。
此外,如果您想进一步确保计算正确进行,如果您可以制作自己的硬件,您可以使用 x 个 CPU 构建您的设备,并连接系统,以便在 x 个 CPU 上自动复制计算并完成多数表决最后机械地(例如使用与/或门)。这通常在飞机和关键任务设备中实现(参见 triple modular redundancy)。这样,您将不会有任何计算开销(因为额外的计算将并行完成),并且您有另一层防止软错误的保护(因为计算重复和多数表决将由硬件直接管理,而不是由软件——它更容易被破坏,因为程序只是存储在内存中的位......)。
似乎没有人提到的一点。你说你在 GCC 中开发并交叉编译到 ARM 上。你怎么知道你没有代码来假设可用 RAM、整数大小、指针大小、执行某个操作需要多长时间、系统将连续运行多长时间或各种类似的东西?这是一个非常普遍的问题。
答案通常是自动化单元测试。编写在开发系统上运行代码的测试工具,然后在目标系统上运行相同的测试工具。寻找差异!
还要检查嵌入式设备上的勘误表。您可能会发现“不要这样做,因为它会崩溃,因此启用该编译器选项并且编译器将解决它”。
简而言之,您最可能的崩溃来源是代码中的错误。在您确定不是这种情况之前,不要(还)担心更深奥的故障模式。
您需要 3 台以上的从机,并且在辐射环境之外有一个主机。所有 I/O 都通过包含投票和/或重试机制的主控。每个从站都必须有一个硬件看门狗,并且碰撞它们的调用应该被 CRC 等包围,以减少非自愿碰撞的可能性。 Bumping 应该由 master 控制,因此失去与 master 的连接等于在几秒钟内重新启动。
此解决方案的一个优点是您可以对主服务器和从服务器使用相同的 API,因此冗余成为一项透明功能。
编辑:从评论中我觉得有必要澄清“CRC 的想法”。如果您用 CRC 或对来自主设备的随机数据进行摘要检查来围绕碰撞,从设备碰撞其自己的看门狗的可能性接近于零。只有当受审查的从机与其他从机对齐时,该随机数据才会从主机发送。随机数据和 CRC/摘要在每次碰撞后立即清除。主从碰撞频率应大于double看门狗超时。每次从主站发送的数据都是唯一生成的。
如何运行您的应用程序的多个实例。如果崩溃是由于随机内存位更改引起的,那么您的一些应用程序实例很可能会通过并产生准确的结果。在给定位翻转概率的情况下,计算您需要多少个实例来实现您希望的微小总体误差可能很容易(对于具有统计背景的人)。
你问的是相当复杂的话题 - 不容易回答。其他答案还可以,但它们只涵盖了您需要做的所有事情的一小部分。
As seen in comments,不可能 100% 解决硬件问题,但很有可能使用各种技术减少或捕获它们。
如果我是你,我会创建最高 Safety integrity level 级别 (SIL-4) 的软件。获取 IEC 61513 文件(用于核工业)并遵循它。
有人提到使用较慢的芯片来防止离子轻易翻转位。以类似的方式,也许使用一个专门的 cpu/ram,它实际上使用多个位来存储一个位。因此提供了硬件容错,因为所有位都不太可能被翻转。所以 1 = 1111 但需要被击中 4 次才能真正翻转。 (4 可能是一个糟糕的数字,因为如果 2 位被翻转,它已经很模糊了)。因此,如果您使用 8,您将获得 8 倍的内存和更慢的访问时间,但数据表示更可靠。您可以在软件级别使用专门的编译器(为所有内容分配更多空间)或语言实现(为以这种方式分配内容的数据结构编写包装器)来执行此操作。或者具有相同逻辑结构但在固件中执行此操作的专用硬件。
也许了解硬件“为这种环境设计”是否意味着会有所帮助。它如何纠正和/或指示 SEU 错误的存在?
在一个与太空探索相关的项目中,我们有一个定制的 MCU,它会在 SEU 错误时引发异常/中断,但有一些延迟,即在导致 SEU 异常的一个 insn 之后可能会通过某些周期/执行指令。
特别容易受到攻击的是数据缓存,因此处理程序会使有问题的缓存行无效并重新启动程序。只是,由于异常的不精确性,以异常引发 insn 为首的 insn 序列可能无法重新启动。
我们确定了危险(不可重新启动)序列(如 lw $3, 0x0($2)
,后跟一个 insn,它修改 $2
并且不依赖于 $3
的数据),并且我对 GCC 进行了修改,因此不会发生此类序列(例如,作为最后的手段,用nop
分隔两个insn)。
只是要考虑的事情...
如果您的硬件出现故障,那么您可以使用机械存储来恢复它。如果您的代码库很小并且有一些物理空间,那么您可以使用机械数据存储。
https://i.stack.imgur.com/MdkE9.jpg
将有一个不受辐射影响的材料表面。会有多个齿轮。机械阅读器将在所有齿轮上运行,并且可以灵活地上下移动。 Down 表示它是 0,up 表示它是 1。从 0 和 1 可以生成代码库。
使用 cyclic scheduler。这使您能够添加定期维护时间以检查关键数据的正确性。最常遇到的问题是堆栈损坏。如果您的软件是循环的,您可以在循环之间重新初始化堆栈。不要为中断调用重复使用堆栈,为每个重要的中断调用设置一个单独的堆栈。
与看门狗概念类似的是截止时间计时器。在调用函数之前启动硬件计时器。如果函数在截止时间计时器中断之前没有返回,则重新加载堆栈并重试。如果在 3/5 次尝试后仍然失败,则需要从 ROM 重新加载。
将您的软件拆分为多个部分并隔离这些部分以使用单独的内存区域和执行时间(尤其是在控制环境中)。示例:信号采集、预处理数据、主要算法和结果实现/传输。这意味着一个部分的失败不会导致程序的其余部分失败。因此,当我们修复信号采集时,其余任务继续处理陈旧的数据。
一切都需要CRC。如果您在 RAM 之外执行,即使您的 .text 也需要 CRC。如果您使用循环调度程序,请定期检查 CRC。一些编译器(不是 GCC)可以为每个部分生成 CRC,并且一些处理器有专门的硬件来进行 CRC 计算,但我想这会超出你的问题范围。检查 CRC 还会提示内存上的 ECC 控制器在单个位错误成为问题之前对其进行修复。
使用看门狗进行启动不仅仅是一次操作。如果您的启动遇到问题,您需要硬件帮助。
首先,围绕故障设计您的应用程序。确保作为正常流程操作的一部分,它预期会重置(取决于您的应用程序以及软故障或硬故障的类型)。这很难做到完美:需要一定程度事务性的关键操作可能需要在组装级别进行检查和调整,以便关键点的中断不会导致外部命令不一致。一旦检测到任何不可恢复的内存损坏或控制流偏差,就会快速失败。如果可能,记录失败。
其次,在可能的情况下,纠正腐败并继续。这意味着经常校验和修复常量表(如果可以的话,还有程序代码);也许在每个主要操作之前或在定时中断上,并将变量存储在自动更正的结构中(再次在每个主要操作之前或在定时中断上从 3 中获得多数票,如果是单个偏差则更正)。如果可能,记录更正。
第三,测试失败。设置一个可重复的测试环境,以伪随机方式翻转内存中的位。这将允许您复制损坏情况并帮助围绕它们设计您的应用程序。
鉴于 supercat 的评论、现代编译器的趋势以及其他因素,我很想回到古代,并在各处用汇编和静态内存分配编写整个代码。对于这种绝对的可靠性,我认为组装不再会产生很大的成本差异。
这里有大量的回复,但我会尝试总结我对此的想法。
某些崩溃或无法正常工作可能是您自己的错误导致的 - 那么当您找到问题时应该很容易修复。但也存在硬件故障的可能性——如果不是不可能的话,总体上很难修复。
我建议首先尝试通过记录(堆栈、寄存器、函数调用)来捕捉有问题的情况——或者通过将它们记录到文件中的某个位置,或者以某种方式直接传输它们(“哦,不——我崩溃了”)。
从这种错误情况中恢复是重新启动(如果软件仍然存在并且正在运行)或硬件重置(例如硬件看门狗)。从第一个开始更容易。
如果问题与硬件相关 - 那么日志记录应该可以帮助您确定发生在哪个函数调用问题中,并且可以让您了解哪些地方不工作以及在哪里工作。
此外,如果代码相对复杂——“分而治之”是有意义的——这意味着你删除/禁用一些你怀疑问题所在的函数调用——通常禁用一半代码并启用另一半——你可以获得“确实有效”/ “不起作用”的决定,之后您可以专注于另一半代码。 (问题出在哪里)
如果一段时间后出现问题 - 则可能会怀疑堆栈溢出 - 那么最好监视堆栈点寄存器 - 如果它们不断增长。
如果您设法完全最小化您的代码,直到“hello world”类型的应用程序 - 它仍然随机失败 - 那么硬件问题是预期的 - 并且需要“硬件升级” - 意味着发明这样的 cpu / ram / ... - 可以更好地耐受辐射的硬件组合。
最重要的事情可能是,如果机器完全停止/重置/不工作,你如何取回日志——可能是 bootstap 应该做的第一件事——如果发现有问题的情况,你应该回家。
如果在您的环境中也可以传输信号并接收响应 - 您可以尝试构建某种在线远程调试环境,但是您必须至少有通信媒体工作并且一些处理器/一些内存处于工作状态。通过远程调试,我的意思是 GDB / gdb 存根类型的方法或您自己的实现,您需要从应用程序中返回(例如下载日志文件、下载调用堆栈、下载 ram、重新启动)
我真的读了很多很棒的答案!
这是我的 2 美分:通过编写软件来检查内存或执行频繁的寄存器比较,建立内存/寄存器异常的统计模型。此外,以虚拟机的形式创建一个模拟器,您可以在其中试验该问题。我想如果你改变结尺寸、时钟频率、供应商、外壳等会观察到不同的行为。
即使是我们的台式电脑内存也有一定的故障率,但这并不影响日常工作。
不定期副业成功案例分享