在 Arduino Flash 内存中使用变量参数






4.13/5 (6投票s)
在会话之间保存您的草图状态。
引言
当您将一个程序编译并上传到Arduino时,程序代码存储在闪存(PROGMEM)中,并且有一个SRAM区域供程序运行时使用其变量。
每次为板通电时,闪存中的程序代码就会运行。在各种系统初始化之后,您的setup()
函数会运行,然后主程序代码在loop()
中会反复执行,直到断电为止。
在这个简单的模型中,没有办法在会话之间保存数据。所有变量将在每次程序运行时重新初始化。
如果您有大型静态常量,则可以通过在编译时将它们放入PROGMEM并使用pgmspace
库访问它们来节省宝贵的SRAM空间。
还有一个内存区域称为EEPROM(电擦可编程只读存储器),您可以使用eeprom
库进行读写。EEPROM和PROGMEM是非易失性的,断电后仍能保留其内容,但可以擦除并写入新值。写入次数有限制 - 尽管相当大(约100,000次写入),但您不想将其用于快速变化的变量数据。对于或多或少静态的变量或参数,它非常有用。
PROGMEM只能在程序首次上传时写入,因此适合存储不变的常量值。即使您每天加载一个新版本的程序,也需要273年才能磨损闪存。
EEPROM可通过eeprom库访问,但您需要小心使用它的频率。如果您的主程序循环每10毫秒执行一次,并且每次都更新EEPROM中的值,那么您将在执行16分钟后达到100,000次写入的限制。
但是,对于需要偶尔更改的数据,使用它是可以的 - 例如,您可能希望在程序每次运行时增加一个计数器,或者设置一个触发阈值,以便下次通电时记住该值。
然而,有一个问题 - 目前无法在程序上传时轻松初始化EEPROM位置。当芯片是新的时,每个位置都将包含&FF(所有位=1),但如果某个位置曾经被写入过,它将不同。
那么问题是如何知道EEPROM中的数据是您的草图上次运行时保存的,还是其他草图留下的数据,或者是否已损坏?
可以破解Arduino IDE系统,以便编译器指令EEMEM正确启用以初始化EEPROM位置。这将允许您在草图首次编译和上传时设置初始值,但这需要您对要编译草图的任何系统的Arduino IDE控制文件有一定的信心 - 并且每次IDE更新时都需要重复此过程。
解决方法是在setup()
函数中内置对您想要使用的EEPROM区域进行一些检查。
背景
Arduino内存类型参考页面在这里:http://playground.arduino.cc/Learning/Memory
如果您有信心,可以按照此处的说明破解Arduino IDE:http://www.fucik.name/Arduino/eemem.php,这将允许您在代码上传时初始化EEPROM。
或者,我们可以采用一种变通方法,该方法涉及在编译时为我们的草图和版本保存一个唯一的PROGMEM值,并在草图在setup()
运行时,将此常量与EEPROM中的特定位置进行比较。如果不匹配,我们将初始化EEPROM中的值并将我们的密钥写入特定位置。
如果匹配,那么我们使用的EEPROM区域中的其余数据可能有效,但我们应该运行其他检查,以防万一在我们写入密钥之后加载了不同的草图并更改了某些其他位置。
根据我们拥有的板卡类型,EEPROM的容量在512到4096字节之间。这给我们留下了不多的空间,所以我们可能需要精打细算地使用多少内存用于密钥。单个字节实际上是不够的,因为很有可能其他东西已经写入了该值。
我们还需要考虑是每次上传新版本的草图时都重置内存,还是小幅升级可以保留EEPROM中的先前值。
大多数使用EEPROM的人可能都会从第一个位置开始使用,所以我们会将密钥保存在内存的开头,这样如果加载了其他草图并使用了EEPROM,从而使我们保存的值无效,它最有可能被损坏。
使用代码
为了演示目的,我们将使用一个简单的草图,该草图设计用于在独立设备上运行,监控模拟输入的状况,并在超过阈值时打开数字输出(例如点亮警告LED或发出警报)。
我们还将跟踪自安装以来程序运行的次数。
int AnaPin = A0;
int LEDpin = 3;
static unsigned long runCount = 0; // how many times the sketch has been run
static int threshold = 200; // the threshold above which the LED will turn on
boolean ledon = false;
void setup(){
pinMode(LEDpin, OUTPUT);
pinMode(AnaPin, INPUT);
runCount += 1;
delay(200)
}
void loop()
{
ledon = (analogRead(AnaPin) >= threshold);
digitalWrite(LEDpin, ledon);
delay(500);
}
在实际应用中,我们可能会在触发器上包含一些去抖动或滞后函数,以防止模拟输入中的噪声引起问题,但对于这个例子,我们每半秒采样一次。
但是我们需要能够保存runCount
和threshold
的值,而不是在程序每次启动时重置它们。
#include <EEPROM.h> //enable use of eeprom
int epromStart = 0; //the address of the first location in eeprom we will use
// ....
// this in setup()
int t;
EEPROM.get(epromStart, runCount); //get the runCount from EEPROM
EEPROM.get((epromStart + sizeof(long)), threshold); //threshold is immedaitely after runCount in EEPROM
runCount += 1;
EEPROM.put(keylen, runCount); //write the updated run count back to eeprom
// carry on with setup
好的,只要EEPROM已用有效值初始化,这样就可以了。为了检查这一点,我们将定义一个PROGMEM中的常量字符串,该字符串将在程序上传时设置。程序第一次运行时,我们将把相同的字符串写入EEPROM,然后我们可以比较两者,如果它们匹配,我们可以假设EEPROM包含对我们有效的数据。
由于我们经常生成包含草图名称的字符串常量,我们将使用它 - 它很可能具有唯一性,如果我们想在上传新版本时使EEPROM中的旧数据无效,我们可以在编译时稍微更改名称。
#include <avr/pgmspace.h> //enable use of flash memory
const char sketchName[] PROGMEM = "EepromDemoV1"; // This will be our key to see if EEPROM is valid
static int keylen;
char strbuffer[50]; // variable to copy strings from flash memory as required
int x;
boolean eok = false;
String key = "";
String chk = "";
char* getProgmemStr(const char* str) {
/** gets a string from PROGMEM into the strbuffer */
strcpy_P(strbuffer, (char*)str);
return strbuffer;
}
char* getEeepromStr(int start, int len) {
/** gets a string from EEPROM into the strbuffer */
for (x = 0; x < len; x++) {
strbuffer[x] = EEPROM.read(x);
}
strbuffer[x] = 0;
return strbuffer;
}
void putEepromStr(int start, String str) {
/** puts a string into the eeprom at a given address */
int strlen = str.length() + 1;
char chArray[strlen];
str.toCharArray(chArray, strlen);
for (x = start; x < (start + strlen); x++) {
EEPROM.write(x, chArray[x]);
}
}
void setup()
{
key = getProgmemStr(sketchName);
keylen = key.length() + 1;
chk = getEeepromStr(0, keylen);
if (key == chk) {
//ok we've got our expected key in eeprom, now does the rest of the data look ok?
//we will be storing the run count as an unsigned long immediately after the key, this could have any any value
// threshold could have any value from 0 to 1023 which is the max we get from the ADC, so if it is greater it is not valid
eok = true;
int t;
EEPROM.get(keylen, runCount); //get the runCount from EEPROM immediately after the key
EEPROM.get((keylen + sizeof(long)), t); //threshold is immedaitely after runCount in EEPROM
if (t > 1023) { //if it is out of range then the EEPROM is invalid and we'll need to reset it
eok = false;
}
else { //ok we can use the values from eeprom
threshold = t;
runCount += 1;
EEPROM.put(keylen, runCount); //write the updated run count back to eeprom
}
}
if (!eok) { //invalid data in EEPROM so either this is a first run or it has been corrupted
putEepromStr(0, key); //write the valid key at the begining of eeprom
chk = String(getEeepromStr(0, keylen));
runCount = 0;
EEPROM.put(keylen, runCount);
EEPROM.put((keylen + sizeof(long)), threshold);
}
// we have left eok as is so we can report it if required
这部分内容很多。让我们分解一下。
首先,我们定义了一个PROGMEM中的字符串常量,一个变量来保存其长度作为字符数组,以及一个缓冲区,用于在从PROGMEM或EEPROM读取字符数组时复制它们。
我个人更喜欢在Arduino代码中使用String对象而不是简单的字符串字符数组,因为它使得代码更易读(因此更易于维护),并提供了许多有用的功能。
然后,我们有三个简短的通用函数,可用于从PROGMEM、EEPROM获取String,以及将String写入EEPROM。
getProgmemStr()
:我们将PROGMEM中的字符数组的指针传递给它,它使用strcpy_P()
将其复制到缓冲区,并返回缓冲区的指针。
对于getEepromStr()
,我们必须传递EEPROM中的起始地址以及我们期望返回的字符数组的长度。同样,它(这次是逐字节)将其复制到缓冲区并以null结尾,以便我们可以将其读取为字符串。
对于写入EEPROM的字符串,putEepromStr()
接收起始地址和String对象,将对象转换为char数组,然后迭代将字节写入EEPROM。
现在,在setup()
的开头,我们可以从progmem中获取密钥,查看其长度,并从EEPROM中获取相应数量的字节并进行比较。
如果它们匹配,那么我们可以假设EEPROM有效,并从中读取threshold和runCount的值,否则我们将使用默认值并将它们写入EEPROM。
我们现在还在添加对threshold值的检查 - 我们知道我们的模拟引脚的最大值是1023,所以如果threshold大于该值,那么我们将假设它是无效的,并将默认值写入所有位置。
现在,我们还需要一种方法来设置阈值并在连接到串行端口的主设备上读取runcount。
我们将实现一个非常简单的串行协议,以便如果我们向板发送“t123x”,它将将其解释为将阈值设置为123(或't'和'x'之间的任何值)的命令。
在串行端口上接收到的任何其他字符都将导致我们报告当前的runCount和threshold。
// at the end of setup()
Serial.begin(115200);
Serial.println();
delay(500);
}
void loop()
{
char serialChar;
if (Serial.available() > 0) {
while (Serial.available() > 0) {
serialChar = Serial.read();
if (serialChar == 't') {
doThreshold();
}
}
Serial.print("key="); Serial.println(key);
Serial.print("chk="); Serial.println(chk);
Serial.print("eok="); Serial.println(eok);
Serial.print("runcount="); Serial.println(runCount);
Serial.print("threshold="); Serial.println(threshold);
}
ledon = (analogRead(AnaPin) >= threshold);
digitalWrite(LEDpin, ledon);
delay(500);
}
void doThreshold(void) {
/* this will read chars from serial until an 'x' is encountered
* anything other than digits will be ignored
* the value recieved will be used to set a new threshold
*/
boolean endOfDigits = false;
String cmdStr = "";
int inChr;
while (!endOfDigits) {
inChr = Serial.read();
if (isDigit(inChr)) {
cmdStr += (char)inChr;
}
else if (inChr == 'x') {
endOfDigits = true;
}
}
int intVal = 0;
if (cmdStr.length() >= 1) { // if we have a number > 0
intVal = cmdStr.toInt();
Serial.print("intVal="); Serial.println(intVal); //echo the value back for debugging
if (threshold != intVal) {
threshold = intVal;
EEPROM.put((keylen + sizeof(long)), threshold);
}
}
}
每次循环(每半秒)一次,如果串行缓冲区中有任何内容,我们将读取它。如果我们发现't',我们将转到doThreshold()
,它将读取串行字符直到我们得到'x'和一个有效的数字。
如果我们没有得到't',我们将通过读取串行缓冲区来清空它,然后写出当前的值。
请注意,我们可以在doThreshold()
中检查我们是否获得了有效值(<1024)。如果您向串行端口发送“t1025x”,它将使用此值并将其写入EEPROM,但下次板通电时,它会在其中找到无效值并自行重置。
关注点
显然,这只是一个简单的例子,并非完全健壮,可以大大改进。
对于完全健壮的解决方案,我们还应该在每次更新值时为我们使用的内存区域计算一个校验和,并将其保存在我们的块的末尾。这也将防止在加载我们自己的程序时数据损坏。
字符串到闪存的读写已打包到函数中,因为我发现自己经常重复使用它们。
您可以使用EEPROM.length()来了解您的特定板卡有多少EEPROM,这将使您能够确保不会因不可预测的结果而溢出它。当然,您可以将数据放在EEPROM中的任何位置,不一定从零开始。
实际上,如果您在独立设备中记录数据,您几乎肯定会使用外部存储器,如SD卡屏蔽板之一。EEPROM最好用于您希望在设备断电时或更换SD卡时保留的参数和状态信息。
历史
首次发布日期:2016年8月4日