为满足客户对非易失数据存储的要求,设计师通常会使用一个串行EEPROM。这些EEPROM器件体积很小、价格便宜、而且在市场上已经被长期使用,设计工程师使用这些器件非常便利。但是在当今对成本非常敏感的市场领域,增加一个并不昂贵的EEPROM可能会使设计超出预算。
很多处理器使用闪存存储程序代码,使用静态RAM存储数据信息。尽管充分利用闪存未使用的部分作为非易失数据存储很有吸引力,但是传统的哈佛架构制约了这种应用。MAXQ架构属于哈佛架构,具有独立的代码和数据总线。但MAXQ器件包含的硬件部分可以实现伪冯诺依曼架构,如同访问数据空间一样访问代码空间。这种额外的多功能性,结合MAXQ的效用函数,可实现存储器的擦写服务,为完整的可读写非易失存储子系统提供了解决方案。
关于闪存的基本考虑
闪存是一种电子可擦除存储。通常也被认为是“主读”。简言之,尽管闪存是可写的,但数据更新并不会很频繁,多数的操作都是读操作。多数闪存器件从字面意思来讲都是可写的,但每次只能整块擦除。这使得那些存储器件通常都不适合用于易变存储,只适合用作从不改变内容的固定数据存储。
有两种类型的闪存:NAND闪存和NOR闪存。NAND闪存用在存储卡和U盘中。通常,由于数据按时钟串行传输,从NAND器件中读数据需要数个周期。这种有序的操作使NAND闪存不适合用作程序代码存储,因为读取时间会太长。相反,NOR闪存类似传统字节宽度或字宽度存储。读取NOR闪存就像读取 ROM器件:先发器件选择和地址命令,在等待足够的存取时间之后,从总线上读取数据。NOR闪存用于MAXQ处理器中。
MAXQ处理器的闪存
MAXQ处理器中使用的闪存单元被擦除时会变成“1”状态。因此,在擦除后,存储单元中的每个位置都将包含0xFFFF。对某一存储位置进行编程会把某些位从“1”变成“0”状态。为了使被编程过的位重新变回“1”状态,整个单元必须被擦除。
任何电擦除的存储器件必须面临的问题是持久性。根据特定的技术,一个闪存单元在完全失效之前,可以忍耐的擦除次数少达1,000次,多达 1,000,000次。因此,使用闪存做数据存储的任何应用都必须保证写操作循环在整个阵列中均匀分布,不允许某一个位置的擦除编程次数比其他位置多很多。
多数闪存器件都允许把之前已经编程过的位置中的没有编程过的位从“1”变成“0”状态。例如,对于多数器件,允许对某个位置编程,把数据从 0xFFFE变为0x7FFE,在这个编程过程中,没有任一个位从“0”变成“1”。然而,MAXQ系列器件中使用的闪存,不允许对已经编程过的位置再编程,即使没有发生从“0”到“1”的变化。这样的写操作会失败,保持数据在0xFFFE状态。
MAXQ器件编程受限制的理由如下。正在被编程的存储单元起初是作为代码空间的,所以必须很谨慎,禁止对已写过的位置的任何写操作。0xFFFF指令指一条无效的源代码,它不太可能出现在有效的代码单元内。因此,阻止对已编程过的位置的写操作能够帮助保持代码单元的完整性。
使用MAXQ2000的设计方案
在这篇文章中,我们主要讨论一个MAXQ器件:MAXQ2000。这个微处理器具有64kB的程序存储空间,具体划分为128个单元,每个单元具有 256个16位的字。下面介绍两种设计方案。第一种方案适用于信息写一次后在产品的生命周期内不会再频繁改动,例如校准数据、版本信息和特征参数等等。第二种方案针对更普通的装置,这种设计要允许数据频繁改动,例如使用信息或者详细记录等。
方案1
问题:校准数据需要存储在产品中。产品会一直需要重新校准,所以校准数据必须保存在一个可改写的存储空间中。
解决方案:这实际上是可以想象的到的最简单的情况。MAXQ2000程序存储空间中预留了两个单元用于存储校准数据,一个是字地址0x7E00,另一个是0x7F00。当MAXQ2000第一次收到保存校准数据的命令,它就会检查这两个单元并确保它们是空的。校准数据被保存到第一个单元。
当MAXQ2000第二次收到保存校准数据的命令,它会再次检查这两个单元。当发现0单元已被使用,它会把校准数据复制到1单元,接着擦除0单元。
当MAXQ2000收到读校准数据的请求(例如在上电时),它会读这两个单元,看哪个正在被使用。没有被擦除的单元用来配置器件到校准状态。
这种方案的最主要优点是简易性。当某些应用需要在上电时作配置时(或者其它恢复配置的情况),这种方法很适用。读程序会接收到一个字指针,并返回这个地址的内容;写程序会接收一个字指针,并尝试对这个地址写操作。擦除程序会擦除这两个单元。
这种方案的缺点是:子程序极其不智能。无法判断写操作是否成功。假如写操作失败了,不会有任何动作来解决问题。为什么这种方案只适用于对已知的空单元写操作,以上这种限制就是其原因。
方案2
问题:非易失存储用来跟踪用电量和其它会经常改变的数据。更新的频率从一周数次到一天数次。电表是一个很好的应用举例。
解决方案:在这种情况下即时传统的EEPROM也需要另求帮助。问题在于:频繁的更新、和可擦除存储技术固有的有限电擦除寿命,排除了使用单个 EEPROM频繁写擦除操作的可能性。假如这种应用需要每小时更新一次。具有10,000次写擦除次数限制的EEPROM只能工作一年,远远低于电表应用所需的10年寿命设计目标。
这个问题的解决方案是实现“损耗平衡”。也就是指不对单一位置重复写操作。相反,在整个存储阵列中执行均匀分布的写操作。
正是基于此目的,损耗平衡技术被接受,并在闪存存储设备中使用。它的算法非常复杂难以理解。但对于我们,介绍简单的机理就足够了。
在这种设计中,存储阵列中的数据条目并不是以地址作参考,而是以数据元编号为参考。数据元编号是一个和数据元素一一对应的任意8位数值。所以在这个方案中最大可以有255个数据元(数据元0预留)。每个数据元包含:包括数据元编号和数据元长度的双字节报头(图1);指示1、2、3或者4个16位字的两位码,以保留足够的剩余空间进行错误管理。
图1. 数据单元报头结构
写数据单元时,写子程序必须知道要写的数据、数据单元的长度和数据要写的位置。很容易可以知道数据写在哪个地址;无论是新数据单元还是更新已有的数据单元,都会写在存储阵列的末尾。写命令会搜索阵列的末尾,紧接着在最后一个记录后写入新的数据单元。假如在当前的存储页面没有足够的空间保存特定长度的数据元,就会出现页面结束标志并打开一个新页面。图2是这种典型的数据页面的结构。
在这个数据页面中,先写数据单元1,并更新。接着写数据单元4,但从不更新。随后是数据单元3,共更新7次。最后写入数据单元2,从不更新。
页面结束标志的出现表明曾执行写操作,但数据单元太长不能填入当前页面。会打开一个新页面以填写数据单元。在整个数据结构的末尾会分配一个空单元,这里本该是一个数据单元的报头。
图2. 典型的数据页面
要注意这种方案并没有涉及重复记录的问题。因为重复记录不会有问题。事实上,读和写程序都会完全忽略重复记录。写操作时,不管同样编号的记录是否存在,新记录都会写在阵列的末尾。当读操作时,只有符合请求记录编号的最后一个(所以是最近的)记录可以读到。
从阵列中读取数据元会比写操作更复杂。读功能会收到数据元编号和数据元应该写入的地址。当执行此命令时,读操作会从阵列的最开始进行搜索。当它找到一个记录符合被请求的数据元,它会保存这个地址并继续搜索。当它找到另外一个匹配的记录时,读命令会用新地址替代已保存的地址。当搜索到阵列的末尾时,保存的地址就会是符合请求记录的最近写入的记录。当执行读操作时,就会把这个数据复制到缓存中。
尽管所介绍的用来从存储阵列中保存和读取记录的主读机制是可行的,还会存在一个问题:没有机制可以重新使用被过时的记录占用的空间(也没有机制可以删除记录。但由于这种方案是针对嵌入式应用开发的,所以可能不会是个很严重的问题。)。如果不重新恢复一些空间,这些之前被分配的空间会很快用尽。由于闪存只能每次擦除一整个页面,恢复空间意味着擦除整个页面。另外一个更严重的问题是闪存页面不能被随便擦除,会存在删除有用信息的风险。唯一的可选方案是在删除整个旧页面之前,把有用信息复制到一个新页面。
从废旧的记录中恢复空间有三个步骤。第一,打开新页面,把每个数据元的最近版本复制到新页面中。第二,擦除旧页面。第三,对新页面做页面标示,是读命令可以找到新页面。
第一个步骤比较复杂,需要更详细的检查。完成这个步骤最简单的方法可以分成两个子步骤:第一,使用一个RAM保存记录编号和阵列中最近记录的地址;第二,从RAM阵列逐一复制最近记录到新闪存页面。这个过程最快,并且相对简单。
使用这两个子步骤存在的问题是MAXQ2000只有1k字的RAM空间。上述方案限制了可以保存到RAM中作缓存的数据量。这明显是不能接受的。
这种难题的解决方案非常耗时,但不管存储阵列变得多大(合理范围内)都是可行的。为源阵列中的每一项条目单独操作,而不是在RAM中建立指针。因此,算法可以简化为:
从源阵列读取一个数据元。
在目标阵列中搜索这个数据单元。假如找到了,就说明这个数据单元已经写入。源指针增加并返回步骤1。
在源阵列中搜索这个数据单元最近记录。
把这个数据单元的最近记录写入目标阵列中。
源指针增加并返回步骤1。
最后,在目标阵列中,每个数据元都有精确的条目对应。填写后的页面如图3示意。这样,就可以安全擦除源页面,并把页面报头写入目标阵列。
图3. 空间恢复后,图2中的数据页面会如此表现。
这个过程对于数据存储会非常安全。然而,当使用闪存器件时还要面对另一个风险:在写或者擦除操作中掉电。假如发生掉电,有可能会破坏一个或者多个页面 (例如写操作)或者不能完全擦除页面(例如擦除操作)。而我们这种紧凑的操作从本质上讲是安全的。可以考虑以下的情况:
假如在填写操作时掉电,源页面仍然保持完整。当重新上电后,很容易辨别新写的页面(他们没有页面报头)并把它擦除,再重新启动填写操作。
假如在正擦除旧页面时掉电,可能会包含无效的报头。可以擦除这些页面并把报头添加到新页面。
假如在正把页面报头写入到新页面时掉电,数据仍然是完整的。页面报头更新操作可以再次重新启动。
简言之,这些预料之外的突发事件不会导致阵列数据被破坏并不可恢复。
方案2的改进
这里介绍的存储子系统没有错误检测的机制。在数据元标识符中有一些位(这里的是6位)没有被使用。可以使用CRC6算法(x6 + x + 1)根据数据元计算出CRC以确保没有发生读写错误。这虽然不是特别强大的算法(它会错过多位错误中的1/64),它可以检测到多数可能发生的错误。
这个方法对系统的另一个限制是读取时间很长。每次读操作都需要读取阵列中的所有记录,以找到最近的记录。有3个方法可以用来缩短读取时间:
在数据单元中为正向指针留一个空字节。当数据更新时,为正向指针分配一个新的入口地址。按照这种方式,数据表格可以按链表的方式移动。
向后移动表格。这样就可以在被请求信号第一次出现时停止搜索。
假如单元数量很少,可以在启动时建立一个RAM阵列,包含单元ID和指针。后面的读取会很快。只需读RAM阵列以决定从哪儿获取数据单元。
结论
非易失数据存储是每位设计工程师迟早都必须面对的问题。使用MAXQ处理器灵活的闪存,就不用再借助于串行存储保存配置数据了。