内部 FLASH 存数据,你一定要知道 ......

最近有个朋友趁着假期这几天跑来和我诉苦,说是他们公司正在大搞降本,领导要求凡是进口的器件全部换成国产,凡是非必要部分一律砍掉,通过软件去修补由于硬件降本所丢失的功能。这也导致原本的软件需要大改,从而去适配新的硬件平台,且原本一些非常容易实现的功能现在变得很复杂。

诚然,现在市场环境比较差,基本所有有竞争对手的制造型企业都在通过对自身产品的压价来抢占市场份额,这也就意味着产品的生产成本必然要下降,毕竟谁也不愿意做亏本的买卖,那么对于研发来说,硬件降本是没法绕开的。

朋友听后释然了不少,确实,目前的大环境就是这样,这并不完全是某个领导或某个企业自身的问题。实际上这次来找我也是在降本过程中有个问题想请教一下 —— 原本他们产品的数据是存储在铁电(一个外部存储器)中,现在降本把铁电去掉了,硬件上也没有其他外置存储器了,此时还有什么数据存储的思路。

没有外置存储器,还要存数据,那就只有一条路,就是 MCU 的内部 FLASH。朋友说他也知道只能采用这种方案,但是利用内部 FLASH 存数据好像比较麻烦,就想问需要注意哪些点。

相信这也不仅仅是我朋友的疑问,很多读者朋友也会遇到使用内部 FLASH 来存储数据的开发场景,今天我们就简单聊聊使用内部 FLASH 存储数据的一些注意点。

容量与存储量

做数据存储,第一个考虑的就是预期的数据存储量有多少,众所周知 MCU 的内部 FLASH 容量是比较小的,主流 MCU FLASH 大小通常都是几十到几百 KB,这里面本身就要存放程序固件,如果有 IAP 功能则还要预留 bootloader 空间,此时留给数据存储的空间就非常少了,因此这种情况下如果要借助内部 FLASH 进行数据存储则必须要提前规划好最大的数据存储量。如果仅仅是实现少量配置参数的存储,那这种方案就是可行的,而如果是存储大量的日志、数据记录,那使用内部 FLASH 存储就显得不太合理了,这时候该上外部存储器还是得上。

数据操作与性能差异

普遍来说一个 MCU 的内部 FLASH 操作必须遵循先擦后写的流程,而不像铁电存储器只需要往某个或某块地址写入新值就能够实现数据的存储或修改。这是由于物理材料的限制,原理在本文中就不赘述了。且内部 FLASH 的擦写循环次数往往比较少,如最主流的 STM32F103 系列,其手册中能看到擦写次数仅 10K 次也就是一万次:

图片

而铁电存储器则具有 1 万亿的读写次数。

图片

这也意味着内部 FLASH 并不适合存储频繁变化的数据,假设你需要每一秒就擦除并存储一次数据,按照 10K 次的擦写次数仅需 2 小时就能把 FLASH 写坏(理论值)!因此内部 FLASH 适合存储一些不经常变化的数据,如不常修改的配置参数,或出厂设置的标定数据。如果对数据存储频率有比较高的要求,那么内部 FLASH 方案是不合理的。

擦除粒度与数据的部分修改

上文中提到内部 FLASH 的操作是需要先擦除才能写入新数据,这就会涉及到 “擦除粒度” 的概念,擦除粒度指的是一次擦除的最小单位,主流 MCU 通常以一个扇区作为最小擦除粒度,以 STM32F103 其中一款为例:

图片

其扇区(页)的大小为 2K,也就意味着即使我要存储的数据只有几十个字节,也需要将其所在扇区的 2K 内容全部擦除才能写入。

如果此时一个扇区中存在多个不同的数据存储块(如地址 0-99 为系统配置参数,地址 100-199 为通讯配置参数),那么你就无法实现在不动其他数据块的情况下仅修改其中一个数据块的功能。

此时即使你只修改其中一个数据块,为了避免其他数据块内容的丢失,你也必须先将待修改数据所在的扇区内容全部读取到缓存中,再在缓存中进行修改,最后擦除扇区并将最新数据写入到擦除的扇区中。(当然这里对于擦除和更新缓存的顺序没有强制要求)

到这里会发现这种操作还会受制于一个关键因素 —— RAM。因为你必须要先读取扇区内容才能实现数据的部分修改,当你要操作的是一个 128 K 的扇区,而你只有几十 K 的 RAM 时,这种方案就无法实现了。 

磨损平衡与 FLASH 寿命延长

我们已经知道 FLASH 的物理寿命是固定的(10K 次),这里所说的延长肯定不是延长其物理寿命,而是指通过软件,在这个 10K 次擦写中实现大于 10K 次的数据修改。

这里就会产生一个新的概念 —— 磨损平衡。我们以一个 2000 字节(此处为了后续好计算取整为 2000,正常 2K 是 2048)扇区举例,如果我有 100 个字节的数据要存储,最简单的方法就是使用扇区的 0-99 地址来存储数据,每次修改时擦整个扇区,然后将数据写入地址 0-99。写到 10K 次以后这个扇区就报废。

这时候你会发现,我只写 100 个字节,但却要擦 2000 字节,也就是说有 1900 个字节的容量是白白浪费掉的!那有没有办法将这些没操作的容量利用起来呢?

当然是有的!

假设我第一次写入,将 0 - 99 的地址写入数据。此时我想修改,原先的方案是擦除这这个扇区,然后继续往 0 - 99 写入;但现在我不这么做,取而代之的,我直接往 100 - 199 这个地址写入最新的数据。是的,由于之前擦除了整个扇区,意味着对于 100 - 199 这个地址来说,是没有写入过的,因此在写入新数据时不需要擦除而是直接写入!现在这个扇区的数据结构是这样的:

图片

此时扇区中存储了两份数据,当我需要读取数据时实际要读的是我最后存储的那一份数据,在上面这个存储逻辑中,也就是只要按照 100 这个偏移从 0 地址开始读,直到读到全空的数据块(一般情况下是全 0xff)则说明上一个数据块里的数据是我们想要的数据。(当然如果真这么做我们会给每一块数据块加入额外的一些信息用来标志每个块的状态,优化读取逻辑,这里只是演示底层逻辑,因此也不过多赘述,如有需要后续会专门讲解这部分逻辑)

此时我们发现,原本每次修改就要擦除重写,而现在只需要不停往后写,直到写满扇区才擦除重写,如果按照 100 个字节一个数据块来写,那 2000 字节的扇区需要写 20 次才会需要擦除,直接将 FLASH 寿命延长了 20 倍!

这,就是磨损平衡的最底层原理!也是绝大多数 FLASH 数据库的底层架构。剩余的无非就是对这个逻辑的状态完善与封装,使得数据更可靠,查找更快速,更新更稳定。

数据封装

上面一小节中我们将数据原封不动存入了 FLASH 中,这样自然是最简单,最快的,但在想要查找数据时我们发现我们需要一块一块地去找,还要找到下一个块才能确定这一个块是不是最新的,此外假设极端情况下存储的数据就是全 0xff(即与擦除后的数据完全一样),就无法区分有效数据和无效数据。最后,即使获取到了数据,我们实际上也完全没法确定这个数据到底是不是对的,只能猜测这数据可能是对的。然而,在实际运行场景下往往可能存在各种各样的干扰,我们不可能依赖于单纯的猜测。

到这里,相信大家都能感觉到,仅仅存储数据本身是有问题的,我们需要加入一些信息,要能够区分出数据的有无,新旧以及对错。也就是对数据进行 “封装”。

一般情况下,我们的封装可以遵循一个通用的结构,即 数据头 + 数据体 + 校验:

图片

在数据头中我们可以加入数据状态的定义,如未写入、正在写入、数据有效、数据过时等等,这样我们读到这个数据块本身即可直到其是否有效,而无需再读到下一个数据。

数据体则存放原始的数据。

校验则是对整个数据块进行特定的计算后生成的校验值,当下一次取出数据的时候,可以再次对取出的数据块进行同样的计算,再与这个读取到的校验值进行判断,如果相同则可以确认读取到的数据就是当初存入的数据,反之说明数据块已经被损坏,读到的数据不再可靠,进而执行一些异常处理。通常这个校验算法为 CRC。

至此可以说我们的数据块已经相对完备了不少,在一些简单的场景下已经能够满足需求了。当然你也可以在数据头中增加更多信息来实现更丰富的功能,或是使用更强大的校验算法来保证数据的稳定性。但至少我们的基础设施已经搭建完成。

数据冗余、备份

在一些对数据稳定性要求非常高的场景下,如航天、医疗安全领域,还会通过做冗余、备份来进一步提高存储数据的可靠性。实现原理很简单,就是使用两个甚至多个扇区来存储同样的数据,即所谓的双备份、三备份等等。

最后,FLASH 存数据固然可行,但也能看出相比于单独搞一个存储器还是有诸多限制,软件实现也更为复杂,而复杂的代码也就意味着出 BUG 的可能性更高,可维护性下降。并且内部 FLASH 在进行操作时也会对系统的运行带来一些影响,如无法响应中断或暂停取指从而导致系统暂时性卡顿等。因此在有条件的情况下还是尽量使用外部存储。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WKJay_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值