以事务方式将数据保存在Arduino上的EEPROM中

EEPROM的存在为开发人员提供了一种方便的工具,用于保存配置参数或电源中断应持续的缓慢变化的状态。 在本文中,我们将研究如何尽可能安全,方便地执行此操作,以免忘记任何内容并且不记住不存在的内容。

假设我们有一个变量,我们想将其存储在EEPROM中。 似乎所有这些工具都掌握在我们手中:

#include <EEPROM.h> int my_var = DEFAULT_VALUE; EEPROM.get(MY_VAR_ADDR, my_var); my_var = NEW_VALUE; EEPROM.put(MY_VAR_ADDR, my_var); 

但是,仔细研究发现,这种方法产生的问题多于解决的问题。 我们将按顺序讨论它们。

1.如何确保我们准确阅读了我们写下的内容(以保证完整性 )? 想象一下下面的图片。 如果因断电或重置信号而突然死亡,我们会给自己写一封信,并将其放在办公桌抽屉中。 在来世,我们打开书桌抽屉,取出一张纸,阅读消息,然后继续我们的任务。 问题在于,盒子里总是有乱七八糟的纸。 因此,我们需要一种区分正确消息和随机消息的方法。 可以向他保证公证人,但是在最简单的情况下,只要我们有办法验证其正确性,他的签名就足够了。 例如,我们可以根据文本使用数学表达式的结果作为签名,从而使随机符合的概率足够小。 在最简单的情况下,这是CRC或校验和。 它不仅可以保护我们免受阅读未写内容的影响,而且还可以防止阅读损坏的消息。 毕竟,文本会随着时间的流逝而褪色,并且隔离的快门中的电子甚至更不耐用-粒子将以足够的能量从太空飞出,并且位会发生变化。 但是还有另一种方法来获取损坏的消息-这不是将其添加到末尾。 它不是那么奇特,因为在记录时,电流消耗急剧增加,这可能会激怒作家的过早死亡。

2.假设我们确信该消息的正确性,但是如何确定是我自己写的(以保证真实性 )。 俗话说,我不同。 突然,在我转世之前,有人坐在这张桌子旁,他执行了另一项任务,现在出于什么原因,他的信息会指引我? 如果我们给便笺贴上特定标签,那么我们就更容易将我们的笔记与陌生人区分开。 例如,这样的标签可以是我们要保存的变量的名称。 唯一的问题是,在EEPROM中没有足够的空间来放置变量名,并且这样做很不方便,因为它们的长度不同。 但幸运的是,有一种更简单的方法-您可以代表变量计算校验和并将其用作快捷方式。 同时,将变量的大小(以字节为单位)添加到此校验和中很有用,以免意外读取错误的数量。 好了,为了完整起见,我们在此处添加了另一个数字标识符,以确保将我们的变量与其他变量区分开,即使它们被称为相同。 我们将此数字称为实例标识符(如果变量名称被视为对象字段,则受OOP启发)。 如果我们将任务升级到一个全新的版本,以便此更新使旧版本保存的所有内容变得毫无意义,那么我们只需要更改实例标识符以使旧版本保存的所有内容无效即可。

3.如何使不完整的写操作保持旧的存储值不变? 也就是说,保存操作应该成功,或者根本不具有任何可观察到的效果。 换句话说,如果我们要谈论的是归结为单个值的无条件更新的事务,那应该是原子的或事务性的。 显然,我们不能通过重写先前的值来确保记录的原子性,我们必须写入新的位置,以便旧的存储值保持完整,至少直到完成新值的记录为止。 如果仅保存值的一部分被更新,但是仍然保持不变的部分仍被复制并写入新位置,则该技术通常称为“写时复制”。 打个比方,我们将给自己写封信,让旧信件保持原封不动,但为每个信件提供越来越多的序列号,以便我们来生有机会找到所写的最后一封信。 但是,与此同时,又出现了一个新问题-如果我们不丢弃无关紧要的旧字母,那么放置字母的位置早晚会结束。 很容易理解,仅存储2个字母就足够了-一个旧的和一个新的,可能是在编写过程中。 因此,字母数字也不需要很多位。

奇怪的是,作者无法在确保完整性,真实性和原子性的情况下找到单一的实现方式来允许将数据存储在EEPROM中。 我不得不自己写信给github.com/olegv142/NvTx

为了将每个变量保存在EEPROM中,使用了两个连续的区域-具有相同结构的单元。 根据变量的大小,文本标签和实例标识符计算出的变量的标识符写入前2个字节中。 接下来,写入数据,然后是2个字节的校验和。 在第一个字节中,两位具有特殊用途。 最高有效位是正确性标志;写时始终将其设置为1。 低位用作时代的一位数字;需要它来查找最后一条消息。 记录是在“一圈”的单元格中完成的。 时代号每次在第一个单元中进行记录时都会更改。 因此,用于确定最后记录的单元的算法是:如果单元的历元相同,则第二个单元最后写入,如果不同,则第一个单元写入。

正确性位似乎是多余的,但是它具有重要的功能。 首先,我们读取存储的数据并检查两个单元的正确性。 如果单元格未通过对正确标识符或校验和的检查,我们将重置正确性位。 随后的写操作可能不会检查单元的正确性,而是依靠此标志,这将开销减少了大约2倍。

那些想深入研究实现细节的人可以在存储库中看到图片和代码。 为了不让读者感到厌烦,我继续使用。 写入/读取数据的功能每个都有5个参数,因此牺牲了它们的使用方便性,而增加了灵活性。 但是它可以通过两组宏进行大量补偿,这使得使用该库就像在EEPROM.get / put中一样简单。 如果仅要将变量保存到给定地址,则使用第一组宏:

 #include <NvTx.h> int my_var = DEFAULT_VALUE; bool have_my_var = NvTxGetAt(my_var, MY_VAR_ADDR); my_var = NEW_VALUE; NvTxPutAt(my_var, MY_VAR_ADDR); 

如果要保存多个变量,则每个变量都必须确定地址,并同时正确考虑大小,以使存储变量的存储区域不会重叠。 为了简化任务,第二组宏实现了自动地址分配,并在编译时执行了 。 例如, Arduino-EEPROMEx库可以在运行时分配内存,同时将每个存储变量的地址存储在RAM中。 NvTx库在EEPROM中分配空间,而不会在可执行代码或RAM内容中添加任何内容。

 #include <NvTx.h> int my_var = DEFAULT_VALUE; char my_string[16] = ""; NvPlace(my_var, MY_START_ADDR, MY_INST_ID); NvAfter(my_string, my_var); bool have_my_var = NvTxGet(my_var); my_var = NEW_VALUE; NvTxPut(my_var); 

NvPlace宏设置EEPROM区域的起始地址,在此我们将存储变量和实例标识符。 NvAfter宏保留一个内存区域,以在第二个内存区域之后立即存储其第一个参数。 分配内存时,还将检查我们没有超出可用的EEPROM大小,并且没有保留重叠的内存区域(如果两个NvAfter宏具有相同的第二个参数,则可能发生这种情况)。 如果违反了两个指定条件中的任何一个,则程序将不会编译。 那些想要处理内存分配机制的人可以在NvTx.h头文件中找到它。 NvPlace和NvAfter宏所做的所有工作都是定义枚举,并基于变量名称形成其名称,并使用非常有用的编译时assert惯用语构造。

希望NvTx库将帮助读者编写可靠的工业级代码。

Source: https://habr.com/ru/post/zh-CN482918/


All Articles