我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
我们正在编译一个嵌入式C++应用程序,它部署在电离辐射轰击的环境中的屏蔽设备中。我们正在为ARM使用GCC和交叉编译。在部署时,我们的应用程序会生成一些错误的数据,并且比我们希望的更频繁地崩溃。硬件是为这个环境而设计的,我们的应用程序已经在这个平台上运行了几年。
我们是否可以对代码进行更改,或对编译时进行改进,以识别/纠正由单个事件扰乱导致的软错误和内存损坏?是否有其他开发人员成功地减少了软错误对长时间运行的应用程序的有害影响?
当前回答
为放射性环境编写代码实际上与为任何任务关键型应用程序编写代码没有什么不同。
除了已经提到的内容外,还有一些杂项提示:
使用任何半专业嵌入式系统都应具备的日常“面包和黄油”安全措施:内部看门狗、内部低电压检测、内部时钟监视器。这些事情在2016年甚至不需要提及,它们几乎是每个现代微控制器的标准。如果您有一个面向安全和/或汽车的MCU,它将具有某些看门狗功能,例如给定的时间窗口,您需要在其中刷新看门狗。如果您有任务关键型实时系统,则首选此选项。一般来说,使用适用于这类系统的MCU,而不是在一包玉米片中收到的普通主流绒毛。现在几乎每个MCU制造商都有专门为安全应用设计的MCU(TI、Freescale、Renesas、ST、Infineon等)。它们有很多内置的安全功能,包括锁步内核:这意味着有两个CPU内核执行相同的代码,它们必须彼此一致。重要事项:您必须确保内部MCU寄存器的完整性。硬件外设的所有可写控制和状态寄存器可能位于RAM内存中,因此易受攻击。为了保护自己免受寄存器损坏,最好选择具有内置寄存器“一次写入”功能的微控制器。此外,您需要在NVM中存储所有硬件寄存器的默认值,并定期将这些值复制到寄存器中。您可以以同样的方式确保重要变量的完整性。注意:始终使用防御性编程。这意味着您必须在MCU中设置所有寄存器,而不仅仅是应用程序使用的寄存器。你不希望一些随机的硬件外设突然醒来。有各种各样的方法来检查RAM或NVM中的错误:校验和、“行走模式”、软件ECC等。现在最好的解决方案是不使用任何这些,而是使用内置ECC和类似检查的MCU。因为在软件中这样做很复杂,因此错误检查本身可能会引入错误和意外问题。使用冗余。您可以将易失性和非易失性内存存储在两个相同的“镜像”段中,这两个段必须始终相等。每个段可以附加CRC校验和。避免使用MCU外部的外部存储器。为所有可能的中断/异常实现默认中断服务例程/默认异常处理程序。即使是你不使用的。默认例程除了关闭自己的中断源之外,不应该做任何事情。理解并接受防御性编程的概念。这意味着您的程序需要处理所有可能的情况,即使是理论上无法发生的情况。示例。高质量的任务关键型固件检测到尽可能多的错误,然后以安全的方式处理或忽略它们。不要编写依赖于指定不良行为的程序。这种行为可能会因辐射或EMI引起的意外硬件变化而发生剧烈变化。确保您的程序没有此类垃圾的最佳方法是使用像MISRA这样的编码标准,以及静态分析器工具。这也有助于防御编程和消除bug(为什么您不想在任何类型的应用程序中检测bug?)。重要提示:不要依赖静态存储持续时间变量的默认值。也就是说,不要信任.data或.bss的默认内容。从初始化点到实际使用变量的点之间可能有任何时间,RAM可能有足够的时间损坏。相反,编写程序,以便在运行时从NVM中设置所有此类变量,就在首次使用此类变量之前。在实践中,这意味着如果变量在文件范围内声明或声明为静态,则永远不应该使用=来初始化它(或者可以,但这是没有意义的,因为无论如何都不能依赖于值)。始终在运行时设置,就在使用之前。如果可以从NVM中重复更新这些变量,那么就这样做。类似地,在C++中,对于静态存储持续时间变量,不要依赖构造函数。让构造函数调用公共的“设置”例程,您也可以稍后在运行时直接从调用方应用程序调用该例程。如果可能的话,请完全删除初始化.data和.bss(并调用C++构造函数)的“向下复制”启动代码,这样在编写依赖于这些的代码时就会出现链接器错误。许多编译器都可以选择跳过这一步,通常称为“最小/快速启动”或类似操作。这意味着必须检查任何外部库,以便它们不包含任何此类依赖。实现并定义程序的安全状态,以便在发生严重错误时恢复到该状态。实施错误报告/错误日志系统总是有帮助的。
其他回答
如何运行应用程序的许多实例。如果崩溃是由于随机的内存位变化造成的,那么你的一些应用程序实例很可能会通过并产生准确的结果。(对于有统计背景的人来说)很容易计算出在给定的比特翻转概率下需要多少个实例才能实现所希望的最小总体错误。
有人提到使用较慢的芯片来防止离子同样容易地翻转比特。以类似的方式,可能使用专门的cpu/ram,它实际上使用多个位来存储单个位。因此,提供了硬件容错,因为不太可能所有的位都被翻转。所以1=1111,但需要被击中4次才能真正翻转。(4可能是一个坏数字,因为如果2位被翻转,它就已经不明确了)。因此,如果您使用8,您得到的ram将减少8倍,访问时间也会慢一些,但数据表示更可靠。您可能可以在软件级别使用专门的编译器(为所有内容分配更多的空间)或语言实现(为以这种方式分配内容的数据结构编写包装器)来实现这一点。或具有相同逻辑结构但在固件中执行此操作的专用硬件。
能帮助你的是看门狗。20世纪80年代,看门狗被广泛用于工业计算。当时,硬件故障更为常见——另一个答案也提到了那个时期。
看门狗是一种组合的硬件/软件功能。硬件是一个简单的计数器,从一个数字(比如1023)向下计数到零。可以使用TTL或其他逻辑。
软件的设计使得一个例程可以监控所有基本系统的正确运行。如果此例程正确完成=发现计算机运行正常,则将计数器设置回1023。
总体设计使得在正常情况下,软件可以防止硬件计数器达到零。如果计数器达到零,计数器的硬件将执行其唯一的任务并重置整个系统。从计数器的角度来看,零等于1024,计数器继续向下计数。
该看门狗可确保所连接的计算机在多次故障情况下重新启动。我必须承认,我不熟悉能够在当今计算机上执行这种功能的硬件。与外部硬件的接口现在比过去复杂得多。
看门狗的一个固有缺点是,从出现故障到看门狗计数器达到零+重新启动时间,系统就不可用。虽然该时间通常比任何外部或人为干预短得多,但在该时间段内,受支持的设备需要能够在没有计算机控制的情况下继续工作。
以下是一些想法和想法:
更创造性地使用ROM。
在ROM中存储任何可以存储的东西。不要计算东西,而是将查找表存储在ROM中。(确保编译器将查找表输出到只读部分!在运行时打印内存地址以进行检查!)将中断向量表存储在RAM中。当然,运行一些测试以查看ROM与RAM相比的可靠性。
为堆栈使用最好的RAM。
堆栈中的SEU可能是最可能的崩溃源,因为它是索引变量、状态变量、返回地址和各种类型的指针通常存在的地方。
执行计时器滴答声和看门狗计时器例程。
您可以在每一次计时器计时时运行一个“健全性检查”例程,以及一个看门狗例程来处理系统锁定。您的主代码还可以周期性地增加一个计数器以指示进度,而健全性检查例程可以确保发生了这种情况。
在软件中执行纠错代码。
您可以为数据添加冗余,以便能够检测和/或纠正错误。这将增加处理时间,可能会使处理器长时间暴露在辐射中,从而增加出错的机会,因此您必须考虑权衡。
记住缓存。
检查CPU缓存的大小。您最近访问或修改的数据可能位于缓存中。我相信您可以禁用至少一些缓存(以较大的性能代价);你应该试试看缓存对SEU的敏感性。如果缓存比RAM更硬,那么您可以定期读取和重新写入关键数据,以确保它保留在缓存中并使RAM恢复正常。
巧妙地使用页面错误处理程序。
如果将内存页标记为不存在,CPU将在您尝试访问它时发出页面错误。您可以创建一个页面错误处理程序,在处理读取请求之前进行一些检查。(PC操作系统使用此功能透明地加载已交换到磁盘的页面。)
对关键的事情使用汇编语言(可能是所有事情)。
使用汇编语言,您知道寄存器中的内容和RAM中的内容;你知道CPU使用的是什么特殊的RAM表,你可以用迂回的方式来设计,以降低风险。
使用objdump实际查看生成的汇编语言,并计算每个例程占用的代码量。
如果你使用的是像Linux这样的大型操作系统,那么你就是在自找麻烦;有太多的复杂性和太多的事情要出错。
记住这是一场概率游戏。
一位评论者说
你为捕捉错误而编写的每一个例程都会因同样的原因而失败。
虽然这是真的,但检查例程正确运行所需的(例如)100字节代码和数据中发生错误的机会要比其他地方发生错误的几率小得多。如果你的ROM非常可靠,并且几乎所有的代码/数据都在ROM中,那么你的可能性就更大了。
使用冗余硬件。
使用具有相同代码的两个或更多相同硬件设置。如果结果不同,应触发重置。对于3个或更多设备,您可以使用“投票”系统来尝试确定哪一个已被破坏。
在小型卫星的软件/固件开发和环境测试方面工作了大约4-5年,我想在这里分享我的经验。
*(小型卫星比大型卫星更容易发生单次事件干扰,因为其电子部件的尺寸相对较小且有限)
非常简洁和直接:没有机制可以从可检测到的错误中恢复过来软件/固件本身的情况,至少没有用于恢复目的的软件/固件的最低工作版本副本,以及支持恢复的硬件(功能)。
现在,这种情况通常在硬件和软件两级处理。在这里,根据您的要求,我将分享我们在软件级别可以做的事情。
…恢复目的。。。。提供在真实环境中更新/重新编译/刷新软件/固件的能力。这几乎是高度电离环境中任何软件/固件的必备功能。如果没有这一点,您可以拥有任意数量的冗余软件/硬件,但在某一点上,它们都会崩溃。所以,准备好这个功能!…最低工作版本。。。在您的代码中具有响应性、多个副本、最低版本的软件/固件。这类似于Windows中的安全模式。不要只拥有一个功能完整的软件版本,而是拥有软件/固件的最低版本的多个副本。最小副本通常比完整副本小得多,并且几乎总是只有以下两个或三个功能:能够监听来自外部系统的命令,能够更新当前软件/固件,能够监控基本操作的内务数据。…复制…某处。。。在某处安装冗余软件/固件。无论有无冗余硬件,您都可以尝试在ARM uC中使用冗余软件/固件。这通常是通过在单独的地址中有两个或多个相同的软件/固件来实现的,这些软件/固件将向彼此发送心跳信号,但一次只有一个处于活动状态。如果已知一个或多个软件/固件没有响应,请切换到其他软件/固件。使用这种方法的好处是,我们可以在发生错误后立即进行功能更换,而无需与负责检测和修复错误的任何外部系统/方进行任何联系(在卫星情况下,通常是任务控制中心(MCC))。严格来说,如果没有冗余硬件,这样做的缺点是实际上无法消除所有单点故障。至少,您仍然会有一个单一的故障点,那就是交换机本身(或者通常是代码的开头)。然而,对于高度电离环境中受尺寸限制的设备(如微微/毫微微卫星),在没有额外硬件的情况下将单点故障减少到一点仍然值得考虑。更重要的是,用于切换的代码肯定会比整个程序的代码少得多,从而显著降低了在其中出现单一事件的风险。但是,如果您没有这样做,您的外部系统中应该至少有一个副本,该副本可以与设备接触并更新软件/固件(在卫星情况下,它也是任务控制中心)。您还可以在设备的永久内存存储中保存副本,该副本可以被触发以恢复正在运行的系统的软件/固件…可检测到的错误情况。。该错误必须是可检测的,通常通过硬件纠错/检测电路或通过一小段纠错/检测代码来检测。最好将这些代码放得小、多,并且独立于主软件/固件。其主要任务仅用于检查/纠正。如果硬件电路/固件是可靠的(例如,它比其余的更抗辐射-或具有多个电路/逻辑),那么您可以考虑使用它进行错误校正。但如果不是,最好将其作为错误检测。可通过外部系统/设备进行校正。对于纠错,您可以考虑使用像Hamming/Golay23这样的基本纠错算法,因为它们可以更容易地在电路/软件中实现。但这最终取决于团队的能力。对于错误检测,通常使用CRC。…支持恢复的硬件现在是这个问题上最困难的方面。最终,恢复需要负责恢复的硬件至少能够正常工作。如果硬件永久损坏(通常发生在其总电离剂量达到一定水平后),则软件无法帮助恢复。因此,对于暴露在高辐射水平下的设备(如卫星)来说,硬件无疑是最重要的关注点。
除了上述预测固件错误的建议外,我还建议您:
子系统间通信协议中的错误检测和/或错误校正算法。这是另一个几乎必须具备的功能,以避免从其他系统接收到不完整/错误的信号过滤ADC读数。请勿直接使用ADC读数。通过中值过滤器、均值过滤器或任何其他过滤器对其进行过滤-切勿相信单个读数。多采样,而不是少采样-合理。