在本章前面的章节中,你已经学习了如何将服务公司的数据组织到结构中。然后,你学习了如何将数据存储在一组结构中。然而,如果每次开发板断电时这些数据都会消失,无论是有意还是无意,这都不是好事情。在本节中,你将学习解决断电后仍能保存数据的方法。
正如你从表1-1中了解到的,每个Atmel兼容开发板都有特定数量的闪存、SRAM和EEPROM存储器可用。闪存和EEPROM存储器都是非易失性的,这意味着这些类型的存储器在断电时不会丢失数据。你还了解到,具有全局作用域的数据在SRAM内存中分配,任何初始化值都从闪存复制到SRAM。
不好之处是,因为程序中的每个元素都可以访问全局数据,所以将很难隔离污染数据的代码部分。如果在函数中移动数据主体,数据现在在堆栈上分配则范围是有限的。因为SRAM比较少,且由于SRAM是易失性存储器,因此在断电时阵列不会持久化。解决这个问题的一种方法是开始使用EEPROM存储器。
到目前为止,我们的示例程序还没有使用EEPROM内存。这并不是说我们避过了EEPROM存储器。相反,前面讲的程序非常简单,从来没有考虑过内存限制,因此没有必要使用它。此外,我们也避免使用它,因为EEPROM内存相对较慢。
通常,EEPROM存储器用于存储配置数据。配置数据可以是从I/O通信的终端波特率到初始化程序所需的传感器数据。正如我们之前指出的,EEPROM具有有限数量的擦除/写入周期,其中EEPROM可以可靠地擦除和写入数据。虽然可能有一百万次这样的重复利用,但大多数开发人员假设EEPROM在大约100000个周期后开发出自己的应用系统。虽然听起来可能有很多循环,但如果你每秒更新一次存储在EEPROM中的变量,那么该程序在不到2天的时间内就有可能出现问题。尽管如此,如果数据很少改变,就像现在一样,在我们的servicePerson阵列中,EEPROM存储器可能是一种可行的替代方案。
Arduino IDE附带一个EEPROM库,你可以在安装Arduino软件的库目录中找到该库。你应该花一点时间阅读EEPROM库及其示例代码。
在以下讨论中,我们的意见针对“自载”EEPROM,而不是任何外部设备可安装在外部设备上的EEPROM。EEPROM存储器不是一种最佳的存储器。选择记录数据有几个原因:首先,由于EEPROM相当慢,它可能无法保持不管是什么设备都能提供的数据;其次,数据记录通常是一个连续的过程,这意味着需要维护指向下一个记录数据字节写入位置的指针。如果在EEPROM内存空间中维护此指针,它可能变得不可靠,因为它可能需要相当频繁地更新(即擦除/写周期)。此外,EEPROM数据量通常非常有限,并且可能会出现存储空间不足的错误,无法使用。最后,EEPROM内存的行为就像一个环形缓冲区。也就是说,如果你的电路板有512字节的EEPROM,你可以尝试写入EEPROM内存中的地址512,然后简单地“环绕”到EEPROM地址0并写入数据。有效的EEPROM地址为0,很明显,如果你需要存储在地址0的任何内容,这有问题了。因为这些限制,数据记录程序经常使用外部设备存储记录数据。
然而,就目前而言,假设你要保存的数据集有限,并且你认为EEPROM现在可能是一个存放它的好地方。而不是提出一个单一的很长的代码清单,我们将对其进行分解,以便在讨论时可以看到相关代码。此外,这个例子是人为的,因为我们从存储在SRAM内存中的数据开始,然后将其移动到EEPROM存储器中。显然,这无助于解决内存限制问题。然而,该示例确实向你展示了使用EEPROM内存时需要解决的一些问题。
如前假设,我们的程序是保存10家服务公司的信息。希望将信息保存在servicePeople结构中,但将其存储在EEPROM中。清单10-4显示了全局数据定义和声明以及setup()循环代码。
清单10-4中的第一条语句是一个#include预处理器指令,用于读取EEPROM.h头文件。该文件包含编译器正确使用EEPROM库所需的信息。#define DEBUG preprocessor指令用于在代码中切换调试打印语句。你可以在setup()循环中看到这方面的示例。例如,声明:
#ifdef DEBUG
Serial.print("EepromMax=");
Serial.println(eepromMax);
#endif
仅当为程序定义了调试时,才使Serial.print()语句出现在程序中。因为代码在清单10-4的顶部有一个#define DEBUG 1预处理器指令,所以打印语句被编译到程序中。如果注释掉#define DEBUG 1指令,则DEBUG为不再定义,并且程序中省略了Serial.print()语句。这样的代码通常称为脚手架代码,因为它在调试完成后被“剔除出”程序,就像建筑完工后脚手架被拆除一样。
代码清单10-4.The setup()loop Code
接下来,该代码定义了一个名为MAXPEOPLE的整数常量,用于设置允许的公司数量范围。你可以使用#define来代替,但如果你愿意,这将为你提供一个实际的变量。然后是servicePeople的结构声明和一个union标记为servicePeopleUnion的union。虽然我们并没有充分利用这个联合,但它至少会让你看到联合是如何被使用的。
然后,定义servicePeople结构数组myPeople[],并用四条记录初始化该数组。数组元素的唯一用途是确定数组已复制到EEPROM存储器中。如果ID成员为0,则表示尚未创建该数组复制到EEPROM存储器中。实际上,不管怎样,将数组复制到EEPROM中都是一个好主意,因为代码在setup()函数中,实际上是初始化步骤的一部分。事实上,你可以将该数组元素的其他三个成员用于其他目的,只要你与成员的数据类型一致。
数组初始化后,将显示几个函数声明,接着是几个全局变量。然后,查找setup()函数,首先要做的是找到电路板可用的EEPROM内存量。没错,你知道这对你来说是什么,但是如果你以后换板呢?清单10-5给出了FindEepromTop()的代码。
代码清单10-5.Source Code for FindEepromTop()
如果试图写入的EEPROM地址高于可用的EEPROM地址,则指向EEPROM的地址指针将返回地址0。也就是说,如果你尝试写入内存地址512而你的电路板只有512字节的EEPROM,那么它会悄悄地将数据字节写入内存EEPROM存储器空间中的地址0。原因是,如果只有512个字节的EEPROM,其有效地址为0到511。尝试写入地址512“环绕”回到第一个内存地址位置。EEPROM内存空间的范围就好像它是一个环形缓冲区。接下来,代码读取内存的第一个字节,以确定数组是否已复制到EEPROM。ReadIntFlag()的代码如清单10-6所示。
代码清单10-6.Source Code for ReadIntFlag()
ReadIntFlag()显示了读取EEPROM内存有多简单。EEPROM库,即与Arduino IDE一起发布的只有两个EEPROM函数:read()和write(),尽管库的示例还显示了如何清除EEPROM内存[与FindEepromTop()函数结合使用]。ClearEprom()函数可以很容易地添加到库中,初始化指向名为myUnion。函数的作用是读取EEPROM存储器的前2个字节。因为ptr指向在union中testID int变量,myUnion的第一个成员存储在EEPROM中,找到的内容将0和1作为int地址(将这两个字节简单地视为对象,而不是特定的数据类型,允许我们从LittleEndian/BigEndian问题中抽象出来,这是一个我们不需要的东西。如果你想进一步探讨这个问题,只需在搜索引擎上搜索)。
如果ReadIntFlag()返回0,你就知道myPeople[]结构数组尚未读入EEPROM存储器。如清单10-4所示,代码确实从ReadIntFlag()返回了一个值,但代码不处理该值。代码进行调用只是为了显示信息,告诉你它是如何工作的。如果你希望避免将数据复制到EEPROM内存中(可能是一秒钟),可以测试ReadIntFlag()调用的返回值,以决定是否复制myPeople[]数组。清单10-4中的代码只是通过调用WriteOneRecord()。该函数的代码如清单10-7所示。(www.xing528.com)
代码清单10-7.Source Code for WriteOneRecord()
WriteOnRecord()显示了如何使用EEPROM write()函数。该函数接受一个索引作为函数的唯一输入myPeople[]数组的参数。byte指针b被初始化为指向SRAM内存中myPeople[]数组的元素所在的左值。变量偏移量是计算此特定位置的左值所必需的,应将myPeople[]数组复制到EEPROM内存中的元素。对EEPROM的调用,write()函数其后将数组元素的每个字节写入EEPROM存储器。for循环的表达式2指示写入的字节数。因此,sizeof(servicePeople)表达式确保36字节写入EEPROM存储器。对WriteOnRecord()的调用称为MAXPEOPLE次(即10次),尽管只有前四个元素包含有用的数据。注意offset如何确保新数据被复制到EEPROM存储器空间中的正确左值。如果这还不清楚,那么继续研究编码,直到它完成。
for循环完成将数据复制到EEPROM内存空间后,程序进入loop()函数进一步进行处理。清单10-8显示了loop()函数的代码。函数中的第一条语句定义并将EEPROMDINDEX变量设置为1。这样做是因为你知道第一条记录不包含任何有用的信息。因此,你只对myPeople[]数组中第一条记录后面的内容感兴趣。
代码清单10-8.Source Code for the loop()Function
loop()函数中没有太多内容。变量initFlag测试以确定数据是否已复制到EEPROM内存空间。由于是这种情况,程序调用ReadOneRecord(EEPROM索引)从EEPROM读取记录。ReadOneRecord()的代码如清单10-9所示。
代码清单10-9.Source Code for ReadOneRecord()
代码与清单10-7非常相似,只是这次你是读取而不是写入数据。变量偏移量是将byte指针bPtr置于EEPROM内存空间中左值所必需的。一旦bPtr设置正确,代码将从EEPROM中读取sizeof(servicePerson)字节的数据(36字节)到union myUnion中。显然,我们希望将这些数据复制到联合的servicePeople结构中,这就是为什么bPtr被设置为myUnion.testServicePeople的地址。请注意如何使用偏移量,以便读取正确的数据。
调 用ReadOneRecord()函 数 返 回 后,代 码 将 检 查myUnion是 否 正 确。testServicePeople.ID为非零。如果这是真的,则调用DataDump()函数。语句中有两个点运算符:
if(myUnion.testServicePeople.ID!=0){……}//Read some real data
这条语句有点像中国话:“盒子里的盒子里的盒子。”联合是一种数据结构,就像一个黑匣子,需要一把钥匙(点运算符)才能“进入”联合数据结构。所以,你拔出钥匙,打开盒子的门,然后走进去。你看到了什么?你会看到一个名为testID的int和另一个名为testServicePeople的黑框。你也知道你需要一个进入testServicePeople结构的不同钥匙(另一个点运算符)。所以,任何对testServicePeople结构内容有用的东西都意味着你需要两组密钥(点运算符)获取被两个黑匣子保护的数据。这就是为什么有两个点运算符才能让你进入myUnion和testServicePeople数据结构内部,才能查看名为ID的结构成员。
对于一条语句中可以使用多少“框中框”级别,没有实际限制。只需记住黑匣子概念,然后注意你用每把钥匙键(点运算符)输入的内容,你应该很容易理解这些事情和解决它。
最后,调用DataDump()函数来显示刚刚读取的数据。清单10-10显示代码。
代码清单10-10.Source Code for DataDump()
如你所见,DataDump()函数所做的只是发送刚刚从EEPROM内存中读取的myUnion.testServicePeople结构的内容。在实际应用中,myPeople[]数据将用于某种形式的附加处理,而不仅仅是转储到串行设备。尽管如此,该程序还是展示了如何使用EEPROM存储器来存储非易失性需要存储的数据。程序运行结果如图10-5所示。
图10-5 程序输出结果
尽管EEPROM提供了一种在断电时保存数据的方法,但主板上有限的EEPROM内存可能不足以满足你的需要。如果是这样,还有什么其他选择吗?
有多种方法可以增加Arduino兼容板的可用数据存储量。例如,数据记录是开发板的常用功能,但如果要存储大量数据,Arduino可能需要一些帮助。
另一种选择是使用SD卡。图10-6显示了SD卡存储工具,与圆珠笔相比,尺寸相当小。
图10-6 SD卡
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。